• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.collect;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.base.Preconditions.checkNotNull;
21 import static com.google.common.base.Preconditions.checkState;
22 import static com.google.common.collect.CollectPreconditions.checkNonnegative;
23 import static com.google.common.collect.CollectPreconditions.checkRemove;
24 import static java.util.Objects.requireNonNull;
25 
26 import com.google.common.annotations.GwtCompatible;
27 import com.google.common.annotations.GwtIncompatible;
28 import com.google.common.annotations.J2ktIncompatible;
29 import com.google.common.primitives.Ints;
30 import com.google.errorprone.annotations.CanIgnoreReturnValue;
31 import java.io.InvalidObjectException;
32 import java.io.ObjectStreamException;
33 import java.io.Serializable;
34 import java.util.ConcurrentModificationException;
35 import java.util.Iterator;
36 import java.util.Map;
37 import java.util.Set;
38 import java.util.function.ObjIntConsumer;
39 import javax.annotation.CheckForNull;
40 import org.checkerframework.checker.nullness.qual.Nullable;
41 
42 /**
43  * Basic implementation of {@code Multiset<E>} backed by an instance of {@code Map<E, Count>}.
44  *
45  * <p>For serialization to work, the subclass must specify explicit {@code readObject} and {@code
46  * writeObject} methods.
47  *
48  * @author Kevin Bourrillion
49  */
50 @GwtCompatible(emulated = true)
51 @ElementTypesAreNonnullByDefault
52 abstract class AbstractMapBasedMultiset<E extends @Nullable Object> extends AbstractMultiset<E>
53     implements Serializable {
54   // TODO(lowasser): consider overhauling this back to Map<E, Integer>
55   private transient Map<E, Count> backingMap;
56 
57   /*
58    * Cache the size for efficiency. Using a long lets us avoid the need for
59    * overflow checking and ensures that size() will function correctly even if
60    * the multiset had once been larger than Integer.MAX_VALUE.
61    */
62   private transient long size;
63 
64   /** Standard constructor. */
AbstractMapBasedMultiset(Map<E, Count> backingMap)65   protected AbstractMapBasedMultiset(Map<E, Count> backingMap) {
66     checkArgument(backingMap.isEmpty());
67     this.backingMap = backingMap;
68   }
69 
70   /** Used during deserialization only. The backing map must be empty. */
setBackingMap(Map<E, Count> backingMap)71   void setBackingMap(Map<E, Count> backingMap) {
72     this.backingMap = backingMap;
73   }
74 
75   // Required Implementations
76 
77   /**
78    * {@inheritDoc}
79    *
80    * <p>Invoking {@link Multiset.Entry#getCount} on an entry in the returned set always returns the
81    * current count of that element in the multiset, as opposed to the count at the time the entry
82    * was retrieved.
83    */
84   @Override
entrySet()85   public Set<Multiset.Entry<E>> entrySet() {
86     return super.entrySet();
87   }
88 
89   @Override
elementIterator()90   Iterator<E> elementIterator() {
91     final Iterator<Map.Entry<E, Count>> backingEntries = backingMap.entrySet().iterator();
92     return new Iterator<E>() {
93       @CheckForNull Map.Entry<E, Count> toRemove;
94 
95       @Override
96       public boolean hasNext() {
97         return backingEntries.hasNext();
98       }
99 
100       @Override
101       @ParametricNullness
102       public E next() {
103         final Map.Entry<E, Count> mapEntry = backingEntries.next();
104         toRemove = mapEntry;
105         return mapEntry.getKey();
106       }
107 
108       @Override
109       public void remove() {
110         checkState(toRemove != null, "no calls to next() since the last call to remove()");
111         size -= toRemove.getValue().getAndSet(0);
112         backingEntries.remove();
113         toRemove = null;
114       }
115     };
116   }
117 
118   @Override
entryIterator()119   Iterator<Entry<E>> entryIterator() {
120     final Iterator<Map.Entry<E, Count>> backingEntries = backingMap.entrySet().iterator();
121     return new Iterator<Multiset.Entry<E>>() {
122       @CheckForNull Map.Entry<E, Count> toRemove;
123 
124       @Override
125       public boolean hasNext() {
126         return backingEntries.hasNext();
127       }
128 
129       @Override
130       public Multiset.Entry<E> next() {
131         final Map.Entry<E, Count> mapEntry = backingEntries.next();
132         toRemove = mapEntry;
133         return new Multisets.AbstractEntry<E>() {
134           @Override
135           @ParametricNullness
136           public E getElement() {
137             return mapEntry.getKey();
138           }
139 
140           @Override
141           public int getCount() {
142             Count count = mapEntry.getValue();
143             if (count == null || count.get() == 0) {
144               Count frequency = backingMap.get(getElement());
145               if (frequency != null) {
146                 return frequency.get();
147               }
148             }
149             return (count == null) ? 0 : count.get();
150           }
151         };
152       }
153 
154       @Override
155       public void remove() {
156         checkState(toRemove != null, "no calls to next() since the last call to remove()");
157         size -= toRemove.getValue().getAndSet(0);
158         backingEntries.remove();
159         toRemove = null;
160       }
161     };
162   }
163 
164   @Override
165   public void forEachEntry(ObjIntConsumer<? super E> action) {
166     checkNotNull(action);
167     backingMap.forEach((element, count) -> action.accept(element, count.get()));
168   }
169 
170   @Override
171   public void clear() {
172     for (Count frequency : backingMap.values()) {
173       frequency.set(0);
174     }
175     backingMap.clear();
176     size = 0L;
177   }
178 
179   @Override
180   int distinctElements() {
181     return backingMap.size();
182   }
183 
184   // Optimizations - Query Operations
185 
186   @Override
187   public int size() {
188     return Ints.saturatedCast(size);
189   }
190 
191   @Override
192   public Iterator<E> iterator() {
193     return new MapBasedMultisetIterator();
194   }
195 
196   /*
197    * Not subclassing AbstractMultiset$MultisetIterator because next() needs to
198    * retrieve the Map.Entry<E, Count> entry, which can then be used for
199    * a more efficient remove() call.
200    */
201   private class MapBasedMultisetIterator implements Iterator<E> {
202     final Iterator<Map.Entry<E, Count>> entryIterator;
203     @CheckForNull Map.Entry<E, Count> currentEntry;
204     int occurrencesLeft;
205     boolean canRemove;
206 
207     MapBasedMultisetIterator() {
208       this.entryIterator = backingMap.entrySet().iterator();
209     }
210 
211     @Override
212     public boolean hasNext() {
213       return occurrencesLeft > 0 || entryIterator.hasNext();
214     }
215 
216     @Override
217     @ParametricNullness
218     public E next() {
219       if (occurrencesLeft == 0) {
220         currentEntry = entryIterator.next();
221         occurrencesLeft = currentEntry.getValue().get();
222       }
223       occurrencesLeft--;
224       canRemove = true;
225       /*
226        * requireNonNull is safe because occurrencesLeft starts at 0, forcing us to initialize
227        * currentEntry above. After that, we never clear it.
228        */
229       return requireNonNull(currentEntry).getKey();
230     }
231 
232     @Override
233     public void remove() {
234       checkRemove(canRemove);
235       /*
236        * requireNonNull is safe because canRemove is set to true only after we initialize
237        * currentEntry (which we never subsequently clear).
238        */
239       int frequency = requireNonNull(currentEntry).getValue().get();
240       if (frequency <= 0) {
241         throw new ConcurrentModificationException();
242       }
243       if (currentEntry.getValue().addAndGet(-1) == 0) {
244         entryIterator.remove();
245       }
246       size--;
247       canRemove = false;
248     }
249   }
250 
251   @Override
252   public int count(@CheckForNull Object element) {
253     Count frequency = Maps.safeGet(backingMap, element);
254     return (frequency == null) ? 0 : frequency.get();
255   }
256 
257   // Optional Operations - Modification Operations
258 
259   /**
260    * {@inheritDoc}
261    *
262    * @throws IllegalArgumentException if the call would result in more than {@link
263    *     Integer#MAX_VALUE} occurrences of {@code element} in this multiset.
264    */
265   @CanIgnoreReturnValue
266   @Override
267   public int add(@ParametricNullness E element, int occurrences) {
268     if (occurrences == 0) {
269       return count(element);
270     }
271     checkArgument(occurrences > 0, "occurrences cannot be negative: %s", occurrences);
272     Count frequency = backingMap.get(element);
273     int oldCount;
274     if (frequency == null) {
275       oldCount = 0;
276       backingMap.put(element, new Count(occurrences));
277     } else {
278       oldCount = frequency.get();
279       long newCount = (long) oldCount + (long) occurrences;
280       checkArgument(newCount <= Integer.MAX_VALUE, "too many occurrences: %s", newCount);
281       frequency.add(occurrences);
282     }
283     size += occurrences;
284     return oldCount;
285   }
286 
287   @CanIgnoreReturnValue
288   @Override
289   public int remove(@CheckForNull Object element, int occurrences) {
290     if (occurrences == 0) {
291       return count(element);
292     }
293     checkArgument(occurrences > 0, "occurrences cannot be negative: %s", occurrences);
294     Count frequency = backingMap.get(element);
295     if (frequency == null) {
296       return 0;
297     }
298 
299     int oldCount = frequency.get();
300 
301     int numberRemoved;
302     if (oldCount > occurrences) {
303       numberRemoved = occurrences;
304     } else {
305       numberRemoved = oldCount;
306       backingMap.remove(element);
307     }
308 
309     frequency.add(-numberRemoved);
310     size -= numberRemoved;
311     return oldCount;
312   }
313 
314   // Roughly a 33% performance improvement over AbstractMultiset.setCount().
315   @CanIgnoreReturnValue
316   @Override
317   public int setCount(@ParametricNullness E element, int count) {
318     checkNonnegative(count, "count");
319 
320     Count existingCounter;
321     int oldCount;
322     if (count == 0) {
323       existingCounter = backingMap.remove(element);
324       oldCount = getAndSet(existingCounter, count);
325     } else {
326       existingCounter = backingMap.get(element);
327       oldCount = getAndSet(existingCounter, count);
328 
329       if (existingCounter == null) {
330         backingMap.put(element, new Count(count));
331       }
332     }
333 
334     size += (count - oldCount);
335     return oldCount;
336   }
337 
338   private static int getAndSet(@CheckForNull Count i, int count) {
339     if (i == null) {
340       return 0;
341     }
342 
343     return i.getAndSet(count);
344   }
345 
346   // Don't allow default serialization.
347   @GwtIncompatible // java.io.ObjectStreamException
348   @J2ktIncompatible
349   private void readObjectNoData() throws ObjectStreamException {
350     throw new InvalidObjectException("Stream data required");
351   }
352 
353   @GwtIncompatible // not needed in emulated source.
354   @J2ktIncompatible
355   private static final long serialVersionUID = -2250766705698539974L;
356 }
357