Java 8 新特性

Zhaoxi_Zhang發表於2018-12-12

本文譯自Java8-tutorial,並對其中內容進行了一些修改和補充。

介面的預設方法

在 Java 8 中,我們可以通過default關鍵字來為介面新增非抽象方法。default關鍵字修飾的方法稱為預設方法,它允許我們新增新的功能到現有庫的介面中,並能確保與採用舊版本介面編寫的程式碼之間相互相容。 對於以下例子:

interface Formula {
    double calculate(int a);

    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}
複製程式碼

Formula介面中,除了有抽象方法calculate,還定義了預設方法sqrtFormula的實現類只需實現抽象方法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 以匿名物件的方式實現了Formula介面,而這只是為了實現sqrt(a * 100),略顯繁瑣的,在下一部分,將討論一種在 Java 8 中更為優雅的實現方式。

Lambda表示式

首先讓我們用 1.8 之前的 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接受一個列表和一個比較器來對給定的列表中的元素進行排序,你會發現你經常需要建立匿名比較器傳給排序函式。 為了避免一直建立匿名物件,Java 8 通過lambad 表示式來簡化語法規則:

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});
複製程式碼

上面的程式碼更加精簡,可讀性也更強,當然,還可以繼續精簡:

Collections.sort(names, (String a, String b) -> b.compareTo(a));
複製程式碼

Lambda 表示式的主體只有一條語句時,花括號{}和return關鍵字可省略。 現在,列表有了一個sort方法,另外,當可以從上下文推斷出引數的型別,同樣可以省略掉引數型別。

Lambad表示式的結構

  • 一個 Lambda 表示式可以有零個或多個引數
  • 引數的型別既可以明確宣告,也可以根據上下文來推斷。例如:(int a)與(a)效果相同
  • 所有引數需包含在圓括號內,引數之間用逗號相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c)
  • 空圓括號代表引數集為空。例如:() -> 42
  • 當只有一個引數,且其型別可推導時,圓括號()可省略。例如:a -> return a*a
  • Lambda 表示式的主體可包含零條或多條語句
  • 如果 Lambda 表示式的主體只有一條語句,花括號{}可省略。匿名函式的返回型別與該主體表示式一致
  • 如果 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註解,上述程式碼仍然是正確的。

方法和構造器引用

方法引用的分類:

  • 類名::靜態方法名
  • 物件::例項方法名
  • 類名::例項方法名
  • 類名::new

類名::靜態方法名

上述例子中的程式碼可以進一步通過靜態方法引用來精簡:

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

類名::例項方法名

public class Person {
    private String name;

    public Person() {
    
    }

    public Student(String name){
        this.name = name;
    }

    public int compareByScore(Student student){
        return this.getScore() - student.getScore();
    }
}
複製程式碼
students.sort(Student::compareByScore);
students.forEach(student -> System.out.println(student.getScore()));
複製程式碼

sort 方法接收的 Lambda 表示式本該有兩個引數,而這個例項方法只有一個引數也滿足 Lambda 表示式的定義。這就是 類名::例項方法名 這種方法引用的特殊之處,當使用 類名::例項方法名 方法引用時,一定是 Lambda 表示式所接收的第一個引數來呼叫例項方法,如果 Lambda 表示式接收多個引數,其餘的引數作為方法的引數傳遞進去。

類名::new

讓我們來看看::關鍵字在構造器引用中是如何使用的。首先,我們定義一個有多個建構函式的類:

class Person {
    String firstName;
    String lastName;

    Person() {}

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}
複製程式碼

接下來,我們建立一個用於建立新人員的工廠介面:

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 構造器傳了一個引用(注:Person類中需要有無參構造器),Java編譯器會自動選擇正確的構造器。

Lambda作用域

Lambda 表示式訪問外部變數的方式與匿名物件非常相似,它可以訪問區域性外圍的 final 變數、成員變數和靜態變數。

訪問區域性變數

我們可以在 Lambda 表示式所在的外部範圍訪問final修飾的區域性變數

final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3
複製程式碼

不同於匿名物件的是,上述變數 num 不一定要被宣告為 final(匿名內部類中的引數必須宣告為 final,其值是 capture by value的),下述程式碼也是正確的:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3
複製程式碼

值得注意的是,雖然 num 變數不需要顯式宣告為 final,但實際上,編譯器要求 Lambda 表示式中捕獲的變數必須實際上是最終變數(也就是初始化後不可再賦新值)所以 num 不可更改,下述程式碼無法通過編譯,原因就是 num 的值被更改了:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
num = 3;
複製程式碼

訪問成員變數和靜態變數

與區域性變數不同的是,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 中包含了很多內建的函式式介面,其中一部分例如ComparatorRunnable在之前的 JDK 版本中就被人熟知。這些現有的介面通過@FunctionalInterface註解被擴充來支援 Lambda。 Java 8中的 API 也提供了一些新的函式式介面來使得程式設計更加簡單。 以下是常用的函式式介面

