/* * Copyright (C) 2007 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.collect; import com.google.common.annotations.GwtCompatible; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Multisets.checkNonnegative; import java.io.InvalidObjectException; import java.io.ObjectStreamException; import java.io.Serializable; import java.util.AbstractSet; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; /** * Basic implementation of {@code Multiset<E>} backed by an instance of {@code * Map<E, AtomicInteger>}. * * <p>For serialization to work, the subclass must specify explicit {@code * readObject} and {@code writeObject} methods. * * @author Kevin Bourrillion */ @GwtCompatible abstract class AbstractMapBasedMultiset<E> extends AbstractMultiset<E> implements Serializable { // TODO: Replace AtomicInteger with a to-be-written IntegerHolder class for // better performance. private transient Map<E, AtomicInteger> backingMap; /* * Cache the size for efficiency. Using a long lets us avoid the need for * overflow checking and ensures that size() will function correctly even if * the multiset had once been larger than Integer.MAX_VALUE. */ private transient long size; /** Standard constructor. */ protected AbstractMapBasedMultiset(Map<E, AtomicInteger> backingMap) { this.backingMap = checkNotNull(backingMap); this.size = super.size(); } Map<E, AtomicInteger> backingMap() { return backingMap; } /** Used during deserialization only. The backing map must be empty. */ void setBackingMap(Map<E, AtomicInteger> backingMap) { this.backingMap = backingMap; } // Required Implementations private transient EntrySet entrySet; /** * {@inheritDoc} * * <p>Invoking {@link Multiset.Entry#getCount} on an entry in the returned * set always returns the current count of that element in the multiset, as * opposed to the count at the time the entry was retrieved. */ @Override public Set<Multiset.Entry<E>> entrySet() { EntrySet result = entrySet; if (result == null) { entrySet = result = new EntrySet(); } return result; } private class EntrySet extends AbstractSet<Multiset.Entry<E>> { @Override public Iterator<Multiset.Entry<E>> iterator() { final Iterator<Map.Entry<E, AtomicInteger>> backingEntries = backingMap.entrySet().iterator(); return new Iterator<Multiset.Entry<E>>() { Map.Entry<E, AtomicInteger> toRemove; public boolean hasNext() { return backingEntries.hasNext(); } public Multiset.Entry<E> next() { final Map.Entry<E, AtomicInteger> mapEntry = backingEntries.next(); toRemove = mapEntry; return new Multisets.AbstractEntry<E>() { public E getElement() { return mapEntry.getKey(); } public int getCount() { int count = mapEntry.getValue().get(); if (count == 0) { AtomicInteger frequency = backingMap.get(getElement()); if (frequency != null) { count = frequency.get(); } } return count; } }; } public void remove() { checkState(toRemove != null, "no calls to next() since the last call to remove()"); size -= toRemove.getValue().getAndSet(0); backingEntries.remove(); toRemove = null; } }; } @Override public int size() { return backingMap.size(); } // The following overrides are for better performance. @Override public void clear() { for (AtomicInteger frequency : backingMap.values()) { frequency.set(0); } backingMap.clear(); size = 0L; } @Override public boolean contains(Object o) { if (o instanceof Entry) { Entry<?> entry = (Entry<?>) o; int count = count(entry.getElement()); return (count == entry.getCount()) && (count > 0); } return false; } @Override public boolean remove(Object o) { if (contains(o)) { Entry<?> entry = (Entry<?>) o; AtomicInteger frequency = backingMap.remove(entry.getElement()); int numberRemoved = frequency.getAndSet(0); size -= numberRemoved; return true; } return false; } } // Optimizations - Query Operations @Override public int size() { return (int) Math.min(this.size, Integer.MAX_VALUE); } @Override public Iterator<E> iterator() { return new MapBasedMultisetIterator(); } /* * Not subclassing AbstractMultiset$MultisetIterator because next() needs to * retrieve the Map.Entry<E, AtomicInteger> entry, which can then be used for * a more efficient remove() call. */ private class MapBasedMultisetIterator implements Iterator<E> { final Iterator<Map.Entry<E, AtomicInteger>> entryIterator; Map.Entry<E, AtomicInteger> currentEntry; int occurrencesLeft; boolean canRemove; MapBasedMultisetIterator() { this.entryIterator = backingMap.entrySet().iterator(); } public boolean hasNext() { return occurrencesLeft > 0 || entryIterator.hasNext(); } public E next() { if (occurrencesLeft == 0) { currentEntry = entryIterator.next(); occurrencesLeft = currentEntry.getValue().get(); } occurrencesLeft--; canRemove = true; return currentEntry.getKey(); } public void remove() { checkState(canRemove, "no calls to next() since the last call to remove()"); int frequency = currentEntry.getValue().get(); if (frequency <= 0) { throw new ConcurrentModificationException(); } if (currentEntry.getValue().addAndGet(-1) == 0) { entryIterator.remove(); } size--; canRemove = false; } } @Override public int count(@Nullable Object element) { AtomicInteger frequency = backingMap.get(element); return (frequency == null) ? 0 : frequency.get(); } // Optional Operations - Modification Operations /** * {@inheritDoc} * * @throws IllegalArgumentException if the call would result in more than * {@link Integer#MAX_VALUE} occurrences of {@code element} in this * multiset. */ @Override public int add(@Nullable E element, int occurrences) { if (occurrences == 0) { return count(element); } checkArgument( occurrences > 0, "occurrences cannot be negative: %s", occurrences); AtomicInteger frequency = backingMap.get(element); int oldCount; if (frequency == null) { oldCount = 0; backingMap.put(element, new AtomicInteger(occurrences)); } else { oldCount = frequency.get(); long newCount = (long) oldCount + (long) occurrences; checkArgument(newCount <= Integer.MAX_VALUE, "too many occurrences: %s", newCount); frequency.getAndAdd(occurrences); } size += occurrences; return oldCount; } @Override public int remove(@Nullable Object element, int occurrences) { if (occurrences == 0) { return count(element); } checkArgument( occurrences > 0, "occurrences cannot be negative: %s", occurrences); AtomicInteger frequency = backingMap.get(element); if (frequency == null) { return 0; } int oldCount = frequency.get(); int numberRemoved; if (oldCount > occurrences) { numberRemoved = occurrences; } else { numberRemoved = oldCount; backingMap.remove(element); } frequency.addAndGet(-numberRemoved); size -= numberRemoved; return oldCount; } // Roughly a 33% performance improvement over AbstractMultiset.setCount(). @Override public int setCount(E element, int count) { checkNonnegative(count, "count"); AtomicInteger existingCounter; int oldCount; if (count == 0) { existingCounter = backingMap.remove(element); oldCount = getAndSet(existingCounter, count); } else { existingCounter = backingMap.get(element); oldCount = getAndSet(existingCounter, count); if (existingCounter == null) { backingMap.put(element, new AtomicInteger(count)); } } size += (count - oldCount); return oldCount; } private static int getAndSet(AtomicInteger i, int count) { if (i == null) { return 0; } return i.getAndSet(count); } private int removeAllOccurrences(@Nullable Object element, Map<E, AtomicInteger> map) { AtomicInteger frequency = map.remove(element); if (frequency == null) { return 0; } int numberRemoved = frequency.getAndSet(0); size -= numberRemoved; return numberRemoved; } // Views @Override Set<E> createElementSet() { return new MapBasedElementSet(backingMap); } class MapBasedElementSet extends ForwardingSet<E> { // This mapping is the usually the same as {@code backingMap}, but can be a // submap in some implementations. private final Map<E, AtomicInteger> map; private final Set<E> delegate; MapBasedElementSet(Map<E, AtomicInteger> map) { this.map = map; delegate = map.keySet(); } @Override protected Set<E> delegate() { return delegate; } // TODO: a way to not have to write this much code? @Override public Iterator<E> iterator() { final Iterator<Map.Entry<E, AtomicInteger>> entries = map.entrySet().iterator(); return new Iterator<E>() { Map.Entry<E, AtomicInteger> toRemove; public boolean hasNext() { return entries.hasNext(); } public E next() { toRemove = entries.next(); return toRemove.getKey(); } public void remove() { checkState(toRemove != null, "no calls to next() since the last call to remove()"); size -= toRemove.getValue().getAndSet(0); entries.remove(); toRemove = null; } }; } @Override public boolean remove(Object element) { return removeAllOccurrences(element, map) != 0; } @Override public boolean removeAll(Collection<?> elementsToRemove) { return Iterators.removeAll(iterator(), elementsToRemove); } @Override public boolean retainAll(Collection<?> elementsToRetain) { return Iterators.retainAll(iterator(), elementsToRetain); } @Override public void clear() { if (map == backingMap) { AbstractMapBasedMultiset.this.clear(); } else { Iterator<E> i = iterator(); while (i.hasNext()) { i.next(); i.remove(); } } } public Map<E, AtomicInteger> getMap() { return map; } } // Don't allow default serialization. @SuppressWarnings("unused") // actually used during deserialization private void readObjectNoData() throws ObjectStreamException { throw new InvalidObjectException("Stream data required"); } private static final long serialVersionUID = -2250766705698539974L; }