22
33import java .util .List ;
44import java .util .Map ;
5- import java .util .Objects ;
65
76import ai .timefold .solver .core .impl .domain .variable .descriptor .ListVariableDescriptor ;
87import ai .timefold .solver .core .impl .domain .variable .index .IndexShadowVariableDescriptor ;
@@ -26,7 +25,7 @@ final class ListVariableState<Solution_> {
2625 private boolean requiresLocationMap = true ;
2726 private InnerScoreDirector <Solution_ , ?> scoreDirector ;
2827 private int unassignedCount = 0 ;
29- private Map <Object , LocationInList > elementLocationMap ;
28+ private Map <Object , MutableLocationInList > elementLocationMap ;
3029
3130 public ListVariableState (ListVariableDescriptor <Solution_ > sourceVariableDescriptor ) {
3231 this .sourceVariableDescriptor = sourceVariableDescriptor ;
@@ -69,8 +68,7 @@ public void initialize(InnerScoreDirector<Solution_, ?> scoreDirector, int initi
6968
7069 public void addElement (Object entity , List <Object > elements , Object element , int index ) {
7170 if (requiresLocationMap ) {
72- var location = ElementLocation .of (entity , index );
73- var oldLocation = elementLocationMap .put (element , location );
71+ var oldLocation = elementLocationMap .put (element , new MutableLocationInList (entity , index ));
7472 if (oldLocation != null ) {
7573 throw new IllegalStateException (
7674 "The supply for list variable (%s) is corrupted, because the element (%s) at index (%d) already exists (%s)."
@@ -100,7 +98,7 @@ public void removeElement(Object entity, Object element, int index) {
10098 "The supply for list variable (%s) is corrupted, because the element (%s) at index (%d) was already unassigned (%s)."
10199 .formatted (sourceVariableDescriptor , element , index , oldElementLocation ));
102100 }
103- var oldIndex = oldElementLocation .index ();
101+ var oldIndex = oldElementLocation .getIndex ();
104102 if (oldIndex != index ) {
105103 throw new IllegalStateException (
106104 "The supply for list variable (%s) is corrupted, because the element (%s) at index (%d) had an old index (%d) which is not the current index (%d)."
@@ -168,13 +166,22 @@ public boolean changeElement(Object entity, List<Object> elements, int index) {
168166
169167 private ChangeType processElementLocation (Object entity , Object element , int index ) {
170168 if (requiresLocationMap ) { // Update the location and figure out if it is different from previous.
171- var newLocation = ElementLocation .of (entity , index );
172- var oldLocation = elementLocationMap .put (element , newLocation );
169+ var oldLocation = elementLocationMap .get (element );
173170 if (oldLocation == null ) {
171+ elementLocationMap .put (element , new MutableLocationInList (entity , index ));
174172 unassignedCount --;
175173 return ChangeType .BOTH ;
176174 }
177- return compareLocations (entity , oldLocation .entity (), index , oldLocation .index ());
175+ var changeType = compareLocations (entity , oldLocation .getEntity (), index , oldLocation .getIndex ());
176+ if (changeType .anythingChanged ) { // Replace the map value in-place, to avoid a put() on the hot path.
177+ if (changeType .entityChanged ) {
178+ oldLocation .setEntity (entity );
179+ }
180+ if (changeType .indexChanged ) {
181+ oldLocation .setIndex (index );
182+ }
183+ }
184+ return changeType ;
178185 } else { // Read the location and figure out if it is different from previous.
179186 var oldEntity = getInverseSingleton (element );
180187 if (oldEntity == null ) {
@@ -199,27 +206,13 @@ private static ChangeType compareLocations(Object entity, Object otherEntity, in
199206 }
200207 }
201208
202- private enum ChangeType {
203-
204- BOTH (true , true ),
205- INDEX (false , true ),
206- NEITHER (false , false );
207-
208- final boolean anythingChanged ;
209- final boolean entityChanged ;
210- final boolean indexChanged ;
211-
212- ChangeType (boolean entityChanged , boolean indexChanged ) {
213- this .anythingChanged = entityChanged || indexChanged ;
214- this .entityChanged = entityChanged ;
215- this .indexChanged = indexChanged ;
216- }
217-
218- }
219-
220209 public ElementLocation getLocationInList (Object planningValue ) {
221210 if (requiresLocationMap ) {
222- return Objects .requireNonNullElse (elementLocationMap .get (planningValue ), ElementLocation .unassigned ());
211+ var mutableLocationInList = elementLocationMap .get (planningValue );
212+ if (mutableLocationInList == null ) {
213+ return ElementLocation .unassigned ();
214+ }
215+ return mutableLocationInList .getLocationInList ();
223216 } else { // At this point, both inverse and index are externalized.
224217 var inverse = externalizedInverseProcessor .getInverseSingleton (planningValue );
225218 if (inverse == null ) {
@@ -235,7 +228,7 @@ public Integer getIndex(Object planningValue) {
235228 if (elementLocation == null ) {
236229 return null ;
237230 }
238- return elementLocation .index ();
231+ return elementLocation .getIndex ();
239232 }
240233 return externalizedIndexProcessor .getIndex (planningValue );
241234 }
@@ -246,34 +239,35 @@ public Object getInverseSingleton(Object planningValue) {
246239 if (elementLocation == null ) {
247240 return null ;
248241 }
249- return elementLocation .entity ();
242+ return elementLocation .getEntity ();
250243 }
251244 return externalizedInverseProcessor .getInverseSingleton (planningValue );
252245 }
253246
254247 public Object getPreviousElement (Object element ) {
255248 if (externalizedPreviousElementProcessor == null ) {
256- var elementLocation = getLocationInList (element );
257- if (!( elementLocation instanceof LocationInList locationInList ) ) {
249+ var mutableLocationInList = elementLocationMap . get (element );
250+ if (mutableLocationInList == null ) {
258251 return null ;
259252 }
260- var index = locationInList . index ();
253+ var index = mutableLocationInList . getIndex ();
261254 if (index == 0 ) {
262255 return null ;
263256 }
264- return sourceVariableDescriptor .getValue (locationInList .entity ()).get (index - 1 );
257+ return sourceVariableDescriptor .getValue (mutableLocationInList .getEntity ())
258+ .get (index - 1 );
265259 }
266260 return externalizedPreviousElementProcessor .getElement (element );
267261 }
268262
269263 public Object getNextElement (Object element ) {
270264 if (externalizedNextElementProcessor == null ) {
271- var elementLocation = getLocationInList (element );
272- if (!( elementLocation instanceof LocationInList locationInList ) ) {
265+ var mutableLocationInList = elementLocationMap . get (element );
266+ if (mutableLocationInList == null ) {
273267 return null ;
274268 }
275- var list = sourceVariableDescriptor .getValue (locationInList . entity ());
276- var index = locationInList . index ();
269+ var list = sourceVariableDescriptor .getValue (mutableLocationInList . getEntity ());
270+ var index = mutableLocationInList . getIndex ();
277271 if (index == list .size () - 1 ) {
278272 return null ;
279273 }
@@ -286,4 +280,66 @@ public int getUnassignedCount() {
286280 return unassignedCount ;
287281 }
288282
283+ private enum ChangeType {
284+
285+ BOTH (true , true ),
286+ INDEX (false , true ),
287+ NEITHER (false , false );
288+
289+ final boolean anythingChanged ;
290+ final boolean entityChanged ;
291+ final boolean indexChanged ;
292+
293+ ChangeType (boolean entityChanged , boolean indexChanged ) {
294+ this .anythingChanged = entityChanged || indexChanged ;
295+ this .entityChanged = entityChanged ;
296+ this .indexChanged = indexChanged ;
297+ }
298+
299+ }
300+
301+ /**
302+ * This class is used to avoid creating a new {@link LocationInList} object every time we need to return a location.
303+ * The actual value is held in a map and can be updated without doing a put() operation, which is more efficient.
304+ * The {@link LocationInList} object is only created when it is actually requested,
305+ * and stored until the next time the mutable state is updated and therefore the cache invalidated.
306+ */
307+ private static final class MutableLocationInList {
308+
309+ private Object entity ;
310+ private int index ;
311+ private LocationInList locationInList ;
312+
313+ public MutableLocationInList (Object entity , int index ) {
314+ this .entity = entity ;
315+ this .index = index ;
316+ }
317+
318+ public Object getEntity () {
319+ return entity ;
320+ }
321+
322+ public void setEntity (Object entity ) {
323+ this .entity = entity ;
324+ this .locationInList = null ;
325+ }
326+
327+ public int getIndex () {
328+ return index ;
329+ }
330+
331+ public void setIndex (int index ) {
332+ this .index = index ;
333+ this .locationInList = null ;
334+ }
335+
336+ public LocationInList getLocationInList () {
337+ if (locationInList == null ) {
338+ locationInList = ElementLocation .of (entity , index );
339+ }
340+ return locationInList ;
341+ }
342+
343+ }
344+
289345}
0 commit comments