详解Java的 不可变集合工厂方法of()

6 分钟阅读

Java 9 开始针对 List,Map,Set 引入一组新的静态工厂方法 of() 用来简化创建 不可变集合

Java 9 以前

java.util.Collections 是Java集合的常用工具,内置提供了很多集合相关的有用方法。

1
2
3
4
5
6
7
8
Collections.unmodifiableList();
Collections.unmodifiableCollection();
Collections.unmodifiableMap();
Collections.unmodifiableSet();
Collections.unmodifiableSortedSet();
Collections.unmodifiableNavigableMap();
Collections.unmodifiableNavigableSet();
Collections.unmodifiableSortedMap();

Collections提供的 unmodifiable 前缀的一系列方法实现原理是通过装饰器模式,屏蔽掉全部变动相关的接口。

Java 17的不可变集合

在Java 9以前unmodifiable虽然修饰了屏蔽掉修改方法。但是由于是装饰器模式,所以原始集合是可以修改的, 当原始集合的变动会反馈到不可变包装的集合的显示。而Java 9通过重写并实现了一整套的不可变集合,解决了原始集合的问题。

新的不可变集合在元素数量小于等于2时,采用了很精简的结构,降低了内存开销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//  List
static final class List12<E> extends AbstractImmutableList<E>
        implements Serializable {

    @Stable
    private final E e0;

    @Stable
    private final Object e1;

    List12(E e0) {
        this.e0 = Objects.requireNonNull(e0);
        // Use EMPTY as a sentinel for an unused element: not using null
        // enables constant folding optimizations over single-element lists
        this.e1 = EMPTY;
    }

    List12(E e0, E e1) {
        this.e0 = Objects.requireNonNull(e0);
        this.e1 = Objects.requireNonNull(e1);
    }

    @Override
    public int size() {
        return e1 != EMPTY ? 2 : 1;
    }
}
//  Map
static final class Map1<K,V> extends AbstractImmutableMap<K,V> {
    @Stable
    private final K k0;
    @Stable
    private final V v0;

    Map1(K k0, V v0) {
        this.k0 = Objects.requireNonNull(k0);
        this.v0 = Objects.requireNonNull(v0);
    }
}

第二点由于是不可变集合,且字段使用finnal修饰,所以是线程安全

创建不可变列表

List.of()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testListOf() {
    System.out.println(List.of());
    System.out.println(List.of(1));
    System.out.println(List.of(1,2));
    System.out.println(List.of(1,2,3));
    System.out.println(List.of(1,2,3,4));
    System.out.println(List.of(1,2,3,4,5,6,7,8,9, 10, 11,12));
}

//  []
//  [1]
//  [1, 2]
//  [1, 2, 3]
//  [1, 2, 3, 4]
//  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

创建不可变映射

Map.of()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
public void testMapOf() {
    System.out.println(Map.of());
    System.out.println(Map.of(1, 2));
    System.out.println(Map.of(1,2,3, 4));
    System.out.println(Map.of(1,2,3,4));
    System.out.println(Map.of(1,2,3,4,5,6,7,8,9, 10, 11,12));
    System.out.println(Map.ofEntries(new Map.Entry<Integer, Integer>() {
        @Override
        public Integer getKey() {
            return 1;
        }

        @Override
        public Integer getValue() {
            return 2;
        }

        @Override
        public Integer setValue(Integer value) {
            throw  new UnsupportedOperationException();
        }
    }));
}

//  {}
//  {1=2}
//  {3=4, 1=2}
//  {3=4, 1=2}
//  {5=6, 7=8, 9=10, 11=12, 1=2, 3=4}
//  {1=2}

创建不可变集

Set.of()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testSetOf() {
    System.out.println(Set.of());
    System.out.println(Set.of(1));
    System.out.println(Set.of(1,2));
    System.out.println(Set.of(1,2,3));
    System.out.println(Set.of(1,2,3,4));
    System.out.println(Set.of(1,2,3,4,5,6,7,8,9, 10, 11,12));
}

//  []
//  [1]
//  [2, 1]
//  [1, 2, 3]
//  [1, 2, 3, 4]
//  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

序列化/反序列化

值得注意的一点是,新的不可变集合序列化/反序列化和普通的java.util.ArrayList, java.util.HashMap, java.util.HashSet有很大不同。 不可变集合本身不支持直接反序列化,是通过使用 CollSer 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static final class List12<E> extends AbstractImmutableList<E>
        implements Serializable {
    @java.io.Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new InvalidObjectException("not serial proxy");
    }

    @java.io.Serial
    private Object writeReplace() {
        if (e1 == EMPTY) {
            return new CollSer(CollSer.IMM_LIST, e0);
        } else {
            return new CollSer(CollSer.IMM_LIST, e0, e1);
        }
    }
}

CollSer将集合转换为数据更紧凑的数组结构,更利于序列化/反序列化。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
final class CollSer implements Serializable {
    @java.io.Serial
    private static final long serialVersionUID = 6309168927139932177L;

    static final int IMM_LIST       = 1;
    static final int IMM_SET        = 2;
    static final int IMM_MAP        = 3;
    static final int IMM_LIST_NULLS = 4;

    /**
     * Indicates the type of collection that is serialized.
     * The low order 8 bits have the value 1 for an immutable
     * {@code List}, 2 for an immutable {@code Set}, 3 for
     * an immutable {@code Map}, and 4 for an immutable
     * {@code List} that allows null elements.
     *
     * Any other value causes an
     * {@link InvalidObjectException} to be thrown. The high
     * order 24 bits are zero when an instance is serialized,
     * and they are ignored when an instance is deserialized.
     * They can thus be used by future implementations without
     * causing compatibility issues.
     *
     * <p>The tag value also determines the interpretation of the
     * transient {@code Object[] array} field.
     * For {@code List} and {@code Set}, the array's length is the size
     * of the collection, and the array contains the elements of the collection.
     * Null elements are not allowed. For {@code Set}, duplicate elements
     * are not allowed.
     *
     * <p>For {@code Map}, the array's length is twice the number of mappings
     * present in the map. The array length is necessarily even.
     * The array contains a succession of key and value pairs:
     * {@code k1, v1, k2, v2, ..., kN, vN.} Nulls are not allowed,
     * and duplicate keys are not allowed.
     *
     * @serial
     * @since 9
     */
    private final int tag;

    /**
     * @serial
     * @since 9
     */
    private transient Object[] array;


    CollSer(int t, Object... a) {
        tag = t;
        array = a;
    }

    /**
     * Reads objects from the stream and stores them
     * in the transient {@code Object[] array} field.
     *
     * @serialData
     * A nonnegative int, indicating the count of objects,
     * followed by that many objects.
     *
     * @param ois the ObjectInputStream from which data is read
     * @throws IOException if an I/O error occurs
     * @throws ClassNotFoundException if a serialized class cannot be loaded
     * @throws InvalidObjectException if the count is negative
     * @since 9
     */
    @java.io.Serial
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        int len = ois.readInt();

        if (len < 0) {
            throw new InvalidObjectException("negative length " + len);
        }

        SharedSecrets.getJavaObjectInputStreamAccess().checkArray(ois, Object[].class, len);
        Object[] a = new Object[len];
        for (int i = 0; i < len; i++) {
            a[i] = ois.readObject();
        }

        array = a;
    }

    /**
     * Writes objects to the stream from
     * the transient {@code Object[] array} field.
     *
     * @serialData
     * A nonnegative int, indicating the count of objects,
     * followed by that many objects.
     *
     * @param oos the ObjectOutputStream to which data is written
     * @throws IOException if an I/O error occurs
     * @since 9
     */
    @java.io.Serial
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        oos.writeInt(array.length);
        for (int i = 0; i < array.length; i++) {
            oos.writeObject(array[i]);
        }
    }

    private Object readResolve() throws ObjectStreamException {
        try {
            if (array == null) {
                throw new InvalidObjectException("null array");
            }

            // use low order 8 bits to indicate "kind"
            // ignore high order 24 bits
            switch (tag & 0xff) {
                case IMM_LIST:
                    return List.of(array);
                case IMM_LIST_NULLS:
                    return ImmutableCollections.listFromTrustedArrayNullsAllowed(
                            Arrays.copyOf(array, array.length, Object[].class));
                case IMM_SET:
                    return Set.of(array);
                case IMM_MAP:
                    if (array.length == 0) {
                        return ImmutableCollections.EMPTY_MAP;
                    } else if (array.length == 2) {
                        return new ImmutableCollections.Map1<>(array[0], array[1]);
                    } else {
                        return new ImmutableCollections.MapN<>(array);
                    }
                default:
                    throw new InvalidObjectException(String.format("invalid flags 0x%x", tag));
            }
        } catch (NullPointerException|IllegalArgumentException ex) {
            InvalidObjectException ioe = new InvalidObjectException("invalid object");
            ioe.initCause(ex);
            throw ioe;
        }
    }
}

知识共享许可协议

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 TinyZ Zzh (包含链接: https://tinyzzh.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。 如有任何疑问,请 与我联系 (tinyzzh815@gmail.com)

TinyZ Zzh

TinyZ Zzh

专注于高并发服务器、网络游戏相关(Java、PHP、Unity3D、Unreal Engine等)技术,热爱游戏事业, 正在努力实现自我价值当中。

评论

  点击开始评论...