为什么在Java中声明一个不可变的类final?

时间:2012-09-06 18:59:19

标签: java immutability final

我读到要在Java中创建class immutable,我们应该执行以下操作,

  1. 不提供任何制定者
  2. 将所有字段标记为私有
  3. 让课程最终
  4. 为什么需要第3步?我为什么要标记班级final

11 个答案:

答案 0 :(得分:117)

如果你没有标记班级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可以防止这种情况发生。

希望这有帮助!

答案 1 :(得分:37)

与许多人认为的相反,制作不可变的课程final 是必需的。

创建不可变类final的标准参数是,如果不这样做,则子类可能会增加可变性,从而违反了超类的约定。该类的客户将承担不变性,但当从它们下面发生变异时会感到惊讶。

如果你把这个参数带到它的逻辑极值,那么所有方法都应该成为final,否则一个子类可能会以一种不符合方法的方式覆盖一个方法超级合同。有趣的是,大多数Java程序员认为这是荒谬的,但是对于不可变类应该是final的想法是可行的。我怀疑它与Java程序员有关,一般不能完全理解不变性的概念,也许还有某种模糊思维与Java中final关键字的多重含义有关。

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

不可变性是课程合同的一部分。它与人们更习惯的一些东西有点不同,因为它说明了不能所做的类(和所有子类),而我认为大多数Java(通常是OOP)程序员倾向于将合同视为与 >

不变性也不仅影响单个方法 - 它影响整个实例 - 但这与Java工作中equalshashCode的方式并没有太大的不同。这两种方法在Object中有一个特定的合同。这份合同非常仔细地列出了这些方法不能做的事情。该子合约在子类中更具体。以违反合同的方式覆盖equalshashCode非常容易。事实上,如果你只重写这两种方法中的一种而没有另一种方法,那么很可能你违反了合同。那么equals和[{1}}是否应在hashCode中声明为final以避免这种情况?我想大多数人都认为他们不应该这样做。同样,没有必要创建不可变类Object

那就是说,你的大部分课程都是不可改变的,可能 应该是final。请参阅 Effective Java Second Edition 第17项:“继承的设计和文档,否则禁止它”。

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

答案 2 :(得分:19)

不要将整个班级标记为最终。

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

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

通过这种方式,您可以允许扩展类(甚至可能是可变类),但是类的不可变方面是受保护的。属性是私有的,无法访问,这些属性的getter是final,无法覆盖。

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

答案 3 :(得分:5)

如果它不是最终版,那么任何人都可以扩展课程并做任何他们喜欢的事情,例如提供setter,遮蔽你的私有变量,并且基本上使它变得可变。

答案 4 :(得分:4)

这会限制其他类扩展你的类。

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

如果一个类将你想要创建的类扩展为不可变类,那么由于继承原则,它可能会改变类的状态。

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

答案 5 :(得分:2)

对于创建不可变类,不必将该类标记为final。

让我从java类本身中选择一个这样的例子“BigInteger”类是不可变的,但它不是最终的。

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

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

不变性意味着一旦创建对象就无法改变对象的状态,这是通过三个拇指规则实现的,这使得编译器能够识别出类是不可变的,它们如下: -

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

有关更多信息,请参阅以下网址

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

答案 6 :(得分:1)

如果你没有最终确定,我可以扩展它并使其不可变。

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传递给任何需要Immutable的类,并且它不会像预期的合约那样。

答案 7 :(得分: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类型变量的对象不再保证是mmutable的。这可能会导致哈希,相等,并发等问题。

答案 8 :(得分:0)

设计本身没有价值。设计总是用来实现目标。这里的目标是什么?我们是否希望减少代码中的意外数量?我们想要防止错误吗?我们盲目遵守规则吗?

此外,设计总是需要付出代价。 cpu

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

  1. 这会阻止多少明显的错误?
  2. 这会阻止多少微妙的错误?
  3. 这会使其他代码更复杂(=更容易出错)?
  4. 这会使测试更容易还是更难?
  5. 您项目中的开发人员有多好?他们需要用大锤多少指导?
  6. 假设您的团队中有许多初级开发人员。他们会拼命尝试任何愚蠢的事情只是因为他们不知道解决他们问题的好方法。使类最终可以防止错误(好)但也可以使它们提出“聪明”的解决方案,例如将所有这些类复制到代码中的任何可变类中。

    另一方面,在任何地方使用课程final之后很难成为课程final如果你找到的话很容易让final课程成为非use strict; use warnings; my @words = qw( first second third fourth ); print join( q{ }, map { $words[int rand @words] } 1 .. 4 ), "\n"; 你需要扩展它。

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

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

答案 9 :(得分:0)

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

答案 10 :(得分:0)

假设您有以下课程:

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]

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