第8章:方法

第49条 检查参数的有效性

49.1 检查参数有效性
  1. 方法和构造器,对于传递给他们的参数的限制,应该在方法体的开头就进行检查,并在文档中清楚地指明这些限制
  2. 如果方法没有检查传入它的参数
    1. 处理过程中失败,并产生令人费解的异常
      1. 比如我们使用第三方类库中的某个方法,抛出了一个该方法文档中所未指明的异常,我们不了解到底是什么原因导致的,可能需要查看源码才能明白
    2. 该方法可以正常返回,但得到错误的结果
    3. 该方法可以正常返回,但使得某个对象处于被破坏的状态,在将来某个时间点,和此段代码完全无关的代码中失败,并抛出异常,也就是违背失败原子性
      1. 失败原子性:失败的方法调用,应该使对象保持在被调用之前的状态,具有这种属性的方法,被称为具有失败原子性
//例如Stack的pop方法,加上了size==0检查,保证了失败原子性
//如果不这样做,那么虽然本方法中也会抛出异常,但该Stack对象的size属性就会变为负数,导致在之后再调用该对象进行操作,仍然会抛出异常,无法正常工作
//抛出异常的代码,和导致size小于0的代码完全无关,我们很难根据之后抛出的异常,找到导致异常(size<0)的原因
public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}
  1. public和protected方法,应该用Javadoc的@throws标签在文档中说明违反参数值限制时会抛出的异常,这种异常通常为
    1. IllegalArgumentException
    2. IndexOutOfBoundsException
    3. NullPointerException
