Browse Source

new: WeakIdentMap
fix: TIdentityMap was inconsistent, e.g. #range bypassed "identity normalization"
fix: MapTester tests "#get" a bit more

Mariusz Sieroń 6 years ago
parent
commit
e46be2b208

+ 427 - 0
assira/src/main/java/net/ranides/assira/collection/maps/WeakIdentMap.java

@@ -0,0 +1,427 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.collection.maps;
+
+import net.ranides.assira.collection.arrays.ArrayUtils;
+import net.ranides.assira.collection.arrays.NativeArray;
+import net.ranides.assira.collection.arrays.NativeArrayUtils;
+import net.ranides.assira.collection.sets.ASet;
+import net.ranides.assira.collection.utils.HashCollection;
+import net.ranides.assira.generic.HashUtils;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.*;
+
+/**
+ * A generic set map implementation.
+ */
+public final class WeakIdentMap<K,V> extends AMap<K,V> {
+
+    private WeakEntry<K,V>[] entries;
+
+    private boolean used[];
+
+    private final float factor;
+
+    private int n;
+
+    private int capacity;
+
+    private int mask;
+
+    private int size;
+
+    private ReferenceQueue<K> queue;
+
+    public WeakIdentMap() {
+        this(HashCollection.INITIAL_SIZE, HashCollection.LOAD_FACTOR);
+    }
+
+    public WeakIdentMap(int expected) {
+        this(expected, HashCollection.LOAD_FACTOR);
+    }
+
+    @SuppressWarnings("unchecked")
+    public WeakIdentMap(int expected, float factor) {
+        if (factor <= 0 || factor > 1) {
+            throw new IllegalArgumentException("Load factor must be greater than 0 and smaller than or equal to 1");
+        }
+        if (expected < 0) {
+            throw new IllegalArgumentException("The expected number of elements must be nonnegative");
+        }
+        this.factor = factor;
+        this.n = HashCollection.arraysize(expected, factor);
+        this.mask = n - 1;
+        this.capacity = HashCollection.factorsize(n, factor);
+        this.entries = new WeakEntry[n];
+        this.used = new boolean[n];
+        this.queue = new ReferenceQueue<>();
+    }
+
+    @Override
+    public int size() {
+        icleanup();
+        return size;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return size == 0;
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+        return new EntryView();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return used[ifind(key)];
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        return entrySet().stream().anyMatch(e -> Objects.equals(value, e.getValue()));
+    }
+
+    @Override
+    public V get(Object key) {
+        int pos = ifind(key);
+        return used[pos] ? entries[pos].value : null;
+    }
+
+    @Override
+    public V put(K key, V value) {
+        icleanup();
+        int pos = ifind(key);
+        if(used[pos]) {
+            V prev = entries[pos].value;
+            entries[pos].value = value;
+            return prev;
+        } else {
+            iinsert(pos, key, value);
+            return null;
+        }
+    }
+
+    @Override
+    public V remove(Object key) {
+        if(key == null) {
+            return null;
+        }
+        int pos = ifind(key);
+        if(!used[pos]) {
+            return null;
+        }
+        V prev = entries[pos].value;
+        ishift(pos);
+        return prev;
+    }
+
+    @Override
+    public boolean remove(Object key, Object value) {
+        int pos = ifind(key);
+        if(used[pos] && Objects.equals(value, entries[pos].getValue())) {
+            ishift(pos);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void clear() {
+        if (size == 0) {
+            return;
+        }
+        size = 0;
+        NativeArrayUtils.fill(NativeArray.wrap(used), false);
+        ArrayUtils.fill(entries, null);
+    }
+
+    private static int hash(Object key) {
+        return HashUtils.murmurHash3(System.identityHashCode(key));
+    }
+
+    private boolean icmp(WeakEntry entry1, Object key) {
+        return Objects.equals(entry1.getKey(), key);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void irehash(int newN) {
+        int i = 0, pos;
+        final boolean iused[] = this.used;
+        WeakEntry cEntry;
+        final WeakEntry ikeys[] = this.entries;
+        final int newMask = newN - 1;
+        final WeakEntry newKey[] = new WeakEntry[newN];
+        final boolean newUsed[] = new boolean[newN];
+        for (int j = size; j-- != 0; ) {
+            while (!iused[i]) {
+                i++;
+            }
+            cEntry = ikeys[i];
+            pos = cEntry.hash & newMask;
+            while (newUsed[pos]) {
+                pos = (pos + 1) & newMask;
+            }
+            newUsed[pos] = true;
+            newKey[pos] = cEntry;
+            i++;
+        }
+        n = newN;
+        mask = newMask;
+        capacity = HashCollection.factorsize(n, factor);
+        this.entries = newKey;
+        this.used = newUsed;
+    }
+
+    private int ifind(Object key) {
+        int i = hash(key) & mask;
+        // There's always an unused entry.
+        while (used[i] && !icmp(entries[i], key)) {
+            i = (i + 1) & mask;
+        }
+        return i;
+    }
+
+    private boolean iinsert(int segment, K key, V value) {
+        used[segment] = true;
+        entries[segment] = new WeakEntry<>(queue, key, value);
+
+        if (++size >= capacity) {
+            irehash(HashCollection.arraysize(size + 1, factor));
+        }
+        return true;
+    }
+
+    private boolean ishift(int pos) {
+        size--;
+        int last;
+        for (; ; ) {
+            last = pos;
+            pos = inext(pos);
+            if (!used[pos]) {
+                break;
+            }
+            imove(last, pos);
+        }
+        iclear(last);
+        return true;
+    }
+
+    private int inext(int last) {
+        int pos = (last + 1) & mask;
+        while (used[pos]) {
+
+            int slot = entries[pos].hash & mask;
+            if (last <= pos ? last >= slot || slot > pos : last >= slot && slot > pos) {
+                break;
+            }
+            pos = (pos + 1) & mask;
+        }
+        return pos;
+    }
+
+    private void imove(int target, int source) {
+        entries[target] = entries[source];
+    }
+
+    private void iclear(int segment) {
+        used[segment] = false;
+        entries[segment] = null;
+    }
+
+    private void icleanup() {
+        // cleanup after cleared References.
+        WeakEntry ref;
+        while ((ref = (WeakEntry)this.queue.poll()) != null) {
+            icleanup(ref);
+        }
+    }
+
+    private void icleanup(WeakEntry entry) {
+        int pos = entry.hash & mask;
+
+        // There's always an unused entry.
+        while (used[pos] && entries[pos].hash!=entry.hash && entries[pos].getKey()!=null) {
+            pos = (pos + 1) & mask;
+        }
+        if(used[pos]) {
+            ishift(pos);
+        }
+    }
+
+    private class EntryView extends ASet<Entry<K, V>> {
+
+        @Override
+        public Iterator<Entry<K, V>> iterator() {
+            return new EntryIterator();
+        }
+
+        @Override
+        public int size() {
+            return size;
+        }
+
+        @Override
+        public boolean remove(Object value) {
+            if(value instanceof Map.Entry) {
+                Entry entry = (Entry)value;
+                return WeakIdentMap.this.remove(entry.getKey(), entry.getValue());
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public boolean add(Entry<K, V> entry) {
+            return null != WeakIdentMap.this.put(entry.getKey(), entry.getValue());
+        }
+    }
+
+    private class EntryIterator implements Iterator<Map.Entry<K,V>> {
+
+        int mpos;
+
+        int mlast;
+
+        int counter;
+
+        ArrayList<WeakEntry<K,V>> wrapped;
+
+        public EntryIterator() {
+            this.mpos = WeakIdentMap.this.n;
+            this.mlast = -1;
+            this.counter = size;
+            this.wrapped = null;
+            boolean used[] = WeakIdentMap.this.used;
+            if (counter != 0) {
+                while (!used[--mpos]) { } // NOPMD
+            }
+        }
+
+        @Override
+        public boolean hasNext() {
+            return counter != 0;
+        }
+
+        @Override
+        public WeakEntry<K,V> next() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+            counter--;
+            // We are just enumerating elements from the wrapped list.
+            if (mpos < 0) {
+                return wrapped.get(-(mlast = --mpos) - 2);
+            }
+            final WeakEntry<K,V> retVal = entries[mlast = mpos];
+            if (counter != 0) {
+                final boolean used[] = WeakIdentMap.this.used;
+                while (mpos-- != 0 && !used[mpos]) { } // NOPMD
+                // When here pos < 0 there are no more elements to be enumerated by scanning, but wrapped might be nonempty.
+            }
+            return retVal;
+        }
+
+        private int ishift(int pos) {
+            int last;
+            for (; ; ) {
+                last = pos;
+                pos = inext(last);
+                if (!used[pos]) {
+                    break;
+                }
+                if (pos < last) {
+                    if (wrapped == null) {
+                        wrapped = new ArrayList<>();
+                    }
+                    wrapped.add(entries[pos]);
+                }
+                imove(last, pos);
+            }
+            iclear(last);
+            return last;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public void remove() {
+            if (mlast == -1) {
+                throw new IllegalStateException();
+            }
+            if (mpos < -1) {
+                // We're removing wrapped entries.
+                WeakIdentMap.this.remove(wrapped.set(-mpos - 2, null).getKey());
+                mlast = -1;
+                return;
+            }
+            size--;
+            if (ishift(mlast) == mpos && counter > 0) {
+                counter++;
+                next();
+            }
+            mlast = -1; // You can no longer remove this entry.
+        }
+    }
+
+    private static final class WeakEntry<K,V> extends WeakReference<K> implements Map.Entry<K, V> {
+        final int hash;
+        V value;
+
+        WeakEntry(ReferenceQueue<K> queue, K key, V value) {
+            super(key, queue);
+            this.hash = hash(key);
+            this.value = value;
+        }
+
+        @Override
+        public final void clear() {
+            // Do nothing if reference is explicity cleared. This prevents
+            // backdoor modification of map entries.
+        }
+
+        @Override
+        public final K getKey() {
+            return this.get();
+        }
+
+        @Override
+        public V getValue() {
+            return this.value;
+        }
+
+        @Override
+        public V setValue(V value) {
+            V prev = this.value;
+            this.value = value;
+            return prev;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            return (obj instanceof Map.Entry) && equals((Map.Entry<?,?>)obj);
+        }
+
+        boolean equals(Map.Entry<?, ?> e) {
+            return getKey() == e.getKey();
+        }
+
+        @Override
+        public int hashCode() {
+            return this.hash;
+        }
+
+        @Override
+        public String toString() {
+            return getKey() + "=" + getValue();
+        }
+
+    }
+
+}

+ 24 - 14
assira/src/main/java/net/ranides/assira/test/TMap.java

@@ -21,9 +21,10 @@ import net.ranides.assira.collection.arrays.NativeArrayUtils;
 import net.ranides.assira.collection.arrays.ArrayAllocator;
 import net.ranides.assira.collection.lists.ListUtils;
 import net.ranides.assira.collection.lookups.ALookup;
+import net.ranides.assira.collection.lookups.HashLookup;
 import net.ranides.assira.collection.lookups.Lookup.LookupEntry;
-import net.ranides.assira.collection.maps.HashMap;
 import net.ranides.assira.collection.maps.AIntMap;
+import net.ranides.assira.collection.maps.IntHashMap;
 import net.ranides.assira.collection.maps.IntMap;
 import net.ranides.assira.collection.maps.IntMap.IntEntry;
 import net.ranides.assira.generic.CompareUtils;
@@ -407,36 +408,45 @@ public abstract class TMap<K,V> implements Serializable {
     public TMap<K,V> asIdentity() {
         return new TIdentityMap();
     }
-    
+
     private class TIdentityMap extends TMap<K, V> {
         
-        private final HashMap<Integer, TItem<K,V>> emap = new HashMap<>();
-        private final HashMap<K, Integer> imap = new HashMap<>();
+        private final IntHashMap<TItem<K,V>> items = new IntHashMap<>();
+        private final IntHashMap<K> keys = new IntHashMap<>();
+        private final HashLookup<K> lookup = new HashLookup<>();
+        private final IntHashMap<V> vals = new IntHashMap<>();
 
-        private TItem<K,V> iitem(int index, int n) {
+        @Override
+        public TItem<K, V> item(int index, int n) {
             if(n != 0) {
                 throw new UnsupportedOperationException("NO TEST");
             }
-            if(!emap.containsKey(index)) {
-                emap.put(index, TMap.this.item(index));
-                imap.put(emap.get(index).key(), index);
-            }
-            return emap.get(index);
+            return items.computeIfAbsent(index, TMap.this::item);
         }
-                
+
         @Override
         protected K key(int index, int n) {
-            return iitem(index, n).key();
+            if(n != 0) {
+                throw new UnsupportedOperationException("NO TEST");
+            }
+            return keys.computeIfAbsent(index, i -> {
+                K k = TMap.this.key(i);
+                lookup.put(k, i);
+                return k;
+            });
         }
 
         @Override
         protected V value(int index, int n) {
-            return iitem(index, n).value();
+            if(n != 0) {
+                throw new UnsupportedOperationException("NO TEST");
+            }
+            return vals.computeIfAbsent(index, TMap.this::value);
         }
 
         @Override
         protected int cmp(K a, K b) {
-            return imap.get(a) - imap.get(b);
+            return lookup.get(a) - lookup.get(b);
         }
 
         @Override

+ 27 - 0
assira/src/test/java/net/ranides/assira/collection/maps/WeakIdentMapTest.java

@@ -0,0 +1,27 @@
+package net.ranides.assira.collection.maps;
+
+import net.ranides.assira.ContractTesters;
+import net.ranides.assira.collection.mockup.TMaps;
+import net.ranides.assira.collection.mockup.TPoint;
+import net.ranides.assira.test.TMap;
+import org.junit.Test;
+
+import java.util.regex.Pattern;
+
+public class WeakIdentMapTest {
+
+    private final TMap<TPoint, Integer> $map = TMaps.MAP_PI.asIdentity();
+
+    @Test
+    public void testSuite() {
+        ContractTesters.runner()
+//                .debug(true)
+                .param("map!", $map)
+                .ignore(Pattern.compile(".*_HC"))
+                .ignore("MapTester.basicSerializable")
+
+                .function(new int[0], array -> $map.list(array).into(new WeakIdentMap<>()))
+                .run();
+    }
+
+}

+ 17 - 3
assira/src/test/java/net/ranides/assira/collection/suite/maps/MapTester.java

@@ -17,6 +17,7 @@ import java.util.function.Function;
 import java.util.function.Supplier;
 import javax.annotation.Resource;
 import net.ranides.assira.collection.sets.HashSet;
+import net.ranides.assira.math.RandomUtils;
 import net.ranides.assira.test.TMap;
 import net.ranides.assira.test.TMap.TItem;
 import net.ranides.assira.test.TMap.TItems;
@@ -198,14 +199,27 @@ public final class MapTester<K,V> {
     
     @TestContract
     public void basicGet(Function<int[], Map<K, V>> f) {
-        Map<K,V> target = f.apply(new int[]{1,2,3});
+        Map<K,V> target = f.apply(new int[]{1,2,3,4});
         
-        assertEquals(3, target.size());
-        assertEquals($default, target.get($var.item(4).key()));
+        assertEquals(4, target.size());
+        assertEquals($default, target.get($var.item(5).key()));
         
         assertEquals($var.item(1).value(), target.get($var.item(1).key()));
         assertEquals($var.item(2).value(), target.get($var.item(2).key()));
         assertEquals($var.item(3).value(), target.get($var.item(3).key()));
+        assertEquals($var.item(4).value(), target.get($var.item(4).key()));
+    }
+
+    @TestContract
+    public void randomGet(Function<int[], Map<K, V>> f) {
+        int[] points = RandomUtils.uniqueInts(new Random(17), 500, 100000, 200000).toIntArray();
+        Map<K,V> target = f.apply(points);
+
+        assertEquals(points.length, target.size());
+
+        for(int p : points) {
+            assertEquals($var.item(p).value(), target.get($var.item(p).key()));
+        }
     }
     
     @TestContract