本章包括 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 != null
和i -> 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
可以通过这两种方法来完成。但这还不够。例如,代码可能需要抛出IllegalStateException
、UnsupportedOperationException
等。对于这种情况,最好采用以下方法:
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
(或三元运算符)可以很容易地提供该问题的解决方案,如以下示例所示(作为变体,name
和color
可以声明为非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 到给定长度的范围内。