如何在Java中使对象不可变?

15

随着这个话题如今变得热门,我不理解某些概念。如果听起来很傻,请原谅我,但当我尝试创建不可变对象时,我发现大多数文章都遵循以下几点:

  • 使类为final - 有道理
  • 不允许属性的mutators(setters)- 有道理
  • 将属性设为private - 有道理

现在我不明白为什么我们需要以下几点:

  • 使构造函数为私有并提供与构造函数相同属性的createInstance方法或工厂方法?这有什么帮助?
  • 将属性设为final - 文章中的某些作者未能解释这一点,有些地方我读到是为了避免意外修改。当没有mutators且类是final时,你如何意外地进行修改?使一个属性为final有什么帮助吗?
  • 除了工厂模式,我可以使用生成器模式吗?

我在此添加我的类和测试案例:

    public final class ImmutableUser {
    private final UUID id;
    private final String firstName;
    private final String lastName;

    public ImmutableUser(UUID id, String firstName, String lastName) {
        super();
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }
    /**
     * @return the id
     */
    public UUID getId() {
        return id;
    }
    /**
     * @return the firstName
     */
    public String getFirstName() {
        return firstName;
    }
    /**
     * @return the lastName
     */
    public String getLastName() {
        return lastName;
    }
}

测试用例

public class ImmutableUserTest {

        @Test(expected = IllegalAccessException.class)
        public void reflectionFailure() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
            ImmutableUser user = new ImmutableUser(UUID.randomUUID(), "john", "liu");
            Field i =user.getClass().getDeclaredField("firstName");
            i.setAccessible(true);
            i.set(user, "cassandra");
            System.out.println("user " + user.getFirstName()); // prints cassandra
        }

    }

这个测试用例失败并打印出cassandra。

如果我做错了什么,请让我知道。


2
这个问题更适合在http://programmers.stackexchange.com/上提问。 - vanza
4
似乎你混淆了可变性和单例模式。 - Shailesh Aswal
1
@vanza 请停止将Programmers.SE当作你的马桶 / http://meta.programmers.stackexchange.com/questions/6483/why-was-my-question-closed-or-down-voted/6490#6490 - gnat
使用静态方法:调用new <className>将分配(为)对象的新实例保留内存。有时这并非必要,甚至是不可取的。以包装类Boolean为例。它有一个公共构造函数...但只有两个可能的值,它们可以轻松地被缓存并从静态的valueOf(...)方法返回。Integer封装类缓存范围[ -128,127](取决于JVM)...这允许您减少使用的对象数量。 - Clockwork-Muse
1
可能是创建一个没有final字段的不可变对象?的重复问题。 - Raedwald
显示剩余6条评论
3个回答

10
  • 将构造函数设为私有并提供与构造函数或工厂方法相同属性的createInstance()方法,这样做有什么好处?

回答:仅仅将构造函数设为私有并提供createInstance()(工厂方法)是不够的:这只是让你在仍然掌控实例创建方式的同时允许用户使用该类和其实例的几个事情之一。

  • 将属性声明为final - 帖子未解释此点,在某些地方我看到了避免意外修改。当没有修改器且类为final时,如何进行意外修改?怎样将属性声明为final有所帮助?

回答:将一个类声明为final意味着用户无法扩展它,因此它会“阻止”用户进行这种“变通”。将属性声明为final不允许该类的用户更改它。它不可能被“意外修改”,但可以使用反射来进行“恶意修改”。我们来看一个例子,假设你有:

final public class SomeClass {
    final Integer i = 1;
}

你可以从另一个类中这样做:

class AnotherClass {

    public static void main (String[] args) throws Exception {

        SomeClass p = new SomeClass();
        Field i =p.getClass().getDeclaredField("i");
        i.setAccessible(true);
        i.set(p, 5);
        System.out.println("p.i = " + p.i); // prints 5
    }
}
  • 工厂能否使用建造者模式?

答案:你可以使用建造者模式或任何帮助你控制类实例创建的模式。

更多信息:
如果你想确保你的类是不可变的,请确保任何 getter 返回类成员的 深拷贝。这种技术称为“保护性/防御性拷贝”。你可以在 这里 阅读更多相关信息。


