All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.facebook.collections.specialized.LongHashSet Maven / Gradle / Ivy

There is a newer version: 0.1.32
Show newest version
/*
 * Copyright (C) 2012 Facebook, 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.facebook.collections.specialized;

import com.facebook.collections.Trackable;
import com.facebook.collectionsbase.Mapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;

import javax.annotation.concurrent.GuardedBy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * stores a set of non-negative long values using a fixed-size array.
 *
 * this class is thread-safe, and allows decent parallelism using a
 * ReadWriteLock.  The iterator is thread-safe, but can still throw
 * a ConcurrentModificationException (TODO: can probably fix this)
 */
public class LongHashSet implements SnapshotableSet, Trackable {
  // these are values for slots
  private static final long INITIAL_EMPTY = -1;
  private static final long REMOVED_EMPTY = -2;
  // this is a reserved return value
  private static final int FULL_SET = -3;
  private static final float MIN_LOAD_FACTOR = 2 / 3.0f;
  private static final float MAX_LOAD_FACTOR = 9 / 10.0f;

  private final Mapper hashFunction;
  // guards all mutations to values
  private final ReadWriteLock lock = new ReentrantReadWriteLock();
  private final AtomicLong version = new AtomicLong(0);
  private long lastCheckedVersion = 0;
  @GuardedBy("lock")
  private volatile long[] values;
  private AtomicInteger size = new AtomicInteger(0);
  private final int maxCapacity;

  public LongHashSet(
    int initialCapacity, int maxCapacity, Mapper hashFunction
  ) {
    Preconditions.checkArgument(
      initialCapacity <= maxCapacity,
      "initial capacity of %s cannot be larger than max of %s",
      initialCapacity,
      maxCapacity
    );
    this.maxCapacity = maxCapacity;
    initArrays(initialCapacity);
    this.hashFunction = hashFunction;
  }

  private void initArrays(int capacity) {
    values = new long[capacity];
    Arrays.fill(values, INITIAL_EMPTY);
  }

  /**
   * creates a bounded set with an initial capacity
   *
   * @param initialCapacity
   * @param maxCapacity
   */
  public LongHashSet(int initialCapacity, int maxCapacity) {
    this(
      initialCapacity,
      maxCapacity,
      new Mapper() {
        @Override
        public Integer map(Long input) {
          return (int) (input ^ (input >>> 32));
        }
      }
    );
  }

  private void resize() {
    if (values.length == maxCapacity) {
      throw new IllegalStateException(
        String.format(
          "cannot resize: max capacity of %d already reached", maxCapacity
        )
      );
    }

    int desiredSize = (int) (values.length / MIN_LOAD_FACTOR);
    int newSize = Math.min(desiredSize, maxCapacity);

    long[] oldValues = values;
    values = new long[newSize];
    internalClear();

    for (long value : oldValues) {
      if (value >= 0) {
        internalAdd(value);
      }
    }
  }

  private int hashValueOf(Long aLong) {
    return Math.abs(hashFunction.map(aLong)) % values.length;
  }

  /**
   * returns location of either the value, or the next location to place
   * the value
   *
   * @param aLong
   * @return if aLong is present in the set, returns the index of it; if it
   *         is not present, returns the index of where it may be placed,
   *         or FULL_SET(-2) if the set is full
   */
  private int findLocationOf(long aLong) {
    int index = hashValueOf(aLong);
    int firstEmptyIndex = -1;
    int total = 0;

    // stop if we find aLong or an empty slot
    while (values[index] != aLong) {
      if (values[index] == INITIAL_EMPTY) {
        return index;
      }

      if (isEmptySlot(index)) {
        if (firstEmptyIndex == -1) {
          firstEmptyIndex = index;
        }
      }

      total++;

      // case that we've seen every slot
      if (total == values.length) {
        if (firstEmptyIndex == -1) {
          return FULL_SET;
        } else {
          return firstEmptyIndex;
        }
      }

      index = (index + 1) % values.length;
    }

    // this means we found aLong
    return index;
  }

  private boolean isEmptySlot(int index) {
    return values[index] < 0;
  }

  private void validateArgument(Long aLong) {
    if (aLong < 0) {
      throw new IllegalArgumentException(
        String.format(
          "only non-negative integers are allowed (tried to use %d)", aLong
        )
      );
    }
  }

