1/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved. 2 * 3 * This program and the accompanying materials are made available under 4 * the terms of the Common Public License v1.0 which accompanies this distribution, 5 * and is available at http://www.eclipse.org/legal/cpl-v10.html 6 * 7 * $Id: ObjectIntMap.java,v 1.1.1.1 2004/05/09 16:57:54 vlad_r Exp $ 8 */ 9package com.vladium.util; 10 11import com.vladium.util.asserts.$assert; 12 13// ---------------------------------------------------------------------------- 14/** 15 * 16 * MT-safety: an instance of this class is <I>not</I> safe for access from 17 * multiple concurrent threads [even if access is done by a single thread at a 18 * time]. The caller is expected to synchronize externally on an instance [the 19 * implementation does not do internal synchronization for the sake of efficiency]. 20 * java.util.ConcurrentModificationException is not supported either. 21 * 22 * @author (C) 2001, Vlad Roubtsov 23 */ 24public 25final class ObjectIntMap 26{ 27 // public: ................................................................ 28 29 // TODO: optimize key comparisons using key.hash == entry.key.hash condition 30 31 /** 32 * Equivalent to <CODE>IntObjectMap(11, 0.75F)</CODE>. 33 */ 34 public ObjectIntMap () 35 { 36 this (11, 0.75F); 37 } 38 39 /** 40 * Equivalent to <CODE>IntObjectMap(capacity, 0.75F)</CODE>. 41 */ 42 public ObjectIntMap (final int initialCapacity) 43 { 44 this (initialCapacity, 0.75F); 45 } 46 47 /** 48 * Constructs an IntObjectMap with specified initial capacity and load factor. 49 * 50 * @param initialCapacity initial number of hash buckets in the table [may not be negative, 0 is equivalent to 1]. 51 * @param loadFactor the load factor to use to determine rehashing points [must be in (0.0, 1.0] range]. 52 */ 53 public ObjectIntMap (int initialCapacity, final float loadFactor) 54 { 55 if (initialCapacity < 0) throw new IllegalArgumentException ("negative input: initialCapacity [" + initialCapacity + "]"); 56 if ((loadFactor <= 0.0) || (loadFactor >= 1.0 + 1.0E-6)) 57 throw new IllegalArgumentException ("loadFactor not in (0.0, 1.0] range: " + loadFactor); 58 59 if (initialCapacity == 0) initialCapacity = 1; 60 61 m_loadFactor = loadFactor > 1.0 ? 1.0F : loadFactor; 62 m_sizeThreshold = (int) (initialCapacity * loadFactor); 63 m_buckets = new Entry [initialCapacity]; 64 } 65 66 67 /** 68 * Overrides Object.toString() for debug purposes. 69 */ 70 public String toString () 71 { 72 final StringBuffer s = new StringBuffer (); 73 debugDump (s); 74 75 return s.toString (); 76 } 77 78 /** 79 * Returns the number of key-value mappings in this map. 80 */ 81 public int size () 82 { 83 return m_size; 84 } 85 86 public boolean contains (final Object key) 87 { 88 if ($assert.ENABLED) $assert.ASSERT (key != null, "null input: key"); 89 90 // index into the corresponding hash bucket: 91 final Entry [] buckets = m_buckets; 92 final int keyHash = key.hashCode (); 93 final int bucketIndex = (keyHash & 0x7FFFFFFF) % buckets.length; 94 95 // traverse the singly-linked list of entries in the bucket: 96 for (Entry entry = buckets [bucketIndex]; entry != null; entry = entry.m_next) 97 { 98 if ((keyHash == entry.m_key.hashCode ()) || entry.m_key.equals (key)) 99 return true; 100 } 101 102 return false; 103 } 104 105 /** 106 * Returns the value that is mapped to a given 'key'. Returns 107 * false if this key has never been mapped. 108 * 109 * @param key mapping key [may not be null] 110 * @param out holder for the found value [must be at least of size 1] 111 * 112 * @return 'true' if this key was mapped to an existing value 113 */ 114 public boolean get (final Object key, final int [] out) 115 { 116 if ($assert.ENABLED) $assert.ASSERT (key != null, "null input: key"); 117 118 // index into the corresponding hash bucket: 119 final Entry [] buckets = m_buckets; 120 final int keyHash = key.hashCode (); 121 final int bucketIndex = (keyHash & 0x7FFFFFFF) % buckets.length; 122 123 // traverse the singly-linked list of entries in the bucket: 124 for (Entry entry = buckets [bucketIndex]; entry != null; entry = entry.m_next) 125 { 126 if ((keyHash == entry.m_key.hashCode ()) || entry.m_key.equals (key)) 127 { 128 out [0] = entry.m_value; 129 return true; 130 } 131 } 132 133 return false; 134 } 135 136 public Object [] keys () 137 { 138 final Object [] result = new Object [m_size]; 139 int scan = 0; 140 141 for (int b = 0; b < m_buckets.length; ++ b) 142 { 143 for (Entry entry = m_buckets [b]; entry != null; entry = entry.m_next) 144 { 145 result [scan ++] = entry.m_key; 146 } 147 } 148 149 return result; 150 } 151 152 /** 153 * Updates the table to map 'key' to 'value'. Any existing mapping is overwritten. 154 * 155 * @param key mapping key [may not be null] 156 * @param value mapping value. 157 */ 158 public void put (final Object key, final int value) 159 { 160 if ($assert.ENABLED) $assert.ASSERT (key != null, "null input: key"); 161 162 Entry currentKeyEntry = null; 163 164 // detect if 'key' is already in the table [in which case, set 'currentKeyEntry' to point to its entry]: 165 166 // index into the corresponding hash bucket: 167 final int keyHash = key.hashCode (); 168 int bucketIndex = (keyHash & 0x7FFFFFFF) % m_buckets.length; 169 170 // traverse the singly-linked list of entries in the bucket: 171 Entry [] buckets = m_buckets; 172 for (Entry entry = buckets [bucketIndex]; entry != null; entry = entry.m_next) 173 { 174 if ((keyHash == entry.m_key.hashCode ()) || entry.m_key.equals (key)) 175 { 176 currentKeyEntry = entry; 177 break; 178 } 179 } 180 181 if (currentKeyEntry != null) 182 { 183 // replace the current value: 184 185 currentKeyEntry.m_value = value; 186 } 187 else 188 { 189 // add a new entry: 190 191 if (m_size >= m_sizeThreshold) rehash (); 192 193 buckets = m_buckets; 194 bucketIndex = (keyHash & 0x7FFFFFFF) % buckets.length; 195 final Entry bucketListHead = buckets [bucketIndex]; 196 final Entry newEntry = new Entry (key, value, bucketListHead); 197 buckets [bucketIndex] = newEntry; 198 199 ++ m_size; 200 } 201 } 202 203 /** 204 * Updates the table to map 'key' to 'value'. Any existing mapping is overwritten. 205 * 206 * @param key mapping key [may not be null] 207 */ 208 public void remove (final Object key) 209 { 210 if ($assert.ENABLED) $assert.ASSERT (key != null, "null input: key"); 211 212 // index into the corresponding hash bucket: 213 final int keyHash = key.hashCode (); 214 final int bucketIndex = (keyHash & 0x7FFFFFFF) % m_buckets.length; 215 216 // traverse the singly-linked list of entries in the bucket: 217 Entry [] buckets = m_buckets; 218 for (Entry entry = buckets [bucketIndex], prev = entry; entry != null; ) 219 { 220 final Entry next = entry.m_next; 221 222 if ((keyHash == entry.m_key.hashCode ()) || entry.m_key.equals (key)) 223 { 224 if (prev == entry) 225 buckets [bucketIndex] = next; 226 else 227 prev.m_next = next; 228 229 -- m_size; 230 break; 231 } 232 233 prev = entry; 234 entry = next; 235 } 236 } 237 238 239 // protected: ............................................................. 240 241 // package: ............................................................... 242 243 244 void debugDump (final StringBuffer out) 245 { 246 if (out != null) 247 { 248 out.append (super.toString ()); out.append (EOL); 249 out.append ("size = " + m_size + ", bucket table size = " + m_buckets.length + ", load factor = " + m_loadFactor + EOL); 250 out.append ("size threshold = " + m_sizeThreshold + EOL); 251 } 252 } 253 254 // private: ............................................................... 255 256 257 /** 258 * The structure used for chaining colliding keys. 259 */ 260 private static final class Entry 261 { 262 Entry (final Object key, final int value, final Entry next) 263 { 264 m_key = key; 265 m_value = value; 266 m_next = next; 267 } 268 269 Object m_key; // reference to the value [never null] 270 int m_value; 271 272 Entry m_next; // singly-linked list link 273 274 } // end of nested class 275 276 277 /** 278 * Re-hashes the table into a new array of buckets. 279 */ 280 private void rehash () 281 { 282 // TODO: it is possible to run this method twice, first time using the 2*k+1 prime sequencer for newBucketCount 283 // and then with that value reduced to actually shrink capacity. As it is right now, the bucket table can 284 // only grow in size 285 286 final Entry [] buckets = m_buckets; 287 288 final int newBucketCount = (m_buckets.length << 1) + 1; 289 final Entry [] newBuckets = new Entry [newBucketCount]; 290 291 // rehash all entry chains in every bucket: 292 for (int b = 0; b < buckets.length; ++ b) 293 { 294 for (Entry entry = buckets [b]; entry != null; ) 295 { 296 final Entry next = entry.m_next; // remember next pointer because we are going to reuse this entry 297 final int entryKeyHash = entry.m_key.hashCode () & 0x7FFFFFFF; 298 299 // index into the corresponding new hash bucket: 300 final int newBucketIndex = entryKeyHash % newBucketCount; 301 302 final Entry bucketListHead = newBuckets [newBucketIndex]; 303 entry.m_next = bucketListHead; 304 newBuckets [newBucketIndex] = entry; 305 306 entry = next; 307 } 308 } 309 310 311 m_sizeThreshold = (int) (newBucketCount * m_loadFactor); 312 m_buckets = newBuckets; 313 } 314 315 316 private final float m_loadFactor; // determines the setting of m_sizeThreshold 317 318 private Entry [] m_buckets; // table of buckets 319 private int m_size; // number of keys in the table, not cleared as of last check 320 private int m_sizeThreshold; // size threshold for rehashing 321 322 private static final String EOL = System.getProperty ("line.separator", "\n"); 323 324} // end of class 325// ---------------------------------------------------------------------------- 326 327