Java 编程问题:二、对象、不变性和`switch`表达式3

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Java 编程问题:二、对象、不变性和`switch`表达式

51 通过生成器模式编写不可变类


当一个类(不可变或可变)有太多字段时,它需要一个具有许多参数的构造器。当其中一些字段是必需的,而其他字段是可选的时,这个类将需要几个构造器来覆盖所有可能的组合。这对于开发人员和类的用户来说都是很麻烦的。这就是构建器模式的用武之地。


根据四人帮(GoF),构建器模式将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示。


生成器模式可以作为一个单独的类或内部的static类来实现。让我们关注第二个案例。User类有三个必填字段(nickname、password、created)和三个可选字段(email、firstname、lastname)。

现在,依赖于构建器模式的不可变的User类将显示如下:

public final class User {
  private final String nickname;
  private final String password;
  private final String firstname;
  private final String lastname;
  private final String email;
  private final Date created;
  private User(UserBuilder builder) {
    this.nickname = builder.nickname;
    this.password = builder.password;
    this.created = builder.created;
    this.firstname = builder.firstname;
    this.lastname = builder.lastname;
    this.email = builder.email;
  }
  public static UserBuilder getBuilder(
      String nickname, String password) {
    return new User.UserBuilder(nickname, password);
  }
  public static final class UserBuilder {
    private final String nickname;
    private final String password;
    private final Date created;
    private String email;
    private String firstname;
    private String lastname;
    public UserBuilder(String nickname, String password) {
      this.nickname = nickname;
      this.password = password;
      this.created = new Date();
    }
    public UserBuilder firstName(String firstname) {
      this.firstname = firstname;
      return this;
    }
    public UserBuilder lastName(String lastname) {
      this.lastname = lastname;
      return this;
    }
    public UserBuilder email(String email) {
      this.email = email;
      return this;
    }
    public User build() {
      return new User(this);
    }
  }
  public String getNickname() {
    return nickname;
  }
  public String getPassword() {
    return password;
  }
  public String getFirstname() {
    return firstname;
  }
  public String getLastname() {
    return lastname;
  }
  public String getEmail() {
    return email;
  }
  public Date getCreated() {
    return new Date(created.getTime());
  }
}


以下是一些用法示例:

import static modern.challenge.User.getBuilder;
...
// user with nickname and password
User user1 = getBuilder("marin21", "hjju9887h").build();
// user with nickname, password and email
User user2 = getBuilder("ionk", "44fef22")
  .email("ion@gmail.com")
  .build();
// user with nickname, password, email, firstname and lastname
User user3 = getBuilder("monika", "klooi0988")
  .email("monika@gmail.com")
  .firstName("Monika")
  .lastName("Ghuenter")
  .build();




52 避免不可变对象中的坏数据


坏数据是任何对不可变对象有负面影响的数据(例如,损坏的数据)。最有可能的是,这些数据来自用户输入或不受我们直接控制的外部数据源。在这种情况下,坏数据可能会击中不可变的对象,最糟糕的是没有修复它的方法。不可变的对象在创建后不能更改;因此,只要对象存在,坏数据就会快乐地存在。


这个问题的解决方案是根据一组全面的约束来验证输入到不可变对象中的所有数据。

执行验证有不同的方法,从自定义验证到内置解决方案。验证可以在不可变对象类的外部或内部执行,具体取决于应用设计。例如,如果不可变对象是通过构建器模式构建的,那么可以在 Builder 类中执行验证。


JSR380 是用于 bean 验证的 Java API(JavaSE/EE)规范,可用于通过注解进行验证。Hibernate 验证器是验证 API 的参考实现,它可以很容易地作为 Maven 依赖项在pom.xml文件中提供(请查看本书附带的源代码)。


此外,我们依赖于专用注解来提供所需的约束(例如,@NotNull、@Min、@Max、@Size和@Email)。在以下示例中,将约束添加到生成器类中,如下所示:


