方法引用与构造器引用

Lambda 已经够简洁了——s -> s.length() 只有 11 个字符。但 Java 还能更简:String::length,9 个字符,而且更接近”自然语言”的表述。这就是方法引用(Method Reference),Lambda 的”极简形态”——当你写的 Lambda 只是”调用某个现有方法”时,可以直接用方法名替代。

方法引用是函数式编程系列的收尾。它看似只是语法糖,实则让代码更接近”声明式”的理想——读起来像在描述”做什么”,而不是”怎么做”。

一、方法引用是什么

方法引用用 :: 操作符,直接引用一个已存在的方法,作为函数式接口的实现。

// Lambda 写法
Function<String, Integer> f1 = s -> s.length();

// 方法引用
Function<String, Integer> f2 = String::length;

两者完全等价——都表示”接收一个 String,返回它的长度”。区别只是写法:方法引用更简洁,且强调了”复用已有方法”的意图。

:: 是 Java 8 引入的新操作符,读作”双冒号”。它的左侧是”方法所属者”(类名或对象),右侧是方法名。

二、四种方法引用

方法引用有四种形式,按”方法属于谁”分类:

形式语法等价 Lambda
静态方法引用类名::静态方法x -> 类名.静态方法(x)
实例方法引用(特定对象)对象::方法x -> 对象.方法(x)
类的实例方法引用类名::实例方法(obj, x) -> obj.方法(x)
构造器引用类名::newx -> new 类名(x)

外加一个特例:数组构造引用 类型[]::new

下面逐一详解。

三、静态方法引用:类名::静态方法

最直观的一种——引用一个类的静态方法。

// Lambda
Function<String, Integer> parser = s -> Integer.parseInt(s);

// 方法引用
Function<String, Integer> parser = Integer::parseInt;

Integer.parseInt(s) 是静态方法调用,所以用 Integer::parseInt

更多例子:

// 数学函数
Function<Double, Double> sqrt = Math::sqrt;        // x -> Math.sqrt(x)
UnaryOperator<Double> negate = Math::negateExact;  // x -> Math.negateExact(x)

// 字符串转大写(valueOf 是静态方法)
Function<Object, String> toStr = String::valueOf;  // x -> String.valueOf(x)

静态方法引用的语义清晰:参数就是静态方法的参数,返回值就是静态方法的返回值。

四、实例方法引用:对象::方法

引用一个特定对象的实例方法。这个对象在创建方法引用时就确定了。

String prefix = "Hello, ";

// Lambda
Consumer<String> c1 = s -> System.out.println(s);

// 方法引用(System.out 是特定对象)
Consumer<String> c2 = System.out::println;

// 自定义对象
Function<String, String> prepend = prefix::concat;   // s -> prefix.concat(s)
System.out.println(prepend.apply("World"));   // Hello, World

System.out 是一个特定的 PrintStream 对象,System.out::println 引用它的 println 方法。

这种形式常用于”把某个对象的方法当函数传”:

List<String> list = new ArrayList<>(List.of("A", "B", "C"));

// 把 list 的 add 方法当 Consumer 用
Consumer<String> adder = list::add;
adder.accept("D");
adder.accept("E");
System.out.println(list);   // [A, B, C, D, E]

五、类的实例方法引用:类名::实例方法

这是最”烧脑”的一种,但理解后很强大。

5.1 第一个参数成为调用者

当你写 类名::实例方法 时,Lambda 的第一个参数成为方法的调用者,其余参数成为方法的参数:

// Lambda: 两个参数,第一个调用 toUpperCase
BiFunction<String, Void, String> f1 = (s, v) -> s.toUpperCase();

// 方法引用:String::toUpperCase
// 等价于 (s) -> s.toUpperCase()
Function<String, String> upper = String::toUpperCase;

对比静态方法引用——静态方法引用的参数全部是方法的参数;而类名::实例方法的第一个参数是调用者

// 静态方法引用:参数是 parseInt 的参数
Function<String, Integer> f1 = Integer::parseInt;
// 等价于 s -> Integer.parseInt(s)

// 类的实例方法引用:第一个参数是调用者
Function<String, String> f2 = String::toUpperCase;
// 等价于 s -> s.toUpperCase()

