11package ai .timefold .solver .core .impl .heuristic .selector .common ;
22
3- import java .util .ArrayList ;
3+ import java .util .AbstractList ;
4+ import java .util .BitSet ;
45import java .util .Collections ;
5- import java .util .IdentityHashMap ;
6- import java .util .LinkedHashMap ;
76import java .util .List ;
87import java .util .Map ;
98import java .util .Objects ;
9+ import java .util .function .Function ;
1010
11- import ai .timefold .solver .core .config .util .ConfigUtils ;
1211import ai .timefold .solver .core .impl .domain .valuerange .descriptor .FromEntityPropertyValueRangeDescriptor ;
1312
1413import org .jspecify .annotations .NullMarked ;
2322@ NullMarked
2423public final class ReachableValues {
2524
26- private final Map <Object , ReachableItemValue > values ;
25+ private final Map <Object , Integer > entitiesIndex ;
26+ private final List <Object > allEntities ;
27+ private final Map <Object , Integer > valuesIndex ;
28+ private final List <ReachableItemValue > allValues ;
2729 private final @ Nullable Class <?> valueClass ;
2830 private final boolean acceptsNullValue ;
2931 private @ Nullable ReachableItemValue firstCachedObject ;
3032 private @ Nullable ReachableItemValue secondCachedObject ;
3133
32- public ReachableValues (Map <Object , ReachableItemValue > values , Class <?> valueClass , boolean acceptsNullValue ) {
33- this .values = values ;
34+ public ReachableValues (Map <Object , Integer > entityIndexMap , List <Object > entityList , Map <Object , Integer > valueIndexMap ,
35+ List <ReachableItemValue > reachableValueList , @ Nullable Class <?> valueClass , boolean acceptsNullValue ) {
36+ this .entitiesIndex = entityIndexMap ;
37+ this .allEntities = entityList ;
38+ this .valuesIndex = valueIndexMap ;
39+ this .allValues = reachableValueList ;
3440 this .valueClass = valueClass ;
3541 this .acceptsNullValue = acceptsNullValue ;
3642 }
@@ -47,7 +53,11 @@ public ReachableValues(Map<Object, ReachableItemValue> values, Class<?> valueCla
4753 firstCachedObject = selected ;
4854 }
4955 if (selected == null ) {
50- selected = values .get (value );
56+ var index = valuesIndex .get (value );
57+ if (index == null ) {
58+ return null ;
59+ }
60+ selected = allValues .get (index );
5161 secondCachedObject = firstCachedObject ;
5262 firstCachedObject = selected ;
5363 }
@@ -59,19 +69,19 @@ public List<Object> extractEntitiesAsList(Object value) {
5969 if (itemValue == null ) {
6070 return Collections .emptyList ();
6171 }
62- return itemValue .randomAccessEntityList ;
72+ return itemValue .getRandomAccessEntityList ( allEntities ) ;
6373 }
6474
6575 public List <Object > extractValuesAsList (Object value ) {
6676 var itemValue = fetchItemValue (value );
6777 if (itemValue == null ) {
6878 return Collections .emptyList ();
6979 }
70- return itemValue .randomAccessValueList ;
80+ return itemValue .getRandomAccessValueList ( allValues ) ;
7181 }
7282
7383 public int getSize () {
74- return values .size ();
84+ return allValues .size ();
7585 }
7686
7787 public boolean isEntityReachable (@ Nullable Object origin , @ Nullable Object entity ) {
@@ -85,7 +95,11 @@ public boolean isEntityReachable(@Nullable Object origin, @Nullable Object entit
8595 if (originItemValue == null ) {
8696 return false ;
8797 }
88- return originItemValue .entityMap .containsKey (entity );
98+ var entityIndex = entitiesIndex .get (entity );
99+ if (entityIndex == null ) {
100+ throw new IllegalStateException ("The entity %s is not indexed." .formatted (entity ));
101+ }
102+ return originItemValue .containsEntity (entityIndex );
89103 }
90104
91105 public boolean isValueReachable (Object origin , @ Nullable Object otherValue ) {
@@ -96,7 +110,11 @@ public boolean isValueReachable(Object origin, @Nullable Object otherValue) {
96110 if (otherValue == null ) {
97111 return acceptsNullValue ;
98112 }
99- return originItemValue .valueMap .containsKey (Objects .requireNonNull (otherValue ));
113+ var otherValueIndex = valuesIndex .get (Objects .requireNonNull (otherValue ));
114+ if (otherValueIndex == null ) {
115+ return false ;
116+ }
117+ return originItemValue .containsValue (otherValueIndex );
100118 }
101119
102120 public boolean acceptsNullValue () {
@@ -110,30 +128,88 @@ public boolean matchesValueClass(Object value) {
110128 @ NullMarked
111129 public static final class ReachableItemValue {
112130 private final Object value ;
113- private final Map <Object , Object > entityMap ;
114- private final Map <Object , Object > valueMap ;
115- private final List <Object > randomAccessEntityList ;
116- private final List <Object > randomAccessValueList ;
131+ private final BitSet entityBitSet ;
132+ private final BitSet valueBitSet ;
133+ // The entity and value list are calculated only when needed.
134+ // The goal is to avoid loading unused data upfront, as it may affect scalability.
135+ private @ Nullable List <Object > onDemandRandomAccessEntityList ;
136+ private @ Nullable List <Object > onDemandRandomAccessValueList ;
117137
118138 public ReachableItemValue (Object value , int entityListSize , int valueListSize ) {
119139 this .value = value ;
120- this .entityMap = new IdentityHashMap <>(entityListSize );
121- this .randomAccessEntityList = new ArrayList <>(entityListSize );
122- this .valueMap = ConfigUtils .isGenericTypeImmutable (value .getClass ()) ? new LinkedHashMap <>(valueListSize )
123- : new IdentityHashMap <>(valueListSize );
124- this .randomAccessValueList = new ArrayList <>(valueListSize );
140+ this .entityBitSet = new BitSet (entityListSize );
141+ this .valueBitSet = new BitSet (valueListSize );
142+ }
143+
144+ public void addEntity (int entityIndex ) {
145+ entityBitSet .set (entityIndex );
146+ }
147+
148+ public void addValue (int valueIndex ) {
149+ valueBitSet .set (valueIndex );
150+ }
151+
152+ boolean containsEntity (int entityIndex ) {
153+ return entityBitSet .get (entityIndex );
125154 }
126155
127- public void addEntity (Object entity ) {
128- if (entityMap .put (entity , entity ) == null ) {
129- randomAccessEntityList .add (entity );
156+ boolean containsValue (int valueIndex ) {
157+ return valueBitSet .get (valueIndex );
158+ }
159+
160+ private static int [] extractAllIndexes (BitSet bitSet ) {
161+ var indexes = new int [bitSet .cardinality ()];
162+ var idx = 0 ;
163+ for (int i = bitSet .nextSetBit (0 ); i >= 0 ; i = bitSet .nextSetBit (i + 1 )) {
164+ indexes [idx ++] = i ;
130165 }
166+ return indexes ;
131167 }
132168
133- public void addValue ( Object value ) {
134- if (valueMap . put ( value , value ) == null ) {
135- randomAccessValueList . add ( value );
169+ List < Object > getRandomAccessEntityList ( List < Object > allEntities ) {
170+ if (onDemandRandomAccessEntityList == null ) {
171+ onDemandRandomAccessEntityList = new ArrayIndexedList <>( extractAllIndexes ( entityBitSet ), allEntities , null );
136172 }
173+ return onDemandRandomAccessEntityList ;
174+ }
175+
176+ List <Object > getRandomAccessValueList (List <ReachableItemValue > allValues ) {
177+ if (onDemandRandomAccessValueList == null ) {
178+ onDemandRandomAccessValueList = new ArrayIndexedList <>(extractAllIndexes (valueBitSet ), allValues , v -> v .value );
179+ }
180+ return onDemandRandomAccessValueList ;
181+ }
182+ }
183+
184+ @ NullMarked
185+ private static final class ArrayIndexedList <T , V > extends AbstractList <V > {
186+
187+ private final int [] valueIndex ;
188+ private final List <T > allValues ;
189+ private final @ Nullable Function <T , V > valueExtractor ;
190+
191+ private ArrayIndexedList (int [] valueIndex , List <T > allValues , @ Nullable Function <T , V > valueExtractor ) {
192+ this .valueIndex = valueIndex ;
193+ this .allValues = allValues ;
194+ this .valueExtractor = valueExtractor ;
195+ }
196+
197+ @ Override
198+ public V get (int index ) {
199+ if (index < 0 || index >= valueIndex .length ) {
200+ throw new ArrayIndexOutOfBoundsException (index );
201+ }
202+ var value = allValues .get (valueIndex [index ]);
203+ if (valueExtractor == null ) {
204+ return (V ) value ;
205+ } else {
206+ return valueExtractor .apply (value );
207+ }
208+ }
209+
210+ @ Override
211+ public int size () {
212+ return valueIndex .length ;
137213 }
138214 }
139215
0 commit comments