Effective Java : 方法

38.检查参数的有效性

不检查

如果不对参数进行检查,可能会出现如下错误:

  1. 处理过程中发生失败,产生令人费解的异常
  2. 正常返回,但是会计算出错误的结果
  3. 能正常返回,但是破坏了某个状态,在不确定的将来某个节点上引发错误(相当不好定位)

正确的做法

  1. 早 javadoc 的 @throws 标签文档中进行说明.(如果违反了参数限制将抛出异常)
  2. 在计算任务之前,就应该先检查他的参数
  3. 如果计算过程抛出异常,已改使用 异常转译 技术,

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

简介

Java 是一门 安全的语言.对于 缓冲区溢出,数组越界,非法指针等内存错误都会自动免疫.\

在下列情况下,需要对类的健壮性做一些工作.

  1. 有人视图破坏这个类的约束条件时.
  2. 对API产生误解的程序员,所导致的不可预期的行为.

如下这个类: Period.java

保护性拷贝的重要性

可以通过如下方式破坏实例的内部信息.

Date start = new Date();
Date end = new Date();
Period period = new Period(start,end);
end.setYear(78);//改变了end对象,造成内部错误

为了避免 受到这种攻击,可以对参数的每个可变参数进行保护性拷贝,并且使用拷贝后的对象,如下:

public Period(Date start, Date end) {
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException(start + " after " + end);
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime()); //使用的是拷贝对象,这样的话,就不怕上面的改变了,因为不会改变内部对象.
    }

注意事项

  • 在 38 条中,我们说要对 参数进行有效性检查,检查也是发生在保护性拷贝之后的/
  • 对于参数类型,可以被不可信任方 子类化的参数,不要使用clone来实现保护性拷贝.
  • 采用新的访问方式,可以提高 类型的 不可变能力
public Date start(){
    return new Date(start.getTime());
}
  • 如果有可能,应该使用不可变对象作为类的内部组件,这样就不用考虑保护性拷贝

40.谨慎设计方法签名

