不变的阶级?


Answers:


135

什么是不可变的对象?

不变对象是在实例化后不会改变状态的对象。

如何使对象不变?

通常,可以通过定义一个不暴露任何成员且没有任何设置方法的类来创建不可变对象。

以下类将创建一个不可变的对象:

class ImmutableInt {
  private final int value;

  public ImmutableInt(int i) {
    value = i;
  }

  public int getValue() {
    return value;
  }
}

如上例所示, ImmutableInt只能在实例化对象时设置,并且通过仅使用getter(getValue),实例化后不能更改对象的状态。

但是,必须注意,该对象引用的所有对象也必须是不可变的,否则可能会更改该对象的状态。

例如,允许对数组的引用或ArrayList通过getter获取将允许通过更改数组或集合来更改内部状态:

class NotQuiteImmutableList<T> {
  private final List<T> list;

  public NotQuiteImmutableList(List<T> list) {
    // creates a new ArrayList and keeps a reference to it.
    this.list = new ArrayList(list); 
  }

  public List<T> getList() {
    return list;
  }
}

上面的代码的问题在于,ArrayList可以通过获取getList和操纵它们,从而导致对象本身的状态被改变,因此,它不是一成不变的。

// notQuiteImmutableList contains "a", "b", "c"
List<String> notQuiteImmutableList= new NotQuiteImmutableList(Arrays.asList("a", "b", "c"));

// now the list contains "a", "b", "c", "d" -- this list is mutable.
notQuiteImmutableList.getList().add("d");

解决此问题的一种方法是从getter调用时返回数组或集合的副本:

public List<T> getList() {
  // return a copy of the list so the internal state cannot be altered
  return new ArrayList(list);
}

不变性的优点是什么?

不变性的优点在于并发。在可变对象中很难保持正确性,因为多个线程可能试图更改同一对象的状态,从而导致某些线程看到同一对象的不同状态,具体取决于读取和写入对象的时间目的。

通过拥有一个不可变的对象,可以确保所有正在查看该对象的线程都将看到相同的状态,因为不可变的对象的状态不会改变。


5
线程安全(对于并发很重要)不是不变性的唯一优点;这也意味着您不必制作防御性的对象副本,并且可以防止错误,因为您不能错误地修改不应修改的对象。
杰斯珀

7
除了返回列表的副本,您还return Collections.unmodifiableList(list);可以返回列表上的只读视图。
Jesper

18
上课也必须做final。否则,可以使用setter方法(或其他类型的变异方法和可变字段)对其进行扩展。
Abhinav Sarkar 2010年

6
@AbhinavSarkarfinal如果类仅包含private字段,则不必如此,因为它们不能从子类访问。
icza 2014年

3
@icza类必须是最终类,因为我们有公共的getter方法,我们可以扩展该类并重写getter方法,然后以我们的方式更改该字段。.所以它不再是不变的
sunil

19

除了已经给出的答案之外,我还建议您阅读第二版《有效Java》中的不变性,因为有些细节很容易遗漏(例如防御性副本)。另外,Effective Java 2nd Ed。是每个Java开发人员必读的文章。


3
这是要查看的确切资源。提到的好处包括从较少的容易出错的代码到线程安全。
gpampara

6

您可以将这样的类设为不可变的:

public final class Immutable
{
    private final String name;

    public Immutable(String name) 
    {
        this.name = name;
    }

    public String getName() { return this.name; } 

    // No setter;
}

以下是使Java类不可变的要求:

  • 必须声明为final(这样就不能创建子类)
  • 该类中的成员必须声明为final(这样,在创建对象后我们就无法更改其值)
  • 为其中的所有变量编写Getter方法以获取Member
  • 没有二传手的方法

不可变的类很有用,因为
-它们是线程安全的。
-他们还表达了关于您的设计的深刻内容:“无法更改。”,适用时,正是您所需要的。


5

不变性主要可以通过两种方式实现:

  • 使用 final实例属性避免重新分配
  • 使用一个类接口,该接口根本不允许任何能够修改类内部内容的操作(仅使用getter而不使用setter)

不变性的优点是可以对这些对象进行以下假设:

  • 您将获得无副作用规则(在函数式编程语言上非常流行),并允许您更轻松地在并发环境中使用对象,因为您知道当它们被使用时,不能以原子或非原子的方式对其进行更改被许多线程使用
  • 语言的实现可以以不同的方式对待这些对象,将它们放置在用于静态数据的内存区域中,从而可以更快,更安全地使用这些对象(这是在JVM中用于字符串的情况)

