【问题标题】:Java immutable classes? [duplicate]Java 不可变类? [复制]
【发布时间】:2012-01-04 22:19:46
【问题描述】:

我找到了一个article,上面有一段有趣的代码:

public class Employee {

    private String firstName;
    private String lastName;

    //private default constructor
    private Employee(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public static Employee valueOf (String firstName, String lastName) {
        return new Employee(firstName, lastName);
    }
}

我真的很想了解创建这种类的优势。 我知道这里这个类的一个对象是不可变的,因为一旦初始化就没有办法改变它的变量值。我以前从来没有做过这样的事情,我真的不明白它的好处。

  • 为什么这是一个好的做法?
  • 您能说出一种可以使用这种方法的情况吗?
  • 常量或只读变量呢?是不是很相似?
  • 在文章中说,这对应用程序的性能不利。 但是为什么

【问题讨论】:

  • 可变状态使得我们很难推断代码的作用。研究函数式编程。
  • 这个类没有访问器方法。这是故意的吗?
  • 有用的文章如果你还没有读过:javapractices.com/topic/TopicAction.do?Id=29
  • 只是一个旁注:为了让这个类是不可变的,这个类必须是最终的;字段也必须是最终的。
  • 那篇文章太糟糕了!不要听从它的建议,它所做的许多陈述都是完全错误的。例如,“引用 1”是错误的:在现代 JVM 中,对象实例化非常快。 “引用 2”指出将实例字段公开是不好的,然后展示了同样糟糕的“最佳方法”:从 getter 中泄漏可变对象(工作日数组)。啊。我认为那篇文章中的每一点都至少有一个问题。

标签: java


【解决方案1】:

不可变类是:

  • 默认线程安全(不会发生并发写入)
  • 可缓存

您可以在Effective Java 中以 Java 语言为背景阅读很多关于它们的信息。

【讨论】:

  • equality check can be done with == 不正确
  • 仅仅因为一个类是不可变的并不意味着可以用“==”检查相等性。例如,字符串是不可变的,并且在许多情况下,“==”不起作用,而 .equals() 会起作用。为了使其工作,它需要与一个扩展工厂相结合,以确保“相等”值的实例不超过一个。
  • 好的,我删除了它,但是如果我对类有完全的实例控制,我可以将它们与 == 进行比较,我的意思是 ziesemer 写的。
  • 并发写入如何发生?该示例没有提供在对象创建后写回对象的任何方法。
【解决方案2】:

如果您使用的是 hashTables,拥有不可变对象是很好的,因为当对象的状态发生变化时您不需要重新计算 hashCode(因为它们是不可更改的)。

【讨论】:

    【解决方案3】:

    不可变类的主要优点是线程安全。线程的大多数问题都来自于共享的、可变的状态。通过使对象不可变,推理它们要容易得多,尤其是在多线程环境中。

    文章称“创建不可变对象会影响应用的性能”。我不确定它为什么这么说。这是完全错误的。不可变对象本身并不会影响应用程序的性能。

    【讨论】:

    • 什么都没有?创建一个新对象并复制其整个状态与更新单个字段一样快?
    【解决方案4】:

    你提到的例子是Immutable Objects。它在programming languages 中广泛使用的概念。

    引用上面的链接。优点是

    • 易于构建、测试和使用
    • 自动线程安全且没有同步问题
    • 不需要复制构造函数
    • 不需要克隆的实现
    • 允许 hashCode 使用延迟初始化,并缓存其返回值
    • 作为场地使用时不需要防守复制
    • 制作好的 Map 键和 Set 元素(这些对象在集合中不得更改状态)
    • 它们的类不变量在构造时建立一次,并且永远不需要再次检查
    • 始终具有“故障原子性”(Joshua Bloch 使用的术语):如果不可变对象 - 抛出异常,则它永远不会处于不良或不确定状态

    【讨论】:

