首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么'final‘关键字会有用?

为什么'final‘关键字会有用?
EN

Software Engineering用户
提问于 2016-05-12 08:33:21
回答 9查看 17.1K关注 0票数 55

Java似乎已经有能力声明多年来不可派生的类,而现在C++也拥有这种能力。然而,鉴于开放/封闭原则的坚实,为什么这是有用的?对我来说,final关键字听起来就像friend --这是合法的,但是如果您使用它,很可能设计是错误的。请提供一些例子,其中一个不可派生的类将是一个伟大的架构或设计模式的一部分。

EN

回答 9

Software Engineering用户

发布于 2016-05-12 08:48:40

final表达了意图。它告诉类、方法或变量的用户“这个元素不应该改变,如果您想要更改它,您还没有理解现有的设计。”

这一点很重要,因为如果您必须预见到您编写的每个类和每个方法都可能会被子类更改以完成完全不同的事情,那么程序架构将非常非常困难。最好是预先决定哪些元素应该是可变的,哪些是不可改变的,并通过final强制执行不可更改的元素。

您也可以通过注释和体系结构文档来实现这一点,但是让编译器执行它能够执行的操作总是比希望未来的用户阅读和服从文档更好。

票数 137
EN

Software Engineering用户

发布于 2016-05-12 10:30:38

它避免了脆弱基类问题。每个类都有一组隐式或显式保证和不变量。Liskov替换原则要求该类的所有子类型也必须提供所有这些保证。但是,如果我们不使用final,就很容易违反这一点。例如,让我们使用密码检查器:

代码语言:javascript
复制
public class PasswordChecker {
  public boolean passwordIsOk(String password) {
    return password == "s3cret";
  }
}

如果我们允许重写该类,一个实现可能会锁定每个人,而另一个实现可能给每个人提供访问权限:

代码语言:javascript
复制
public class OpenDoor extends PasswordChecker {
  public boolean passwordIsOk(String password) {
    return true;
  }
}

这通常是不好的,因为子类现在的行为与原始类非常不兼容。如果我们真的打算用其他行为来扩展这门课,那么责任链就会更好:

代码语言:javascript
复制
PasswordChecker passwordChecker =
  new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
  new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
 new DefaultPasswordChecker(
   new OpenDoor(null)
 );

public interface PasswordChecker {
  boolean passwordIsOk(String password);
}

public final class DefaultPasswordChecker implements PasswordChecker {
  private PasswordChecker next;

  public DefaultPasswordChecker(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    if ("s3cret".equals(password)) return true;
    if (next != null) return next.passwordIsOk(password);
    return false;
  }
}

public final class OpenDoor implements PasswordChecker {
  private PasswordChecker next;

  public OpenDoor(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    return true;
  }
}

当更复杂的类调用自己的方法时,问题变得更加明显,而这些方法可以被覆盖。在打印数据结构或编写HTML时,我有时会遇到这种情况。每个方法负责一些小部件。

代码语言:javascript
复制
public class Page {
  ...;

  @Override
  public String toString() {
    PrintWriter out = ...;
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    writeHeader(out);
    writeMainContent(out);
    writeMainFooter(out);
    out.print("</body>");

    out.print("</html>");
    ...
  }

  void writeMainContent(PrintWriter out) {
    out.print("<div class='article'>");
    out.print(htmlEscapedContent);
    out.print("</div>");
  }

  ...
}

我现在创建一个子类,它添加了更多的样式:

代码语言:javascript
复制
class SpiffyPage extends Page {
  ...;


  @Override
  void writeMainContent(PrintWriter out) {
    out.print("<div class='row'>");

    out.print("<div class='col-md-8'>");
    super.writeMainContent(out);
    out.print("</div>");

    out.print("<div class='col-md-4'>");
    out.print("<h4>About the Author</h4>");
    out.print(htmlEscapedAuthorInfo);
    out.print("</div>");

    out.print("</div>");
  }
}

现在暂时忽略一下,这不是生成HTML页面的好方法,如果我想再次更改布局会发生什么?我必须创建一个SpiffyPage子类,以某种方式包装该内容。我们在这里看到的是模板方法模式的意外应用。模板方法是要重写的基类中定义良好的扩展点.

如果基类发生变化,会发生什么情况?如果HTML内容变化太大,这可能会破坏子类提供的布局。因此,在以后更改基类是不安全的。如果您的所有类都在同一个项目中,这一点并不明显,但是如果基类是其他人构建的某个发布软件的一部分,则非常明显。

