1. Lambda表达式
函数式接口
,如OnClickListener接口只有一个方法,Java中大多数回调接口都有这个特征:比如Runnable和Comparator;我们把这些只拥有一个方法的接口称之为函数式接口
。
Lambda表达式(也称为闭包)允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据:函数式程序员对这一概念非常熟悉。
Lambda表达式是匿名方法。
(int x, int y) -> x + y //接收x和y两个整形参数并返回他们的和
() -> 66 //不接收任何参数直接返回66
(String name) -> {System.out.println(name);} //接收一个字符串然后打印出来
(View view) -> {view.setText("lalala");} //接收一个View对象并调用setText方法
Lambda表达式语法由参数列表
、->
和函数体
组成。函数体既可以是一个表达式也可以是一个代码块。
-
表达式:表达式会被执行然后返回结果。它简化掉了
return
关键字。 - 代码块:顾名思义就是一坨代码,和普通方法中的语句一样。
目标类型--上下文推导
通过前面的例子我们可以看到,lambda表达式没有名字,那我们怎么知道它的类型呢?答案是通过上下文推导而来的。
OnClickListener listener = (View v) -> {v.setText("lalala");};
Runnable runnable = () -> doSomething(); //这个表达式是Runnable类型的
Callback callback = () -> doSomething(); //这个表达式是Callback类型的
编译器利用lambda表达式所在的上下文所期待的类型来推导表达式的类型,这个被期待的类型被称为目标类型
。lambda表达式只能出现在目标类型为函数式接口
的上下文中。
Lambda表达式的类型和目标类型的方法签名必须一致,编译器会对此做检查,一个lambda表达式要想赋值给目标类型
T
则必须满足下面所有的条件:
T
是一个函数式接口- lambda表达式的参数必须和
T
的方法参数在数量、类型和顺序上一致(一一对应)- lambda表达式的返回值必须和
T
的方法的返回值一致或者是它的子类- lambda表达式抛出的异常和
T
的方法的异常一致或者是它的子类
由于目标类型是知道lambda表达式的参数类型,所以我们没必要把已知的类型重复一遍。也就是说lambda表达式的参数类型可以从目标类型获取:
//编译器可以推导出s1和s2是String类型
Comparator<String> c = (s1, s2) -> s1.compareTo(s2);
//当表达式的参数只有一个时括号也是可以省略的
button.setOnClickListener(v -> v.setText("lalala"));
作用域--this仍指向外部环境
在内部类中使用变量名和this非常容易出错。内部类通过继承得到的成员变量(包括来说object的)可能会把外部类的成员变量覆盖掉,未做限制的this引用会指向内部类自己而非外部类。
而lambda表达式的语义就十分简单:基于词法作用域的理念,它不会从父类中继承任何变量,也不用引入新的作用域。lambda表达式的参数及函数体里面的变量和它外部环境的变量具有相同的语义(this关键字也是一样)。
变量捕获
在Java7中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格:如果捕获的变量没有被声明为final
就会产生一个编译错误。但是在Java8中放宽了这一限制--对于lambda表达式和内部类,允许在其中捕获那些符合有效只读的局部变量(如果一个局部变量在初始化后从未被修改过,那么它就是有效只读)。
Runnable getRunnable(String name){
String hello = "hello";
return () -> System.out.println(hello + "," + name);
}
对于this
的引用以及通过this
对未限定字段的引用和未限定方法的调用本质上都属于使用final
局部变量。包含此类引用的lambda表达式相当于捕获了this
实例。在其他情况下,lambda对象不会保留任何对this
的应用。
这个特性对内存管理是极好的:要知道在java中一个非静态内部类会默认持有外部类实例的强引用,这往往会造成内存泄露。而在lambda表达式中如果没有捕获外部类成员则不会保留对外部类实例的引用。
不过尽管Java8放宽了对捕获变量的语法限制,但试图修改捕获变量的行为是被禁止的,比如下面这个例子就是非法的:
int sum = 0;
list.forEach(i -> {sum += i;});
为什么要禁止这种行为呢?因为这样的lambda表达式很容易引起race condition,可以使用Stream API
来实现这种行为。
2. 方法引用
lambda表达式允许我们定义一个匿名方法,并以函数式接口的方式使用它。Java8能够在已有的方法上实现同样的特性。
方法引用和lambda表达式拥有相同的特性(他们都需要一个目标类型,并且需要被转化为函数式接口的实例),不过我们不需要为方法引用提供方法体,我们可以直接通过方法名引用已有方法。
以下面的代码为例,假设我们要按照userName
排序
class User{
private String userName;
public String getUserName() {
return userName;
}
...
}
List<User> users = new ArrayList<>();
Comparator<User> comparator = Comparator.comparing(u -> u.getUserName());
Collections.sort(users, comparator);
我们可以用方法引用替换上面的lambda表达式
Comparator<User> comparator = Comparator.comparing(User::getUserName);
这里的User::getUserName
被看做是lambda表达式的简写形式。尽管方法引用不一定会把代码变得更紧凑,但它拥有更明确的语义--如果我们想要调用的方法拥有一个名字,那么我们就可以通过方法名调用它。
方法引用有很多种,它们的语法如下:
- 静态方法引用:ClassName::methodName
- 实例上的实例方法引用:instanceReference::methodName
- 超类上的实例方法引用:super::methodName
- 类型上的实例方法引用:ClassName::methodName
- 构造方法引用:Class::new
- 数组构造方法引用:TypeName[]::new
方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
下面,我们以定义了4个方法的Car这个类作为例子,区分Java中支持的4种不同的方法引用。
`public` `static` `class` `Car {`` ``public` `static` `Car create( ``final` `Supplier< Car > supplier ) {`` ``return` `supplier.get();`` ``} `` ` ` ``public` `static` `void` `collide( ``final` `Car car ) {`` ``System.out.println( ``"Collided "` `+ car.toString() );`` ``}`` ` ` ``public` `void` `follow( ``final` `Car another ) {`` ``System.out.println( ``"Following the "` `+ another.toString() );`` ``}`` ` ` ``public` `void` `repair() { `` ``System.out.println( ``"Repaired "` `+ ``this``.toString() );`` ``}``}`
`Collided com.javacodegeeks.java8.method.references.MethodReferences$Car``@7a81197d``Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car``@7a81197d``Following the com.javacodegeeks.java8.method.references.MethodReferences$Car``@7a81197d`
关于方法引用的更多详情请参考官方文档。
3. 默认方法
在Java中一个接口一旦发布就已经被定型,除非我们能够一次性的更新所有该接口的实现,否者在接口的添加新方法将会破坏现有接口的实现。默认方法就是为了解决这一问题的,这样接口在发布之后依然能够继续演化。
默认方法就是向接口增加新的行为。它是一种新的方法:接口方法可以是抽象的或者是默认的。默认方法拥有默认实现,接口实现类通过继承得到该默认实现。默认方法不是抽象的,所以我们可以放心的向函数式接口里增加默认方法,而不用担心函数式接口单抽象方法的限制。
public interface Iterator<E> {
boolean hasNext();
E next();
//默认方法
default void remove() {
throw new UnsupportedOperationException("remove");
}
//默认方法
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
和其他方法一样,默认方法也可以被继承。
除了上面看到的默认方法,Java8中还允许我们在接口中定义静态方法。这使得我们可以从接口中直接调用它相关的辅助方法,而不是从其它的辅助类中调用(如Collections)。在做集合中元素比较的时候,我们一般需要使用静态辅助方法生成实现Comparator的比较器,在Java8中我们可以直接把该静态方法定义在Comparator接口中:
//生成实现Comparator的比较器
public static <T, U extends Comparable<? super U>>
Comparator<T> comparing(Function<T, U> keyExtractor) {
return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
4. Stream-集合功能增强
Stream作为Java8的新特性,主要用于对集合对象进行各种非常便利高效的聚合和大批量数据的操作。结合Lambda表达式可以极大的提高开发效率和代码可读性。
假设我们需要把一个集合中的所有形状设置成红色,那么我们可以这样写
//以前的写法,外部迭代
for (Shape shape : shapes){
shape.setColor(RED)
}
如果使用Java8扩展后的集合框架则可以这样写:
//java8 stream ,内部迭代
shapes.foreach(s -> s.setColor(RED));
什么是Stream
像一个更高级的Interator
,Stream提供了强大的数据集合操作功能,并被深入整合到现有的集合类和其它的JDK类型中。流的操作可以被组合成流水线(Pipeline)。
//只改变蓝色的变为红色
shapes.stream()//生成该集合元素的流
.filter(s -> s.getColor() == BLUE)//只包含蓝色形状的流
.forEach(s -> s.setColor(RED));
//把蓝色的形状提取到新的List里
List<Shape> blue = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.collect(Collectors.toList());//把其接收的元素聚集到一起(这里是List)
//`collect()`方法的参数被用来指定如何进行聚集操作,`toList()`以把元素输出到List中
如果每个形状都被保存在Box
里,然后我们想知道哪个盒子至少包含一个蓝色形状,我们可以这么写:
Set<Box> hasBlueShape = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.map(s -> s.getContainingBox())//shape 里面有个getContainingBox()
.collect(Collectors.toSet());
map()
操作通过映射函数(这里的映射函数接收一个形状,然后返回包含它的盒子)对输入流里面的元素进行依次转换,然后产生新流。
如果我们需要得到蓝色物体的总重量,我们可以这样表达:
int sum = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.mapToInt(s -> s.getWeight())//shape 里面有个getWeight(),返回值是int类型
.sum();
Stream vs Collection
流(Stream)和集合(Collection)的区别:
- Collection主要用来对元素进行管理和访问;
- Stream并不支持对其元素进行直接操作和直接访问,而只支持通过声明式操作在其之上进行运算后得到结果;
- Stream不存储值
- 对Stream的操作会产生一个结果,但是Stream并不会改变数据源;
- 大多数Stream的操作(filter,map,sort等)都是以惰性的方式实现的。这使得我们可以使用一次遍历完成整个流水线操作,并可以用短路操作提供更高效的实现。
惰性求值 vs 急性求值
filter()
和map()
这样的操作既可以被急性求值,也可以被惰性求值,在实际中进行惰性运算可以带来很多好处。
惰性运算(延迟运算)实际中可以带来很多好处,比如说,如果我们进行惰性过滤,我们就可以把过滤和流水线里的其它操作混合在一起,从而不需要对数据进行多遍遍历。相类似的,如果我们在一个大型集合里搜索第一个满足某个条件的元素,我们可以在找到后直接停止,而不是继续处理整个集合。(这一点对无限数据源是很重要,惰性求值对于有限数据源起到的是优化作用,但对无限数据源起到的是决定作用,没有惰性求值,对无限数据源的操作将无法终止)
对于filter()
和map()
这样的操作,我们很自然的会把它当成是惰性求值操作,不过它们是否真的是惰性取决于它们的具体实现。另外,像sum()
这样生成值的操作和forEach()
这样产生副作用的操作都是天然急性求值,因为它们必须要产生具体的结果。
我们拿下面这段代码举例:
int sum = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.mapToInt(s -> s.getWeight())
.sum();
这里的filter()
和map()
都是惰性的,这就意味着在调用sum()
之前不会从数据源中提取任何元素。在sum()
操作之后才会把filter()
、map()
和sum()
放在对数据源一次遍历中。这样可以大大减少维持中间结果所带来的开销。
例
假设我们有一个房源库项目,这个房源库中有一系列的小区,每个小区都有小区名和房源列表,每套房子又有价格、面积等属性。现在我们需要筛选出含有100平米以上房源的小区,并按照小区名排序。
我们先来看看不用Streams API如何实现:
List<Community> result = new ArrayList<>();
for (Community community : communities) {
for (House house : community.houses) {
if (house.area > 100) {
result.add(community);
break;
}
}
}
Collections.sort(result, new Comparator<Community>() {
@Override
public int compare(Community c1, Community c2) {
return c1.name.compareTo(c2.name);
}
});
return result;
如果使用Streams API:
return communities.stream()
.filter(c -> c.houses.stream().anyMatch(h -> h.area>100))
.sorted(Comparator.comparing(c -> c.name))
.collect(Collectors.toList());
以后所有的集合操作,全都都要转成stream操作!!!
5. 重复注解 -- 编码中不会直接用到
自从Java 5引入了注解机制,这一特性就变得非常流行并且广为使用。然而,使用注解的一个限制是相同的注解在同一位置只能声明一次,不能声明多次。Java 8打破了这条规则,引入了重复注解机制,这样相同的注解可以在同一地方声明多次。
重复注解机制本身必须用@Repeatable注解。事实上,这并不是语言层面上的改变,更多的是编译器的技巧,底层的原理保持不变。让我们看一个快速入门的例子:
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class RepeatingAnnotations {
/*注解类*/
/*Filters仅仅是Filter注解的数组,但程序员不会显示的调用该注解,Java编译器并不想让程序员意识到Filters的存在*/
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
public @interface Filters {
Filter[] value();
}
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
//重复该注解时,自动替换为Filters注解
@Repeatable( Filters.class )
public @interface Filter {
String value();
}
@Filter( "filter1" )
@Filter( "filter2" )
public interface Filterable {
}
public static void main(String[] args) {
for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
System.out.println( filter.value() );
}
}
}
正如我们看到的,这里有个使用@Repeatable( Filters.class )注解的注解类Filter,Filters仅仅是Filter注解的数组,但Java编译器并不想让程序员意识到Filters的存在。这样,接口Filterable就拥有了两次Filter(并没有提到Filters)注解。
同时,反射相关的API提供了新的函数getAnnotationsByType()来返回重复注解的类型(请注意Filterable.class.getAnnotation( Filters.class )经编译器处理后将会返回Filters的实例)。
程序输出结果如下:
filter1
filter2
6. Android Studio中应用java8
Jack(Java Android Compiler Kit)
要想在Android项目中使用Java8的新特性,要采用新的Jack(Java Android Compiler Kit)编译。新的 Android 工具链将 Java 源语言编译成 Android 可读取的 Dalvik 可执行文件字节码,且有其自己的 .jack 库格式,在一个工具中提供了大多数工具链功能:重新打包、压缩、模糊化以及 Dalvik 可执行文件分包。
以下是构建 Android Dalvik 可执行文件可用的两种工具链的对比:
- 旧版 javac 工具链:
javac (.java --> .class) --> dx (.class --> .dex)
- 新版 Jack 工具链:
Jack (.java --> .jack --> .dex)
配置
为了在项目中使用Java8,我们还需要项目module中的gradle.build文件中加入如下代码:
android {
defaultConfig {
****
jackOptions {
enabled true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
如果你项目的minSdkVersion>=24,还可以使用Stream API。