  @Override
  public int size() {
    return size.get();
  }

  @Override
  public boolean isEmpty() {
    return size.get() == 0;
  }

  @Override
  public boolean contains(Object o) {
    if (!(o instanceof Long)) {
      throw new IllegalArgumentException("type of long required");
    }

    int index;

    lock.readLock().lock();

    try {
      index = findLocationOf((Long) o);

      return index != FULL_SET && !isEmptySlot(index);
    } finally {
      lock.readLock().unlock();
    }
  }

  @Override
  public Iterator iterator() {
    final AtomicLong snapshotVersion = new AtomicLong(version.get());
    final long sizeSnapshot = size.get();

    return new Iterator() {
      // invariant: location < values.length. This holds because we
      // require that the set does not change while we iterator. location
      // is only incremented in next() which checks the bound. Remove
      private int location = -1;
      private int visited = 0;
      private boolean canRemove = false;

      @Override
      public boolean hasNext() {
        lock.readLock().lock();

        try {
          if (version.get() != snapshotVersion.get()) {
            throw new ConcurrentModificationException();
          }

          return visited < sizeSnapshot;
        } finally {
          lock.readLock().unlock();
        }
      }

      @Override
      public Long next() {
        lock.readLock().lock();

        try {
          if (version.get() != snapshotVersion.get()) {
            throw new ConcurrentModificationException();
          }

          if (location >= values.length) {
            throw new NoSuchElementException();
          }

          do {
            location++;

            if (location >= values.length) {
              throw new NoSuchElementException();
            }
          } while (isEmptySlot(location));

          visited++;
          canRemove = true;

          //noinspection unchecked
          return values[location];
        } finally {
          lock.readLock().unlock();
        }
      }

      @Override
      public void remove() {
        lock.writeLock().lock();

        try {
          if (!canRemove) {
            // TODO : clean this up?
            throw new IllegalStateException(
              "repeated remove() calls or next() not called"
            );
          }

          // this works because we verify the set hasn't changed and
          // then update the snapshotVersion to a new one
          if (version.get() != snapshotVersion.get()) {
            throw new ConcurrentModificationException();
          }

          values[location] = REMOVED_EMPTY;
          size.decrementAndGet();
          // update the snapshotVersion and also set the valid snapshotVersion to check
          // for changes against
          snapshotVersion.set(version.incrementAndGet());
          canRemove = false;
        } finally {
          lock.writeLock().unlock();
        }
      }
    };
  }

  @Override
  public Object[] toArray() {
    return toArray(new Object[size.get()]);
  }

  @Override
  public  T[] toArray(T[] a) {
    if (!a.getClass().getComponentType().isAssignableFrom(Long.class)) {
      throw new ArrayStoreException("array must be of type Long");
    }

    T[] result;

    lock.readLock().lock();

    try {
      if (a.length >= size.get()) {
        result = a;
      } else {
        result = (T[]) java.lang.reflect.Array
          .newInstance(a.getClass().getComponentType(), size.get());
      }

      int i = 0;

      for (Object value : this) {
        result[i++] = (T) value;
      }
    } finally {
      lock.readLock().unlock();
    }

    return result;
  }

  @VisibleForTesting
  boolean add(Integer anInteger) {
    return add(anInteger.longValue());
  }