...
public static final class UserBuilder {
  @NotNull(message = "cannot be null")
  @Size(min = 3, max = 20, message = "must be between 3 and 20 
    characters")
  private final String nickname;
  @NotNull(message = "cannot be null")
  @Size(min = 6, max = 50, message = "must be between 6 and 50 
    characters")
  private final String password;
  @Size(min = 3, max = 20, message = "must be between 3 and 20 
    characters")
  private String firstname;
  @Size(min = 3, max = 20, message = "must be between 3 and 20 
    characters")
  private String lastname;
  @Email(message = "must be valid")
  private String email;
  private final Date created;
  public UserBuilder(String nickname, String password) {
    this.nickname = nickname;
    this.password = password;
    this.created = new Date();
  }
...

最后,验证过程通过ValidatorAPI 从代码中触发(这仅在 JavaSE 中需要)。如果进入生成器类的数据无效,则不创建不可变对象(不要调用build()方法):

User user;
Validator validator 
  = Validation.buildDefaultValidatorFactory().getValidator();
User.UserBuilder userBuilder 
  = new User.UserBuilder("monika", "klooi0988")
    .email("monika@gmail.com")
    .firstName("Monika").lastName("Gunther");
final Set<ConstraintViolation<User.UserBuilder>> violations 
  = validator.validate(userBuilder);
if (violations.isEmpty()) {
  user = userBuilder.build();
  System.out.println("User successfully created on: " 
    + user.getCreated());
} else {
  printConstraintViolations("UserBuilder Violations: ", violations);
}


这样,坏数据就不能触及不可变的对象。如果没有生成器类,则可以直接在不可变对象的字段级别添加约束。前面的解决方案只是在控制台上显示潜在的冲突,但是根据情况,该解决方案可能执行不同的操作(例如,抛出特定的异常)。



53 克隆对象


克隆对象不是一项日常任务,但正确地克隆对象很重要。克隆对象主要是指创建对象的副本。拷贝主要有两种类型:拷贝(尽可能少拷贝)和拷贝(复制所有内容)。


假设下面的类:

public class Point {
  private double x;
  private double y;
  public Point() {}
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }
  // getters and setters
}

所以,我们在一个类中映射了一个类型点(x, y)。现在,让我们进行一些克隆。



手动克隆

快速方法包括添加一个手动将当前Point复制到新Point的方法(这是一个浅复制):

public Point clonePoint() {
  Point point = new Point();
  point.setX(this.x);
  point.setY(this.y);
  return point;
}


这里的代码非常简单。只需创建一个新的Point实例,并用当前Point的字段填充其字段。返回的Point是当前Point的浅拷贝(因为Point不依赖其他对象,所以深拷贝是完全相同的):

Point point = new Point(...);
Point clone = point.clonePoint();




通过clone()克隆


Object类包含一个名为clone()的方法。此方法对于创建浅拷贝非常有用(也可以用于深拷贝)。为了使用它,类应该遵循给定的步骤:


   实现Cloneable接口(如果该接口没有实现,则抛出CloneNotSupportedException。

   覆盖clone()方法(Object.clone()为protected)。

   调用super.clone()。


Cloneable接口不包含任何方法。这只是 JVM 可以克隆这个对象的一个信号。一旦实现了这个接口,代码就需要覆盖Object.clone()方法。这是需要的,因为Object.clone()是protected,为了通过super调用它,代码需要覆盖这个方法。如果将clone()添加到子类中,这可能是一个严重的缺点,因为所有超类都应该定义一个clone()方法,以避免super.clone()链调用失败。


此外,Object.clone()不依赖构造器调用,因此开发人员无法控制对象构造:


public class Point implements Cloneable {
  private double x;
  private double y;
  public Point() {}
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }
  @Override
  public Point clone() throws CloneNotSupportedException {
    return (Point) super.clone();
  }
  // getters and setters
}

创建克隆的步骤如下:

Point point = new Point(...);
Point clone = point.clone();




通过构造器克隆


此克隆技术要求您使用构造器来丰富类,该构造器接受表示将用于创建克隆的类实例的单个参数。


让我们看看代码:

public class Point {
  private double x;
  private double y;
  public Point() {}
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }
  public Point(Point another) {
    this.x = another.x;
    this.y = another.y;
  }
  // getters and setters
}



创建克隆的步骤如下:

Point point = new Point(...);
Point clone = new Point(point);




通过克隆库进行克隆


当一个对象依赖于另一个对象时,需要一个深度副本。执行深度复制意味着复制对象,包括其依赖链。例如,假设Point有一个Radius类型的字段:

public class Radius {
  private int start;
  private int end;
  // getters and setters
}
public class Point {
  private double x;
  private double y;
  private Radius radius;
  public Point(double x, double y, Radius radius) {
    this.x = x;
    this.y = y;
    this.radius = radius;
  }
  // getters and setters
}


执行Point的浅拷贝将创建xy的拷贝,但不会创建radius对象的拷贝。这意味着影响radius对象的修改也将反映在克隆中。是时候进行深度复制了。


一个麻烦的解决方案将涉及到调整以前提出的浅拷贝技术以支持深拷贝。幸运的是,有一些现成的解决方案可以应用,其中之一就是克隆库

import com.rits.cloning.Cloner;
...
Point point = new Point(...);
Cloner cloner = new Cloner();
Point clone = cloner.deepClone(point);


代码是不言自明的。请注意,克隆库还附带了其他一些好处,如下面的屏幕截图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EXjwIwfB-1657077359612)(img/c75891ec-3902-49da-8290-62d5f7b09c70.png)]




通过序列化克隆


这种技术需要可序列化的对象(实现java.io.Serializable。基本上,对象在新对象中被序列化(writeObject())和反序列化(readObject())。可以实现这一点的助手方法如下所示:

private static <T> T cloneThroughSerialization(T t) {
  try {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(t);
    ByteArrayInputStream bais 
      = new ByteArrayInputStream(baos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bais);
    return (T) ois.readObject();
  } catch (IOException | ClassNotFoundException ex) {
    // log exception
    return t;
  }
}

因此,对象在ObjectOutputStream中序列化,在ObjectInputStream中反序列化。通过此方法克隆对象的步骤如下:

Point point = new Point(...);
Point clone = cloneThroughSerialization(point);

ApacheCommonsLang 通过SerializationUtils提供了一个基于序列化的内置解决方案。在它的方法中,这个类提供了一个名为clone()的方法,可以如下使用:

Point point = new Point(...);
Point clone = SerializationUtils.clone(point);




通过 JSON 克隆


几乎所有 Java 中的 JSON 库都可以序列化任何普通的旧 Java 对象(POJO),而不需要任何额外的配置/映射。在项目中有一个 JSON 库(很多项目都有)可以避免我们添加额外的库来提供深度克隆。主要来说,该解决方案可以利用现有的 JSON 库来获得相同的效果。


以下是使用Gson库的示例:

private static <T> T cloneThroughJson(T t) {
  Gson gson = new Gson();
  String json = gson.toJson(t);
  return (T) gson.fromJson(json, t.getClass());
}
Point point = new Point(...);
Point clone = cloneThroughJson(point);

除此之外,您还可以选择编写专用于克隆对象的库。



54 覆盖toString()


toString()方法在java.lang.Object中定义,JDK 附带了它的默认实现。此默认实现自动用于print()、println()、printf()、开发期间调试、日志记录、异常中的信息消息等的所有对象。


不幸的是,默认实现返回的对象的字符串表示形式信息量不大。例如,让我们考虑下面的User类:

public class User {
  private final String nickname;
  private final String password;
  private final String firstname;
  private final String lastname;
  private final String email;
  private final Date created;
  // constructor and getters skipped for brevity
}


现在,让我们创建这个类的一个实例,并在控制台上打印它:

User user = new User("sparg21", "kkd454ffc",
  "Leopold", "Mark", "markl@yahoo.com");
System.out.println(user);


这个println()方法的输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-39Eby1Uc-1657077359613)(img/d41b9942-344f-444f-b3a4-dbd2513c92cf.png)]

