Java 8 教程

佳興的夢囈發表於2019-04-03

注:該教程翻譯自 winterbe 的 blog

歡迎閱讀我對 Java 8 的介紹。本教程將逐步指導您完成所有新語言功能。 在簡短的程式碼示例的基礎上,您將學習如何使用預設介面方法,lambda 表示式,方法引用和可重複註釋。 在本文的最後,您將熟悉最新的 API 更改,如流,功能介面,地圖擴充套件和新的 Date API 。 沒有文字牆,只有註釋和程式碼。 請盡情享用!

介面的預設方法

Java 8使我們能夠通過使用default關鍵字向介面新增非抽象方法實現。 此功能也稱為虛擬擴充套件方法。

這是我第一個例子:

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中實現單個方法物件有一種更好的方法。

Lambda 表示式

讓我們從如何在Java的早期版本中對字串列表進行排序的簡單示例開始:

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);
});
複製程式碼

正如您所看到的,程式碼更短,更易於閱讀。它還可以更短:

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表示式訪問外部作用域變數與匿名物件非常相似。 您可以從本地外部作用域以及例項欄位和靜態變數訪問最終變數。

訪問區域性變數

我們可以從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

Accessing fields and static variables-訪問欄位和靜態變數

與區域性變數相比,我們對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);
        };
    }
}
複製程式碼

Accessing Default Interface Methods-訪問預設介面方法

還記得第一節中的公式示例嗎? 介面公式定義了一個預設方法sqrt,可以從包含匿名物件的每個公式例項訪問該方法。 這不適用於lambda表示式。

無法從lambda表示式中訪問預設方法。 以下程式碼無法編譯:

Formula formula = (a) -> sqrt(a * 100);
複製程式碼

內建功能介面

JDK 1.8 API包含許多內建功能介面。 其中一些在舊版本的Java中是眾所周知的,比如ComparatorRunnable。 擴充套件了這些現有介面,以通過@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

Suppliers生成給定通用型別的結果。與Function不同,Supplier不接受引數。

Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person
複製程式碼

Consumers

Consumers表示要對單個輸入引數執行的操作。

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
複製程式碼

Comparators

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

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"
複製程式碼

Streams

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

Filter容許用謂詞去過濾流的所有元素。此操作是中間操作,它使我們能夠對結果呼叫另一個流操作(forEach)。ForEach接受為過濾流中的每個元素執行的使用者。ForEach是一個終端操作。它的返回值是void,所以我們不能呼叫另一個流操作。

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只會建立流的排序,而不會改變原集合的順序。 也就是說stringCollection的順序是不變的:

System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
複製程式碼

Map

中間操作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"
複製程式碼

Match

可以使用各種匹配操作來檢查某個謂詞是否與流匹配。所有這些操作都是終端操作,並返回一個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

Count是一個終端操作,返回流中元素的數量,為long型別。

long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

System.out.println(startsWithB);    // 3
複製程式碼

Reduce

該終端操作使用給定的函式對流的元素執行縮減。結果是一個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"
複製程式碼

Parallel Streams

如上所述,流可以是順序的,也可以是並行的。對順序流的操作在單個執行緒上執行,而對並行流的操作在多個執行緒上併發執行。

下面的示例演示了使用並行流提高效能是多麼容易。

首先,我們建立一個獨特元素的大列表:

int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
    UUID uuid = UUID.randomUUID();
    values.add(uuid.toString());
}
複製程式碼

現在我們測量對這個集合的流進行排序所需的時間。

Sequential Sort

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
複製程式碼

Parallel Sort

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

前邊提到,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函式來更改現有的值。

Date API

Java 8 在包Java .time下包含一個全新的日期和時間API。 新的日期API可以與Joda-Time庫進行比較,但是它not the same

下面的示例涵蓋了這個新API的最重要部分。

Clock

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
複製程式碼

Timezones

時區用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

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

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。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 語法的詳細資訊,請閱讀這裡

Annotations

Java 8中的註解是可重複的。讓我們直接來看一個例子。

首先,我們定義一個容器註解,它包含一個實際註解陣列:

@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 編譯器隱式地設定了 @ 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, StampedLockCompletableFuture —僅舉幾個例子。

我還在我的 blog 上發表了一些後續文章,您可能會感興趣:

你還可以 關注我的 Twitter 。 感謝閱讀!

Java 8 教程

(本文結束)

相關文章