【问题标题】:How to make object immutable in java如何在java中使对象不可变
【发布时间】:2014-08-24 01:52:07
【问题描述】:

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

  • 使类最终化 - 有意义
  • 不允许属性的修改器(设置器) - 有意义
  • 将属性设为私有 - 有意义

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

  • 将构造函数设为私有并提供与构造函数或工厂方法具有相同属性的 createInstance 方法?它有什么帮助?
  • Make attributes final - 帖子的帖子未能解释这一点以及我阅读的一些内容以避免意外修改。当没有 mutators 并且 class 是 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。

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

【问题讨论】:

标签: java immutability


【解决方案1】:

将构造函数设为私有并使用构建器模式对于不变性来说不是必需的。然而,因为你的类不能提供 setter 并且如果它有很多字段,使用带有许多参数的构造函数可能会损害可读性,因此使用构建器模式的想法(需要一个普遍的构造函数)。

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

使用 final 字段是必不可少的,不仅是为了确保它们不会被修改,而且因为否则你会失去一些重要的线程安全保证。事实上,不变性的一个方面是它为您带来线程安全。如果您不将字段设为 final,那么您的类将实际上是不可变的。参见例如Must all properties of an immutable object be final?

【讨论】:

    【解决方案2】:
    • 将构造函数设为私有并提供与构造函数或工厂方法具有相同属性的 createInstance 方法?它有什么帮助?

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

    • Make attributes final - 帖子未能解释这一点,我在某处阅读以避免意外修改。当没有 mutators 并且 class 是 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 返回类成员的deep-copy。这种技术称为“保护性/防御性复制”。你可以阅读更多关于它here

    【讨论】:

    • 关于使课程最终确定的好点。但是,如果所有字段都是private,则子类无论如何都无法更改超类的状态。并且(如果相关的话)将类设为 final 可以防止我们模拟该类以进行测试,并防止我们使用 CGLib 或 Javassist 之类的库创建代理,这可能是一个严重的障碍。所以,我个人在上课之前会三思而后行final。
    • @AlexR 好点;为什么建议针对接口而不是类进行设计
    • @morgano,通常我们在创建实体类时不使用接口(例如我的示例中的Person)。虽然我同意你的观点,每个包含业务逻辑的类都应该实现声明合约的接口。
    • @AlexR “但是,如果所有字段都是私有子类,则无论如何都无法更改超类的状态。” - 真的!但是......恶意攻击者可能会继承您的类并覆盖私有成员的“getter”,以便提供他自己的“熟”替代成员,这可能是有害的(Joshua Bloch 在类Date 中展示了这样一个示例)他的书《有效的 Java》)。当然,通过将一个类声明为final,你会限制自己使用测试等——这是权衡的一部分。
    • @alfasin,我明白了。好吧,攻击者可以做任何事情。例如,他可以使用字节码工程技术(例如使用 ASM)实现从您的类中删除 final 修饰符的代理。但是,您是对的:这需要更加认真的努力。
    【解决方案3】:

    我将从创建属性final 开始。创建属性final 保证您不能更改属性值。我认为这是显而易见的。 (稍后我会写额外的评论来改变引用不可变对象的内容)。

    现在,当您的所有属性都是 final 时,它们必须通过构造函数启动。但是有些类有很多属性,所以构造函数变得很大。此外,有时某些属性可以初始化为默认值。尝试支持这一点会导致我们使用几乎随机的参数组合来实现几个构造函数。但是 Builder 模式可以帮助我们。但是如何让用户使用 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 或至少不可修改的集合。

    【讨论】:

    • 您的构建器方法不应该返回this吗?你肯定是这样使用它们的:)
    • @dlev,当然,你是对的。我会在我的回答中解决这个问题
    猜你喜欢
    • 2014-09-03
    • 1970-01-01
    • 2021-05-04
    • 1970-01-01
    • 2010-09-17
    • 1970-01-01
    • 2015-09-22
    • 1970-01-01
    • 2021-02-23
    相关资源
    最近更新 更多