//以下为BigInteger类中mod方法的文档注释,这里就指明了转入的参数m,在小于等于0时,应抛出ArithmeticException,因此该方法的方法体开头就对m进行了检查,并抛出文档注释中说明的ArithmeticException
/**
 * 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) {
	//1. 该方法注释中,没有说明当m为null时候,会抛出空指针异常,因为这个异常在class-level的注释中已经指定了
	//2. 所谓class-level注释是Class定义上面的那些注释,而不在方法上面
	//3. class-level注释表示该类中所有公有方法中的所有参数,都遵循该注释内容,避免了在每个方法中都加上对抛出NullPointerException异常的注释
	//4. 可以使用@Nullable注解,表示某个参数允许为null传入,还有很多注释也有类似功能
    if (m.signum() <= 0)
        throw new ArithmeticException("Modulus <= 0: " + m);
}
  1. Java7后增加Objects.requireNonNull可以替代手工进行null检查
//java7之前
void print(String m){
    if(m==null){
        throw new NullPointerException();
    }
}
//java7以后
void print(String m){
    //m为null时候抛出异常
    Objects.requireNonNull(m);
    //甚至可以指定异常详情
    Objects.requireNonNull(m,"m不能为空");
}
  1. java9中Objects又增加了替代手工进行范围检查的几个方法
//一般用于处理list和array
//index+1超出length抛异常
int checkIndex(int index, int length)
//不检查toIndex是否有效
int checkFromToIndex(int fromIndex, int toIndex, int length)
//不检查fromIndex+size是否有效
int checkFromIndexSize(int fromIndex, int size, int length) 
  1. 断言:assert关键字
    1. assert一般用于程序调试
    2. 断言就是程序中的一条语句,它对一个boolean表达式进行检查,为真继续执行,为假抛出AssertionError
    3. 必须通过-ea参数,来开启断言功能
    4. 断言可以保证在只在测试阶段(-ea)生效,生产环境失效
    5. 断言不生效时,不会有成本开销
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;
}
  1. 对于private方法,因为你是包的创建者,也就是只有你自己写代码中可以调用该方法,因此你可以确保正确的参数传入,在测试时使用断言来对传入参数进行检查,到上生产环境时,不必像使用if-else或swtich时,将这些代码删除,生产环境断言不开启,自然也不会消耗性能
  2. 对于有些参数,方法本身没用到,却被保存起来供以后使用
    1. 普通方法:检验参数有效性可以避免调试工作变的复杂,因为当后面使用时抛出的异常,我们很难找到是前面哪段代码所导致
    2. 构造方法:可以避免构造出来的对象违反了这个类的约束条件
//传入的参数a当时没用到,只是保存起来,如果方法开头不对a进行非空检查,当使用返回的这个List时,就会报错
//而我们又很难通过该报错信息定位到这个List的来源
static List<Integer> intArrayAsList(int[] a) {
    Objects.requireNonNull(a);
	// The diamond operator is only legal here in Java 9 and later
	// If you're using an earlier release, specify <Integer>
    return new AbstractList<>() {
        @Override
        public Integer get(int i) {
            return a[i]; // Autoboxing (Item 6)
        }

        @Override
        public Integer set(int i, Integer val) {
            int oldVal = a[i];
            a[i] = val; // Auto-unboxing
            return oldVal; // Autoboxing
        }

        @Override
        public int size() {
            return a.length;
        }
    };
}
49.2 不必检查参数有效性的场景
  1. 有效性检查工作非常耗费资源,或根本不切合实际,并且有效性检查会在计算过程中完成(隐式有效性检查)
    1. Collections.sort(List),必须确保List中元素可以相互比较
    2. 但提前检查这些元素是否可以相互比较没什么意义
  2. 不加选择地依赖隐式有效性检查会导致失败原子性的丧失
  3. 如果隐士有效性检查抛出的异常,与方法文档中标明的异常不符,应将计算时产生的异常,转换为文档中声明的异常
49.3 最佳实践
  1. 不要对参数任意限制,而是应该写出对所有参数值都通用的方法
  2. 应将对参数的限制写到文档中,并且在方法体开头就显式的检查这些参数,从而进行限制

50 必要时进行保护式拷贝

50.1 保护性拷贝的作用
  1. 防止客户端恶意破坏类的约束条件
  2. 对API不了解的程序员,错误使用类,导致约束条件被破坏
50.2 破坏约束条件
  1. Period
import java.util.Date;

public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = start;
        this.end = end;
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException(
                    start + " after " + end);
    }

    public Date start() {
        return start;
    }

    public Date end() {
        return end;
    }
}
  1. Client
//客户端通过修改传入的end对象的值,从而使Period对象中的end属性值比start属性值更早,违反了Period规定的约束
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);
50.3 解决方案
50.3.1 使用不可变的类Instant、LocalDateTime、ZonedDateTime替代可变的Date

针对此例可以,但如果某些类中,必须使用可变的成员变量,就需要进行保护性拷贝,来防止成员被篡改,从而破坏类原本定义的约束条件

50.3.2 进行保护性拷贝
  1. 保护性拷贝需要在参数的有效性检查之前进行,而且有效性检查需要针对的是拷贝之后的对象,而不是原始传入对象
    1. “危险阶段”:也被称为Time-Of-Check/Time-Of-Use,或TOCTOU攻击,表示从检查参数开始到拷贝结束(如果先检查再拷贝)这段期间
    2. 先执行拷贝,后检查,可以避免危险阶段期间,其他线程改变类的参数
public Period(Date start, Date end) {
//        this.start = (Date) start.clone();
//        this.end = (Date) end.clone();
    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);
}
  1. 对于参数类型可以被不可信任方子类化的参数,不要使用clone进行保护性拷贝
    1. Period构造函数中,传入的start是有可能是Date的子类型的对象,而Date子类型的对象是可以随意定义的,有可能是不安全的,这种情况,不应该使用Date.clone方法,进行保护性拷贝
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class DateSon extends Date {
    public List<Date> list = new ArrayList<>();
    public DateSon(){
        super();
        list.add(this);
    }
    public DateSon(long date){
        super(date);
        list.add(this);
    }
    @Override
    public Object clone() {
        return list.get(0);
    }

    public static void main(String[] args) throws InterruptedException {
        Date start = new DateSon(System.currentTimeMillis());
        TimeUnit.SECONDS.sleep(5);
        Date end = new DateSon(System.currentTimeMillis());
        Period p = new Period(start,end);
        System.out.println(p.start()+","+p.end());
        //1. Period中如果使用Date的clone进行保护性拷贝,此处仍然能将Period对象中的end属性调整的比start还要提前
        //2. 因为DateSon恶意重写了clone方法,将DateSon自身的引用返回给了客户端
        end.setYear(78);
        System.out.println(p.start()+","+p.end());
    }
}

  1. 还需对属性的getter方法进行修改,使其返回可变内部域的保护性拷贝
//修改getter方法时,可以使用clone方法进行保护性拷贝,因为在构造器中已经限制了start和end一定是Date的实例
//item 13中描述过,最好使用构造器或静态工厂完成对象拷贝的功能
public Date start() {
    return new Date(start.getTime());
}

public Date end() {
    return new Date(end.getTime());
}
//如果不这样做,客户端可以先获取属性的引用,再通过该引用修改属性的值,从而破坏类的约束(end必须大于start)
p.end().setYear(78);
System.out.println(p.start()+","+p.end());
50.4 使用保护性拷贝的场景
  1. 当自己编写的类无法容忍对象进入数据结构之后发生变化
    1. 例如需要客户端提供的对象引用作为自己编写的类中的一个Set类型的成员的元素
    2. 并且无法接受插入到这个Set中的元素值被改变
  2. 对数组进行保护性拷贝:item15中讲过
//做法1:使公共数组私有并添加一个公共的不可变列表
private static final Thing[] PRIVATE_VALUES = { ... };

public static final List<Thing> VALUES =

Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
//做法2:将数组设置为private,并添加一个返回私有数组拷贝的公共方法private static final Thing[] PRIVATE_VALUES = { ... };

public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}
50.5 最佳实践
  1. 只要有可能,都应使用不可变的对象作为对象内部的组件,这样就不必为保护性拷贝操心
    1. 例如Java8后使用Instant、LocalDateTime、ZonedDateTime替代可变的Date
    2. Java8之前,将start和end定义为long型,保存Date.getTime的值,而不使用Date类型引用
  2. 可以不使用保护性拷贝的情况
    1. 如果类信任它的调用者不会修改内部的组件,这种情况只需在类的文档中清楚的说明调用者不应修改其相关属性值,来替代保护性拷贝
      1. 例如类及其客户端处于同一包中,因为都是自己定义的,处于掌控之中
    2. 类所包含的方法或构造器,调用他们时,如果移交对象的控制权,而且客户端破坏了类的约束条件不会破坏除客户端外的其他对象
      1. 例如包装类,就算客户端篡改了beverage的值,也只会导致客户端自身受到影响
//包装类具体实现
package decorator.pattern;
public class Mocha extends CondimentDecorator{
	Beverage beverage;
	public Mocha(Beverage beverage){
		this.beverage = beverage;
	}
	@Override
	public String getDescription() {
		return beverage.getDescription()+", Mocha";
	}
	@Override
	public double cost() {
		//方法调用时,将控制权移交给了传入的beverage指向的对象,没法进行保护性拷贝
		return .20+beverage.cost();
	}
}
  1. 结论
    1. 一个类包含可变组件,且该可变组件来自客户端(构造器)或可以返回给客户端(getter),这个类就必须保护性拷贝这些组件
    2. 如果拷贝成本过高,且类信任其客户端不会恶意修改组件,只需在文档中指明客户端不应修改收到影响的组件,由此来替代保护性拷贝

第51条 谨慎设计方法签名

51.1 谨慎地选择方法的名称
  1. 应遵守标准命名习惯:参照item68
    1. 方法名应易于理解、与同一包中其他名称风格一致
    2. 应与大众认可的名称相一致
51.2 不要过于追求便利的方法
  1. 这里的便利方法,我理解就好像是Collections.synchronizedList(),可以便利的帮你创建一个同步的List,如果没有该方法,你可能需要这样创建一个同步的List
//实际上就是synchronizedList的具体实现
return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
  1. 不应过于追求便利方法的原因
    1. 方法太多使类难以学习、使用、文档化、测试、维护,对于接口更是如此
    2. 应该对类或接口支持的每一个动作,都提供一个功能齐全的方法,当某个方法特别常用,才考虑为其提供便利方法
51.3 避免过长的参数列表
  1. 参数列表应该不超过4个
  2. 因为列表太长不好记
  3. 当列表过场,而且这个长的列表中,有很多参数类型都相同,问题会更大,使用该方法的用户有可能不小心弄错参数的顺序,但程序仍然可以编译并运行,但结果却是错的
//调用该方法时,如果错误的将一个age当做了id传入,程序能编译,能运行,但结果不是想要的
public void print(long id,long age,long lenght,long oppacct,long acct){

}
51.3.1 解决方案

正交性(orthogonality):改变A不会导致B改变,就说A和B正交,编程中一般指执行一条指令时,除了该指令外什么都不会发生
功能-权重比(power-to-weight ratio):API能够实现功能的多少/学习API耗费的精力,功能权重比高,意味着API只提供较少的public方法,却能实现大量的功能,一般方法间正交性越强,其功能-权重比也会越高

  1. 把方法分解为多个方法,每个方法只需要长参数列表中的几个参数,这样可能会导致方法数增加,但通过提升方法间的正交性,可以最大化减少方法的数量(提升功能-权重比)
    1. 例如List接口中没提供获取其某个子列表中的某个元素的索引的方法,可以想象这个方法正常需要三个参数,子列表起始位置、子列表终止位置、以及具体想获取哪个元素的索引
    2. List将这个功能拆成了两个方法,先通过需要两个参数的subList方法,获取子列表,再通过需要一个参数的indexOf,获取某元素在子列表中的索引
  2. 如果一个频繁出现的参数序列,可以被看做是代表了某个独特的实体,就可以使用创建辅助类的方法
//rank为值,suit为花色,rank和suit为参数序列
public void print(String rank,String suit){

}
//可以改为如下写法
public class Card(){
	private String rank;
	private String suit;
}

public void print(Card card){

}
  1. Builder模式
51.4 对于参数类型,优先使用接口而不是类
  1. 例如对于方法的输入参数,应该使用Map而不是HashMap,这样可以保证该方法可以接收任何类型的Map实现,不然的话,客户端如果数据本来以TreeMap存放,还得先通过拷贝操作将TreeMap转为HashMap才能使用该方法
51.5 使用只有两个元素的枚举类型替代boolean变量
//温度计
public class Thermometer {
    //1. newInstance(boolean b)没有newInstance(TemperatureScale b)好
    //2. 后者更容易阅读和编写
    //3. 后者更容易添加新的选项,例如为TemperatureScale增加一个KELVIN(开氏度),如果为boolean还得重新增加一个静态工厂方法
    //4. 还可以为每个温标常量提供一个方法,传入一个double值,用于转换为对应摄氏度的值
    public static Thermometer newInstance(boolean b) {
        //简单创建对象只为编译不报错,实际上创建对象时,会根据传入的参数值不同,得到不同的对象
        return new Thermometer();
    }

    public static Thermometer newInstance(TemperatureScale b) {
        return new Thermometer();
    }
}
//温标
enum TemperatureScale {
    //华氏温度,摄氏温度
    FAHRENHEIT, CELSIUS
}

第52条 慎用重载

52.1 胡乱使用重载的危害
  1. 乱用重载所导致的重载可能在运行时候发生怪异错误才会显现出来,导致很多程序员无法诊断这种错误
import java.math.BigInteger;
import java.util.*;

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()
        };
        //classify方法本意是根据传入该方法的不同类型,来判断具体是哪种集合
        for (Collection<?> c : collections)
            //但对于方法调用时,认为c都是Collection<?>类型,因此三次打印都是"Unknown Collection"
            //直到真正打印时,才发现没有得到想要的结果
            System.out.println(classify(c));
    }
}
  1. 可以用如下方法替代
public static String classify(Collection<?> c) {
    return c instanceof Set ? "Set" :
            c instanceof List ? "List" : "Unknown Collection";
}
52.2 如何才算胡乱使用重载
52.2.1 根本不同

“根本不同(radically different)”:一个对象不可能是两个根本不同的类的实例,这样程序员使用这种重载方法时,就一定能确定,自己使用的是具体哪个方法

  1. 如果两个类(不包括接口)互相不是对方的后代,这两个类就是不相关的(unrelated),而不相关的两个类,就属于根本不同
  2. 数组和Object之外的其他类,都属于根本不同
  3. 数组和Serializable、Cloneable之外的其他接口,也属于根本不同
52.2.1 如何才算不胡乱使用重载
  1. 保守来讲,永远不应该导出两个具有相同参数数目的重载方法
    1. 如果方法使用了可变参数,保守的策略就是,根本不要重载它,因为其任何同名方法,都有可能与其使用相同个数个参数
  2. 如果必须有两个相同功能方法使用相同数目的参数列表
    1. 对于普通方法:可以考虑为第二个方法换个名字
      1. 例如对于ObjectOutputStream中的write方法,用于将一个对象序列化到磁盘上,如果使用重载,那么会出现write(Object o)、write(int i)、write(long l),属于胡乱使用重载,因此该类中,为了防止这种情况,分别提供了名称不同的writeObject(Object o)、writeInt(int i)、writeLong(long l)等方法
      2. 这样命名还带来一个好处,可以为其read方法带来一个对应的命名,例如readObject、readInt、readLong
    2. 对于构造方法:没法换名字,但可以改为使用静态工厂方法创建对象(item 1)
  3. 如果对于重载的两个方法,虽然其参数数目相同,但至少有一个对应的参数"根本不同",就不属于混乱使用重载
//例如ArrayList有如下两种构造器,虽然参数个数相同,但类型"根本不同",程序员使用时,一定不会用乱
public ArrayList(int initialCapacity)
public ArrayList(Collection<? extends E> c) 
52.3 引起混乱的几种场景
52.3.1 自动装箱引起混乱(不属于根本不同)
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

public class SetList {
    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        List<Integer> list = new ArrayList<>();
        for (int i = -3; i < 3; i++) {
            set.add(i);
            list.add(i);
        }
        System.out.println(set + " " + list);
        for (int i = 0; i < 3; i++) {
        	//1. 对于set,remove方法使用的是boolean remove(Object o),没有歧义
            set.remove(i);
            //2. 对于list,其remove(int index)与其自身的remove(Object o)构成了重载,而i是int型,因此使用List中的remove方法,依次删除集合中索引为0、1、2对应的元素,导致混乱
            //3. java5之前,还没引入泛型,也没有自动装箱,Object和int完全是两种不同的类型,无法构成重载,也就是说Java添加了自动装箱和泛型后,破坏了List,导致其API中胡乱的使用了重载
            list.remove(i);
            //3. 可以使用如下两种方法进行修正
//            list.remove((Integer)i);
//            list.remove(Integer.valueOf(i));
        }
        System.out.println(set + " " + list);
    }
}
52.3.2 lambda引起混乱(不属于根本不同)

“不精确的方法引用”(inexact method reference):例如System.out::println为一个方法引用,但这个方法引用,在不同的情况下,代表的方法并不相同,有可能是PrintStream中的println() ,也可能是println(boolean x) ,因此叫做不精确方法引用

  1. 如果重载的一对方法或构造器,在同一个参数位置,都使用了函数式接口,即使这两个函数式接口中提供的函数并不相同,也会引起混乱,例如submit
  2. 如果编译时使用-Xlint:overloads,Java编译器是能检测出这种重载,并给出警告
//该方法可以正常编译
new Thread(System.out::println).start();
ExecutorService exec = Executors.newCachedThreadPool();
//submit方法无法正常通过编译,这个是java编译器的问题,由以下两个原因共同引发其不能编译,虽然这不影响人为判断,但编译器就判断不了
//1. submit方法有重载方法,一个参数为Callable,另一个为Runnable
//2. println也有重载方法,虽然其重载方法与其参数列表长度都不同
//3. 如果人为判断,因为所有的println重载方法,返回值都是void,所以此处应该使用的是Runnable为参数的submit方法,但编译器却无法识别并通过
//4. 如果println压根没有重载方法,那么编译可以通过
exec.submit(System.out::println);
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestWu {
    static void println(){

    }
    //没有该重载方法,下方submit方法,可以正常编译通过
//    static void println(boolean b){
//
//    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.submit(TestWu::println);
    }
}
52.3.3 新版本发行引起混乱
  1. Java4时,String类有一个contentEquals(StringBuffer)方法
  2. Java5中,引进了CharSequence接口,所有表示字符序列的类都实现这个接口
  3. 因此String类自然多了一个判断String中的字符序列与其他字符序列是否相同的contentEquals的重载方法contentEquals(CharSequence),此时引起混乱
  4. 但可以通过让两个造成混乱的重载方法返回相同的结果来解决这个混乱,因为其实用谁都一样,用错了也没关系
//修改了contentEquals(StringBuffer sb)的具体实现,保证其与它的重载方法,返回结果完全相同
public boolean contentEquals(StringBuffer sb) {
    return contentEquals((CharSequence) sb);
}
  1. JDK中有很多地方确实违背了本例中的原则,不应被效仿,例如String的valueOf(char[]) 、valueOf(Object)这两个重载方法
52.4 最佳实践
  1. 尽量避免相同参数数目的方法的重载
    1. 如果做不到,至少应该保证有一个参数,与另一个方法同样位置的参数根本不同
      1. 如果还做不到,比如正在改造一个现有类,实现新接口,那么应保证两个重载方法的返回结果相同(String实现CharSequence)

第53条 慎用可变参数

53.1 可变参数的问题

“可匹配不同长度的变量的方法”(variable arity method)

  1. 可变参数方法一般称为variable arity method(变量 数量 方法)
  2. 可变参数是为printf和反射机制而设计的
  3. 可变参数方法的问题
public class TestWu {
    static int min(int... args) {
    //1. 该方法,如果调用时,一个参数都不传入,会造成int min = args[0]时,数组越界
    //2. 虽然可以通过下面屏蔽掉的代码解决这个问题,但代码并不美观
//        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;
    }

    public static void main(String[] args) {
        min();
    }
}
//解决方案,该方法同时保证了必须有参数传入,且代码简洁
static int min(int firstArg, int... args) {
    int min = firstArg;
    for (int i : args)
        if (i < min)
            min = i;
    return min;
}
//客户端代码,如果一个参数都没有会报错,无法编译
//min();
min(1);
  1. 可变参数方法的性能问题
    1. 每次调用可变参数方法都会导致一次数组分配与初始化,影响性能
    2. 如果想追求性能,又能确保某个可变参数方法95%情况下,只是用3个或以下个参数,可以进行如下改写
    3. EnumSet类对其静态工厂方法of,就使用了这种处理方式,从而减少枚举集合成本,方便其替代位域(item 36)
//这样,只有5%的情况,会创建数组,提升了性能
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) { }
53.2 最佳实践
  1. 应该再可变参数前,加上那些必需的参数
  2. 需要关注可变参数的性能影响

第54条 返回零长度的数组或集合,而不是null

54.1 返回null的问题
  1. 返回零长度数组或集合的问题
    1. 客户端代码编程复杂,需要增加对null的校验
    2. 返回该容器的方法本身编程复杂
//API
private final List<Cheese> cheesesInStock = new ArrayList<>();

public List<Cheese> getCheeses() {
    return new ArrayList<>(cheesesInStock);
}
//客户端代码
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
	System.out.println("Jolly good, just the thing.");
//修改方案:代码要比加null判断的更简洁
//1. 集合
public List<Cheese> getCheeses() {
	return new ArrayList<>(cheesesInStock);
}
//2. 数组
public Cheese[] getCheeses() {
	return cheesesInStock.toArray(new Cheese[0]);
}
  1. 避免分配零长度容器所需要的开销的方法
    1. 即可以不为0长度的数组或集合分配内存,就能返回他们
    2. 但在这个级别担心性能问题不明智
    3. 如下改造只适用于确实有证据证明分配的零长度集合或数组影响了系统性能
//1. 对集合的改造,使用Collections.emptyList、Collections.emptySet、Collections.emptyMap返回空集合
//这三个方法返回的都是不可变对象,因此不用担心其他线程或方法,改变其属性值,即不可变对象可以被自由共享(item 17)
public List<Cheese> getCheeses() {
	return cheesesInStock.isEmpty() ? Collections.emptyList(): new ArrayList<>(cheesesInStock);
}
//2. 数组的改造
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
	return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
  1. 不要尝试为传入toArray的方法的数组参数,提前分配内存,从而提升性能,这只会适得其反
return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);
54.2 最佳实践
  1. 应该返回零长度的集合或数组,而永远不要返回null
  2. 返回null会使API更难用,更容易出错,而且不会提升性能

55 谨慎返回Optional

55.1 编写特定情况下无法返回值的方法
55.1.1 抛出异常
  1. 异常应该保存起来(item 69)
  2. 创建异常时会捕捉整个堆栈轨迹,因此抛出异常的开销很高
    1. Optional的orElseThrow可以保证,只在Optional为空时候,才创建异常对象,节省资源
55.1.2 返回null
  1. 方法返回null会使客户端必须增加特殊的代码来处理null值
  2. 如果客户端忽略了没处理null值,并将null值保存在某个数据结构中,那么未来可能会在和存放部分代码完全不相关的代码处抛出NullPointerException
55.1.3 使用Optional
  1. java8后新增
  2. Optional本质上是一个不可变的集合,可以存放单个非null的T引用,或只是一个空集合,Optional并没实现Collection,虽然原则上是可以的
  3. 不包含任何元素的Optional称为空empty,非空的empty中一定存放了一个非null的元素,我们一般描述为,有一个元素存在于这个Optional中
  4. 使用返回Optional<T>的方法替代返回T的方法,返回Optional的方法比抛出异常的方法更灵活,也更容易,比返回null的方法更不容易出错
//当c是空,抛出异常,可以考虑用返回一个空的Optional来替代抛出异常
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;
}
//修改方案
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);
}
  1. 返回Optional对象的方法,永远不要返回null值,这违背了Optional的本意
public  Optional test(){
    return null;
}
  1. 返回Optional的方法本质上与抛出受检异常的方法相同,他们都强制使用该方法的客户端,面对没有返回值的现实
    1. 受检异常是必须try,catch进行处理,会添加额外样本代码
    2. Optional的orElse等方法,可以要求客户端必须提供,当Optional为空时,其返回的值,get方法一般不用,除非你确定Optional一定不是空
    3. 未受检异常和返回null值,客户端都可以完全忽略,不做任何处理
55.2 Optional API
55.2.1 创建Optional实例
//返回一个空的Optional,空的Optional对象的get方法会产NoSuchElementException
static Optional empty()
//如果value为空,会抛出NullPointerException
static Optional of(T value)
//如果value为空,返回一个空Optional,否则返回一个存放value值的Optional
static Optional ofNullable(T value)
55.2.2 访问Optional对象的值
//返回Optional中存放的元素值,空Optional抛出NoSuchElementException
//只有确保Optional不为空时,才使用该方法取元素,一般更应该使用orElse开头的方法获取Optional中的元素值
T get()
//判断是否为空Optional
boolean isPresent()
//Optional中存放了元素,使用action消费这个元素
void ifPresent(Consumer action)
//ifPresent:如果为空,执行action,OrElse:如果非空,默认执行emptyAction
void ifPresentOrElse(Consumer action, Runnable emptyAction)
55.2.3 返回默认值
//Optional为空时,返回默认值
T orElse(T other)
//与orElse区别在于,即使Optional不为空,orElse内代码仍然会调用,而orElseGet不会
T orElseGet(Supplier supplier)
//如果Optional为空,默认抛出一个异常
orElseThrow(Supplier exceptionSupplier)
//java9新增,如果不是空Optional,supplier中代码不会执行
Optional or(Supplier supplier)
55.2.4 转换值
//与Stream的map和flatMap区别相同
//将Optional中存放的元素,通过mapper转换为一个新对象,重新放入Optional
//mapper的apply方法,返回值是一个存放于Optional中的元素,而不是Optional
Optional map(Function mapper)
//与Stream的flatMap相似,相当于将Optional中所有元素(其实只有一个)依次使用mapper来生成一个新的Optional对象,最后将这些对象合并(因为只有一个元素,因此只生成一个对象,也就不用合并了)
//mapper的apply方法,返回值直接是一个Optional对象
Optional flatMap(Function mapper)
55.2.5 过滤值
//将Optional中的元素放入predicate进行校验,如果校验成功,返回原Optional,否则返回一个空Optional
Optional filter(Predicate predicate)
55.2.6 将Optional转换为只包含一个元素的Stream
//注意,此处不是将Optional放入流中,而是将Optional中的元素,放入Stream中
Stream<T> stream()
55.2.7 Optional 类的链式方法
  1. 链式方法
public class User {
    private Address address;

    public Optional<Address> getAddress() {
        return Optional.ofNullable(address);
    }

    // ...
}
public class Address {
    private Country country;

    public Optional<Country> getCountry() {
        return Optional.ofNullable(country);
    }

    // ...
}
//客户端代码,不再需要使用一堆if进行null判断
public void whenChaining_thenOk() {
    User user = new User("anna@gmail.com", "1234");

    String result = Optional.ofNullable(user)
      .flatMap(u -> u.getAddress())
      .flatMap(a -> a.getCountry())
      .map(c -> c.getIsocode())
      .orElse("default");

    assertEquals(result, "default");
}
  1. 使用isPresent()会增加样板代码,使代码冗长,不清晰
//ProcessHandle为java9引入,表示一个进程
ProcessHandle ph=null;
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("Parent PID: " + (parentProcess.isPresent() ?
        String.valueOf(parentProcess.get().pid()) : "N/A"));
//1. 此处无法orElseGet方法替换,因为orElseGet方法,如果Optional不为空时,直接返回Optional中的ProcessHandle,而无法返回ProcessHandle的pid()
//2. 可以用如下方法替代isPresent的功能
System.out.println("Parent PID: " +
        ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
  1. 将Stream<Optional<T>>转为Stream<T>
//java8中的做法,streamOfOptionals表示Stream\<Optional\<T>>类型的流,该方法返回一个Stream\<T>类型的流
streamOfOptionals.filter(Optional::isPresent).map(Optional::get)
//java9为Optional新增了stream方法,因此可以改写为如下方式
//flatMap就是对流中所有Optional元素,调用他们的stream方法,生成一个只包含Optional内唯一元素的一个流,由于Stream中每个Optional元素都生成一个流,最后flatMap将这些流合并,就得到了一个Stream\<T>类型的流
streamOfOptionals.flatMap(Optional::stream)
55.3 使用Optional的场景
  1. 如果方法可能无法返回结果,且当无返回结果时,客户端必须执行特殊的处理,那么就应该返回Optional<T>
  2. 一般不用于方法/构造器的参数,会使代码变的复杂
User user = new User("john@gmail.com", "1234", Optional.empty());
  1. 不要使用Optional存放容器
    1. 其中容器包括集合、映射、Stream、数组、Optional自身,即不要返回空Optional<List<T>>,而应该返回一个空的List<T>
    2. 因为如果返回Optional,客户端还需要对这个Optional做特殊处理,比如对其使用orElseGet当List为null时候,返回一个默认的List,最后还是要对这个返回的无元素的List做处理,不如直接返回一个空的List<T>
  2. Optional主要被当做返回值的类型
  3. 可以将其与流或其它返回 Optional 的方法结合,以构建流畅的API
List<User> users = new ArrayList<>();
//注意findFirst、max等方法,返回的是一个Optional对象,这样避免了返回空的user,同时省略了if对null的特殊处理代码,使整体代码非常简洁
User user = users.stream().findFirst().orElse(new User("default", "1234"));
  1. Optional创建对象时,必须进行分配和初始化,从Optional中读取内容也需要额外开销,所以Optional不适用于特别注重性能的情况,是否真正影响性能,需要测量得到结论
  2. 不要使用Optional存放基本类型中的int、long、double,因为int会先转为Integer,再存放于Optional(两级包装),相当于为了存放int会多创建了两个对象,使开销变高,类库的设计师提供了OptionalInt、OptionalLong和OptionalDouble解决这个问题
    1. 没提供其他小型基本类型的Optional,例如Boolean、Byte、Character、Short、Float,可能是因为他们两级包装耗费的开销相对较小,所以他们可以直接放入Optional中
  3. 不要用Optional对象作为键、值,或集合中的元素,如果key对象使用Optional,那么会造成将有两种方式表达不存在key值,key为null,或者key为空的Optional,这会引起混乱,增加客户端编程复杂度
  4. 当类中大量属性可以缺失,而这些属性又是基本类型变量,无法准确表达这种缺失,因为例如int,默认值是0,无法返回null表示缺失,此时可以考虑使用Optional作为类的属性的类型,例如item2中的NutritionFacts,可以使用Optional作为属性的类型,并修改getter方法,返回Optional对象
55.4 最佳实践
  1. 如果你在编写一个并不总能返回一个值的方法时,而且你知道使用你这个方法的客户端,每次都需要为了处理这个"无法返回的值",而添加特殊的样板代码,应考虑使用Optional作为返回值
  2. 使用Optional时要考虑到其性能影响,如果确实影响性能,方法还是应该返回null或者抛出异常
  3. 尽量不要将Optional用作返回值以外的其他用途

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

“导出的API”(exposed API):API为Application Programming Interface,应用程序接口,exposed表示公开的,这里指的是那些类库中提供给我们可以直接使用的那些方法

56.1 Javadoc
  1. 在c或c++中,API文档是自己写的,那么保持文档和代码的版本一致非常麻烦,比如有多个版本的API文档,也有很多版本的代码,那么很难把它们一一对应
  2. Java提供了Javadoc工具,根据java源代码中的特殊格式的注释,自动生成API文档,这种特殊格式的注释称为文档注释
  3. 文档注释的规范,在How to Write Doc Comments的网页上进行了说明,但这个文档在Java4版本之后就没进行过更新,各版本新增的文档标签如下
    1. Java5:{@literal}、{@code}
    2. Java8:{@implSpec}
    3. Java9:{@index}
https://www.oracle.com/technetwork/java/javase/documentation/index-137868.html
56.2 编写文档注释
  1. 应在每个被导出的类、接口、构造器、方法、域声明前加文档注释
  2. 如果类可序列化,应对其序列化形式编写文档
  3. 如果哪个元素上没有加文档注释,那么javadoc生成的文档中,就只有该元素的声明
  4. 使用没有文档注释的API非常痛苦,也容易出错
  5. 公有类不应该使用默认构造器,因为这会导致无法为该构造器提供文档注释
  6. 如果编写的代码还希望被别人维护,那么还应该为那些没有被导出的元素也编写文档注释(这样其他人就能看懂你所有实现细节)
56.3 方法上的文档注释
  1. 应简洁地描述它和客户端的约定
    1. 对于专门为继承而设计的方法,应该说明它是具体如何完成工作的,防止子类覆盖该功能,使用@implSpec标签,Javadoc工具默认忽略该标签,需要人为传入如下命令行参数进行开启,具体@implSpec用法参考item 19
    -tag "apiNote:a:API Note:"
    -tag "implSpec:a:Implementation Requirements:"
    -tag "implNote:a:Implementation Note:" 
    
    1. 如果不是专门为继承设计的方法,应该说明这个方法做了什么即可
  2. 应列举出方法的所有前置条件(precondition)和后置条件(postcondition)
    1. 前置条件为客户端想调用该方法所必须满足的条件,一般前提条件由@throws标签描述,每个未受检异常其实都是由于一个不满足前提条件的情况所引发的,也可以在一些受影响的参数的@param标记中指定前提条件
    2. 后置条件为方法的调用成功导致一定会产生的事实
//例如输出平方根的方法,前提条件就是x必须大于等于0,后置条件就是该方法一定会由标准输出打印平方根
print_sqrt(double x)
  1. 应描述方法的副作用,副作用指不是为了获取后置条件而明确要求的那些变化,例如方法启动了一个后台线程,文档中就应该描述这一点
  2. 为了完整地描述方法的约定,应该让每个参数都有一个@param标签,以及一个@return标签(除非这个方法返回void),以及每个该方法抛出的异常都有一个@throws标签。如果@return中内容就是代码中返回值那个字符串,一般可省略
56.3.1 示例
  1. @param与@return后跟名词短语,描述这个参数或返回值所表示的值,极少情况下也可 能使用算数表达式替代名词短语,例如BigDecimal类
  2. @throws后一般跟一个if+名词短语,描述产生该异常的条件,这三个标签后面都不用句号表示结束
  3. <p>和<i>为html中的标签,因为javadoc生成的文档是html格式,因此有些程序员把html表格嵌入到他们的文档注释中,但这种做法不多见
  4. @code有两个作用,1是让其内代码片段以代码字体(code font)呈现在html中,2是允许我们在其内使用<、>这种html中的元字符(可以理解为html中的关键字),而不需要进行转义
  5. 如果不需要以代码字体呈现,只是不想对<、>、&这些html元字符进行转义,可以使用{@literal}标签,例如A geometric series converges if |r| < 1在文档注释中可以写为A geometric series converges if {@literal |r| < 1},源代码中文档注释的可读性变差,文档注释在源代码和产生的文档中,都应该是易于阅读的,如果无法保证让二者都易于阅读,产生的文档的可读性优先于源代码可读性
  6. 如果代码是多行,应该使用
    {@code 多行代码段}
    ,这样可以不用转义就保留多行代码中的换行,但如果代码中使用了@符,必须进行转义
  7. 文档注释中使用了this list这个字眼,this用于实例方法的文档注释中时,都表示该方法的调用者
/**
* 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);
56.4 概要描述
  1. 每个元素的文档注释中第一句话,都是该对该元素的一个概要描述,本例中Returns the element…就是,为了避免混淆,同一个类或接口中两个成员或构造器,不应使用同样的概要描述,特别要注意重载的情况
  2. javadoc生成概要描述的文档时,一旦发现概要描述中存在句号(.),且该句号后跟着空格、跳格、行终结符,就会直接结束,例如概要描述"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."
  3. 概要描述一般并不是完整的句子,对于方法和构造器,应该是完整的动词短语,描述方法执行的动作
ArrayList(int initialCapacity)—Constructs an empty list with
the specified initial capacity.
//此处使用returns这种第三人称时态,比使用第二人称更确切
Collection.size()—Returns the number of elements in this collection.
  1. 对于类、接口、属性,概要描述应该是名词短语,描述该类或接口的实例,或属性所代表的事物
Instant—An instantaneous point on the time-line.
Math.PI—The double value that is closer than any other to pi, the ratio of
the circumference of a circle to its diameter.
56.5 java9新增@index标签
  1. Java9之后,在Javadoc生成的html格式的文档中,添加了客户端索引功能。简化了在大型API文档集中进行搜索的任务,采用了文档右上角搜索框的形式
  2. 在这个搜索框中输入时,会出现一个下拉列表,上面显示匹配你输入的字符串的类、方法、属性这些API元素,如果希望这个下拉列表中,出现自己设置的某些信息,可以使用@index,此时输入IEEE,就能在下拉列表中出现,点击后就可以跳转到该处
* This method complies with the {@index IEEE 754} standard.
56.6 其他规则
  1. 为泛型方法或泛型类型编写文档注释时,要确保对所有类型参数都进行说明
/**
* 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> { ... }
  1. 为枚举编写文档时,应该对所有枚举值也都添加文档注释。如果文档注释很短,可以将整个注释放在一行上
/**
* 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;
}
  1. 应该为Annotation所有成员(value)以及该Annotation本身(ExceptionTest)编写文档注释
    1. 对成员的文档注释,和对成员变量注释规则相同,都使用名词短语
    2. 对该Annotation本身的文档注释,使用动词短语,说明当程序元素具有这种类型的注释时,表示什么意思
/**
* 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();
}
  1. 对整个包添加注释,称为包级别文档注释,包级的javadoc注释放置在程序包目录内名为package-info.java的文件中,该文件包含注释和包声明
/**
 * Provides the classes necessary to create an applet and the classes an applet uses 
 * to communicate with its applet context. 
 * <p>
 * The applet framework involves two entities: 
 * the applet and the applet context. An applet is an embeddable window (see the 
 * {@link java.awt.Panel} class) with a few extra methods that the applet context 
 * can use to initialize, start, and stop the applet.
 *
 * @since 1.0
 * @see java.awt
 */
