瀏覽代碼

new: MapUtils#lazy
new: Cache

Ranides Atterwim 10 年之前
父節點
當前提交
4d8071c807

+ 282 - 0
assira/src/main/java/net/ranides/assira/collection/maps/Cache.java

@@ -0,0 +1,282 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.collection.maps;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.util.AbstractMap;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p>
+ * Kolekcja zaprojektowana jako podręczna pamięć na obiekty, których koszt 
+ * utworzenia jest relatywnie wysoki. Kolekcja przechowuje {@code holdsize} 
+ * ostatnio używanych elementów, bez względu na to, czy istnieją inne referencje 
+ * do nich. Przez "użycie" rozumiemy dowolną operację zapis lub odczytu. 
+ * W szczególności taka strategia pozwala polegać na metodzie "containsKey", 
+ * która uchroni wartość przed natychmiastowym usunięciem - jeśli tylko stwierdzono, 
+ * że klucz istnieje. Dodatkowo kolekcja przechowuje {@code cachesize} elementów 
+ * za pomocą WeakReference - obiekty te mogą zostać zwolnione przez GC, jeśli 
+ * zajdzie taka potrzeba.
+ * </p><p>
+ * Uwaga: wszystkie widoki zwracane przez "cache" są niebezpieczne w tym sensie, 
+ * że nie można wywoływać <b>żadnej</b> metody na obiekcie "cache" w czasie 
+ * iterowania po widoku. Jest to spowodowane tym, że cache uwzględnia czas dostępu 
+ * do obiektów: nawet odczyt zmienia wewnętrzną reprezentację. W efekcie, jeśli 
+ * podczas iteracji zostanie wywołana jakakolwiek inna metoda, to zostanie rzucone 
+ * ConcurentModificationException. 
+ *  * 
+ * @todo (assira #6) issue: holdlist contains duplicate entries
+ *        if you invoke #get(key) x times, it will put x "key" entries inside list
+ * </p>
+ * <div class="message-note">thread-safe class</div>
+ * @param <K> 
+ * @param <V> 
+ * @author ranides
+ */
+@SuppressWarnings("element-type-mismatch")
+public class Cache<K,V> extends AbstractMap<K,V> {
+
+    private final CacheMap<K,V> map;
+
+    private final CacheList<V> list;
+    
+    private final Map<K,V> view;
+    
+    private final ReferenceQueue<V> queue = new ReferenceQueue<>();
+
+    /**
+     * Tworzy cache, który przechowuje maksymalnie 1024 elementy, w tym 32 
+     * ostatnio użyte są chronione przez usunięciem przez GC.
+     */
+    public Cache() {
+        this(1024, 32);
+        
+    }
+    
+    /**
+     * Tworzy cache, którzy przechowuje {@code cachesize} ostatnio używanych 
+     * elementów, bez względu na to, czy istnieją do nich inne referencje.
+     * @param cachesize ilość przechowywanych elementów
+     */
+    public Cache(int cachesize) {
+        this(cachesize, cachesize);
+    }
+
+    /**
+     * Tworzy cache, którzy przechowuje {@code cachesize} ostatnio używanych 
+     * elementów, w tym chroni przez usunięciem przez GC ostatnie {@code holdsize}
+     * elementów.
+     * @param cachesize ilość przechowywanych elementów
+     * @param holdsize ilość elementów chronionych przed GC
+     */
+    public Cache(int cachesize, int holdsize) {
+        if( cachesize < holdsize ) {
+            throw new IllegalArgumentException("cachesize must be greater or equal to holdsize");
+        }
+        this.list   = new CacheList<>(Math.max(1,holdsize));
+        this.map    = new CacheMap<>(cachesize);
+        this.view   = MapUtils.map(map, (v)->asValue(v));
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        synchronized(this) {
+            return null != get(key);
+        }
+    }
+
+    @Override
+    public V get(Object key) {
+        synchronized(this) {
+            Reference<V> ref = map.get(key);
+            if(ref == null) {
+                return null;
+            }
+            // From the SoftReference we get the value, which can be
+            // null if it was removed in the processQueue() method defined below
+            V result = ref.get();
+            if (result == null) {
+                map.remove(key);
+            } else {
+                // hold value for a while...
+                list.hold(result);
+            }
+            return result;
+        }
+    }
+    
+    /**
+     * Wstawia wartość do cache'u i zwraca ją jako wynik. Jeśli w mapie
+     * istniała już jakaś wartość, to ją zastępuje.
+     * @param key
+     * @param value
+     * @return 
+     */
+    public V pass(K key, V value) {
+        synchronized(this) {
+            release();
+            map.put(key, new ReferenceEntry<>(key, value, queue));
+            return value;
+        }
+    }
+
+    @Override
+    public V put(K key, V value) {
+        synchronized(this) {
+            release();
+            V prev = asValue(map.get(key));
+            map.put(key, new ReferenceEntry<>(key, value, queue));
+            return prev;
+        }
+    }
+
+    @Override
+    public V remove(Object key) {
+        synchronized(this) {
+            release();
+            return asValue(map.remove(key));
+        }
+    }
+
+    @Override
+    public void clear() {
+        synchronized(this) {
+            list.clear();
+            release();
+            map.clear();
+        }
+    }
+
+    @Override
+    public int size() {
+        synchronized(this) {
+            release();
+            return map.size();
+        }
+    }
+    
+    /**
+     * Returns number of holded entries
+     * Please note, that holdlist could contain duplicated entries, 
+     * so returned number could be deceptively greater than size of map. 
+     * @return
+     */
+    public int holdsize() {
+        synchronized(this) {
+            release();
+            return list.size();
+        }
+    }
+    
+    /**
+     * Holds value specified by key
+     * @param key
+     * @return 
+     */
+    public boolean hold(K key) {
+        synchronized(this) {
+            return null != get(key);
+        }
+    }
+    
+    /**
+     * Holds as many entries as possible
+     * @return number of holded values
+     */
+    public int hold() {
+        synchronized(this) {
+            for(V value : values() ) {
+                if(null != value) {
+                    list.hold(value);
+                }
+            }
+            return list.size();
+        }
+    }
+    
+    @Override
+    public Set<Entry<K,V>> entrySet() {
+        release();
+        return view.entrySet();
+    }
+
+    @Override
+    public Set<K> keySet() {
+        release();
+        return view.keySet();
+    }
+
+    @Override
+    public Collection<V> values() {
+        release();
+        return view.values();
+    }
+    
+    private void release() {
+        synchronized(this) {
+            Reference<? extends V> ref;
+            while ( null != (ref = queue.poll()) ) { map.remove( asKey(ref) ); }
+        }
+    }
+    
+    private static <TV> TV asValue(Reference<? extends TV> ref) {
+        return (ref != null) ? ref.get() : null;
+    }
+    
+    @SuppressWarnings("unchecked")
+    private static <TK> TK asKey(Reference<?> ref) {
+        return (ref != null) ? ((ReferenceEntry<TK,?>)ref).key : null;
+    }
+    
+    private static class ReferenceEntry<KT,VT> extends SoftReference<VT> {
+        
+        private final KT key;
+
+        public ReferenceEntry(KT key, VT value, ReferenceQueue<VT> queue) {
+            super(value, queue);
+            this.key = key;
+        }
+    }
+    
+    private static class CacheMap<TK,TV> extends LinkedHashMap<TK, Reference<TV>> {
+        
+        private final int maxsize;
+
+        public CacheMap(int maxsize) {
+            super( Math.min(Math.round(maxsize / 0.75f)+2, 2048), 0.75f, true);
+            this.maxsize = maxsize;
+        }
+        
+        @Override
+        protected boolean removeEldestEntry(Entry<TK, Reference<TV>> eldest) {
+            return size() > maxsize;
+        }
+        
+    };
+    
+    private static class CacheList<TV> extends ArrayDeque<TV> {
+        
+        private final int maxsize;
+
+        public CacheList(int maxsize) {
+            this.maxsize = maxsize;
+        }
+        
+        public void hold(TV value) {
+            addFirst(value);
+            if (size() > maxsize) { removeLast(); }
+        }
+        
+    }
+    
+}

+ 65 - 0
assira/src/main/java/net/ranides/assira/collection/maps/MapUtils.java

@@ -7,9 +7,12 @@
 package net.ranides.assira.collection.maps;
 
 import java.util.Collection;
+import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import net.ranides.assira.collection.CollectionUtils;
 import net.ranides.assira.collection.sets.SetUtils;
 import net.ranides.assira.functional.ProjectionFunction;
@@ -33,6 +36,10 @@ public final class MapUtils {
         return new MapProjection<>(map, function);
     }
     
+    public static <K,V> Map<K,V> lazy(Map<K,V> map, Supplier<V> supplier) {
+        return new LazyMap<>(map, supplier);
+    }
+    
     private static class MapAdapter<K, TV, RV> extends AMap<K, RV> implements Map<K, RV> {
 
         private final Map<K, TV> content;
@@ -213,4 +220,62 @@ public final class MapUtils {
 
     }
     
+    private static class LazyMap<K,V> extends AMap<K,V> {
+        
+        private final Map<K,V> data;
+        
+        private final Supplier<V> supplier;
+
+        public LazyMap(Map<K, V> data, Supplier<V> supplier) {
+            this.data = data;
+            this.supplier = supplier;
+        }
+
+        @Override
+        public Set<Entry<K, V>> entrySet() {
+            return data.entrySet();
+        }
+
+        @Override
+        public int size() {
+            return data.size();
+        }
+
+        @Override
+        public boolean containsKey(Object key) {
+            return data.containsKey(key);
+        }
+
+        @Override
+        public boolean containsValue(Object value) {
+            return data.containsValue(value);
+        }
+
+        @Override
+        @SuppressWarnings("element-type-mismatch")
+        public V get(Object key) {
+            V ret = data.get(key);
+            if(null == ret && !data.containsKey(key)) {
+                data.put((K)key, ret = supplier.get());
+            }
+            return ret;
+        }
+
+        @Override
+        public void clear() {
+            data.clear();
+        }
+
+        @Override
+        public V remove(Object key) {
+            return data.remove(key);
+        }
+
+        @Override
+        public V put(K key, V value) {
+            return data.put(key, value);
+        }
+        
+    }
+    
 }