IntelliJ一直建议我用方法引用替换lambda表达式。这两者之间是否存在客观差异?
vcudknz31#
让我来谈谈为什么我们在语言中添加了这个特性,显然我们并不严格需要(所有方法引用都可以表示为lambda)。请注意,* 没有正确答案 。任何说“总是使用方法ref而不是lambda”或“总是使用lambda而不是方法ref”的人都应该被忽略。这个问题在精神上与“什么时候应该使用命名类还是匿名类”非常相似?答案是一样的: 当你发现它更可读 *。当然有情况下,肯定是一个或肯定是另一个,但有一个灰色的主机在中间,和判断必须使用。方法refs背后的理论很简单:如果一个方法有一个名字,那么通过名字来引用它,而不是通过一个命令式的代码包,最终只是反过来调用它,通常(但并不总是!)更清晰易读。关于性能或计数字符的争论大多是转移注意力的,你应该忽略它们。目标是编写代码,清楚地知道它做了什么。通常(但并不总是!)方法引用在这个指标上获胜,所以我们将它们作为一个选项,在这些情况下使用。关于方法引用是澄清还是混淆意图的一个关键考虑因素是,从上下文来看,所表示的函数的形状是否显而易见。在某些情况下(例如,map(Person::getLastName),从上下文来看,需要一个将一个事物Map到另一个事物的函数是非常清楚的,并且在这种情况下,方法引用闪耀。在其他情况下,使用方法ref要求读者想知道描述的是什么类型的函数;这是一个警告信号,说明lambda可能更易读,即使它更长。最后,我们发现,大多数人 * 一开始 * 会避开方法引用,因为它们感觉比lambda表达式更新更奇怪,所以最初会发现它们“可读性较差”,但随着时间的推移,当他们习惯了语法时,通常会改变它们的行为,并在可能的时候倾向于方法引用。所以要注意,你自己的主观初始化“可读性较低”。这种React几乎肯定包含了熟悉性偏见的某些方面,你应该给予自己一个机会,让自己在表达风格观点之前对两者都感到舒服。
map(Person::getLastName)
6psbrbz92#
由多个语句组成的长lambda表达式可能会降低代码的可读性。在这种情况下,在方法中提取这些语句并引用它可能是更好的选择。另一个原因可能是可重用性。你可以构造一个方法,并从代码的不同位置调用它,而不是复制和粘贴几个语句的lambda表达式。
l7wslrjt3#
由于用户stuchl4n3k在评论中写道问题可能发生异常。假设某个变量field是null,则:
field
null
field = null; runThisLater(()->field.method()); field = new SomeObject();
不会崩溃,而
field = null; runThisLater(field::method); field = new SomeObject();
将崩溃,并出现 java.lang.NullPointerException:尝试在方法引用语句行调用虚拟方法'java.lang.Class java.lang.Object.getClass()',至少在Android上。今天的IntelliJ在建议这种重构时指出“可能会改变语义”。当“引用”特定对象的示例方法时会发生这种情况。为什么?让我们检查15.13.3.方法引用的运行时评估的前两段:在运行时,方法引用表达式的求值类似于类示例创建表达式的求值,因为正常完成会产生对对象的引用。方法引用表达式的求值与方法本身的调用不同。首先,如果方法引用表达式以 ExpressionName 或 Primary 开头,则计算此子表达式。如果子表达式计算为null,则引发NullPointerException,并且方法引用表达式突然完成。如果子表达式突然完成,则方法引用表达式出于相同的原因突然完成。在lambda表达式的情况下,我不确定,final类型是在编译时从方法声明中派生出来的。这只是对实际情况的简化。但是让我们假设方法runThisLater已经被声明为例如void runThisLater(SamType obj),其中SamType是一些 *Functional接口 *,那么runThisLater(()->field.method());将转换为如下内容:
NullPointerException
runThisLater
void runThisLater(SamType obj)
runThisLater(()->field.method());
runThisLater(new SamType() { void doSomething() { field.method(); } });
其他信息:
wljmcqd84#
虽然所有的方法引用都可以用lambdas表示,但当涉及到副作用时,语义上存在潜在的差异。@areacode的例子在一种情况下抛出NPE,而在另一种情况下不抛出NPE,这对于所涉及的副作用非常明确。然而,在使用CompletableFuture时,您可能会遇到更微妙的情况:让我们通过以下辅助函数slow模拟一个需要一段时间(2秒)才能完成的任务:
NPE
CompletableFuture
slow
private static <T> Supplier<T> slow(T s) { return () -> { try { Thread.sleep(2000); } catch (InterruptedException e) {} return s; }; }
然后
var result = CompletableFuture.supplyAsync(slow(Function.identity())) .thenCompose(supplyAsync(slow("foo"))::thenApply);
有效地并行运行两个异步任务,允许未来在大约2秒后完成。另一方面,如果我们将::thenApply方法引用重构为lambda,两个异步任务将依次运行,并且future仅在大约4秒后完成。
::thenApply
附注:虽然这个例子看起来很做作,但当您尝试regain the applicative instance hidden in the future时,它确实出现了。
4条答案
按热度按时间vcudknz31#
让我来谈谈为什么我们在语言中添加了这个特性,显然我们并不严格需要(所有方法引用都可以表示为lambda)。
请注意,* 没有正确答案 。任何说“总是使用方法ref而不是lambda”或“总是使用lambda而不是方法ref”的人都应该被忽略。
这个问题在精神上与“什么时候应该使用命名类还是匿名类”非常相似?答案是一样的: 当你发现它更可读 *。当然有情况下,肯定是一个或肯定是另一个,但有一个灰色的主机在中间,和判断必须使用。
方法refs背后的理论很简单:如果一个方法有一个名字,那么通过名字来引用它,而不是通过一个命令式的代码包,最终只是反过来调用它,通常(但并不总是!)更清晰易读。
关于性能或计数字符的争论大多是转移注意力的,你应该忽略它们。目标是编写代码,清楚地知道它做了什么。通常(但并不总是!)方法引用在这个指标上获胜,所以我们将它们作为一个选项,在这些情况下使用。
关于方法引用是澄清还是混淆意图的一个关键考虑因素是,从上下文来看,所表示的函数的形状是否显而易见。在某些情况下(例如,
map(Person::getLastName)
,从上下文来看,需要一个将一个事物Map到另一个事物的函数是非常清楚的,并且在这种情况下,方法引用闪耀。在其他情况下,使用方法ref要求读者想知道描述的是什么类型的函数;这是一个警告信号,说明lambda可能更易读,即使它更长。最后,我们发现,大多数人 * 一开始 * 会避开方法引用,因为它们感觉比lambda表达式更新更奇怪,所以最初会发现它们“可读性较差”,但随着时间的推移,当他们习惯了语法时,通常会改变它们的行为,并在可能的时候倾向于方法引用。所以要注意,你自己的主观初始化“可读性较低”。这种React几乎肯定包含了熟悉性偏见的某些方面,你应该给予自己一个机会,让自己在表达风格观点之前对两者都感到舒服。
6psbrbz92#
由多个语句组成的长lambda表达式可能会降低代码的可读性。在这种情况下,在方法中提取这些语句并引用它可能是更好的选择。
另一个原因可能是可重用性。你可以构造一个方法,并从代码的不同位置调用它,而不是复制和粘贴几个语句的lambda表达式。
l7wslrjt3#
由于用户stuchl4n3k在评论中写道问题可能发生异常。
假设某个变量
field
是null
,则:不会崩溃,而
将崩溃,并出现 java.lang.NullPointerException:尝试在方法引用语句行调用虚拟方法'java.lang.Class java.lang.Object.getClass()',至少在Android上。
今天的IntelliJ在建议这种重构时指出“可能会改变语义”。
当“引用”特定对象的示例方法时会发生这种情况。为什么?让我们检查15.13.3.方法引用的运行时评估的前两段:
在运行时,方法引用表达式的求值类似于类示例创建表达式的求值,因为正常完成会产生对对象的引用。方法引用表达式的求值与方法本身的调用不同。
首先,如果方法引用表达式以 ExpressionName 或 Primary 开头,则计算此子表达式。如果子表达式计算为null,则引发
NullPointerException
,并且方法引用表达式突然完成。如果子表达式突然完成,则方法引用表达式出于相同的原因突然完成。在lambda表达式的情况下,我不确定,final类型是在编译时从方法声明中派生出来的。这只是对实际情况的简化。但是让我们假设方法
runThisLater
已经被声明为例如void runThisLater(SamType obj)
,其中SamType是一些 *Functional接口 *,那么runThisLater(()->field.method());
将转换为如下内容:其他信息:
wljmcqd84#
虽然所有的方法引用都可以用lambdas表示,但当涉及到副作用时,语义上存在潜在的差异。@areacode的例子在一种情况下抛出
NPE
,而在另一种情况下不抛出NPE
,这对于所涉及的副作用非常明确。然而,在使用CompletableFuture
时,您可能会遇到更微妙的情况:让我们通过以下辅助函数
slow
模拟一个需要一段时间(2秒)才能完成的任务:然后
有效地并行运行两个异步任务,允许未来在大约2秒后完成。
另一方面,如果我们将
::thenApply
方法引用重构为lambda,两个异步任务将依次运行,并且future仅在大约4秒后完成。附注:虽然这个例子看起来很做作,但当您尝试regain the applicative instance hidden in the future时,它确实出现了。