View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.collections4.map;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.ByteArrayOutputStream;
21  import java.io.IOException;
22  import java.io.ObjectInputStream;
23  import java.io.ObjectOutputStream;
24  import java.io.Serializable;
25  import java.lang.ref.WeakReference;
26  import java.util.ArrayList;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.function.Consumer;
30  
31  import org.apache.commons.collections4.BulkTest;
32  import org.apache.commons.collections4.map.AbstractHashedMap.HashEntry;
33  import org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceEntry;
34  import org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceStrength;
35  
36  import junit.framework.Test;
37  
38  /**
39   * Tests for ReferenceMap.
40   *
41   */
42  public class ReferenceMapTest<K, V> extends AbstractIterableMapTest<K, V> {
43  
44      public ReferenceMapTest(final String testName) {
45          super(testName);
46      }
47  
48      public static Test suite() {
49          return BulkTest.makeSuite(ReferenceMapTest.class);
50      }
51  
52      @Override
53      public ReferenceMap<K, V> makeObject() {
54          return new ReferenceMap<>(ReferenceStrength.WEAK, ReferenceStrength.WEAK);
55      }
56  
57      @Override
58      public boolean isAllowNullKey() {
59          return false;
60      }
61  
62      @Override
63      public boolean isAllowNullValue() {
64          return false;
65      }
66  
67      @Override
68      public String getCompatibilityVersion() {
69          return "4";
70      }
71  
72  //    public void testCreate() throws Exception {
73  //        resetEmpty();
74  //        writeExternalFormToDisk(
75  //            (java.io.Serializable) map,
76  //            "src/test/resources/data/test/ReferenceMap.emptyCollection.version4.obj");
77  //        resetFull();
78  //        writeExternalFormToDisk(
79  //            (java.io.Serializable) map,
80  //            "src/test/resources/data/test/ReferenceMap.fullCollection.version4.obj");
81  //    }
82  
83      //-----------------------------------------------------------------------
84      @SuppressWarnings("unchecked")
85      public void testNullHandling() {
86          resetFull();
87          assertEquals(null, map.get(null));
88          assertEquals(false, map.containsKey(null));
89          assertEquals(false, map.containsValue(null));
90          assertEquals(null, map.remove(null));
91          assertEquals(false, map.entrySet().contains(null));
92          assertEquals(false, map.keySet().contains(null));
93          assertEquals(false, map.values().contains(null));
94          try {
95              map.put(null, null);
96              fail();
97          } catch (final NullPointerException ex) {}
98          try {
99              map.put((K) new Object(), null);
100             fail();
101         } catch (final NullPointerException ex) {}
102         try {
103             map.put(null, (V) new Object());
104             fail();
105         } catch (final NullPointerException ex) {}
106     }
107 
108     //-----------------------------------------------------------------------
109 /*
110     // Tests often fail because gc is uncontrollable
111 
112     public void testPurge() {
113         ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
114         Object[] hard = new Object[10];
115         for (int i = 0; i < hard.length; i++) {
116             hard[i] = new Object();
117             map.put(hard[i], new Object());
118         }
119         gc();
120         assertTrue("map should be empty after purge of weak values", map.isEmpty());
121 
122         for (int i = 0; i < hard.length; i++) {
123             map.put(new Object(), hard[i]);
124         }
125         gc();
126         assertTrue("map should be empty after purge of weak keys", map.isEmpty());
127 
128         for (int i = 0; i < hard.length; i++) {
129             map.put(new Object(), hard[i]);
130             map.put(hard[i], new Object());
131         }
132 
133         gc();
134         assertTrue("map should be empty after purge of weak keys and values", map.isEmpty());
135     }
136 
137 
138     public void testGetAfterGC() {
139         ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
140         for (int i = 0; i < 10; i++) {
141             map.put(Integer.valueOf(i), Integer.valueOf(i));
142         }
143 
144         gc();
145         for (int i = 0; i < 10; i++) {
146             Integer I = Integer.valueOf(i);
147             assertTrue("map.containsKey should return false for GC'd element", !map.containsKey(I));
148             assertTrue("map.get should return null for GC'd element", map.get(I) == null);
149         }
150     }
151 
152 
153     public void testEntrySetIteratorAfterGC() {
154         ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
155         Object[] hard = new Object[10];
156         for (int i = 0; i < 10; i++) {
157             hard[i] = Integer.valueOf(10 + i);
158             map.put(Integer.valueOf(i), Integer.valueOf(i));
159             map.put(hard[i], hard[i]);
160         }
161 
162         gc();
163         Iterator iterator = map.entrySet().iterator();
164         while (iterator.hasNext()) {
165             Map.Entry entry = (Map.Entry)iterator.next();
166             Integer key = (Integer)entry.getKey();
167             Integer value = (Integer)entry.getValue();
168             assertTrue("iterator should skip GC'd keys", key.intValue() >= 10);
169             assertTrue("iterator should skip GC'd values", value.intValue() >= 10);
170         }
171 
172     }
173 
174     public void testMapIteratorAfterGC() {
175         ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
176         Object[] hard = new Object[10];
177         for (int i = 0; i < 10; i++) {
178             hard[i] = Integer.valueOf(10 + i);
179             map.put(Integer.valueOf(i), Integer.valueOf(i));
180             map.put(hard[i], hard[i]);
181         }
182 
183         gc();
184         MapIterator iterator = map.mapIterator();
185         while (iterator.hasNext()) {
186             Object key1 = iterator.next();
187             Integer key = (Integer) iterator.getKey();
188             Integer value = (Integer) iterator.getValue();
189             assertTrue("iterator keys should match", key == key1);
190             assertTrue("iterator should skip GC'd keys", key.intValue() >= 10);
191             assertTrue("iterator should skip GC'd values", value.intValue() >= 10);
192         }
193 
194     }
195 
196     public void testMapIteratorAfterGC2() {
197         ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
198         Object[] hard = new Object[10];
199         for (int i = 0; i < 10; i++) {
200             hard[i] = Integer.valueOf(10 + i);
201             map.put(Integer.valueOf(i), Integer.valueOf(i));
202             map.put(hard[i], hard[i]);
203         }
204 
205         MapIterator iterator = map.mapIterator();
206         while (iterator.hasNext()) {
207             Object key1 = iterator.next();
208             gc();
209             Integer key = (Integer) iterator.getKey();
210             Integer value = (Integer) iterator.getValue();
211             assertTrue("iterator keys should match", key == key1);
212             assertTrue("iterator should skip GC'd keys", key.intValue() >= 10);
213             assertTrue("iterator should skip GC'd values", value.intValue() >= 10);
214         }
215 
216     }
217 */
218 
219     WeakReference<K> keyReference;
220     WeakReference<V> valueReference;
221 
222     @SuppressWarnings("unchecked")
223     public Map<K, V> buildRefMap() {
224         final K key = (K) new Object();
225         final V value = (V) new Object();
226 
227         keyReference = new WeakReference<>(key);
228         valueReference = new WeakReference<>(value);
229 
230         final Map<K, V> testMap = new ReferenceMap<>(ReferenceStrength.WEAK, ReferenceStrength.HARD, true);
231         testMap.put(key, value);
232 
233         assertEquals("In map", value, testMap.get(key));
234         assertNotNull("Weak reference released early (1)", keyReference.get());
235         assertNotNull("Weak reference released early (2)", valueReference.get());
236         return testMap;
237     }
238 
239     /** Tests whether purge values setting works */
240     public void testPurgeValues() throws Exception {
241         // many thanks to Juozas Baliuka for suggesting this method
242         final Map<K, V> testMap = buildRefMap();
243 
244         int iterations = 0;
245         int bytz = 2;
246         while (true) {
247             System.gc();
248             if (iterations++ > 50) {
249                 fail("Max iterations reached before resource released.");
250             }
251             testMap.isEmpty();
252             if (keyReference.get() == null && valueReference.get() == null) {
253                 break;
254 
255             }
256             // create garbage:
257             @SuppressWarnings("unused")
258             final byte[] b = new byte[bytz];
259             bytz = bytz * 2;
260         }
261     }
262 
263     public void testCustomPurge() {
264         List<Integer> expiredValues = new ArrayList<>();
265         @SuppressWarnings("unchecked")
266         final Consumer<Integer> consumer = (Consumer<Integer> & Serializable) v -> expiredValues.add(v);
267         final Map<Integer, Integer> map = new ReferenceMap<Integer, Integer>(ReferenceStrength.WEAK, ReferenceStrength.HARD, false) {
268             private static final long serialVersionUID = 1L;
269 
270             @Override
271             protected ReferenceEntry<Integer, Integer> createEntry(HashEntry<Integer, Integer> next, int hashCode, Integer key, Integer value) {
272                 return new AccessibleEntry<>(this, next, hashCode, key, value, consumer);
273             }
274         };
275         for (int i = 100000; i < 100010; i++) {
276             map.put(Integer.valueOf(i), Integer.valueOf(i));
277         }
278         int iterations = 0;
279         int bytz = 2;
280         while (true) {
281             System.gc();
282             if (iterations++ > 50 || bytz < 0) {
283                 fail("Max iterations reached before resource released.");
284             }
285             map.isEmpty();
286             if (!expiredValues.isEmpty()) {
287                 break;
288             }
289             // create garbage:
290             @SuppressWarnings("unused")
291             final byte[] b = new byte[bytz];
292             bytz = bytz * 2;
293         }
294         assertFalse("Value should be stored", expiredValues.isEmpty());
295     }
296 
297     /**
298      * Test whether after serialization the "data" HashEntry array is the same size as the original.<p>
299      *
300      * See <a href="https://issues.apache.org/jira/browse/COLLECTIONS-599">COLLECTIONS-599: HashEntry array object naming data initialized with double the size during deserialization</a>
301      */
302     public void testDataSizeAfterSerialization() throws IOException, ClassNotFoundException {
303 
304         final ReferenceMap<String,String> serialiseMap = new ReferenceMap<>(ReferenceStrength.WEAK, ReferenceStrength.WEAK, true);
305         serialiseMap.put("KEY", "VALUE");
306 
307         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
308         try (ObjectOutputStream out = new ObjectOutputStream(baos)) {
309             out.writeObject(serialiseMap);
310         }
311 
312         final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
313         try (ObjectInputStream in = new ObjectInputStream(bais)) {
314             @SuppressWarnings("unchecked")
315             final
316             ReferenceMap<String,String> deserialisedMap = (ReferenceMap<String,String>) in.readObject();
317             assertEquals(1, deserialisedMap.size());
318             assertEquals(serialiseMap.data.length, deserialisedMap.data.length);
319         }
320 
321     }
322 
323     @SuppressWarnings("unused")
324     private static void gc() {
325         try {
326             // trigger GC
327             final byte[][] tooLarge = new byte[1000000000][1000000000];
328             fail("you have too much RAM");
329         } catch (final OutOfMemoryError ex) {
330             System.gc(); // ignore
331         }
332     }
333 
334     private static class AccessibleEntry<K, V> extends ReferenceEntry<K, V> {
335         final AbstractReferenceMap<K, V> parent;
336         final Consumer<V> consumer;
337 
338         public AccessibleEntry(final AbstractReferenceMap<K, V> parent, final HashEntry<K, V> next, final int hashCode, final K key, final V value, final Consumer<V> consumer) {
339             super(parent, next, hashCode, key, value);
340             this.parent = parent;
341             this.consumer = consumer;
342         }
343 
344         @Override
345         protected void onPurge() {
346             if (parent.isValueType(ReferenceStrength.HARD)) {
347                 consumer.accept(getValue());
348             }
349         }
350     }
351 }