在前面的屏幕截图中,避免输出的解决方案包括覆盖toString()方法。例如,让我们覆盖它以公开用户详细信息,如下所示:


@Override
public String toString() {
  return "User{" + "nickname=" + nickname + ", password=" + password
    + ", firstname=" + firstname + ", lastname=" + lastname
    + ", email=" + email + ", created=" + created + '}';
}

这次,println()将显示以下输出:

User {
  nickname = sparg21, password = kkd454ffc, 
  firstname = Leopold, lastname = Mark, 
  email = markl@yahoo.com, created = Fri Feb 22 10: 49: 32 EET 2019
}


这比以前的输出信息更丰富。

但是,请记住,toString()是为不同的目的自动调用的。例如,日志记录可以如下所示:

logger.log(Level.INFO, "This user rocks: {0}", user);


在这里,用户密码将命中日志,这可能表示有问题。在应用中公开日志敏感数据(如密码、帐户和秘密 IP)绝对是一种不好的做法。


因此,请特别注意仔细选择进入toString()的信息,因为这些信息最终可能会被恶意利用。在我们的例子中,密码不应该是toString()的一部分:

@Override
public String toString() {
  return "User{" + "nickname=" + nickname
    + ", firstname=" + firstname + ", lastname=" + lastname
    + ", email=" + email + ", created=" + created + '}';
}

