为什么要用Java声明一个不变的类final?


83

我读到要使Java中的类不可变,我们应该执行以下操作:

  1. 不提供任何二传手
  2. 将所有字段标记为私有
  3. 最终上课

为什么需要步骤3?我为什么要上课final呢?


java.math.BigIntegerclass是示例,它的值是不可变的,但不是最终值。
Nandkumar Tekale 2012年

@Nandkumar如果您有一个BigInteger,则不知道它是否是不变的。这是一个混乱的设计。/java.io.File是一个更有趣的示例。
汤姆·霍顿-钉路2012年

1
不允许子类覆盖方法-最简单的方法是将类声明为final。一个更复杂的方法是使构造私人和结构实例工厂方法-从- docs.oracle.com/javase/tutorial/essential/concurrency/...
劳尔

1
@mkobitFile之所以有趣是因为它很可能是不可变的,因此会导致TOCTOU(检查时间/使用时间)攻击。受信任的代码检查File路径是否可接受,然后使用该路径。子类File可以在检查和使用之间更改值。
Tom Hawtin-定位线

1
Make the class final或确保所有子类都是不可变的
O.Badr,

Answers:


138

如果您不标记该类final,那么我可能会突然使您看似不变的类真正变得可变。例如,考虑以下代码:

public class Immutable {
     private final int value;

     public Immutable(int value) {
         this.value = value;
     }

     public int getValue() {
         return value;
     }
}

现在,假设我执行以下操作:

public class Mutable extends Immutable {
     private int realValue;

     public Mutable(int value) {
         super(value);

         realValue = value;
     }

     public int getValue() {
         return realValue;
     }
     public void setValue(int newValue) {
         realValue = newValue;
     }

    public static void main(String[] arg){
        Mutable obj = new Mutable(4);
        Immutable immObj = (Immutable)obj;              
        System.out.println(immObj.getValue());
        obj.setValue(8);
        System.out.println(immObj.getValue());
    }
}

注意,在我的Mutable子类中,我重写了getValue读取我的子类中声明的新的可变字段的行为。结果,您的类(最初看起来是不可变的)实际上并不是不可变的。我可以Mutable在需要对象的任何地方传递该对象Immutable,假设对象确实是不可变的,这可能会做非常不好的事情。标记基类final可以防止这种情况的发生。

希望这可以帮助!


13
如果我做Mutable m = new Mutable(4); m.setValue(5); 在这里,我在玩Mutable类对象,而不是Immutable类对象。所以,我仍然感到困惑,为什么Immutable类不是不可变的
Anand

18
@ anand-假设您有一个接受Immutable参数的函数。我可以将一个Mutable对象传递给该函数,因为Mutable extends Immutable。在该函数内部,虽然您认为对象是不可变的,但我可以拥有一个辅助线程,该线程可以在函数运行时更改其值。我还可以给您一个Mutable函数存储的对象,然后稍后在外部更改其值。换句话说,如果您的函数假定该值是不可变的,则它很容易中断,因为我可以给您一个可变的对象并在以后更改它。那有意义吗?
templatetypedef

6
@ anand-传递Mutable对象的方法不会更改对象。令人担忧的是,该方法可能假设对象实际上不是不可变的。例如,该方法可能假定,因为它认为对象是不可变的,因此可以将其用作a中的键HashMap。然后,我可以传入一个Mutable,以等待该函数将对象存储为键,然后更改该Mutable对象,从而破坏该功能。现在,HashMap由于我更改了与对象关联的键,因此查找将失败。那有意义吗?
templatetypedef

3
@ templatetypedef-是的,我知道了now..must说是一个真棒explanation..took一点时间掌握它,虽然
阿南德

1
@ SAM-是的。但是,这并不重要,因为被覆盖的函数再也不会引用该变量。
templatetypedef

47

相反的是,许多人认为,做一个不变类final必需的。

