【问题标题】:Why should I use the keyword "final" on a method parameter in Java?为什么要在 Java 中的方法参数上使用关键字“final”?
【发布时间】:2018-07-22 07:39:00
【问题描述】:

我不明白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;
}

【问题讨论】:

  • 关于术语的快速说明 - Java 根本没有传递引用。它具有传递引用按值,这不是一回事。使用真正的引用传递语义,您的代码结果会有所不同。
  • 按引用传递和按值传递引用有什么区别?
  • 在 C 上下文中描述这种差异更容易(至少对我而言)。如果我将指针传递给类似的方法:int foo(int <i>bar)</i>,则该指针是按值传递的。意味着它被复制了,所以如果我在该方法中做一些事情,比如 free(bar); bar = malloc(...); 那么我刚刚做了一件非常糟糕的事情。 free 调用实际上会释放指向的内存块(所以我传入的任何指针现在都悬空了)。但是,int foo(int &amp;bar)表示代码有效,传入的指针的值会发生变化。
  • 第一个应该是int foo(int* bar),最后一个应该是int foo(int* &amp;bar)。后者是通过引用传递指针,前者是通过值传递引用。
  • @Martin,在我看来,这是一个很好的问题;请参阅问题的标题和帖子内容,以解释为什么提出 question。可能我对这里的规则有误解,但这正是我在搜索"uses of final parameters in methods"时想要的问题

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


【解决方案1】:

停止变量的重新赋值

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

当您希望编译器阻止 变量不会被重新分配给不同的对象。

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

示例

让我们看看实际效果。

考虑这个简单的方法,其中两个变量(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 应该被设计为自动强制所有参数/参数变量为“最终”。换句话说,参数不应该是变量。但事后看来是 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 带来了新的records 功能。记录是定义一个类的一种非常简短的方法,该类的中心目的只是以不可变和透明的方式携带数据。

您只需声明类名及其成员字段的名称和类型。编译器隐式提供构造函数、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;
    }
}

【讨论】:

  • “作为良好的编程习惯(在任何语言中),你永远不应该重新分配参数/参数变量 [..]” 抱歉,我真的不得不在这个问题上给你打电话。重新分配参数是 Javascript 等语言中的标准做法,其中传递的参数数量(或者即使有任何传递)不是由方法签名决定的。例如。给定一个签名,如:“function say(msg)”,人们将确保分配参数“msg”,如下所示:“msg = msg || 'Hello World!';”。世界上最好的 Javascript 程序员正在打破你的良好做法。只需阅读 jQuery 源代码。
  • @StijndeWitt 您的示例显示了重新分配参数变量的问题。您丢失了一无所获的信息作为回报:(a)您丢失了传递的原始值,(b)您丢失了调用方法的意图(调用者是否传递了“Hello World!”还是我们默认了)。 a & b 都适用于测试、长代码以及稍后进一步更改值时。我坚持我的说法:arg vars 应该永远被重新分配。您的代码应为:message = ( msg || 'Hello World"' )。根本没有理由 使用单独的变量。唯一的成本是几个字节的内存。
  • @Basil:更多的代码(以字节为单位)和 Javascript 中确实很重要。沉重。与许多事情一样,它是基于意见的。完全有可能完全忽略这种编程实践,仍然编写出色的代码。一个人的编程实践并不会让每个人都在实践。随心所欲地坚持下去,无论如何我选择写不同的东西。这会让我成为一个糟糕的程序员,还是我的代码糟糕?
  • 使用message = ( msg || 'Hello World"' ) 可能会让我以后不小心使用msg。当我打算的合同是“没有/空/未定义参数的行为与传递"Hello World" 没有区别”时,在函数的早期提交它是一种很好的编程实践。 [这可以通过以if (!msg) return myfunc("Hello World"); 开头而无需重新分配来实现,但是使用多个参数会变得笨拙。] 在函数中的逻辑应该关心是否使用默认值的极少数情况下,我宁愿指定一个特殊的哨兵值(最好是公开的)。
  • @BeniCherniavsky-Paskin 您描述的风险仅仅是因为messagemsg 之间的相似性。但是,如果他将其称为 processedMsg 或其他提供额外上下文的名称 - 出错的可能性要低得多。专注于他所说的而不是他所说的“如何”。 ;)
【解决方案2】:

有时最好明确(为了便于阅读)变量不会改变。这是一个简单的例子,使用final 可以省去一些麻烦:

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

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

【讨论】:

  • 顺便说一句,您将看到警告“对变量测试的赋值无效”
  • @AvrDragon 但是,我们也可以忽略警告。所以,最好有一些东西会阻止我们走得更远,比如编译错误,我们将通过使用 final 关键字来获得。
  • @AvrDragon 这取决于开发环境。无论如何,您不应该依赖 IDE 为您捕获此类内容,除非您想养成坏习惯。
  • @b1naryatr0phy 实际上这是一个编译器警告,而不仅仅是 IDE 提示
  • @SumitDesai "但是,我们也可以忽略警告。所以,最好有一些东西会阻止我们走得更远,比如编译错误,我们将通过使用 final关键词。”我同意你的观点,但这是一个非常强烈的声明,我认为许多 Java 开发人员会不同意。编译器警告的存在是有原因的,有能力的开发人员不应该需要错误来“强迫”他们考虑其影响。
