【问题标题】:Why would one declare an immutable class final in Java?为什么要在 Java 中声明一个不可变类 final?
【发布时间】:2012-08-31 15:52:08
【问题描述】:

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

  1. 不提供任何setter
  2. 将所有字段标记为私有
  3. 使课程成为最终课程

为什么需要第 3 步?我为什么要标记课程final

【问题讨论】:

  • java.math.BigInteger 类是示例,它的值是不可变的,但不是最终的。
  • @Nandkumar 如果你有一个BigInteger,你不知道它是否是不可变的。这是一个乱七八糟的设计。 /java.io.File 是一个更有趣的例子。
  • 不允许子类覆盖方法 - 最简单的方法是将类声明为 final。更复杂的方法是使构造函数私有并在工厂方法中构造实例 - 来自 - docs.oracle.com/javase/tutorial/essential/concurrency/…
  • @mkobit File 很有趣,因为它很可能被认为是不可变的,因此会导致 TOCTOU(检查时间/使用时间)攻击。受信任的代码检查File 是否具有可接受的路径,然后使用该路径。子类File 可以在检查和使用之间更改值。
  • Make the class final 或确保所有子类都是不可变的

标签: java immutability final


【解决方案1】:

如果你不标记类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 的行为,以读取在我的子类中声明的新的可变字段。结果,您的类最初看起来是不可变的,但实际上并不是不可变的。我可以在任何需要Immutable 对象的地方传递这个Mutable 对象,假设对象是真正不可变的,这可能会对代码造成非常糟糕的事情。标记基类final 可以防止这种情况发生。

希望这会有所帮助!

【讨论】:

  • 如果我做 Mutable m = new Mutable(4); m.setValue(5);在这里,我玩的是 Mutable 类对象,而不是 Immutable 类对象。所以,我仍然很困惑为什么 Immutable 类不是不可变的
  • @anand- 想象一下,你有一个函数接受 Immutable 作为参数。我可以将Mutable 对象传递给该函数,因为Mutable extends Immutable。在该函数内部,虽然您认为您的对象是不可变的,但我可以有一个辅助线程在函数运行时更改值。我还可以给你一个函数存储的Mutable 对象,然后在外部更改它的值。换句话说,如果您的函数假定该值是不可变的,那么它很容易中断,因为我可以给您一个可变对象并稍后更改它。这有意义吗?
  • @anand- 将Mutable 对象传入的方法不会更改对象。令人担忧的是,该方法可能会假定对象是不可变的,而实际上它不是。例如,该方法可能假设,因为它认为对象是不可变的,所以它可以用作HashMap 中的键。然后我可以通过传入Mutable 来破坏该函数,等待它将对象存储为键,然后更改Mutable 对象。现在,HashMap 中的查找将失败,因为我更改了与对象关联的键。这有意义吗?
  • @templatetypedef- 是的,我现在明白了..必须说这是一个很棒的解释..虽然花了一点时间来掌握它
  • @SAM- 没错。但是,这并不重要,因为被覆盖的函数永远不会再次引用该变量。
【解决方案2】:

与许多人的看法相反,创建不可变类final不需要的。

创建不可变类final 的标准论据是,如果您不这样做,那么子类可以添加可变性,从而违反超类的约定。该类的客户将假定不变性,但当某些东西从他们下面发生变异时会感到惊讶。

如果你把这个论点带到它的逻辑极端,那么所有方法应该被设为final,否则子类可能会以不符合约定的方式覆盖方法它的超类。有趣的是,大多数 Java 程序员都认为这很荒谬,但对不可变类应该是 final 的想法还是很满意的。我怀疑这与 Java 程序员一般不完全接受不变性的概念有关,并且可能与 Java 中 final 关键字的多重含义有关。

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

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

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

也就是说,您的大多数类,无论是否不可变,可能应该final。参见Effective Java Second Edition第 17 条:“设计和文档继承,否则禁止它”。

因此,您的第 3 步的正确版本应该是:“使类成为最终的,或者在设计子类时,清楚地记录所有子类必须继续是不可变的。”

