执行 map-reduce 操作的通用方法。 (Java-8)

Posted

技术标签:

【中文标题】执行 map-reduce 操作的通用方法。 (Java-8)【英文标题】:Generic method to perform a map-reduce operation. (Java-8) 【发布时间】:2015-08-29 20:34:05 【问题描述】:

如何在 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();
    

错误:java:名称冲突: sum(java.util.function.Function) 和 sum(java.util.function.Function) 具有相同的擦除

【问题讨论】:

与 Java 7、6 和 5 相同:您不能。这就是消息告诉您的内容。 【参考方案1】:

Benji Weber once wrote of a way to circumvent this.您需要做的是定义自定义功能接口,扩展您的参数类型:

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();
    

另一种方法是改用 java.util.function.ToIntFunction 和 java.util.function.ToDoubleFunction:

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();
    

【讨论】:

您对使用包装器接口的建议很好,但不是“干净”,因为每次需要引入新操作或新类型时都需要修改Test。只有两种类型(Integer 或 Doble)和两种运算(sum 和 multiply),您需要 4 个方法。更不用说您必须为每种类型创建的包装器接口。这是 API 的爆炸式增长。为链接 +1。 这不太好:) 类似的例子见Comparator comparingDouble(ToDoubleFunction), comparingInt(ToIntFunction) - 这些方法有不同的名字,因为重载不是一个好主意。【参考方案2】:

您在问题中提供的示例与 Java 8 无关,而是与泛型在 Java 中的工作方式有关。 Function&lt;T, Integer&gt; functionFunction&lt;T, Double&gt; function 在编译时会经过type-erasure 并转换为Function。方法重载的经验法则是具有不同数量、类型或顺序的参数。由于您的两种方法都将转换为采用 Function 参数,因此编译器会抱怨它。

话虽如此,srborlongan 已经提供了一种解决问题的方法。该解决方案的问题在于,您必须针对不同类型(整数、双精度等)的每种操作(加法、减法等)不断修改您的 Test 类。另一种解决方案是使用method overriding 而不是method overloading

Test 类更改如下:

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);

创建一个Test 的子类,它将添加两个Integers。

public class MapStringToIntAddtionOperation extends Test<String,Integer> 

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


然后客户端代码可以使用上面的代码如下:

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));

使用这种方法的好处是你的Test类符合open-closed的原则。要添加诸如乘法之类的新运算,您所要做的就是添加Testoverride 的新子类operation 方法来将两个数字相乘。将其与Decorator 模式结合使用,您甚至可以最大限度地减少必须创建的子类的数量。

注意此答案中的示例是指示性的。有很多改进的领域(例如将Test 设为函数式接口而不是抽象类)超出了问题的范围。

【讨论】:

使用不同的方法名要容易得多:) sumInt, sumDouble 这可以通过在Test 中使operation 成为protected 方法,在子类中创建sumXYZ 方法并在其中调用覆盖的operation 方法来轻松安排。 :) 但是就像我说的,有很多改进的领域超出了答案的范围..【参考方案3】:

@srborlongan 的解决方案不会很好用 :)

查看类似的示例 - Comparator 方法 - comparingDouble(ToDoubleFunction)comparingInt(ToIntFunction) 等。这些方法有不同的名称,因为这里重载不是一个好主意。

原因是,当你做sum(t-&gt;...)时,编译器无法推断出调用哪个方法;实际上它需要先解决方法重载,选择一个方法,然后再推断隐式 lambda 表达式的类型(基于该方法的签名)

这令人失望。在早期,Java8 有更复杂的推理引擎,Comparator 重载了comparing() 方法;并且sum(t-&gt;...) 也将被正确推断。不幸的是,他们决定简单地这样做:(我们现在就在这里。

使用函数参数重载方法的经验法则:函数接口的参数必须不同,除非两者都是 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 )

使用 arity 0 重载是可以的原因是 lambda 表达式不会是隐式的 - 所有参数类型都是已知的(因为没有参数!),我们不需要上下文信息来推断 lambda 类型

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

【讨论】:

以上是关于执行 map-reduce 操作的通用方法。 (Java-8)的主要内容,如果未能解决你的问题,请参考以下文章

map-reduce 中不同组件/动作的执行顺序

java字节码I++ ++j

map-reduce 如何在 HDFS 与 S3 上工作?

MongoDB下Map-Reduce使用简单翻译及示例

翻译MongoDB指南/聚合——聚合管道

数据库曾删改通用方法封装(根据曾 删 改 的sql语句来执行相应的业务操作,)