Home | History | Annotate | Download | only in collect
      1 /*
      2  * Copyright (C) 2007 Google Inc.
      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 com.google.common.annotations.VisibleForTesting;
     20 import static com.google.common.base.Preconditions.checkArgument;
     21 import static com.google.common.collect.Multisets.checkNonnegative;
     22 import com.google.common.collect.Serialization.FieldSetter;
     23 
     24 import java.io.IOException;
     25 import java.io.ObjectInputStream;
     26 import java.io.ObjectOutputStream;
     27 import java.io.Serializable;
     28 import java.util.AbstractSet;
     29 import java.util.Iterator;
     30 import java.util.List;
     31 import java.util.Map;
     32 import java.util.Set;
     33 import java.util.concurrent.ConcurrentHashMap;
     34 import java.util.concurrent.ConcurrentMap;
     35 
     36 import javax.annotation.Nullable;
     37 
     38 /**
     39  * A multiset that supports concurrent modifications and that provides atomic
     40  * versions of most {@code Multiset} operations (exceptions where noted). Null
     41  * elements are not supported.
     42  *
     43  * @author Cliff L. Biffle
     44  * @since 2010.01.04 <b>stable</b> (imported from Google Collections Library)
     45  */
     46 public final class ConcurrentHashMultiset<E> extends AbstractMultiset<E>
     47     implements Serializable {
     48   /*
     49    * The ConcurrentHashMultiset's atomic operations are implemented in terms of
     50    * ConcurrentMap's atomic operations. Many of them, such as add(E, int), are
     51    * read-modify-write sequences, and so are implemented as loops that wrap
     52    * ConcurrentMap's compare-and-set operations (like putIfAbsent).
     53    */
     54 
     55   /** The number of occurrences of each element. */
     56   private final transient ConcurrentMap<E, Integer> countMap;
     57 
     58   // This constant allows the deserialization code to set a final field. This
     59   // holder class makes sure it is not initialized unless an instance is
     60   // deserialized.
     61   private static class FieldSettersHolder {
     62     @SuppressWarnings("unchecked")
     63     // eclipse doesn't like the raw type here, but it's harmless
     64     static final FieldSetter<ConcurrentHashMultiset> COUNT_MAP_FIELD_SETTER
     65         = Serialization.getFieldSetter(
     66             ConcurrentHashMultiset.class, "countMap");
     67   }
     68 
     69   /**
     70    * Creates a new, empty {@code ConcurrentHashMultiset} using the default
     71    * initial capacity, load factor, and concurrency settings.
     72    */
     73   public static <E> ConcurrentHashMultiset<E> create() {
     74     return new ConcurrentHashMultiset<E>(new ConcurrentHashMap<E, Integer>());
     75   }
     76 
     77   /**
     78    * Creates a new {@code ConcurrentHashMultiset} containing the specified
     79    * elements, using the default initial capacity, load factor, and concurrency
     80    * settings.
     81    *
     82    * @param elements the elements that the multiset should contain
     83    */
     84   public static <E> ConcurrentHashMultiset<E> create(
     85       Iterable<? extends E> elements) {
     86     ConcurrentHashMultiset<E> multiset = ConcurrentHashMultiset.create();
     87     Iterables.addAll(multiset, elements);
     88     return multiset;
     89   }
     90 
     91   /**
     92    * Creates an instance using {@code countMap} to store elements and their
     93    * counts.
     94    *
     95    * <p>This instance will assume ownership of {@code countMap}, and other code
     96    * should not maintain references to the map or modify it in any way.
     97    *
     98    * @param countMap backing map for storing the elements in the multiset and
     99    *     their counts. It must be empty.
    100    * @throws IllegalArgumentException if {@code countMap} is not empty
    101    */
    102   @VisibleForTesting ConcurrentHashMultiset(
    103       ConcurrentMap<E, Integer> countMap) {
    104     checkArgument(countMap.isEmpty());
    105     this.countMap = countMap;
    106   }
    107 
    108   // Query Operations
    109 
    110   /**
    111    * Returns the number of occurrences of {@code element} in this multiset.
    112    *
    113    * @param element the element to look for
    114    * @return the nonnegative number of occurrences of the element
    115    */
    116   @Override public int count(@Nullable Object element) {
    117     try {
    118       return unbox(countMap.get(element));
    119     } catch (NullPointerException e) {
    120       return 0;
    121     } catch (ClassCastException e) {
    122       return 0;
    123     }
    124   }
    125 
    126   /**
    127    * {@inheritDoc}
    128    *
    129    * <p>If the data in the multiset is modified by any other threads during this
    130    * method, it is undefined which (if any) of these modifications will be
    131    * reflected in the result.
    132    */
    133   @Override public int size() {
    134     long sum = 0L;
    135     for (Integer value : countMap.values()) {
    136       sum += value;
    137     }
    138     return (int) Math.min(sum, Integer.MAX_VALUE);
    139   }
    140 
    141   /*
    142    * Note: the superclass toArray() methods assume that size() gives a correct
    143    * answer, which ours does not.
    144    */
    145 
    146   @Override public Object[] toArray() {
    147     return snapshot().toArray();
    148   }
    149 
    150   @Override public <T> T[] toArray(T[] array) {
    151     return snapshot().toArray(array);
    152   }
    153 
    154   /*
    155    * We'd love to use 'new ArrayList(this)' or 'list.addAll(this)', but
    156    * either of these would recurse back to us again!
    157    */
    158   private List<E> snapshot() {
    159     List<E> list = Lists.newArrayListWithExpectedSize(size());
    160     for (Multiset.Entry<E> entry : entrySet()) {
    161       E element = entry.getElement();
    162       for (int i = entry.getCount(); i > 0; i--) {
    163         list.add(element);
    164       }
    165     }
    166     return list;
    167   }
    168 
    169   // Modification Operations
    170 
    171   /**
    172    * Adds a number of occurrences of the specified element to this multiset.
    173    *
    174    * @param element the element to add
    175    * @param occurrences the number of occurrences to add
    176    * @return the previous count of the element before the operation; possibly
    177    *     zero
    178    * @throws IllegalArgumentException if {@code occurrences} is negative, or if
    179    *     the resulting amount would exceed {@link Integer#MAX_VALUE}
    180    */
    181   @Override public int add(E element, int occurrences) {
    182     if (occurrences == 0) {
    183       return count(element);
    184     }
    185     checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
    186 
    187     while (true) {
    188       int current = count(element);
    189       if (current == 0) {
    190         if (countMap.putIfAbsent(element, occurrences) == null) {
    191           return 0;
    192         }
    193       } else {
    194         checkArgument(occurrences <= Integer.MAX_VALUE - current,
    195             "Overflow adding %s occurrences to a count of %s",
    196             occurrences, current);
    197         int next = current + occurrences;
    198         if (countMap.replace(element, current, next)) {
    199           return current;
    200         }
    201       }
    202       // If we're still here, there was a race, so just try again.
    203     }
    204   }
    205 
    206   /**
    207    * Removes a number of occurrences of the specified element from this
    208    * multiset. If the multiset contains fewer than this number of occurrences to
    209    * begin with, all occurrences will be removed.
    210    *
    211    * @param element the element whose occurrences should be removed
    212    * @param occurrences the number of occurrences of the element to remove
    213    * @return the count of the element before the operation; possibly zero
    214    * @throws IllegalArgumentException if {@code occurrences} is negative
    215    */
    216   @Override public int remove(@Nullable Object element, int occurrences) {
    217     if (occurrences == 0) {
    218       return count(element);
    219     }
    220     checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
    221 
    222     while (true) {
    223       int current = count(element);
    224       if (current == 0) {
    225         return 0;
    226       }
    227       if (occurrences >= current) {
    228         if (countMap.remove(element, current)) {
    229           return current;
    230         }
    231       } else {
    232         // We know it's an "E" because it already exists in the map.
    233         @SuppressWarnings("unchecked")
    234         E casted = (E) element;
    235 
    236         if (countMap.replace(casted, current, current - occurrences)) {
    237           return current;
    238         }
    239       }
    240       // If we're still here, there was a race, so just try again.
    241     }
    242   }
    243 
    244   /**
    245    * Removes <b>all</b> occurrences of the specified element from this multiset.
    246    * This method complements {@link Multiset#remove(Object)}, which removes only
    247    * one occurrence at a time.
    248    *
    249    * @param element the element whose occurrences should all be removed
    250    * @return the number of occurrences successfully removed, possibly zero
    251    */
    252   private int removeAllOccurrences(@Nullable Object element) {
    253     try {
    254       return unbox(countMap.remove(element));
    255     } catch (NullPointerException e) {
    256       return 0;
    257     } catch (ClassCastException e) {
    258       return 0;
    259     }
    260   }
    261 
    262   /**
    263    * Removes exactly the specified number of occurrences of {@code element}, or
    264    * makes no change if this is not possible.
    265    *
    266    * <p>This method, in contrast to {@link #remove(Object, int)}, has no effect
    267    * when the element count is smaller than {@code occurrences}.
    268    *
    269    * @param element the element to remove
    270    * @param occurrences the number of occurrences of {@code element} to remove
    271    * @return {@code true} if the removal was possible (including if {@code
    272    *     occurrences} is zero)
    273    */
    274   public boolean removeExactly(@Nullable Object element, int occurrences) {
    275     if (occurrences == 0) {
    276       return true;
    277     }
    278     checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
    279 
    280     while (true) {
    281       int current = count(element);
    282       if (occurrences > current) {
    283         return false;
    284       }
    285       if (occurrences == current) {
    286         if (countMap.remove(element, occurrences)) {
    287           return true;
    288         }
    289       } else {
    290         @SuppressWarnings("unchecked") // it's in the map, must be an "E"
    291         E casted = (E) element;
    292         if (countMap.replace(casted, current, current - occurrences)) {
    293           return true;
    294         }
    295       }
    296       // If we're still here, there was a race, so just try again.
    297     }
    298   }
    299 
    300   /**
    301    * Adds or removes occurrences of {@code element} such that the {@link #count}
    302    * of the element becomes {@code count}.
    303    *
    304    * @return the count of {@code element} in the multiset before this call
    305    * @throws IllegalArgumentException if {@code count} is negative
    306    */
    307   @Override public int setCount(E element, int count) {
    308     checkNonnegative(count, "count");
    309     return (count == 0)
    310         ? removeAllOccurrences(element)
    311         : unbox(countMap.put(element, count));
    312   }
    313 
    314   /**
    315    * Sets the number of occurrences of {@code element} to {@code newCount}, but
    316    * only if the count is currently {@code oldCount}. If {@code element} does
    317    * not appear in the multiset exactly {@code oldCount} times, no changes will
    318    * be made.
    319    *
    320    * @return {@code true} if the change was successful. This usually indicates
    321    *     that the multiset has been modified, but not always: in the case that
    322    *     {@code oldCount == newCount}, the method will return {@code true} if
    323    *     the condition was met.
    324    * @throws IllegalArgumentException if {@code oldCount} or {@code newCount} is
    325    *     negative
    326    */
    327   @Override public boolean setCount(E element, int oldCount, int newCount) {
    328     checkNonnegative(oldCount, "oldCount");
    329     checkNonnegative(newCount, "newCount");
    330     if (newCount == 0) {
    331       if (oldCount == 0) {
    332         // No change to make, but must return true if the element is not present
    333         return !countMap.containsKey(element);
    334       } else {
    335         return countMap.remove(element, oldCount);
    336       }
    337     }
    338     if (oldCount == 0) {
    339       return countMap.putIfAbsent(element, newCount) == null;
    340     }
    341     return countMap.replace(element, oldCount, newCount);
    342   }
    343 
    344   // Views
    345 
    346   @Override Set<E> createElementSet() {
    347     final Set<E> delegate = countMap.keySet();
    348     return new ForwardingSet<E>() {
    349       @Override protected Set<E> delegate() {
    350         return delegate;
    351       }
    352       @Override public boolean remove(Object object) {
    353         try {
    354           return delegate.remove(object);
    355         } catch (NullPointerException e) {
    356           return false;
    357         } catch (ClassCastException e) {
    358           return false;
    359         }
    360       }
    361     };
    362   }
    363 
    364   private transient EntrySet entrySet;
    365 
    366   @Override public Set<Multiset.Entry<E>> entrySet() {
    367     EntrySet result = entrySet;
    368     if (result == null) {
    369       entrySet = result = new EntrySet();
    370     }
    371     return result;
    372   }
    373 
    374   private class EntrySet extends AbstractSet<Multiset.Entry<E>> {
    375     @Override public int size() {
    376       return countMap.size();
    377     }
    378 
    379     @Override public boolean isEmpty() {
    380       return countMap.isEmpty();
    381     }
    382 
    383     @Override public boolean contains(Object object) {
    384       if (object instanceof Multiset.Entry) {
    385         Multiset.Entry<?> entry = (Multiset.Entry<?>) object;
    386         Object element = entry.getElement();
    387         int entryCount = entry.getCount();
    388         return entryCount > 0 && count(element) == entryCount;
    389       }
    390       return false;
    391     }
    392 
    393     @Override public Iterator<Multiset.Entry<E>> iterator() {
    394       final Iterator<Map.Entry<E, Integer>> backingIterator
    395           = countMap.entrySet().iterator();
    396       return new Iterator<Multiset.Entry<E>>() {
    397         public boolean hasNext() {
    398           return backingIterator.hasNext();
    399         }
    400 
    401         public Multiset.Entry<E> next() {
    402           Map.Entry<E, Integer> backingEntry = backingIterator.next();
    403           return Multisets.immutableEntry(
    404               backingEntry.getKey(), backingEntry.getValue());
    405         }
    406 
    407         public void remove() {
    408           backingIterator.remove();
    409         }
    410       };
    411     }
    412 
    413     /*
    414      * Note: the superclass toArray() methods assume that size() gives a correct
    415      * answer, which ours does not.
    416      */
    417 
    418     @Override public Object[] toArray() {
    419       return snapshot().toArray();
    420     }
    421 
    422     @Override public <T> T[] toArray(T[] array) {
    423       return snapshot().toArray(array);
    424     }
    425 
    426     /*
    427      * We'd love to use 'new ArrayList(this)' or 'list.addAll(this)', but
    428      * either of these would recurse back to us again!
    429      */
    430     private List<Multiset.Entry<E>> snapshot() {
    431       List<Multiset.Entry<E>> list = Lists.newArrayListWithExpectedSize(size());
    432       for (Multiset.Entry<E> entry : this) {
    433         list.add(entry);
    434       }
    435       return list;
    436     }
    437 
    438     @Override public boolean remove(Object object) {
    439       if (object instanceof Multiset.Entry) {
    440         Multiset.Entry<?> entry = (Multiset.Entry<?>) object;
    441         Object element = entry.getElement();
    442         int entryCount = entry.getCount();
    443         return countMap.remove(element, entryCount);
    444       }
    445       return false;
    446     }
    447 
    448     @Override public void clear() {
    449       countMap.clear();
    450     }
    451 
    452     /**
    453      * The hash code is the same as countMap's, though the objects aren't equal.
    454      */
    455     @Override public int hashCode() {
    456       return countMap.hashCode();
    457     }
    458   }
    459 
    460   /**
    461    * We use a special form of unboxing that treats null as zero.
    462    */
    463   private static int unbox(Integer i) {
    464     return (i == null) ? 0 : i;
    465   }
    466 
    467   /**
    468    * @serialData the number of distinct elements, the first element, its count,
    469    *     the second element, its count, and so on
    470    */
    471   private void writeObject(ObjectOutputStream stream) throws IOException {
    472     stream.defaultWriteObject();
    473     // creating HashMultiset to handle concurrent changes
    474     Serialization.writeMultiset(HashMultiset.create(this), stream);
    475   }
    476 
    477   private void readObject(ObjectInputStream stream)
    478       throws IOException, ClassNotFoundException {
    479     stream.defaultReadObject();
    480     FieldSettersHolder.COUNT_MAP_FIELD_SETTER.set(
    481         this, new ConcurrentHashMap<Object, Object>());
    482     Serialization.populateMultiset(this, stream);
    483   }
    484 
    485   private static final long serialVersionUID = 0L;
    486 }
    487