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


381

当在方法参数上使用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;
}

101
关于术语的一个快速点-Java根本没有传递引用。它具有按值传递的引用这不是同一回事。使用真正的按引用传递语义,您的代码结果将有所不同。
乔恩·斯基特

6
按引用传递和按值传递引用之间有什么区别?
NobleUplift 2013年

在C上下文中描述这种差异更容易(至少对我而言)。如果我将指针传递给诸如<code> int foo(int bar)</ code>之类的方法,则该指针将按值传递。这意味着它已被复制,因此,如果我在该方法内执行某些操作,例如<code> free(bar); bar = malloc(...); </ code>然后我做了一件非常糟糕的事情。free调用实际上将释放所指向的内存块(因此,无论我传入的指针如何都悬而未决)。但是,<code> int foo(int&bar)</ bar>表示代码有效,并且传入的指针的值将被更改。
jerslan

1
第一个应该是int foo(int* bar),最后一个int foo(int* &bar)。后者按引用传递指针,前者按值传递引用。
jerslan

2
我认为@Martin是个好问题;请参阅问题的标题,以及帖子内容,以解释提出问题的原因。也许我在这里误解了规则,但这正是我在搜索“方法中使用最终参数”时想要的问题。
Victor Zamanian

Answers:


239

停止变量的重新分配

尽管这些答案在理论上很有趣,但我还没有阅读简短的简单答案:

如果希望编译器阻止将变量重新分配给其他对象,请使用关键字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应该被设计为自动将所有参数/参数变量强制为“最终”。换句话说,参数不应该是变量。但是事后的看法是20/20的愿景,而Java设计师当时全神贯注。

因此,总是添加final所有参数吗?

我们应该添加final到每个要声明的方法参数中吗?

  • 从理论上讲,是的。
  • 实际上,没有。only仅在方法的代码很长或很复杂时才
    添加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
 }

23
“作为一种良好的编程习惯(使用任何一种语言),都绝对不要重新分配参数/参数变量[..]。”很抱歉,我真的不得不在这一点上大声疾呼。在Java语言之类的语言中,重新分配参数是标准做法,在该方法中,传递的参数数量(甚至传递的参数数量)不受方法签名的约束。例如,给定一个签名,例如:“ function say(msg)”,人们将确保已分配参数“ msg”,例如:“ msg = msg ||'Hello World!';”。世界上最好的Javascript程序员正在打破您的良好实践。只需阅读jQuery源。
Stijn de Witt

37
@StijndeWitt您的示例显示了重新分配参数变量的问题。您丢失了信息,却一无所获:(a)您丢失了传递的原始值,(b)您丢失了调用方法的意图(调用方是否传递了“ Hello World!”或我们是否默认了)。a和b都可用于测试长代码以及在以后进一步更改值时使用。我支持我的声明:永远不要重新分配arg vars 。您的代码应为:message = ( msg || 'Hello World"' )。根本没有理由使用单独的var。唯一的花费就是几个字节的内存。
罗勒·布尔克

9
@Basil:更多的代码(以字节为单位)和Javascript确实很重要。沉重。与许多事物一样,它基于观点。完全有可能完全忽略这种编程习惯,而仍然编写出色的代码。一个人的编程实践并不能成为每个人的实践。随您所愿,我还是选择写不同的方式。这会使我成为不良的程序员,还是使我的代码成为不良代码?
Stijn de Witt 2014年

14
使用message = ( msg || 'Hello World"' )可能会导致我后来意外使用msg。当我打算使用的合同是“无法通过传递无/空/未定义的arg的行为"Hello World"”时,在函数中尽早进行提交是一种很好的编程习惯。[这可以通过从头开始而无需重新分配而实现,if (!msg) return myfunc("Hello World");但是使用多个参数变得笨拙。]在极少数情况下,函数中的逻辑应该关心是否使用默认值,我宁愿指定一个特殊的哨兵值(最好是public)。 。
贝尼·切尔尼亚夫斯基-帕斯金,2015年

6
@ BeniCherniavsky -帕斯金你所描述的风险仅仅是之间的相似性,因为messagemsg。但是,如果他称其为“类似” processedMsg或提供其他上下文的其他东西,则犯错的可能性会大大降低。专注于他所说的内容,而不是他所说的“如何”。;)
alfasin

230

有时,明确表示(出于可读性)变量不会更改是很好的。这是一个简单的示例,使用final可以节省一些麻烦:

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

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


65
顺便说一句,您仍然会看到警告“分配给变量测试无效”
AvrDragon 2012年

12
@AvrDragon但是,我们也可能会忽略该警告。因此,总有一些可以阻止我们进一步发展的东西总是更好的,例如编译错误,这可以通过使用final关键字来获得。
Sumit Desai 2013年

10
@AvrDragon这取决于开发环境。无论如何,除非您要养成不良习惯,否则您不应该依赖IDE来捕获此类东西。
b1nary.atr0phy

25
@ b1naryatr0phy实际上是一个编译器警告,而不仅仅是IDE-tip
AvrDragon

8
@SumitDesai“但是,我们也可能会忽略警告。因此,总有一些可以阻止我们进一步发展的东西总是更好的,例如编译错误,这可以通过使用final关键字来获得。” 我同意你的观点,但这是一个非常有力的声明,我认为许多Java开发人员都不会同意。出现编译器警告是有原因的,有能力的开发人员不需要错误就可以“迫使”它们考虑其含义。
Stuart Rossiter,2015年

127

是的,除了匿名类,可读性和意图声明之外,它几乎一文不值。这三样东西一文不值吗?

就我个人而言final,除非在匿名内部类中使用变量,否则我通常不使用局部变量和参数,但是我可以肯定那些想明确指出参数值本身不会改变的人的观点(甚至如果它所引用的对象更改了其内容)。对于那些发现增加可读性的人,我认为这是完全合理的事情。

