diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/BiasedRandomUnionMoveIterator.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/BiasedRandomUnionMoveIterator.java index 3950a9b7462..5867e85e446 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/BiasedRandomUnionMoveIterator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/BiasedRandomUnionMoveIterator.java @@ -41,17 +41,13 @@ public BiasedRandomUnionMoveIterator(List> childMoveSele @Override public boolean hasNext() { - if (stale) { - refreshMoveIteratorMap(); - } + refreshMoveIteratorMap(); return !moveIteratorMap.isEmpty(); } @Override public Move next() { - if (stale) { - refreshMoveIteratorMap(); - } + refreshMoveIteratorMap(); double randomOffset = RandomUtils.nextDouble(workingRandom, probabilityWeightTotal); Map.Entry>> entry = moveIteratorMap.floorEntry(randomOffset); // The entry is never null because randomOffset < probabilityWeightTotal @@ -64,6 +60,9 @@ public Move next() { } private void refreshMoveIteratorMap() { + if (!stale) { + return; + } moveIteratorMap.clear(); double probabilityWeightOffset = 0.0; for (ProbabilityItem probabilityItem : probabilityItemMap.values()) { @@ -74,6 +73,7 @@ private void refreshMoveIteratorMap() { } } probabilityWeightTotal = probabilityWeightOffset; + stale = false; } private static final class ProbabilityItem { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeChainImpl.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeChainImpl.java index fa494084c86..76d3ab12d48 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeChainImpl.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeChainImpl.java @@ -267,7 +267,8 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(startSplitPointToConnectedRange, splitPointSet, startSplitPointToNextGap); + // splitPointSet excluded on purpose: see the comment on ConnectedRangeImpl.equals(). + return Objects.hash(startSplitPointToConnectedRange, startSplitPointToNextGap); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeImpl.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeImpl.java index 57e316f502a..6c509a320cd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeImpl.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeImpl.java @@ -21,6 +21,8 @@ final class ConnectedRangeImpl, Differ private int minimumOverlap; private int maximumOverlap; private boolean hasOverlap; + private int cachedHashCode; + private boolean hashCodeDirty = true; ConnectedRangeImpl(NavigableSet> splitPointSet, BiFunction differenceFunction, @@ -67,6 +69,7 @@ void addRange(Range range) { } minimumOverlap = -1; maximumOverlap = -1; + hashCodeDirty = true; count++; } @@ -85,6 +88,7 @@ void mergeConnectedRange(ConnectedRangeImpl laterCo count += laterConnectedRange.count; minimumOverlap = -1; maximumOverlap = -1; + hashCodeDirty = true; hasOverlap |= laterConnectedRange.hasOverlap; } @@ -153,22 +157,38 @@ public int getMaximumOverlap() { public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof ConnectedRangeImpl that)) - return false; - return count == that.count && - getMinimumOverlap() == that.getMinimumOverlap() - && getMaximumOverlap() == that.getMaximumOverlap() - && hasOverlap == that.hasOverlap && Objects.equals( - splitPointSet, that.splitPointSet) - && Objects.equals(startSplitPoint, - that.startSplitPoint) - && Objects.equals(endSplitPoint, that.endSplitPoint); + // splitPointSet is the tracker's full, problem-wide split-point set, + // shared by reference across every ConnectedRangeImpl built from it. + // It's compared here for correctness, but deliberately excluded from hashCode(): + // equals() gets it for free because both sides are almost always the same reference + // (Objects.equals short-circuits on ==), + // whereas hashCode() has no such shortcut + // and would have to walk and hash every split point in the whole problem, + // not just this range's. + // The checks are ordered from cheapest to most expensive. + // Min/max overlap are excluded as they are derived from the split points. + return o instanceof ConnectedRangeImpl that + && hasOverlap == that.hasOverlap + && count == that.count + && Objects.equals(startSplitPoint, that.startSplitPoint) + && Objects.equals(endSplitPoint, that.endSplitPoint) + && Objects.equals(splitPointSet, that.splitPointSet); } @Override public int hashCode() { - return Objects.hash(splitPointSet, startSplitPoint, endSplitPoint, count, - getMinimumOverlap(), getMaximumOverlap(), hasOverlap); + if (hashCodeDirty) { + // Hand-rolled instead of Objects.hash(...): + // that boxes every argument and allocates a varargs array per call, + // on what is already a hot path. + var result = Boolean.hashCode(hasOverlap); + result = 31 * result + count; + result = 31 * result + startSplitPoint.hashCode(); + result = 31 * result + endSplitPoint.hashCode(); + cachedHashCode = result; + hashCodeDirty = false; + } + return cachedHashCode; } @Override @@ -177,8 +197,6 @@ public String toString() { "start=" + startSplitPoint + ", end=" + endSplitPoint + ", count=" + count + - ", minimumOverlap=" + getMinimumOverlap() + - ", maximumOverlap=" + getMaximumOverlap() + ", hasOverlap=" + hasOverlap + '}'; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/RangeSplitPoint.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/RangeSplitPoint.java index dae7e650a70..e92f2136c2f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/RangeSplitPoint.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/RangeSplitPoint.java @@ -5,9 +5,23 @@ import java.util.Iterator; import java.util.Map; import java.util.Objects; +import java.util.function.Function; -public class RangeSplitPoint> +public final class RangeSplitPoint> implements Comparable> { + + // Reuse these instances in createCollections(). + @SuppressWarnings("rawtypes") + private static final Comparator COMPARATOR_START = comparator(Range::getStart); + @SuppressWarnings("rawtypes") + private static final Comparator COMPARATOR_END = comparator(Range::getEnd); + + private static > Comparator> + comparator(Function, Point_> function) { + return Comparator.comparing(function) + .thenComparingInt(range -> System.identityHashCode(range.getValue())); + } + final Point_ splitPoint; Map startpointRangeToCountMap; Map endpointRangeToCountMap; @@ -19,14 +33,12 @@ public RangeSplitPoint(Point_ splitPoint) { } protected void createCollections() { - startpointRangeToCountMap = new IdentityHashMap<>(); - endpointRangeToCountMap = new IdentityHashMap<>(); - rangesStartingAtSplitPointSet = new TreeMultiSet<>( - Comparator., Point_> comparing(Range::getEnd) - .thenComparingInt(range -> System.identityHashCode(range.getValue()))); - rangesEndingAtSplitPointSet = new TreeMultiSet<>( - Comparator., Point_> comparing(Range::getStart) - .thenComparingInt(range -> System.identityHashCode(range.getValue()))); + // Almost always holds a single entry (one range starting/ending at this exact point); + // avoid IdentityHashMap's default 64-slot table for what's typically a 1-entry map. + startpointRangeToCountMap = new IdentityHashMap<>(1); + endpointRangeToCountMap = new IdentityHashMap<>(1); + rangesStartingAtSplitPointSet = new TreeMultiSet<>(COMPARATOR_END); + rangesEndingAtSplitPointSet = new TreeMultiSet<>(COMPARATOR_START); } public boolean addRangeStartingAtSplitPoint(Range range) { @@ -72,9 +84,18 @@ public boolean containsRangeEnding(Range range) { } public Iterator getValuesStartingFromSplitPointIterator() { - return rangesStartingAtSplitPointSet.stream() - .map(Range::getValue) - .iterator(); + var iterator = rangesStartingAtSplitPointSet.iterator(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Range_ next() { + return iterator.next().getValue(); + } + }; } public boolean isEmpty() { @@ -83,12 +104,11 @@ public boolean isEmpty() { @Override public boolean equals(Object o) { - if (this == o) + if (this == o) { return true; - if (o == null || getClass() != o.getClass()) - return false; - RangeSplitPoint that = (RangeSplitPoint) o; - return splitPoint.equals(that.splitPoint); + } + return o instanceof RangeSplitPoint other + && splitPoint.equals(other.splitPoint); } public boolean isBefore(RangeSplitPoint other) { @@ -101,7 +121,7 @@ public boolean isAfter(RangeSplitPoint other) { @Override public int hashCode() { - return Objects.hash(splitPoint); + return Objects.hashCode(splitPoint); } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java index 8c9ebd8a549..6cfec8b4e58 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Random; import java.util.TreeSet; @@ -17,51 +16,6 @@ import org.junit.jupiter.api.Test; class ConnectedRangeTrackerTest { - private static class TestRange { - int start; - int end; - - public TestRange(int start, int end) { - this.start = start; - this.end = end; - } - - public int getStart() { - return start; - } - - public int getEnd() { - return end; - } - - public void setStart(int start) { - this.start = start; - } - - public void setEnd(int end) { - this.end = end; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - TestRange range = (TestRange) o; - return start == range.start && end == range.end; - } - - @Override - public int hashCode() { - return Objects.hash(start, end); - } - - @Override - public String toString() { - return "(" + start + ", " + end + ")"; - } - } private ConnectedRangeTracker getIntegerConnectedRangeTracker() { return new ConnectedRangeTracker<>(TestRange::getStart, TestRange::getEnd, (a, b) -> b - a); @@ -69,16 +23,15 @@ private ConnectedRangeTracker getIntegerConnectedRa @Test void testNonConsecutiveRanges() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - Range a = tree.getRange(new TestRange(0, 2)); - Range b = tree.getRange(new TestRange(3, 4)); - Range c = tree.getRange(new TestRange(5, 7)); + var tree = getIntegerConnectedRangeTracker(); + var a = tree.getRange(new TestRange(0, 2)); + var b = tree.getRange(new TestRange(3, 4)); + var c = tree.getRange(new TestRange(5, 7)); tree.add(a); tree.add(b); tree.add(c); - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + var connectedRangeList = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); assertThat(connectedRangeList).hasSize(3); assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2)); @@ -101,16 +54,15 @@ void testNonConsecutiveRanges() { @Test void testConsecutiveRanges() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - Range a = tree.getRange(new TestRange(0, 2)); - Range b = tree.getRange(new TestRange(2, 4)); - Range c = tree.getRange(new TestRange(4, 7)); + var tree = getIntegerConnectedRangeTracker(); + var a = tree.getRange(new TestRange(0, 2)); + var b = tree.getRange(new TestRange(2, 4)); + var c = tree.getRange(new TestRange(4, 7)); tree.add(a); tree.add(b); tree.add(c); - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + var connectedRangeList = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); assertThat(connectedRangeList).hasSize(1); assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2), new TestRange(2, 4), new TestRange(4, 7)); @@ -121,15 +73,14 @@ void testConsecutiveRanges() { @Test void testDuplicateRanges() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - Range a = tree.getRange(new TestRange(0, 2)); - Range b = tree.getRange(new TestRange(4, 7)); + var tree = getIntegerConnectedRangeTracker(); + var a = tree.getRange(new TestRange(0, 2)); + var b = tree.getRange(new TestRange(4, 7)); tree.add(a); tree.add(a); tree.add(b); - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + var connectedRangeList = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); assertThat(connectedRangeList).hasSize(2); assertThat(connectedRangeList.get(0)).containsExactly(a.getValue(), a.getValue()); @@ -143,11 +94,11 @@ void testDuplicateRanges() { @Test void testRangeRemoval() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - TestRange removedRange = new TestRange(2, 4); - Range a = tree.getRange(new TestRange(0, 2)); - Range b = tree.getRange(removedRange); - Range c = tree.getRange(new TestRange(4, 7)); + var tree = getIntegerConnectedRangeTracker(); + var removedRange = new TestRange(2, 4); + var a = tree.getRange(new TestRange(0, 2)); + var b = tree.getRange(removedRange); + var c = tree.getRange(new TestRange(4, 7)); tree.add(a); tree.add(b); tree.add(c); @@ -158,8 +109,7 @@ void testRangeRemoval() { tree.remove(b); - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + var connectedRangeList = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); assertThat(connectedRangeList).hasSize(2); assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2)); @@ -169,12 +119,12 @@ void testRangeRemoval() { @Test void testRangeAddUpdatingOldGap() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - TestRange beforeAll = new TestRange(1, 2); - TestRange newStart = new TestRange(3, 8); - TestRange oldStart = new TestRange(4, 5); - TestRange betweenOldAndNewStart = new TestRange(6, 7); - TestRange afterAll = new TestRange(9, 10); + var tree = getIntegerConnectedRangeTracker(); + var beforeAll = new TestRange(1, 2); + var newStart = new TestRange(3, 8); + var oldStart = new TestRange(4, 5); + var betweenOldAndNewStart = new TestRange(6, 7); + var afterAll = new TestRange(9, 10); tree.add(tree.getRange(beforeAll)); verifyGaps(tree); @@ -194,17 +144,17 @@ void testRangeAddUpdatingOldGap() { @Test void testOverlappingRange() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - Range a = tree.getRange(new TestRange(0, 2)); - TestRange removedTestRange1 = new TestRange(1, 3); - Range removedRange1 = tree.getRange(removedTestRange1); - Range c = tree.getRange(new TestRange(2, 4)); + var tree = getIntegerConnectedRangeTracker(); + var a = tree.getRange(new TestRange(0, 2)); + var removedTestRange1 = new TestRange(1, 3); + var removedRange1 = tree.getRange(removedTestRange1); + var c = tree.getRange(new TestRange(2, 4)); - Range d = tree.getRange(new TestRange(5, 6)); + var d = tree.getRange(new TestRange(5, 6)); - Range e = tree.getRange(new TestRange(7, 9)); - TestRange removedTestRange2 = new TestRange(7, 9); - Range removedRange2 = tree.getRange(removedTestRange2); + var e = tree.getRange(new TestRange(7, 9)); + var removedTestRange2 = new TestRange(7, 9); + var removedRange2 = tree.getRange(removedTestRange2); tree.add(a); tree.add(removedRange1); @@ -213,8 +163,7 @@ void testOverlappingRange() { tree.add(e); tree.add(removedRange2); - var connectedRanges = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + var connectedRanges = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); assertThat(connectedRanges).hasSize(3); assertThat(connectedRanges.get(0)).containsExactly(a.getValue(), removedTestRange1, c.getValue()); @@ -284,7 +233,7 @@ void testOverlappingRange() { assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(1); verifyGaps(tree); - Range g = tree.getRange(new TestRange(6, 7)); + var g = tree.getRange(new TestRange(6, 7)); tree.add(g); connectedRanges = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); assertThat(connectedRanges).hasSize(2); @@ -300,17 +249,47 @@ void testOverlappingRange() { assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1); } + @Test + void testHashCodeConsistentWithEquals() { + var treeA = getIntegerConnectedRangeTracker(); + treeA.add(treeA.getRange(new TestRange(0, 2))); + treeA.add(treeA.getRange(new TestRange(2, 4))); + + var treeB = getIntegerConnectedRangeTracker(); + treeB.add(treeB.getRange(new TestRange(0, 2))); + treeB.add(treeB.getRange(new TestRange(2, 4))); + + var rangeA = new IterableList<>(treeA.getConnectedRangeChain().getConnectedRanges()).get(0); + var rangeB = new IterableList<>(treeB.getConnectedRangeChain().getConnectedRanges()).get(0); + + assertThat(rangeA) + .isEqualTo(rangeB) + .hasSameHashCodeAs(rangeB); + } + + @Test + void testHashCodeReflectsMutation() { + var tree = getIntegerConnectedRangeTracker(); + tree.add(tree.getRange(new TestRange(0, 2))); + var range = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()).get(0); + var hashBefore = range.hashCode(); + + // Merges into the same ConnectedRange instance, so a stale cached hash would go unnoticed. + tree.add(tree.getRange(new TestRange(2, 4))); + var hashAfter = range.hashCode(); + + assertThat(hashAfter).isNotEqualTo(hashBefore); + } + void verifyGaps(ConnectedRangeTracker tree) { - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - var gapList = - new IterableList<>(tree.getConnectedRangeChain().getGaps()); + var connectedRangeList = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + var gapList = new IterableList<>(tree.getConnectedRangeChain().getGaps()); if (connectedRangeList.size() == 0) { return; } assertThat(gapList).hasSize(connectedRangeList.size() - 1); - for (int i = 0; i < connectedRangeList.size() - 1; i++) { + for (var i = 0; i < connectedRangeList.size() - 1; i++) { assertThat(gapList.get(i).getPreviousRangeEnd()).isEqualTo(connectedRangeList.get(i).getEnd()); assertThat(gapList.get(i).getNextRangeStart()).isEqualTo(connectedRangeList.get(i + 1).getStart()); assertThat(gapList.get(i).getLength()) @@ -326,7 +305,7 @@ private static int rangeGapCompare(RangeGap a, if (a == null || b == null) { return (a == null) ? -1 : 1; } - boolean out = Objects.equals(a.getPreviousRangeEnd(), b.getPreviousRangeEnd()) && + var out = Objects.equals(a.getPreviousRangeEnd(), b.getPreviousRangeEnd()) && Objects.equals(a.getNextRangeStart(), b.getNextRangeStart()) && Objects.equals(a.getLength(), b.getLength()); @@ -345,14 +324,12 @@ private static int rangeClusterCompare(ConnectedRange first) + || !(b instanceof ConnectedRangeImpl second)) { throw new IllegalArgumentException("Expected (" + a + ") and (" + b + ") to both be ConnectedRangeImpl"); } - var first = (ConnectedRangeImpl) a; - var second = (ConnectedRangeImpl) b; - - boolean out = first.getStartSplitPoint().compareTo(second.getStartSplitPoint()) == 0 && + var out = first.getStartSplitPoint().compareTo(second.getStartSplitPoint()) == 0 && first.getEndSplitPoint().compareTo(second.getEndSplitPoint()) == 0 && first.getMinimumOverlap() == second.getMinimumOverlap() && first.getMaximumOverlap() == second.getMaximumOverlap(); @@ -365,29 +342,26 @@ private static int rangeClusterCompare(ConnectedRange> rangeToInstanceMap = new HashMap<>(); - TreeSet> splitPoints = new TreeSet<>(); - ConnectedRangeTracker tree = - new ConnectedRangeTracker<>(TestRange::getStart, TestRange::getEnd, (a, b) -> b - a); - for (int j = 0; j < 100; j++) { + var random = new Random(1); + + for (var i = 0; i < 100; i++) { + var rangeToInstanceMap = new HashMap>(); + var splitPoints = new TreeSet>(); + var tree = new ConnectedRangeTracker<>(TestRange::getStart, TestRange::getEnd, (a, b) -> b - a); + for (var j = 0; j < 100; j++) { // Create a random range - String old = formatConnectedRangeTracker(tree); - int from = random.nextInt(5); - int to = from + random.nextInt(5); - TestRange data = new TestRange(from, to); - Range range = rangeToInstanceMap.computeIfAbsent(data, tree::getRange); - Range treeRange = - new Range<>(data, TestRange::getStart, TestRange::getEnd); + var old = formatConnectedRangeTracker(tree); + var from = random.nextInt(5); + var to = from + random.nextInt(5); + var data = new TestRange(from, to); + var range = rangeToInstanceMap.computeIfAbsent(data, tree::getRange); + var treeRange = new Range<>(data, TestRange::getStart, TestRange::getEnd); splitPoints.add(treeRange.getStartSplitPoint()); splitPoints.add(treeRange.getEndSplitPoint()); // Get the split points from the set (since those split points have collections) - RangeSplitPoint startSplitPoint = - splitPoints.floor(treeRange.getStartSplitPoint()); - RangeSplitPoint endSplitPoint = splitPoints.floor(treeRange.getEndSplitPoint()); + var startSplitPoint = splitPoints.floor(treeRange.getStartSplitPoint()); + var endSplitPoint = splitPoints.floor(treeRange.getEndSplitPoint()); // Create the collections if they do not exist if (startSplitPoint.startpointRangeToCountMap == null) { @@ -419,21 +393,18 @@ void testRandomRanges() { // Recompute all connected ranges RangeSplitPoint previous = null; - RangeSplitPoint current = splitPoints.isEmpty() ? null : splitPoints.first(); + var current = splitPoints.isEmpty() ? null : splitPoints.first(); List> rangeClusterList = new ArrayList<>(); List> gapList = new ArrayList<>(); while (current != null) { - rangeClusterList - .add(ConnectedRangeImpl.getConnectedRangeStartingAt(splitPoints, (a, b) -> a - b, current)); + rangeClusterList.add(ConnectedRangeImpl.getConnectedRangeStartingAt(splitPoints, (a, b) -> a - b, current)); if (previous != null) { - ConnectedRangeImpl before = - rangeClusterList.get(rangeClusterList.size() - 2); - ConnectedRangeImpl after = - rangeClusterList.get(rangeClusterList.size() - 1); + var before = rangeClusterList.get(rangeClusterList.size() - 2); + var after = rangeClusterList.getLast(); gapList.add(new RangeGapImpl<>(before, after, after.getStart() - before.getEnd())); } previous = current; - current = splitPoints.higher(rangeClusterList.get(rangeClusterList.size() - 1).getEndSplitPoint()); + current = splitPoints.higher(rangeClusterList.getLast().getEndSplitPoint()); } // Verify the mutable version matches the recompute version @@ -450,12 +421,11 @@ void testRandomRanges() { } } - private String formatConnectedRangeTracker(ConnectedRangeTracker rangeTree) { - List> listOfConnectedRanges = new ArrayList<>(); - for (ConnectedRange cluster : rangeTree.getConnectedRangeChain() - .getConnectedRanges()) { - List rangesInCluster = new ArrayList<>(); - for (TestRange range : cluster) { + private static String formatConnectedRangeTracker(ConnectedRangeTracker rangeTree) { + var listOfConnectedRanges = new ArrayList>(); + for (var cluster : rangeTree.getConnectedRangeChain().getConnectedRanges()) { + var rangesInCluster = new ArrayList(); + for (var range : cluster) { rangesInCluster.add(range); } listOfConnectedRanges.add(rangesInCluster); @@ -465,4 +435,50 @@ private String formatConnectedRangeTracker(ConnectedRangeTracker