不可变与相等的副作用相同。例如,一个不变的对象可能会产生副作用,例如记录到文件中。说使对象不可变也使它没有副作用是有点不准确的。
Grundlefleck 2010年

1
@Grundleflek,我认为这可能会割裂头发。如果该类将日志文件作为其合同的一部分进行修改并且该日志文件可供其他类访问,则该类不是不可变的。如果日志文件对其他类是隐藏的,而不是该类合同的一部分,则该类实际上是不可变的,并且实际上对于所有意图和目的都是无副作用的。Wikipedia副作用页面上的(未提供资源的)介绍内容为:“ ...如果表达式除了产生值之外,还修改了某些状态或与调用函数具有可观察的交互作用,则该表达式具有副作用。”
Jeff Axelrod

3

不变类在实例化后不能重新分配值。构造函数将值分配给其私有变量。在对象变为空之前,由于无法使用设置方法,因此无法更改值。

要一成不变,应满足以下条件,

  • 所有变量应为私有
  • 增幅器方法(设置器)。
  • 通过将类定为final(严格不变性)或方法final(每周不变性)来避免方法重写。
  • 如果它包含非原始或可变类,则进行深层克隆。

/**
* Strong immutability - by making class final
*/
public final class TestImmutablity {

// make the variables private
private String Name;

//assign value when the object created
public TestImmutablity(String name) {
this.Name = name;
}

//provide getters to access values
public String getName() {

return this.Name;
}
}

优点:不可变对象包含其初始化值,直到其死亡。

java-immutable-classes-short-note


2

不变的阶级是指那些对象在创建后无法更改的类。

不可变的类对于

  • 缓存目的
  • 并发环境(ThreadSafe)
  • 难以继承
  • 在任何环境下都无法更改价值

弦类

代码示例

public final class Student {
    private final String name;
    private final String rollNumber;

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

    public String getName() {
        return this.name;
    }

    public String getRollNumber() {
        return this.rollNumber;
    }
}

2

如何使Java类不可变?

从具有JEP 359的JDK 14+开始,我们可以使用“ records”。这是创建Immutable类的最简单,最轻松的方式。

记录类是一组固定的字段(称为记录)的浅层不变的透明载体,为记录components提供state描述。每个component都会产生一个final字段,其中包含提供的值和accessor检索该值方法。字段名称和访问者名称与组件名称匹配。

让我们考虑创建不可变矩形的示例

record Rectangle(double length, double width) {}

无需声明任何构造函数,无需实现equals&hashCode方法。只是任何记录都需要名称和状态描述。

var rectangle = new Rectangle(7.1, 8.9);
System.out.print(rectangle.length()); // prints 7.1

如果要在对象创建期间验证值,则必须显式声明构造函数。

public Rectangle {

    if (length <= 0.0) {
      throw new IllegalArgumentException();
    }
  }

记录的主体可以声明静态方法,静态字段,静态初始化器,构造函数,实例方法和嵌套类型。

实例方法

record Rectangle(double length, double width) {

  public double area() {
    return this.length * this.width;
  }
}

静态字段,方法

由于状态应该是组件的一部分,因此我们无法将实例字段添加到记录中。但是,我们可以添加静态字段和方法:

record Rectangle(double length, double width) {

  static double aStaticField;

  static void aStaticMethod() {
    System.out.println("Hello Static");
  }
}

不变性有什么需求,使用它有什么好处?

先前发布的答案足以证明对不变性的需求,这是有利的


0

制作不可变对象的另一种方法是使用Immutables.org库:

假定已添加必需的依赖项,请使用抽象访问器方法创建一个抽象类。您可以通过使用接口甚至注释(@interface)进行注释来做同样的事情:

package info.sample;

import java.util.List;
import java.util.Set;
import org.immutables.value.Value;

@Value.Immutable
public abstract class FoobarValue {
  public abstract int foo();
  public abstract String bar();
  public abstract List<Integer> buz();
  public abstract Set<Long> crux();
}

现在可以生成并使用生成的不可变实现:

package info.sample;

import java.util.List;

public class FoobarValueMain {
  public static void main(String... args) {
    FoobarValue value = ImmutableFoobarValue.builder()
        .foo(2)
        .bar("Bar")
        .addBuz(1, 3, 4)
        .build(); // FoobarValue{foo=2, bar=Bar, buz=[1, 3, 4], crux={}}

    int foo = value.foo(); // 2

    List<Integer> buz = value.buz(); // ImmutableList.of(1, 3, 4)
  }
}

