1.簡介
毫無疑問,Java 8是自Java 5(2004年)發佈以來Java語言最大的一次版本升級,Java 8帶來了很多的新特性,比如編譯器、類庫、開發工具和JVM(Java虛擬機)。在這篇教程中我們將會學習這些新特性,並通過真實例子演示說明它們適用的場景。
本教程由下面幾部分組成,它們分別涉及到Java平臺某一特定方面的內容:
- 語言
- 編譯器
- 類庫
- 開發工具
- 運行時(Java虛擬機)
2.Java的新特性
總體來說,Java 8是一個大的版本升級。有人可能會說,Java 8的新特性非常令人期待,但是也要花費大量的時間去學習。這一節我們會講到這些新特性。
2.1 Lambda表達式和函數式接口
Lambda表達式(也叫做閉包)是Java 8中最大的也是期待已久的變化。它允許我們將一個函數當作方法的參數(傳遞函數),或者說把代碼當作數據,這是每個函數式編程者熟悉的概念。很多基於JVM平臺的語言一開始就支持Lambda表達式,但是Java程序員沒有選擇,只能使用匿名內部類來替代Lambda表達式。
Lambda表達式的設計被討論了很久,而且花費了很多的功夫來交流。不過最後取得了一個折中的辦法,得到了一個新的簡明並且緊湊的Lambda表達式結構。最簡單的Lambda表達式可以用逗號分隔的參數列表、->符號和功能語句塊來表示。示例如下:
1
|
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach( e -> System.out.println( e ) );
|
請注意到編譯器會根據上下文來推測參數的類型,或者你也可以顯示地指定參數類型,只需要將類型包在括號裏。舉個例子:
1
|
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach( ( String e ) -> System.out.println( e ) );
|
如果Lambda的功能語句塊太複雜,我們可以用大括號包起來,跟普通的Java方法一樣,如下:
1
2
3
|
String separator =
","
;
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach(
( String e ) -> System.out.print( e + separator ) );
|
Lambda表達式可能會引用類的成員或者局部變量(會被隱式地轉變成final類型),下面兩種寫法的效果是一樣的:
1
2
3
|
String separator =
","
;
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach(
( String e ) -> System.out.print( e + separator ) );
|
和
1
2
3
|
final
String separator =
","
;
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach(
( String e ) -> System.out.print( e + separator ) );
|
Lambda表達式可能會有返回值,編譯器會根據上下文推斷返回值的類型。如果lambda的語句塊只有一行,不需要return關鍵字。下面兩個寫法是等價的:
1
|
Arrays.asList(
"a"
,
"b"
,
"d"
).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
|
和
1
2
3
4
|
Arrays.asList(
"a"
,
"b"
,
"d"
).sort( ( e1, e2 ) -> {
int
result = e1.compareTo( e2 );
return
result;
} );
|
語言的設計者們思考了很多如何讓現有的功能和lambda表達式友好兼容。於是就有了函數接口這個概念。函數接口是一種只有一個方法的接口,像這樣地,函數接口可以隱式地轉換成lambda表達式。
java.lang.Runnable 和java.util.concurrent.Callable是函數接口兩個最好的例子。但是在實踐中,函數接口是非常脆弱的,只要有人在接口裏添加多一個方法,那麼這個接口就不是函數接口了,就會導致編譯失敗。Java 8提供了一個特殊的註解@FunctionalInterface來克服上面提到的脆弱性並且顯示地表明函數接口的目的(java裏所有現存的接口都已經加上了@FunctionalInterface)。讓我們看看一個簡單的函數接口定義:
1
2
3
4
|
@FunctionalInterface
public
interface
Functional {
void
method();
}
|
我們要記住默認的方法和靜態方法(下一節會具體解釋)不會違反函數接口的約定,例子如下:
1
2
3
4
5
6
7
|
@FunctionalInterface
public
interface
FunctionalDefaultMethods {
void
method();
default
void
defaultMethod() {
}
}
|
支持Lambda是Java 8最大的賣點,他有巨大的潛力吸引越來越多的開發人員轉到這個開發平臺來,並且在純Java裏提供最新的函數式編程的概念。對於更多的細節,請參考官方文檔。
2.2 接口的默認方法和靜態方法
Java 8增加了兩個新的概念在接口聲明的時候:默認和靜態方法。默認方法和Trait有些類似,但是目標不一樣。默認方法允許我們在接口裏添加新的方法,而不會破壞實現這個接口的已有類的兼容性,也就是說不會強迫實現接口的類實現默認方法。
默認方法和抽象方法的區別是抽象方法必須要被實現,默認方法不是。作爲替代方式,接口可以提供一個默認的方法實現,所有這個接口的實現類都會通過繼承得倒這個方法(如果有需要也可以重寫這個方法),讓我們來看看下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
private
interface
Defaulable {
// Interfaces now allow default methods, the implementer may or
// may not implement (override) them.
default
String notRequired() {
return
"Default implementation"
;
}
}
private
static
class
DefaultableImpl
implements
Defaulable {
}
private
static
class
OverridableImpl
implements
Defaulable {
@Override
public
String notRequired() {
return
"Overridden implementation"
;
}
}
|
接口Defaulable使用default關鍵字聲明瞭一個默認方法notRequired(),類DefaultableImpl實現了Defaulable接口,沒有對默認方法做任何修改。另外一個類OverridableImpl重寫類默認實現,提供了自己的實現方法。
Java 8 的另外一個有意思的新特性是接口裏可以聲明靜態方法,並且可以實現。例子如下:
1
2
3
4
5
6
|
private
interface
DefaulableFactory {
// Interfaces now allow static methods
static
Defaulable create( Supplier< Defaulable > supplier ) {
return
supplier.get();
}
}
|
下面是把接口的靜態方法和默認方法放在一起的示例(::new 是構造方法引用,後面會有詳細描述):
1
2
3
4
5
6
7
|
public
static
void
main( String[] args ) {
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::
new
);
System.out.println( defaulable.notRequired() );
defaulable = DefaulableFactory.create( OverridableImpl::
new
);
System.out.println( defaulable.notRequired() );
}
|
控制檯的輸出如下:
Default implementation
Overridden implementation
JVM平臺的接口的默認方法實現是很高效的,並且方法調用的字節碼指令支持默認方法。默認方法使已經存在的接口可以修改而不會影響編譯的過程。java.util.Collection中添加的額外方法就是最好的例子:stream(), parallelStream(), forEach(), removeIf()
雖然默認方法很強大,但是使用之前一定要仔細考慮是不是真的需要使用默認方法,因爲在層級很複雜的情況下很容易引起模糊不清甚至變異錯誤。更多的詳細信息請參考官方文檔。
2.3 方法引用
方法引用提供了一個很有用的語義來直接訪問類或者實例的已經存在的方法或者構造方法。結合Lambda表達式,方法引用使語法結構緊湊簡明。不需要複雜的引用。
下面我們用Car 這個類來做示例,Car這個類有不同的方法定義。讓我們來看看java 8支持的4種方法引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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() );
}
}
|
第一種方法引用是構造方法引用,語法是:Class::new ,對於泛型來說語法是:Class<T >::new,請注意構造方法沒有參數:
1
2
|
final
Car car = Car.create( Car::
new
);
final
List< Car > cars = Arrays.asList( car );
|
第二種方法引用是靜態方法引用,語法是:Class::static_method請注意這個靜態方法只支持一個類型爲Car的參數。
1
|
cars.forEach( Car::collide );
|
第三種方法引用是類實例的方法引用,語法是:Class::method請注意方法沒有參數。
1
|
cars.forEach( Car::repair );
|
最後一種方法引用是引用特殊類的方法,語法是:instance::method,請注意只接受Car類型的一個參數。
1
2
|
final
Car police = Car.create( Car::
new
);
cars.forEach( police::follow );
|
運行這些例子我們將會在控制檯得到如下信息(Car的實例可能會不一樣):
Collided [email protected]a81197d
Repaired [email protected]a81197d
Following the [email protected]a81197d
關於方法引用更多的示例和詳細信息,請參考官方文檔
2.4 重複註釋
自從Java 5支持註釋以來,註釋變得特別受歡迎因而被廣泛使用。但是有一個限制,同一個地方的不能使用同一個註釋超過一次。 Java 8打破了這個規則,引入了重複註釋,允許相同註釋在聲明使用的時候重複使用超過一次。
重複註釋本身需要被@Repeatable註釋。實際上,他不是一個語言上的改變,只是編譯器層面的改動,技術層面仍然是一樣的。讓我們來看看例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package
com.javacodegeeks.java8.repeatable.annotations;
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 {
@Target
( ElementType.TYPE )
@Retention
( RetentionPolicy.RUNTIME )
public
@interface
Filters {
Filter[] value();
}
@Target
( ElementType.TYPE )
@Retention
( RetentionPolicy.RUNTIME )
@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() );
}
}
}
|
我們可以看到,註釋Filter被@Repeatable( Filters.class )註釋。Filters 只是一個容器,它持有Filter, 編譯器盡力向程序員隱藏它的存在。通過這樣的方式,Filterable接口可以被Filter註釋兩次。
另外,反射的API提供一個新方法getAnnotationsByType() 來返回重複註釋的類型(請注意Filterable.class.getAnnotation( Filters.class )將會返回編譯器注入的Filters實例)。
程序的輸出將會是這樣:
filter1
filter2
更多詳細信息請參考官方文檔。
2.5 更好的類型推斷
Java 8在類型推斷方面改進了很多,在很多情況下,編譯器可以推斷參數的類型,從而保持代碼的整潔。讓我們看看例子:
package com.javacodegeeks.java8.type.inference;
1
2
3
4
5
6
7
8
9
10
11
|
package
com.javacodegeeks.java8.type.inference;
public
class
Value<T> {
public
static
<T> T defaultValue() {
return
null
;
}
public
T getOrDefault( T value, T defaultValue ) {
return
( value !=
null
) ? value : defaultValue;
}
}
|
這裏是Value< String >的用法
1
2
3
4
5
6
7
8
|
package
com.javacodegeeks.java8.type.inference;
public
class
TypeInference {
public
static
void
main(String[] args) {
final
Value<String> value =
new
Value<>();
value.getOrDefault(
"22"
, Value.defaultValue() );
}
}
|
參數Value.defaultValue()的類型被編譯器推斷出來,不需要顯式地提供類型。在java 7, 相同的代碼不會被編譯,需要寫成:Value.< String >defaultValue()
2.6 註解的擴展
Java 8擴展了註解可以使用的範圍,現在我們幾乎可以在所有的地方:局部變量、泛型、超類和接口實現、甚至是方法的Exception聲明。一些例子如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package
com.javacodegeeks.java8.annotations;
import
java.lang.annotation.ElementType;
import
java.lang.annotation.Retention;
import
java.lang.annotation.RetentionPolicy;
import
java.lang.annotation.Target;
import
java.util.ArrayList;
import
java.util.Collection;
public
class
Annotations {
@Retention
( RetentionPolicy.RUNTIME )
@Target
( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
public
@interface
NonEmpty {
}
public
static
class
Holder<
@NonEmpty
T >
extends
@NonEmpty
Object {
public
void
method()
throws
@NonEmpty
Exception {
}
}
@SuppressWarnings
(
"unused"
)
public
static
void
main(String[] args) {
final
Holder< String > holder =
new
@NonEmpty
Holder< String >();
@NonEmpty
Collection<
@NonEmpty
String > strings =
new
ArrayList<>();
}
}
|
Java 8 新增加了兩個註解的程序元素類型ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER ,這兩個新類型描述了可以使用註解的新場合。註解處理API(Annotation Processing API)也做了一些細微的改動,來識別這些新添加的註解類型。
3.Java編譯器的新特性
3.1 參數名字
很長時間以來,Java程序員想盡辦法把參數名字保存在java字節碼裏,並且讓這些參數名字在運行時可用。Java 8 終於把這個需求加入到了Java語言(使用反射API和Parameter.getName() 方法)和字節碼裏(使用java編譯命令javac的–parameters參數)。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package
com.javacodegeeks.java8.parameter.names;
import
java.lang.reflect.Method;
import
java.lang.reflect.Parameter;
public
class
ParameterNames {
public
public
static
void
main(String[] args)
throws
Exception {
Method method = ParameterNames.
class
.getMethod(
"main"
, String[].
class
);
for
Method method = ParameterNames. class .getMethod( "main" , String[]. class );
for
(
final
Parameter parameter: method.getParameters() ) {
for
(
|