4

I was trying to convert this

 String r = "";
 for ( Persona p : list ) {
    r += p.lastName;
 }

To stream().filter.collect() form, but I want to know how to write the collect with a lambda expression (not method references). I couldn't find a good example.

This is what I have

class B {
    public static void main( String ... args ) {
        List<Person> p = Arrays.asList(
                new Person("John", "Wilson"),
                new Person("Scott", "Anderson"),
                new Person("Bruce", "Kent"));

        String r;
        String s = p.stream()
            .filter( p -> p.lastName.equals("kent"))
            .collect((r, p) -> r += p.lastName);/// ?????
    }
}
class Person {
    String name;
    String lastName;
    public Person( String name, String lastName ) {
        this.name = name;
        this.lastName = lastName;
    }
}

All the examples I find are using method references whereas I think there should be an easy way to write a lambda expression instead.

OscarRyz
  • 196,001
  • 113
  • 385
  • 569
  • 4
    Have you looked at `Collectors.joining()` or is the question about implementing it yourself? – Paul Boddington Apr 06 '16 at 21:04
  • http://stackoverflow.com/questions/31456898/convert-a-for-loop-to-concat-string-into-a-lambda-expression – Savior Apr 06 '16 at 21:07
  • The question is how to use a lambda expression, something like: `.collect( (acc, p ) -> acc += p.lastName);` – OscarRyz Apr 06 '16 at 21:07
  • 2
    @OscarRyz That's not really how it works. You first have to map each person to their lastname with `map(person -> person.lastName)` and then you can collect those Strings with `.collect(joining())`. – Tunaki Apr 06 '16 at 21:09
  • 1
    The lesson you're supposed to learn is that you 99% of the time ought to be using a pre-build collector, and all the rest of the time you should be using `Collector.of` or using `collect(Supplier, BiConsumer, BiConsumer)`. – Louis Wasserman Apr 06 '16 at 21:28
  • Well, the question is precisely to learn that 1%. I know how to do it with method references, but I'm struggling with lambdas expressions. The fact that I have more comments than answers indicates that this is hard for everyone else too. – OscarRyz Apr 06 '16 at 21:32

4 Answers4

4

Assuming you don't want to use any ready-made collector like Collectors.joining(), you could indeed create your own collector.

But, as the javadoc indicates, collect() expects either 3 functional interface instances as argument, or a Collector. So you can't just pass a single lambda expression to collect().

Assuming you want to use the first version, taking 3 lambda expressions, you'll note, by reading the javadoc, that the result must be a mutable object, and String is not mutable. So you should instead use a StringBuilder. For example:

StringBuilder s = 
    p.stream()
     .filter( p -> p.lastName.equals("kent"))
     .map(p -> p.lastName)
     .collect(StringBuilder::new,
              StringBuilder::append,
              StringBuilder::append);

This uses method references, but all method references can be written as lambda expressions. The above is equivalent to

StringBuilder s = 
    p.stream()
     .filter( p -> p.lastName.equals("kent"))
     .map(p -> p.lastName)
     .collect(() -> new StringBuilder(),
              (stringBuilder, string) -> stringBuilder.append(string),
              (stringBuilder1, stringBuilder2) -> stringBuilder1.append(stringBuilder2));
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • Yes, that's exactly what I was looking for. So the first expression says how to create objects used by the other two expressions, the second, how to accumulate, and the third how to combine the accumulated result with the next accumulated result? At that point wouldn't make more sense to use `reduce` instead? – OscarRyz Apr 06 '16 at 21:29
  • 2
    This is discussed, in length, in the doc: https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#MutableReduction. Using reduce() would force the creation of a many String copies, making the reduction much slower than a mutable reduction. – JB Nizet Apr 06 '16 at 21:35
  • Note that `Collectors.joining()` is equivalent to `Collector.of(StringBuilder::new, StringBuilder::append, StringBuilder::append, StringBuilder::toString)`, differing only in having the finisher function which will turn the `StringBuilder` into a `String`. – Holger Apr 07 '16 at 15:57
2

You can also use the .reduce() function, it's easier to read than the collect.

    String s = p.stream()
        .filter( p -> p.lastName.equals("kent"))
        .reduce("", (acc, p) -> acc + p.lastName(), String::concat);

Version with StringBuilder:

    StringBuilder s = p.stream()
        .filter( p -> p.lastName.equals("kent"))
        .reduce(new StringBuilder(), (acc, p) -> acc.append(p.lastName()), StringBuilder::append);
Jiri Kremser
  • 12,471
  • 7
  • 45
  • 72
  • 1
    But it's much, much, much slower. How does that answer the question, BTW? – JB Nizet Apr 06 '16 at 21:35
  • 1
    This won't compile. `reduce` requires a `BinaryOperator`, but there's no `+` operator applicable to `Persona`. – Paul Boddington Apr 06 '16 at 21:37
  • right, but "premature optimization is the root of all evil" I'd prefer, the readability. But it can be tweaked to use the StringBuilder/Buffer – Jiri Kremser Apr 06 '16 at 21:38
  • Premature optimization is when you're optimizing without knowing if you have a perf problem, and without knowing if the optimized code is more efficient than the non-optimized one. That's not the case here. Appending on strings is known to be very slow, and collecting is known to be much faster. If you want readability, use `Collectors.joining()`. But the question was about how to write its own collect using lambda expressions. – JB Nizet Apr 06 '16 at 21:40
  • with a map operation to extract the last name, you would get around the lack of a `BinaryOperator`. For instance, String s = persons.stream() .map( p -> p.lastName ) .reduce((r, ln) -> r + ln) .orElse(""); – Hank D Apr 07 '16 at 05:24
  • Don’t use `reduce` with a `Stringbuilder` the way you do. This is broken. You have to understand the difference between [Reduction](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Reduction) and [Mutable Reduction](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#MutableReduction). – Holger Apr 07 '16 at 16:01
1

Perhaps you were envisioning a reduce operation rather than a collect, which would allow you to use a lambda expression like you envisioned. For example,

String s = persons.stream()
        .map( (s1, str) -> s1.concat(str) )
        .reduce("", String::concat);

This is an exercise, of course. The String concatenation is inefficient, so it would be best to use collect(Collectors.joining()), which uses StringBuffer as its accumulator.

Hank D
  • 6,271
  • 2
  • 26
  • 35
  • What is the point of using `StringBuil..Buffer` if you create it on every iteration? – nyxz Dec 12 '16 at 15:46
0

You could omit the mapping with this solution:

Collector<Person, StringBuilder, String> collector = Collector.of(  
    StringBuilder::new,
    (buf, person) -> buf.append( person.lastName ),
    (buf1, buf2) -> {
      buf1.append( buf2 );
      return( buf1 );
    },
    buf -> buf.toString() 
  );

list.stream()
  .filter( p -> p.lastName.equals( "Kent" ) )
  .collect( collector );
Kaplan
  • 11