制作不可变类的标准论点final是,如果不这样做,则子类会增加可变性,从而违反了超类的约定。班级的客户将承担不变性,但是当某些东西从他们下面变出来时,他们会感到惊讶。

如果将此参数推至逻辑极限,则应采用所有方法final,否则子类可能会以不符合其超类协定的方式覆盖方法。有趣的是,大多数Java程序员都认为这很荒谬,但是对于不可变的类应该是的想法还是可以接受的final。我怀疑这与Java程序员有关,总的来说,它并不完全与不变性的概念有关,也许与finalJava关键字的多种含义有关的某种模糊思维。

符合您的超类的约定不是编译器可以或应该始终执行的。编译器可以执行合同的某些方面(例如:最少的一组方法及其类型签名),但是编译器无法执行典型合同的许多部分。

不变性是集体合同的一部分。这与人们更习惯的某些操作有所不同,因为它说明了类(以及所有子类)不能做什么,而我认为大多数Java(通常是OOP)程序员倾向于将合同与一个班级可以做什么,而不是它不能做什么。

不变性不仅会影响单个方法,而且会影响整个实例,但这equalshashCodeJava的工作方式和方式并没有太大不同。这两种方法在中都有特定的合同Object。该合同非常仔细地列出了这些方法无法完成的工作。该合同在子类中更加具体。这很容易被覆盖equalshashCode违反合同。实际上,如果您仅覆盖这两种方法中的一种而没有另一种,则很可能违反了合同。所以应该equalshashCode已经宣布finalObject避免这种情况?我认为大多数人会认为他们不应该这样做。同样,也不必制作不可变的类final

也就是说,您的大多数类(无论是否不变)都应该final。请参阅有效的Java第二版项目17:“设计和记录继承,否则禁止继承”。

因此,第3步的正确版本是:“将类定型,或者在设计子类时,明确记录所有子类必须继续不可变。”


3
值得注意的是,Java“期望”但没有强制执行,给定两个对象XY,其值X.equals(Y)将是不可变的(只要XY继续引用相同的对象)。对于哈希码,它也有类似的期望。显然,没有人应该期望编译器强制实现等价关系和哈希码的不变性(因为它不能做到)。我认为人们没有理由期望该类型的其他方面会强制实施它。
supercat 2012年

1
同样,在许多情况下,具有其合同规定不变性的抽象类型可能很有用。例如,可能有一个抽象的ImmutableMatrix类型,给定一个坐标对,该类型将返回a double。一个人可能会派生一个GeneralImmutableMatrix使用数组作为后备存储的a,但也可能有一个ImmutableDiagonalMatrix例子,它仅沿对角线存储一个项目数组(读取项X,Y如果X == y将产生Arr [X],否则返回零) 。
supercat 2012年

2
我最喜欢这种解释。始终制作不可变的类会final限制其用途,尤其是在设计要扩展的API时。为了线程安全,使您的类尽可能不变,但是保持其可扩展性是有意义的。您可以将字段设置为protected final而不是private final。然后,在子文档中显式地建立关于遵守不变性和线程安全性保证的子类的契约。
curioustechizen

4
+1-我一直与您同在,直到Bloch报价。我们不必如此偏执。99%的编码是合作的。如果有人真的需要重写某些东西,那就没关系了。我们中有99%的人未编写某些核心API,例如String或Collection。不幸的是,许多Bloch的建议都是基于这种用例的。它们对大多数程序员,特别是新程序员并没有真正的帮助。
宇2015年

我宁愿做一成不变的课堂final。仅仅因为equals并且hashcode不可能是最终的,并不意味着我可以容忍不可变的类,除非我有正当的理由,在这种情况下,我可以使用文档或测试作为防御线。
O.Badr

21

不要将整个班级都标记为决赛。

有充分的理由允许如其他答案中所述扩展不可变的类,因此将类标记为final并非总是一个好主意。

最好将属性标记为私有和最终标记,如果您想保护“合同”,则将获取者标记为最终标记。

