427

我不明白在方法参数上使用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;
}
4

12 回答 12

302

停止变量的重新分配

虽然这些答案在智力上很有趣,但我还没有阅读简短的简单答案:

当您希望编译器防止将变量重新分配给不同的对象时,请使用关键字final 。

无论变量是静态变量、成员变量、局部变量还是自变量/参数变量,效果都是完全一样的。

例子

让我们看看实际效果。

考虑这个简单的方法,其中两个变量(argx)都可以重新分配不同的对象。

// 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

从不重新分配参数

作为良好的编程习惯(在任何语言中),您不应该参数/参数变量重新分配给调用方法传递的对象以外的对象。在上面的例子中,永远不要写这一行arg = 。既然人类会犯错,而程序员也是人,那就请编译器来帮助我们吧。将每个参数/参数变量标记为“最终”,以便编译器可以找到并标记任何此类重新分配。

回想起来

正如其他答案中所指出的......鉴于Java的最初设计目标是帮助程序员避免愚蠢的错误,例如读取数组末尾的内容,Java应该被设计为自动将所有参数/参数变量强制执行为“最终”。换句话说,Arguments 不应该是 variables。但事后看来是 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.

这些字段是只读的,没有设置器。所以 arecord是一种不需要标记参数的情况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;
    }
}
于 2012-04-30T08:20:56.447 回答
237

有时最好明确(为了可读性)变量不会改变。这是一个简单的示例,其中 usingfinal可以节省一些可能的麻烦:

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

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

于 2009-02-02T05:18:28.847 回答
129

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

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

如果有人实际上声称它确实以某种方式保持数据不变,那么你的观点会更重要——但我不记得看到过任何这样的说法。您是在暗示有大量开发人员建议其final效果比实际效果更大吗?

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

于 2009-02-01T09:54:45.683 回答
77

让我解释一下你必须使用 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
    };
}

这里的fromandto参数需要是 final 的,所以它们可以在匿名类中使用。

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

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

于 2009-02-01T11:23:51.980 回答
27

我一直在参数上使用final。

加了那么多吗?并不真地。

我会把它关掉吗?不。

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

我希望看到这在未来的 Java 版本中成为默认值。通过值/引用传递的事情让很多初级程序员感到不安。

还有一件事.. 我的方法的参数数量往往很少,因此方法声明上的额外文本不是问题。

于 2009-02-01T15:20:09.693 回答
20

在方法参数中使用 final 与调用方参数发生的情况无关。它只是为了将它标记为在该方法内部没有改变。当我尝试采用更函数式的编程风格时,我看到了其中的价值。

于 2009-02-01T10:13:55.303 回答
8

就我个人而言,我不会在方法参数上使用 final,因为它给参数列表增加了太多的混乱。我更喜欢强制方法参数不会通过 Checkstyle 之类的方法更改。

对于局部变量,我尽可能使用 final,我什至让 Eclipse 在我的个人项目设置中自动执行此操作。

我当然想要像 C/C++ const 这样更强大的东西。

于 2009-02-01T11:01:50.137 回答
5

由于 Java 传递参数的副本,我觉得它的相关性final相当有限。我猜这个习惯来自 C++ 时代,您可以通过执行const char const *. 我觉得这种东西让你相信开发者天生就傻到他妈的,需要保护他真正输入的每一个字符。我可以谦虚地说,即使我省略了,我也写了很少的错误final(除非我不希望有人覆盖我的方法和类)。也许我只是一个老派的开发者。

于 2013-10-29T13:18:22.197 回答
2

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

事实上,问题final在于它只强制引用不变,兴高采烈地允许被引用的对象成员发生变异,调用者不知道。因此,这方面的最佳实践是在调用方进行防御性编程,创建深度不可变的实例或对象的深层副本,这些对象有被不道德的 API 劫持的危险。

于 2016-06-20T21:04:21.910 回答
1

我从不在参数列表中使用 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;
}

扮演魔鬼的拥护者,这样做到底有什么问题?

于 2013-01-07T16:08:33.677 回答
0

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

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

于 2009-02-02T05:42:40.373 回答
-1

跟进 Michel 的帖子。我给自己做了另一个例子来解释它。我希望它可以帮助。

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

但是在我们完成方法thisIsWhy()之后,参数(对象) MyObj 是否还存在?

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

Java 真正实现这一点的方式是生成一个副本,也是MyParam 对象内部参数 MyObj obj的隐藏引用(但它不是 MyParam 中的正式字段,因此我们看不到它)

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

但是,如果我们没有将参数放在 final 中,这会导致我们可以将新的内存(对象)重新分配给参数 MyObj obj

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

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

没有冲突,但“令人困惑!” 因为它们都使用相同的“参考名称”,即“obj”

为避免这种情况,请将其设置为“最终”以避免程序员执行“容易出错”的代码。

于 2016-03-17T18:33:39.823 回答