11package ai .timefold .solver .core .impl .util ;
22
3+ import java .lang .reflect .Array ;
34import java .util .AbstractList ;
4- import java .util .ArrayList ;
5+ import java .util .Arrays ;
56import java .util .ConcurrentModificationException ;
67import java .util .Iterator ;
78import java .util .List ;
2425 * therefore, the insertion position of later elements isn't changed.
2526 * Gaps are removed (the list is fully compacted) when {@link #forEach(Consumer)} or {@link #add(int, Object)} is called.
2627 * {@link #get(int)} and related index-based operations compact only the prefix up to the requested index.
27- * This keeps the overhead low while giving us most benefits of {@link ArrayList} .
28+ * This keeps the overhead low while giving us most benefits of an array-backed list .
2829 * <p>
2930 * Primary fast-path methods are {@link #addEntry(Object)} and {@link Entry#remove()}, both run in O(1).
3031 * All standard {@link List} methods are also available and may run in O(n) or worse.
3435 * @param <T>
3536 */
3637@ NullMarked
37- public final class ElementAwareArrayList <T extends @ Nullable Object > extends AbstractList <T > {
38+ public final class ElementAwareArrayList <T extends @ Nullable Object >
39+ extends AbstractList <T > {
3840
3941 private static final int REMOVED_POSITION = -1 ;
4042
41- private final List <@ Nullable Entry > entryList = new ArrayList <>();
43+ private static final int DEFAULT_CAPACITY = 16 ;
44+ private @ Nullable Entry @ Nullable [] entries ;
4245 private int lastElementPosition = -1 ;
4346 private int gapCount = 0 ; // Always equals the total number of null slots in entryList.
4447
@@ -49,23 +52,41 @@ public final class ElementAwareArrayList<T extends @Nullable Object> extends Abs
4952 */
5053 public Entry addEntry (T element ) {
5154 modCount ++;
52- if (gapCount > 0 && entryList . get ( lastElementPosition ) == null ) { // Reuse a gap if it exists.
55+ if (gapCount > 0 && entries [ lastElementPosition ] == null ) { // Reuse a gap if it exists.
5356 var newEntry = new Entry (element , lastElementPosition );
54- entryList . set ( lastElementPosition , newEntry ) ;
57+ entries [ lastElementPosition ] = newEntry ;
5558 gapCount --;
5659 return newEntry ;
5760 }
5861 var newEntry = new Entry (element , ++lastElementPosition );
59- entryList .add (newEntry );
62+ resize (lastElementPosition + 1 );
63+ entries [lastElementPosition ] = newEntry ;
6064 return newEntry ;
6165 }
6266
67+ @ SuppressWarnings ("unchecked" )
68+ private void resize (int minCapacity ) {
69+ if (entries == null ) {
70+ entries = (Entry []) Array .newInstance (Entry .class , Math .max (DEFAULT_CAPACITY , minCapacity ));
71+ return ;
72+ }
73+ if (minCapacity <= entries .length ) {
74+ return ;
75+ }
76+ entries = Arrays .copyOf (entries , Math .max (entries .length * 2 , minCapacity ));
77+ }
78+
79+ @ Override
80+ public T get (int index ) {
81+ return getEntry (index ).element ();
82+ }
83+
6384 private Entry getEntry (int index ) {
6485 if (index < 0 || index >= size ()) {
6586 throw new IndexOutOfBoundsException (
6687 "The index (%d) must be >= 0 and < size (%d)." .formatted (index , size ()));
6788 } else if (gapCount == 0 ) {
68- return Objects .requireNonNull (entryList . get ( index ) );
89+ return Objects .requireNonNull (entries [ index ] );
6990 }
7091 return partialCompact (index );
7192 }
@@ -77,27 +98,24 @@ private Entry partialCompact(int rightBoundaryPosition) {
7798 var encounteredGaps = 0 ;
7899 var lastNonNullPosition = -1 ;
79100 for (var currentPosition = 0 ; currentPosition <= lastElementPosition ; currentPosition ++) {
80- var entry = entryList . get ( currentPosition ) ;
101+ var entry = entries [ currentPosition ] ;
81102 if (entry == null ) {
82103 encounteredGaps ++;
83104 } else {
84105 lastNonNullPosition ++;
85106 if (encounteredGaps > 0 ) {
86107 var targetPosition = currentPosition - encounteredGaps ;
87108 entry .moveTo (targetPosition );
88- entryList . set ( targetPosition , entry ) ;
89- entryList . set ( currentPosition , null ) ; // For consistency; the list is never in an invalid state.
109+ entries [ targetPosition ] = entry ;
110+ entries [ currentPosition ] = null ; // For consistency; the list is never in an invalid state.
90111 modCount ++;
91112 }
92113 if (lastNonNullPosition == rightBoundaryPosition ) {
93114 // Invariant: positions [0, index] are all non-null,
94115 // so all gapCount nulls lie in [index+1, lastElementPosition].
95116 // If that suffix is entirely nulls (equivalent to index == size()-1), trim it now.
96117 if (gapCount == lastElementPosition - rightBoundaryPosition ) {
97- entryList .subList (rightBoundaryPosition + 1 , lastElementPosition + 1 ).clear ();
98- lastElementPosition = rightBoundaryPosition ;
99- gapCount = 0 ;
100- modCount ++;
118+ truncateTo (rightBoundaryPosition );
101119 }
102120 return entry ;
103121 }
@@ -107,9 +125,15 @@ private Entry partialCompact(int rightBoundaryPosition) {
107125 "The index (%d) must be >= 0 and < size (%d)." .formatted (rightBoundaryPosition , size ()));
108126 }
109127
110- @ Override
111- public T get (int index ) {
112- return getEntry (index ).element ();
128+ private void truncateTo (int newLastPosition ) {
129+ if (newLastPosition < 0 ) {
130+ clear ();
131+ return ;
132+ }
133+ Arrays .fill (entries , newLastPosition + 1 , lastElementPosition + 1 , null );
134+ lastElementPosition = newLastPosition ;
135+ gapCount = 0 ;
136+ modCount ++;
113137 }
114138
115139 @ Override
@@ -131,38 +155,49 @@ public void add(int index, T element) {
131155 return ;
132156 }
133157 if (gapCount == 0 ) {
134- modCount ++;
135- var newEntry = new Entry (element , index );
136- entryList .add (index , newEntry );
137- lastElementPosition ++;
138- for (var i = index + 1 ; i <= lastElementPosition ; i ++) {
139- entryList .get (i ).moveTo (i );
140- }
158+ addWithoutGaps (index , element );
141159 return ;
142160 }
143161 // Compact prefix [0, index-1] so physical position k == logical position k for all k < index.
144162 if (index > 0 ) {
145163 partialCompact (index - 1 ); // Increases modCount.
146164 }
147- var newEntry = new Entry (element , index );
148- if (entryList .get (index ) == null ) {
165+ if (entries [index ] == null ) {
149166 // Gap at the target position: fill it directly without shifting the array.
150- entryList . set ( index , newEntry );
167+ entries [ index ] = new Entry ( element , index );
151168 gapCount --;
152169 } else {
153170 // No gap at the target position: rotate entries rightward into the nearest gap in the suffix,
154171 // consuming that gap rather than growing the backing list.
155- var displaced = newEntry ;
156- for (var i = index ; i <= lastElementPosition ; i ++) {
157- var current = entryList .get (i );
158- displaced .moveTo (i );
159- entryList .set (i , displaced );
160- if (current == null ) {
161- gapCount --;
162- break ;
163- }
164- displaced = current ;
172+ addWithGaps (index , new Entry (element , index ));
173+ }
174+ }
175+
176+ private void addWithoutGaps (int index , T element ) {
177+ modCount ++;
178+ var newEntry = new Entry (element , index );
179+ resize (lastElementPosition + 2 );
180+ for (var i = lastElementPosition ; i >= index ; i --) {
181+ var shifted = entries [i ];
182+ entries [i + 1 ] = shifted ;
183+ shifted .moveTo (i + 1 );
184+ }
185+ entries [index ] = newEntry ;
186+ lastElementPosition ++;
187+ }
188+
189+ private void addWithGaps (int index , Entry newEntry ) {
190+ modCount ++;
191+ var displaced = newEntry ;
192+ for (var i = index ; i <= lastElementPosition ; i ++) {
193+ var current = entries [i ];
194+ displaced .moveTo (i );
195+ entries [i ] = displaced ;
196+ if (current == null ) {
197+ gapCount --;
198+ break ;
165199 }
200+ displaced = current ;
166201 }
167202 }
168203
@@ -191,9 +226,9 @@ private void remove(Entry entry) {
191226 }
192227 var positionPreRemoval = entry .position ;
193228 if (positionPreRemoval == lastElementPosition ) { // Removing the last element; just trim the list.
194- entryList . remove ( lastElementPosition --) ;
229+ entries [ lastElementPosition --] = null ;
195230 } else {
196- entryList . set ( positionPreRemoval , null ) ;
231+ entries [ positionPreRemoval ] = null ;
197232 gapCount ++;
198233 }
199234 entry .moveTo (REMOVED_POSITION ); // Mark the entry as removed.
@@ -202,11 +237,20 @@ private void remove(Entry entry) {
202237 }
203238
204239 private void clearIfPossible () {
205- if (gapCount == 0 || lastElementPosition + 1 != gapCount ) {
206- return ;
240+ if (isEmpty ()) {
241+ // All positions, if any, are gaps. Clear the list entirely.
242+ innerClear ();
207243 }
208- // All positions are gaps. Clear the list entirely.
209- entryList .clear ();
244+ }
245+
246+ @ Override
247+ public void clear () {
248+ innerClear ();
249+ modCount ++;
250+ }
251+
252+ private void innerClear () {
253+ entries = null ;
210254 gapCount = 0 ;
211255 lastElementPosition = -1 ;
212256 }
@@ -237,7 +281,7 @@ public void forEach(Consumer<? super T> action) {
237281 @ SuppressWarnings ("DataFlowIssue" )
238282 private void forEachWithoutGaps (Consumer <? super T > elementConsumer ) {
239283 for (var currentPosition = 0 ; currentPosition <= lastElementPosition ; currentPosition ++) {
240- elementConsumer .accept (entryList . get ( currentPosition ) .element ());
284+ elementConsumer .accept (entries [ currentPosition ] .element ());
241285 }
242286 }
243287
@@ -252,30 +296,27 @@ private void forEachWithoutGaps(Consumer<? super T> elementConsumer) {
252296 private void forEachCompacting (Consumer <? super T > elementConsumer ) {
253297 var liveCount = size ();
254298 if (liveCount == 0 ) {
255- clearIfPossible (); // The list may still contain gaps, so try to clear it entirely.
299+ clear ();
256300 return ;
257301 }
258302 var compactPosition = 0 ;
259303 for (var currentPosition = 0 ; currentPosition <= lastElementPosition ; currentPosition ++) {
260- var entry = entryList . get ( currentPosition ) ;
304+ var entry = entries [ currentPosition ] ;
261305 if (entry == null ) {
262306 continue ;
263307 }
264308 elementConsumer .accept (entry .element ());
265309 if (currentPosition != compactPosition ) {
266310 entry .moveTo (compactPosition );
267- entryList . set ( compactPosition , entry ) ;
268- entryList . set ( currentPosition , null ) ; // Prevent stale data.
311+ entries [ compactPosition ] = entry ;
312+ entries [ currentPosition ] = null ; // Prevent stale data.
269313 modCount ++;
270314 }
271315 if (++compactPosition == liveCount ) {
272316 break ;
273317 }
274318 }
275- entryList .subList (compactPosition , lastElementPosition + 1 ).clear ();
276- lastElementPosition = compactPosition - 1 ;
277- gapCount = 0 ;
278- modCount ++;
319+ truncateTo (compactPosition - 1 );
279320 }
280321
281322 @ Override
@@ -352,9 +393,9 @@ public T next() {
352393 if (logicalPosition >= size ()) {
353394 throw new NoSuchElementException ();
354395 }
355- var entry = entryList . get ( currentPosition ) ;
396+ var entry = entries [ currentPosition ] ;
356397 while (entry == null ) {
357- entry = entryList . get ( ++currentPosition ) ;
398+ entry = entries [ ++currentPosition ] ;
358399 }
359400 currentPosition ++;
360401 logicalPosition ++;
@@ -369,9 +410,9 @@ public T previous() {
369410 if (logicalPosition <= 0 ) {
370411 throw new NoSuchElementException ();
371412 }
372- var entry = entryList . get (-- currentPosition ) ;
413+ Entry entry = null ;
373414 while (entry == null ) {
374- entry = entryList . get ( --currentPosition ) ;
415+ entry = entries [ --currentPosition ] ;
375416 }
376417 logicalPosition --;
377418 lastEntry = entry ;
0 commit comments