21

您能否澄清一下, 当我们将其设为不可变关键字时,为什么在课前需要final关键字。我的意思是,如果我们将它的所有属性声明为 private 和 final,那么它也是一个不可变类,不是吗?

对不起,如果这个问题看起来很简单,但我真的很困惑。帮帮我。

编辑:我知道一个声明为 final 的类不能被子类化。但是如果每个属性都是私有的和 final 的,那有什么区别呢?

4

7 回答 7

25

正如 stacker 所说,final确保该类没有被子类化。这很重要,因此任何依赖其不变性的代码都可以安全地执行此操作。

例如,不可变类型(其中每个字段也是不可变类型)可以在线程之间自由使用,而不必担心数据竞争等。现在考虑:

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

看起来您可以Person毫无问题地跨线程自由共享实例。但是当你共享的对象实际上是一个可变子类时呢?

public class Employee extends Person {
    private String company;

    public Employee(String name, String company) {
        super(name);
        this.company = company;
    }

    public void setCompany(String company) {
        this.company = company;
    }

    public String getCompany() {
        return company; 
    }
}

现在实例在线程之间共享Employee 不安全的,因为它们不是不可变的。但是进行共享的代码可能只知道它们是Person……导致它们产生虚假的安全感的实例。

缓存也是如此——缓存和重用不可变类型应该是安全的,对吧?好吧,缓存真正属于不可变类型的实例安全的——但如果你正在处理一个本身不允许突变但允许子类的类型它突然就不再安全了。

想想java.lang.Object。它没有任何可变字段,但将每个Object引用视为对不可变类型的引用显然是个坏主意。基本上,这取决于您是否将不变性视为类型或对象的属性。一个真正不可变的类型声明“任何时候你看到这个类型的引用,你都可以把它当作不可变的”——而允许任意子类化的类型不能做出这样的声明。

顺便说一句,有一个折衷方案:如果您可以将子类化限制在“受信任”的地方,您可以确保一切都是不可变的,但仍然允许子类化。Java 中的访问使这变得棘手,但在 C# 中,例如,您可以拥有一个公共类,它只允许在同一程序集中进行子类化 - 提供一个在不变性方面很好且强大的公共 API,同时仍然允许多态性的好处.

于 2012-12-29T09:05:43.753 回答
4

声明为 final 的类不能被子类化。另请参阅http://docs.oracle.com/javase/tutorial/java/IandI/final.html

The Java Language Specification中描述了 final 关键字所有用法的不同语义

  • 4.12.4 最终变量第 80 页
  • 8.1.1.2 最终类第 184 页
  • 8.3.1.2 最终字段第 209 页
  • 8.4.3.3 最终方法第 223 页
于 2012-12-29T08:59:21.267 回答
2

不需要final 来创建一个不可变的类。即你可以创建一个不可变的类而不是最终的。

但是,如果您使其成为最终的,那么某人可以扩展一个类并创建一个可变的子类(通过添加新的可变字段或以一种使您能够改变受保护的字段的方式覆盖方法原始的不可变类)。这是一个潜在的问题 - 它违反了Liskov Substitution Principle,从某种意义上说,您会期望所有子类型都保留不变性的属性。

因此,最好将不可变类设为 final 以避免这种风险。

于 2012-12-29T09:07:14.957 回答
2

关键字名称所暗示的“最终”意味着最终关键字所附加的属性不能更改(就值而言),换句话说,它的行为就像一个常量。

根据您的问题,如果该类的所有成员都设为私有和最终但该类不是最终的,那么可以继承同一个类但超类成员是不可变的,因为它们附加了 final 关键字。

于 2012-12-29T09:09:11.567 回答
1

不可变对象是一种状态保证在其整个生命周期内保持不变的对象。虽然没有 final 实现不变性是完全可能的,但它的使用使人类(软件开发人员)和机器(编译器)明确了这一目的。

不可变对象具有一些非常理想的特性:

they are simple to understand and easy to use
they are inherently thread-safe: they require no synchronization
they make great building blocks for other objects 

