为什么最终关键字对于不可变类是必要的?

时间:2012-12-29 08:56:44

标签: java immutability final

您能否澄清为什么在课堂上我们将最终关键字作为不可变的关键字时需要它。 我的意思是,如果我们将所有属性声明为私有和最终属性,那么它也是一个不可变的类,不是吗?

很抱歉,如果这个问题看似简单,但我真的很困惑。帮助我。

Editted: 我知道一个声明为final的类不能被子类化。但是如果每个属性都是私有的,那么最终会有什么不同呢?

7 个答案:

答案 0 :(得分:24)

正如堆栈器所说,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,同时仍然允许多态的好处

答案 1 :(得分:4)

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

The Java Language Specification

中描述了final关键字的所有用法的不同语义
  • 4.12.4 final变量第80页
  • 8.1.1.2 final Classes Page 184
  • 8.3.1.2 final Fields Page 209
  • 8.4.3.3最终方法Page 223

答案 2 :(得分:2)

'final'作为关键字的名称建议意味着最终关键字所附加的属性不能更改(就值而言),换句话说,它的行为类似于常量。

根据你的问题,如果该类的所有成员都是私有的并且是最终的但是该类不是最终的,则可以继承相同的类,但是超类成员是不可变的,因为最终关键字被附加到它们。

答案 3 :(得分:1)

你没有严格需要 final来创建一个不可变的类。即,你可以在没有最终成绩的情况下创建一个不可变的类。

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

因此,通常最好将不可变类最终化以避免这种风险。

答案 4 :(得分:1)

不可变对象是一个对象,它保证状态在整个生命周期内保持相同。虽然完全可以在没有最终版本的情况下实现不变性,但它的使用对于人类(软件开发人员)和机器(编译器)来说是明确的。

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

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.

宣布最终的类不能被分类。其他课程不能延长最终课程。它为安全性和线程安全性提供了一些好处。

答案 5 :(得分:1)

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

但是当你创建一个子类并且需要覆盖equals和hashcode时,事情就会崩溃。并且不能因为你让它们成为最终的...所以整个事情都被打破了,所以只要让全班最终防止程序员偶然成为一个傻瓜。

作为实现这种不稳定版本不变性的替代方法,您有几种选择。

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

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

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

当你真的必须引用原始的可变对象时(比如将它存储在现有的类中你无法改变),但是需要在某处获得额外的数据,你需要使用Map方法来保存额外的数据周围,​​或类似的东西。

答案 6 :(得分:0)

如果一个不可变的类Foo被密封(“最终”),那么任何接收Foo引用的人都可以确信如果Foo被正确实现,那么引用的实例实际上是不可改变的。如果一个不可变类没有被密封,那么接收Foo引用的人可以确信如果引用对象的实际类(可能是Foo或某种派生类型由一些任意未知的人实现)是正确实现的,实例将是不可变的。保持Foo未密封意味着任何依赖Foo不可变的人都必须相信每个编写一个派生自Foo的类的人都会正确地实现它。如果想要确定每个对Foo的引用实际上都是针对不可变实例而不必依赖衍生类的作者来遵守合同,那么使Foo final可以帮助这样做保证。

另一方面,类可能派生自Foo但可能违反其不变性的可能性与源自任何其他类的类的可能性并不完全不同违反其父类的合同。接受可由外部代码子类化的任何类型引用的任何代码都可能被赋予违反其父代合同的子类实例。

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