Java 8 指南
Java 8 指南
原文地址,譯文地址,作者: Benjamin Winterberg,譯者:Autumn,校對:李任
2014年3月16日
“Java is still not dead—and people are starting to figure that out.”
歡迎閱讀我對 Java 8 的介紹。本指南將一步步地通過所有的新的語言特性來引導你認識Java 8。在簡短的示例代碼的幫助下,你將會學習到如何使用默認的接口方法、lambda表達式、方法引用以及可重復的注解。在文章的最后,你將會熟悉最新的API變化,例如:streams、函數式接口、map 擴展以及新的 Date API。
沒有過多的文本 — 僅僅是一些具有注釋的代碼片段。一起享受吧!
接口的默認方法
Java 8 使我們能夠使用default 關鍵字給接口增加非抽象的方法實現。這個特性也被叫做 擴展方法(Extension Methods)。如下例所示:
interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } }
除了抽象方法calculate ,接口 Formula 同樣定義了默認的方法 sqrt。具體類只需要實現抽象方法calculate。默認的方法sqrt可以在其未實現時“開箱即用”。
Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0
formula 被創建的像一個匿名對象。代碼看起來很啰嗦:對一個簡單的sqrt(a * 100)計算需6行。正如我們在下一節將要看到的,對只有單方法的類的實現,在 Java 8中有個更佳的方式。
Lambda表達式
我們先來講一個簡單的例子:在 Java 之前的版本中是如何排序一個字符串list的:
List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator() { @Override public int compare(String a, String b) { return b.compareTo(a); } });
靜態方法Collections.sort 接受一個list和比較方法來對給定的list元素排序。你總是會發現你需要創建匿名的比較方法并且傳遞給排序方法。
不同于整天創建匿名對象,Java 8有一個簡短的多的語法:lambda表達式:
Collections.sort(names, (String a, String b) -> { return b.compareTo(a); });
正如你所見,代碼更短也更易于閱讀,而且它還可以更短:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
對于一行的方法體,你可以省略{} 和return 關鍵字,而且它還可以更短:
Collections.sort(names, (a, b) -> b.compareTo(a));
Java 編譯器知道參數類型,所以你也可以省略它們。下面讓我們一同深入探究下lambda表達式是如何被更廣泛地使用的。
函數式接口
lambda表達式是如何符合 Java 類型系統的?每個lambda對應于一個給定的類型,用一個接口來說明。而這個被稱為函數式接口(functional interface)的接口必須僅僅包含一個抽象方法聲明。每個那個類型的lambda表達式都將會被匹配到這個抽象方法上。因此默認的方法并不是抽象的,你可以給你的函數式接口自由地增加默認的方法。
我們可以使用任意的接口作為lambda表達式,只要這個接口只包含一個抽象方法。為了保證你的接口滿足需求,你需要增加@FunctionalInterface 注解。編譯器知道這個注解,一旦你試圖給這個接口增加第二個抽象方法聲明時,它將拋出一個編譯器錯誤。例如:
@FunctionalInterface interface Converter<F, T> { T convert(F from); } Converter<String, Integer> converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123
請記住如果@FunctionalInterface 這個注解被遺漏,此代碼依然有效。
方法和構造器引用
通過使用靜態方法引用,如上的示例代碼可以被進一步的簡化:
Converter<String, Integer> converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123
Java 8使你能夠通過::關鍵字傳遞對方法或者構造器的引用。如上例子告訴我們如何引用一個靜態的方法。但是我們也可以引用對象的方法:
class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter<String, String> converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J"
我們一起來看看::關鍵字是如何為構造器工作的。首先,我們定義一個具有多個不同構造器的示例bean:
class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
其次,我們指定一個Person的工廠接口,它用來創建新的Person:
interface PersonFactory</pre> { P create(String firstName, String lastName); }
為避免手工實現這個工廠,我們通過構造器引用將所有事情連接起來:
PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");
我們通過Person::new創建了一個對 Person 構造器的引用。Java編譯器通過匹配PersonFactory.create標記自動選擇合適的構造器。
Lambda作用域
通過lambda表達式訪問作用域變量非常類似于匿名對象。你可以通過局部作用域以及實例域和靜態變量來訪問final變量。
訪問局部變量
我們可以通過lambda表達式的作用域讀到final類型的局部變量:
final int num = 1; Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2);
然而不同于匿名對象,變量num 并不需要必須被聲明為 final,如下代碼同樣有效:
int num = 1; Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2);
然而,為了代碼可以編譯, num 必須隱含為final類型,如下代碼不會編譯通過:
int num = 1; Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num); num = 3;
在lambda表達式里對 num 賦值也同樣是被禁止的。
訪問實例域和靜態變量
區別于局部變量,我們在lambda表達式里對實例域和靜態變量具有讀寫權限。這種行為在匿名對象中是眾所周知的。
class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter<Integer, String> stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter<Integer, String> stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
訪問默認的接口方法
還記得第一節的formula 例子嗎?接口Formula 定義了一個默認的方法sqrt ,它可以被每個formula 實例(包含匿名對象)來訪問。lambda表達式對此并不奏效。
默認的方法不能在lambda表達式內部被訪問到,如下代碼不能通過編譯:
Formula formula = (a) -> sqrt( a * 100);
內置功能接口
JDK 1.8 API 包含很多內置的功能接口。其中一些在舊的 Java 版本中就眾所周知了,例如Comparator 以及 Runnable。通過@FunctionalInterface標記,這些現有的接口已被擴展為lambda所能支持的。
然而 Java 8 API 同樣擁有眾多新的功能接口來使你的生活更加簡單。這些新接口中的一些從Google Guava 庫中已經廣為人知。即使你對此庫很熟悉,你也應該仔細看看那些接口是如何通過一些有用的方法所擴展的。
謂詞
謂詞是單參數的布爾值函數。該接口包含多個默認的方法使謂詞轉換成復雜的邏輯表達式(與,或,非)
Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
函數
函數接受單一參數,產出結果。默認的方法可被用來將多個函數鏈接起來(compose,andThen)。
Function<String, Integer> toInteger = Integer::valueOf; Function<String, String> backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
生產者
生產者產生一個給定的泛型類型的結果。區別于函數,生產者不接受參數。
Supplier personSupplier = Person::new; personSupplier.get(); // new Person
消費者
消費者代表了將要對一個單一輸入參數采取的運算。
Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
比較器
比較器在較老的 Java版本中眾所周知。Java 8給這個接口增加了多個默認的方法。
Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
Optionals
Optionals 并不是函數式接口,而是一個避免空指針異常NullPointerException的俏皮工具。這是一個下一節用到的重要的概念,所以讓我們快速地看一下它是如何工作的。
Optional 是對空或者非空的一個值的簡單的容器。想象一下,一個應該返回非空值結果的方法卻有時候什么也沒返回。在Java 8 中,你將返回一個Optional 而不是null。
Optional optional = Optional.of("bam"); optional.isPresent(); // true optional.get(); // "bam" optional.orElse("fallback"); // "bam" optional.ifPresent((s) -> System.out.println(s.charAt(0))); <i>// "b"</i>
Streams
java.util.Stream 代表元素的一個序列,一個或者多個運算可以在這個序列上運行。Stream運算可以是中間的(intermediate),也可是末端的(terminal)。末端運算返回具有特定類型的結果,中間運算返回 stream 自身,所以聶藝將多個方法調用串聯在一行。Streams是在一個源上創建的,例如,一個java.util.Collection 類似的lists或者ses(不支持maps)。Sream運算可以被順序執行或者并行執行。
讓我們一起看看順序streams是如何工作的。首先,我們創建一個字符串類型的list作為示例源:
List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
在Java 8 中Collections 被擴展了,因而你可以簡單地通過調用Collection.stream() 或者 Collection.parallelStream()創建 streams 。如下節將解釋最普遍的流運算。
Filter
Filter 接受一個謂詞來過濾出流中所有的元素。此運算是一個中間運算,它可以使我們在結果上調用其它的stream運算(forEach)。forEach 接受一個可以對每個流過濾出的元素進行操作的消費者。forEach 是一個末端運算,換句話說,我們不能再調用其他的流運算
stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Sorted
Sorted 是一個返回流的排序視圖的中間運算。除非你傳遞一個定制的Comparator ,元素將被以自然順序進行排序。
stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2"
請記住,sorted 真的僅僅對此stream創建一個排序的視圖,它并不操縱背后的聚集(collection)。stringCollection 的順序并未改變:
System<b>.</b>out<b>.</b>println<b>(</b>stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bb<i>b2, ddd1</i>
Map
中間運算 map 將每個元素通過給定的函數轉變為其它對象。如下示例講每個string轉換為一個大寫字母的string。但是你也可以使用map 將每個對象轉換為其它了下。轉換結果的類型依賴于你傳遞給map 的類型。
stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "A<i>AA2", "AAA1"</i>
匹配
多個匹配運算可以被用來檢驗是否一個特定的謂詞與某stream匹配。所有的這些運算都為末端運算,并且返回一個布爾值結果。
boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
計數
計數是一個末端運算,以long類型返回在stream中的元素的數目。
long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Reduce
這個末端運算使用給定的函數對stream的元素進行一個減縮運算。結果是一個保存有減縮值的Optional 。
Optional<b><</b>String> reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
Parallel Streams
正如上述提到的,streams可以是順序或者并行的。在順序streams上的操作是在一個單線程中完成的,然而在并行streams上的操作時在多個線程上并發完成的。
如下例子證明了通過使用并發流是如何簡單地提高運算性能的。
首先,我們創建一個無重復元素的大的list:
int max <b>=</b> 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
現在我們對需要多少時間來完成對其排序進行統計。
順序排序
long t0 <b>=</b> System<b>.</b>nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
并行排序
long t0 <b>=</b> System<b>.</b>nanoTime<b>();</b> long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms
正如你所看到的,兩個代碼片段幾乎相同,但是并行排序快了將近50%。而所有你所需要做的僅僅是將stream() 改為 parallelStream()。
Map
正如已經提到的,maps并不支持streams。然而,maps現在支持多種新的有用的方法來完成普通的任務。
Map<Integer, String> map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val));
如上的代碼應該是意義很明確的:putIfAbsent避免了我們寫多余的null檢查;forEach 接受一個消費者去對每個map的值做運算。
這個例子顯示了如何利用函數在map上進行操作。
map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33
接下來,我們學習如何對給定的key刪除entry(只有它對應到了一個給定的值時生效):
map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null
另一個有用的方法:
map.getOrDefault(42, "not found");
合并map的記錄很方便:
map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
如果沒有相應記錄,合并會將key/value 對放入map,否則合并函數將會被調用來改變現有的值。
Date API
Java 8在包java.time包含一個全新的日期和時間的API。這個新的Date API堪比Joda-Time庫,然而,它們并不完全相同。如下的例子將會覆蓋這個新API的大多數重要的部分。
Clock
Clock 提供對現在時間和日期的訪問。Clocks 能感知時區,可能被用來替代System.currentTimeMillis() 來獲取現在毫秒數。這樣一個在時間線上即時的點也被類Instant所表示。Instant可以被用來創建java.util.Date 對象。
Clock clock = Clock.systemDefaultZone(); long millis = clock.millis(); Instant instant = clock.instant(); Date legacyDate = Date.from(instant); // legacy java.util.Date
Timezones
Timezones由一個ZoneId代表。他們可以被靜態工廠方法很容易地訪問。 Timezones 定義偏移量,這對于instants 和本地日期、時間之間的轉換非常重要。
System.out.println(ZoneId.getAvailableZoneIds()); // prints all available timezone ids ZoneId zone1 = ZoneId.of("Europe/Berlin"); ZoneId zone2 = ZoneId.of("Brazil/East"); System.out.println(zone1.getRules()); System.out.println(zone2.getRules()); // ZoneRules[currentStandardOffset=+01:00] // ZoneRules[currentStandardOffset=-03:00]
LocalTime
LocalTime 代表一個不帶時區的時間,例如:10pm 或者 17:30:15。如下示例為如上定義的時區創建兩個本地時間 。那么,我們就可以比較兩個時間,并且計算出兩個時間間的時間差(小時或者分鐘為單位)。
LocalTime now1 = LocalTime.now(zone1); LocalTime now2 = LocalTime.now(zone2); System.out.println(now1.isBefore(now2)); // false long hoursBetween = ChronoUnit.HOURS.between(now1, now2); long minutesBetween = ChronoUnit.MINUTES.between(now1, now2); System.out.println(hoursBetween); // -3 System.out.println(minutesBetween); // -239
伴隨著LocalTime ,這里存在多種工廠方法來簡化新實例的創建,也包含對時間字符串的解析。
LocalTime late = LocalTime.of(23, 59, 59); System.out.println(late); // 23:59:59 DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedTime(FormatStyle.SHORT) .withLocale(Locale.GERMAN); LocalTime leetTime = LocalTime.parse("13:37", germanFormatter); System.out.println(leetTime); // 13:37
LocalDate
LocalDate 代表一個明確的日期,例如2014-03-11。它是不可變的,并且非常類似LocalTime。 這個示例將證明如何通過增加或者減少日期、月份、年來計算出一個新的日期。記住每次操作將返回一個新的實例。
LocalDate today = LocalDate.now(); LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS); LocalDate yesterday = tomorrow.minusDays(2); LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4); DayOfWeek dayOfWeek = independenceDay.getDayOfWeek(); System.out.println(dayOfWeek); // FRIDAY
從字符串中解析LocalDate就像解析 LocalTime一樣簡單:
DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedDate(FormatStyle.MEDIUM) .withLocale(Locale.GERMAN); LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter); System.out.println(xmas); // 2014-12-24
LocalDateTime
LocalDateTime 代表日期-時間。它將如上所示的日期和時間合并為一個實例。LocalDateTime 是不可變的,它類似LocalTime和 LocalDate。我們可以使用方法獲取日期-時間中特定的域:
LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59); DayOfWeek dayOfWeek = sylvester.getDayOfWeek(); System.out.println(dayOfWeek); // WEDNESDAY Month month = sylvester.getMonth(); System.out.println(month); // DECEMBER long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY); System.out.println(minuteOfDay); // 1439
知道額外的時區信息,它就可以轉換為一個instant,instant可以很容易地轉換為java.util.Date 類型的日期。
Instant instant = sylvester .atZone(ZoneId.systemDefault()) .toInstant(); Date legacyDate = Date.from(instant); System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
形式化日期-時間的工作就像形式化日期或者時間一樣。我們可以從客戶化的模式來創建格式(formatter),而不是使用預先定義的格式。
DateTimeFormatter formatter = DateTimeFormatter .ofPattern("MMM dd, yyyy - HH:mm"); LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter); String string = formatter.format(parsed); System.out.println(string); // Nov 03, 2014 - 07:13
區別于 java.text.NumberFormat ,新的DateTimeFormatter 時而不可變的且是線程安全的。
模式語法的細節可以點擊這里。
Annotations/
在Java8 中Annotations是可重復的。讓我們通過一個具體的例子來理解它。
首先,我們定義一個包裝器注解,它擁有一組真正的注解:
@interface Hints { Hint[] value(); } @Repeatable(Hints.class) @interface Hint { String value(); }
Java 8 中,通過聲明注解@Repeatable使我能夠使用多個具有相同類型的注解。
變體 1: 使用容器注解(守舊派)
@Hints({@Hint("hint1"), @Hint("hint2")}) class Person {}
變體 2: 使用可重復的注解(新派)
@Hint("hint1") @Hint("hint2") class Person {}
使用變體2,Java編譯器可以隱式地設置 @Hints注解。這對于通過反射來閱讀注解信息是很重要的。
Hint hint = Person.class.getAnnotation(Hint.class); System.out.println(hint); // null Hints hints1 = Person.class.getAnnotation(Hints.class); System.out.println(hints1.value().length); // 2 Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class); System.out.println(hints2.length);
盡管我們從不在Person類上聲明@Hints 注解,它依然通過getAnnotation(Hints.class)可讀。然而,更多便捷的方法是 getAnnotationsByType,它將具有對所有帶注釋的@Hint 注解直接訪問的能力。
此外,在Java8中對注解的使用被擴大到兩個新的targets:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @interface MyAnnotation {}
就是這樣
我對Java 8 編程的指南就到這里。肯定還有更多的東西值得探究。由你決定去探索在Java 8 編程中所有其它偉大的改進,例如,Arrays.parallelSort、StampedLock以及 CompletableFuture 等等。
我希望這個文章對你有用,而且你喜歡閱讀。這個指南的所有的源碼請點擊GitHub。請自由地創建新的庫或者通過推ter.發送反饋。
Benjamin is lead software engineer at Pondus, marathon finisher and an excited table football player. Get in touch on 推ter, Google+ and GitHub
原創文章,轉載請注明: 轉載自并發編程網 – ifeve.com