如果这个扩展策略是有意的,我们可以允许用户交换每个部分的生成方式。或者,可以为每个块提供一个外部提供的策略。或者,我们可以套牢解码器。这相当于上述代码,但要显式得多,而且更加灵活:

代码语言:javascript
复制
Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());

public interface PageLayout {
  void writePage(PrintWriter out, PageLayout top);
  void writeMainContent(PrintWriter out, PageLayout top);
  ...
}

public final class Page {
  private PageLayout layout = new DefaultPageLayout();

  public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
    layout = wrapper.apply(layout);
  }

  ...
  @Override public String toString() {
    PrintWriter out = ...;
    layout.writePage(out, layout);
    ...
  }
}

public final class DefaultPageLayout implements PageLayout {
  @Override public void writeLayout(PrintWriter out, PageLayout top) {
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    top.writeHeader(out, top);
    top.writeMainContent(out, top);
    top.writeMainFooter(out, top);
    out.print("</body>");

    out.print("</html>");
  }

  @Override public void writeMainContent(PrintWriter out, PageLayout top) {
    ... /* as above*/
  }
}

public final class SpiffyPageDecorator implements PageLayout {
  private PageLayout inner;

  public SpiffyPageDecorator(PageLayout inner) {
    this.inner = inner;
  }

  @Override
  void writePage(PrintWriter out, PageLayout top) {
    inner.writePage(out, top);
  }

  @Override
  void writeMainContent(PrintWriter out, PageLayout top) {
    ...
    inner.writeMainContent(out, top);
    ...
  }
}

(需要附加的top参数,以确保对writeMainContent的调用通过装饰器链的顶部。这模拟了子类的一个特性,称为开放递归。)

如果我们有多个装饰师,我们现在可以更自由地混合它们。

与稍微调整现有功能的愿望相比,更多的是希望重用现有类的某些部分。我见过这样的情况:有人想要一个类,您可以在其中添加项并对所有项进行迭代。正确的解决办法应该是:

代码语言:javascript
复制
final class Thingies implements Iterable<Thing> {
  private ArrayList<Thing> thingList = new ArrayList<>();

  @Override public Iterator<Thing> iterator() {
    return thingList.iterator();
  }

  public void add(Thing thing) {
    thingList.add(thing);
  }

  ... // custom methods
}

相反,他们创建了一个子类:

代码语言:javascript
复制
class Thingies extends ArrayList<Thing> {
  ... // custom methods
}

这意味着ArrayList的整个接口已经成为我们接口的一部分。用户可以在特定的索引上使用remove()get()。这是故意的吗?好的。但是,我们往往没有仔细考虑所有的后果。

因此,最好是

  • 在没有仔细思考的情况下,千万不要extend一个类。
  • 始终将类标记为final,除非您打算重写任何方法。
  • 在需要交换实现的地方创建接口,例如用于单元测试。

有许多“规则”必须打破的例子,但它通常会引导您进行良好、灵活的设计,并避免由于基类中的意外更改(或者将子类的意外使用作为基类的实例)而产生的错误。

有些语言有更严格的执行机制:

  • 默认情况下,所有方法都是最终的,必须显式地标记为virtual
  • 它们提供的私有继承不是继承接口,而是只继承实现。
  • 它们要求基类方法被标记为虚拟的,并要求所有重写都被标记。这避免了子类定义新方法的问题,但具有相同签名的方法后来添加到基类中,但不打算作为虚拟方法。
票数 60
EN

Software Engineering用户

发布于 2016-05-12 18:57:28

我感到惊讶的是,还没有人提到约书亚·布洛赫的“有效的Java,第二版”(至少每个Java开发人员都需要阅读它)。书中的第17条详细讨论了这一点,标题是:“为继承设计和文档,否则禁止它”。

我不会重复这本书中所有的好建议,但是这些特别的段落似乎是相关的:

但是普通的混凝土类呢?传统上,它们既不是最终的,也不是为子类设计和记录的,但是这种状态是危险的。每次在此类中进行更改,扩展该类的客户端类都有可能中断。这不仅仅是一个理论问题。在修改了非最终具体类的内部元素之后,收到与子类相关的bug报告并不少见,这些类不是为继承而设计和记录的。解决此问题的最佳解决方案是禁止在未设计和记录为安全子类的类中进行子类化。有两种禁止子类的方法。这两种方法中比较容易的是声明类的期末考试。另一种方法是使所有构造函数都是私有的或包私有的,并添加公共静态工厂来代替构造函数。这个选项提供了内部使用子类的灵活性,在项目15中讨论了这两种方法都可以接受。

票数 33
EN
页面原文内容由Software Engineering提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://softwareengineering.stackexchange.com/questions/318245

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档