带你快速看完9.8分神作《Effective Java》—— 方法篇(二)

简介: 49 检查参数的有效性 50 必要时进行保护性拷贝 51 谨慎设计方法 52 慎用重载 53 慎用可变参数 54 返回空的数组或集合,不要返回null 55 谨慎返回optional 56 为所有已公开的API 元素编写文档注释

54 返回空的数组或集合,不要返回null


像如下的方法并不罕⻅:


private final List<Cheese> cheesesInStock = ...;
/**
* @return a list containing all of the cheeses in the shop,
* or null if no cheeses are available for purchase.
*/
public List<Cheese> getCheeses() {
  return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}


把没有奶酪(Cheese)可买的情况当做一种特例,这是不合常理的。这样需要在客户端中必须有额外的代码来处理null的返回值:


List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
  System.out.println("Jolly good, just the thing.");


这样做很容易出错,因为编写客户端的程序员可能忘记编写特殊情况代码来处理null返回。


下面是返回可能为空的集合的典型代码。一般情况下,这些都是必须的:


public List<Cheese> getCheeses() {
  return new ArrayList<>(cheesesInStock);
}

如果有证据表明分配空集合会损害性能,可以通过重复返回相同的不可变空集合来避免多次分配


// Optimization - avoids allocating empty collections
public List<Cheese> getCheeses() {
  return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheesesInStock);
}

数组的情况与集合的情况相同。永远不要返回null,而是返回⻓度为零的数组。


// Optimization - avoids allocating empty arrays
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
  return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}


55 谨慎返回optional


在Java 8之前,编写在特定情况下无法返回任何值的方法时,可以采用两种方法。要么抛出异常,要么返回null。这两种方式都不完美:


抛出异常代价很高,因为在创建异常时捕获整个堆栈trace

返回null有可能抛NullPointerException异常


在Java 8中,还有第三种方法来编写可能无法返回任何值的方法。Optional<T>类表示一个不可变的容器,它可以包含一个非null的T引用,也可以什么都不包含。


不包含任何内容的Optional被称为空(empty)。非空的包含值称的Optional被称为存在(present)


返回Optional的方法比抛出异常的方法更灵活、更容易使用,而且比返回null的方法更不容易出错。


例如在第30条中,有一个根据集合中元素的自然顺序计算集合最大值的方法:


public static <E extends Comparable<E>> E max(Collection<E> c) {
  if (c.isEmpty())
    throw new IllegalArgumentException("Empty collection");
  E result = null;
  for (E e : c)
    if (result == null || e.compareTo(result) > 0)
      result = Objects.requireNonNull(e);
  return result;
}


如果给定集合为空,此方法将抛出IllegalArgumentException异常。更好的替代方法是返回Optional<E>。下面是修改后的方法:


public static <E extends Comparable<E>>
  Optional<E> max(Collection<E> c) {
  if (c.isEmpty())
    return Optional.empty();
  E result = null;
  for (E e : c)
    if (result == null || e.compareTo(result) > 0)
      result = Objects.requireNonNull(e);
  return Optional.of(result);
}


将null传递给Optional.of(value)是一个编程错误,会抛NullPointerException异常。Optional.ofNullable(value)方法接受一个可能为null的值,如果传入null则返回一个空的Optional。



Stream 上的很多终止操作返回Optional。可以用Stream重写max方法,Stream的max 操作会为我们生成Optional的工作:


public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
  return c.stream().max(Comparator.naturalOrder());
}


如果方法返回一个Optional,则客户端可以选择在方法无法返回值时要采取的操作。有以下两种方式:

1. 指定默认值


String lastWordInLexicon = max(words).orElse("No words...");

2. 抛出任何适当的异常


Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);


有时候,可能会遇到这样一种情况:获取默认值的代价很高,我们希望避免这种代价。对于这些情况,Optional提供了一个方法orElseGet,传入一个Supplier<T>。



Optional还提供了isPresent()方法,可以将其视为安全阀。如果Optional包含值,则返回true;否则返回false。



当使用Stream时,经常会遇到Stream<Optional<T>>,为了推动进程还需要一个包含了非空optional中所有元素的Stream<T>。Java 8里可以这样写:



streamOfOptionals
  .filter(Optional::isPresent)
  .map(Optional::get)


