001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.units.collections; 006 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.List; 011 012/** 013 * A variant on {@code java.util.HashMap<K, V>} that uses primitive long ints for map keys instead 014 * of autoboxed Long objects like would be used for a {@code Map<Long, V>}. 015 * 016 * @param <V> the type of the values stored in the map 017 */ 018public class LongToObjectHashMap<V> { 019 private static final int kInitialSize = 0; 020 private static final int kInitialCapacity = 8; // NOTE: must be a power of two 021 022 /** 023 * The default load factor of the hashmap. If the ratio of the number of entries to the map's 024 * capacity exceeds this value, the map will be resized (doubled capacity) in order for more 025 * values to be easily inserted. 026 */ 027 private static final double kLoadFactor = 75.00 / 100; 028 029 /** The current number of key-value pairs in the map. */ 030 private int m_size = kInitialSize; 031 032 /** 033 * The current maximum capacity of the map. Note that it will be resized before m_size reaches 034 * this value. 035 */ 036 private int m_capacity = kInitialCapacity; 037 038 /** 039 * The keys in the map. This is a sparse array, and the location of a key may not be equal to the 040 * result of calling {@link #bucket(long)} on that key. To handle hash collisions, if a bucket is 041 * already in use when trying to insert a value, the bucket number is incremented (wrapping around 042 * to 0 if it's equal to m_capacity) and <i>that</i> bucket is checked to see if it's available. 043 * This process continues until an empty bucket is found (which is guaranteed because m_size is 044 * always less than m_capacity). 045 */ 046 private long[] m_keys = new long[m_capacity]; 047 048 /** Tracks which buckets are actually used (have a key-value mapping). */ 049 private boolean[] m_uses = new boolean[m_capacity]; 050 051 /** 052 * The values in the map. See the documentation for m_keys for how indexing into this array works. 053 */ 054 @SuppressWarnings("unchecked") 055 private V[] m_values = (V[]) new Object[m_capacity]; 056 057 /** Default constructor. */ 058 public LongToObjectHashMap() {} 059 060 /** 061 * Puts a value {@code value} corresponding to key {@code key} in the map. 062 * 063 * @param key the associated key 064 * @param value the value to insert 065 * @return the previous value that was mapped to the key, or null if no such value existed 066 */ 067 public V put(long key, V value) { 068 int bucket = bucket(key); 069 070 // Increment the bucket until we hit an open space (there's always going to be at least one) 071 while (m_uses[bucket]) { 072 if (m_keys[bucket] == key) { 073 // replace the existing value 074 var oldValue = m_values[bucket]; 075 m_values[bucket] = value; 076 return oldValue; 077 } 078 bucket = safeIncrement(bucket); 079 } 080 081 m_uses[bucket] = true; 082 m_keys[bucket] = key; 083 m_values[bucket] = value; 084 m_size++; 085 086 if (m_size > maxSize()) { 087 grow(); 088 } 089 return null; 090 } 091 092 /** 093 * Gets the value associated with the given key. 094 * 095 * @param key the key to retrieve the value for 096 * @return the value mapped to the key, or null if the key is not in the map 097 */ 098 public V get(long key) { 099 int bucket = bucket(key); 100 while (m_uses[bucket]) { 101 if (m_keys[bucket] == key) { 102 // found it 103 return m_values[bucket]; 104 } 105 bucket = safeIncrement(bucket); 106 } 107 return null; 108 } 109 110 /** 111 * Removes the value associated with the given key and returns it. 112 * 113 * @param key the key to remove 114 * @return the value corresponding to the key, or null if the key is not in the map 115 */ 116 public V remove(long key) { 117 int bucket = bucket(key); 118 while (m_uses[bucket]) { 119 if (m_keys[bucket] == key) { 120 // found it 121 // TODO: Shrink the map when below a certain load factor 122 // Current use cases don't remove elements from the map, so there's not much use 123 // for shrinking at the moment. 124 m_size--; 125 m_keys[bucket] = 0L; 126 m_uses[bucket] = false; 127 128 var oldValue = m_values[bucket]; 129 m_values[bucket] = null; 130 return oldValue; 131 } 132 bucket = safeIncrement(bucket); 133 } 134 135 return null; 136 } 137 138 /** 139 * Checks if a key is contained in the map. 140 * 141 * @param key the key to check 142 * @return true if the key has an associated value, false if not 143 */ 144 public boolean containsKey(long key) { 145 int bucket = bucket(key); 146 while (m_uses[bucket]) { 147 if (m_keys[bucket] == key) { 148 return true; 149 } 150 bucket = safeIncrement(bucket); 151 } 152 return false; 153 } 154 155 /** Clears and removes all entries from the map. */ 156 public void clear() { 157 if (m_size == 0) { 158 // Nothing to do 159 return; 160 } 161 m_size = 0; 162 163 Arrays.fill(m_uses, false); 164 Arrays.fill(m_keys, 0L); 165 Arrays.fill(m_values, null); 166 } 167 168 /** 169 * Gets the number of key-value pairs currently contained in the map. 170 * 171 * @return the current size of the map 172 */ 173 public int size() { 174 return m_size; 175 } 176 177 // package-private for tests 178 int capacity() { 179 return m_capacity; 180 } 181 182 /** 183 * Checks if the map contains any entries. 184 * 185 * @return true if at least one entry is present, false otherwise 186 */ 187 public boolean isEmpty() { 188 return m_size == 0; 189 } 190 191 /** 192 * Gets the keys contained in the map. Ordering is not guaranteed. The returned set is read-only 193 * and immutable. This uses a custom class for primitive long values to avoid unnecessary 194 * autoboxing to {@code java.lang.Long}. 195 * 196 * @return a read-only set of keys 197 */ 198 public ReadOnlyPrimitiveLongSet keySet() { 199 // copy the sparse key array into a compact array 200 final long[] keys = new long[m_size]; 201 int i = 0; 202 for (int bucket = 0; bucket < m_capacity; bucket++) { 203 if (m_uses[bucket]) { 204 keys[i] = m_keys[bucket]; 205 i++; 206 } 207 } 208 209 return new ReadOnlyPrimitiveLongSet(keys); 210 } 211 212 /** 213 * Gets the values contained in the map. Ordering is not guaranteed. The returned collection is 214 * read-only and immutable. 215 * 216 * @return a read-only collection of values 217 */ 218 public Collection<V> values() { 219 Collection<V> values = new ArrayList<>(); 220 for (int bucket = 0; bucket < m_capacity; bucket++) { 221 if (m_uses[bucket]) { 222 values.add(m_values[bucket]); 223 } 224 } 225 return List.copyOf(values); // return a readonly copy 226 } 227 228 /** 229 * Interface for map iterator function. 230 * 231 * @param <V> Value type. 232 */ 233 @FunctionalInterface 234 public interface IteratorFunction<V> { 235 /** 236 * Accepts a key-value pair from the map. 237 * 238 * @param key The key. 239 * @param value The value. 240 */ 241 void accept(long key, V value); 242 } 243 244 /** 245 * Iterates over every key-value pair in the map and passes them to the given function. 246 * 247 * @param function the function to apply to every key-value pair. 248 */ 249 public void forEach(IteratorFunction<? super V> function) { 250 for (int bucket = 0; bucket < m_capacity; bucket++) { 251 if (m_uses[bucket]) { 252 function.accept(m_keys[bucket], m_values[bucket]); 253 } 254 } 255 } 256 257 private void grow() { 258 final int currentSize = m_size; 259 final int oldCapacity = m_capacity; 260 if (oldCapacity * kLoadFactor >= currentSize) { 261 // We're below the maximum allowed size for the current capacity 262 // Nothing to do 263 return; 264 } 265 266 final int newCapacity = oldCapacity * 2; 267 final int newMask = newCapacity - 1; 268 269 final boolean[] oldUses = m_uses; 270 final long[] oldKeys = m_keys; 271 final V[] oldValues = m_values; 272 273 final boolean[] newUses = new boolean[newCapacity]; 274 final long[] newKeys = new long[newCapacity]; 275 @SuppressWarnings("unchecked") 276 final V[] newValues = (V[]) new Object[newCapacity]; 277 278 for (int oldBucket = 0; oldBucket < oldCapacity; oldBucket++) { 279 if (!oldUses[oldBucket]) { 280 // Bucket is empty, skip 281 continue; 282 } 283 final long key = oldKeys[oldBucket]; 284 final V value = oldValues[oldBucket]; 285 286 int newBucket = (int) (hash(key) & newMask); 287 while (newUses[newBucket]) { 288 newBucket = (newBucket + 1) & newMask; 289 } 290 291 newUses[newBucket] = true; 292 newKeys[newBucket] = key; 293 newValues[newBucket] = value; 294 } 295 296 m_capacity = newCapacity; 297 m_uses = newUses; 298 m_keys = newKeys; 299 m_values = newValues; 300 } 301 302 private int maxSize() { 303 return (int) (m_capacity * kLoadFactor); 304 } 305 306 /** 307 * Calculates a hashcode for an input key. Does some bit shuffling to account for poor hash 308 * functions. 309 * 310 * @param key the key to hash 311 * @return a hashcode for the input key 312 */ 313 private long hash(long key) { 314 return 31 + (key ^ (key >>> 15) ^ (key >>> 31) ^ (key << 31)); 315 } 316 317 /** 318 * The mask to use when translating a hashcode to a bucket index. Relies on m_capacity being a 319 * power of two. 320 */ 321 private int mask() { 322 return m_capacity - 1; 323 } 324 325 /** 326 * Calculates the desired bucket index for a particular key. Does nothing to handle the case where 327 * the calculated index is already in use by another key. 328 * 329 * @param key the key to get the bucket for 330 * @return the desired bucket index 331 */ 332 private int bucket(long key) { 333 var hash = hash(key); 334 return (int) (hash & mask()); 335 } 336 337 /** 338 * Increments a bucket index by 1, wrapping around to 0 if the index is already at the maximum. 339 * 340 * @param bucket the index to increment 341 * @return the incremented bucket index 342 */ 343 private int safeIncrement(int bucket) { 344 return (bucket + 1) & mask(); 345 } 346}