设计原则:

  • 谨慎的选择方法名称
  • 不要过于追求提供便利的方法
  • 避免过长的参数列表,可以有如下三种方式
  1. 把方法分解为多个方法,每个方法只需要参数列表的一个子集.
  2. 创建辅助类,用于保存参数的分组
  3. 采用 Builder模式.
  • 对于参数类型,优先考虑接口不是类
  • 对于Boolean,要优先使用两个元素的`枚举类型,如下所示

public enum TemperatureScale{FAHRENHRIT,CELSIUS;}

41.慎用重载

示例

  1. 首先来看一个示例:
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() };
//打印了三次 Unknown Collection
        for (Collection<?> c : collections)
            System.out.println(classify(c));//这里并不会按照我们期望的那样选择调用某一个 重载方法
    }
}

因为要 调用 哪个重载方法是在编译时确定的.虽然运行时是不同的.

  1. 再看另一个示例:
class Wine {
    String name() {
        return "wine";
    }
}
class SparklingWine extends Wine {
    @Override
    String name() {
        return "sparkling wine";
    }
}
class Champagne extends SparklingWine {
    @Override
    String name() {
        return "champagne";
    }
}

public class Overriding {
    public static void main(String[] args) {
        Wine[] wines = { new Wine(), new SparklingWine(), new Champagne() };
        //打印出了 wine,sparkling wine,champagne
        for (Wine wine : wines)
            System.out.println(wine.name());
    }
}
  1. 这里就不会出现上面的问题,因为对于重载方法的选择是静态的,对于重写方法的选择则是动态的
  1. 对于第一个示例中,我们通过如下方式修正:
public static String classify(Collection<?> c){
    return c instanceof Set?"Set":c instanceof List?"List":"Unknown Collection";
}

建议

  • 永远不要到处两个 具有相同参数数目的 重载方法.
  • 如果是可变参数,保守策略是 永远不要重载它
  • 对于每一对 重载方法,应 保持 方法中具有 根本不同的类型(像上面的 ListCollection 在根本上是由关系的 ) | 如果两个类都不是对方的后代,则是不相关的.

包装类引起的误会

比如如下示例:

Set<Integer> set = new TreeSet<Integer>();
List<Integer> list = new ArrayList<Integer>();
        for (int i = -3; i < 3; i++) {
            set.add(i);
            list.add(i);
        }
        for (int i = 0; i < 3; i++) {
        // 这里调用的是 Set#remove(E),//set.remove(Integer);
            set.remove(i);
        //而这里确是List#remove(i),
            list.remove(i);
        }

为了修正上面的错误,可以使用 list.remove(Integer.valueOf(i));
这里就是 自动装箱 引起的错误.

小结

简而言之,

  1. 对于多个具有相同参数数目的方法来说,应尽量避免重载
  2. 对于构造器,如果不能避免,应该保证传递同样的参数时,重载方法的行为一致.

42.慎用可变参数

错误用例

将以 数组当做final 参数的现有方法,改造成 可变参数 来替代

List<String> list = Arrays.asList("to","too","two");

这么做会引发一些问题:

历史问题:

  • 比如之前打印数组,最好使用下面的代码:
System.out.println(Arrays.asList(mArray));//这样是为了避免 调用到 Object.toString
  • 这种做法只有在 引用类型 上才有效 ,比如,如下示例:
int[] digits = {1,2,3,5,4,6};
System.out.println(Arrays.asList(digits));

这里会产生错误, 因为 asList 的方法声明为 asList(Object[]);

可变参数

  • 当可变参数引入后,就出现了最开始的用法了,这时候,如下用法
int[] digits = {1,2,3,5,4,6};
System.out.println(Arrays.asList(digits));// digits 当做可变参数中的一个参数

这时候,会打印出 [[I@3e25a5 类似无意义的结果,

  • 最新可用方法,如果使用下面的方法,则没有问题
int[] digits = {1,2,3,5,4,6};
System.out.println(Arrays.toString(digits));

总结

  • 可变参数的每次调用都会 进行一次数组的分配和初始化.避免性能损耗,可以使用如下方式,在大多数时候 , 调用 非可变参数 方法
public void foo(){}
public void foo(int a){}
public void foo(int a,int b){}
public void foo(int a,int b,int ... args){}

EnumSet 就是采用这种机制.

  • 不必改造具有final数组参数的每个方法.只有确定 它是在数量不定的值上执行 才使用可变参数.

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

返回null的示例

比如,如下示例:

  private final List<Cheese> mCheeses = ...;
  public Cheese[] getCheeses(){
    if (mCheeses.size() ==0)
      return null;
  }

针对上面的代码,客户端的处理

  Cheese[] mCheeses = getCheeses();
  if(mCheeses != null &&Arrays.asList(mCheeses).contains(Cheese.STILTON)){
    System.out.println("Jolly goog,just the thing");
  }

返回null ,而不是集合的形式,几乎每次客户端都要判空处理
客户端如果忘记处理,则会引起严重的问题

返回集合

  public static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
  private final List<Cheese> mCheeses = ...;
  public Cheese[] getCheeses(){
  // 如果集合是 空的 ,将返回零长度数组,如果达到足够容纳这个集合,就返回这个数组
    return mCheeses.toArray(EMPTY_CHEESE_ARRAY);
  }

不仅服务端代码简单可扩展了,也方便了客户端

小结

简而言之, 返回类型 为数组或集合的方法 没理由 返回 null,而不是返回一个 零长度的数组或者集合.

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

原则

  1. 为了 正确的编写 API 文档,必须在每个被导出的类,接口,构造器,方法和域声明之前增加一个文档注释
  2. 方法的文档注释应该简洁的描述出,他和客户端之间的约定
  3. 插入代码标签,可以使用<pre>{@code }</pre>
  4. 为了让<,>,等字符出现在文档中,可以使用{@literal}标签
  5. 为了避免混淆,同一个类或者接口中的两个成员或者构造器,不应该使用同样的概要或者描述.
  6. 为泛型或者方法编写文档时,确保说明所有的类型参数
  7. 为注解类型编写文档时,确保说明所有成员
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值