【问题标题】:Java, Making a class ImmutableJava,使类不可变
【发布时间】:2018-10-19 19:17:43
【问题描述】:

我在网上遇到了这个练习,我有两个类,我应该使 Tutor 类不可变。但是,我唯一能想到的是将final 添加到name 字段。对于构造函数,我认为我不需要更改 name 变量的初始化,因为 String 是不可变的。我不确定如何处理集合以及如何使构造函数的这一部分不可变。 根据练习,我不应该更改 Student 类(我可以看到它是可变的)

public class Student {    
    private String name;
    private String course;

    public Student(String name, String course) {    
        this.name = name;
        this.course = course;    
    }

    public String getName() {
        return name;
    }

    public String getCourse() {
        return course;
    }

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

    public void setCourse(String course) {
        this.course = course;
    }
}

public final class Tutor {
    private String name;
    private final Set<Student> tutees;

    public Tutor(String name, Student[] students) {
        this.name = name;
        tutees = new HashSet<Student>();
        for (int i = 0; i < students.length; i++)
            tutees.add(students[i]);
    }

    public Set<Student> getTutees() {
        return Collections.unmodifiableSet(tutees);
    }

    public String getName() {
        return name;
    }
}

【问题讨论】:

  • 可能重复? link
  • 导师名称仍然可以在导师类本身内更改,因此可能不被认为是不可变的。也许也可以尝试将字符串名称设为final。顺便说一句,您应该知道在类定义上设置final 意味着该类不能是extended。虽然不会使类不可变

标签: java immutability


【解决方案1】:

要使 Tutor 类不可变,您应该在 Tutor 内的所有字段上使用“final”修饰符,而不是在 Tutor 的类定义上。