并不是所有的返回类型都能从Optional的处理中获益。容器类型,包括集合、映射、Stream、数组和Optional,不应该封装在Optional中。与其返回一个空的Optional<List<T>>,不还如返回一个空的List<T>。



那么什么时候应该声明一个方法来返回Optional<T> 而不是T 呢?


如果可能无法返回结果,并且在没有返回结果,客户端还必须执行特殊处理的情况下,则应声明返回Optional <T>的方法。



使用Optional还有一些其他的注意事项:


永远不应该返回基本包装类型的Optional(小型的Boolean,Byte,Character,Short 和 Float 除外)


不适合将optional作为键、值、集合或数组中的元素


除了作为返回值之外,不要在任何其他地方中使用Optional



56 为所有已公开的API 元素编写文档注释


如果API要可用,就必须对其编写文档化。



要正确地记录API,必须在每个导出的类、接口、构造方法、方法和属性声明之前加上文档注释。如果一个类是可序列化的,应该对它的序列化形式编写文档。puiblic类不应该使用无参构造方法,因为无法为它们提供文档注释。要编写可维护的代码,还应该为所有没有被导出的类、接口、构造方法、方法和属性编写文档注释,尽管这些注释不需要像导出API元素那样完整。



方法的文档注释应该简洁地描述方法与其客户端之间的约定。这个约定应该说明方法做了什么,而不是它如何完成工作的。文档注释应该列举方法的所有前置条件以及后置条件


前置条件:为了使客户能够调用这个方法,必须要满足的条件

后置条件:调用完成之后,哪些条件必须满足


通常,每个未受检的异常都对应一个前提违例(precondition violation),要在受影响的参数的 @param 标签中指定前置条件



方法还应在文档中记录它的副作用(side effort)。例如,如果方法启动后台线程,则应该在文档里说明。



文档注释应该为每个参数都有一个 @param 标签,一个 @return 标签,以及一个 @throw 标签。



@param 或 @return 标签后面的文本应该是一个名词短语,描述参数或返回值所表示的值。@throw 标签后面的文本应该包含单词「if」。@param 、@return 或 @throw 标签后面的短语或子句都不用句号来结束。


/**
* Returns the element at the specified position in this list.
*
* <p>This method is <i>not</i> guaranteed to run in constant
* time. In some implementations it may run in time proportional
* to the element position.
*
* @param index index of element to return; must be
* non-negative and less than the size of this list
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= this.size()})
*/
E get(int index);


在此文档注释中使用了HTML标记(<p>和<i>)。Javadoc实用工具将文档注释转换为HTML。



@throw子句中的代码片段周围使用Javadoc的{@code}标签。它使代码片段以 code font(代码字体)形式呈现



@implSpec 注释描述了方法与其子类之间的约定,如果子类继承了该方法,或者通过super调用了方法,则允许子类依赖实现行为。

/**
* Returns true if this collection is empty.
*
* @implSpec
* This implementation returns {@code this.size() == 0}.
*
* @return true if this collection is empty
*/
public boolean isEmpty() { ... }

包含HTML元字符的文档,例如小于号(<),大于号(>)和 and 符号(&),是用{@literal}标签将它们包围起来:


* A geometric series converges if {@literal |r| < 1}.

文档注释在源代码和生成的文档中都应该是易读的。



每个文档注释的第一个「句子」是注释所在元素的概要描述。同一个类或接口中的两个成员或构造方法不应具有相同的概要描述。



注意概要描述是否包含句点,例如以「A college degree, such as B.S., M.S. or Ph.D.」会导致概要描述为「A college degree, such as B.S., M.S」。缩写「M.S.」中的第二个句号后面跟着一个空格。最好的解决方案是用{@literal}标签


/**
* A college degree, such as B.S., {@literal M.S.} or Ph.D.
*/
public class Degree { ... }


概要描述应该是一个动词短语,描述了该方法执行的操作。例如:


ArrayList(int initialCapacity) ——构造具有指定初始容量的空列表。

Collection.size() ——返回此集合中的元素个数。

对于类,接口和属性,概要描述应该是描述由类或接口的实例或属性本身表示的事物的名词短语。


Instant ——时间线上的瞬时点。

Math.PI ——更加接近pi的double类型数值,即圆的周⻓与其直径之比。


为泛型类型或方法写文档时,请务必记录所有类型参数:


