带你快速看完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总体架构的外部文档来补充文档注释。如

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


相关文章
|
13天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
53 4
|
23天前
|
Java API
Java 对象释放与 finalize 方法
关于 Java 对象释放的疑惑解答,以及 finalize 方法的相关知识。
43 17
|
17天前
|
Java 测试技术 Maven
Java一分钟之-PowerMock:静态方法与私有方法测试
通过本文的详细介绍,您可以使用PowerMock轻松地测试Java代码中的静态方法和私有方法。PowerMock通过扩展Mockito,提供了强大的功能,帮助开发者在复杂的测试场景中保持高效和准确的单元测试。希望本文对您的Java单元测试有所帮助。
32 2
|
1月前
|
算法 Java Linux
java制作海报二:java使用Graphics2D 在图片上合成另一个照片,并将照片切割成头像,头像切割成圆形方法详解
这篇文章介绍了如何使用Java的Graphics2D类在图片上合成另一个照片,并将照片切割成圆形头像的方法。
49 1
java制作海报二:java使用Graphics2D 在图片上合成另一个照片,并将照片切割成头像,头像切割成圆形方法详解
|
25天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
27天前
|
Java 大数据 API
别死脑筋,赶紧学起来!Java之Steam() API 常用方法使用,让开发简单起来!
分享Java Stream API的常用方法,让开发更简单。涵盖filter、map、sorted等操作,提高代码效率与可读性。关注公众号,了解更多技术内容。
|
25天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
25天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
16 1
|
25天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1
|
25天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
24 1