这样,您可以允许扩展类(即使是可变的类也可以),但是可以保护类的不变性。属性是私有的,无法访问,这些属性的吸气剂是最终的,不能被覆盖。

使用您的不可变类实例的任何其他代码都将能够依赖于类的不可变方面,即使所传递的子类在其他方面是可变的。当然,由于它需要一个类的实例,所以甚至不知道这些其他方面。


1
我宁愿将所有不可变方面封装在一个单独的类中,并通过组合使用它。相比将可变状态和不可变状态混合在单个代码单元中,进行推理要容易得多。
toniedzwiedz 2014年

1
完全同意。如果可变的类包含不可变的类,那么组合将是实现此目的的一种非常好的方法。如果您的结构与之相反,那么这将是一个不错的局部解决方案。
贾斯汀·欧姆

5

如果不是最终的,那么任何人都可以扩展该类并做他们想做的任何事情,例如提供设置器,隐藏私有变量并基本上使其可变。


尽管您是对的,但不清楚的是,如果基类字段都是私有且最终的,那么可以添加可变行为这一事实必然会破坏事情。真正的危险是,子类可能会通过重写行为将以前不可变的对象变成可变的对象。
templatetypedef

4

这限制了其他类的扩展。

最终课程不能被其他课程扩展。

如果一个类将您要使其变为不可变的类进行扩展,则由于继承原则,它可能会更改该类的状态。

请澄清“它可能会改变”。子类可以覆盖超类行为,例如使用方法覆盖(例如templatetypedef / Ted Hop答案)


2
没错,但是为什么这里有必要?
templatetypedef

@templatetypedef:你太快了。我正在用支持点编辑我的答案。
科萨2012年

4
现在,您的答案太含糊,以至于我根本无法回答这个问题。“由于继承原则,它可能会更改类的状态”是什么意思?除非您已经知道为什么要标记它final,否则我看不出这个答案有什么帮助。
templatetypedef

3

如果您未将其定型,则可以对其进行扩展并使其不可变。

public class Immutable {
  privat final int val;
  public Immutable(int val) {
    this.val = val;
  }

  public int getVal() {
    return val;
  }
}

public class FakeImmutable extends Immutable {
  privat int val2;
  public FakeImmutable(int val) {
    super(val);
  }

  public int getVal() {
    return val2;
  }

  public void setVal(int val2) {
    this.val2 = val2;
  }
}

现在,我可以将FakeImmutable传递给任何期望不可变的类,并且它不会表现为期望的契约。


2
我认为这与我的回答几乎相同。
templatetypedef

是的,过了中途检查,没有新答案。然后发布时,您的位置就在我面前1分钟。至少我们没有为所有事物使用相同的名称。
RogerLindsjö2012年

一个更正:在FakeImmutable类中,构造函数名称应为FakeImmutable NOT不可变
Sunny Gupta

2

对于创建不可变的类,并非强制性地将该类标记为最终类。

让我从Java类本身中获取一个这样的例子。“ BigInteger”类是不可变的,但不是最终的。

实际上,不变性是一个概念,根据该概念,创建的对象不能被修改。

让我们从JVM的角度考虑,从JVM的角度来看,所有线程都必须共享对象的相同副本,并且在任何线程访问它之前,它都已完全构建,并且对象的状态在其构建后不会改变。

不变性意味着一旦创建了对象就无法更改其状态,这是通过三个经验法则实现的,这三个规则使编译器认识到类是不变的,它们如下:

  • 所有非私有字段都应为最终字段
  • 确保该类中没有可以直接或间接更改对象字段的方法
  • 类中定义的任何对象引用都不能在类外部进行修改

有关更多信息,请参见下面的URL

http://javaunturnedtopics.blogspot.in/2016/07/can-we-create-immutable-class-without.html


1

