Can I use only one stream expression to get the result with two condition in JDK8?

Viewed 102

Here is my code,but I find that it is not good.Can I use only one stream expression to get the result with two condition in JDK8 ?I want to make sure that id conditon is first and name condition is second.

  public Student getStudent(String id,String name)  {
    try {
        List<Student> students = studentDao.getStudent();
        if (CollectionUtils.isNotEmpty(students)) {
            Student studentDomain=students.stream()
                    .filter(p -> id .equals(p.getId()))
                    .findAny().orElse(null);
            if(studentDomain==null){
                studentDomain=students.stream()
                        .filter(p -> name.equals(p.getName()))
                        .findAny()
                        .orElse(null);
            }
            return studentDomain;
        } else {
            return null;
        }
    }catch (Exception ex){ 
        return null;
    }
}
6 Answers

Streams should not be overused, sometimes it makes sense to get back to loops. In your case foreach would be simpler and nicer. Something like (pseudocode):

Student res;

for (Student s : students) {
    if s.id == id -> { res = s; break; }
    if s.name == name -> res = s;
}

Checked it again, sorry. With prioritized id you have to check twice. Which justifies another method:

public Student getStudent(String id, String name) {
    try {
        List<Student> students = studentDao.getStudent();
        Student studentDomain = find(students, s -> id.equals(s.getId()));
        return studentDomain != null 
                                ? studentDomain 
                                : find(students, s -> name.equals(s.getName()));
    } catch (Exception ex) {
        return null;
    }
}

static <T> T find(Collection<T> l, Predicate<T> p) {
    return l.stream().filter(p).findAny().orElse(null);
}

No, you cannot do it in one single stream. In one stream one could keep a matched name as side effect and check the id. But that is ugly.

In this case you have to check all students for an id and failing that, for a name, again over all students.

When reaching the end of a Stream you cannot skip back to the beginning - in the same stream.

However it would be beneficial to have a Map from id to Student.

When there is a Optional<Student> StudentDao#findById:

return studentDao.findById(id)

When there is (an inferiaor) <Student> StudentDao#findById:

return Optional.ofNullable(studentDao.findById(id))

