List、Set、Map 的 of() 方法


JDK9 在 ListSetMap 等,都提供了 of() 方法,表面上看來,它們似乎只是建立 ListSetMap 實例的便捷方法,例如:

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) 的方式建立,也就是鍵、值、鍵值的方式來指定。ListSetMapof() 方法建立的是不可變物件,你不能對它們呼叫有副作用的方法,否則會拋出 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)

那麼可以避免方才 CollectionsunmodifiableXXX() 上提到之問題嗎?這些 of() 方法多數都是採可變長度引數的方式定義,而是重載了多個不同參數個數的版本,以 Listof() 方法為例:

List、Set、Map 的 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 的不可變群集,那以上講的也不是什麼新鮮事啦!