> From: "Ethan McCue" <et...@mccue.dev> > To: "Remi Forax" <fo...@univ-mlv.fr> > Cc: "John Hendrikx" <john.hendr...@gmail.com>, "core-libs-dev" > <core-libs-dev@openjdk.org> > Sent: Wednesday, August 24, 2022 4:27:01 PM > Subject: Re: Proposal: Collection mutability marker interfaces
>> so it's perhaps better to call add() inside a try/catch on > > UnsupportedOperationException. > As much as sin is wrong, sketching out what that might look like... forgive > the > toyness of the example > VS > final class Ex { > private Ex() {} > /* > * Adds the odd numbers from 1 to 10 to the List then makes all the odds even. > * > * Takes ownership of the list, avoids making copies if it doesn't need to > */ > static List<Integer> addOdds(List<Integer> l) { > for (int i = 1; i <= 10; i++) { > try { > l.add(i); > } catch (UnsupportedOperationException e) { > l = new ArrayList<>(l); i -= 1; // restart with an ArrayList > } > } > for (int i = 0; i < l.size(); i++) { > if (l.get(i) % 2 == 1) { > try { > l.set(i, l.get(i) + 1); > } catch (UnsupportedOperationException e) { > l = new ArrayList<>(l); > } > } > } > } > } as Roger said, there is no way in Java to know if the caller has not kept a reference (unlike Rust), so having trouble to write this kind of code is more a feature than an issue :) This kind of examples scream the Stream API, which has the correct semantics IntStream.rangeClosed(1, 10).map(i -> i % 2 == 0? i + 1: i).boxed().toList() Rémi > On Wed, Aug 24, 2022 at 10:03 AM Remi Forax < [ mailto:fo...@univ-mlv.fr | > fo...@univ-mlv.fr ] > wrote: >>> From: "Ethan McCue" < [ mailto:et...@mccue.dev | et...@mccue.dev ] > >>> To: "John Hendrikx" < [ mailto:john.hendr...@gmail.com | >>> john.hendr...@gmail.com >>> ] > >>> Cc: "core-libs-dev" < [ mailto:core-libs-dev@openjdk.org | >>> core-libs-dev@openjdk.org ] > >>> Sent: Wednesday, August 24, 2022 3:38:26 PM >>> Subject: Re: Proposal: Collection mutability marker interfaces >>> A use case that doesn't cover is adding to a collection. >>> Say as part of a method's contract you state that you take ownership of a >>> List. >>> You aren't going to copy even if the list is mutable. >>> Later on, you may want to add to the list. Add is supported on ArrayList so >>> you >>> don't need to copy and replace your reference, but you would if the list you >>> were given was made with List.of or Arrays.asList >> You can ask if the spliterator considers the collection as immutable or not, >> list.spliterator().hasCharacteristics(Spliterator.IMMUTABLE) >> sadly, List.of()/Map.of() does not report the spliterator characteristics >> correctly (the spliterator implementations are inherited from >> AbstracList/AbstractMap). >> so it's perhaps better to call add() inside a try/catch on >> UnsupportedOperationException. >> Rémi >>> On Wed, Aug 24, 2022, 8:13 AM John Hendrikx < [ >>> mailto:john.hendr...@gmail.com | >>> john.hendr...@gmail.com ] > wrote: >>>> Would it be an option to not make the receiver responsible for the decision >>>> whether to make a copy or not? Instead put this burden (using default >>>> methods) >>>> on the various collections? >>>> If List/Set/Map had a method like this: >>>> List<T> immutableCopy(); // returns a (shallow) immutable copy if list is >>>> mutable (basically always copies, unless proven otherwise) >>>> Paired with methods on Collections to prevent collections from being >>>> modified: >>>> Collections.immutableList(List<T>) >>>> This wrapper is similar to `unmodifiableList` except it implements >>>> `immutableCopy` as `return this`. >>>> Then for the various scenario's, where `x` is an untrusted source of List >>>> with >>>> unknown status: >>>> // Create a defensive copy; result is a private list that cannot be >>>> modified: >>>> List<T> y = x.immutableCopy(); >>>> // Create a defensive copy for sharing, promising it won't ever change: >>>> List<T> y = Collections.immutableList(x.immutableCopy()); >>>> // Create a defensive copy for mutating: >>>> List<T> y = new ArrayList<>(x); // same as always >>>> // Create a mutable copy, modify it, then expose as immutable: >>>> List<T> y = new ArrayList<>(x); // same as always >>>> y.add( <some element> ); >>>> List<T> z = Collections.immutableList(y); >>>> y = null; // we promise `z` won't change again by clearing the only path to >>>> mutating it! >>>> The advantage would be that this information isn't part of the type system >>>> where >>>> it can easily get lost. The actual implementation knows best whether a copy >>>> must be made or not. >>>> Of course, the immutableList wrapper can be used incorrectly and the >>>> promise >>>> here can be broken by keeping a reference to the original (mutable) list, >>>> but I >>>> think that's an acceptable trade-off. >>>> --John >>>> PS. Chosen names are just for illustration; there is some discussion as >>>> what >>>> "unmodifiable" vs "immutable" means in the context of collections that may >>>> contain elements that are mutable. In this post, immutable refers to >>>> shallow >>>> immutability . >>>> On 24/08/2022 03:24, Ethan McCue wrote: >>>>> Ah, I'm an idiot. >>>>> There is still a proposal here somewhere...maybe. right now non jdk lists >>>>> can't >>>>> participate in the special casing? >>>>> On Tue, Aug 23, 2022, 9:00 PM Paul Sandoz < [ >>>>> mailto:paul.san...@oracle.com | >>>>> paul.san...@oracle.com ] > wrote: >>>>>> List.copyOf already does what you want. >>>>>> [ >>>>>> https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/List.java#L1068 >>>>>> | >>>>>> https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/List.java#L1068 >>>>>> ] >>>>>> [ >>>>>> https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/ImmutableCollections.java#L168 >>>>>> | >>>>>> https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/ImmutableCollections.java#L168 >>>>>> ] >>>>>> Paul. >>>>>>> On Aug 23, 2022, at 4:49 PM, Ethan McCue < [ mailto:et...@mccue.dev | >>>>>> > et...@mccue.dev ] > wrote: >>>>>> > Hi all, >>>>>>> I am running into an issue with the collections framework where I have >>>>>>> to choose >>>>>> > between good semantics for users and performance. >>>>>>> Specifically I am taking a java.util.List from my users and I need to >>>>>>> choose to >>>>>> > either >>>>>>> * Not defensively copy and expose a potential footgun when I pass that >>>>>>> List to >>>>>> > another thread >>>>>> > * Defensively copy and make my users pay an unnecessary runtime cost. >>>>>>> What I would really want, in a nutshell, is for List.copyOf to be a >>>>>>> no-op when >>>>>> > used on lists made with List.of(). >>>>>>> Below the line is a pitch I wrote up on reddit 7 months ago for a >>>>>>> mechanism I >>>>>>> think could accomplish that. My goal is to share the idea a bit more >>>>>>> widely and >>>>>> > to this specific audience to get feedback. >>>>>>> [ >>>>>>> https://www.reddit.com/r/java/comments/sf8qrv/comment/hv8or92/?utm_source=share&utm_medium=web2x&context=3 >>>>>>> | >>>>>>> https://www.reddit.com/r/java/comments/sf8qrv/comment/hv8or92/?utm_source=share&utm_medium=web2x&context=3 >>>>>> > ] >>>>>> > Important also for context is Ron Pressler's comment above. >>>>>> > -------------- >>>>>> > What if the collections api added more marker interfaces like >>>>>> > RandomAccess? >>>>>>> It's already a common thing for codebases to make explicit null checks >>>>>>> at error >>>>>> > boundaries because the type system can't encode null | List<String>. >>>>>> > This feels like a similar problem. >>>>>>> If you have a List<T> in the type system then you don't know for sure >>>>>>> you can >>>>>>> call any methods on it until you check that its not null. In the same >>>>>>> way, >>>>>>> there is a set of methods that you don't know at the type/interface >>>>>>> level if >>>>>> > you are allowed to call. >>>>>> > If the List is actually a __ >>>>>> > Then you can definitely call >>>>>> > And you know other reference holders might call >>>>>> > And you can confirm its this case by >>>>>> > null >>>>>> > no methods >>>>>> > no methods >>>>>> > list == null >>>>>> > List.of(...) >>>>>> > get, size >>>>>> > get, size >>>>>> > ??? >>>>>> > Collections.unmodifiableList(...) >>>>>> > get, size >>>>>> > get, size, add, set >>>>>> > ??? >>>>>> > Arrays.asList(...) >>>>>> > get, size, set >>>>>> > get, size, set >>>>>> > ??? >>>>>> > new ArrayList<>() >>>>>> > get, size, add, set >>>>>> > get, size, add, set >>>>>> > ??? >>>>>>> While yes, there is no feasible way to encode these things in the type >>>>>>> system. >>>>>> > Its not impossible to encode it at runtime though. >>>>>> > interface FullyImmutable { >>>>>> > // So you know the existence of this implies the absence >>>>>> > // of the others >>>>>> > default Void cantIntersect() { return null; } >>>>>> > } >>>>>> > interace MutationCapability { >>>>>> > default String cantIntersect() { return ""; } >>>>>> > } >>>>>> > interface Addable extends MutationCapability {} >>>>>> > interface Settable extends MutationCapability {} >>>>>> > If the List is actually a __ >>>>>> > Then you can definitely call >>>>>> > And you know other reference holders might call >>>>>> > And you can confirm its this case by >>>>>> > null >>>>>> > no methods >>>>>> > no methods >>>>>> > list == null >>>>>> > List.of(...) >>>>>> > get, size >>>>>> > get, size >>>>>> > instanceof FullyImmutable >>>>>> > Collections.unmodifiableList(...) >>>>>> > get, size >>>>>> > get, size, add, set >>>>>> > !(instanceof Addable) && !(instanceof Settable) >>>>>> > Arrays.asList(...) >>>>>> > get, size, set >>>>>> > get, size, set >>>>>> > instanceof Settable >>>>>> > new ArrayList<>() >>>>>> > get, size, add, set >>>>>> > get, size, add, set >>>>>> > instanceof Settable && instanceof Addable >>>>>>> In the same way a RandomAccess check let's implementations decide >>>>>>> whether they >>>>>>> want to try an alternative algorithm or crash, some marker "capability" >>>>>>> interfaces would let users of a collection decide if they want to clone >>>>>>> what >>>>>> > they are given before working on it. >>>>>> > -------------- >>>>>>> So the applicability of this would be that the list returned by List.of >>>>>>> could >>>>>>> implement FullyImmutable, signifying that there is no caller which >>>>>>> might have a >>>>>>> mutable handle on the collection. Then List.of could check for this >>>>>>> interface >>>>>> > and skip a copy.