3

I am trying to join a list of names:

List<String> names;
names = books.stream()
        .map( b -> b.getName() )
        .filter( n -> ( (n != null) && (!n.isEmpty()) ) )
        .collect(Collectors.joining(", "));

This does not compile saying:

incompatible types. inference variable R has incompatible bounds

So after some research, it seems that there is something I misunderstood. I thought that .map( b -> b.getName() ) returned/changed the type to a String, and it seems something is wrong there. If I use .map(Book::getName) instead, I still get an error, but I probably don't fully understand the difference.

However, this does not complain:

List<String> names;
names = books.stream()
        .map( b -> b.getName() )
        .map( Book::getName )
        .filter( n -> ( (n != null) && (!n.isEmpty()) ) )
        .collect(Collectors.joining(", "));

Can someone explain me why? Some didactic explanation about differences between .map( b -> b.getName() ) and .map(Book::getName) are appreciated too, since I think I didn't get it right.

user1156544
  • 1,725
  • 2
  • 25
  • 51
  • 2
    Do you really think this amount of brackets (`.filter( n -> ( (n != null) && (!n.isEmpty()) ) )` improves the readability compared to a straight-forward `.filter(n -> n!=null && !n.isEmpty())`? – Holger Feb 07 '17 at 10:02
  • Mmmm... I normally use parenthesis any time I have operators like &&, just to make it clear. In this case you are right it looks cumbersome – user1156544 Feb 07 '17 at 10:06
  • 2
    I understand that the operator precedence between `&&` and `||` is not intuitively to answer, so placing braces for clarity, even where unnecessary, has a point. But for a sole `&&` operator, there is no way of misinterpreting the expression. Further, there is another outer bracket pair unrelated to the logical expression. Since you didn’t write `b -> (b.getName())` either, having an outer pair at the other expression, is inconsistent. – Holger Feb 07 '17 at 10:13
  • 1
    wait... `.map(b -> b.getName()).map(Book::getName)` works but only `.map(b -> b.getName())` doesn't? So `books` is actually not a collection of `Book`? So `b.getName()` returns a `Book`? Probably that is also the reason, why it's not complaining. – Roland Feb 07 '17 at 10:17
  • You are right @Holger. @Roland, `books` is actually a `Set`. Using only one map and removing the List from names does work as one expects. But not sure why the IDE didn't complain when having the List and both maps because it makes no sense to me. – user1156544 Feb 07 '17 at 10:25
  • 1
    @user1156544, you should write this in question... that's confusing – cybersoft Feb 07 '17 at 10:29

3 Answers3

7

The joining(", ") collector will collect and join all Strings into a single string using the given separator. The returning type of collect in this case is String, but you are trying to assign the result to a List. If you want to collect Strings into a List, use Collectors.toList().

If you have a collection with Book instances, then it will be enough to map a stream of Books to a stream of Strings once.

Difference between lamdba & method refrence

  • A lamdba expression may be written as a block, containing multiple operations:

    b -> {
        // here you can have other operations
        return b.getName(); 
    }
    

    if lambda has single operation, it can be shortened:

    b -> b.getName()
    
  • Method reference is just a "shortcut" for a lambda with a single operation. This way:

    b -> b.getName()
    

    can be replaced with:

    Book::getName
    

    but if you have a lambda like this:

    b -> b.getName().toLowerCase()
    

    you cant use a reference to the getName method, because you are making and additional call to toLowerCase().

Manoel Campos
  • 933
  • 9
  • 12
cybersoft
  • 1,453
  • 13
  • 31
  • Ok, I see my stupid mistake now. What are the implications of using b -> b.getName() over Book::getName? – user1156544 Feb 07 '17 at 09:57
  • 5
    A method reference might be slightly more efficient, but it’s *really slightly*, so that shouldn’t drive design decisions. Another point is that `Book::getName` refers to the declaring class and thus, sometimes works in scenarios where the compiler can’t infer the type of `b` when using `b -> b.getName()`, though this could also be solved by using `(Book b) -> b.getName()` then. – Holger Feb 07 '17 at 10:17
  • Method reference may also be dangerous: [see](https://stackoverflow.com/questions/33925551) and [see](https://stackoverflow.com/questions/27031244/) – cybersoft Jun 01 '17 at 09:41
4

If you are using Collectors.joining(), the result will be a single concatenated String:

String names = books.stream()
        .map( b -> b.getName() )
        .filter(n -> (n != null) && !n.isEmpty())
        .collect(Collectors.joining(", "));

The Collectors.toList() is the one that returns a List:

List<String> namesList = books.stream()
        .map( b -> b.getName() )
        .filter(n -> (n != null) && !n.isEmpty())
        .collect(Collectors.toList());

Book::getName is a method reference and will have the same result as b -> b.getName(). Method reference is clearer and enables to pass other existing methods as a parameter to methods such as map(), as long as the passed method conforms to the signature of the expected functional interface. In this case, map() expects an instance of the Function interface. Thus, you can give any reference to a method that conforms to the signature of the abstract R apply(T t) method from such an interface.

Since you are mapping a Book to a String, the actual signature for the method to be given to the map() must be String apply(Book t). This can be read as "receive a Book and return a String". This way, any method you pass that conforms to this definition is valid. When you pass a method reference Book::getName, the getName method itself doesn't conform to the signature presented above (because it has no parameter at all), but it conforms to the definition of such a signature: you pass a book and return a String from its name.

Thus, consider that, inside the class where you have your book list, you also have a method which performs any operation over a Book, returning a String. The method below is an example that receives a Book and gets the first 10 characters from its name:

public String getReducedBookName(Book b){
  if(b.getName() == null)
     return "";

  String name = b.getName();
  return name.substring(0, name.length() > 10 ? 10 : name.length());
}

You can also pass this method (which is not inside the Book class) as parameter to the map() method:

String names = books.stream()
            .map(this::getReducedBookName)
            .filter(n -> !n.isEmpty())
            .collect(Collectors.joining(", "));
Manoel Campos
  • 933
  • 9
  • 12
0

if you prefer mapping over map

as String

String names = books.stream().collect(mapping(Book::getName,
    filtering(s -> s != null && ! s.isBlank(),
      joining(", "))));

as List

List<String> names = books.stream().collect(mapping(Book::getName,
    filtering(s -> s != null && ! s.isBlank(),
      toList())));
Kaplan
  • 2,572
  • 13
  • 14