# HG changeset patch # User mduigou # Date 1365443864 25200 # Node ID 7a4c4553870b8a492e6af71a9bdbd69b5be7fa68 # Parent b702977e7212d2d8ed16c5f4e321a9f2e6227ffb 8004518: Add in-place operations to Map 8010122: Add atomic operations to Map Reviewed-by: darcy, briangoetz, mduigou Contributed-by: Doug Lea
, Henry Jen , Akhil Arora , Peter Levart diff --git a/src/share/classes/java/util/Collections.java b/src/share/classes/java/util/Collections.java --- a/src/share/classes/java/util/Collections.java +++ b/src/share/classes/java/util/Collections.java @@ -28,6 +28,9 @@ import java.io.ObjectOutputStream; import java.io.IOException; import java.lang.reflect.Array; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; /** * This class consists exclusively of static methods that operate on or return @@ -1391,6 +1394,50 @@ public int hashCode() {return m.hashCode();} public String toString() {return m.toString();} + // Override default methods in Map + @Override + public V putIfAbsent(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public V replace(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V computeIfPresent(K key, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V compute(K key, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V merge(K key, V value, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + /** * We need this class in addition to UnmodifiableSet as * Map.Entries themselves permit modification of the backing Map @@ -1590,9 +1637,9 @@ * * Failure to follow this advice may result in non-deterministic behavior. * - *

The returned collection does not pass the hashCode - * and equals operations through to the backing collection, but - * relies on Object's equals and hashCode methods. This is + *

The returned collection does not pass the {@code hashCode} + * and {@code equals} operations through to the backing collection, but + * relies on {@code Object}'s equals and hashCode methods. This is * necessary to preserve the contracts of these operations in the case * that the backing collection is a set or a list.

* @@ -2107,6 +2154,50 @@ public String toString() { synchronized (mutex) {return m.toString();} } + + // Override default methods in Map + @Override + public V getOrDefault(Object k, V defaultValue) { + synchronized (mutex) {return m.getOrDefault(k, defaultValue);} + } + + @Override + public void forEach(BiConsumer action) { + synchronized (mutex) {m.forEach(action);} + } + @Override + public V putIfAbsent(K key, V value) { + synchronized (mutex) {return m.putIfAbsent(key, value);} + } + @Override + public boolean remove(Object key, Object value) { + synchronized (mutex) {return m.remove(key, value);} + } + @Override + public boolean replace(K key, V oldValue, V newValue) { + synchronized (mutex) {return m.replace(key, oldValue, newValue);} + } + @Override + public V replace(K key, V value) { + synchronized (mutex) {return m.replace(key, value);} + } + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + synchronized (mutex) {return m.computeIfAbsent(key, mappingFunction);} + } + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + synchronized (mutex) {return m.computeIfPresent(key, remappingFunction);} + } + @Override + public V compute(K key, BiFunction remappingFunction) { + synchronized (mutex) {return m.compute(key, remappingFunction);} + } + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + synchronized (mutex) {return m.merge(key, value, remappingFunction);} + } + private void writeObject(ObjectOutputStream s) throws IOException { synchronized (mutex) {s.defaultWriteObject();} } diff --git a/src/share/classes/java/util/HashMap.java b/src/share/classes/java/util/HashMap.java --- a/src/share/classes/java/util/HashMap.java +++ b/src/share/classes/java/util/HashMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,11 @@ */ package java.util; + import java.io.*; +import java.util.function.Consumer; +import java.util.function.BiFunction; +import java.util.function.Function; /** * Hash table based implementation of the Map interface. This @@ -350,6 +354,13 @@ return null == entry ? null : entry.getValue(); } + @Override + public V getOrDefault(Object key, V defaultValue) { + Entry entry = getEntry(key); + + return null == entry ? defaultValue : entry.getValue(); + } + /** * Returns true if this map contains a mapping for the * specified key. @@ -567,6 +578,238 @@ return (e == null ? null : e.value); } + // optimized implementations of default methods in Map + + @Override + public V putIfAbsent(K key, V value) { + int hash = (key == null) ? 0 : hash(key); + int i = indexFor(hash, table.length); + @SuppressWarnings("unchecked") + Entry e = (Entry)table[i]; + for(; e != null; e = e.next) { + if (e.hash == hash && Objects.equals(e.key, key)) { + if(e.value != null) { + return e.value; + } + e.value = value; + modCount++; + e.recordAccess(this); + return null; + } + } + + modCount++; + addEntry(hash, key, value, i); + return null; + } + + @Override + public boolean remove(Object key, Object value) { + int hash = (key == null) ? 0 : hash(key); + int i = indexFor(hash, table.length); + @SuppressWarnings("unchecked") + Entry prev = (Entry)table[i]; + Entry e = prev; + + while (e != null) { + Entry next = e.next; + if (e.hash == hash && Objects.equals(e.key, key)) { + if(!Objects.equals(e.value, value)) { + return false; + } + modCount++; + size--; + if (prev == e) + table[i] = next; + else + prev.next = next; + e.recordRemoval(this); + return true; + } + prev = e; + e = next; + } + + return false; + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + int hash = (key == null) ? 0 : hash(key); + int i = indexFor(hash, table.length); + @SuppressWarnings("unchecked") + Entry e = (Entry)table[i]; + for(; e != null; e = e.next) { + if (e.hash == hash && Objects.equals(e.key, key) && Objects.equals(e.value, oldValue)) { + e.value = newValue; + e.recordAccess(this); + return true; + } + } + + return false; + } + + @Override + public V replace(K key, V value) { + int hash = (key == null) ? 0 : hash(key); + int i = indexFor(hash, table.length); + @SuppressWarnings("unchecked") + Entry e = (Entry)table[i]; + for(; e != null; e = e.next) { + if (e.hash == hash && Objects.equals(e.key, key)) { + V oldValue = e.value; + e.value = value; + e.recordAccess(this); + return oldValue; + } + } + + return null; + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + int hash = (key == null) ? 0 : hash(key); + int i = indexFor(hash, table.length); + @SuppressWarnings("unchecked") + Entry e = (Entry)table[i]; + for(; e != null; e = e.next) { + if (e.hash == hash && Objects.equals(e.key, key)) { + V oldValue = e.value; + return oldValue == null ? (e.value = mappingFunction.apply(key)) : oldValue; + } + } + + V newValue = mappingFunction.apply(key); + if (newValue != null) { + modCount++; + addEntry(hash, key, newValue, i); + } + + return newValue; + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + int hash = (key == null) ? 0 : hash(key); + int i = indexFor(hash, table.length); + @SuppressWarnings("unchecked") + Entry prev = (Entry)table[i]; + Entry e = prev; + + while (e != null) { + Entry next = e.next; + if (e.hash == hash && Objects.equals(e.key, key)) { + V oldValue = e.value; + if (oldValue == null) + break; + V newValue = remappingFunction.apply(key, oldValue); + modCount++; + if (newValue == null) { + size--; + if (prev == e) + table[i] = next; + else + prev.next = next; + e.recordRemoval(this); + } + else { + e.value = newValue; + e.recordAccess(this); + } + return newValue; + } + prev = e; + e = next; + } + + return null; + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + int hash = (key == null) ? 0 : hash(key); + int i = indexFor(hash, table.length); + @SuppressWarnings("unchecked") + Entry prev = (Entry)table[i]; + Entry e = prev; + + while (e != null) { + Entry next = e.next; + if (e.hash == hash && Objects.equals(e.key, key)) { + V oldValue = e.value; + V newValue = remappingFunction.apply(key, oldValue); + if (newValue != oldValue) { + modCount++; + if (newValue == null) { + size--; + if (prev == e) + table[i] = next; + else + prev.next = next; + e.recordRemoval(this); + } else { + e.value = newValue; + e.recordAccess(this); + } + } + return newValue; + } + prev = e; + e = next; + } + + V newValue = remappingFunction.apply(key, null); + if (newValue != null) { + modCount++; + addEntry(hash, key, newValue, i); + } + + return newValue; + } + + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + int hash = (key == null) ? 0 : hash(key); + int i = indexFor(hash, table.length); + @SuppressWarnings("unchecked") + Entry prev = (Entry)table[i]; + Entry e = prev; + + while (e != null) { + Entry next = e.next; + if (e.hash == hash && Objects.equals(e.key, key)) { + V oldValue = e.value; + V newValue = remappingFunction.apply(oldValue, value); + modCount++; + if (newValue == null) { + size--; + if (prev == e) + table[i] = next; + else + prev.next = next; + e.recordRemoval(this); + } else { + e.value = newValue; + e.recordAccess(this); + } + return newValue; + } + prev = e; + e = next; + } + + if (value != null) { + modCount++; + addEntry(hash, key, value, i); + } + + return value; + } + + // end of optimized implementations of default methods in Map + /** * Removes and returns the entry associated with the specified key * in the HashMap. Returns null if the HashMap contains no mapping diff --git a/src/share/classes/java/util/Hashtable.java b/src/share/classes/java/util/Hashtable.java --- a/src/share/classes/java/util/Hashtable.java +++ b/src/share/classes/java/util/Hashtable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1994, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1994, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,10 @@ */ package java.util; + import java.io.*; +import java.util.function.Function; +import java.util.function.BiFunction; /** * This class implements a hash table, which maps keys to values. Any @@ -455,6 +458,26 @@ } } + private void addEntry(int hash, K key, V value, int index) { + modCount++; + + Entry tab[] = table; + if (count >= threshold) { + // Rehash the table if the threshold is exceeded + rehash(); + + tab = table; + hash = hash(key); + index = (hash & 0x7FFFFFFF) % tab.length; + } + + // Creates the new entry. + @SuppressWarnings("unchecked") + Entry e = (Entry) tab[index]; + tab[index] = new Entry<>(hash, key, value, e); + count++; + } + /** * Maps the specified key to the specified * value in this hashtable. Neither the key nor the @@ -492,21 +515,7 @@ } } - modCount++; - if (count >= threshold) { - // Rehash the table if the threshold is exceeded - rehash(); - - tab = table; - hash = hash(key); - index = (hash & 0x7FFFFFFF) % tab.length; - } - - // Creates the new entry. - @SuppressWarnings("unchecked") - Entry e = (Entry)tab[index]; - tab[index] = new Entry<>(hash, key, value, e); - count++; + addEntry(hash, key, value, index); return null; } @@ -892,6 +901,227 @@ return h; } + @Override + public synchronized V getOrDefault(Object key, V defaultValue) { + V result = get(key); + return (null == result) ? defaultValue : result; + } + + @Override + public synchronized void replaceAll( + BiFunction function) + { + Map.super.replaceAll(function); + } + + @Override + public synchronized V putIfAbsent(K key, V value) { + Objects.requireNonNull(value); + + // Makes sure the key is not already in the hashtable. + Entry tab[] = table; + int hash = hash(key); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry entry = (Entry)tab[index]; + for(; entry != null ; entry = entry.next) { + if ((entry.hash == hash) && entry.key.equals(key)) { + V old = entry.value; + if (old == null) { + entry.value = value; + } + return old; + } + } + + addEntry(hash, key, value, index); + return null; + } + + @Override + public synchronized boolean remove(Object key, Object value) { + Objects.requireNonNull(value); + + Entry tab[] = table; + int hash = hash(key); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry e = (Entry)tab[index]; + for(Entry prev = null ; e != null ; prev = e, e = e.next) { + if ((e.hash == hash) && e.key.equals(key) && e.value.equals(value)) { + modCount++; + if (prev != null) { + prev.next = e.next; + } else { + tab[index] = e.next; + } + count--; + e.value = null; + return true; + } + } + return false; + } + + @Override + public synchronized boolean replace(K key, V oldValue, V newValue) { + Entry tab[] = table; + int hash = hash(key); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry e = (Entry) tab[index]; + for (; e != null ; e = e.next) { + if ((e.hash == hash) && e.key.equals(key)) { + if (e.value.equals(oldValue)) { + e.value = newValue; + return true; + } else { + return false; + } + } + } + return false; + } + + @Override + public synchronized V replace(K key, V value) { + Entry tab[] = table; + int hash = hash(key); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry e = (Entry) tab[index]; + for (; e != null; e = e.next) { + if ((e.hash == hash) && e.key.equals(key)) { + V oldValue = e.value; + e.value = value; + return oldValue; + } + } + return null; + } + + @Override + public synchronized V computeIfAbsent(K key, Function mappingFunction) { + Objects.requireNonNull(mappingFunction); + + Entry tab[] = table; + int hash = hash(key); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry e = (Entry) tab[index]; + for(; e != null; e = e.next) { + if (e.hash == hash && e.key.equals(key)) { + // Hashtable not accept null value + return e.value; + } + } + + V newValue = mappingFunction.apply(key); + if (newValue != null) { + addEntry(hash, key, newValue, index); + } + + return newValue; + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + + Entry tab[] = table; + int hash = hash(key); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry e = (Entry) tab[index]; + for(Entry prev = null; e != null; prev = e, e = e.next) { + if (e.hash == hash && e.key.equals(key)) { + V newValue = remappingFunction.apply(key, e.value); + if (newValue == null) { + modCount++; + if (prev != null) { + prev.next = e.next; + } else { + tab[index] = e.next; + } + count--; + } else { + e.value = newValue; + } + return newValue; + } + } + return null; + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + + Entry tab[] = table; + int hash = hash(key); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry e = (Entry) tab[index]; + for(Entry prev = null; e != null; prev = e, e = e.next) { + if (e.hash == hash && Objects.equals(e.key, key)) { + V newValue = remappingFunction.apply(key, e.value); + if (newValue == null) { + modCount++; + if (prev != null) { + prev.next = e.next; + } else { + tab[index] = e.next; + } + count--; + } else { + e.value = newValue; + } + return newValue; + } + } + + V newValue = remappingFunction.apply(key, null); + if (newValue != null) { + addEntry(hash, key, newValue, index); + } + + return newValue; + } + + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + + Entry tab[] = table; + int hash = hash(key); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry e = (Entry) tab[index]; + for(Entry prev = null; e != null; prev = e, e = e.next) { + if (e.hash == hash && e.key.equals(key)) { + V newValue = remappingFunction.apply(e.value, value); + if (newValue == null) { + modCount++; + if (prev != null) { + prev.next = e.next; + } else { + tab[index] = e.next; + } + count--; + } else { + e.value = newValue; + } + return newValue; + } + } + + if (value != null) { + addEntry(hash, key, value, index); + } + + return value; + } + /** * Save the state of the Hashtable to a stream (i.e., serialize it). * diff --git a/src/share/classes/java/util/Map.java b/src/share/classes/java/util/Map.java --- a/src/share/classes/java/util/Map.java +++ b/src/share/classes/java/util/Map.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,6 +25,10 @@ package java.util; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + /** * An object that maps keys to values. A map cannot contain duplicate keys; * each key can map to at most one value. @@ -475,4 +479,584 @@ */ int hashCode(); + // Defaultable methods + + /** + * Returns the value to which the specified key is mapped, + * or {@code defaultValue} if this map contains no mapping + * for the key. + * + * @param key the key whose associated value is to be returned + * @return the value to which the specified key is mapped, or + * {@code defaultValue} if this map contains no mapping for the key + * @throws ClassCastException if the key is of an inappropriate type for + * this map + * (optional) + * @throws NullPointerException if the specified key is null and this map + * does not permit null keys + * (optional) + */ + default V getOrDefault(Object key, V defaultValue) { + V v; + return (null != (v = get(key))) + ? v + : containsKey(key) ? null : defaultValue; + } + + /** + * Performs the given action on each entry in this map, in the + * order entries are returned by an entry set iterator, until all entries + * have been processed or the action throws an {@code Exception}. + * Exceptions thrown by the action are relayed to the caller. + * + *

