Java 8 指南

jopen 9年前發布 | 25K 次閱讀 Java 8 Java開發

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!