package java.lang.applet;
  1. 模块级的注释放在module-info.java文件中
  2. 文档注释中应该体现类或静态方法的线程安全级别(item 82),如果可序列化,应该提供它的序列化形式(item 87)
  3. Javadoc具有"继承"方法注释的能力,达到重用接口中的文档注释的目的,不需要拷贝那些文档注释,减轻维护多个几乎相同的文档注释的负担
    1. 如果API元素没有文档注释,Javadoc自动搜索最为适用的文档注释,接口文档注释优于超类的文档注释
    2. 也可以利用{@inheritDoc}标签从超类型中继承文档注释的部分内容
  4. 对于由多个相互关联的类组成的复杂API,除了文档注释外,还应该提供一个外部文档描述API的总体结构,这个文档的链接可以添加到相关的类或包级文档注释中
  5. Javadoc可以对本条目中提出的一些建议进行自动检测,Java7中使用-Xdoclint开启,Java8和Java9中检测功能默认开启
  6. 还可以使用checkstyle这种IDE插件,运行一个HTML有效性检查器,进一步检查生成好的HTML文档, 降低文档注释中出错的可能性
    1. 可以检查HTML标签的错误用法
    2. 以及应该被转义的HTML元字符
  7. 还可以利用W3C markup validation service [W3C-validator],对HTML文档在线检验
  8. Java9开始默认生成HTML4.01格式的文档,可以通过-html5生成HTML5格式的文档
  9. 最终确定生成的文档是否能清晰地描述自己的API的方法就是去阅读生成的这个html格式的文档,通过阅读能发现文档注释中的问题,并进行修改,就像修改代码一样
56.7 最佳实践
  1. 为API编写文档,使用文档注释是最好、最有效的途径
  2. 对于所有可导出的API元素来说,使用文档注释应被看作是强制性要求
  3. 文档注释内是可以编写HTML标签的,会被Javadoc识别并转换,所以HTML元字符如果想直接写在文档注释中,需要转义
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值