Effective Java第七章:方法 + 实践

第38条:检查参数有效性

我们在给类属性赋值的时候会校验数值是否有效,比如:数值类型大于0,对象不能为null,如果没有校验参数,可能会导致系统抛异常,甚至计算出错误的结果,书中建议:

  • 对于公有方法,在文档中用javadoc的@throws标签抛出异常,标注抛出的异常,
/**
     * Returns a BigInteger whose value is {@code (this mod m}).  This method
     * differs from {@code remainder} in that it always returns a
     * <i>non-negative</i> BigInteger.
     *
     * @param  m the modulus.
     * @return {@code this mod m}
     * @throws ArithmeticException {@code m} &le; 0       // 注意此处 
     * @see    #remainder
     */
    public BigInteger mod(BigInteger m) {
        if (m.signum <= 0)
            throw new ArithmeticException("BigInteger: modulus not positive");

        BigInteger result = this.remainder(m);
        return (result.signum >= 0 ? result : result.add(m));
    }
  • 对于非公有方法使用断言来检查参数
    断言条件为真才会继续执行下去,否则都会失败,直接抛出AssertionError。
// 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
}

备注:此处的assert是java自带的,与单元测试的assert工具类不一样,这样在代码里写assert不一定生效,因为使用java assert需要配置jvm参数才可以生效,在运行含有assert的程序时,必须指定-ea选项,这一点需要注意。

对于类中保留的属性,也要做校验,以后用起来导致程序出错,追溯错误来源可能会比较麻烦,多花很多时间,书中建议在方法中将这些校验的条件写到文档中,这样在调用的时候就可以清楚知道方法限制,减少编程错误。

必要时进行保护性拷贝

这一章也是讲解参数校验,保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象,总结一句话就是:必要时,先拷贝,再校验。什么是必要时呢 ?例如针对不可变对象,如果对象是一个引用对象,如例:

// Broken "immutable" time period class
public final class Period {
	private final Date start;
	private final Date end;
	public Period(Date start, Date end) {
		if (start.compareTo(end) > 0)
		throw new IllegalArgumentException(
		start + " after " + end);
		this.start = start;
		this.end = end;
	}
	public Date start() {
		return start;
	}
	public Date end() {
		return end;
	}
}

这个对象是可以被改变的,比如:

	// Attack the internals of a Period instance
	Date start = new Date();
	Date end = new Date();
	Period p = new Period(start, end);
	
	end.setYear(78);      // 改变对象值

引用对象修改之后,Period类中的对象也会被更改,所以我们必须体用保护性拷贝:

// 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方法来得到这个对象,主要是因为:对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝,这里Date是非final的,不能保证clone方法一定返回类为java.util.Date的对象:它可能返回专门处于恶意的目的而设计的不可信子类的实例。
但上述这样的修改也不一定是安全的:

	Date start = new Date();
	Date end = new Date();
	Period p = new Period(start, end);
	
	end.setYear(78);   // 改变不了
	p.getEnd().setYear(78);  // 想想会不会有问题 ? 

所以还要再做一层防护:

public Date start() {
	return new Date(start.getTime());
}
public Date end() {
	return new Date(end.getTime());
}

总结:

谨慎设计方法签名

本章主要是讲解若干个API设计技巧的总结:

  • 谨慎地选择方法的名称。选择与大众认可的名称相一致的名称
    比如 toString方法,尽可能参考标准的命名规范,便于其他人理解。
  • 不要过于追求提供便利的方法。如果不能确定,还是不提供快捷为好
    每个方法都应该尽其所能,
  • 避免过长的参数列表。目标是4个参数,或者更少
    适用法也记不住这么多参数,频繁参考javaDoc,不便于使用接口,而且方法中相同类型的参数对顺序还有要求,如果记错顺序,代码就会计算错误,不便于查错。
    3个技巧缩短过长的参数列表:
    (1)把多个参数的方法拆开成多个方法,每个方法都是参数的子集
    (2)创建辅佐类作为入参,参数都做类属性,
    (3)采用Builder模式,特别是参数可变的时候,进行多次setter方式构建Builder对象
    // 使用建造者模式
         ComputerB computerB = new ComputerB.ComputerBuilder("主板","cpu","hd","电源","显卡")
                 .setMouse("鼠标").setMousePad("垫子").build() ;
    
  • 对于参数类型,要优先使用接口而不是类
    使用Map接口作为入参,而不是hashMap、hashTable等具体的实现类,如果使用具体类,则限制了方法只等特定的实现,不方便扩展。
  • 对于boolean参数,要优先使用两个元素的枚举类型 ?

慎用重载

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));
    }
}

我们期望的结果可能是:

Set
List
Unknown Collection

但实际中三遍都是“Unknown Collection”,这是因为classify方法被重载了,要调用那个方法是在编译的时候做出决定的,对于for循环中的全部三次迭代,虽然运行时类型都是不一样,但参数的编译时类型都是相同的:Collection<?>,所以最终都是用classify(Collection<?> c),每次循环迭代都是这个重载方法。所以重载并不能给我们带来理想的效果,如果上述代码换成覆盖方法,子类方法覆盖父类可以达到同样的效果,但是也可以通过类型,在运行时给与不同的结果:

public static String classify(Collection<?> c) {
	return c instanceof Set ? "Set" :
	          c instanceof List ? "List" : "Unknown Collection";
}

重载可以让代码等到运行时凸显问题,不容易诊断出这些错误,所以应该竟可能避免乱用重载,要求我们永远不要导出两个具有相同参数数目的重载方法,

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);
		}
		for (int i = 0; i < 3; i++) {
			set.remove(i);
			list.remove(i);
		}
		System.out.println(set + " " + list);
	}
}

