3

I'm facing an odd issue

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.Collection;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * Created by mklueh on 13/09/2022
 */
public class Reproducer {

    private static final String COMMA_SEPARATED_STRING = "2020-05-09,hello ,10     ,11.345 ,true    ,1000000000000";

    /**
     * Not working
     */
    @Nested
    public class WithReturn {

        public String withReturn(Stream<String> stream, String delimiter, Function<? super String, String[]> tokenizer) {
            return stream.map(s -> {
                             return Arrays.stream(tokenizer != null ? tokenizer.apply(s) : s.split(delimiter))
                                          .map(String::strip) //Comment out to make it compile
                                          .collect(Collectors.toList());
                         })
                         .flatMap(Collection::stream)
                         .collect(Collectors.joining(","));
        }

        @Test
        void usingDelimiter() {
            assertEquals(COMMA_SEPARATED_STRING.replaceAll(" ", ""),
                    withReturn(Stream.of(COMMA_SEPARATED_STRING), ",", null));
        }

        @Test
        void usingTokenizer() {
            assertEquals(COMMA_SEPARATED_STRING.replaceAll(" ", ""),
                    withReturn(Stream.of(COMMA_SEPARATED_STRING), null, s -> s.split(",")));
        }
    }

    /**
     * Working
     */
    @Nested
    public class WithReturnAndStripLambda {

        public String withReturnAndStripLambda(Stream<String> stream, String delimiter, Function<? super String, String[]> tokenizer) {
            return stream.map(s -> {
                             return Arrays.stream(tokenizer != null ? tokenizer.apply(s) : s.split(delimiter))
                                          .map(s1 -> s1.strip())
                                          .collect(Collectors.toList());
                         })
                         .flatMap(Collection::stream)
                         .collect(Collectors.joining(","));
        }

        @Test
        void usingDelimiter() {
            assertEquals(COMMA_SEPARATED_STRING.replaceAll(" ", ""),
                    withReturnAndStripLambda(Stream.of(COMMA_SEPARATED_STRING), ",", null));
        }

        @Test
        void usingTokenizer() {
            assertEquals(COMMA_SEPARATED_STRING.replaceAll(" ", ""),
                    withReturnAndStripLambda(Stream.of(COMMA_SEPARATED_STRING), null, s -> s.split(",")));
        }
    }


    /**
     * Working
     */
    @Nested
    public class WithMethodExpression {

        public String withMethodExpression(Stream<String> stream, String delimiter, Function<? super String, String[]> tokenizer) {
            return stream.map(s -> Arrays.stream(tokenizer != null ? tokenizer.apply(s) : s.split(delimiter))
                                         .map(String::strip)
                                         .collect(Collectors.toList())
                         )
                         .flatMap(Collection::stream)
                         .collect(Collectors.joining(","));

    }

    @Test
    void usingDelimiter() {
        assertEquals(COMMA_SEPARATED_STRING.replaceAll(" ", ""),
                withMethodExpression(Stream.of(COMMA_SEPARATED_STRING), ",", null));
    }

    @Test
    void usingTokenizer() {
        assertEquals(COMMA_SEPARATED_STRING.replaceAll(" ", ""),
                withMethodExpression(Stream.of(COMMA_SEPARATED_STRING), null, s -> s.split(",")));
    }

  }

}

invalid method reference

error: incompatible types: invalid method reference
                            .map(String::strip)
                                 ^
    method strip in class String cannot be applied to given types
      required: no arguments
      found: long
      reason: actual and formal argument lists differ in length

And the IntelliJ IDEA even recommends to perform the auto-refactoring to get rid of the lambda function in favor of the method reference

enter image description here

as well as replacing the return with expression lambda

enter image description here

What is wrong here and why does IntelliJ recommend to break code / not recognize that the recommendation is causing compiler errors?