The default implementation should be overridden by implementations if + * they can provide a more performant implementation than an iterator-based + * one. + * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method. Any + * class overriding this method must specify its concurrency + * properties. In particular, all implementations of + * subinterface {@link java.util.concurrent.ConcurrentMap} + * must ensure that this operation is performed atomically. + * + * @implSpec + *

The default implementation is equivalent to, for this {@code map}: + *

 {@code
+     *     for ((Map.Entry entry : map.entrySet())
+     *         action.accept(entry.getKey(), entry.getValue());
+     * }
+ * + * @param action The action to be performed for each entry + * @throws NullPointerException if the specified action is null + * @since 1.8 + */ + default void forEach(BiConsumer action) { + Objects.requireNonNull(action); + for (Map.Entry entry : entrySet()) { + action.accept(entry.getKey(), entry.getValue()); + } + } + + /** + * Apply the specified function to each entry in this map, in the + * order entries are returned by an entry set iterator, and replacing + * each entry's value with the result of calling the function's + * {@link BiFunction#apply(Object, Object) BiFunction.apply(K key, V, value)} + * method with the current entry's key and value. Exceptions thrown by the + * function are relayed to the caller. + * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method. Any + * class overriding this method must specify its concurrency + * properties. In particular, all implementations of + * subinterface {@link java.util.concurrent.ConcurrentMap} + * must ensure that this operation is performed atomically. + * + * @implSpec + *

The default implementation is equivalent to, for this {@code map}: + *

 {@code
+     *     for ((Map.Entry entry : map.entrySet())
+     *         entry.setValue(function.apply(entry.getKey(), entry.getValue()));
+     * }
+ * + * @param function the function to apply to each entry + * @throws UnsupportedOperationException if the put operation + * is not supported by this map + * @throws ClassCastException if the class of the specified value + * prevents it from being stored in the backing map + * @throws NullPointerException if the specified function is null, or the + * specified key or value is null, and this map does not permit null + * keys or values + * @throws IllegalArgumentException if some property of the specified key + * or value prevents it from being stored in this map + * @throws IllegalStateException implementations may, but are not + * required to, throw this exception if the entry has been + * removed from the backing map. + * @since 1.8 + */ + default void replaceAll(BiFunction function) { + Objects.requireNonNull(function); + for (Map.Entry entry : entrySet()) { + entry.setValue(function.apply(entry.getKey(), entry.getValue())); + } + } + + /** + * If the specified key is not already associated with a value (or is mapped + * to {@code null}) associates it with the given value and returns + * {@code null}, else returns the current value. + * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method. Any + * class overriding this method must specify its concurrency + * properties. In particular, all implementations of + * subinterface {@link java.util.concurrent.ConcurrentMap} + * must ensure that this operation is performed atomically. + * + * @implSpec + * The default implementation is equivalent to, for this {@code + * map}: + * + *

 {@code
+     * if (map.get(key) == null)
+     *   return map.put(key, value);
+     * else
+     *   return map.get(key);}
+ * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or + * null if there was no mapping for the key. + * (A null return can also indicate that the map + * previously associated null with the key, + * if the implementation supports null values.) + * @throws UnsupportedOperationException if the put operation + * is not supported by this map + * (optional) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * @throws NullPointerException if the specified key or value is null, + * and this map does not permit null keys or values + * @throws IllegalArgumentException if some property of the specified key + * or value prevents it from being stored in this map + * @since 1.8 + */ + default V putIfAbsent(K key, V value) { + V v = get(key); + if(v == null) { + if(null != put(key, value)) { + throw new ConcurrentModificationException(); + } + return null; + } else { + return v; + } + } + + /** + * Removes the entry for the specified key only if it is currently + * mapped to the specified value. + * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method. Any + * class overriding this method must specify its concurrency + * properties. In particular, all implementations of + * subinterface {@link java.util.concurrent.ConcurrentMap} + * must ensure that this operation is performed atomically. + * + * @implSpec + * The default implementation is equivalent to, for this {@code map}: + * + *

 {@code
+     * if (map.containsKey(key) && Objects.equals(map.get(key), value)) {
+     *   map.remove(key);
+     *   return true;
+     * } else
+     *   return false;}
+ * + * @param key key with which the specified value is associated + * @param value value expected to be associated with the specified key + * @return true if the value was removed + * @throws UnsupportedOperationException if the remove operation + * is not supported by this map + * (optional) + * @throws ClassCastException if the key or value is of an inappropriate + * type for this map + * (optional) + * @throws NullPointerException if the specified key or value is null, + * and this map does not permit null keys or values + * (optional) + * @since 1.8 + */ + default boolean remove(Object key, Object value) { + if (!containsKey(key) || !Objects.equals(get(key), value)) + return false; + remove(key); + return true; + } + + /** + * Replaces the entry for the specified key only if currently + * mapped to the specified value. + * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method. Any + * class overriding this method must specify its concurrency + * properties. In particular, all implementations of + * subinterface {@link java.util.concurrent.ConcurrentMap} + * must ensure that this operation is performed atomically. + * + * @implSpec + * The default implementation is equivalent to, for this {@code map}: + * + *

 {@code
+     * if (map.containsKey(key) && Objects.equals(map.get(key), value)) {
+     *   map.put(key, newValue);
+     *   return true;
+     * } else
+     *   return false;}
+ * + * @param key key with which the specified value is associated + * @param oldValue value expected to be associated with the specified key + * @param newValue value to be associated with the specified key + * @return true if the value was replaced + * @throws UnsupportedOperationException if the put operation + * is not supported by this map + * (optional) + * @throws ClassCastException if the class of a specified key or value + * prevents it from being stored in this map + * @throws NullPointerException if a specified key or value is null, + * and this map does not permit null keys or values + * @throws IllegalArgumentException if some property of a specified key + * or value prevents it from being stored in this map + * @since 1.8 + */ + default boolean replace(K key, V oldValue, V newValue) { + if (!containsKey(key) || !Objects.equals(get(key), oldValue)) + return false; + put(key, newValue); + return true; + } + + /** + * Replaces the entry for the specified key only if it is + * currently mapped to some value. + * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method. Any + * class overriding this method must specify its concurrency + * properties. In particular, all implementations of + * subinterface {@link java.util.concurrent.ConcurrentMap} + * must ensure that this operation is performed atomically. + * + * @implSpec + * The default implementation is equivalent to, for this {@code map}: + * + *

 {@code
+     * if (map.containsKey(key)) {
+     *   return map.put(key, value);
+     * } else
+     *   return null;}
+ * + * @param key key with which the specified value is associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or + * null if there was no mapping for the key. + * (A null return can also indicate that the map + * previously associated null with the key, + * if the implementation supports null values.) + * @throws UnsupportedOperationException if the put operation + * is not supported by this map + * (optional) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * (optional) + * @throws NullPointerException if the specified key or value is null, + * and this map does not permit null keys or values + * @throws IllegalArgumentException if some property of the specified key + * or value prevents it from being stored in this map + * @since 1.8 + */ + default V replace(K key, V value) { + return containsKey(key) ? put(key, value) : null; + } + + /** + * If the specified key is not already associated with a value (or + * is mapped to {@code null}), attempts to compute its value using + * the given mapping function and enters it into this map unless + * {@code null}. + * + *

If the function returns {@code null} no mapping is recorded. If + * the function itself throws an (unchecked) exception, the + * exception is rethrown, and no mapping is recorded. The most + * common usage is to construct a new object serving as an initial + * mapped value or memoized result, as in: + * + *

 {@code
+     * map.computeIfAbsent(key, k -> new Value(f(k)));}
+ * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method or the + * application of the mapping function. Any class overriding this + * method must specify its concurrency properties. In particular, + * all implementations of subinterface {@link + * java.util.concurrent.ConcurrentMap} must document whether the + * function is applied once atomically only if the value is not + * present. Any class that permits null values must document + * whether and how this method distinguishes absence from null + * mappings. + * + * @implSpec + * The default implementation is equivalent to the following + * steps for this {@code map}, then returning the current value or + * {@code null} if now absent: + * + *

 {@code
+     * if (map.get(key) == null) {
+     *   V newValue = mappingFunction.apply(key);
+     *   if (newValue != null)
+     *      map.putIfAbsent(key, newValue);
+     * }}
+ * + * @param key key with which the specified value is to be associated + * @param mappingFunction the function to compute a value + * @return the current (existing or computed) value associated with + * the specified key, or null if the computed value is null + * @throws NullPointerException if the specified key is null and + * this map does not support null keys, or the + * mappingFunction is null + * @throws UnsupportedOperationException if the put operation + * is not supported by this map + * (optional) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * (optional) + * @since 1.8 + */ + default V computeIfAbsent(K key, Function mappingFunction) { + V v, newValue; + return ((v = get(key)) == null && + (newValue = mappingFunction.apply(key)) != null && + (v = putIfAbsent(key, newValue)) == null) ? newValue : v; + } + + /** + * If the value for the specified key is present and non-null, + * attempts to compute a new mapping given the key and its current + * mapped value. + * + *

If the function returns {@code null}, the mapping is removed (or + * remains absent if initially absent). If the function itself throws an + * (unchecked) exception, the exception is rethrown, and the current mapping + * is left unchanged. + * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method or the + * application of the remapping function. Any class overriding + * this method must specify its concurrency properties. In + * particular, all implementations of subinterface {@link + * java.util.concurrent.ConcurrentMap} must document whether the + * function is applied once atomically only if the value is + * present. Any class that permits null values must document + * whether and how this method distinguishes absence from null + * mappings. + * + * @implSpec + * The default implementation is equivalent to performing the + * following steps for this {@code map}, then returning the + * current value or {@code null} if now absent: + * + *

 {@code
+     * if (map.get(key) != null) {
+     *   V oldValue = map.get(key);
+     *   V newValue = remappingFunction.apply(key, oldValue);
+     *   if (newValue != null)
+     *     map.replace(key, oldValue, newValue);
+     *   else
+     *     map.remove(key, oldValue);
+     * }}
+ * + * In concurrent contexts, the default implementation may retry + * these steps when multiple threads attempt updates. + * + * @param key key with which the specified value is to be associated + * @param remappingFunction the function to compute a value + * @return the new value associated with the specified key, or null if none + * @throws NullPointerException if the specified key is null and + * this map does not support null keys, or the + * remappingFunction is null + * @throws UnsupportedOperationException if the put operation + * is not supported by this map + * (optional) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * (optional) + * @since 1.8 + */ + default V computeIfPresent(K key, + BiFunction remappingFunction) { + V v; + while ((v = get(key)) != null) { + V newValue = remappingFunction.apply(key, v); + if (newValue != null) { + if (replace(key, v, newValue)) + return newValue; + } + else if (remove(key, v)) + return null; + } + return v; + } + + /** + * Attempts to compute a mapping for the specified key and its + * current mapped value (or {@code null} if there is no current + * mapping). For example, to either create or append a {@code + * String msg} to a value mapping: + * + *
 {@code
+     * map.compute(key, (k, v) -> (v == null) ? msg : v.concat(msg))}
+ * (Method {@link #merge} is often simpler to use for such purposes.) + * + *

If the function returns {@code null}, the mapping is removed (or + * remains absent if initially absent). If the function itself throws an + * (unchecked) exception, the exception is rethrown, and the current mapping + * is left unchanged. + * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method or the + * application of the remapping function. Any class overriding + * this method must specify its concurrency properties. In + * particular, all implementations of subinterface {@link + * java.util.concurrent.ConcurrentMap} must document whether the + * function is applied exactly once atomically. Any class that + * permits null values must document whether and how this method + * distinguishes absence from null mappings. + * + * @implSpec + * The default implementation is equivalent to + * performing the following steps for this {@code map}, then + * returning the current value or {@code null} if absent: + * + *

 {@code
+     * V oldValue = map.get(key);
+     * V newValue = remappingFunction.apply(key, oldValue);
+     * if (newValue != null)
+     *   map.replace(key, oldValue, newValue);
+     * else
+     *   map.remove(key, oldValue);
+     * }
+ * + * In concurrent contexts, the default implementation may retry + * these steps when multiple threads attempt updates. + * + * @param key key with which the specified value is to be associated + * @param remappingFunction the function to compute a value + * @return the new value associated with the specified key, or null if none + * @throws NullPointerException if the specified key is null and + * this map does not support null keys, or the + * remappingFunction is null + * @throws UnsupportedOperationException if the put operation + * is not supported by this map + * (optional) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * (optional) + * @since 1.8 + */ + default V compute(K key, + BiFunction remappingFunction) { + V oldValue = get(key); + for (;;) { + V newValue = remappingFunction.apply(key, oldValue); + if (oldValue != null) { + if (newValue != null) { + if (replace(key, oldValue, newValue)) + return newValue; + } + else if (remove(key, oldValue)) { + return null; + } + oldValue = get(key); + } + else { + if (newValue != null) { + if ((oldValue = putIfAbsent(key, newValue)) == null) + return newValue; + } + else + return null; + } + } + } + + /** + * If the specified key is not already associated with a + * (non-null) value, associates it with the given value. + * Otherwise, replaces the value with the results of the given + * remapping function, or removes if {@code null}. This method may + * be of use when combining multiple mapped values for a key. For + * example. to either create or append a {@code String msg} to a + * value mapping: + * + *
 {@code
+     * map.merge(key, msg, String::concat)}
+ * + *

If the function returns {@code null}, the mapping is removed (or + * remains absent if initially absent). If the function itself throws an + * (unchecked) exception, the exception is rethrown, and the current mapping + * is left unchanged. + * + *

The default implementation makes no guarantees about + * synchronization or atomicity properties of this method or the + * application of the remapping function. Any class overriding + * this method must specify its concurrency properties. In + * particular, all implementations of subinterface {@link + * java.util.concurrent.ConcurrentMap} must document whether the + * function is applied exactly once atomically. Any class that + * permits null values must document whether and how this method + * distinguishes absence from null mappings. + * + * @implSpec + * The default implementation is equivalent to performing the + * following steps for this {@code map}, then returning the + * current value or {@code null} if absent: + * + *

 {@code
+     * V oldValue = map.get(key);
+     * V newValue = (oldValue == null) ? value :
+     *              remappingFunction.apply(oldValue, value);
+     * if (newValue == null)
+     *   map.remove(key, oldValue);
+     * else if (oldValue == null)
+     *   map.putIfAbsent(key, newValue);
+     * else
+     *   map.replace(key, oldValue, newValue);
+     * }
+ * + * In concurrent contexts, the default implementation may retry + * these steps when multiple threads attempt updates. + * + * @param key key with which the specified value is to be associated + * @param value the value to use if absent + * @param remappingFunction the function to recompute a value if present + * @return the new value associated with the specified key, or null if none + * @throws UnsupportedOperationException if the put operation + * is not supported by this map + * (optional) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * (optional) + * @throws NullPointerException if the specified key is null and + * this map does not support null keys, or the + * remappingFunction is null + * @since 1.8 + */ + default V merge(K key, V value, + BiFunction remappingFunction) { + V oldValue = get(key); + for (;;) { + if (oldValue != null) { + V newValue = remappingFunction.apply(oldValue, value); + if (newValue != null) { + if (replace(key, oldValue, newValue)) + return newValue; + } + else if (remove(key, oldValue)) { + return null; + } + oldValue = get(key); + } + else { + if (value != null) { + if ((oldValue = putIfAbsent(key, value)) == null) + return value; + } + else + return null; + } + } + } } diff --git a/test/java/util/Map/ToArray.java b/test/java/util/Map/Defaults.java copy from test/java/util/Map/ToArray.java copy to test/java/util/Map/Defaults.java --- a/test/java/util/Map/ToArray.java +++ b/test/java/util/Map/Defaults.java @@ -23,165 +23,495 @@ /* * @test - * @bug 8008785 - * @summary Ensure toArray() implementations return correct results. + * @bug 8010122 8004518 + * @summary Test Map default methods * @author Mike Duigou + * @run testng Defaults */ -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.function.Supplier; -public class ToArray { +import org.testng.annotations.Test; +import org.testng.annotations.DataProvider; +import static org.testng.Assert.fail; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertSame; +public class Defaults { + + @Test(dataProvider = "Nulls Map") + public void testGetOrDefaultNulls(String description, Map map) { + assertTrue(map.containsKey(null), "null key absent"); + assertNull(map.get(null), "value not null"); + assertSame(map.get(null), map.getOrDefault(null, EXTRA_VALUE), "values should match"); + } + + @Test(dataProvider = "Map") + public void testGetOrDefault(String description, Map map) { + assertTrue(map.containsKey(KEYS[1]), "expected key missing"); + assertSame(map.get(KEYS[1]), map.getOrDefault(KEYS[1], EXTRA_VALUE), "values should match"); + assertFalse(map.containsKey(EXTRA_KEY), "expected absent key"); + assertSame(map.getOrDefault(EXTRA_KEY, EXTRA_VALUE), EXTRA_VALUE, "value not returned as default"); + assertNull(map.getOrDefault(EXTRA_KEY, null), "null not returned as default"); + } + + @Test(dataProvider = "R/W Nulls Map") + public void testPutIfAbsentNulls(String description, Map map) { + assertTrue(map.containsKey(null), "null key absent"); + assertNull(map.get(null), "value not null"); + assertNull(map.putIfAbsent(null, EXTRA_VALUE), "previous not null"); + assertTrue(map.containsKey(null), "null key absent"); + assertSame(map.get(null), EXTRA_VALUE, "unexpected value"); + assertSame(map.putIfAbsent(null, null), EXTRA_VALUE, "previous not expected value"); + assertTrue(map.containsKey(null), "null key absent"); + assertSame(map.get(null), EXTRA_VALUE, "unexpected value"); + assertSame(map.remove(null), EXTRA_VALUE, "removed unexpected value"); + + assertFalse(map.containsKey(null), description + ": key present after remove"); + assertNull(map.putIfAbsent(null, null), "previous not null"); + assertTrue(map.containsKey(null), "null key absent"); + assertNull(map.get(null), "value not null"); + assertNull(map.putIfAbsent(null, EXTRA_VALUE), "previous not null"); + assertSame(map.get(null), EXTRA_VALUE, "value not expected"); + } + + @Test(dataProvider = "R/W Map") + public void testPutIfAbsent(String description, Map map) { + assertTrue(map.containsKey(KEYS[1])); + Object expected = map.get(KEYS[1]); + assertTrue(null == expected || expected == VALUES[1]); + assertSame(map.putIfAbsent(KEYS[1], EXTRA_VALUE), expected); + assertSame(map.get(KEYS[1]), expected); + + assertFalse(map.containsKey(EXTRA_KEY)); + assertSame(map.putIfAbsent(EXTRA_KEY, EXTRA_VALUE), null); + assertSame(map.get(EXTRA_KEY), EXTRA_VALUE); + } + + @Test(dataProvider = "Nulls Map") + public void testForEach(String description, Map map) { + IntegerEnum[] EACH_KEY = new IntegerEnum[map.size()]; + + map.forEach((k, v) -> { + int idx = (null == k) ? 0 : k.ordinal(); // substitute for index. + assertNull(EACH_KEY[idx]); + EACH_KEY[idx] = (idx == 0) ? KEYS[0] : k; // substitute for comparison. + assertSame(v, map.get(k)); + }); + + assertEquals(KEYS, EACH_KEY); + } + + @Test(dataProvider = "R/W Nulls Map") + public static void testRemoveNulls(String description, Map map) { + assertTrue(map.containsKey(null), "null key absent"); + assertNull(map.get(null), "value not null"); + assertFalse(map.remove(null, EXTRA_VALUE), description); + assertTrue(map.containsKey(null)); + assertNull(map.get(null)); + assertTrue(map.remove(null, null)); + assertFalse(map.containsKey(null)); + assertNull(map.get(null)); + assertFalse(map.remove(null, null)); + } + + @Test(dataProvider = "R/W Map") + public static void testRemove(String description, Map map) { + assertTrue(map.containsKey(KEYS[1])); + Object expected = map.get(KEYS[1]); + assertTrue(null == expected || expected == VALUES[1]); + assertFalse(map.remove(KEYS[1], EXTRA_VALUE), description); + assertSame(map.get(KEYS[1]), expected); + assertTrue(map.remove(KEYS[1], expected)); + assertNull(map.get(KEYS[1])); + assertFalse(map.remove(KEYS[1], expected)); + + assertFalse(map.containsKey(EXTRA_KEY)); + assertFalse(map.remove(EXTRA_KEY, EXTRA_VALUE)); + } + + @Test(dataProvider = "R/W Nulls Map") + public void testReplaceKVNulls(String description, Map map) { + assertTrue(map.containsKey(null), "null key absent"); + assertNull(map.get(null), "value not null"); + assertSame(map.replace(null, EXTRA_VALUE), null); + assertSame(map.get(null), EXTRA_VALUE); + } + + @Test(dataProvider = "R/W Map") + public void testReplaceKV(String description, Map map) { + assertTrue(map.containsKey(KEYS[1])); + Object expected = map.get(KEYS[1]); + assertTrue(null == expected || expected == VALUES[1]); + assertSame(map.replace(KEYS[1], EXTRA_VALUE), expected); + assertSame(map.get(KEYS[1]), EXTRA_VALUE); + + assertFalse(map.containsKey(EXTRA_KEY)); + assertNull(map.replace(EXTRA_KEY, EXTRA_VALUE)); + assertFalse(map.containsKey(EXTRA_KEY)); + assertNull(map.get(EXTRA_KEY)); + assertNull(map.put(EXTRA_KEY, EXTRA_VALUE)); + assertSame(map.get(EXTRA_KEY), EXTRA_VALUE); + assertSame(map.replace(EXTRA_KEY, (String)expected), EXTRA_VALUE); + assertSame(map.get(EXTRA_KEY), expected); + } + + @Test(dataProvider = "R/W Nulls Map") + public void testReplaceKVVNulls(String description, Map map) { + assertTrue(map.containsKey(null), "null key absent"); + assertNull(map.get(null), "value not null"); + assertFalse(map.replace(null, EXTRA_VALUE, EXTRA_VALUE)); + assertNull(map.get(null)); + assertTrue(map.replace(null, null, EXTRA_VALUE)); + assertSame(map.get(null), EXTRA_VALUE); + assertTrue(map.replace(null, EXTRA_VALUE, EXTRA_VALUE)); + assertSame(map.get(null), EXTRA_VALUE); + } + + @Test(dataProvider = "R/W Map") + public void testReplaceKVV(String description, Map map) { + assertTrue(map.containsKey(KEYS[1])); + Object expected = map.get(KEYS[1]); + assertTrue(null == expected || expected == VALUES[1]); + assertFalse(map.replace(KEYS[1], EXTRA_VALUE, EXTRA_VALUE)); + assertSame(map.get(KEYS[1]), expected); + assertTrue(map.replace(KEYS[1], (String)expected, EXTRA_VALUE)); + assertSame(map.get(KEYS[1]), EXTRA_VALUE); + assertTrue(map.replace(KEYS[1], EXTRA_VALUE, EXTRA_VALUE)); + assertSame(map.get(KEYS[1]), EXTRA_VALUE); + + assertFalse(map.containsKey(EXTRA_KEY)); + assertFalse(map.replace(EXTRA_KEY, EXTRA_VALUE, EXTRA_VALUE)); + assertFalse(map.containsKey(EXTRA_KEY)); + assertNull(map.get(EXTRA_KEY)); + assertNull(map.put(EXTRA_KEY, EXTRA_VALUE)); + assertTrue(map.containsKey(EXTRA_KEY)); + assertSame(map.get(EXTRA_KEY), EXTRA_VALUE); + assertTrue(map.replace(EXTRA_KEY, EXTRA_VALUE, EXTRA_VALUE)); + assertSame(map.get(EXTRA_KEY), EXTRA_VALUE); + } + + @Test(dataProvider = "R/W Nulls Map") + public void testComputeIfAbsentNulls(String description, Map map) { + assertTrue(map.containsKey(null), "null key absent"); + assertNull(map.get(null), "value not null"); + assertSame(map.computeIfAbsent(null, (k) -> EXTRA_VALUE), EXTRA_VALUE, description); + assertSame(map.get(null), EXTRA_VALUE, description); + } + + @Test(dataProvider = "R/W Map") + public void testComputeIfAbsent(String description, Map map) { + assertTrue(map.containsKey(KEYS[1])); + Object expected = map.get(KEYS[1]); + assertTrue(null == expected || expected == VALUES[1], description + String.valueOf(expected)); + expected = (null == expected) ? EXTRA_VALUE : expected; + assertSame(map.computeIfAbsent(KEYS[1], (k) -> EXTRA_VALUE), expected, description); + assertSame(map.get(KEYS[1]), expected, description); + + assertFalse(map.containsKey(EXTRA_KEY)); + assertSame(map.computeIfAbsent(EXTRA_KEY, (k) -> EXTRA_VALUE), EXTRA_VALUE); + assertSame(map.get(EXTRA_KEY), EXTRA_VALUE); + } + + @Test(dataProvider = "R/W Nulls Map") + public void testComputeIfPresentNulls(String description, Map map) { + assertTrue(map.containsKey(null)); + assertNull(map.get(null)); + assertSame(map.computeIfPresent(null, (k, v) -> { + fail(); + return EXTRA_VALUE; + }), null, description); + assertTrue(map.containsKey(null)); + assertSame(map.get(null), null, description); + } + + @Test(dataProvider = "R/W Map") + public void testComputeIfPresent(String description, Map map) { + assertTrue(map.containsKey(KEYS[1])); + Object value = map.get(KEYS[1]); + assertTrue(null == value || value == VALUES[1], description + String.valueOf(value)); + Object expected = (null == value) ? null : EXTRA_VALUE; + assertSame(map.computeIfPresent(KEYS[1], (k, v) -> { + assertSame(v, value); + return EXTRA_VALUE; + }), expected, description); + assertSame(map.get(KEYS[1]), expected, description); + + assertFalse(map.containsKey(EXTRA_KEY)); + assertSame(map.computeIfPresent(EXTRA_KEY, (k, v) -> { + fail(); + return EXTRA_VALUE; + }), null); + assertFalse(map.containsKey(EXTRA_KEY)); + assertSame(map.get(EXTRA_KEY), null); + } + + @Test(dataProvider = "R/W Nulls Map") + public void testComputeNulls(String description, Map map) { + assertTrue(map.containsKey(null), "null key absent"); + assertNull(map.get(null), "value not null"); + assertSame(map.compute(null, (k, v) -> { + assertSame(k, null); + assertNull(v); + return EXTRA_VALUE; + }), EXTRA_VALUE, description); + assertTrue(map.containsKey(null)); + assertSame(map.get(null), EXTRA_VALUE, description); + } + + @Test(dataProvider = "R/W Map") + public void testCompute(String description, Map map) { + assertTrue(map.containsKey(KEYS[1])); + Object value = map.get(KEYS[1]); + assertTrue(null == value || value == VALUES[1], description + String.valueOf(value)); + assertSame(map.compute(KEYS[1], (k, v) -> { + assertSame(k, KEYS[1]); + assertSame(v, value); + return EXTRA_VALUE; + }), EXTRA_VALUE, description); + assertSame(map.get(KEYS[1]), EXTRA_VALUE, description); + assertNull(map.compute(KEYS[1], (k, v) -> { + assertSame(v, EXTRA_VALUE); + return null; + }), description); + assertFalse(map.containsKey(KEYS[1])); + + assertFalse(map.containsKey(EXTRA_KEY)); + assertSame(map.compute(EXTRA_KEY, (k, v) -> { + assertNull(v); + return EXTRA_VALUE; + }), EXTRA_VALUE); + assertTrue(map.containsKey(EXTRA_KEY)); + assertSame(map.get(EXTRA_KEY), EXTRA_VALUE); + } + + + @Test(dataProvider = "R/W Nulls Map") + public void testMergeNulls(String description, Map map) { + assertTrue(map.containsKey(null), "null key absent"); + assertNull(map.get(null), "value not null"); + assertSame(map.merge(null, EXTRA_VALUE, (v, vv) -> { + assertNull(v); + assertSame(vv, EXTRA_VALUE); + return vv; + }), EXTRA_VALUE, description); + assertTrue(map.containsKey(null)); + assertSame(map.get(null), EXTRA_VALUE, description); + } + + @Test(dataProvider = "R/W Map") + public void testMerge(String description, Map map) { + assertTrue(map.containsKey(KEYS[1])); + Object value = map.get(KEYS[1]); + assertTrue(null == value || value == VALUES[1], description + String.valueOf(value)); + assertSame(map.merge(KEYS[1], EXTRA_VALUE, (v, vv) -> { + assertSame(v, value); + assertSame(vv, EXTRA_VALUE); + return vv; + }), EXTRA_VALUE, description); + assertSame(map.get(KEYS[1]), EXTRA_VALUE, description); + assertNull(map.merge(KEYS[1], EXTRA_VALUE, (v, vv) -> { + assertSame(v, EXTRA_VALUE); + assertSame(vv, EXTRA_VALUE); + return null; + }), description); + assertFalse(map.containsKey(KEYS[1])); + + assertFalse(map.containsKey(EXTRA_KEY)); + assertSame(map.merge(EXTRA_KEY, EXTRA_VALUE, (v, vv) -> { + assertNull(v); + assertSame(vv, EXTRA_VALUE); + return EXTRA_VALUE; + }), EXTRA_VALUE); + assertTrue(map.containsKey(EXTRA_KEY)); + assertSame(map.get(EXTRA_KEY), EXTRA_VALUE); + } + + enum IntegerEnum { + + e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, + e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, + e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, + e30, e31, e32, e33, e34, e35, e36, e37, e38, e39, + e40, e41, e42, e43, e44, e45, e46, e47, e48, e49, + e50, e51, e52, e53, e54, e55, e56, e57, e58, e59, + e60, e61, e62, e63, e64, e65, e66, e67, e68, e69, + e70, e71, e72, e73, e74, e75, e76, e77, e78, e79, + e80, e81, e82, e83, e84, e85, e86, e87, e88, e89, + e90, e91, e92, e93, e94, e95, e96, e97, e98, e99, + EXTRA_KEY; + public static final int SIZE = values().length; + }; + private static final int TEST_SIZE = IntegerEnum.SIZE - 1; /** - * Number of elements per map. + * Realized keys ensure that there is always a hard ref to all test objects. */ - private static final int TEST_SIZE = 5000; + private static final IntegerEnum[] KEYS = new IntegerEnum[TEST_SIZE]; + /** + * Realized values ensure that there is always a hard ref to all test + * objects. + */ + private static final String[] VALUES = new String[TEST_SIZE]; - private static void realMain(String[] args) throws Throwable { - Map[] maps = (Map[]) new Map[]{ - new HashMap<>(), - new Hashtable<>(), - new IdentityHashMap<>(), - new LinkedHashMap<>(), - new TreeMap<>(), - new WeakHashMap<>(), - new ConcurrentHashMap<>(), - new ConcurrentSkipListMap<>() - }; - - // for each map type. - for (Map map : maps) { - try { - testMap(map); - } catch(Exception all) { - unexpected("Failed for " + map.getClass().getName(), all); - } + static { + IntegerEnum[] keys = IntegerEnum.values(); + for (int each = 0; each < TEST_SIZE; each++) { + KEYS[each] = keys[each]; + VALUES[each] = String.valueOf(each); } } + private static final IntegerEnum EXTRA_KEY = IntegerEnum.EXTRA_KEY; + private static final String EXTRA_VALUE = String.valueOf(TEST_SIZE); - private static final Integer[] KEYS = new Integer[TEST_SIZE]; - - private static final Long[] VALUES = new Long[TEST_SIZE]; - - static { - for (int each = 0; each < TEST_SIZE; each++) { - KEYS[each] = Integer.valueOf(each); - VALUES[each] = Long.valueOf(each + TEST_SIZE); - } + @DataProvider(name = "Map", parallel = true) + public static Iterator allNullsMapProvider() { + return makeAllMaps().iterator(); } + @DataProvider(name = "Nulls Map", parallel = true) + public static Iterator allMapProvider() { + return makeRWMaps(true).iterator(); + } - private static void testMap(Map map) { - System.out.println("Testing " + map.getClass()); - System.out.flush(); + @DataProvider(name = "R/W Map", parallel = true) + public static Iterator rwMapProvider() { + return makeRWMapsNoNulls().iterator(); + } - // Fill the map + @DataProvider(name = "R/W Nulls Map", parallel = true) + public static Iterator rwNullsMapProvider() { + return makeRWMaps(true).iterator(); + } + + private static Collection makeAllMapsNoNulls() { + Collection all = new ArrayList<>(); + + all.addAll(makeRWMaps(false)); + all.addAll(makeRWNoNullsMaps()); + all.addAll(makeROMaps(false)); + + return all; + } + + private static Collection makeRWMapsNoNulls() { + Collection all = new ArrayList<>(); + + all.addAll(makeRWMaps(false)); + all.addAll(makeRWNoNullsMaps()); + + return all; + } + + private static Collection makeAllMaps() { + Collection all = new ArrayList<>(); + + all.addAll(makeROMaps(false)); + all.addAll(makeRWMaps(false)); + all.addAll(makeRWNoNullsMaps()); + all.addAll(makeRWMaps(true)); + all.addAll(makeROMaps(true)); + + return all; + } + + private static Collection makeAllRWMaps() { + Collection all = new ArrayList<>(); + + all.addAll(makeRWMaps(false)); + all.addAll(makeRWNoNullsMaps()); + all.addAll(makeRWMaps(true)); + + return all; + } + + private static Collection makeRWMaps(boolean nulls) { + return Arrays.asList( + new Object[]{"HashMap", makeMap(HashMap::new, nulls)}, + new Object[]{"IdentityHashMap", makeMap(IdentityHashMap::new, nulls)}, + new Object[]{"LinkedHashMap", makeMap(LinkedHashMap::new, nulls)}, + new Object[]{"WeakHashMap", makeMap(WeakHashMap::new, nulls)}, + new Object[]{"Collections.checkedMap(HashMap)", Collections.checkedMap(makeMap(HashMap::new, nulls), IntegerEnum.class, String.class)}, + new Object[]{"Collections.synchronizedMap(HashMap)", Collections.synchronizedMap(makeMap(HashMap::new, nulls))}); + } + + private static Collection makeRWNoNullsMaps() { + return Arrays.asList( + // null hostile + new Object[]{"EnumMap", makeMap(() -> new EnumMap(IntegerEnum.class), false)}, + new Object[]{"Hashtable", makeMap(Hashtable::new, false)}, + new Object[]{"TreeMap", makeMap(TreeMap::new, false)}, + new Object[]{"ConcurrentHashMap", makeMap(ConcurrentHashMap::new, false)}, + new Object[]{"ConcurrentSkipListMap", makeMap(ConcurrentSkipListMap::new, false)}, + new Object[]{"Collections.checkedMap(ConcurrentHashMap)", Collections.checkedMap(makeMap(ConcurrentHashMap::new, false), IntegerEnum.class, String.class)}, + new Object[]{"Collections.synchronizedMap(EnumMap)", Collections.synchronizedMap(makeMap(() -> new EnumMap(IntegerEnum.class), false))}); + } + + private static Collection makeROMaps(boolean nulls) { + return Arrays.asList(new Object[][]{ + new Object[]{"Collections.unmodifiableMap(HashMap)", Collections.unmodifiableMap(makeMap(HashMap::new, nulls))} + }); + } + + private static Map makeMap(Supplier> supplier, boolean nulls) { + Map result = supplier.get(); + for (int each = 0; each < TEST_SIZE; each++) { - map.put(KEYS[each], VALUES[each]); + if (nulls) { + result.put((each == 0) ? null : KEYS[each], null); + } else { + result.put(KEYS[each], VALUES[each]); + } } - // check the keys - Object[] keys = map.keySet().toArray(); - Arrays.sort(keys); + return result; + } - for(int each = 0; each < TEST_SIZE; each++) { - check( "unexpected key", keys[each] == KEYS[each]); + public interface Thrower { + + public void run() throws T; + } + + public static void assertThrows(Thrower thrower, Class throwable) { + assertThrows(thrower, throwable, null); + } + + public static void assertThrows(Thrower thrower, Class throwable, String message) { + Throwable result; + try { + thrower.run(); + result = null; + } catch (Throwable caught) { + result = caught; } - // check the values - Object[] values = map.values().toArray(); - Arrays.sort(values); - - for(int each = 0; each < TEST_SIZE; each++) { - check( "unexpected value", values[each] == VALUES[each]); - } - - // check the entries - Map.Entry[] entries = map.entrySet().toArray(new Map.Entry[TEST_SIZE]); - Arrays.sort( entries,new Comparator>() { - public int compare(Map.Entry o1, Map.Entry o2) { - return o1.getKey().compareTo(o2.getKey()); - }}); - - for(int each = 0; each < TEST_SIZE; each++) { - check( "unexpected entry", entries[each].getKey() == KEYS[each] && entries[each].getValue() == VALUES[each]); - } + assertInstance(result, throwable, + (null != message) + ? message + : "Failed to throw " + throwable.getCanonicalName()); } - //--------------------- Infrastructure --------------------------- - static volatile int passed = 0, failed = 0; - - static void pass() { - passed++; + public static void assertInstance(T actual, Class expected) { + assertInstance(expected.isInstance(actual), null); } - static void fail() { - failed++; - (new Error("Failure")).printStackTrace(System.err); - } - - static void fail(String msg) { - failed++; - (new Error("Failure: " + msg)).printStackTrace(System.err); - } - - static void abort() { - fail(); - System.exit(1); - } - - static void abort(String msg) { - fail(msg); - System.exit(1); - } - - static void unexpected(String msg, Throwable t) { - System.err.println("Unexpected: " + msg); - unexpected(t); - } - - static void unexpected(Throwable t) { - failed++; - t.printStackTrace(System.err); - } - - static void check(boolean cond) { - if (cond) { - pass(); - } else { - fail(); - } - } - - static void check(String desc, boolean cond) { - if (cond) { - pass(); - } else { - fail(desc); - } - } - - static void equal(Object x, Object y) { - if (Objects.equals(x, y)) { - pass(); - } else { - fail(x + " not equal to " + y); - } - } - - public static void main(String[] args) throws Throwable { - Thread.currentThread().setName(ToArray.class.getName()); -// Thread.currentThread().setPriority(Thread.MAX_PRIORITY); - try { - realMain(args); - } catch (Throwable t) { - unexpected(t); - } - - System.out.printf("%nPassed = %d, failed = %d%n%n", passed, failed); - if (failed > 0) { - throw new Error("Some tests failed"); - } + public static void assertInstance(T actual, Class expected, String message) { + assertTrue(expected.isInstance(actual), message); } }