【讨论】:

    【解决方案2】:

    使对象不可变的正确方法是:

    1. 声明对象最终
    2. 不提供 setter 方法
    3. 将所有字段设为私有
    4. 使可变字段最终化
    5. 在构造函数中使用深拷贝
    6. 在 getter 方法中克隆对象,因此您不会返回实际引用。

    【讨论】:

    • 在 getter 方法中克隆对象,因此您不会返回实际引用,如果它们已经是不可变的,则不值得返回副本。
    • 我知道Immutability的基本规则,我不知道如何在构造函数中制作tutees的防御副本
    • Greg 是对的,因为像final type name = initvalue; 这样没有定义的任何东西都可以在运行时通过反射进行更改。 final + initialisation 可以作为常量引入代码中,从而在代码中作为常量出现。所以即使你用反射改变了它,代码也不会对它做出反应。但这种行为也是 javac 和 JVM 特定的......
    【解决方案3】:

    您真的需要归还学生集吗?如果你真的需要,你可以通过使用只提供 getter 的接口来隐藏它,比如

    interface IStudent {
        public String getName();
        public String getCourse(); 
    }
    
    class Student : implements IStudent { ...} 
    

    在你的导师中你返回Set&lt;IStudent&gt;

    【讨论】:

      【解决方案4】:

      Tutor 类展示了许多促进其不变性的方面:

      • 类是final的
      • Set&lt;Student&gt; 受到保护,不会被修改
      • 没有方法可以直接改变类的状态

      但是,构造函数的防御性副本并不完整。
      它还必须复制传递的数组的Students 元素。否则,构造函数的客户端可能会更改它们的任何实例并使Tutor 实例可变,例如:

      Student[] students = ...;
      Tutor tutor = new Tutor(name, students);
      students[0].setName("new Name!"); // break the immutability of Tutor
      

      你应该这样写:

      public Tutor(String name, Student[] students){
        this.name = name;
        tutees = new HashSet<Student>();
        for (Student student : students){   
            Student copy = new Student(student.getName(), 
                                          student.getCourse());
            tutees.add(copy);
         }     
      }
      

      另外请注意,getTutees() 返回的 Set 是不可修改的,但其中包含的元素就像 Student 一样是可变的。 因此,要使 Tutor 不可变,您还必须在返回 getTutees() 时创建 Student 元素的副本,例如:

      public Set<Student> getTutees(){
         Set<Student> students = new HashSet<>();
         for (Student student : tutees){
            Student copy = new Student(student.getName(), 
                                          student.getCourse());
            students.add(copy);
         }     
         return Collections.unmodifiableSet(students);
      }
      

      您可能会注意到,在这些条件下获得不可变性(我们希望不可变但包含引用可变实例的集合的实例)需要编写更多代码(读取/维护/测试)并执行更多处理(执行起来很慢)。
      如果Student 是一个不可变类,那么原来的getTutees() 和原来的构造函数就足够了。

      【讨论】:

      • 我会更进一步,将它们全部存储在变量Set&lt;Student&gt; studentSetParam中,然后像tutees = Collections.unmodifiableSet(studentsSetParam)一样初始化属性。
      • 这是我的第一个想法,但它仍然没有真正使 Tutor 的完整状态不可变,它只是防止更改集合和传入的原始学生对象。我仍然可以调用 @ 987654337@ 并修改任何这些复制的实例。 (不过,这可能是作业的初衷。)
      • @Luiggi Mendoza 确实有可能。
      • 非常感谢您的努力!但是,在构造函数和 getTutees 方法中,我都不能写 Student student = new Student();因为 Student 对象需要两个参数
      • @davidxxx 再次感谢,只是为了确保 - 我们在构造函数和方法中使用 getName() 和 getCourse() 的地方 - 它应该声明 student.getName() 不是学生? :)
      【解决方案5】:

      Java SE 16

      您可以使用作为 Java SE 16 的一部分引入的 JEP 395: Records 功能来创建不可变类,而无需太多仪式。

      如果你已经浏览过上面的链接,那么你一定已经想通了,你可以这样做

      record Tutor(String name, Set<Student> tutees) { }
      

      你得到的是:

      1. final class Tutor
      2. 签名与标头Tutor(String name, Set&lt;Student&gt; tutees) 相同的规范构造函数。
      3. private final 字段、nametutees 以及它们对应的具有相同名称和返回类型的 public 访问器方法。
      4. 自动创建equalshashCodetoString方法。

      演示:

      Student.java

      record Student(String name, String course) { }
      

      Tutor.java

      import java.util.Set;
      
      record Tutor(String name, Set<Student> tutees) { }
      

      Main.java

      import java.util.Set;
      
      class Main {
          public static void main(String[] args) {
              Set<Student> cscStudents = Set.of(
                                                  new Student("Harry", "Java-8"),
                                                  new Student("Tina", "Java-9"),
                                                  new Student("Andy", "Java-11")
                                              );
      
              Set<Student> scienceStudents = Set.of(
                                                  new Student("Tony", "Phy"),
                                                  new Student("Kerry", "Chem"),
                                                  new Student("John", "Bio")
                                              );
      
              Tutor t1 = new Tutor("Mark", cscStudents);
              Tutor t2 = new Tutor("Robin", scienceStudents);
              Tutor t3 = new Tutor("Mark", Set.of(
                                                  new Student("Andy", "Java-11"),
                                                  new Student("Harry", "Java-8"),
                                                  new Student("Tina", "Java-9")
                                              )
                                  );
      
              System.out.println(t1);
              System.out.println();
      
              System.out.println(t1.tutees());
              System.out.println();
      
              System.out.println("Students of " + t1.name() + ":");
              t1.tutees()
                  .stream()
                  .forEach( t -> System.out.println(t.name()) );
              System.out.println();
      
              System.out.println(t1.equals(t2));
              System.out.println(t1.equals(t3));
          }
      }
      

      输出:

      Tutor[name=Mark, tutees=[Student[name=Andy, course=Java-11], Student[name=Harry, course=Java-8], Student[name=Tina, course=Java-9]]]
      
      [Student[name=Andy, course=Java-11], Student[name=Harry, course=Java-8], Student[name=Tina, course=Java-9]]
      
      Students of Mark:
      Andy
      Harry
      Tina
      
      false
      true
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-11-23
        • 2015-10-29
        相关资源
        最近更新 更多