Marian Klühspies
  • 15,824
  • 16
  • 93
  • 136
  • 2
    Please provide a [mre]. I cannot reproduce the issue. – Unmitigated Sep 13 '22 at 13:58
  • @Unmitigated while creating the reproducer, I've found the issue. The line of the productive code looks like this ```.stream(tokenizer != null ? tokenizer.apply(s) : s.split(delimiter))``` and it seems the type of this construct is not known. But it's really sneaky that the IDE recommends it and does not recognize there is an issue – Marian Klühspies Sep 13 '22 at 14:30
  • 1
    Your comment creates even more confusion. The line `.stream(tokenizer != null ? tokenizer.apply(s) : s.split(delimiter))` is entirely unrelated to the subsequent `map` call. As already said, you should post a [mre]. – Holger Sep 13 '22 at 18:58
  • @Holger you are right, the context is not clear. Imagine something like this ```Arrays.stream(tokenizer != null ? tokenizer.apply(s) : s.split(delimiter)) .map(String::strip).collect(Collectors.toList())``` but as I've said, resolving this construct and using if/else instead solves this issue – Marian Klühspies Sep 13 '22 at 19:38
  • 1
    But using if/else shouldn’t be necessary. Even imaginable scenarios like `tokenizer.apply(s)` returning a different type could not explain an error message about a `long` argument found when using this method reference. Having a workaround is not the same as solving (and hence understanding) the problem. Otherwise, you could have stayed with the lambda expression. – Holger Sep 14 '22 at 06:39
  • @Holger maybe this is related https://stackoverflow.com/questions/41331912/nullpointer-exception-with-conditional-operator-ternary-operator-but-not-with I can assume that there is indeed a value in a string and the correct type cannot be determined – Marian Klühspies Sep 14 '22 at 07:00
  • 1
    No, this linked question is about a *numeric* condition. This does not apply to expressions of type `String[]`. If you are able to reproduce the problem, it shouldn’t be so hard to provide a [mre]. I don’t understand why you don’t do this. – Holger Sep 14 '22 at 07:02
  • @Holger I've added a full example with all cases I detected – Marian Klühspies Sep 14 '22 at 08:56
  • 1
    I believe there's no point in collecting into a `List` (with `.collect(Collectors.toList())`) and then again streaming over it (using `.flatMap(Collection::stream)`). Remove the `toList()` part and then use `flatMap(Function.identity())`, as you *are* already returning a `Stream` inside the previous step. – MC Emperor Sep 14 '22 at 09:03
  • 1
    This seems to be a blatant bug in `javac`, as there should be no difference between `x -> expression` and `x -> { return expression; }` Also, Eclipse compiles the code without problems. But as @MCEmperor said, you can simply use `stream.flatMap(s -> Arrays.stream(tokenizer != null? tokenizer.apply(s): s.split(delimiter))) .map(String::trim) .collect(Collectors.joining(","))` without the detour of collecting into a list, just to call `stream()` again or performing a `map` step, just to do `flatMap` afterwards, when you can use `flatMap` in the first place. – Holger Sep 14 '22 at 09:17
  • Same problem if you add the explicit type, i.e. `.map((String sa) -> sa.strip())`. Can't really see where the compiler is getting confused, but does seem like a compiler bug. – Michael Sep 14 '22 at 09:19
  • 1
    Of course, using `"\\p{javaWhitespace}*,\\p{javaWhitespace}*"` as delimiter eliminates the need for `strip()` and using `.replaceAll("\\p{javaWhitespace}*,\\p{javaWhitespace}*", ",")` on the input string eliminates the need for the Stream operation entirely… – Holger Sep 14 '22 at 09:25
  • To be clear, this is an example without any deeper meaning just to show the issue. It has nothing to do with the production code – Marian Klühspies Sep 14 '22 at 09:31

1 Answers1

4

This seems very likely to be a bug in javac. What IntelliJ is suggesting should be a safe and functionally equivalent replacement.

Looks to be accounted for by JDK-8268312, which is marked as fixed in Java 20.


In case it's not, here's some other interesting behaviour I found when fiddling with your specific example:

In addition to failing for method references, it also fails when you explicitly specify the type in the lambda's parameter list as String, i.e. String::strip becomes (String sa) -> sa.trim()

Here's a more minimal example than yours. Note that I intentionally made both paths of the ternary within Arrays.stream do the exact same thing. I also used trim so this can be run on Java 8, as well as later JDKs.

Stream.of("A").flatMap(s -> {
    return Arrays.stream(true ? s.split(",") : s.split(","))
        .map((String sa) -> sa.trim());
})
.collect(Collectors.joining(","));

For some reason, it compiles fine if the flatMap argument is an expression lambda, rather than a statement lambda, which should not make a difference to the type system.

Stream.of("A").flatMap(s ->
    Arrays.stream(true ? s.split(",") : s.split(","))
        .map((String sa) -> sa.trim())
)
.collect(Collectors.joining(","));

It also works if you cast the result of the ternary to String[], even though that's already the type of that expression, meaning the cast should be redundant.

Stream.of("A").flatMap(s -> {
        return Arrays.stream((String[]) (true ? s.split(",") : s.split(",")))
            .map((String sa) -> sa.trim());
    })
    .collect(Collectors.joining(","));
Michael
  • 41,989
  • 11
  • 82
  • 128
  • 3
    Interestingly, this example compiles fine with JDK 8, whereas the bug report’s example stops compiling around update 60 of JDK 8. Even more interesting is that following the error message’s advice to compile again with `-Xdiags:verbose` to get more information, makes the error disappear. But it seems, you found the right bug report, though the rabbit hole might go far deeper. – Holger Sep 14 '22 at 10:32