View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.util;
16  
17  import java.io.IOException;
18  import java.io.ObjectInputStream;
19  import java.io.ObjectOutputStream;
20  import java.io.Serializable;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.NoSuchElementException;
28  import java.util.Objects;
29  import java.util.Set;
30  
31  /**
32   * Simple and efficient linked map or better ordered map implementation to
33   * replace the default linked list which is heavy.
34   * <p>
35   * This map does not support null and it is not thread-safe. It implements the
36   * map interface but only for compatibility reason in the sense of replacing a
37   * regular map. Iterator and streaming methods are either not implemented or
38   * less efficient.
39   * </p>
40   * <p>
41   * It goes the extra mile to avoid the overhead of wrapper objects.
42   * </p>
43   * <p>
44   * Because you typically know what you do, we run minimal index checks only and
45   * rely on the default exceptions by Java. Why should we do things twice?
46   * </p>
47   * <p>
48   * Important Note: This is meant for small maps because to save on memory
49   * allocation and churn, we are not keeping a wrapper for a reference from the
50   * map to the list, only from the list to the map. Hence when you remove a key,
51   * we have to iterate the entire list. Mostly, half of it most likely, but still
52   * expensive enough. When you have something small like 10 to 20 entries, this
53   * won't matter that much especially when a remove might be a rare event.
54   * </p>
55   * <p>
56   * This is based on FashHashMap from XLT which is based on a version from:
57   * https://github.com/mikvor/hashmapTest/blob/master/src/main/java/map/objobj/ObjObjMap.java
58   * No concrete license specified at the source. The project is public domain.
59   * </p>
60   *
61   * @param <K> the type of the key
62   * @param <V> the type of the value
63   *
64   * @author Ren&eacute; Schwietzke
65   */
66  public class OrderedFastHashMap<K, V> implements Map<K, V>, Serializable {
67      // our placeholders in the map
68      private static final Object FREE_KEY_ = null;
69      private static final Object REMOVED_KEY_ = new Object();
70  
71      // Fill factor, must be between (0 and 1)
72      private static final double FILLFACTOR_ = 0.7d;
73  
74      // The map with the key value pairs */
75      private Object[] mapData_;
76  
77      // We will resize a map once it reaches this size
78      private int mapThreshold_;
79  
80      // Current map size
81      private int mapSize_;
82  
83      // the list to impose order, the list refers to the key and value
84      // position in the map, hence needs an update every time the
85      // map sees an update (in regards to positions).
86      private int[] orderedList_;
87  
88      // the size of the orderedList, in case we proactivly sized
89      // it larger
90      private int orderedListSize_;
91  
92      /**
93       * Default constructor which create an ordered map with default size.
94       */
95      public OrderedFastHashMap() {
96          this(8);
97      }
98  
99      /**
100      * Custom constructor to get a map with a custom size and fill factor. We are
101      * not spending time on range checks, rather use a default if things are wrong.
102      *
103      * @param size the size to use, must 0 or positive, negative values default to 0
104      */
105     public OrderedFastHashMap(final int size) {
106         if (size > 0) {
107             final int capacity = arraySize(size, FILLFACTOR_);
108 
109             this.mapData_ = new Object[capacity << 1];
110             this.mapThreshold_ = (int) (capacity * FILLFACTOR_);
111 
112             this.orderedList_ = new int[capacity];
113         }
114         else {
115             this.mapData_ = new Object[0];
116             this.mapThreshold_ = 0;
117 
118             this.orderedList_ = new int[0];
119         }
120     }
121 
122     /**
123      * Get a value for a key, any key type is permitted due to
124      * the nature of the Map interface.
125      *
126      * @param key the key
127      * @return the value or null, if the key does not exist
128      */
129     @Override
130     public V get(final Object key) {
131         final int length = this.mapData_.length;
132 
133         // nothing in it
134         if (length == 0) {
135             return null;
136         }
137 
138         int ptr = (key.hashCode() & ((length >> 1) - 1)) << 1;
139         Object k = mapData_[ptr];
140 
141         if (k == FREE_KEY_) {
142             return null; // end of chain already
143         }
144 
145         // we checked FREE
146         if (k.hashCode() == key.hashCode() && k.equals(key)) {
147             return (V) this.mapData_[ptr + 1];
148         }
149 
150         // we have not found it, search longer
151         final int originalPtr = ptr;
152         while (true) {
153             ptr = (ptr + 2) & (length - 1); // that's next index
154 
155             // if we searched the entire array, we can stop
156             if (originalPtr == ptr) {
157                 return null;
158             }
159 
160             k = this.mapData_[ptr];
161 
162             if (k == FREE_KEY_) {
163                 return null;
164             }
165 
166             if (k != REMOVED_KEY_) {
167                 if (k.hashCode() == key.hashCode() && k.equals(key)) {
168                     return (V) this.mapData_[ptr + 1];
169                 }
170             }
171         }
172     }
173 
174     /**
175      * Adds a key and value to the internal position structure.
176      *
177      * @param key the key
178      * @param value the value to store
179      * @param listPosition defines where to add the new key/value pair
180      *
181      * @return the old value or null if they key was not known before
182      */
183     private V put(final K key, final V value, final Position listPosition) {
184         if (mapSize_ >= mapThreshold_) {
185             rehash(this.mapData_.length == 0 ? 4 : this.mapData_.length << 1);
186         }
187 
188         int ptr = getStartIndex(key) << 1;
189         Object k = mapData_[ptr];
190 
191         if (k == FREE_KEY_) {
192             // end of chain already
193             mapData_[ptr] = key;
194             mapData_[ptr + 1] = value;
195 
196             // ok, remember position, it is a new entry
197             orderedListAdd(listPosition, ptr);
198 
199             mapSize_++;
200 
201             return null;
202         }
203         else if (k.equals(key)) {
204             // we check FREE and REMOVED prior to this call
205             final Object ret = mapData_[ptr + 1];
206             mapData_[ptr + 1] = value;
207 
208             // existing entry, no need to update the position
209 
210             return (V) ret;
211         }
212 
213         int firstRemoved = -1;
214         if (k == REMOVED_KEY_) {
215             firstRemoved = ptr; // we may find a key later
216         }
217 
218         while (true) {
219             ptr = (ptr + 2) & (this.mapData_.length - 1); // that's next index calculation
220             k = mapData_[ptr];
221 
222             if (k == FREE_KEY_) {
223                 if (firstRemoved != -1) {
224                     ptr = firstRemoved;
225                 }
226                 mapData_[ptr] = key;
227                 mapData_[ptr + 1] = value;
228 
229                 // ok, remember position, it is a new entry
230                 orderedListAdd(listPosition, ptr);
231 
232                 mapSize_++;
233 
234                 return null;
235             }
236             else if (k.equals(key)) {
237                 final Object ret = mapData_[ptr + 1];
238                 mapData_[ptr + 1] = value;
239 
240                 // same key, different value, this does not change the order
241 
242                 return (V) ret;
243             }
244             else if (k == REMOVED_KEY_) {
245                 if (firstRemoved == -1) {
246                     firstRemoved = ptr;
247                 }
248             }
249         }
250     }
251 
252     /**
253      * Remove a key from the map. Returns the stored value or
254      * null of the key is not known.
255      *
256      * @param key the key to remove
257      * @return the stored value or null if the key does not exist
258      */
259     @Override
260     public V remove(final Object key) {
261         final int length = this.mapData_.length;
262         // it is empty
263         if (length == 0) {
264             return null;
265         }
266 
267         int ptr = getStartIndex(key) << 1;
268         Object k = this.mapData_[ptr];
269 
270         if (k == FREE_KEY_) {
271             return null; // end of chain already
272         }
273         else if (k.equals(key)) {
274             // we check FREE and REMOVED prior to this call
275             this.mapSize_--;
276 
277             if (this.mapData_[(ptr + 2) & (length - 1)] == FREE_KEY_) {
278                 this.mapData_[ptr] = FREE_KEY_;
279             }
280             else {
281                 this.mapData_[ptr] = REMOVED_KEY_;
282             }
283 
284             final V ret = (V) this.mapData_[ptr + 1];
285             this.mapData_[ptr + 1] = null;
286 
287             // take this out of the list
288             orderedListRemove(ptr);
289 
290             return ret;
291         }
292 
293         while (true) {
294             ptr = (ptr + 2) & (length - 1); // that's next index calculation
295             k = this.mapData_[ptr];
296 
297             if (k == FREE_KEY_) {
298                 return null;
299             }
300             else if (k.equals(key)) {
301                 this.mapSize_--;
302                 if (this.mapData_[(ptr + 2) & (length - 1)] == FREE_KEY_) {
303                     this.mapData_[ptr] = FREE_KEY_;
304                 }
305                 else {
306                     this.mapData_[ptr] = REMOVED_KEY_;
307                 }
308 
309                 final V ret = (V) this.mapData_[ptr + 1];
310                 this.mapData_[ptr + 1] = null;
311 
312                 // take this out of the list
313                 orderedListRemove(ptr);
314 
315                 return ret;
316             }
317         }
318     }
319 
320     /**
321      * Returns the size of the map, effectively the number of entries.
322      *
323      * @return the size of the map
324      */
325     @Override
326     public int size() {
327         return mapSize_;
328     }
329 
330     /**
331      * Rehash the map.
332      *
333      * @param newCapacity the new size of the map
334      */
335     private void rehash(final int newCapacity) {
336         this.mapThreshold_ = (int) (newCapacity / 2 * FILLFACTOR_);
337 
338         final Object[] oldData = this.mapData_;
339 
340         this.mapData_ = new Object[newCapacity];
341 
342         // we just have to grow it and not touch it at all after that,
343         // just use it as source for the new map via the old
344         final int[] oldOrderedList = this.orderedList_;
345         final int oldOrderedListSize = this.orderedListSize_;
346         this.orderedList_ = new int[newCapacity];
347 
348         this.mapSize_ = 0;
349         this.orderedListSize_ = 0;
350 
351         // we use our ordered list as source and the old
352         // array as reference
353         // we basically rebuild the map and the ordering
354         // from scratch
355         for (int i = 0; i < oldOrderedListSize; i++) {
356             final int pos = oldOrderedList[i];
357 
358             // get us the old data
359             final K key = (K) oldData[pos];
360             final V value = (V) oldData[pos + 1];
361 
362             // write the old to the new map without updating
363             // the positioning
364             put(key, value, Position.LAST);
365         }
366     }
367 
368     /**
369      * Returns a list of all keys in order of addition.
370      * This is an expensive operation, because we get a static
371      * list back that is not backed by the implementation. Changes
372      * to the returned list are not reflected in the map.
373      *
374      * @return a list of keys as inserted into the map
375      */
376     public List<K> keys() {
377         final List<K> result = new ArrayList<>(this.orderedListSize_);
378 
379         for (int i = 0; i < this.orderedListSize_; i++) {
380             final int pos = this.orderedList_[i];
381             final Object o = this.mapData_[pos];
382             result.add((K) o);
383         }
384 
385         return result;
386     }
387 
388     /**
389      * Returns a list of all values ordered by when the key was
390      * added. This is an expensive operation, because we get a static
391      * list back that is not backed by the implementation. Changes
392      * to the returned list are not reflected in the map.
393      *
394      * @return a list of values
395      */
396     @Override
397     public List<V> values() {
398         final List<V> result = new ArrayList<>(this.orderedListSize_);
399 
400         for (int i = 0; i < this.orderedListSize_; i++) {
401             final int pos = this.orderedList_[i];
402             final Object o = this.mapData_[pos + 1];
403             result.add((V) o);
404         }
405 
406         return result;
407     }
408 
409     /**
410      * Clears the map, reuses the data structure by clearing it out. It won't shrink
411      * the underlying arrays!
412      */
413     @Override
414     public void clear() {
415         this.mapSize_ = 0;
416         this.orderedListSize_ = 0;
417         Arrays.fill(this.mapData_, FREE_KEY_);
418         // Arrays.fill(this.orderedList, 0);
419     }
420 
421     /**
422      * Get us the start index from where we search or insert into the map.
423      *
424      * @param key the key to calculate the position for
425      * @return the start position
426      */
427     private int getStartIndex(final Object key) {
428         // key is not null here
429         return key.hashCode() & ((this.mapData_.length >> 1) - 1);
430     }
431 
432     /**
433      * Return the least power of two greater than or equal to the specified value.
434      *
435      * <p>
436      * Note that this function will return 1 when the argument is 0.
437      * </p>
438      *
439      * @param x a long integer smaller than or equal to 2<sup>62</sup>.
440      * @return the least power of two greater than or equal to the specified value.
441      */
442     private static long nextPowerOfTwo(final long x) {
443         if (x == 0) {
444             return 1;
445         }
446 
447         long r = x - 1;
448         r |= r >> 1;
449         r |= r >> 2;
450         r |= r >> 4;
451         r |= r >> 8;
452         r |= r >> 16;
453 
454         return (r | r >> 32) + 1;
455     }
456 
457     /**
458      * Returns the least power of two smaller than or equal to 2<sup>30</sup> and
459      * larger than or equal to <code>Math.ceil( expected / f )</code>.
460      *
461      * @param expected the expected number of elements in a hash table.
462      * @param f        the load factor.
463      * @return the minimum possible size for a backing array.
464      * @throws IllegalArgumentException if the necessary size is larger than
465      *                                  2<sup>30</sup>.
466      */
467     private static int arraySize(final int expected, final double f) {
468         final long s = Math.max(2, nextPowerOfTwo((long) Math.ceil(expected / f)));
469 
470         if (s > (1 << 30)) {
471             throw new IllegalArgumentException(
472                     "Too large (" + expected + " expected elements with load factor " + f + ")");
473         }
474         return (int) s;
475     }
476 
477     /**
478      * Returns an entry consisting of key and value at a given position.
479      * This position relates to the ordered key list that maintain the
480      * addition order for this map.
481      *
482      * @param index the position to fetch
483      * @return an entry of key and value
484      * @throws IndexOutOfBoundsException when the ask for the position is invalid
485      */
486     public Entry<K, V> getEntry(final int index) {
487         if (index < 0 || index >= this.orderedListSize_) {
488             throw new IndexOutOfBoundsException("Index: %s, Size: %s".formatted(index, this.orderedListSize_));
489         }
490 
491         final int pos = this.orderedList_[index];
492         return new Entry(this.mapData_[pos], this.mapData_[pos + 1]);
493     }
494 
495     /**
496      * Returns the key at a certain position of the ordered list that
497      * keeps the addition order of this map.
498      *
499      * @param index the position to fetch
500      * @return the key at this position
501      * @throws IndexOutOfBoundsException when the ask for the position is invalid
502      */
503     public K getKey(final int index) {
504         if (index < 0 || index >= this.orderedListSize_) {
505             throw new IndexOutOfBoundsException("Index: %s, Size: %s".formatted(index, this.orderedListSize_));
506         }
507 
508         final int pos = this.orderedList_[index];
509         return (K) this.mapData_[pos];
510     }
511 
512     /**
513      * Returns the value at a certain position of the ordered list that
514      * keeps the addition order of this map.
515      *
516      * @param index the position to fetch
517      * @return the value at this position
518      * @throws IndexOutOfBoundsException when the ask for the position is invalid
519      */
520     public V getValue(final int index) {
521         if (index < 0 || index >= this.orderedListSize_) {
522             throw new IndexOutOfBoundsException("Index: %s, Size: %s".formatted(index, this.orderedListSize_));
523         }
524 
525         final int pos = this.orderedList_[index];
526         return (V) this.mapData_[pos + 1];
527     }
528 
529     /**
530      * Removes a key and value from this map based on the position
531      * in the backing list, rather by key as usual.
532      *
533      * @param index the position to remove the data from
534      * @return the value stored
535      * @throws IndexOutOfBoundsException when the ask for the position is invalid
536      */
537     public V remove(final int index) {
538         if (index < 0 || index >= this.orderedListSize_) {
539             throw new IndexOutOfBoundsException("Index: %s, Size: %s".formatted(index, this.orderedListSize_));
540         }
541 
542         final int pos = this.orderedList_[index];
543         final K key = (K) this.mapData_[pos];
544 
545         return remove(key);
546     }
547 
548     @Override
549     public V put(final K key, final V value) {
550         return this.put(key, value, Position.LAST);
551     }
552 
553     /**
554      * Insert at the beginning.
555      * @param key the key
556      * @param value the value
557      * @return the inserted value
558      */
559     public V addFirst(final K key, final V value) {
560         return this.put(key, value, Position.FIRST);
561     }
562 
563     /**
564      * Append at the end.
565      * @param key the key
566      * @param value the value
567      * @return the appended value
568      */
569     public V add(final K key, final V value) {
570         return this.put(key, value, Position.LAST);
571     }
572 
573     /**
574      * Append at the end.
575      * @param key the key
576      * @param value the value
577      * @return the appended value
578      */
579     public V addLast(final K key, final V value) {
580         return this.put(key, value, Position.LAST);
581     }
582 
583     /**
584      * Returns the first value.
585      *
586      * @return the first value.
587      */
588     public V getFirst() {
589         return getValue(0);
590     }
591 
592     /**
593      * Returns the last value.
594      *
595      * @return the last value.
596      */
597     public V getLast() {
598         return getValue(this.orderedListSize_ - 1);
599     }
600 
601     /**
602      * Removes the first entry.
603      * @return the removed value or null if the map was empty.
604      */
605     public V removeFirst() {
606         if (this.orderedListSize_ > 0) {
607             final int pos = this.orderedList_[0];
608             final K key = (K) this.mapData_[pos];
609             return remove(key);
610         }
611         return null;
612     }
613 
614     /**
615      * Removes the last entry.
616      * @return the removed value or null if the map was empty.
617      */
618     public V removeLast() {
619         if (this.orderedListSize_ > 0) {
620             final int pos = this.orderedList_[this.orderedListSize_ - 1];
621             final K key = (K) this.mapData_[pos];
622             return remove(key);
623         }
624         return null;
625     }
626 
627     /**
628      * Checks of a key is in the map.
629      *
630      * @param key the key to check
631      * @return true of the key is in the map, false otherwise
632      */
633     @Override
634     public boolean containsKey(final Object key) {
635         return get(key) != null;
636     }
637 
638     @Override
639     public boolean containsValue(final Object value) {
640         // that is expensive, we have to iterate everything
641         for (int i = 0; i < this.orderedListSize_; i++) {
642             final int pos = this.orderedList_[i] + 1;
643             final Object v = this.mapData_[pos];
644 
645             // do we match?
646             if (v == value || v.equals(value)) {
647                 return true;
648             }
649         }
650 
651         return false;
652     }
653 
654     @Override
655     public boolean isEmpty() {
656         return this.mapSize_ == 0;
657     }
658 
659     @Override
660     public Set<Map.Entry<K, V>> entrySet() {
661         return new OrderedEntrySet<>(this);
662     }
663 
664     @Override
665     public Set<K> keySet() {
666         return new OrderedKeySet<>(this);
667     }
668 
669     /**
670      * Just reverses the ordering of the map as created so far.
671      */
672     public void reverse() {
673         // In-place reversal
674         final int to = this.orderedListSize_ / 2;
675 
676         for (int i = 0; i < to; i++) {
677             // Swapping the elements
678             final int j = this.orderedList_[i];
679             this.orderedList_[i] = this.orderedList_[this.orderedListSize_ - i - 1];
680             this.orderedList_[this.orderedListSize_ - i - 1] = j;
681         }
682     }
683 
684     /**
685      * We have to overwrite the export due to the use of static object as marker.
686      *
687      * @param aInputStream the inputstream to read from
688      * @throws IOException when the reading from the source fails
689      * @throws ClassNotFoundException in case we cannot restore a class
690      */
691     private void readObject(final ObjectInputStream aInputStream) throws ClassNotFoundException, IOException {
692         // perform the default de-serialization first
693         aInputStream.defaultReadObject();
694 
695         // we have to restore order, keep relevant data
696         final Object[] srcData = Arrays.copyOf(this.mapData_, this.mapData_.length);
697         final int[] srcIndex = Arrays.copyOf(this.orderedList_, this.orderedList_.length);
698         final int orderedListSize = this.orderedListSize_;
699 
700         // now, empty the original map
701         clear();
702 
703         // sort things in, so we get a nice clean new map, this will
704         // also cleanup what was previously a removed entry, we have not
705         // kept that information anyway
706         for (int i = 0; i < orderedListSize; i++) {
707             final int pos = srcIndex[i];
708 
709             final K key = (K) srcData[pos];
710             final V value = (V) srcData[pos + 1];
711             put(key, value);
712         }
713     }
714 
715     /**
716      * We have to overwrite the export due to the use of static object as marker.
717      *
718      * @param aOutputStream the stream to write to
719      * @throws IOException in case we have issue writing our data to the stream
720      */
721     private void writeObject(final ObjectOutputStream aOutputStream) throws IOException {
722         // we will remove all placeholder object references,
723         // when putting it back together, we rebuild the map from scratch
724         for (int i = 0; i < this.mapData_.length; i++) {
725             final Object entry = this.mapData_[i];
726             if (entry == FREE_KEY_ || entry == REMOVED_KEY_) {
727                 this.mapData_[i] = null;
728             }
729         }
730 
731         // perform the default serialization for all non-transient, non-static fields
732         aOutputStream.defaultWriteObject();
733     }
734 
735     /**
736      * This set does not support any modifications through its interface. All such
737      * methods will throw {@link UnsupportedOperationException}.
738      */
739     static class OrderedEntrySet<K, V> implements Set<Map.Entry<K, V>> {
740         private final OrderedFastHashMap<K, V> backingMap_;
741 
742         OrderedEntrySet(final OrderedFastHashMap<K, V> backingMap) {
743             this.backingMap_ = backingMap;
744         }
745 
746         @Override
747         public int size() {
748             return this.backingMap_.size();
749         }
750 
751         @Override
752         public boolean isEmpty() {
753             return this.backingMap_.isEmpty();
754         }
755 
756         @Override
757         public boolean contains(final Object o) {
758             if (o instanceof Map.Entry ose) {
759                 final Object k = ose.getKey();
760                 final Object v = ose.getValue();
761 
762                 final Object value = this.backingMap_.get(k);
763                 if (value != null) {
764                     return v.equals(value);
765                 }
766             }
767 
768             return false;
769         }
770 
771         @Override
772         public Object[] toArray() {
773             final Object[] array = new Object[this.backingMap_.orderedListSize_];
774             return toArray(array);
775         }
776 
777         @Override
778         @SuppressWarnings("unchecked")
779         public <T> T[] toArray(final T[] a) {
780             final T[] array;
781             if (a.length >= this.backingMap_.orderedListSize_) {
782                 array = a;
783             }
784             else {
785                 array = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(),
786                         this.backingMap_.orderedListSize_);
787             }
788 
789             for (int i = 0; i < this.backingMap_.orderedListSize_; i++) {
790                 array[i] = (T) this.backingMap_.getEntry(i);
791             }
792 
793             return array;
794         }
795 
796         @Override
797         public Iterator<Map.Entry<K, V>> iterator() {
798             return new OrderedEntryIterator();
799         }
800 
801         @Override
802         public boolean add(final Map.Entry<K, V> e) {
803             throw new UnsupportedOperationException();
804         }
805 
806         @Override
807         public boolean remove(final Object o) {
808             throw new UnsupportedOperationException();
809         }
810 
811         @Override
812         public boolean containsAll(final Collection<?> c) {
813             throw new UnsupportedOperationException();
814         }
815 
816         @Override
817         public boolean addAll(final Collection<? extends Map.Entry<K, V>> c) {
818             throw new UnsupportedOperationException();
819         }
820 
821         @Override
822         public boolean retainAll(final Collection<?> c) {
823             throw new UnsupportedOperationException();
824         }
825 
826         @Override
827         public boolean removeAll(final Collection<?> c) {
828             throw new UnsupportedOperationException();
829         }
830 
831         @Override
832         public void clear() {
833             throw new UnsupportedOperationException();
834         }
835 
836         class OrderedEntryIterator implements Iterator<Map.Entry<K, V>> {
837             private int pos_ = 0;
838 
839             @Override
840             public boolean hasNext() {
841                 return pos_ < backingMap_.orderedListSize_;
842             }
843 
844             @Override
845             public Map.Entry<K, V> next() {
846                 if (pos_ < backingMap_.orderedListSize_) {
847                     return backingMap_.getEntry(pos_++);
848                 }
849                 throw new NoSuchElementException();
850             }
851         }
852     }
853 
854     static class OrderedKeySet<K, V> implements Set<K> {
855         private final OrderedFastHashMap<K, V> backingMap_;
856 
857         OrderedKeySet(final OrderedFastHashMap<K, V> backingMap) {
858             this.backingMap_ = backingMap;
859         }
860 
861         @Override
862         public int size() {
863             return this.backingMap_.size();
864         }
865 
866         @Override
867         public boolean isEmpty() {
868             return this.backingMap_.isEmpty();
869         }
870 
871         @Override
872         public boolean contains(final Object o) {
873             return this.backingMap_.containsKey(o);
874         }
875 
876         @Override
877         public Object[] toArray() {
878             final Object[] array = new Object[this.backingMap_.orderedListSize_];
879             return toArray(array);
880         }
881 
882         @Override
883         @SuppressWarnings("unchecked")
884         public <T> T[] toArray(final T[] a) {
885             final T[] array;
886 
887             if (a.length >= this.backingMap_.orderedListSize_) {
888                 array = a;
889             }
890             else {
891                 array = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(),
892                         this.backingMap_.orderedListSize_);
893             }
894 
895             for (int i = 0; i < this.backingMap_.orderedListSize_; i++) {
896                 array[i] = (T) this.backingMap_.getKey(i);
897             }
898 
899             return array;
900         }
901 
902         @Override
903         public Iterator<K> iterator() {
904             return new OrderedKeyIterator();
905         }
906 
907         class OrderedKeyIterator implements Iterator<K> {
908             private int pos_ = 0;
909 
910             @Override
911             public boolean hasNext() {
912                 return this.pos_ < backingMap_.orderedListSize_;
913             }
914 
915             @Override
916             public K next() {
917                 if (this.pos_ < backingMap_.orderedListSize_) {
918                     return backingMap_.getKey(this.pos_++);
919                 }
920                 throw new NoSuchElementException();
921             }
922         }
923 
924         @Override
925         public boolean add(final K e) {
926             throw new UnsupportedOperationException();
927         }
928 
929         @Override
930         public boolean remove(final Object o) {
931             throw new UnsupportedOperationException();
932         }
933 
934         @Override
935         public boolean containsAll(final Collection<?> c) {
936             throw new UnsupportedOperationException();
937         }
938 
939         @Override
940         public boolean addAll(final Collection<? extends K> c) {
941             throw new UnsupportedOperationException();
942         }
943 
944         @Override
945         public boolean retainAll(final Collection<?> c) {
946             throw new UnsupportedOperationException();
947         }
948 
949         @Override
950         public boolean removeAll(final Collection<?> c) {
951             throw new UnsupportedOperationException();
952         }
953 
954         @Override
955         public void clear() {
956             throw new UnsupportedOperationException();
957         }
958     }
959 
960     @Override
961     public void putAll(final Map<? extends K, ? extends V> src) {
962         if (src == this) {
963             throw new IllegalArgumentException("Cannot add myself");
964         }
965 
966         for (final Map.Entry<? extends K, ? extends V> entry : src.entrySet()) {
967             put(entry.getKey(), entry.getValue(), Position.LAST);
968         }
969     }
970 
971     private void orderedListAdd(final Position listPosition, final int position) {
972         // the list should still have room, otherwise the map was
973         // grown already and the ordering list with it
974         if (listPosition == Position.FIRST) {
975             System.arraycopy(this.orderedList_, 0, this.orderedList_, 1, this.orderedList_.length - 1);
976             this.orderedList_[0] = position;
977             this.orderedListSize_++;
978         }
979         else if (listPosition == Position.LAST) {
980             this.orderedList_[this.orderedListSize_] = position;
981             this.orderedListSize_++;
982         }
983         else {
984             // if none, we are rebuilding the map and don't have to do a thing
985         }
986     }
987 
988     private void orderedListRemove(final int position) {
989         // find the positional information
990         int i = 0;
991         for ( ; i < this.orderedListSize_; i++) {
992             if (this.orderedList_[i] == position) {
993                 this.orderedList_[i] = -1;
994                 if (i < this.orderedListSize_) {
995                     // not the last element, compact
996                     System.arraycopy(this.orderedList_, i + 1, this.orderedList_, i, this.orderedListSize_ - i);
997                 }
998                 this.orderedListSize_--;
999 
1000                 return;
1001             }
1002         }
1003 
1004         if (i == this.orderedListSize_) {
1005             throw new IllegalArgumentException("Position %s was not in order list".formatted(position));
1006         }
1007     }
1008 
1009     @Override
1010     public String toString() {
1011         final int maxLen = 10;
1012 
1013         return "mapData=%s, mapFillFactor=%s, mapThreshold=%s, mapSize=%s,%norderedList=%s, orderedListSize=%s"
1014                 .formatted(
1015                     mapData_ != null
1016                         ? Arrays.asList(mapData_).subList(0, Math.min(mapData_.length, maxLen))
1017                         : null,
1018                     FILLFACTOR_, mapThreshold_, mapSize_,
1019                     orderedList_ != null
1020                         ? Arrays.toString(Arrays.copyOf(orderedList_, Math.min(orderedList_.length, maxLen)))
1021                         : null,
1022                     orderedListSize_);
1023     }
1024 
1025     /**
1026      * Helper for identifying if we need to position our new entry differently.
1027      */
1028     private enum Position {
1029         NONE, FIRST, LAST
1030     }
1031 
1032     /**
1033      * Well, we need that to satisfy the map implementation concept.
1034      *
1035      * @param <K> the key
1036      * @param <V> the value
1037      */
1038     static class Entry<K, V> implements Map.Entry<K, V> {
1039         private final K key_;
1040         private final V value_;
1041 
1042         Entry(final K key, final V value) {
1043             this.key_ = key;
1044             this.value_ = value;
1045         }
1046 
1047         @Override
1048         public K getKey() {
1049             return key_;
1050         }
1051 
1052         @Override
1053         public V getValue() {
1054             return value_;
1055         }
1056 
1057         @Override
1058         public V setValue(final V value) {
1059             throw new UnsupportedOperationException("This map does not support write-through via an entry");
1060         }
1061 
1062         @Override
1063         public int hashCode() {
1064             return Objects.hashCode(key_) ^ Objects.hashCode(value_);
1065         }
1066 
1067         @Override
1068         public String toString() {
1069             return key_ + "=" + value_;
1070         }
1071 
1072         @Override
1073         public boolean equals(final Object o) {
1074             if (o == this) {
1075                 return true;
1076             }
1077 
1078             if (o instanceof Map.Entry<?, ?> e) {
1079 
1080                 if (Objects.equals(key_, e.getKey()) && Objects.equals(value_, e.getValue())) {
1081                     return true;
1082                 }
1083             }
1084 
1085             return false;
1086         }
1087     }
1088 }