/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ // // Copyright (C) 2010 catchpole.net // // 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 hivemall.utils.collections; import hivemall.utils.lang.Copyable; import hivemall.utils.math.MathUtils; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * An optimized Hashed Map implementation. * <p/> * <p> * This Hashmap does not allow nulls to be used as keys or values. * <p/> * <p> * It uses single open hashing arrays sized to binary powers (256, 512 etc) rather than those * divisable by prime numbers. This allows the hash offset calculation to be a simple binary masking * operation. */ public final class OpenHashMap<K, V> implements Map<K, V>, Externalizable { private K[] keys; private V[] values; // total number of entries in this table private int size; // number of bits for the value table (eg. 8 bits = 256 entries) private int bits; // the number of bits in each sweep zone. private int sweepbits; // the size of a sweep (2 to the power of sweepbits) private int sweep; // the sweepmask used to create sweep zone offsets private int sweepmask; public OpenHashMap() {}// for Externalizable public OpenHashMap(int size) { resize(MathUtils.bitsRequired(size < 256 ? 256 : size)); } public V put(K key, V value) { if (key == null) { throw new NullPointerException(this.getClass().getName() + " key"); } for (;;) { int off = getBucketOffset(key); int end = off + sweep; for (; off < end; off++) { K searchKey = keys[off]; if (searchKey == null) { // insert keys[off] = key; size++; V previous = values[off]; values[off] = value; return previous; } else if (compare(searchKey, key)) { // replace V previous = values[off]; values[off] = value; return previous; } } resize(this.bits + 1); } } public V get(Object key) { int off = getBucketOffset(key); int end = sweep + off; for (; off < end; off++) { if (keys[off] != null && compare(keys[off], key)) { return values[off]; } } return null; } public V remove(Object key) { int off = getBucketOffset(key); int end = sweep + off; for (; off < end; off++) { if (keys[off] != null && compare(keys[off], key)) { keys[off] = null; V previous = values[off]; values[off] = null; size--; return previous; } } return null; } public int size() { return size; } public void putAll(Map<? extends K, ? extends V> m) { for (K key : m.keySet()) { put(key, m.get(key)); } } public boolean isEmpty() { return size == 0; } public boolean containsKey(Object key) { return get(key) != null; } public boolean containsValue(Object value) { for (V v : values) { if (v != null && compare(v, value)) { return true; } } return false; } public void clear() { Arrays.fill(keys, null); Arrays.fill(values, null); size = 0; } public Set<K> keySet() { Set<K> set = new HashSet<K>(); for (K key : keys) { if (key != null) { set.add(key); } } return set; } public Collection<V> values() { Collection<V> list = new ArrayList<V>(); for (V value : values) { if (value != null) { list.add(value); } } return list; } public Set<Entry<K, V>> entrySet() { Set<Entry<K, V>> set = new HashSet<Entry<K, V>>(); for (K key : keys) { if (key != null) { set.add(new MapEntry<K, V>(this, key)); } } return set; } private static final class MapEntry<K, V> implements Map.Entry<K, V> { private final Map<K, V> map; private final K key; public MapEntry(Map<K, V> map, K key) { this.map = map; this.key = key; } public K getKey() { return key; } public V getValue() { return map.get(key); } public V setValue(V value) { return map.put(key, value); } } public void writeExternal(ObjectOutput out) throws IOException { // remember the number of bits out.writeInt(this.bits); // remember the total number of entries out.writeInt(this.size); // write all entries for (int x = 0; x < this.keys.length; x++) { if (keys[x] != null) { out.writeObject(keys[x]); out.writeObject(values[x]); } } } @SuppressWarnings("unchecked") public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // resize to old bit size int bitSize = in.readInt(); if (bitSize != bits) { resize(bitSize); } // read all entries int size = in.readInt(); for (int x = 0; x < size; x++) { this.put((K) in.readObject(), (V) in.readObject()); } } @Override public String toString() { return this.getClass().getSimpleName() + ' ' + this.size; } @SuppressWarnings("unchecked") private void resize(int bits) { this.bits = bits; this.sweepbits = bits / 4; this.sweep = MathUtils.powerOf(2, sweepbits) * 4; this.sweepmask = MathUtils.bitMask(bits - this.sweepbits) << sweepbits; // remember old values so we can recreate the entries K[] existingKeys = this.keys; V[] existingValues = this.values; // create the arrays this.values = (V[]) new Object[MathUtils.powerOf(2, bits) + sweep]; this.keys = (K[]) new Object[values.length]; this.size = 0; // re-add the previous entries if resizing if (existingKeys != null) { for (int x = 0; x < existingKeys.length; x++) { if (existingKeys[x] != null) { put(existingKeys[x], existingValues[x]); } } } } private int getBucketOffset(Object key) { return (key.hashCode() << this.sweepbits) & this.sweepmask; } private static boolean compare(final Object v1, final Object v2) { return v1 == v2 || v1.equals(v2); } public IMapIterator<K, V> entries() { return new MapIterator(); } private final class MapIterator implements IMapIterator<K, V> { int nextEntry; int lastEntry = -1; MapIterator() { this.nextEntry = nextEntry(0); } /** find the index of next full entry */ int nextEntry(int index) { while (index < keys.length && keys[index] == null) { index++; } return index; } @Override public boolean hasNext() { return nextEntry < keys.length; } @Override public int next() { free(lastEntry); if (!hasNext()) { return -1; } int curEntry = nextEntry; this.lastEntry = curEntry; this.nextEntry = nextEntry(curEntry + 1); return curEntry; } @Override public K getKey() { return keys[lastEntry]; } @Override public V getValue() { return values[lastEntry]; } @Override public <T extends Copyable<V>> void getValue(T probe) { probe.copyFrom(getValue()); } private void free(int index) { if (index >= 0) { keys[index] = null; values[index] = null; } } } }