Search code examples
javafilterjava-8java-streamcollectors

Filtering Object having a null property along with Objects with non-null property matching certain criteria without getting NPE


I have a list of objects List<Objs>.

Each object has a property objId of type String which can be initialized with a comma-separated string like "1,2,3".

And I have a string array sections containing comma-separated strings like "2,4".

I'm splitting those strings using split(',') into arrays and then performing comparison for each element (e.g. "1,2" -> {"1,","2"}).

I'm trying to collect all the objects which are either match StringUtils.isEmpty(object.getObjId) or their objId match one of the sections values.

That is the desired behavior depending on the value of objId:

Case:1 objId = "1,2,3" and sections = "2,4", as long as both have one value match "2", hence object should be included into the list.

Case:2 objId = "1,3" and sections = "2", since both don't have match value and hence object should not be included into the list.

Case:3 objId = null || "" and doesn't matter what sections = "{any}" (meaning the contents of sections doesn't matter), the object should be included into the list.

Case:4 sections = null || "", only object having objId = null || "" should be included into the list.

The sample code below always returns me a NullPointerException. Could someone help to correct me?

My code:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestStream {

    private List<Objs> getData() {
        List<Objs> objects = new ArrayList<>();
        Objs objs1 = new Objs();
        Objs objs2 = new Objs();
        Objs objs3 = new Objs();
        Objs objs4 = new Objs();
        objs1.setObjId("1,2,3");
        objects.add(objs1);
        objs1.setObjId("4,5,6");
        objects.add(objs2);
        objs2.setObjId("7,84,3");
        objects.add(objs3);
        objs3.setObjId(null);
        objects.add(objs4);
        return objects;
    }
    private List<Objs> test () {
        String[] sections = "2,8".split(",");
        List<Objs> objects = getData();
        List<Objs> result = objects.stream()
            .filter(object ->  object.getObjId() == null)
            .filter(object -> Arrays.stream(sections).anyMatch(object.getObjId()::contains))
            .skip(0)
            .limit(2)
            .collect(Collectors.toList());
    return result;
    }
    public static void main(String[] args) {
        TestStream testStream= new TestStream();
        List<Objs> result = testStream.test();
        System.out.println(Arrays.toString(result.toArray()));
    }

    class Objs {
        private String objId;
        public void setObjId(String id) { objId = id;}
        public String getObjId() { return objId;}
    }
}

The skip and limit is for paging, currently I set it to skip 0 and limit for each page is 3 which means I am trying to retrieve the first page, each page can only have 3 items.


Solution

  • NullPointerException is being produced in the nested stream created inside the second filter:

    .filter(object -> Arrays.stream(sections).anyMatch(object.getObjId()::contains))
    

    It occurs while attempting to invoke method contains on the property of your object retrieved by getObjId() and objId happens to be null.

    Why getObjId() returns null ? There's an object in your sample data which has this field set to null and in the first filter you're specifically telling that you want to retain in the stream pipeline only those objects whose objId is null:

    filter(object -> object.getObjId() == null)
    

    You might have a misconception on how filter works, it will retain in the stream only those elements for which the predicate passed to the filter would be evaluated to true, not the opposite.

    Here is a quote from the documentation:

    Returns a stream consisting of the elements of this stream that match the given predicate.

    You can to change the predicate in the first filter to object.getObjId() != null, but that can lead to incorrect results because Case 3 of your requirements will not be fulfilled.

    Case:3 objId = null || "" and doesn't matter what sections = "{any}" (meaning the contents of sections doesn't matter), the object should be included into the list.

    If filter out only objects having non-null id, then we will lose the objects with id null (if any), and empty string id also might not pass the criteria. Otherwise, if we would not apply any filtering before accessing id, it will result in getting NullPointerException.

    Conclusion: to retain objects having id equal to null and to an empty string and also objects having id matching to one of the strings in the sections array, we can not use two separate filters.

    Instead, we need to we need to combine these filters in a single filter.

    To simplify the code, we can define two predicates. One as static field for checking if the id is null or an empty a string. Another as a local variable because it'll depend on the sections array.

    That's how it might look like (the full code provided in the link below):

    public static final Predicate<Objs> IS_NULL_OR_EMPTY =
         object -> object.getObjId() == null || object.getObjId().isEmpty();
    
    private List<Objs> test(String section) {
        String[] sections = section.split(",");
        List<Objs> objects = getData();
        
        Predicate<Objs> hasMatchingSection =
            object -> Arrays.stream(sections).anyMatch(object.getObjId()::contains);
        
        return objects.stream()
            .filter(IS_NULL_OR_EMPTY.or(hasMatchingSection))
            .limit(2)
            .collect(Collectors.toList());
    }
    

    I didn't apply skip() because it doesn't make sense skipping 0 elements.

    A link to Online Demo