Java 8 (又稱為 jdk 1.8) 是 Java 語言開發的一個主要版本。 Oracle 公司於 2014 年 3 月 18 日發布 Java 8 ,這個版本包含語言、編譯器、庫、工具和JVM等方面的十多個新特性。 下面就來介紹下語言方面的新特性。
語法相關新特性
默認接口方法
從 Java 8 開始,接口支持定義默認實現方法。所謂的默認方法,就是指接口中定義的抽象方法可以由接口本身提供默認實現,而不一定要實現類去實現這個抽象方法。
比如我定義了一個 Programmer 接口,在 Java 8 之前,我們必須在實現類中實現所有的抽象方法,比如下面的 JavaProgrammer 類。
package com.csx.feature.defaultm;
public interface Programmer {
/**
* 編程操作
*/
void coding();
/**
* 介紹自己
* @return
*/
String introduce();
}
實現類:
package com.csx.feature.defaultm;
public class JavaProgrammer implements Programmer {
@Override
public void coding() {
System.out.println("l am writing a bug...");
}
@Override
public String introduce() {
return "hi, l am a Java programmer";
}
}
在 Java 8 中,我們可以使用 default 關鍵字來定義接口中的默認方法實現。比如下面我們將introduce
方法定義成一個具有默認實現的方法。
package com.csx.feature.defaultm;
public interface Programmer {
/**
* 編程操作
*/
void coding();
/**
* 介紹自己
* @return
*/
default String introduce(){
return "hi, l am a C++ programmer";
}
}
我們再定義一個實現類,就不需要再實現這個方法了(當然,實現類中還是可以實現這個方法的,實現的方法會將接口的默認方法覆蓋)。
package com.csx.feature.defaultm;
public class CJJProgrammer implements Programmer {
@Override
public void coding() {
System.out.println("l am writing a bug.......");
}
}
上面的實現類 CJJProgrammer 就不再必須要實現 introduce 方法,因為這個方法在接口中已經有默認實現了。
使用接口默認方法的最主要目的是:修改接口后不需要大范圍的修改以前老的實現類。
比如說現在給 Programmer 接口新添加一個新的方法 reading():
package com.csx.feature.defaultm;
public interface Programmer {
void reading();
void coding();
default String introduce(){
return "hi, l am a C++ programmer";
}
}
新添加這個方法后,每個實現類中必須也加上這個方法的實現,不然代碼編譯會報錯。假如之前的實現類很多的話,那么修改的工作量將是非常大的。這時你就可以為這個方法提供默認實現:
public interface Programmer {
default void reading(){
System.out.println("reading 11.11 shopping list...");
}
void coding();
default String introduce(){
return "hi, l am a C++ programmer";
}
}
多個默認方法
考慮這樣的情況,一個類實現了多個接口,且這些接口有相同的默認方法。
public interface Vehicle {
default void print(){
System.out.println("我是一輛車!");
}
}
public interface FourWheeler {
default void print(){
System.out.println("我是一輛四輪車!");
}
}
第一個解決方案是創建自己的默認方法,來覆蓋重寫接口的默認方法:
public class Car implements Vehicle, FourWheeler {
default void print(){
System.out.println("我是一輛四輪汽車!");
}
}
第二種解決方案可以使用 super 來調用指定接口的默認方法:
public class Car implements Vehicle, FourWheeler {
public void print(){
Vehicle.super.print();
}
}
靜態默認方法
Java 8 的另一個特性是接口可以聲明(並且可以提供實現)靜態方法。
package com.csx.feature.defaultm;
public interface Programmer {
static final String BLOG = "程序員自由之路";
static String blogName(){
return BLOG;
}
void coding();
default String introduce(){
return "hi, l am a C++ programmer";
}
}
Lambda 表達式
Lambda 表達式,也可稱為閉包,它是推動 Java 8 發布的最重要新特性。Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞進方法中)。
使用 Lambda 表達式可以使代碼變的更加簡潔緊湊,其本質是一個Java語法糖,具體內容可以參考我的博客Java中的語法糖中關於Lambda的章節。
Lambda 表達式的語法如下:
(p1) -> exp;
或者
(p1,p2) -> {
exp1;
exp2;
}
以下是lambda表達式的重要特征:
- 可選類型聲明:不需要聲明參數類型,編譯器可以統一識別參數值。
- 可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號。
- 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
- 可選的返回關鍵字:如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定明表達式返回了一個數值。
lambda 表達式的最大作用是簡化了匿名內部類的使用,是的代碼看起來更加簡潔。(注意:Lambda 表達式並不能提升代碼執行的效率)。
lambda 表達式還有一個作用就是推動了 Java 中的函數化編程,使得可以將一個函數作為參數傳給方法。(之前必須傳一個對象的引用給方法,然后再通過這個對象引用調用具體的方法)。
變量作用域
lambda 表達式只能引用不被修改的外層局部變量,否則會編譯錯誤。
int num = 1;
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
// 這個 num 變量被 lambda 表達式引用,又被修改了,所以會編譯報錯。
num = 5;
其實如果lambda表達式引用了外層的局部變量,編譯器會自動將這個變量設置成final修飾。
final int num = 1;
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
// final變量賦值后不能修改,所以會編譯報錯。
// num = 5;
函數式接口
函數式接口(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的接口。函數式接口可以被隱式轉換為 lambda 表達式。
@FunctionalInterface
interface GreetingService {
// 非抽象接口可以有多個
static void sayBye(){
System.out.println("bye");
}
void sayMessage(String message);
}
那么就可以使用Lambda表達式來表示該接口的一個實現(注:JAVA 8 之前一般是用匿名類實現的):
GreetingService greetService1 = message -> System.out.println("Hello " + message);
從 Java 8 開始,很多之前的接口,都被調整成函數式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
同時,Java 8 也增加了很多新的函數式接口,主要在java.util.function
這個包下面。常用的有:
-
Predicate
:接受一個輸入參數,返回一個布爾值結果。 -
Supplier
:無參數,返回一個結果。 -
Consumer
:代表了接受一個輸入參數並且無返回的操作
在實踐中,函數式接口非常脆弱:只要某個開發者在該接口中添加一個函數,則該接口就不再是函數式接口進而導致編譯失敗。為了克服這種代碼層面的脆弱性,並顯式說明某個接口是函數式接口,Java 8 提供了一個特殊的注解@FunctionalInterface(Java 庫中的所有相關接口都已經帶有這個注解了)。
不過有一點需要注意,默認方法和靜態方法不會破壞函數式接口的定義,因此如下的代碼是合法的。
@FunctionalInterface
interface GreetingService {
// 非抽象接口可以有多個
static void sayBye(){
System.out.println("bye");
}
default void sayHi() {
System.out.println("bye");
}
void sayMessage(String message);
}
方法引用
在學習lambda表達式之后,我們通常使用lambda表達式來創建匿名方法。然而,有時候我們僅僅是調用了一個已存在的方法。如下:
Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));
在Java8中,我們可以直接通過方法引用來簡寫lambda表達式中已經存在的方法。
Arrays.sort(stringsArray, String::compareToIgnoreCase);
這種特性就叫做方法引用(Method Reference)。
方法引用是用來直接訪問類或者實例的已經存在的方法或者構造方法。方法引用提供了一種引用而不執行方法的方式,它需要由兼容的函數式接口構成的目標類型上下文。計算時,方法引用會創建函數式接口的一個實例。我們需要把握的重點是:函數引用只是簡化Lambda表達式的一種手段而已。
當Lambda表達式中只是執行一個方法調用時,不用Lambda表達式,直接通過方法引用的形式可讀性更高一些。方法引用是一種更簡潔易懂的Lambda表達式。
注意方法引用是一個特殊的Lambda表達式,其中方法引用的操作符是雙冒號"::"。
具體關於方法引用的內容,請參考這篇文章
下面就舉個列子:
方法引用的標准形式是:類名::方法名
。(注意:只需要寫方法名,不需要寫括號)
有以下四種形式的方法引用:
類型 | 示例 |
---|---|
引用靜態方法 | ContainingClass::staticMethodName |
引用某個對象的實例方法 | containingObject::instanceMethodName |
引用某個類型的任意對象的實例方法 | ContainingType::methodName |
引用構造方法 | ClassName::new |
1、靜態方法引用
組成語法格式:ClassName::staticMethodName
注意:
- 靜態方法引用比較容易理解,和靜態方法調用相比,只是把 . 換為 ::
- 在目標類型兼容的任何地方,都可以使用靜態方法引用。
例子:
String::valueOf 等價於lambda表達式 (s) -> String.valueOf(s);
Math::pow 等價於lambda表達式 (x, y) -> Math.pow(x, y);
2、特定實例對象的方法引用
這種語法與用於靜態方法的語法類似,只不過這里使用對象引用而不是類名。****實例方法引用又分以下三種類型:
-
實例上的實例方法引用
組成語法格式:instanceReference::methodName
-
超類上的實例方法引用
組成語法格式:super::methodName
方法的名稱由methodName指定,通過使用super,可以引用方法的超類版本。
-
類型上的實例方法引用
組成語法格式:ClassName::methodName (會先創建一個對象??)
3、任意對象(屬於同一個類)的實例方法引用
如下示例,這里引用的是字符串數組中任意一個對象的compareToIgnoreCase方法。
String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
4、構造方法引用
構造方法引用又分構造方法引用和數組構造方法引用。
a.構造方法引用(也可以稱作構造器引用)
組成語法格式:Class::new
構造函數本質上是靜態方法,只是方法名字比較特殊,使用的是new 關鍵字。
例子:String::new, 等價於lambda表達式 () -> new String()
b.數組構造方法引用
組成語法格式:TypeName[]::new
例子:int[]::new 是一個含有一個參數的構造器引用,這個參數就是數組的長度。等價於lambda表達式 x -> new int[x]。
假想存在一個接收int參數的數組構造方法
IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 創建數組 int[10]
支持重復注解並拓寬注解的應用場景
自從Java 5中引入注解以來,這個特性開始變得非常流行,並在各個框架和項目中被廣泛使用。不過,注解有一個很大的限制是:在同一個地方不能多次使用同一個注解。Java 8打破了這個限制,引入了重復注解的概念,允許在同一個地方多次使用同一個注解。
在Java 8中使用@Repeatable注解定義重復注解,實際上,這並不是語言層面的改進,而是編譯器做的一個trick,底層的技術仍然相同。
Java 8拓寬了注解的應用場景。現在,注解幾乎可以使用在任何元素上:局部變量、接口類型、超類和接口實現類,甚至可以用在函數的異常定義上。
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<>();
}
}
ElementType.TYPE_USER和ElementType.TYPE_PARAMETER是Java 8新增的兩個注解,用於描述注解的使用場景。Java 語言也做了對應的改變,以識別這些新增的注解。
具體支持哪些使用場景,建議查看ElementType這個類。
工具相關新特性
Stream API
Java 8 API添加了一個新的抽象稱為流Stream,可以讓你以一種聲明的方式處理數據。
Stream 使用一種類似用 SQL 語句從數據庫查詢數據的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。
Stream API可以極大提高Java程序員的生產力,讓程序員寫出高效率、干凈、簡潔的代碼。
這種風格將要處理的元素集合看作一種流, 流在管道中傳輸, 並且可以在管道的節點上進行處理, 比如篩選, 排序,聚合等。
元素流在管道中經過中間操作(intermediate operation)的處理,最后由最終操作(terminal operation)得到前面處理的結果。
關於 Stream API 的具體使用方式,我之前寫過一篇文章詳細介紹過。點擊談談集合.Stream API前往閱讀。
Optional 類
Java應用中最常見的bug就是空值異常。在Java 8之前,Google Guava引入了Optionals類來解決NullPointerException,從而避免源碼被各種null檢查污染,以便開發者寫出更加整潔的代碼。Java 8也將Optional加入了官方庫。
Optional僅僅是一個容易:存放T類型的值或者null。它提供了一些有用的接口來避免顯式的null檢查,可以參考Java 8官方文檔了解更多細節。
關於這個類的具體使用方式,可以參考我整理的這篇文章。
另外,建議大家看看流行的開源框架中,這些新特性是怎么使用的。我們在沒有熟練掌握這些新功能之前,不妨模仿下這些框架的用法,也不失為一種好的學習方法。
時間API
Java 8引入了新的Date-Time API(JSR 310)來改進時間、日期的處理。時間和日期的管理一直是最令Java開發者痛苦的問題。java.util.Date和后來的java.util.Calendar一直沒有解決這個問題(甚至令開發者更加迷茫)。
因為上面這些原因,誕生了第三方庫Joda-Time,可以替代Java的時間管理API。Java 8中新的時間和日期管理API深受Joda-Time影響,並吸收了很多Joda-Time的精華。新的java.time包包含了所有關於日期、時間、時區、Instant(跟日期類似但是精確到納秒)、duration(持續時間)和時鍾操作的類。新設計的API認真考慮了這些類的不變性(從java.util.Calendar吸取的教訓),如果某個實例需要修改,則返回一個新的對象。
關於時間API的詳細使用,可以參考我之前整理的文章
Nashorn JavaScript引擎
Java 8提供了新的Nashorn JavaScript引擎,使得我們可以在JVM上開發和運行JS應用。Nashorn JavaScript引擎是javax.script.ScriptEngine的另一個實現版本,這類Script引擎遵循相同的規則,允許Java和JavaScript交互使用,例子代碼如下:
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
這個代碼的輸出結果如下:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
Base64
對Base64編碼的支持已經被加入到Java 8官方庫中,這樣不需要使用第三方庫就可以進行Base64編碼,例子代碼如下:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Base64s {
public static void main(String[] args) {
final String text = "Base64 finally in Java 8!";
final String encoded = Base64
.getEncoder()
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
final String decoded = new String(
Base64.getDecoder().decode( encoded ),
StandardCharsets.UTF_8 );
System.out.println( decoded );
}
}
新的Base64API也支持URL和MINE的編碼解碼。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。
並行數組
Java8版本新增了很多新的方法,用於支持並行數組處理。最重要的方法是parallelSort(),可以顯著加快多核機器上的數組排序。下面的例子論證了parallexXxx系列的方法:
package com.javacodegeeks.java8.parallel.arrays;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class ParallelArrays {
public static void main( String[] args ) {
long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
}
}
上述這些代碼使用parallelSetAll()方法生成20000個隨機數,然后使用parallelSort()方法進行排序。這個程序會輸出亂序數組和排序數組的前10個元素。
並發相關新特性
基於新增的lambda表達式和steam特性,為Java 8中為java.util.concurrent.ConcurrentHashMap類添加了新的方法來支持聚焦操作;另外,也為java.util.concurrentForkJoinPool類添加了新的方法來支持通用線程池操作。
Java 8還添加了新的java.util.concurrent.locks.StampedLock類,用於支持基於容量的鎖——該鎖有三個模型用於支持讀寫操作(可以把這個鎖當做是java.util.concurrent.locks.ReadWriteLock的替代者)。
在java.util.concurrent.atomic包中也新增了不少工具類,列舉如下:
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
Java命令行工具相關
Nashorn引擎:jjs
jjs是一個基於標准Nashorn引擎的命令行工具,可以接受js源碼並執行。例如,我們寫一個func.js文件,內容如下:
function f() {
return 1;
};
print( f() + 1 );
可以在命令行中執行這個命令:jjs func.js,控制台輸出結果是:
2
如果需要了解細節,可以參考官方文檔。
類依賴分析器:jdeps
jdeps是一個相當棒的命令行工具,它可以展示包層級和類層級的Java類依賴關系,它以.class文件、目錄或者Jar文件為輸入,然后會把依賴關系輸出到控制台。
我們可以利用jedps分析下Spring Framework庫,為了讓結果少一點,僅僅分析一個JAR文件:org.springframework.core-3.0.5.RELEASE.jar。
jdeps org.springframework.core-3.0.5.RELEASE.jar
這個命令會輸出很多結果,我們僅看下其中的一部分:依賴關系按照包分組,如果在classpath上找不到依賴,則顯示"not found".
org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
-> java.io
-> java.lang
-> java.lang.annotation
-> java.lang.ref
-> java.lang.reflect
-> java.util
-> java.util.concurrent
-> org.apache.commons.logging not found
-> org.springframework.asm not found
-> org.springframework.asm.commons not found
org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
-> java.lang
-> java.lang.annotation
-> java.lang.reflect
-> java.util
更多的細節可以參考官方文檔。
編譯器相關特性
1. 獲取方法的參數名稱
為了在運行時獲得Java程序中方法的參數名稱,老一輩的Java程序員必須使用不同方法,例如Paranamer liberary。Java 8終於將這個特性規范化,在語言層面(使用反射API和Parameter.getName()方法)和字節碼層面(使用新的javac編譯器以及-parameters參數)提供支持。
package com.javacodegeeks.java8.parameter.names;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class ParameterNames {
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod( "main", String[].class );
for( final Parameter parameter: method.getParameters() ) {
System.out.println( "Parameter: " + parameter.getName() );
}
}
}
在Java 8中這個特性是默認關閉的,因此如果不帶-parameters參數編譯上述代碼並運行,則會輸出如下結果:
Parameter: arg0
如果帶-parameters參數,則會輸出如下結果(正確的結果):
Parameter: args
如果你使用Maven進行項目管理,則可以在maven-compiler-plugin編譯器的配置項中配置-parameters參數:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerArgument>-parameters</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
JVM的新特性
使用Metaspace(JEP 122)代替持久代(PermGen space)。在JVM參數方面,使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize代替原來的-XX:PermSize和-XX:MaxPermSize。