方法引用静态方法或返回lambdas的静态方法

时间:2019-05-27 19:16:47

标签: java java-8

在开发过程中,我总是不得不一遍又一遍地重写相同的lambda表达式,这是多余的,在大多数情况下,我公司实施的代码格式化策略无济于事。因此,我将这些常见lambdas 作为静态方法移至实用程序类,并将其用作方法引用。我最好的例子是与Java.util.stream.Collectors.toMap(Function,Function,BinaryOperator,Supplier)结合使用的Throwing合并。 总是必须写(a,b)-> {抛出新的IllegalArgumentException(“ Some message”);};只是因为我想使用自定义地图实现非常麻烦。

//First Form

public static <E> E throwingMerger(E k1, E k2) {
    throw new IllegalArgumentException("Duplicate key " + k1 + " not allowed!");
  }

//Given a list of Car objects with proper getters
Map<String,Car> numberPlateToCar=cars.stream()//
   .collect(toMap(Car::getNumberPlate,identity(),StreamUtils::throwingMerger,LinkedHasMap::new))
//Second Form 

  public static <E> BinaryOperator<E> throwingMerger() {
    return (k1, k2) -> {
      throw new IllegalArgumentException("Duplicate key " + k1 + " not allowed!");
    };
  }
Map<String,Car> numberPlateToCar=cars.stream()//
   .collect(toMap(Car::getNumberPlate,identity(),StreamUtils.throwingMerger(),LinkedHasMap::new))

我的问题如下:

  • 以上哪种方法是正确的,为什么?

  • 其中一个是提供性能优势还是损害性能?

2 个答案:

答案 0 :(得分:3)

两个变体都不比另一个更正确。

此外,由于相关字节码甚至相同,因此没有明显的性能差异。无论哪种情况,都会在您的类中有一个持有throw语句的方法,以及一个运行时生成的类的实例,它将调用该方法。

请注意,您可以在JDK本身中找到这两种模式。

  • Function.identity()Map.Entry.comparingByKey()是包含lambda表达式的工厂方法的示例
  • Double::sumObjects::isNullObjects::nonNull是对仅以这种方式被引用的目标方法存在的方法引用的示例

通常,如果还有直接调用方法的用例,则最好将它们作为API方法提供,也可以由方法引用来引用,例如Integer::compareObjects::requireNonNullMath::max

另一方面,提供工厂方法会使该方法引用一个实现细节,您可以在有理由的时候更改该实现细节。例如,您是否知道Comparator.naturalOrder()不是 实施为T::compareTo?大多数时候,您不需要知道。

当然,带有附加参数的工厂方法根本无法用方法引用代替;有时,您希望类的无参数方法与采用参数的方法对称。


内存消耗只有很小的差异。给定当前的实施方式,每次发生例如Objects::isNull将导致创建运行时类和实例,然后将其重新用于特定的代码位置。相反,Function.identity()中的实现仅实现一个代码位置,因此仅实现一个运行时类和实例。另请参见this answer

但是必须强调的是,这是特定于特定实现的,因为该策略是由JRE实现的,此外,我们所讨论的是有限的,数量很少的代码位置,因此是对象。


顺便说一句,这些方法并不矛盾。您甚至可以同时拥有:

// for calling directly
public static <E> E alwaysThrow(E k1, E k2) {
    // by the way, k1 is not the key, see https://stackoverflow.com/a/45210944/2711488
    throw new IllegalArgumentException("Duplicate key " + k1 + " not allowed!");
}
// when needing a shared BinaryOperator
public static <E> BinaryOperator<E> throwingMerger() {
    return ContainingClass::alwaysThrow;
}

请注意还有一点要考虑; factory方法始终返回特定接口的实例化实例,即BinaryOperator。对于需要根据上下文绑定到不同接口的方法,无论如何,您都需要在这些位置引用方法。这就是为什么你可以写

DoubleBinaryOperator sum1 = Double::sum;
BinaryOperator<Double> sum2 = Double::sum;
BiFunction<Integer,Integer,Double> sum3 = Double::sum;

如果只有工厂方法返回DoubleBinaryOperator,这是不可能的。

答案 1 :(得分:1)

编辑:忽略我关于避免不必要分配的评论,请参阅Holgers关于原因的答案。

两者之间不会有明显的性能差异-第一个变体是避免不必要的分配。我更喜欢方法引用,因为函数不会捕获任何值,因此在这种情况下不需要lambda。与创建IllegalArgumentException(在抛出之前必须先填充其堆栈跟踪记录(这非常昂贵))相比,性能差异可以忽略不计。

请记住:与性能无关,这更多是关于可读性和传达代码的作用。如果您因为这种代码而碰到了性能瓶颈,那么lambda和stream就是不走的路,因为它们是包含许多间接指令的相当精致的抽象。