Skip to content

Conversation

@zbynek
Copy link
Collaborator

@zbynek zbynek commented Nov 16, 2025

Fixes #9495 by changing the error message.

@zbynek zbynek added Category-JRE ready This PR has been reviewed by a maintainer and is ready for a CI run. labels Nov 16, 2025
@niloc132 niloc132 added this to the 2.13 milestone Nov 18, 2025
return toMap(
keyMapper,
valueMapper,
return Collector.of(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lambdas and method references are pretty expensive to generate, and while the new error message will be shorter, the total generated code is probably going to increase for this change.

Is there another approach we can use to avoid this, composing the toMap() call with some other collision check? At a glance, I'd either say composing something into the other lambdas we have to produce (likely parameterizing them, which isn't all that great), or maybe a call like mapping(collisionCheckHere, toMap(...))?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only way I was able to reduce the number of lambdas was parametrizing the private toMap 🤔
The extra parameter could be avoided if we say that mergeFunction == null should be interpreted as unique == true in the private method (and possibly add a null check to the public one).

I don't think this can be easily solved with composition since either collisionCheck would have to return both T and K wrapped in an Entry or keyMapper would have to be called twice (in collision checking and in accumulator).

HashMap::new,
(map, item) -> {
K key = keyMapper.apply(item);
U newValue = Objects.requireNonNull(valueMapper.apply(item));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null check is surprising - it isn't specified at https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Collectors.html#toMap(java.util.function.Function,java.util.function.Function), but it does seem to be part of the current JDK at least:

jshell> Stream.of(1).collect(Collectors.toMap(ignore -> "", ignore -> null))
|  Exception java.lang.NullPointerException
|        at Objects.requireNonNull (Objects.java:209)
|        at Collectors.lambda$uniqKeysMapAccumulator$1 (Collectors.java:180)
|        at ReduceOps$3ReducingSink.accept (ReduceOps.java:169)
|        at Streams$StreamBuilderImpl.forEachRemaining (Streams.java:411)
|        at AbstractPipeline.copyInto (AbstractPipeline.java:509)
|        at AbstractPipeline.wrapAndCopyInto (AbstractPipeline.java:499)
|        at ReduceOps$ReduceOp.evaluateSequential (ReduceOps.java:921)
|        at AbstractPipeline.evaluate (AbstractPipeline.java:234)
|        at ReferencePipeline.collect (ReferencePipeline.java:682)
|        at (#9:1)

You could consider composing the existing function with disallowNulls(...).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

composing the existing function with disallowNulls

Wouldn't that instantiate more lambdas?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instantiating lambdas isn't awful, its declaring them - any time you write -> or ::, you can imagine you just made a new class instead. Not just an anonymous inner class, but build the rest of the machinery in your head for there to be a constructor to pass in the outer variables/instances (fields, etc), potentially the getClass, the type info to know the supertype/interfaces of the lambda, etc.

I wouldn't bat an eye at someone adding lambdas in application code, but core JRE emulation code for something as commonly used as toMap seems worth working a little harder on - even if it means the error message might be a little subpar. Library code I'd want to work slightly harder where its easy, but not worry about it (e.g. prefer if (optional.isPresent()) {...} over optional.map(...) sorts of things).

Instantiating them is only as bad as instantiating anything else, new Constructor(args, to, close, over), since at the end of the day (non-JsFunction) lambdas are just implementations of SAM interfaces. We could probably do better (...the JVM does), but it both adds complexity to the compiler, and could risk making something like incremental builds stop working. Instantiating them also only happens once, at the declaration site - but if you pull that out into a util method (like disallowNulls()), you just pay all that once, and then get to do a(b) to wrap the existing lambda in another lambda each time it is needed.

JsFunction lambdas are a little different in how they are built, but are actually slightly cheaper to call in GWT-generated Java than non-JsFunction lambdas - rather than instance.apply(args)/a.b(c) when you invoke, its just instance(args)/a(c), savings of 2ish bytes per callsite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Category-JRE ready This PR has been reviewed by a maintainer and is ready for a CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve Collectors.toMap(Function,Function) error message for duplicate keys

2 participants