本文譯自Java8-tutorial,並對其中內容進行了一些修改和補充。
介面的預設方法
在 Java 8 中,我們可以通過default
關鍵字來為介面新增非抽象方法。default
關鍵字修飾的方法稱為預設方法,它允許我們新增新的功能到現有庫的介面中,並能確保與採用舊版本介面編寫的程式碼之間相互相容。
對於以下例子:
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
複製程式碼
在Formula
介面中,除了有抽象方法calculate
,還定義了預設方法sqrt
,Formula
的實現類只需實現抽象方法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 中包含了很多內建的函式式介面,其中一部分例如Comparator
、Runnable
在之前的 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().stream
,map.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
是不可變的,與 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
複製程式碼
通過一個時區的附件資訊可以轉換為一個例項,這個例項很容易轉為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.NumberFormat
,DateTimeFormatter
是不可變的並且是執行緒安全的。
瞭解更多有關日期格式化的資訊可以參考這裡
註解
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 {}
複製程式碼