Pablo Matias Gomez
Pablo Matias Gomez

Reputation: 6823

How to do filter and map without overhead of repeating operation

I have some cases where using Java 8 Stream makes me repeat the execution of some operation where it could be avoided if done without the Stream, but I think that the problem is not with the stream, but me.

Some example:

private class Item {
    String id;
    List<String> strings;
}

// This method, filters only the Items that have the strToFind, and
// then maps it to a new string, that has the id and the str found
private void doIt(List<Item> items, String strToFind) {
    items.stream().filter(item -> {
        return item.strings.stream().anyMatch(str -> this.operation(str, strToFind));
    }).map(item -> {
        return item.id + "-" + item.strings.stream()
            .filter(str -> this.operation(str, strToFind)).findAny().get();
    });
}

// This operation can have a lot of overhead, therefore
// it would be really bad to apply it twice
private boolean operation(String str, String strToFind) {
    return str.equals(strToFind);
}

As you can see, the function operation is being called twice for each item, and I don't want that. What I thought first was to map directly and return "null" if not found and then filter nulls, but if I do that, I will lose the reference to the Item and therefore, can't use the id.

Upvotes: 3

Views: 382

Answers (3)

Markus Benko
Markus Benko

Reputation: 1507

Although not the shortest code (but this has not been asked for) I believe this works quite straightforward using Optional but does not involve any null mappings and/or checks and type information (String vs. Object) is not accidentally lost:

items.stream()
    .map(item -> item.strings.stream()
        .filter(str -> this.operation(str, strToFind))
        .findAny()
        .<String>map(string -> item.id + "-" + string))
    .filter(Optional::isPresent)
    .map(Optional::get);

It's pretty much a combination of Jeremy Grand's and Holger's answers.

Upvotes: 2

Holger
Holger

Reputation: 298399

You can use

private void doIt(List<Item> items, String strToFind) {
    items.stream()
         .flatMap(item -> item.strings.stream().unordered()
             .filter(str -> this.operation(str, strToFind)).limit(1)
             .map(string -> item.id + "-" + string))
         // example terminal operation
         .forEach(System.out::println);
}

The .unordered() and .limit(1) exist to produce the same behavior like anyMatch() and findAny() of your original code. Of course, .unordered() is not required to get a correct result.

In Java 9, you could also use

private void doIt(List<Item> items, String strToFind) {
    items.stream()
         .flatMap(item -> item.strings.stream()
             .filter(str -> this.operation(str, strToFind))
             .map(string -> item.id + "-" + string).findAny().stream())
         // example terminal operation
         .forEach(System.out::println);
}

keeping the findAny() operation, but unfortunately, Java 8 lacks the Optional.stream() method and trying to emulate it would create code less readable than the limit(1) approach.

Upvotes: 3

Jeremy Grand
Jeremy Grand

Reputation: 2370

I think you might want this behaviour :

items.stream().map(item -> {
        Optional<String> optional = item.strings.stream().filter(string -> operation(string, strToFind)).findAny();
        if(optional.isPresent()){
            return item.id + "-" + optional.get();
        }
        return null;
    }).filter(e -> e != null);

EDIT : Because you're losing the information obtained in the filter when you're doing the map afterwards, but nothing prevents you from doing the operation in the map only and filtering afterwards.

EDIT 2 : As @Jorn Vernee pointed out, you can shorten it further :

private void doIt(List<Item> items, String strToFind) {
    items.stream().map(item -> item.strings.stream().filter(string -> operation(string, strToFind)).findAny()
            .map(found -> item.id + "-" + found).orElse(null)).filter(e -> e != null);
}

Upvotes: 5

Related Questions