Generic method to perform a map-reduce operation.

2019-02-09 07:04发布

问题:

How to overload a Function with generic parameter in Java 8?

public class Test<T> {

    List<T> list = new ArrayList<>();

    public int sum(Function<T, Integer> function) {
        return list.stream().map(function).reduce(Integer::sum).get();
    }


    public double sum(Function<T, Double> function) {
        return list.stream().map(function).reduce(Double::sum).get();
    }
}

Error: java: name clash: sum(java.util.function.Function<T,java.lang.Double>) and sum(java.util.function.Function<T,java.lang.Integer>) have the same erasure

回答1:

The example you present in your question has got nothing to do with Java 8 and everything to do with how generics work in Java. Function<T, Integer> function and Function<T, Double> function will go through type-erasure when compiled and will be transformed to Function. The rule of thumb for method overloading is to have different number, type or sequence of parameters. Since both your methods will transform to take a Function argument, the compiler complains about it.

That being said, srborlongan has already provided one way to resolve the issue. The problem with that solution is that you have to keep modifying your Test class for each and every type of operation (addition,subtraction,etc) on different types (Integer,Double, etc). An alternate solution would be to use method overriding instead of method overloading :

Change the Test class a bit as follows :

public abstract class Test<I,O extends Number> {

    List<I> list = new ArrayList<>();

    public O performOperation(Function<I,O> function) {
        return list.stream().map(function).reduce((a,b)->operation(a,b)).get();
    }

    public void add(I i) {
        list.add(i);
    }

    public abstract O operation(O a,O b);
}

Create a subclass of Test that will add two Integers.

public class MapStringToIntAddtionOperation extends Test<String,Integer> {

    @Override
    public Integer operation(Integer a,Integer b) {
        return a+b;
    }

}

Client code can then use the above code as follows :

public static void main(String []args) {
    Test<String,Integer> test = new MapStringToIntAddtionOperation();
    test.add("1");
    test.add("2");
    System.out.println(test.performOperation(Integer::parseInt));
}

The advantage of using this approach is that your Test class is in line with the open-closed principle. To add a new operation such as multiplication, all you have to do is add a new subclass of Test and override the operation method to multiply two numbers. Club this with the Decorator pattern and you can even minimize the number of sub-classes that you have to create.

Note The example in this answer is indicative. There are a lot of areas of improvement (such as make Test a functional interface instead of an abstract class) which are beyond the scope of the question.



回答2:

Benji Weber once wrote of a way to circumvent this. What you need to do is to define custom functional interfaces that extend the types for your parameters:

public class Test<T> {

    List<T> list = new ArrayList<>();

    @FunctionalInterface
    public interface ToIntFunction extends Function<T, Integer>{}
    public int sum(ToIntegerFunction function) {
        return list.stream().map(function).reduce(Integer::sum).get();
    }


    @FunctionalInterface
    public interface ToDoubleFunction extends Function<T, Double>{}
    public double sum(ToDoubleFunction function) {
        return list.stream().map(function).reduce(Double::sum).get();
    }
}

Another way is to use java.util.function.ToIntFunction and java.util.function.ToDoubleFunction instead:

public class Test<T> {

    List<T> list = new ArrayList<>();

    @FunctionalInterface
    public int sum(ToIntFunction function) {
        return list.stream().mapToInt(function).sum();
    }

    public double sum(ToDoubleFunction function) {
        return list.stream().mapToDouble(function).sum();
    }
}


回答3:

@srborlongan 's solution won't work very well :)

See a similar example - Comparator methods - comparingDouble(ToDoubleFunction), comparingInt(ToIntFunction), etc. The methods have different names, because overloading is not a good idea here.

The reason is, when you do sum(t->{...}), the compiler is unable to infer which method to call; actually it needs to resolve method overloading first, to pick one method, before inferring the type of the implicit lambda expression (based on that method's signature)

This is disappointing. In the earlier stage, Java8 had a more sophisticated inference engine, and Comparator had overloaded comparing() methods; and sum(t->{...}) would be correctly inferred too. Unfortunately, they decided to simply it :( And here we are now.

Rule of thumb for overloading methods with functional arguments: the arities of the functional interfaces must be different, unless both are 0.

// OK, different arity
m1( X->Y )
m1( (X1, X2)->Y )

// not OK, both are arity 1
m2( X->Y )
m2( A->B )

    m2( t->{...} ); // fail; type of `t` cannot be inferred 

// OK! both are arity 0
m3( ()->Y )
m3( ()->B )

The reason why overloading with arity 0 is OK is that the lambda expressions won't be implicit - all argument types are known (because there's no argument!), we don't need contextual information for inferring the lambda type

m3( ()-> return new Y() );   // lambda type is ()->Y
m3( ()-> return new B() );   // lambda type is ()->B