通常,toString()是通过 IDE 生成的方法。因此,在 IDE 为您生成代码之前,请注意您选择了哪些字段。



55 switch表达式

在简要概述 JDK12 中引入的switch表达式之前,让我们先来看一个典型的老式方法示例:

private static Player createPlayer(PlayerTypes playerType) {
  switch (playerType) {
    case TENNIS:
      return new TennisPlayer();
    case FOOTBALL:
      return new FootballPlayer();
    case SNOOKER:      
      return new SnookerPlayer();
    case UNKNOWN:
      throw new UnknownPlayerException("Player type is unknown");
    default:
      throw new IllegalArgumentException(
        "Invalid player type: " + playerType);
  }
}

如果我们忘记了default,那么代码将无法编译。

显然,前面的例子是可以接受的。在最坏的情况下,我们可以添加一个伪变量(例如,player),一些杂乱的break语句,如果default丢失,就不会收到投诉。所以,下面的代码是一个老派,非常难看的switch

private static Player createPlayerSwitch(PlayerTypes playerType) {
  Player player = null;
  switch (playerType) {
    case TENNIS:
      player = new TennisPlayer();
      break;
    case FOOTBALL:
      player = new FootballPlayer();
      break;
    case SNOOKER:
      player = new SnookerPlayer();
      break;
    case UNKNOWN:
      throw new UnknownPlayerException(
        "Player type is unknown");
    default:
      throw new IllegalArgumentException(
        "Invalid player type: " + playerType);
  }
  return player;
}

如果我们忘记了default,那么编译器方面就不会有任何抱怨了。在这种情况下,丢失的default案例可能导致null播放器。


然而,自从 JDK12 以来,我们已经能够依赖于switch表达式。在 JDK12 之前,switch是一个语句,一个用来控制流的构造(例如,if语句),而不表示结果。另一方面,表达式的求值结果。因此,switch表达可产生结果。


前面的switch表达式可以用 JDK12 的样式写成如下:

private static Player createPlayer(PlayerTypes playerType) {
  return switch (playerType) {
    case TENNIS ->
      new TennisPlayer();
    case FOOTBALL ->
      new FootballPlayer();
    case SNOOKER ->
      new SnookerPlayer();
    case UNKNOWN ->
      throw new UnknownPlayerException(
        "Player type is unknown");
    // default is not mandatory
    default ->
      throw new IllegalArgumentException(
        "Invalid player type: " + playerType);
  };
}

这次,default不是强制性的。我们可以跳过它。


JDK12switch足够聪明,可以在switch没有覆盖所有可能的输入值时发出信号。这在 Java enum值的情况下非常有用。JDK12switch可以检测所有enum值是否都被覆盖,如果没有被覆盖,则不会强制一个无用的default。例如,如果我们删除default并向PlayerTypes enum添加一个新条目(例如GOLF),那么编译器将通过一条消息向它发送信号,如下面的屏幕截图(这是来自 NetBeans 的):


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CdkcxPb3-1657077359613)(img/fd6ef71d-5ca7-4757-a37c-3e111bb83418.png)]

注意,在标签和执行之间,我们将冒号替换为箭头(Lambda 样式的语法)。此箭头的主要作用是防止跳转,这意味着只执行其右侧的代码块。不需要使用break。


