为什么我要在Java中的方法参数上使用关键字“final”?

时间:2009-02-01 09:49:43

标签: java pass-by-reference final pass-by-value

在方法参数上使用时,我无法理解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;
}

12 个答案:

答案 0 :(得分:217)

有时候明确(为了可读性)变量不会改变是很好的。这是一个简单的例子,使用final可以节省一些可能的麻烦:

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

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

答案 1 :(得分:213)

停止变量的重新分配

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

  

如果希望编译器阻止a,请使用关键字 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

永远不会重新分配参数

作为良好的编程习惯(在任何语言中),您应该永远将参数/参数变量重新分配给除调用方法传递的对象之外的对象。在上面的示例中,永远不应该写行arg =。由于人类犯错误,程序员是人类,我们请求编译器协助我们。将每个参数/参数变量标记为“final”,以便编译器可以查找并标记任何此类重新分配。

在Retrospect中

如其他答案所述...... 鉴于Java的原始设计目标是帮助程序员避免诸如读取数组末尾之类的愚蠢错误,Java应该被设计为自动将所有参数/参数变量强制实施为“最终”。换句话说,参数不应该是变量。但事后看来是20/20的愿景,而Java设计师当时已经满手了。

为完整性添加了另一个案例

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
 }

答案 2 :(得分:126)

是的,排除匿名类,可读性和意图声明,它几乎一文不值。这三件事情虽然毫无价值吗?

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

如果有人声称​​确实以不支持的方式保持数据不变 - 那么你的观点会更加重要 - 但我不记得看到任何此类声明。您是否建议有大量开发人员建议final比实际效果更好?

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

答案 3 :(得分:75)

让我解释一下你 使用final的一个案例,Jon已经提到过:

如果在方法中创建匿名内部类并在该类中使用局部变量(例如方法参数),则编译器会强制您使参数最终:

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参数必须是最终的,因此可以在匿名类中使用它们。

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

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

答案 4 :(得分:25)

我一直在参数上使用final。

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

我会把它关掉吗?否。

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

我希望看到这在Java的未来版本中成为默认值。价值/参考物的传递使很多初级程序员绊倒了。

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

答案 5 :(得分:18)

在方法参数中使用final与调用者方面的参数无关。它只是意味着它在该方法内部没有变化。当我尝试采用更具功能性的编程风格时,我会看到它的价值。

答案 6 :(得分:8)

我个人不会在方法参数上使用final,因为它会给参数列表增加太多的混乱。 我更倾向于强制执行方法参数不会像Checkstyle那样改变。

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

我当然喜欢像C / C ++ const那样强大的东西。

答案 7 :(得分:4)

由于Java传递了参数的副本,我觉得它的相关性相当有限。我想这来自C ++时代,你可以通过const char const *来禁止改变参考内容。我觉得这种东西让你相信开发人员固有的愚蠢就像f ***一样,需要保护自己不要真正反对他所输入的每一个角色。在所有的谦卑中,我必须说我写了很少的错误,即使我提出final,除非我不希望有人覆盖我的方法和东西......也许我只是一个老派的开发者。 ..: - O

答案 8 :(得分: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;
}

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

答案 9 :(得分:1)

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

实际上,final的问题在于它只强制引用不变,非常高兴地允许引用的对象成员发生变异,而调用者并不知道。因此,在这方面的最佳做法是在调用者方面进行防御性编程,创建深层不可变的实例或有可能被不道德的API抢劫的对象的深层副本。

答案 10 :(得分:0)

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

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

答案 11 :(得分:-1)

跟进米歇尔的帖子。我自己做了另一个例子来解释它。我希望它可以提供帮助。

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中的方法中使用 [参数MyObj obj]

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

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

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

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

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

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

  1. 我们现在有一个隐藏的[MyObj obj]指向[堆中的内存A]现在存在于MyParam对象中。
  2. 我们还有另一个[MyObj obj],它是[存储器B在堆中]的参数点,现在存在于thisIsWhy方法中。
  3. 没有冲突,但是“令人困惑!!”因为他们都使用相同的“引用名称”,即“obj”

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