如何在java中使对象不可变

2022-09-04 01:07:29

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

  • 使课程最终 - 有意义
  • 不允许属性的赋值器(setters) - 有意义
  • 使属性私有 - 有意义

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

  • 将构造函数设为私有,并提供与构造函数或工厂方法具有相同属性的 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。

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


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

:将构造函数设为私有并提供(工厂方法)本身并没有帮助:这是您应该做的少数几件事之一,以便允许用户在您仍然控制实例创建方式的同时实际使用类及其实例。createInstance()

  • 使属性最终确定 - 帖子未能解释这一点,并且在我读到的某个地方避免了意外的修改。当没有突变体并且类是最终的时,如何意外修改?使属性最终确定如何有所帮助?

:将类声明为意味着用户无法扩展它,因此它“阻止”用户进行这种“解决方法”。将属性声明为 将不允许类的用户对其进行更改。它不能被“意外地修改”,但它可以使用反射“恶毒地修改”。让我们看一个例子,假设你有:finalfinal

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
    }
}
  • 可以代替工厂使用生成器模式吗?

:您可以使用生成器模式或任何帮助您控制类实例创建的模式。

进一步:
如果要确保类是不可变的,请确保 any 返回类成员的深层副本。这种技术被称为“保护/防御性复制”。您可以在此处阅读更多相关信息getter


答案 2

我会从制作属性开始。创建属性可保证您无法更改属性值。我认为这是显而易见的。(稍后我将编写额外的注释来更改引用不可变对象的内容)。finalfinal

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

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> childrenPersongetChildren()Iterable