0

不可变类只是其实例无法修改的类。

每个实例中包含的所有信息在对象的生存期内都是固定的,因此无法观察到任何变化。

不可变的类比可变的类更容易设计,实现和使用。

要使类不可变,请遵循以下五个规则:

  1. 不要提供修改对象状态的方法

  2. 确保该类不能扩展。

  3. 将所有字段定为最终值。

  4. 将所有字段设为私有。

  5. 确保以独占方式访问任何可变组件。

不可变的对象本质上是线程安全的。他们不需要同步。

不变的对象可以自由共享。

不可变的对象是其他对象的重要构建块


0

@Jack,在课堂上拥有最终的领域和二传手将不会使班级一成不变。final关键字仅确保永不重新分配变量。您需要在getter方法中返回所有字段的深层副本。这将确保从getter方法获取对象后,该对象的内部状态不会受到干扰。


-1

作为非英语母语者,我不喜欢“不可变类”的常见解释是“构造的类对象是不可变的”。相反,我本人倾向于将其解释为“类对象本身是不可变的”。

也就是说,“不变类”是一种不变的对象。区别在于回答好处是什么。据我所知/解释,不可变类阻止其对象进行运行时行为修改。


-1

这里的大多数答案都是好的,有的提到了规则,但是当我们需要遵循这些规则时,我用言语“为什么?所以在给下面的解释

  • 将成员变量声明为“ final” –当我们将它们声明为final时,编译器会强制我们对其进行初始化。我们可以直接通过默认构造函数,arg构造函数直接进行初始化(请参见下面的示例代码),并且初始化后我们无法对其进行修改,因为它们是最终的。
  • 当然,如果我们尝试将Setters用于这些最终变量,则编译器将引发错误。

    public class ImmutableClassExplored {
    
        public final int a; 
        public final int b;
    
        /* OR  
        Generally we declare all properties as private, but declaring them as public 
        will not cause any issues in our scenario if we make them final     
        public final int a = 109;
        public final int b = 189;
    
         */
        ImmutableClassExplored(){
            this. a = 111;
            this.b = 222;
        }
    
        ImmutableClassExplored(int a, int b){
            this.a = a;
            this.b= b;
        }
    }
    

我们需要将class声明为“ final”吗?

  • 如果在类声明中没有final关键字,则可以继承类。因此,子类可以覆盖getter方法。在这里,我们必须考虑两种情况:

1.只有原始成员:如果类只有原始成员,那么我们就没有问题了,那么我们不需要将class声明为final。

2.将对象作为成员变量:如果我们将对象作为成员变量,则必须使这些对象的成员也成为最终成员。意味着我们需要遍历树的深处并使所有对象/图元最终成为不可能的对象。因此,解决方法是使类为final,以防止继承。因此,没有子类重写getter方法的问题。


-1

Lombok的@Value注释可用于生成不可变的类。就像下面的代码一样简单。

@Value
public class LombokImmutable {
    int id;
    String name;
}

根据龙目岛网站上的文档:

@Value是@Data的不变变量;默认情况下,所有字段都设为私有和最终字段,并且不会生成设置器。默认情况下,该类本身也将最终确定为final,因为不可改变性不能强加于子类。像@Data一样,还会生成有用的toString(),equals()和hashCode()方法,每个字段都有一个getter方法,并且还会生成一个覆盖每个参数的构造函数(除了在字段声明中初始化的最终字段之外) 。

一个完整的工作示例可以在这里找到


在回答具有可接受的答案(寻找绿色✓)以及其他答案的旧问题之前,请确保您的答案添加了新的内容或对他们有所帮助。回答OP的问题时请多加注意,如何使Java类不可变,不可变性有什么需求,使用它有什么好处?。–您只给出部分答案,仅说明您可以使用第三方框架/库Lombok,这不一定是OP的问题。Java 15也退出了,为什么在record可以使用Java的情况下使用Lombok ?
伊沃森

我提供了实现这一目标的一种选择。并非每个人都在使用Java 15,今天的大多数应用程序都在早期版本上运行,所以您说为什么使用Lombok并没有多大意义。在我当前的项目中,我们最近迁移到Java 11并使用Lombok实现不可变的类。另外,我的答案是对已经存在的答案的补充。不可变的问题已经得到了回答,我想您希望我重写它以便完成。
Anubhav

很公平。我既不反对也不赞成投票。我试图指出其他两个人不赞成这个答案的原因。是否以及如何编辑答案取决于您。
伊沃森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.