1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.atlassian.util.concurrent;
18
19 import static com.atlassian.util.concurrent.TestUtil.serialize;
20 import static java.util.Arrays.asList;
21 import static org.junit.Assert.assertArrayEquals;
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertFalse;
24 import static org.junit.Assert.assertNotNull;
25 import static org.junit.Assert.assertNull;
26 import static org.junit.Assert.assertTrue;
27 import static org.junit.Assert.fail;
28
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.Map;
35 import java.util.Map.Entry;
36 import java.util.concurrent.ConcurrentMap;
37 import java.util.concurrent.atomic.AtomicInteger;
38 import java.util.concurrent.atomic.AtomicReference;
39
40 import org.junit.Test;
41
42 public class CopyOnWriteMapTest {
43
44 @Test
45 public void factoryCalledOnConstructor() {
46 final AtomicInteger count = new AtomicInteger();
47 final Map<String, String> init = MapBuilder.build("1", "o1", "2", "o2", "3", "o3");
48 final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
49 private static final long serialVersionUID = 8866224559807093002L;
50
51 @Override
52 public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
53 count.getAndIncrement();
54 return new HashMap<String, String>(map);
55 }
56 };
57 assertEquals(1, count.get());
58 assertEquals(3, map.size());
59 assertTrue(map.containsKey("2"));
60 assertTrue(map.containsValue("o3"));
61 assertEquals("o1", map.get("1"));
62 }
63
64 @Test
65 public void factoryCalledOnWrite() {
66 final AtomicInteger count = new AtomicInteger();
67 final Map<String, String> map = new CopyOnWriteMap<String, String>() {
68 private static final long serialVersionUID = -3858713272422952372L;
69
70 @Override
71 public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
72 count.getAndIncrement();
73 return new HashMap<String, String>(map);
74 }
75 };
76
77 assertEquals("should be called in ctor", 1, count.get());
78 map.put("test", "test");
79 assertEquals("should be called in put", 2, count.get());
80 assertEquals(1, map.size());
81 assertTrue(map.containsKey("test"));
82 assertTrue(map.containsValue("test"));
83 assertEquals("should not be called in reads", 2, count.get());
84 map.putAll(MapBuilder.build("1", "test1", "2", "test2", "3", "test3"));
85 assertEquals("should be called in putAll", 3, count.get());
86 assertEquals(4, map.size());
87 assertTrue(map.containsKey("1"));
88 assertTrue(map.containsValue("test3"));
89 map.remove("2");
90 assertEquals("should be called in remove", 4, count.get());
91 assertEquals(3, map.size());
92 assertFalse(map.containsValue("test2"));
93 map.clear();
94 assertEquals("should be called in clear", 5, count.get());
95 assertEquals(0, map.size());
96 }
97
98 @Test
99 public void delegateHashMap() throws Exception {
100 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
101 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
102 assertEquals(map, cowMap);
103 assertEquals(map.hashCode(), cowMap.hashCode());
104 assertEquals(map.toString(), cowMap.toString());
105 }
106
107 @Test
108 public void delegateKeySet() throws Exception {
109 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
110 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
111 assertEquals(map.keySet(), cowMap.keySet());
112 assertEquals(map.keySet().hashCode(), cowMap.keySet().hashCode());
113 assertEquals(map.keySet().toString(), cowMap.keySet().toString());
114 }
115
116 @Test
117 public void delegateEqualityValues() throws Exception {
118 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
119 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
120 assertEquals(new ArrayList<String>(map.values()), new ArrayList<String>(cowMap.values()));
121 assertEquals(new ArrayList<String>(map.values()).hashCode(), new ArrayList<String>(cowMap.values()).hashCode());
122 assertEquals(map.values().toString(), cowMap.values().toString());
123 }
124
125 @Test
126 public void delegateEntrySet() throws Exception {
127 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
128 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
129 assertEquals(map.entrySet(), cowMap.entrySet());
130 assertEquals(map.entrySet().hashCode(), cowMap.entrySet().hashCode());
131 assertEquals(map.entrySet().toString(), cowMap.entrySet().toString());
132 }
133
134 @Test
135 public void delegateLinked() throws Exception {
136 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
137 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
138 assertEquals(map, cowMap);
139 assertEquals(map.hashCode(), cowMap.hashCode());
140 assertEquals(map.toString(), cowMap.toString());
141 }
142
143 @Test
144 public void delegateKeySetLinked() throws Exception {
145 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
146 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
147 assertEquals(map.keySet(), cowMap.keySet());
148 assertEquals(map.keySet().hashCode(), cowMap.keySet().hashCode());
149 assertEquals(map.keySet().toString(), cowMap.keySet().toString());
150 }
151
152 @Test
153 public void delegateValuesLinked() throws Exception {
154 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
155 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
156 assertEquals(new ArrayList<String>(map.values()), new ArrayList<String>(cowMap.values()));
157 assertEquals(new ArrayList<String>(map.values()).hashCode(), new ArrayList<String>(cowMap.values()).hashCode());
158 assertEquals(map.values().toString(), cowMap.values().toString());
159 }
160
161 @Test
162 public void delegateEntrySetLinked() throws Exception {
163 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
164 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
165 assertEquals(map.entrySet(), cowMap.entrySet());
166 assertEquals(map.entrySet().hashCode(), cowMap.entrySet().hashCode());
167 assertEquals(map.entrySet().toString(), cowMap.entrySet().toString());
168 }
169
170 @Test
171 public void putIfAbsentWorksIfAbsent() throws Exception {
172 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").toMap());
173 assertNull(map.putIfAbsent("key2", "value2"));
174 assertEquals(2, map.size());
175 assertTrue(map.containsKey("key2"));
176 assertTrue(map.containsValue("value2"));
177 }
178
179 @Test
180 public void putIfAbsentFailsIfPresent() throws Exception {
181 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
182 "value2").toMap());
183 map.putIfAbsent("key2", "value3");
184 assertNotNull(map.putIfAbsent("key2", "value2"));
185 assertEquals(2, map.size());
186 assertTrue(map.containsKey("key2"));
187 assertTrue(map.containsValue("value2"));
188 assertFalse(map.containsValue("value3"));
189 }
190
191 @Test
192 public void removeWorksIfValueSame() throws Exception {
193 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
194 "value2").toMap());
195 assertTrue(map.remove("key2", "value2"));
196 assertEquals(1, map.size());
197 assertFalse(map.containsKey("key2"));
198 assertFalse(map.containsValue("value2"));
199 }
200
201 @Test
202 public void removeFailsIfValueDifferent() throws Exception {
203 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
204 "value2").toMap());
205 assertFalse(map.remove("key2", "value3"));
206 assertEquals(2, map.size());
207 assertTrue(map.containsKey("key2"));
208 assertTrue(map.containsValue("value2"));
209 assertFalse(map.containsValue("value3"));
210 }
211
212 @Test
213 public void unconditionalReplaceWorksIfKeyMapped() throws Exception {
214 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
215 "value2").toMap());
216 assertNotNull(map.replace("key2", "value3"));
217 assertEquals(2, map.size());
218 assertTrue(map.containsKey("key2"));
219 assertTrue(map.containsValue("value3"));
220 assertFalse(map.containsValue("value2"));
221 }
222
223 @Test
224 public void unconditionalReplaceFailsIfKeyMissing() throws Exception {
225 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
226 "value2").toMap());
227 assertNull(map.replace("key3", "value3"));
228 assertEquals(2, map.size());
229 assertTrue(map.containsKey("key2"));
230 assertTrue(map.containsValue("value2"));
231 assertFalse(map.containsKey("key3"));
232 assertFalse(map.containsValue("value3"));
233 }
234
235 @Test
236 public void conditionalReplaceWorksIfKeyAndValueMapped() throws Exception {
237 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
238 "value2").toMap());
239 assertTrue(map.replace("key2", "value2", "value3"));
240 assertEquals(2, map.size());
241 assertTrue(map.containsKey("key2"));
242 assertTrue(map.containsValue("value3"));
243 assertFalse(map.containsValue("value2"));
244 }
245
246 @Test
247 public void conditionalReplaceWorksIfValueNull() throws Exception {
248 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
249 null).toMap());
250 assertTrue(map.replace("key2", null, "value3"));
251 assertEquals(2, map.size());
252 assertTrue(map.containsKey("key2"));
253 assertTrue(map.containsValue("value3"));
254 assertFalse(map.containsValue(null));
255 }
256
257 @Test
258 public void conditionalReplaceFailsIfValueDifferent() throws Exception {
259 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
260 "value2").toMap());
261 assertFalse(map.replace("key2", "value3", "value4"));
262 assertEquals(2, map.size());
263 assertTrue(map.containsKey("key2"));
264 assertTrue(map.containsValue("value2"));
265 assertFalse(map.containsValue("value3"));
266 assertFalse(map.containsValue("value4"));
267 }
268
269 @Test
270 public void conditionalReplaceFailsIfKeyMissing() throws Exception {
271 final Map<String, String> init = MapBuilder.<String, String> builder().add("key", "value").add("key2", "value2").toMap();
272 final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(init);
273 assertFalse(map.replace("key3", "value2", "value3"));
274 assertEquals(2, map.size());
275 assertTrue(map.containsKey("key2"));
276 assertTrue(map.containsValue("value2"));
277 assertFalse(map.containsKey("key3"));
278 assertFalse(map.containsValue("value3"));
279 }
280
281 @Test
282 public void modifiableValues() throws Exception {
283 final AtomicInteger count = new AtomicInteger();
284 final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").add("sup", "tester").toMap();
285 final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
286 private static final long serialVersionUID = 3275978982528321604L;
287
288 @Override
289 public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
290 count.getAndIncrement();
291 return new HashMap<String, String>(map);
292 }
293 };
294 assertEquals(1, count.get());
295 final Collection<String> values = map.values();
296 try {
297 values.add("something");
298 fail("UnsupportedOp expected");
299 } catch (final UnsupportedOperationException ignore) {}
300 assertEquals(1, count.get());
301 try {
302 values.addAll(asList("one", "two", "three"));
303 fail("UnsupportedOp expected");
304 } catch (final UnsupportedOperationException ignore) {}
305 final Iterator<String> iterator = values.iterator();
306 assertTrue(iterator.hasNext());
307 assertNotNull(iterator.next());
308 try {
309 iterator.remove();
310 fail("UnsupportedOp expected");
311 } catch (final UnsupportedOperationException ignore) {}
312 assertEquals(1, count.get());
313 assertFalse(values.remove("blah"));
314 assertEquals("not modified if element not present to be removed", 1, count.get());
315 assertTrue(values.remove("test"));
316 assertEquals(2, count.get());
317 assertEquals(2, map.size());
318 assertFalse(values.retainAll(asList("testing", "tester")));
319 assertEquals(3, count.get());
320 assertEquals(2, map.size());
321 assertTrue(values.removeAll(asList("test", "testing")));
322 assertEquals(4, count.get());
323 assertEquals(1, map.size());
324 values.clear();
325 assertTrue(map.isEmpty());
326 }
327
328 @Test
329 public void modifiableEntrySet() throws Exception {
330 final AtomicInteger count = new AtomicInteger();
331 final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").add("tester", "tester")
332 .toMap();
333 final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
334 private static final long serialVersionUID = -2882860445706454721L;
335
336 @Override
337 public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
338 count.getAndIncrement();
339 return new HashMap<String, String>(map);
340 }
341 };
342 assertEquals(1, count.get());
343 final Collection<Entry<String, String>> entries = map.entrySet();
344 class E implements Map.Entry<String, String> {
345 final String e;
346
347 public E(final String e) {
348 this.e = e;
349 }
350
351 public String getKey() {
352 return e;
353 }
354
355 public String getValue() {
356 return e;
357 }
358
359 public String setValue(final String value) {
360 throw new RuntimeException("should not be called, don't use UnsupportedOp here");
361 }
362 }
363
364 try {
365 entries.add(new E("something"));
366 fail("UnsupportedOp expected");
367 } catch (final UnsupportedOperationException ignore) {}
368 assertEquals(1, count.get());
369 try {
370 entries.addAll(asList(new E("one"), new E("two"), new E("three")));
371 fail("UnsupportedOp expected");
372 } catch (final UnsupportedOperationException ignore) {}
373 final Iterator<Entry<String, String>> iterator = entries.iterator();
374 assertTrue(iterator.hasNext());
375 assertNotNull(iterator.next());
376 try {
377 iterator.remove();
378 fail("UnsupportedOp expected");
379 } catch (final UnsupportedOperationException ignore) {}
380 assertEquals(1, count.get());
381 assertFalse(entries.remove("blah"));
382 assertEquals("not modified if element not present to be removed", 1, count.get());
383 assertTrue(entries.remove(new E("test")));
384 assertEquals(2, count.get());
385 assertEquals(2, map.size());
386 assertFalse(entries.retainAll(asList(new E("testing"), new E("tester"))));
387 assertEquals(3, count.get());
388 assertEquals(2, map.size());
389 assertTrue(entries.removeAll(asList(new E("test"), new E("testing"))));
390 assertEquals(4, count.get());
391 assertEquals(1, map.size());
392 entries.clear();
393 assertTrue(map.isEmpty());
394 }
395
396 @Test
397 public void modifiableKeySet() throws Exception {
398 final AtomicInteger count = new AtomicInteger();
399
400 final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").add("tester", "tester")
401 .toMap();
402 final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
403 private static final long serialVersionUID = 7273654247572679525L;
404
405 @Override
406 public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
407 count.getAndIncrement();
408 return new HashMap<String, String>(map);
409 }
410 };
411 assertEquals(1, count.get());
412 final Collection<String> keys = map.keySet();
413 try {
414 keys.add("something");
415 fail("UnsupportedOp expected");
416 } catch (final UnsupportedOperationException ignore) {}
417 assertEquals(1, count.get());
418 try {
419 keys.addAll(asList("one", "two", "three"));
420 fail("UnsupportedOp expected");
421 } catch (final UnsupportedOperationException ignore) {}
422 final Iterator<String> iterator = keys.iterator();
423 assertTrue(iterator.hasNext());
424 assertNotNull(iterator.next());
425 try {
426 iterator.remove();
427 fail("UnsupportedOp expected");
428 } catch (final UnsupportedOperationException ignore) {}
429 assertEquals(1, count.get());
430 assertFalse(keys.remove("blah"));
431 assertEquals("not modified if element not present to be removed", 1, count.get());
432 assertTrue(keys.remove("test"));
433 assertEquals(2, count.get());
434 assertEquals(2, map.size());
435 assertFalse(keys.retainAll(asList("testing", "tester")));
436 assertEquals(3, count.get());
437 assertEquals(2, map.size());
438 assertTrue(keys.removeAll(asList("test", "testing")));
439 assertEquals(4, count.get());
440 assertEquals(1, map.size());
441 keys.clear();
442 assertTrue(map.isEmpty());
443 }
444
445 @Test(expected = IllegalArgumentException.class)
446 public void nullMap() throws Exception {
447 new CopyOnWriteMap<String, String>(null) {
448 private static final long serialVersionUID = 4223850632932526917L;
449
450
451 @Override
452 public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
453 return new HashMap<String, String>(map);
454 };
455
456 };
457 }
458
459 @Test(expected = IllegalArgumentException.class)
460 public void copyFunctionReturnsNull() throws Exception {
461 new CopyOnWriteMap<String, String>() {
462 private static final long serialVersionUID = 831716474176011289L;
463
464 @Override
465 public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
466 return null;
467 };
468 };
469 }
470
471 @Test
472 public void serializableHashMap() {
473 assertMutableMapSerializable(CopyOnWriteMap.<Object, Object> newHashMap());
474 }
475
476 @Test
477 public void serializableLinkedMap() {
478 assertMutableMapSerializable(CopyOnWriteMap.<Object, Object> newLinkedMap());
479 }
480
481 @Test
482 public void toStringTest() throws Exception {
483 final AtomicReference<Map<String, String>> ref = new AtomicReference<Map<String, String>>();
484 final CopyOnWriteMap<String, String> cowMap = new CopyOnWriteMap<String, String>() {
485 private static final long serialVersionUID = -17380087385174856L;
486
487 @Override
488 protected <N extends Map<? extends String, ? extends String>> java.util.Map<String, String> copy(final N map) {
489 ref.set(new HashMap<String, String>(map));
490 return ref.get();
491 };
492 };
493 assertEquals(ref.get().toString(), cowMap.toString());
494 }
495
496 @Test
497 public void isEmpty() throws Exception {
498 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap();
499 assertTrue(cowMap.isEmpty());
500 assertTrue(cowMap.keySet().isEmpty());
501 assertTrue(cowMap.entrySet().isEmpty());
502 assertTrue(cowMap.values().isEmpty());
503 cowMap.put("1", "1");
504 assertFalse(cowMap.isEmpty());
505 assertFalse(cowMap.keySet().isEmpty());
506 assertFalse(cowMap.entrySet().isEmpty());
507 assertFalse(cowMap.values().isEmpty());
508 }
509
510 @Test
511 public void equality() throws Exception {
512 final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
513 final CopyOnWriteMap<String, String> map = CopyOnWriteMap.newHashMap(init);
514 assertEquals(init, map);
515 assertEquals(map, init);
516 assertEquals(init.hashCode(), map.hashCode());
517 assertEquals(map.hashCode(), init.hashCode());
518 assertEquals(init.keySet(), map.keySet());
519 assertEquals(map.keySet(), init.keySet());
520 assertEquals(init.entrySet(), map.entrySet());
521 assertEquals(map.entrySet(), init.entrySet());
522 assertFalse(init.values().equals(map.values()));
523 assertFalse(map.values().equals(init.values()));
524 }
525
526 @Test
527 public void toArray() throws Exception {
528 final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
529 final CopyOnWriteMap<String, String> map = CopyOnWriteMap.newHashMap(init);
530 assertArrayEquals(init.keySet().toArray(new String[2]), map.keySet().toArray(new String[2]));
531 assertArrayEquals(init.values().toArray(new String[2]), map.values().toArray(new String[2]));
532 assertArrayEquals(init.entrySet().toArray(new Map.Entry[2]), map.entrySet().toArray(new Map.Entry[2]));
533 }
534
535 @Test
536 public void contains() throws Exception {
537 final Map<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.build("1", "o1", "2", "o2", "3", "o3"));
538 assertTrue(map.containsKey("2"));
539 assertTrue(map.containsValue("o2"));
540 assertTrue(map.keySet().contains("2"));
541 assertTrue(map.keySet().containsAll(asList(new String[] { "1", "2", "3" })));
542 assertTrue(map.values().contains("o2"));
543 assertTrue(map.values().containsAll(asList(new String[] { "o1", "o2", "o3" })));
544 }
545
546 static void assertMutableMapSerializable(final Map<Object, Object> map) {
547 map.put("1", "one");
548 assertSerializable(map);
549 assertTrue(map.containsKey("1"));
550 assertTrue(map.containsValue("one"));
551 assertEquals("1", map.keySet().iterator().next());
552 assertEquals("one", map.values().iterator().next());
553 final Map.Entry<Object, Object> entry = map.entrySet().iterator().next();
554 assertEquals("1", entry.getKey());
555 assertEquals("one", entry.getValue());
556 }
557
558 static void assertSerializable(final Map<?, ?> map) {
559 assertEquals(map, serialize(map));
560 }
561 }
562
563 class MapBuilder<K, V> {
564 private final Map<K, V> map = new HashMap<K, V>();
565
566 static <S> Map<S, S> build(final S... elements) {
567 if (elements.length % 2 != 0) {
568 throw new IllegalArgumentException("must have even number of elements: " + elements.length);
569 }
570 final MapBuilder<S, S> result = new MapBuilder<S, S>();
571 for (int i = 0; i < elements.length; i = i + 2) {
572 result.add(elements[i], elements[i + 1]);
573 }
574 return result.toMap();
575 }
576
577 static <K, V> MapBuilder<K, V> builder() {
578 return new MapBuilder<K, V>();
579 }
580
581 MapBuilder<K, V> add(final K key, final V value) {
582 map.put(key, value);
583 return this;
584 }
585
586 Entry<K, V> entry(final K key, final V value) {
587 return new Entry<K, V>() {
588 public K getKey() {
589 return key;
590 }
591
592 public V getValue() {
593 return value;
594 }
595
596 public V setValue(final V arg0) {
597 throw new UnsupportedOperationException();
598 };
599 };
600 }
601
602 Map<K, V> toMap() {
603 return Collections.unmodifiableMap(new HashMap<K, V>(map));
604 }
605 }