Java 8 新特性
Lambda expressions
Default Methods for Interfaces
Java 8允许我们在接口中定义non-abstract method,这在Java 8之前是不被允许的。定义的方法是使用default关键词:
1
2
3
4
5
6
7interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}我们可以使用deault关键词定义的方法:
1
2
3
4
5
6
7
8
9Formula formula = new Formula() {
public double calculate(int a) {
return sqrt(a * 100);
}
};
formula.calculate(100); // 100.0
formula.sqrt(16); // 4.0
这里我们使用了匿名内部类。匿名内部类的一个很大的好处就是:接口是不能实例化的,如果要实例化,我们需要先定义实现这个接口的类。但很多时候,我们其实是不关心实现接口的类是怎样的,而只关心接口的方法是如何重写(override)的。匿名内部类就可以帮助我们达到目的。
Lambda expressions
- 匿名内部类已经简化了我们对于接口的使用,但是大家还是觉得麻烦,我们其实只是想使用一下接口里的某个方法,为什么要定义这么多东西,于是Lambda表达式诞生了。对于一个复杂的匿名内部类:我们使用Lambda表达式可以简化成如下,即我们省略了类名以及重写对的方法名。
1
2
3
4
5
6
7
8List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
Collections.sort(names, new Comparator<String>() {
public int compare(String a, String b) {
return b.compareTo(a);
}
});如果Lambda表达式重写的方法只有一行,我们可以省略1
2
3Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});{}
和return
;同时函数参数的类型也可以由编译器自行判断。所以最终简化为:1
Collections.sort(names, (a, b) -> b.compareTo(a));
Functional Interfaces
- 从上面的例子我们可以看出lambda表达式的巨大作用,但这个时候我们有一个疑问:接口中可能有多个方法,Java编译器是怎么知道我们重写的是哪个方法呢? 答案是Java编译器并没有那么聪明,我们只能在函数式接口上使用Lambda表达式。
- 函数式接口:接口的定义中有且只有一个抽象方法,我们把这样的接口叫做函数式接口。这里我们就可以理解为什么Java 8中要为接口引入default方法,原因就是default方法不算抽象方法,函数式接口中可以有多个default方法。这个时候我们可能还有疑问:Comparator接口有两个抽象方法:
int compare(T o1, T o2);
和boolean equals(Object obj);
,为什么还是函数式接口呢?原因是boolean equals(Object obj);
是Object自带的方法,并不算抽象方法(FunctionalInterface的定义中提到:If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface’s abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere)。https://stackoverflow.com/questions/23721759/functionalinterface-comparator-has-2-abstract-methods. - 所有的函数式接口可以使用Lambda表达式,因为它只有一个抽象方法,所以编译器可以知道要重写的就是这个方法。
- Java提供一个
@FunctionalInterface
的注解,当我们想要定义一个函数式接口的时候,我们可以加上这个注解,那么编译器会帮助我们判断哦我们写的接口符不符合只有一个抽象方法的标准,不符合的话会报错。所以@FunctionalInterface
注解和Override
注解有着类似的作用。1
2
3
4
interface Converter<F, T> {
T convert(F from);
}1
2
3Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted); // 123
Method and Constructor References
- Java 8还新引入了方法引用,使用方法引用,我们可以进一步简化Lambda表达式。方法引用的原理就是引用的方法的参数与返回值与所需要方法的参数与返回值完全相同。这个例子中需要的override的方法是
1
2
3Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted); // 123Integer convert(String from)
,这与Integer valueOf(String input)
是完全一致的,所以这里我们可以使用方法引用 - 四种方法引用
Kind Syntax Examples
Reference to a static method ContainingClass::staticMethodName Person::compareByAge; MethodReferencesExamples::appendStrings
Reference to an instance method of a particular object containingObject::instanceMethodName myComparisonProvider::compareByName;myApp::appendStrings2
Reference to an instance method of an arbitrary object of a particular type ContainingType::methodName String::compareToIgnoreCase;
String::concat
Reference to a constructor ClassName::new HashSet::new
方法引用的三条规则:
成员方法的方法签名,前面会追加 this 的类型。
静态方法的方法签名,因为没有 this, 不会追加任何东西。
当 :: 前是一个实例时,这个实例会作为第一个参数给绑定到目标方法签名上。
第二条规则很好理解,Integer.valueOf()
就是一个很好的例子,下面分别解释第一条与第三条规则。
第一条规则:一个类中的方法正常来说我们是不能直接使用的,除非它是一个静态方法,但是对于lambda方法引用来说,它是可以使用的。同时会在方法的参数列表最前面增加一个this的类型。
1 | public static void main(String[] args){ |
上面的例子中,Arrays.sort
的第二个参数应该是一个实现了int compare(String a, String b)
的comparator,我们查看String的compareTo方法int compareTo(String anotherString)
发现参数并不相符。这里可以使用方法引用的原因就符合第一条规则,这里String的compareTo方法的签名其实是int compareTo(String this, String anotherString)
。我们发现与compare方法是相符的。
第三条规则:对于第三条规则,我们可以按照第一条规则继续理解。String的compareTo方法的签名其实是int compareTo(String this, String anotherString)
,但由于这里我们::
前面是一个对象,而不是类名,所以String this
会被这个对象所代替,这个时候String this
就不能算做方法的参数了.
1 | public class Test { |
map的参数是一个实现了R apply(T t)
的Function,明显不符合int compareTo(String this, String anotherString)
的参数。但是由于我们这里的fake变量是一个对象,所以方法签名其实是int compareTo(fake, String anotherString)
,本质上只有一个参数,符合R apply(T t)
的参数,所以可以使用。
因此,我们也就理解了为什么System.out::println
可以用在void forEach(Consumer<? super T> action)
,但是PrintStream::println
不可以:
Consumer需要override的方法是void accept(T t)
,而PrintStream::println
的方法参数是void println(printStream this, int t)
,明显不符合。而System.out
是PrintStream的一个静态对象,符合第三条规则,所以可以被forEach使用
- 构造器的方法引用例子:
1
2
3
4
5
6
7
8
9
10
11class Person {
String firstName;
String lastName;
Person() {}
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}Java编译器自动根据1
2
3
4
5interface PersonFactory<P extends Person> {
P create(String firstName, String lastName);
}
PersonFactory<Person> personFactory = Person::new;PersonFactory.create
的参数类型选择合适的构造器
Lambda Scopes
https://github.com/winterbe/java8-tutorial#lambda-scopes
Built-in Functional Interfaces
Java 8有很多定义好的函数式接口,在后面的Stream介绍中,他们会被广泛的使用。
Predicates
Predicates are boolean-valued functions of one argument. The interface contains various default methods for composing predicates to complex logical terms (and, or, negate)
1 | Predicate<String> predicate = (s) -> s.length() > 0; |
Functions
Functions accept one argument and produce a result. Default methods can be used to chain multiple functions together (compose, andThen).
1 | Function<String, Integer> toInteger = Integer::valueOf; |
Suppliers
Suppliers produce a result of a given generic type. Unlike Functions, Suppliers don’t accept arguments.
1 | Supplier<Person> personSupplier = Person::new; |
Consumers
Consumers represent operations to be performed on a single input argument.
1 | Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName); |
Comparators
Comparators are well known from older versions of Java. Java 8 adds various default methods to the interface.
1 | Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); |
Stream
stream解析
举map为例:
1 | public interface Stream<T> extends BaseStream<T, Stream<T>> { |
可以看到map的参数是一个函数式接口Function
,有意思的是这个函数式接口的泛型分别是? super T
和? extends R
,那么它们代表了什么呢?
首先我们需要明白的一点是:这个例子中的T
与R
是泛型的形参;?
是泛型的实参。T
和R
在使用stream的时候会被实例化,比如我们T
被String
实例化, R
被Integer
实例化,这个时候我们就能清晰的理解了:map的输入是一个Function
,Function
的第一个泛型参数是String或String的父类(到底是什么在定义的时候还不知道,所以用?代替),Function的第二个泛型参数是Integer或Integer的子类(到底是什么在定义的时候还不知道,所以用?代替)。我们要搞清楚,这里我么是在使用Function而不是定义Function,所以使用的是实参?
,不能因为看到T和R就混淆,这里的T和R是Stream类的形参。
再根据PECS原则:Producer Extends Consumer Super,进一步深入理解:Function
的V apply(P p)
方法会使用第一个泛型参数,并返回第二个泛型参数,所以第一个参数确实应该定义成producer,第二个确实应该定义成consumer。
加强版类型推断(Target Type)
在泛型部分我们介绍了类型推断,也就是方法泛型不需要声明泛型的类别,Java会自动根据方法的参数判断,但是在stream中,只根据方法的参数判断是不够的,加强版类型推断还可以根据返回值判断泛型的类别:
1 | public class Test { |
这个例子中map需要一个实现了R apply(T t)
的Function,T我们根据stream可以推断出是类型是String,但是R并不能由参数推断出来,而是由返回值推断出来是Integer
下面的内容来自官方教程: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
Target Typing
How do you determine the type of a lambda expression? Recall the lambda expression that selected members who are male and between the ages 18 and 25 years:
1 | p -> p.getGender() == Person.Sex.MALE |
This lambda expression was used in the following two methods:
public static void printPersons(List<Person> roster, CheckPerson tester)
in Approach 3: Specify Search Criteria Code in a Local Class
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
in Approach 6: Use Standard Functional Interfaces with Lambda Expressions
When the Java runtime invokes the method printPersons, it’s expecting a data type of CheckPerson, so the lambda expression is of this type. However, when the Java runtime invokes the method printPersonsWithPredicate, it’s expecting a data type of Predicate
Variable declarations
Assignments
Return statements
Array initializers
Method or constructor arguments
Lambda expression bodies
Conditional expressions, ?:
Cast expressions
Target Types and Method Arguments
For method arguments, the Java compiler determines the target type with two other language features: overload resolution and type argument inference.
Consider the following two functional interfaces ( java.lang.Runnable and java.util.concurrent.Callable
1 | public interface Runnable { |
1 | public interface Callable<V> { |
The method Runnable.run does not return a value, whereas Callable
Suppose that you have overloaded the method invoke as follows (see Defining Methods for more information about overloading methods):
1 | void invoke(Runnable r) { |
1 | <T> T invoke(Callable<T> c) { |
Which method will be invoked in the following statement?
String s = invoke(() -> "done");
The method invoke(Callable() -> "done"
is Callable
stream functions
stream的函数定义查看Java官方文档
How streams work
A stream represents a sequence of elements and supports different kind of operations to perform computations upon those elements:
1
2
3
4
5
6
7
8
9
10List<String> myList =
Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList
.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
// C1
// C2Stream operations are either intermediate or terminal. Intermediate operations return a stream so we can chain multiple intermediate operations without using semicolons. Terminal operations are either void or return a non-stream result. In the above example filter, map and sorted are intermediate operations whereas forEach is a terminal operation.
Most stream operations accept some kind of lambda expression parameter, a functional interface specifying the exact behavior of the operation. Most of those operations must be both non-interfering and stateless. What does that mean?
A function is non-interfering when it does not modify the underlying data source of the stream, e.g. in the above example no lambda expression does modify myList by adding or removing elements from the collection.
A function is stateless when the execution of the operation is deterministic, e.g. in the above example no lambda expression depends on any mutable variables or states from the outer scope which might change during execution.
Different kind of streams
Streams can be created from various data sources, especially collections. Lists and Sets support new methods
stream()
andparallelStream()
to either create a sequential or a parallel stream. Parallel streams are capable of operating on multiple threads and will be covered in a later section of this tutorial. We focus on sequential streams for now:1
2
3
4Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println); // a1Calling the method stream() on a list of objects returns a regular object stream. But we don’t have to create collections in order to work with streams as we see in the next code sample:
1
2
3Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println); // a1Just use Stream.of() to create a stream from a bunch of object references.
Besides regular object streams Java 8 ships with special kinds of streams for working with the primitive data types int, long and double. As you might have guessed it’s IntStream, LongStream and DoubleStream.
IntStreams can replace the regular for-loop utilizing IntStream.range():1
2
3
4
5
6IntStream.range(1, 4)
.forEach(System.out::println);
// 1
// 2
// 3All those primitive streams work just like regular object streams with the following differences: Primitive streams use specialized lambda expressions, e.g. IntFunction instead of Function or IntPredicate instead of Predicate. And primitive streams support the additional terminal aggregate operations sum() and average():
1
2
3
4Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println); // 5.0Sometimes it’s useful to transform a regular object stream to a primitive stream or vice versa. For that purpose object streams support the special mapping operations mapToInt(), mapToLong() and mapToDouble:
1
2
3
4
5Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println); // 3Primitive streams can be transformed to object streams via mapToObj():
1
2
3
4
5
6
7IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3Here’s a combined example: the stream of doubles is first mapped to an int stream and than mapped to an object stream of strings:
1
2
3
4
5
6
7
8Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
Processing Order
- Now that we’ve learned how to create and work with different kinds of streams, let’s dive deeper into how stream operations are processed under the hood.
An important characteristic of intermediate operations is laziness. Look at this sample where a terminal operation is missing:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
When executing this code snippet, nothing is printed to the console. That is because intermediate operations will only be executed when a terminal operation is present.
Let’s extend the above example by the terminal operation forEach:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
Executing this code snippet results in the desired output on the console:
filter: d2
forEach: d2
filter: a2
forEach: a2
filter: b1
forEach: b1
filter: b3
forEach: b3
filter: c
forEach: c
The order of the result might be surprising. A naive approach would be to execute the operations horizontally one after another on all elements of the stream. But instead each element moves along the chain vertically. The first string “d2” passes filter then forEach, only then the second string “a2” is processed.
This behavior can reduce the actual number of operations performed on each element, as we see in the next example:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
// map: d2
// anyMatch: D2
// map: a2
// anyMatch: A2
The operation anyMatch returns true as soon as the predicate applies to the given input element. This is true for the second element passed “A2”. Due to the vertical execution of the stream chain, map has only to be executed twice in this case. So instead of mapping all elements of the stream, map will be called as few as possible.
Why order matters
The next example consists of two intermediate operations map and filter and the terminal operation forEach. Let’s once again inspect how those operations are being executed:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
As you might have guessed both map and filter are called five times for every string in the underlying collection whereas forEach is only called once.
We can greatly reduce the actual number of executions if we change the order of the operations, moving filter to the beginning of the chain:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
Now, map is only called once so the operation pipeline performs much faster for larger numbers of input elements. Keep that in mind when composing complex method chains.
Let’s extend the above example by an additional operation, sorted:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
Sorting is a special kind of intermediate operation. It’s a so called stateful operation since in order to sort a collection of elements you have to maintain state during ordering.
Executing this example results in the following console output:
sort: a2; d2
sort: b1; a2
sort: b1; d2
sort: b1; a2
sort: b3; b1
sort: b3; d2
sort: c; b3
sort: c; d2
filter: a2
map: a2
forEach: A2
filter: b1
filter: b3
filter: c
filter: d2
First, the sort operation is executed on the entire input collection. In other words sorted is executed horizontally. So in this case sorted is called eight times for multiple combinations on every element in the input collection.
Once again we can optimize the performance by reordering the chain:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
In this example sorted is never been called because filter reduces the input collection to just one element. So the performance is greatly increased for larger input collections.
Reusing Streams
Java 8 streams cannot be reused. As soon as you call any terminal operation the stream is closed:
1 | Stream<String> stream = |
Calling noneMatch after anyMatch on the same stream results in the following exception:
java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)
To overcome this limitation we have to to create a new stream chain for every terminal operation we want to execute, e.g. we could create a stream supplier to construct a new stream with all intermediate operations already set up:
1 | Supplier<Stream<String>> streamSupplier = |
Each call to get() constructs a new stream on which we are save to call the desired terminal operation.
Reference
https://github.com/winterbe/java8-tutorial
https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/