函式式介面 引數型別 返回型別 抽象方法名 描述 其他方法
Runnable void run 作為無引數或返回值的動作執行
Supplier T get 提供一個T型別的值
Consumer T void accept 處理一個T型別的值 andThen
BiConsumer<T,U> T,U void accept 處理T和U型別的值 andThen
Function<T,R> T R apply 有一個T型別引數的函式 compose,andThen,identity
BiFunction<T,U,R> T,U R apply 有T和U型別引數的函式 andThen
UnaryOperator T T apply 型別T上的一元操作符 compose,andThen,identity
BinaryOperator T,T T apply 型別T上的二元操作符 andThen,maxBy,minBy
Predicate T boolean test 布林值函式 and,or,negate,isEqual
BiPredicate<T,U> T,U boolean test 有兩個引數的布林值函式 and,or,negate

Predicate

Predicate 是一個布林型別的函式,該函式只有一個引數,該介面包含了多種預設方法,用於處理複雜的邏輯動詞。

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

Function

Function接受一個引數並且返回一個結果,可以使用預設方法(compose,andThen)將多個函式連結起來。

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123");     // "123"
複製程式碼

Supplier

Supplier返回一個給定型別的結果,與Function不同的是,Supplier不接受任何引數。

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

Consumer

Comsumer代表了在一個輸入引數上需要進行的操作.

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

Comparator

Comparator在之前的 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
複製程式碼

Optional

Optional並非是一個函式式介面,但卻是一個精巧的工具介面,用來防止NullPointerException,這個概念對於下一部分顯得很重要,所以我們在這快速瀏覽一下Optional是如何工作的。 Optional是一個簡單的值容器,這個值可以是 null,也可以是 non-null 的。考慮一個方法可能返回一個 non-null 值的結果,也有可能返回一個空值。在 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代表了可以在其上面執行一個或多個操作的元素序列。流操作是中間或者完結操作。完結操作會返回一個某種型別的值,而中間操作會返回流本身,因此你可以連續連結多個方法的呼叫。Stream 是在一個源的基礎上建立出來的,例如java.util.Collection中的 lists 或 sets(不支援 maps)。流操作可以被順序或者並行執行。 讓我們先來了解下序列流是如何工作的,首先,我們通過字串列表的形式建立一個示例程式碼:

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 中的集合已被擴充,因此你可以直接呼叫Collection.stream()·Collection.parallelStream()來建立流。接下來的部分將會解釋最常用的流操作。

Filter

Filter 接受一個 predicate 型別的介面來過濾流中的元素。該操作是一箇中間操作,因此它允許我們在返回結果的時候再呼叫其他流操作(forEach)。ForEach 接受一個 Consumer 型別的介面變數,用來執行對多慮的流中的每一個元素的操作。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只是建立了流排序後的檢視,並沒有操作操作集合,集合中元素的順序是沒有改變的。

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變數中。

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

正如上面提到的,stream 可以是順序的也可以是並行的。順序操作通過單執行緒執行,而並行操作通過多執行緒執行。 下面的例子說明了使用並行流提高執行效率是多麼的容易。 首先我們建立一個包含不同元素的列表:

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

兩個程式碼片段幾乎一樣,但是使用並行操作來排序的效率提高了接近一半,而你需要做得就僅是將stream替換為parallelStream

Map

正如前面提到的,map 是不支援流操作的。Map介面本身沒有可用的stream()方法,但是你可以根據鍵-值對或項通過map.keySet().streammap.values().stream()map.entrySet().stream()來建立指定的流。 此外,map 支援多種新的、有用的方法來處理常規任務。

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防止我們寫入額外的空值檢查,forEach接受一個 Consumer 為 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
複製程式碼

接下來,我們學習如何刪除給定鍵的條目,只有當前鍵值對映到給定值時,才能刪除指定條目

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

如果不存在該鍵值的條目,合併或者將鍵/值放入 map 中,或者呼叫合併函式來更改現有值。

日期API

Java 8 在java.time包下包含了全新的日期和時間 API,這個新的日期 API 與 Joda-Time 庫相似,但不完全一樣。下面的例子涵蓋了大部分新的 API。

Clock

Clock 提供了對當前日期和時間的訪問,Clocks 知道當前時區,可以使用它替代System.currentTimeMillis()來獲取當前的毫秒時間。時間線上的某一時刻也由類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

Timezone 由一個ZoneId來表示,他們可以通過靜態工廠方法獲得。時區定義了某一時刻和當地日期、時間之間轉換的偏移量。

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 p.m 或者 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是不可變的,與 LocalTimeLocalDate工作原理類似。我們可以利用方法去獲取日期時間中的某些欄位值。

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

通過一個時區的附件資訊可以轉換為一個例項,這個例項很容易轉為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
複製程式碼

日期-時間的格式化類似於 Date 或 Time。我們可以使用自定義模式來取代預定義的格式進行格式化。

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.NumberFormatDateTimeFormatter是不可變的並且是執行緒安全的。 瞭解更多有關日期格式化的資訊可以參考這裡

註解

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(Hint.class)來讀取。然後,更便利的方法是getAnnotationByType,它可以直接訪問@Hint註解。 此外,Java 8 中關於註解的使用,其還擴充了兩個新的目標:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
複製程式碼

相關文章