Gunnar
Gunnar

Reputation: 18990

Use specific consumer per element type with Java 8 stream

I'm having a Java 8 stream of numbers:

Stream<Number> numbers = ...;

I'd like to iterate the stream and invoke a specific consumer based on the type of each element. I.e. for Integer elements I'd like to invoke a Consumer<Integer>, for Long a Consumer<Long> etc.

There is the forEach() method but this expects a Consumer<? super Number>, requiring that implementation (usually a Lambda expression) to instanceof-switch on the exact type itself.

In terms of an API, I'm essentially looking for something like this:

numbers.forEach( callbacks -> {
    callbacks.on( Integer.class, i -> { /* handle Integer */ } );
    callbacks.on( Long.class, l -> { /* handle Long */ } )
} );

Is there any existing API which would allow me to register a specific consumer per stream element sub-type in a way similar to this?

Upvotes: 5

Views: 2062

Answers (2)

Tamas Hegedus
Tamas Hegedus

Reputation: 29906

I do not know about such built-ins, so here is an option: Gather all the Consumers in a Map keyed by the type it consumes, and look up the right consumer. I would recommend this solution if you work with a lot of types, and those types can change dynamically. The most efficient solution (by cpu consumption) would probably be to use a switch on item.getType().

public class Casts {
    public static void main(String[] args) {

        Stream<Number> stream = Arrays.stream(new Number[] { 3, 4L });

        Map<Class<?>, Consumer<? super Number>> consumers = new HashMap<>();
        putCastConsumer(consumers, Long.class,
                i -> System.out.println("Mapped long " + i));
        putCastConsumer(consumers, Integer.class,
                i -> System.out.println("Mapped int " + i));

        consumeByType(stream, consumers);
    }

    public static <U, T extends U> void putCastConsumer(
            final Map<Class<?>, Consumer<? super U>> map,
            final Class<T> clazz,
            final Consumer<T> consumer) {
        map.put(clazz, value -> consumer.accept(clazz.cast(value)));
    }

    public static <T> void consumeByType(
            final Stream<T> stream,
            final Map<Class<?>, Consumer<? super T>> consumers) {
        stream.forEach(item -> consumers.get(item.getClass()).accept(item));
    }
}

Upvotes: 2

Misha
Misha

Reputation: 28133

Are you sure you don't want to just run the stream twice? It will be more readable.

But if you want, you can define a type-checking consumer like this:

public static<T> Consumer<Object> acceptType(Class<T> clazz, Consumer<? super T> cons) {
    return t -> {
        if (clazz.isInstance(t)) {
            cons.accept(clazz.cast(t));
        }
    };
}

You can then combine multiple consumers using andThen:

Consumer<Object> combined = acceptType(Integer.class, i -> ...)
                   .andThen(acceptType(Long.class, lng -> ...))

If you want to hide andThen, you can define

static<T> Consumer<T> doAll(Consumer<T>... consumers) {
    return Arrays.stream(consumers)
            .reduce(Consumer::andThen)
            .orElse(t -> {});
}

Then it becomes

nums.forEach(doAll(
            acceptType(Integer.class, i -> ...),
            acceptType(Long.class, lng -> ..),
            ...
));

Upvotes: 7

Related Questions