如何在java中使对象不可变

时间:2014-07-03 04:57:02

标签: java immutability

由于这是当前的一个热门话题,我无法理解某些概念。请原谅我,如果我听起来很愚蠢,但当我尝试创建不可变对象的大多数帖子时,我发现了以下几点

  • 让课程结束 - 有意义
  • 不允许使用mutators(setter)作为属性 - 有意义
  • 将属性设为私有 - 有意义

现在我无法理解为什么我们需要以下几点

  • 将构造函数设为私有,并提供与构造函数或工厂方法具有相同属性的createInstance方法?它有什么用?
  • 使属性最终 - 帖子的帖子无法解释这一点以及我读到的一些内容以避免意外修改。如果没有变异器并且课程是最终的,你怎么能意外修改?如何让属性最终得到帮助?
  • 我可以使用构建器模式而不是工厂模式吗?

我在这里添加我的课程和测试用例:

    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。

如果我做错了,请告诉我。

3 个答案:

答案 0 :(得分:9)

我从制作属性final开始。使属性final保证您无法更改属性值。我认为这是显而易见的。 (我将在以后更改引用不可变对象的内容时写下额外的注释)。

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

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或至少不可修改的集合。

答案 1 :(得分:9)

  • 将构造函数设为私有,并提供与构造函数或工厂方法具有相同属性的createInstance方法?它有什么用?

回答:将构造函数设为私有并提供createInstance()(工厂方法)本身并没有帮助:这是为了让用户真正使用它应该做的一件事情当您仍然可以控制实例的创建方式时,类及其实例。

  • 让属性最终 - 帖子无法解释这一点以及我读到的某个地方,以避免意外修改。如果没有变异器并且课程是最终的,你怎么能意外修改?如何让属性最终得到帮助?

回答:将某个类声明为final意味着用户无法对其进行扩展,因此它会阻止&#34;阻止&#34;用户来自这种&#34;解决方法&#34;。将属性声明为final不允许该类的用户更改它。它不能被意外修改&#34;,但它可能被恶意修改&#34;使用反射。让我们看一个例子,说你有:

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都返回班级成员的 deep-copy 。这种技术被称为&#34;保护/防御性副本&#34;。您可以阅读更多相关信息 here

答案 2 :(得分:6)

使构造函数变为私有并使用构建器模式对于不可变性不是必需的。但是因为你的类不能提供setter,如果它有很多字段,使用带有许多参数的构造函数可能对可读性有害,因此想要使用构建器模式(需要一个pervade构造函数)。

其他答案似乎错过了重要的一点。

使用最终字段至关重要,不仅要确保它们不被修改,还要因为否则会丢失一些重要的线程安全保证。实际上,不变性的一个方面是它为您带来了线程安全性。如果您没有将字段设为最终字段,那么您的课程将变为有效不可变。请参阅示例Must all properties of an immutable object be final?