为什么我不应该使用不可变的POJO而是JavaBeans?

80
我已经实现了几个Java应用程序,目前只是桌面应用程序。在应用程序中,我喜欢使用不可变对象传递数据,而不是使用具有mutators(setter和getter)的对象,也称为JavaBeans。
但在Java世界中,使用JavaBeans似乎更为常见,我不明白为什么我应该使用它们。就个人而言,如果代码只涉及不可变对象而不是一直改变状态,那么它看起来更好。
在《Effective Java 2ed》中的第15项“最小化可变性”中也推荐使用不可变对象。
如果我将一个名为Person的对象实现为JavaBean,它将如下所示:
public class Person {
    private String name;
    private Place birthPlace;

    public Person() {}

    public setName(String name) {
        this.name = name;
    }

    public setBirthPlace(Place birthPlace) {
        this.birthPlace = birthPlace;
    }

    public String getName() {
        return name;
    }

    public Place getBirthPlace() {
        return birthPlace;
    }
}

同样的 Person 实现为一个不可变对象:

public class Person {
    private final String name;
    private final Place birthPlace;

    public Person(String name, Place birthPlace) {
        this.name = name;
        this.birthPlace = birthPlace;
    }

    public String getName() {
        return name;
    }

    public Place getBirthPlace() {
        return birthPlace;
    }
}

或者更接近C语言中的struct结构体:

public class Person {
    public final String name;
    public final Place birthPlace;

    public Person(String name, Place birthPlace) {
        this.name = name;
        this.birthPlace = birthPlace;
    }
}

我也可以在不可变对象中使用getter方法来隐藏实现细节。但是因为我只将其用作结构体,所以我更喜欢跳过"getter"并保持简单。

简单地说,我不明白为什么使用JavaBeans更好,或者我是否可以并且应该继续使用我的不可变POJO?

许多Java库似乎更支持JavaBeans,但也许随着时间的推移,对不可变POJO的支持会变得更加流行?


1
我认为你的意思是“项目15:最小化可变性”,而不是“最小化不可变性”。 - matt b
@matt:谢谢 =)我现在已经更新了。 - Jonas
7
你的不可变性方法很棒,可以避免许多线程问题。+1 - whiskeysierra
1
如果使用JPA,您需要一个默认构造函数。 - Neil McGuigan
7个回答

79

当你需要时,优先选择JavaBeans

  • 您需要与期望使用JavaBeans的环境进行交互
  • 您有许多属性,对于这些属性,完全在实例化时进行初始化非常不方便
  • 您有某些原因昂贵或无法复制但需要变异的状态
  • 您认为在某些时候可能必须更改属性访问的方式(例如,从存储到计算值,访问授权等)
  • 您想符合编码标准,而这些标准盲目地坚持使用JavaBeans会更“面向对象”

当你需要时,优先选择不可变POJOs

  • 您有一些简单属性
  • 您不需要与假定JavaBean约定的环境进行交互
  • 当克隆对象时轻松(或至少是可能)复制状态
  • 您根本不打算克隆该对象
  • 您相当确定您永远不必修改上述属性的访问方式
  • 您不介意听到关于代码不足够“面向对象”的抱怨(或嘲笑声)

12
构造函数可以进行边界检查和输入验证,如果输入无效或超出范围,我不希望创建对象。 - Jonas
25
你在豆子中的第二个观点并不支持豆子,而是支持构建器模式。你仍然可以构造一个不可变对象。 - user23743
5
你没有提到使用不可变的POJO时线程安全性的问题,这是使用它们的最大原因之一。另一个重要因素是简单性——当一个对象不发生改变时,长期来看更容易处理。此外,使用不可变的POJOs,你无需担心通常需要进行克隆操作,只需传递即可。 - deterb
你根本不打算克隆对象,为什么?也许我想要一个基于现有实体的新实体。这样做有什么问题吗?复制构造函数呢? - Silviu Burcea
1
正如其他人所评论的那样,支持可变性的许多观点可以使用构建器模式来解决,并且最近许多框架已经增强以对不可变对象进行一定程度的支持。在我看来,对于大多数问题域对象而言,不可变对象是客观上更好的选择。采纳的障碍是Java语言本身。构建器模式对于大多数开发人员而言需要过多的维护开销。使用“lombok项目”可以在一定程度上缓解这种情况,但是基于模式匹配的全面结构化和析构化语言更好。 - dsmith
显示剩余3条评论

40

在这次讨论中,我惊讶于线程这个词没有出现过。

不可变类的主要优势之一是由于没有可变的共享状态,它们本质上更加线程安全。

这不仅使您的编码更加简单,还会带来两个性能方面的好处:

  • 需要较少的同步。

  • 更多使用final变量的机会,这可以促进后续的编译器优化。

我真的很想朝着不可变对象而非JavaBean风格的类的方向发展。通过getter和setter公开对象的内部可能不应该是默认选择。


19

这取决于你想要做什么。如果你正在使用持久层,并将一些行从数据库中提取到POJO中,并且想要更改属性并保存回去,则使用JavaBean风格会更好,特别是如果你有很多属性。

考虑到你的人拥有许多字段,如名字、中间名、姓氏、出生日期、家庭成员、教育、工作、薪水等。

而这个人碰巧是一个女性,她刚结婚并同意更改了姓氏,你需要更新数据库。

