From a409763f7606719d9fb587777b58c54ed0143bab Mon Sep 17 00:00:00 2001 From: Jasper Siepkes Date: Wed, 7 Jun 2023 10:13:20 +0200 Subject: [PATCH] classlib: add the copyOf method to List, Set and Map interfaces (#708) The copyOf static method was added in Java 10 to the List, Set and Map interfaces. Since no additions were made since Java 10 this commit brings the List, Set and Map interfaces to 100% completion for the latest LTS (Java 17) at the time of writing. --- .../org/teavm/classlib/java/util/TList.java | 4 + .../org/teavm/classlib/java/util/TMap.java | 9 +++ .../org/teavm/classlib/java/util/TSet.java | 4 + .../java/util/TTemplateCollections.java | 80 +++++++++++++++++-- .../teavm/classlib/java/util/ListTest.java | 20 +++++ .../org/teavm/classlib/java/util/MapTest.java | 19 +++++ .../org/teavm/classlib/java/util/SetTest.java | 20 +++++ 7 files changed, 151 insertions(+), 5 deletions(-) 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();