1. 概述

在本文中,我们将深入探讨 Java 8 引入的 双冒号操作符::),并分析它在哪些场景下可以简化代码逻辑、提升可读性。

2. Lambda 表达式到双冒号操作符的演进

自从引入 Lambda 表达式以来,Java 的语法变得越来越简洁。

比如我们要创建一个比较器,传统写法如下:

Comparator<Computer> c = (Computer c1, Computer c2) -> c1.getAge().compareTo(c2.getAge());

利用类型推断后可以简化为:

Comparator<Computer> c = (c1, c2) -> c1.getAge().compareTo(c2.getAge());

不过,我们还能不能更进一步?来看这个写法:

Comparator<Computer> c = Comparator.comparing(Computer::getAge);

✅ 这就是使用 :: 操作符来替代 Lambda 调用特定方法的方式。最终效果是代码更加清晰、易读。

3. 双冒号操作符是如何工作的?

简单来说,当我们使用方法引用时,目标引用放在 :: 前面,方法名放在后面。

例如:

Computer::getAge;

这表示对 Computer 类中的 getAge 方法进行引用。

我们可以将其赋值给一个函数式接口,并调用:

Function<Computer, Integer> getAge = Computer::getAge;
Integer computerAge = getAge.apply(c1);

⚠️ 注意:我们是先引用了方法,再传入合适的参数去执行它。

4. 方法引用的不同形式

:: 操作符可以在多种场景中使用,下面分别介绍。

4.1. 静态方法引用

我们可以引用静态方法,比如:

List<Computer> inventory = Arrays.asList(
  new Computer(2015, "white", 35),
  new Computer(2009, "black", 65)
);
inventory.forEach(ComputerUtils::repair);

4.2. 已有对象的实例方法引用

有时候我们需要引用某个已存在对象的实例方法,比如:

Computer c1 = new Computer(2015, "white");
Computer c2 = new Computer(2009, "black");
Computer c3 = new Computer(2014, "black");
Arrays.asList(c1, c2, c3).forEach(System.out::println);

这里我们使用了 System.outprintln 方法。

4.3. 特定类型任意对象的实例方法引用

Computer c1 = new Computer(2015, "white", 100);
Computer c2 = new MacbookPro(2009, "black", 100);
List<Computer> inventory = Arrays.asList(c1, c2);
inventory.forEach(Computer::turnOnPc);

✅ 注意:我们不是引用某个具体实例的方法,而是对类型本身的方法进行引用。

在第四行,turnOnPc 方法会分别在 c1c2 上被调用,即多态行为。

4.4. 父类方法引用

假设 Computer 类中有如下方法:

public Double calculateValue(Double initialValue) {
    return initialValue / 1.50;
}

而在 MacbookPro 子类中覆盖并调用父类方法:

@Override
public Double calculateValue(Double initialValue){
    Function<Double, Double> function = super::calculateValue;
    Double pcValue = function.apply(initialValue);
    return pcValue + (initialValue / 10);
}

此时调用:

macbookPro.calculateValue(999.99);

会先执行父类的 calculateValue 方法,再做额外计算。

5. 构造器引用

5.1. 创建对象实例

通过构造器引用创建对象非常直观:

@FunctionalInterface
public interface InterfaceComputer {
    Computer create();
}

InterfaceComputer c = Computer::new;
Computer computer = c.create();

如果构造器需要两个参数:

BiFunction<Integer, String, Computer> c4Function = Computer::new; 
Computer c4 = c4Function.apply(2013, "white");

如果参数更多,则需要自定义函数式接口:

@FunctionalInterface 
interface TriFunction<A, B, C, R> { 
    R apply(A a, B b, C c); 
    default <V> TriFunction<A, B, C, V> andThen(Function<? super R, ? extends V> after) { 
        Objects.requireNonNull(after); 
        return (A a, B b, C c) -> after.apply(apply(a, b, c)); 
    } 
}

然后使用:

TriFunction<Integer, String, Integer, Computer> c6Function = Computer::new;
Computer c3 = c6Function.apply(2008, "black", 90);

5.2. 创建数组

使用构造器引用创建数组也很方便:

Function<Integer, Computer[]> computerCreator = Computer[]::new;
Computer[] computerArray = computerCreator.apply(5);

6. 总结

从上面的例子可以看出,双冒号操作符在 Java 8 中引入后,极大地简化了方法引用的写法,特别是在配合 Stream API 使用时,代码可读性和简洁性都得到了显著提升。

⚠️ 要想真正掌握它,建议深入理解函数式接口的使用机制,这有助于你写出更优雅的代码。


原始标题:The Double Colon Operator in Java | Baeldung