Java 8 教程

注:該教程翻譯自 winterbe 的 bloghtml

歡迎閱讀我對 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#

Lambda 表達式

讓咱們從如何在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表達式訪問外部做用域變量與匿名對象很是類似。 您能夠從本地外部做用域以及實例字段和靜態變量訪問最終變量。

訪問局部變量

咱們能夠從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 。 感謝閱讀!

(本文結束)

相關文章
相關標籤/搜索