如何更好地编写Java方法

22 篇文章 0 订阅
18 篇文章 0 订阅

49. 检查参数的有效性

编写方法和构造器的时候,要考虑参数的限制,把限制通过文档注明,并通过显示的方式来检查这些限制。

否则,后果是什么

明白为什么不好有时候比怎么做才好更重要。
不检查参数的问题在于:

  1. 方法可能在处理过程中失败,产生令人费解的结果。当然这是最显而易见的问题。
  2. 更糟糕的是,方法返回了,但是结果是错误的。
  3. 更糟糕的是,方法返回了正确结果,但是未来在某个时候,突然给你一个惊喜,而你完全找不到问题出在什么地方。这破坏了所谓的失败原子性(failure atomicity)

最佳实践

对于共有方法,应该使用throw显示地抛出异常,对于私有的方法,应该使用assert。这很好理解,因为保证公有方法调用是客户端的责任,而私有方法,则是我们的责任,assert就够排查了。
首先对于共有方法,以下值得注意的地方:

  • 文档中说明参数的限制,以及抛出的异常是为什么
  • 显示检查参数,抛出适当的异常
  • m非空检查隐藏在m.signum()的调用中,该异常应该放在BigInteger的类级文档上,避免每一个方法都注释该异常。
/**
 * Returns a BigInteger whose value is (this mod m). This method
 * differs from the remainder method in that it always returns a
 * non-negative BigInteger.
 *
 * @param m the modulus, which must be positive
 * @return this mod m
 * @throws ArithmeticException if m is less than or equal to 0
 */
public BigInteger mod(BigInteger m) {
    if (m.signum() <= 0)
        throw new ArithmeticException("Modulus <= 0: " + m);

    ... // Do the computation
}

对于私有方法,使用assert显示检查参数。

// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length) {
    assert a != null;
    assert offset >= 0 && offset <= a.length;
    assert length >= 0 && length <= a.length - offset;
    ... // Do the computation
}

然后是,Java中在Objects类中一些有用的方法。

List<Point> pointList = Objects.requireNonNull(points, "points不能为空");

50. 必要时进行保护性拷贝

一个类、某数据结构,如果接受从客户端(外部)得到的可变组件,或者返回内部的可变组件时,就应该进行必要的保护性拷贝。如果确定客户端不会变动,或者保护性拷贝的成本过高,那么必须在文档注明。
其中的关键是:

  • 接受外部的可变组件的时候,就必须意识到如果这个组件在传入之后被改变,是否可以忍受
  • 返回内部可变组件的时候同样需要谨慎地考虑

对构造器可变参数进行保护性拷贝

以下代码有几个很有意思的点需要注意:

  • 拷贝一份,而不直接适用外部传入的可变对象
  • 拷贝的时候不要用clone,除非确定该类不会被子类化(子类化覆盖clone可以破环我们的保护性拷贝)
  • 合法性校验在保护性拷贝之后,虽然奇怪,但是本身合法性校验应该针对内部拷贝的对象,而不是外部的引用,否则可能混过我们的安全校验
  • JDK1.8之后提供了不可变的Date类,Instant,在大部分情况下,优先选择它。
// Repaired constructor - makes defensive copies of parameters
// 构造器进行保护性拷贝
public Period(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());
    if (this.start.compareTo(this.end) > 0)
        throw new IllegalArgumentException(
            this.start + " after " + this.end);
}

返回内部可变组件时要考虑

应该使用保护性拷贝,另外,下面代码中可以安全地使用clone,因为我们可以确保内部是Date,而不是其他恶意的子类。

// Repaired accessors - make defensive copies of internal fields
public Date start() {
    return new Date(start.getTime());
}
public Date end() {
    return new Date(end.getTime());
}

顺便重复一下15条给数组提供保护性拷贝的两种方式:

  • clone
  • 提供不可变视图(注意,内部并未进行深复制)
// 维护一个不可变长的备份
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
	Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

// 返回copy
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
	return PRIVATE_VALUES.clone();
}

51. 谨慎设计方法签名

  • 方法的命名,认真花一些时间取一个合适的名字是值得的,并保持全局统一
  • 不要为了便利提供方法,方法多会影响类库API的可读性,每个方法应该尽其所能。看起来和“方法不要超过一页”好像有点矛盾,因此把方法一通封装,导出多个子方法,这种方式看起来带了的坏处更多
  • 避免过长的参数列表,一方面出于可读性保证,另一方面,太多参数测试也不方便。
    • 正交分解方法——比较难理解,意思是把方法按功能划分成尽量不相关的小方法,举个例子List API提供了indexOf, lastIndexOf, subList却没有提供“找到列表的第一个索引和最后一个索引之间的子列表”的方法,该方法可以用上述正交子方法导出
    • 参数实体类
    • builder模式
  • 参数类型接口优先,拓展性更好
  • boolean类型参数用两个元素的enum类型代替,好处是命名有意义,而且方便拓展

52. 明智使用重载

