001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.collections; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.lang.reflect.*; 023import java.util.*; 024import java.util.stream.*; 025 026import org.apache.juneau.commons.utils.*; 027 028/** 029 * An unmodifiable, fixed-size map implementation backed by parallel key and value arrays. 030 * 031 * <p> 032 * This class provides a simple, efficient, immutable map implementation for scenarios where the 033 * set of keys and values is known in advance and doesn't need to change. It's particularly useful 034 * for small maps (typically less than 10 entries) where the overhead of a {@link HashMap} is not 035 * justified. 036 * 037 * <h5 class='section'>Features:</h5> 038 * <ul class='spaced-list'> 039 * <li><b>Unmodifiable:</b> All mutation operations throw {@link UnsupportedOperationException} 040 * <li><b>Fixed Keys and Values:</b> Keys and values are set at construction time 041 * <li><b>Insertion Order:</b> Preserves the order of keys as provided in the constructor 042 * <li><b>Null Support:</b> Supports null keys and values (unlike {@link Map#of()}) 043 * <li><b>Array-Backed:</b> Uses simple arrays for storage, avoiding hash computation overhead 044 * <li><b>Memory Efficient:</b> Minimal memory footprint compared to {@link HashMap} 045 * <li><b>Predictable Performance:</b> O(n) lookup time, but faster than {@link HashMap} for small n 046 * </ul> 047 * 048 * <h5 class='section'>Use Cases:</h5> 049 * <ul class='spaced-list'> 050 * <li>Immutable configuration maps with a fixed set of known keys 051 * <li>Small lookup tables that should not be modified 052 * <li>Method return values where immutability is desired 053 * <li>Situations where map size is small and keys are known at compile time 054 * </ul> 055 * 056 * <h5 class='section'>Usage:</h5> 057 * <p class='bjava'> 058 * <jc>// Create an unmodifiable map with fixed keys and values</jc> 059 * String[] <jv>keys</jv> = {<js>"host"</js>, <js>"port"</js>, <js>"timeout"</js>}; 060 * Object[] <jv>values</jv> = {<js>"localhost"</js>, 8080, 30000}; 061 * 062 * SimpleMap<String,Object> <jv>config</jv> = <jk>new</jk> SimpleMap<>(<jv>keys</jv>, <jv>values</jv>); 063 * 064 * <jc>// Get values</jc> 065 * String <jv>host</jv> = (String)<jv>config</jv>.get(<js>"host"</js>); <jc>// Returns: "localhost"</jc> 066 * Integer <jv>port</jv> = (Integer)<jv>config</jv>.get(<js>"port"</js>); <jc>// Returns: 8080</jc> 067 * 068 * <jc>// Cannot modify</jc> 069 * <jv>config</jv>.put(<js>"port"</js>, 9090); <jc>// Throws UnsupportedOperationException</jc> 070 * </p> 071 * 072 * <h5 class='section'>Restrictions:</h5> 073 * <ul class='spaced-list'> 074 * <li><b>Immutable:</b> Cannot add, remove, or modify entries after construction 075 * <li><b>Fixed Size:</b> Size is determined at construction time 076 * <li><b>Unique Keys:</b> Keys must be unique (no duplicates) 077 * <li><b>Array Length:</b> Keys and values arrays must have the same length 078 * </ul> 079 * 080 * <h5 class='section'>Performance Characteristics:</h5> 081 * <ul class='spaced-list'> 082 * <li><b>get(key):</b> O(n) - Linear search through keys array 083 * <li><b>size():</b> O(1) - Direct array length access 084 * <li><b>Memory:</b> Lower overhead than {@link HashMap} for small maps 085 * </ul> 086 * 087 * <h5 class='section'>Thread Safety:</h5> 088 * <p> 089 * This class is thread-safe since it's immutable after construction. Multiple threads can safely 090 * read from a SimpleMap concurrently without synchronization. 091 * 092 * <h5 class='section'>Example - Configuration Map:</h5> 093 * <p class='bjava'> 094 * <jc>// Create immutable configuration</jc> 095 * SimpleMap<String,Object> <jv>defaults</jv> = <jk>new</jk> SimpleMap<>( 096 * <jk>new</jk> String[]{<js>"maxConnections"</js>, <js>"timeout"</js>, <js>"retries"</js>}, 097 * <jk>new</jk> Object[]{100, 5000, 3} 098 * ); 099 * 100 * <jc>// Safe to share across threads</jc> 101 * <jk>return</jk> <jv>defaults</jv>; 102 * </p> 103 * 104 * <h5 class='section'>Comparison with Map.of():</h5> 105 * <ul class='spaced-list'> 106 * <li><b>Null Support:</b> SimpleMap supports null keys/values, Map.of() does not 107 * <li><b>Insertion Order:</b> SimpleMap preserves insertion order, Map.of() does not 108 * <li><b>Size Limit:</b> Map.of() limited to 10 entries, SimpleMap has no limit 109 * </ul> 110 * 111 * <h5 class='section'>See Also:</h5> 112 * <ul> 113 * <li class='jc'>{@link SimpleMap} - The modifiable version of this class 114 * <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview > juneau-commons</a> 115 * </ul> 116 * 117 * @param <K> The key type. 118 * @param <V> The value type. 119 */ 120@SuppressWarnings({ 121 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 122 "java:S3740", // Raw Map/Iterator types used in simple map structural operations where key/value types are erased 123 "javabugs:S2259" // entrySet() never returns null under the Map contract. 124}) 125public class SimpleMap<K,V> extends AbstractMap<K,V> { 126 127 /** 128 * Inner class representing a single key-value entry in this map. 129 * <p> 130 * This entry is unmodifiable - calling {@link #setValue(Object)} will throw 131 * {@link UnsupportedOperationException}. 132 */ 133 class SimpleUnmodifiableMapEntry implements Map.Entry<K,V> { 134 135 /** The index into the keys/values arrays for this entry. */ 136 private final int index; 137 138 /** 139 * Constructor. 140 * @param index The array index for this entry. 141 */ 142 SimpleUnmodifiableMapEntry(int index) { 143 this.index = index; 144 } 145 146 @Override /* Map.Entry */ 147 public K getKey() { return keys[index]; } 148 149 @Override /* Map.Entry */ 150 public V getValue() { return values2[index]; } 151 152 @Override /* Map.Entry */ 153 public V setValue(V val) { 154 throw uoex("Map is unmodifiable"); 155 } 156 157 @Override 158 public String toString() { 159 return getKey() + "=" + getValue(); 160 } 161 162 @Override 163 public boolean equals(Object o) { 164 return (o instanceof Map.Entry<?,?> o2) && Shorts.eq(this, o2, (x,y) -> Shorts.eq(x.getKey(), y.getKey()) && Shorts.eq(x.getValue(), y.getValue())); 165 } 166 167 @Override 168 public int hashCode() { 169 K key = getKey(); 170 V value = getValue(); 171 return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); 172 } 173 } 174 175 /** The array of keys. Keys are immutable after construction. */ 176 final K[] keys; 177 178 /** The array of values. Values are immutable after construction. */ 179 final V[] values2; 180 181 /** Pre-constructed entries array for {@link #entrySet()}. */ 182 final SimpleUnmodifiableMapEntry[] entries; 183 184 /** 185 * Constructs a new SimpleMap with the specified keys and values. 186 * 187 * <p> 188 * The keys and values arrays are stored by reference (not copied), so external 189 * modifications to the arrays after construction will affect the map's behavior. 190 * For true immutability, ensure the arrays are not modified after passing them 191 * to this constructor. 192 * 193 * <h5 class='section'>Example:</h5> 194 * <p class='bjava'> 195 * String[] <jv>keys</jv> = {<js>"name"</js>, <js>"age"</js>, <js>"city"</js>}; 196 * Object[] <jv>values</jv> = {<js>"John"</js>, 30, <js>"NYC"</js>}; 197 * 198 * SimpleMap<String,Object> <jv>person</jv> = <jk>new</jk> SimpleMap<>(<jv>keys</jv>, <jv>values</jv>); 199 * </p> 200 * 201 * @param keys The map keys. Must not be <jk>null</jk>. Individual keys can be <jk>null</jk>. 202 * @param values The map values. Must not be <jk>null</jk> and must have the same length as keys. 203 * Individual values can be <jk>null</jk>. 204 * @throws IllegalArgumentException if: 205 * <ul> 206 * <li>The keys array is <jk>null</jk> 207 * <li>The values array is <jk>null</jk> 208 * <li>The keys and values arrays have different lengths 209 * <li>Any key appears more than once (duplicate keys) 210 * </ul> 211 */ 212 @SuppressWarnings({ 213 "unchecked" // Type erasure requires cast for array-based map construction 214 }) 215 public SimpleMap(K[] keys, V[] values) { 216 reqnn("keys", keys, "values", values); 217 req(keys.length == values.length, "keys '%s' and values '%s' array lengths differ", keys.length, values.length); 218 219 // Check for duplicate keys 220 for (var i = 0; i < keys.length; i++) { 221 for (var j = i + 1; j < keys.length; j++) { 222 if (Shorts.eq(keys[i], keys[j])) { 223 throw iaex("Duplicate key found: %s", keys[i]); 224 } 225 } 226 } 227 228 this.keys = keys; 229 this.values2 = values; 230 entries = (SimpleUnmodifiableMapEntry[])Array.newInstance(SimpleUnmodifiableMapEntry.class, keys.length); 231 for (var i = 0; i < keys.length; i++) { 232 entries[i] = new SimpleUnmodifiableMapEntry(i); 233 } 234 } 235 236 /** 237 * Returns a {@link Set} view of the mappings contained in this map. 238 * 239 * <p> 240 * The returned set is unmodifiable and backed by the underlying entries array. 241 * 242 * <h5 class='section'>Example:</h5> 243 * <p class='bjava'> 244 * SimpleMap<String,Integer> <jv>map</jv> = <jk>new</jk> SimpleMap<>( 245 * <jk>new</jk> String[]{<js>"a"</js>, <js>"b"</js>}, 246 * <jk>new</jk> Integer[]{1, 2} 247 * ); 248 * 249 * <jk>for</jk> (Map.Entry<String,Integer> <jv>entry</jv> : <jv>map</jv>.entrySet()) { 250 * System.<jsf>out</jsf>.println(<jv>entry</jv>.getKey() + <js>"="</js> + <jv>entry</jv>.getValue()); 251 * } 252 * </p> 253 * 254 * @return An unmodifiable set view of the mappings in this map. 255 */ 256 @Override /* Map */ 257 public Set<Map.Entry<K,V>> entrySet() { 258 return u(toSet(entries)); 259 } 260 261 /** 262 * Returns the value associated with the specified key. 263 * 264 * <p> 265 * This method performs a linear search through the keys array, using {@link Object#equals(Object)} 266 * for comparison. For small maps (typically less than 10 entries), this is often faster than 267 * the hash lookup in {@link HashMap}. 268 * 269 * <h5 class='section'>Example:</h5> 270 * <p class='bjava'> 271 * SimpleMap<String,Integer> <jv>map</jv> = <jk>new</jk> SimpleMap<>( 272 * <jk>new</jk> String[]{<js>"age"</js>, <js>"score"</js>}, 273 * <jk>new</jk> Integer[]{25, 100} 274 * ); 275 * 276 * Integer <jv>age</jv> = <jv>map</jv>.get(<js>"age"</js>); <jc>// Returns: 25</jc> 277 * Integer <jv>height</jv> = <jv>map</jv>.get(<js>"height"</js>); <jc>// Returns: null (key not found)</jc> 278 * </p> 279 * 280 * @param key The key whose associated value is to be returned. Can be <jk>null</jk>. 281 * @return The value associated with the specified key, or <jk>null</jk> if the key is not found 282 * (or if the key is mapped to <jk>null</jk>). 283 */ 284 @Override /* Map */ 285 public V get(Object key) { 286 for (var i = 0; i < keys.length; i++) 287 if (Shorts.eq(keys[i], key)) 288 return values2[i]; 289 return null; 290 } 291 292 /** 293 * Returns a {@link Set} view of the keys contained in this map. 294 * 295 * <p> 296 * The returned set is unmodifiable and backed by the underlying keys array. 297 * 298 * <h5 class='section'>Example:</h5> 299 * <p class='bjava'> 300 * SimpleMap<String,Integer> <jv>map</jv> = <jk>new</jk> SimpleMap<>( 301 * <jk>new</jk> String[]{<js>"x"</js>, <js>"y"</js>, <js>"z"</js>}, 302 * <jk>new</jk> Integer[]{1, 2, 3} 303 * ); 304 * 305 * Set<String> <jv>keys</jv> = <jv>map</jv>.keySet(); <jc>// Returns: [x, y, z]</jc> 306 * </p> 307 * 308 * @return An unmodifiable set view of the keys in this map. 309 */ 310 @Override /* Map */ 311 public Set<K> keySet() { 312 return u(toSet(keys)); 313 } 314 315 /** 316 * Throws {@link UnsupportedOperationException} as this map is unmodifiable. 317 * 318 * @param key Ignored. 319 * @param value Ignored. 320 * @return Never returns normally. 321 * @throws UnsupportedOperationException Always thrown, as this map cannot be modified. 322 */ 323 @Override /* Map */ 324 public V put(K key, V value) { 325 throw uoex("Map is unmodifiable"); 326 } 327 328 /** 329 * Returns a string representation of this map. 330 * 331 * <p> 332 * The format follows the standard Java map convention: <c>"{key1=value1, key2=value2, ...}"</c> 333 * 334 * @return A string representation of this map. 335 */ 336 @Override 337 public String toString() { 338 return entrySet().stream().map(Object::toString).collect(Collectors.joining(", ", "{", "}")); 339 } 340 341 /** 342 * Compares the specified object with this map for equality. 343 * 344 * <p> 345 * Returns <jk>true</jk> if the given object is also a map and the two maps represent the same 346 * mappings. More formally, two maps <c>m1</c> and <c>m2</c> represent the same mappings if 347 * <c>m1.entrySet().equals(m2.entrySet())</c>. 348 * 349 * <p> 350 * This implementation compares the entry sets of the two maps. 351 * 352 * @param o Object to be compared for equality with this map. 353 * @return <jk>true</jk> if the specified object is equal to this map. 354 */ 355 @Override 356 public boolean equals(Object o) { 357 return (o instanceof Map o2) && Shorts.eq(this, o2, (x, y) -> x.entrySet().equals(y.entrySet())); 358 } 359 360 /** 361 * Returns the hash code value for this map. 362 * 363 * <p> 364 * The hash code of a map is defined to be the sum of the hash codes of each entry in the map's 365 * <c>entrySet()</c> view. This ensures that <c>m1.equals(m2)</c> implies that 366 * <c>m1.hashCode()==m2.hashCode()</c> for any two maps <c>m1</c> and <c>m2</c>, as required 367 * by the general contract of {@link Object#hashCode()}. 368 * 369 * <p> 370 * This implementation computes the hash code from the entry set. 371 * 372 * @return The hash code value for this map. 373 */ 374 @Override 375 public int hashCode() { 376 return entrySet().hashCode(); 377 } 378}