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

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


相关文章
|
3月前
|
前端开发 JavaScript Java
Java 开发中 Swing 界面嵌入浏览器实现方法详解
摘要:Java中嵌入浏览器可通过多种技术实现:1) JCEF框架利用Chromium内核,适合复杂网页;2) JEditorPane组件支持简单HTML显示,但功能有限;3) DJNativeSwing-SWT可内嵌浏览器,需特定内核支持;4) JavaFX WebView结合Swing可完美支持现代网页技术。每种方案各有特点,开发者需根据项目需求选择合适方法,如JCEF适合高性能要求,JEditorPane适合简单展示。(149字)
380 1
|
13天前
|
Java 编译器 Go
【Java】(5)方法的概念、方法的调用、方法重载、构造方法的创建
Java方法是语句的集合,它们在一起执行一个功能。方法是解决一类问题的步骤的有序组合方法包含于类或对象中方法在程序中被创建,在其他地方被引用方法的优点使程序变得更简短而清晰。有利于程序维护。可以提高程序开发的效率。提高了代码的重用性。方法的名字的第一个单词应以小写字母作为开头,后面的单词则用大写字母开头写,不使用连接符。例如:addPerson。这种就属于驼峰写法下划线可能出现在 JUnit 测试方法名称中用以分隔名称的逻辑组件。
127 4
|
22天前
|
算法 安全 Java
除了类,Java中的接口和方法也可以使用泛型吗?
除了类,Java中的接口和方法也可以使用泛型吗?
78 11
|
18天前
|
编解码 Java 开发者
Java String类的关键方法总结
以上总结了Java `String` 类最常见和重要功能性方法。每种操作都对应着日常编程任务,并且理解每种操作如何影响及处理 `Strings` 对于任何使用 Java 的开发者来说都至关重要。
164 5
|
26天前
|
Java 开发者
Java 函数式编程全解析:静态方法引用、实例方法引用、特定类型方法引用与构造器引用实战教程
本文介绍Java 8函数式编程中的四种方法引用:静态、实例、特定类型及构造器引用,通过简洁示例演示其用法,帮助开发者提升代码可读性与简洁性。
|
2月前
|
算法 Java 开发者
Java 项目实战数字华容道与石头迷阵游戏开发详解及实战方法
本文介绍了使用Java实现数字华容道和石头迷阵游戏的技术方案与应用实例,涵盖GUI界面设计、二维数组操作、游戏逻辑控制及自动解法算法(如A*),适合Java开发者学习游戏开发技巧。
210 46
|
3月前
|
Java 索引
Java ArrayList中的常见删除操作及方法详解。
通过这些方法,Java `ArrayList` 提供了灵活而强大的操作来处理元素的移除,这些方法能够满足不同场景下的需求。
425 30
|
2月前
|
算法 Java
Java语言实现链表反转的方法
这种反转方法不需要使用额外的存储空间,因此空间复杂度为,它只需要遍历一次链表,所以时间复杂度为,其中为链表的长度。这使得这种反转链表的方法既高效又实用。
241 0
|
3月前
|
安全 Java API
Java 17 及以上版本核心特性在现代开发实践中的深度应用与高效实践方法 Java 开发实践
本项目以“学生成绩管理系统”为例,深入实践Java 17+核心特性与现代开发技术。采用Spring Boot 3.1、WebFlux、R2DBC等构建响应式应用,结合Record类、模式匹配、Stream优化等新特性提升代码质量。涵盖容器化部署(Docker)、自动化测试、性能优化及安全加固,全面展示Java最新技术在实际项目中的应用,助力开发者掌握现代化Java开发方法。
147 1
|
2月前
|
存储 Java 数据处理
Java映射操作:深入Map.getOrDefault与MapUtils方法
结合 `getOrDefault`方法的简洁性及 `MapUtils`的丰富功能,Java的映射操作变得既灵活又高效。合理地使用这些工具能够显著提高数据处理的速度和质量。开发人员可以根据具体的应用场景选择适宜的方法,以求在性能和可读性之间找到最佳平衡。
112 0