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 到给定长度的范围内。

相关文章
|
4天前
|
存储 Java 数据管理
Java零基础-Java对象详解
【10月更文挑战第7天】Java零基础教学篇,手把手实践教学!
20 6
|
4天前
|
Java 测试技术 编译器
🎯Java零基础-Switch条件语句详解 🎯
【10月更文挑战第8天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
18 6
|
4天前
|
缓存 Java UED
Java中的多线程编程:从基础到实践
【10月更文挑战第13天】 Java作为一门跨平台的编程语言,其强大的多线程能力一直是其核心优势之一。本文将从最基础的概念讲起,逐步深入探讨Java多线程的实现方式及其应用场景,通过实例讲解帮助读者更好地理解和应用这一技术。
22 3
|
4天前
|
Java 开发者
在Java编程中,正确的命名规范不仅能提升代码的可读性和可维护性,还能有效避免命名冲突。
【10月更文挑战第13天】在Java编程中,正确的命名规范不仅能提升代码的可读性和可维护性,还能有效避免命名冲突。本文将带你深入了解Java命名规则,包括标识符的基本规则、变量和方法的命名方式、常量的命名习惯以及如何避免关键字冲突,通过实例解析,助你写出更规范、优雅的代码。
26 3
|
4天前
|
Java 程序员
在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。
【10月更文挑战第13天】在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。本文介绍了Java关键字的基本概念及其重要性,并通过定义类和对象、控制流程、访问修饰符等示例,展示了关键字的实际应用。掌握这些关键字,是成为优秀Java程序员的基础。
12 3
|
4天前
|
Java 程序员 编译器
在Java编程中,保留字(如class、int、for等)是具有特定语法意义的预定义词汇,被语言本身占用,不能用作变量名、方法名或类名。
在Java编程中,保留字(如class、int、for等)是具有特定语法意义的预定义词汇,被语言本身占用,不能用作变量名、方法名或类名。本文通过示例详细解析了保留字的定义、作用及与自定义标识符的区别,帮助开发者避免因误用保留字而导致的编译错误,确保代码的正确性和可读性。
16 3
|
4天前
|
算法 Java
在Java编程中,关键字和保留字是基础且重要的组成部分,正确理解和使用它们
【10月更文挑战第13天】在Java编程中,关键字和保留字是基础且重要的组成部分。正确理解和使用它们,如class、int、for、while等,不仅能够避免语法错误,还能提升代码的可读性和执行效率。本指南将通过解答常见问题,帮助你掌握Java关键字的正确使用方法,以及如何避免误用保留字,使你的代码更加高效流畅。
20 3
|
3天前
|
存储 安全 Java
了解final关键字在Java并发编程领域的作用吗?
在Java并发编程中,`final`关键字不仅用于修饰变量、方法和类,还在多线程环境中确保对象状态的可见性和不变性。本文深入探讨了`final`关键字的作用,特别是其在final域重排序规则中的应用,以及如何防止对象的“部分创建”问题,确保线程安全。通过具体示例,文章详细解析了final域的写入和读取操作的重排序规则,以及这些规则在不同处理器上的实现差异。
了解final关键字在Java并发编程领域的作用吗?
|
2月前
|
算法 Java 开发者
Java 编程入门:从零到一的旅程
本文将带领读者开启Java编程之旅,从最基础的语法入手,逐步深入到面向对象的核心概念。通过实例代码演示,我们将一起探索如何定义类和对象、实现继承与多态,并解决常见的编程挑战。无论你是编程新手还是希望巩固基础的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
2月前
|
机器学习/深度学习 Java TensorFlow
深度学习中的图像识别:从理论到实践Java中的多线程编程入门指南
【8月更文挑战第29天】本文将深入探讨深度学习在图像识别领域的应用,从基础理论到实际应用案例,带领读者一步步理解如何利用深度学习技术进行图像识别。我们将通过一个简单的代码示例,展示如何使用Python和TensorFlow库实现一个基本的图像识别模型。无论你是初学者还是有一定经验的开发者,都能从中获得启发和学习。 【8月更文挑战第29天】在Java世界里,线程是程序执行的最小单元,而多线程则是提高程序效率和响应性的关键武器。本文将深入浅出地引导你理解Java多线程的核心概念、创建方法以及同步机制,帮助你解锁并发编程的大门。