【解决方案3】:

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

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

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

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

【讨论】:

  • 但是用他的丹麦语来解释 Krusty,他们最近为我们做了什么? =)
  • 尤瓦尔。那很好笑!我想即使是用剑刃强制也可以实现和平!
  • 这个问题似乎更像是在问,“罗马人没有为我们做了什么?”,因为它更像是对最终关键字的批评不做。
  • “你的意思是说有大量的开发人员认为 final 的效果比实际效果要大吗?”对我来说,主要问题:我强烈怀疑使用它的很大一部分开发人员认为它强制调用者传递的项目的不变性,而实际上它没有。当然,人们随后会被卷入关于编码标准是否应该“防止”概念误解(“有能力”的开发人员应该意识到这一点)的辩论(然后这会导致超出 SO 范围的意见) -类型问题)!
  • @SarthakMittal:除非您实际使用它,否则不会复制该值,如果您想知道的话。
【解决方案4】:

让我解释一下您必须使用 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 参数需要是最终的,以便它们可以在匿名类中使用。

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

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

【讨论】:

  • 你让我失去了“但如果它们不是最终的......”你能试着改写一下吗,也许我没有足够的咖啡。
  • 你有一个局部变量 from - 问题是如果你在方法中使用 anon 类实例并更改 from 的值会发生什么 - 人们会期望更改在方法中可见,因为他们只看到一个变量。为了避免这种混淆,它必须是最终的。
  • 它不会复制,它只是对任何被引用对象的引用。
  • @vickirk:如果是引用类型,请确保它制作了引用的副本。
  • 顺便说一句,假设我们没有引用这些变量的匿名类,您知道在 HotSpot 眼中final 函数参数和非最终函数参数之间是否有任何区别?
【解决方案5】:

我一直在参数上使用 final。

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

我会关掉它吗?没有。

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

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

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

【讨论】:

  • 我也正要建议这一点,最终版本将成为未来版本的默认值,并且您必须指定“可变”或构思的更好的关键字。这是一篇很好的文章:lpar.ath0.com/2008/08/26/java-annoyance-final-parameters
  • 已经很久了,但是你能提供一个你发现的错误的例子吗?
  • 查看投票最多的答案。这是一个很好的例子,其中成员变量没有设置,而是参数发生了变异。
【解决方案6】:

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

【讨论】:

  • 没错,它不是函数接口的一部分,只是实现。接口/抽象方法声明中的参数上的 java allows (but dirsregards) final 令人困惑。
【解决方案7】:

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

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

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

【讨论】:

  • 不确定 IDE 和工具参考是否适用于 OP 发布或主题。即“最终”是一个编译时检查引用没有改变/抢劫。此外,要真正执行此类事情,请参阅关于不保护最终引用的子成员的答案。例如,在构建 API 时,使用 IDE 或工具不会帮助外部各方使用/扩展此类代码。
【解决方案8】:

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

【讨论】:

    【解决方案9】:

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

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

    【讨论】:

    • “final 的问题是它只强制引用不变” -- 不正确,Java 本身阻止了这一点。传递给方法的变量不能被该方法更改其引用。
    • 发帖前请先研究一下...stackoverflow.com/questions/40480/…
    • 简单地说,如果对引用的引用确实不能更改,那么就不会讨论防御性复制、不变性、不需要 final 关键字等。
    • 要么你误会我,要么你搞错了。如果我将一个对象引用传递给一个方法,并且该方法重新分配它,那么当方法完成执行时,对于(我)调用者来说,原始引用保持不变。 Java 是严格按值传递的。而且你还肆无忌惮地断言我没有做过任何研究。
    • 投反对票,因为 op 询问为什么使用 final,而您只给出了一个不正确的理由。
    【解决方案10】:

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

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

    【讨论】:

    • 谨慎区分 IDE 和运行时 JVM。当编译后的字节码在服务器上运行时,IDE 所做的任何事情都无关紧要,除非 IDE 添加了代码以防止成员变量被盗,例如代码中的缺陷,而本意是不应该重新分配变量但被错误地分配了 - 因此目的是最终关键字。
    【解决方案11】:

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

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

    【讨论】:

      【解决方案12】:

      跟进 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 方法中的参数。

      没有冲突,但是“CONFUSING!!”因为它们都使用相同的“引用名称”,即“obj”

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

      【讨论】:

        猜你喜欢
        • 2010-10-04
        • 2014-05-11
        • 1970-01-01
        • 1970-01-01
        • 2011-06-09
        • 2014-01-14
        • 2014-12-05
        • 2013-08-11
        相关资源
        最近更新 更多