区别:Integer::parseInt 的参数是传给 parseInt 的;String::toUpperCase 的参数是调用 toUpperCase 的对象

5.2 两个参数的情况

// Lambda: 两个参数,第一个调用 compareTo
Comparator<String> c1 = (a, b) -> a.compareTo(b);

// 方法引用:第一个参数 a 成为调用者,第二个参数 b 成为 compareTo 的参数
Comparator<String> c2 = String::compareTo;

String::compareTo 等价于 (a, b) -> a.compareTo(b)——第一个参数 a 调用 compareTo,第二个参数 b 传进去。

这种形式在比较器、字符串操作中极其常见:

// 按字典序排序
list.sort(String::compareTo);   // 等价于 (a, b) -> a.compareTo(b)

// 是否包含
BiPredicate<String, String> contains = String::contains;
// 等价于 (s, sub) -> s.contains(sub)
contains.test("hello world", "world");   // true

5.3 为什么有这种设计

初看”第一个参数当调用者”很别扭,但它解决了”实例方法也是函数”的问题。在面向对象里,方法是属于对象的——s.length() 必须有个 s 来调用。但在函数式里,我们想把它当成 String -> int 的函数。String::length 正是这种”把方法从对象中解放”的写法——编译器自动把第一个参数当调用者。

六、构造器引用:类名::new

构造器引用用 类名::new 表示,等价于 args -> new 类名(args)

// Lambda
Supplier<List<String>> s1 = () -> new ArrayList<>();

// 构造器引用
Supplier<List<String>> s2 = ArrayList::new;

// 带参数
Function<Integer, ArrayList<String>> f1 = n -> new ArrayList<>(n);
Function<Integer, ArrayList<String>> f2 = ArrayList::new;   // 调用 ArrayList(int) 构造器

构造器引用会根据”目标类型”的签名匹配对应构造器——Supplier 无参,匹配无参构造器;Function<Integer, ...> 一参,匹配 ArrayList(int) 构造器。

6.1 实战:把流元素转成对象

构造器引用在 Stream 中很有用——把元素流”转换成对象流”:

// 把字符串流转成 Person 流
List<Person> people = names.stream()
    .map(Person::new)        // 调用 new Person(name)
    .collect(Collectors.toList());

// 把字符串流转成 List
List<List<String>> lists = sizes.stream()
    .map(ArrayList::new)     // 调用 new ArrayList(size)
    .collect(Collectors.toList());

6.2 复制构造器

// 用复制构造器做防御性拷贝
List<String> original = List.of("A", "B", "C");
ArrayList<String> copy = original.stream()
    .collect(Collectors.toCollection(ArrayList::new));
// 或者
ArrayList<String> copy2 = new ArrayList<>(original);   // 更直接

七、数组构造引用:类型[]::new

数组也能用 new 创建,所以也有”数组构造引用”:

// Lambda
IntFunction<String[]> f1 = n -> new String[n];

// 数组构造引用
IntFunction<String[]> f2 = String[]::new;

String[] arr = f2.apply(5);   // 长度 5 的 String[]

数组构造引用在 Stream 的 toArray 中常用:

// 把流转成数组
String[] arr = stream.toArray(String[]::new);
// 等价于 stream.toArray(size -> new String[size]);

toArray() 不带参数返回 Object[],丢失类型。toArray(String[]::new) 才能返回 String[]——这是个高频技巧。

八、方法引用 vs Lambda:何时用哪个

方法引用更简洁,但不是所有情况都该用

用方法引用

  • Lambda 体只有一个方法调用,且参数完全对应。
  • 调用的方法名能清晰表达意图。
// ✅ 清晰,用方法引用
list.forEach(System.out::println);
list.sort(String::compareTo);
names.stream().map(String::toUpperCase).forEach(System.out::println);

用 Lambda

  • Lambda 体有额外逻辑(不止一个方法调用)。
  • 参数需要转换或处理。
  • 方法名不能清晰表达意图。
// ❌ 这里方法引用会丢信息
list.stream().map(Person::getName)            // 还行,但要猜
list.stream().map(p -> p.getName())           // 更清晰
list.stream().map(p -> p.getName().toLowerCase())   // 必须用 Lambda(有额外操作)

