你不知道怎么在Java中创建不可变类?详细教程

简介:   如果对象的状态在构造后无法更改,则该对象是不可变的。不可变的对象不会让其他对象修改其状态。对象的字段在构造函数中仅初始化一次,就再也不会更改。  在本文中,我们将定义在Java中创建不可变类的典型步骤,并阐明开发人员在创建不可变类时所犯的常见错误。

  如果对象的状态在构造后无法更改,则该对象是不可变的。不可变的对象不会让其他对象修改其状态。对象的字段在构造函数中仅初始化一次,就再也不会更改。

  在本文中,我们将定义在Java中创建不可变类的典型步骤,并阐明开发人员在创建不可变类时所犯的常见错误。

  如今,每个软件应用程序的“必备”规范都是分布式的。多线程应用程序总是使程序员们感到脑壳疼,因为开发人员需要同时保护其对象的状态不受多个线程的并发修改,所以呢,开发人员通常在修改对象状态时使用同步块。

  对于不可变的类,状态永远不会被修改。状态的每次修改都会产生一个新实例,因此每个线程使用不同的实例,并且开发人员不必担心并发修改。

  字符串(string)是Java中最流行的不可变类。初始化后,其值将无法修改。trim()、substring()、replace()总是返回一个新的实例,不会影响当前的实例,这就是为什么我们通常所说的trim(),如下所示:

  String alex="Alex";

  alex=alex.trim();

  JDK的另一个例子是包装类,比如:Integer、Float、Boolean……这些类不会修改它们的状态,但你每次试图修改,都会创建一个新实例。

  Integer a=3;

  a +=3;

  调用+=3之后,将创建一个新实例,其值保持为:6,并且第一个实例就丢失了。

  为了创建一个不可变的类,就要遵循以下步骤:

  将您的类定为最终类,以便其他类无法对其进行扩展。将所有字段定为最终字段,以使它们在构造函数中仅初始化一次,之后再也不会修改。不要公开setter方法。在公开修改类状态的方法时,必须始终返回该类的新实例。如果该类包含一个可变对象:在构造函数内部,请确保使用传递的参数的克隆副本,并且永远不要将可变字段设置为通过构造函数传递的实际实例,这是为了防止传递对象的客户端事后对其进行修改。确保始终返回该字段的克隆副本,并且永远不要返回真实对象实例。

  3.1 简单不可变类

  让我们按照上述步骤创建一个自己的不可变类(ImmutableStudent.java)。

  package com.programmer.gate.beans;

  public final class ImmutableStudent {

  private final int id;

  private final String name;

  public ImmutableStudent(int id, String name) {

  this.name=name;

  this.id=id;

  }

  public int getId() {

  return id;

  }

  public String getName() {

  return name;

  }

  }

  上面的类是一个非常简单的不可变类,它不包含任何可变对象,并且从不以任何方式公开其字段;这些类型的类通常用于缓存。

  3.2 将可变对象传递给不可变类

  现在,让我们的示例复杂一点,我们创建一个名为Age的可变类,并将其作为字段添加到ImmutableStudent中:

  package com.programmer.gate.beans;

  public class Age {

  private int day;

  private int month;

  private int year;

  public int getDay() {

  return day;

  }

  public void setDay(int day) {

  this.day=day;

  }

  public int getMonth() {

  return month;

  }

  public void setMonth(int month) {

  this.month=month;

  }

  public int getYear() {

  return year;

  }

  public void setYear(int year) {

  this.year=year;

  }

  }

  package com.programmer.gate.beans;

  public final class ImmutableStudent {

  private final int id;

  private final String name;

  private final Age age;

  public ImmutableStudent(int id, String name, Age age) {

  this.name=name;

  this.id=id;

  this.age=age;

  }

  public int getId() {

  return id;

  }

  public String getName() {

  return name;

  }

  public Age getAge() {

  return age;

  }

  }

  因此,我们在不可变类中添加了一个类型为Age的新可变字段,并将其作为普通字段赋给构造函数。

  下面我来创建一个简单的测试类,并验证ImmutableStudent是否不再不可变:

  public static void main(String[] args) {

  Age age=new Age();

  age.setDay(1);

  age.setMonth(1);

  age.setYear(1992);

  ImmutableStudent student=new ImmutableStudent(1, "Alex", age);

  System.out.println("Alex age year before modification=" + student.getAge().getYear());

  age.setYear(1993);

  System.out.println("Alex age year after modification=" + student.getAge().getYear());

  }

  运行上面的测试后,我们得到以下输出:

  Alex age year before modification=1992

  Alex age year after modification=1993

  我们声称ImmutableStudent是一个不可变的类,其状态在二手交易平台构造之后就不会修改,但是在上面的示例中,即使在构造Alex对象之后,我们也能够修改Alex的年龄。如果返回ImmutableStudent构造函数的实现,则会发现Age字段已分配给Age参数的实例,因此,只要在类外部修改了所引用的Age,该更改就会直接反映在Alex的状态上。

  为了解决这个问题并使我们的类再次变得不可变,我们从上面提到的创建不可变类的步骤开始执行步骤5。因此,我们修改了构造函数,以克隆传递的Age参数并使用其克隆实例。

  public ImmutableStudent(int id, String name, Age age) {

  this.name=name;

  this.id=id;

  Age cloneAge=new Age();

  cloneAge.setDay(age.getDay());

  cloneAge.setMonth(age.getMonth());

  cloneAge.setYear(age.getYear());

  this.age=cloneAge;

  }

  现在,如果运行测试,将得到以下输出:

  Alex age year before modification=1992

  Alex age year after modification=1992

  正如上面所见,Alex的年龄在构造之后从未受到影响,我们的类又回到了不变的状态。

  但是,我们的类仍然有泄漏,并且不是完全不变的,所以我采取以下测试方案:

  public static void main(String[] args) {

  Age age=new Age();

  age.setDay(1);

  age.setMonth(1);

  age.setYear(1992);

  ImmutableStudent student=new ImmutableStudent(1, "Alex", age);

  System.out.println("Alex age year before modification=" + student.getAge().getYear());

  student.getAge().setYear(1993);

  System.out.println("Alex age year after modification=" + student.getAge().getYear());

  }

  输出为:

  Alex age year before modification=1992

  Alex age year after modification=1993

  再次根据步骤4,从不可变对象返回可变字段时,应该返回克隆实例,而不是该字段的真实实例。

  所以,我们修改了getAge()以便返回该对象的年龄的副本:

  public Age getAge() {

  Age cloneAge=new Age();

  cloneAge.setDay(this.age.getDay());

  cloneAge.setMonth(this.age.getMonth());

  cloneAge.setYear(this.age.getYear());

  return cloneAge;

  }

  现在,该类变得完全不可变,并且没有为其他对象提供任何方法或方法来修改其状态。

  Alex age year before modification=1992

  Alex age year after modification=1992

  不可变的类具有很多优点,尤其是在多线程环境中正确使用时。唯一的缺点是它们比传统类消耗更多的内存,因为每次对其进行修改后,都会在内存中创建一个新对象……但是,与这些类提供的优点相比,开发人员不应高估其消耗,因为它可以忽略不计类的类型。

  最后,如果一个对象仅能向其他对象呈现一种状态,则无论它们如何以及何时调用其方法,该对象都是不可变的。如果是这样,则通过任何线程安全的定义都是线程安全的。

  希望上文能对你有帮助,喜欢可以关注哦。