显然 final 将帮助我们定义不可变对象。首先将我们的对象标记为不可变,这使得其他程序员易于使用和理解。其次是保证对象的状态永远不会改变,这启用了线程安全属性:当一个线程可以更改数据而另一个线程正在读取相同的数据时,线程并发问题是相关的。因为不可变对象从不更改其数据,所以不需要同步访问它。

通过满足以下所有条件来创建不可变类:

Declare all fields private final.
Set all fields in the constructor.
Don't provide any methods that modify the state of the object; provide only getter methods (no setters).
Declare the class final, so that no methods may be overridden.
Ensure exclusive access to any mutable components, e.g. by returning copies.

声明为 final 的类不能被子类化。其他类不能扩展最终类。它为安全性和线程安全提供了一些好处。

于 2012-12-29T09:08:08.647 回答
1

如果所有公共和受保护的方法都是最终的,并且它们都不允许修改私有字段,并且所有公共和受保护的字段都是最终的和不可变的,那么我想它可以说类是半不可变的,或者某种常量。

但是当你创建一个子类并且需要覆盖 equals 和 hashcode 时,事情就会崩溃。不能因为你把它们做成final...所以整个事情都坏了,所以把整个类做成final,以防止程序员不小心成为傻瓜。

作为执行这种混蛋版本不变性的替代方法,您有多种选择。

如果要将额外数据附加到不可变实例,请使用Map. 就像如果您想在姓名中添加年龄一样,您不会这样做class NameAge extends String... :-)

如果要添加方法,请创建一个静态实用函数类。这有点笨拙,但它是当前的 Java 方式,例如 Apache commons 充满了这样的类。

如果要添加额外的方法和数据,请创建一个包装类,将方法委托给不可变类的方法。无论如何,任何需要使用额外方法的人都需要了解它们,并且在转换为派生的非不可变类或new MyWrapper(myImmutableObj)为许多用例执行类似操作时没有太大的实际区别。

当您确实必须引用原始不可变对象(例如将其存储在现有类中您无法更改),但需要在某处额外数据时,您需要使用该Map方法来保留额外数据或类似的东西。

于 2012-12-29T09:37:30.987 回答
0

如果不可变类Foo是密封的(“final”),那么任何接收到 a 引用的人Foo都可以确信,如果Foo正确实现,则引用的实例实际上是不可变的。如果不可变类不是密封的,那么接收到 a 引用的人Foo可以确信,如果被引用对象的实际类(可能是某个任意未知人员实现的Foo某个派生类型)被正确实现,则该实例将是不可变的。不密封意味着任何依赖于不可变的人都必须相信每个编写派生类的人都会正确地实现它。如果一个人想确定每一个对FooFooFooFoo实际上将针对不可变实例,而不必依赖派生类的作者来遵守合同,使Foofinal 有助于这种保证。

另一方面,一个类可能派生于Foo但违反其不变性的可能性与派生自任何其他类的类可能违反其父类的契约的可能性并没有太大区别。任何接受可以被外部代码子类化的任何类型的引用的代码都可能被赋予一个违反其父合同的子类的实例。

决定一个不可变类是否应该被密封时的基本问题与任何其他类相同:保持类型不密封的好处是否超过这样做可能带来的任何危险。在某些情况下,拥有一个可扩展的不可变类,甚至是一个抽象类或接口,其具体实现在合同上都有义务是不可变的,这可能是有意义的;例如,一个绘图包可能有一个ImmutableShape具有一些具体字段、属性和方法的类来定义 2D 转换,但它是一个抽象Draw方法,允许定义派生类型ImmutablePolygon, ImmutableTextObject,ImmutableBezierCurve等。如果有人实现了一个ImmutableGradientFilledEllipse类但没有该类型制作自己的可变副本GradientColorSelector,渐变填充多边形的颜色可能会意外改变,但这将是ImmutableGradientFilledEllipse类的错误,而不是消耗代码。尽管一个被破坏的实现有可能无法维护“不变性”契约,一个可扩展的ImmutableShape类将比一个密封的类更通用。

于 2013-01-02T17:25:44.507 回答