I would say this pattern creates some ambiguity. What should the result be, for instance, if the caller does not call either include() OR exclude()? There is no chance to catch this situation statically, so either you have to deal with this ambiguity in your library or throw a runtime exception.
As a suggestion, why not make the return value of the callback an action defining what to do with the element?
If we call what you want to do a Milter (Map + Filter), you might end up with something like this (Don't have a compiler here so this may not be picture-perfect):
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Test
{
public static void main(String[] args)
{
Milter.Visitor<Integer> visitor = new Milter.Visitor<Integer>() {
@Override
public MilterAction<Integer> visit(Integer element) {
if (element % 2 == 0) {
return MilterAction.exclude();
} else {
return MilterAction.include(element + 2);
}
}
};
Milter<Integer> oddsPlus2Milter = new Milter<Integer>(visitor);
List<Integer> sourceList = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
oddsPlus2Milter.run(sourceList).forEach(System.out::println);
}
}
class Milter<T> {
public interface Visitor<I> {
MilterAction<I> visit(I element);
}
private Visitor<T> visitor;
Milter (Visitor<T> visitor) {
this.visitor = visitor;
}
List<T> run(List<T> source) {
List<T> milteredResult = new ArrayList<T>();
source.forEach(element -> {
MilterAction<T> visitResult = this.visitor.visit(element);
if (visitResult.shouldInclude()) {
milteredResult.add(visitResult.getValue());
}
});
return milteredResult;
}
}
class MilterAction<T> {
public static <I> MilterAction<I> include(I value) {
return new MilterAction<I>(FilterAction.INCLUDE, value);
}
public static <I> MilterAction<I> exclude() {
return new MilterAction<I>(FilterAction.EXCLUDE, null);
}
public enum FilterAction {
INCLUDE,
EXCLUDE
}
private FilterAction action;
private T value;
MilterAction(FilterAction action, T value) {
this.action = action;
this.value = value;
}
public T getValue() {
return value;
}
public boolean shouldInclude() {
return this.action.equals(FilterAction.INCLUDE);
}
}
This way, your compiler can ensure for you that all consumer's of the Milter class provide it with a visitor that always returns an appropriate action.