为什么我要使用关键字“final”;在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;
}
171898 次浏览

是的,除去匿名类、可读性和意图声明,它几乎毫无价值。但这三样东西毫无价值吗?

就我个人而言,我倾向于不使用final作为局部变量和参数,除非我在匿名内部类中使用变量,但我当然可以看到那些想要明确参数值本身不会改变的人的观点(即使它引用的对象改变了其内容)。对于那些发现这增加了可读性的人来说,我认为这是一件完全合理的事情。

你的观点会更重要,如果有人真的声称它做了保持数据不变的方式,它没有-但我不记得看到任何这样的说法。你的意思是有大量的开发人员认为final有比实际更大的影响吗?

编辑:我真的应该用Monty Python的参考来总结所有这些;这个问题似乎有点类似于问“罗马人为我们做过什么?”

在方法形参中使用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
};
}

这里fromto参数需要是final,这样它们才能在匿名类中使用。

这种需求的原因是:局部变量存在于堆栈中,因此它们只在方法执行时存在。但是,匿名类实例是从方法返回的,因此它可能存在更长的时间。不能保留堆栈,因为后续的方法调用需要它。

因此,Java所做的是将这些局部变量的副本作为隐藏的实例变量放入匿名类中(如果检查字节代码,可以看到它们)。但如果它们不是最终的,人们可能会期望匿名类和方法看到另一个对变量所做的更改。为了保持只有一个变量而不是两个副本的错觉,它必须是最终的。

我总是在参数上使用final。

加了那么多吗?不是真的。

我会把它关掉吗?不。

原因:我发现了3个错误,人们编写了草率的代码,未能在访问器中设置成员变量。所有的漏洞都被证明很难找到。

我希望在未来的Java版本中将此设置为默认值。通过值/引用传递的东西绊倒了很多初级程序员。

还有一件事…我的方法倾向于参数数量较少,因此方法声明上的额外文本不是问题。

有时显式地(为了可读性)变量不变是很好的。下面是一个简单的例子,使用final可以避免一些可能令人头疼的问题:

public void setTest(String test) {
test = test;
}

如果你在setter上忘记了'this'关键字,那么你想设置的变量就不会被设置。然而,如果你在参数上使用final关键字,那么错误将在编译时被捕获。

在参数声明中添加final的另一个原因是,它有助于识别作为“Extract Method”重构的一部分需要重命名的变量。我发现,在开始大型方法重构之前向每个参数添加final可以快速告诉我在继续之前是否有任何需要解决的问题。

但是,我通常会在重构结束时将它们作为多余的部分删除。

停止变量的重赋

虽然这些答案在学术上很有趣,但我并没有读到简短的答案:

使用关键字最后当你想要编译器阻止一个错误

无论变量是静态变量、成员变量、局部变量还是参数/形参变量,效果都完全相同。

例子

让我们看看实际效果。

考虑这个简单的方法,其中两个变量(参数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.
}

将局部变量标记为最后。这将导致编译器错误。

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";
}

相反,让我们将参数变量标记为最后。这也会导致编译器错误。

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.
}

这个故事的寓意:

如果你想确保一个变量总是指向同一个对象, 标记变量最后.

永远不要重赋参数

作为良好的编程实践(在任何语言中),您应该从来没有将参数/参数变量重新分配给调用方法传递的对象以外的对象。在上面的例子中,永远不要写arg = 这一行。因为人会犯错,程序员也是人,所以让编译器来帮助我们。将每个形参/实参变量标记为“final”,以便编译器可以发现并标记任何此类重赋。

回想起来

如其他答案所述… 考虑到Java最初的设计目标是帮助程序员避免愚蠢的错误,例如读取数组的末尾,Java应该被设计成自动强制所有参数/参数变量为“final”。换句话说,参数不应该是变量。但是后知后觉是20/20的观点,而Java设计师当时已经忙得不可开交了

所以,总是添加final到所有参数?

我们是否应该将final添加到每个被声明的方法参数中?

  • 理论上,是的。
  • 实际上,没有。
    ➥仅当方法的代码较长或复杂时才添加final,在这种情况下,实参可能被误认为局部变量或成员变量并可能被重新赋值。

如果你接受从不重新分配参数的做法,你将倾向于为每个参数添加final。但这是乏味的,使声明有点难以阅读。

对于简单的短代码,其中参数显然是一个参数,而不是局部变量或成员变量,我不需要添加final。如果代码非常明显,我或任何其他程序员在进行维护或重构时都不可能意外地将参数变量误认为是参数以外的东西,那么就不必费心了。在我自己的工作中,我只在较长或较复杂的代码中添加final,因为那里的参数可能被误认为是局部变量或成员变量。

#为完整性添加的另一个案例

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 &hashCodetoString

字段是只读的,没有设置符。所以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;
}
}
我从不在参数列表中使用final,它只会像之前的受访者所说的那样增加混乱。另外,在Eclipse中,你可以设置参数赋值来生成错误,所以在参数列表中使用final对我来说似乎是多余的。 有趣的是,当我启用Eclipse参数赋值设置时,它生成了一个错误,捕获了这段代码(这只是我记住流程的方式,而不是实际的代码)。: - < / p >
private String getString(String A, int i, String B, String C)
{
if (i > 0)
A += B;


if (i > 100)
A += C;


return A;
}

唱反调,这样做到底有什么错?

由于Java传递参数的副本,我觉得final的相关性相当有限。我猜这个习惯来自c++时代,你可以通过执行const char const *来禁止修改引用内容。我觉得这类内容会让你认为开发者天生就很愚蠢,需要保护他们不受自己输入的任何角色的伤害。恕我直言,尽管我省略了final(除非我不想让别人重写我的方法和类),但我写的bug很少。也许我只是个老派开发者。

跟进米歇尔的帖子。我给自己举了另一个例子来解释。我希望它能有所帮助。

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 ()中,我们实际上在MyParam中将[参数MyObj obj] 没有分配真正的参考。相反,我们只在MyParam中的方法中使用[参数MyObj obj]

但在我们完成方法thisIsWhy ()参数(对象)MyObj是否仍然存在?< / >强

似乎应该如此,因为我们可以看到在main中仍然调用showObjName ()方法,并且它需要到达obj。即使方法已经返回,MyParam仍然会使用/到达方法参数!

Java真正实现这一点的方法是在MyParam对象中生成参数MyObj obj的一个隐藏引用(但它不是MyParam中的一个正式字段,所以我们看不到它)

当我们调用“showobjname”时,它将使用该引用来获取相应的值。

但如果我们没有将参数设为final,这将导致一种情况,我们可以将一个新的内存(对象)重新分配给参数MyObj obj

从技术上讲,根本没有冲突!如果我们被允许这样做,下面将是情况:

  1. 我们现在在MyParam对象中有一个隐藏的[MyObj obj]指向一个[Memory a in heap]。
  2. 我们还有另一个[MyObj obj],它是指向[堆中的内存B]的参数,现在存在于thisIsWhy方法中。

没有冲突,但是“令人困惑! !“因为它们都在使用相同的“参考名称”;它是"客体"

为了避免这种情况,请将其设置为“final”;为了避免程序员做“容易出错”的事情;代码。

简单的回答:final有一点帮助,但是……在客户端使用防御性编程。

实际上,final的问题是它只强制参考不变,允许被引用的对象成员发生突变,而调用者不知道。因此,在这方面的最佳实践是在调用方进行防御性编程,创建深度不可变的实例或对象的深度副本,这些实例或对象有被肆无忌惮的api窃取的危险。