本文首先发表在 码蜂笔记:http://coderbee.net/index.php/java/20130914/467
测试环境
$ java -version
java version "1.8.0-ea"
Java(TM) SE Runtime Environment (build 1.8.0-ea-b106)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b48, mixed mode)
IntelliJ IDEA 12.1.4
接口改进
以前Java的接口里只能声明方法和定义常量,现在可以在接口里定义静态方法和默认方法。
定义静态方法
定义静态(static)方法带来的好处就是可以减少创建工具类的需求了。比如 java.util.Collection
接口定义了一个集合,对于此接口实例操作的很多通用方法都是通过工具类 java.util.Collections
来提供的,现在可以在接口里定义静态方法,我们就可以把工具类里的静态方法直接移到接口类里,不需要独立创建工具类了。
下面是Java8里添加到接口 java.util.Comparator
的一个静态方法:
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}
定义默认方法
另一个改进是,可以在不破坏现有接口实现的前提下添加默认(default)方法。比如 java.lang.Iterable
接口现在拥有一个默认的 forEach
方法:
public default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
默认方法不能覆盖 equals, hashCode, toString
方法
一个接口不能提供Object
类里的任何方法的默认实现。这意味着不能在接口里提供 equals, hashCode, toString
方法的默认实现。
关于这个,Brian Goetz 给出了四个理由,具体见: response to “Allow default methods to override Object’s methods”。
这里列出一个具有足够说服力的理由:
It would become more difficult to reason about when a default method is invoked. Right now it’s simple: if a class implements a method, that always wins over a default implementation. Since all instances of interfaces are Objects, all instances of interfaces have non-default implementations of equals/hashCode/toString already. Therefore, a default version of these on an interface is always useless, and it may as well not compile.
大意是:由于所有接口的实例都是Objects,那么所有实例都已有 equals/hashCode/toString
这些方法的非默认实现。因此,接口上这些方法的默认版本是没有用的,可能不会被编译。
函数接口
函数接口是Java8引入的一个核心概念。如果一个接口只定义了一个抽象方法,它就是函数接口。例如java.lang.Runnable
就是一个函数接口,因为它只定义了一个抽象方法: public void run()
。
默认方法不是抽象的,所以一个函数接口可以定义任意多的默认方法。
新引入的注解 @FunctionalInterface
用来表明接口准备成为函数接口,但不管有没有这个注解,编译器都认为只有一个抽象方法的接口是函数接口。
Lambdas
函数接口一个极具价值的属性是可以在lambdas里实例化。下面是lambdas的一些实例:
(param1 ParamType1, param2 ParamTeyp2, ... ) -> { statements; } // lambdas 完整形式
(int x, int y) -> { return x + y; } // 完整输入参数类型,具有返回语句的语句块
(x, y) -> x + y // 自动类型推导参数类型,语句块只有一条 return 语句时可以省略大括号和return关键字,
x -> x * x // 只有一个输入参数,可以省略入参列表的小括号。
() -> x // 如果没有入参,小括号不能省略。
x -> { System.out.println(x); } // 没有返回语句,语句块的大括号不能省略。
方法引用
静态方法引用: String::valueOf
。
非静态方法引用: Object::toString
。
构造函数引用: ArrayList::new
。
捕获方法引用: x::toString
。
方法引用等价的lambda表达式:
String::valueOf x -> String.valueOf(x)
Object::toString x -> x.toString()
x::toString () -> x.toString() // 这个好像不行
ArrayList::new () -> new ArrayList<>()
当然,在Java里可以重载方法。类可以有很多有同样名字但不同参数的方法,构造函数也一样。用哪个方法取决于它是用于哪个函数接口的。
一个lambda与一个函数接口有相同的“形状”时被认为是兼容的。这里的“形状”是指:输入输出的类型和声明的受检查异常。
举例
List<String> strList = new ArrayList<>();
strList.add("no");
strList.add("hello");
strList.add("world");
long count = strList
.stream()
.filter((String str) -> { return str != null; }) // 完整的lambda表达式
.filter(str -> { return str.length() > 0; }) // 参数类型自动推导;只有一个输入参数,省略小括号
.filter(str -> str.length() > 3) // 语句块只是返回一个表达式的值,省略return语句直接返回表达式的值;省略语句块的花括号。
.map(String::length) // 用方法代替lambda表达式
.count();
System.out.println("\n\ncount:" + count);
Runnable r = () -> { System.out.println("Running!"); };
Comparator<String> c = (a, b) -> Integer.compare(a.length(), b.length());
捕获与非捕获lambdas
如果lambda表达式访问一个定义在lambda体之外的非静态变量或对象,则说Lambda表达式是“捕获的”。在下面的例子里,return返回的lambda捕获了变量 x
:
int x = 5;
return y -> x + y;
为了让这个lambda声明是合法的,它捕获的变量必须是“effectively final”。这样,它们必须标记为 final
或 赋值(被lambda捕获)之后就不能再修改。
一个lambda捕获与否对性能有影响。一个非捕获的lambda一般比一个捕获的更高效。虽然这没有在任何规范里定义,你不能依赖于程序员的正确性。一个非捕获的lambda只需要计算一次。然后,它将返回一个完全相同的实例。捕获的lambda在每次遇到时都需要进行计算,当前的动作与实例化匿名内部类非常类似。
举例
private static int count = 0;
public static void main(String[] args) {
// basic ();
testCapture();
}
public static void testCapture() {
Runnable run = capture();
count++;
Runnable run2 = capture();
new Thread(run).start();
new Thread(run2).start();
}
public static Runnable capture() {
int x = 5;
Runnable r = () -> { System.out.println("x is:" + x); System.out.println("count is:" + count); };
// 在这里给x赋值也会报错 Variable used in lambda expression should be effectively final
return r;
}
执行后的输出是:
x is:5
count is:1
x is:5
count is:1
对于x的值都没有问题,但count的值就要注意了。进入testCapture()
方法的时候,count的值是0,第一次捕获的时候也是0,然后进行加1,再次进行捕获,然后用两个线程计算捕获的值,得到的值是1和1,而不是0和1,因为,lambda捕获只是捕获了变量的引用,而不是变量的值。
lambdas 不能做的
有些约束是lambdas不提供的,为了简单和由于时间约束。
非final变量捕获
如果一个变量被赋予新值,它就不能用在lambda里。“final”关键字不是必须的,但变量必须是“effectively final”(如前所述)。下面的代码将报编译错误:
int count = 0;
List<String> strings = Arrays.asList("a", "b", "c");
strings.forEach(s -> {
count++; // error: can't modify the value of count
});
注意,如果count是个非局部变量,它将是可以修改的。
异常透明
如果lambda代码体会抛出受检查的异常,函数接口必须声明它会抛出的受检查异常。异常不会传播到包含方法。下面的代码不能编译:
void appendAll(Iterable<String> values, Appendable out)
throws IOException { // doesn't help with the error
values.forEach(s -> {
out.append(s); // error: can't throw IOException here
// Consumer.accept(T) doesn't allow it
});
}
简单说就是lambda的代码块不能抛出受检查异常。
流程控制(break,early return)
在上面的forEach例子里,传统的“continue”可以是通过lambda体内的“return;”语句来达到。然而,没有方法来中断loop循环或从lambda内返回一个结果到包含方法。举例:
import java.util.Arrays;
public class Lambdas2 {
public static void main(String[] args) {
Arrays.asList(args).forEach(s -> {
if (s.length() > 3) {
return; // 这个return语句并不能终止loop循环。
}
});
}
}
上面的代码生成的字节码是这样的:
$ javap -c Lambdas2.class
Compiled from "Lambdas2.java"
public class Lambdas2 {
public Lambdas2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: aload_0
1: invokestatic #2 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
4: invokedynamic #3, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
9: invokeinterface #4, 2 // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)V
14: return
}
结合前面 java.lang.Iterable
接口的forEach
方法实现可知,目前的lambda应该是编译成一个对象的,然后再对每个元素调用此对象的accept
方法,也就是说lambda代码块被编译成Consumer的accept方法的代码,所以不能在lambda代码块里控制forEach
的loop流程。
为什么抽象类不能用lambda实例化?
一个抽象类,即使它只有一个抽象方法,也不能用lambda实例化。
大多数反对这个的争论是这会增加阅读lambda的困难。以这种方式实例化一个抽象类会导致执行额外的隐藏代码:抽象类构造器里的代码。
另一个理由是这抛弃了优化lambda的可能。在将来,lambda可能将不再实例化为对象。允许用户以lambda的形式声明抽象类将阻止这类优化。
更多解释见:response to “Allow lambdas to implement abstract classes”
相关推荐
该书由 Mario Fusco、Alan Mycroft 和 Raoul-Gabriel Urma 合著,旨在帮助读者深入了解 Java 8,并掌握其中的关键...其他新特性: 简要介绍 Java 8 中引入的其他新特性,如接口的默认方法、方法引用、Optional 类型等。
Java 8引入了许多新的功能和改进,包括Lambda表达式、接口默认方法、函数式接口、流API、Date/Time API、Nashorn JavaScript引擎等。这些新功能和改进使得Java编程变得更加简单、直观和高效,并提升了JVM的性能和...
java8集合源码java8 lambda 流 rxjava 在 Java 8 版本中,Java 提供了对函数式编程、新的 JavaScript 引擎、用于日期时间操作的新 API、新的流 API 等的支持。 新的功能: Lambda 表达式 向 Java 添加函数处理能力。...
Jdk8新特性 Jdk9新特性 jdk10新特性 jdk11新特性 jdk12新特性 jdk13新特性 Jdk14新特性 Jdk新特性 总结的不全,还请各位同学补充。 Jdk8新特性 Lambda / 方法引用 接口新增default方法 Stream API Optional API 新...
本文档将Java8的新特新逐一添加,比如如何使用默认接口方法,lambda表达式,方法引用以及多重Annotation,之后你将会学到最新的API上的改进,比如流,函数式接口,Map以及全新的日期API。祝你Java技术更上一层楼!
JDK 1.8最显著的特性是它引入了Lambda 表达式、接口默认方法和静态方法、新的Java类库支持,以及一些性能和安全改进。Lambda表达式使得编写具有函数式编程风格的代码更加容易,而默认方法和静态方法使得接口可以实现...
第9章你可能错过的Java 7特性 188 9.1 异常处理改进 190 9.1.1 try-with-resources语句 190 9.1.2 忽略异常 191 9.1.3 捕获多个异常 192 9.1.4 更简单地处理反射方法的异常 193 9.2 使用文件 193 9.2.1 Path 194 ...
支持 Java 8 的新特性:JDK-8u333 支持 Java 8 中的新特性,如 Lambda 表达式、Stream API、函数式接口等。 兼容性改进:JDK-8u333 增强了与其他 Java 版本的兼容性,提高了向后兼容性。 一些 API 和语言改进:这个...
java8流源码Java8特性 实现 Java8 特性。 理解 lambdas - Lambdas 是存在于隔离中的函数。 Lambda 表现为匹配接口的实例。 . 使用 lambdas 。 函数式接口 - 接口是 Lambda 的类型,具有单一抽象方法的接口称为函数式...
包括生产力、易用性、改进的多语言编程、安全性和改进的性能等特性。 欢迎来到最大的、开放的、基于标准的、社区驱动的平台的最新版本。 1- Lambda 表达式,一种新的语言功能,已在此版本中引入。 它们使您能够将...
java8流源码java8-功能 一些重要的 Java 8 特性是 Java 流 API Iterable 接口中的 forEach() 方法 接口中的默认和静态方法 函数式接口和 Lambda 表达式 Java时间API 集合 API 改进 并发 API 改进 选修课 Java 中的 ...
本教程将Java8的新特新逐一列出,并将使用简单的代码示例来指导你如何使用默认接口方法,lambda表达式,方法引用以及多重Annotation,之后你将会学到最新的API上的改进,比如流,函数式接口,Map以及全新的日期API
1. Lambda表达式:Lambda表达式是一种新的语言特性,允许将函数作为参数传递给方法,使得代码更加简洁和易于阅读。 2. Stream API:Stream API是一个新的数据处理框架,允许以声明式方式处理数据集合,提供了更高效...
所以 Spring4 必须支持 Java6,7 和8,为了保持向后兼容性, Spring 框架没有适应 Java8 带来的许多新特性,比如 lambda 表达式。 Spring5 的基准版本为8,因此它使用了 Java8 和9的许多新特性。例如: Spring ...
Ivor Horton是撰著Java、C和C++编程语言图书的杰出作家之一。大家一致认为,他的著作独具风格,无论是编程新手,还是经验丰富的编程人员,都很容易理解其内容。在个人实践中,Ivor Horton也是一名系统顾问。他从事...