假设您有以下课程:

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class PaymentImmutable {
    private final Long id;
    private final List<String> details;
    private final Date paymentDate;
    private final String notes;

    public PaymentImmutable (Long id, List<String> details, Date paymentDate, String notes) {
        this.id = id;
        this.notes = notes;
        this.paymentDate = paymentDate == null ? null : new Date(paymentDate.getTime());
        if (details != null) {
            this.details = new ArrayList<String>();

            for(String d : details) {
                this.details.add(d);
            }
        } else {
            this.details = null;
        }
    }

    public Long getId() {
        return this.id;
    }

    public List<String> getDetails() {
        if(this.details != null) {
            List<String> detailsForOutside = new ArrayList<String>();
            for(String d: this.details) {
                detailsForOutside.add(d);
            }
            return detailsForOutside;
        } else {
            return null;
        }
    }

}

然后扩展它并打破其不变性。

public class PaymentChild extends PaymentImmutable {
    private List<String> temp;
    public PaymentChild(Long id, List<String> details, Date paymentDate, String notes) {
        super(id, details, paymentDate, notes);
        this.temp = details;
    }

    @Override
    public List<String> getDetails() {
        return temp;
    }
}

在这里我们对其进行测试:

public class Demo {

    public static void main(String[] args) {
        List<String> details = new ArrayList<>();
        details.add("a");
        details.add("b");
        PaymentImmutable immutableParent = new PaymentImmutable(1L, details, new Date(), "notes");
        PaymentImmutable notImmutableChild = new PaymentChild(1L, details, new Date(), "notes");

        details.add("some value");
        System.out.println(immutableParent.getDetails());
        System.out.println(notImmutableChild.getDetails());
    }
}

输出结果将是:

[a, b]
[a, b, some value]

如您所见,当原始类保持其不变性时,子类可以是可变的。因此,除非您将类定型,否则在您的设计中您不能确定所使用的对象是不可变的。


0

假设以下类不是final

public class Foo {
    private int mThing;
    public Foo(int thing) {
        mThing = thing;
    }
    public int doSomething() { /* doesn't change mThing */ }
}

这显然是不可变的,因为即使子类也无法修改mThing。但是,子类可以是可变的:

public class Bar extends Foo {
    private int mValue;
    public Bar(int thing, int value) {
        super(thing);
        mValue = value;
    }
    public int getValue() { return mValue; }
    public void setValue(int value) { mValue = value; }
}

现在,可分配给类型变量的对象Foo不再保证是可变的。这可能导致诸如哈希,相等,并发等问题。


0

设计本身没有任何价值。设计始终用于实现目标。这里的目标是什么?我们是否要减少代码中的惊喜?我们要防止错误吗?我们是否盲目遵守规则?

而且,设计总是要付出代价的。每个值得称呼的设计都意味着你有目标冲突

考虑到这一点,您需要找到以下问题的答案:

  1. 这将阻止多少个明显的错误?
  2. 这将阻止多少个细微的错误?
  3. 这会使其他代码变得更复杂的频率(=更容易出错)?
  4. 这会使测试变得容易还是困难?
  5. 您的项目中的开发人员有多好?他们需要多少把大锤引导?

假设您的团队中有许多初级开发人员。他们会拼命尝试任何愚蠢的事情,因为他们还不知道解决问题的好方法。将类设置为final可以防止错误(良好),但是也可以使它们提出“聪明的”解决方案,例如将所有这些类复制到代码中各处的可变类中。

另一方面,在所有地方使用完一个类final后,很难创建一个类,但是如果以后发现需要扩展它,则很容易将其设为final非类final

如果正确使用接口,则可以通过始终使用接口,然后在需要时添加可变实现,来避免出现“我需要使此可变”问题。

结论:此答案没有“最佳”解决方案。这取决于您愿意支付的价格以及必须支付的价格。


0

equals()的默认含义与引用相等相同。对于不可变的数据类型,这几乎总是错误的。因此,您必须重写equals()方法,将其替换为自己的实现。链接

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.