Java

Java 9之Collection Factory

Java 9以前為了創造出一個collection(list, set…etc)並且初始化內容,除了打一堆code慢慢把collection做出來外,Java原生支援的方式大概只有像是Array.asList(為什麼只有List…?)或是anonymous subtype。

Java 9提供了一組Collection Factory,可快速產生List、Set和Map。這組Factory所提供的collections除了有immutable的特性外,根據官方文件的說明,在運作的效率也較好。

If the data set is read frequently, and the values change only rarely, then you may find that the overall speed is faster, even when you include the performance impact of destroying and rebuilding an immutable Map when a value changes.

使用方式

使用上相當直覺,主要就是用List.of()、Set.of()和Map.of()三個static methods為主軸。以下為實際用Jshell執行的結果:

jshell> List.of(1,2,3)
$6 ==> [1, 2, 3]

jshell> Set.of(1,2,3)
$7 ==> [3, 2, 1]

jshell> Map.of("key1", 1, "key2", 2)
$8 ==> {key2=2, key1=1}

一些小細節

關於這組新的factory methods,JEP 269內記載著他誕生的原因以及他的前世今生(?),也說明了一些API內奇怪的設計。以List.of為例,如果看Java doc會發現API的形式蠻”有趣”的。

Java List.of APIs

同樣形式的API的設計在Set.of和Map.of也能看到。以下為JEP 269針對此設計的相關說明:

Special-case APIs (fixed-argument overloads) for up to ten of elements will be provided. While this introduces some clutter in the API, it avoids array allocation, initialization, and garbage collection overhead that is incurred by varargs calls.

簡而言之就是提昇運作效率,降低記憶體overhead。另外在操作上,除了因immutable特性所以無法修改collection的內容外,null的數值也不允許。

About Immutability

官方文件中還有針對immutability做了一段有趣的說明。在Java 8時代的Unmodifiable和Java 9引入的這組immutable collections是否相同?

Immutable and Unmodifiable Are Not the Same

上述來自官方的quote說明了兩者行為顯然是不同的。以下方例子來說

jshell> Collections.unmodifiableList(myList)
$14 ==> [1, 2, 3]

jshell> List<Integer> unmodList = Collections.unmodifiableList(myList)
unmodList ==> [1, 2, 3]

jshell> myList.add(4)
$16 ==> true

jshell> unmodList.add(5)
|  java.lang.UnsupportedOperationException thrown: 
|        at Collections$UnmodifiableCollection.add (Collections.java:1056)
|        at (#17:1)

可以看到即使有unmodList這個物件限制原myList的存取,但只要有人能取得myList物件,資料依然仍順利add進collection。

OpenJDK實做上有什麼特別的嗎?

官方文件寫歸寫,覺得還是自己看看骨子裡JDK在這段做了什麼。直接連到OpenJDK的source code,以List class為例可以看到以下幾個片段:

static <E> List<E> of() {
    return ImmutableCollections.List0.instance();
}
static <E> List<E> of(E e1) {
    return new ImmutableCollections.List1<>(e1);
}
static <E> List<E> of(E e1, E e2) {
    return new ImmutableCollections.List2<>(e1, e2);
}
static <E> List<E> of(E e1, E e2, E e3) {
    return new ImmutableCollections.ListN<>(e1, e2, e3);
}

從source code可以看出OpenJDK在0、1和2個elements的時候,都還是使用特別做的ImmutableCollections.List methods處理,但是到3個elements以上的時候就改用ImmutableCollections.ListN。

轉到ImmutableCollections class的code可以看到List0、List1和List2都是類似以下的寫法:

static final class List1<E> extends AbstractImmutableList<E> {
    @Stable
    private final E e0;

    List1(E e0) {
        this.e0 = Objects.requireNonNull(e0);
    }

    @Override
    public int size() {
        return 1;
    }

    @Override
    public E get(int index) {
        Objects.checkIndex(index, 1);
        return e0;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new InvalidObjectException("not serial proxy");
    }

    private Object writeReplace() {
        return new CollSer(CollSer.IMM_LIST, e0);
    }

    @Override
    public boolean contains(Object o) {
        return o.equals(e0); // implicit nullcheck of o
    }

    @Override
    public int hashCode() {
        return 31 + e0.hashCode();
    }
}

不難看出實做上真的是直接開了一個變數來儲存來模仿List的行為。那ListN的行為呢?

static final class ListN<E> extends AbstractImmutableList<E> {
    @Stable
    private final E[] elements;

    @SafeVarargs
    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;
    }

    @Override
    public int size() {
        return elements.length;
    }

    @Override
    public E get(int index) {
        Objects.checkIndex(index, elements.length);
        return elements[index];
    }

    @Override
    public boolean contains(Object o) {
        for (E e : elements) {
            if (o.equals(e)) { // implicit nullcheck of o
                return true;
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        int hash = 1;
        for (E e : elements) {
            hash = 31 * hash + e.hashCode();
        }
        return hash;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new InvalidObjectException("not serial proxy");
    }

    private Object writeReplace() {
        return new CollSer(CollSer.IMM_LIST, elements);
    }
}

顯然ListN就回到一般Array的實做方式。也就是在List的情境當List.of帶入的參數有三個以上的時候,就會以Array的方式運作。同樣的特性在SetMap也都能看到。

Add a Comment

Your email address will not be published. Required fields are marked *