函数编程很是关键的几个特性以下:java
函数编程支持函数做为第一类对象,有时称为 闭包或者 仿函数(functor)对象。实质上,闭包是起函数的做用并能够像对象同样操做的对象。
与此相似,FP 语言支持 高阶函数。高阶函数能够用另外一个函数(间接地,用一个表达式) 做为其输入参数,在某些状况下,它甚至返回一个函数做为其输出参数。这两种结构结合在一块儿使得能够用优雅的方式进行模块化编程,这是使用 FP 的最大好处。程序员
在惰性计算中,表达式不是在绑定到变量时当即计算,而是在求值程序须要产生表达式的值时进行计算。延迟的计算使您能够编写可能潜在地生成无穷输出的函数。由于不会计算多于程序的其他部分所须要的值,因此不须要担忧由无穷计算所致使的 out-of-memory 错误。express
所谓"反作用"(side effect),指的是函数内部与外部互动(最典型的状况,就是修改全局变量的值),产生运算之外的其余结果。函数式编程强调没有"反作用",意味着函数要保持独立,全部功能就是返回一个新的值,没有其余行为,尤为是不得修改外部变量的值。
综上所述,函数式编程能够简言之是: 使用不可变值和函数, 函数对一个值进行处理, 映射成另外一个值。这个值在面向对象语言中能够理解为对象,另外这个值还能够做为函数的输入。编程
完整的Lambda表达式由三部分组成:参数列表、箭头、声明语句;数组
(Type1 param1, Type2 param2, ..., TypeN paramN) -> { statment1; statment2; //............. return statmentM;}
1. 绝大多数状况,编译器均可以从上下文环境中推断出lambda表达式的参数类型,因此参数能够省略:安全
(param1,param2, ..., paramN) -> { statment1; statment2; //............. return statmentM;}
二、 当lambda表达式的参数个数只有一个,能够省略小括号:数据结构
param1 -> { statment1; statment2; //............. return statmentM;}
三、 当lambda表达式只包含一条语句时,能够省略大括号、return和语句结尾的分号:闭包
param1 -> statment
Java 是一门面向对象编程语言。面向对象编程语言和函数式编程语言中的基本元素(Basic Values)均可以动态封装程序行为:面向对象编程语言使用带有方法的对象封装行为,函数式编程语言使用函数封装行为。但这个相同点并不明显,由于Java 对象每每比较“重量级”:实例化一个类型每每会涉及不一样的类,并须要初始化类里的字段和方法。oracle
不过有些 Java 对象只是对单个函数的封装。例以下面这个典型用例:Java API 中定义了一个接口(通常被称为回调接口),用户经过提供这个接口的实例来传入指定行为,例如:app
1
2
3
|
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
|
这里并不须要专门定义一个类来实现 ActionListener
,由于它只会在调用处被使用一次。用户通常会使用匿名类型把行为内联(inline):
1
2
3
4
5
|
button.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
});
|
不少库都依赖于上面的模式。对于并行 API 更是如此,由于咱们须要把待执行的代码提供给并行 API,并行编程是一个很是值得研究的领域,由于在这里摩尔定律获得了重生:尽管咱们没有更快的 CPU 核心(core),可是咱们有更多的 CPU 核心。而串行 API 就只能使用有限的计算能力。
随着回调模式和函数式编程风格的日益流行,咱们须要在Java中提供一种尽量轻量级的将代码封装为数据(Model code as data)的方法。匿名内部类并非一个好的 选择,由于:
this
和变量名容易令人产生误解final
的局部变量上面的多数问题均在Java SE 8中得以解决:
不过,Java SE 8 的目标并不是解决全部上述问题。所以捕获可变变量(问题 4)和非局部控制流(问题 5)并不在 Java SE 8的范畴以内。(尽管咱们可能会在将来提供对这些特性的支持)
尽管匿名内部类有着种种限制和问题,可是它有一个良好的特性,它和Java类型系统结合的十分紧密:每个函数对象都对应一个接口类型。之因此说这个特性是良好的,是由于:
上面提到的 ActionListener
接口只有一个方法,大多数回调接口都拥有这个特征:好比 Runnable
接口和 Comparator
接口。咱们把这些只拥有一个方法的接口称为 函数式接口。(以前它们被称为 SAM类型,即 单抽象方法类型(Single Abstract Method))
咱们并不须要额外的工做来声明一个接口是函数式接口:编译器会根据接口的结构自行判断(判断过程并不是简单的对接口方法计数:一个接口可能冗余的定义了一个 Object
已经提供的方法,好比 toString()
,或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴)。不过API做者们能够经过 @FunctionalInterface
注解来显式指定一个接口是函数式接口(以免无心声明了一个符合函数式标准的接口),加上这个注解以后,编译器就会验证该接口是否知足函数式接口的要求。
实现函数式类型的另外一种方式是引入一个全新的 结构化 函数类型,咱们也称其为“箭头”类型。例如,一个接收 String
和 Object
并返回 int
的函数类型能够被表示为 (String, Object) -> int
。咱们仔细考虑了这个方式,但出于下面的缘由,最终将其否认:
m(T->U)
和 m(X->Y)
进行重载(Overload)因此咱们选择了“使用已知类型”这条路——由于现有的类库大量使用了函数式接口,经过沿用这种模式,咱们使得现有类库可以直接使用 lambda 表达式。例以下面是 Java SE 7 中已经存在的函数式接口:
除此以外,Java SE 8中增长了一个新的包:java.util.function
,它里面包含了经常使用的函数式接口,例如:
Predicate<T>
——接收 T
并返回 boolean
Consumer<T>
——接收 T
,不返回值Function<T, R>
——接收 T
,返回 R
Supplier<T>
——提供 T
对象(例如工厂),不接收值UnaryOperator<T>
——接收 T
对象,返回 T
BinaryOperator<T>
——接收两个 T
,返回 T
除了上面的这些基本的函数式接口,咱们还提供了一些针对原始类型(Primitive type)的特化(Specialization)函数式接口,例如 IntSupplier
和 LongBinaryOperator
。(咱们只为 int
、long
和 double
提供了特化函数式接口,若是须要使用其它原始类型则须要进行类型转换)一样的咱们也提供了一些针对多个参数的函数式接口,例如 BiFunction<T, U, R>
,它接收 T
对象和 U
对象,返回 R
对象。
匿名类型最大的问题就在于其冗余的语法。有人戏称匿名类型致使了“高度问题”(height problem):好比前面 ActionListener
的例子里的五行代码中仅有一行在作实际工做。
lambda表达式是匿名方法,它提供了轻量级的语法,从而解决了匿名内部类带来的“高度问题”。
下面是一些lambda表达式:
1
2
3
|
(
int x, int y) -> x + y
() ->
42
(String s) -> { System.out.println(s); }
|
第一个 lambda 表达式接收 x
和 y
这两个整形参数并返回它们的和;第二个 lambda 表达式不接收参数,返回整数 ‘42’;第三个 lambda 表达式接收一个字符串并把它打印到控制台,不返回值。
lambda 表达式的语法由参数列表、箭头符号 ->
和函数体组成。函数体既能够是一个表达式,也能够是一个语句块:
return
语句会把控制权交给匿名方法的调用者break
和 continue
只能在循环中使用表达式函数体适合小型 lambda 表达式,它消除了 return
关键字,使得语法更加简洁。
lambda 表达式也会常常出如今嵌套环境中,好比说做为方法的参数。为了使 lambda 表达式在这些场景下尽量简洁,咱们去除了没必要要的分隔符。不过在某些状况下咱们也能够把它分为多行,而后用括号包起来,就像其它普通表达式同样。
下面是一些出如今语句中的 lambda 表达式:
1
2
3
4
5
6
7
8
|
FileFilter java = (File f) -> f.getName().endsWith(
"*.java");
String user = doPrivileged(() -> System.getProperty(
"user.name"));
new Thread(() -> {
connectToService();
sendNotification();
}).start();
|
须要注意的是,函数式接口的名称并非 lambda 表达式的一部分。那么问题来了,对于给定的 lambda 表达式,它的类型是什么?答案是:它的类型是由其上下文推导而来。例如,下面代码中的 lambda 表达式类型是 ActionListener
:
1
|
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());
|
这就意味着一样的 lambda 表达式在不一样上下文里能够拥有不一样的类型:
1
2
3
|
Callable<String> c = () ->
"done";
PrivilegedAction<String> a = () ->
"done";
|
第一个 lambda 表达式 () -> "done"
是 Callable
的实例,而第二个 lambda 表达式则是 PrivilegedAction
的实例。
编译器负责推导 lambda 表达式类型。它利用 lambda 表达式所在上下文 所期待的类型 进行推导,这个 被期待的类型 被称为 目标类型。lambda 表达式只能出如今目标类型为函数式接口的上下文中。
固然,lambda 表达式对目标类型也是有要求的。编译器会检查 lambda 表达式的类型和目标类型的方法签名(method signature)是否一致。当且仅当下面全部条件均知足时,lambda 表达式才能够被赋给目标类型 T
:
T
是一个函数式接口T
的方法参数在数量和类型上一一对应T
的方法返回值相兼容(Compatible)T
的方法 throws
类型相兼容因为目标类型(函数式接口)已经“知道” lambda 表达式的形式参数(Formal parameter)类型,因此咱们没有必要把已知类型再重复一遍。也就是说,lambda 表达式的参数类型能够从目标类型中得出:
1
|
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
|
在上面的例子里,编译器能够推导出 s1
和 s2
的类型是 String
。此外,当 lambda 的参数只有一个并且它的类型能够被推导得知时,该参数列表外面的括号能够被省略:
1
2
3
|
FileFilter java = f -> f.getName().endsWith(
".java");
button.addActionListener(e -> ui.dazzle(e.getModifiers()));
|
这些改进进一步展现了咱们的设计目标:“不要把高度问题转化成宽度问题。”咱们但愿语法元素可以尽量的少,以便代码的读者可以直达 lambda 表达式的核心部分。
lambda 表达式并非第一个拥有上下文相关类型的 Java 表达式:泛型方法调用和“菱形”构造器调用也经过目标类型来进行类型推导:
1
2
3
4
5
|
List<String> ls = Collections.emptyList();
List<Integer> li = Collections.emptyList();
Map<String, Integer> m1 =
new HashMap<>();
Map<Integer, String> m2 =
new HashMap<>();
|
以前咱们提到 lambda 表达式智能出如今拥有目标类型的上下文中。下面给出了这些带有目标类型的上下文:
? :
)在前三个上下文(变量声明、赋值和返回语句)里,目标类型便是被赋值或被返回的类型:
1
2
3
4
5
6
7
8
|
Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);
public Runnable toDoLater() {
return () -> {
System.out.println(
"later");
}
}
|
数组初始化器和赋值相似,只是这里的“变量”变成了数组元素,而类型是从数组类型中推导得知:
1
2
3
4
|
filterFiles(
new FileFilter[] {
f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith(
"q")
});
|
方法参数的类型推导要相对复杂些:目标类型的确认会涉及到其它两个语言特性:重载解析(Overload resolution)和参数类型推导(Type argument inference)。
重载解析会为一个给定的方法调用(method invocation)寻找最合适的方法声明(method declaration)。因为不一样的声明具备不一样的签名,当 lambda 表达式做为方法参数时,重载解析就会影响到 lambda 表达式的目标类型。编译器会经过它所得之的信息来作出决定。若是 lambda 表达式具备 显式类型(参数类型被显式指定),编译器就能够直接 使用lambda 表达式的返回类型;若是lambda表达式具备 隐式类型(参数类型被推导而知),重载解析则会忽略 lambda 表达式函数体而只依赖 lambda 表达式参数的数量。
若是在解析方法声明时存在二义性(ambiguous),咱们就须要利用转型(cast)或显式 lambda 表达式来提供更多的类型信息。若是 lambda 表达式的返回类型依赖于其参数的类型,那么 lambda 表达式函数体有可能能够给编译器提供额外的信息,以便其推导参数类型。
1
2
|
List<Person> ps = ...
Stream<String> names = ps.stream().map(p -> p.getName());
|
在上面的代码中,ps
的类型是 List<Person>
,因此 ps.stream()
的返回类型是 Stream<Person>
。map()
方法接收一个类型为 Function<T, R>
的函数式接口,这里 T
的类型便是 Stream
元素的类型,也就是 Person
,而 R
的类型未知。因为在重载解析以后 lambda 表达式的目标类型仍然未知,咱们就须要推导 R
的类型:经过对 lambda 表达式函数体进行类型检查,咱们发现函数体返回 String
,所以 R
的类型是 String
,于是 map()
返回 Stream<String>
。绝大多数状况下编译器都能解析出正确的类型,但若是碰到没法解析的状况,咱们则须要:
p
提供显式类型)以提供额外的类型信息Function<Person, String>
R
提供一个实际类型。(.<String>map(p -> p.getName())
)lambda 表达式自己也能够为它本身的函数体提供目标类型,也就是说 lambda 表达式能够经过外部目标类型推导出其内部的返回类型,这意味着咱们能够方便的编写一个返回函数的函数:
1
|
Supplier<Runnable> c = () -> () -> { System.out.println(
"hi"); };
|
相似的,条件表达式能够把目标类型“分发”给其子表达式:
1
|
Callable<Integer> c = flag ? (() ->
23) : (() -> 42);
|
最后,转型表达式(Cast expression)能够显式提供 lambda 表达式的类型,这个特性在没法确认目标类型时很是有用:
1
2
|
// Object o = () -> { System.out.println("hi"); }; 这段代码是非法的
Object o = (Runnable) () -> { System.out.println(
"hi"); };
|
除此以外,当重载的方法都拥有函数式接口时,转型能够帮助解决重载解析时出现的二义性。
目标类型这个概念不只仅适用于 lambda 表达式,泛型方法调用和“菱形”构造方法调用也能够从目标类型中受益,下面的代码在 Java SE 7 是非法的,但在 Java SE 8 中是合法的:
1
2
3
|
List<String> ls = Collections.checkedList(
new ArrayList<>(), String.class);
Set<Integer> si = flag ? Collections.singleton(
23) : Collections.emptySet();
|
在内部类中使用变量名(以及 this
)很是容易出错。内部类中经过继承获得的成员(包括来自 Object
的方法)可能会把外部类的成员掩盖(shadow),此外未限定(unqualified)的 this
引用会指向内部类本身而非外部类。
相对于内部类,lambda 表达式的语义就十分简单:它不会从超类(supertype)中继承任何变量名,也不会引入一个新的做用域。lambda 表达式基于词法做用域,也就是说 lambda 表达式函数体里面的变量和它外部环境的变量具备相同的语义(也包括 lambda 表达式的形式参数)。此外,’this’ 关键字及其引用在 lambda 表达式内部和外部也拥有相同的语义。
为了进一步说明词法做用域的优势,请参考下面的代码,它会把 "Hello, world!"
打印两遍:
1
2
3
4
5
6
7
8
9
10
11
|
public class Hello {
Runnable r1 = () -> { System.out.println(
this); }
Runnable r2 = () -> { System.out.println(toString()); }
public String toString() { return "Hello, world"; }
public static void main(String... args) {
new Hello().r1.run();
new Hello().r2.run();
}
}
|
与之相相似的内部类实现则会打印出相似 Hello$1@5b89a773
和 Hello$2@537a7706
之类的字符串,这每每会使开发者大吃一惊。
基于词法做用域的理念,lambda 表达式不能够掩盖任何其所在上下文中的局部变量,它的行为和那些拥有参数的控制流结构(例如 for
循环和 catch
从句)一致。
我的补充:这个说法很拗口,因此我在这里加一个例子以演示词法做用域:
1
2
3
4
5
|
int i = 0;
int sum = 0;
for (int i = 1; i < 10; i += 1) { //这里会出现编译错误,由于i已经在for循环外部声明过了
sum += i;
}
|
在 Java SE 7 中,编译器对内部类中引用的外部变量(即捕获的变量)要求很是严格:若是捕获的变量没有被声明为 final
就会产生一个编译错误。咱们如今放宽了这个限制——对于 lambda 表达式和内部类,咱们容许在其中捕获那些符合 有效只读(Effectively final)的局部变量。
简单的说,若是一个局部变量在初始化后从未被修改过,那么它就符合有效只读的要求,换句话说,加上 final
后也不会致使编译错误的局部变量就是有效只读变量。
1
2
3
4
|
Callable<String> helloCallable(String name) {
String hello =
"Hello";
return () -> (hello + ", " + name);
}
|
对 this
的引用,以及经过 this
对未限定字段的引用和未限定方法的调用在本质上都属于使用 final
局部变量。包含此类引用的 lambda 表达式至关于捕获了 this
实例。在其它状况下,lambda 对象不会保留任何对 this
的引用。
这个特性对内存管理是一件好事:内部类实例会一直保留一个对其外部类实例的强引用,而那些没有捕获外部类成员的 lambda 表达式则不会保留对外部类实例的引用。要知道内部类的这个特性每每会形成内存泄露。
尽管咱们放宽了对捕获变量的语法限制,但试图修改捕获变量的行为仍然会被禁止,好比下面这个例子就是非法的:
1
2
|
int sum = 0;
list.forEach(e -> { sum += e.size(); });
|
为何要禁止这种行为呢?由于这样的 lambda 表达式很容易引发 race condition。除非咱们可以强制(最好是在编译时)这样的函数不能离开其当前线程,但若是这么作了可能会致使更多的问题。简而言之,lambda 表达式对 值 封闭,对 变量 开放。
我的补充:lambda 表达式对 值 封闭,对 变量 开放的原文是:lambda expressions close over values, not variables,我在这里增长一个例子以说明这个特性:
1
2
3
4
5
|
int sum = 0;
list.forEach(e -> { sum += e.size(); });
// Illegal, close over values
List<Integer> aList =
new List<>();
list.forEach(e -> { aList.add(e); });
// Legal, open over variables
|
lambda 表达式不支持修改捕获变量的另外一个缘由是咱们可使用更好的方式来实现一样的效果:使用规约(reduction)。java.util.stream
包提供了各类通用的和专用的规约操做(例如 sum
、min
和 max
),就上面的例子而言,咱们可使用规约操做(在串行和并行下都是安全的)来代替 forEach
:
1
2
3
4
|
int sum =
list.stream()
.mapToInt(e -> e.size())
.sum();
|
sum()
等价于下面的规约操做:
1
2
3
4
|
int sum =
list.stream()
.mapToInt(e -> e.size())
.reduce(
0 , (x, y) -> x + y);
|
规约须要一个初始值(以防输入为空)和一个操做符(在这里是加号),而后用下面的表达式计算结果:
1
|
0 + list[0] + list[1] + list[2] + ...
|
规约也能够完成其它操做,好比求最小值、最大值和乘积等等。若是操做符具备可结合性(associative),那么规约操做就能够容易的被并行化。因此,与其支持一个本质上是并行并且容易致使 race condition 的操做,咱们选择在库中提供一个更加并行友好且不容易出错的方式来进行累积(accumulation)。
lambda 表达式容许咱们定义一个匿名方法,并容许咱们以函数式接口的方式使用它。咱们也但愿可以在 已有的 方法上实现一样的特性。
方法引用和 lambda 表达式拥有相同的特性(例如,它们都须要一个目标类型,并须要被转化为函数式接口的实例),不过咱们并不须要为方法引用提供方法体,咱们能够直接经过方法名称引用已有方法。
如下面的代码为例,假设咱们要按照 name
或 age
为 Person
数组进行排序:
1
2
3
4
5
6
7
8
9
10
11
12
|
class Person {
private final String name;
private final int age;
public int getAge() { return age; }
public String getName() {return name; }
...
}
Person[] people = ...
Comparator<Person> byName = Comparator.comparing(p -> p.getName());
Arrays.sort(people, byName);
|
在这里咱们能够用方法引用代替lambda表达式:
1
|
Comparator<Person> byName = Comparator.comparing(Person::getName);
|
这里的 Person::getName
能够被看做为 lambda 表达式的简写形式。尽管方法引用不必定(好比在这个例子里)会把语法变的更紧凑,但它拥有更明确的语义——若是咱们想要调用的方法拥有一个名字,咱们就能够经过它的名字直接调用它。
由于函数式接口的方法参数对应于隐式方法调用时的参数,因此被引用方法签名能够经过放宽类型,装箱以及组织到参数数组中的方式对其参数进行操做,就像在调用实际方法同样:
1
2
3
4
|
Consumer<Integer> b1 = System::exit;
// void exit(int status)
Consumer<String[]> b2 = Arrays:sort;
// void sort(Object[] a)
Consumer<String> b3 = MyProgram::main;
// void main(String... args)
Runnable r = Myprogram::mapToInt
// void main(String... args)
|
方法引用有不少种,它们的语法以下:
ClassName::methodName
instanceReference::methodName
super::methodName
ClassName::methodName
Class::new
TypeName[]::new
对于静态方法引用,咱们须要在类名和方法名之间加入 ::
分隔符,例如 Integer::sum
对于具体对象上的实例方法引用,咱们则须要在对象名和方法名之间加入分隔符:
1
2
|
Set<String> knownNames = ...
Predicate<String> isKnown = knownNames::contains;
|
这里的隐式 lambda 表达式(也就是实例方法引用)会从 knownNames
中捕获 String
对象,而它的方法体则会经过Set.contains
使用该 String
对象。
有了实例方法引用,在不一样函数式接口之间进行类型转换就变的很方便:
1
2
|
Callable<Path> c = ...
Privileged<Path> a = c::call;
|
引用任意对象的实例方法则须要在实例方法名称和其所属类型名称间加上分隔符:
1
|
Function<String, String> upperfier = String::toUpperCase;
|
这里的隐式 lambda 表达式(即 String::toUpperCase
实例方法引用)有一个 String
参数,这个参数会被 toUpperCase
方法使用。
若是类型的实例方法是泛型的,那么咱们就须要在 ::
分隔符前提供类型参数,或者(多数状况下)利用目标类型推导出其类型。
须要注意的是,静态方法引用和类型上的实例方法引用拥有同样的语法。编译器会根据实际状况作出决定。
通常咱们不须要指定方法引用中的参数类型,由于编译器每每能够推导出结果,但若是须要咱们也能够显式在 ::
分隔符以前提供参数类型信息。
和静态方法引用相似,构造方法也能够经过 new
关键字被直接引用:
1
|
SocketImplFactory factory = MySocketImpl::
new;
|
若是类型拥有多个构造方法,那么咱们就会经过目标类型的方法参数来选择最佳匹配,这里的选择过程和调用构造方法时的选择过程是同样的。
若是待实例化的类型是泛型的,那么咱们能够在类型名称以后提供类型参数,不然编译器则会依照”菱形”构造方法调用时的方式进行推导。
数组的构造方法引用的语法则比较特殊,为了便于理解,你能够假想存在一个接收 int
参数的数组构造方法。参考下面的代码:
1
2
|
IntFunction<
int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 建立数组 int[10]
|
lambda 表达式和方法引用大大提高了 Java 的表达能力(expressiveness),不过为了使把 代码即数据 (code-as-data)变的更加容易,咱们须要把这些特性融入到已有的库之中,以便开发者使用。
Java SE 7 时代为一个已有的类库增长功能是很是困难的。具体的说,接口在发布以后就已经被定型,除非咱们可以一次性更新全部该接口的实现,不然向接口添加方法就会破坏现有的接口实现。默认方法(以前被称为 虚拟扩展方法 或 守护方法)的目标便是解决这个问题,使得接口在发布以后仍能被逐步演化。
这里给出一个例子,咱们须要在标准集合 API 中增长针对 lambda 的方法。例如 removeAll
方法应该被泛化为接收一个函数式接口 Predicate
,但这个新的方法应该被放在哪里呢?咱们没法直接在 Collection
接口上新增方法——否则就会破坏现有的 Collection
实现。咱们却是能够在 Collections
工具类中增长对应的静态方法,但这样就会把这个方法置于“二等公民”的境地。
默认方法 利用面向对象的方式向接口增长新的行为。它是一种新的方法:接口方法能够是 抽象的 或是 默认的。默认方法拥有其默认实现,实现接口的类型经过继承获得该默认实现(若是类型没有覆盖该默认实现)。此外,默认方法不是抽象方法,因此咱们能够放心的向函数式接口里增长默认方法,而不用担忧函数式接口的单抽象方法限制。
下面的例子展现了如何向 Iterator
接口增长默认方法 skip
:
1
2
3
4
5
6
7
8
9
|
interface Iterator<E> {
boolean hasNext();
E next();
void remove();
default void skip(int i) {
for ( ; i > 0 && hasNext(); i -= 1) next();
}
}
|
根据上面的 Iterator
定义,全部实现 Iterator
的类型都会自动继承 skip
方法。在使用者的眼里,skip
不过是接口新增的一个虚拟方法。在没有覆盖 skip
方法的 Iterator
子类实例上调用 skip
会执行 skip
的默认实现:调用 hasNext
和 next
若干次。子类能够经过覆盖 skip
来提供更好的实现——好比直接移动游标(cursor),或是提供为操做提供原子性(Atomicity)等。
当接口继承其它接口时,咱们既能够为它所继承而来的抽象方法提供一个默认实现,也能够为它继承而来的默认方法提供一个新的实现,还能够把它继承而来的默认方法从新抽象化。
除了默认方法,Java SE 8 还在容许在接口中定义 静态 方法。这使得咱们能够从接口直接调用和它相关的辅助方法(Helper method),而不是从其它的类中调用(以前这样的类每每以对应接口的复数命名,例如 Collections
)。好比,咱们通常须要使用静态辅助方法生成实现 Comparator
的比较器,在Java SE 8中咱们能够直接把该静态方法定义在 Comparator
接口中:
1
2
3
4
|
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));
}
|
和其它方法同样,默认方法也能够被继承,大多数状况下这种继承行为和咱们所期待的一致。不过,当类型或者接口的超类拥有多个具备相同签名的方法时,咱们就须要一套规则来解决这个冲突:
为了演示第二条规则,咱们假设 Collection
和 List
接口均提供了 removeAll
的默认实现,而后 Queue
继承并覆盖了 Collection
中的默认方法。在下面的 implement
从句中,List
中的方法声明会优先于 Queue
中的方法声明:
1
|
class LinkedList<E> implements List<E>, Queue<E> { ... }
|
当两个独立的默认方法相冲突或是默认方法和抽象方法相冲突时会产生编译错误。这时程序员须要显式覆盖超类方法。通常来讲咱们会定义一个默认方法,而后在其中显式选择超类方法:
1
2
3
|
interface Robot implements Artist, Gun {
default void draw() { Artist.super.draw(); }
}
|
super
前面的类型必须是有定义或继承默认方法的类型。这种方法调用并不仅限于消除命名冲突——咱们也能够在其它场景中使用它。
最后,接口在 inherits
和 extends
从句中的声明顺序和它们被实现的顺序无关。
咱们在设计lambda时的一个重要目标就是新增的语言特性和库特性可以无缝结合(designed to work together)。接下来,咱们经过一个实际例子(按照姓对名字列表进行排序)来演示这一点:
好比说下面的代码:
1
2
3
4
5
6
|
List<Person> people = ...
Collections.sort(people,
new Comparator<Person>() {
public int compare(Person x, Person y) {
return x.getLastName().compareTo(y.getLastName());
}
})
|
冗余代码实在太多了!
有了lambda表达式,咱们能够去掉冗余的匿名类:
1
2
|
Collections.sort(
people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));
|
尽管代码简洁了不少,但它的抽象程度依然不好:开发者仍然须要进行实际的比较操做(并且若是比较的值是原始类型那么状况会更糟),因此咱们要借助 Comparator
里的 comparing
方法实现比较操做:
1
|
Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));
|
在类型推导和静态导入的帮助下,咱们能够进一步简化上面的代码:
1
|
Collections.sort(people, comparing(p -> p.getLastName()));
|
咱们注意到这里的 lambda 表达式其实是 getLastName
的代理(forwarder),因而咱们能够用方法引用代替它:
1
|
Collections.sort(people, comparing(Person::getLastName));
|
最后,使用 Collections.sort
这样的辅助方法并非一个好主意:它不但使代码变的冗余,也没法为实现 List
接口的数据结构提供特定(specialized)的高效实现,并且因为 Collections.sort
方法不属于 List
接口,用户在阅读 List
接口的文档时不会察觉在另外的 Collections
类中还有一个针对 List
接口的排序(sort()
)方法。
默认方法能够有效的解决这个问题,咱们为 List
增长默认方法 sort()
,而后就能够这样调用:
1
|
people.sort(comparing(Person::getLastName));;
|
此外,若是咱们为 Comparator
接口增长一个默认方法 reversed()
(产生一个逆序比较器),咱们就能够很是容易的在前面代码的基础上实现降序排序。
1
|
people.sort(comparing(Person::getLastName).reversed());;
|
Java SE 8 提供的新语言特性并不算多——lambda 表达式,方法引用,默认方法和静态接口方法,以及范围更广的类型推导。可是把它们结合在一块儿以后,开发者能够编写出更加清晰简洁的代码,类库编写者能够编写更增强大易用的并行类库。
1.8以前JDK自带的日期处理类很是不方便,咱们处理的时候常常是使用的第三方工具包,好比commons-lang包等。不过1.8出现以后这个改观了不少,好比日期时间的建立、比较、调整、格式化、时间间隔等。
这些类都在java.time包下。比原来实用了不少。
1 /* 2 JDK8新特性 3 */ 4 public class JDK8_features { 5 6 public List<Integer> list = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10); 7 8 /** 9 * 1.Lambda表达式 10 */ 11 @Test 12 public void testLambda(){ 13 list.forEach(System.out::println); 14 list.forEach(e -> System.out.println("方式二:"+e)); 15 } 16 17 /** 18 * 2.Stream函数式操做流元素集合 19 */ 20 @Test 21 public void testStream(){ 22 List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10); 23 System.out.println("求和:"+nums 24 .stream()//转成Stream 25 .filter(team -> team!=null)//过滤 26 .distinct()//去重 27 .mapToInt(num->num*2)//map操做 28 .skip(2)//跳过前2个元素 29 .limit(4)//限制取前4个元素 30 .peek(System.out::println)//流式处理对象函数 31 .sum());// 32 } 33 34 /** 35 * 3.接口新增:默认方法与静态方法 36 * default 接口默认实现方法是为了让集合类默认实现这些函数式处理,而不用修改现有代码 37 * (List继承于Iterable<T>,接口默认方法没必要须实现default forEach方法) 38 */ 39 @Test 40 public void testDefaultFunctionInterface(){ 41 //能够直接使用接口名.静态方法来访问接口中的静态方法 42 JDK8Interface1.staticMethod(); 43 //接口中的默认方法必须经过它的实现类来调用 44 new JDK8InterfaceImpl1().defaultMethod(); 45 //多实现类,默认方法重名时必须复写 46 new JDK8InterfaceImpl2().defaultMethod(); 47 } 48 49 public class JDK8InterfaceImpl1 implements JDK8Interface1 { 50 //实现接口后,由于默认方法不是抽象方法,重写/不重写都成! 51 // @Override 52 // public void defaultMethod(){ 53 // System.out.println("接口中的默认方法"); 54 // } 55 } 56 57 public class JDK8InterfaceImpl2 implements JDK8Interface1,JDK8Interface2 { 58 //实现接口后,默认方法名相同,必须复写默认方法 59 @Override 60 public void defaultMethod() { 61 //接口的 62 JDK8Interface1.super.defaultMethod(); 63 System.out.println("实现类复写重名默认方法!!!!"); 64 } 65 } 66 67 /** 68 * 4.方法引用,与Lambda表达式联合使用 69 */ 70 @Test 71 public void testMethodReference(){ 72 //构造器引用。语法是Class::new,或者更通常的Class< T >::new,要求构造器方法是没有参数; 73 final Car car = Car.create( Car::new ); 74 final List< Car > cars = Arrays.asList( car ); 75 //静态方法引用。语法是Class::static_method,要求接受一个Class类型的参数; 76 cars.forEach( Car::collide ); 77 //任意对象的方法引用。它的语法是Class::method。无参,全部元素调用; 78 cars.forEach( Car::repair ); 79 //特定对象的方法引用,它的语法是instance::method。有参,在某个对象上调用方法,将列表元素做为参数传入; 80 final Car police = Car.create( Car::new ); 81 cars.forEach( police::follow ); 82 } 83 84 public static class Car { 85 public static Car create( final Supplier< Car > supplier ) { 86 return supplier.get(); 87 } 88 89 public static void collide( final Car car ) { 90 System.out.println( "静态方法引用 " + car.toString() ); 91 } 92 93 public void repair() { 94 System.out.println( "任意对象的方法引用 " + this.toString() ); 95 } 96 97 public void follow( final Car car ) { 98 System.out.println( "特定对象的方法引用 " + car.toString() ); 99 } 100 } 101 102 /** 103 * 5.引入重复注解 104 * 1.@Repeatable 105 * 2.能够不用之前的“注解容器”写法,直接写2次相同注解便可 106 * 107 * Java 8在编译器层作了优化,相同注解会以集合的方式保存,所以底层的原理并无变化。 108 */ 109 @Test 110 public void RepeatingAnnotations(){ 111 RepeatingAnnotations.main(null); 112 } 113 114 /** 115 * 6.类型注解 116 * 新增类型注解:ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER(在Target上) 117 * 118 */ 119 @Test 120 public void ElementType(){ 121 Annotations.main(null); 122 } 123 124 /** 125 * 7.最新的Date/Time API (JSR 310) 126 */ 127 @Test 128 public void DateTime(){ 129 //1.Clock 130 final Clock clock = Clock.systemUTC(); 131 System.out.println( clock.instant() ); 132 System.out.println( clock.millis() ); 133 134 //2. ISO-8601格式且无时区信息的日期部分 135 final LocalDate date = LocalDate.now(); 136 final LocalDate dateFromClock = LocalDate.now( clock ); 137 138 System.out.println( date ); 139 System.out.println( dateFromClock ); 140 141 // ISO-8601格式且无时区信息的时间部分 142 final LocalTime time = LocalTime.now(); 143 final LocalTime timeFromClock = LocalTime.now( clock ); 144 145 System.out.println( time ); 146 System.out.println( timeFromClock ); 147 148 // 3.ISO-8601格式无时区信息的日期与时间 149 final LocalDateTime datetime = LocalDateTime.now(); 150 final LocalDateTime datetimeFromClock = LocalDateTime.now( clock ); 151 152 System.out.println( datetime ); 153 System.out.println( datetimeFromClock ); 154 155 // 4.特定时区的日期/时间, 156 final ZonedDateTime zonedDatetime = ZonedDateTime.now(); 157 final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock ); 158 final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) ); 159 160 System.out.println( zonedDatetime ); 161 System.out.println( zonedDatetimeFromClock ); 162 System.out.println( zonedDatetimeFromZone ); 163 164 //5.在秒与纳秒级别上的一段时间 165 final LocalDateTime from = LocalDateTime.of( 2018, Month.APRIL, 16, 0, 0, 0 ); 166 final LocalDateTime to = LocalDateTime.of( 2019, Month.APRIL, 16, 23, 59, 59 ); 167 168 final Duration duration = Duration.between( from, to ); 169 System.out.println( "Duration in days: " + duration.toDays() ); 170 System.out.println( "Duration in hours: " + duration.toHours() ); 171 } 172 173 /** 174 * 8.新增base64加解密API 175 */ 176 @Test 177 public void testBase64(){ 178 final String text = "就是要测试加解密!!abjdkhdkuasu!!@@@@"; 179 String encoded = Base64.getEncoder() 180 .encodeToString( text.getBytes( StandardCharsets.UTF_8 ) ); 181 System.out.println("加密后="+ encoded ); 182 183 final String decoded = new String( 184 Base64.getDecoder().decode( encoded ), 185 StandardCharsets.UTF_8 ); 186 System.out.println( "解密后="+decoded ); 187 } 188 189 /** 190 * 9.数组并行(parallel)操做 191 */ 192 @Test 193 public void testParallel(){ 194 long[] arrayOfLong = new long [ 20000 ]; 195 //1.给数组随机赋值 196 Arrays.parallelSetAll( arrayOfLong, 197 index -> ThreadLocalRandom.current().nextInt( 1000000 ) ); 198 //2.打印出前10个元素 199 Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 200 i -> System.out.print( i + " " ) ); 201 System.out.println(); 202 //3.数组排序 203 Arrays.parallelSort( arrayOfLong ); 204 //4.打印排序后的前10个元素 205 Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 206 i -> System.out.print( i + " " ) ); 207 System.out.println(); 208 } 209 210 /** 211 * 10.JVM的PermGen空间被移除:取代它的是Metaspace(JEP 122)元空间 212 */ 213 @Test 214 public void testMetaspace(){ 215 //-XX:MetaspaceSize初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整 216 //-XX:MaxMetaspaceSize最大空间,默认是没有限制 217 //-XX:MinMetaspaceFreeRatio在GC以后,最小的Metaspace剩余空间容量的百分比,减小为分配空间所致使的垃圾收集 218 //-XX:MaxMetaspaceFreeRatio在GC以后,最大的Metaspace剩余空间容量的百分比,减小为释放空间所致使的垃圾收集 219 } 220 221 } 222 引用到的相关类: 223 public interface JDK8Interface1 { 224 225 //1.接口中能够定义静态方法了 226 public static void staticMethod(){ 227 System.out.println("接口中的静态方法"); 228 } 229 230 //2.使用default以后就能够定义普通方法的方法体了 231 public default void defaultMethod(){ 232 System.out.println("接口中的默认方法"); 233 } 234 } 235 236 public interface JDK8Interface2 { 237 238 //接口中能够定义静态方法了 239 public static void staticMethod(){ 240 System.out.println("接口中的静态方法"); 241 } 242 //使用default以后就能够定义普通方法的方法体了 243 public default void defaultMethod(){ 244 System.out.println("接口中的默认方法"); 245 } 246 } 247 248 /* 249 JDK8新特性 250 */ 251 public class RepeatingAnnotations { 252 @Target( ElementType.TYPE ) 253 @Retention( RetentionPolicy.RUNTIME ) 254 public @interface Filters { 255 Filter[] value(); 256 } 257 258 @Target( ElementType.TYPE ) 259 @Retention( RetentionPolicy.RUNTIME ) 260 @Repeatable( Filters.class ) 261 public @interface Filter { 262 String value(); 263 String value2(); 264 }; 265 266 @Filter( value="filter1",value2="111" ) 267 @Filter( value="filter2", value2="222") 268 //@Filters({@Filter( value="filter1",value2="111" ),@Filter( value="filter2", value2="222")}).注意:JDK8以前:1.没有@Repeatable2.采用本行“注解容器”写法 269 public interface Filterable { 270 } 271 272 public static void main(String[] args) { 273 //获取注解后遍历打印值 274 for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) { 275 System.out.println( filter.value() +filter.value2()); 276 } 277 } 278 } 279 280 /* 281 @Description:新增类型注解:ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER(在Target上) 282 */ 283 public class Annotations { 284 @Retention( RetentionPolicy.RUNTIME ) 285 @Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } ) 286 public @interface NonEmpty { 287 } 288 289 public static class Holder< @NonEmpty T > extends @NonEmpty Object { 290 public void method() throws @NonEmpty Exception { 291 } 292 } 293 294 public static void main(String[] args) { 295 final Holder< String > holder = new @NonEmpty Holder< String >(); 296 @NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>(); 297 } 298 }