From eed42e33b0b92f8b2a4acff92618c275caae1e14 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Mon, 16 Mar 2020 18:33:25 +0300 Subject: [PATCH] classlib: implement ConcurrentHashMap --- .../classlib/java/util/TAbstractMap.java | 10 +- .../teavm/classlib/java/util/THashMap.java | 10 +- .../classlib/java/util/TLinkedHashMap.java | 9 - .../org/teavm/classlib/java/util/TMap.java | 10 +- .../util/concurrent/TConcurrentHashMap.java | 807 ++++++++++++++++++ .../java/util/concurrent/TConcurrentMap.java | 172 ++++ .../java/util/concurrent/TMapEntry.java | 87 ++ .../classlib/java/util/LinkedHashMapTest.java | 341 ++++---- .../concurrent/ConcurrentHashMapTest.java | 309 +++++++ .../MultiThreadConcurrentHashMapTest.java | 174 ++++ 10 files changed, 1716 insertions(+), 213 deletions(-) create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TConcurrentHashMap.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TConcurrentMap.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TMapEntry.java create mode 100644 tests/src/test/java/org/teavm/classlib/java/util/concurrent/ConcurrentHashMapTest.java create mode 100644 tests/src/test/java/org/teavm/classlib/java/util/concurrent/MultiThreadConcurrentHashMapTest.java diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TAbstractMap.java b/classlib/src/main/java/org/teavm/classlib/java/util/TAbstractMap.java index 6bb439399..18224e0c7 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/TAbstractMap.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TAbstractMap.java @@ -20,12 +20,6 @@ import org.teavm.classlib.java.lang.TCloneNotSupportedException; import org.teavm.classlib.java.lang.TObject; import org.teavm.classlib.java.lang.TUnsupportedOperationException; -/** - * - * @author Alexey Andreev - * @param - * @param - */ public abstract class TAbstractMap extends TObject implements TMap { public static class SimpleEntry implements TMap.Entry, TSerializable { private K key; @@ -131,8 +125,8 @@ public abstract class TAbstractMap extends TObject implements TMap { } } - TSet cachedKeySet; - TCollection cachedValues; + transient TSet cachedKeySet; + transient TCollection cachedValues; protected TAbstractMap() { } diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/THashMap.java b/classlib/src/main/java/org/teavm/classlib/java/util/THashMap.java index 3d1794518..5b4dbda68 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/THashMap.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/THashMap.java @@ -404,8 +404,7 @@ public class THashMap extends TAbstractMap implements TCloneable, TS final HashEntry findNonNullKeyEntry(Object key, int index, int keyHash) { HashEntry m = elementData[index]; - while (m != null - && (m.origKeyHash != keyHash || !areEqualKeys(key, m.key))) { + while (m != null && (m.origKeyHash != keyHash || !areEqualKeys(key, m.key))) { m = m.next; } return m; @@ -498,13 +497,6 @@ public class THashMap extends TAbstractMap implements TCloneable, TS return result; } - HashEntry createEntry(K key, int index, V value) { - HashEntry entry = new HashEntry<>(key, value); - entry.next = elementData[index]; - elementData[index] = entry; - return entry; - } - HashEntry createHashedEntry(K key, int index, int hash) { HashEntry entry = new HashEntry<>(key, hash); entry.next = elementData[index]; diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TLinkedHashMap.java b/classlib/src/main/java/org/teavm/classlib/java/util/TLinkedHashMap.java index 65934b337..13302bb8b 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/TLinkedHashMap.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TLinkedHashMap.java @@ -284,15 +284,6 @@ public class TLinkedHashMap extends THashMap implements TMap { return m.value; } - @Override - HashEntry createEntry(K key, int index, V value) { - LinkedHashMapEntry m = new LinkedHashMapEntry<>(key, value); - m.next = elementData[index]; - elementData[index] = m; - linkEntry(m); - return m; - } - @Override HashEntry createHashedEntry(K key, int index, int hash) { LinkedHashMapEntry m = new LinkedHashMapEntry<>(key, hash); diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TMap.java b/classlib/src/main/java/org/teavm/classlib/java/util/TMap.java index 0776d2d2b..7335634a8 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/TMap.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TMap.java @@ -56,7 +56,7 @@ public interface TMap { V get(Object key); - default V getOrDefault(K key, V defaultValue) { + default V getOrDefault(Object key, V defaultValue) { return containsKey(key) ? get(key) : defaultValue; } @@ -109,6 +109,7 @@ public interface TMap { } default V computeIfAbsent(K key, Function mappingFunction) { + Objects.requireNonNull(mappingFunction); V v = get(key); if (v == null) { V newValue = mappingFunction.apply(key); @@ -121,6 +122,7 @@ public interface TMap { } default V computeIfPresent(K key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); V v = get(key); if (v != null) { V oldValue = v; @@ -136,6 +138,7 @@ public interface TMap { } default V compute(K key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); V oldValue = get(key); V newValue = remappingFunction.apply(key, oldValue); if (oldValue != null) { @@ -151,9 +154,9 @@ public interface TMap { } default V merge(K key, V value, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); V oldValue = get(key); - V newValue = (oldValue == null) ? value - : remappingFunction.apply(oldValue, value); + V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value); if (newValue == null) { remove(key); } else { @@ -163,6 +166,7 @@ public interface TMap { } default void forEach(BiConsumer action) { + Objects.requireNonNull(action); final TIterator> iterator = entrySet().iterator(); while (iterator.hasNext()) { final Entry entry = iterator.next(); diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TConcurrentHashMap.java b/classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TConcurrentHashMap.java new file mode 100644 index 000000000..8a54d18c0 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TConcurrentHashMap.java @@ -0,0 +1,807 @@ +/* + * Copyright 2020 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.teavm.classlib.java.util.concurrent; + +import java.util.Arrays; +import java.util.ConcurrentModificationException; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.teavm.classlib.java.io.TSerializable; +import org.teavm.classlib.java.lang.TCloneNotSupportedException; +import org.teavm.classlib.java.lang.TCloneable; +import org.teavm.classlib.java.lang.TIllegalArgumentException; +import org.teavm.classlib.java.lang.TIllegalStateException; +import org.teavm.classlib.java.lang.TObject; +import org.teavm.classlib.java.util.TAbstractCollection; +import org.teavm.classlib.java.util.TAbstractMap; +import org.teavm.classlib.java.util.TAbstractSet; +import org.teavm.classlib.java.util.TCollection; +import org.teavm.classlib.java.util.TConcurrentModificationException; +import org.teavm.classlib.java.util.TIterator; +import org.teavm.classlib.java.util.TMap; +import org.teavm.classlib.java.util.TNoSuchElementException; +import org.teavm.classlib.java.util.TSet; +import org.teavm.interop.Rename; + +public class TConcurrentHashMap extends TAbstractMap + implements TConcurrentMap, TCloneable, TSerializable { + private transient int elementCount; + private transient HashEntry[] elementData; + private transient int modCount; + private static final int DEFAULT_SIZE = 16; + private final float loadFactor; + private int threshold; + private transient TSet cachedKeySet; + private transient TCollection cachedValues; + + static class HashEntry extends TMapEntry { + final int origKeyHash; + HashEntry next; + boolean removed; + + HashEntry(K theKey, int hash) { + super(theKey, null); + this.origKeyHash = hash; + } + + @Override + @SuppressWarnings("unchecked") + public Object clone() { + HashEntry entry = (HashEntry) super.clone(); + if (next != null) { + entry.next = (HashEntry) next.clone(); + } + return entry; + } + } + + private static class AbstractMapIterator { + private int position; + int expectedModCount; + HashEntry futureEntry; + HashEntry currentEntry; + HashEntry prevEntry; + + final TConcurrentHashMap associatedMap; + + AbstractMapIterator(TConcurrentHashMap hm) { + associatedMap = hm; + expectedModCount = hm.modCount; + futureEntry = null; + } + + public boolean hasNext() { + if (futureEntry != null) { + return true; + } + while (position < associatedMap.elementData.length) { + if (associatedMap.elementData[position] == null) { + position++; + } else { + return true; + } + } + return false; + } + + final void checkConcurrentMod() throws ConcurrentModificationException { + if (expectedModCount != associatedMap.modCount) { + throw new TConcurrentModificationException(); + } + } + + final void makeNext() { + checkConcurrentMod(); + if (!hasNext()) { + throw new TNoSuchElementException(); + } + if (futureEntry == null) { + currentEntry = associatedMap.elementData[position++]; + futureEntry = currentEntry.next; + prevEntry = null; + } else { + if (currentEntry != null) { + prevEntry = currentEntry; + } + currentEntry = futureEntry; + futureEntry = futureEntry.next; + } + } + + public final void remove() { + checkConcurrentMod(); + if (currentEntry == null) { + throw new TIllegalStateException(); + } + if (prevEntry == null) { + int index = currentEntry.origKeyHash & (associatedMap.elementData.length - 1); + associatedMap.elementData[index] = associatedMap.elementData[index].next; + } else { + prevEntry.next = currentEntry.next; + } + currentEntry = null; + expectedModCount++; + associatedMap.modCount++; + associatedMap.elementCount--; + } + } + + + private static class EntryIterator extends AbstractMapIterator + implements TIterator> { + EntryIterator(TConcurrentHashMap map) { + super(map); + } + + @Override + public Entry next() { + makeNext(); + return currentEntry; + } + } + + private static class KeyIterator extends AbstractMapIterator implements TIterator { + KeyIterator(TConcurrentHashMap map) { + super(map); + } + + @Override + public K next() { + makeNext(); + return currentEntry.key; + } + } + + private static class ValueIterator extends AbstractMapIterator implements TIterator { + ValueIterator(TConcurrentHashMap map) { + super(map); + } + + @Override + public V next() { + makeNext(); + return currentEntry.value; + } + } + + static class HashMapEntrySet extends TAbstractSet> { + private final TConcurrentHashMap associatedMap; + + HashMapEntrySet(TConcurrentHashMap hm) { + associatedMap = hm; + } + + TConcurrentHashMap hashMap() { + return associatedMap; + } + + @Override + public int size() { + return associatedMap.elementCount; + } + + @Override + public void clear() { + associatedMap.clear(); + } + + @Override + public boolean remove(Object object) { + if (object instanceof TMap.Entry) { + var oEntry = (Entry) object; + var entry = associatedMap.getEntryByKeyAndValue(oEntry.getKey(), oEntry.getValue()); + if (entry != null) { + associatedMap.removeEntry(entry); + return true; + } + } + return false; + } + + @Override + public boolean contains(Object object) { + if (object instanceof TMap.Entry) { + var oEntry = (Entry) object; + return associatedMap.getEntryByKeyAndValue(oEntry.getKey(), oEntry.getValue()) != null; + } + return false; + } + + @Override + public TIterator> iterator() { + return new EntryIterator<>(associatedMap); + } + } + + @SuppressWarnings("unchecked") + HashEntry[] newElementArray(int s) { + return new HashEntry[s]; + } + + public TConcurrentHashMap() { + this(DEFAULT_SIZE); + } + + public TConcurrentHashMap(int capacity) { + this(capacity, 0.75f); // default load factor of 0.75 + } + + private static int calculateCapacity(int x) { + if (x >= 1 << 30) { + return 1 << 30; + } + if (x == 0) { + return 16; + } + x = x - 1; + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + return x + 1; + } + + public TConcurrentHashMap(int capacity, float loadFactor) { + if (capacity >= 0 && loadFactor > 0) { + capacity = calculateCapacity(capacity); + elementCount = 0; + elementData = newElementArray(capacity); + this.loadFactor = loadFactor; + computeThreshold(); + } else { + throw new TIllegalArgumentException(); + } + } + + public TConcurrentHashMap(TMap map) { + this(calculateCapacity(map.size())); + putAllImpl(map); + } + + @Override + public void clear() { + if (elementCount > 0) { + elementCount = 0; + Arrays.fill(elementData, null); + modCount++; + } + } + + @Rename("clone") + @SuppressWarnings("unchecked") + public TObject clone0() { + try { + var map = (TConcurrentHashMap) super.clone(); + map.elementCount = 0; + map.elementData = newElementArray(elementData.length); + map.putAll(this); + + return map; + } catch (TCloneNotSupportedException e) { + return null; + } + } + + private void computeThreshold() { + threshold = (int) (elementData.length * loadFactor); + } + + @Override + public boolean containsKey(Object key) { + return getEntry(key) != null; + } + + @Override + public boolean containsValue(Object value) { + repeatTable: do { + var table = elementData; + + if (value != null) { + for (var i = 0; i < table.length; i++) { + do { + var first = table[i]; + if (first == null) { + break; + } + var entry = first; + + while (entry != null) { + boolean equal = areEqualValues(value, entry.value); + if (table != elementData) { + continue repeatTable; + } + if (equal) { + return true; + } + entry = entry.next; + } + + if (first == table[i]) { + break; + } + } while (true); + } + } else { + for (var i = 0; i < elementData.length; i++) { + var entry = elementData[i]; + while (entry != null) { + if (entry.value == null) { + return true; + } + entry = entry.next; + } + } + } + return false; + } while (true); + } + + @Override + public TSet> entrySet() { + return new HashMapEntrySet<>(this); + } + + @Override + public V get(Object key) { + var m = getEntry(key); + if (m != null) { + return m.value; + } + return null; + } + + @Override + public boolean remove(Object key, Object value) { + var entry = getEntryByKeyAndValue(key, value); + if (entry != null) { + removeEntry(entry); + return true; + } + return false; + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + var entry = getEntryByKeyAndValue(key, oldValue); + if (entry != null) { + entry.setValue(newValue); + return true; + } + return false; + } + + @Override + public V replace(K key, V value) { + var entry = getEntry(key); + if (entry == null) { + return null; + } + + var result = entry.getValue(); + entry.setValue(value); + return result; + } + + @Override + public V getOrDefault(Object key, V defaultValue) { + var m = getEntry(key); + return m != null ? m.getValue() : defaultValue; + } + + @Override + public void forEach(BiConsumer action) { + Objects.requireNonNull(action); + var table = elementData; + for (var entry : table) { + while (entry != null) { + if (!entry.removed) { + action.accept(entry.getKey(), entry.getValue()); + } + entry = entry.next; + } + } + } + + @Override + public V putIfAbsent(K key, V value) { + var hash = computeHashCode(key); + var entry = getEntry(key, hash); + + if (entry != null) { + return entry.getValue(); + } + var index = computeIndex(hash); + entry = placeHashedEntry(key, index, hash); + entry.setValue(value); + return null; + } + + @Override + public void replaceAll(BiFunction function) { + Objects.requireNonNull(function); + var table = elementData; + for (var entry : table) { + while (entry != null) { + if (!entry.removed) { + var newValue = function.apply(entry.getKey(), entry.getValue()); + Objects.requireNonNull(newValue); + entry.setValue(newValue); + } + entry = entry.next; + } + } + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + Objects.requireNonNull(mappingFunction); + + var hash = computeHashCode(key); + var entry = getEntry(key, hash); + if (entry != null) { + return entry.getValue(); + } + + var newValue = mappingFunction.apply(key); + entry = getEntry(key, hash); + if (entry != null) { + return entry.getValue(); + } + var index = computeIndex(hash); + entry = placeHashedEntry(key, index, hash); + entry.setValue(newValue); + return newValue; + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + int hash = computeHashCode(key); + + V newValue = null; + var newValueComputed = false; + while (true) { + var entry = getEntry(key, hash); + if (entry == null) { + return null; + } + + var oldValue = entry.getValue(); + if (!newValueComputed) { + newValueComputed = true; + newValue = remappingFunction.apply(key, oldValue); + } + entry = getEntry(key, hash); + if (entry == null) { + return null; + } else if (entry.getValue() == oldValue) { + entry.setValue(newValue); + return newValue; + } + } + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + var hash = computeHashCode(key); + + while (true) { + var entry = getEntry(key, hash); + if (entry == null) { + var newValue = remappingFunction.apply(key, null); + if (getEntry(key, hash) == null) { + if (newValue != null) { + var index = computeIndex(hash); + entry = placeHashedEntry(key, index, hash); + entry.setValue(newValue); + } + return newValue; + } + } else { + var oldValue = entry.getValue(); + var newValue = remappingFunction.apply(key, oldValue); + entry = getEntry(key, hash); + if (entry != null && entry.getValue() == oldValue) { + if (newValue == null) { + removeEntry(entry); + } else { + entry.setValue(newValue); + } + return newValue; + } + } + } + } + + private HashEntry getEntry(Object key) { + return getEntry(key, computeHashCode(key)); + } + + private HashEntry getEntry(Object key, int hash) { + if (key == null) { + return findNullKeyEntry(); + } else { + repeatTable: + do { + var table = elementData; + int index = hash & (table.length - 1); + + repeatElement: + do { + var first = table[index]; + if (first == null) { + return null; + } + var m = first; + + while (m != null) { + if (!m.removed && m.origKeyHash == hash) { + var equal = areEqualKeys(key, m.key); + if (table != elementData) { + continue repeatTable; + } + if (equal) { + if (m.removed) { + continue repeatElement; + } + return m; + } + } + m = m.next; + } + + if (first == table[index]) { + return null; + } + } while (true); + } while (true); + } + } + + private HashEntry getEntryByKeyAndValue(Object key, Object value) { + var hash = computeHashCode(key); + repeatTable: + do { + var table = elementData; + int index = hash & (table.length - 1); + + repeatElement: + do { + var first = table[index]; + if (first == null) { + return null; + } + var m = first; + + while (m != null) { + if (m.origKeyHash == hash) { + boolean equal = key != null ? areEqualKeys(key, m.key) : m.key == null; + if (table != elementData) { + continue repeatTable; + } + if (m.removed) { + continue repeatElement; + } + if (equal) { + equal = areEqualValues(value, m.value); + if (table != elementData) { + continue repeatTable; + } + if (m.removed) { + continue repeatElement; + } + return equal ? m : null; + } + } + m = m.next; + } + + if (first == table[index]) { + return null; + } + } while (true); + } while (true); + } + + private HashEntry findNullKeyEntry() { + var m = elementData[0]; + while (m != null && m.key != null) { + m = m.next; + } + return m; + } + + @Override + public boolean isEmpty() { + return elementCount == 0; + } + + @Override + public TSet keySet() { + if (cachedKeySet == null) { + cachedKeySet = new TAbstractSet<>() { + @Override public boolean contains(Object object) { + return containsKey(object); + } + @Override public int size() { + return TConcurrentHashMap.this.size(); + } + @Override public void clear() { + TConcurrentHashMap.this.clear(); + } + @Override public boolean remove(Object key) { + HashEntry entry = TConcurrentHashMap.this.getEntry(key); + if (entry != null) { + TConcurrentHashMap.this.removeEntry(entry); + return true; + } + return false; + } + @Override public TIterator iterator() { + return new KeyIterator<>(TConcurrentHashMap.this); + } + }; + } + return cachedKeySet; + } + + @Override + public V put(K key, V value) { + return putImpl(key, value); + } + + private V putImpl(K key, V value) { + var hash = computeHashCode(key); + var entry = getEntry(key, hash); + var index = computeIndex(hash); + + if (entry == null) { + entry = placeHashedEntry(key, index, hash); + } + + V result = entry.value; + entry.value = value; + return result; + } + + private HashEntry placeHashedEntry(K key, int index, int hash) { + var entry = createHashedEntry(key, index, hash); + modCount++; + if (++elementCount > threshold) { + rehash(); + } + return entry; + } + + private HashEntry createHashedEntry(K key, int index, int hash) { + var entry = new HashEntry(key, hash); + entry.next = elementData[index]; + elementData[index] = entry; + return entry; + } + + @Override + public void putAll(TMap map) { + if (!map.isEmpty()) { + putAllImpl(map); + } + } + + private void putAllImpl(TMap map) { + int capacity = elementCount + map.size(); + if (capacity > threshold) { + rehash(capacity); + } + for (var iter = map.entrySet().iterator(); iter.hasNext();) { + var entry = iter.next(); + putImpl(entry.getKey(), entry.getValue()); + } + } + + private void rehash(int capacity) { + int length = calculateCapacity(capacity == 0 ? 1 : capacity << 1); + + var newData = newElementArray(length); + for (int i = 0; i < elementData.length; i++) { + var entry = elementData[i]; + elementData[i] = null; + while (entry != null) { + int index = entry.origKeyHash & (length - 1); + var next = entry.next; + entry.next = newData[index]; + newData[index] = entry; + entry = next; + } + } + elementData = newData; + computeThreshold(); + } + + private void rehash() { + rehash(elementData.length); + } + + @Override + public V remove(Object key) { + var entry = getEntry(key); + if (entry == null) { + return null; + } + removeEntry(entry); + return entry.value; + } + + private void removeEntry(HashEntry entry) { + var index = entry.origKeyHash & (elementData.length - 1); + var m = elementData[index]; + if (m == entry) { + elementData[index] = entry.next; + } else { + while (m.next != entry) { + m = m.next; + } + m.next = entry.next; + } + modCount++; + elementCount--; + entry.removed = true; + } + + @Override + public int size() { + return elementCount; + } + + @Override + public TCollection values() { + if (cachedValues == null) { + cachedValues = new TAbstractCollection<>() { + @Override public boolean contains(Object object) { + return containsValue(object); + } + @Override public int size() { + return TConcurrentHashMap.this.size(); + } + @Override public void clear() { + TConcurrentHashMap.this.clear(); + } + @Override public TIterator iterator() { + return new ValueIterator<>(TConcurrentHashMap.this); + } + }; + } + return cachedValues; + } + + private static int computeHashCode(Object key) { + return key == null ? 0 : key.hashCode(); + } + + private static boolean areEqualKeys(Object key1, Object key2) { + return (key1 == key2) || key1.equals(key2); + } + + private static boolean areEqualValues(Object value1, Object value2) { + return (value1 == value2) || value1.equals(value2); + } + + private int computeIndex(int hash) { + return (hash & 0x7FFFFFFF) % elementData.length; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TConcurrentMap.java b/classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TConcurrentMap.java new file mode 100644 index 000000000..41e44c249 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TConcurrentMap.java @@ -0,0 +1,172 @@ +/* + * Copyright 2020 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.util.concurrent; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.teavm.classlib.java.util.TIterator; +import org.teavm.classlib.java.util.TMap; + +public interface TConcurrentMap extends TMap { + @Override + default V getOrDefault(Object key, V defaultValue) { + V result = get(key); + return result != null ? result : defaultValue; + } + + @Override + default void forEach(BiConsumer action) { + Objects.requireNonNull(action); + TIterator> iterator = entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + K key; + V value; + try { + key = entry.getKey(); + value = entry.getValue(); + } catch (IllegalStateException e) { + continue; + } + action.accept(key, value); + } + } + + @Override + V putIfAbsent(K key, V value); + + @Override + boolean remove(Object key, Object value); + + @Override + boolean replace(K key, V oldValue, V newValue); + + @Override + V replace(K key, V value); + + @Override + default void replaceAll(BiFunction function) { + Objects.requireNonNull(function); + TIterator> iterator = entrySet().iterator(); + main: while (iterator.hasNext()) { + Entry entry = iterator.next(); + while (true) { + K key; + V value; + try { + key = entry.getKey(); + value = entry.getValue(); + } catch (IllegalStateException e) { + continue main; + } + if (replace(key, value, function.apply(key, value))) { + break; + } + } + } + } + + @Override + default V computeIfAbsent(K key, Function mappingFunction) { + Objects.requireNonNull(mappingFunction); + V oldValue = get(key); + if (oldValue == null) { + V newValue = mappingFunction.apply(key); + return newValue != null ? putIfAbsent(key, newValue) : null; + } else { + return oldValue; + } + } + + @Override + default V computeIfPresent(K key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + while (true) { + V oldValue = get(key); + if (oldValue == null) { + return null; + } + V newValue = remappingFunction.apply(key, oldValue); + if (newValue != null) { + if (replace(key, oldValue, newValue)) { + return newValue; + } + } else { + if (remove(key, oldValue)) { + return null; + } + } + } + } + + @Override + default V compute(K key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + V newValue; + while (true) { + V oldValue = get(key); + newValue = remappingFunction.apply(key, oldValue); + if (oldValue != null) { + if (newValue != null) { + if (replace(key, oldValue, newValue)) { + break; + } + } else { + if (remove(key, oldValue)) { + break; + } + } + } else if (newValue != null) { + if (putIfAbsent(key, newValue) == null) { + break; + } + } else { + break; + } + } + return newValue; + } + + @Override + default V merge(K key, V value, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + V newValue; + while (true) { + V oldValue = get(key); + newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value); + if (newValue == null) { + if (oldValue != null) { + remove(key, oldValue); + } else { + break; + } + } else { + if (oldValue == null) { + if (putIfAbsent(key, newValue) == null) { + break; + } + } else { + if (replace(key, oldValue, newValue)) { + break; + } + } + } + } + return newValue; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TMapEntry.java b/classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TMapEntry.java new file mode 100644 index 000000000..b25659e23 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/concurrent/TMapEntry.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.teavm.classlib.java.util.concurrent; + +import java.util.Objects; +import org.teavm.classlib.java.util.TMap; + +class TMapEntry implements TMap.Entry, Cloneable { + + final K key; + volatile V value; + + interface Type { + RT get(TMapEntry entry); + } + + TMapEntry(K theKey) { + key = theKey; + } + + TMapEntry(K theKey, V theValue) { + key = theKey; + value = theValue; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + return null; + } + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object instanceof TMap.Entry) { + TMap.Entry entry = (TMap.Entry) object; + return Objects.equals(key, entry.getKey()); + } + return false; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public int hashCode() { + return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); + } + + @Override + public V setValue(V object) { + V result = value; + value = object; + return result; + } + + @Override + public String toString() { + return key + "=" + value; + } +} diff --git a/tests/src/test/java/org/teavm/classlib/java/util/LinkedHashMapTest.java b/tests/src/test/java/org/teavm/classlib/java/util/LinkedHashMapTest.java index 5a07ec1d4..12e529198 100644 --- a/tests/src/test/java/org/teavm/classlib/java/util/LinkedHashMapTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/util/LinkedHashMapTest.java @@ -34,6 +34,8 @@ package org.teavm.classlib.java.util; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -41,12 +43,8 @@ import static org.junit.Assert.fail; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import java.util.TreeMap; import org.junit.Test; import org.junit.runner.RunWith; @@ -54,8 +52,8 @@ import org.teavm.classlib.support.MapTest2Support; import org.teavm.junit.TeaVMTestRunner; import org.teavm.junit.WholeClassCompilation; -@SuppressWarnings({ "UnnecessaryUnboxing", "ClassInitializerMayBeStatic", "UnnecessaryTemporaryOnConversionToString", - "MismatchedQueryAndUpdateOfCollection", "StringEquality" }) +@SuppressWarnings({ "UnnecessaryUnboxing", "ClassInitializerMayBeStatic", + "MismatchedQueryAndUpdateOfCollection" }) @RunWith(TeaVMTestRunner.class) @WholeClassCompilation public class LinkedHashMapTest { @@ -72,7 +70,7 @@ public class LinkedHashMapTest { objArray = new Object[hmSize]; objArray2 = new Object[hmSize]; for (int i = 0; i < objArray.length; i++) { - objArray[i] = new Integer(i); + objArray[i] = i; objArray2[i] = objArray[i].toString(); } } @@ -82,23 +80,21 @@ public class LinkedHashMapTest { for (int i = 0; i < objArray.length; i++) { hm.put(objArray2[i], objArray[i]); } - hm.put("org/teavm/metaprogramming/test", null); - hm.put(null, "org/teavm/metaprogramming/test"); + hm.put("test", null); + hm.put(null, "test"); } @Test public void test_Constructor() { - // Test for method java.util.LinkedHashMap() new MapTest2Support(new LinkedHashMap<>()).runTest(); - LinkedHashMap hm2 = new LinkedHashMap<>(); + var hm2 = new LinkedHashMap<>(); assertEquals("Created incorrect LinkedHashMap", 0, hm2.size()); } @Test public void test_ConstructorI() { - // Test for method java.util.LinkedHashMap(int) - LinkedHashMap hm2 = new LinkedHashMap<>(5); + var hm2 = new LinkedHashMap<>(5); assertEquals("Created incorrect LinkedHashMap", 0, hm2.size()); try { new LinkedHashMap<>(-1); @@ -107,19 +103,17 @@ public class LinkedHashMapTest { // as expected } - LinkedHashMap empty = new LinkedHashMap<>(0); + var empty = new LinkedHashMap<>(0); assertNull("Empty LinkedHashMap access", empty.get("nothing")); empty.put("something", "here"); - //CHECKSTYLE.OFF:StringLiteralEquality - assertTrue("cannot get element", empty.get("something") == "here"); - //CHECKSTYLE.ON:StringLiteralEquality + assertSame("cannot get element", "here", empty.get("something")); } @Test public void test_ConstructorIF() { // Test for method java.util.LinkedHashMap(int, float) - LinkedHashMap hm2 = new LinkedHashMap<>(5, (float) 0.5); + var hm2 = new LinkedHashMap<>(5, (float) 0.5); assertEquals("Created incorrect LinkedHashMap", 0, hm2.size()); try { new LinkedHashMap<>(0, 0); @@ -127,23 +121,21 @@ public class LinkedHashMapTest { } catch (IllegalArgumentException e) { // as expected } - LinkedHashMap empty = new LinkedHashMap<>(0, 0.75f); + var empty = new LinkedHashMap(0, 0.75f); assertNull("Empty hashtable access", empty.get("nothing")); empty.put("something", "here"); - //CHECKSTYLE.OFF:StringLiteralEquality - assertTrue("cannot get element", empty.get("something") == "here"); - //CHECKSTYLE.ON:StringLiteralEquality + assertSame("cannot get element", "here", empty.get("something")); } @Test public void test_ConstructorLjava_util_Map() { // Test for method java.util.LinkedHashMap(java.util.Map) - Map myMap = new TreeMap<>(); + var myMap = new TreeMap<>(); for (int counter = 0; counter < hmSize; counter++) { myMap.put(objArray2[counter], objArray[counter]); } - LinkedHashMap hm2 = new LinkedHashMap<>(myMap); + var hm2 = new LinkedHashMap<>(myMap); for (int counter = 0; counter < hmSize; counter++) { assertSame("Failed to construct correct LinkedHashMap", hm.get(objArray2[counter]), hm2.get(objArray2[counter])); @@ -152,46 +144,42 @@ public class LinkedHashMapTest { @Test public void test_getLjava_lang_Object() { - // Test for method java.lang.Object - // java.util.LinkedHashMap.get(java.lang.Object) assertNull("Get returned non-null for non existent key", hm.get("T")); hm.put("T", "HELLO"); - assertEquals("Get returned incorecct value for existing key", "HELLO", hm.get("T")); + assertEquals("Get returned incorrect value for existing key", "HELLO", hm.get("T")); - LinkedHashMap m = new LinkedHashMap<>(); - m.put(null, "org/teavm/metaprogramming/test"); - assertEquals("Failed with null key", "org/teavm/metaprogramming/test", m.get(null)); - assertNull("Failed with missing key matching null hash", m.get(new Integer(0))); + var m = new LinkedHashMap(); + m.put(null, "test"); + assertEquals("Failed with null key", "test", m.get(null)); + assertNull("Failed with missing key matching null hash", m.get(0)); } @Test public void test_putLjava_lang_ObjectLjava_lang_Object() { - // Test for method java.lang.Object - // java.util.LinkedHashMap.put(java.lang.Object, java.lang.Object) hm.put("KEY", "VALUE"); assertEquals("Failed to install key/value pair", "VALUE", hm.get("KEY")); - LinkedHashMap m = new LinkedHashMap<>(); - m.put(new Short((short) 0), "short"); - m.put(null, "org/teavm/metaprogramming/test"); - m.put(new Integer(0), "int"); - assertEquals("Failed adding to bucket containing null", "short", m.get(new Short((short) 0))); - assertEquals("Failed adding to bucket containing null2", "int", m.get(new Integer(0))); + var m = new LinkedHashMap(); + m.put((short) 0, "short"); + m.put(null, "test"); + m.put(0, "int"); + assertEquals("Failed adding to bucket containing null", "short", m.get((short) 0)); + assertEquals("Failed adding to bucket containing null2", "int", m.get(0)); } @Test public void test_putAllLjava_util_Map() { - LinkedHashMap hm2 = new LinkedHashMap<>(); + var hm2 = new LinkedHashMap<>(); hm2.putAll(hm); for (int i = 0; i < 1000; i++) { - assertTrue("Failed to clear all elements", hm2.get(new Integer(i).toString()).equals(new Integer(i))); + assertEquals("Failed to clear all elements", hm2.get(String.valueOf(i)), i); } } @Test public void test_entrySet() { assertEquals("Returned set of incorrect size", hm.size(), hm.entrySet().size()); - for (Entry m : hm.entrySet()) { + for (var m : hm.entrySet()) { assertTrue("Returned incorrect entry set", hm.containsKey(m.getKey()) && hm.containsValue(m.getValue())); } } @@ -199,92 +187,89 @@ public class LinkedHashMapTest { @Test public void test_keySet() { // Test for method java.util.Set java.util.LinkedHashMap.keySet() - Set s = hm.keySet(); + var s = hm.keySet(); assertEquals("Returned set of incorrect size()", s.size(), hm.size()); for (int i = 0; i < objArray.length; i++) { assertTrue("Returned set does not contain all keys", s.contains(objArray[i].toString())); } - LinkedHashMap m = new LinkedHashMap<>(); + var m = new LinkedHashMap(); m.put(null, "org/teavm/metaprogramming/test"); assertTrue("Failed with null key", m.keySet().contains(null)); assertNull("Failed with null key", m.keySet().iterator().next()); - Map map = new LinkedHashMap<>(101); - map.put(new Integer(1), "1"); - map.put(new Integer(102), "102"); - map.put(new Integer(203), "203"); - Iterator it = map.keySet().iterator(); - Integer remove1 = it.next(); + var map = new LinkedHashMap(101); + map.put(1, "1"); + map.put(102, "102"); + map.put(203, "203"); + var it = map.keySet().iterator(); + var remove1 = it.next(); it.hasNext(); it.remove(); - Integer remove2 = it.next(); + var remove2 = it.next(); it.remove(); - ArrayList list = new ArrayList<>(Arrays.asList(new Integer[] { 1, 102, 203 })); + var list = new ArrayList<>(Arrays.asList(1, 102, 203)); list.remove(remove1); list.remove(remove2); - assertTrue("Wrong result", it.next().equals(list.get(0))); + assertEquals("Wrong result", it.next(), list.get(0)); assertEquals("Wrong size", 1, map.size()); - assertTrue("Wrong contents", map.keySet().iterator().next().equals(list.get(0))); + assertEquals("Wrong contents", map.keySet().iterator().next(), list.get(0)); - Map map2 = new LinkedHashMap<>(101); - map2.put(new Integer(1), "1"); - map2.put(new Integer(4), "4"); - Iterator it2 = map2.keySet().iterator(); - Integer remove3 = it2.next(); + var map2 = new LinkedHashMap(101); + map2.put(1, "1"); + map2.put(4, "4"); + var it2 = map2.keySet().iterator(); + var remove3 = it2.next(); Integer next; if (remove3.intValue() == 1) { - next = new Integer(4); + next = 4; } else { - next = new Integer(1); + next = 1; } it2.hasNext(); it2.remove(); - assertTrue("Wrong result 2", it2.next().equals(next)); + assertEquals("Wrong result 2", it2.next(), next); assertEquals("Wrong size 2", 1, map2.size()); - assertTrue("Wrong contents 2", map2.keySet().iterator().next().equals(next)); + assertEquals("Wrong contents 2", map2.keySet().iterator().next(), next); } @Test public void test_values() { // Test for method java.util.Collection java.util.LinkedHashMap.values() - Collection c = hm.values(); - assertTrue("Returned collection of incorrect size()", c.size() == hm.size()); + var c = hm.values(); + assertEquals("Returned collection of incorrect size()", c.size(), hm.size()); for (int i = 0; i < objArray.length; i++) { assertTrue("Returned collection does not contain all keys", c.contains(objArray[i])); } - LinkedHashMap myLinkedHashMap = new LinkedHashMap<>(); + var myLinkedHashMap = new LinkedHashMap<>(); for (int i = 0; i < 100; i++) { myLinkedHashMap.put(objArray2[i], objArray[i]); } - Collection values = myLinkedHashMap.values(); + var values = myLinkedHashMap.values(); values.remove(0); - assertTrue("Removing from the values collection should remove from the original map", - !myLinkedHashMap.containsValue(0)); + assertFalse("Removing from the values collection should remove from the original map", + myLinkedHashMap.containsValue(0)); } @Test public void test_removeLjava_lang_Object() { - // Test for method java.lang.Object - // java.util.LinkedHashMap.remove(java.lang.Object) int size = hm.size(); - Integer y = new Integer(9); - Integer x = (Integer) hm.remove(y.toString()); - assertTrue("Remove returned incorrect value", x.equals(new Integer(9))); - assertNull("Failed to remove given key", hm.get(new Integer(9))); - assertTrue("Failed to decrement size", hm.size() == (size - 1)); + var y = Integer.valueOf(9); + var x = (Integer) hm.remove(y.toString()); + assertEquals("Remove returned incorrect value", x, Integer.valueOf(9)); + assertNull("Failed to remove given key", hm.get(9)); + assertEquals("Failed to decrement size", hm.size(), size - 1); assertNull("Remove of non-existent key returned non-null", hm.remove("LCLCLC")); - LinkedHashMap m = new LinkedHashMap<>(); + var m = new LinkedHashMap(); m.put(null, "org/teavm/metaprogramming/test"); - assertNull("Failed with same hash as null", m.remove(new Integer(0))); + assertNull("Failed with same hash as null", m.remove(0)); assertEquals("Failed with null key", "org/teavm/metaprogramming/test", m.remove(null)); } @Test public void test_clear() { - // Test for method void java.util.LinkedHashMap.clear() hm.clear(); assertEquals("Clear failed to reset size", 0, hm.size()); for (int i = 0; i < hmSize; i++) { @@ -294,53 +279,50 @@ public class LinkedHashMapTest { @Test public void test_clone() { - // Test for method java.lang.Object java.util.LinkedHashMap.clone() @SuppressWarnings("unchecked") - LinkedHashMap hm2 = (LinkedHashMap) hm.clone(); - assertTrue("Clone answered equivalent LinkedHashMap", hm2 != hm); + var hm2 = (LinkedHashMap) hm.clone(); + assertNotSame("Clone answered equivalent LinkedHashMap", hm2, hm); for (int counter = 0; counter < hmSize; counter++) { - assertTrue("Clone answered unequal LinkedHashMap", hm.get(objArray2[counter]) - == hm2.get(objArray2[counter])); + assertSame("Clone answered unequal LinkedHashMap", hm.get(objArray2[counter]), hm2.get(objArray2[counter])); } - LinkedHashMap map = new LinkedHashMap<>(); + var map = new LinkedHashMap(); map.put("key", "value"); // get the keySet() and values() on the original Map - Set keys = map.keySet(); - Collection values = map.values(); + var keys = map.keySet(); + var values = map.values(); assertEquals("values() does not work", "value", values.iterator().next()); assertEquals("keySet() does not work", "key", keys.iterator().next()); @SuppressWarnings("unchecked") - AbstractMap map2 = (AbstractMap) map.clone(); + var map2 = (AbstractMap) map.clone(); map2.put("key", "value2"); - Collection values2 = map2.values(); - assertTrue("values() is identical", values2 != values); + var values2 = map2.values(); + assertNotSame("values() is identical", values2, values); // values() and keySet() on the cloned() map should be different assertEquals("values() was not cloned", "value2", values2.iterator().next()); map2.clear(); map2.put("key2", "value3"); - Set key2 = map2.keySet(); - assertTrue("keySet() is identical", key2 != keys); + var key2 = map2.keySet(); + assertNotSame("keySet() is identical", key2, keys); assertEquals("keySet() was not cloned", "key2", key2.iterator().next()); } @Test public void test_clone_Mock() { - LinkedHashMap hashMap = new MockMap(); + var hashMap = new MockMap(); String value = "value a"; hashMap.put("key", value); - MockMap cloneMap = (MockMap) hashMap.clone(); + var cloneMap = (MockMap) hashMap.clone(); assertEquals(value, cloneMap.get("key")); assertEquals(hashMap, cloneMap); assertEquals(1, cloneMap.num); hashMap.put("key", "value b"); - assertFalse(hashMap.equals(cloneMap)); + assertNotEquals(hashMap, cloneMap); } - class MockMap extends LinkedHashMap { - private static final long serialVersionUID = 1L; + static class MockMap extends LinkedHashMap { int num; @Override @@ -357,207 +339,198 @@ public class LinkedHashMapTest { @Test public void test_containsKeyLjava_lang_Object() { - // Test for method boolean - // java.util.LinkedHashMap.containsKey(java.lang.Object) assertTrue("Returned false for valid key", hm.containsKey(String.valueOf(876))); - assertTrue("Returned true for invalid key", !hm.containsKey("KKDKDKD")); + assertFalse("Returned true for invalid key", hm.containsKey("KKDKDKD")); - LinkedHashMap m = new LinkedHashMap<>(); - m.put(null, "org/teavm/metaprogramming/test"); + var m = new LinkedHashMap(); + m.put(null, "test"); assertTrue("Failed with null key", m.containsKey(null)); - assertTrue("Failed with missing key matching null hash", !m.containsKey(new Integer(0))); + assertFalse("Failed with missing key matching null hash", m.containsKey(0)); } @Test public void test_containsValueLjava_lang_Object() { - // Test for method boolean - // java.util.LinkedHashMap.containsValue(java.lang.Object) - assertTrue("Returned false for valid value", hm.containsValue(new Integer(875))); - assertTrue("Returned true for invalid valie", !hm.containsValue(new Integer(-9))); + assertTrue("Returned false for valid value", hm.containsValue(875)); + assertFalse("Returned true for invalid valie", hm.containsValue(-9)); } @Test public void test_isEmpty() { - // Test for method boolean java.util.LinkedHashMap.isEmpty() assertTrue("Returned false for new map", new LinkedHashMap<>().isEmpty()); - assertTrue("Returned true for non-empty", !hm.isEmpty()); + assertFalse("Returned true for non-empty", hm.isEmpty()); } @Test public void test_size() { - // Test for method int java.util.LinkedHashMap.size() - assertTrue("Returned incorrect size", - hm.size() == (objArray.length + 2)); + assertEquals("Returned incorrect size", hm.size(), objArray.length + 2); } @Test public void test_ordered_entrySet() { int i; int sz = 100; - LinkedHashMap lhm = new LinkedHashMap<>(); + var lhm = new LinkedHashMap(); for (i = 0; i < sz; i++) { - Integer ii = new Integer(i); + var ii = Integer.valueOf(i); lhm.put(ii, ii.toString()); } - assertTrue("Returned set of incorrect size 1", lhm.size() == lhm.entrySet().size()); + assertEquals("Returned set of incorrect size 1", lhm.size(), lhm.entrySet().size()); i = 0; - for (Map.Entry m : lhm.entrySet()) { - Integer jj = m.getKey(); - assertTrue("Returned incorrect entry set 1", jj.intValue() == i++); + for (var m : lhm.entrySet()) { + var jj = m.getKey(); + assertEquals("Returned incorrect entry set 1", jj.intValue(), i++); } - LinkedHashMap lruhm = new LinkedHashMap<>(200, .75f, true); + var lruhm = new LinkedHashMap(200, .75f, true); for (i = 0; i < sz; i++) { - Integer ii = new Integer(i); + var ii = Integer.valueOf(i); lruhm.put(ii, ii.toString()); } - Set> s3 = lruhm.entrySet(); - Iterator> it3 = s3.iterator(); - assertTrue("Returned set of incorrect size 2", lruhm.size() == s3.size()); + var s3 = lruhm.entrySet(); + var it3 = s3.iterator(); + assertEquals("Returned set of incorrect size 2", lruhm.size(), s3.size()); for (i = 0; i < sz && it3.hasNext(); i++) { - Map.Entry m = it3.next(); - assertTrue("Returned incorrect entry set 2", m.getKey().intValue() == i); + var m = it3.next(); + assertEquals("Returned incorrect entry set 2", m.getKey().intValue(), i); } /* fetch the even numbered entries to affect traversal order */ int p = 0; for (i = 0; i < sz; i += 2) { - String ii = lruhm.get(new Integer(i)); + var ii = lruhm.get(i); p = p + Integer.parseInt(ii); } assertEquals("invalid sum of even numbers", 2450, p); - Set> s2 = lruhm.entrySet(); - Iterator> it2 = s2.iterator(); - assertTrue("Returned set of incorrect size 3", lruhm.size() == s2.size()); + var s2 = lruhm.entrySet(); + var it2 = s2.iterator(); + assertEquals("Returned set of incorrect size 3", lruhm.size(), s2.size()); for (i = 1; i < sz && it2.hasNext(); i += 2) { - Entry m = it2.next(); - assertTrue("Returned incorrect entry set 3", m.getKey().intValue() == i); + var m = it2.next(); + assertEquals("Returned incorrect entry set 3", m.getKey().intValue(), i); } for (i = 0; i < sz && it2.hasNext(); i += 2) { - Entry m = it2.next(); - assertTrue("Returned incorrect entry set 4", m.getKey().intValue() == i); + var m = it2.next(); + assertEquals("Returned incorrect entry set 4", m.getKey().intValue(), i); } - assertTrue("Entries left to iterate on", !it2.hasNext()); + assertFalse("Entries left to iterate on", it2.hasNext()); } @Test public void test_ordered_keySet() { int i; int sz = 100; - LinkedHashMap lhm = new LinkedHashMap<>(); + var lhm = new LinkedHashMap(); for (i = 0; i < sz; i++) { - Integer ii = new Integer(i); + var ii = Integer.valueOf(i); lhm.put(ii, ii.toString()); } - Set s1 = lhm.keySet(); - Iterator it1 = s1.iterator(); - assertTrue("Returned set of incorrect size", lhm.size() == s1.size()); + var s1 = lhm.keySet(); + var it1 = s1.iterator(); + assertEquals("Returned set of incorrect size", lhm.size(), s1.size()); for (i = 0; it1.hasNext(); i++) { - Integer jj = it1.next(); - assertTrue("Returned incorrect entry set", jj.intValue() == i); + var jj = it1.next(); + assertEquals("Returned incorrect entry set", jj.intValue(), i); } - LinkedHashMap lruhm = new LinkedHashMap<>(200, .75f, true); + var lruhm = new LinkedHashMap(200, .75f, true); for (i = 0; i < sz; i++) { - Integer ii = new Integer(i); + var ii = Integer.valueOf(i); lruhm.put(ii, ii.toString()); } - Set s3 = lruhm.keySet(); - Iterator it3 = s3.iterator(); - assertTrue("Returned set of incorrect size", lruhm.size() == s3.size()); + var s3 = lruhm.keySet(); + var it3 = s3.iterator(); + assertEquals("Returned set of incorrect size", lruhm.size(), s3.size()); for (i = 0; i < sz && it3.hasNext(); i++) { Integer jj = it3.next(); - assertTrue("Returned incorrect entry set", jj.intValue() == i); + assertEquals("Returned incorrect entry set", jj.intValue(), i); } /* fetch the even numbered entries to affect traversal order */ int p = 0; for (i = 0; i < sz; i += 2) { - String ii = lruhm.get(new Integer(i)); + var ii = lruhm.get(i); p = p + Integer.parseInt(ii); } assertEquals("invalid sum of even numbers", 2450, p); - Set s2 = lruhm.keySet(); - Iterator it2 = s2.iterator(); - assertTrue("Returned set of incorrect size", lruhm.size() == s2.size()); + var s2 = lruhm.keySet(); + var it2 = s2.iterator(); + assertEquals("Returned set of incorrect size", lruhm.size(), s2.size()); for (i = 1; i < sz && it2.hasNext(); i += 2) { - Integer jj = it2.next(); - assertTrue("Returned incorrect entry set", jj.intValue() == i); + var jj = it2.next(); + assertEquals("Returned incorrect entry set", jj.intValue(), i); } for (i = 0; i < sz && it2.hasNext(); i += 2) { - Integer jj = it2.next(); - assertTrue("Returned incorrect entry set", jj.intValue() == i); + var jj = it2.next(); + assertEquals("Returned incorrect entry set", jj.intValue(), i); } - assertTrue("Entries left to iterate on", !it2.hasNext()); + assertFalse("Entries left to iterate on", it2.hasNext()); } @Test public void test_ordered_values() { int i; int sz = 100; - LinkedHashMap lhm = new LinkedHashMap<>(); + var lhm = new LinkedHashMap(); for (i = 0; i < sz; i++) { - Integer ii = new Integer(i); - lhm.put(ii, new Integer(i * 2)); + lhm.put(i, i * 2); } - Collection s1 = lhm.values(); - Iterator it1 = s1.iterator(); - assertTrue("Returned set of incorrect size 1", lhm.size() == s1.size()); + var s1 = lhm.values(); + var it1 = s1.iterator(); + assertEquals("Returned set of incorrect size 1", lhm.size(), s1.size()); for (i = 0; it1.hasNext(); i++) { - Integer jj = it1.next(); - assertTrue("Returned incorrect entry set 1", jj.intValue() == i * 2); + var jj = it1.next(); + assertEquals("Returned incorrect entry set 1", jj.intValue(), i * 2); } - LinkedHashMap lruhm = new LinkedHashMap<>(200, .75f, true); + var lruhm = new LinkedHashMap(200, .75f, true); for (i = 0; i < sz; i++) { - Integer ii = new Integer(i); - lruhm.put(ii, new Integer(i * 2)); + lruhm.put(i, i * 2); } - Collection s3 = lruhm.values(); - Iterator it3 = s3.iterator(); - assertTrue("Returned set of incorrect size", lruhm.size() == s3.size()); + var s3 = lruhm.values(); + var it3 = s3.iterator(); + assertEquals("Returned set of incorrect size", lruhm.size(), s3.size()); for (i = 0; i < sz && it3.hasNext(); i++) { - Integer jj = it3.next(); - assertTrue("Returned incorrect entry set", jj.intValue() == i * 2); + var jj = it3.next(); + assertEquals("Returned incorrect entry set", jj.intValue(), i * 2); } // fetch the even numbered entries to affect traversal order int p = 0; for (i = 0; i < sz; i += 2) { - Integer ii = lruhm.get(new Integer(i)); + var ii = lruhm.get(i); p = p + ii.intValue(); } - assertTrue("invalid sum of even numbers", p == 2450 * 2); + assertEquals("invalid sum of even numbers", 2450 * 2, p); - Collection s2 = lruhm.values(); - Iterator it2 = s2.iterator(); - assertTrue("Returned set of incorrect size", lruhm.size() == s2.size()); + var s2 = lruhm.values(); + var it2 = s2.iterator(); + assertEquals("Returned set of incorrect size", lruhm.size(), s2.size()); for (i = 1; i < sz && it2.hasNext(); i += 2) { - Integer jj = it2.next(); - assertTrue("Returned incorrect entry set", jj.intValue() == i * 2); + var jj = it2.next(); + assertEquals("Returned incorrect entry set", jj.intValue(), i * 2); } for (i = 0; i < sz && it2.hasNext(); i += 2) { - Integer jj = it2.next(); - assertTrue("Returned incorrect entry set", jj.intValue() == i * 2); + var jj = it2.next(); + assertEquals("Returned incorrect entry set", jj.intValue(), i * 2); } - assertTrue("Entries left to iterate on", !it2.hasNext()); + assertFalse("Entries left to iterate on", it2.hasNext()); } @Test public void test_to_String() { - LinkedHashMap lhm = new LinkedHashMap(); + var lhm = new LinkedHashMap<>(); lhm.put("A", lhm); lhm.put("B", "C"); assertEquals("{A=(this Map), B=C}", lhm.toString()); - assertEquals("{}", new LinkedHashMap().toString()); + assertEquals("{}", new LinkedHashMap<>().toString()); } } diff --git a/tests/src/test/java/org/teavm/classlib/java/util/concurrent/ConcurrentHashMapTest.java b/tests/src/test/java/org/teavm/classlib/java/util/concurrent/ConcurrentHashMapTest.java new file mode 100644 index 000000000..fdf9e84b9 --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/util/concurrent/ConcurrentHashMapTest.java @@ -0,0 +1,309 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.util.concurrent; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.TeaVMTestRunner; +import org.teavm.junit.WholeClassCompilation; + +@RunWith(TeaVMTestRunner.class) +@WholeClassCompilation +public class ConcurrentHashMapTest { + @Test + public void constructor() { + try { + new ConcurrentHashMap<>(-1); + fail("Expected exception not thrown"); + } catch (IllegalArgumentException e) { + // ok + } + + try { + new ConcurrentHashMap<>(1, -1f); + fail("Expected exception not thrown"); + } catch (IllegalArgumentException e) { + // ok + } + + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + assertEquals(2, map.size()); + assertEquals(Integer.valueOf(23), map.get("q")); + assertNull(map.get("e")); + } + + @Test + public void getPut() { + var map = new ConcurrentHashMap<>(); + + assertEquals(0, map.size()); + assertNull(map.get("q")); + assertFalse(map.containsKey("q")); + + assertNull(map.put("q", 23)); + assertEquals(1, map.size()); + assertEquals(23, map.get("q")); + assertTrue(map.containsKey("q")); + + assertEquals(23, map.put("q", 24)); + assertEquals(1, map.size()); + assertEquals(24, map.get("q")); + + assertNull(map.put("w", 42)); + assertEquals(2, map.size()); + assertEquals(24, map.get("q")); + } + + @Test + public void remove() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + assertEquals(Integer.valueOf(23), map.remove("q")); + assertEquals(1, map.size()); + assertNull(map.get("q")); + + assertNull(map.remove("q")); + assertEquals(1, map.size()); + } + + @Test + public void removeKeyValue() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + assertFalse(map.remove("q", 42)); + assertEquals(2, map.size()); + assertEquals(Integer.valueOf(23), map.get("q")); + + assertTrue(map.remove("q", 23)); + assertEquals(1, map.size()); + assertNull(map.get("q")); + } + + @Test + public void clear() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + map.clear(); + assertEquals(0, map.size()); + assertNull(map.get("q")); + assertNull(map.get("w")); + assertFalse(map.entrySet().iterator().hasNext()); + } + + @Test + public void containsValue() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + assertTrue(map.containsValue(23)); + assertTrue(map.containsValue(42)); + assertFalse(map.containsValue(99)); + assertFalse(map.containsValue("q")); + } + + @Test + public void entrySet() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + var entries = map.entrySet(); + assertEquals(2, entries.size()); + + var entryList = new ArrayList<>(entries); + entryList.sort(Map.Entry.comparingByKey()); + assertEquals(2, entryList.size()); + assertEquals("q", entryList.get(0).getKey()); + assertEquals(Integer.valueOf(23), entryList.get(0).getValue()); + assertEquals("w", entryList.get(1).getKey()); + assertEquals(Integer.valueOf(42), entryList.get(1).getValue()); + + for (var entry : entries) { + if (entry.getKey().equals("w")) { + entry.setValue(43); + } + } + assertEquals(Integer.valueOf(43), map.get("w")); + + entries.removeIf(entry -> entry.getKey().equals("q")); + assertEquals(1, entries.size()); + assertEquals(1, map.size()); + assertNull(map.get("q")); + assertEquals(Integer.valueOf(43), map.get("w")); + } + + @Test + public void replace() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + assertEquals(Integer.valueOf(23), map.replace("q", 24)); + assertEquals(Integer.valueOf(24), map.get("q")); + assertEquals(2, map.size()); + + assertNull(map.replace("e", 55)); + assertEquals(2, map.size()); + assertNull(map.get("e")); + + assertFalse(map.replace("w", 123, 43)); + assertEquals(Integer.valueOf(42), map.get("w")); + + assertTrue(map.replace("w", 42, 43)); + assertEquals(Integer.valueOf(43), map.get("w")); + } + + @Test + public void getOrDefault() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + assertEquals(Integer.valueOf(23), map.getOrDefault("q", 24)); + assertEquals(Integer.valueOf(55), map.getOrDefault("e", 55)); + } + + @Test + public void forEach() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + var values = new Object[2]; + var size = new int[1]; + map.forEach((k, v) -> { + size[0]++; + if (k.equals("q")) { + values[0] = v; + } else if (k.equals("w")) { + values[1] = v; + } + }); + + assertEquals(2, size[0]); + assertArrayEquals(new Object[] { 23, 42 }, values); + } + + @Test + public void putIfAbsent() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + assertEquals((Object) 23, map.putIfAbsent("q", 24)); + assertEquals(2, map.size()); + assertEquals((Object) 23, map.get("q")); + + assertNull(map.putIfAbsent("e", 55)); + assertEquals(3, map.size()); + assertEquals((Object) 55, map.get("e")); + } + + @Test + public void replaceAll() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + map.replaceAll((k, v) -> v + k.charAt(0) * 100); + assertEquals(2, map.size()); + assertEquals((Object) 11323, map.get("q")); + assertEquals((Object) 11942, map.get("w")); + } + + @Test + public void computeIfAbsent() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + var count = new int[1]; + Function f = k -> { + count[0]++; + return (int) k.charAt(0); + }; + assertEquals((Object) 23, map.computeIfAbsent("q", f)); + assertEquals(0, count[0]); + assertEquals(2, map.size()); + assertEquals((Object) 23, map.get("q")); + + assertEquals((Object) 101, map.computeIfAbsent("e", f)); + assertEquals(1, count[0]); + assertEquals(3, map.size()); + assertEquals((Object) 101, map.get("e")); + } + + @Test + public void computeIfPresent() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + var count = new int[1]; + BiFunction f = (k, v) -> { + count[0]++; + return (int) k.charAt(0) * 100 + v; + }; + assertEquals((Object) 11323, map.computeIfPresent("q", f)); + assertEquals(1, count[0]); + assertEquals(2, map.size()); + assertEquals((Object) 11323, map.get("q")); + + assertNull(map.computeIfPresent("e", f)); + assertEquals(1, count[0]); + assertEquals(2, map.size()); + assertNull(map.get("e")); + } + + @Test + public void compute() { + var map = new ConcurrentHashMap<>(Map.of("q", 23, "w", 42)); + + var count = new int[1]; + BiFunction f = (k, v) -> { + count[0]++; + return (int) k.charAt(0) * 100 + (v != null ? v : 0); + }; + + assertEquals((Object) 11323, map.compute("q", f)); + assertEquals(1, count[0]); + assertEquals(2, map.size()); + assertEquals((Object) 11323, map.get("q")); + + assertEquals((Object) 10100, map.compute("e", f)); + assertEquals(2, count[0]); + assertEquals(3, map.size()); + assertEquals((Object) 10100, map.get("e")); + } + + @Test + public void largeMap() { + var map = new ConcurrentHashMap(); + + for (var i = 0; i < 10000; ++i) { + map.put(i, i + 1); + } + + assertEquals(10000, map.size()); + for (var i = 0; i < 10000; ++i) { + assertEquals((Object) (i + 1), map.get(i)); + } + + map = new ConcurrentHashMap<>(); + for (var i = 9999; i >= 0; --i) { + map.put(i, i + 1); + } + + assertEquals(10000, map.size()); + for (var i = 0; i < 10000; ++i) { + assertEquals((Object) (i + 1), map.get(i)); + } + } +} diff --git a/tests/src/test/java/org/teavm/classlib/java/util/concurrent/MultiThreadConcurrentHashMapTest.java b/tests/src/test/java/org/teavm/classlib/java/util/concurrent/MultiThreadConcurrentHashMapTest.java new file mode 100644 index 000000000..2e8cf57a9 --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/util/concurrent/MultiThreadConcurrentHashMapTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.util.concurrent; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.SkipJVM; +import org.teavm.junit.TeaVMTestRunner; +import org.teavm.junit.WholeClassCompilation; + +@RunWith(TeaVMTestRunner.class) +@WholeClassCompilation +public class MultiThreadConcurrentHashMapTest { + private ArrayBlockingQueue backgroundTasks = new ArrayBlockingQueue<>(100); + private boolean stopped; + + public MultiThreadConcurrentHashMapTest() { + var t = new Thread(() -> { + while (!stopped) { + try { + backgroundTasks.take().run(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }); + t.start(); + } + + @After + public void dispose() { + backgroundTasks.add(() -> stopped = true); + } + + @Test + public void containsValue() { + var key = new Wrapper("q"); + var value = new Wrapper("23"); + var map = new ConcurrentHashMap<>(Map.of(key, value)); + + assertTrue(map.containsValue(new Wrapper("23"))); + assertTrue(map.containsValue(new Wrapper("23", () -> map.remove(key)))); + } + + // In JVM we have deadlock here, since JVM implementation relies on blocking. + // Our algorithm is non-blocking so no problem for TeaVM. + @Test + @SkipJVM + public void concurrentPut() { + var key = new Wrapper("q"); + var value = new Wrapper("23"); + var map = new ConcurrentHashMap<>(Map.of(key, value)); + + map.put(key, value); + var old = map.put(new Wrapper("q", () -> map.put(key, new Wrapper("24"))), new Wrapper("25")); + + assertEquals("24", old.s); + assertEquals("25", map.get(key).s); + } + + @Test + @SkipJVM + public void concurrentPutRemove() { + var key = new Wrapper("q"); + var value = new Wrapper("23"); + var map = new ConcurrentHashMap<>(Map.of(key, value)); + + map.put(key, value); + var old = map.put(new Wrapper("q", () -> map.remove(key)), new Wrapper("25")); + + assertNull(old); + assertEquals("25", map.get(key).s); + } + + @Test + @SkipJVM + public void concurrentRemovePut() { + var key = new Wrapper("q"); + var value = new Wrapper("23"); + var map = new ConcurrentHashMap<>(Map.of(key, value)); + + map.put(key, value); + var old = map.remove(new Wrapper("q", () -> map.put(key, new Wrapper("24")))); + + assertEquals("24", old.s); + assertNull(map.get(key)); + } + + private void runInBackground(Runnable runnable) { + backgroundTasks.add(runnable); + } + + private class Wrapper { + private final String s; + private Runnable task; + + Wrapper(String s) { + this(s, null); + } + + Wrapper(String s, Runnable task) { + this.s = s; + this.task = task; + } + + @Override + public boolean equals(Object o) { + awaitIfNecessary(); + if (o instanceof Wrapper) { + ((Wrapper) o).awaitIfNecessary(); + } + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + var key = (Wrapper) o; + return Objects.equals(s, key.s); + } + + @Override + public int hashCode() { + awaitIfNecessary(); + return s.hashCode(); + } + + private void awaitIfNecessary() { + if (task != null) { + var t = task; + task = null; + runInBackground(() -> { + t.run(); + send(); + }); + synchronized (this) { + try { + wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } + + void send() { + synchronized (this) { + notifyAll(); + } + } + } +}