Skip to content

Commit a126dc8

Browse files
committed
HHH-18326 New utility collections based on instance identity
1 parent bd557ef commit a126dc8

File tree

5 files changed

+629
-1
lines changed

5 files changed

+629
-1
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.engine.spi;
6+
7+
/**
8+
* Contract for classes whose instances are uniquely identifiable through a simple {@code int} value,
9+
* and can be mapped via {@link org.hibernate.internal.util.collections.InstanceIdentityMap}.
10+
*/
11+
public interface InstanceIdentity {
12+
/**
13+
* Retrieve the unique identifier of this instance
14+
*
15+
* @return the unique instance identifier
16+
*/
17+
int $$_hibernate_getInstanceId();
18+
19+
/**
20+
* Set the value of the unique identifier for this instance
21+
*
22+
* @param instanceId the unique identifier value to set
23+
*/
24+
void $$_hibernate_setInstanceId(int instanceId);
25+
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.internal.util.collections;
6+
7+
import org.checkerframework.checker.nullness.qual.NonNull;
8+
import org.checkerframework.checker.nullness.qual.Nullable;
9+
import org.hibernate.engine.spi.InstanceIdentity;
10+
11+
import java.util.Collection;
12+
import java.util.Map;
13+
import java.util.Objects;
14+
import java.util.Set;
15+
import java.util.function.BiConsumer;
16+
import java.util.stream.Collectors;
17+
18+
/**
19+
* Utility collection backed by {@link PagedArray} that takes advantage of {@link InstanceIdentity}'s
20+
* unique identifier to store objects and provide {@link Map}-like functionalities.
21+
* <p>
22+
* Methods accessing / modifying the map with {@link Object} typed parameters will need
23+
* to type check against the instance identity interface which might be inefficient,
24+
* so it's recommended to use the position (int) based variant of those methods.
25+
* <p>
26+
* Iterating through the whole map is most efficient with {@link #forEach}, and since
27+
* we simply iterate the underlying array, it's also concurrent and reentrant safe.
28+
*/
29+
public class InstanceIdentityMap<K extends InstanceIdentity, V> implements Map<K, V> {
30+
private static final class Entry<K, V> implements Map.Entry<K, V> {
31+
private final K key;
32+
private final V value;
33+
34+
public Entry(K key, V value) {
35+
this.key = key;
36+
this.value = value;
37+
}
38+
39+
@Override
40+
public K getKey() {
41+
return key;
42+
}
43+
44+
@Override
45+
public V getValue() {
46+
return value;
47+
}
48+
49+
@Override
50+
public V setValue(V value) {
51+
throw new UnsupportedOperationException();
52+
}
53+
54+
@Override
55+
public boolean equals(Object o) {
56+
if ( o == this ) {
57+
return true;
58+
}
59+
60+
return o instanceof Map.Entry<?, ?> e
61+
&& Objects.equals( key, e.getKey() )
62+
&& Objects.equals( value, e.getValue() );
63+
}
64+
65+
@Override
66+
public int hashCode() {
67+
int result = key.hashCode();
68+
result = 31 * result + Objects.hashCode( value );
69+
return result;
70+
}
71+
}
72+
73+
private final PagedArray<Entry<K, V>> backingArray;
74+
75+
public InstanceIdentityMap() {
76+
backingArray = new PagedArray<>();
77+
}
78+
79+
@Override
80+
public int size() {
81+
return backingArray.size();
82+
}
83+
84+
@Override
85+
public boolean isEmpty() {
86+
return backingArray.isEmpty();
87+
}
88+
89+
90+
/**
91+
* Returns {@code true} if this map contains a mapping for the specified instance id.
92+
*
93+
* @param key key instance to double-check instance equality
94+
* @return {@code true} if this map contains a mapping for the specified instance id
95+
* @implNote This method accesses the backing array with the provided instance id, but performs an instance
96+
* equality check ({@code ==}) with the provided key to ensure it corresponds to the mapped one
97+
*/
98+
public boolean containsKey(K key) {
99+
return get( key ) != null;
100+
}
101+
102+
/**
103+
* @inheritDoc
104+
* @implNote This only works for {@link InstanceIdentity} keys, and it's inefficient
105+
* since we need to do a type check. Prefer using {@link #containsKey(K)}.
106+
*/
107+
@Override
108+
public boolean containsKey(Object key) {
109+
//noinspection unchecked
110+
return containsKey( (K) key );
111+
}
112+
113+
@Override
114+
public boolean containsValue(Object value) {
115+
for ( V v : values() ) {
116+
if ( Objects.equals( value, v ) ) {
117+
return true;
118+
}
119+
}
120+
return false;
121+
}
122+
123+
/**
124+
* Returns the value to which the specified instance id is mapped, or {@code null} if this map
125+
* contains no mapping for the instance id.
126+
*
127+
* @param key key instance to double-check instance equality
128+
* @return the value to which the specified instance id is mapped,
129+
* or {@code null} if this map contains no mapping for the instance id
130+
* @implNote This method accesses the backing array with the provided instance id, but performs an instance
131+
* equality check ({@code ==}) with the provided key to ensure it corresponds to the mapped one
132+
*/
133+
public @Nullable V get(K key) {
134+
final int instanceId = key.$$_hibernate_getInstanceId();
135+
if ( instanceId < 0 ) {
136+
return null;
137+
}
138+
139+
final Entry<K, V> entry = backingArray.get( instanceId );
140+
return entry != null && entry.getKey() == key ? entry.getValue() : null;
141+
}
142+
143+
/**
144+
* @inheritDoc
145+
* @implNote This only works for {@link InstanceIdentity} keys, and it's inefficient
146+
* since we need to do a type check. Prefer using {@link #get(K)}.
147+
*/
148+
@Override
149+
public @Nullable V get(Object key) {
150+
//noinspection unchecked
151+
return get( (K) key );
152+
}
153+
154+
@Override
155+
public @Nullable V put(K key, V value) {
156+
final int instanceId = key.$$_hibernate_getInstanceId();
157+
final Entry<K, V> old = backingArray.set( instanceId, new Entry<>( key, value ) );
158+
return old != null ? old.getValue() : null;
159+
}
160+
161+
/**
162+
* Removes the mapping for an instance id from this map if it is present (optional operation).
163+
*
164+
* @param key key instance to double-check instance equality
165+
* @return the previous value associated with {@code instanceId}, or {@code null} if there was no mapping for it.
166+
* @implNote This method accesses the backing array with the provided instance id, but performs an instance
167+
* equality check ({@code ==}) with the provided key to ensure it corresponds to the mapped one
168+
*/
169+
public @Nullable V remove(K key) {
170+
final int instanceId = key.$$_hibernate_getInstanceId();
171+
final Entry<K, V> old = backingArray.remove( instanceId );
172+
if ( old != null ) {
173+
// Check that the provided instance really matches with the key contained in the map
174+
if ( old.getKey() != key ) {
175+
// If it doesn't, reset the array value to the old entry
176+
backingArray.set( instanceId, old );
177+
}
178+
return old.getValue();
179+
}
180+
return null;
181+
}
182+
183+
/**
184+
* @inheritDoc
185+
* @implNote This only works for {@link InstanceIdentity} keys, and it's inefficient
186+
* since we need to do a type check. Prefer using {@link #remove(K)}.
187+
*/
188+
@Override
189+
public @Nullable V remove(Object key) {
190+
//noinspection unchecked
191+
return remove( (K) key );
192+
}
193+
194+
@Override
195+
public void putAll(Map<? extends K, ? extends V> m) {
196+
for ( Map.Entry<? extends K, ? extends V> entry : m.entrySet() ) {
197+
put( entry.getKey(), entry.getValue() );
198+
}
199+
}
200+
201+
@Override
202+
public @Nullable V putIfAbsent(K key, V value) {
203+
V v = get( key );
204+
if ( v == null ) {
205+
v = put( key, value );
206+
}
207+
return v;
208+
}
209+
210+
@Override
211+
public void clear() {
212+
backingArray.clear();
213+
}
214+
215+
/**
216+
* Returns a read-only Set view of the keys contained in this map.
217+
*/
218+
@Override
219+
public @NonNull Set<K> keySet() {
220+
return backingArray.stream().map( Entry::getKey ).collect( Collectors.toUnmodifiableSet() );
221+
}
222+
223+
/**
224+
* Returns a read-only Collection view of the values contained in this map.
225+
*/
226+
@Override
227+
public @NonNull Collection<V> values() {
228+
return backingArray.stream().map( Entry::getValue ).collect( Collectors.toUnmodifiableSet() );
229+
}
230+
231+
/**
232+
* Returns a read-only Set view of the mappings contained in this map.
233+
*/
234+
@Override
235+
public @NonNull Set<Map.Entry<K, V>> entrySet() {
236+
return backingArray.stream().collect( Collectors.toUnmodifiableSet() );
237+
}
238+
239+
@Override
240+
public void forEach(BiConsumer<? super K, ? super V> action) {
241+
backingArray.forEach( element -> action.accept( element.getKey(), element.getValue() ) );
242+
}
243+
244+
public Map.Entry<K, V>[] toArray() {
245+
//noinspection unchecked
246+
return backingArray.stream().toArray( Map.Entry[]::new );
247+
}
248+
}

0 commit comments

Comments
 (0)