Search code examples
javajava-streamiterationcollectors

How to avoid multiple Streams with Java 8


I am having the below code

trainResponse.getIds().stream()
        .filter(id -> id.getType().equalsIgnoreCase("Company"))
        .findFirst()
        .ifPresent(id -> {
            domainResp.setId(id.getId());
        });

trainResponse.getIds().stream()
        .filter(id -> id.getType().equalsIgnoreCase("Private"))
        .findFirst()
        .ifPresent(id ->
            domainResp.setPrivateId(id.getId())
        );

Here I'm iterating/streaming the list of Id objects 2 times.

The only difference between the two streams is in the filter() operation.

How to achieve it in single iteration, and what is the best approach (in terms of time and space complexity) to do this?


Solution

  • You can achieve that with Stream IPA in one pass though the given set of data and without increasing memory consumption (i.e. the result will contain only ids having required attributes).

    For that, you can create a custom Collector that will expect as its parameters a Collection attributes to look for and a Function responsible for extracting the attribute from the stream element.

    That's how this generic collector could be implemented.

    /** *
     * @param <T> - the type of stream elements
     * @param <F> - the type of the key (a field of the stream element)
     */
    class CollectByKey<T, F> implements Collector<T, Map<F, T>, Map<F, T>> {
        private final Set<F> keys;
        private final Function<T, F> keyExtractor;
        
        public CollectByKey(Collection<F> keys, Function<T, F> keyExtractor) {
            this.keys = new HashSet<>(keys);
            this.keyExtractor = keyExtractor;
        }
        
        @Override
        public Supplier<Map<F, T>> supplier() {
            return HashMap::new;
        }
        
        @Override
        public BiConsumer<Map<F, T>, T> accumulator() {
            return this::tryAdd;
        }
        
        private void tryAdd(Map<F, T> map, T item) {
            F key = keyExtractor.apply(item);
            if (keys.remove(key)) {
                map.put(key, item);
            }
        }
        
        @Override
        public BinaryOperator<Map<F, T>> combiner() {
            return this::tryCombine;
        }
        
        private Map<F, T> tryCombine(Map<F, T> left, Map<F, T> right) {
            right.forEach(left::putIfAbsent);
            return left;
        }
        
        @Override
        public Function<Map<F, T>, Map<F, T>> finisher() {
            return Function.identity();
        }
        
        @Override
        public Set<Characteristics> characteristics() {
            return Collections.emptySet();
        }
    }
    

    main() - demo (dummy Id class is not shown)

    public class CustomCollectorByGivenAttributes {
        public static void main(String[] args) {
            List<Id> ids = List.of(new Id(1, "Company"), new Id(2, "Fizz"),
                                   new Id(3, "Private"), new Id(4, "Buzz"));
            
            Map<String, Id> idByType = ids.stream()
                    .collect(new CollectByKey<>(List.of("Company", "Private"), Id::getType));
            
            idByType.forEach((k, v) -> {
                if (k.equalsIgnoreCase("Company")) domainResp.setId(v);
                if (k.equalsIgnoreCase("Private")) domainResp.setPrivateId(v);
            });
        
            System.out.println(idByType.keySet()); // printing keys - added for demo purposes
        }
    }
    

    Output

    [Company, Private]
    

    Note, after the set of keys becomes empty (i.e. all resulting data has been fetched) the further elements of the stream will get ignored, but still all remained data is required to be processed.