// 参数不对应时
ToIntFunction<String> f = s -> s.length();    // Lambda
// 不能写 String::length(语义相同但有时不如 Lambda 直观)

// 调用静态方法但需要额外处理
ToIntFunction<String> f = s -> Integer.parseInt(s.trim());   // 有 trim,必须 Lambda

经验法则:如果方法引用让你”一眼看懂”,就用它;如果让你”愣一下才明白”,就用 Lambda。

九、实战:综合运用四种方法引用

Java · 在线运行

十、方法引用的等价转换速查

把方法引用翻译成 Lambda,关键看”方法属于谁”:

方法引用等价 Lambda说明
Integer::parseInts -> Integer.parseInt(s)静态方法
System.out::printlns -> System.out.println(s)特定对象的方法
String::lengths -> s.length()类的实例方法(一参)
String::compareTo(a, b) -> a.compareTo(b)类的实例方法(两参)
ArrayList::new() -> new ArrayList<>()无参构造器
ArrayList::newn -> new ArrayList<>(n)一参构造器
String[]::newn -> new String[n]数组构造

理解这条规则:类的实例方法引用,第一个参数当调用者,其余参数当方法参数。其他三种形式参数完全对应。

十一、方法引用的陷阱

11.1 重载歧义

当一个类有多个同名重载方法,方法引用可能产生歧义:

// ArrayList 有多个 add:add(E) 和 add(int, E)
// 下面这个 OK,因为 Consumer<String> 只有一个参数
Consumer<String> c = list::add;

// 但有时候编译器无法确定用哪个重载
// 需要显式写 Lambda 消除歧义

11.2 泛型方法引用

引用泛型方法时,类型推断有时不够:

// 通常 OK
Function<String, List<String>> f = ArrayList::new;

// 偶尔需要显式类型
Function<String, List<String>> f = (Function<String, List<String>>) ArrayList::new;

11.3 方法引用不能有额外逻辑

// ❌ 不能用方法引用(有 trim 操作)
Function<String, Integer> f = s -> Integer.parseInt(s.trim());

// ✅ 只能拆成两步
Function<String, String> trim = String::strip;
Function<String, Integer> parse = Integer::parseInt;
Function<String, Integer> combined = trim.andThen(parse);

十二、本章小结

形式语法示例等价 Lambda
静态方法引用类名::静态方法Integer::parseInts -> Integer.parseInt(s)
实例方法引用对象::方法System.out::printlns -> System.out.println(s)
类的实例方法引用类名::实例方法String::lengths -> s.length()
构造器引用类名::newArrayList::new() -> new ArrayList<>()
数组构造引用类型[]::newString[]::newn -> new String[n]

核心规则

  • 前三种形式,参数对应方法参数。
  • 类的实例方法引用,第一个参数当调用者
  • 构造器引用按目标类型签名匹配构造器。

使用原则

  • 方法引用让代码更简洁,但只在”一眼看懂”时用
  • 有额外逻辑、参数转换、重载歧义时,用 Lambda。
  • 别为了”显得高级”而强行方法引用——可读性优先。

结语:极简之美

方法引用是 Java 函数式编程的”最后一公里”。它把 Lambda 进一步压缩到”只写方法名”,让代码读起来像散文:

names.stream()
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

读这行代码就像读一句话:“把名字流转大写、排序、打印”——没有 s ->、没有 { }、没有 return,只有”做什么”。这就是声明式编程的理想形态。

至此,第五阶段”函数式编程”的三章全部完成:

  • Lambda 表达式——把函数从对象中解放,(x) -> x * x 是函数的新形态。
  • 内置函数式接口——FunctionPredicateConsumerSupplier 是 Lambda 的标准舞台。
  • 方法引用——String::length 把 Lambda 再压缩一步,逼近声明式的极致。

这套函数式编程的能力,配合第四阶段的 Stream API,构成了现代 Java 的”现代风格”。掌握它们,你写的 Java 代码会从”啰嗦的命令式”蜕变为”优雅的声明式”——这是从 Java 8 到今天,每个 Java 程序员都该走的路。

Java 的世界很大,集合框架与函数式编程只是其中两块基石。未来的旅程还有并发编程、IO/NIO、JVM 原理、Spring 生态……但有了这两块基石垫底,你已经站在了扎实的台阶上,足以眺望更远的风景。