/**
* An object that maps keys to values. A map cannot contain
* duplicate keys; each key can map to at most one value.
*
* (Remainder omitted)
*
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
public interface Map<K, V> { ... }

在记录枚举类型时,一定要记录常量:


/**
 * An instrument section of a symphony orchestra.
 */
 public enum OrchestraSection {
   /** Woodwinds, such as flute, clarinet, and oboe. */
   WOODWIND,
   /** Brass instruments, such as french horn and trumpet. */
   BRASS,
  /** Percussion instruments, such as timpani and cymbals. */
  PERCUSSION,
  /** Stringed instruments, such as violin and cello. */
  STRING;
}

在为注解类型记录文档时,一定要记录任何成员:


/**
  * Indicates that the annotated method is a test method that
  * must throw the designated exception to pass.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
  /**
  * The exception that the annotated test method must throw
  * in order to pass. (The test is permitted to throw any
  * subtype of the type described by this class object.)
  */
  Class<? extends Throwable> value();
}


无论类或静态方法是否线程安全,都应该在文档中描述其线程安全级别



Javadoc具有「继承(inherit)」方法注释的能力。如果API元素没有文档注释,Javadoc将搜索最适用的文档注释,接口文档注释优先于超类文档注释。



对于由多个相互关联的类组成的复杂API,通常需要用描述API总体架构的外部文档来补充文档注释。如

果存在这样的文档,相关的类或包文档注释应该包含到外部文档的链接。


相关文章
|
4月前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
4月前
|
Java
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
117 9
|
2月前
|
存储 Java 索引
Java快速入门之数组、方法
### Java快速入门之数组与方法简介 #### 一、数组 数组是一种容器,用于存储同种数据类型的多个值。定义数组时需指定数据类型,如`int[]`只能存储整数。数组的初始化分为静态和动态两种: - **静态初始化**:直接指定元素,系统自动计算长度,如`int[] arr = {1, 2, 3};` - **动态初始化**:手动指定长度,系统给定默认值,如`int[] arr = new int[3];` 数组访问通过索引完成,索引从0开始,最大索引为`数组.length - 1`。遍历数组常用`for`循环。常见操作包括求和、找最值、统计特定条件元素等。
|
18天前
|
存储 安全 算法
Java容器及其常用方法汇总
Java Collections框架提供了丰富的接口和实现类,用于管理和操作集合数据。
Java容器及其常用方法汇总
|
20天前
|
存储 缓存 Java
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
73 3
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
|
18天前
|
Java API
java.time常用方法汇总
`java.time` API 是从 Java 8 开始引入的时间日期处理库,旨在替代老旧的 `java.util.Date` 和 `Calendar`。它提供了更简洁、强大和灵活的方式处理日期、时间、时区及时间间隔,支持全球化和时间计算需求。API 包含获取当前时间、创建指定时间、解析和格式化字符串、进行加减运算、比较时间、获取年月日时分秒、计算时间间隔、时区转换以及判断闰年等功能。示例代码展示了如何使用这些功能,极大简化了开发中的时间处理任务。
|
2月前
|
Java
Java快速入门之类、对象、方法
本文简要介绍了Java快速入门中的类、对象和方法。首先,解释了类和对象的概念,类是对象的抽象,对象是类的具体实例。接着,阐述了类的定义和组成,包括属性和行为,并展示了如何创建和使用对象。然后,讨论了成员变量与局部变量的区别,强调了封装的重要性,通过`private`关键字隐藏数据并提供`get/set`方法访问。最后,介绍了构造方法的定义和重载,以及标准类的制作规范,帮助初学者理解如何构建完整的Java类。
|
2月前
|
Java 程序员 调度
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
69 9
|
2月前
|
安全 Java 程序员
Java面试必问!run() 和 start() 方法到底有啥区别?
在多线程编程中,run和 start方法常常让开发者感到困惑。为什么调用 start 才能启动线程,而直接调用 run只是普通方法调用?这篇文章将通过一个简单的例子,详细解析这两者的区别,帮助你在面试中脱颖而出,理解多线程背后的机制和原理。
72 12
|
2月前
|
算法 Java API
Java 方法注释:规范、实用和高质量的写法
本文深入探讨了如何编写高质量的 Java 方法注释
83 11

热门文章

最新文章