You need only search by name:

    .orElseGet(() -> studentDao.getStudent().stream()
                    .filter(p -> name.equals(p.getName()))
                    .findAny()
                    .orElse(null);

The best would be to leave filtering by name to the StudentDao.

Also a better practice would be

public Optional<Student> getStudent(String id, String name)  {


getStudent("6534266", "Joe").ifPresent(student -> ...);

For Java 8 you can use the following approach:

public Student getStudent(String id,String name)  {

    List<Student> students = studentDao.getStudent();
    
    return students.stream()
        .filter(student -> student.getId().equals(id))
        .findFirst()
        .orElseGet(() -> students.stream()
            .filter(student -> student.getName().equals(name))
            .findFirst()
            .orElse(null)
        );
}

I've ignored your attempt to catch a Exception. Firstly, never super-types Exception and Throwable, your catch blocks should target a specific exception. And never catch runtime exceptions, unless you're not performing some kind of actions (like to undo partial changes made in the try block) and then propagating (throwing from the catch) this exception further.

In case if you expect the list of student returned by studentDao.getStudent() to be null, it would be way cleaner to make it return an empty list by default instead of clattering your code with defensive null-checks everywhere you use it.

Do you expect that elements in the collection of students would be null? Well, that an antipattern - never store null in the collection.

Note that returning null from getStudent makes sense only if you have a code calling this method which consumes null (makes null-checks) and which can not be changed. Otherwise - leverage the boons of Java 8 to full power, return Optional<Student>.

Here's the possible solution for Java 9 (method or() was introduced with Java 9) returning Optional<Student>:

public Optional<Student> getStudent(String id,String name)  {
    
    List<Student> students = studentDao.getStudent();
    
    return students.stream()
        .filter(student -> student.getId().equals(id))
        .findFirst()
        .or(() -> students.stream()
            .filter(student -> student.getName().equals(name))
            .findFirst()
        );
}

Another approach is to make a mutable result container which collects the results, prioritizing any found id over any found name.

First, let's define such a result container:

public class OrderedKeyMatcher<T> {

    private int predicateLevel;
    private T result;
    private final List<Predicate<T>> predicates;

    public OrderedKeyMatcher(Predicate<T>... predicates) {
        this.predicates = List.of(predicates);
        this.predicateLevel = predicates.length;
    }

    public OrderedKeyMatcher<T> accept(T t) {
        for (int i = 0; i < predicateLevel; i++) {
            if (predicates.get(i).test(t)) {
                predicateLevel = i;
                result = t;
                break;
            }
        }
        return this;
    }

    public boolean isDefinitelyFound() {
        return predicateLevel == 0;
    }

    public Optional<T> result() {
        return Optional.ofNullable(result);
    }
}

In short, this is how it works:

  • In that order, the predicates field holds the predicates to which the students are tested.
  • The result field holds any found student.
  • The predicateLevel holds an integer, which keeps track of what predicate matched the last result. This is because of the following: if we found a student with the given name, we don't need to search any further for students with the given name, because we already found one; however, we still want to try to find a student with the given id.
  • Further, if a student is matched by the first predicate, we can tell the stream to stop altogether because we have definitely found a student matching or predicates. The isDefinitelyFound() method can be used for this.

This is then what the stream looks like:

var matcher = new OrderedKeyMatcher<Student>(
    s -> Objects.equals(s.id(), id),
    s -> Objects.equals(s.name(), name)
);

Optional<Student> optStudent = studentDao.getStudent().stream()
    .takeWhile(unused -> !matcher.isDefinitelyFound())
    .reduce(matcher, OrderedKeyMatcher::accept, (a, b) -> a)
    .result();
  • takeWhile is used to short-circuit the stream if a result is found. As noted above, if we find a student with a matching name, then we still need to inspect other elements to see if there's a student with a matching id, but once we find a student with a matching id, then we can tell the stream to terminate.
  • reduce collects the elements into our result container.1
  • At last, with result() we can collect the result as an Optional.

Example

Let's take a look at the following list:

List.of(
    new Student("11", "John"),
    new Student("13", "Jill"),
    new Student("17", "Jack"),
    new Student("17", "Anthony"),
    new Student("23", "Bonnie"),
    new Student("47", "Megan")
);

Suppose we are trying to match id = "23" and name = "Jack". The expected and actual result is that we'll find student Bonnie because she has id 23. Now let's say we'll remove Bonnie from the list, and search again. The result is that we find Jack.


1. Note that I used (a, b) -> a as a merge function, which is, of course, a dummy merge function, as it drops at least half of the results.

There is the possibility by library abacus-common

StreamEx.of(student).findFirstOrAny(p -> id.equals(p.getId()), p -> name.equals(p.getName());

Not sure which db access library you're using for studentDao, There is another library abacus-jdbc created based on abacus-common. It provides the Stream apis for Dao:

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @Id
    private long id;
    private String name;
    //......
}

......

public interface StudentDao extends CrudDao<Student, Long, SQLBuilder.PSC, StudentDao>  {
    // no implementation required.
}

......
static final StudentDao studentDao = JdbcUtil.createDao(StendentDao.class, dataSouce);

......
long id = 123;
String name = "xyz";
// if student queried by condition: eq("id", 123).or("name", name)
Optional<Student> result = studentDao.stream(eq("id", 123).or(eq("name", name)))
                                 .findFirstOrAny(p -> p.getId() == id);

// Or by other condition:
result = studentDao.stream(cond)
    .findFirstOrAny(p -> p.getId() == id, p -> name.equals(p.getName());

Disclaimer: I'm the developer of abacus-common and abacus-jdbc.

Related