diff --git a/README.md b/README.md index c10c6ea65..8885b471b 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,32 @@ -# Vavr +# vavr-champ -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) -[![GitHub Release](https://img.shields.io/github/release/vavr-io/vavr.svg?style=flat-square)](https://github.com/vavr-io/vavr/releases) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.vavr/vavr/badge.svg?style=flat-square)](http://search.maven.org/#search|gav|1|g:"io.vavr"%20AND%20a:"vavr") -[![Build Status](https://github.com/vavr-io/vavr/actions/workflows/build.yml/badge.svg)](https://github.com/vavr-io/vavr/actions/workflows/build.yml) -[![Code Coverage](https://codecov.io/gh/vavr-io/vavr/branch/master/graph/badge.svg)](https://codecov.io/gh/vavr-io/vavr) +vavr-champ is binary compatible with the vavr library, but uses CHAMP-based collections internally. -

- Vavr's custom image -

+For more details see the [JavaDoc](https://www.randelshofer.ch/vavr/javadoc/): -Vavr is an object-functional language extension to Java 8 that aims to reduce the number of lines of code and increase code quality. -It provides persistent collections, functional abstractions for error handling, concurrent programming, pattern matching, and much more. +- [HashMap](https://www.randelshofer.ch/vavr/javadoc/io/vavr/collection/HashMap.html) -Vavr fuses the power of object-oriented programming with the elegance and robustness of functional programming. -The most interesting part is a feature-rich, persistent collection library that smoothly integrates with Java's standard collections. +- [HashSet](https://www.randelshofer.ch/vavr/javadoc/io/vavr/collection/HashSet.html + ) -Because Vavr does not depend on any libraries (other than the JVM), you can easily add it as a standalone .jar to your classpath. +- [LinkedHashMap](https://www.randelshofer.ch/vavr/javadoc/io/vavr/collection/LinkedHashMap.html) -### Stargazers over time -[![Stargazers over time](https://starchart.cc/vavr-io/vavr.svg?variant=adaptive)](https://starchart.cc/vavr-io/vavr) +- [LinkedHashSet](https://www.randelshofer.ch/vavr/javadoc/io/vavr/collection/LinkedHashSet.html) +To use it instead of the original `vavr` library, you can specify `vavr-champ` as your dependency. -## Using Vavr +Maven: -See [User Guide](http://docs.vavr.io) and/or [Javadoc](http://www.javadoc.io/doc/io.vavr/vavr). +``` + + ch.randelshofer + vavr-champ + 0.10.5 + +``` -### Gradle tasks: +Gradle: -* Build: `./gradlew check` - * test reports: `./build/reports/tests/test/index.html` - * coverage reports: `./build/reports/jacoco/test/html/index.html` -* Javadoc (linting): `./gradlew javadoc` - -### Contributing - -Currently, there are two significant branches: -- `master` (represents a stream of work leading to the release of a new major version) -- `version/0.x` (continues 0.10.4 with minor updates and bugfixes) - -A small number of users have reported problems building Vavr. Read our [contribution guide](./CONTRIBUTING.md) for details. +``` +implementation group: 'ch.randelshofer', name: 'vavr-champ', version: '0.10.5' +``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4631c2529..fb9e46f45 100644 --- a/build.gradle +++ b/build.gradle @@ -159,7 +159,7 @@ publishing { artifact tasks.named('testSourcesJar') pom { name = project.name - description = "Vavr is an object-functional library for Java 8+" + description = "vavr-champ is binary compatible with vavr, but uses CHAMP-based collections internally." url = 'https://www.vavr.io' inceptionYear = '2014' licenses { @@ -170,22 +170,16 @@ publishing { } developers { developer { - name = 'Daniel Dietrich' - email = 'cafebab3@gmail.com' - organization = 'Vavr' - organizationUrl = 'https://github.com/vavr-io' - } - developer { - name = 'Grzegorz Piwowarek' - email = 'gpiwowarek@gmail.com' - organization = 'Vavr' - organizationUrl = 'https://github.com/vavr-io' + name = 'Werner Randelshofer' + email = 'werner.randelshofer@bluewin.ch' + organization = 'Werner Randelshofer' + organizationUrl = 'https://www.randelshofer.ch' } } scm { - connection = 'scm:git:https://github.com/vavr-io/vavr.git' - developerConnection = 'scm:git:https://github.com/vavr-io/vavr.git' - url = 'https://github.com/vavr-io/vavr/tree/master' + connection = 'scm:git:https://github.com/wrandelshofer/vavr.git' + developerConnection = 'scm:git:https://github.com/wrandelshofer/vavr.git' + url = 'https://github.com/wrandelshofer/vavr/tree/master' } } } @@ -205,7 +199,7 @@ nexusPublishing { repositories { sonatype { username.set(providers.systemProperty("ossrhUsername").orElse("").forUseAtConfigurationTime()) - password.set(providers.systemProperty("ossrhPassword").orElse("").forUseAtConfigurationTime()) + password.set(providers.systemProperty("ossrhPassword").orElse("").forUseAtConfigurationTime()) } } } diff --git a/deployment/pom.xml b/deployment/pom.xml new file mode 100644 index 000000000..afce06043 --- /dev/null +++ b/deployment/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + ch.randelshofer + vavr-champ + 0.10.5 + jar + + ch.randelshofer:vavr-champ + vavr-champ is binary compatible with vavr, but uses CHAMP-based collections internally. + https://github.com/wrandelshofer/vavr + + + + MIT License + https://github.com/wrandelshofer/vavr/blob/b562173e641c75037bb8b7a3f929af997e8d3a5c/LICENSE + repo + + + + + + Werner Randelshofer + werner.randelshofer@bluewin.ch + ch.randelshofer + http://www.randelshofer.ch + + + + + scm:git:git://github.com/wrandelshofer/vavr.git + scm:git:ssh://github.com/wrandelshofer/vavr.git + https://github.com/wrandelshofer/vavr/tree/master + + + + diff --git a/settings.gradle b/settings.gradle index 400fe5c39..44268a022 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'vavr' +rootProject.name = 'vavr-champ' diff --git a/src-gen/main/java/io/vavr/API.java b/src-gen/main/java/io/vavr/API.java index aca28cc00..2e907f44b 100644 --- a/src-gen/main/java/io/vavr/API.java +++ b/src-gen/main/java/io/vavr/API.java @@ -30,14 +30,32 @@ G E N E R A T O R C R A F T E D \*-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-*/ -import static io.vavr.API.Match.*; - -import io.vavr.collection.*; +import io.vavr.collection.Array; +import io.vavr.collection.CharSeq; +import io.vavr.collection.HashMap; +import io.vavr.collection.HashSet; +import io.vavr.collection.IndexedSeq; +import io.vavr.collection.Iterator; +import io.vavr.collection.LinkedHashMap; +import io.vavr.collection.LinkedHashSet; +import io.vavr.collection.List; +import io.vavr.collection.Map; +import io.vavr.collection.PriorityQueue; +import io.vavr.collection.Queue; +import io.vavr.collection.Seq; +import io.vavr.collection.Set; +import io.vavr.collection.SortedMap; +import io.vavr.collection.SortedSet; +import io.vavr.collection.Stream; +import io.vavr.collection.TreeMap; +import io.vavr.collection.TreeSet; +import io.vavr.collection.Vector; import io.vavr.concurrent.Future; import io.vavr.control.Either; import io.vavr.control.Option; import io.vavr.control.Try; import io.vavr.control.Validation; + import java.io.PrintStream; import java.util.Comparator; import java.util.Formatter; @@ -48,6 +66,26 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import static io.vavr.API.Match.Case; +import static io.vavr.API.Match.Case0; +import static io.vavr.API.Match.Case1; +import static io.vavr.API.Match.Case2; +import static io.vavr.API.Match.Case3; +import static io.vavr.API.Match.Case4; +import static io.vavr.API.Match.Case5; +import static io.vavr.API.Match.Case6; +import static io.vavr.API.Match.Case7; +import static io.vavr.API.Match.Case8; +import static io.vavr.API.Match.Pattern0; +import static io.vavr.API.Match.Pattern1; +import static io.vavr.API.Match.Pattern2; +import static io.vavr.API.Match.Pattern3; +import static io.vavr.API.Match.Pattern4; +import static io.vavr.API.Match.Pattern5; +import static io.vavr.API.Match.Pattern6; +import static io.vavr.API.Match.Pattern7; +import static io.vavr.API.Match.Pattern8; + /** * The most basic Vavr functionality is accessed through this API class. * @@ -6081,7 +6119,7 @@ public static final class Case0 implements Case { private static final long serialVersionUID = 1L; private final Pattern0 pattern; - private final Function f; + private transient final Function f; private Case0(Pattern0 pattern, Function f) { this.pattern = pattern; @@ -6104,7 +6142,7 @@ public static final class Case1 implements Case { private static final long serialVersionUID = 1L; private final Pattern1 pattern; - private final Function f; + private transient final Function f; private Case1(Pattern1 pattern, Function f) { this.pattern = pattern; @@ -6127,7 +6165,7 @@ public static final class Case2 implements Case { private static final long serialVersionUID = 1L; private final Pattern2 pattern; - private final BiFunction f; + private transient final BiFunction f; private Case2(Pattern2 pattern, BiFunction f) { this.pattern = pattern; diff --git a/src/main/java/META-INF/thirdparty-LICENSE b/src/main/java/META-INF/thirdparty-LICENSE new file mode 100644 index 000000000..eb129954a --- /dev/null +++ b/src/main/java/META-INF/thirdparty-LICENSE @@ -0,0 +1,56 @@ +---- +capsule +https://github.com/usethesource/capsule +https://github.com/usethesource/capsule/blob/3856cd65fa4735c94bcfa94ec9ecf408429b54f4/LICENSE + +BSD 2-Clause "Simplified" License + +Copyright (c) Michael Steindorfer and Contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +---- +JHotDraw 8 +https://github.com/wrandelshofer/jhotdraw8 +https://github.com/wrandelshofer/jhotdraw8/blob/c49844aebd395b6a31eb5785aa58f6b6675fac6a/LICENSE + +MIT License + +Copyright © 2023 The authors and contributors of JHotDraw. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/main/java/io/vavr/collection/BitMappedTrie.java b/src/main/java/io/vavr/collection/BitMappedTrie.java index afcd84248..c6d982551 100644 --- a/src/main/java/io/vavr/collection/BitMappedTrie.java +++ b/src/main/java/io/vavr/collection/BitMappedTrie.java @@ -28,6 +28,8 @@ import java.io.Serializable; import java.util.NoSuchElementException; +import java.util.Spliterators; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -370,6 +372,65 @@ private int map(Function mapper, Object results, int } int length() { return length; } + + static class BitMappedTrieSpliterator extends Spliterators.AbstractSpliterator { + private final int globalLength; + private int globalIndex; + + private int index; + private Object leaf; + private int length; + private final BitMappedTrie root; + private T current; + + public BitMappedTrieSpliterator(BitMappedTrie root, int fromIndex, int characteristics) { + super(Math.max(0,root.length - fromIndex), characteristics); + this.root = root; + globalLength = root.length; + globalIndex = Math.max(0,fromIndex); + index = lastDigit(root.offset + globalIndex); + leaf = root.getLeaf(globalIndex); + length = root.type.lengthOf(leaf); + } + public boolean moveNext() { + if (globalIndex >= globalLength) { + return false; + } + if (index == length) { + setCurrentArray(); + } + current = root.type.getAt(leaf, index); + index++; + globalIndex++; + return true; + } + + public T current() { + return current; + } + + public void skip(int count) { + globalIndex += count; + index = lastDigit(root.offset + globalIndex); + leaf = root.getLeaf(globalIndex); + length = root.type.lengthOf(leaf); + } + + @Override + public boolean tryAdvance(Consumer action) { + if (moveNext()){ + action.accept(current); + return true; + } + return false; + } + + private void setCurrentArray() { + index = 0; + leaf = root.getLeaf(globalIndex); + length = root.type.lengthOf(leaf); + } + } } @FunctionalInterface diff --git a/src/main/java/io/vavr/collection/ChampIteration.java b/src/main/java/io/vavr/collection/ChampIteration.java new file mode 100644 index 000000000..5d7ab18e9 --- /dev/null +++ b/src/main/java/io/vavr/collection/ChampIteration.java @@ -0,0 +1,210 @@ +/* + * ____ ______________ ________________________ __________ + * \ \/ / \ \/ / __/ / \ \/ / \ + * \______/___/\___\______/___/_____/___/\___\______/___/\___\ + * + * The MIT License (MIT) + * + * Copyright 2023 Vavr, https://vavr.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.vavr.collection; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Provides iterators and spliterators for CHAMP tries. + */ +class ChampIteration { + + /** + * Adapts a {@link Spliterator} to the {@link Iterator} interface. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ * + * @param the element type + */ + static class IteratorFacade implements Iterator, Consumer { + private final Spliterator spliterator; + + IteratorFacade(Spliterator spliterator) { + this.spliterator = spliterator; + } + + boolean hasCurrent = false; + E current; + + public void accept(E t) { + hasCurrent = true; + current = t; + } + + @Override + public boolean hasNext() { + if (!hasCurrent) { + spliterator.tryAdvance(this); + } + return hasCurrent; + } + + @Override + public E next() { + if (!hasCurrent && !hasNext()) + throw new NoSuchElementException(); + else { + hasCurrent = false; + E t = current; + current = null; + return t; + } + } + + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + if (hasCurrent) { + hasCurrent = false; + E t = current; + current = null; + action.accept(t); + } + spliterator.forEachRemaining(action); + } + } + + /** + * Data iterator over a CHAMP trie. + *

+ * XXX This iterator carefully replicates the iteration sequence of the original HAMT-based + * HashSet class. We can not use a more performant implementation, because HashSetTest + * requires that we use a specific iteration sequence. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ */ + static class ChampSpliterator extends Spliterators.AbstractSpliterator { + private final Function mappingFunction; + private final Deque> stack = new ArrayDeque<>(ChampTrie.Node.MAX_DEPTH); + private K current; + + @SuppressWarnings("unchecked") + public ChampSpliterator(ChampTrie.Node root, Function mappingFunction, int characteristics, long size) { + super(size, characteristics); + if (root.nodeArity() + root.dataArity() > 0) { + stack.push(new StackElement<>(root)); + } + this.mappingFunction = mappingFunction == null ? i -> (E) i : mappingFunction; + } + + public E current() { + return mappingFunction.apply(current); + } + + + int getNextBitpos(StackElement elem) { + return 1 << Integer.numberOfTrailingZeros(elem.map); + } + + boolean isDone(StackElement elem) { + return elem.index >= elem.size; + } + + + int moveIndex(StackElement elem) { + return elem.index++; + } + + boolean moveNext() { + while (!stack.isEmpty()) { + StackElement elem = stack.peek(); + ChampTrie.Node node = elem.node; + + if (node instanceof ChampTrie.HashCollisionNode) { + ChampTrie.HashCollisionNode hcn = (ChampTrie.HashCollisionNode) node; + current = hcn.getData(moveIndex(elem)); + if (isDone(elem)) { + stack.pop(); + } + return true; + } else if (node instanceof ChampTrie.BitmapIndexedNode) { + ChampTrie.BitmapIndexedNode bin = (ChampTrie.BitmapIndexedNode) node; + int bitpos = getNextBitpos(elem); + elem.map ^= bitpos; + moveIndex(elem); + if (isDone(elem)) { + stack.pop(); + } + if ((bin.nodeMap() & bitpos) != 0) { + stack.push(new StackElement<>(bin.getNode(bin.nodeIndex(bitpos)))); + } else { + current = bin.getData(bin.dataIndex(bitpos)); + return true; + } + } + } + return false; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (moveNext()) { + action.accept(current()); + return true; + } + return false; + } + + static class StackElement { + final ChampTrie.Node node; + final int size; + int index; + int map; + + public StackElement(ChampTrie.Node node) { + this.node = node; + this.size = node.nodeArity() + node.dataArity(); + this.index = 0; + this.map = (node instanceof ChampTrie.BitmapIndexedNode) + ? (((ChampTrie.BitmapIndexedNode) node).dataMap() | ((ChampTrie.BitmapIndexedNode) node).nodeMap()) : 0; + } + } + } +} diff --git a/src/main/java/io/vavr/collection/ChampSequenced.java b/src/main/java/io/vavr/collection/ChampSequenced.java new file mode 100644 index 000000000..2a1e0df6f --- /dev/null +++ b/src/main/java/io/vavr/collection/ChampSequenced.java @@ -0,0 +1,725 @@ +/* + * ____ ______________ ________________________ __________ + * \ \/ / \ \/ / __/ / \ \/ / \ + * \______/___/\___\______/___/_____/___/\___\______/___/\___\ + * + * The MIT License (MIT) + * + * Copyright 2023 Vavr, https://vavr.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.vavr.collection; + +import io.vavr.Tuple2; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Objects; +import java.util.Spliterators; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +import static io.vavr.collection.ChampTrie.BitmapIndexedNode.emptyNode; + +/** + * Provides data elements for sequenced CHAMP tries. + */ +class ChampSequenced { + /** + * A spliterator for a {@code VectorMap} or {@code VectorSet}. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ * + * @param the key type + */ + static class ChampVectorSpliterator extends Spliterators.AbstractSpliterator { + private final BitMappedTrie.BitMappedTrieSpliterator vector; + private final Function mapper; + private K current; + + ChampVectorSpliterator(Vector vector, Function mapper, int fromIndex, long est, int additionalCharacteristics) { + super(est, additionalCharacteristics); + this.vector = new BitMappedTrie.BitMappedTrieSpliterator<>(vector.trie, fromIndex, 0); + this.mapper = mapper; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (moveNext()) { + action.accept(current); + return true; + } + return false; + } + + K current() { + return current; + } + + boolean moveNext() { + boolean success = vector.moveNext(); + if (!success) return false; + if (vector.current() instanceof ChampTombstone) { + ChampTombstone t = (ChampTombstone) vector.current(); + vector.skip(t.after()); + vector.moveNext(); + } + current = mapper.apply(vector.current()); + return true; + } + } + + /** + * @param + */ + static class ChampReverseVectorSpliterator extends Spliterators.AbstractSpliterator { + private final Vector vector; + private final Function mapper; + private int index; + private K current; + + ChampReverseVectorSpliterator(Vector vector, Function mapper, int fromIndex, int additionalCharacteristics, long est) { + super(est, additionalCharacteristics); + this.vector = vector; + this.mapper = mapper; + index = vector.size() - 1-fromIndex; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (moveNext()) { + action.accept(current); + return true; + } + return false; + } + + boolean moveNext() { + if (index < 0) { + return false; + } + Object o = vector.get(index--); + if (o instanceof ChampTombstone) { + ChampTombstone t = (ChampTombstone) o; + index -= t.before(); + o = vector.get(index--); + } + current = mapper.apply(o); + return true; + } + + K current() { + return current; + } + } + + /** + * A {@code SequencedData} stores a sequence number plus some data. + *

+ * {@code SequencedData} objects are used to store sequenced data in a CHAMP + * trie (see {@link ChampTrie.Node}). + *

+ * The kind of data is specified in concrete implementations of this + * interface. + *

+ * All sequence numbers of {@code SequencedData} objects in the same CHAMP trie + * are unique. Sequence numbers range from {@link Integer#MIN_VALUE} (exclusive) + * to {@link Integer#MAX_VALUE} (inclusive). + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ */ + static interface ChampSequencedData { + /** + * We use {@link Integer#MIN_VALUE} to detect overflows in the sequence number. + *

+ * {@link Integer#MIN_VALUE} is the only integer number which can not + * be negated. + *

+ * Therefore, we can not use {@link Integer#MIN_VALUE} as a sequence number + * anyway. + */ + int NO_SEQUENCE_NUMBER = Integer.MIN_VALUE; + + static ChampTrie.BitmapIndexedNode buildSequencedTrie(ChampTrie.BitmapIndexedNode root, ChampTrie.IdentityObject owner) { + ChampTrie.BitmapIndexedNode seqRoot = emptyNode(); + ChampTrie.ChangeEvent details = new ChampTrie.ChangeEvent<>(); + for (ChampIteration.ChampSpliterator i = new ChampIteration.ChampSpliterator(root, null, 0, 0); i.moveNext(); ) { + K elem = i.current(); + seqRoot = seqRoot.put(owner, elem, seqHash(elem.getSequenceNumber()), + 0, details, (oldK, newK) -> oldK, ChampSequencedData::seqEquals, ChampSequencedData::seqHash); + } + return seqRoot; + } + + /** + * Returns true if the sequenced elements must be renumbered because + * {@code first} or {@code last} are at risk of overflowing. + *

+ * {@code first} and {@code last} are estimates of the first and last + * sequence numbers in the trie. The estimated extent may be larger + * than the actual extent, but not smaller. + * + * @param size the size of the trie + * @param first the estimated first sequence number + * @param last the estimated last sequence number + * @return + */ + static boolean mustRenumber(int size, int first, int last) { + return size == 0 && (first != -1 || last != 0) + || last > Integer.MAX_VALUE - 2 + || first < Integer.MIN_VALUE + 2; + } + + static Vector vecBuildSequencedTrie(ChampTrie.BitmapIndexedNode root, ChampTrie.IdentityObject owner, int size) { + ArrayList list = new ArrayList<>(size); + for (ChampIteration.ChampSpliterator i = new ChampIteration.ChampSpliterator(root, Function.identity(), 0, Long.MAX_VALUE); i.moveNext(); ) { + list.add(i.current()); + } + list.sort(Comparator.comparing(ChampSequencedData::getSequenceNumber)); + return Vector.ofAll(list); + } + + static boolean vecMustRenumber(int size, int offset, int vectorSize) { + return size == 0 + || vectorSize >>> 1 > size + || (long) vectorSize - offset > Integer.MAX_VALUE - 2 + || offset < Integer.MIN_VALUE + 2; + } + + /** + * Renumbers the sequence numbers in all nodes from {@code 0} to {@code size}. + *

+ * Afterwards the sequence number for the next inserted entry must be + * set to the value {@code size}; + * + * @param size the size of the trie + * @param root the root of the trie + * @param sequenceRoot the sequence root of the trie + * @param owner the owner that will own the renumbered trie + * @param hashFunction the hash function for data elements + * @param equalsFunction the equals function for data elements + * @param factoryFunction the factory function for data elements + * @param + * @return a new renumbered root + */ + static ChampTrie.BitmapIndexedNode renumber(int size, + ChampTrie.BitmapIndexedNode root, + ChampTrie.BitmapIndexedNode sequenceRoot, + ChampTrie.IdentityObject owner, + ToIntFunction hashFunction, + BiPredicate equalsFunction, + BiFunction factoryFunction + + ) { + if (size == 0) { + return root; + } + ChampTrie.BitmapIndexedNode newRoot = root; + ChampTrie.ChangeEvent details = new ChampTrie.ChangeEvent<>(); + int seq = 0; + + for (ChampIteration.ChampSpliterator i = new ChampIteration.ChampSpliterator<>(sequenceRoot, Function.identity(), 0, 0); i.moveNext(); ) { + K e = i.current(); + K newElement = factoryFunction.apply(e, seq); + newRoot = newRoot.put(owner, + newElement, + Objects.hashCode(e), 0, details, + (oldk, newk) -> oldk.getSequenceNumber() == newk.getSequenceNumber() ? oldk : newk, + equalsFunction, hashFunction); + seq++; + } + return newRoot; + } + + /** + * Renumbers the sequence numbers in all nodes from {@code 0} to {@code size}. + *

+ * Afterward, the sequence number for the next inserted entry must be + * set to the value {@code size}; + * + * @param + * @param size the size of the trie + * @param root the root of the trie + * @param vector the sequence root of the trie + * @param owner the owner that will own the renumbered trie + * @param hashFunction the hash function for data elements + * @param equalsFunction the equals function for data elements + * @param factoryFunction the factory function for data elements + * @return a new renumbered root and a new vector with matching entries + */ + @SuppressWarnings("unchecked") + static Tuple2, Vector> vecRenumber( + int size, + ChampTrie.BitmapIndexedNode root, + Vector vector, + ChampTrie.IdentityObject owner, + ToIntFunction hashFunction, + BiPredicate equalsFunction, + BiFunction factoryFunction) { + if (size == 0) { + new Tuple2<>(root, vector); + } + ChampTrie.BitmapIndexedNode renumberedRoot = root; + Vector renumberedVector = Vector.of(); + ChampTrie.ChangeEvent details = new ChampTrie.ChangeEvent<>(); + BiFunction forceUpdate = (oldk, newk) -> newk; + int seq = 0; + for (ChampVectorSpliterator i = new ChampVectorSpliterator(vector, o -> (K) o, 0, Long.MAX_VALUE, 0); i.moveNext(); ) { + K current = i.current(); + K data = factoryFunction.apply(current, seq++); + renumberedVector = renumberedVector.append(data); + renumberedRoot = renumberedRoot.put(owner, data, hashFunction.applyAsInt(current), 0, details, forceUpdate, equalsFunction, hashFunction); + } + + return new Tuple2<>(renumberedRoot, renumberedVector); + } + + + static boolean seqEquals(K a, K b) { + return a.getSequenceNumber() == b.getSequenceNumber(); + } + + static int seqHash(K e) { + return seqHash(e.getSequenceNumber()); + } + + /** + * Computes a hash code from the sequence number, so that we can + * use it for iteration in a CHAMP trie. + *

+ * Convert the sequence number to unsigned 32 by adding Integer.MIN_VALUE. + * Then reorders its bits from 66666555554444433333222221111100 to + * 00111112222233333444445555566666. + * + * @param sequenceNumber a sequence number + * @return a hash code + */ + static int seqHash(int sequenceNumber) { + int u = sequenceNumber + Integer.MIN_VALUE; + return (u >>> 27) + | ((u & 0b00000_11111_00000_00000_00000_00000_00) >>> 17) + | ((u & 0b00000_00000_11111_00000_00000_00000_00) >>> 7) + | ((u & 0b00000_00000_00000_11111_00000_00000_00) << 3) + | ((u & 0b00000_00000_00000_00000_11111_00000_00) << 13) + | ((u & 0b00000_00000_00000_00000_00000_11111_00) << 23) + | ((u & 0b00000_00000_00000_00000_00000_00000_11) << 30); + } + + static ChampTrie.BitmapIndexedNode seqRemove(ChampTrie.BitmapIndexedNode seqRoot, ChampTrie.IdentityObject owner, + K key, ChampTrie.ChangeEvent details) { + return seqRoot.remove(owner, + key, seqHash(key.getSequenceNumber()), 0, details, + ChampSequencedData::seqEquals); + } + + static ChampTrie.BitmapIndexedNode seqUpdate(ChampTrie.BitmapIndexedNode seqRoot, ChampTrie.IdentityObject owner, + K key, ChampTrie.ChangeEvent details, + BiFunction replaceFunction) { + return seqRoot.put(owner, + key, seqHash(key.getSequenceNumber()), 0, details, + replaceFunction, + ChampSequencedData::seqEquals, ChampSequencedData::seqHash); + } + + final static ChampTombstone TOMB_ZERO_ZERO = new ChampTombstone(0, 0); + + static Tuple2, Integer> vecRemove(Vector vector, K oldElem, int offset) { + // If the element is the first, we can remove it and its neighboring tombstones from the vector. + int size = vector.size(); + int index = oldElem.getSequenceNumber() + offset; + if (index == 0) { + if (size > 1) { + Object o = vector.get(1); + if (o instanceof ChampTombstone) { + ChampTombstone t = (ChampTombstone) o; + return new Tuple2<>(vector.removeRange(0, 2 + t.after()), offset - 2 - t.after()); + } + } + return new Tuple2<>(vector.tail(), offset - 1); + } + + // If the element is the last , we can remove it and its neighboring tombstones from the vector. + if (index == size - 1) { + Object o = vector.get(size - 2); + if (o instanceof ChampTombstone) { + ChampTombstone t = (ChampTombstone) o; + return new Tuple2<>(vector.removeRange(size - 2 - t.before(), size), offset); + } + return new Tuple2<>(vector.init(), offset); + } + + // Otherwise, we replace the element with a tombstone, and we update before/after skip counts + assert index > 0 && index < size - 1; + Object before = vector.get(index - 1); + Object after = vector.get(index + 1); + if (before instanceof ChampTombstone && after instanceof ChampTombstone) { + ChampTombstone tb = (ChampTombstone) before; + ChampTombstone ta = (ChampTombstone) after; + vector = vector.update(index - 1 - tb.before(), new ChampTombstone(0, 2 + tb.before() + ta.after())); + vector = vector.update(index, TOMB_ZERO_ZERO); + vector = vector.update(index + 1 + ta.after(), new ChampTombstone(2 + tb.before() + ta.after(), 0)); + } else if (before instanceof ChampTombstone) { + ChampTombstone tb = (ChampTombstone) before; + vector = vector.update(index - 1 - tb.before(), new ChampTombstone(0, 1 + tb.before())); + vector = vector.update(index, new ChampTombstone(1 + tb.before(), 0)); + } else if (after instanceof ChampTombstone) { + ChampTombstone ta = (ChampTombstone) after; + vector = vector.update(index, new ChampTombstone(0, 1 + ta.after())); + vector = vector.update(index + 1 + ta.after(), new ChampTombstone(1 + ta.after(), 0)); + } else { + vector = vector.update(index, TOMB_ZERO_ZERO); + } + return new Tuple2<>(vector, offset); + } + + + static Vector removeRange(Vector v, int fromIndex, int toIndex) { + ChampTrie.ChampListHelper.checkIndex(fromIndex, toIndex + 1); + ChampTrie.ChampListHelper.checkIndex(toIndex, v.size() + 1); + if (fromIndex == 0) { + return v.slice(toIndex, v.size()); + } + if (toIndex == v.size()) { + return v.slice(0, fromIndex); + } + final Vector begin = v.slice(0, fromIndex); + return begin.appendAll(() -> v.iterator(toIndex)); + } + + + static Vector vecUpdate(Vector newSeqRoot, ChampTrie.IdentityObject owner, K newElem, ChampTrie.ChangeEvent details, + BiFunction replaceFunction) { + return newSeqRoot; + } + + /** + * Gets the sequence number of the data. + * + * @return sequence number in the range from {@link Integer#MIN_VALUE} + * (exclusive) to {@link Integer#MAX_VALUE} (inclusive). + */ + int getSequenceNumber(); + + + } + + /** + * A {@code SequencedElement} stores an element of a set and a sequence number. + *

+ * {@code hashCode} and {@code equals} are based on the element - the sequence + * number is not included. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ */ + static class ChampSequencedElement implements ChampSequencedData { + + private final E element; + private final int sequenceNumber; + + ChampSequencedElement(E element) { + this.element = element; + this.sequenceNumber = NO_SEQUENCE_NUMBER; + } + + ChampSequencedElement(E element, int sequenceNumber) { + this.element = element; + this.sequenceNumber = sequenceNumber; + } + public static int keyHash( Object a) { + return Objects.hashCode(a); + } + + + static ChampSequencedElement forceUpdate(ChampSequencedElement oldK, ChampSequencedElement newK) { + return newK; + } + + static ChampSequencedElement update(ChampSequencedElement oldK, ChampSequencedElement newK) { + return oldK; + } + + + static ChampSequencedElement updateAndMoveToFirst(ChampSequencedElement oldK, ChampSequencedElement newK) { + return oldK.getSequenceNumber() == newK.getSequenceNumber() + 1 ? oldK : newK; + } + + + static ChampSequencedElement updateAndMoveToLast(ChampSequencedElement oldK, ChampSequencedElement newK) { + return oldK.getSequenceNumber() == newK.getSequenceNumber() - 1 ? oldK : newK; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChampSequencedElement that = (ChampSequencedElement) o; + return Objects.equals(element, that.element); + } + + @Override + public int hashCode() { + return Objects.hashCode(element); + } + + E getElement() { + return element; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + @Override + public String toString() { + return "{" + + "" + element + + ", seq=" + sequenceNumber + + '}'; + } + } + + /** + * A {@code ChampSequencedEntry} stores an entry of a map and a sequence number. + *

+ * {@code hashCode} and {@code equals} are based on the key and the value + * of the entry - the sequence number is not included. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ */ + static class ChampSequencedEntry extends AbstractMap.SimpleImmutableEntry + implements ChampSequencedData { + + private static final long serialVersionUID = 0L; + private final int sequenceNumber; + + ChampSequencedEntry(K key) { + super(key, null); + sequenceNumber = NO_SEQUENCE_NUMBER; + } + + ChampSequencedEntry(K key, V value) { + super(key, value); + sequenceNumber = NO_SEQUENCE_NUMBER; + } + ChampSequencedEntry(K key, V value, int sequenceNumber) { + super(key, value); + this.sequenceNumber = sequenceNumber; + } + + static ChampSequencedEntry forceUpdate(ChampSequencedEntry oldK, ChampSequencedEntry newK) { + return newK; + } + static boolean keyEquals(ChampSequencedEntry a, ChampSequencedEntry b) { + return Objects.equals(a.getKey(), b.getKey()); + } + + static boolean keyAndValueEquals(ChampSequencedEntry a, ChampSequencedEntry b) { + return Objects.equals(a.getKey(), b.getKey()) && Objects.equals(a.getValue(), b.getValue()); + } + + static int entryKeyHash(ChampSequencedEntry a) { + return Objects.hashCode(a.getKey()); + } + + static int keyHash( Object key) { + return Objects.hashCode(key); + } + static ChampSequencedEntry update(ChampSequencedEntry oldK, ChampSequencedEntry newK) { + return Objects.equals(oldK.getValue(), newK.getValue()) ? oldK : + new ChampSequencedEntry<>(oldK.getKey(), newK.getValue(), oldK.getSequenceNumber()); + } + + + static ChampSequencedEntry updateAndMoveToFirst(ChampSequencedEntry oldK, ChampSequencedEntry newK) { + return Objects.equals(oldK.getValue(), newK.getValue()) + && oldK.getSequenceNumber() == newK.getSequenceNumber() + 1 ? oldK : newK; + } + + + static ChampSequencedEntry updateAndMoveToLast(ChampSequencedEntry oldK, ChampSequencedEntry newK) { + return Objects.equals(oldK.getValue(), newK.getValue()) + && oldK.getSequenceNumber() == newK.getSequenceNumber() - 1 ? oldK : newK; + } + + // FIXME This behavior is enforced by AbstractMapTest.shouldPutExistingKeyAndNonEqualValue().
+ // This behavior replaces the existing key with the new one if it has not the same identity.
+ // This behavior does not match the behavior of java.util.HashMap.put(). + // This behavior violates the contract of the map: we do create a new instance of the map, + // although it is equal to the previous instance. + static ChampSequencedEntry updateWithNewKey(ChampSequencedEntry oldK, ChampSequencedEntry newK) { + return Objects.equals(oldK.getValue(), newK.getValue()) + && oldK.getKey() == newK.getKey() + ? oldK + : new ChampSequencedEntry<>(newK.getKey(), newK.getValue(), oldK.getSequenceNumber()); + } + public int getSequenceNumber() { + return sequenceNumber; + } + } + + /** + * A tombstone is used by {@code VectorSet} to mark a deleted slot in its Vector. + *

+ * A tombstone stores the minimal number of neighbors 'before' and 'after' it in the + * Vector. + *

+ * When we insert a new tombstone, we update 'before' and 'after' values only on + * the first and last tombstone of a sequence of tombstones. Therefore, a delete + * operation requires reading of up to 3 neighboring elements in the vector, and + * updates of up to 3 elements. + *

+ * There are no tombstones at the first and last element of the vector. When we + * remove the first or last element of the vector, we remove the tombstones. + *

+ * Example: Tombstones are shown as before.after. + *

+     *
+     *
+     *                              Indices:  0   1   2   3   4   5   6   7   8   9
+     * Initial situation:           Values:  'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j'
+     *
+     * Deletion of element 5:
+     * - read elements at indices 4, 5, 6                    'e' 'f' 'g'
+     * - notice that none of them are tombstones
+     * - put tombstone 0.0 at index 5                            0.0
+     *
+     * After deletion of element 5:          'a' 'b' 'c' 'd' 'e' 0.0 'g' 'h' 'i' 'j'
+     *
+     * After deletion of element 7:          'a' 'b' 'c' 'd' 'e' 0.0 'g' 0.0 'i' 'j'
+     *
+     * Deletion of element 8:
+     * - read elements at indices 7, 8, 9                                0.0 'i' 'j'
+     * - notice that 7 is a tombstone 0.0
+     * - put tombstones 0.1, 1.0 at indices 7 and 8
+     *
+     * After deletion of element 8:          'a' 'b' 'c' 'd' 'e' 0.0 'g' 0.1 1.0 'j'
+     *
+     * Deletion of element 6:
+     * - read elements at indices 5, 6, 7                        0.0 'g' 0.1
+     * - notice that two of them are tombstones
+     * - put tombstones 0.3, 0.0, 3.0 at indices 5, 6 and 8
+     *
+     * After deletion of element 6:          'a' 'b' 'c' 'd' 'e' 0.3 0.0 0.1 3.0 'j'
+     *
+     * Deletion of the last element 9:
+     * - read elements at index 8                                            3.0
+     * - notice that it is a tombstone
+     * - remove the last element and the neighboring tombstone sequence
+     *
+     * After deletion of element 9:          'a' 'b' 'c' 'd' 'e'
+     * 
+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ * The design of this class is inspired by 'VectorMap.scala'. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com + *
+ *
VectorMap.scala + *
The Scala library. Copyright EPFL and Lightbend, Inc. Apache License 2.0.
+ *
github.com + *
+ *
+ */ + static final class ChampTombstone { + private final int before; + private final int after; + + /** + * @param before minimal number of neighboring tombstones before this one + * @param after minimal number of neighboring tombstones after this one + */ + ChampTombstone(int before, int after) { + this.before = before; + this.after = after; + } + + public int before() { + return before; + } + + public int after() { + return after; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + ChampTombstone that = (ChampTombstone) obj; + return this.before == that.before && + this.after == that.after; + } + + @Override + public int hashCode() { + return Objects.hash(before, after); + } + + @Override + public String toString() { + return "ChampTombstone[" + + "before=" + before + ", " + + "after=" + after + ']'; + } + + + } +} diff --git a/src/main/java/io/vavr/collection/ChampTransience.java b/src/main/java/io/vavr/collection/ChampTransience.java new file mode 100644 index 000000000..6860e821d --- /dev/null +++ b/src/main/java/io/vavr/collection/ChampTransience.java @@ -0,0 +1,232 @@ +/* + * ____ ______________ ________________________ __________ + * \ \/ / \ \/ / __/ / \ \/ / \ + * \______/___/\___\______/___/_____/___/\___\______/___/\___\ + * + * The MIT License (MIT) + * + * Copyright 2023 Vavr, https://vavr.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.vavr.collection; + +import io.vavr.Tuple2; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +/** + * Provides abstract base classes for transient collections. + */ +class ChampTransience { + /** + * Abstract base class for a transient CHAMP collection. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ * + * @param the data type of the CHAMP trie + */ + abstract static class ChampAbstractTransientCollection { + /** + * The current owner id of this map. + *

+ * All nodes that have the same non-null owner id, are exclusively owned + * by this map, and therefore can be mutated without affecting other map. + *

+ * If this owner id is null, then this map does not own any nodes. + */ + + ChampTrie.IdentityObject owner; + + /** + * The root of this CHAMP trie. + */ + ChampTrie.BitmapIndexedNode root; + + /** + * The number of entries in this map. + */ + int size; + + /** + * The number of times this map has been structurally modified. + */ + int modCount; + + int size() { + return size; + } + + boolean isEmpty() { + return size == 0; + } + + ChampTrie.IdentityObject makeOwner() { + if (owner == null) { + owner = new ChampTrie.IdentityObject(); + } + return owner; + } + } + + /** + * Abstract base class for a transient CHAMP map. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ * + * @param the element type + */ + abstract static class ChampAbstractTransientMap extends ChampAbstractTransientCollection { + @SuppressWarnings("unchecked") + boolean removeAll(Iterable c) { + if (isEmpty()) { + return false; + } + boolean modified = false; + for (Object key : c) { + ChampTrie.ChangeEvent details = removeKey((K)key); + modified |= details.isModified(); + } + return modified; + } + + abstract ChampTrie.ChangeEvent removeKey(K key); + abstract void clear(); + abstract V put(K key, V value); + + boolean putAllTuples(Iterable> c) { + boolean modified = false; + for (Tuple2 e : c) { + V oldValue = put(e._1,e._2); + modified = modified || !Objects.equals(oldValue, e); + } + return modified; + } + + @SuppressWarnings("unchecked") + boolean retainAllTuples(Iterable> c) { + if (isEmpty()) { + return false; + } + if (c instanceof Collection && ((Collection) c).isEmpty() + || c instanceof Traversable && ((Traversable) c).isEmpty()) { + clear(); + return true; + } + if (c instanceof Collection) { + Collection that = (Collection) c; + return filterAll(e -> that.contains(e.getKey())); + }else if (c instanceof java.util.Map) { + java.util.Map that = (java.util.Map) c; + return filterAll(e -> that.containsKey(e.getKey())&&Objects.equals(e.getValue(),that.get(e.getKey()))); + } else { + java.util.HashSet that = new HashSet<>(); + c.forEach(t->that.add(new AbstractMap.SimpleImmutableEntry<>(t._1,t._2))); + return filterAll(that::contains); + } + } + + abstract boolean filterAll(Predicate> predicate); + } + + /** + * Abstract base class for a transient CHAMP set. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ * + * @param the element type + * @param the data type of the CHAMP trie + */ + abstract static class ChampAbstractTransientSet extends ChampAbstractTransientCollection { + abstract void clear(); + abstract boolean remove(Object o); + boolean removeAll( Iterable c) { + if (isEmpty()) { + return false; + } + if (c == this) { + clear(); + return true; + } + boolean modified = false; + for (Object o : c) { + modified |= remove(o); + } + return modified; + } + + abstract java.util.Iterator iterator(); + boolean retainAll( Iterable c) { + if (isEmpty()) { + return false; + } + if (c instanceof Collection && ((Collection) c).isEmpty()) { + Collection cc = (Collection) c; + clear(); + return true; + } + Predicate predicate; + if (c instanceof Collection) { + Collection that = (Collection) c; + predicate = that::contains; + } else { + HashSet that = new HashSet<>(); + c.forEach(that::add); + predicate = that::contains; + } + boolean removed = false; + for (Iterator i = iterator(); i.hasNext(); ) { + E e = i.next(); + if (!predicate.test(e)) { + remove(e); + removed = true; + } + } + return removed; + } + } +} diff --git a/src/main/java/io/vavr/collection/ChampTrie.java b/src/main/java/io/vavr/collection/ChampTrie.java new file mode 100644 index 000000000..e84711f95 --- /dev/null +++ b/src/main/java/io/vavr/collection/ChampTrie.java @@ -0,0 +1,1724 @@ +/* + * ____ ______________ ________________________ __________ + * \ \/ / \ \/ / __/ / \ \/ / \ + * \______/___/\___\______/___/_____/___/\___\______/___/\___\ + * + * The MIT License (MIT) + * + * Copyright 2023 Vavr, https://vavr.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.vavr.collection; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; + +import static io.vavr.collection.ChampTrie.ChampListHelper.arrayEquals; +import static io.vavr.collection.ChampTrie.NodeFactory.newBitmapIndexedNode; +import static io.vavr.collection.ChampTrie.NodeFactory.newHashCollisionNode; + +/** + * 'Compressed Hash-Array Mapped Prefix-tree' (CHAMP) trie. + *

+ * References: + *

+ *
The Capsule Hash Trie Collections Library. + *
Copyright (c) Michael Steindorfer. BSD-2-Clause License
+ *
github.com + *
+ * + */ +class ChampTrie { + /** + * Represents a node in a 'Compressed Hash-Array Mapped Prefix-tree' + * (CHAMP) trie. + *

+ * A trie is a tree structure that stores a set of data objects; the + * path to a data object is determined by a bit sequence derived from the data + * object. + *

+ * In a CHAMP trie, the bit sequence is derived from the hash code of a data + * object. A hash code is a bit sequence with a fixed length. This bit sequence + * is split up into parts. Each part is used as the index to the next child node + * in the tree, starting from the root node of the tree. + *

+ * The nodes of a CHAMP trie are compressed. Instead of allocating a node for + * each data object, the data objects are stored directly in the ancestor node + * at which the path to the data object starts to become unique. This means, + * that in most cases, only a prefix of the bit sequence is needed for the + * path to a data object in the tree. + *

+ * If the hash code of a data object in the set is not unique, then it is + * stored in a {@link HashCollisionNode}, otherwise it is stored in a + * {@link BitmapIndexedNode}. Since the hash codes have a fixed length, + * all {@link HashCollisionNode}s are located at the same, maximal depth + * of the tree. + *

+ * In this implementation, a hash code has a length of + * {@value #HASH_CODE_LENGTH} bits, and is split up in little-endian order into parts of + * {@value #BIT_PARTITION_SIZE} bits (the last part contains the remaining bits). + *

+ * References: + *

+ * This class has been derived from 'The Capsule Hash Trie Collections Library'. + *

+ *
The Capsule Hash Trie Collections Library. + *
Copyright (c) Michael Steindorfer. BSD-2-Clause License
+ *
github.com + *
+ * + * @param the type of the data objects that are stored in this trie + */ + abstract static class Node { + /** + * Represents no data. + * We can not use {@code null}, because we allow storing null-data in the + * trie. + */ + static final Object NO_DATA = new Object(); + static final int HASH_CODE_LENGTH = 32; + /** + * Bit partition size in the range [1,5]. + *

+ * The bit-mask must fit into the 32 bits of an int field ({@code 32 = 1<<5}). + * (You can use a size of 6, if you replace the bit-mask fields with longs). + */ + static final int BIT_PARTITION_SIZE = 5; + static final int BIT_PARTITION_MASK = (1 << BIT_PARTITION_SIZE) - 1; + static final int MAX_DEPTH = (HASH_CODE_LENGTH + BIT_PARTITION_SIZE - 1) / BIT_PARTITION_SIZE + 1; + + + Node() { + } + + /** + * Given a masked dataHash, returns its bit-position + * in the bit-map. + *

+ * For example, if the bit partition is 5 bits, then + * we 2^5 == 32 distinct bit-positions. + * If the masked dataHash is 3 then the bit-position is + * the bit with index 3. That is, 1<<3 = 0b0100. + * + * @param mask masked data hash + * @return bit position + */ + static int bitpos(int mask) { + return 1 << mask; + } + + static E getFirst( Node node) { + while (node instanceof BitmapIndexedNode) { + BitmapIndexedNode bxn = (BitmapIndexedNode) node; + int nodeMap = bxn.nodeMap(); + int dataMap = bxn.dataMap(); + if ((nodeMap | dataMap) == 0) { + break; + } + int firstNodeBit = Integer.numberOfTrailingZeros(nodeMap); + int firstDataBit = Integer.numberOfTrailingZeros(dataMap); + if (nodeMap != 0 && firstNodeBit < firstDataBit) { + node = node.getNode(0); + } else { + return node.getData(0); + } + } + if (node instanceof HashCollisionNode) { + HashCollisionNode hcn = (HashCollisionNode) node; + return hcn.getData(0); + } + throw new NoSuchElementException(); + } + + static E getLast( Node node) { + while (node instanceof BitmapIndexedNode) { + BitmapIndexedNode bxn = (BitmapIndexedNode) node; + int nodeMap = bxn.nodeMap(); + int dataMap = bxn.dataMap(); + if ((nodeMap | dataMap) == 0) { + break; + } + if (Integer.compareUnsigned(nodeMap, dataMap) > 0) { + node = node.getNode(node.nodeArity() - 1); + } else { + return node.getData(node.dataArity() - 1); + } + } + if (node instanceof HashCollisionNode) { + HashCollisionNode hcn = (HashCollisionNode) node; + return hcn.getData(hcn.dataArity() - 1); + } + throw new NoSuchElementException(); + } + + static int mask(int dataHash, int shift) { + return (dataHash >>> shift) & BIT_PARTITION_MASK; + } + + static Node mergeTwoDataEntriesIntoNode(IdentityObject owner, + K k0, int keyHash0, + K k1, int keyHash1, + int shift) { + if (shift >= HASH_CODE_LENGTH) { + Object[] entries = new Object[2]; + entries[0] = k0; + entries[1] = k1; + return NodeFactory.newHashCollisionNode(owner, keyHash0, entries); + } + + int mask0 = mask(keyHash0, shift); + int mask1 = mask(keyHash1, shift); + + if (mask0 != mask1) { + // both nodes fit on same level + int dataMap = bitpos(mask0) | bitpos(mask1); + + Object[] entries = new Object[2]; + if (mask0 < mask1) { + entries[0] = k0; + entries[1] = k1; + } else { + entries[0] = k1; + entries[1] = k0; + } + return NodeFactory.newBitmapIndexedNode(owner, (0), dataMap, entries); + } else { + Node node = mergeTwoDataEntriesIntoNode(owner, + k0, keyHash0, + k1, keyHash1, + shift + BIT_PARTITION_SIZE); + // values fit on next level + + int nodeMap = bitpos(mask0); + return NodeFactory.newBitmapIndexedNode(owner, nodeMap, (0), new Object[]{node}); + } + } + + abstract int dataArity(); + + /** + * Checks if this trie is equivalent to the specified other trie. + * + * @param other the other trie + * @return true if equivalent + */ + abstract boolean equivalent( Object other); + + /** + * Finds a data object in the CHAMP trie, that matches the provided data + * object and data hash. + * + * @param data the provided data object + * @param dataHash the hash code of the provided data + * @param shift the shift for this node + * @param equalsFunction a function that tests data objects for equality + * @return the found data, returns {@link #NO_DATA} if no data in the trie + * matches the provided data. + */ + abstract Object find(D data, int dataHash, int shift, BiPredicate equalsFunction); + + abstract D getData(int index); + + IdentityObject getOwner() { + return null; + } + + abstract Node getNode(int index); + + abstract boolean hasData(); + + boolean isNodeEmpty() { + return !hasData() && !hasNodes(); + } + + boolean hasMany() { + return hasNodes() || dataArity() > 1; + } + + abstract boolean hasDataArityOne(); + + abstract boolean hasNodes(); + + boolean isAllowedToUpdate( IdentityObject y) { + IdentityObject x = getOwner(); + return x != null && x == y; + } + + abstract int nodeArity(); + + /** + * Removes a data object from the trie. + * + * @param owner A non-null value means, that this method may update + * nodes that are marked with the same unique id, + * and that this method may create new mutable nodes + * with this unique id. + * A null value means, that this method must not update + * any node and may only create new immutable nodes. + * @param data the data to be removed + * @param dataHash the hash-code of the data object + * @param shift the shift of the current node + * @param details this method reports the changes that it performed + * in this object + * @param equalsFunction a function that tests data objects for equality + * @return the updated trie + */ + abstract Node remove(IdentityObject owner, D data, + int dataHash, int shift, + ChangeEvent details, + BiPredicate equalsFunction); + + /** + * Inserts or replaces a data object in the trie. + * + * @param owner A non-null value means, that this method may update + * nodes that are marked with the same unique id, + * and that this method may create new mutable nodes + * with this unique id. + * A null value means, that this method must not update + * any node and may only create new immutable nodes. + * @param newData the data to be inserted, + * or to be used for merging if there is already + * a matching data object in the trie + * @param dataHash the hash-code of the data object + * @param shift the shift of the current node + * @param details this method reports the changes that it performed + * in this object + * @param updateFunction only used if there is a matching data object + * in the trie. + * Given the existing data object (first argument) and + * the new data object (second argument), yields a + * new data object or returns either of the two. + * In all cases, the update function must return + * a data object that has the same data hash + * as the existing data object. + * @param equalsFunction a function that tests data objects for equality + * @param hashFunction a function that computes the hash-code for a data + * object + * @return the updated trie + */ + abstract Node put(IdentityObject owner, D newData, + int dataHash, int shift, ChangeEvent details, + BiFunction updateFunction, + BiPredicate equalsFunction, + ToIntFunction hashFunction); + /** + * Inserts or replaces data elements from the specified other trie in this trie. + * + * @param owner + * @param otherNode a node with the same shift as this node from the other trie + * @param shift the shift of this node and the other node + * @param bulkChange updates the field {@link BulkChangeEvent#inBoth} + * @param updateFunction the update function for data elements + * @param equalsFunction the equals function for data elements + * @param hashFunction the hash function for data elements + * @param details the change event for single elements + * @return the updated trie + */ + abstract Node putAll(IdentityObject owner, Node otherNode, int shift, + BulkChangeEvent bulkChange, + BiFunction updateFunction, + BiPredicate equalsFunction, + ToIntFunction hashFunction, + ChangeEvent details); + + /** + * Removes data elements in the specified other trie from this trie. + * + * @param owner + * @param otherNode a node with the same shift as this node from the other trie + * @param shift the shift of this node and the other node + * @param bulkChange updates the field {@link BulkChangeEvent#removed} + * @param updateFunction the update function for data elements + * @param equalsFunction the equals function for data elements + * @param hashFunction the hash function for data elements + * @param details the change event for single elements + * @return the updated trie + */ + abstract Node removeAll(IdentityObject owner, Node otherNode, int shift, + BulkChangeEvent bulkChange, + BiFunction updateFunction, + BiPredicate equalsFunction, + ToIntFunction hashFunction, + ChangeEvent details); + + /** + * Retains data elements in this trie that are also in the other trie - removes the rest. + * + * @param owner + * @param otherNode a node with the same shift as this node from the other trie + * @param shift the shift of this node and the other node + * @param bulkChange updates the field {@link BulkChangeEvent#removed} + * @param updateFunction the update function for data elements + * @param equalsFunction the equals function for data elements + * @param hashFunction the hash function for data elements + * @param details the change event for single elements + * @return the updated trie + */ + abstract Node retainAll(IdentityObject owner, Node otherNode, int shift, + BulkChangeEvent bulkChange, + BiFunction updateFunction, + BiPredicate equalsFunction, + ToIntFunction hashFunction, + ChangeEvent details); + + /** + * Retains data elements in this trie for which the provided predicate returns true. + * + * @param owner + * @param predicate a predicate that returns true for data elements that should be retained + * @param shift the shift of this node and the other node + * @param bulkChange updates the field {@link BulkChangeEvent#removed} + * @return the updated trie + */ + abstract Node filterAll(IdentityObject owner, Predicate predicate, int shift, + BulkChangeEvent bulkChange); + + abstract int calculateSize();} + + /** + * Represents a bitmap-indexed node in a CHAMP trie. + *

+ * References: + *

+ * Portions of the code in this class have been derived from 'The Capsule Hash Trie Collections Library', and from + * 'JHotDraw 8'. + *

+ *
The Capsule Hash Trie Collections Library. + *
Copyright (c) Michael Steindorfer. BSD-2-Clause License
+ *
github.com + *
+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com + *
+ *
+ * + * @param the data type + */ + static class BitmapIndexedNode extends Node { + static final BitmapIndexedNode EMPTY_NODE = newBitmapIndexedNode(null, (0), (0), new Object[]{}); + + final Object [] mixed; + private final int nodeMap; + private final int dataMap; + + BitmapIndexedNode(int nodeMap, + int dataMap, Object [] mixed) { + this.nodeMap = nodeMap; + this.dataMap = dataMap; + this.mixed = mixed; + assert mixed.length == nodeArity() + dataArity(); + } + + @SuppressWarnings("unchecked") + static BitmapIndexedNode emptyNode() { + return (BitmapIndexedNode) EMPTY_NODE; + } + + BitmapIndexedNode copyAndInsertData(IdentityObject owner, int bitpos, + D data) { + int idx = dataIndex(bitpos); + Object[] dst = ChampListHelper.copyComponentAdd(this.mixed, idx, 1); + dst[idx] = data; + return newBitmapIndexedNode(owner, nodeMap, dataMap | bitpos, dst); + } + + BitmapIndexedNode copyAndMigrateFromDataToNode(IdentityObject owner, + int bitpos, Node node) { + + int idxOld = dataIndex(bitpos); + int idxNew = this.mixed.length - 1 - nodeIndex(bitpos); + assert idxOld <= idxNew; + + // copy 'src' and remove entryLength element(s) at position 'idxOld' and + // insert 1 element(s) at position 'idxNew' + Object[] src = this.mixed; + Object[] dst = new Object[src.length]; + System.arraycopy(src, 0, dst, 0, idxOld); + System.arraycopy(src, idxOld + 1, dst, idxOld, idxNew - idxOld); + System.arraycopy(src, idxNew + 1, dst, idxNew + 1, src.length - idxNew - 1); + dst[idxNew] = node; + return newBitmapIndexedNode(owner, nodeMap | bitpos, dataMap ^ bitpos, dst); + } + + BitmapIndexedNode copyAndMigrateFromNodeToData(IdentityObject owner, + int bitpos, Node node) { + int idxOld = this.mixed.length - 1 - nodeIndex(bitpos); + int idxNew = dataIndex(bitpos); + + // copy 'src' and remove 1 element(s) at position 'idxOld' and + // insert entryLength element(s) at position 'idxNew' + Object[] src = this.mixed; + Object[] dst = new Object[src.length]; + assert idxOld >= idxNew; + System.arraycopy(src, 0, dst, 0, idxNew); + System.arraycopy(src, idxNew, dst, idxNew + 1, idxOld - idxNew); + System.arraycopy(src, idxOld + 1, dst, idxOld + 1, src.length - idxOld - 1); + dst[idxNew] = node.getData(0); + return newBitmapIndexedNode(owner, nodeMap ^ bitpos, dataMap | bitpos, dst); + } + + BitmapIndexedNode copyAndSetNode(IdentityObject owner, int bitpos, + Node node) { + + int idx = this.mixed.length - 1 - nodeIndex(bitpos); + if (isAllowedToUpdate(owner)) { + // no copying if already editable + this.mixed[idx] = node; + return this; + } else { + // copy 'src' and set 1 element(s) at position 'idx' + final Object[] dst = ChampListHelper.copySet(this.mixed, idx, node); + return newBitmapIndexedNode(owner, nodeMap, dataMap, dst); + } + } + + @Override + int dataArity() { + return Integer.bitCount(dataMap); + } + + int dataIndex(int bitpos) { + return Integer.bitCount(dataMap & (bitpos - 1)); + } + + int index(int map, int bitpos) { + return Integer.bitCount(map & (bitpos - 1)); + } + + int dataMap() { + return dataMap; + } + + @SuppressWarnings("unchecked") + @Override + boolean equivalent( Object other) { + if (this == other) { + return true; + } + BitmapIndexedNode that = (BitmapIndexedNode) other; + Object[] thatNodes = that.mixed; + // nodes array: we compare local data from 0 to splitAt (excluded) + // and then we compare the nested nodes from splitAt to length (excluded) + int splitAt = dataArity(); + return nodeMap() == that.nodeMap() + && dataMap() == that.dataMap() + && arrayEquals(mixed, 0, splitAt, thatNodes, 0, splitAt) + && arrayEquals(mixed, splitAt, mixed.length, thatNodes, splitAt, thatNodes.length, + (a, b) -> ((Node) a).equivalent(b) ); + } + + + @Override + + Object find(D key, int dataHash, int shift, BiPredicate equalsFunction) { + int bitpos = bitpos(mask(dataHash, shift)); + if ((nodeMap & bitpos) != 0) { + return getNode(nodeIndex(bitpos)).find(key, dataHash, shift + BIT_PARTITION_SIZE, equalsFunction); + } + if ((dataMap & bitpos) != 0) { + D k = getData(dataIndex(bitpos)); + if (equalsFunction.test(k, key)) { + return k; + } + } + return NO_DATA; + } + + + @Override + @SuppressWarnings("unchecked") + + D getData(int index) { + return (D) mixed[index]; + } + + + @Override + @SuppressWarnings("unchecked") + Node getNode(int index) { + return (Node) mixed[mixed.length - 1 - index]; + } + + @Override + boolean hasData() { + return dataMap != 0; + } + + @Override + boolean hasDataArityOne() { + return Integer.bitCount(dataMap) == 1; + } + + @Override + boolean hasNodes() { + return nodeMap != 0; + } + + @Override + int nodeArity() { + return Integer.bitCount(nodeMap); + } + + int nodeIndex(int bitpos) { + return Integer.bitCount(nodeMap & (bitpos - 1)); + } + + int nodeMap() { + return nodeMap; + } + + @Override + BitmapIndexedNode remove(IdentityObject owner, + D data, + int dataHash, int shift, + ChangeEvent details, BiPredicate equalsFunction) { + int mask = mask(dataHash, shift); + int bitpos = bitpos(mask); + if ((dataMap & bitpos) != 0) { + return removeData(owner, data, dataHash, shift, details, bitpos, equalsFunction); + } + if ((nodeMap & bitpos) != 0) { + return removeSubNode(owner, data, dataHash, shift, details, bitpos, equalsFunction); + } + return this; + } + + private BitmapIndexedNode removeData(IdentityObject owner, D data, int dataHash, int shift, ChangeEvent details, int bitpos, BiPredicate equalsFunction) { + int dataIndex = dataIndex(bitpos); + int entryLength = 1; + if (!equalsFunction.test(getData(dataIndex), data)) { + return this; + } + D currentVal = getData(dataIndex); + details.setRemoved(currentVal); + if (dataArity() == 2 && !hasNodes()) { + int newDataMap = + (shift == 0) ? (dataMap ^ bitpos) : bitpos(mask(dataHash, 0)); + Object[] nodes = {getData(dataIndex ^ 1)}; + return newBitmapIndexedNode(owner, 0, newDataMap, nodes); + } + int idx = dataIndex * entryLength; + Object[] dst = ChampListHelper.copyComponentRemove(this.mixed, idx, entryLength); + return newBitmapIndexedNode(owner, nodeMap, dataMap ^ bitpos, dst); + } + + private BitmapIndexedNode removeSubNode(IdentityObject owner, D data, int dataHash, int shift, + ChangeEvent details, + int bitpos, BiPredicate equalsFunction) { + Node subNode = getNode(nodeIndex(bitpos)); + Node updatedSubNode = + subNode.remove(owner, data, dataHash, shift + BIT_PARTITION_SIZE, details, equalsFunction); + if (subNode == updatedSubNode) { + return this; + } + if (!updatedSubNode.hasNodes() && updatedSubNode.hasDataArityOne()) { + if (!hasData() && nodeArity() == 1) { + return (BitmapIndexedNode) updatedSubNode; + } + return copyAndMigrateFromNodeToData(owner, bitpos, updatedSubNode); + } + return copyAndSetNode(owner, bitpos, updatedSubNode); + } + + @Override + BitmapIndexedNode put(IdentityObject owner, + D newData, + int dataHash, int shift, + ChangeEvent details, + BiFunction updateFunction, + BiPredicate equalsFunction, + ToIntFunction hashFunction) { + int mask = mask(dataHash, shift); + int bitpos = bitpos(mask); + if ((dataMap & bitpos) != 0) { + final int dataIndex = dataIndex(bitpos); + final D oldData = getData(dataIndex); + if (equalsFunction.test(oldData, newData)) { + D updatedData = updateFunction.apply(oldData, newData); + if (updatedData == oldData) { + details.found(oldData); + return this; + } + details.setReplaced(oldData, updatedData); + return copyAndSetData(owner, dataIndex, updatedData); + } + Node updatedSubNode = + mergeTwoDataEntriesIntoNode(owner, + oldData, hashFunction.applyAsInt(oldData), + newData, dataHash, shift + BIT_PARTITION_SIZE); + details.setAdded(newData); + return copyAndMigrateFromDataToNode(owner, bitpos, updatedSubNode); + } else if ((nodeMap & bitpos) != 0) { + Node subNode = getNode(nodeIndex(bitpos)); + Node updatedSubNode = subNode + .put(owner, newData, dataHash, shift + BIT_PARTITION_SIZE, details, updateFunction, equalsFunction, hashFunction); + return subNode == updatedSubNode ? this : copyAndSetNode(owner, bitpos, updatedSubNode); + } + details.setAdded(newData); + return copyAndInsertData(owner, bitpos, newData); + } + + + private BitmapIndexedNode copyAndSetData(IdentityObject owner, int dataIndex, D updatedData) { + if (isAllowedToUpdate(owner)) { + this.mixed[dataIndex] = updatedData; + return this; + } + Object[] newMixed = ChampListHelper.copySet(this.mixed, dataIndex, updatedData); + return newBitmapIndexedNode(owner, nodeMap, dataMap, newMixed); + } + + + @SuppressWarnings("unchecked") + @Override + BitmapIndexedNode putAll(IdentityObject owner, Node other, int shift, + BulkChangeEvent bulkChange, + BiFunction updateFunction, + BiPredicate equalsFunction, + ToIntFunction hashFunction, + ChangeEvent details) { + BitmapIndexedNode that = (BitmapIndexedNode) other; + if (this == that) { + bulkChange.inBoth += this.calculateSize(); + return this; + } + + int newBitMap = nodeMap | dataMap | that.nodeMap | that.dataMap; + Object[] buffer = new Object[Integer.bitCount(newBitMap)]; + int newDataMap = this.dataMap | that.dataMap; + int newNodeMap = this.nodeMap | that.nodeMap; + for (int mapToDo = newBitMap; mapToDo != 0; mapToDo ^= Integer.lowestOneBit(mapToDo)) { + int mask = Integer.numberOfTrailingZeros(mapToDo); + int bitpos = bitpos(mask); + + boolean thisIsData = (this.dataMap & bitpos) != 0; + boolean thatIsData = (that.dataMap & bitpos) != 0; + boolean thisIsNode = (this.nodeMap & bitpos) != 0; + boolean thatIsNode = (that.nodeMap & bitpos) != 0; + + if (!(thisIsNode || thisIsData)) { + // add 'mixed' (data or node) from that trie + if (thatIsData) { + buffer[index(newDataMap, bitpos)] = that.getData(that.dataIndex(bitpos)); + } else { + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = that.getNode(that.nodeIndex(bitpos)); + } + } else if (!(thatIsNode || thatIsData)) { + // add 'mixed' (data or node) from this trie + if (thisIsData) { + buffer[index(newDataMap, bitpos)] = this.getData(dataIndex(bitpos)); + } else { + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = this.getNode(nodeIndex(bitpos)); + } + } else if (thisIsNode && thatIsNode) { + // add a new node that joins this node and that node + Node thisNode = this.getNode(this.nodeIndex(bitpos)); + Node thatNode = that.getNode(that.nodeIndex(bitpos)); + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = thisNode.putAll(owner, thatNode, shift + BIT_PARTITION_SIZE, bulkChange, + updateFunction, equalsFunction, hashFunction, details); + } else if (thisIsData && thatIsNode) { + // add a new node that joins this data and that node + D thisData = this.getData(this.dataIndex(bitpos)); + Node thatNode = that.getNode(that.nodeIndex(bitpos)); + details.reset(); + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = thatNode.put(null, thisData, hashFunction.applyAsInt(thisData), shift + BIT_PARTITION_SIZE, details, + (a, b) -> updateFunction.apply(b, a), + equalsFunction, hashFunction); + if (details.isUnchanged()) { + bulkChange.inBoth++; + } else if (details.isReplaced()) { + bulkChange.replaced = true; + bulkChange.inBoth++; + } + newDataMap ^= bitpos; + } else if (thisIsNode) { + // add a new node that joins this node and that data + D thatData = that.getData(that.dataIndex(bitpos)); + Node thisNode = this.getNode(this.nodeIndex(bitpos)); + details.reset(); + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = thisNode.put(owner, thatData, hashFunction.applyAsInt(thatData), shift + BIT_PARTITION_SIZE, details, updateFunction, equalsFunction, hashFunction); + if (!details.isModified()) { + bulkChange.inBoth++; + } + newDataMap ^= bitpos; + } else { + // add a new node that joins this data and that data + D thisData = this.getData(this.dataIndex(bitpos)); + D thatData = that.getData(that.dataIndex(bitpos)); + if (equalsFunction.test(thisData, thatData)) { + bulkChange.inBoth++; + D updated = updateFunction.apply(thisData, thatData); + buffer[index(newDataMap, bitpos)] = updated; + bulkChange.replaced |= updated != thisData; + } else { + newDataMap ^= bitpos; + newNodeMap ^= bitpos; + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = mergeTwoDataEntriesIntoNode(owner, thisData, hashFunction.applyAsInt(thisData), thatData, hashFunction.applyAsInt(thatData), shift + BIT_PARTITION_SIZE); + } + } + } + return new BitmapIndexedNode<>(newNodeMap, newDataMap, buffer); + } + + @Override + BitmapIndexedNode removeAll(IdentityObject owner, Node other, int shift, BulkChangeEvent bulkChange, BiFunction updateFunction, BiPredicate equalsFunction, ToIntFunction hashFunction, ChangeEvent details) { + BitmapIndexedNode that = (BitmapIndexedNode) other; + if (this == that) { + bulkChange.inBoth += this.calculateSize(); + return this; + } + + int newBitMap = nodeMap | dataMap; + Object[] buffer = new Object[Integer.bitCount(newBitMap)]; + int newDataMap = this.dataMap; + int newNodeMap = this.nodeMap; + for (int mapToDo = newBitMap; mapToDo != 0; mapToDo ^= Integer.lowestOneBit(mapToDo)) { + int mask = Integer.numberOfTrailingZeros(mapToDo); + int bitpos = bitpos(mask); + + boolean thisIsData = (this.dataMap & bitpos) != 0; + boolean thatIsData = (that.dataMap & bitpos) != 0; + boolean thisIsNode = (this.nodeMap & bitpos) != 0; + boolean thatIsNode = (that.nodeMap & bitpos) != 0; + + if (!(thisIsNode || thisIsData)) { + // programming error + assert false; + } else if (!(thatIsNode || thatIsData)) { + // keep 'mixed' (data or node) from this trie + if (thisIsData) { + buffer[index(newDataMap, bitpos)] = this.getData(dataIndex(bitpos)); + } else { + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = this.getNode(nodeIndex(bitpos)); + } + } else if (thisIsNode && thatIsNode) { + // remove all in that node from all in this node + Node thisNode = this.getNode(this.nodeIndex(bitpos)); + Node thatNode = that.getNode(that.nodeIndex(bitpos)); + Node result = thisNode.removeAll(owner, thatNode, shift + BIT_PARTITION_SIZE, bulkChange, updateFunction, equalsFunction, hashFunction, details); + if (result.isNodeEmpty()) { + newNodeMap ^= bitpos; + } else if (result.hasMany()) { + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = result; + } else { + newNodeMap ^= bitpos; + newDataMap ^= bitpos; + buffer[index(newDataMap, bitpos)] = result.getData(0); + } + } else if (thisIsData && thatIsNode) { + // remove this data if it is contained in that node + D thisData = this.getData(this.dataIndex(bitpos)); + Node thatNode = that.getNode(that.nodeIndex(bitpos)); + Object result = thatNode.find(thisData, hashFunction.applyAsInt(thisData), shift + BIT_PARTITION_SIZE, equalsFunction); + if (result == NO_DATA) { + buffer[index(newDataMap, bitpos)] = thisData; + } else { + newDataMap ^= bitpos; + bulkChange.removed++; + } + } else if (thisIsNode) { + // remove that data from this node + D thatData = that.getData(that.dataIndex(bitpos)); + Node thisNode = this.getNode(this.nodeIndex(bitpos)); + details.reset(); + Node result = thisNode.remove(owner, thatData, hashFunction.applyAsInt(thatData), shift + BIT_PARTITION_SIZE, details, equalsFunction); + if (details.isModified()) { + bulkChange.removed++; + } + if (result.isNodeEmpty()) { + newNodeMap ^= bitpos; + } else if (result.hasMany()) { + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = result; + } else { + newDataMap ^= bitpos; + newNodeMap ^= bitpos; + buffer[index(newDataMap, bitpos)] = result.getData(0); + } + } else { + // remove this data if it is equal to that data + D thisData = this.getData(this.dataIndex(bitpos)); + D thatData = that.getData(that.dataIndex(bitpos)); + if (equalsFunction.test(thisData, thatData)) { + bulkChange.removed++; + newDataMap ^= bitpos; + } else { + buffer[index(newDataMap, bitpos)] = thisData; + } + } + } + return newCroppedBitmapIndexedNode(buffer, newDataMap, newNodeMap); + } + + + private BitmapIndexedNode newCroppedBitmapIndexedNode(Object[] buffer, int newDataMap, int newNodeMap) { + int newLength = Integer.bitCount(newNodeMap | newDataMap); + if (newLength != buffer.length) { + Object[] temp = buffer; + buffer = new Object[newLength]; + int dataCount = Integer.bitCount(newDataMap); + int nodeCount = Integer.bitCount(newNodeMap); + System.arraycopy(temp, 0, buffer, 0, dataCount); + System.arraycopy(temp, temp.length - nodeCount, buffer, dataCount, nodeCount); + } + return new BitmapIndexedNode<>(newNodeMap, newDataMap, buffer); + } + + @Override + BitmapIndexedNode retainAll(IdentityObject owner, Node other, int shift, BulkChangeEvent bulkChange, BiFunction updateFunction, BiPredicate equalsFunction, ToIntFunction hashFunction, ChangeEvent details) { + BitmapIndexedNode that = (BitmapIndexedNode) other; + if (this == that) { + bulkChange.inBoth += this.calculateSize(); + return this; + } + + int newBitMap = nodeMap | dataMap; + Object[] buffer = new Object[Integer.bitCount(newBitMap)]; + int newDataMap = this.dataMap; + int newNodeMap = this.nodeMap; + for (int mapToDo = newBitMap; mapToDo != 0; mapToDo ^= Integer.lowestOneBit(mapToDo)) { + int mask = Integer.numberOfTrailingZeros(mapToDo); + int bitpos = bitpos(mask); + + boolean thisIsData = (this.dataMap & bitpos) != 0; + boolean thatIsData = (that.dataMap & bitpos) != 0; + boolean thisIsNode = (this.nodeMap & bitpos) != 0; + boolean thatIsNode = (that.nodeMap & bitpos) != 0; + + if (!(thisIsNode || thisIsData)) { + // programming error + assert false; + } else if (!(thatIsNode || thatIsData)) { + // remove 'mixed' (data or node) from this trie + if (thisIsData) { + newDataMap ^= bitpos; + bulkChange.removed++; + } else { + newNodeMap ^= bitpos; + bulkChange.removed += this.getNode(this.nodeIndex(bitpos)).calculateSize(); + } + } else if (thisIsNode && thatIsNode) { + // retain all in that node from all in this node + Node thisNode = this.getNode(this.nodeIndex(bitpos)); + Node thatNode = that.getNode(that.nodeIndex(bitpos)); + Node result = thisNode.retainAll(owner, thatNode, shift + BIT_PARTITION_SIZE, bulkChange, updateFunction, equalsFunction, hashFunction, details); + if (result.isNodeEmpty()) { + newNodeMap ^= bitpos; + } else if (result.hasMany()) { + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = result; + } else { + newNodeMap ^= bitpos; + newDataMap ^= bitpos; + buffer[index(newDataMap, bitpos)] = result.getData(0); + } + } else if (thisIsData && thatIsNode) { + // retain this data if it is contained in that node + D thisData = this.getData(this.dataIndex(bitpos)); + Node thatNode = that.getNode(that.nodeIndex(bitpos)); + Object result = thatNode.find(thisData, hashFunction.applyAsInt(thisData), shift + BIT_PARTITION_SIZE, equalsFunction); + if (result == NO_DATA) { + newDataMap ^= bitpos; + bulkChange.removed++; + } else { + buffer[index(newDataMap, bitpos)] = thisData; + } + } else if (thisIsNode) { + // retain this data if that data is contained in this node + D thatData = that.getData(that.dataIndex(bitpos)); + Node thisNode = this.getNode(this.nodeIndex(bitpos)); + Object result = thisNode.find(thatData, hashFunction.applyAsInt(thatData), shift + BIT_PARTITION_SIZE, equalsFunction); + if (result == NO_DATA) { + bulkChange.removed += this.getNode(this.nodeIndex(bitpos)).calculateSize(); + newNodeMap ^= bitpos; + } else { + newDataMap ^= bitpos; + newNodeMap ^= bitpos; + buffer[index(newDataMap, bitpos)] = result; + bulkChange.removed += this.getNode(this.nodeIndex(bitpos)).calculateSize() - 1; + } + } else { + // retain this data if it is equal to that data + D thisData = this.getData(this.dataIndex(bitpos)); + D thatData = that.getData(that.dataIndex(bitpos)); + if (equalsFunction.test(thisData, thatData)) { + buffer[index(newDataMap, bitpos)] = thisData; + } else { + bulkChange.removed++; + newDataMap ^= bitpos; + } + } + } + return newCroppedBitmapIndexedNode(buffer, newDataMap, newNodeMap); + } + + @Override + BitmapIndexedNode filterAll(IdentityObject owner, Predicate predicate, int shift, BulkChangeEvent bulkChange) { + int newBitMap = nodeMap | dataMap; + Object[] buffer = new Object[Integer.bitCount(newBitMap)]; + int newDataMap = this.dataMap; + int newNodeMap = this.nodeMap; + for (int mapToDo = newBitMap; mapToDo != 0; mapToDo ^= Integer.lowestOneBit(mapToDo)) { + int mask = Integer.numberOfTrailingZeros(mapToDo); + int bitpos = bitpos(mask); + boolean thisIsNode = (this.nodeMap & bitpos) != 0; + if (thisIsNode) { + Node thisNode = this.getNode(this.nodeIndex(bitpos)); + Node result = thisNode.filterAll(owner, predicate, shift + BIT_PARTITION_SIZE, bulkChange); + if (result.isNodeEmpty()) { + newNodeMap ^= bitpos; + } else if (result.hasMany()) { + buffer[buffer.length - 1 - index(newNodeMap, bitpos)] = result; + } else { + newNodeMap ^= bitpos; + newDataMap ^= bitpos; + buffer[index(newDataMap, bitpos)] = result.getData(0); + } + } else { + D thisData = this.getData(this.dataIndex(bitpos)); + if (predicate.test(thisData)) { + buffer[index(newDataMap, bitpos)] = thisData; + } else { + newDataMap ^= bitpos; + bulkChange.removed++; + } + } + } + return newCroppedBitmapIndexedNode(buffer, newDataMap, newNodeMap); + } + + int calculateSize() { + int size = dataArity(); + for (int i = 0, n = nodeArity(); i < n; i++) { + Node node = getNode(i); + size += node.calculateSize(); + } + return size; + } + } + + /** + * Represents a hash-collision node in a CHAMP trie. + *

+ * XXX hash-collision nodes may become huge performance bottlenecks. + * If the trie contains keys that implement {@link Comparable} then a hash-collision + * nodes should be a sorted tree structure (for example a red-black tree). + * Otherwise, hash-collision node should be a vector (for example a bit mapped trie). + *

+ * References: + *

+ * Portions of the code in this class have been derived from 'The Capsule Hash Trie Collections Library', and from + * 'JHotDraw 8'. + *

+ *
The Capsule Hash Trie Collections Library. + *
Copyright (c) Michael Steindorfer. BSD-2-Clause License
+ *
github.com + *
+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com + *
+ *
+ * + * @param the data type + */ + static class HashCollisionNode extends Node { + private static final HashCollisionNode EMPTY = new HashCollisionNode<>(0, new Object[0]); + private final int hash; + Object[] data; + + HashCollisionNode(int hash, Object[] data) { + this.data = data; + this.hash = hash; + } + + @Override + int dataArity() { + return data.length; + } + + @Override + boolean hasDataArityOne() { + return false; + } + + @SuppressWarnings("unchecked") + @Override + boolean equivalent(Object other) { + if (this == other) { + return true; + } + HashCollisionNode that = (HashCollisionNode) other; + Object[] thatEntries = that.data; + if (hash != that.hash || thatEntries.length != data.length) { + return false; + } + + // Linear scan for each key, because of arbitrary element order. + Object[] thatEntriesCloned = thatEntries.clone(); + int remainingLength = thatEntriesCloned.length; + outerLoop: + for (Object key : data) { + for (int j = 0; j < remainingLength; j += 1) { + Object todoKey = thatEntriesCloned[j]; + if (Objects.equals(todoKey, key)) { + // We have found an equal entry. We do not need to compare + // this entry again. So we replace it with the last entry + // from the array and reduce the remaining length. + System.arraycopy(thatEntriesCloned, remainingLength - 1, thatEntriesCloned, j, 1); + remainingLength -= 1; + + continue outerLoop; + } + } + return false; + } + + return true; + } + + @SuppressWarnings("unchecked") + @Override + Object find(D key, int dataHash, int shift, BiPredicate equalsFunction) { + for (Object entry : data) { + if (equalsFunction.test(key, (D) entry)) { + return entry; + } + } + return NO_DATA; + } + + @Override + @SuppressWarnings("unchecked") + D getData(int index) { + return (D) data[index]; + } + + @Override + Node getNode(int index) { + throw new IllegalStateException("Is leaf node."); + } + + + @Override + boolean hasData() { + return data.length > 0; + } + + @Override + boolean hasNodes() { + return false; + } + + @Override + int nodeArity() { + return 0; + } + + + @SuppressWarnings("unchecked") + @Override + Node remove(IdentityObject owner, D data, + int dataHash, int shift, ChangeEvent details, BiPredicate equalsFunction) { + for (int idx = 0, i = 0; i < this.data.length; i += 1, idx++) { + if (equalsFunction.test((D) this.data[i], data)) { + @SuppressWarnings("unchecked") D currentVal = (D) this.data[i]; + details.setRemoved(currentVal); + + if (this.data.length == 1) { + return BitmapIndexedNode.emptyNode(); + } else if (this.data.length == 2) { + // Create root node with singleton element. + // This node will either be the new root + // returned, or be unwrapped and inlined. + return newBitmapIndexedNode(owner, 0, bitpos(mask(dataHash, 0)), + new Object[]{getData(idx ^ 1)}); + } + // copy keys and remove 1 element at position idx + Object[] entriesNew = ChampListHelper.copyComponentRemove(this.data, idx, 1); + if (isAllowedToUpdate(owner)) { + this.data = entriesNew; + return this; + } + return newHashCollisionNode(owner, dataHash, entriesNew); + } + } + return this; + } + + @SuppressWarnings("unchecked") + @Override + Node put(IdentityObject owner, D newData, + int dataHash, int shift, ChangeEvent details, + BiFunction updateFunction, BiPredicate equalsFunction, + ToIntFunction hashFunction) { + assert this.hash == dataHash; + + for (int i = 0; i < this.data.length; i++) { + D oldData = (D) this.data[i]; + if (equalsFunction.test(oldData, newData)) { + D updatedData = updateFunction.apply(oldData, newData); + if (updatedData == oldData) { + details.found(oldData); + return this; + } + details.setReplaced(oldData, updatedData); + if (isAllowedToUpdate(owner)) { + this.data[i] = updatedData; + return this; + } + final Object[] newKeys = ChampListHelper.copySet(this.data, i, updatedData); + return newHashCollisionNode(owner, dataHash, newKeys); + } + } + + // copy entries and add 1 more at the end + Object[] entriesNew = ChampListHelper.copyComponentAdd(this.data, this.data.length, 1); + entriesNew[this.data.length] = newData; + details.setAdded(newData); + if (isAllowedToUpdate(owner)) { + this.data = entriesNew; + return this; + } + return newHashCollisionNode(owner, dataHash, entriesNew); + } + + @Override + int calculateSize() { + return dataArity(); + } + + @SuppressWarnings("unchecked") + @Override + Node putAll(IdentityObject owner, Node otherNode, int shift, BulkChangeEvent bulkChange, BiFunction updateFunction, BiPredicate equalsFunction, ToIntFunction hashFunction, ChangeEvent details) { + if (otherNode == this) { + bulkChange.inBoth += dataArity(); + return this; + } + HashCollisionNode that = (HashCollisionNode) otherNode; + + // XXX HashSetTest requires that we use a specific iteration sequence. We could use a more performant + // algorithm, if we would not have to care about the iteration sequence. + + // The buffer initially contains all data elements from this node. + // Every time we find a data element in that node, that is not in this node, we add it to the end + // of the buffer. + // Buffer content: + // 0..thisSize-1 = data elements from this node + // thisSize..resultSize-1 = data elements from that node that are not also contained in this node + final int thisSize = this.dataArity(); + final int thatSize = that.dataArity(); + Object[] buffer = Arrays.copyOf(this.data, thisSize + thatSize); + System.arraycopy(this.data, 0, buffer, 0, this.data.length); + Object[] thatArray = that.data; + int resultSize = thisSize; + boolean updated = false; + outer: + for (int i = 0; i < thatSize; i++) { + D thatData = (D) thatArray[i]; + for (int j = 0; j < thisSize; j++) { + D thisData = (D) buffer[j]; + if (equalsFunction.test(thatData, thisData)) { + D updatedData = updateFunction.apply(thisData, thatData); + buffer[j] = updatedData; + updated |= updatedData != thisData; + bulkChange.inBoth++; + continue outer; + } + } + buffer[resultSize++] = thatData; + } + return newCroppedHashCollisionNode(updated | resultSize != thisSize, buffer, resultSize); + } + + @SuppressWarnings("unchecked") + @Override + Node removeAll(IdentityObject owner, Node otherNode, int shift, BulkChangeEvent bulkChange, BiFunction updateFunction, BiPredicate equalsFunction, ToIntFunction hashFunction, ChangeEvent details) { + if (otherNode == this) { + bulkChange.removed += dataArity(); + return (Node) EMPTY; + } + HashCollisionNode that = (HashCollisionNode) otherNode; + + // XXX HashSetTest requires that we use a specific iteration sequence. We could use a more performant + // algorithm, if we would not have to care about the iteration sequence. + + // The buffer initially contains all data elements from this node. + // Every time we find a data element that must be removed, we remove it from the buffer. + // Buffer content: + // 0..resultSize-1 = data elements from this node that have not been removed + final int thisSize = this.dataArity(); + final int thatSize = that.dataArity(); + int resultSize = thisSize; + Object[] buffer = this.data.clone(); + Object[] thatArray = that.data; + outer: + for (int i = 0; i < thatSize && resultSize > 0; i++) { + D thatData = (D) thatArray[i]; + for (int j = 0; j < resultSize; j++) { + D thisData = (D) buffer[j]; + if (equalsFunction.test(thatData, thisData)) { + System.arraycopy(buffer, j + 1, buffer, j, resultSize - j - 1); + resultSize--; + bulkChange.removed++; + continue outer; + } + } + } + return newCroppedHashCollisionNode(thisSize != resultSize, buffer, resultSize); + } + + + private HashCollisionNode newCroppedHashCollisionNode(boolean changed, Object[] buffer, int size) { + if (changed) { + if (buffer.length != size) { + buffer = Arrays.copyOf(buffer, size); + } + return new HashCollisionNode<>(hash, buffer); + } + return this; + } + + @SuppressWarnings("unchecked") + @Override + Node retainAll(IdentityObject owner, Node otherNode, int shift, BulkChangeEvent bulkChange, BiFunction updateFunction, BiPredicate equalsFunction, ToIntFunction hashFunction, ChangeEvent details) { + if (otherNode == this) { + bulkChange.removed += dataArity(); + return (Node) EMPTY; + } + HashCollisionNode that = (HashCollisionNode) otherNode; + + // XXX HashSetTest requires that we use a specific iteration sequence. We could use a more performant + // algorithm, if we would not have to care about the iteration sequence. + + // The buffer initially contains all data elements from this node. + // Every time we find a data element that must be retained, we add it to the buffer. + final int thisSize = this.dataArity(); + final int thatSize = that.dataArity(); + int resultSize = 0; + Object[] buffer = this.data.clone(); + Object[] thatArray = that.data; + Object[] thisArray = this.data; + outer: + for (int i = 0; i < thatSize; i++) { + D thatData = (D) thatArray[i]; + for (int j = resultSize; j < thisSize; j++) { + D thisData = (D) thisArray[j]; + if (equalsFunction.test(thatData, thisData)) { + buffer[resultSize++] = thisData; + continue outer; + } + } + bulkChange.removed++; + } + return newCroppedHashCollisionNode(thisSize != resultSize, buffer, resultSize); + } + + @SuppressWarnings("unchecked") + @Override + Node filterAll(IdentityObject owner, Predicate predicate, int shift, BulkChangeEvent bulkChange) { + final int thisSize = this.dataArity(); + int resultSize = 0; + Object[] buffer = new Object[thisSize]; + Object[] thisArray = this.data; + outer: + for (int i = 0; i < thisSize; i++) { + D thisData = (D) thisArray[i]; + if (predicate.test(thisData)) { + buffer[resultSize++] = thisData; + } else { + bulkChange.removed++; + } + } + return newCroppedHashCollisionNode(thisSize != resultSize, buffer, resultSize); + } + } + + /** + * A {@link BitmapIndexedNode} that provides storage space for a 'owner' identity. + *

+ * References: + *

+ * This class has been derived from 'The Capsule Hash Trie Collections Library'. + *

+ *
The Capsule Hash Trie Collections Library. + *
Copyright (c) Michael Steindorfer. BSD-2-Clause License
+ *
github.com + *
+ * @param the key type + */ + static class MutableBitmapIndexedNode extends BitmapIndexedNode { + private static final long serialVersionUID = 0L; + private final IdentityObject owner; + + MutableBitmapIndexedNode(IdentityObject owner, int nodeMap, int dataMap, Object [] nodes) { + super(nodeMap, dataMap, nodes); + this.owner = owner; + } + + @Override + IdentityObject getOwner() { + return owner; + } + } + + /** + * A {@link HashCollisionNode} that provides storage space for a 'owner' identity.. + *

+ * References: + *

+ * This class has been derived from 'The Capsule Hash Trie Collections Library'. + *

+ *
The Capsule Hash Trie Collections Library. + *
Copyright (c) Michael Steindorfer. BSD-2-Clause License
+ *
github.com + *
+ * + * @param the key type + */ + static class MutableHashCollisionNode extends HashCollisionNode { + private static final long serialVersionUID = 0L; + private final IdentityObject owner; + + MutableHashCollisionNode(IdentityObject owner, int hash, Object [] entries) { + super(hash, entries); + this.owner = owner; + } + + @Override + IdentityObject getOwner() { + return owner; + } + } + + /** + * Provides factory methods for {@link Node}s. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ */ + static class NodeFactory { + + /** + * Don't let anyone instantiate this class. + */ + private NodeFactory() { + } + + static BitmapIndexedNode newBitmapIndexedNode( + IdentityObject owner, int nodeMap, + int dataMap, Object[] nodes) { + return owner == null + ? new BitmapIndexedNode<>(nodeMap, dataMap, nodes) + : new MutableBitmapIndexedNode<>(owner, nodeMap, dataMap, nodes); + } + + static HashCollisionNode newHashCollisionNode( + IdentityObject owner, int hash, Object [] entries) { + return owner == null + ? new HashCollisionNode<>(hash, entries) + : new MutableHashCollisionNode<>(owner, hash, entries); + } + } + + /** + * This class is used to report a change (or no changes) of data in a CHAMP trie. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ * + * @param the data type + */ + static class ChangeEvent { + private Type type = Type.UNCHANGED; + private D oldData; + private D newData; + + ChangeEvent() { + } + + boolean isUnchanged() { + return type == Type.UNCHANGED; + } + + boolean isAdded() { + return type== Type.ADDED; + } + + /** + * Call this method to indicate that a data element has been added. + */ + void setAdded( D newData) { + this.newData = newData; + this.type = Type.ADDED; + } + + void found(D data) { + this.oldData = data; + } + + D getOldData() { + return oldData; + } + + D getNewData() { + return newData; + } + + D getOldDataNonNull() { + return Objects.requireNonNull(oldData); + } + + D getNewDataNonNull() { + return Objects.requireNonNull(newData); + } + + /** + * Call this method to indicate that the value of an element has changed. + * + * @param oldData the old value of the element + * @param newData the new value of the element + */ + void setReplaced( D oldData, D newData) { + this.oldData = oldData; + this.newData = newData; + this.type = Type.REPLACED; + } + + /** + * Call this method to indicate that an element has been removed. + * + * @param oldData the value of the removed element + */ + void setRemoved( D oldData) { + this.oldData = oldData; + this.type = Type.REMOVED; + } + + /** + * Returns true if the CHAMP trie has been modified. + */ + boolean isModified() { + return type != Type.UNCHANGED; + } + + /** + * Returns true if the data element has been replaced. + */ + boolean isReplaced() { + return type == Type.REPLACED; + } + + void reset() { + type = Type.UNCHANGED; + oldData = null; + newData = null; + } + + enum Type { + UNCHANGED, + ADDED, + REMOVED, + REPLACED + } + } + + static class BulkChangeEvent { + int inBoth; + boolean replaced; + int removed; + } + + /** + * An object with a unique identity within this VM. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ */ + static class IdentityObject implements Serializable { + + private static final long serialVersionUID = 0L; + + IdentityObject() { + } + } + + /** + * Provides helper methods for lists that are based on arrays. + *

+ * References: + *

+ * The code in this class has been derived from JHotDraw 8. + *

+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
+ * + * @author Werner Randelshofer + */ + static class ChampListHelper { + /** + * Don't let anyone instantiate this class. + */ + private ChampListHelper() { + + } + + + /** + * Copies 'src' and inserts 'numComponents' at position 'index'. + *

+ * The new components will have a null value. + * + * @param src an array + * @param index an index + * @param numComponents the number of array components to be added + * @param the array type + * @return a new array + */ + static T[] copyComponentAdd(T[] src, int index, int numComponents) { + if (index == src.length) { + return Arrays.copyOf(src, src.length + numComponents); + } + @SuppressWarnings("unchecked") final T[] dst = (T[]) java.lang.reflect.Array.newInstance(src.getClass().getComponentType(), src.length + numComponents); + System.arraycopy(src, 0, dst, 0, index); + System.arraycopy(src, index, dst, index + numComponents, src.length - index); + return dst; + } + + /** + * Copies 'src' and removes 'numComponents' at position 'index'. + * + * @param src an array + * @param index an index + * @param numComponents the number of array components to be removed + * @param the array type + * @return a new array + */ + static T[] copyComponentRemove(T[] src, int index, int numComponents) { + if (index == src.length - numComponents) { + return Arrays.copyOf(src, src.length - numComponents); + } + @SuppressWarnings("unchecked") final T[] dst = (T[]) Array.newInstance(src.getClass().getComponentType(), src.length - numComponents); + System.arraycopy(src, 0, dst, 0, index); + System.arraycopy(src, index + numComponents, dst, index, src.length - index - numComponents); + return dst; + } + + /** + * Copies 'src' and sets 'value' at position 'index'. + * + * @param src an array + * @param index an index + * @param value a value + * @param the array type + * @return a new array + */ + static T[] copySet(T[] src, int index, T value) { + final T[] dst = Arrays.copyOf(src, src.length); + dst[index] = value; + return dst; + } + + /** + * Checks if the specified array ranges are equal. + * + * @param a array a + * @param aFrom from index in array a + * @param aTo to index in array a + * @param b array b + * @param bFrom from index in array b + * @param bTo to index in array b + * @return true if equal + */ + static boolean arrayEquals(Object[] a, int aFrom, int aTo, + Object[] b, int bFrom, int bTo) { + if (aTo - aFrom != bTo - bFrom) return false; + int bOffset = bFrom - aFrom; + for (int i = aFrom; i < aTo; i++) { + if (!Objects.equals(a[i], b[i + bOffset])) { + return false; + } + } + return true; + } + /** + * Checks if the specified array ranges are equal. + * + * @param a array a + * @param aFrom from index in array a + * @param aTo to index in array a + * @param b array b + * @param bFrom from index in array b + * @param bTo to index in array b + * @return true if equal + */ + static boolean arrayEquals(Object[] a, int aFrom, int aTo, + Object[] b, int bFrom, int bTo, + BiPredicate c) { + if (aTo - aFrom != bTo - bFrom) return false; + int bOffset = bFrom - aFrom; + for (int i = aFrom; i < aTo; i++) { + if (!c.test(a[i], b[i + bOffset])) { + return false; + } + } + return true; + } + + /** + * Checks if the provided index is {@literal >= 0} and {@literal <=} size; + * + * @param index the index + * @param size the size + * @throws IndexOutOfBoundsException if index is out of bounds + */ + static void checkIndex(int index, int size) { + if (index < 0 || index >= size) throw new IndexOutOfBoundsException("index=" + index + " size=" + size); + } + } +} diff --git a/src/main/java/io/vavr/collection/Collections.java b/src/main/java/io/vavr/collection/Collections.java index bc64d1a7d..97fafac5d 100644 --- a/src/main/java/io/vavr/collection/Collections.java +++ b/src/main/java/io/vavr/collection/Collections.java @@ -348,7 +348,7 @@ static , T> C retainAll(C source, Iterable if (source.isEmpty()) { return source; } else { - final Set retained = HashSet.ofAll(elements); + final Set retained = elements instanceof Set ? (Set) (Set) elements : HashSet.ofAll(elements); return (C) source.filter(retained::contains); } } diff --git a/src/main/java/io/vavr/collection/HashArrayMappedTrie.java b/src/main/java/io/vavr/collection/HashArrayMappedTrie.java deleted file mode 100644 index 0fd045a78..000000000 --- a/src/main/java/io/vavr/collection/HashArrayMappedTrie.java +++ /dev/null @@ -1,792 +0,0 @@ -/* ____ ______________ ________________________ __________ - * \ \/ / \ \/ / __/ / \ \/ / \ - * \______/___/\___\______/___/_____/___/\___\______/___/\___\ - * - * The MIT License (MIT) - * - * Copyright 2024 Vavr, https://vavr.io - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package io.vavr.collection; - -import io.vavr.Tuple; -import io.vavr.Tuple2; -import io.vavr.collection.HashArrayMappedTrieModule.EmptyNode; -import io.vavr.control.Option; - -import java.io.Serializable; -import java.util.NoSuchElementException; -import java.util.Objects; - -import static java.lang.Integer.bitCount; -import static java.util.Arrays.copyOf; -import static io.vavr.collection.HashArrayMappedTrieModule.Action.PUT; -import static io.vavr.collection.HashArrayMappedTrieModule.Action.REMOVE; - -/** - * An immutable Hash array mapped trie (HAMT). - */ -interface HashArrayMappedTrie extends Iterable> { - - static HashArrayMappedTrie empty() { - return EmptyNode.instance(); - } - - boolean isEmpty(); - - int size(); - - Option get(K key); - - V getOrElse(K key, V defaultValue); - - boolean containsKey(K key); - - HashArrayMappedTrie put(K key, V value); - - HashArrayMappedTrie remove(K key); - - @Override - Iterator> iterator(); - - /** - * Provide unboxed access to the keys in the trie. - */ - Iterator keysIterator(); - - /** - * Provide unboxed access to the values in the trie. - */ - Iterator valuesIterator(); -} - -interface HashArrayMappedTrieModule { - - enum Action { - PUT, REMOVE - } - - class LeafNodeIterator implements Iterator> { - - // buckets levels + leaf level = (Integer.SIZE / AbstractNode.SIZE + 1) + 1 - private final static int MAX_LEVELS = Integer.SIZE / AbstractNode.SIZE + 2; - - private final int total; - private final Object[] nodes = new Object[MAX_LEVELS]; - private final int[] indexes = new int[MAX_LEVELS]; - - private int level; - private int ptr = 0; - - LeafNodeIterator(AbstractNode root) { - total = root.size(); - level = downstairs(nodes, indexes, root, 0); - } - - @Override - public boolean hasNext() { - return ptr < total; - } - - @SuppressWarnings("unchecked") - @Override - public LeafNode next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - Object node = nodes[level]; - while (!(node instanceof LeafNode)) { - node = findNextLeaf(); - } - ptr++; - if (node instanceof LeafList) { - final LeafList leaf = (LeafList) node; - nodes[level] = leaf.tail; - return leaf; - } else { - nodes[level] = EmptyNode.instance(); - return (LeafSingleton) node; - } - } - - @SuppressWarnings("unchecked") - private Object findNextLeaf() { - AbstractNode node = null; - while (level > 0) { - level--; - indexes[level]++; - node = getChild((AbstractNode) nodes[level], indexes[level]); - if (node != null) { - break; - } - } - level = downstairs(nodes, indexes, node, level + 1); - return nodes[level]; - } - - private static int downstairs(Object[] nodes, int[] indexes, AbstractNode root, int level) { - while (true) { - nodes[level] = root; - indexes[level] = 0; - root = getChild(root, 0); - if (root == null) { - break; - } else { - level++; - } - } - return level; - } - - @SuppressWarnings("unchecked") - private static AbstractNode getChild(AbstractNode node, int index) { - if (node instanceof IndexedNode) { - final Object[] subNodes = ((IndexedNode) node).subNodes; - return index < subNodes.length ? (AbstractNode) subNodes[index] : null; - } else if (node instanceof ArrayNode) { - final ArrayNode arrayNode = (ArrayNode) node; - return index < AbstractNode.BUCKET_SIZE ? (AbstractNode) arrayNode.subNodes[index] : null; - } - return null; - } - } - - /** - * An abstract base class for nodes of a HAMT. - * - * @param Key type - * @param Value type - */ - abstract class AbstractNode implements HashArrayMappedTrie { - - static final int SIZE = 5; - static final int BUCKET_SIZE = 1 << SIZE; - static final int MAX_INDEX_NODE = BUCKET_SIZE >> 1; - static final int MIN_ARRAY_NODE = BUCKET_SIZE >> 2; - - static int hashFragment(int shift, int hash) { - return (hash >>> shift) & (BUCKET_SIZE - 1); - } - - static int toBitmap(int hash) { - return 1 << hash; - } - - static int fromBitmap(int bitmap, int bit) { - return bitCount(bitmap & (bit - 1)); - } - - static Object[] update(Object[] arr, int index, Object newElement) { - final Object[] newArr = copyOf(arr, arr.length); - newArr[index] = newElement; - return newArr; - } - - static Object[] remove(Object[] arr, int index) { - final Object[] newArr = new Object[arr.length - 1]; - System.arraycopy(arr, 0, newArr, 0, index); - System.arraycopy(arr, index + 1, newArr, index, arr.length - index - 1); - return newArr; - } - - static Object[] insert(Object[] arr, int index, Object newElem) { - final Object[] newArr = new Object[arr.length + 1]; - System.arraycopy(arr, 0, newArr, 0, index); - newArr[index] = newElem; - System.arraycopy(arr, index, newArr, index + 1, arr.length - index); - return newArr; - } - - abstract Option lookup(int shift, int keyHash, K key); - - abstract V lookup(int shift, int keyHash, K key, V defaultValue); - - abstract AbstractNode modify(int shift, int keyHash, K key, V value, Action action); - - Iterator> nodes() { - return new LeafNodeIterator<>(this); - } - - @Override - public Iterator> iterator() { - return nodes().map(node -> Tuple.of(node.key(), node.value())); - } - - @Override - public Iterator keysIterator() { - return nodes().map(LeafNode::key); - } - - @Override - public Iterator valuesIterator() { - return nodes().map(LeafNode::value); - } - - @Override - public Option get(K key) { - return lookup(0, Objects.hashCode(key), key); - } - - @Override - public V getOrElse(K key, V defaultValue) { - return lookup(0, Objects.hashCode(key), key, defaultValue); - } - - @Override - public boolean containsKey(K key) { - return get(key).isDefined(); - } - - @Override - public HashArrayMappedTrie put(K key, V value) { - return modify(0, Objects.hashCode(key), key, value, PUT); - } - - @Override - public HashArrayMappedTrie remove(K key) { - return modify(0, Objects.hashCode(key), key, null, REMOVE); - } - - @Override - public final String toString() { - return iterator().map(t -> t._1 + " -> " + t._2).mkString("HashArrayMappedTrie(", ", ", ")"); - } - } - - /** - * The empty node. - * - * @param Key type - * @param Value type - */ - final class EmptyNode extends AbstractNode implements Serializable { - - private static final long serialVersionUID = 1L; - - private static final EmptyNode INSTANCE = new EmptyNode<>(); - - private EmptyNode() { - } - - @SuppressWarnings("unchecked") - static EmptyNode instance() { - return (EmptyNode) INSTANCE; - } - - @Override - Option lookup(int shift, int keyHash, K key) { - return Option.none(); - } - - @Override - V lookup(int shift, int keyHash, K key, V defaultValue) { - return defaultValue; - } - - @Override - AbstractNode modify(int shift, int keyHash, K key, V value, Action action) { - return (action == REMOVE) ? this : new LeafSingleton<>(keyHash, key, value); - } - - @Override - public boolean isEmpty() { - return true; - } - - @Override - public int size() { - return 0; - } - - @Override - public Iterator> nodes() { - return Iterator.empty(); - } - - /** - * Instance control for object serialization. - * - * @return The singleton instance of EmptyNode. - * @see Serializable - */ - private Object readResolve() { - return INSTANCE; - } - } - - /** - * Representation of a HAMT leaf. - * - * @param Key type - * @param Value type - */ - abstract class LeafNode extends AbstractNode { - - abstract K key(); - - abstract V value(); - - abstract int hash(); - - static AbstractNode mergeLeaves(int shift, LeafNode leaf1, LeafSingleton leaf2) { - final int h1 = leaf1.hash(); - final int h2 = leaf2.hash(); - if (h1 == h2) { - return new LeafList<>(h1, leaf2.key(), leaf2.value(), leaf1); - } - final int subH1 = hashFragment(shift, h1); - final int subH2 = hashFragment(shift, h2); - final int newBitmap = toBitmap(subH1) | toBitmap(subH2); - if (subH1 == subH2) { - final AbstractNode newLeaves = mergeLeaves(shift + SIZE, leaf1, leaf2); - return new IndexedNode<>(newBitmap, newLeaves.size(), new Object[] { newLeaves }); - } else { - return new IndexedNode<>(newBitmap, leaf1.size() + leaf2.size(), - subH1 < subH2 ? new Object[] { leaf1, leaf2 } : new Object[] { leaf2, leaf1 }); - } - } - - @Override - public boolean isEmpty() { - return false; - } - } - - /** - * Representation of a HAMT leaf node with single element. - * - * @param Key type - * @param Value type - */ - final class LeafSingleton extends LeafNode implements Serializable { - - private static final long serialVersionUID = 1L; - - private final int hash; - @SuppressWarnings("serial") // Conditionally serializable - private final K key; - @SuppressWarnings("serial") // Conditionally serializable - private final V value; - - LeafSingleton(int hash, K key, V value) { - this.hash = hash; - this.key = key; - this.value = value; - } - - private boolean equals(int keyHash, K key) { - return keyHash == hash && Objects.equals(key, this.key); - } - - @Override - Option lookup(int shift, int keyHash, K key) { - return Option.when(equals(keyHash, key), value); - } - - @Override - V lookup(int shift, int keyHash, K key, V defaultValue) { - return equals(keyHash, key) ? value : defaultValue; - } - - @Override - AbstractNode modify(int shift, int keyHash, K key, V value, Action action) { - if (keyHash == hash && Objects.equals(key, this.key)) { - return (action == REMOVE) ? EmptyNode.instance() : new LeafSingleton<>(hash, key, value); - } else { - return (action == REMOVE) ? this : mergeLeaves(shift, this, new LeafSingleton<>(keyHash, key, value)); - } - } - - @Override - public int size() { - return 1; - } - - @Override - public Iterator> nodes() { - return Iterator.of(this); - } - - @Override - int hash() { - return hash; - } - - @Override - K key() { - return key; - } - - @Override - V value() { - return value; - } - } - - /** - * Representation of a HAMT leaf node with more than one element. - * - * @param Key type - * @param Value type - */ - final class LeafList extends LeafNode implements Serializable { - - private static final long serialVersionUID = 1L; - - private final int hash; - @SuppressWarnings("serial") // Conditionally serializable - private final K key; - @SuppressWarnings("serial") // Conditionally serializable - private final V value; - private final int size; - private final LeafNode tail; - - LeafList(int hash, K key, V value, LeafNode tail) { - this.hash = hash; - this.key = key; - this.value = value; - this.size = 1 + tail.size(); - this.tail = tail; - } - - @Override - Option lookup(int shift, int keyHash, K key) { - if (hash != keyHash) { - return Option.none(); - } - return nodes().find(node -> Objects.equals(node.key(), key)).map(LeafNode::value); - } - - @Override - V lookup(int shift, int keyHash, K key, V defaultValue) { - if (hash != keyHash) { - return defaultValue; - } - V result = defaultValue; - final Iterator> iterator = nodes(); - while (iterator.hasNext()) { - final LeafNode node = iterator.next(); - if (Objects.equals(node.key(), key)) { - result = node.value(); - break; - } - } - return result; - } - - @Override - AbstractNode modify(int shift, int keyHash, K key, V value, Action action) { - if (keyHash == hash) { - final AbstractNode filtered = removeElement(key); - if (action == REMOVE) { - return filtered; - } else { - return new LeafList<>(hash, key, value, (LeafNode) filtered); - } - } else { - return (action == REMOVE) ? this : mergeLeaves(shift, this, new LeafSingleton<>(keyHash, key, value)); - } - } - - private static AbstractNode mergeNodes(LeafNode leaf1, LeafNode leaf2) { - if (leaf2 == null) { - return leaf1; - } - if (leaf1 instanceof LeafSingleton) { - return new LeafList<>(leaf1.hash(), leaf1.key(), leaf1.value(), leaf2); - } - if (leaf2 instanceof LeafSingleton) { - return new LeafList<>(leaf2.hash(), leaf2.key(), leaf2.value(), leaf1); - } - LeafNode result = leaf1; - LeafNode tail = leaf2; - while (tail instanceof LeafList) { - final LeafList list = (LeafList) tail; - result = new LeafList<>(list.hash, list.key, list.value, result); - tail = list.tail; - } - return new LeafList<>(tail.hash(), tail.key(), tail.value(), result); - } - - private AbstractNode removeElement(K k) { - if (Objects.equals(k, this.key)) { - return tail; - } - LeafNode leaf1 = new LeafSingleton<>(hash, key, value); - LeafNode leaf2 = tail; - boolean found = false; - while (!found && leaf2 != null) { - if (Objects.equals(k, leaf2.key())) { - found = true; - } else { - leaf1 = new LeafList<>(leaf2.hash(), leaf2.key(), leaf2.value(), leaf1); - } - leaf2 = leaf2 instanceof LeafList ? ((LeafList) leaf2).tail : null; - } - return mergeNodes(leaf1, leaf2); - } - - @Override - public int size() { - return size; - } - - @Override - public Iterator> nodes() { - return new Iterator>() { - LeafNode node = LeafList.this; - - @Override - public boolean hasNext() { - return node != null; - } - - @Override - public LeafNode next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - final LeafNode result = node; - if (node instanceof LeafSingleton) { - node = null; - } else { - node = ((LeafList) node).tail; - } - return result; - } - }; - } - - @Override - int hash() { - return hash; - } - - @Override - K key() { - return key; - } - - @Override - V value() { - return value; - } - } - - /** - * Representation of a HAMT indexed node. - * - * @param Key type - * @param Value type - */ - final class IndexedNode extends AbstractNode implements Serializable { - - private static final long serialVersionUID = 1L; - - private final int bitmap; - private final int size; - @SuppressWarnings("serial") // Conditionally serializable - private final Object[] subNodes; - - IndexedNode(int bitmap, int size, Object[] subNodes) { - this.bitmap = bitmap; - this.size = size; - this.subNodes = subNodes; - } - - @SuppressWarnings("unchecked") - @Override - Option lookup(int shift, int keyHash, K key) { - final int frag = hashFragment(shift, keyHash); - final int bit = toBitmap(frag); - if ((bitmap & bit) != 0) { - final AbstractNode n = (AbstractNode) subNodes[fromBitmap(bitmap, bit)]; - return n.lookup(shift + SIZE, keyHash, key); - } else { - return Option.none(); - } - } - - @SuppressWarnings("unchecked") - @Override - V lookup(int shift, int keyHash, K key, V defaultValue) { - final int frag = hashFragment(shift, keyHash); - final int bit = toBitmap(frag); - if ((bitmap & bit) != 0) { - final AbstractNode n = (AbstractNode) subNodes[fromBitmap(bitmap, bit)]; - return n.lookup(shift + SIZE, keyHash, key, defaultValue); - } else { - return defaultValue; - } - } - - @SuppressWarnings("unchecked") - @Override - AbstractNode modify(int shift, int keyHash, K key, V value, Action action) { - final int frag = hashFragment(shift, keyHash); - final int bit = toBitmap(frag); - final int index = fromBitmap(bitmap, bit); - final int mask = bitmap; - final boolean exists = (mask & bit) != 0; - final AbstractNode atIndx = exists ? (AbstractNode) subNodes[index] : null; - final AbstractNode child = - exists ? atIndx.modify(shift + SIZE, keyHash, key, value, action) - : EmptyNode. instance().modify(shift + SIZE, keyHash, key, value, action); - final boolean removed = exists && child.isEmpty(); - final boolean added = !exists && !child.isEmpty(); - final int newBitmap = removed ? mask & ~bit : added ? mask | bit : mask; - if (newBitmap == 0) { - return EmptyNode.instance(); - } else if (removed) { - if (subNodes.length <= 2 && subNodes[index ^ 1] instanceof LeafNode) { - return (AbstractNode) subNodes[index ^ 1]; // collapse - } else { - return new IndexedNode<>(newBitmap, size - atIndx.size(), remove(subNodes, index)); - } - } else if (added) { - if (subNodes.length >= MAX_INDEX_NODE) { - return expand(frag, child, mask, subNodes); - } else { - return new IndexedNode<>(newBitmap, size + child.size(), insert(subNodes, index, child)); - } - } else { - if (!exists) { - return this; - } else { - return new IndexedNode<>(newBitmap, size - atIndx.size() + child.size(), update(subNodes, index, child)); - } - } - } - - private ArrayNode expand(int frag, AbstractNode child, int mask, Object[] subNodes) { - int bit = mask; - int count = 0; - int ptr = 0; - final Object[] arr = new Object[BUCKET_SIZE]; - for (int i = 0; i < BUCKET_SIZE; i++) { - if ((bit & 1) != 0) { - arr[i] = subNodes[ptr++]; - count++; - } else if (i == frag) { - arr[i] = child; - count++; - } else { - arr[i] = EmptyNode.instance(); - } - bit = bit >>> 1; - } - return new ArrayNode<>(count, size + child.size(), arr); - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public int size() { - return size; - } - } - - /** - * Representation of a HAMT array node. - * - * @param Key type - * @param Value type - */ - final class ArrayNode extends AbstractNode implements Serializable { - - private static final long serialVersionUID = 1L; - - @SuppressWarnings("serial") // Conditionally serializable - private final Object[] subNodes; - private final int count; - private final int size; - - ArrayNode(int count, int size, Object[] subNodes) { - this.subNodes = subNodes; - this.count = count; - this.size = size; - } - - @SuppressWarnings("unchecked") - @Override - Option lookup(int shift, int keyHash, K key) { - final int frag = hashFragment(shift, keyHash); - final AbstractNode child = (AbstractNode) subNodes[frag]; - return child.lookup(shift + SIZE, keyHash, key); - } - - @SuppressWarnings("unchecked") - @Override - V lookup(int shift, int keyHash, K key, V defaultValue) { - final int frag = hashFragment(shift, keyHash); - final AbstractNode child = (AbstractNode) subNodes[frag]; - return child.lookup(shift + SIZE, keyHash, key, defaultValue); - } - - @SuppressWarnings("unchecked") - @Override - AbstractNode modify(int shift, int keyHash, K key, V value, Action action) { - final int frag = hashFragment(shift, keyHash); - final AbstractNode child = (AbstractNode) subNodes[frag]; - final AbstractNode newChild = child.modify(shift + SIZE, keyHash, key, value, action); - if (child.isEmpty() && !newChild.isEmpty()) { - return new ArrayNode<>(count + 1, size + newChild.size(), update(subNodes, frag, newChild)); - } else if (!child.isEmpty() && newChild.isEmpty()) { - if (count - 1 <= MIN_ARRAY_NODE) { - return pack(frag, subNodes); - } else { - return new ArrayNode<>(count - 1, size - child.size(), update(subNodes, frag, EmptyNode.instance())); - } - } else { - return new ArrayNode<>(count, size - child.size() + newChild.size(), update(subNodes, frag, newChild)); - } - } - - @SuppressWarnings("unchecked") - private IndexedNode pack(int idx, Object[] elements) { - final Object[] arr = new Object[count - 1]; - int bitmap = 0; - int size = 0; - int ptr = 0; - for (int i = 0; i < BUCKET_SIZE; i++) { - final AbstractNode elem = (AbstractNode) elements[i]; - if (i != idx && !elem.isEmpty()) { - size += elem.size(); - arr[ptr++] = elem; - bitmap = bitmap | (1 << i); - } - } - return new IndexedNode<>(bitmap, size, arr); - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public int size() { - return size; - } - } -} diff --git a/src/main/java/io/vavr/collection/HashMap.java b/src/main/java/io/vavr/collection/HashMap.java index 0bbd4ccf9..048254ea5 100644 --- a/src/main/java/io/vavr/collection/HashMap.java +++ b/src/main/java/io/vavr/collection/HashMap.java @@ -4,7 +4,7 @@ * * The MIT License (MIT) * - * Copyright 2024 Vavr, https://vavr.io + * Copyright 2023 Vavr, https://vavr.io * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -30,28 +30,110 @@ import io.vavr.Tuple2; import io.vavr.control.Option; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; import java.io.Serializable; +import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.function.*; +import java.util.Spliterator; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collector; +import static io.vavr.collection.ChampTrie.BitmapIndexedNode.emptyNode; + /** - * An immutable {@code HashMap} implementation based on a - * Hash array mapped trie (HAMT). + * Implements an immutable map using a Compressed Hash-Array Mapped Prefix-tree + * (CHAMP). + *

+ * Features: + *

    + *
  • supports up to 230 entries
  • + *
  • allows null keys and null values
  • + *
  • is immutable
  • + *
  • is thread-safe
  • + *
  • does not guarantee a specific iteration order
  • + *
+ *

+ * Performance characteristics: + *

    + *
  • put: O(1)
  • + *
  • remove: O(1)
  • + *
  • containsKey: O(1)
  • + *
  • toMutable: O(1) + O(log N) distributed across subsequent updates in the mutable copy
  • + *
  • clone: O(1)
  • + *
  • iterator.next(): O(1)
  • + *
+ *

+ * Implementation details: + *

+ * This map performs read and write operations of single elements in O(1) time, + * and in O(1) space. + *

+ * The CHAMP trie contains nodes that may be shared with other maps. + *

+ * If a write operation is performed on a node, then this map creates a + * copy of the node and of all parent nodes up to the root (copy-path-on-write). + * Since the CHAMP trie has a fixed maximal height, the cost is O(1). + *

+ * All operations on this map can be performed concurrently, without a need for + * synchronisation. + *

+ * References: + *

+ * Portions of the code in this class have been derived from 'The Capsule Hash Trie Collections Library', and from + * 'JHotDraw 8'. + *

+ *
Michael J. Steindorfer (2017). + * Efficient Immutable Collections.
+ *
michael.steindorfer.name + *
+ *
The Capsule Hash Trie Collections Library. + *
Copyright (c) Michael Steindorfer. BSD-2-Clause License
+ *
github.com + *
+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com + *
+ *
+ * + * @param the key type + * @param the value type */ -public final class HashMap implements Map, Serializable { +public final class HashMap implements Map, Serializable { + private final ChampTrie.BitmapIndexedNode> root; private static final long serialVersionUID = 1L; - private static final HashMap EMPTY = new HashMap<>(HashArrayMappedTrie.empty()); + private static final HashMap EMPTY = new HashMap<>(emptyNode(), 0); - private final HashArrayMappedTrie trie; + /** + * We do not guarantee an iteration order. Make sure that nobody accidentally relies on it. + *

+ * XXX HashSetTest requires a specific iteration order of HashSet! Therefore, we can not use SALT here. + */ + static final int SALT = 0;//new java.util.Random().nextInt(); + /** + * The size of the map. + */ + final int size; - private HashMap(HashArrayMappedTrie trie) { - this.trie = trie; + HashMap(ChampTrie.BitmapIndexedNode> root, int size) { + this.root=root; + this.size = size; } /** @@ -71,9 +153,9 @@ public static Collector, ArrayList>, HashMap The key type - * @param The value type - * @param Initial {@link java.util.stream.Stream} elements type + * @param The key type + * @param The value type + * @param Initial {@link java.util.stream.Stream} elements type * @return A {@link HashMap} Collector. */ public static Collector, HashMap> collector(Function keyMapper) { @@ -85,11 +167,11 @@ public static Collector, HashMap> coll * Returns a {@link java.util.stream.Collector} which may be used in conjunction with * {@link java.util.stream.Stream#collect(java.util.stream.Collector)} to obtain a {@link HashMap}. * - * @param keyMapper The key mapper + * @param keyMapper The key mapper * @param valueMapper The value mapper - * @param The key type - * @param The value type - * @param Initial {@link java.util.stream.Stream} elements type + * @param The key type + * @param The value type + * @param Initial {@link java.util.stream.Stream} elements type * @return A {@link HashMap} Collector. */ public static Collector, HashMap> collector( @@ -129,7 +211,7 @@ public static HashMap narrow(HashMap hash * @return A new Map containing the given entry */ public static HashMap of(Tuple2 entry) { - return new HashMap<>(HashArrayMappedTrie. empty().put(entry._1, entry._2)); + return HashMap.empty().put(entry._1, entry._2); } /** @@ -141,12 +223,7 @@ public static HashMap of(Tuple2 entry) { * @return A new Map containing the given map */ public static HashMap ofAll(java.util.Map map) { - Objects.requireNonNull(map, "map is null"); - HashArrayMappedTrie tree = HashArrayMappedTrie.empty(); - for (java.util.Map.Entry entry : map.entrySet()) { - tree = tree.put(entry.getKey(), entry.getValue()); - } - return wrap(tree); + return HashMap.empty().putAllEntries(map.entrySet()); } /** @@ -161,8 +238,8 @@ public static HashMap ofAll(java.util.Map * @return A new Map */ public static HashMap ofAll(java.util.stream.Stream stream, - Function keyMapper, - Function valueMapper) { + Function keyMapper, + Function valueMapper) { return Maps.ofStream(empty(), stream, keyMapper, valueMapper); } @@ -177,7 +254,7 @@ public static HashMap ofAll(java.util.stream.Stream * @return A new Map */ public static HashMap ofAll(java.util.stream.Stream stream, - Function> entryMapper) { + Function> entryMapper) { return Maps.ofStream(empty(), stream, entryMapper); } @@ -191,7 +268,7 @@ public static HashMap ofAll(java.util.stream.Stream * @return A new Map containing the given entry */ public static HashMap of(K key, V value) { - return new HashMap<>(HashArrayMappedTrie. empty().put(key, value)); + return HashMap.empty().put(key, value); } /** @@ -443,13 +520,10 @@ public static HashMap fill(int n, Supplier HashMap ofEntries(java.util.Map.Entry... entries) { Objects.requireNonNull(entries, "entries is null"); - HashArrayMappedTrie trie = HashArrayMappedTrie.empty(); - for (java.util.Map.Entry entry : entries) { - trie = trie.put(entry.getKey(), entry.getValue()); - } - return wrap(trie); + return HashMap.empty().putAllEntries(Arrays.asList(entries)); } /** @@ -461,13 +535,10 @@ public static HashMap ofEntries(java.util.Map.Entry HashMap ofEntries(Tuple2... entries) { Objects.requireNonNull(entries, "entries is null"); - HashArrayMappedTrie trie = HashArrayMappedTrie.empty(); - for (Tuple2 entry : entries) { - trie = trie.put(entry._1, entry._2); - } - return wrap(trie); + return HashMap.empty().putAllTuples(Arrays.asList(entries)); } /** @@ -478,18 +549,9 @@ public static HashMap ofEntries(Tuple2... * @param The value type * @return A new Map containing the given entries */ - @SuppressWarnings("unchecked") public static HashMap ofEntries(Iterable> entries) { Objects.requireNonNull(entries, "entries is null"); - if (entries instanceof HashMap) { - return (HashMap) entries; - } else { - HashArrayMappedTrie trie = HashArrayMappedTrie.empty(); - for (Tuple2 entry : entries) { - trie = trie.put(entry._1, entry._2); - } - return trie.isEmpty() ? empty() : wrap(trie); - } + return HashMap.empty().putAllTuples(entries); } @Override @@ -512,7 +574,8 @@ public Tuple2, HashMap> computeIfPresent(K key, BiFunction(key, null), Objects.hashCode(key), 0, + HashMap::keyEquals) != ChampTrie.Node.NO_DATA; } @Override @@ -552,48 +615,56 @@ public HashMap dropWhile(Predicate> predicate) { @Override public HashMap filter(BiPredicate predicate) { - return Maps.filter(this, this::createFromEntries, predicate); + TransientHashMap t = toTransient(); + t.filterAll(e->predicate.test(e.getKey(),e.getValue())); + return t.root==this.root?this: t.toImmutable(); } @Override public HashMap filterNot(BiPredicate predicate) { - return Maps.filterNot(this, this::createFromEntries, predicate); + return filter(predicate.negate()); } @Override public HashMap filter(Predicate> predicate) { - return Maps.filter(this, this::createFromEntries, predicate); + TransientHashMap t = toTransient(); + t.filterAll(e->predicate.test(new Tuple2<>(e.getKey(),e.getValue()))); + return t.root==this.root?this: t.toImmutable(); } @Override public HashMap filterNot(Predicate> predicate) { - return Maps.filterNot(this, this::createFromEntries, predicate); + return filter(predicate.negate()); } @Override public HashMap filterKeys(Predicate predicate) { - return Maps.filterKeys(this, this::createFromEntries, predicate); + TransientHashMap t = toTransient(); + t.filterAll(e->predicate.test(e.getKey())); + return t.root==this.root?this: t.toImmutable(); } @Override public HashMap filterNotKeys(Predicate predicate) { - return Maps.filterNotKeys(this, this::createFromEntries, predicate); + return filterKeys(predicate.negate()); } @Override public HashMap filterValues(Predicate predicate) { - return Maps.filterValues(this, this::createFromEntries, predicate); + TransientHashMap t = toTransient(); + t.filterAll(e->predicate.test(e.getValue())); + return t.root==this.root?this: t.toImmutable(); } @Override public HashMap filterNotValues(Predicate predicate) { - return Maps.filterNotValues(this, this::createFromEntries, predicate); + return filterValues(predicate.negate()); } @Override public HashMap flatMap(BiFunction>> mapper) { Objects.requireNonNull(mapper, "mapper is null"); - return foldLeft(HashMap. empty(), (acc, entry) -> { + return foldLeft(HashMap.empty(), (acc, entry) -> { for (Tuple2 mappedEntry : mapper.apply(entry._1, entry._2)) { acc = acc.put(mappedEntry); } @@ -602,13 +673,17 @@ public HashMap flatMap(BiFunction get(K key) { - return trie.get(key); + Object result = root.find(new AbstractMap.SimpleImmutableEntry<>(key, null), Objects.hashCode(key), 0, HashMap::keyEquals); + return result == ChampTrie.Node.NO_DATA || result == null + ? Option.none() + : Option.some(((AbstractMap.SimpleImmutableEntry) result).getValue()); } @Override public V getOrElse(K key, V defaultValue) { - return trie.getOrElse(key, defaultValue); + return get(key).getOrElse(defaultValue); } @Override @@ -625,18 +700,21 @@ public Iterator> grouped(int size) { public Tuple2 head() { if (isEmpty()) { throw new NoSuchElementException("head of empty HashMap"); - } else { - return iterator().next(); } + AbstractMap.SimpleImmutableEntry entry = ChampTrie.Node.getFirst(root); + return new Tuple2<>(entry.getKey(), entry.getValue()); } + /** + * XXX We return tail() here. I believe that this is correct. + * See identical code in {@link HashSet#init} + */ @Override public HashMap init() { - if (trie.isEmpty()) { - throw new UnsupportedOperationException("init of empty HashMap"); - } else { - return remove(last()._1); + if (isEmpty()) { + throw new UnsupportedOperationException("tail of empty HashMap"); } + return remove(last()._1); } @Override @@ -656,7 +734,7 @@ public boolean isAsync() { @Override public boolean isEmpty() { - return trie.isEmpty(); + return size == 0; } /** @@ -671,7 +749,7 @@ public boolean isLazy() { @Override public Iterator> iterator() { - return trie.iterator(); + return new ChampIteration.IteratorFacade<>(spliterator()); } @Override @@ -681,12 +759,21 @@ public Set keySet() { @Override public Iterator keysIterator() { - return trie.keysIterator(); + return new ChampIteration.IteratorFacade<>(keysSpliterator()); + } + + private Spliterator keysSpliterator() { + return new ChampIteration.ChampSpliterator<>(root, AbstractMap.SimpleImmutableEntry::getKey, + Spliterator.DISTINCT | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.IMMUTABLE, size); } @Override public Tuple2 last() { - return Collections.last(this); + if (isEmpty()) { + throw new NoSuchElementException("last of empty HashMap"); + } + AbstractMap.SimpleImmutableEntry entry = ChampTrie.Node.getLast(root); + return new Tuple2<>(entry.getKey(), entry.getValue()); } @Override @@ -714,12 +801,12 @@ public HashMap mapValues(Function valueMapp @Override public HashMap merge(Map that) { - return Maps.merge(this, this::createFromEntries, that); + return putAllTuples(that); } @Override public HashMap merge(Map that, - BiFunction collisionResolution) { + BiFunction collisionResolution) { return Maps.merge(this, this::createFromEntries, that, collisionResolution); } @@ -750,7 +837,17 @@ public HashMap put(K key, U value, BiFunction put(K key, V value) { - return new HashMap<>(trie.put(key, value)); + final ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent<>(); + final ChampTrie.BitmapIndexedNode> newRootNode = root.put(null, new AbstractMap.SimpleImmutableEntry<>(key, value), + Objects.hashCode(key), 0, details, + HashMap::updateWithNewKey, HashMap::keyEquals, HashMap::entryKeyHash); + if (details.isModified()) { + if (details.isReplaced()) { + return new HashMap<>(newRootNode, size); + } + return new HashMap<>(newRootNode, size + 1); + } + return this; } @Override @@ -760,31 +857,45 @@ public HashMap put(Tuple2 entry) { @Override public HashMap put(Tuple2 entry, - BiFunction merge) { + BiFunction merge) { return Maps.put(this, entry, merge); } - @Override - public HashMap remove(K key) { - final HashArrayMappedTrie result = trie.remove(key); - return result.size() == trie.size() ? this : wrap(result); + private HashMap putAllEntries(Iterable> c) { + TransientHashMap t=toTransient(); + t.putAllEntries(c); + return t.root==this.root?this: t.toImmutable(); } - @Override - public HashMap removeAll(Iterable keys) { - Objects.requireNonNull(keys, "keys is null"); - HashArrayMappedTrie result = trie; - for (K key : keys) { - result = result.remove(key); + @SuppressWarnings("unchecked") + private HashMap putAllTuples(Iterable> c) { + if (isEmpty()&& c instanceof HashMap){ + HashMap that = (HashMap) c; + return (HashMap)that; } + TransientHashMap t=toTransient(); + t.putAllTuples(c); + return t.root==this.root?this: t.toImmutable(); + } - if (result.isEmpty()) { - return empty(); - } else if (result.size() == trie.size()) { - return this; - } else { - return wrap(result); + @Override + public HashMap remove(K key) { + final int keyHash = Objects.hashCode(key); + final ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent<>(); + final ChampTrie.BitmapIndexedNode> newRootNode = + root.remove(null, new AbstractMap.SimpleImmutableEntry<>(key, null), keyHash, 0, details, + HashMap::keyEquals); + if (details.isModified()) { + return new HashMap<>(newRootNode, size - 1); } + return this; + } + + @Override + public HashMap removeAll(Iterable c) { + TransientHashMap t=toTransient(); + t.removeAll(c); + return t.root==this.root?this: t.toImmutable(); } @Override @@ -814,14 +925,9 @@ public HashMap replaceAll(BiFunction fu @Override public HashMap retainAll(Iterable> elements) { - Objects.requireNonNull(elements, "elements is null"); - HashArrayMappedTrie tree = HashArrayMappedTrie.empty(); - for (Tuple2 entry : elements) { - if (contains(entry)) { - tree = tree.put(entry._1, entry._2); - } - } - return wrap(tree); + TransientHashMap t=toTransient(); + t.retainAllTuples(elements); + return t.root==this.root?this: t.toImmutable(); } @Override @@ -833,7 +939,7 @@ public HashMap scan( @Override public int size() { - return trie.size(); + return size; } @Override @@ -856,13 +962,18 @@ public Tuple2, HashMap> span(Predicate> return Maps.span(this, this::createFromEntries, predicate); } + @Override + public Spliterator> spliterator() { + return new ChampIteration.ChampSpliterator<>(root, entry -> new Tuple2<>(entry.getKey(), entry.getValue()), + Spliterator.DISTINCT | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.IMMUTABLE, size); + } + @Override public HashMap tail() { - if (trie.isEmpty()) { + if (isEmpty()) { throw new UnsupportedOperationException("tail of empty HashMap"); - } else { - return remove(head()._1); } + return remove(head()._1); } @Override @@ -895,19 +1006,39 @@ public java.util.HashMap toJavaMap() { return toJavaMap(java.util.HashMap::new, t -> t); } + TransientHashMap toTransient() { + return new TransientHashMap<>(this); + } + @Override public Stream values() { - return trie.valuesIterator().toStream(); + return valuesIterator().toStream(); } @Override public Iterator valuesIterator() { - return trie.valuesIterator(); + return new ChampIteration.IteratorFacade<>(valuesSpliterator()); + } + + private Spliterator valuesSpliterator() { + return new ChampIteration.ChampSpliterator<>(root, AbstractMap.SimpleImmutableEntry::getValue, + Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.IMMUTABLE, size); } @Override - public boolean equals(Object o) { - return Collections.equals(this, o); + public boolean equals(final Object other) { + if (other == this) { + return true; + } + if (other == null) { + return false; + } + if (other instanceof HashMap) { + HashMap that = (HashMap) other; + return size == that.size &&root. equivalent(that.root); + } else { + return Collections.equals(this, other); + } } @Override @@ -929,13 +1060,265 @@ public String toString() { return mkString(stringPrefix() + "(", ", ", ")"); } - private static HashMap wrap(HashArrayMappedTrie trie) { - return trie.isEmpty() ? empty() : new HashMap<>(trie); - } - // We need this method to narrow the argument of `ofEntries`. // If this method is static with type args , the jdk fails to infer types at the call site. private HashMap createFromEntries(Iterable> tuples) { return HashMap.ofEntries(tuples); } + + static boolean keyEquals(AbstractMap.SimpleImmutableEntry a, AbstractMap.SimpleImmutableEntry b) { + return Objects.equals(a.getKey(), b.getKey()); + } + static int keyHash(Object e) { + return SALT ^ Objects.hashCode(e); + } + static int entryKeyHash(AbstractMap.SimpleImmutableEntry e) { + return SALT^Objects.hashCode(e.getKey()); + } + + static boolean entryKeyEquals(AbstractMap.SimpleImmutableEntry a, AbstractMap.SimpleImmutableEntry b) { + return Objects.equals(a.getKey(), b.getKey()); + } + + // FIXME This behavior is enforced by AbstractMapTest.shouldPutExistingKeyAndNonEqualValue().
+ // This behavior replaces the existing key with the new one if it has not the same identity.
+ // This behavior does not match the behavior of java.util.HashMap.put(). + // This behavior violates the contract of the map: we do create a new instance of the map, + // although it is equal to the previous instance. + static AbstractMap.SimpleImmutableEntry updateWithNewKey(AbstractMap.SimpleImmutableEntry oldv, AbstractMap.SimpleImmutableEntry newv) { + return Objects.equals(oldv.getValue(), newv.getValue()) + && oldv.getKey() == newv.getKey() + ? oldv + : newv; + } + + static AbstractMap.SimpleImmutableEntry updateEntry(AbstractMap.SimpleImmutableEntry oldv, AbstractMap.SimpleImmutableEntry newv) { + return Objects.equals(oldv.getValue(), newv.getValue()) ? oldv : newv; + } + + private Object writeReplace() throws ObjectStreamException { + return new SerializationProxy<>(this); + } + + /** + * A serialization proxy which, in this context, is used to deserialize immutable, linked Lists with final + * instance fields. + * + * @param The key type + * @param The value type + */ + // DEV NOTE: The serialization proxy pattern is not compatible with non-final, i.e. extendable, + // classes. Also, it may not be compatible with circular object graphs. + private static final class SerializationProxy implements Serializable { + + private static final long serialVersionUID = 1L; + + // the instance to be serialized/deserialized + private transient HashMap map; + + /** + * Constructor for the case of serialization, called by {@link HashMap#writeReplace()}. + *

+ * The constructor of a SerializationProxy takes an argument that concisely represents the logical state of + * an instance of the enclosing class. + * + * @param map a map + */ + SerializationProxy(HashMap map) { + this.map = map; + } + + /** + * Write an object to a serialization stream. + * + * @param s An object serialization stream. + * @throws java.io.IOException If an error occurs writing to the stream. + */ + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + s.writeInt(map.size()); + for (Tuple2 e : map) { + s.writeObject(e._1); + s.writeObject(e._2); + } + } + + /** + * Read an object from a deserialization stream. + * + * @param s An object deserialization stream. + * @throws ClassNotFoundException If the object's class read from the stream cannot be found. + * @throws InvalidObjectException If the stream contains no list elements. + * @throws IOException If an error occurs reading from the stream. + */ + @SuppressWarnings("unchecked") + private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException { + s.defaultReadObject(); + final int size = s.readInt(); + if (size < 0) { + throw new InvalidObjectException("No elements"); + } + ChampTrie.IdentityObject owner = new ChampTrie.IdentityObject(); + ChampTrie.BitmapIndexedNode> newRoot = emptyNode(); + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent<>(); + int newSize = 0; + for (int i = 0; i < size; i++) { + final K key = (K) s.readObject(); + final V value = (V) s.readObject(); + int keyHash = Objects.hashCode(key); + newRoot = newRoot.put(owner, new AbstractMap.SimpleImmutableEntry(key, value), keyHash, 0, details, HashMap::updateEntry, Objects::equals, Objects::hashCode); + if (details.isModified()) newSize++; + } + map = newSize == 0 ? empty() : new HashMap<>(newRoot, newSize); + } + + /** + * {@code readResolve} method for the serialization proxy pattern. + *

+ * Returns a logically equivalent instance of the enclosing class. The presence of this method causes the + * serialization system to translate the serialization proxy back into an instance of the enclosing class + * upon deserialization. + * + * @return A deserialized instance of the enclosing class. + */ + private Object readResolve() { + return map; + } + } + + /** + * Supports efficient bulk-operations on a hash map through transience. + * + * @param the key type + * @param the value type + */ + static class TransientHashMap extends ChampTransience.ChampAbstractTransientMap> { + + TransientHashMap(HashMap m) { + root = m.root; + size = m.size; + } + + TransientHashMap() { + this(empty()); + } + + public V put(K key, V value) { + AbstractMap.SimpleImmutableEntry oldData = putEntry(key, value, false).getOldData(); + return oldData == null ? null : oldData.getValue(); + } + + boolean putAllEntries(Iterable> c) { + if (c == this) { + return false; + } + boolean modified = false; + for (java.util.Map.Entry e : c) { + V oldValue = put(e.getKey(), e.getValue()); + modified = modified || !Objects.equals(oldValue, e.getValue()); + } + return modified; + } + + @SuppressWarnings("unchecked") + boolean putAllTuples(Iterable> c) { + if (c instanceof HashMap) { + HashMap that = (HashMap) c; + ChampTrie.BulkChangeEvent bulkChange = new ChampTrie.BulkChangeEvent(); + ChampTrie.BitmapIndexedNode> newRootNode = root.putAll(makeOwner(), (ChampTrie.Node>) (ChampTrie.Node) that.root, 0, bulkChange, HashMap::updateEntry, HashMap::entryKeyEquals, + HashMap::entryKeyHash, new ChampTrie.ChangeEvent<>()); + if (bulkChange.inBoth == that.size() && !bulkChange.replaced) { + return false; + } + root = newRootNode; + size += that.size - bulkChange.inBoth; + modCount++; + return true; + } + return super.putAllTuples(c); + } + + ChampTrie.ChangeEvent> putEntry(final K key, V value, boolean moveToLast) { + int keyHash = keyHash(key); + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent<>(); + root = root.put(makeOwner(), new AbstractMap.SimpleImmutableEntry<>(key, value), keyHash, 0, details, + HashMap::updateEntry, + HashMap::entryKeyEquals, + HashMap::entryKeyHash); + if (details.isModified() && !details.isReplaced()) { + size += 1; + modCount++; + } + return details; + } + + + @SuppressWarnings("unchecked") + ChampTrie.ChangeEvent> removeKey(K key) { + int keyHash = keyHash(key); + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent<>(); + root = root.remove(makeOwner(), new AbstractMap.SimpleImmutableEntry<>(key, null), keyHash, 0, details, + HashMap::entryKeyEquals); + if (details.isModified()) { + size = size - 1; + modCount++; + } + return details; + } + + @Override + void clear() { + root = emptyNode(); + size = 0; + modCount++; + } + + public HashMap toImmutable() { + owner = null; + return isEmpty() + ? empty() + : new HashMap<>(root, size); + } + + @SuppressWarnings("unchecked") + boolean retainAllTuples(Iterable> c) { + if (isEmpty()) { + return false; + } + if (c instanceof Collection && ((Collection) c).isEmpty() + || c instanceof Traversable && ((Traversable) c).isEmpty()) { + clear(); + return true; + } + if (c instanceof HashMap) { + HashMap that = (HashMap) c; + ChampTrie.BulkChangeEvent bulkChange = new ChampTrie.BulkChangeEvent(); + ChampTrie.BitmapIndexedNode> newRootNode = root.retainAll(makeOwner(), + (ChampTrie.Node>) (ChampTrie.Node) that.root, + 0, bulkChange, HashMap::updateEntry, HashMap::entryKeyEquals, + HashMap::entryKeyHash, new ChampTrie.ChangeEvent<>()); + if (bulkChange.removed == 0) { + return false; + } + root = newRootNode; + size -= bulkChange.removed; + modCount++; + return true; + } + return super.retainAllTuples(c); + } + + @SuppressWarnings("unchecked") + boolean filterAll(Predicate> predicate) { + ChampTrie.BulkChangeEvent bulkChange = new ChampTrie.BulkChangeEvent(); + ChampTrie.BitmapIndexedNode> newRootNode = root.filterAll(makeOwner(), predicate, 0, bulkChange); + if (bulkChange.removed == 0) { + return false; + } + root = newRootNode; + size -= bulkChange.removed; + modCount++; + return true; + } + } } diff --git a/src/main/java/io/vavr/collection/HashSet.java b/src/main/java/io/vavr/collection/HashSet.java index 784591ab1..375b21394 100644 --- a/src/main/java/io/vavr/collection/HashSet.java +++ b/src/main/java/io/vavr/collection/HashSet.java @@ -4,7 +4,7 @@ * * The MIT License (MIT) * - * Copyright 2024 Vavr, https://vavr.io + * Copyright 2023 Vavr, https://vavr.io * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,33 +26,107 @@ */ package io.vavr.collection; -import io.vavr.*; +import io.vavr.PartialFunction; +import io.vavr.Tuple; +import io.vavr.Tuple2; import io.vavr.control.Option; -import java.io.*; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.function.*; +import java.util.Spliterator; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collector; /** - * An immutable {@code HashSet} implementation. + * Implements an immutable set using a Compressed Hash-Array Mapped Prefix-tree + * (CHAMP). + *

+ * Features: + *

    + *
  • supports up to 230 entries
  • + *
  • allows null elements
  • + *
  • is immutable
  • + *
  • is thread-safe
  • + *
  • does not guarantee a specific iteration order
  • + *
+ *

+ * Performance characteristics: + *

    + *
  • add: O(1)
  • + *
  • remove: O(1)
  • + *
  • contains: O(1)
  • + *
  • toMutable: O(1) + O(log N) distributed across subsequent updates in the mutable copy
  • + *
  • clone: O(1)
  • + *
  • iterator.next(): O(1)
  • + *
+ *

+ * Implementation details: + *

+ * This set performs read and write operations of single elements in O(1) time, + * and in O(1) space. + *

+ * The CHAMP trie contains nodes that may be shared with other sets. + *

+ * If a write operation is performed on a node, then this set creates a + * copy of the node and of all parent nodes up to the root (copy-path-on-write). + * Since the CHAMP trie has a fixed maximal height, the cost is O(1). + *

+ * References: + *

+ * Portions of the code in this class have been derived from 'The Capsule Hash Trie Collections Library', and from + * 'JHotDraw 8'. + *

+ *
Michael J. Steindorfer (2017). + * Efficient Immutable Collections.
+ *
michael.steindorfer.name + *
+ *
The Capsule Hash Trie Collections Library. + *
Copyright (c) Michael Steindorfer. BSD-2-Clause License
+ *
github.com + *
+ *
JHotDraw 8. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com + *
+ *
* - * @param Component type + * @param the element type */ @SuppressWarnings("deprecation") public final class HashSet implements Set, Serializable { private static final long serialVersionUID = 1L; - private static final HashSet EMPTY = new HashSet<>(HashArrayMappedTrie.empty()); + private static final HashSet EMPTY = new HashSet<>(ChampTrie.BitmapIndexedNode.emptyNode(), 0); + private final ChampTrie.BitmapIndexedNode root; + /** + * The size of the set. + */ + final int size; - private final HashArrayMappedTrie tree; + /** + * We do not guarantee an iteration order. Make sure that nobody accidentally relies on it. + *

+ * XXX HashSetTest requires a specific iteration order of HashSet! Therefore, we can not use SALT here. + */ + static final int SALT = 0;//new Random().nextInt(); - private HashSet(HashArrayMappedTrie tree) { - this.tree = tree; + HashSet(ChampTrie.BitmapIndexedNode root, int size) { + this.root = root; + this.size = size; } @SuppressWarnings("unchecked") @@ -93,7 +167,7 @@ public static HashSet narrow(HashSet hashSet) { * @return A new HashSet instance containing the given element */ public static HashSet of(T element) { - return HashSet. empty().add(element); + return HashSet.empty().add(element); } /** @@ -107,13 +181,10 @@ public static HashSet of(T element) { * @throws NullPointerException if {@code elements} is null */ @SafeVarargs + @SuppressWarnings("varargs") public static HashSet of(T... elements) { Objects.requireNonNull(elements, "elements is null"); - HashArrayMappedTrie tree = HashArrayMappedTrie.empty(); - for (T element : elements) { - tree = tree.put(element, element); - } - return tree.isEmpty() ? empty() : new HashSet<>(tree); + return HashSet.empty().addAll(Arrays.asList(elements)); } /** @@ -155,12 +226,7 @@ public static HashSet fill(int n, Supplier s) { @SuppressWarnings("unchecked") public static HashSet ofAll(Iterable elements) { Objects.requireNonNull(elements, "elements is null"); - if (elements instanceof HashSet) { - return (HashSet) elements; - } else { - final HashArrayMappedTrie tree = addAll(HashArrayMappedTrie.empty(), elements); - return tree.isEmpty() ? empty() : new HashSet<>(tree); - } + return elements instanceof HashSet? (HashSet) elements :HashSet.of().addAll(elements); } /** @@ -481,33 +547,47 @@ public static HashSet rangeClosedBy(long from, long toInclusive, long step @Override public HashSet add(T element) { - return contains(element) ? this : new HashSet<>(tree.put(element, element)); + int keyHash = keyHash(element); + ChampTrie.ChangeEvent details = new ChampTrie.ChangeEvent<>(); + ChampTrie.BitmapIndexedNode newRootNode = root.put(null, element, keyHash, 0, details, HashSet::updateElement, Objects::equals, HashSet::keyHash); + if (details.isModified()) { + return new HashSet<>(newRootNode, size + 1); + } + return this; } + /** + * Update function for a set: we always keep the old element. + * + * @param oldElement the old element + * @param newElement the new element + * @param the element type + * @return always returns the old element + */ + static E updateElement(E oldElement, E newElement) { + return oldElement; + } + + + @SuppressWarnings("unchecked") @Override public HashSet addAll(Iterable elements) { - Objects.requireNonNull(elements, "elements is null"); - if (isEmpty() && elements instanceof HashSet) { - @SuppressWarnings("unchecked") - final HashSet set = (HashSet) elements; - return set; - } - final HashArrayMappedTrie that = addAll(tree, elements); - if (that.size() == tree.size()) { - return this; - } else { - return new HashSet<>(that); + if(isEmpty()&&elements instanceof HashSet){ + return (HashSet) elements; } + TransientHashSet t = toTransient(); + t.addAll(elements); + return t.root==this.root?this: t.toImmutable(); } @Override public HashSet collect(PartialFunction partialFunction) { - return ofAll(iterator(). collect(partialFunction)); + return ofAll(iterator().collect(partialFunction)); } @Override public boolean contains(T element) { - return tree.get(element).isDefined(); + return root.find(element, keyHash(element), 0, Objects::equals) != ChampTrie.Node.NO_DATA; } @Override @@ -566,16 +646,9 @@ public HashSet dropWhile(Predicate predicate) { @Override public HashSet filter(Predicate predicate) { - Objects.requireNonNull(predicate, "predicate is null"); - final HashSet filtered = HashSet.ofAll(iterator().filter(predicate)); - - if (filtered.isEmpty()) { - return empty(); - } else if (filtered.length() == length()) { - return this; - } else { - return filtered; - } + TransientHashSet t = toTransient(); + t.filterAll(predicate); + return t.root==this.root?this: t.toImmutable(); } @Override @@ -590,9 +663,8 @@ public HashSet flatMap(Function that = foldLeft(HashArrayMappedTrie.empty(), - (tree, t) -> addAll(tree, mapper.apply(t))); - return new HashSet<>(that); + return foldLeft(HashSet.empty(), + (tree, t) -> tree.addAll(mapper.apply(t))); } } @@ -618,10 +690,10 @@ public boolean hasDefiniteSize() { @Override public T head() { - if (tree.isEmpty()) { + if (isEmpty()) { throw new NoSuchElementException("head of empty set"); } - return iterator().next(); + return ChampTrie.Node.getFirst(root); } @Override @@ -631,6 +703,11 @@ public Option headOption() { @Override public HashSet init() { + //XXX I would like to remove the last element here, but this would break HashSetTest.shouldGetInitOfNonNil(). + //if (isEmpty()) { + // throw new UnsupportedOperationException("init of empty set"); + //} + //return remove(last()); return tail(); } @@ -641,18 +718,7 @@ public Option> initOption() { @Override public HashSet intersect(Set elements) { - Objects.requireNonNull(elements, "elements is null"); - if (isEmpty() || elements.isEmpty()) { - return empty(); - } else { - final int size = size(); - if (size <= elements.size()) { - return retainAll(elements); - } else { - final HashSet results = HashSet. ofAll(elements).retainAll(this); - return (size == results.size()) ? this : results; - } - } + return retainAll(elements); } /** @@ -667,7 +733,7 @@ public boolean isAsync() { @Override public boolean isEmpty() { - return tree.isEmpty(); + return size == 0; } /** @@ -687,17 +753,21 @@ public boolean isTraversableAgain() { @Override public Iterator iterator() { - return tree.keysIterator(); + return new ChampIteration.IteratorFacade<>(spliterator()); + } + + static int keyHash(Object e) { + return SALT ^ Objects.hashCode(e); } @Override public T last() { - return Collections.last(this); + return ChampTrie.Node.getLast(root); } @Override public int length() { - return tree.size(); + return size; } @Override @@ -706,11 +776,11 @@ public HashSet map(Function mapper) { if (isEmpty()) { return empty(); } else { - final HashArrayMappedTrie that = foldLeft(HashArrayMappedTrie.empty(), (tree, t) -> { - final U u = mapper.apply(t); - return tree.put(u, u); - }); - return new HashSet<>(that); + return foldLeft(HashSet.empty(), + (tree, t) -> { + final U u = mapper.apply(t); + return tree.add(u); + }); } } @@ -731,6 +801,10 @@ public HashSet orElse(Supplier> supplier) { @Override public Tuple2, HashSet> partition(Predicate predicate) { + //XXX HashSetTest#shouldPartitionInOneIteration prevents that we can use a faster implementation + //XXX HashSetTest#partitionShouldBeUnique prevents that we can use a faster implementation + //XXX I believe that these tests are wrong, because predicates should not have side effects! + //return new Tuple2<>(filter(predicate),filter(predicate.negate())); return Collections.partition(this, HashSet::ofAll, predicate); } @@ -744,23 +818,27 @@ public HashSet peek(Consumer action) { } @Override - public HashSet remove(T element) { - final HashArrayMappedTrie newTree = tree.remove(element); - return (newTree == tree) ? this : new HashSet<>(newTree); + public HashSet remove(T key) { + int keyHash = keyHash(key); + ChampTrie.ChangeEvent details = new ChampTrie.ChangeEvent<>(); + ChampTrie.BitmapIndexedNode newRootNode = root.remove(null, key, keyHash, 0, details, Objects::equals); + if (details.isModified()) { + return size == 1 ? HashSet.empty() : new HashSet<>(newRootNode, size - 1); + } + return this; } @Override public HashSet removeAll(Iterable elements) { - return Collections.removeAll(this, elements); + TransientHashSet t = toTransient(); + t.removeAll(elements); + return t.root==this.root?this: t.toImmutable(); } @Override public HashSet replace(T currentElement, T newElement) { - if (tree.containsKey(currentElement)) { - return remove(currentElement).add(newElement); - } else { - return this; - } + HashSet removed = remove(currentElement); + return removed != this ? removed.add(newElement) : this; } @Override @@ -770,7 +848,9 @@ public HashSet replaceAll(T currentElement, T newElement) { @Override public HashSet retainAll(Iterable elements) { - return Collections.retainAll(this, elements); + TransientHashSet t = toTransient(); + t.retainAll(elements); + return t.root==this.root?this: t.toImmutable(); } @Override @@ -810,9 +890,15 @@ public Tuple2, HashSet> span(Predicate predicate) { return Tuple.of(HashSet.ofAll(t._1), HashSet.ofAll(t._2)); } + @Override + public Spliterator spliterator() { + return new ChampIteration.ChampSpliterator<>(root, Function.identity(), + Spliterator.DISTINCT | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.IMMUTABLE, size); + } + @Override public HashSet tail() { - if (tree.isEmpty()) { + if (isEmpty()) { throw new UnsupportedOperationException("tail of empty set"); } return remove(head()); @@ -820,11 +906,7 @@ public HashSet tail() { @Override public Option> tailOption() { - if (tree.isEmpty()) { - return Option.none(); - } else { - return Option.some(tail()); - } + return isEmpty() ? Option.none() : Option.some(tail()); } @Override @@ -871,29 +953,19 @@ public U transform(Function, ? extends U> f) { @Override public java.util.HashSet toJavaSet() { + // XXX If the return value was not required to be a java.util.HashSet + // we could provide a mutable HashSet in O(1) return toJavaSet(java.util.HashSet::new); } + TransientHashSet toTransient() { + return new TransientHashSet<>(this); + } + @SuppressWarnings("unchecked") @Override public HashSet union(Set elements) { - Objects.requireNonNull(elements, "elements is null"); - if (isEmpty()) { - if (elements instanceof HashSet) { - return (HashSet) elements; - } else { - return HashSet.ofAll(elements); - } - } else if (elements.isEmpty()) { - return this; - } else { - final HashArrayMappedTrie that = addAll(tree, elements); - if (that.size() == tree.size()) { - return this; - } else { - return new HashSet<>(that); - } - } + return addAll(elements); } @Override @@ -947,14 +1019,6 @@ public String toString() { return mkString(stringPrefix() + "(", ", ", ")"); } - private static HashArrayMappedTrie addAll(HashArrayMappedTrie initial, - Iterable additional) { - HashArrayMappedTrie that = initial; - for (T t : additional) { - that = that.put(t, t); - } - return that; - } // -- Serialization @@ -967,7 +1031,7 @@ private static HashArrayMappedTrie addAll(HashArrayMappedTrie in * @return A SerializationProxy for this enclosing class. */ private Object writeReplace() { - return new SerializationProxy<>(this.tree); + return new SerializationProxy<>(this); } /** @@ -995,7 +1059,7 @@ private static final class SerializationProxy implements Serializable { private static final long serialVersionUID = 1L; // the instance to be serialized/deserialized - private transient HashArrayMappedTrie tree; + private transient HashSet tree; /** * Constructor for the case of serialization, called by {@link HashSet#writeReplace()}. @@ -1005,7 +1069,7 @@ private static final class SerializationProxy implements Serializable { * * @param tree a Cons */ - SerializationProxy(HashArrayMappedTrie tree) { + SerializationProxy(HashSet tree) { this.tree = tree; } @@ -1018,8 +1082,8 @@ private static final class SerializationProxy implements Serializable { private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); s.writeInt(tree.size()); - for (Tuple2 e : tree) { - s.writeObject(e._1); + for (T e : tree) { + s.writeObject(e); } } @@ -1037,13 +1101,17 @@ private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOEx if (size < 0) { throw new InvalidObjectException("No elements"); } - HashArrayMappedTrie temp = HashArrayMappedTrie.empty(); + ChampTrie.IdentityObject owner = new ChampTrie.IdentityObject(); + ChampTrie.BitmapIndexedNode newRoot = ChampTrie.BitmapIndexedNode.emptyNode(); + ChampTrie.ChangeEvent details = new ChampTrie.ChangeEvent<>(); + int newSize = 0; for (int i = 0; i < size; i++) { - @SuppressWarnings("unchecked") - final T element = (T) s.readObject(); - temp = temp.put(element, element); + @SuppressWarnings("unchecked") final T element = (T) s.readObject(); + int keyHash = keyHash(element); + newRoot = newRoot.put(owner, element, keyHash, 0, details, HashSet::updateElement, Objects::equals, HashSet::keyHash); + if (details.isModified()) newSize++; } - tree = temp; + tree = newSize == 0 ? empty() : new HashSet<>(newRoot, newSize); } /** @@ -1056,7 +1124,169 @@ private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOEx * @return A deserialized instance of the enclosing class. */ private Object readResolve() { - return tree.isEmpty() ? HashSet.empty() : new HashSet<>(tree); + return tree; + } + } + + /** + * Supports efficient bulk-operations on a set through transience. + * + * @param the element type + */ + static class TransientHashSet extends ChampTransience.ChampAbstractTransientSet { + TransientHashSet(HashSet s) { + root = s.root; + size = s.size; + } + + TransientHashSet() { + this(empty()); + } + + public HashSet toImmutable() { + owner = null; + return isEmpty() + ? empty() + : new HashSet<>(root, size); + } + + boolean add(E e) { + ChampTrie.ChangeEvent details = new ChampTrie.ChangeEvent<>(); + root = root.put(makeOwner(), + e, keyHash(e), 0, details, + (oldKey, newKey) -> oldKey, + Objects::equals, HashSet::keyHash); + if (details.isModified()) { + size++; + modCount++; + } + return details.isModified(); + } + + @SuppressWarnings("unchecked") + boolean addAll(Iterable c) { + if (c == root) { + return false; + } + if (isEmpty() && (c instanceof HashSet)) { + HashSet cc = (HashSet) c; + root = (ChampTrie.BitmapIndexedNode) cc.root; + size = cc.size; + return true; + } + if (c instanceof HashSet) { + HashSet that = (HashSet) c; + ChampTrie.BulkChangeEvent bulkChange = new ChampTrie.BulkChangeEvent(); + ChampTrie.BitmapIndexedNode newRootNode = root.putAll(makeOwner(), (ChampTrie.Node) that.root, 0, bulkChange, HashSet::updateElement, Objects::equals, HashSet::keyHash, new ChampTrie.ChangeEvent<>()); + if (bulkChange.inBoth == that.size()) { + return false; + } + root = newRootNode; + size += that.size - bulkChange.inBoth; + modCount++; + return true; + } + boolean added = false; + for (E e : c) { + added |= add(e); + } + return added; + } + + @Override + public java.util.Iterator iterator() { + return new ChampIteration.IteratorFacade<>(spliterator()); + } + + + public Spliterator spliterator() { + return new ChampIteration.ChampSpliterator<>(root, Function.identity(), Spliterator.DISTINCT | Spliterator.SIZED, size); + } + + @SuppressWarnings("unchecked") + @Override + boolean remove(Object key) { + int keyHash = keyHash(key); + ChampTrie.ChangeEvent details = new ChampTrie.ChangeEvent<>(); + root = root.remove(owner, (E) key, keyHash, 0, details, Objects::equals); + if (details.isModified()) { + size--; + return true; + } + return false; + } + + @SuppressWarnings("unchecked") + boolean removeAll(Iterable c) { + if (isEmpty() + || (c instanceof Collection) && ((Collection) c).isEmpty()) { + return false; + } + if (c instanceof HashSet) { + HashSet that = (HashSet) c; + ChampTrie.BulkChangeEvent bulkChange = new ChampTrie.BulkChangeEvent(); + ChampTrie.BitmapIndexedNode newRootNode = root.removeAll(makeOwner(), (ChampTrie.BitmapIndexedNode) that.root, 0, bulkChange, HashSet::updateElement, Objects::equals, HashSet::keyHash, new ChampTrie.ChangeEvent<>()); + if (bulkChange.removed == 0) { + return false; + } + root = newRootNode; + size -= bulkChange.removed; + modCount++; + return true; + } + return super.removeAll(c); + } + + void clear() { + root = ChampTrie.BitmapIndexedNode.emptyNode(); + size = 0; + modCount++; + } + + @SuppressWarnings("unchecked") + boolean retainAll(Iterable c) { + if (isEmpty()) { + return false; + } + if ((c instanceof Collection && ((Collection) c).isEmpty())) { + Collection cc = (Collection) c; + clear(); + return true; + } + ChampTrie.BulkChangeEvent bulkChange = new ChampTrie.BulkChangeEvent(); + ChampTrie.BitmapIndexedNode newRootNode; + if (c instanceof HashSet) { + HashSet that = (HashSet) c; + newRootNode = root.retainAll(makeOwner(), (ChampTrie.BitmapIndexedNode) that.root, 0, bulkChange, HashSet::updateElement, Objects::equals, HashSet::keyHash, new ChampTrie.ChangeEvent<>()); + } else if (c instanceof Collection) { + Collection that = (Collection) c; + newRootNode = root.filterAll(makeOwner(), that::contains, 0, bulkChange); + } else { + java.util.HashSet that = new java.util.HashSet<>(); + c.forEach(that::add); + newRootNode = root.filterAll(makeOwner(), that::contains, 0, bulkChange); + } + if (bulkChange.removed == 0) { + return false; + } + root = newRootNode; + size -= bulkChange.removed; + modCount++; + return true; + } + + public boolean filterAll(Predicate predicate) { + ChampTrie.BulkChangeEvent bulkChange = new ChampTrie.BulkChangeEvent(); + ChampTrie.BitmapIndexedNode newRootNode + = root.filterAll(makeOwner(), predicate, 0, bulkChange); + if (bulkChange.removed == 0) { + return false; + } + root = newRootNode; + size -= bulkChange.removed; + modCount++; + return true; + } } } diff --git a/src/main/java/io/vavr/collection/LinkedHashMap.java b/src/main/java/io/vavr/collection/LinkedHashMap.java index 28f255cb5..10e9cb3a8 100644 --- a/src/main/java/io/vavr/collection/LinkedHashMap.java +++ b/src/main/java/io/vavr/collection/LinkedHashMap.java @@ -4,7 +4,7 @@ * * The MIT License (MIT) * - * Copyright 2024 Vavr, https://vavr.io + * Copyright 2023 Vavr, https://vavr.io * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,31 +26,144 @@ */ package io.vavr.collection; -import io.vavr.*; +import io.vavr.Tuple; +import io.vavr.Tuple2; import io.vavr.control.Option; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; import java.util.Objects; -import java.util.function.*; +import java.util.Spliterator; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collector; +import static io.vavr.collection.ChampSequenced.ChampSequencedData.vecRemove; +import static io.vavr.collection.ChampTrie.BitmapIndexedNode.emptyNode; + /** - * An immutable {@code LinkedHashMap} implementation that has predictable (insertion-order) iteration. + * Implements an immutable map using a Compressed Hash-Array Mapped Prefix-tree + * (CHAMP) and a bit-mapped trie (Vector). + *

+ * Features: + *

    + *
  • supports up to 230 entries
  • + *
  • allows null keys and null values
  • + *
  • is immutable
  • + *
  • is thread-safe
  • + *
  • iterates in the order, in which keys were inserted
  • + *
+ *

+ * Performance characteristics: + *

    + *
  • put, putFirst, putLast: O(log N) in an amortized sense, because we sometimes have to + * renumber the elements.
  • + *
  • remove: O(log N) in an amortized sense, because we sometimes have to renumber the elements.
  • + *
  • containsKey: O(1)
  • + *
  • toMutable: O(1) + O(log N) distributed across subsequent updates in + * the mutable copy
  • + *
  • clone: O(1)
  • + *
  • iterator creation: O(1)
  • + *
  • iterator.next: O(log N)
  • + *
  • getFirst, getLast: O(log N)
  • + *
+ *

+ * Implementation details: + *

+ * This map performs read and write operations of single elements in O(log N) time, + * and in O(log N) space, where N is the number of elements in the set. + *

+ * The CHAMP trie contains nodes that may be shared with other maps. + *

+ * If a write operation is performed on a node, then this set creates a + * copy of the node and of all parent nodes up to the root (copy-path-on-write). + * Since the CHAMP trie has a fixed maximal height, the cost is O(1). + *

+ * Insertion Order: + *

+ * This map uses a counter to keep track of the insertion order. + * It stores the current value of the counter in the sequence number + * field of each data entry. If the counter wraps around, it must renumber all + * sequence numbers. + *

+ * The renumbering is why the {@code add} and {@code remove} methods are O(1) + * only in an amortized sense. + *

+ * To support iteration, we use a Vector. The Vector has the same contents + * as the CHAMP trie. However, its elements are stored in insertion order. + *

+ * If an element is removed from the CHAMP trie that is not the first or the + * last element of the Vector, we replace its corresponding element in + * the Vector by a tombstone. If the element is at the start or end of the Vector, + * we remove the element and all its neighboring tombstones from the Vector. + *

+ * A tombstone can store the number of neighboring tombstones in ascending and in descending + * direction. We use these numbers to skip tombstones when we iterate over the vector. + * Since we only allow iteration in ascending or descending order from one of the ends of + * the vector, we do not need to keep the number of neighbors in all tombstones up to date. + * It is sufficient, if we update the neighbor with the lowest index and the one with the + * highest index. + *

+ * If the number of tombstones exceeds half of the size of the collection, we renumber all + * sequence numbers, and we create a new Vector. + *

+ * References: + *

+ * Portions of the code in this class have been derived from JHotDraw8 'VectorMap.java'. + *

+ * For a similar design, see 'VectorMap.scala'. Note, that this code is not a derivative + * of that code. + *

+ *
JHotDraw 8. VectorMap.java. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
The Scala library. VectorMap.scala. Copyright EPFL and Lightbend, Inc. Apache License 2.0.
+ *
github.com + *
+ *
+ * + * @param the key type + * @param the value type */ -public final class LinkedHashMap implements Map, Serializable { - - private static final long serialVersionUID = 1L; - - private static final LinkedHashMap EMPTY = new LinkedHashMap<>(Queue.empty(), HashMap.empty()); - - private final Queue> list; - private final HashMap map; +@SuppressWarnings("exports") +public class LinkedHashMap implements Map, Serializable { + private static final long serialVersionUID = 1L; + private static final LinkedHashMap EMPTY = new LinkedHashMap<>( + emptyNode(), Vector.empty(), 0, 0); +private final ChampTrie.BitmapIndexedNode> root; + /** + * Offset of sequence numbers to vector indices. + * + *
vector index = sequence number + offset
+ */ + final int offset; + /** + * The size of the map. + */ + final int size; + /** + * In this vector we store the elements in the order in which they were inserted. + */ + final Vector vector; - private LinkedHashMap(Queue> list, HashMap map) { - this.list = list; - this.map = map; + LinkedHashMap(ChampTrie.BitmapIndexedNode> root, + Vector vector, + int size, int offset) { + this.root=root; + this.size = size; + this.offset = offset; + this.vector = Objects.requireNonNull(vector); } /** @@ -70,9 +183,9 @@ public static Collector, ArrayList>, LinkedHash * {@link java.util.stream.Stream#collect(java.util.stream.Collector)} to obtain a {@link LinkedHashMap}. * * @param keyMapper The key mapper - * @param The key type - * @param The value type - * @param Initial {@link java.util.stream.Stream} elements type + * @param The key type + * @param The value type + * @param Initial {@link java.util.stream.Stream} elements type * @return A {@link LinkedHashMap} Collector. */ public static Collector, LinkedHashMap> collector(Function keyMapper) { @@ -84,11 +197,11 @@ public static Collector, LinkedHashMap * Returns a {@link java.util.stream.Collector} which may be used in conjunction with * {@link java.util.stream.Stream#collect(java.util.stream.Collector)} to obtain a {@link LinkedHashMap}. * - * @param keyMapper The key mapper + * @param keyMapper The key mapper * @param valueMapper The value mapper - * @param The key type - * @param The value type - * @param Initial {@link java.util.stream.Stream} elements type + * @param The key type + * @param The value type + * @param Initial {@link java.util.stream.Stream} elements type * @return A {@link LinkedHashMap} Collector. */ public static Collector, LinkedHashMap> collector( @@ -96,7 +209,7 @@ public static Collector, LinkedHashMap> collecto Objects.requireNonNull(keyMapper, "keyMapper is null"); Objects.requireNonNull(valueMapper, "valueMapper is null"); return Collections.toListAndThen(arr -> LinkedHashMap.ofEntries(Iterator.ofAll(arr) - .map(t -> Tuple.of(keyMapper.apply(t), valueMapper.apply(t))))); + .map(t -> Tuple.of(keyMapper.apply(t), valueMapper.apply(t))))); } @SuppressWarnings("unchecked") @@ -129,9 +242,8 @@ public static LinkedHashMap narrow(LinkedHashMap LinkedHashMap of(Tuple2 entry) { - final HashMap map = HashMap.of(entry); - final Queue> list = Queue.of((Tuple2) entry); - return wrap(list, map); + Objects.requireNonNull(entry, "entry is null"); + return LinkedHashMap.empty().put(entry._1,entry._2); } /** @@ -144,11 +256,9 @@ public static LinkedHashMap of(Tuple2 ent */ public static LinkedHashMap ofAll(java.util.Map map) { Objects.requireNonNull(map, "map is null"); - LinkedHashMap result = LinkedHashMap.empty(); - for (java.util.Map.Entry entry : map.entrySet()) { - result = result.put(entry.getKey(), entry.getValue()); - } - return result; + TransientLinkedHashMap m = new TransientLinkedHashMap<>(); + m.putAllEntries(map.entrySet()); + return m.toImmutable(); } /** @@ -162,7 +272,7 @@ public static LinkedHashMap ofAll(java.util.Map LinkedHashMap ofAll(java.util.stream.Stream stream, - Function> entryMapper) { + Function> entryMapper) { return Maps.ofStream(empty(), stream, entryMapper); } @@ -178,8 +288,8 @@ public static LinkedHashMap ofAll(java.util.stream.Stream LinkedHashMap ofAll(java.util.stream.Stream stream, - Function keyMapper, - Function valueMapper) { + Function keyMapper, + Function valueMapper) { return Maps.ofStream(empty(), stream, keyMapper, valueMapper); } @@ -193,9 +303,7 @@ public static LinkedHashMap ofAll(java.util.stream.Stream LinkedHashMap of(K key, V value) { - final HashMap map = HashMap.of(key, value); - final Queue> list = Queue.of(Tuple.of(key, value)); - return wrap(list, map); + return LinkedHashMap.empty().put(key,value); } /** @@ -210,9 +318,10 @@ public static LinkedHashMap of(K key, V value) { * @return A new Map containing the given entries */ public static LinkedHashMap of(K k1, V v1, K k2, V v2) { - final HashMap map = HashMap.of(k1, v1, k2, v2); - final Queue> list = Queue.of(Tuple.of(k1, v1), Tuple.of(k2, v2)); - return wrapNonUnique(list, map); + TransientLinkedHashMap t = new TransientLinkedHashMap(); + t.put(k1,v1); + t.put(k2,v2); + return t.toImmutable(); } /** @@ -229,9 +338,11 @@ public static LinkedHashMap of(K k1, V v1, K k2, V v2) { * @return A new Map containing the given entries */ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3) { - final HashMap map = HashMap.of(k1, v1, k2, v2, k3, v3); - final Queue> list = Queue.of(Tuple.of(k1, v1), Tuple.of(k2, v2), Tuple.of(k3, v3)); - return wrapNonUnique(list, map); + TransientLinkedHashMap t = new TransientLinkedHashMap(); + t.put(k1,v1); + t.put(k2,v2); + t.put(k3,v3); + return t.toImmutable(); } /** @@ -250,9 +361,12 @@ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3) * @return A new Map containing the given entries */ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) { - final HashMap map = HashMap.of(k1, v1, k2, v2, k3, v3, k4, v4); - final Queue> list = Queue.of(Tuple.of(k1, v1), Tuple.of(k2, v2), Tuple.of(k3, v3), Tuple.of(k4, v4)); - return wrapNonUnique(list, map); + TransientLinkedHashMap t = new TransientLinkedHashMap(); + t.put(k1,v1); + t.put(k2,v2); + t.put(k3,v3); + t.put(k4,v4); + return t.toImmutable(); } /** @@ -273,9 +387,13 @@ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, * @return A new Map containing the given entries */ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) { - final HashMap map = HashMap.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5); - final Queue> list = Queue.of(Tuple.of(k1, v1), Tuple.of(k2, v2), Tuple.of(k3, v3), Tuple.of(k4, v4), Tuple.of(k5, v5)); - return wrapNonUnique(list, map); + TransientLinkedHashMap t = new TransientLinkedHashMap(); + t.put(k1,v1); + t.put(k2,v2); + t.put(k3,v3); + t.put(k4,v4); + t.put(k5,v5); + return t.toImmutable(); } /** @@ -298,9 +416,14 @@ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, * @return A new Map containing the given entries */ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6) { - final HashMap map = HashMap.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6); - final Queue> list = Queue.of(Tuple.of(k1, v1), Tuple.of(k2, v2), Tuple.of(k3, v3), Tuple.of(k4, v4), Tuple.of(k5, v5), Tuple.of(k6, v6)); - return wrapNonUnique(list, map); + TransientLinkedHashMap t = new TransientLinkedHashMap(); + t.put(k1,v1); + t.put(k2,v2); + t.put(k3,v3); + t.put(k4,v4); + t.put(k5,v5); + t.put(k6,v6); + return t.toImmutable(); } /** @@ -325,9 +448,15 @@ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, * @return A new Map containing the given entries */ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7) { - final HashMap map = HashMap.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7); - final Queue> list = Queue.of(Tuple.of(k1, v1), Tuple.of(k2, v2), Tuple.of(k3, v3), Tuple.of(k4, v4), Tuple.of(k5, v5), Tuple.of(k6, v6), Tuple.of(k7, v7)); - return wrapNonUnique(list, map); + TransientLinkedHashMap t = new TransientLinkedHashMap(); + t.put(k1,v1); + t.put(k2,v2); + t.put(k3,v3); + t.put(k4,v4); + t.put(k5,v5); + t.put(k6,v6); + t.put(k7,v7); + return t.toImmutable(); } /** @@ -354,9 +483,16 @@ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, * @return A new Map containing the given entries */ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8) { - final HashMap map = HashMap.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8); - final Queue> list = Queue.of(Tuple.of(k1, v1), Tuple.of(k2, v2), Tuple.of(k3, v3), Tuple.of(k4, v4), Tuple.of(k5, v5), Tuple.of(k6, v6), Tuple.of(k7, v7), Tuple.of(k8, v8)); - return wrapNonUnique(list, map); + TransientLinkedHashMap t = new TransientLinkedHashMap(); + t.put(k1,v1); + t.put(k2,v2); + t.put(k3,v3); + t.put(k4,v4); + t.put(k5,v5); + t.put(k6,v6); + t.put(k7,v7); + t.put(k8,v8); + return t.toImmutable(); } /** @@ -385,9 +521,17 @@ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, * @return A new Map containing the given entries */ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9) { - final HashMap map = HashMap.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8, k9, v9); - final Queue> list = Queue.of(Tuple.of(k1, v1), Tuple.of(k2, v2), Tuple.of(k3, v3), Tuple.of(k4, v4), Tuple.of(k5, v5), Tuple.of(k6, v6), Tuple.of(k7, v7), Tuple.of(k8, v8), Tuple.of(k9, v9)); - return wrapNonUnique(list, map); + TransientLinkedHashMap t = new TransientLinkedHashMap(); + t.put(k1,v1); + t.put(k2,v2); + t.put(k3,v3); + t.put(k4,v4); + t.put(k5,v5); + t.put(k6,v6); + t.put(k7,v7); + t.put(k8,v8); + t.put(k9,v9); + return t.toImmutable(); } /** @@ -418,9 +562,18 @@ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, * @return A new Map containing the given entries */ public static LinkedHashMap of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) { - final HashMap map = HashMap.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8, k9, v9, k10, v10); - final Queue> list = Queue.of(Tuple.of(k1, v1), Tuple.of(k2, v2), Tuple.of(k3, v3), Tuple.of(k4, v4), Tuple.of(k5, v5), Tuple.of(k6, v6), Tuple.of(k7, v7), Tuple.of(k8, v8), Tuple.of(k9, v9), Tuple.of(k10, v10)); - return wrapNonUnique(list, map); + TransientLinkedHashMap t = new TransientLinkedHashMap(); + t.put(k1,v1); + t.put(k2,v2); + t.put(k3,v3); + t.put(k4,v4); + t.put(k5,v5); + t.put(k6,v6); + t.put(k7,v7); + t.put(k8,v8); + t.put(k9,v9); + t.put(k10,v10); + return t.toImmutable(); } /** @@ -466,14 +619,7 @@ public static LinkedHashMap fill(int n, Supplier LinkedHashMap ofEntries(java.util.Map.Entry... entries) { - HashMap map = HashMap.empty(); - Queue> list = Queue.empty(); - for (java.util.Map.Entry entry : entries) { - final Tuple2 tuple = Tuple.of(entry.getKey(), entry.getValue()); - map = map.put(tuple); - list = list.append(tuple); - } - return wrapNonUnique(list, map); + return LinkedHashMap.empty().putAllEntries(Arrays.asList(entries)); } /** @@ -486,9 +632,7 @@ public static LinkedHashMap ofEntries(java.util.Map.Entry LinkedHashMap ofEntries(Tuple2... entries) { - final HashMap map = HashMap.ofEntries(entries); - final Queue> list = Queue.of((Tuple2[]) entries); - return wrapNonUnique(list, map); + return LinkedHashMap.empty().putAllTuples(Arrays.asList(entries)); } /** @@ -502,17 +646,7 @@ public static LinkedHashMap ofEntries(Tuple2 LinkedHashMap ofEntries(Iterable> entries) { Objects.requireNonNull(entries, "entries is null"); - if (entries instanceof LinkedHashMap) { - return (LinkedHashMap) entries; - } else { - HashMap map = HashMap.empty(); - Queue> list = Queue.empty(); - for (Tuple2 entry : entries) { - map = map.put(entry); - list = list.append((Tuple2) entry); - } - return wrapNonUnique(list, map); - } + return LinkedHashMap.empty().putAllTuples(entries); } @Override @@ -535,7 +669,8 @@ public Tuple2, LinkedHashMap> computeIfPresent(K key, BiFunction @Override public boolean containsKey(K key) { - return map.containsKey(key); + return root.find(new ChampSequenced.ChampSequencedEntry<>(key), ChampSequenced.ChampSequencedEntry.keyHash(key), 0, + ChampSequenced.ChampSequencedEntry::keyEquals) != ChampTrie.Node.NO_DATA; } @Override @@ -555,7 +690,7 @@ public LinkedHashMap distinctBy(Function, ? exten @Override public LinkedHashMap drop(int n) { - return Maps.drop(this, this::createFromEntries, LinkedHashMap::empty, n); + return n<=0?this:ofEntries(iterator(n)); } @Override @@ -575,48 +710,56 @@ public LinkedHashMap dropWhile(Predicate> predicate) @Override public LinkedHashMap filter(BiPredicate predicate) { - return Maps.filter(this, this::createFromEntries, predicate); + TransientLinkedHashMap t = toTransient(); + t.filterAll(e->predicate.test(e.getKey(),e.getValue())); + return t.root==this.root?this: t.toImmutable(); } @Override public LinkedHashMap filterNot(BiPredicate predicate) { - return Maps.filterNot(this, this::createFromEntries, predicate); + return filter(predicate.negate()); } @Override public LinkedHashMap filter(Predicate> predicate) { - return Maps.filter(this, this::createFromEntries, predicate); + TransientLinkedHashMap t = toTransient(); + t.filterAll(e->predicate.test(new Tuple2<>(e.getKey(),e.getValue()))); + return t.root==this.root?this: t.toImmutable(); } @Override public LinkedHashMap filterNot(Predicate> predicate) { - return Maps.filterNot(this, this::createFromEntries, predicate); + return filter(predicate.negate()); } @Override public LinkedHashMap filterKeys(Predicate predicate) { - return Maps.filterKeys(this, this::createFromEntries, predicate); + TransientLinkedHashMap t = toTransient(); + t.filterAll(e->predicate.test(e.getKey())); + return t.root==this.root?this: t.toImmutable(); } @Override public LinkedHashMap filterNotKeys(Predicate predicate) { - return Maps.filterNotKeys(this, this::createFromEntries, predicate); + return filterKeys(predicate.negate()); } @Override public LinkedHashMap filterValues(Predicate predicate) { - return Maps.filterValues(this, this::createFromEntries, predicate); + TransientLinkedHashMap t = toTransient(); + t.filterAll(e->predicate.test(e.getValue())); + return t.root==this.root?this: t.toImmutable(); } @Override public LinkedHashMap filterNotValues(Predicate predicate) { - return Maps.filterNotValues(this, this::createFromEntries, predicate); + return filterValues(predicate.negate()); } @Override public LinkedHashMap flatMap(BiFunction>> mapper) { Objects.requireNonNull(mapper, "mapper is null"); - return foldLeft(LinkedHashMap. empty(), (acc, entry) -> { + return foldLeft(LinkedHashMap.empty(), (acc, entry) -> { for (Tuple2 mappedEntry : mapper.apply(entry._1, entry._2)) { acc = acc.put(mappedEntry); } @@ -624,14 +767,18 @@ public LinkedHashMap flatMap(BiFunction get(K key) { - return map.get(key); + Object result = root.find( + new ChampSequenced.ChampSequencedEntry<>(key), + ChampSequenced.ChampSequencedEntry.keyHash(key), 0, ChampSequenced.ChampSequencedEntry::keyEquals); + return ((result instanceof ChampSequenced.ChampSequencedEntry) ? Option.some((V) ((ChampSequenced.ChampSequencedEntry) result).getValue()) : Option.none()); } @Override public V getOrElse(K key, V defaultValue) { - return map.getOrElse(key, defaultValue); + return get(key).getOrElse(defaultValue); } @Override @@ -644,18 +791,19 @@ public Iterator> grouped(int size) { return Maps.grouped(this, this::createFromEntries, size); } + @SuppressWarnings("unchecked") @Override public Tuple2 head() { - return list.head(); + java.util.Map.Entry entry = (java.util.Map.Entry) vector.head(); + return new Tuple2<>(entry.getKey(), entry.getValue()); } @Override public LinkedHashMap init() { if (isEmpty()) { throw new UnsupportedOperationException("init of empty LinkedHashMap"); - } else { - return LinkedHashMap.ofEntries(list.init()); } + return remove(last()._1); } @Override @@ -675,7 +823,7 @@ public boolean isAsync() { @Override public boolean isEmpty() { - return map.isEmpty(); + return size==0; } /** @@ -695,18 +843,23 @@ public boolean isSequential() { @Override public Iterator> iterator() { - return list.iterator(); + return new ChampIteration.IteratorFacade<>(spliterator()); + } + + Iterator> iterator(int startIndex) { + return new ChampIteration.IteratorFacade<>(spliterator(startIndex)); } - @SuppressWarnings("unchecked") @Override public Set keySet() { - return LinkedHashSet.wrap((LinkedHashMap) this); + return LinkedHashSet.ofAll(iterator().map(Tuple2::_1)); } @Override + @SuppressWarnings("unchecked") public Tuple2 last() { - return list.last(); + java.util.Map.Entry entry = (java.util.Map.Entry) vector.last(); + return new Tuple2<>(entry.getKey(), entry.getValue()); } @Override @@ -734,7 +887,7 @@ public LinkedHashMap mapValues(Function mapper @Override public LinkedHashMap merge(Map that) { - return Maps.merge(this, this::createFromEntries, that); + return putAllTuples(that); } @Override @@ -781,15 +934,54 @@ public LinkedHashMap put(K key, U value, BiFunction put(K key, V value) { - final Queue> newList; - final Option currentEntry = get(key); - if (currentEntry.isDefined()) { - newList = list.replace(Tuple.of(key, currentEntry.get()), Tuple.of(key, value)); - } else { - newList = list.append(Tuple.of(key, value)); + return putLast(key, value, false); + } + + private LinkedHashMap putAllEntries(Iterable> c) { + TransientLinkedHashMap t=toTransient(); + t.putAllEntries(c); + return t.root==this.root?this: t.toImmutable(); + } + @SuppressWarnings("unchecked") + private LinkedHashMap putAllTuples(Iterable> c) { + if (isEmpty()&& c instanceof LinkedHashMap){ + LinkedHashMap that = (LinkedHashMap) c; + return (LinkedHashMap)that; + } + TransientLinkedHashMap t=toTransient(); + t.putAllTuples(c); + return t.root==this.root?this: t.toImmutable(); + } + private LinkedHashMap putLast( K key, V value, boolean moveToLast) { + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent>(); + ChampSequenced.ChampSequencedEntry newEntry = new ChampSequenced.ChampSequencedEntry<>(key, value, vector.size() - offset); + ChampTrie.BitmapIndexedNode> newRoot =root. put(null, newEntry, + ChampSequenced.ChampSequencedEntry.keyHash(key), 0, details, + moveToLast ? ChampSequenced.ChampSequencedEntry::updateAndMoveToLast : ChampSequenced.ChampSequencedEntry::updateWithNewKey, + ChampSequenced.ChampSequencedEntry::keyEquals, ChampSequenced.ChampSequencedEntry::entryKeyHash); + if (details.isReplaced() + && details.getOldDataNonNull().getSequenceNumber() == details.getNewDataNonNull().getSequenceNumber()) { + Vector newVector = vector.update(details.getNewDataNonNull().getSequenceNumber() - offset, details.getNewDataNonNull()); + return new LinkedHashMap<>(newRoot, newVector, size, offset); + } + if (details.isModified()) { + Vector newVector = vector; + int newOffset = offset; + int newSize = size; + if (details.isReplaced()) { + if (moveToLast) { + ChampSequenced.ChampSequencedEntry oldElem = details.getOldDataNonNull(); + Tuple2, Integer> result = ChampSequenced.ChampSequencedData.vecRemove(newVector, oldElem, newOffset); + newVector = result._1; + newOffset = result._2; + } + } else { + newSize++; + } + newVector = newVector.append(newEntry); + return renumber(newRoot, newVector, newSize, newOffset); } - final HashMap newMap = map.put(key, value); - return wrap(newList, newMap); + return this; } @Override @@ -799,60 +991,104 @@ public LinkedHashMap put(Tuple2 entry) { @Override public LinkedHashMap put(Tuple2 entry, - BiFunction merge) { + BiFunction merge) { return Maps.put(this, entry, merge); } @Override public LinkedHashMap remove(K key) { - if (containsKey(key)) { - final Queue> newList = list.removeFirst(t -> Objects.equals(t._1, key)); - final HashMap newMap = map.remove(key); - return wrap(newList, newMap); - } else { - return this; + int keyHash = ChampSequenced.ChampSequencedEntry.keyHash(key); + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent>(); + ChampTrie.BitmapIndexedNode> newRoot = root.remove(null, + new ChampSequenced.ChampSequencedEntry<>(key), + keyHash, 0, details, ChampSequenced.ChampSequencedEntry::keyEquals); + if (details.isModified()) { + ChampSequenced.ChampSequencedEntry oldElem = details.getOldDataNonNull(); + Tuple2, Integer> result = ChampSequenced.ChampSequencedData.vecRemove(vector, oldElem, offset); + return renumber(newRoot, result._1, size - 1, result._2); } + return this; } @Override public LinkedHashMap removeAll(Iterable keys) { Objects.requireNonNull(keys, "keys is null"); - final HashSet toRemove = HashSet.ofAll(keys); - final Queue> newList = list.filter(t -> !toRemove.contains(t._1)); - final HashMap newMap = map.filter(t -> !toRemove.contains(t._1)); - return newList.size() == size() ? this : wrap(newList, newMap); + TransientLinkedHashMap t = toTransient(); +return t.removeAll(keys)?t.toImmutable():this; + } + + private LinkedHashMap renumber( + ChampTrie.BitmapIndexedNode> root, + Vector vector, + int size, int offset) { + + if (ChampSequenced.ChampSequencedData.vecMustRenumber(size, offset, this.vector.size())) { + ChampTrie.IdentityObject owner = new ChampTrie.IdentityObject(); + Tuple2>, Vector> result = ChampSequenced.ChampSequencedData.>vecRenumber( + size, root, vector, owner, ChampSequenced.ChampSequencedEntry::entryKeyHash, ChampSequenced.ChampSequencedEntry::keyEquals, + (e, seq) -> new ChampSequenced.ChampSequencedEntry<>(e.getKey(), e.getValue(), seq)); + return new LinkedHashMap<>( + result._1, result._2, + size, 0); + } + return new LinkedHashMap<>(root, vector, size, offset); } - @Override - public LinkedHashMap replace(Tuple2 currentElement, Tuple2 newElement) { - Objects.requireNonNull(currentElement, "currentElement is null"); - Objects.requireNonNull(newElement, "newElement is null"); - - // We replace the whole element, i.e. key and value have to be present. - if (!Objects.equals(currentElement, newElement) && contains(currentElement)) { - - Queue> newList = list; - HashMap newMap = map; - - final K currentKey = currentElement._1; - final K newKey = newElement._1; + public LinkedHashMap replace(Tuple2 currentEntry, Tuple2 newEntry) { + // currentEntry and newEntry are the same => do nothing + if (Objects.equals(currentEntry, newEntry)) { + return this; + } - // If current key and new key are equal, the element will be automatically replaced, - // otherwise we need to remove the pair (newKey, ?) from the list manually. - if (!Objects.equals(currentKey, newKey)) { - final Option value = newMap.get(newKey); - if (value.isDefined()) { - newList = newList.remove(Tuple.of(newKey, value.get())); - } - } + // try to remove currentEntry from the 'root' trie + final ChampTrie.ChangeEvent> detailsCurrent = new ChampTrie.ChangeEvent<>(); + ChampTrie.IdentityObject owner = new ChampTrie.IdentityObject(); + ChampTrie.BitmapIndexedNode> newRoot = root.remove(owner, + new ChampSequenced.ChampSequencedEntry(currentEntry._1, currentEntry._2), + Objects.hashCode(currentEntry._1), 0, detailsCurrent, ChampSequenced.ChampSequencedEntry::keyAndValueEquals); + // currentElement was not in the 'root' trie => do nothing + if (!detailsCurrent.isModified()) { + return this; + } - newList = newList.replace(currentElement, newElement); - newMap = newMap.remove(currentKey).put(newElement); + // removedData was in the 'root' trie, and we have just removed it + // => also remove its entry from the 'sequenceRoot' trie + Vector newVector = vector; + int newOffset = offset; + ChampSequenced.ChampSequencedEntry removedData = detailsCurrent.getOldData(); + int seq = removedData.getSequenceNumber(); + Tuple2, Integer> result = ChampSequenced.ChampSequencedData.vecRemove(newVector, removedData, offset); + newVector=result._1; + newOffset=result._2; + + // try to update the trie with the newData + ChampTrie.ChangeEvent> detailsNew = new ChampTrie.ChangeEvent<>(); + ChampSequenced.ChampSequencedEntry newData = new ChampSequenced.ChampSequencedEntry<>(newEntry._1, newEntry._2, seq); + newRoot = newRoot.put(owner, + newData, Objects.hashCode(newEntry._1), 0, detailsNew, + ChampSequenced.ChampSequencedEntry::forceUpdate, + ChampSequenced.ChampSequencedEntry::keyEquals, ChampSequenced.ChampSequencedEntry::entryKeyHash); + boolean isReplaced = detailsNew.isReplaced(); + + // there already was data with key newData.getKey() in the trie, and we have just replaced it + // => remove the replaced data from the vector + if (isReplaced) { + ChampSequenced.ChampSequencedEntry replacedData = detailsNew.getOldData(); + result = ChampSequenced.ChampSequencedData.vecRemove(newVector, replacedData, newOffset); + newVector=result._1; + newOffset=result._2; + } - return wrap(newList, newMap); + // we have just successfully added or replaced the newData + // => insert the newData in the vector + newVector = seq+newOffset renumbering may be necessary + return renumber(newRoot, newVector, size - 1, newOffset); } else { - return this; + // we did not change the size of the map => no renumbering is needed + return new LinkedHashMap<>(newRoot, newVector, size, newOffset); } } @@ -878,14 +1114,20 @@ public LinkedHashMap replaceAll(BiFunction retainAll(Iterable> elements) { - Objects.requireNonNull(elements, "elements is null"); - LinkedHashMap result = empty(); - for (Tuple2 entry : elements) { - if (contains(entry)) { - result = result.put(entry._1, entry._2); - } - } - return result; + TransientLinkedHashMap t=toTransient(); + t.retainAllTuples(elements); + return t.root==this.root?this: t.toImmutable(); + } + + Iterator> reverseIterator() { + return new ChampIteration.IteratorFacade<>(reverseSpliterator()); + } + + @SuppressWarnings("unchecked") + Spliterator> reverseSpliterator() { + return new ChampSequenced.ChampReverseVectorSpliterator<>(vector, + e -> new Tuple2 (((java.util.Map.Entry) e).getKey(),((java.util.Map.Entry) e).getValue()), + 0, size(),Spliterator.SIZED | Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.IMMUTABLE); } @Override @@ -897,7 +1139,7 @@ public LinkedHashMap scan( @Override public int size() { - return map.size(); + return size; } @Override @@ -920,13 +1162,24 @@ public Tuple2, LinkedHashMap> span(Predicate> spliterator() { + return spliterator(0); + } + @SuppressWarnings("unchecked") + Spliterator> spliterator(int startIndex) { + return new ChampSequenced.ChampVectorSpliterator<>(vector, + e -> new Tuple2 (((java.util.Map.Entry) e).getKey(),((java.util.Map.Entry) e).getValue()), + startIndex, size(),Spliterator.SIZED | Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.IMMUTABLE); + } + @Override public LinkedHashMap tail() { if (isEmpty()) { throw new UnsupportedOperationException("tail of empty LinkedHashMap"); - } else { - return wrap(list.tail(), map.remove(list.head()._1())); } + return remove(head()._1); } @Override @@ -959,6 +1212,9 @@ public java.util.LinkedHashMap toJavaMap() { return toJavaMap(java.util.LinkedHashMap::new, t -> t); } + TransientLinkedHashMap toTransient() { + return new TransientLinkedHashMap<>(this); + } @Override public Seq values() { return map(t -> t._2); @@ -988,36 +1244,274 @@ public String toString() { return mkString(stringPrefix() + "(", ", ", ")"); } + // We need this method to narrow the argument of `ofEntries`. + // If this method is static with type args , the jdk fails to infer types at the call site. + private LinkedHashMap createFromEntries(Iterable> tuples) { + return LinkedHashMap.ofEntries(tuples); + } + + private Object writeReplace() throws ObjectStreamException { + return new LinkedHashMap.SerializationProxy<>(this); + } + /** - * Construct Map with given values and key order. + * A serialization proxy which, in this context, is used to deserialize immutable, linked Lists with final + * instance fields. * - * @param list The list of key-value tuples with unique keys. - * @param map The map of key-value tuples. - * @param The key type - * @param The value type - * @return A new Map containing the given map with given key order + * @param The key type + * @param The value type */ - private static LinkedHashMap wrap(Queue> list, HashMap map) { - return list.isEmpty() ? empty() : new LinkedHashMap<>(list, map); + // DEV NOTE: The serialization proxy pattern is not compatible with non-final, i.e. extendable, + // classes. Also, it may not be compatible with circular object graphs. + private static final class SerializationProxy implements Serializable { + + private static final long serialVersionUID = 1L; + + // the instance to be serialized/deserialized + private transient LinkedHashMap map; + + /** + * Constructor for the case of serialization, called by {@link LinkedHashMap#writeReplace()}. + *

+ * The constructor of a SerializationProxy takes an argument that concisely represents the logical state of + * an instance of the enclosing class. + * + * @param map a map + */ + SerializationProxy(LinkedHashMap map) { + this.map = map; + } + + /** + * Write an object to a serialization stream. + * + * @param s An object serialization stream. + * @throws java.io.IOException If an error occurs writing to the stream. + */ + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + s.writeInt(map.size()); + for (Tuple2 e : map) { + s.writeObject(e._1); + s.writeObject(e._2); + } + } + + /** + * Read an object from a deserialization stream. + * + * @param s An object deserialization stream. + * @throws ClassNotFoundException If the object's class read from the stream cannot be found. + * @throws InvalidObjectException If the stream contains no list elements. + * @throws IOException If an error occurs reading from the stream. + */ + @SuppressWarnings("unchecked") + private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException { + s.defaultReadObject(); + final int size = s.readInt(); + if (size < 0) { + throw new InvalidObjectException("No elements"); + } + TransientLinkedHashMap t = new TransientLinkedHashMap<>(); + for (int i = 0; i < size; i++) { + final K key = (K) s.readObject(); + final V value = (V) s.readObject(); + t.put(key,value); + } + map =t.toImmutable(); + } + + /** + * {@code readResolve} method for the serialization proxy pattern. + *

+ * Returns a logically equivalent instance of the enclosing class. The presence of this method causes the + * serialization system to translate the serialization proxy back into an instance of the enclosing class + * upon deserialization. + * + * @return A deserialized instance of the enclosing class. + */ + private Object readResolve() { + return map; + } } /** - * Construct Map with given values and key order. + * Supports efficient bulk-operations on a linked hash map through transience. * - * @param list The list of key-value tuples with non-unique keys. - * @param map The map of key-value tuples. - * @param The key type - * @param The value type - * @return A new Map containing the given map with given key order + * @param the key type + * @param the value type */ - private static LinkedHashMap wrapNonUnique(Queue> list, HashMap map) { - return list.isEmpty() ? empty() : new LinkedHashMap<>(list.reverse().distinctBy(Tuple2::_1).reverse().toQueue(), map); - } + static class TransientLinkedHashMap extends ChampTransience.ChampAbstractTransientMap> { + /** + * Offset of sequence numbers to vector indices. + * + *

vector index = sequence number + offset
+ */ + private int offset; + /** + * In this vector we store the elements in the order in which they were inserted. + */ + private Vector vector; + + TransientLinkedHashMap(LinkedHashMap m) { + vector = m.vector; + root = m.root; + offset = m.offset; + size = m.size; + } - // We need this method to narrow the argument of `ofEntries`. - // If this method is static with type args , the jdk fails to infer types at the call site. - private LinkedHashMap createFromEntries(Iterable> tuples) { - return LinkedHashMap.ofEntries(tuples); - } + TransientLinkedHashMap() { + this(empty()); + } + + public V put(K key, V value) { + ChampSequenced.ChampSequencedEntry oldData = putLast(key, value, false).getOldData(); + return oldData == null ? null : oldData.getValue(); + } + + boolean putAllEntries(Iterable> c) { + if (c == this) { + return false; + } + boolean modified = false; + for (java.util.Map.Entry e : c) { + modified |= putLast(e.getKey(), e.getValue(), false).isModified(); + } + return modified; + } + + boolean putAllTuples(Iterable> c) { + if (c == this) { + return false; + } + boolean modified = false; + for (Tuple2 e : c) { + modified |= putLast(e._1, e._2, false).isModified(); + } + return modified; + } + + ChampTrie.ChangeEvent> putLast(final K key, V value, boolean moveToLast) { + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent>(); + ChampSequenced.ChampSequencedEntry newEntry = new ChampSequenced.ChampSequencedEntry<>(key, value, vector.size() - offset); + ChampTrie.IdentityObject owner = makeOwner(); + root = root.put(owner, newEntry, + Objects.hashCode(key), 0, details, + moveToLast ? ChampSequenced.ChampSequencedEntry::updateAndMoveToLast : ChampSequenced.ChampSequencedEntry::updateWithNewKey, + ChampSequenced.ChampSequencedEntry::keyEquals, ChampSequenced.ChampSequencedEntry::entryKeyHash); + if (details.isReplaced() + && details.getOldDataNonNull().getSequenceNumber() == details.getNewDataNonNull().getSequenceNumber()) { + vector = vector.update(details.getNewDataNonNull().getSequenceNumber() - offset, details.getNewDataNonNull()); + return details; + } + if (details.isModified()) { + if (details.isReplaced()) { + Tuple2, Integer> result = ChampSequenced.ChampSequencedData.vecRemove(vector, details.getOldDataNonNull(), offset); + vector = result._1; + offset = result._2; + } else { + size++; + } + modCount++; + vector = vector.append(newEntry); + renumber(); + } + return details; + } + + @SuppressWarnings("unchecked") + boolean removeAll(Iterable c) { + if (isEmpty()) { + return false; + } + boolean modified = false; + for (Object key : c) { + ChampTrie.ChangeEvent> details = removeKey((K) key); + modified |= details.isModified(); + } + return modified; + } + + ChampTrie.ChangeEvent> removeKey(K key) { + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent>(); + root = root.remove(null, + new ChampSequenced.ChampSequencedEntry<>(key), + Objects.hashCode(key), 0, details, ChampSequenced.ChampSequencedEntry::keyEquals); + if (details.isModified()) { + ChampSequenced.ChampSequencedEntry oldElem = details.getOldDataNonNull(); + Tuple2, Integer> result = ChampSequenced.ChampSequencedData.vecRemove(vector, oldElem, offset); + vector = result._1; + offset = result._2; + size--; + modCount++; + renumber(); + } + return details; + } + @Override + void clear() { + root= emptyNode(); + vector=Vector.empty(); + offset=0; + size=0; + } + + void renumber() { + if (ChampSequenced.ChampSequencedData.vecMustRenumber(size, offset, vector.size())) { + ChampTrie.IdentityObject owner = makeOwner(); + Tuple2>, Vector> result = ChampSequenced.ChampSequencedData.vecRenumber(size, root, vector, owner, + ChampSequenced.ChampSequencedEntry::entryKeyHash, ChampSequenced.ChampSequencedEntry::keyEquals, + (e, seq) -> new ChampSequenced.ChampSequencedEntry<>(e.getKey(), e.getValue(), seq)); + root = result._1; + vector = result._2; + offset = 0; + } + } + + public LinkedHashMap toImmutable() { + owner = null; + return isEmpty() + ? empty() + : new LinkedHashMap<>(root, vector, size, offset); + } + + static class VectorSideEffectPredicate implements Predicate> { + Vector newVector; + int newOffset; + Predicate> predicate; + + public VectorSideEffectPredicate(Predicate> predicate, Vector vector, int offset) { + this.predicate = predicate; + this.newVector = vector; + this.newOffset = offset; + } + + @Override + public boolean test(ChampSequenced.ChampSequencedEntry e) { + if (!predicate.test(e)) { + Tuple2, Integer> result = vecRemove(newVector, e, newOffset); + newVector = result._1; + newOffset = result._2; + return false; + } + return true; + } + } + + boolean filterAll(Predicate> predicate) { + ChampTrie.BulkChangeEvent bulkChange = new ChampTrie.BulkChangeEvent(); + VectorSideEffectPredicate vp = new VectorSideEffectPredicate<>(predicate, vector, offset); + ChampTrie.BitmapIndexedNode> newRootNode = root.filterAll(makeOwner(), vp, 0, bulkChange); + if (bulkChange.removed == 0) { + return false; + } + root = newRootNode; + vector = vp.newVector; + offset = vector.isEmpty()?0:vp.newOffset; + size -= bulkChange.removed; + modCount++; + return true; + } + } } diff --git a/src/main/java/io/vavr/collection/LinkedHashSet.java b/src/main/java/io/vavr/collection/LinkedHashSet.java index 8edb4c63a..ddb3b028a 100644 --- a/src/main/java/io/vavr/collection/LinkedHashSet.java +++ b/src/main/java/io/vavr/collection/LinkedHashSet.java @@ -4,7 +4,7 @@ * * The MIT License (MIT) * - * Copyright 2024 Vavr, https://vavr.io + * Copyright 2023 Vavr, https://vavr.io * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,33 +26,147 @@ */ package io.vavr.collection; -import io.vavr.*; +import io.vavr.PartialFunction; +import io.vavr.Tuple; +import io.vavr.Tuple2; import io.vavr.control.Option; -import java.io.*; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; -import java.util.NoSuchElementException; import java.util.Objects; -import java.util.function.*; +import java.util.Spliterator; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collector; +import static io.vavr.collection.ChampSequenced.ChampSequencedData.vecRemove; +import static io.vavr.collection.ChampTrie.BitmapIndexedNode.emptyNode; + + /** - * An immutable {@code HashSet} implementation that has predictable (insertion-order) iteration. + * Implements a mutable set using a Compressed Hash-Array Mapped Prefix-tree + * (CHAMP) and a bit-mapped trie (Vector). + *

+ * Features: + *

    + *
  • supports up to 230 elements
  • + *
  • allows null elements
  • + *
  • is immutable
  • + *
  • is thread-safe
  • + *
  • iterates in the order, in which elements were inserted
  • + *
+ *

+ * Performance characteristics: + *

    + *
  • add: O(log N) in an amortized sense, because we sometimes have to + * renumber the elements.
  • + *
  • remove: O(log N) in an amortized sense, because we sometimes have to + * renumber the elements.
  • + *
  • contains: O(1)
  • + *
  • toMutable: O(1) + O(log N) distributed across subsequent updates in + * the mutable copy
  • + *
  • clone: O(1)
  • + *
  • iterator creation: O(1)
  • + *
  • iterator.next: O(log N)
  • + *
  • getFirst(), getLast(): O(log N)
  • + *
+ *

+ * Implementation details: + *

+ * This set performs read and write operations of single elements in O(log N) time, + * and in O(log N) space, where N is the number of elements in the set. + *

+ * The CHAMP trie contains nodes that may be shared with other sets. + *

+ * If a write operation is performed on a node, then this set creates a + * copy of the node and of all parent nodes up to the root (copy-path-on-write). + * Since the CHAMP trie has a fixed maximal height, the cost is O(1). + *

+ * Insertion Order: + *

+ * This set uses a counter to keep track of the insertion order. + * It stores the current value of the counter in the sequence number + * field of each data entry. If the counter wraps around, it must renumber all + * sequence numbers. + *

+ * The renumbering is why the {@code add} and {@code remove} methods are O(1) + * only in an amortized sense. + *

+ * To support iteration, we use a Vector. The Vector has the same contents + * as the CHAMP trie. However, its elements are stored in insertion order. + *

+ * If an element is removed from the CHAMP trie that is not the first or the + * last element of the Vector, we replace its corresponding element in + * the Vector by a tombstone. If the element is at the start or end of the Vector, + * we remove the element and all its neighboring tombstones from the Vector. + *

+ * A tombstone can store the number of neighboring tombstones in ascending and in descending + * direction. We use these numbers to skip tombstones when we iterate over the vector. + * Since we only allow iteration in ascending or descending order from one of the ends of + * the vector, we do not need to keep the number of neighbors in all tombstones up to date. + * It is sufficient, if we update the neighbor with the lowest index and the one with the + * highest index. + *

+ * If the number of tombstones exceeds half of the size of the collection, we renumber all + * sequence numbers, and we create a new Vector. + *

+ * References: + *

+ * Portions of the code in this class have been derived from JHotDraw8 'VectorSet.java'. + *

+ * For a similar design, see 'VectorMap.scala'. Note, that this code is not a derivative + * of that code. + *

+ *
JHotDraw 8. VectorSet.java. Copyright © 2023 The authors and contributors of JHotDraw. + * MIT License.
+ *
github.com
+ *
The Scala library. VectorMap.scala. Copyright EPFL and Lightbend, Inc. Apache License 2.0.
+ *
github.com + *
+ *
* - * @param Component type + * @param the element type */ -@SuppressWarnings("deprecation") public final class LinkedHashSet implements Set, Serializable { private static final long serialVersionUID = 1L; - private static final LinkedHashSet EMPTY = new LinkedHashSet<>(LinkedHashMap.empty()); + private static final LinkedHashSet EMPTY = new LinkedHashSet<>( + emptyNode(), Vector.of(), 0, 0); - private final LinkedHashMap map; + private final ChampTrie.BitmapIndexedNode> root; + /** + * Offset of sequence numbers to vector indices. + * + *
vector index = sequence number + offset
+ */ + final int offset; + /** + * The size of the set. + */ + final int size; + /** + * In this vector we store the elements in the order in which they were inserted. + */ + final Vector vector; - private LinkedHashSet(LinkedHashMap map) { - this.map = map; + LinkedHashSet( + ChampTrie.BitmapIndexedNode> root, + Vector vector, + int size, int offset) { + this.root = root; + this.size = size; + this.offset = offset; + this.vector = Objects.requireNonNull(vector); } @SuppressWarnings("unchecked") @@ -60,9 +174,6 @@ public static LinkedHashSet empty() { return (LinkedHashSet) EMPTY; } - static LinkedHashSet wrap(LinkedHashMap map) { - return new LinkedHashSet<>(map); - } /** * Returns a {@link Collector} which may be used in conjunction with @@ -97,7 +208,7 @@ public static LinkedHashSet narrow(LinkedHashSet linkedHashS * @return A new LinkedHashSet instance containing the given element */ public static LinkedHashSet of(T element) { - return LinkedHashSet. empty().add(element); + return LinkedHashSet.empty().add(element); } /** @@ -111,13 +222,10 @@ public static LinkedHashSet of(T element) { * @throws NullPointerException if {@code elements} is null */ @SafeVarargs + @SuppressWarnings("varargs") public static LinkedHashSet of(T... elements) { Objects.requireNonNull(elements, "elements is null"); - LinkedHashMap map = LinkedHashMap.empty(); - for (T element : elements) { - map = map.put(element, element); - } - return map.isEmpty() ? LinkedHashSet.empty() : new LinkedHashSet<>(map); + return LinkedHashSet.empty().addAll(Arrays.asList(elements)); } /** @@ -159,12 +267,7 @@ public static LinkedHashSet fill(int n, Supplier s) { @SuppressWarnings("unchecked") public static LinkedHashSet ofAll(Iterable elements) { Objects.requireNonNull(elements, "elements is null"); - if (elements instanceof LinkedHashSet) { - return (LinkedHashSet) elements; - } else { - final LinkedHashMap mao = addAll(LinkedHashMap.empty(), elements); - return mao.isEmpty() ? empty() : new LinkedHashSet<>(mao); - } + return elements instanceof LinkedHashSet? (LinkedHashSet) elements :LinkedHashSet.of().addAll(elements); } /** @@ -493,7 +596,34 @@ public static LinkedHashSet rangeClosedBy(long from, long toInclusive, lon */ @Override public LinkedHashSet add(T element) { - return contains(element) ? this : new LinkedHashSet<>(map.put(element, element)); + return addLast(element, false); + } + + private LinkedHashSet addLast(T e, boolean moveToLast) { + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent>(); + ChampSequenced.ChampSequencedElement newElem = new ChampSequenced.ChampSequencedElement(e, vector.size() - offset); + ChampTrie.BitmapIndexedNode> newRoot = root.put(null, newElem, + Objects.hashCode(e), 0, details, + moveToLast ? ChampSequenced.ChampSequencedElement::updateAndMoveToLast : ChampSequenced.ChampSequencedElement::update, + Objects::equals, Objects::hashCode); + if (details.isModified()) { + Vector newVector = vector; + int newOffset = offset; + int newSize = size; + if (details.isReplaced()) { + if (moveToLast) { + ChampSequenced.ChampSequencedElement oldElem = details.getOldData(); + Tuple2, Integer> result = ChampSequenced.ChampSequencedData.vecRemove(newVector, oldElem, newOffset); + newVector = result._1; + newOffset = result._2; + } + } else { + newSize++; + } + newVector = newVector.append(newElem); + return renumber(newRoot, newVector, newSize, newOffset); + } + return this; } /** @@ -504,40 +634,30 @@ public LinkedHashSet add(T element) { * @param elements The elements to be added. * @return A new set containing all elements of this set and the given {@code elements}, if not already contained. */ + @SuppressWarnings("unchecked") @Override public LinkedHashSet addAll(Iterable elements) { - Objects.requireNonNull(elements, "elements is null"); - if (isEmpty() && elements instanceof LinkedHashSet) { - @SuppressWarnings("unchecked") - final LinkedHashSet set = (LinkedHashSet) elements; - return set; - } - final LinkedHashMap that = addAll(map, elements); - if (that.size() == map.size()) { - return this; - } else { - return new LinkedHashSet<>(that); + if(isEmpty()&&elements instanceof LinkedHashSet){ + return (LinkedHashSet) elements; } + TransientLinkedHashSet t = toTransient(); + t.addAll(elements); + return t.root==this.root?this: t.toImmutable(); } @Override public LinkedHashSet collect(PartialFunction partialFunction) { - return ofAll(iterator(). collect(partialFunction)); + return ofAll(iterator().collect(partialFunction)); } @Override public boolean contains(T element) { - return map.get(element).isDefined(); + return root.find(new ChampSequenced.ChampSequencedElement<>(element), Objects.hashCode(element), 0, Objects::equals) != ChampTrie.Node.NO_DATA; } @Override public LinkedHashSet diff(Set elements) { - Objects.requireNonNull(elements, "elements is null"); - if (isEmpty() || elements.isEmpty()) { - return this; - } else { - return removeAll(elements); - } + return removeAll(elements); } @Override @@ -562,7 +682,7 @@ public LinkedHashSet drop(int n) { if (n <= 0) { return this; } else { - return LinkedHashSet.ofAll(iterator().drop(n)); + return LinkedHashSet.ofAll(iterator(n)); } } @@ -590,9 +710,9 @@ public LinkedHashSet dropWhile(Predicate predicate) { @Override public LinkedHashSet filter(Predicate predicate) { - Objects.requireNonNull(predicate, "predicate is null"); - final LinkedHashSet filtered = LinkedHashSet.ofAll(iterator().filter(predicate)); - return filtered.length() == length() ? this : filtered; + TransientLinkedHashSet t = toTransient(); + t.filterAll(predicate); + return t.root==this.root?this: t.toImmutable(); } @Override @@ -604,13 +724,13 @@ public LinkedHashSet filterNot(Predicate predicate) { @Override public LinkedHashSet flatMap(Function> mapper) { Objects.requireNonNull(mapper, "mapper is null"); - if (isEmpty()) { - return empty(); - } else { - final LinkedHashMap that = foldLeft(LinkedHashMap.empty(), - (tree, t) -> addAll(tree, mapper.apply(t))); - return new LinkedHashSet<>(that); + LinkedHashSet flatMapped = empty(); + for (T t : this) { + for (U u : mapper.apply(t)) { + flatMapped = flatMapped.add(u); + } } + return flatMapped; } @Override @@ -634,26 +754,26 @@ public boolean hasDefiniteSize() { return true; } + @SuppressWarnings("unchecked") @Override public T head() { - if (map.isEmpty()) { - throw new NoSuchElementException("head of empty set"); - } - return map.head()._1(); + return ((ChampSequenced.ChampSequencedElement) vector.head()).getElement(); } @Override public Option headOption() { - return map.headOption().map(Tuple2::_1); + return isEmpty() ? Option.none() : Option.some(head()); } @Override public LinkedHashSet init() { - if (map.isEmpty()) { - throw new UnsupportedOperationException("tail of empty set"); - } else { - return new LinkedHashSet<>(map.init()); + // XXX Traversable.init() specifies that we must throw + // UnsupportedOperationException instead of NoSuchElementException + // when this set is empty. + if (isEmpty()) { + throw new UnsupportedOperationException(); } + return remove(last()); } @Override @@ -663,12 +783,7 @@ public Option> initOption() { @Override public LinkedHashSet intersect(Set elements) { - Objects.requireNonNull(elements, "elements is null"); - if (isEmpty() || elements.isEmpty()) { - return empty(); - } else { - return retainAll(elements); - } + return retainAll(elements); } /** @@ -683,7 +798,7 @@ public boolean isAsync() { @Override public boolean isEmpty() { - return map.isEmpty(); + return size == 0; } /** @@ -708,31 +823,32 @@ public boolean isSequential() { @Override public Iterator iterator() { - return map.iterator().map(t -> t._1); + return new ChampIteration.IteratorFacade<>(spliterator()); } + Iterator iterator(int startIndex) { + return new ChampIteration.IteratorFacade<>(spliterator(startIndex)); + } + + @SuppressWarnings("unchecked") @Override public T last() { - return map.last()._1; + return ((ChampSequenced.ChampSequencedElement) vector.last()).getElement(); } @Override public int length() { - return map.size(); + return size; } @Override public LinkedHashSet map(Function mapper) { Objects.requireNonNull(mapper, "mapper is null"); - if (isEmpty()) { - return empty(); - } else { - final LinkedHashMap that = foldLeft(LinkedHashMap.empty(), (tree, t) -> { - final U u = mapper.apply(t); - return tree.put(u, u); - }); - return new LinkedHashSet<>(that); + LinkedHashSet mapped = empty(); + for (T t : this) { + mapped = mapped.add(mapper.apply(t)); } + return mapped; } @Override @@ -766,25 +882,110 @@ public LinkedHashSet peek(Consumer action) { @Override public LinkedHashSet remove(T element) { - final LinkedHashMap newMap = map.remove(element); - return (newMap == map) ? this : new LinkedHashSet<>(newMap); + int keyHash = Objects.hashCode(element); + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent>(); + ChampTrie.BitmapIndexedNode> newRoot = root.remove(null, + new ChampSequenced.ChampSequencedElement<>(element), + keyHash, 0, details, Objects::equals); + if (details.isModified()) { + ChampSequenced.ChampSequencedElement removedElem = details.getOldDataNonNull(); + Tuple2, Integer> result = ChampSequenced.ChampSequencedData.vecRemove(vector, removedElem, offset); + return renumber(newRoot, result._1, size - 1, + result._2); + } + return this; } @Override public LinkedHashSet removeAll(Iterable elements) { - return Collections.removeAll(this, elements); + TransientLinkedHashSet t = toTransient(); + t.removeAll(elements); + return t.root==this.root?this: t.toImmutable(); + } + + /** + * Renumbers the sequenced elements in the trie if necessary. + * + * @param root the root of the trie + * @param vector the root of the vector + * @param size the size of the trie + * @param offset the offset that must be added to a sequence number to get the index into the vector + * @return a new {@link LinkedHashSet} instance + */ + private LinkedHashSet renumber( + ChampTrie.BitmapIndexedNode> root, + Vector vector, + int size, int offset) { + + if (ChampSequenced.ChampSequencedData.vecMustRenumber(size, offset, this.vector.size())) { + ChampTrie.IdentityObject owner = new ChampTrie.IdentityObject(); + Tuple2>, Vector> result = ChampSequenced.ChampSequencedData.>vecRenumber( + size, root, vector, owner, Objects::hashCode, Objects::equals, + (e, seq) -> new ChampSequenced.ChampSequencedElement<>(e.getElement(), seq)); + return new LinkedHashSet<>( + result._1(), result._2(), + size, 0); + } + return new LinkedHashSet<>(root, vector, size, offset); } @Override public LinkedHashSet replace(T currentElement, T newElement) { - if (!Objects.equals(currentElement, newElement) && contains(currentElement)) { - final Tuple2 currentPair = Tuple.of(currentElement, currentElement); - final Tuple2 newPair = Tuple.of(newElement, newElement); - final LinkedHashMap newMap = map.replace(currentPair, newPair); - return new LinkedHashSet<>(newMap); - } else { + // currentElement and newElem are the same => do nothing + if (Objects.equals(currentElement, newElement)) { return this; } + + // try to remove currentElem from the 'root' trie + final ChampTrie.ChangeEvent> detailsCurrent = new ChampTrie.ChangeEvent<>(); + ChampTrie.IdentityObject owner = new ChampTrie.IdentityObject(); + ChampTrie.BitmapIndexedNode> newRoot = root.remove(owner, + new ChampSequenced.ChampSequencedElement<>(currentElement), + Objects.hashCode(currentElement), 0, detailsCurrent, Objects::equals); + // currentElement was not in the 'root' trie => do nothing + if (!detailsCurrent.isModified()) { + return this; + } + + // currentElement was in the 'root' trie, and we have just removed it + // => also remove its entry from the 'sequenceRoot' trie + Vector newVector = vector; + int newOffset = offset; + ChampSequenced.ChampSequencedElement currentData = detailsCurrent.getOldData(); + int seq = currentData.getSequenceNumber(); + Tuple2, Integer> result = ChampSequenced.ChampSequencedData.vecRemove(newVector, currentData, newOffset); + newVector = result._1; + newOffset = result._2; + + // try to update the trie with the newElement + ChampTrie.ChangeEvent> detailsNew = new ChampTrie.ChangeEvent<>(); + ChampSequenced.ChampSequencedElement newData = new ChampSequenced.ChampSequencedElement<>(newElement, seq); + newRoot = newRoot.put(owner, + newData, Objects.hashCode(newElement), 0, detailsNew, + ChampSequenced.ChampSequencedElement::forceUpdate, + Objects::equals, Objects::hashCode); + boolean isReplaced = detailsNew.isReplaced(); + + // there already was an element with key newElement._1 in the trie, and we have just replaced it + // => remove the replaced entry from the 'sequenceRoot' trie + if (isReplaced) { + ChampSequenced.ChampSequencedElement replacedEntry = detailsNew.getOldData(); + result = ChampSequenced.ChampSequencedData.vecRemove(newVector, replacedEntry, newOffset); + newVector = result._1; + newOffset = result._2; + } + + // we have just successfully added or replaced the newElement + // => insert the new entry in the 'sequenceRoot' trie + newVector = seq + newOffset < newVector.size() ? newVector.update(seq + newOffset, newData) : newVector.append(newData); + + if (isReplaced) { + // we reduced the size of the map by one => renumbering may be necessary + return renumber(newRoot, newVector, size - 1, newOffset); + } else { + // we did not change the size of the map => no renumbering is needed + return new LinkedHashSet<>(newRoot, newVector, size, newOffset); + } } @Override @@ -794,7 +995,21 @@ public LinkedHashSet replaceAll(T currentElement, T newElement) { @Override public LinkedHashSet retainAll(Iterable elements) { - return Collections.retainAll(this, elements); + TransientLinkedHashSet t = toTransient(); + t.retainAll(elements); + return t.root==this.root?this: t.toImmutable(); + } + + + private Iterator reverseIterator() { + return new ChampIteration.IteratorFacade<>(reverseSpliterator()); + } + + @SuppressWarnings("unchecked") + private Spliterator reverseSpliterator() { + return new ChampSequenced.ChampReverseVectorSpliterator<>(vector, + e -> ((ChampSequenced.ChampSequencedElement) e).getElement(), + 0, size(), Spliterator.SIZED | Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.IMMUTABLE); } @Override @@ -834,12 +1049,24 @@ public Tuple2, LinkedHashSet> span(Predicate pred return Tuple.of(LinkedHashSet.ofAll(t._1), LinkedHashSet.ofAll(t._2)); } + @SuppressWarnings("unchecked") + @Override + public Spliterator spliterator() { + return spliterator(0); + } + + @SuppressWarnings("unchecked") + Spliterator spliterator(int startIndex) { + return new ChampSequenced.ChampVectorSpliterator<>(vector, + e -> ((ChampSequenced.ChampSequencedElement) e).getElement(), + startIndex, size(), Spliterator.SIZED | Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.IMMUTABLE); + } + @Override public LinkedHashSet tail() { - if (map.isEmpty()) { - throw new UnsupportedOperationException("tail of empty set"); - } - return wrap(map.tail()); + // XXX AbstractTraversableTest.shouldThrowWhenTailOnNil requires that we throw UnsupportedOperationException instead of NoSuchElementException. + if (isEmpty()) throw new UnsupportedOperationException(); + return remove(head()); } @Override @@ -849,7 +1076,7 @@ public Option> tailOption() { @Override public LinkedHashSet take(int n) { - if (map.size() <= n) { + if (size() <= n) { return this; } return LinkedHashSet.ofAll(() -> iterator().take(n)); @@ -857,7 +1084,7 @@ public LinkedHashSet take(int n) { @Override public LinkedHashSet takeRight(int n) { - if (map.size() <= n) { + if (size() <= n) { return this; } return LinkedHashSet.ofAll(() -> iterator().takeRight(n)); @@ -891,39 +1118,24 @@ public U transform(Function, ? extends U> f) { @Override public java.util.LinkedHashSet toJavaSet() { + // XXX If the return value was not required to be a java.util.LinkedHashSet + // we could provide a mutable LinkedHashSet in O(1) return toJavaSet(java.util.LinkedHashSet::new); } + TransientLinkedHashSet toTransient() { + return new TransientLinkedHashSet<>(this); + } + /** - * Adds all of the elements of {@code elements} to this set, replacing existing ones if they already present. - *

- * Note that this method has a worst-case quadratic complexity. - *

- * See also {@link #addAll(Iterable)}. + * Adds all of the elements of {@code that} set to this set, if not already present. * * @param elements The set to form the union with. * @return A new set that contains all distinct elements of this and {@code elements} set. */ - @SuppressWarnings("unchecked") @Override public LinkedHashSet union(Set elements) { - Objects.requireNonNull(elements, "elements is null"); - if (isEmpty()) { - if (elements instanceof LinkedHashSet) { - return (LinkedHashSet) elements; - } else { - return LinkedHashSet.ofAll(elements); - } - } else if (elements.isEmpty()) { - return this; - } else { - final LinkedHashMap that = addAll(map, elements); - if (that.size() == map.size()) { - return this; - } else { - return new LinkedHashSet<>(that); - } - } + return addAll(elements); } @Override @@ -977,15 +1189,6 @@ public String toString() { return mkString(stringPrefix() + "(", ", ", ")"); } - private static LinkedHashMap addAll(LinkedHashMap initial, - Iterable additional) { - LinkedHashMap that = initial; - for (T t : additional) { - that = that.put(t, t); - } - return that; - } - // -- Serialization /** @@ -997,7 +1200,7 @@ private static LinkedHashMap addAll(LinkedHashMap init * @return A SerializationProxy for this enclosing class. */ private Object writeReplace() { - return new SerializationProxy<>(this.map); + return new SerializationProxy<>(this); } /** @@ -1025,7 +1228,7 @@ private static final class SerializationProxy implements Serializable { private static final long serialVersionUID = 1L; // the instance to be serialized/deserialized - private transient LinkedHashMap map; + private transient LinkedHashSet set; /** * Constructor for the case of serialization, called by {@link LinkedHashSet#writeReplace()}. @@ -1033,10 +1236,10 @@ private static final class SerializationProxy implements Serializable { * The constructor of a SerializationProxy takes an argument that concisely represents the logical state of * an instance of the enclosing class. * - * @param map a Cons + * @param set a Cons */ - SerializationProxy(LinkedHashMap map) { - this.map = map; + SerializationProxy(LinkedHashSet set) { + this.set = set; } /** @@ -1047,9 +1250,9 @@ private static final class SerializationProxy implements Serializable { */ private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); - s.writeInt(map.size()); - for (Tuple2 e : map) { - s.writeObject(e._1); + s.writeInt(set.size()); + for (T e : set) { + s.writeObject(e); } } @@ -1067,13 +1270,12 @@ private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOEx if (size < 0) { throw new InvalidObjectException("No elements"); } - LinkedHashMap temp = LinkedHashMap.empty(); + LinkedHashSet temp = LinkedHashSet.empty(); for (int i = 0; i < size; i++) { - @SuppressWarnings("unchecked") - final T element = (T) s.readObject(); - temp = temp.put(element, element); + @SuppressWarnings("unchecked") final T element = (T) s.readObject(); + temp = temp.add(element); } - map = temp; + set = temp; } /** @@ -1086,7 +1288,176 @@ private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOEx * @return A deserialized instance of the enclosing class. */ private Object readResolve() { - return map.isEmpty() ? LinkedHashSet.empty() : new LinkedHashSet<>(map); + return LinkedHashSet.empty().addAll(set); + } + } + + /** + * Supports efficient bulk-operations on a linked hash set through transience. + * + * @param the element type + */ + static class TransientLinkedHashSet extends ChampTransience.ChampAbstractTransientSet> { + int offset; + Vector vector; + + TransientLinkedHashSet(LinkedHashSet s) { + root = s.root; + size = s.size; + this.vector = s.vector; + this.offset = s.offset; + } + + TransientLinkedHashSet() { + this(empty()); + } + + @Override + void clear() { + root = emptyNode(); + vector = Vector.empty(); + size = 0; + modCount++; + offset = -1; + } + + + public LinkedHashSet toImmutable() { + owner = null; + return isEmpty() + ? empty() + : new LinkedHashSet<>(root, vector, size, offset); + } + + boolean add(E element) { + return addLast(element, false); + } + + private boolean addLast(E e, boolean moveToLast) { + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent>(); + ChampSequenced.ChampSequencedElement newElem = new ChampSequenced.ChampSequencedElement(e, vector.size() - offset); + root = root.put(makeOwner(), newElem, + Objects.hashCode(e), 0, details, + moveToLast ? ChampSequenced.ChampSequencedElement::updateAndMoveToLast : ChampSequenced.ChampSequencedElement::update, + Objects::equals, Objects::hashCode); + if (details.isModified()) { + + if (details.isReplaced()) { + if (moveToLast) { + ChampSequenced.ChampSequencedElement oldElem = details.getOldData(); + Tuple2, Integer> result = vecRemove(vector, oldElem, offset); + vector = result._1; + offset = result._2; + } + } else { + size++; + } + vector = vector.append(newElem); + renumber(); + return true; + } + return false; + } + + @SuppressWarnings("unchecked") + boolean addAll(Iterable c) { + if (c == root) { + return false; + } + if (isEmpty() && (c instanceof LinkedHashSet)) { + LinkedHashSet cc = (LinkedHashSet) c; + root = (ChampTrie.BitmapIndexedNode>) (ChampTrie.BitmapIndexedNode) cc.root; + size = cc.size; + return true; + } + boolean modified = false; + for (E e : c) { + modified |= add(e); + } + return modified; + } + + @Override + java.util.Iterator iterator() { + return new ChampIteration.IteratorFacade<>(spliterator()); + } + + @SuppressWarnings("unchecked") + Spliterator spliterator() { + return new ChampSequenced.ChampVectorSpliterator<>(vector, + (Object o) -> ((ChampSequenced.ChampSequencedElement) o).getElement(), 0, + size(), Spliterator.SIZED | Spliterator.DISTINCT | Spliterator.ORDERED); + } + + @SuppressWarnings("unchecked") + @Override + boolean remove(Object element) { + int keyHash = Objects.hashCode(element); + ChampTrie.ChangeEvent> details = new ChampTrie.ChangeEvent>(); + root = root.remove(makeOwner(), + new ChampSequenced.ChampSequencedElement<>((E) element), + keyHash, 0, details, Objects::equals); + if (details.isModified()) { + ChampSequenced.ChampSequencedElement removedElem = details.getOldDataNonNull(); + Tuple2, Integer> result = vecRemove(vector, removedElem, offset); + vector = result._1; + offset = result._2; + size--; + renumber(); + return true; + } + return false; + } + + + private void renumber() { + if (ChampSequenced.ChampSequencedData.vecMustRenumber(size, offset, vector.size())) { + ChampTrie.IdentityObject owner = new ChampTrie.IdentityObject(); + Tuple2>, Vector> result = ChampSequenced.ChampSequencedData.>vecRenumber( + size, root, vector, owner, Objects::hashCode, Objects::equals, + (e, seq) -> new ChampSequenced.ChampSequencedElement<>(e.getElement(), seq)); + root = result._1; + vector = result._2; + offset = 0; + } + } + + static class VectorSideEffectPredicate implements Predicate> { + Vector newVector; + int newOffset; + Predicate predicate; + + public VectorSideEffectPredicate(Predicate predicate, Vector vector, int offset) { + this.predicate = predicate; + this.newVector = vector; + this.newOffset = offset; + } + + @Override + public boolean test(ChampSequenced.ChampSequencedElement e) { + if (!predicate.test(e.getElement())) { + Tuple2, Integer> result = vecRemove(newVector, e, newOffset); + newVector = result._1; + newOffset = result._2; + return false; + } + return true; + } + } + + boolean filterAll(Predicate predicate) { + VectorSideEffectPredicate vp = new VectorSideEffectPredicate<>(predicate, vector, offset); + ChampTrie.BulkChangeEvent bulkChange = new ChampTrie.BulkChangeEvent(); + ChampTrie.BitmapIndexedNode> newRootNode = root.filterAll(makeOwner(), vp, 0, bulkChange); + if (bulkChange.removed == 0) { + return false; + } + root = newRootNode; + vector = vp.newVector; + offset = vp.newOffset; + size -= bulkChange.removed; + modCount++; + return true; } } } diff --git a/src/main/java/io/vavr/collection/Vector.java b/src/main/java/io/vavr/collection/Vector.java index 4f30580c8..25afd9580 100644 --- a/src/main/java/io/vavr/collection/Vector.java +++ b/src/main/java/io/vavr/collection/Vector.java @@ -26,16 +26,28 @@ */ package io.vavr.collection; -import io.vavr.*; +import io.vavr.PartialFunction; +import io.vavr.Tuple; +import io.vavr.Tuple2; import io.vavr.collection.JavaConverters.ListView; import io.vavr.collection.VectorModule.Combinations; import io.vavr.control.Option; import java.io.Serializable; -import java.util.*; -import java.util.function.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Random; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collector; +import static io.vavr.collection.ChampTrie.ChampListHelper.checkIndex; import static io.vavr.collection.Collections.withSize; import static io.vavr.collection.JavaConverters.ChangePolicy.IMMUTABLE; import static io.vavr.collection.JavaConverters.ChangePolicy.MUTABLE; @@ -43,7 +55,7 @@ /** * Vector is the default Seq implementation that provides effectively constant time access to any element. * Many other operations (e.g. `tail`, `drop`, `slice`) are also effectively constant. - * + *

* The implementation is based on a `bit-mapped trie`, a very wide and shallow tree (i.e. depth ≤ 6). * * @param Component type of the Vector. @@ -54,19 +66,22 @@ public final class Vector implements IndexedSeq, Serializable { private static final Vector EMPTY = new Vector<>(BitMappedTrie.empty()); final BitMappedTrie trie; - private Vector(BitMappedTrie trie) { this.trie = trie; } + + private Vector(BitMappedTrie trie) { + this.trie = trie; + } @SuppressWarnings("ObjectEquality") private Vector wrap(BitMappedTrie trie) { return (trie == this.trie) - ? this - : ofAll(trie); + ? this + : ofAll(trie); } private static Vector ofAll(BitMappedTrie trie) { return (trie.length() == 0) - ? empty() - : new Vector<>(trie); + ? empty() + : new Vector<>(trie); } /** @@ -76,7 +91,9 @@ private static Vector ofAll(BitMappedTrie trie) { * @return The empty Vector. */ @SuppressWarnings("unchecked") - public static Vector empty() { return (Vector) EMPTY; } + public static Vector empty() { + return (Vector) EMPTY; + } /** * Returns a {@link Collector} which may be used in conjunction with @@ -100,7 +117,9 @@ public static Collector, Vector> collector() { * @return the given {@code vector} instance as narrowed type {@code Vector}. */ @SuppressWarnings("unchecked") - public static Vector narrow(Vector vector) { return (Vector) vector; } + public static Vector narrow(Vector vector) { + return (Vector) vector; + } /** * Returns a singleton {@code Vector}, i.e. a {@code Vector} of one element. @@ -306,15 +325,15 @@ public static Vector ofAll(short... elements) { } public static Vector range(char from, char toExclusive) { - return ofAll(ArrayType. asPrimitives(char.class, Iterator.range(from, toExclusive))); + return ofAll(ArrayType.asPrimitives(char.class, Iterator.range(from, toExclusive))); } public static Vector rangeBy(char from, char toExclusive, int step) { - return ofAll(ArrayType. asPrimitives(char.class, Iterator.rangeBy(from, toExclusive, step))); + return ofAll(ArrayType.asPrimitives(char.class, Iterator.rangeBy(from, toExclusive, step))); } public static Vector rangeBy(double from, double toExclusive, double step) { - return ofAll(ArrayType. asPrimitives(double.class, Iterator.rangeBy(from, toExclusive, step))); + return ofAll(ArrayType.asPrimitives(double.class, Iterator.rangeBy(from, toExclusive, step))); } /** @@ -334,7 +353,7 @@ public static Vector rangeBy(double from, double toExclusive, double ste * @return a range of int values as specified or the empty range if {@code from >= toExclusive} */ public static Vector range(int from, int toExclusive) { - return ofAll(ArrayType. asPrimitives(int.class, Iterator.range(from, toExclusive))); + return ofAll(ArrayType.asPrimitives(int.class, Iterator.range(from, toExclusive))); } /** @@ -360,7 +379,7 @@ public static Vector range(int from, int toExclusive) { * @throws IllegalArgumentException if {@code step} is zero */ public static Vector rangeBy(int from, int toExclusive, int step) { - return ofAll(ArrayType. asPrimitives(int.class, Iterator.rangeBy(from, toExclusive, step))); + return ofAll(ArrayType.asPrimitives(int.class, Iterator.rangeBy(from, toExclusive, step))); } /** @@ -380,7 +399,7 @@ public static Vector rangeBy(int from, int toExclusive, int step) { * @return a range of long values as specified or the empty range if {@code from >= toExclusive} */ public static Vector range(long from, long toExclusive) { - return ofAll(ArrayType. asPrimitives(long.class, Iterator.range(from, toExclusive))); + return ofAll(ArrayType.asPrimitives(long.class, Iterator.range(from, toExclusive))); } /** @@ -406,19 +425,19 @@ public static Vector range(long from, long toExclusive) { * @throws IllegalArgumentException if {@code step} is zero */ public static Vector rangeBy(long from, long toExclusive, long step) { - return ofAll(ArrayType. asPrimitives(long.class, Iterator.rangeBy(from, toExclusive, step))); + return ofAll(ArrayType.asPrimitives(long.class, Iterator.rangeBy(from, toExclusive, step))); } public static Vector rangeClosed(char from, char toInclusive) { - return ofAll(ArrayType. asPrimitives(char.class, Iterator.rangeClosed(from, toInclusive))); + return ofAll(ArrayType.asPrimitives(char.class, Iterator.rangeClosed(from, toInclusive))); } public static Vector rangeClosedBy(char from, char toInclusive, int step) { - return ofAll(ArrayType. asPrimitives(char.class, Iterator.rangeClosedBy(from, toInclusive, step))); + return ofAll(ArrayType.asPrimitives(char.class, Iterator.rangeClosedBy(from, toInclusive, step))); } public static Vector rangeClosedBy(double from, double toInclusive, double step) { - return ofAll(ArrayType. asPrimitives(double.class, Iterator.rangeClosedBy(from, toInclusive, step))); + return ofAll(ArrayType.asPrimitives(double.class, Iterator.rangeClosedBy(from, toInclusive, step))); } /** @@ -438,7 +457,7 @@ public static Vector rangeClosedBy(double from, double toInclusive, doub * @return a range of int values as specified or the empty range if {@code from > toInclusive} */ public static Vector rangeClosed(int from, int toInclusive) { - return ofAll(ArrayType. asPrimitives(int.class, Iterator.rangeClosed(from, toInclusive))); + return ofAll(ArrayType.asPrimitives(int.class, Iterator.rangeClosed(from, toInclusive))); } /** @@ -464,7 +483,7 @@ public static Vector rangeClosed(int from, int toInclusive) { * @throws IllegalArgumentException if {@code step} is zero */ public static Vector rangeClosedBy(int from, int toInclusive, int step) { - return ofAll(ArrayType. asPrimitives(int.class, Iterator.rangeClosedBy(from, toInclusive, step))); + return ofAll(ArrayType.asPrimitives(int.class, Iterator.rangeClosedBy(from, toInclusive, step))); } /** @@ -484,7 +503,7 @@ public static Vector rangeClosedBy(int from, int toInclusive, int step) * @return a range of long values as specified or the empty range if {@code from > toInclusive} */ public static Vector rangeClosed(long from, long toInclusive) { - return ofAll(ArrayType. asPrimitives(long.class, Iterator.rangeClosed(from, toInclusive))); + return ofAll(ArrayType.asPrimitives(long.class, Iterator.rangeClosed(from, toInclusive))); } /** @@ -510,20 +529,20 @@ public static Vector rangeClosed(long from, long toInclusive) { * @throws IllegalArgumentException if {@code step} is zero */ public static Vector rangeClosedBy(long from, long toInclusive, long step) { - return ofAll(ArrayType. asPrimitives(long.class, Iterator.rangeClosedBy(from, toInclusive, step))); + return ofAll(ArrayType.asPrimitives(long.class, Iterator.rangeClosedBy(from, toInclusive, step))); } /** * Transposes the rows and columns of a {@link Vector} matrix. * - * @param matrix element type + * @param matrix element type * @param matrix to be transposed. * @return a transposed {@link Vector} matrix. * @throws IllegalArgumentException if the row lengths of {@code matrix} differ. - *

- * ex: {@code - * Vector.transpose(Vector(Vector(1,2,3), Vector(4,5,6))) → Vector(Vector(1,4), Vector(2,5), Vector(3,6)) - * } + *

+ * ex: {@code + * Vector.transpose(Vector(Vector(1,2,3), Vector(4,5,6))) → Vector(Vector(1,4), Vector(2,5), Vector(3,6)) + * } */ public static Vector> transpose(Vector> matrix) { return io.vavr.collection.Collections.transpose(matrix, Vector::ofAll, Vector::of); @@ -616,7 +635,9 @@ public static Vector unfold(T seed, Function append(T element) { return appendAll(io.vavr.collection.List.of(element)); } + public Vector append(T element) { + return appendAll(io.vavr.collection.List.of(element)); + } @Override public Vector appendAll(Iterable iterable) { @@ -624,7 +645,7 @@ public Vector appendAll(Iterable iterable) { if (isEmpty()) { return ofAll(iterable); } - if (io.vavr.collection.Collections.isEmpty(iterable)){ + if (io.vavr.collection.Collections.isEmpty(iterable)) { return this; } return new Vector<>(trie.appendAll(iterable)); @@ -652,20 +673,28 @@ public Vector asJavaMutable(Consumer> action) { @Override public Vector collect(PartialFunction partialFunction) { - return ofAll(iterator(). collect(partialFunction)); + return ofAll(iterator().collect(partialFunction)); } @Override - public Vector> combinations() { return rangeClosed(0, length()).map(this::combinations).flatMap(Function.identity()); } + public Vector> combinations() { + return rangeClosed(0, length()).map(this::combinations).flatMap(Function.identity()); + } @Override - public Vector> combinations(int k) { return Combinations.apply(this, Math.max(k, 0)); } + public Vector> combinations(int k) { + return Combinations.apply(this, Math.max(k, 0)); + } @Override - public Iterator> crossProduct(int power) { return io.vavr.collection.Collections.crossProduct(empty(), this, power); } + public Iterator> crossProduct(int power) { + return io.vavr.collection.Collections.crossProduct(empty(), this, power); + } @Override - public Vector distinct() { return distinctBy(Function.identity()); } + public Vector distinct() { + return distinctBy(Function.identity()); + } @Override public Vector distinctBy(Comparator comparator) { @@ -752,13 +781,19 @@ public Seq> group() { } @Override - public Map> groupBy(Function classifier) { return io.vavr.collection.Collections.groupBy(this, classifier, Vector::ofAll); } + public Map> groupBy(Function classifier) { + return io.vavr.collection.Collections.groupBy(this, classifier, Vector::ofAll); + } @Override - public Iterator> grouped(int size) { return sliding(size, size); } + public Iterator> grouped(int size) { + return sliding(size, size); + } @Override - public boolean hasDefiniteSize() { return true; } + public boolean hasDefiniteSize() { + return true; + } @Override public int indexOf(T element, int from) { @@ -780,10 +815,14 @@ public Vector init() { } @Override - public Option> initOption() { return isEmpty() ? Option.none() : Option.some(init()); } + public Option> initOption() { + return isEmpty() ? Option.none() : Option.some(init()); + } @Override - public Vector insert(int index, T element) { return insertAll(index, Iterator.of(element)); } + public Vector insert(int index, T element) { + return insertAll(index, Iterator.of(element)); + } @Override public Vector insertAll(int index, Iterable elements) { @@ -792,15 +831,17 @@ public Vector insertAll(int index, Iterable elements) { final Vector begin = take(index).appendAll(elements); final Vector end = drop(index); return (begin.size() > end.size()) - ? begin.appendAll(end) - : end.prependAll(begin); + ? begin.appendAll(end) + : end.prependAll(begin); } else { throw new IndexOutOfBoundsException("insert(" + index + ", e) on Vector of length " + length()); } } @Override - public Vector intersperse(T element) { return ofAll(iterator().intersperse(element)); } + public Vector intersperse(T element) { + return ofAll(iterator().intersperse(element)); + } /** * A {@code Vector} is computed synchronously. @@ -813,7 +854,9 @@ public boolean isAsync() { } @Override - public boolean isEmpty() { return length() == 0; } + public boolean isEmpty() { + return length() == 0; + } /** * A {@code Vector} is computed eagerly. @@ -826,12 +869,14 @@ public boolean isLazy() { } @Override - public boolean isTraversableAgain() { return true; } + public boolean isTraversableAgain() { + return true; + } @Override public Iterator iterator() { return isEmpty() ? Iterator.empty() - : trie.iterator(); + : trie.iterator(); } @Override @@ -845,7 +890,9 @@ public int lastIndexOf(T element, int end) { } @Override - public int length() { return trie.length(); } + public int length() { + return trie.length(); + } @Override public Vector map(Function mapper) { @@ -867,8 +914,8 @@ public Vector orElse(Supplier> supplier) { public Vector padTo(int length, T element) { final int actualLength = length(); return (length <= actualLength) - ? this - : appendAll(Iterator.continually(element) + ? this + : appendAll(Iterator.continually(element) .take(length - actualLength)); } @@ -931,7 +978,9 @@ public Vector> permutations() { } @Override - public Vector prepend(T element) { return prependAll(io.vavr.collection.List.of(element)); } + public Vector prepend(T element) { + return prependAll(io.vavr.collection.List.of(element)); + } @Override public Vector prependAll(Iterable iterable) { @@ -939,7 +988,7 @@ public Vector prependAll(Iterable iterable) { if (isEmpty()) { return ofAll(iterable); } - if (io.vavr.collection.Collections.isEmpty(iterable)){ + if (io.vavr.collection.Collections.isEmpty(iterable)) { return this; } return new Vector<>(trie.prependAll(iterable)); @@ -983,8 +1032,8 @@ public Vector removeAt(int index) { final Vector begin = take(index); final Vector end = drop(index + 1); return (begin.size() > end.size()) - ? begin.appendAll(end) - : end.prependAll(begin); + ? begin.appendAll(end) + : end.prependAll(begin); } else { throw new IndexOutOfBoundsException("removeAt(" + index + ")"); } @@ -1000,6 +1049,20 @@ public Vector removeAll(Iterable elements) { return io.vavr.collection.Collections.removeAll(this, elements); } + Vector removeRange(int fromIndex, int toIndex) { + int size = size(); + checkIndex(fromIndex, toIndex + 1); + checkIndex(toIndex, size + 1); + if (fromIndex == 0) { + return slice(toIndex, size); + } + if (toIndex == size) { + return slice(0, fromIndex); + } + final Vector begin = slice(0, fromIndex); + return begin.appendAll(() -> slice(toIndex, size).iterator()); + } + @Override public Vector replace(T currentElement, T newElement) { return indexOfOption(currentElement) @@ -1096,8 +1159,7 @@ public Vector sorted() { if (isEmpty()) { return this; } else { - @SuppressWarnings("unchecked") - final T[] list = (T[]) toJavaArray(); + @SuppressWarnings("unchecked") final T[] list = (T[]) toJavaArray(); Arrays.sort(list); return Vector.of(list); } @@ -1144,7 +1206,7 @@ public Tuple2, Vector> splitAtInclusive(Predicate predic final T value = get(i); if (predicate.test(value)) { return (i == (length() - 1)) ? Tuple.of(this, empty()) - : Tuple.of(take(i + 1), drop(i + 1)); + : Tuple.of(take(i + 1), drop(i + 1)); } } return Tuple.of(this, empty()); @@ -1175,7 +1237,9 @@ public Vector tail() { } @Override - public Option> tailOption() { return isEmpty() ? Option.none() : Option.some(tail()); } + public Option> tailOption() { + return isEmpty() ? Option.none() : Option.some(tail()); + } @Override public Vector take(int n) { @@ -1266,7 +1330,9 @@ public Vector zipWithIndex(BiFunction Vector> apply(Vector elements, int k) { return (k == 0) - ? Vector.of(Vector.empty()) - : elements.zipWithIndex().flatMap( + ? Vector.of(Vector.empty()) + : elements.zipWithIndex().flatMap( t -> apply(elements.drop(t._2 + 1), (k - 1)).map((Vector c) -> c.prepend(t._1))); } } diff --git a/src/main/java/io/vavr/control/Validation.java b/src/main/java/io/vavr/control/Validation.java index 513bda5b7..fccd67dae 100644 --- a/src/main/java/io/vavr/control/Validation.java +++ b/src/main/java/io/vavr/control/Validation.java @@ -26,7 +26,14 @@ */ package io.vavr.control; -import io.vavr.*; +import io.vavr.Function2; +import io.vavr.Function3; +import io.vavr.Function4; +import io.vavr.Function5; +import io.vavr.Function6; +import io.vavr.Function7; +import io.vavr.Function8; +import io.vavr.Value; import io.vavr.collection.Array; import io.vavr.collection.Iterator; import io.vavr.collection.List; @@ -224,6 +231,12 @@ public static Validation, Seq> sequence(Iterable value type in the case of invalid + * @param value type in the case of valid + * @param values An iterable of Validation instances. + * @return A valid Validation of the last value if all Validation instances are valid + * or an invalid Validation containing an accumulated List of errors. + * @throws NullPointerException if values is null */ @SuppressWarnings("varargs") @SafeVarargs diff --git a/src/test/java/io/vavr/collection/HashArrayMappedTrieTest.java b/src/test/java/io/vavr/collection/HashArrayMappedTrieTest.java index 08006dc1e..e69de29bb 100644 --- a/src/test/java/io/vavr/collection/HashArrayMappedTrieTest.java +++ b/src/test/java/io/vavr/collection/HashArrayMappedTrieTest.java @@ -1,234 +0,0 @@ -/* ____ ______________ ________________________ __________ - * \ \/ / \ \/ / __/ / \ \/ / \ - * \______/___/\___\______/___/_____/___/\___\______/___/\___\ - * - * The MIT License (MIT) - * - * Copyright 2024 Vavr, https://vavr.io - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package io.vavr.collection; - -import io.vavr.Tuple; -import io.vavr.Tuple2; -import io.vavr.control.Option; -import org.junit.jupiter.api.Test; - -import java.util.Random; -import java.util.function.Function; - -import static org.assertj.core.api.Assertions.assertThat; - -public class HashArrayMappedTrieTest { - - @Test - public void testLeafSingleton() { - HashArrayMappedTrie hamt = empty(); - hamt = hamt.put(new WeakInteger(1), 1); - assertThat(hamt.get(new WeakInteger(1))).isEqualTo(Option.some(1)); - assertThat(hamt.get(new WeakInteger(11))).isEqualTo(Option.none()); - assertThat(hamt.getOrElse(new WeakInteger(1), 2)).isEqualTo(1); - assertThat(hamt.getOrElse(new WeakInteger(11), 2)).isEqualTo(2); - assertThat(hamt.get(new WeakInteger(2))).isEqualTo(Option.none()); - assertThat(hamt.getOrElse(new WeakInteger(2), 2)).isEqualTo(2); - } - - @Test - public void testLeafList() { - HashArrayMappedTrie hamt = empty(); - hamt = hamt.put(new WeakInteger(1), 1).put(new WeakInteger(31), 31); - assertThat(hamt.get(new WeakInteger(1))).isEqualTo(Option.some(1)); - assertThat(hamt.get(new WeakInteger(11))).isEqualTo(Option.none()); - assertThat(hamt.get(new WeakInteger(31))).isEqualTo(Option.some(31)); - assertThat(hamt.getOrElse(new WeakInteger(1), 2)).isEqualTo(1); - assertThat(hamt.getOrElse(new WeakInteger(11), 2)).isEqualTo(2); - assertThat(hamt.getOrElse(new WeakInteger(31), 2)).isEqualTo(31); - assertThat(hamt.get(new WeakInteger(2))).isEqualTo(Option.none()); - assertThat(hamt.getOrElse(new WeakInteger(2), 2)).isEqualTo(2); - } - - @Test - public void testGetExistingKey() { - HashArrayMappedTrie hamt = empty(); - hamt = hamt.put(1, 2).put(4, 5).put(null, 7); - assertThat(hamt.containsKey(1)).isTrue(); - assertThat(hamt.get(1)).isEqualTo(Option.some(2)); - assertThat(hamt.getOrElse(1, 42)).isEqualTo(2); - assertThat(hamt.containsKey(4)).isTrue(); - assertThat(hamt.get(4)).isEqualTo(Option.some(5)); - assertThat(hamt.containsKey(null)).isTrue(); - assertThat(hamt.get(null)).isEqualTo(Option.some(7)); - } - - @Test - public void testGetUnknownKey() { - HashArrayMappedTrie hamt = empty(); - assertThat(hamt.get(2)).isEqualTo(Option.none()); - assertThat(hamt.getOrElse(2, 42)).isEqualTo(42); - hamt = hamt.put(1, 2).put(4, 5); - assertThat(hamt.containsKey(2)).isFalse(); - assertThat(hamt.get(2)).isEqualTo(Option.none()); - assertThat(hamt.getOrElse(2, 42)).isEqualTo(42); - assertThat(hamt.containsKey(null)).isFalse(); - assertThat(hamt.get(null)).isEqualTo(Option.none()); - } - - @Test - public void testRemoveFromEmpty() { - HashArrayMappedTrie hamt = empty(); - hamt = hamt.remove(1); - assertThat(hamt.size()).isEqualTo(0); - } - - @Test - public void testRemoveUnknownKey() { - HashArrayMappedTrie hamt = empty(); - hamt = hamt.put(1, 2).remove(3); - assertThat(hamt.size()).isEqualTo(1); - hamt = hamt.remove(1); - assertThat(hamt.size()).isEqualTo(0); - } - - @Test - public void testDeepestTree() { - final List ints = List.tabulate(Integer.SIZE, i -> 1 << i).sorted(); - HashArrayMappedTrie hamt = empty(); - hamt = ints.foldLeft(hamt, (h, i) -> h.put(i, i)); - assertThat(List.ofAll(hamt.keysIterator()).sorted()).isEqualTo(ints); - } - - @Test - public void testBigData() { - testBigData(5000, t -> t); - } - - @Test - public void testBigDataWeakHashCode() { - testBigData(5000, t -> Tuple.of(new WeakInteger(t._1), t._2)); - } - - private , V> void testBigData(int count, Function, Tuple2> mapper) { - final Comparator cmp = new Comparator<>(); - final java.util.Map rnd = rnd(count, mapper); - for (java.util.Map.Entry e : rnd.entrySet()) { - cmp.set(e.getKey(), e.getValue()); - } - cmp.test(); - for (K key : new java.util.TreeSet<>(rnd.keySet())) { - rnd.remove(key); - cmp.remove(key); - } - cmp.test(); - } - - @Test - public void shouldLookupNullInZeroKey() { - HashArrayMappedTrie trie = empty(); - // should contain all node types - for (int i = 0; i < 5000; i++) { - trie = trie.put(i, i); - } - trie = trie.put(null, 2); - assertThat(trie.get(0).get()).isEqualTo(0); // key.hashCode = 0 - assertThat(trie.get(null).get()).isEqualTo(2); // key.hashCode = 0 - } - - // - toString - - @Test - public void shouldMakeString() { - assertThat(empty().toString()).isEqualTo("HashArrayMappedTrie()"); - assertThat(empty().put(1, 2).toString()).isEqualTo("HashArrayMappedTrie(1 -> 2)"); - } - - // -- helpers - - private HashArrayMappedTrie of(int... ints) { - HashArrayMappedTrie h = empty(); - for (int i : ints) { - h = h.put(h.size(), i); - } - return h; - } - - private HashArrayMappedTrie empty() { - return HashArrayMappedTrie.empty(); - } - - private class WeakInteger implements Comparable { - final int value; - - @Override - public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } - final WeakInteger that = (WeakInteger) o; - return value == that.value; - } - - WeakInteger(int value) { - this.value = value; - } - - @Override - public int hashCode() { - return Math.abs(value) % 10; - } - - @Override - public int compareTo(WeakInteger other) { - return Integer.compare(value, other.value); - } - } - - private final class Comparator { - private final java.util.Map classic = new java.util.HashMap<>(); - private Map hamt = HashMap.empty(); - - void test() { - assertThat(hamt.size()).isEqualTo(classic.size()); - hamt.iterator().forEachRemaining(e -> assertThat(classic.get(e._1)).isEqualTo(e._2)); - classic.forEach((k, v) -> { - assertThat(hamt.get(k).get()).isEqualTo(v); - assertThat(hamt.getOrElse(k, null)).isEqualTo(v); - }); - } - - void set(K key, V value) { - classic.put(key, value); - hamt = hamt.put(key, value); - } - - void remove(K key) { - classic.remove(key); - hamt = hamt.remove(key); - } - } - - private java.util.Map rnd(int count, Function, Tuple2> mapper) { - final Random r = new Random(); - final java.util.HashMap mp = new java.util.HashMap<>(); - for (int i = 0; i < count; i++) { - final Tuple2 entry = mapper.apply(Tuple.of(r.nextInt(), r.nextInt())); - mp.put(entry._1, entry._2); - } - return mp; - } -}