Class IndexBiMap<K,V>
- Direct Known Subclasses:
IndexBiMap.StateMapBase
A peculiar data structure that allows to bidirectionally map two types of indexed data K and V.
It can be seen as a generalization and a specialization of a classic BiMap collection at the same time.
Usually a BiMap uses two types of data K and V to produce mappings of type [K -> V]
and [V -> K].
However, for VirtualizedFX's needs this works on three types of data: Integer, K and V.
This is especially useful when we want to "convert" a flat data structure like a List or even an array so that every
element is mapped by its position in the origin collection. Take this list as an example:
List<String> strings = List.of(
"String 0",
"String 1",
"String 2",
"String 3",
"String 0",
"String 5",
"String 8",
"String 2",
"String 7",
"String 0",
)
With a data structure like this you could achieve something like this:
IndexBiMap<Integer, String> biMap = new IndexBiMap<>();
for (int i = 0: i < strings.size(); i++) {
biMap.put(i, i, strings.get(i));
}
// The above example shows how this collection can be used as a classical BiMap too (in case K are integers!)
However, the true nature of this data structure is to achieve something like this:
List<V> values = ..; // These values are generated from a String in the strings list
IndexBiMap<String, V> biMap = new IndexBiMap<>();
for (int i = 0; i < strings.size(); i++) {
String s = strings.get(i);
V val = values.get(i);
biMap.put(i, s, val);
}
// Get a V val from index...
V val = biMap.get(0); // returns "String 0"
// Get a V val from a K object (will be simplified for easier comprehension, read more below)
Integer index = biMap.get("String 3"); // returns 3
V val = biMap.get(index); // returns "String 3"
If you carefully analyze the above examples, you may be able to spot an issue with this data structure: duplicates.
Since this is basically a wrapper for two Maps, the moment we decide to map K objects to V values,
we introduce this issue, and we must deal with it.
In collections such as the List (even arrays) there is one to one association between the index/position of an element and the element itself.
At index 0 we have "String 0", nothing else. Same applies for other indexes.
This means that we can also see the List as a Map of type [Integer->V], however, the contrary does not apply. In fact,
a string "String 1" could appear at different positions in the List. So, how do we deal with this?
Simple, we store the positions in a collection.
The IndexBiMap, like said above, uses two maps:
-
A
TreeMapfor the mappings of type[Integer -> V]. A simpleHashMapcould be used too, but I thought that having positions sorted is a nice to have, especially if we want to 'poll' values from the map as we would do from aDeque(TreeMap is aSequencedMap). This is called thebyIndexmap. -
An
IdentityHashMapfor the mappings of type[K -> Collection<Integer>]. To be precise the collection used to store all the positions for a certainKobject is aSequencedSet, which again allows us to 'poll' values when needed. Here, such operation, is especially needed because since all the positions in the set point to the same element, it does not matter which one we take, soSequencedCollection.removeFirst()comes in handy. This is called thebyKeymap. -
These are two to "concrete" mappings, however the second one is automatically resolved by the map like this:
[K -> Integer -> V]which can be simplified to[K -> V].
Q:Why an IdentityHashMap?
A: There are two things to consider. First, let's not forget that the true nature of this data structure is to
map a type K which may come from our dataset, to certain V objects that depend on K
items. For example: in VirtualizedFX cells depend on the items T and we want to reuse them as much as possible
for performance reasons. Second, the above example is not really suited to understand this, because String in
Java is a particular case. Strings that are equal in value are also equal in reference, same object, because Java caches
string literals for performance and memory reasons. Instead, let's consider this example:
// Let's say this is our model class
public record User(int id, String name, String email) {}
List<User> users = ...;
// In this list, we may have two Users with a different reference (different object) but with the same values (equal)
// Each user is used in an object of type V
List<V> values = ...;
// Let's suppose now, we make some changes to the users list (additions, removals, updates,...)
// And now we need to update the V values as well
// We want to ignore those for which the User is the same, and update those for which the User at pos i in the list is now different
// Here's where IDENTITY is way more important than EQUALITY
// Let's suppose that during the update, because we consider equality instead of identity, we accidentally swap the
// User objects of two V values. What do you think it may happen?
// It may happen that if we update the values of a certain User object (ignore that it is a record), we may not see the
// change in the associated V object, because we swapped it. So, instead, we would see the change in an entirely different V object
Retrievals
This data structure provides to way of retrieving V values, either by index get(Integer) or by key
get(Object).
Additions
This data structure provides a single put(Integer, Object, Object) method, which requires both the key and the
value, as well as the value's index.
Removals Removals are critical operations!
There are three removal methods, although one is a delegate: remove(Integer), remove(Integer, boolean)
and remove(Object).
As also described in the method documentation, when values are removed by index, we may potentially have an invalid
data structure afterward. While it's easy to resolve a reverse mapping like this [K -> Integer -> V], it's not
as straightforward to do this [Integer -> K] because we don't have such direct mapping.
In other words, when a value is removed from the byIndex map, there is not a fast way to also remove it from
the byKey map. The only way is to iterate over its Sets and when the index is found in one of them,
remove it and break out of the loop. Other details here: remove(Integer, boolean).
Misc
This data structure also allows you to check whether a value is present either by index or by key: contains(Integer),
contains(Object).
There is also a simple check on the two maps sizes for the data structure's validity: isValid().
Usage in VirtualizedFX
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic classIndexBiMap.RowsStateMap<T, C extends VFXCell<T>>Extension ofIndexBiMap.StateMapBasewhich uses mappings of type:[Integer -> VFXCell],[Column -> Collection<Integer>]and[Column -> Integer -> VFXCell].static classIndexBiMap.StateMap<T, C extends VFXCell<T>>Extension ofIndexBiMap.StateMapBasewhich uses mappings of type:[Integer -> VFXCell],[T -> Collection<Integer>]and[T -> Integer -> VFXCell].static classExtension ofIndexBiMapthat introduces polling methods:IndexBiMap.StateMapBase.pollFirst(),IndexBiMap.StateMapBase.pollLast(). -
Field Summary
FieldsModifier and TypeFieldDescriptionprotected final SequencedMap<Integer, V> protected final Map<K, SequencedSet<Integer>> -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionFlattens the values of thebyKeymap (which uses mappings of type[k, SequencedSet<Integer>]to a singleSet.voidclear()Clears both the maps.booleanbooleanTries to retrieve a value for the given index from thebyIndexmap.Tries to retrieve a list of values for the given key from thebyKeymap.Map<K, SequencedSet<Integer>> getByKey()booleanisEmpty()booleanisValid()The size of thebyKeymap cannot be retrieved by simply callingMap.size()because of duplicates.voidAdds the appropriate mappings for the given parameters to both the maps by this data structure.Delegates the removal toremove(Integer, boolean).Tries to remove a value from thebyIndexmap by the given index.Before the actual value can be removed, the mapping [K,SequencedSet] must be resolved. resolve()Starting from the two mappings[Integer, V]``[K, SequencedSet<Integer>]this method wants to resolve them to a single mapping of type[K, V].intsize()
-
Field Details
-
byIndex
-
byKey
-
-
Constructor Details
-
IndexBiMap
public IndexBiMap()
-
-
Method Details
-
get
-
get
Tries to retrieve a list of values for the given key from the
byKeymap.First retrieves a
SequencedSet, then if it isnullreturns an empty list. If it is empty, then the mapping is not valid anymore, therefore, it's removed from the map, and empty list returned. Otherwise, the indexes in theSetare resolved by callingget(Integer)and the values returned in a list. We also make sure that the list will not contain anynullvalue.See
IndexBiMapto understand why this may return multiple values (hint: duplicates). -
put
Adds the appropriate mappings for the given parameters to both the maps by this data structure.
First the entry
[Integer, V]is added to thebyIndexmap.Then the entry
[K, SequencedSet<Integer>]is added to thebyKeymap.See
IndexBiMapto understand why the second mapping is like that. -
contains
- Returns:
- whether an entry for the given index is present in the
byIndexmap
-
contains
- Returns:
- whether an entry for the given key is present in the
byKeymap
-
remove
Tries to remove a value from the
byIndexmap by the given index.When such operation occurs, the two wrapped maps become desynchronized; therefore, the bimap becomes invalid. The issue is that there is no fast way to also remove the value from the
byKeymap, because the mappings are[K, SequencedSet<Integer>], so the only way is to iterate over the other map.So, by design choice, this data structure prioritizes speed rather than reliability.
The
validateparameter allows you to keep the data structure valid by performing the aforementioned iteration. For everySequencedSetin thebyKeymap we attempt at removing theindexparameter. If the removal was successful, it means that we found the correct mapping thus we break out of the loop. Also, if theSetbecomes empty after the removal, the mapping is removed from thebyKeymap.Q: How can you be sure that the Set containing the given index is the right mapping?
A: there is a little issue with this data structure. The moment we decide to have mappings from a key
Kto an indexInteger, we must take into account duplicates. For example, let's consider a list. We can confidently assert that at every index corresponds one and only one item. However, we cannot make the reverse claim: that at every item corresponds one and only one index. It's possible for the same item to appear multiple times at different positions within the list.So, by the first assertion, if for a key
KtheSetof indexes contains the given index, then it is for sure the right mapping. -
remove
Delegates the removal to
remove(Integer, boolean).By default, this will not validate the data structure, meaning that after a removal done by this method, the bimap will become invalid!
See
remove(Integer, boolean)for what invalid means. -
remove
Before the actual value can be removed, the mapping [K,SequencedSet
] must be resolved. First a
Setof indexes is retrieved (not removed!) from thebyKeymap by the given key. If theSetis null or empty,nullis returned. In the latter case, the mapping is also removed.Otherwise, one of the indexes is removed from it by using
SequencedCollection.removeFirst(), and then we can remove and return the value by the retrieved index from thebyIndexmap.This method is the reason we use a
SequencedSetto store the indexes. We can benefit from the fast operations of aSetwhile being able to poll the head of the collection just likeDeque.poll().Note The retrieved Set contains all the positions for the given
Kobject. Which means that, no matter which index we remove from the Set, it will point to the sameKinstance anyway. SeeIdentityHashMap. -
size
public int size()- Returns:
- the size of this data structure. Since it is expected for both the maps to have the same size,
this delegates to the
Map.size()method of thebyIndexmap.
-
isEmpty
public boolean isEmpty()- Returns:
- whether the data structure is empty. Since it is expected for both the maps to have the same size,
this delegates to the
Map.isEmpty()method of thebyKeymap.
-
isValid
public boolean isValid()The size of thebyKeymap cannot be retrieved by simply callingMap.size()because of duplicates. For this reason, first we need to flatten the values into another collection, and then we can check the size of that collection.- Returns:
- whether the two maps have the same size
- See Also:
-
clear
public void clear()Clears both the maps. -
resolve
Starting from the two mappings
[Integer, V]``[K, SequencedSet<Integer>]this method wants to resolve them to a single mapping of type[K, V]. The issue, however, once again is duplicates.Because the
byKeymap is designed to take duplicates into account, we would have to resolve the mappings as follows[K, Collection<V>]. However, by design, I decided that it's better to have a flat collection.For this reason, the method iterates on each entry of the
byKeymap, and for each index in theSequencedSetcreates anMap.Entryof type[K, V], by resolving the index to a value usingget(Integer), and then adds it to aList.Because of the nested for loops, this may be a costly operation, use only if necessary!
-
getByIndex
- Returns:
- the map used to store the values by their index [Integer,V], a copy!
-
getByKey
- Returns:
- the map used to store the indexes by key [K,Integer], a copy!
-
byKeysFlattened
Flattens the values of thebyKeymap (which uses mappings of type[k, SequencedSet<Integer>]to a singleSet. UsesStream.flatMap(Function).
-