什么时候应该对方法参数和局部变量使用final?


171

我发现了一些参考(例如),它们建议final尽可能多地使用它,我想知道这有多重要。这主要是在方法参数和局部变量的上下文中,而不是最终方法或类。对于常量,这很明显。

一方面,编译器可以进行一些优化,使程序员的意图更加清晰。另一方面,它增加了冗长性,并且优化可能是微不足道的。

我需要努力记住的东西吗?


这里是一个相关的帖子,您可以查看:stackoverflow.com/questions/137868/…–
mattlant


1
我之所以感到欣慰,是因为我不知道在阅读本文之前是否可以将final用作参数的修饰符。谢谢!
Kip

Answers:


178

痴迷于:

  • 最终字段-将字段标记为最终字段会强制它们在构造结束时进行设置,从而使该字段引用不变。这样可以安全地发布字段,并且可以避免在以后的读取中进行同步。(请注意,对于对象引用,只有字段引用是不可变的-对象引用所引用的内容仍然可以更改,并且会影响不可变性。)
  • 最终静态字段-尽管我在以前使用静态最终字段的许多情况下都使用枚举。

考虑但要谨慎使用:

  • 最终课程-框架/ API设计是我唯一考虑的情况。
  • 最终方法-与最终类基本相同。如果您正在使用诸如疯狂的模板方法模式和将内容标记为final,则可能是您对继承的依赖过多,而对委派的依赖不足。

除非感到肛门,否则忽略:

  • 方法参数和局部变量-我很少这样做,主要是因为我很懒,并且发现它会使代码混乱。我将完全承认,标记标记和我将不修改的局部变量是“正确的”。我希望它是默认设置。但是事实并非如此,我发现遍历Final的代码更加难以理解。如果我使用别人的代码,则不会删除它们,但是如果我正在编写新代码,则不会放入它们。一个例外是必须标记最终内容以便可以访问它来自一个匿名内部类。

  • 编辑:请注意,@ adam-gent提到的最终局部变量实际上非常有用的一个用例是,在if/ else分支中将值分配给var时。


8
约书亚·布洛赫(Joshua Bloch)认为,所有类都应定义为最终类,除非它们是为继承而设计的。我同意他的观点。我将final添加到实现接口的每个类中(以便能够创建单元测试)。同时将所有不会被覆盖的受保护/类方法标记为final。
rmaruszewski

61
出于对乔什·布洛赫(Josh Bloch)的所有应有的尊重(这是相当可观的),我不同意一般情况。在构建API的情况下,请确保将其锁定。在您自己的代码中添加Bui,建立后来必须拆除的墙是浪费时间。
亚历克斯·米勒

9
这绝对不是“浪费时间”,特别是因为它根本不花时间...在应用程序中,final默认情况下,我通常制作几乎所有类。但是,除非您使用真正现代的Java IDE(即IDEA),否则您可能不会注意到这些好处。
罗杰里奥(Rogério)2010年

9
IDEA具有(开箱即用的)数百种代码检查功能,其中一些可以检测final类/方法中未使用/不必要的代码。例如,如果final方法声明抛出一个已检查的异常,但从未实际抛出,则IDEA会告诉您,您可以从throws子句中删除该异常。有时,您还会发现未使用的整个方法,当无法覆盖它们时可以检测到。
罗杰里奥(Rogério)2010年

8
@rmaruszewski将类标记为final会阻止大多数模拟框架模拟它们,从而使您的代码难以测试。只有在不扩展课程至为重要的情况下,我才将课程定下来。
Mike Rylander 2014年

44

是我应该努力记住的事情吗?

否,如果您使用的是Eclipse,因为您可以配置“保存操作”以自动为您添加这些最终修饰符。这样您就可以以更少的精力获得收益。


3
Save Action的绝佳提示,对此一无所知。
健全的

1
我主要考虑的好处是,由于意外地将变量赋给了错误的变量,而不是可能或可能不会进行的任何优化,final使代码更安全,免受错误影响。
彼得·希尔顿

4
这真的对您有问题吗?您实际上有多少次因此而导致错误?
Alex Miller

1
+1是Eclipse中有用的技巧。我认为我们应该尽可能地使用final以避免错误。
emeraldhieu

您也可以在IntelliJ中执行此操作吗?
Koray Tugay 2015年

