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