diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TList.java b/classlib/src/main/java/org/teavm/classlib/java/util/TList.java index 184792436..04a2f886c 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/TList.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TList.java @@ -155,4 +155,8 @@ public interface TList extends TCollection { } return new TTemplateCollections.ImmutableArrayList<>(elements.clone()); } + + static TList copyOf(TCollection collection) { + return new TTemplateCollections.ImmutableArrayList<>(collection); + } } 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 f5a5fd7d8..0776d2d2b 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 @@ -294,4 +294,13 @@ public interface TMap { static TMap.Entry entry(K k, V v) { return new TTemplateCollections.ImmutableEntry<>(requireNonNull(k), requireNonNull(v)); } + + @SuppressWarnings("unchecked") + static TMap copyOf(TMap map) { + if (map instanceof TTemplateCollections.NEtriesMap) { + return (TTemplateCollections.NEtriesMap) map; + } else { + return new TTemplateCollections.NEtriesMap<>(map); + } + } } diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TSet.java b/classlib/src/main/java/org/teavm/classlib/java/util/TSet.java index 7e20f4fdf..189404134 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/TSet.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TSet.java @@ -73,4 +73,8 @@ public interface TSet extends TCollection { static TSet of(E... elements) { return new TTemplateCollections.NElementSet<>(elements); } + + static TSet copyOf(TCollection collection) { + return new TTemplateCollections.NElementSet<>(collection); + } } diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TTemplateCollections.java b/classlib/src/main/java/org/teavm/classlib/java/util/TTemplateCollections.java index d9c36c79f..0ca1b9339 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/TTemplateCollections.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TTemplateCollections.java @@ -36,6 +36,23 @@ public final class TTemplateCollections { this.list = list; } + @SuppressWarnings("unchecked") + public ImmutableArrayList(TCollection collection) { + T[] list = (T[]) new Object[collection.size()]; + + TIterator iter = collection.iterator(); + int index = 0; + while (iter.hasNext()) { + T element = iter.next(); + if (element == null) { + throw new NullPointerException(); + } + list[index++] = element; + } + + this.list = list; + } + @Override public T get(int index) { return list[index]; @@ -193,6 +210,9 @@ public final class TTemplateCollections { static class NElementSet extends AbstractImmutableSet { private T[] data; + /** + * Throws an exception on duplicate elements. + */ @SafeVarargs NElementSet(T... data) { T[] table = data.clone(); @@ -232,6 +252,42 @@ public final class TTemplateCollections { this.data = table; } + /** + * Duplicate elements will be ignored (does NOT throw an exception). + */ + @SuppressWarnings("unchecked") + NElementSet(TCollection collection) { + T[] temp = (T[]) new Object[collection.size()]; + + int index = 0; + outerLoop: + for (T element : (T[]) collection.toArray()) { + if (element == null) { + throw new NullPointerException(String.format("Element at index %s is null", index)); + } + + int indexTemp = Math.abs(element.hashCode()) % temp.length; + while (temp[indexTemp] != null) { + if (temp[indexTemp].equals(element)) { + continue outerLoop; + } + indexTemp = (indexTemp + 1) % temp.length; + } + temp[indexTemp] = element; + index++; + } + + T[] result = (T[]) new Object[index]; + index = 0; + for (T element : temp) { + if (element != null) { + result[index++] = element; + } + } + + this.data = result; + } + @Override public TIterator iterator() { return new TIterator() { @@ -416,17 +472,31 @@ public final class TTemplateCollections { @SuppressWarnings("unchecked") @SafeVarargs NEtriesMap(Entry... data) { - Entry[] table = new Entry[data.length]; + this.data = toEntryArray(data); + } + + @SuppressWarnings("unchecked") + NEtriesMap(TMap map) { + this.data = toEntryArray(map.entrySet().toArray(new TMap.Entry[0])); + } + + /** + * Creates an array where the {@code Entry} elements are positioned in such a way they + * are compatible with the contract of {@link java.util.Map}. + */ + @SuppressWarnings("unchecked") + private Entry[] toEntryArray(Entry[] entries) { + Entry[] table = new Entry[entries.length]; Arrays.fill(table, null); - for (Entry entry : data) { + for (Entry entry : entries) { Objects.requireNonNull(entry.getKey()); Objects.requireNonNull(entry.getValue()); - int suggestedIndex = Math.abs(entry.getKey().hashCode()) % data.length; + int suggestedIndex = Math.abs(entry.getKey().hashCode()) % entries.length; int index = suggestedIndex; boolean found = false; - while (index < data.length) { + while (index < entries.length) { Entry existingEntry = table[index]; if (existingEntry == null) { found = true; @@ -451,7 +521,7 @@ public final class TTemplateCollections { table[index] = new ImmutableEntry<>(entry.getKey(), entry.getValue()); } - this.data = table; + return table; } @Override diff --git a/tests/src/test/java/org/teavm/classlib/java/util/ListTest.java b/tests/src/test/java/org/teavm/classlib/java/util/ListTest.java index a6cfbcd0e..f9d6587c3 100644 --- a/tests/src/test/java/org/teavm/classlib/java/util/ListTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/util/ListTest.java @@ -19,6 +19,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -48,6 +50,24 @@ public class ListTest { List.of("q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "a")); } + @Test + public void copyOfWorks() { + testOf(new String[0], List.copyOf(new ArrayList<>())); + testOf(new String[] { "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "a" }, + List.copyOf(Arrays.asList("q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "a"))); + + try { + // copyOf() must throw a NullPointerException on any 'null' element. + List listWithNull = new ArrayList<>(1); + listWithNull.add(null); + + List.copyOf(listWithNull); + fail("Expected NullPointerException"); + } catch (NullPointerException e) { + // ok + } + } + private void testOf(String[] expected, List actual) { if (actual.size() != expected.length) { fail("Expected size is " + expected.length + ", actual size is " + actual.size()); diff --git a/tests/src/test/java/org/teavm/classlib/java/util/MapTest.java b/tests/src/test/java/org/teavm/classlib/java/util/MapTest.java index 8eea9b1b0..a064366e1 100644 --- a/tests/src/test/java/org/teavm/classlib/java/util/MapTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/util/MapTest.java @@ -18,6 +18,7 @@ package org.teavm.classlib.java.util; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.HashMap; @@ -54,6 +55,24 @@ public class MapTest { Map.entry("p", 9), Map.entry("a", 10))); } + @Test + public void copyOfWorks() { + testOf(new String[0], Map.copyOf(new HashMap<>())); + testOf(new String[] { "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "a" }, + Map.copyOf( + Map.ofEntries(Map.entry("q", 0), Map.entry("w", 1), Map.entry("e", 2), Map.entry("r", 3), + Map.entry("t", 4), Map.entry("y", 5), Map.entry("u", 6), Map.entry("i", 7), Map.entry("o", 8), + Map.entry("p", 9), Map.entry("a", 10)))); + } + + @Test + public void copyOfOptimized() { + Map mapCopy1 = Map.copyOf(Map.of("q", 0, "w", 1, "e", 2)); + Map mapCopy2 = Map.copyOf(mapCopy1); + + assertSame("Must not create copies of immutable collections", mapCopy1, mapCopy2); + } + private void testOf(String[] expected, Map actual) { if (actual.size() != expected.length) { fail("Expected size is " + expected.length + ", actual size is " + actual.size()); diff --git a/tests/src/test/java/org/teavm/classlib/java/util/SetTest.java b/tests/src/test/java/org/teavm/classlib/java/util/SetTest.java index 6cb67d333..8174bf4e0 100644 --- a/tests/src/test/java/org/teavm/classlib/java/util/SetTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/util/SetTest.java @@ -20,6 +20,8 @@ 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.Arrays; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import org.junit.Test; @@ -61,6 +63,24 @@ public class SetTest { expectIAE(() -> Set.of("q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "q")); } + @Test + public void copyOfWorks() { + testOf(new String[0], Set.copyOf(new HashSet<>())); + testOf(new String[] { "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "a" }, + Set.copyOf(Arrays.asList("q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "a"))); + // Duplicates must be silently removed by copyOf(). Unlike of() where they throw an exception. + testOf(new String[] { "q", "e", "r", "u", "i", "o", "p" }, + Set.copyOf(Arrays.asList("q", "q", "e", "r", "q", "q", "u", "i", "o", "p", "q"))); + + try { + // copyOf() must throw a NullPointerException on any 'null' element. + Set.copyOf(Arrays.asList("q", "q", "e", "r", "q", "q", "u", "i", "o", "p", "q", null)); + fail("Expected NullPointerException"); + } catch (NullPointerException e) { + // ok + } + } + private void expectIAE(Runnable r) { try { r.run();