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