Effective Java读书笔记——第七章 方法

    xiaoxiao2021-03-25  67

    本章讨论方法设计的几个方面:

    1、如何处理参数和返回值?

    2、如何设计方法签名?

    3、如何为方法编写文档?

    第38条:检查参数的有效性

    很多构造器和方法都会对传进的参数做有效性判断。如索引必须是非负数。对象索引不能为null等。

    对于public、protected或包访问权限的方法,最好使用注解的方式提供检查参数的警告,下面的方法计算BigInteger的模并返回,要求该模的值大于0,当小于会或等于0的时候,将抛出异常:

    @throws 如果m小于或等于0,抛出ArithmeticException 异常 public BigInter mod(BigInteger m) { if(m.signum() <= 0) { throw new ArithmeticException("Modulus <= 0: "+ m); } }

    对于private方法,作为报的创建者,可以使用断言的方式检查参数的有效性:

    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; //满足条件继续计算 ... }

    由于一般情况下,构造方法都是公有的,所以每当编写构造方法的时候,应考虑那些参数有哪些限制。应该把这些限制写到文档里,冰鞋在这个方法的额开头处通过显式的检查来实施这些限制。养成这样的习惯很重要。

    第39条:必要时进行保护性拷贝

    考虑下面这个类:

    public final class Period { private final Date start; private final Date end; @throw IllegalArgumentException if start is after end @throw NullPointerException is start or end is null public Period(Date start , Date end) { if(start.compareTo(end) > 0) { throw new IllegalArgumentExcwption(start + " after " + end); } this.start = start; this.end = end; } public Date start() { return start; } public Date end() { return end; } }

    上面的类便面上看起来是不可变的,但Date是可变的:

    Date start = new Date(); Date end = new Date(); Period p = new Period(start,end); end.setYear(78);//修改了end实例,使 Period类不再是不可变的

    为了避免Period类随意被修改,应该对于构造器的每个可变参数进行保护性拷贝。并且使用备份的对象作为Period实例的组件,而不是用原始的对象:

    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 IllegalArgument(start + " after " + end); } }

    需要注意的是,保护性拷贝是在检查参数有效性之前进行的。并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。

    还有一点需要注意:不要使用clone方法进行保护性拷贝。

    其实上述改进后的Period类仍然存在被改变的漏洞:

    Date start = new Date(); Date end = new Date(); Period p = new Period(start,end); p.end().setYear(78);

    改进的方式是:

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

    第40条:谨慎设计方法签名

    谨慎选择方法名称;

    不要过于追求提供便利的方法;

    避免过长的参数列表

    第41条:慎用重载

    考虑下面的类:

    public class CollectionClassifier { public static String classify(Set<?> s) { return "set"; } public static String classify(List<?> lst) { return "List"; } public static String classify(Colection<?> c) { return "Unknown"; } public static void main(String[] args) { Collection<?>[] collections = {new HashSet<String>(),new ArrayList<BigIntger>(),new HashMap<String,String>().values()}; for(Collection<?> c : collections) { System.out.println(classify(c)); } } }

    实际的打印结果并非是“Set”、“List”、“Unknown”;

    而是三个“Unknown”。原因是程序需要调用哪个重载方法,是在编译器确定的,而不是运行期,由于在编译期,打印classify(c)的时候,所有的c都被当做是Collection对象,所以将打印三个Unknown。

    所以这里要区分几个概念:程序重载哪个方法是静态的过程,也就是说,重载是在编译器就确定的;而重写是多态的一个必要条件,选择被覆盖的方法的正确版本是在运行时进行的:

    class Wine { String name() { return "wine"; } } class SparklingWine extends Wine { @Override String name() { return "Sarkling 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() }; for(Wine wine : wines) { System.out.println(wine.name()); } } }

    name方法是在类Wine中被声明的,但是在类Sparkling和Champagne中被覆盖。所以诚意会打印出“wine,sparkling wine、champagne”,虽然在迭代中,实例的编译时类型都为Wine,当调用被覆盖的方法时,对象的编译时类型不会影响到哪个方法将被执行,因为“最为具体的”那个覆盖笨笨总是会得到执行。

    而重载却是在编译时进行的,完全基于参数的编译时类型。

    所以对classify方法的修改方式是:

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

    看下面的例子:

    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]。

    并不是[-3, -2, -1]、[-3, -2, -1]。

    原因就是set.remove(i)调用的是重载的方法remove(E e),E是集合(在这里是Integer)的元素类型,将i从int自动装箱到Integer中,这是期待的行为,所以第一个返回正确的结果。而list.remode(i)调用的重载方法是remove(int i),它总列表的指定位置上删除元素。所以在依次去掉第一个位置、第二个位置、第三个位置的元素后,结果自然就是[-2, 0, 2]。

    改进的方式是,让编译器在编译的时候就“知道”应该调用remove(E e)这个方法:

    for(int i = 0;i < 3;++i) { set.remove(i); list.remove((Integer)i); //下面的方式也可以 //list.remove(Integer.valueOf(i)); }

    从这个例子可以看出 List实际上有两个重载的remove方法:remove(E e)和remove(int i)。在JDK1.5之前,有remove(Object obj)和remove(int i),由于没有泛型和自动装箱的概念,所以这两个方法既然不同,没有关系,但是从JDK1.5开始,引入了这两个概念,remove(Object obj)和remove(int i)方法(前者实际变成了remove(E e))就有关系了。这实际上破坏了List接口。所以在引入泛型和自动装箱概念后,请慎用重载。

    简而言之,“能够重载方法”并不意味着“应该重载方法”,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。

    第42条:慎用可变参数

    可变参数方法指的是:接受0个或多个指定类型的参数。它的机制是先创建一个数组,数组的size是在调用方法的位置所传递的参数的个数,然后将参数值或引用传到数组中,最后将数组传递给方法。(可变参数方法是在JDK1.5及以后版本中增加的)

    例如,下面就是一个可变参数的方法:

    static int sum(int... args) { int sum = 0; for(int arg : args) { sum += arg; } return sum; }

    当调用sum(1,2,3)时返回6,调用sum()时返回0。

    有时候,有必要编写需要需要1个或者多个某种类型参数的方法,而不是0个或者多个。如下面方法计算int参数的最小值:

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

    上面的问题是,客户端一旦传递了一个没有任何参数的方法,程序将在运行时抛出异常,而且除非将min初始化为Integer.MAX_VALUE,否则将无法使用for-each 循环。

    解决方式是:

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

    这种方式解决了上述问题。


    在重视性能的情况下,使用可变参数要特别小心,因为可变参数方法的每一次调用都会导致一次数组的初始化和内存分配。解决方式是考察对某个方法的调用,如果大部分情况下只是传入三个火少于三个的情况,那个可以重载几个版本:

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

    简而言之,在定义参数数目不定的方法时,可变参数方法是一种很方便的方法,但是它们不应该被过度滥用。如果使用不当,会造成效率的低下、或是运行时异常。

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

    考虑下面的代码段:

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

    代码把没有奶酪的请款当成是一种特例。这是不合理的,因为给客户端增加了额外的负担:

    Cheese[] cheeses = shop.getCheeses(); if(cheeses != null && Arrays.asList(cheeses).contains(Cheese.STILTION)) { System.out.println("..."); }

    但是我们希望的是下面这段代码,它看起来更简洁,但显然如果getCheeses()方法返回空的话,程序会有异常:

    if(Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON)) { System.out.println("..."); }

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

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

    略。。。

    转载请注明原文地址: https://ju.6miu.com/read-39536.html

    最新回复(0)