能够使用重载方法的时候不一定要使用重载,因为事实上重载的机制是比较容易引起歧义的(或者让程序员无法把握),所以建议是:

  1. 理想的情况下,参数数目不一致的时候,不用重载,用另命名来代替
  2. 对于构造器方法,无法通过命名来解决,则一旦参数数量一样,必须保证有至少一个参数,类型完全不相关(指互相不为子类,无法互相转型)。否则应该考虑工厂模式来解决问题。
  3. 如果实在参数数目一直,而且会混淆,那么必须保证方法的行为一致。

之所以要求如此激进——尽可能保证重载方法参数数量不一致,那就完全不会出错,那是因为重载本身比较复杂。

重载的问题

关键问题在于,重载从底层来看,是在编译期间决定,通过参数的静态类型(非实际的子类)决定的,而且它会选择“更加合适”的版本。例如以下代码,它选择最终会输出三次"Unknown Collection"。

// Broken! - What does this program print?
public class CollectionClassifier {
    public static String classify(Set<?> s) {
        return "Set";
    }
    public static String classify(List<?> lst) {
        return "List";
    }
    public static String classify(Collection<?> c) {
        return "Unknown Collection";
    }
    public static void main(String[] args) {
        Collection<?>[] collections = {
                new HashSet<String>(),
                new ArrayList<BigInteger>(),
                new HashMap<String, String>().values()
        };
        for (Collection<?> c : collections)
            System.out.println(classify(c));
    }
}

所谓选择“更加合适”的版本,我们来看一个例子。下面代码重载顺序和方法声明顺序是一样的——参数是char,则调用;否则转int;否则转long;否则自动装箱;否则转接口类型;否则转Object;最后才是可变长参数,它的重载优先级最低。

class Overload {
    public static void sayHello(char arg){
        System.out.println("Hello char");
    }
    public static void sayHello(int arg){
        System.out.println("Hello int");
    }
    public static void sayHello(long arg){
        System.out.println("Hello long");
    }
    public static void sayHello(Character arg){
        System.out.println("Hello character");
    }
    public static void sayHello(Serializable arg){
        System.out.println("Hello Serializable ");
    }
    public static void sayHello(Object obj){
        System.out.println("Hello object");
    }
    public static void sayHello(char ...arg){
        System.out.println("Hello char ...");
    }
    public static void main(String[] args) {
        sayHello('a');
    }
}

53. 明智地使用可变参数

可变参数是一种比较便利的形式,它提供一个数组接受不定数量的参数。使用可变参数的时候,要先包含所有必要参数,然后还要考虑可变参数的性能(数组初始化)。

包含必要参数

首先来看以下案例。
这个代码是没有问题的,但它的不好的地方在于:

  • 必须显示地检查参数合法性,如果不传入任何参数,则不会在编译期报错,而会在运行期报错
  • 可以更加简洁
// The WRONG way to use varargs to pass one or more arguments!
static int min(int... args) {
    if (args.length == 0)
        throw new IllegalArgumentException("Too few arguments");
    int min = args[0];
    for (int i = 1; i < args.length; i++)
        if (args[i] < min)
            min = args[i];
    return min;
}

它是对的,但是它可以优化。这也是可变参数的使用的技巧——首先包含所有必要的参数。

// The right way to use varargs to pass one or more arguments
static int min(int firstArg, int... remainingArgs) {
    int min = firstArg;
    for (int arg : remainingArgs)
        if (arg < min)
            min = arg;
    return min;
}

考虑可变参数的性能

可变参数默认会初始化一个数组接收参数,这样的话,在某些情况下会有性能问题。例如,某方法95%的调用都是3个参数以内,这个时候通常的最佳实践是:

public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
public void foo(int a1, int a2, int a3, int... rest) { }

54. 返回空集合,而不是null

永远不要返回null,而不返回零长度的集合(包括数组)。这样没有任何性能优势,还会容易出错,让API更加难用。
例如以下案例:

/**
 * @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);
}
// 客户端必须增加额外的判断
if (cheeses != null && cheeses.contains(Cheese.STILTON))
        System.out.println("Jolly good, just the thing.");

返回null的缺点:

  • 要求客户端必须要额外的代码来处理
  • 这种问题时间长了可能被忘记,或忘记处理但是暂时正常运行。

最佳实践:返回集合

这点性能开销可以忽略不记。

//The right way to return a possibly empty collection
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);
}

最佳实践:返回数组

//The right way to return a possibly empty array
public Cheese[] getCheeses() {
    return cheesesInStock.toArray(new Cheese[0]);
}

这是一种常见的写法,但是你可能会好奇new Cheese[0]是干嘛的。简单来说:

  • 提供返回类型信息
  • 如果列表长度为0,则直接返回该零长度数组实例

源码就是这么做的:

public <T> T[] toArray(T[] a) {
    int size = size();
    if (a.length < size)
        return Arrays.copyOf(this.a, size,
                             (Class<? extends T[]>) a.getClass());
    System.arraycopy(this.a, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

同样的,几乎没有用的优化,除非分析到问题就是在这:

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

顺便一提,不要尝试提前分配,实验表明这个只会影响效率。

// Don’t do this - preallocating the array harms performance!
return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);

55. 明智地返回optional

当方法需要返回“空值”,而这个空值又希望让调用者去处理的时候,可以选择返回optional。但需要注意它是包装了一层,有一些性能方面的影响。此外,不要用optional做返回值以外它其他用途(map中的键值,集合,数组中的元素等)。
理解上述建议,我认为应该先了解optional,并且知道它的最佳实践。

Optional

JDK8新增了Optional类,其实只是一层很浅的包装,有时候会让人觉得多此一举,根本没有必要。那其实是没理解好optional的设计。
简单来说,Optional是用函数式编程形式去减少大量的空值判断逻辑。
这一点需要一个案例来理解:

User user = ...
if (user != null) {
    String userName = user.getUserName();
    if (userName != null) {
        return userName.toUpperCase();
    } else {
        return null;
    }
} else {
    return null;
}

// 使用optional
User user = ...
Optional<User> userOpt = Optional.ofNullable(user);

return userOpt.map(User::getUserName)
            .map(String::toUpperCase)
            .orElse(null);

Optional本身在map,filter等等方法已经提供了对空值的处理,支持函数式的编程写法,虽然只是一层包装的,但可以使代码集中于业务。
Optional的封装很轻量,所以容易用错。

Optional常见方法

工厂方法

of(T), ofNullable(T), empty(T)用于新建optional对象,注意of()方法如果为参数为null会抛出异常,所以需要确定不会为null才使用,否则用ofNullable()

默认值

ofElse(T)如果调用Optional对象为空,返回传入的默认值。
ofElseGet(Supplier<? **extends **T> other)参数为lambda,如果为空执行获得默认值,这个方法名应该叫ofElseCompute()
orElseThrow(Supplier<? **extends **X> exceptionSupplier)如果为空,抛出指定的异常

流式方法

map(Function<? **super **T, ? **extends **U> mapper),如果不为空对内部应用mapper,并返回新的Optional对象
filter(Predicate<? **super **T> predicate),如果不为空,对内部使用predicate过滤
ifPresent(Consumer<? **super **T> consumer),如果不为空,应用consumer

不推荐使用方法

isPresent(),判断内部是否为空
get(),获得内部value
这两个方法是兜底用的,一般不推荐使用,否则Optional就完全没有意义。例如:

Optional<User> userOpt = Optional.ofNullable(user);
if (userOpt.isPresent()) {
    User user = userOpt.get();
    // do something...
} else {
    // do something...
}

和if-else就完全没有区别,isPresent()本身就是对if-else的封装。

if (user != null) {
    // do something...
} else {
    // do something...
}

Optional的最佳实践

有了上述知识,可以讨论Optional的正确使用方式。

  • Optional是用函数式编程形式去减少大量的空值判断逻辑
  • 只作为返回值使用,它暗示调用者(客户端)可以使用Optional去处理null值。

56. 为所有导出的API元素编写文档注释

文档注释是维护代码和文档同步的最方便的工具,要为API编写统一风格、符合风格的文档注释,并且Java文档注释支持任何HTML元素,记得为非HTML语法的符号转义。以下是一些最佳实践。
支持HTML元素可能会引起一些易读性问题,原则是导出文档和注释都应该是易于阅读的,如果无法兼顾,前者更为重要。

方法注释

方法文档注释应该描述它和客户端(调用者)的约定。

  • 说明这个方法做了什么,而不是怎么做。
  • 调用这个方法要满足什么前置条件和后置条件。
  • 方法的副作用,例如不得已需要返回可变组件(提供保护性拷贝代价太大,或者证明客户端不会错误使用该引用)需要说明。
  • 提供@param, @return, @throws说明。@throws应该包含“如果……”,说明什么条件下抛出什么异常。

案例:

/**
 * 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);
@implSpec注释

@implSpec是为了描述方法和子类之间的约定,注明子类实现该方法应该注意什么。例如在19条中,为了继承而设计的类,实际上不正确的实现会有安全问题,破坏封装性,这个时候应该在父类中用该注释说明,要求子类实现必须遵守约定。
案例:

/**
 * 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() { ... }
转义

因为Java文档注释支持任何HTML元素,记得为非HTML语法的符号转义。

/**
 * 通用转义:
 * A geometric series converges if {@literal |r| < 1}.
 * 专门用于代码:
 * This implementation returns {@code this.size() == 0}.
 */

概要描述

文档的第一句话应该非常精炼,整个注释区域的内容。

  • 如果是方法,说明返回了什么,做了什么。
  • 如果是字段,对象,说明是什么。
说明线程安全性和可序列化性
为泛型、枚举、注解提供注释

泛型类型都是什么。

/**
 * 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> { ... }
    When documenting an enum type, be sure to

枚举,每个常量都简要说明是什么。
注解类型,指出该注解有什么用,什么场合使用。注解类的方法当作字段来注释,指明它是什么。

/**
 * 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();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值