JDK9 在 List
、Set
、Map
等,都提供了 of()
方法,表面上看來,它們似乎只是建立 List
、Set
、Map
實例的便捷方法,例如:
jshell> List<String> nameLt = List.of("Justin", "Monica");
nameLt ==> [Justin, Monica]
jshell> Set<String> nameSet = Set.of("Justin", "Monica");
nameSet ==> [Monica, Justin]
jshell> Map<String, Integer> scoreMap = Map.of("Justin", 95, "Monica", 100);
scoreMap ==> {Justin=95, Monica=100}
比較特別的是 Map.of()
,它是採取 Map.of(K1, V1, K2, V2)
的方式建立,也就是鍵、值、鍵值的方式來指定。List
、Set
、Map
的 of()
方法建立的是不可變物件,你不能對它們呼叫有副作用的方法,否則會拋出 UnsupportedOperationException
,例如:
jshell> nameLt.add("Irene");
| java.lang.UnsupportedOperationException thrown:
| at ImmutableCollections.uoe (ImmutableCollections.java:70)
| at ImmutableCollections$AbstractImmutableList.add (ImmutableCollections.java:76)
| at (#5:1)
jshell> nameSet.add("Irene");
| java.lang.UnsupportedOperationException thrown:
| at ImmutableCollections.uoe (ImmutableCollections.java:70)
| at ImmutableCollections$AbstractImmutableSet.add (ImmutableCollections.java:280)
| at (#6:1)
jshell> scoreMap.put("Irene", 100);
| java.lang.UnsupportedOperationException thrown:
| at ImmutableCollections.uoe (ImmutableCollections.java:70)
| at ImmutableCollections$AbstractImmutableMap.put (ImmutableCollections.java:557)
| at (#7:1)
那麼可以避免方才 Collections
的 unmodifiableXXX()
上提到之問題嗎?這些 of()
方法多數都是採可變長度引數的方式定義,而是重載了多個不同參數個數的版本,以 List
的 of()
方法為例:
在引數少於 10 個的情況下,會使用對應個數的 of()
版本,因而不會有參考原 List
實例的問題,至於那個 of(E… elements)
版本,內部並不會直接參考原本 elements
參考的實例,而是建立一個新陣列,然後對 elements
的元素逐一淺層複製,底下列出 JDK 中的原始碼實作片段以便瞭解:
ListN(E... input) {
// copy and check manually to avoid TOCTOU
@SuppressWarnings("unchecked")
E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input
for (int i = 0; i < input.length; i++) {
tmp[i] = Objects.requireNonNull(input[i]);
}
this.elements = tmp;
}
因此在資料結構上,就算你對該版本的 of()
方法直接傳入陣列,也沒有參考至原 elements
參考之物件的疑慮,從而更進一步支援了不可變特性,然而要注意,因為是元素是淺層複製,如果你直接變更了元素的狀態,of()
方法傳回的物件還是會反應出對應的狀態變更。例如:
jshell> class Student {
...> String name;
...> }
| created class Student
jshell> Student student = new Student();
student ==> Student@cb644e
jshell> student.name = "Justin";
$3 ==> "Justin"
jshell> List<Student> students = List.of(student);
students ==> [Student@cb644e]
jshell> students.get(0).name;
$5 ==> "Justin"
jshell> student.name = "Monica";
$6 ==> "Monica"
jshell> students.get(0).name;
$7 ==> "Monica"
以上面的程式片段來說,如果你想要更進一步的不可變特性,應該令 Student
類別在定義時也支援不可變特性,如此一來,使用 List.of()
方法才有意義,例如:
jshell> class Student {
...> final String name;
...> Student(String name) {
...> this.name = name;
...> }
...> }
| created class Student
jshell> Student student = new Student("Justin");
student ==> Student@cb644e
jshell> List<Student> students = List.of(student);
students ==> [Student@cb644e]
你也許會想到 Arrays.asList()
方法,似乎與 List.of()
方法很像,Arrays.asList()
方法傳回的物件長度固定,確實也是無法修改,由於方法定義時使用不定長度引數,也可以直接指定陣列作為引數,這就會引發類似的問題:
jshell> String[] names = {"Justin", "Monica"};
names ==> String[2] { "Justin", "Monica" }
jshell> List<String> nameLt = Arrays.asList(names);
nameLt ==> [Justin, Monica]
jshell> names[0] = "Irene";
$3 ==> "Irene"
jshell> nameLt;
nameLt ==> [Irene, Monica]
會發生這個問題的理由類似,Arrays.asList()
傳回的物件,內部參考了 names
參考之物件(你可以試著查看 Arrays.java 的原始碼實作來驗證);如果你需要的是不可變物件,而不是無法修改的物件,那麼在 JDK9 之後,建議改用 List.of()
,而不是 Arrays.asList()
了。
好吧!如果你看過 guava-libraries 的不可變群集,那以上講的也不是什麼新鮮事啦!