/* * Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved. * * 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.hazelcast.util.collection; import com.hazelcast.nio.serialization.SerializableByConvention; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.Serializable; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import static com.hazelcast.util.Preconditions.checkNotNull; /** * Provides fast {@link Set} implementation for cases where items are known to not * contain duplicates. * <p> * It requires creation via {@link com.hazelcast.util.collection.InflatableSet.Builder} * <p> * The builder doesn't call equals/hash methods on initial data insertion, hence it avoids * performance penalty in the case these methods are expensive. It also means it does * not detect duplicates - it's the responsibility of the caller to make sure no duplicated * entries are inserted. * <p> * Once InflatableSet is constructed via {@link Builder#build()} then it acts as a regular set. It has * been designed to mimic {@link HashSet}. On new entry insertion or lookup via * {@link #contains(Object)} it inflates itself: The backing list is copied into * internal {@link HashSet}. This obviously costs time and space. We are making a bet the * Set won't be modified in most cases. * <p> * It's intended to be used in cases where the contract mandates us to return Set, * but we know our data does not contain duplicates. It performs best in cases * biased towards sequential iteration. * * @param <T> the type of elements maintained by this set */ @SerializableByConvention public final class InflatableSet<T> extends AbstractSet<T> implements Set<T>, Serializable, Cloneable { private static final long serialVersionUID = 0L; private enum State { // only array-backed representation exists COMPACT, // both array-backed & hashset-backed representation exist. // this is needed as we are creating HashSet on contains() // but we don't want invalidate existing iterators. HYBRID, // only hashset based representation exists INFLATED } private final List<T> compactList; private Set<T> inflatedSet; private State state; /** * This constructor is intended to be used by {@link com.hazelcast.util.collection.InflatableSet.Builder} only. * * @param compactList list of elements for the InflatableSet */ private InflatableSet(List<T> compactList) { this.state = State.COMPACT; this.compactList = compactList; } /** * This copy-constructor is intended to be used by {@link #clone()} method only. * * @param other other InflatableSet which should be cloned */ private InflatableSet(InflatableSet<T> other) { compactList = new ArrayList<T>(other.compactList.size()); compactList.addAll(other.compactList); if (other.inflatedSet != null) { inflatedSet = new HashSet<T>(other.inflatedSet); } state = other.state; } public static <T> Builder<T> newBuilder(int initialCapacity) { return new Builder<T>(initialCapacity); } public static <T> Builder<T> newBuilder(List<T> list) { return new Builder<T>(list); } @Override public int size() { if (state == State.INFLATED) { return inflatedSet.size(); } else { return compactList.size(); } } @Override public boolean isEmpty() { if (state == State.INFLATED) { return inflatedSet.isEmpty(); } else { return compactList.isEmpty(); } } @Override public boolean contains(Object o) { if (state == State.COMPACT) { toHybridState(); } return inflatedSet.contains(o); } @Override public Iterator<T> iterator() { if (state == State.INFLATED) { return inflatedSet.iterator(); } return new HybridIterator(); } @Override public boolean add(T t) { toInflatedState(); return inflatedSet.add(t); } @Override public boolean remove(Object o) { switch (state) { case HYBRID: compactList.remove(o); return inflatedSet.remove(o); case INFLATED: return inflatedSet.remove(o); default: return compactList.remove(o); } } @Override public void clear() { switch (state) { case HYBRID: inflatedSet.clear(); compactList.clear(); break; case INFLATED: inflatedSet.clear(); break; default: compactList.clear(); } } /** * Returns a shallow copy of this <tt>InflatableSet</tt> instance: the keys and * values themselves are not cloned. * * @return a shallow copy of this set */ @Override @SuppressFBWarnings(value = "CN_IDIOM", justification = "Deliberate, documented contract violation") @SuppressWarnings({"checkstyle:superclone", "CloneDoesntCallSuperClone"}) protected Object clone() { return new InflatableSet<T>(this); } private void inflateIfNeeded() { if (inflatedSet == null) { inflatedSet = new HashSet<T>(compactList); } } private void toHybridState() { if (state == State.HYBRID) { return; } state = State.HYBRID; inflateIfNeeded(); } private void toInflatedState() { if (state == State.INFLATED) { return; } state = State.INFLATED; inflateIfNeeded(); invalidateIterators(); } private void invalidateIterators() { if (compactList.size() == 0) { compactList.clear(); } else { compactList.remove(0); } } private class HybridIterator implements Iterator<T> { private Iterator<T> innerIterator; private T currentValue; HybridIterator() { innerIterator = compactList.iterator(); } @Override public boolean hasNext() { return innerIterator.hasNext(); } @Override public T next() { currentValue = innerIterator.next(); return currentValue; } @Override public void remove() { innerIterator.remove(); if (inflatedSet != null) { inflatedSet.remove(currentValue); } } } /** * Builder for {@link InflatableSet}. * This is the only way to create a new instance of InflatableSet. * * @param <T> the type of elements maintained by this set */ public static final class Builder<T> { private List<T> list; private Builder(int initialCapacity) { this.list = new ArrayList<T>(initialCapacity); } private Builder(List<T> list) { this.list = checkNotNull(list, "list cannot be null"); } public int size() { return list.size(); } public Builder add(T item) { list.add(item); return this; } public InflatableSet<T> build() { InflatableSet<T> set = new InflatableSet<T>(list); // make sure no further insertions are possible list = Collections.emptyList(); return set; } } }