15

“最终”的开发时收益至少与运行时收益一样重要。它告诉代码的未来编辑者有关您意图的信息。

将类标记为“最终”表示您在类的设计或实现过程中并未努力优雅地处理扩展。如果读者可以对课程进行更改,并希望删除“最终”修饰符,则后果自负。由他们来确保类可以很好地处理扩展。

将变量标记为“ final”(并将其分配给构造函数)对于依赖项注入很有用。它指示变量的“协作者”性质。

将方法标记为“ final”在抽象类中很有用。它清楚地描述了扩展点在哪里。


11

final一直都在使Java更加基于表达式。请参阅Java的条件(if,else,switch)不是我一直讨厌的基于表达式,尤其是如果您习惯于函数式编程(例如ML,Scala或Lisp)。

因此,在使用条件时,应尝试始终(IMHO)使用最终变量。

让我举一个例子:

    final String name;
    switch(pluginType) {
        case CANDIDATE_EXPORT:
            name = "Candidate Stuff";
            break;
        case JOB_POSTING_IMPORT:
            name = "Blah";
            break;
        default:
            throw new IllegalStateException();
    }

现在,如果添加另一个case语句并且不设置,name则编译器将失败。如果不对每种情况都进行中断(设置变量),则编译器也会失败。这使您可以使Java与Lisp的let表达式非常相似,并且可以使您的代码不会大量缩进(因为有词法作用域变量)。

并且正如@Recurse指出的(但显然是-1我),您可以进行上述操作而不必String name final获取编译器错误(我从未说过您不能这样做),但是您可以轻松地使编译器错误消失,并在切换后设置名称如果不使用以下语句,则该语句将break舍弃表达式的语义或更糟的是忘记了不会导致错误的语句(尽管@Recurse说了什么)final

    String name;
    switch(pluginType) {
        case CANDIDATE_EXPORT:
            name = "Candidate Stuff";
            //break; whoops forgot break.. 
            //this will cause a compile error for final ;P @Recurse
        case JOB_POSTING_IMPORT:
            name = "Blah";
            break;
    }
    // code, code, code
    // Below is not possible with final
    name = "Whoops bug";

由于存在错误设置名称(除了忘记了break另一个错误),现在我可以不小心这样做了:

    String name;
    switch(pluginType) {
        case CANDIDATE_EXPORT:
            name = "Candidate Stuff";
            break;
        //should have handled all the cases for pluginType
    }
    // code, code, code
    // Below is not possible with final
    name = "Whoops bug";

最终变量强制对应该使用的名称进行一次评估。类似于具有返回值的函数必须始终返回值(忽略异常)的方式,名称切换块将必须解析名称并因此绑定到该切换块,这使得重构代码块更加容易(即Eclipe重构:extract方法) 。

以上在OCaml中:

type plugin = CandidateExport | JobPostingImport

let p = CandidateExport

let name = match p with
    | CandidateExport -> "Candidate Stuff"
    | JobPostingImport -> "Blah" ;;

match ... with ...像的功能,即表达式的计算结果。注意它看起来像我们的switch语句。

这是Scheme(球拍或鸡肉)中的一个示例:

(define name 
    (match b
      ['CandidateExport "Candidate Stuff"]
      ['JobPostingImport "Blah"]))

1
除了Java编译器已经给您一个“名称可能未初始化的”错误,并拒绝使用或不使用最终代码进行编译之外。
2014年

4
是的,但是没有决赛,您可以随时重置。我实际上已经看到这种情况,开发人员将an else if (...)变成了a if(...),从而重置了变量。我向他展示了最终变量永远不会发生的事情。基本上final迫使您一次只分配一次变量...所以:P
Adam Gent 2014年

9

我发现将标记的方法参数和局部标记作为final一种有用的重构辅助工具,当所讨论的方法是几页长的难以理解的混乱时。撒上final宽松,看看有什么“不能分配给最后一个变量”错误的编译器(或您的IDE)抛出了,你也许会发现为什么叫变量“数据”结束的空但有几个(过时)评论发誓CAN不会发生

然后,您可以通过使用声明为更接近使用点的新变量替换重用的变量来修复某些错误。然后,您发现可以将方法的整个部分都用大括号括起来,突然之间,您只不过是“提取方法”的一个IDE按键,您的怪物就变得更容易理解了。

如果您的方法还不是无法解决的残骸,那么我想将这些东西定型以防止人们将其变成上述残骸是有价值的。但是,如果这是一个简短的方法(请参阅:并非不可维护),那么您可能会冒很多冗长的风险。特别是,Java函数签名很难容纳80个字符,因为每个参数不能再添加六个!


1
最后一点非常有效,尽管很久以前我放弃了80个字符的限制,因为在过去十年中屏幕分辨率发生了一些变化。我可以轻松地在屏幕上显示300个字符的行,而无需滚动。但是,如果没有final每个参数,可读性当然会更好。
brimborium

8

好吧,这一切都取决于您的样式...如果您喜欢在不修改变量的情况下看到最终版本,请使用它。如果您不喜欢看到它,请忽略它。

我个人尽可能地避免冗长,所以我倾向于避免使用不必要的额外关键字。

虽然我更喜欢动态语言,所以避免冗长可能并不奇怪。

因此,我想说的就是选择您倾向于的方向并坚持下去(无论如何,要保持一致)。


附带说明一下,我已经在使用和不使用这种模式的项目中工作,而且我发现错误或错误的数量没有差异……我认为这不是一个会极大影响的模式改进错误计数或其他任何方法,但这又是一种风格,如果您喜欢表达自己不想修改的意图,请继续使用它。


6

在参数设置中有用,可避免意外更改参数值并引入细微的错误。我过去常常忽略此建议,但是花了大约4个小时。我建议您采用一种可怕的方法(包含数百行代码和多个fors,ifs嵌套以及各种不良做法)。

 public int processSomethingCritical( final int x, final int y ){
 // hundreds of lines here 
     // for loop here...
         int x2 = 0;
        x++; // bug aarrgg...
 // hundreds of lines there
 // if( x == 0 ) { ...

 }

当然,在完美的世界中这不会发生,但是..嗯..有时您必须支持其他代码。:(


2
此方法比缺少final更为严重。尽管并非不可能,但很少见的是,有充分的理由使方法变得如此杂乱,以至于会发生这类错误。稍加考虑,对变量名进行处理将对此类事故大有帮助。
ykaganovich's

2
如果单个方法中有“数百行代码”,则可能需要将其分解为几个较小的方法。
Steve Kuo

5

如果您正在编写某个应用程序,例如某人将在一年后必须阅读代码,那么可以,请使用final变量,该变量不应一直修改。这样,您的代码将更加“自我记录”,并且还减少了其他开发人员执行愚蠢的事情的机会,例如使用局部常量作为局部临时变量。

如果您正在编写一些一次性代码,那么,不用费心找出所有常量并将它们设为最终常量。


4

我将尽可能多地使用final。如果您无意中更改了该字段,则将进行标记。我还将方法参数设置为final。这样做时,当我尝试“设置”参数而忘记了Java按值传递时,我从接管的代码中捕获了多个错误。


2

从问题尚不清楚这是否很明显,但是将方法参数定为final仅影响方法的主体。它传达方法的意图给调用任何有趣的信息。传入的对象仍可以在方法内进行更改(最终不是const),并且变量的范围在方法内。

为了回答您的确切问题,除非有代码要求(例如,变量是从内部类引用的),否则我不会费心地将实例或局部变量(包括方法参数)定为final,或者澄清一些非常复杂的逻辑。

对于实例变量,如果它们在逻辑上是常数,我将使它们成为最终变量。


2

变量有很多用途final。这里仅仅是少数

最终常数

 public static class CircleToolsBetter {
     public final static double PI = 3.141;
        public double getCircleArea(final double radius) {
          return (Math.pow(radius, 2) * PI);
        }
    }

然后可以将其用于代码的其他部分,或由其他类访问,这样,如果您要更改值,则不必一一更改。

最终变量

public static String someMethod(final String environmentKey) {
    final String key = "env." + environmentKey;
    System.out.println("Key is: " + key);
    return (System.getProperty(key));

  }

}

在此类中,您将构建一个范围限定的最终变量,该变量将为参数environmentKey添加前缀。在这种情况下,final变量仅在执行范围内是final,这在方法的每次执行时都不同。每次输入方法时,将重新构造最终结果。一旦构造它,就不能在方法执行范围内对其进行更改。这使您可以在方法持续时间内修复方法中的变量。见下文:

public class FinalVariables {


  public final static void main(final String[] args) {
    System.out.println("Note how the key variable is changed.");
    someMethod("JAVA_HOME");
    someMethod("ANT_HOME");
  }
}

最终常数

public double equation2Better(final double inputValue) {
    final double K = 1.414;
    final double X = 45.0;

double result = (((Math.pow(inputValue, 3.0d) * K) + X) * M);
double powInputValue = 0;         
if (result > 360) {
  powInputValue = X * Math.sin(result); 
} else {
  inputValue = K * Math.sin(result);   // <= Compiler error   
}

当您有很长的代码行时,这些功能特别有用,它会生成编译器错误,因此当有人意外更改了不应更改的变量时,您不会遇到逻辑/业务错误。

最终收藏

在谈论集合的不同情况下,您需要将它们设置为不可修改。

 public final static Set VALID_COLORS; 
    static {
      Set temp = new HashSet( );
      temp.add(Color.red);
      temp.add(Color.orange);
      temp.add(Color.yellow);
      temp.add(Color.green);
      temp.add(Color.blue);
      temp.add(Color.decode("#4B0082")); // indigo
      temp.add(Color.decode("#8A2BE2")); // violet
      VALID_COLORS = Collections.unmodifiableSet(temp);
    }

否则,如果您未将其设置为不可修改:

Set colors = Rainbow.VALID_COLORS;
colors.add(Color.black); // <= logic error but allowed by compiler

最终类最终方法不能分别扩展或覆盖。

编辑:以解决有关封装的最终问题:

有两种方法可以使班级决赛。第一种是在类声明中使用关键字final:

public final class SomeClass {
  //  . . . Class contents
}

使类最终化的第二种方法是将其所有构造函数声明为私有:

public class SomeClass {
  public final static SOME_INSTANCE = new SomeClass(5);
  private SomeClass(final int value) {
  }

如果将其标记为final,则可以省去麻烦,如果发现它确实是final,以演示一下此Test类。乍一看。

public class Test{
  private Test(Class beanClass, Class stopClass, int flags)
    throws Exception{
    //  . . . snip . . . 
  }
}

不幸的是,由于该类的唯一构造函数是私有的,因此无法扩展该类。对于Test类,没有理由该类应为最终类。Test类是隐式final类如何引起问题的一个很好的例子。

因此,当您通过将类的构造函数设为私有来隐式地使类为final时,应将其标记为final。


1

正如您提到的那样,在某种程度上需要权衡取舍,但是与隐式使用相比,我更喜欢显式使用某些内容。这将为将来的代码维护者消除一些歧义-即使只是您自己。


1

如果您具有内部(匿名)类,并且该方法需要访问包含方法的变量,则需要将该变量作为最终变量。

除此之外,您所说的是对的。


现在,java 8通过有效的最终变量提供了灵活性。
Ravindra babu 2015年

0

使用final关键字的变量,如果你制作的变量immutable

通过将变量声明为最终变量,它可以帮助开发人员排除高度多线程环境中变量的可能修改问题。

在Java 8发行版中,我们还有一个名为“ effectively final variable”的概念。非最终变量可以作为最终变量。

从lambda表达式引用的局部变量必须是final或有效的final

如果在局部块中初始化后未对其进行修改,则该变量被认为是有效的final。这意味着您现在可以在匿名类或lambda表达式内使用不带final关键字的局部变量,前提是它们必须有效地是final。

直到Java 7,您才能在匿名类中使用非最终局部变量,但是从Java 8您可以

看看这篇文章


-1

首先,final关键字用于使变量恒定。常量意味着它不会改变。例如:

final int CM_PER_INCH = 2.54;

您将声明变量final,因为每英寸的厘米不变。

如果您尝试覆盖最终值,则变量是首先声明的变量。例如:

final String helloworld = "Hello World";
helloworld = "A String"; //helloworld still equals "Hello World"

有一个编译错误,类似于:

local variable is accessed from inner class, must be declared final

如果您的变量无法声明为final或不想将其声明为final,请尝试以下操作:

final String[] helloworld = new String[1];
helloworld[0] = "Hello World!";
System.out.println(helloworld[0]);
helloworld[0] = "A String";
System.out.println(helloworld[0]);

这将打印:

Hello World!
A String
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.