提问人: 提问时间:2/1/2009 最后编辑:Basil Bourque 更新时间:11/22/2023 访问量:176961
为什么要在 Java 中的方法参数上使用关键字“final”?
Why should I use the keyword "final" on a method parameter in Java?
问:
我不明白当关键字用于方法参数时,它在哪里真正方便。final
如果我们排除匿名类的使用、可读性和意图声明,那么它对我来说似乎几乎毫无价值。
强制执行某些数据保持不变并不像看起来那么强大。
如果参数是基元,则它不会起作用,因为参数是作为值传递给方法的,并且更改它不会在范围之外产生任何影响。
如果我们通过引用传递参数,那么引用本身就是一个局部变量,如果引用是从方法内部更改的,则在方法范围之外不会产生任何影响。
考虑下面的简单测试示例。 该测试通过,尽管该方法更改了给定给它的引用的值,但它没有效果。
public void testNullify() {
Collection<Integer> c = new ArrayList<Integer>();
nullify(c);
assertNotNull(c);
final Collection<Integer> c1 = c;
assertTrue(c1.equals(c));
change(c);
assertTrue(c1.equals(c));
}
private void change(Collection<Integer> c) {
c = new ArrayList<Integer>();
}
public void nullify(Collection<?> t) {
t = null;
}
答:
是的,不包括匿名类、可读性和意图声明,它几乎毫无价值。这三样东西一文不值吗?
就我个人而言,我倾向于不用于局部变量和参数,除非我在匿名内部类中使用变量,但我当然可以看到那些想要明确参数值本身不会改变的人的观点(即使它引用的对象改变了它的内容)。对于那些发现这增加了可读性的人来说,我认为这是一件完全合理的事情。final
如果有人真的声称它确实以某种方式保持数据不变,那么你的观点会更重要 - 但我不记得看到过任何这样的说法。你是说有相当多的开发人员认为它比实际效果更好吗?final
编辑:我真的应该用 Monty Python 参考来总结所有这些;这个问题似乎有点类似于问“罗马人曾经为我们做过什么?
评论
在方法参数中使用 final 与调用方端的参数发生的情况无关。它只是为了将其标记为在该方法中不更改。当我尝试采用一种更实用的编程风格时,我看到了它的价值。
评论
final
就我个人而言,我不在方法参数上使用 final,因为它会给参数列表增加太多混乱。 我更愿意强制执行方法参数不会通过 Checkstyle 之类的东西来更改。
对于局部变量,我尽可能使用 final,我甚至让 Eclipse 在我的个人项目设置中自动执行此操作。
我当然想要像 C/C++ const 这样更强大的东西。
评论
让我解释一下你必须使用 final 的一种情况,Jon 已经提到过:
如果在方法中创建一个匿名内部类,并在该类中使用局部变量(如方法参数),则编译器会强制您将参数设为 final:
public Iterator<Integer> createIntegerIterator(final int from, final int to)
{
return new Iterator<Integer>(){
int index = from;
public Integer next()
{
return index++;
}
public boolean hasNext()
{
return index <= to;
}
// remove method omitted
};
}
这里的 and 参数必须是最终的,以便它们可以在匿名类中使用。from
to
提出该要求的原因是:局部变量位于堆栈上,因此它们仅在执行方法时存在。但是,匿名类实例是从该方法返回的,因此它可能会存在更长的时间。您无法保留堆栈,因为后续方法调用需要它。
因此,Java 所做的是将这些局部变量的副本作为隐藏实例变量放入匿名类中(如果您检查字节码,您可以看到它们)。但是,如果它们不是最终的,人们可能会期望匿名类和方法看到另一个人对变量所做的更改。为了保持只有一个变量而不是两个副本的错觉,它必须是最终的。
评论
final
我一直在参数上使用 final。
它增加了那么多吗?没有。
我会把它关掉吗?不。
原因是:我发现了 3 个错误,人们编写了草率的代码并且未能在访问器中设置成员变量。事实证明,所有错误都很难找到。
我希望看到它在未来版本的 Java 中成为默认值。传递值/引用的事情绊倒了很多初级程序员。
还有一件事..我的方法往往具有较少数量的参数,因此方法声明中的额外文本不是问题。
评论
有时,明确表示变量不会改变(为了可读性)是件好事。这里有一个简单的例子,使用可以省去一些可能的麻烦:final
public void setTest(String test) {
test = test;
}
如果您忘记了 setter 上的“this”关键字,则不会设置要设置的变量。但是,如果在参数上使用了关键字,则会在编译时捕获 bug。final
评论
将 final 添加到参数声明的另一个原因是,它有助于识别需要重命名的变量,作为“提取方法”重构的一部分。我发现,在开始大型方法重构之前,将 final 添加到每个参数中会快速告诉我在继续之前是否需要解决任何问题。
但是,我通常会在重构结束时将它们删除为多余的。
停止变量的重新赋值
虽然这些答案在智力上很有趣,但我还没有读过简短的简单答案:
当您希望编译器阻止 变量被重新分配给其他对象。
无论变量是静态变量、成员变量、局部变量还是参数/参数变量,效果都是完全相同的。
例
让我们看看效果如何。
考虑这个简单的方法,其中两个变量(arg 和 x)都可以重新分配不同的对象。
// Example use of this method:
// this.doSomething( "tiger" );
void doSomething( String arg ) {
String x = arg; // Both variables now point to the same String object.
x = "elephant"; // This variable now points to a different String object.
arg = "giraffe"; // Ditto. Now neither variable points to the original passed String.
}
将局部变量标记为 final。这会导致编译器错误。
void doSomething( String arg ) {
final String x = arg; // Mark variable as 'final'.
x = "elephant"; // Compiler error: The final local variable x cannot be assigned.
arg = "giraffe";
}
相反,让我们将参数变量标记为 final。这也会导致编译器错误。
void doSomething( final String arg ) { // Mark argument as 'final'.
String x = arg;
x = "elephant";
arg = "giraffe"; // Compiler error: The passed argument variable arg cannot be re-assigned to another object.
}
故事的寓意:
如果要确保变量始终指向同一对象, 将变量标记为 final。
从不重新分配参数
作为良好的编程实践(在任何语言中),切勿将参数/参数变量重新分配给调用方法传递的对象以外的对象。在上面的例子中,永远不应该写 .既然人类会犯错误,而程序员也是人,那就请编译器来协助我们吧。将每个参数/参数变量标记为“final”,以便编译器可以查找并标记任何此类重新赋值。arg =
回顾过去
正如其他答案中所指出的... 鉴于 Java 最初的设计目标是帮助程序员避免愚蠢的错误,例如读取数组的末尾,Java 应该被设计为自动将所有参数/参数变量强制执行为“final”。换句话说,参数不应该是变量。但事后诸葛亮是 20/20 的愿景,Java 设计人员当时忙得不可开交。
那么,总是添加到所有参数中?final
我们是否应该向每个声明的方法参数添加内容?final
- 从理论上讲,是的。
- 在实践中,没有。
➥ 仅当方法的代码很长或很复杂时才添加,其中参数可能会被误认为是局部变量或成员变量,并可能被重新分配。final
如果你接受从不重新分配一个参数的做法,你会倾向于在每个参数上添加一个。但这很乏味,使声明更难阅读。final
对于参数显然是参数,而不是局部变量或成员变量的简短简单代码,我不会费心添加 .如果代码非常明显,我或任何其他程序员在进行维护或重构时都不会意外地将参数变量误认为参数以外的东西,那么请不要打扰。在我自己的工作中,我只添加更长或更复杂的代码,其中参数可能会被误认为是局部变量或成员变量。final
final
为完整性而添加 #Another 案例
public class MyClass {
private int x;
//getters and setters
}
void doSomething( final MyClass arg ) { // Mark argument as 'final'.
arg = new MyClass(); // Compiler error: The passed argument variable arg cannot be re-assigned to another object.
arg.setX(20); // allowed
// We can re-assign properties of argument which is marked as final
}
record
Java 16 带来了新的记录功能。记录是定义类的一种非常简短的方法,其中心目的是仅以不可变和透明的方式携带数据。
您只需声明类名及其成员字段的名称和类型即可。编译器隐式提供构造函数 getter 和 。equals
hashCode
toString
这些字段是只读的,没有 setter。所以 a 是一种无需标记参数的情况。它们实际上已经是最终的。事实上,编译器在声明记录的字段时禁止使用。record
final
final
public record Employee( String name , LocalDate whenHired ) // 🡄 Marking `final` here is *not* allowed.
{
}
如果提供可选构造函数,则可以将 标记 .final
public record Employee(String name , LocalDate whenHired) // 🡄 Marking `final` here is *not* allowed.
{
public Employee ( final String name , final LocalDate whenHired ) // 🡄 Marking `final` here *is* allowed.
{
this.name = name;
whenHired = LocalDate.MIN; // 🡄 Compiler error, because of `final`.
this.whenHired = whenHired;
}
}
评论
message = ( msg || 'Hello World"' )
message = ( msg || 'Hello World"' )
msg
"Hello World"
if (!msg) return myfunc("Hello World");
message
msg
processedMsg
我从不在参数列表中使用 final,它只会像以前的受访者所说的那样增加混乱。同样在 Eclipse 中,您可以设置参数赋值以生成错误,因此在参数列表中使用 final 对我来说似乎是多余的。 有趣的是,当我启用参数分配的 Eclipse 设置时,在它上面生成错误时,捕获了此代码(这只是我记得流程的方式,而不是实际代码。
private String getString(String A, int i, String B, String C)
{
if (i > 0)
A += B;
if (i > 100)
A += C;
return A;
}
扮演魔鬼的代言人,这样做到底有什么错?
评论
由于 Java 传递了参数的副本,我觉得它的相关性相当有限。我想这个习惯来自C++时代,在这个时代,你可以通过执行 .我觉得这种东西会让你相信开发人员天生就很愚蠢,需要保护他免受他输入的每个角色的影响。谦虚地说,即使我省略了,我写的错误也很少(除非我不希望有人覆盖我的方法和类)。也许我只是一个老派的开发人员。final
const char const *
final
跟进米歇尔的帖子。我给自己做了另一个例子来解释它。我希望它能有所帮助。
public static void main(String[] args){
MyParam myParam = thisIsWhy(new MyObj());
myParam.setArgNewName();
System.out.println(myParam.showObjName());
}
public static MyParam thisIsWhy(final MyObj obj){
MyParam myParam = new MyParam() {
@Override
public void setArgNewName() {
obj.name = "afterSet";
}
@Override
public String showObjName(){
return obj.name;
}
};
return myParam;
}
public static class MyObj{
String name = "beforeSet";
public MyObj() {
}
}
public abstract static class MyParam{
public abstract void setArgNewName();
public abstract String showObjName();
}
从上面的代码中,在 thisIsWhy() 方法中,我们实际上没有将 [参数 MyObj obj] 分配给 MyParam 中的真实引用。相反,我们只是在 MyParam 中的方法中使用 [argument MyObj obj]。
但是在我们完成thisIsWhy()方法之后,argument(object)MyObj是否仍然存在?
似乎应该,因为我们可以看到,我们仍然调用方法 showObjName() 并且它需要到达 obj。MyParam 仍将使用/到达方法参数,即使已经返回的方法!
Java 真正实现这一点的方式是生成一个副本,也是 MyParam 对象中参数 MyObj obj 的隐藏引用(但它不是 MyParam 中的正式字段,因此我们无法看到它)
当我们调用“showObjName”时,它将使用该引用来获取相应的值。
但是,如果我们没有将参数放在最终状态,这会导致一种情况,我们可以将新的内存(对象)重新分配给参数 MyObj obj。
从技术上讲,根本没有冲突!如果我们被允许这样做,情况如下:
- 我们现在有一个隐藏的 [MyObj obj] 指向现在位于 MyParam 对象中的 [堆中的内存 A]。
- 我们还有另一个 [MyObj obj],它是 [堆中的内存 B] 的参数点,现在存在于 thisIsWhy 方法中。
没有冲突,但“令人困惑!!因为它们都使用相同的“引用名称”,即“obj”。
为避免这种情况,请将其设置为“final”,以避免程序员执行“容易出错”的代码。
简短的回答:有一点帮助,但是......请改用客户端的防御性编程。final
事实上,问题在于它只强制执行引用是不变的,兴高采烈地允许引用的对象成员在调用者不知情的情况下发生变异。因此,这方面的最佳实践是在调用方进行防御性编程,创建深度不可变的实例或对象的深度副本,这些对象有被不道德的 API 抢劫的危险。final
评论
int foo(int* bar)
int foo(int* &bar)