JEP 431: Sequenced Collections
openjdk.org
openjdk.org
But I find his responses very well thought out and reasoned even though I personally may not like the outcome I can always see that the choice he made is perhaps the least worst option. This is no mean feat for a language still trying to evolve, even rapidly, after 25 years of cruft. This is unlike Brian Goetz’s work in Valhalla etc where you need more expertise to make meaningful suggestions.
Needless to say, very happy with this JEP. My only wish is make these updates come sooner but perhaps you need time to prepare and reason about new stuff instead of adding every little feature that may seem useful at first glance.
https://clojure.org/reference/sequences
Sequenced Collections: "Introduce new interfaces to represent collections with a defined encounter order. Each such collection has a well-defined first element, second element, and so forth, up to the last element. It also provides uniform APIs for accessing its first and last elements, and for processing its elements in reverse order."
Clojure Seq: "Clojure uses the ISeq interface to allow many data structures to provide access to their elements as sequences. The seq function yields an implementation of ISeq appropriate to the collection. Seqs differ from iterators in that they are persistent and immutable, not stateful cursors into a collection. As such, they are useful for much more than foreach - functions can consume and produce seqs, they are thread safe, they can share structure etc."
These ideas are quite old, the main issue has been the decades that have taken to finally reach mainstream.
Why not break it into two interfaces? One for immutable and one with the mutable operations so that this is naturally expressible and checked by the type system?
And it’s been an issue since forever, and this is specifically introducing new super-interfaces, so why miss the opportunity again?
2. People are never going to migrate from List, and so we:
3. Must do the same exact thing again and again, to ensure:
4. That people are never going to migrate from List
At some point you have to stop playing with types and start coding.
Parent comment is right: throwing an exception is surprising behavior and the interface’s design breaks the Liskov substitution principle.
To tell mutable and immutable collections apart, you just need two separate interfaces, a feature available in Java for decades, and an approach widely and routinely used everywhere in the language.
Part of the problem might be that Java's existing interfaces for collections don't have that split. `List`, `Set`, and `Map` already declare mutators, and they're used pervasively.
On the other hand, the Java folks clearly recognize that mutability is the wrong default for modern programming languages, so why double down on the mistakes of the past? They might argue that it makes more sense to follow established patterns than it does to create new ones, because it creates less friction for Java developers.
> Add, put, and UnsupportedOperationException
Not every solution to every problem is worth it. Sometimes it's best to do nothing and spend the time on more worthwhile things, and perhaps a cheaper solution would present itself in the future.
Reading more carefully, the proposal aims to lift the sequenced type higher up the hierarchy, so client code doesn't need to code to specific implementations.
So far, operations available on collection types are influenced by the implementation. So what happens if I call the proposed addFirst() on an ArrayList, rather than a LinkedList? For the former case, I doubt all the elements will get shifted up by one, so would it create a new underlying array?
(Edit: of course it would, the underlying array is recreated for resize operations already. But it would involve a slow array copy operation for what may be a common case operation). I guess my point here is, if you lift operations into a common super-type some of those operations may have implementation challenges for some subtypes.
(Edit 2: turns out it's already possible to insert an element at the head of an ArrayList using add(0, foo) so there's no change to supported methods, just an abstraction of existing APIs, as far as I can tell).
[1] https://docs.oracle.com/en/java/javase/17/docs/api/java.base...
Or getLast on an arraylist (or doubly linked list) versus singly linked.
So suddenly updating from Java N to Java N+1 will no longer compile your code.
> Introducing new methods high in the inheritance hierarchy runs the risk of clashes over obvious method names such as reversed and getFirst.
> We will analyze a large corpus of Java code in order to assess these risks.
How does JVM handle this at runtime if you don't recompile, though? In CLR, on IL level, interface implementations and overrides both explicitly designate the original method they override/implement - so even if an already-compiled class has a method with the same name and signature as the newly added interface method, it wouldn't be considered an implementation of that method until you recompile the class against the new interface. But in JVM, isn't it handled by name at runtime as well?
[0]: https://publicobject.com/2016/02/08/linkedhashmap-is-always-...
And naturally the whole COM like OO ABIs also support it.
Firstly, assume we have a string lying around:
String string = "mississippi";
Let's create some unsorted collections, from a literal (ish) expression, collecting a stream to a set, and collecting a stream to a map: Set<Integer> literal = Set.of(1, 2, 3);
Set<Integer> collected = string.chars().boxed()
.collect(Collectors.toSet());
Map<Integer, Character> map = IntStream.range(0, string.length()).boxed()
.collect(Collectors.toMap(Function.identity(),
string::charAt));
Pretty easy.Now if we want them to be sorted:
SortedSet<Integer> literal = new TreeSet<>(Set.of(1, 2, 3));
SortedSet<Integer> collected = string.chars().boxed()
.collect(Collectors.toCollection(TreeSet::new));
SortedMap<Integer, Character> map = IntStream.range(0, string.length()).boxed()
.collect(Collectors.toMap(Function.identity(),
string::charAt,
(a, b) -> { throw new UnsupportedOperationException(); },
TreeMap::new));
Firstly, there is no SortedSet.of, so we have to explicitly wrap a literal set in a concrete implementation of SortedSet; as well as being a little less pretty, this means there's no chance for the JDK to use efficient specialised implementations of small constant sets, as it can for unsorted sets, and lists. Secondly, there is no toSortedSet collector, so we have to use the generic collection collector, again with a concrete implementation of SortedSet. Thirdly, there is no toSortedMap collector, so we have to use the generic toMap collector which again takes a concrete map implementation, but also requires us to handle key collisions ourself; this last point is particularly bad, because it is impossible to write a handler which is as good as the one used by the default toMap, because the handler doesn't get to see the colliding key!What if we want them to be sorted in reverse?
SortedSet<Integer> literal = new TreeSet<>(Comparator.reverseOrder());
literal.addAll(Set.of(1, 2, 3));
SortedSet<Integer> collected = string.chars().boxed()
.collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.reverseOrder())));
SortedMap<Integer, Character> map = IntStream.range(0, string.length()).boxed()
.collect(Collectors.toMap(Function.identity(),
string::charAt,
(a, b) -> { throw new UnsupportedOperationException(); },
() -> new TreeMap<>(Comparator.reverseOrder())));
Where we use a collector, we have to expand the map implementation constructor method reference to a lambda, so we can pass in a comparator; mildly annoying but not too bad. But for the literal, there is no constructor which takes both a comparator and elements, so we have to create the set with the comparator, and then add the elements! It becomes impossible to do this in a single expression, or to use an immutable collection, and it's several times the volume of code as the unsorted case.If you want to write the reverse sorted literal as an expression, you can do this:
SortedSet<Integer> literal = Stream.of(1, 2, 3)
.collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.reverseOrder())));
Which is still embarrassing.It's probably the Blub paradox speaking, but I haven't felt the need for an ISequencedCollection<T> in C#.