具有非纯函数链的Java Stream API

时间:2019-03-07 09:13:47

标签: java java-stream

假设我有一个Person类

public class Person {
    private final String name;
    private final int age;
    private boolean rejected;
    private String rejectionComment;

    public void reject(String comment) {
        this.rejected = true;
        this.rejectionComment = comment;
    }

    // constructor & getters are ommited
}

我的应用就是这样

class App {
    public static void main(String[] args) {
        List<Person> persons = Arrays.asList(
            new Person("John", 10),
            new Person("Sarah", 20),
            new Person("Daniel", 30)
        )

        persons.forEach(p -> {
            rejectIfYoungerThan15(p);
            rejectIfNameStartsWithD(p);
            // other rejection functions
        }
    }

    private static void rejectIfYoungerThan15(Person p) {
        if (!p.isRejected() && p.getAge() < 15) {
            p.reject("Too young")
        }
    }

    private static void rejectIfNameStartsWithD(Person p) {
        if (!p.isRejected() && p.getName().startsWith("D")) {
            p.reject("Name starts with 'D'")
        }
    }

    // other rejection functions
}

问题是我不喜欢必须在每个拒绝功能中执行!p.isRejected()检查。而且,将已经被拒绝的人传递给下一个过滤器是没有意义的。 所以我的想法是使用Stream.filter的机制,使之类似

persons.stream().filter(this::rejectIfYoungerThan15).filter(this::rejectIfNameStartsWithD)...

如果未通过传递的true,则更改这些方法的签名以返回Person,否则返回false

但是在我看来,将filter与非纯函数一起使用是一个非常糟糕的主意。

您对如何更优雅地制作它有任何想法吗?

3 个答案:

答案 0 :(得分:1)

当您将检查功能更改为仅检查条件(即不调用p.isRejected())并返回boolean时,您已经采取了必要的短路步骤:

private static boolean rejectIfYoungerThan15(Person p) {
    if(p.getAge() < 15) {
        p.reject("Too young");
        return true;
    }
    return false;
}

private static boolean rejectIfNameStartsWithD(Person p) {
    if(p.getName().startsWith("D")) {
        p.reject("Name starts with 'D'");
        return true;
    }
    return false;
}

可用作

   persons.forEach(p -> {
        if(rejectIfYoungerThan15(p)) return;
        if(rejectIfNameStartsWithD(p)) return;
        // other rejection functions
    }
}

Stream的filter操作除了检查返回的boolean值并纾困外没有任何其他作用。但是根据Stream的实际终端操作,短路可能会更远,最终导致无法检查所有元素,因此您不应在此处引入Stream操作。

答案 1 :(得分:0)

从lambda调用这些方法很好,但是,为了提高可读性,您可以重命名这些方法以显示其作用并返回boolean,例如:

private boolean hasEligibleAge(Person p){..}
private boolean hasValidName(Person p){..}

另一种方法是将这些方法包装到另一种方法中(以反映业务逻辑/流程),例如:

private boolean isEligible(Person p){
    //check age
    //check name
}

答案 2 :(得分:0)

您应该使Person不可变,并让拒绝方法返回一个新的Person。这样您就可以链接map个呼叫。像这样:

public class Person {
    private final String name;
    private final int age;
    private final boolean rejected;
    private final String rejectionComment;

    public Person reject(String comment) {
        return new Person(name, age, true, comment);
    }

    // ...

}

class App {

    // ...

    private static Person rejectIfYoungerThan15(Person p) {
        if (!p.isRejected() && p.getAge() < 15) {
            return p.reject("Too young");
        }
        return p;
    }
}

现在您可以执行以下操作:

persons.stream()
       .map(App::rejectIfYoungerThan15)
       .map(App::rejectIfNameStartsWithD)
       .collect(Collectors.toList());

如果要删除被拒绝的人,可以在映射后添加过滤器:

.filter(person -> !person.isRejected())

编辑:

如果需要使拒绝短路,可以将拒绝功能组合为一个新功能,并使其在第一次拒绝后停止。像这样:

/* Remember that the stream is lazy, so it will only call new rejections 
 * while the person isn't rejected.
 */
public Function<Person, Person> shortCircuitReject(List<Function<Person, Person>> rejections) {
    return person -> rejections.stream()
            .map(rejection -> rejection.apply(person))
            .filter(Person::isRejected)
            .findFirst()
            .orElse(person);
}

现在您的信息流可以如下所示:

List<Function<Person, Person>> rejections = Arrays.asList(
    App::rejectIfYoungerThan15, 
    App::rejectIfNameStartsWithD);

List<Person> persons1 = persons.stream()
    .map(shortCircuitReject(rejections))
    .collect(Collectors.toList());