如果您使用不可变的POJO,您会提取一个表示她的Person对象,然后创建一个新的Person对象,将所有未更改的属性保持不变,并添加新姓氏,然后保存它。

如果使用Java Bean,则只需调用setLastName()方法并保存即可。

“最小化可变性”而不是“永远不使用可变对象”。有些情况下使用可变对象效果更好,真正取决于您决定可否使对象可变更适合您的程序。您不应总是说“必须使用不可变对象”,而应看看在开始伤害自己之前可以制作多少不可变类。


2
但是你的例子几乎从不是“真实”应用程序中需要完成的任务。通常,您需要将更新后的数据对象传递给验证框架(Hibernate Validator、Spring Validator等),在该框架中,该对象将受到各种操作的影响,这些操作可能由不同的开发人员编写。如果对象是可变的,则无法确定是否正在使用传递的相同数据(如果在您之前执行了某些验证,则会直接更改对象)。这使得验证过程不太确定,更依赖于严格的顺序,并且无法并行化。 - dsmith
1
此外,“真正的”应用程序可能会有某种内存或分布式缓存(EHCache等),其中包含您的域对象。如果您知道您的对象是不可变的,则无需承担复制读取的开销。您可以安全地检索和使用内存中的对象,而不必担心缓存完整性。在我看来,对于这种业务域对象,您总是希望具有不可变的对象。可变对象适用于本地范围和其他一些专业情况,这里我没有足够的字符来介绍。 - dsmith

8

总结其他回答,我认为:

  • 不可变性有助于正确性(可以通过引用传递结构体,你知道没有任何东西会被错误/恶意客户端破坏)和代码简化
  • 可变性有助于同质性:Spring和其他框架创建一个没有参数的对象,设置对象属性,voi là。同时,使用相同的类来提供数据并保存修改,使接口更容易(你不需要get(id): Clientsave(MutableClient),MutableClient是Client的某个后代)。

如果存在中间点(创建、设置属性、使不可变),可能会更加鼓励使用不可变方法的框架。

无论如何,我建议将不可变对象视为“只读Java Bean”,强调如果你是个好孩子,不要触碰那个危险的setProperty方法,一切都会很好。


我认为最好的方式是让库和框架采用不可变方法。不可变的优势是内在的,而可变的优势是偶然的。 - dsmith

3

从Java 7开始,你可以使用不可变的bean来获得最佳的效果。在构造函数上使用@ConstructorProperties注释。

public class Person {
    private final String name;
    private final Place birthPlace;

    @ConstructorProperties({"name", "birthPlace"})
    public Person(String name, Place birthPlace) {
        this.name = name;
        this.birthPlace = birthPlace;
    }

    public String getName() {
        return name;
    }

    public Place getBirthPlace() {
        return birthPlace;
    }
}


1

说实话,我不认为不可变对象会变得非常流行。

我看到了它的优点,但像Hibernate和Spring这样的框架目前非常时髦(也是有很好的原因的),并且它们与bean一起使用效果最佳。

所以我不认为不可变性是坏的,但它肯定会限制您与当前框架的集成选项。

编辑 评论促使我稍微澄清一下我的答案。

在某些问题领域,不可变性非常有用,而且确实被使用。但我认为当前的默认值似乎是可变的,因为这是大多数人预期的,只有在具有明显优势时才是不可变的。

虽然在Spring中确实可以使用带参数的构造函数,但它似乎旨在将传统和/或第三方代码与漂亮的全新Spring代码一起使用。至少这是我从文档中了解到的。


2
IoC/DI的思想基于设置/注入状态,这就是为什么Spring中不太流行不可变性。然而,Joda-Time是不可变性处理正确的一个很好的例子。 - lunohodov
你也可以使用构造函数参数在Spring中注入依赖项,只是似乎没有多少人这样做(可能是因为当你有很多依赖项时,有一个10个以上参数的构造函数让人感到不安)。 - Andrei Fierbinteanu
6
我已经使用了Google Guice来进行依赖注入/控制反转,并且在构造函数中注入依赖没有任何问题。 - Jonas
通常情况下,如果使用对可变对象的不可变引用或对不可变对象的可变引用来存储数据,那么在处理事物时(特别是在多线程环境中),会更容易。拥有自由可变的可变对象引用会使事情变得更加复杂。请注意,如果 XY 引用同一个对象,并且想要更改 X.something,则可以保留 X 的 标识 并更改 Y.something,或者保留 Y.something 不变并更改 X 的 标识,但无法在更改 X 的状态的同时保留 X 的标识和 Y 的状态。 - supercat
有些人推崇使用不可变对象,但有时却未能认识到对象标识的重要性,以及可变对象可以以不可变对象无法做到的方式具有不可变标识。 - supercat

0

在Java编程中,“不可变”指的是一旦创建后,就不应该有状态的改变,无论是期望的还是意外的!

这种技术在防御性编程中非常有用,因为另一个实体不能改变其状态。

不希望发生更改的示例:外部系统(单线程或多线程)从您的层获取引用并对对象进行操作,可能会故意或无意地更改它。现在它可以是POJO、集合或对象引用,而且您不希望其中的内容更改或者您想要保护数据。您肯定会将对象设置为不可变作为防御性技术。

期望更改的示例:不需要不可变性,因为它会妨碍正确的编程程序。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接