【讨论】:

  • 值得注意的是,Java“预期”但不强制,给定两个对象XYX.equals(Y) 的值将是不可变的(只要XY 继续引用相同的对象)。它对哈希码有类似的期望。很明显,没有人应该期望编译器强制执行等价关系和哈希码的不变性(因为它根本不能)。我认为人们没有理由期望它在类型的其他方面被强制执行。
  • 此外,在许多情况下,拥有一个其契约指定不变性的抽象类型可能很有用。例如,可能有一个抽象的 ImmutableMatrix 类型,给定一个坐标对,返回一个double。可能会派生一个GeneralImmutableMatrix,它使用数组作为后备存储,但也可能有例如ImmutableDiagonalMatrix 仅沿对角线存储项目数组(如果 X==y 读取项目 X,Y 将产生 Arr[X] 否则为零)。
  • 我最喜欢这个解释。总是创建一个不可变类final 会限制它的用处,尤其是在您设计一个旨在扩展的 API 时。为了线程安全,让你的类尽可能不可变是有意义的,但要保持它是可扩展的。您可以将字段设为protected final 而不是private final。然后,明确地建立一个关于遵守不变性和线程安全保证的子类的合同(在文档中)。
  • +1 - 在 Bloch 报价之前,我一直与您同在。我们不必如此偏执。 99% 的编码都是合作的。如果有人真的需要覆盖某些东西,这没什么大不了的,让他们吧。我们 99% 的人都没有编写一些核心 API,例如 String 或 Collection。不幸的是,布洛赫的许多建议都是基于这种用例。它们对大多数程序员没有真正的帮助,尤其是新程序员。
  • 我更喜欢创建不可变类final。仅仅因为 equalshashcode 不能是 final 并不意味着我可以容忍不可变类的相同,除非我有正当理由这样做,在这种情况下我可以使用文档或测试作为防线.
【解决方案3】:

不要将整个班级标记为final。

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

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

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

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

【讨论】:

  • 我宁愿将所有不可变方面封装在一个单独的类中,并通过组合来使用它。这比在单个代码单元中混合可变和不可变状态更容易推理。
  • 完全同意。如果您的可变类包含您的不可变类,组合将是实现此目的的一种非常好的方法。如果你的结构是相反的,那将是一个很好的部分解决方案。
【解决方案4】:

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

【讨论】:

  • 虽然你是对的,但不清楚为什么如果基类字段都是私有的和最终的,你可以添加可变行为的事实必然会破坏事情。真正的危险是子类可能会通过覆盖行为将以前不可变的对象变成可变对象。
【解决方案5】:

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

final 类不能被其他类扩展。

如果一个类扩展了你想要使其不可变的类,它可能会由于继承原则而改变类的状态。

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

【讨论】:

  • 确实如此,但这里为什么需要呢?
  • @templatetypedef:你太快了。我正在用支持点编辑我的答案。
  • 现在你的答案很模糊,我认为它根本不能回答这个问题。 “由于继承原则,它可能会改变类的状态”是什么意思?除非您已经知道为什么应该将其标记为 final,否则我看不出这个答案有什么帮助。
【解决方案6】:

如果你不把它定为最终的,我可以扩展它并使其不可变。

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 的类,它不会像预期的合约那样运行。

【讨论】:

  • 我认为这与我的答案几乎相同。
  • 是的,在发帖过程中检查了一半,没有新的答案。然后当发布时,你的,比我早 1 分钟。至少我们没有为所有东西使用相同的名称。
  • 一个更正:在 FakeImmutable 类中,构造函数名称应该是 FakeImmutable NOT Immutable
  • 根据这个逻辑,java.time.LocalDatejava.math.BigDecimal 等的对象不应被称为不可变,而文档声称如此。真相是什么?
  • 我认为stackoverflow.com/a/12327580/917548 解释了关于不变性的更多细节。你可以有一个合同说这个类是不可变的。使用 final 表示不应扩展/增强此类。
【解决方案7】:

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

让我从标准库本身举一个这样的例子:BigInteger 是不可变的,但它不是 final

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

让我们从 JVM 的角度来思考。从 JVM 的角度来看,这个类的对象应该在任何线程可以访问它之前完全构造,并且对象的状态在构造之后不应该改变。

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

  • 所有非private 字段应为final
  • 确保类中没有可以直接或间接更改对象字段的方法
  • 类中定义的任何对象引用都不能从类外部修改

更多信息请参考this URL

【讨论】:

    【解决方案8】:

    假设您有以下课程:

    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。

    【讨论】:

      【解决方案9】:

      假设以下类不是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 类型变量的对象不再保证是可变的。这可能会导致哈希、相等、并发等问题。

      【讨论】:

        【解决方案10】:

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

        另外,设计总是要付出代价的。 Every design that deserves the name means you have a conflict of goals.

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

        1. 这可以防止多少明显的错误?
        2. 这可以防止多少细微的错误?
        3. 这会使其他代码更复杂(= 更容易出错)的频率如何?
        4. 这会使测试更容易还是更难?
        5. 您项目中的开发人员有多优秀?他们需要多少大锤指导?

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

        另一方面,在任何地方都使用final 之后,创建一个类将非常困难,但如果您发现需要扩展它,则可以很容易地创建一个非final 类非final .

        如果你正确使用接口,你可以避免“我需要使这个可变”的问题,方法是始终使用接口,然后在需要时添加可变实现。

        结论:这个答案没有“最佳”解决方案。这取决于你愿意付出什么样的代价以及你必须付出什么样的代价。

        【讨论】:

          【解决方案11】:

          equals() 的默认含义与引用相等相同。对于不可变数据类型,这几乎总是错误的。所以你必须重写 equals() 方法,用你自己的实现替换它。 link

          【讨论】:

            猜你喜欢
            • 2011-01-29
            • 1970-01-01
            • 1970-01-01
            • 2015-12-27
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多