  @Override
  public boolean add(Long aLong) {
    validateArgument(aLong);

    lock.writeLock().lock();

    try {
      int maxUsedBuckets = (int) (MAX_LOAD_FACTOR * values.length);
      // resize if we are too full and we can add buckets
      if (size.get() > maxUsedBuckets && values.length < maxCapacity) {
        resize();
      }

      if (internalAdd(aLong)) {
        version.incrementAndGet();

        return true;
      } else {
        return false;
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  private boolean internalAdd(long value) {
    int index = findLocationOf(value);

    if (index == FULL_SET) {
      throw new IllegalStateException(
        String.format(
          "set is full with %d elements, cannot add more",
          values.length
        )
      );
    }

    if (isEmptySlot(index)) {
      values[index] = value;
      size.incrementAndGet();

      return true;
    } else {
      return false;
    }
  }

  @Override
  public boolean remove(Object o) {
    if (!(o instanceof Long)) {
      throw new IllegalArgumentException("type of long required");
    }

    validateArgument((Long) o);

    lock.writeLock().lock();

    try {
      int index = findLocationOf((Long) o);

      if (index != FULL_SET && !isEmptySlot(index)) {
        values[index] = REMOVED_EMPTY;
        version.incrementAndGet();
        size.decrementAndGet();

        return true;
      }

      return false;
    } finally {
      lock.writeLock().unlock();
    }
  }

  @Override
  public boolean containsAll(Collection c) {
    // grab the lock since returning true here means that the set contains
    // all the elements at one moment in time
    lock.readLock().lock();

    try {
      for (Object element : c) {
        if (!contains(element)) {
          return false;
        }
      }

      return true;
    } finally {
      lock.readLock().unlock();
    }
  }

  @Override
  public boolean addAll(Collection c) {
    boolean changed = false;

    //rely on add() holding the writeLock
    for (Long element : c) {
      if (add(element)) {
        changed = true;
      }
    }

    return changed;
  }

  @Override
  public boolean retainAll(Collection c) {
    boolean changed = false;

    // semantics of this method are that only elements in the collection
    // are present after completion; hold the lock so that this holds
    // for at least one moment

    lock.writeLock().lock();

    try {
      Iterator iterator = this.iterator();

      while (iterator.hasNext()) {
        if (!c.contains(iterator.next())) {
          // remove() here will update the version
          iterator.remove();
          size.decrementAndGet();
          changed = true;
        }
      }
    } finally {
      lock.writeLock().unlock();
    }

    return changed;
  }

  @Override
  public boolean removeAll(Collection c) {
    boolean changed = false;
    // simpler to reason about if we lock the set here
    lock.writeLock().lock();

    try {
      if (size() > c.size()) {
        for (Iterator i = c.iterator(); i.hasNext(); ) {
          changed |= remove(i.next());
        }
      } else {
        for (Iterator i = iterator(); i.hasNext(); ) {
          if (c.contains(i.next())) {
            i.remove();
            changed = true;
          }
        }
      }

      return changed;
    } finally {
      lock.writeLock().unlock();
    }
  }

  @Override
  public void clear() {
    lock.writeLock().lock();

    try {
      internalClear();
      version.incrementAndGet();
    } finally {
      lock.writeLock().unlock();
    }
  }

  /**
   * efficient deep-copy method
   *
   * @return deep copy of this set
   */
  @Override
  public SnapshotableSet makeSnapshot() {
    lock.readLock().lock();

    try {
      // create  new set
      LongHashSet copy = new LongHashSet(values.length, maxCapacity, hashFunction);

      // set the size and copy the values array
      copy.size.set(size.get());
      System.arraycopy(values, 0, copy.values, 0, values.length);

      return copy;
    } finally {
      lock.readLock().unlock();
    }
  }

  @Override
  public SnapshotableSet makeTransientSnapshot() {
    return new SnapshotableSetImpl(
      Collections.synchronizedSet(new HashSet(this)),
      new SnapshotableSetImplFactory(new HashSetFactory())
    );
  }

  private void internalClear() {
    size.set(0);
    Arrays.fill(values, INITIAL_EMPTY);
  }

  @Override
  public synchronized boolean hasChanged() {
    long pastVersion = lastCheckedVersion;
    lastCheckedVersion = version.get();

    return (lastCheckedVersion != pastVersion);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof Set)) {
      return false;
    }
    @SuppressWarnings("unchecked") Collection c = (Collection) o;
    if (c.size() != size()) {
      return false;
    }
    try {
      return containsAll(c);
    } catch (ClassCastException unused) {
      return false;
    } catch (NullPointerException unused) {
      return false;
    }
  }

  @Override
  public int hashCode() {
    int h = 0;
    Iterator i = iterator();

    while (i.hasNext()) {
      Long value = i.next();

      if (value != null) {
        h += value.hashCode();
      }
    }

    return h;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder((int) (size.get() * 8));
    Set copy = makeSnapshot();
    boolean first = true;

    sb.append("{");

    for (Long value : copy) {
      if (!first) {
        sb.append(", ");
      }

      sb.append(value.longValue());
      first = false;
    }

    sb.append("}");

    return sb.toString();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy