注:該教程翻譯自 winterbe 的 blog。html
歡迎閱讀我對 Java 8 的介紹。本教程將逐步指導您完成全部新語言功能。 在簡短的代碼示例的基礎上,您將學習如何使用默認接口方法,lambda 表達式,方法引用和可重複註釋。 在本文的最後,您將熟悉最新的 API 更改,如流,功能接口,地圖擴展和新的 Date API 。 沒有文字牆,只有註釋和代碼。 請盡情享用!java
Java 8使咱們可以經過使用default關鍵字向接口添加非抽象方法實現。 此功能也稱爲虛擬擴展方法。git
這是我第一個例子:github
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
複製代碼
除了抽象方法計算接口公式還定義了默認方法sqrt。 具體類只須要實現抽象方法計算。 默認方法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
複製代碼
該公式是做爲匿名對象實現的。 代碼很是冗長:6行代碼用於簡單計算sqrt(a * 100)
。 正如咱們將在下一節中看到的,在Java 8中實現單個方法對象有一種更好的方法。c#
讓咱們從如何在Java的早期版本中對字符串列表進行排序的簡單示例開始:api
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});
複製代碼
靜態實用程序方法Collections.sort
接受列表和比較器,以便對給定列表的元素進行排序。 您常常會發現本身建立匿名比較器並將它們傳遞給sort方法。數組
Java 8不是隻會建立匿名對象,而是帶有更短的語法,lambda表達式:安全
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});
複製代碼
正如您所看到的,代碼更短,更易於閱讀。它還能夠更短:bash
Collections.sort(names, (String a, String b) -> b.compareTo(a));
複製代碼
對於一行方法體,您能夠跳過大括號{}
和return
關鍵字。 讓它變得更短:
names.sort((a, b) -> b.compareTo(a));
複製代碼
List如今有一個sort
方法。 java編譯器也知道參數類型,所以您也能夠跳過它們。 讓咱們更深刻地瞭解lambda表達式如何在野外使用。
lambda表達式如何適合Java的類型系統? 每一個lambda對應於由接口指定的給定類型。 所謂的功能接口必須只包含一個抽象方法聲明。 該類型的每一個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"
複製代碼
讓咱們看看::
關鍵字如何爲構造函數工做。 首先,咱們定義一個具備不一樣構造函數的示例類:
class Person {
String firstName;
String lastName;
Person() {}
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
複製代碼
接下來,咱們指定一個用來建立新persons的person factory接口:
interface PersonFactory<P extends Person> {
P create(String firstName, String lastName);
}
複製代碼
咱們不是手動實現工廠,而是經過構造函數引用將全部內容粘合在一塊兒:
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
複製代碼
咱們經過Person :: new
建立對Person構造函數的引用。Java編譯器經過匹配PersonFactory.create
的簽名自動選擇正確的構造函數。
從lambda表達式訪問外部做用域變量與匿名對象很是類似。 您能夠從本地外部做用域以及實例字段和靜態變量訪問最終變量。
咱們能夠從lambda表達式外部範圍讀取final
修飾的局部變量:
final int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
複製代碼
但與匿名對象不一樣,變量num
沒必要聲明爲final。 此代碼也有效:
int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
複製代碼
可是,對於要編譯的代碼,num
必須是implicitly 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);
};
}
}
複製代碼
還記得第一節中的公式示例嗎? 接口公式定義了一個默認方法sqrt,能夠從包含匿名對象的每一個公式實例訪問該方法。 這不適用於lambda表達式。
沒法從lambda表達式中訪問默認方法。 如下代碼沒法編譯:
Formula formula = (a) -> sqrt(a * 100);
複製代碼
JDK 1.8 API包含許多內置功能接口。 其中一些在舊版本的Java中是衆所周知的,好比Comparator
或Runnable
。 擴展了這些現有接口,以經過@FunctionalInterface
註釋啓用Lambda支持。
但Java 8 API也充滿了新的功能接口,讓您的生活更輕鬆。 其中一些新接口在Google Guava庫中是衆所周知的。 即便您熟悉此庫,也應密切關注如何經過一些有用的方法擴展來擴展這些接口。
謂詞是一個參數的布爾值函數。 該接口包含各類默認方法,用於將謂詞組合成複雜的邏輯術語 (and, or, negate) 。
Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
複製代碼
函數接受一個參數並生成結果。 默認方法可用於將多個函數連接在一塊兒(compose,andThen)。
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
複製代碼
Suppliers生成給定通用類型的結果。與Function不一樣,Supplier不接受參數。
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
複製代碼
Consumers表示要對單個輸入參數執行的操做。
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
複製代碼
Comparators在Java的舊版本中是很是出名的。 Java 8爲此接口添加了不少默認方法。
Comparator<Person> 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不是功能接口,而是用於防止NullPointerException的漂亮工具。 這是下一節的一個重要概念,讓咱們快速瞭解一下Optionals的工做原理。
Optional是一個變量值的簡單容器,這個值多是空或者非空。想象一個方法可能返回一個非空結果但有時不返回任何內容,在 Java 8 中,返回的不是null
,而是Optional
。
Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
複製代碼
java.util.Stream
表示能夠在其上執行一個或多個操做的元素序列。 流操做能夠是中間操做或者終端操做。當終端操做返回某種類型的結果時,中間操做會返回流自己,所以您能夠連續連接多個方法調用。流是在源上建立的,例如lists或sets之類的java.util.Collection
(不支持maps)。流操做能夠順序執行,也能夠並行執行。
Streams很是強大,因此我寫了一個單獨的Java 8 Streams Tutorial。 您還應該查看Sequency 做爲Web的相似庫。
咱們先來看看順序流是如何工做的。首先,咱們以字符串列表的形式建立一個示例源:
List<String> 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()
來建立流。如下部分介紹了最多見的流操做。
Filter允許用謂詞去過濾流的全部元素。此操做是中間操做,它使咱們可以對結果調用另外一個流操做(forEach
)。ForEach接受爲過濾流中的每一個元素執行的使用者。ForEach是一個終端操做。它的返回值是void
,因此咱們不能調用另外一個流操做。
stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa2", "aaa1"
複製代碼
Sorted是一箇中間操做,它返回流的排序視圖。 除非您傳遞自定義 Comparator
,不然元素會按天然順序排序。
stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa1", "aaa2"
複製代碼
注意,sorted只會建立流的排序,而不會改變原集合的順序。 也就是說stringCollection的順序是不變的:
System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
複製代碼
中間操做map
經過給定的函數將每一個元素轉換爲另外一個對象。下面的示例將每一個字符串轉換爲大寫字符串。而且你也可使用map
將每一個對象轉換成另外一種類型。結果流的泛型類型取決於傳遞給map
的函數的泛型類型。
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
複製代碼
可使用各類匹配操做來檢查某個謂詞是否與流匹配。全部這些操做都是終端操做,並返回一個boolean結果。
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
複製代碼
Count是一個終端操做,返回流中元素的數量,爲long
類型。
long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();
System.out.println(startsWithB); // 3
複製代碼
該終端操做使用給定的函數對流的元素執行縮減。結果是一個Optional
類型的值,其中包含了reduced的值。
Optional<String> reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
複製代碼
如上所述,流能夠是順序的,也能夠是並行的。對順序流的操做在單個線程上執行,而對並行流的操做在多個線程上併發執行。
下面的示例演示了使用並行流提升性能是多麼容易。
首先,咱們建立一個獨特元素的大列表:
int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
UUID uuid = UUID.randomUUID();
values.add(uuid.toString());
}
複製代碼
如今咱們測量對這個集合的流進行排序所需的時間。
long t0 = System.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 = System.nanoTime();
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()
。
前邊提到,maps不能直接使用流。Map
接口自己沒有stream()
方法,可是您能夠經過Map.keyset().stream()
、map.values().stream()
和 map.entrySet().stream()
在映射的鍵、值或條目上建立專門的流。
此外,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防止咱們編寫額外的if null檢查;forEach容許使用者對映射的每一個值執行操做。
下面的例子展現瞭如何使用函數在map上進行計算(compute):
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
複製代碼
接下來,咱們學習如何刪除一個給定鍵的條目,當前鍵值與給定值匹配時將被刪除:
map.remove(3, "val3");
map.get(3); // val33
map.remove(3, "val33");
map.get(3); // null
複製代碼
其餘有用的方法
map.getOrDefault(42, "not found"); // 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 的條目,則將key/value放入map中,或者調用merge函數來更改現有的值。
Java 8 在包Java .time
下包含一個全新的日期和時間API。 新的日期API能夠與Joda-Time庫進行比較,可是它not the same。
下面的示例涵蓋了這個新API的最重要部分。
Clock 提供對當前日期和時間的訪問。Clocks 知道一個時區,可使用它來代替 System.currentTimeMillis()
來檢索當前時間(自Unix紀元以來的毫秒爲單位)。這種時間線上的瞬時點也用Instant
類表示。Instants 能夠用來建立遺留的 java.util.Date
對象。
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant); // legacy java.util.Date
複製代碼
時區用ZoneId
表示。能夠經過靜態工廠方法輕鬆地訪問它們。時區定義偏移量,這些偏移量對於在 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 表示沒有時區的時間,例如晚上10點或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 表示一個不一樣的日期,例如 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
是不可變的,其工做原理相似於 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。Instants 能夠很容易地轉換爲 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
複製代碼
格式化日期-時間就像格式化日期或時間同樣工做。咱們能夠用自定義模式建立格式化器,而沒必要使用預約義的格式。
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
是不可變的,而且線程安全的。
有關 pattern 語法的詳細信息,請閱讀這裏。
Java 8中的註解是可重複的。讓咱們直接來看一個例子。
首先,咱們定義一個容器註解,它包含一個實際註解數組:
@interface Hints {
Hint[] value();
}
@Repeatable(Hints.class)
@interface Hint {
String value();
}
複製代碼
Java 8 經過聲明註釋 @Repeatable
使咱們可以使用同一類型的多個註釋。
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}
複製代碼
@Hint("hint1")
@Hint("hint2")
class Person {}
複製代碼
使用方式 2,java 編譯器隱式地設置了 @ hint
註解。這對於經過反射讀取註解信息很是重要。
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); // 2
複製代碼
雖然咱們從未在 Person
類上聲明 @Hints
註解,可是它仍然能夠經過 getAnnotation(Hints.class)
讀取。然而,更方便的方法是 getAnnotationsByType
,它容許直接訪問全部帶有 @Hint
的註解。
此外,Java 8 中註解的使用擴展到了兩個新目標:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
複製代碼
個人 Java 8 編程指南到此結束。若是您想了解更多關於 JDK8 API 的全部新類和特性,請查看個人 JDK8 API Explorer。
它能夠幫助你找出 JDK 8 中全部的新類和隱藏的精華,好比 Arrays.parallelSort
, StampedLock
和 CompletableFuture
—僅舉幾個例子。
我還在個人 blog 上發表了一些後續文章,您可能會感興趣:
你還能夠 關注個人 Twitter 。 感謝閱讀!
(本文結束)