    • 很好地列出了优点。我只想补充一点,员工类通常不是以不可变的方式实现的,因为那时不需要上述任何一点,但是共享可变状态可以更容易地保持员工的所有视图一致,即对 Employee 的任何修改都会每个人都可以立即看到它。如果我们必须创建一个新对象来表示更改的状态,旧的引用仍然会看到旧的状态。有时这是需要的,有时不是,并且更新所有引用以指向新对象是低效且麻烦的。
    • 我不确定“允许 hashCode 使用延迟初始化,并缓存其返回值”。您的意思是在第一次调用其hashCode() 方法时计算对象的哈希码并将其存储在字段中,然后返回该字段?因为在那种情况下,对象实际上是不是不可变的,并且它失去了“自动线程安全”的属性。多个线程不能安全地同时调用hashCode(),除非它们可以确定该字段之前已经初始化(或者除非您使用synchronized/volatile/whatnot)。
    • @Ruakh:是的。我相信即使在这种情况下也有办法使其多线程安全 - 例如,如果您可以原子地确定尚未计算哈希码,然后在初始化哈希码的同时计算并返回正确的值,那么您知道,对于本地执行线程,正确的值始终是返回的值。您可能会计算两次该值,但我认为您仍将始终获得正确的值。我希望我手头有“Java 并发实践”,这样我就可以查看详细信息(并确保我是对的)。
    • @jprete:当然有办法,但它们需要明确的行动;它与真正不可变对象所提供的“自动[]线程安全[ty]”完全不同。
    • @jprete:没有办法原子地确定哈希码没有被计算,如果没有就计算它。正如 ruakh 所暗示的,如果对象首先是真正不可变的,那么没有理由不能在构造函数中计算哈希码。
    【解决方案5】:

    -为什么这是一个好习惯?

    因为您可以传递类并确保它永远不会被“流氓”代码修改。 Java 字符串也是如此,它们是不可变的。

    -您能说出一种可以使用这种方法的情况吗?

    在多个团队一起工作的大型项目中,或者在设计框架或 API 时非常有用。在这些情况下,由于您不对代码的某些部分负责,因此您永远不能相信传递给代码其他部分的对象不会被更改。如果您需要确保对象不会被修改,请使用不变性。

    -常量或只读变量呢?是不是很相似?

    在 Java 中没有,因为我们既没有 const 也没有只读的。我们所拥有的只是 final 关键字,它确保对象引用在第一次赋值之后不会被修改。但是即使引用不能,底层对象仍然可以修改。不可变类确保对象状态在创建后不会改变。

    -在文章中说,这对应用程序的性能不利。但为什么呢?

    因为每次需要修改对象时,都需要创建新的实例。 Strings 也一样,你不能做myString.append("42"),你需要做myString = myString+"42",这会创建一个新的String对象。

    【讨论】:

    • 我几乎赞成,除了关于性能的 cmets,因为他们错了。是的,使用不可变对象意味着您要分配更多的对象,但在现代 JVM 实现中,这应该是闪电般的速度。
    • 是的,分配并不昂贵,但是 new 运算符不仅分配一个对象,它还调用构造函数。在上面的示例中,字符串连接涉及复制整个字符串的 content,这对于大型或频繁复制的字符串来说会变得很昂贵。
    • 分配不可变对象的新副本的成本部分(或完全!)通过您不再需要制作防御性副本这一事实来弥补。我的经验是值对象的防御性复制比修改频繁得多,所以如果对象是不可变的,那么实际上复制的次数会更少。
    • 对于值对象我完全同意——但Employee 不是值对象。
    【解决方案6】:

    文章说:

    要使类不可变,您可以将其所有构造函数定义为私有,然后创建一个公共静态方法来初始化和对象并返回它。

    其实这是错误的。这两个概念并不真正相关。

    例如您可以将 Employee 类的构造函数声明为 public,它仍然是不可变的。

    或者你可以将一个可变对象作为参数传递给工厂方法或声明一个修改器方法

    -> Employee 将是可变的,尽管您使用的是工厂方法和私有构造函数。

    【讨论】:

      【解决方案7】:

      在您给定的示例中,他将构造函数设为私有,从而直接从外部控制对象创建。

      意思:由于构造函数是私有的,所以不能做

      Employee e = new Employee("steve","jobs"); 
      

      来自这个类之外。

      通过这样做,这个类的程序员可以控制这个类的对象创建。

      当您编写非常庞大的服务器端类时,这是非常有益的,因为它的大小,创建一个对象可能会占用大量内存。现在你如何保护你的客户,而不是为你的类创建更多的对象?

      上述问题的答案很简单,只需将您的构造函数设为私有,然后您自己就可以在任何时候使用静态方法为您的类创建对象。 注意:静态方法可以通过类名直接访问。

      注意:这种设计模式将在单例设计模式中大量使用,对于给定的类只需要一个对象。

      【讨论】:

        猜你喜欢
        • 2017-11-23
        • 1970-01-01
        • 2013-05-08
        • 2015-03-30
        • 2018-10-19
        • 1970-01-01
        • 2019-05-30
        • 1970-01-01
        • 2012-08-13
        相关资源
        最近更新 更多