Java方法关键字"final"及其用途

5

当我创建复杂的类型层次结构(多级,每级多种类型)时,我喜欢在实现某些接口声明的方法上使用final关键字。例如:

interface Garble {
  int zork();
}

interface Gnarf extends Garble {
  /**
   * This is the same as calling {@link #zblah(0)}
   */
  int zblah();
  int zblah(int defaultZblah);
}

然后

abstract class AbstractGarble implements Garble {
  @Override
  public final int zork() { ... }
}

abstract class AbstractGnarf extends AbstractGarble implements Gnarf {
  // Here I absolutely want to fix the default behaviour of zblah
  // No Gnarf shouldn't be allowed to set 1 as the default, for instance
  @Override
  public final int zblah() { 
    return zblah(0);
  }

  // This method is not implemented here, but in a subclass
  @Override
  public abstract int zblah(int defaultZblah);
}

我这样做有几个原因:

  1. 它帮助我开发类型层次结构。当我向层次结构添加一个类时,非常清楚,我必须实现哪些方法,以及哪些方法可能不能覆盖(如果我忘记了有关层次结构的详细信息)
  2. 根据设计原则和模式(如模板方法模式),我认为覆盖具体内容是不好的。我不希望其他开发人员或我的用户这样做。

所以final关键字对我来说非常完美。我的问题是:

为什么在实际使用中它很少被使用?你能否给我展示一些例子/理由,在类似情况下使用final会非常糟糕?

5个回答

4

为什么很少在实际开发中使用final?

因为你应该再写一个单词来使变量/方法变成final。

能否给我展示一些例子或者理由,在类似于我的情况下使用final会非常糟糕的情况?

通常这种情况出现在第三方库中。在某些情况下,我想要扩展某个类并更改一些行为。特别是在没有接口/实现分离的非开源库中使用是危险的。


+1 鼓励接口和实现分离。有时这是可以接受的,但会使类型层次结构变得非常严格。这种情况下,“final”关键字可能会带来一些麻烦。 - Lukas Eder
+1 我注意到有时我有强烈的欲望杀死使用“private”方法的库的作者。'private'方法非常相似。我认为限制类层次结构的用户并不好,即使作者认为这可能会破坏某些东西(当然也有例外,但每个规则都有例外 :))。用户必须自己决定,但应该通过警告。 - Stas
2
此外,@Stas,请不要杀了我 :) - Lukas Eder
@Stas(您):您说得对。但是当然有最佳实践! - Lukas Eder
1
@Lukas Eder: 正确) 但是大多数最佳实践通常都有自己的小缺点。API设计师应该理解这一点。让我们看一个假设的例子。假设我想记录zblah返回的所有值。最简单的方法是覆盖它,但是我不能在您的API中这样做。当然,这个例子还不够好。 - Stan Kurilin
显示剩余4条评论

3

当我编写一个抽象类并想要清楚地表明哪些方法是固定的时,我总是使用final。我认为这是此关键字最重要的功能。

但是,如果您不打算扩展类,为什么还要大惊小怪呢?当然,如果您正在为他人编写库,则尽可能保护它,但是当您编写“终端用户代码”时,有一点可以使您的代码防错只会让维护开发人员感到恼火,因为他们将尝试解决您构建的迷宫。

同样适用于使类成为最终版本。虽然某些类本质上应该是最终版本,但往往有目光短浅的开发人员会简单地将继承树中所有叶子类标记为final

毕竟,编码有两个不同的目的:向计算机提供指令和向阅读代码的其他开发人员传递信息。第二个目的大多被忽略了,即使它几乎与使您的代码工作一样重要。放入不必要的final关键字就是一个很好的例子:它不会改变代码的行为,因此它的唯一目的应该是通信。但是你要传达什么?如果您将某个方法标记为final,则维护者将假定您有充分的理由这样做。如果事实证明您没有这样做,那么您所做的就只是让其他人感到困惑。

我的方法是(我可能完全错误):除非它改变了代码的工作方式或传达了有用信息,否则不要写任何东西。


你说得对。我正在编写库代码,我认为我有充分的理由来传达你建议的“final”的语义...在日常应用逻辑中,我几乎不使用“final”。误用的风险太小了。 - Lukas Eder

2

我认为final关键字之所以不常用,有两个原因:

  1. 人们不知道它的存在
  2. 人们在构建方法时没有想到使用它。

