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

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

本章包括 18 个涉及对象、不变性和switch表达式的问题。本章从处理null引用的几个问题入手。它继续处理有关检查索引、equals()和hashCode()以及不变性(例如,编写不可变类和从不可变类传递/返回可变对象)的问题。本章的最后一部分讨论了克隆对象和 JDK12switch表达式。本章结束时,您将掌握对象和不变性的基本知识。此外,你将知道如何处理新的switch表达式。在任何 Java 开发人员的武库中,这些都是有价值的、非可选的知识。




问题


使用以下问题来测试您的对象、不变性和switch表达式编程能力。我强烈建议您在转向解决方案和下载示例程序之前,尝试一下每个问题:


使用命令式代码检查null函数式引用:编写程序,对给定的函数式引用和命令式代码进行null检查。


检查null引用并抛出一个定制的NullPointerException错误:编写一个程序,对给定的引用执行null检查并抛出带有定制消息的NullPointerException。


检查null引用并抛出指定的异常(例如,IllegalArgumentException:编写一个程序,对给定的引用执行null检查并抛出指定的异常。


检查null引用并返回非null默认引用:编写程序,对给定引用执行null检查,如果是非null,则返回;否则返回非null默认引用。


检查从 0 到长度范围内的索引:编写一个程序,检查给定索引是否在 0(含)到给定长度(不含)之间。如果给定索引超出 0 到给定长度的范围,则抛出IndexOutOfBoundsException。


检查从 0 到长度范围内的子范围:编写一个程序,检查给定的开始到给定的结束的给定的子范围,是否在 0 到给定的长度的范围内。如果给定的子范围不在范围内,则抛出IndexOutOfBoundsException。


解释equals()和hashCode()并举例说明equals()和hashCode()方法在 Java 中是如何工作的。


不可变对象概述:解释并举例说明什么是 Java 中的不可变对象。


不可变字符串:解释String类不可变的原因。


编写不可变类:写一个表示不可变类的程序。


向不可变类传递或从不可变类返回可变对象:编写一个程序,向不可变类传递或从不可变类返回可变对象。


通过构建器模式编写一个不可变类:编写一个表示不可变类中构建器模式实现的程序。51. 避免不可变对象中的坏数据:编写防止不可变对象中的坏数据的程序。


克隆对象:编写一个程序,演示浅层和深层克隆技术。


覆盖toString():解释并举例说明覆盖toString()的实践。


switch表达式:简要概述 JDK12 中的switch表达式。


多个case标签:写一段代码,用多个case标签举例说明 JDK12switch。


语句块:编写一段代码,用于举例说明 JDK12 switch,其中的case标签指向花括号块。


以下各节介绍上述每个问题的解决方案。记住,通常没有一个正确的方法来解决一个特定的问题。另外,请记住,这里显示的解释仅包括解决问题所需的最有趣和最重要的细节。下载示例解决方案以查看更多详细信息,并尝试程序



40 在函数式和命令式代码中检查空引用


与函数样式或命令式代码无关,检查null引用是一种常用且推荐的技术,用于减少著名的NullPointerException异常的发生。这种检查被大量用于方法参数,以确保传递的引用不会导致NullPointerException或意外行为。


例如,将List<Integer>传递给方法可能需要至少两个null检查。首先,该方法应该确保列表引用本身不是null。其次,根据列表的使用方式,该方法应确保列表不包含null对象:

List<Integer> numbers 
  = Arrays.asList(1, 2, null, 4, null, 16, 7, null);



此列表将传递给以下方法:

public static List<Integer> evenIntegers(List<Integer> integers) {
  if (integers == null) {
    return Collections.EMPTY_LIST;
  }
  List<Integer> evens = new ArrayList<>();
  for (Integer nr: integers) {
    if (nr != null && nr % 2 == 0) {
      evens.add(nr);
    }
  }
  return evens;
}


注意,前面的代码使用依赖于==和!=运算符(integers==null、nr !=null的经典检查。从 JDK8 开始,java.util.Objects类包含两个方法,它们基于这两个操作符包装null检查:object == null包装在Objects.isNull()中,object != null包装在Objects.nonNull()中。

基于这些方法,前面的代码可以重写如下:


public static List<Integer> evenIntegers(List<Integer> integers) {
  if (Objects.isNull(integers)) {
    return Collections.EMPTY_LIST;
  }
  List<Integer> evens = new ArrayList<>();
  for (Integer nr: integers) {
    if (Objects.nonNull(nr) && nr % 2 == 0) {
      evens.add(nr);
    }
  }
  return evens;
}


现在,代码在某种程度上更具表现力,但这并不是这两种方法的主要用法。实际上,这两个方法是为了另一个目的(符合 API 注解)而添加的——在 Java8 函数式代码中用作谓词。在函数式代码中,null检查可以如下例所示完成:

public static int sumIntegers(List<Integer> integers) {
  if (integers == null) {
    throw new IllegalArgumentException("List cannot be null");
  }
  return integers.stream()
    .filter(i -> i != null)
    .mapToInt(Integer::intValue).sum();
}
public static boolean integersContainsNulls(List<Integer> integers) {
  if (integers == null) {
    return false;
  }
  return integers.stream()
    .anyMatch(i -> i == null);
}


很明显,i -> i != nulli -> i == null的表达方式与周围的代码不一样。让我们用Objects.nonNull()Objects.isNull()替换这些代码片段:

public static int sumIntegers(List<Integer> integers) {
  if (integers == null) {
    throw new IllegalArgumentException("List cannot be null");
  }
  return integers.stream()
    .filter(Objects::nonNull)
    .mapToInt(Integer::intValue).sum();
}
public static boolean integersContainsNulls(List<Integer> integers) {
  if (integers == null) {
    return false;
  }
  return integers.stream()
    .anyMatch(Objects::isNull);
}


或者,我们也可以使用Objects.nonNull()Objects.isNull()方法作为参数:

public static int sumIntegers(List<Integer> integers) {
  if (Objects.isNull(integers)) {
    throw new IllegalArgumentException("List cannot be null");
  }
  return integers.stream()
    .filter(Objects::nonNull)
    .mapToInt(Integer::intValue).sum();
}
public static boolean integersContainsNulls(List<Integer> integers) {
  if (Objects.isNull(integers)) {
    return false;
  }
  return integers.stream()
    .anyMatch(Objects::isNull);
}

令人惊叹的!因此,作为结论,无论何时需要进行null检查,函数式代码都应该依赖于这两种方法,而在命令式代码中,这是一种偏好。



41 检查空引用并引发自定义NullPointerException


检查null引用并用定制消息抛出NullPointerException可以使用以下代码完成(此代码执行这四次,在构造器中执行两次,在assignDriver()方法中执行两次):

public class Car {
  private final String name;
  private final Color color;
  public Car(String name, Color color) {
    if (name == null) {
      throw new NullPointerException("Car name cannot be null");
    }
    if (color == null) {
      throw new NullPointerException("Car color cannot be null");
    }
    this.name = name;
    this.color = color;
  }
  public void assignDriver(String license, Point location) {
    if (license == null) {
      throw new NullPointerException("License cannot be null");
    }
    if (location == null) {
      throw new NullPointerException("Location cannot be null");
    }
  }
}


因此,这段代码通过结合==操作符和NullPointerException类的手动实例化来解决这个问题。从 JDK7 开始,这种代码组合隐藏在一个名为Objects.requireNonNull()static方法中。通过这种方法,前面的代码可以用表达的方式重写:

public class Car {
  private final String name;
  private final Color color;
  public Car(String name, Color color) {
    this.name = Objects.requireNonNull(name, "Car name cannot be 
      null");
    this.color = Objects.requireNonNull(color, "Car color cannot be 
      null");
  }
  public void assignDriver(String license, Point location) {
    Objects.requireNonNull(license, "License cannot be null");
    Objects.requireNonNull(location, "Location cannot be null");
  }
}


因此,如果指定的引用是null,那么Objects.requireNonNull()将抛出一个包含所提供消息的NullPointerException。否则,它将返回选中的引用。


在构造器中,当提供的引用是null时,有一种典型的抛出NullPointerException的方法。但在方法上(例如,assignDriver()),这是一个有争议的方法。一些开发人员更喜欢返回一个无害的结果或者抛出IllegalArgumentException。下一个问题,检查空引用并抛出指定的异常(例如,IllegalArgumentException),解决了IllegalArgumentException方法。


在 JDK7 中,有两个Objects.requireNonNull()方法,一个是以前使用的,另一个是抛出带有默认消息的NullPointerException,如下例所示:


this.name = Objects.requireNonNull(name);

从 JDK8 开始,还有一个Objects.requireNonNull()。这个将NullPointerException的自定义消息封装在Supplier中。这意味着消息创建被推迟,直到给定的引用是null(这意味着使用+操作符连接消息的各个部分不再是一个问题)。

举个例子:

this.name = Objects.requireNonNull(name, () 
  -> "Car name cannot be null ... Consider one from " + carsList);

如果此引用不是null,则不创建消息。



42 检查空引用并引发指定的异常


当然,一种解决方案需要直接依赖于==操作符,如下所示:

if (name == null) {
  throw new IllegalArgumentException("Name cannot be null");
}

因为没有requireNonNullElseThrow()方法,所以这个问题不能用java.util.Objects的方法来解决。抛出IllegalArgumentException或其他指定的异常可能需要一组方法,如下面的屏幕截图所示:


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ure4cICg-1657077359609)(img/fcd54997-7ebf-46d7-8eea-c80bb23be82a.png)]

让我们关注一下requireNonNullElseThrowIAE()方法。这两个方法抛出IllegalArgumentException,其中一个自定义消息被指定为String或Supplier(在null被求值为true之前避免创建):


public static <T> T requireNonNullElseThrowIAE(
    T obj, String message) {
  if (obj == null) {
    throw new IllegalArgumentException(message);
  }
  return obj;
}
public static <T> T requireNonNullElseThrowIAE(T obj,
    Supplier<String> messageSupplier) {
  if (obj == null) {
    throw new IllegalArgumentException(messageSupplier == null 
      ? null : messageSupplier.get());
  }
  return obj;
}

所以,投掷IllegalArgumentException可以通过这两种方法来完成。但这还不够。例如,代码可能需要抛出IllegalStateExceptionUnsupportedOperationException等。对于这种情况,最好采用以下方法:

public static <T, X extends Throwable> T requireNonNullElseThrow(
    T obj, X exception) throws X {
  if (obj == null) {
    throw exception;
  }
  return obj;
}
public static <T, X extends Throwable> T requireNotNullElseThrow(
    T obj, Supplier<<? extends X> exceptionSupplier) throws X {
  if (obj != null) {
    return obj;
  } else {
    throw exceptionSupplier.get();
  }
}

考虑将这些方法添加到名为MyObjects的助手类中。如以下示例所示调用这些方法:

public Car(String name, Color color) {
  this.name = MyObjects.requireNonNullElseThrow(name,
    new UnsupportedOperationException("Name cannot be set as null"));
  this.color = MyObjects.requireNotNullElseThrow(color, () ->
    new UnsupportedOperationException("Color cannot be set as null"));
}

此外,我们也可以通过这些例子来丰富MyObjects中的其他异常。



43 检查空引用并返回非空默认引用


通过if-else(或三元运算符)可以很容易地提供该问题的解决方案,如以下示例所示(作为变体,namecolor可以声明为非final,并在声明时用默认值初始化):

public class Car {
  private final String name;
  private final Color color;
  public Car(String name, Color color) {
    if (name == null) {
      this.name = "No name";
    } else {
      this.name = name;
    }
    if (color == null) {
      this.color = new Color(0, 0, 0);
    } else {
      this.color = color;
    }
  }
}


但是,从 JDK9 开始,前面的代码可以通过Objects类的两个方法简化。这些方法是requireNonNullElse()requireNonNullElseGet()。它们都有两个参数,一个是检查空值的引用,另一个是在检查的引用为null时返回的非null默认引用:

public class Car {
  private final String name;
  private final Color color;
  public Car(String name, Color color) {
    this.name = Objects.requireNonNullElse(name, "No name");
    this.color = Objects.requireNonNullElseGet(color,
      () -> new Color(0, 0, 0));
  }
}



44 检查从 0 到长度范围内的索引


首先,让我们用一个简单的场景来突出这个问题。此场景可能在以下简单类中实现:

public class Function {
  private final int x;
  public Function(int x) {
    this.x = x;
  }
  public int xMinusY(int y) {
    return x - y;
  }
  public static int oneMinusY(int y) {
    return 1 - y;
  }
}

注意,前面的代码片段没有对x和y进行任何范围限制。现在,让我们施加以下范围(这在数学函数中非常常见):


   x必须介于 0(含)和 11(不含)之间,所以x属于[0, 11)。

   在xMinusY()方法中,y必须在 0(含)x(不含)之间,所以y属于[0, x)。

   在oneMinusY()方法中,y必须介于 0(包含)和 16(排除)之间,所以y属于[0, 16)。


这些范围可以通过if语句在代码中施加,如下所示:

public class Function {
  private static final int X_UPPER_BOUND = 11;
  private static final int Y_UPPER_BOUND = 16;
  private final int x;
  public Function(int x) {
    if (x < 0 || x >= X_UPPER_BOUND) {
      throw new IndexOutOfBoundsException("..."); 
    }
    this.x = x;
  }
  public int xMinusY(int y) {
    if (y < 0 || y >= x) {
      throw new IndexOutOfBoundsException("...");
    }
    return x - y;
  }
  public static int oneMinusY(int y) {
    if (y < 0 || y >= Y_UPPER_BOUND) {
      throw new IndexOutOfBoundsException("...");
    }
    return 1 - y;
  }
}

考虑用更有意义的异常替换IndexOutOfBoundsException(例如,扩展IndexOutOfBoundsException并创建一个类型为RangeOutOfBoundsException的自定义异常)。


从 JDK9 开始,可以重写代码以使用Objects.checkIndex()方法。此方法验证给定索引是否在 0 到长度的范围内,并返回该范围内的给定索引或抛出IndexOutOfBoundsException:


public class Function {
  private static final int X_UPPER_BOUND = 11;
  private static final int Y_UPPER_BOUND = 16;
  private final int x;
  public Function(int x) {
    this.x = Objects.checkIndex(x, X_UPPER_BOUND);
  }
  public int xMinusY(int y) {
    Objects.checkIndex(y, x);
    return x - y;
  }
  public static int oneMinusY(int y) {
    Objects.checkIndex(y, Y_UPPER_BOUND);
    return 1 - y;
  }
}

例如,调用oneMinusY(),如下一个代码片段所示,将导致IndexOutOfBoundsException,因为y可以取[0, 16]之间的值:

int result = Function.oneMinusY(20);


现在,让我们进一步检查从 0 到给定长度的子范围。



45 检查从 0 到长度范围内的子范围


让我们遵循上一个问题的相同流程。所以,这一次,Function类将如下所示:

public class Function {
  private final int n;
  public Function(int n) {
    this.n = n;
  }
  public int yMinusX(int x, int y) {
    return y - x;
  }
}


注意,前面的代码片段没有对x、y和n进行任何范围限制。现在,让我们施加以下范围:

   n必须介于 0(含)和 101(不含)之间,所以n属于[0, 101]。

   在yMinusX()方法中,由x和y、x、y限定的范围必须是[0, n]的子范围。


这些范围可以通过if语句在代码中施加,如下所示:

public class Function {
  private static final int N_UPPER_BOUND = 101;
  private final int n;
  public Function(int n) {
    if (n < 0 || n >= N_UPPER_BOUND) {
      throw new IndexOutOfBoundsException("...");
    }
    this.n = n;
  }
  public int yMinusX(int x, int y) {
    if (x < 0 || x > y || y >= n) {
      throw new IndexOutOfBoundsException("...");
    }
    return y - x;
  }
}



基于前面的问题,n的条件可以替换为Objects.checkIndex()。此外,JDK9Objects类还提供了一个名为checkFromToIndex(int start, int end, int length)的方法,该方法检查给定的子范围给定的开始、给定的结束是否在 0 到给定的长度的范围内。因此,此方法可应用于yMinusX()方法,以检查x与y所限定的范围是否为 0 到n的子范围:


public class Function {
  private static final int N_UPPER_BOUND = 101;
  private final int n;
  public Function(int n) {
    this.n = Objects.checkIndex(n, N_UPPER_BOUND);
  }
  public int yMinusX(int x, int y) {
    Objects.checkFromToIndex(x, y, n);
    return y - x;
  }
}

例如,由于x大于y,下面的测试将导致IndexOutOfBoundsException

Function f = new Function(50);
int r = f.yMinusX(30, 20);

除了这个方法之外,Objects还有另一个名为checkFromIndexSize(int start, int size, int length)的方法。该方法检查给定开始时间给定开始时间加给定大小的子范围,是否在 0 到给定长度的范围内。

相关文章
|
10天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
9天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
8天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
15天前
|
Java
探索Java中的Lambda表达式
【10月更文挑战第37天】本文将带你深入理解Java的Lambda表达式,从基础语法到高级特性,通过实例讲解其在函数式编程中的应用。我们还将探讨Lambda表达式如何简化代码、提高开发效率,并讨论其在实际项目中的应用。
|
11天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
13天前
|
Java
java switch case多个条件
通过本文的介绍,我们详细探讨了Java中 `switch case`语句的多种用法和优化方法。从基本语法到合并多个条件,再到使用枚举、常量和函数优化,`switch case`语句在Java编程中提供了一种灵活且高效的控制流方式。掌握这些技巧,能够编写出更加简洁、可读性强的代码,提高开发效率和代码质量。希望本文能为您在实际开发中提供有价值的参考和指导。
38 2
|
14天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
2天前
|
Java API 数据库
Java 反射机制:动态编程的 “魔法钥匙”
Java反射机制是允许程序在运行时访问类、方法和字段信息的强大工具,被誉为动态编程的“魔法钥匙”。通过反射,开发者可以创建更加灵活、可扩展的应用程序。
|
18天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
1天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
下一篇
无影云桌面