不要断定箭头将switch语句转换为switch表达式。switch表达可用于结肠和break,如下所示:


private static Player createPlayer(PlayerTypes playerType) {
  return switch (playerType) {
    case TENNIS:
      break new TennisPlayer();
    case FOOTBALL:
      break new FootballPlayer();
    case SNOOKER:
      break new SnookerPlayer();
    case UNKNOWN:
      throw new UnknownPlayerException(
        "Player type is unknown");
    // default is not mandatory
    default:
      throw new IllegalArgumentException(
        "Invalid player type: " + playerType);
  };
}

我们的示例在enum上发布了switch,但是 JDK12switch也可以在int、Integer、short、Short、byte、Byte、char、Character和String上使用。


注意,JDK12 带来了switch表达式作为预览特性。这意味着它很容易在接下来的几个版本中发生更改,需要在编译和运行时通过--enable-preview命令行选项来解锁它。



相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2天前
|
存储 Java
java的对象详解
在Java中,对象是根据类模板实例化的内存实体,具有唯一标识符、属性及行为。通过`new`关键字实例化对象并用构造方法初始化。变量存储的是对象引用而非对象本身,属性描述对象状态,方法定义其行为。Java利用垃圾回收机制自动处理不再使用的对象内存回收,极大地简化了对象生命周期管理,同时对象具备封装、继承和多态性,促进了代码的重用与模块化设计。这使得Java程序更易于理解、维护和扩展。
|
2天前
|
Java 调度 开发者
Java并发编程:深入理解线程池
在Java的世界中,线程池是提升应用性能、实现高效并发处理的关键工具。本文将深入浅出地介绍线程池的核心概念、工作原理以及如何在实际应用中有效利用线程池来优化资源管理和任务调度。通过本文的学习,读者能够掌握线程池的基本使用技巧,并理解其背后的设计哲学。
|
3天前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
3天前
|
并行计算 Java 开发者
探索Java中的Lambda表达式:简化代码,提升效率
Lambda表达式在Java 8中引入,旨在简化集合操作和并行计算。本文将通过浅显易懂的语言,带你了解Lambda表达式的基本概念、语法结构,并通过实例展示如何在Java项目中应用Lambda表达式来优化代码,提高开发效率。我们将一起探讨这一现代编程工具如何改变我们的Java编码方式,并思考它对程序设计哲学的影响。
|
3天前
|
安全 Java 数据安全/隐私保护
- 代码加密混淆工具-Java 编程安全性
在Java编程领域,保护代码安全与知识产权至关重要。本文探讨了代码加密混淆工具的重要性,并介绍了五款流行工具:ProGuard、DexGuard、Jscrambler、DashO 和 Ipa Guard。这些工具通过压缩、优化、混淆和加密等手段,提升代码安全性,保护知识产权。ProGuard 是开源工具,用于压缩和混淆Java代码;DexGuard 专为Android应用程序设计,提供强大加密功能;Jscrambler 基于云,保护Web和移动应用的JavaScript及HTML5代码;DashO 支持多种Java平台和
16 1
|
2天前
|
安全 Java UED
Java并发编程:解锁多线程的潜力
在Java的世界里,并发编程如同一场精心编排的交响乐,每个线程扮演着不同的乐手,共同奏响性能与效率的和声。本文将引导你走进Java并发编程的大门,探索如何在多核处理器上优雅地舞动多线程,从而提升应用的性能和响应性。我们将从基础概念出发,逐步深入到高级技巧,让你的代码在并行处理的海洋中乘风破浪。
|
2天前
|
Java 程序员
Java编程中的对象和类: 初学者指南
【9月更文挑战第9天】在Java的世界中,对象和类构成了编程的基石。本文将引导你理解这两个概念的本质,并展示如何通过它们来构建你的程序。我们将一起探索类的定义,对象的创建,以及它们如何互动。准备好了吗?让我们开始这段Java的旅程吧!
|
8天前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
65 6
【Java学习】多线程&JUC万字超详解
|
1天前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
3天前
|
缓存 监控 Java
java中线程池的使用
java中线程池的使用