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.

6
  • 4
    Have you looked at Collectors.joining() or is the question about implementing it yourself? Commented Apr 6, 2016 at 21:04
  • stackoverflow.com/questions/31456898/… Commented Apr 6, 2016 at 21:07
  • The question is how to use a lambda expression, something like: .collect( (acc, p ) -> acc += p.lastName); Commented Apr 6, 2016 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()). Commented Apr 6, 2016 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). Commented Apr 6, 2016 at 21:28

4 Answers 4

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));
Sign up to request clarification or add additional context in comments.

3 Comments

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?
This is discussed, in length, in the doc: docs.oracle.com/javase/8/docs/api/java/util/stream/…. Using reduce() would force the creation of a many String copies, making the reduction much slower than a mutable reduction.
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.
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);

6 Comments

But it's much, much, much slower. How does that answer the question, BTW?
This won't compile. reduce requires a BinaryOperator<T>, but there's no + operator applicable to Persona.
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
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.
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("");
|
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.

1 Comment

What is the point of using StringBuil..Buffer if you create it on every iteration?
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 );

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.