Java8有许多令人激动的新特性,使用了将近一年的Java8之后,我对Java8的一些新特性熟练程度虽然算不上精通但也算熟练应用了。加上之前买了《Java8实战》《Java函数式编程》,翻阅一下,写一点总结,把之前写的几篇关于Java8的博文串起来,完善以前不熟练时候的一些写法,争取做到更加通俗易懂,且将项目里有趣的应用一起写成例子,丰富文章,让大家更加了解Java8新特性。
因为是Java8的新特性,所以,对于Java基础将不会太多涉及,最多也可能是一笔带过。
开篇
Java8秉持的一个重要观念:行为参数化。这个要怎么理解呢?举个例子,农场里的大叔种了好多的水果,有红苹果,绿苹果,桃子,橘子,到了丰收的季节了(不纠结季节问题)。大叔想要过滤这些采集的水果,比如我想过滤出不同种类的水果,或者想过滤出同一个水果里不同颜色的苹果,亦或者想过滤出苹果里重量大于100g的。那么我们的想法可能是建立这么多个遍历水果不同过滤条件的方法。那么既然Java8里的观念是行为参数化,我们对这个事件进行一波抽象,你做这么多的事归根结底是不是都是为了过滤,那么过滤就是一种行为,过滤的条件可以认为是一种策略,我们可以将这种行为封装起来,在运行的时候选择一种策略(算法),这就形成一个方法族。你也可以认为这类似于策略模式。
所以Java8为了实现这种做法就有了能够将方法(一段代码)作为参数传递的新特性!这是不是就很酷了,你可以完全抛弃以前的匿名内部类了,一起拥抱Java8的新特性!
Lambda表达式
Lambda表达式定义
Lambda表达式可以理解为可传递的匿名函数的一种方式:没有名称,但是又参数列表、函数主体、返回类型,也可能还有可抛出的异常列表。
Lambda 表达式语法:( )->{ }
第一部分为一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;
第二部分为一个箭头符号,分割左右两部分;
第三部分为方法体,可以是表达式和代码块。
写法一:
(parameters) -> expression
写法二:
(parameters) -> { statements; }
可以对比一下Java 8与之前的写法,你会发现简直简洁了不只一点点!
//Before Java 8:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Before Java8 ");
}
}).start();
//Java 8 way:
new Thread(() -> System.out.println("In Java8!"));
函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。例如:
public void process(Runnable r){ r.run();
}
process(() -> System.out.println("This is awesome!!"));
此代码执行时将打印“This is awesome!!”
。Lambda表达式()-> System.out.println("This is awesome!!")
不接受参数且返回void。 这恰恰是Runnable接口中run方法的签名。所以我们使用的时候要保证Lambda表达式和函数式接口的抽象方法的签名一致
函数式接口
函数式接口就是只定义一个抽象方法的接口。
函数式接口(Functional Interface)是Java 8对一类特殊类型的接口的称呼。 这类接口只定义了唯一的抽象方法的接口(除了隐含的Object对象的公共方法),因此最开始也就做SAM类型的接口(Single Abstract Method)。
为什么会单单从接口中定义出此类接口呢? 原因是在Java Lambda的实现中, 开发组不想再为Lambda表达式单独定义一种特殊的Structural函数类型,称之为箭头类型(arrow type), 依然想采用Java既有的类型系统(class, interface, method等), 原因是增加一个结构化的函数类型会增加函数类型的复杂性,破坏既有的Java类型,并对成千上万的Java类库造成严重的影响。 权衡利弊, 因此最终还是利用SAM 接口作为 Lambda表达式的目标类型。
JDK中已有的一些接口本身就是函数式接口,如Runnable
。 JDK 8中又增加了java.util.function
包, 提供了常用的函数式接口。
函数式接口代表的一种契约, 一种对某个特定函数类型的契约。 在它出现的地方,实际期望一个符合契约要求的函数。 Lambda表达式不能脱离上下文而存在,它必须要有一个明确的目标类型,而这个目标类型就是某个函数式接口。
四个常见的函数式接口:
接口类型 | 接口 | 含义 | 常用方法 |
---|---|---|---|
功能型函数式接口 | Function<T,R> | 接受一个输入参数,返回一个结果 | apply() |
消费型函数式接口 | Consumer<T> | 输入一个参数且无返回结果 | accept() |
断言型函数式接口 | Predicate<T> | 接受一个输入参数,返回一个布尔值 | test() |
供给型函数式接口 | Supplier<T> | 无参数,返回一个结果 | get() |
Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。
当然function
包里不止这一些预定义的函数式接口,还有诸如二元操作(BiConsumer、BiFunction、BiPredicate等),针对于基本类型的(DoubleFunction、IntConsumer、LongSupplier等),提取相同的泛型的(BinaryOperator、LongBinaryOperator等)等等,具体可以查看jdk的function
包了解更多。
类型检查、类型推断
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。
有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。
Lambda可以从赋值的上下文、方法调用的上下文(参数和返回值),以及类型转换的上下文中获得目标类型。
特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean
,而不是Consumer
上下文(T -> void)
所要求的void
:
// Predicate 返 回 了 一 个
boolean Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);
Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。例如:
Comparator<Apple> c =(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c =(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
局部变量的使用
Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。例如,下面的Lambda捕获了portNumber
变量:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
尽管如此,还有一点点小麻烦:关于能对这些变量做什么有一些限制。Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final, 或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。) 例如,下面的代码无法编译,因为portNumber 变量被赋值两次:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
你可能会问为什么局部变量有这些限制?
第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。Lambda是单独在一个线程中运行的,因此Java在访问自由局部变量时,实际上是在访问它的副本。如果可以调用局部变量可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此为了避免导致一些线程不安全的行为才做了这个限制
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中解释,这种模式会阻碍很容易做到的并行处理)。
Java8的Lambda和匿名类可以做类似于闭包的事情:
它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。如前所述,这种限制存在的原因在于局部变量保存在栈上, 并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(实例变量可以,因为它们保存在堆中,而堆 是在线程之间共享的)。
方法引用
如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。显式地指明方法的名称,你的代码的可读性会更好。这是lambda表达式的一个简化写法,所引用的方法其实是lambda表达式的方法体实现,语法也很简单,
左边是容器(可以是类名,实例名),
中间是::
,
右边是相应的方法名
要求:实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致!至于返回值不作要求。写法:
ObjectReference::methodName
几种方法引用的方法:
引用方式 | 代码写法 |
---|---|
类构造器引用 | ClassName::new |
类型上静态方法引用 | ClassName::methodName |
实例上实例方法引用 | instanceReference::methodName |
类型上实例方法引用 | ClassName::methodName |
//构造方法的引用
Supplier<User> supplier = User::new;
//静态方法的引用
Supplier<User> supplier =User::staticMethod;
实例上实例方法引用
Supplier<String> supplier = new User()::printAndReturn;
//类型上实例方法引用
BiPredicate<String, String> biPredicate = String::equals;
@Data
public class User {
public User(String name) {
this.name = name;
}
public User() {
}
public static User staticMethod (){
System.out.println("静态方法");
return null;
}
private Address address;
private String name;
private String gender;
private int age;
private List<String> emails;
public void printUserSay(String say){
System.out.println("User Speak Ba la Ba la "+say );
}
public void printSomething(){
System.out.println("User Speak Ba la Ba la " );
}
public String printAndReturn(){
System.out.println("User Speak Ba la Ba la " );
return null;
}
public User(String name, String gender, int age) {
this.name = name;
this.gender = gender;
this.age = age;
}
public void printUser(User user){
System.out.println("User Speak Ba la Ba la "+user.getName() );
}
}
那么问题来了!
什么时候用实例上实例方法引用(instanceReference::methodName),什么时候用类型上实例方法引用(ClassName::methodName)
User user = new User();
List<User> users = Lists.newArrayList();
users.forEach(User::printSomething);
users.forEach(user::printSomething);//wrong 无法编译
//foreach源码 可以看到foreach是接收一个Consumer
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
接口的第一个参数需要具有该方法,且剩下的参数与方法的参数保持一致,且第一个参数要是该引用方法的所在的类型或其父类。
换句话说:若Lambda表达式的参数列表的第一个参数,是实例方法的调用者,后面的参数(或无参)是实例方法的参数时,就可以使用类名::方法名的形式。
所以回到例子上,我们可以看到printSomething
这个方法是没有入参和返回结果的,如果我们使用User::printSomething
,正好符合上文说的如果使用类名::方法名那么你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。所以也就符合了consumer
的要求了。