通常我会陷入第二个原因。我有时会重写具体方法。在某些情况下,这是不好的,但很多时候它不会与设计原则冲突,实际上可能是最好的解决方案。因此,当我实现一个接口时,我通常不会深入思考每个方法是否需要使用final关键字。特别是由于我经常处理变化频繁的业务应用程序。


+1 好答案。虽然,我认为使用 final 关键字并不能将事物“最终化”到不可更改的程度。但这取决于项目。在我的情况下,我既设计了实现,也设计了接口,类似于 Java 集合 API... - Lukas Eder
是的,更改仍然是可能的。我想在我创建的几个对象中,覆盖具体方法通常是可以接受的,所以需要添加final的情况很少,所以我会忘记它。 - jzd
此外,IDE中的代码模板通常会创建所有公共非最终内容。+1 - WReach

2
为什么它在实际开发中使用得这么少呢?这与我的经验不符。我看到它在各种库中都被广泛使用。只举一个(随机)例子:看一下http://code.google.com/p/guava-libraries/中的抽象类,例如com.google.common.collect.AbstractIterator。其中peek()、hasNext()、next()和endOfData()是final的,只留下computeNext()供实现者使用。我认为这是非常常见的例子。
使用final的主要原因是允许实现者更改算法——您提到了“模板方法”模式:仍然可以修改模板方法,或者通过添加一些前/后操作来增强它(而不必用几十个前/后挂钩淹没整个类)。
使用final的主要好处是避免意外的实现错误,或者当方法依赖于未指定的类内部时(因此可能会在将来更改)。

+1个提示。这正是我所考虑的用例。但我几乎从未见过... - Lukas Eder
接受的答案既回答了我的问题,也证明了我对于final不经常用于方法的假设是错误的,并且证明了我使用它是正确的 :) 谢谢,Chris。 - Lukas Eder

1
为什么它在实际应用中很少被使用?
因为它不应该是必要的。而且它也不能完全关闭实现,因此实际上可能会给你一种虚假的安全感。
这是由于Liskov替换原则。该方法具有契约,在正确设计的继承图中,该契约得到了履行(否则就是一个错误)。例如:
interface Animal {
    void bark();
}

abstract class AbstractAnimal implements Animal{
   final void bark() {
       playSound("whoof.wav"); // you were thinking about a dog, weren't you?
   }
}

class Dog extends AbstractAnimal {
  // ok
}

class Cat extends AbstractAnimal() {
  // oops - no barking allowed!
}

如果不允许子类做正确的事情(对于它来说),可能会引入错误。或者你可能需要另一个开发人员将继承树放在你的Garble接口旁边,因为你的最终方法不允许它执行应该执行的操作。

虚假的安全感是非静态最终方法的典型特征。静态方法不应该使用实例的状态(它不能)。非静态方法可能会这样做。你的最终(非静态)方法可能也会这样做,但它不拥有实例变量 - 它们可能与预期不同。因此,你给继承自AbstractGarble的类的开发人员增加了负担 - 确保实例字段在任何时候都处于你的实现所期望的状态。而没有给开发人员提供一种在调用你的方法之前准备状态的方法,如下所示:

int zblah() {
    prepareState();
    return super.zblah();
}

在我看来,除非你有非常好的理由,否则不应该以这种方式关闭实现。如果你记录了方法契约并提供了一个JUnit测试,你应该能够信任其他开发人员。使用JUnit测试,他们可以验证Liskov替换原则
顺便说一下,我偶尔会关闭一个方法。特别是如果它在框架的边界部分。我的方法会进行一些簿记,然后继续由其他人实现的抽象方法:
final boolean login() {
    bookkeeping();
    return doLogin();
}
abstract boolean doLogin();

这样就不会有人忘记做账了,但他们可以提供自定义登录。当然,你是否喜欢这样的设置取决于你自己 :)


有趣的观点!我不知道里氏替换原则。然而,我并不完全同意你的论证。Animal.bark() 方法放错了位置,是糟糕的设计。如果设计不好,当然实现就不应该封闭,因为用户可能需要创建解决设计缺陷的变通方法,例如 Cat.bark()。如果我在设计上小心,并提供可以被覆盖的“模板方法”,那么我认为我可以像你建议的那样拥有很多 login() / doLogin() 对。 - Lukas Eder
@Lukas Eder,我同意动物的例子很糟糕。我只是想不到更好的例子。然而,需求随着时间的推移超出了您所能想象的范围,这是一个真实的危险。可悲的是,这在我的几次经历中发生过 :( - extraneon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接