如果有人实际上声称它确实以某种非固定的方式保持数据不变,那么您的观点将更为重要-但我不记得看到任何此类声明。您是否建议有大量开发人员建议其final作用大于实际作用?

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


17
但套用香酥与丹麦,你有什么他们为我们所做的最近?=)
James Schek

尤瓦尔 那很好笑!我想,即使是由剑刃来实现,和平也会发生!
gonzobrains

1
这个问题似乎更像是问:“ 罗马人还没有为我们做些什么?”,因为它更多地是对final关键字不起作用的批评。
NobleUplift 2013年

“您是否建议有大量开发人员暗示final的作用远大于其实际作用?” 对我来说,这是个主要问题:我强烈怀疑使用它的开发人员中有很大一部分认为,如果不执行,它将强制调用者传递的项目保持不变。当然,然后有人参与了关于编码标准是否应该“防止”概念上的误解(“有能力的”开发人员应该意识到)的辩论(然后这会导致SO范围外的观点)型问题)!
Stuart Rossiter,2015年

1
@SarthakMittal:如果您真的想知道,除非您实际使用它,否则不会复制该值。
乔恩·斯基特

76

让我解释一下您必须使用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要做的是将这些局部变量的副本作为隐藏实例变量放入匿名类中(如果检查字节码,则可以看到它们)。但是,如果它们不是最终的,则可能期望匿名类和看到更改的方法,而另一类会对变量进行更改。为了保持这种幻想,即只有一个变量而不是两个副本,它必须是最终的。


1
您从“但是如果他们还没有最终决定...”中迷失了我,您可以尝试改写一下吗,也许我还没有足够的咖啡。
hhafez

1
您有一个from的局部变量-问题是,如果在方法内部使用anon类实例并且它更改了from的值,将会发生什么?人们希望更改在方法中可见,因为他们只看到一个变量。为了避免这种混淆,它必须是最终的。
Michael Borgwardt '02

它不会复制,只是对所引用对象的引用。
vickirk 2010年

1
@vickirk:如果是引用类型,请确保它复制了该引用。
Michael Borgwardt'2

顺便说一句,假设我们没有引用这些变量的匿名类,您是否知道finalHotSpot认为函数参数和非最终函数参数之间有任何区别?
Pacerier,2011年

25

我一直在使用final作为参数。

它增加了这么多吗?并不是的。

我可以关掉它吗?没有。

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

我希望看到它在Java的未来版本中成为默认设置。通过值/引用传递的东西使很多初级程序员不堪重负。

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


4
我也将建议这样做,即final是将来版本中的默认值,并且您必须指定构想为“可变”或更好的关键字。这是一篇很不错的文章: lpar.ath0.com/2008/08/26/java-annoyance-final-parameters
Jeff Axelrod

已经有很长的时间了,但是您能否提供一个捕获到的错误的示例?
user949300

看到投票最多的答案。那是一个非常好的示例,其中未设置成员变量,而是对参数进行了更改。
Fortyrunner

18

在方法中使用final参数与调用方参数的变化无关。它仅用于将其标记为在该方法内未更改。当我尝试采用一种更具功能性的编程风格时,我从中看到了价值。


2
确实,它不是功能接口的一部分,而只是实现。Java 允许(但不满意) final接口/抽象方法声明中的参数令人困惑。
贝尼·切尔尼亚夫斯基-帕斯金

8

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

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

我当然想要更强大的东西,例如C / C ++ const。


不确定IDE和工具参考是否适用于OP发布或主题。即,“最终”是编译时检查,以确保未更改/篡改引用。此外,要真正强制执行此类操作,请参见有关对最终引用的子成员没有任何保护的答案。例如,在构建API时,使用IDE或工具不会帮助外部各方使用/扩展此类代码。
达雷尔·蒂格

4

由于Java传递了参数的副本,所以我觉得它的相关性final很有限。我想这个习惯来自C ++时代,您可以通过执行禁止更改参考内容const char const *。我觉得这种事情使您相信开发人员天生就愚蠢到f ***,需要真正防止他键入的每个字符。我可能要谦虚地说,即使我忽略了,我也只编写了很少的错误final(除非我不希望有人重写我的方法和类)。也许我只是一个老派的开发人员。


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

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


要区分IDE和运行时JVM。当编译后的字节码在服务器上运行时,IDE无关紧要,除非IDE添加了代码以防止成员变量抢劫(例如,在代码中不应重新分配但错误地分配了变量的代码中的缺陷)时使用-因此,最终关键字。
Darrell Teague

1

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

确实,问题final在于它仅强制执行引用不变,而使调用者不了解的对象对象可以高兴地进行突变。因此,这方面的最佳实践是在调用方进行防御性编程,从而创建深不可更改的实例或对象的深层副本,这些对象有被不道德的API抢劫的危险。


2
“ final的问题在于它仅强制引用保持不变” -不正确,Java本身阻止了这种情况。传递给方法的变量不能由该方法更改其引用。
Madbreaks'Apr 11'18


简单地说,如果它是真的,引用的引用不能改变,就没有防守拷贝,不可改变的讨论,没有必要final关键字等
达雷尔-蒂格

您可能误会我,或者您误会了。如果我将对象引用传递给方法,并且该方法将其重新分配,则当方法完成执行时,原始引用对于(我)调用方而言将保持不变。Java严格按值传递。而且,您冒昧地断言我没有做任何研究。
Madbreaks '18 -4-12

否决票是因为op询问为什么要使用final,而您只给出了一个错误的原因。
Madbreaks '18 -4-12

0

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

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


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

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

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

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

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

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

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

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.