目录
相关文章
|
4天前
|
XML 测试技术 数据格式
《手把手教你》系列基础篇(八十五)-java+ selenium自动化测试-框架设计基础-TestNG自定义日志-下篇(详解教程)
【7月更文挑战第3天】TestNG教程展示了如何自定义日志记录。首先创建一个名为`TestLog`的测试类,包含3个测试方法,其中一个故意失败以展示日志。使用`Assert.assertTrue`和`Reporter.log`来记录信息。接着创建`CustomReporter`类,继承`TestListenerAdapter`,覆盖`onTestFailure`, `onTestSkipped`, 和 `onTestSuccess`,在这些方法中自定义日志输出。
21 6
|
1天前
|
Java 关系型数据库 测试技术
《手把手教你》系列基础篇(八十九)-java+ selenium自动化测试-框架设计基础-Logback实现日志输出-上篇(详解教程)
【7月更文挑战第7天】Apache Log4j2的安全漏洞促使考虑使用logback作为替代的日志框架。Logback由log4j创始人设计,提供更好的性能,更低的内存使用,并且能够自动重载配置文件。它分为logback-core、logback-classic(实现了SLF4J API)和logback-access(用于Servlet容器集成)三个模块。配置涉及Logger、Appender(定义日志输出目的地)和Layout(格式化日志)。
|
4天前
|
Java 测试技术 Apache
《手把手教你》系列基础篇(八十六)-java+ selenium自动化测试-框架设计基础-Log4j实现日志输出(详解教程)
【7月更文挑战第4天】Apache Log4j 是一个广泛使用的 Java 日志框架,它允许开发者控制日志信息的输出目的地、格式和级别。Log4j 包含三个主要组件:Loggers(记录器)负责生成日志信息,Appenders(输出源)确定日志输出的位置(如控制台、文件、数据库等),而 Layouts(布局)则控制日志信息的格式。通过配置 Log4j,可以灵活地定制日志记录行为。
19 4
|
3天前
|
开发框架 Java Android开发
Java中的类反射与动态代理详解
Java中的类反射与动态代理详解
|
4天前
|
Java 数据安全/隐私保护
Java中的类与对象详解
Java中的类与对象详解
|
4天前
|
Java 数据安全/隐私保护
Java中的类继承与多态详解
Java中的类继承与多态详解
|
5天前
|
设计模式 Java
Java中的动态加载与卸载类
Java中的动态加载与卸载类
|
2天前
|
XML Java 测试技术
《手把手教你》系列基础篇(八十八)-java+ selenium自动化测试-框架设计基础-Log4j 2实现日志输出-下篇(详解教程)
【7月更文挑战第6天】本文介绍了如何使用Log4j2将日志输出到文件中,重点在于配置文件的结构和作用。配置文件包含两个主要部分:`appenders`和`loggers`。`appenders`定义了日志输出的目标,如控制台(Console)或其他文件,如RollingFile,设置输出格式和策略。`loggers`定义了日志记录器,通过`name`属性关联到特定的类或包,并通过`appender-ref`引用`appenders`来指定输出位置。`additivity`属性控制是否继承父logger的配置。
|
3天前
|
XML Java 测试技术
《手把手教你》系列基础篇(八十七)-java+ selenium自动化测试-框架设计基础-Log4j 2实现日志输出-上篇(详解教程)
【7月更文挑战第5天】Apache Log4j 2是一个日志框架,它是Log4j的升级版,提供了显著的性能提升,借鉴并改进了Logback的功能,同时修复了Logback架构中的问题。Log4j2的特点包括API与实现的分离,支持SLF4J,自动重新加载配置,以及高级过滤选项。它还引入了基于lambda表达式的延迟评估,低延迟的异步记录器和无垃圾模式。配置文件通常使用XML,但也可以是JSON或YAML,其中定义了日志级别、输出目的地(Appender)和布局(Layout)。
12 0
|
3天前
|
安全 Java
Java中的集合类性能比较与选择
Java中的集合类性能比较与选择