185

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

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

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

4

15 回答 15

190

沉迷于:

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

考虑但明智地使用:

  • 最终类 - 框架/API 设计是我考虑的唯一情况。
  • 最终方法 - 与最终类基本相同。如果您正在使用模板方法模式(例如疯狂并将内容标记为 final),那么您可能过于依赖继承而没有足够的委托。

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

  • 方法参数和局部变量——我很少这样做,主要是因为我很懒,而且我发现它会使代码混乱。我完全承认我不会修改的标记参数和局部变量是“更正确的”。我希望它是默认值。但事实并非如此,而且我发现随着决赛的结束,代码更难理解。如果我在别人的代码中,我不会把它们拉出来,但如果我正在编写新代码,我不会把它们放进去。一个例外是你必须标记一些最终的东西以便你可以访问它来自匿名内部类。

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

于 2008-09-30T19:14:50.687 回答
45

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

不,如果您使用的是 Eclipse,因为您可以配置保存操作来自动为您添加这些最终修饰符。然后,您会以更少的努力获得好处。

于 2008-09-30T18:31:04.720 回答
16

final一直在使用使 Java 更加基于表达式。请参阅 Java 的条件if,else,switch

因此,在使用条件时,您应该尝试始终(恕我直言)使用最终变量。

让我给你举个例子:

    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 me),您可以执行上述操作而无需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";

final 变量强制对应该是什么名称进行单一评估。类似于具有返回值的函数必须始终返回一个值(忽略异常),名称切换块将必须解析名称并因此绑定到该切换块,这使得重构代码块更容易(即 Ecipe 重构:提取方法) .

OCaml 中的上述内容:

type plugin = CandidateExport | JobPostingImport

let p = CandidateExport

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

match ... with ...评估像一个函数,即表达式。注意它看起来像我们的 switch 语句。

这是 Scheme (Racket or Chicken) 中的一个示例:

(define name 
    (match b
      ['CandidateExport "Candidate Stuff"]
      ['JobPostingImport "Blah"]))
于 2013-09-17T17:28:28.833 回答
15

“最终”的开发时优势至少与运行时优势一样重要。它告诉未来的代码编辑者你的意图。

将类标记为“final”表明您在设计或实现类期间没有努力优雅地处理扩展。如果读者可以对类进行更改,并且想要删除“final”修饰符,他们可以自行承担风险。由他们来确保类能够很好地处理扩展。

将变量标记为“final”(并在构造函数中分配它)对于依赖注入很有用。它表明变量的“合作者”性质。

将方法标记为“final”在抽象类中很有用。它清楚地描绘了扩展点的位置。

于 2008-09-30T19:06:11.050 回答
10

好吧,这一切都取决于你的风格......如果你喜欢在不修改变量的情况下看到最终结果,那么就使用它。如果您不喜欢看到它...然后将其排除在外。

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

不过,我更喜欢动态语言,所以我喜欢避免冗长也就不足为奇了。

所以,我想说的是,选择你所倾向的方向并顺其自然(无论如何,尽量保持一致)。


作为旁注,我从事过使用和不使用这种模式的项目,并且我发现错误或错误的数量没有差异......我不认为这是一种模式会极大提高你的错误数量或任何东西,但它再次是风格,如果你喜欢表达你不会修改它的意图,那么继续使用它。

于 2008-09-30T18:31:56.373 回答
9

我发现将方法参数和局部变量标记final为当所讨论的方法是几页长的难以理解的混乱时,它可用作重构辅助工具。随意洒final,看看编译器(或您的 IDE)抛出的“无法分配给最终变量”错误,您可能会发现为什么名为“数据”的变量最终为空,即使有几个(过时的)评论发誓可以不会发生。

然后,您可以通过用更接近使用点声明的新变量替换重用变量来修复一些错误。然后你发现你可以将方法的整个部分包裹在范围括号中,突然间你离“提取方法”只有一个 IDE 按键,你的怪物变得更容易理解了。

如果您的方法还不是无法维护的残骸,我想将东西定型可能会阻止人们将其变成所说的残骸;但如果它是一个简短的方法(请参阅:不是不可维护的),那么您可能会增加很多冗长。特别是,Java 函数签名很难容纳 80 个字符,因为每个参数不增加 6 个字符!

于 2008-10-01T01:41:39.927 回答
6

在参数中避免意外更改参数值并引入微妙的错误很有用。我过去常常忽略这个建议,但花了大约 4 小时后。以一种可怕的方法(数百行代码和多个 fors、嵌套 if 和各种不良做法)我建议你这样做。

 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 ) { ...

 }

当然,在一个完美的世界里,这不会发生,但是.. 好吧.. 有时你必须支持其他代码。:(

于 2008-10-01T01:55:52.780 回答
5

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

如果您正在编写一些一次性代码,那么,不,不要费心识别所有常量并使它们成为最终的。

于 2008-09-30T19:17:52.083 回答
4

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

于 2008-09-30T19:22:02.663 回答
2

从问题中不清楚这是否明显,但是将方法参数设为 final 只会影响方法的主体。它不会调用者传达有关方法意图的任何有趣信息。传入的对象仍然可以在方法内发生变异(final 不是 consts),并且变量的范围在方法内。

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

例如变量,如果它们在逻辑上是常量,我会将它们设为最终变量。

于 2008-10-01T01:09:09.820 回答
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。在这种情况下,最终变量仅在执行范围内是最终的,在方法的每次执行时都是不同的。每次输入方法时,都会重新构建最终的方法。一经构造,在方法执行范围内无法更改。这允许您在方法的持续时间内修复方法中的变量。见下文:

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
}

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

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 类如何导致问题的一个很好的例子。

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

于 2013-06-21T06:40:29.140 回答
1

正如您所提到的,这是一种权衡,但我更喜欢显式使用某些东西而不是隐式使用。这将有助于为未来的代码维护者消除一些歧义——即使只是你自己。

于 2008-09-30T18:38:05.750 回答
1

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

除此之外,你说的是对的。

于 2008-09-30T18:45:50.177 回答
0

如果final您将该变量设为immutable

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

随着 java 8 的发布,我们多了一个概念,叫做“ effectively final variable”。非最终变量可以作为最终变量上升。

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

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

在 Java 7 之前,您不能在匿名类中使用非最终局部变量,但从 Java 8 开始,您可以

看看这篇文章

于 2015-11-28T13:33:25.417 回答
-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
于 2013-07-08T03:52:23.673 回答