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.out
的 println
方法。
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
方法会分别在 c1
和 c2
上被调用,即多态行为。
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 使用时,代码可读性和简洁性都得到了显著提升。
⚠️ 要想真正掌握它,建议深入理解函数式接口的使用机制,这有助于你写出更优雅的代码。