关于将类声明为final的观点很好。然而,如果所有字段都是“private”,子类无论如何也无法更改超类的状态。而且(如果相关的话),将类声明为final会防止我们在测试中模拟该类,并使用诸如CGLib或Javassist之类的库创建代理,这可能是一个严重的障碍。因此,在将类声明为“final”之前,我个人会三思而后行。 - AlexR
@AlexR 很好的观点;因此,建议针对接口进行设计,而不仅仅是类。 - morgano
@morgano,通常我们在创建实体类(例如我的示例中的Person)时不使用接口。尽管我同意您的观点,即包含业务逻辑的每个类都应该实现声明契约的接口。 - AlexR
@alfasin,我明白了。嗯,攻击者可以做任何事情。例如,他可以使用字节码工程技术(例如使用ASM)实现删除您的类中的“final”修饰符的代理。但是,你是对的:这需要更加认真的努力。 - AlexR
反射也可以使最终字段变为非最终。Field field = MyClass.class.getDeclaredField("val"); field.setAccessible(true); field.setInt(r, 2);将把“val”设置为2,尽管它是最终的。甚至string也可以被改变 - Richard Tingle
显示剩余7条评论

9

我会从使属性变为final开始。将属性设为final可以保证您无法更改属性值。我认为这很明显。(稍后我将写一篇关于更改不可变对象引用内容的附加评论。)

现在,当您的所有属性都是final时,它们必须通过构造函数进行初始化。然而,有些类有很多属性,因此构造函数变得非常庞大。此外,有时候某些属性可以初始化为默认值。尝试支持这一点会导致我们实现几个构造函数,其中几乎随意组合参数。但是,建造者模式可以帮助我们。但是如何让用户使用Builder而不是直接调用构造函数呢?答案是将构造函数设为private并创建返回builder的静态方法:

public class Person {
    private final String firstName;
    private final String lastName;
    private final Person mother;
    private final Person father;

    private Person(String firstName, String lastName, Person mother, Person father) {
        // init the fields....
    }

    public static PersonBuilder builder() {
        return new PersonBuilder();
    }


    public static class PersonBuilder {
        // here fields are NOT final 
        private String firstName;
        private String lastName;
        private Person mother;
        private Person father;

        public PersonBuilder bornBy(Person mother) {
            this.mother = mother;
             return this;
        }

        public PersonBuilder conceivedBy(Person father) {
             this.father = father;
             return this;
        }

        public PersonBuilder named(String firstName) {
             this.firstName = firstName;
             return this;
        }

        public PersonBuilder fromFamily(String lastName) {
             this.lastName = lastName;
             return this;
        }

        Person build() {
              return new Person(name, lastName, mother, father);
        } 
    }
}

以下是典型的使用模式:

Person adam = Person.builder().named("Adam").build(); // no mother, father, family
Person eve = Person.builder().named("Eve").build(); // no mother, father, family
Person cain = Person.builder().named("Cain").conerivedBy(adam).bornBy(eve); // this one has parents

正如您所看到的,建造者模式通常比工厂更好,因为它更加灵活。

我认为您在问题中遗漏了一个要点:对其他(可变)对象的引用。例如,如果我们将字段Collection<Person> children添加到我们的Person类中,我们必须确保getChildren()返回的是Iterable或至少是不可修改的集合。


2
你的构建方法不应该返回 this 吗?毕竟你正在那样使用它们 :) - dlev
@dlev,没错,你说得对。我会在我的答案中修复这个问题。 - AlexR

6
将构造函数设置为私有以及使用建造者模式对于不可变性并非必要。然而,因为您的类不能提供setter方法,如果它有许多字段,使用含有多个参数的构造函数可能会影响可读性,因此可以考虑使用建造者模式(需要一个有默认值的构造函数)。
然而,其他回答似乎忽略了一个重要的点。
使用final字段是必要的,不仅可以确保它们不被修改,还因为否则您将失去一些重要的线程安全保证。事实上,不可变性的一个方面就是它可以带来线程安全。如果您没有将字段设置为final,则您的类会变成“有效地不可变”。请参阅例如Must all properties of an immutable object be final?

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