实际输出的结果是:

[-3,-2,-1]  [-2,0,2]

分析原因就是set.remove(i)调用的是选择的重载方法 remove(E),这里的E是集合的元素类型(Integer),所以调用的方法remove的是具体的元素,list.remove(i)调用的选择重载方法 remove(int i),表示从列表的指定位置去除元素,修改方法就是:

for (int i = 0; i < 3; i++) {
	set.remove(i);
	list.remove((Integer) i);
	// or remove(Integer.valueOf(i))
}

这也是java 1.5 引进泛型和自动装箱所带来的问题,破坏了List接口,所以谨慎选择重载方法,

慎用可变参数

可变参数方法接受0个或者多个指定类型的参数。 可变参数的机制是通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法。

 // Simple use of varargs - Page 197
    static int sum(int... args) {
        int sum = 0;
        for (int arg : args)
            sum += arg;
        return sum;
    }

我们可能需要对这么一个方法如此那做检查,对于一个取最小值的方法来说:

static int min(int... args) {
      if (args.length == 0)
          throw new IllegalArgumentException("无参数");
      int min = args[0];   // 这里取的第一个数
      for (int i = 1; i < args.length; i++)
          if (args[i] < min)
              min = args[i];
      return min;
  }

这样写也可以满足需求,但是有两个问题:(1)运行时无参数进来会抛异常。(2)代码不是非常美观。
因为这个取最低值方法会要求入参至少有一个,才可以比较得到最低值,所以可以考虑方法入参为两个:一个是指定类型的正常参数,这个参数来源如可变参数中的一个,另一个入参是这种类型的可变参数,如例:

 static int min(int firstArg, int... remainingArgs) {
        int min = firstArg;
        for (int arg : remainingArgs)
            if (arg < min)
                min = arg;
        return min;
}

当你真的需要让一个方法带有不定数量的参数的时候,可变参数才会变得非常有效。它本来是为printf和反射机制设定的。

在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法每次调用都会导致进行一次数组分配和初始化。如果只是凭经验确定,无法承受这一成本,但是又需要可变参数的灵活性。这时候,需要评估,假如某个方法95%会调用3个或更少的参数,那么就声明该方法的5个重载(和上一条一样,这几个重载的方法必须尽量保证方法的功能相同,返回值相同),每个重载方法带有0-3个参数,超过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) { }

看另一个问题:

    public static void foo(int a1, int a2) {
        System.out.println("a1="+a1+",a2="+a2);
    }
    public static void foo(int a1, int... rest) {
        System.out.println("a1="+a1);
        for(int a : rest){
            System.out.println("-a:"+a);
        }
    }    
    public static void main(String[] args) {  
        foo(1,2);
    }

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

我们经常看到这样的方法:

private final List<Cheese> cheesesInStock = ...;
/**
 * @return an array containing all of the cheeses in the shop,
 *  or null if no cheese are available for purchase.
 */
 public Cheese[] getCheeses(){
   if(cheesesInStock.size() == 0)
     return null;
   ...
 }

客户端调用这个方法,都要先判断返回的数据是不是null,例如:

Cheese[] cheeses = shop.getCheeses();
if(chesses != null && Arrays.asList(cheeses).contains(Chees.STILTON))
     System.out.println("Joll good, just the thing.");

方法返回null,别人调用这个方法就必须处理null情况,这在编写代码中很容易被忽略,极易造成错误。所以我们尽可能的返回零数组或者空集合,比如我们可以定义一个不可变的数组:

数组:
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

空集合:
Collections.emptyList();
public static final <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }
    
  @SuppressWarnings("rawtypes")
 public static final List EMPTY_LIST = new EmptyList<>();

特别要注意的地方:实际代码项目里不要这么写,这个EMPTY_LIST是一个静态不可变的对象,在序列化、反序列号过程中会有问题,场景:你有一个类,有两个List容器属性,但具体的类型不一样,你同样给塞EMPTY_LIST,然后反序列化后可能会,这两个属性编程共享对象了,并且可以添加数据进去,而且一个属性的数据变更 会引起另一个属性的数据变更,即使两者类型不一样,因为此种情况会跳过类型校验,但公司的某次实践中发现了这个问题,特此警告。另一方面,目前jdk高版本已经优化了很多,new 一个list对象的成本已经很低了,并不会初始化一定空间,例如代码:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

所以还是不要用空的不可变的list对象。

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

为方法类编写文档注释方便其他开发人员了解方法的功能,特别是API方法的调用,方便其他人了解方法入参、返回值、约束等等。

/**
* 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);

@code标签可用于任何需要展示代码的地方,被该标签包围的内容会以特殊的字体显示,并且不对其中内容做任何HTML解析。

java 1.5 以后新增三个特性:泛型、枚举、注解,
泛型要说明所有的类型参数

/**
 * @author net
 * @version 1.0
 * @param <K> the type of keys maintained by the map
 * @param <V> the type of mapped values
 */
public interface Map<K, V> {
    //dosomething
}

枚举:要确保在文档中说明常量,以及类型,还有任何公有的方法。

/**
 * three primary colours
 * @author net
 * @version 1.0
 * return enum
 */
public enum Color {
    /** Red, the color of blood. */
    RED,
    /** Green, the color of grass.  */
    GREEN,
    /** Blue, the color of sea. */
    BLUE;
}

注解:要确保在文档中说明所有成员,以及本身类型。对于该类型的概要描述,要使用一个动词短语,说明当程序元素具有这种类型的注解时它表示什么意思。

import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Indicates that the annotated method is a test method that
 * must throw the designated exception to succeed.
 * @author net
 * @version 1.0
 */
@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 Exception> value();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值