以前看到别人的Java代码里有泛型了,接口了,就特别害怕,不知道是干啥的,虽然上网也查了一些资料,但觉得还是理解的不够深入,导致遇到同样的问题,还是得重新查一遍资料。
Lambda表达式
这个几把玩意出现了好多次了,自己感觉实现同样的功能,用原来的代码写法就可以了嘛,为啥非得用这个东西呢?就算查资料去尝试学习它,自己的内心对它还是有抵触情绪的。直到公司的项目里同事的代码出现了Lambda写法,不学不行了,要不会影响对项目代码的理解的,就生出了再学习它一下的意图。
为啥需要Lambda?
学习一项技术,一定要知道它出现是解决什么问题的,不能知其然而不知其所以然。
Java 是一流的面向对象语言,除了部分简单数据类型,Java 中的一切都是对象,即使数组也是一种对象,每个类创建的实例也是对象。在 Java 中定义的函数或方法不可能完全独立,也不能将方法作为参数或返回一个方法给实例。
从 Swing 开始,我们总是通过匿名类给方法传递函数功能,以下是旧版的事件监听代码:
someObject.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
//Event listener implementation goes here...
}
});
在上面的例子里,为了给 Mouse 监听器添加自定义代码,我们定义了一个匿名内部类 MouseAdapter 并创建了它的对象,通过这种方式,我们将一些函数功能传给 addMouseListener 方法。
简而言之,在 Java 里将普通的方法或函数像参数一样传值并不简单,为此,Java 8 增加了一个语言级的新特性,名为 Lambda 表达式。Lambda 表达式是一种匿名函数(对 Java 而言这并不完全正确,但现在姑且这么认为),简单地说,它是没有声明的方法,也即没有访问修饰符、返回值声明和名字。
Java 中的 Lambda 表达式通常使用 (argument) -> (body) 语法书写,例如:
(arg1, arg2...) -> { body }
(type1 arg1, type2 arg2...) -> { body }
以下是一些 Lambda 表达式的例子:
(int a, int b) -> { return a + b; }
() -> System.out.println("Hello World");
(String s) -> { System.out.println(s); }
() -> 42
() -> { return 3.1415 };
让我们了解一下 Lambda 表达式的结构:
- 一个 Lambda 表达式可以有零个或多个参数
- 参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)与(a)效果相同
- 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c)
- 空圆括号代表参数集为空。例如:() -> 42
- 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a
- Lambda 表达式的主体可包含零条或多条语句
- 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致
- 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空。
什么是函数式接口 ?
在 Java 中,Marker(标记)类型的接口是一种没有方法或属性声明的接口,简单地说,marker 接口是空接口。相似地,函数式接口是只包含一个抽象方法声明的接口。
扩展
1、什么是标记接口?
一个空的接口称为标记接口(Tag Interface),Java中很多标记接口,比如Serializable,EventListener, Remote(java.rmi.Remote)等。
package java.util;
public interface EventListener{
}
2、 标记接口有什么特点?
标记接口没有任何成员变量和方法,它就是空的。你肯定疑惑,既然是空的,其他类怎么去实现(implement)它?它用来做什么?
实际上,其他类implement它是为了声明该类在某个特定集合中的成员资格,比如当一个类实现(implement)了Serializable接口,它的目的是声明其是Serializable中的一个成员,当JVM虚拟机看到该类是Serializable的,那么它在处理序列化/反序列化时会做一些特殊的处理。
标记接口对JVM是很有意义,你可以创建自己的标记接口来分离或分类代码,从而提高代码的可阅读行。
java.lang.Runnable 就是一种函数式接口,在 Runnable 接口中只声明了一个方法 void run(),相似地,ActionListener 接口也是一种函数式接口,我们使用匿名内部类来实例化函数式接口的对象,有了 Lambda 表达式,这一方式可以得到简化。
每个 Lambda 表达式都能隐式地赋值给函数式接口,例如,我们可以通过 Lambda 表达式创建 Runnable 接口的引用。
Runnable r = () -> System.out.println("hello world");
当不指明函数式接口时,编译器会自动解释这种转化
new Thread(
() -> System.out.println("hello world")
).start();
因此,在上面的代码中,编译器会自动推断:根据线程类的构造函数签名 public Thread(Runnable r) { },将该 Lambda 表达式赋给 Runnable 接口。
@FunctionalInterface 是 Java 8 新加入的一种接口,用于指明该接口类型声明是根据 Java 语言规范定义的函数式接口。常见的函数式接口,比如Function,Consumer,Predicate,Supplier都是用@FunctionalInterface进行声明的。
以下是一种自定义的函数式接口:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
根据定义,函数式接口只能有一个抽象方法,如果你尝试添加第二个抽象方法,将抛出编译时错误。例如:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
public void doSomeMoreWork();
}
错误:
Unexpected @FunctionalInterface annotation
@FunctionalInterface ^ WorkerInterface is not a functional interface multiple
non-overriding abstract methods found in interface WorkerInterface 1 error
函数式接口定义好后,我们可以在 API 中使用它,同时利用 Lambda 表达式。例如:
//定义一个函数式接口
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
public class WorkerInterfaceTest {
public static void execute(WorkerInterface worker) {
worker.doSomeWork();
}
public static void main(String [] args) {
//不用Lambda表达式的写法
execute(new WorkerInterface() {
@Override
public void doSomeWork() {
System.out.println("哈喽,华妹妹!");
}
});
//用Lambda表达式的写法
execute( () -> System.out.println("华妹妹,我用Lambda表达式向你问好!😘") );
}
}
输出:
哈喽,华妹妹!
华妹妹,我用Lambda表达式向你问好!😘
这上面的例子里,我们创建了自定义的函数式接口并与 Lambda 表达式一起使用。execute() 方法现在可以将 Lambda 表达式作为参数。
Lambda 表达式举例
学习 Lambda 表达式的最好方式是学习例子。
线程可以通过以下方法初始化:
//旧方法:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from thread");
}
}).start();
//新方法:
new Thread(
() -> System.out.println("Hello from thread")
).start();
事件处理可以使用 Java 8 的 Lambda 表达式解决。下面的代码中,我们将使用新旧两种方式向一个 UI 组件添加 ActionListener:
//Old way:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("The button was clicked using old fashion code!");
}
});
//New way:
button.addActionListener( (e) -> {
System.out.println("The button was clicked. From Lambda expressions !");
});
还有一种情况下使用Lambda表达式是很爽的,在开始之前,我们先来了解一下Function,Consumer,Predicate,Supplier这几个Java中常用的函数式接口。
先看一下Function接口定义:
@FunctionalInterface
public interface Function<T, R>
接口接受两个泛型类型<T, R>.
再看一下接口定义的方法(非静态,非default), 支持lambda表达式的接口只允许定义一个抽象方法(@FunctionalInterface注解的接口,只允许定义一个抽象方法),只要记住这一点,你就不会弄混了。
R apply(T t);
/**
* T 入参类型, t 输入参数
* R 返回值类型
*/
OK,现在明确了, 该接口的lambda表达式应该是接受一个入参,最后要有一个返回值,写法应该是这样的: (x) -> {return y;}
如果你的lambda表达式非常简单,只有一行,那么你可以不写return,不加花括号{},返回值后面可以不加分号。
下面就可以写example了,写一个简单的,再写一个标准的:
public void testFunction(){
//简单的,只有一行
Function<Integer, String> function1 = (x) -> "test result: " + x;
//标准的,有花括号, return, 分号.
Function<String, String> function2 = (x) -> {
return "after function1";
};
System.out.println(function1.apply(6));
System.out.println(function1.andThen(function2).apply(6));
}
OK,Function的例子写完了,接下来写其他的,其实原理懂了,其他的就都简单了,然后就是熟能生巧了。
再看看Supplier的接口定义,这个接口定义比较简单,我就都贴上来了
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
接口接受一个泛型<T>,接口方法是一个无参数的方法,有一个类型为T的返回值。 OK, 那么接口的lambda表达式应该是这样的: () -> { return something; },好,下面来写一个example:
public void testSupplier(){
//简写
Supplier<String> supplier1 = () -> "Test supplier";
System.out.println(supplier1.get());
//标准格式
Supplier<Integer> supplier2 = () -> {
return 20;
};
System.out.println(supplier2.get() instanceof Integer);
}
到这里你或许有一点疑惑,这Supplier到底能用在哪啊?Java 8里新增了一个异步线程的类,很牛逼,很强大的类:CompletableFuture, 里面的很多方法的入参都用到的Supplier,例如: supplyAsync方法。 本文暂时不介绍CompletableFuture。
接下来是Consumer,我们来看一下接口的定义:
@FunctionalInterface
public interface Consumer<T>
然后再看一下里面的抽象方法:
void accept(T t);
现在了解了: 接口接受一个泛型<T>,接口方法是入参类型为T, 无返回值的方法, OK,下面开始写example:
public void testConsumer(){
Consumer<String> consumer1 = (x) -> System.out.print(x);
Consumer<String> consumer2 = (x) -> {
System.out.println(" after consumer 1");
};
consumer1.andThen(consumer2).accept("test consumer1");
}
接下来看一下Predicate接口
接口定义:
@FunctionalInterface
public interface Predicate<T>
抽象方法:
boolean test(T t);
接口接受一个泛型<T>, 接口方法的入参类型是T, 返回值是一个布尔值, OK, 下面写example:
public void testPredicate(){
Predicate<String> predicate = (x) -> x.length() > 0;
System.out.println(predicate.test("String"));
}
Predicate接口在stream里面用的比较多,感兴趣的可以去看看stream,java 8 里另一个新的东西,很好玩。
看完上面这几个接口的定义和用法,你会发现它们和其他普通的接口没啥不一样的,都只是声明,而不实现细节,唯一特殊之处,就是它被@FunctionalInterface修饰,支持Lambda表达式罢了!
回到刚才的那个话题,有一种情况下用Lambda是很爽的,看下面的代码:
先声明一个数组
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
如果我们要计算所有元素的和,怎么写代码?
public int sumAll(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}
又来个需求,如果只想计算所有偶数的和怎么写代码?我们会基于上面的代码,做一个判断:
public int sumAllEven(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
if (number % 2 == 0) {
total += number;
}
}
return total;
}
哈哈,又来几个需求,比如说计算只大于3的所有数之和,你是不是又得把刚才的代码粘贴过来,改叭改叭呢?
看看用Lambda怎么写的吧,结合Predicate使用
public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
int total = 0;
for (int number : numbers) {
if (p.test(number)) {
total += number;
}
}
return total;
}
对应上面三个需求,分别调用:
sumAll(numbers, n -> true);
sumAll(numbers, n -> n % 2 == 0);
sumAll(numbers, n -> n > 3);
非常的灵活啊!先写到这里吧!