對於Java開發者來說,Java8的版本顯然是一個具有里程碑意義的版本,蘊含了許多令人激動的新特性,如果能利用好這些新特性,能夠大大提升我們的開發效率。Java8的函數語言程式設計能夠大大減少程式碼量和便於維護,同時,還有一些跟併發相關的功能。開發中常用到的新特性如下:
- 介面的預設方法和靜態方法
- 函式式介面FunctionInterface與lambda表示式
- 方法引用
- Stream
- Optional
- Date/time API的改進
- 其他改進
1. 介面的預設方法和靜態方法
在Java8之前,介面中只能包含抽象方法。那麼這有什麼樣弊端呢?比如,想再Collection介面中新增一個spliterator抽象方法,那麼也就意味著之前所有實現Collection介面的實現類,都要重新實現spliterator這個方法才行。而介面的預設方法就是為了解決介面的修改與介面實現類不相容的問題,作為程式碼向前相容的一個方法。
那麼如何在介面中定義一個預設方法呢?來看下JDK中Collection中如何定義spliterator方法的:
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
複製程式碼
可以看到定義介面的預設方法是通過default關鍵字。因此,在Java8中介面能夠包含抽象方法外還能夠包含若干個預設方法(即有完整邏輯的例項方法)。
public interface IAnimal {
default void breath(){
System.out.println("breath!");
};
}
public class DefaultMethodTest implements IAnimal {
public static void main(String[] args) {
DefaultMethodTest defaultMethod = new DefaultMethodTest();
defaultMethod.breath();
}
}
輸出結果為:breath!
複製程式碼
可以看出IAnimal介面中有由default定義的預設方法後,那麼其實現類DefaultMethodTest也同樣能夠擁有例項方法breath。但是如果一個類繼承多個介面,多個介面中有相同的方法就會產生衝突該如何解決?實際上預設方法的改進,使得java類能夠擁有類似多繼承的能力,即一個物件例項,將擁有多個介面的例項方法,自然而然也會存在方法重複衝突的問題。
下面來看一個例子:
public interface IDonkey{
default void run() {
System.out.println("IDonkey run");
}
}
public interface IHorse {
default void run(){
System.out.println("Horse run");
}
}
public class DefaultMethodTest implements IDonkey,IHorse {
public static void main(String[] args) {
DefaultMethodTest defaultMethod = new DefaultMethodTest();
defaultMethod.breath();
}
}
複製程式碼
定義兩個介面:IDonkey和IHorse,這兩個介面中都有相同的run方法。DefaultMethodTest實現了這兩個介面,由於這兩個介面有相同的方法,因此就會產生衝突,不知道以哪個介面中的run方法為準,編譯會出錯:inherits unrelated defaults for run.....
解決方法
針對由預設方法引起的方法衝突問題,只有通過重寫衝突方法,並方法繫結的方式,指定以哪個介面中的預設方法為準。
public class DefaultMethodTest implements IAnimal,IDonkey,IHorse {
public static void main(String[] args) {
DefaultMethodTest defaultMethod = new DefaultMethodTest();
defaultMethod.run();
}
@Override
public void run() {
IHorse.super.run();
}
}
複製程式碼
DefaultMethodTest重寫了run方法,並通過 IHorse.super.run();
指定以IHorse中的run方法為準。
靜態方法
在Java8中還有一個特性就是,介面中還可以宣告靜態方法,如下例:
public interface IAnimal {
default void breath(){
System.out.println("breath!");
}
static void run(){}
}
複製程式碼
2.函式式介面FunctionInterface與lambda表示式
函式式介面
Java8最大的變化是引入了函式式思想,也就是說函式可以作為另一個函式的引數。函式式介面,要求介面中有且僅有一個抽象方法,因此經常使用的Runnable,Callable介面就是典型的函式式介面。可以使用@FunctionalInterface
註解,宣告一個介面是函式式介面。如果一個介面滿足函式式介面的定義,會預設轉換成函式式介面。但是,最好是使用@FunctionalInterface
註解顯式宣告。這是因為函式式介面比較脆弱,如果開發人員無意間新增了其他方法,就破壞了函式式介面的要求,如果使用註解@FunctionalInterface
,開發人員就會知道當前介面是函式式介面,就不會無意間破壞該介面。下面舉一個例子:
@java.lang.FunctionalInterface
public interface FunctionalInterface {
void handle();
}
複製程式碼
該介面只有一個抽象方法,並且使用註解顯式宣告。但是,函式式介面要求只有一個抽象方法卻可以擁有若干個預設方法的(例項方法),比如下例:
@java.lang.FunctionalInterface
public interface FunctionalInterface {
void handle();
default void run() {
System.out.println("run");
}
}
複製程式碼
該介面中,除了有抽象方法handle外,還有預設方法(例項方法)run。另外,任何被Object實現的方法都不能當做是抽象方法。
lambda表示式
lambda表示式是函數語言程式設計的核心,lambda表示式即匿名函式,是一段沒有函式名的函式體,可以作為引數直接傳遞給相關的呼叫者。lambda表示式極大的增加了Java語言的表達能力。lambda的語法結構為:
(parameters) -> expression
或
(parameters) ->{ statements; }
複製程式碼
-
可選型別宣告:不需要宣告引數型別,編譯器可以統一識別引數值。
-
可選的引數圓括號:一個引數無需定義圓括號,但多個引數需要定義圓括號。
-
可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
-
可選的返回關鍵字:如果主體只有一個表示式返回值則編譯器會自動返回值,大括號需要指定明表示式返回了一個數值。
完整示例為(摘自菜鳥教程)
public class Java8Tester {
public static void main(String args[]){
Java8Tester tester = new Java8Tester();
// 型別宣告
MathOperation addition = (int a, int b) -> a + b;
// 不用型別宣告
MathOperation subtraction = (a, b) -> a - b;
// 大括號中的返回語句
MathOperation multiplication = (int a, int b) -> { return a * b; };
// 沒有大括號及返回語句
MathOperation division = (int a, int b) -> a / b;
System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
System.out.println("10 / 5 = " + tester.operate(10, 5, division));
// 不用括號
GreetingService greetService1 = message ->
System.out.println("Hello " + message);
// 用括號
GreetingService greetService2 = (message) ->
System.out.println("Hello " + message);
greetService1.sayMessage("Runoob");
greetService2.sayMessage("Google");
}
interface MathOperation {
int operation(int a, int b);
}
interface GreetingService {
void sayMessage(String message);
}
private int operate(int a, int b, MathOperation mathOperation){
return mathOperation.operation(a, b);
}
}
複製程式碼
另外,lambda還可以訪問外部區域性變數,如下例所示:
int adder = 5;
Arrays.asList(1, 2, 3, 4, 5).forEach(e -> System.out.println(e + adder));
複製程式碼
實際上在lambda中訪問類的成員變數或者區域性變數時,會隱式轉換成final型別變數,所以上例實際上等價於:
final int adder = 5;
Arrays.asList(1, 2, 3, 4, 5).forEach(e -> System.out.println(e + adder));
複製程式碼
3. 方法引用
方法引用是為了進一步簡化lambda表示式,通過類名或者例項名與方法名的組合來直接訪問到類或者例項已經存在的方法或者構造方法。方法引用使用**::來定義,::**的前半部分表示類名或者例項名,後半部分表示方法名,如果是構造方法就使用NEW
來表示。
方法引用在Java8中使用方式相當靈活,總的來說,一共有以下幾種形式:
- 靜態方法引用:ClassName::methodName;
- 例項上的例項方法引用:instanceName::methodName;
- 超類上的例項方法引用:supper::methodName;
- 類的例項方法引用:ClassName:methodName;
- 構造方法引用Class:new;
- 陣列構造方法引用::TypeName[]::new
下面來看一個例子:
public class MethodReferenceTest {
public static void main(String[] args) {
ArrayList<Car> cars = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Car car = Car.create(Car::new);
cars.add(car);
}
cars.forEach(Car::showCar);
}
@FunctionalInterface
interface Factory<T> {
T create();
}
static class Car {
public void showCar() {
System.out.println(this.toString());
}
public static Car create(Factory<Car> factory) {
return factory.create();
}
}
}
輸出結果:
learn.MethodReferenceTest$Car@769c9116
learn.MethodReferenceTest$Car@6aceb1a5
learn.MethodReferenceTest$Car@2d6d8735
learn.MethodReferenceTest$Car@ba4d54
learn.MethodReferenceTest$Car@12bc6874
複製程式碼
在上面的例子中使用了Car::new
,即通過構造方法的方法引用的方式進一步簡化了lambda的表示式,Car::showCar
,即表示例項方法引用。
4. Stream
Java8中有一種新的資料處理方式,那就是流Stream,結合lambda表示式能夠更加簡潔高效的處理資料。Stream使用一種類似於SQL語句從資料庫查詢資料的直觀方式,對資料進行如篩選、排序以及聚合等多種操作。
4.1 什麼是流Stream
Stream是一個來自資料來源的元素佇列並支援聚合操作,更像是一個更高版本的Iterator,原始版本的Iterator,只能一個個遍歷元素並完成相應操作。而使用Stream,只需要指定什麼操作,如“過濾長度大於10的字串”等操作,Stream會內部遍歷並完成指定操作。
Stream中的元素在管道中經過中間操作(intermediate operation)的處理後,最後由最終操作(terminal operation)得到最終的結果。
- 資料來源:是Stream的來源,可以是集合、陣列、I/O channel等轉換而成的Stream;
- 基本操作:類似於SQL語句一樣的操作,比如filter,map,reduce,find,match,sort等操作。
當我們操作一個流時,實際上會包含這樣的執行過程:
獲取資料來源-->轉換成Stream-->執行操作,返回一個新的Stream-->再以新的Stream繼續執行操作--->直至最後操作輸出最終結果。
4.2 生成Stream的方式
生成Stream的方式主要有這樣幾種:
-
從介面Collection中和Arrays:
- Collection.stream();
- Collection.parallelStream(); //相較於序列流,並行流能夠大大提升執行效率
- Arrays.stream(T array);
-
Stream中的靜態方法:
- Stream.of();
- generate(Supplier s);
- iterate(T seed, UnaryOperator f);
- empty();
-
其他方法
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
- BufferedReader.lines()
下面對前面常見的兩種方式給出示例:
public class StreamTest {
public static void main(String[] args) {
//1.使用Collection中的方法和Arrays
String[] strArr = new String[]{"a", "b", "c"};
List<String> list = Arrays.asList(strArr);
Stream<String> stream = list.stream();
Stream<String> stream1 = Arrays.stream(strArr);
//2. 使用Stream中提供的靜態方法
Stream<String> stream2 = Stream.of(strArr);
Stream<Double> stream3 = Stream.generate(Math::random);
Stream<Object> stream4 = Stream.empty();
Stream.iterate(1, i -> i++);
}
}
複製程式碼
4.3 Stream的操作
常見的Stream操作有這樣幾種:
- Intermediate(中間操作):中間操作是指對流中資料元素做出相應轉換或操作後依然返回為一個流Stream,仍然可以供下一次流操作使用。常用的有:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip。
- Termial(結束操作):是指最終對Stream做出聚合操作,輸出結果。
中間操作
filter:對Stream中元素進行過濾
過濾元素為空的字串:
long count = stream.filter(str -> str.isEmpty()).count();
複製程式碼
map:對Stream中元素按照指定規則對映成另一個元素
將每一個元素都新增字串“_map”
stream.map(str -> str + "_map").forEach(System.out::println);
複製程式碼
map方法是一對一的關係,將stream中的每一個元素按照對映規則成另外一個元素,而如果是一對多的關係的話就需要使用flatmap方法。
concat:對流進行合併操作
concat方法將兩個Stream連線在一起,合成一個Stream。若兩個輸入的Stream都時排序的,則新Stream也是排序的;若輸入的Stream中任何一個是並行的,則新的Stream也是並行的;若關閉新的Stream時,原兩個輸入的Stream都將執行關閉處理。
Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5, 6)).
forEach(System.out::println);
複製程式碼
distinct:對流進行去重操作
去除流中重複的元素
Stream<String> stream = Stream.of("a", "a", "b", "c");
stream.distinct().forEach(System.out::println);
輸出結果:
a
b
c
複製程式碼
limit:限制流中元素的個數
擷取流中前兩個元素:
Stream<String> stream = Stream.of("a", "a", "b", "c");
stream.limit(2).forEach(System.out::println);
輸出結果:
a
a
複製程式碼
skip:跳過流中前幾個元素
丟掉流中前兩個元素:
Stream<String> stream = Stream.of("a", "a", "b", "c");
stream.skip(2).forEach(System.out::println);
輸出結果:
b
c
複製程式碼
peek:對流中每一個元素依次進行操作,類似於forEach操作
JDK中給出的例子:
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
輸出結果:
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
複製程式碼
sorted:對流中元素進行排序,可以通過sorted(Comparator<? super T> comparator)自定義比較規則
Stream<Integer> stream = Stream.of(3, 2, 1);
stream.sorted(Integer::compareTo).forEach(System.out::println);
輸出結果:
1
2
3
複製程式碼
match:檢查流中元素是否匹配指定的匹配規則
Stream 有三個 match 方法,從語義上說:
- allMatch:Stream 中全部元素符合傳入的 predicate,返回 true;
- anyMatch:Stream 中只要有一個元素符合傳入的 predicate,返回 true;
- noneMatch:Stream 中沒有一個元素符合傳入的 predicate,返回 true。
如檢查Stream中每個元素是否都大於5:
Stream<Integer> stream = Stream.of(3, 2, 1);
boolean match = stream.allMatch(integer -> integer > 5);
System.out.println(match);
輸出結果:
false
複製程式碼
結束操作
count:統計Stream中元素的個數
long count = stream.filter(str -> str.isEmpty()).count();
複製程式碼
max/min:找出流中最大或者最小的元素
Stream<Integer> stream = Stream.of(3, 2, 1);
System.out.println(stream.max(Integer::compareTo).get());
輸出結果:
3
複製程式碼
forEach
forEach方法前面已經用了好多次,其用於遍歷Stream中的所元素,避免了使用for迴圈,讓程式碼更簡潔,邏輯更清晰。
示例:
Stream.of(5, 4, 3, 2, 1)
.sorted()
.forEach(System.out::println);
// 列印結果
// 1,2,3,4,5
複製程式碼
reduce
Stream歸約方法總結如下:
5. Optional
為了解決空指標異常,在Java8之前需要使用if-else這樣的語句去防止空指標異常,而在Java8就可以使用Optional來解決。Optional可以理解成一個資料容器,甚至可以封裝null,並且如果值存在呼叫isPresent()方法會返回true。為了能夠理解Optional。先來看一個例子:
public class OptionalTest {
private String getUserName(User user) {
return user.getUserName();
}
class User {
private String userName;
public User(String userName) {
this.userName = userName;
}
public String getUserName() {
return userName;
}
}
}
複製程式碼
事實上,getUserName方法對輸入引數並沒有進行判斷是否為null,因此,該方法是不安全的。如果在Java8之前,要避免可能存在的空指標異常的話就需要使用if-else
進行邏輯處理,getUserName會改變如下:
private String getUserName(User user) {
if (user != null) {
return user.getUserName();
}
return null;
}
複製程式碼
這是十分繁瑣的一段程式碼。而如果使用Optional則會要精簡很多:
private String getUserName(User user) {
Optional<User> userOptional = Optional.ofNullable(user);
return userOptional.map(User::getUserName).orElse(null);
}
複製程式碼
Java8之前的if-else的邏輯判斷,這是一種指令式程式設計的方式,而使用Optional更像是一種函數語言程式設計,關注於最後的結果,而中間的處理過程交給JDK內部實現。
到現在,可以直觀的知道Optional對避免空指標異常很有效,下面,對Optional的API進行歸納:
建立Optional
- Optional.empty():通過靜態工廠方法Optional.empty,建立一個空的Optional物件;
- Optional of(T value):如果value為null的話,立即丟擲NullPointerException;
- Optional ofNullable(T value):使用靜態工廠方法Optional.ofNullable,你可以建立一個允許null值的Optional物件。
例項程式碼:
//建立Optional
Optional<Object> optional = Optional.empty();
Optional<Object> optional1 = Optional.ofNullable(null);
Optional<String> optional2 = Optional.of(null);
複製程式碼
常用方法
1. boolean equals(Object obj):判斷其他物件是否等於 Optional;
2. Optional<T> filter(Predicate<? super <T> predicate):如果值存在,並且這個值匹配給定的 predicate,返回一個Optional用以描述這個值,否則返回一個空的Optional;
3. <U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper):如果值存在,返回基於Optional包含的對映方法的值,否則返回一個空的Optional;
4. T get():如果在這個Optional中包含這個值,返回值,否則丟擲異常:NoSuchElementException;
5. int hashCode():返回存在值的雜湊碼,如果值不存在 返回 0;
6. void ifPresent(Consumer<? super T> consumer):如果值存在則使用該值呼叫 consumer , 否則不做任何事情;
7. boolean isPresent():如果值存在則方法會返回true,否則返回 false;
8. <U>Optional<U> map(Function<? super T,? extends U> mapper):如果存在該值,提供的對映方法,如果返回非null,返回一個Optional描述結果;
9. T orElse(T other):如果存在該值,返回值, 否則返回 other;
10. T orElseGet(Supplier<? extends T> other):如果存在該值,返回值, 否則觸發 other,並返回 other 呼叫的結果;
11. <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier):如果存在該值,返回包含的值,否則丟擲由 Supplier 繼承的異常;
12. String toString():返回一個Optional的非空字串,用來除錯
複製程式碼
Optional常用方法總結:
6. Date/time API的改進
在Java8之前的版本中,日期時間API存在很多的問題,比如:
- 執行緒安全問題:java.util.Date是非執行緒安全的,所有的日期類都是可變的;
- 設計很差:在java.util和java.sql的包中都有日期類,此外,用於格式化和解析的類在java.text包中也有定義。而每個包將其合併在一起,也是不合理的;
- 時區處理麻煩:日期類不提供國際化,沒有時區支援,因此Java中引入了java.util.Calendar和Java.util.TimeZone類;
針對這些問題,Java8重新設計了日期時間相關的API,Java 8通過釋出新的Date-Time API (JSR 310)來進一步加強對日期與時間的處理。在java.util.time包中常用的幾個類有:
- 它通過指定一個時區,然後就可以獲取到當前的時刻,日期與時間。Clock可以替換System.currentTimeMillis()與TimeZone.getDefault()
- Instant:一個instant物件表示時間軸上的一個時間點,Instant.now()方法會返回當前的瞬時點(格林威治時間);
- Duration:用於表示兩個瞬時點相差的時間量;
- LocalDate:一個帶有年份,月份和天數的日期,可以使用靜態方法now或者of方法進行建立;
- LocalTime:表示一天中的某個時間,同樣可以使用now和of進行建立;
- LocalDateTime:兼有日期和時間;
- ZonedDateTime:通過設定時間的id來建立一個帶時區的時間;
- DateTimeFormatter:日期格式化類,提供了多種預定義的標準格式;
示例程式碼如下:
public class TimeTest {
public static void main(String[] args) {
Clock clock = Clock.systemUTC();
Instant instant = clock.instant();
System.out.println(instant.toString());
LocalDate localDate = LocalDate.now();
System.out.println(localDate.toString());
LocalTime localTime = LocalTime.now();
System.out.println(localTime.toString());
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime.toString());
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println(zonedDateTime.toString());
}
}
輸出結果為:
2018-04-14T12:50:27.437Z
2018-04-14
20:50:27.646
2018-04-14T20:50:27.646
2018-04-14T20:50:27.647+08:00[Asia/Shanghai]
複製程式碼
7. 其他改進
Java8還在其他細節上也做出了改變,歸納如下:
- 之前的版本,註解在同一個位置只能宣告一次,而Java8版本中提供@Repeatable註解,來實現可重複註解;
- String類中提供了join方法來完成字串的拼接;
- 在Arrays上提供了並行化處理陣列的方式,比如利用Arrays類中的parallelSort可完成並行排序;
- 在Java8中在併發應用層面上也是下足了功夫:(1)提供功能更強大的Future:CompletableFuture;(2)StampedLock可用來替代ReadWriteLock;(3)效能更優的原子類::LongAdder,LongAccumulator以及DoubleAdder和DoubleAccumulator;
- 編譯器新增一些特性以及提供一些新的Java工具
參考資料
Stream的參考資料:
Optional的參考資料:
Java8新特性的介紹: