Why doesn't Java 21's EnumSet implement the new SequencedSet interface?
Asked Answered
X

2

6

When Java 21 introduced sequenced collections, I was really excited to start using them. But then I quickly found that there are various corners of the JDK where it seems like the new sequenced interfaces would naturally apply, but they weren't applied. I couldn't find any acknowledgement of this, or evidence that there is any future plan to further extend the use of the sequenced collection interfaces into more of the JDK. This kind of surprises me, as normally the authors of the JDK tend to keep things very consistent and complete.

A specific example is EnumSet. As far as I can tell, there is no fundamental reason why EnumSet couldn't/shouldn't implement SequencedSet. Is it just that nobody wanted to put in the effort to implement a reversed() method for EnumSet? Or is there something more problematic that prevents it?

Xanthochroism answered 19/1 at 17:58 Comment(6)
It’s not that no one wanted to put in the effort to write a reversed() implementation. It’s that an EnumSet cannot be reversed. EnumSets are always in constant declaration order, no matter what. reversed() would have to return a Set which is not an EnumSet at all.Fading
@Fading To be fair, reversed() doesn't appear to mandate the returned object must be the same type, just a reversed view. For example, TreeSet::reversed is only defined to return a NavigableSet, not necessarily a TreeSet.Titanism
@Travis Check out JDK-6278287 and mail.openjdk.org/pipermail/core-libs-dev/2023-May/106211.htmlTitanism
@Titanism Thanks for those links. Glad to see that I'm not the only one who thinks this makes good sense. The discussion there didn't mention a huge argument for doing it... suppose I have a library interface that demands SequencedSet<T> and does not always operate on enum types. I should be able to pass it an EnumSet rather than awkwardly and inefficiently copying or wrapping the EnumSet.Xanthochroism
@TravisFurrer - If your goal in asking this question is to make a request to the OpenJDK developers ... this is not the right way to do it. Ditto if your goal is to start a discussion.Snowflake
My goal here was only to get an answer to the question I asked. It seemed like maybe there was some technical detail I was missing and needed to learn.Xanthochroism
H
13

Some folks have already dug up the relevant OpenJDK artifacts that relate to this question, namely enhancement request JDK-6278287 and this email thread. Here's a full discussion of the topic.

Fundamentals

First, there's no fundamental reason why EnumSet couldn't implement SequencedSet or even NavigableSet. (And similarly for EnumMap implementing SequencedMap or NavigableMap. Henceforth I'll just talk about the Set types, but the discussion also mostly applies to the corresponding Map types.)

Enum types have a defined ordering, and thus the enums present in an EnumSet will always have a defined encounter order. EnumSet's iterator is already defined to iterate them in order. It would be perfectly sensible to add the SequencedSet operations that operate on the first and last elements and to provide a reversed view.

An interesting wrinkle is what the type of the reversed view should be. The obvious type would be SequencedSet<E extends Enum<E>>. This will work, but this would drop the "EnumSet"-ness from the return type. EnumSet doesn't add any new instance methods over the Set interface, but EnumSet is used in some places in the API, e.g. EnumSet::complementOf. In addition, some of the EnumSet method implementations check the argument type, e.g., they use instanceof RegularEnumSet and choose a fast path implementation for that case. That would be lost if the reversed-view wrapper didn't implement EnumSet. (Well, those implementations could be retrofitted with additional instanceof checks to accommodate the reversed views.)

An alternative would be to modify the specification of EnumSet somehow so that it would allow iteration to be in reverse order. This would allow reversed() to have a return type of EnumSet. This would mitigate the above problems, but it might cause new problems for existing code that relies on iteration of any EnumSet always being in forward order. Such code might be broken if a reversed view were passed to it.

NavigableSet provides a superset of the operations of SequencedSet. In addition to access to first/last elements and reversed view, it also provides various ways to get subset views (headSet, subSet, tailSet). These are also perfectly sensible given that enum values are totally ordered.

Usefulness

The primary use case for EnumSet seems to be as an array of flags, as a replacement for storing bits in an int or long and using bitwise boolean operations. The most common usage mode is to set and query these flags individually. For example, "Is this resource writable?" can be answered by using bitwise operations on an int, or by using EnumSet.contains(). The main advantage of EnumSet over bits is that it's type-safe. Adding SequencedSet doesn't help anything with this use case (but it doesn't hurt, either).

Another use case for EnumSet is as a set of commands or functions. One of the respondents on the email thread talked about using enum values to represent commands and processing them in-order by iterating an EnumSet. It's a different use case, but it's mostly already supported by the currently available forward iteration. He pretty much admitted that retrofitting SequencedSet wouldn't help all that much.

I suppose it's a possibility that somebody might want to operate on the first or last elements of an EnumSet, or iterate it in reverse order, but I haven't heard of such use cases. (That doesn't mean they don't exist of course. If they do exist, I'd appreciate hearing about them.) Thus far, though, this doesn't seem compelling. It's even harder to imagine use cases for the various subset views of NavigableSet.

Cost

Adding a reversed view probably isn't terribly difficult, but it's more than a one-liner. Most of the effort involves adding the reversed view, which often involves a fairly thin wrapper class that mostly delegates to the backing implementation. There might be two wrappers, one for RegularEnumSet and one for JumboEnumSet. Otherwise there's not much work other than some bit twiddling to get the last bit set and to iterate them in reverse order.

In addition, there is the testing and specification work that's required when adding APIs to the JDK. (Most people tend to underestimate the effort involved with this.)

NavigableSet's subset views are rather more work. Implementing the subset views is rather fiddly, and there's no "AbstractNavigableSet" that would make it easy to use a shared implementation.

There is also the need to consider interaction with other potential enhancements, such as a potential unmodifiable EnumSet, which would probably also need companion classes to RegularEnumSet and JumboEnumSet. Adding SequencedSet implementations and reversed views might make it harder to add unmodifiable implementations in the future. This probably isn't insurmountable, but it does add complexity.

Completeness

Since enums are totally ordered, shouldn't a collection of them implement the best type already available, for the sake of completeness? After all, the net API footprint is essentially zero -- no new APIs are being added to the system -- and no new concepts are being added.

I'm a bit sympathetic to this. When you're using a system, you expect that the pieces will fit together orthogonally and interchangeably. You can store enums in regular Collection, SequencedSet, or NavigableSet; but if you store enums in an EnumSet, which is specialized for storing enums, you lose the possibility of those more powerful interfaces. This can impose uncomfortable tradeoffs on users.

On the other hand, there are an arbitrary number of things that could be done in the name of completeness or consistency, and time and effort are in fact scarce. It follows that we can do only a subset of what's ideal, even if that means parts of the system are inconsistent or incomplete. How is that subset chosen? That leads us to....

Priorities

Here's where we have to apply judgment. First, there's the direct cost-benefit tradeoff. The cost of EnumSet implementing SequencedSet is moderate, and the cost of NavigableSet is rather higher. The benefit of implementing SequencedSet seems rather low, and NavigableSet even lower.

There's also the concept of what I'll call net benefit. EnumSet doesn't implement SequencedSet, but what if you needed something that SequencedSet provides, like a reversed view? You could probably write something like

List.copyOf(enumSet).reversed()

It's a bit more code, it involves some memory allocation, some operations are probably less efficient, and it's a copy instead of a view. The point is that getting a reversed view of EnumSet isn't impossible; it's merely a bit inconvenient and a bit less efficient. So, the net benefit of having the EnumSet implement SequencedSet is rather lower.

There's also opportunity cost: if we did this, what other thing would we not do? I'm not going to enumerate all the things that my team and I are working on, but I think they're all pretty important, and likely more important than having EnumSet implement SequencedSet.

One thing I mentioned earlier was the possibility of an unmodifiable EnumSet: see JDK-5039214. (An unmodifiable EnumSet would in fact be immutable, since enum values are immutable.) Given the platform's emphasis on thread-safety and unmodifiable data, this seems fairly important. There are also some reasonable optimizations that this would offer compared to the current "dumb" unmodifiable wrappers. I'd argue that doing this is likely more important than implementing SequencedSet, although I have to admit that nobody is working on this one either. However, if we were to do something in this area, it seems likely we'd want to implement an unmodifiable EnumSet before we enhanced it to implement SequencedSet.

Summary

Sometimes, the answer to "Why wasn't this done?" is something like, well this is a bad idea because.... That isn't the case here.

This case, which is rather more typical, falls into the large category of things that might be good ideas, but whose net benefits seem lower than other things. Thus it isn't being worked on now, but it might be worked on in the future. Or not, depending on what other things happen in the future.

Hormonal answered 23/1 at 6:55 Comment(3)
And also people could donate the code (although OpenJDK doesn’t make that easy). I think it’s quite good that it’s not supporting so much cruft, makes the API easier to understand. (But yes have a of() which produces immutable, please)Julian
For the record, I fully understand that there are difficult priorities at play, and that there is a ton of work that goes into the JDK. I appreciate that process, and highly respect those who are carefully navigating it. The pace of progress has been, and continues to be, impressive! My question here was intended as a purely academic question, to learn if there were subtle technical barriers. I'm happy to learn that there are (mostly) not, and therefore there is hope for the value of sequenced collections to improve over the long term. I'll be rooting for any future JEP along these lines.Xanthochroism
@Julian I'm not sure what exactly you were calling "cruft". I agree efforts to prevent API bloat in the JDK are important. In the case of sequenced collections not applied in all logical places, I personally find it more cognitively taxing to work with that irregularity. It's harder to learn and use sequenced collections. Writing a public interface with sequenced collections requires thinking more about what limitations they will impose upon the callers. I can't see how adding "implements SequencedSet" to EnumSet could be considered as any kind of bloat.Xanthochroism
S
6

Why doesn't Java 21's EnumSet implement the new SequencedSet interface?

Here is my summary of the comments and the sources they link to:

  • There is no (convincing) technical argument why EnumSet can't or shouldn't implement SequencedSet.

  • Implementing reversed() wouldn't be a major problem. It is just code.

So why doesn't it?

Well ... unless someone like Stuart Marks gives us a definitive answer, we don't and cannot know what the real reasons were! My guess is that the reasons were some combination of:

  • it was an oversight,
  • "we don't have time right now", or
  • "we don't know if it is worth the effort".

But it doesn't really matter why. And as @Slaw pointed out, there is a OpenJDK bug report for this that you can track: https://bugs.openjdk.org/browse/JDK-6278287. This is evidence that the team are aware of this problem, though we can't tell you if there are plans to fix it.

Snowflake answered 20/1 at 9:25 Comment(9)
Did you see mail.openjdk.org/pipermail/core-libs-dev/2023-May/106248.html (Stuart Marks's reply in the thread that Slaw linked to)?Scoter
Yes. And the rest of the emails in that thread too. They are included in my summary.Snowflake
It just seems weird to give your own "guess", on the basis that he hasn't given a "definitive" answer, and never mention that he's actually given various reasons.Scoter
Why is that weird? Why can't I give my opinion if I clearly state it as such? My opinion is that the actually explanation for EnumSet not (yet) implement SequencedSet is some combination of those reasons. And ... indeed ... there is evidence that Stuart was unconvinced of the utility in the emails! However, that is not sufficient evidence to say that this was / is the (sole) definitive reason.Snowflake
You can absolutely give your opinion! It's just weird to do so without mentioning the reasons given by the person who you say could give a "definitive" answer.Scoter
Fine ... if you want to write a better answer, you are free to do so.Snowflake
Working on it… :-)Hormonal
@StuartMarks I wondered for quite some time why EnumSet doesn’t implement NavigableSet until I found this great answer from Eric Lippert: “Features start off as unimplemented and only become implemented when people spend effort implementing them” which answers so many “why” questions. But even more pressing (to me) is the absence of an immutable enum set. One issue when using the wrapper is that containsAll stops being an efficient & operation.Conformist
@Conformist Right, providing an unmodifiable EnumSet is arguably more important than retrofitting SequencedSet or NavigableSet.Hormonal

© 2022 - 2024 — McMap. All rights reserved.