《Effective Java》——学习笔记(方法&通用程序设计)

版权声明:欢迎转载,请注明出处,谢谢! https://blog.csdn.net/benhuo931115/article/details/79444796

方法

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

在方法体的开头处检查参数,对于公有的方法,要用Javadoc的@throws标签在文档中说明违反参数值限制时会抛出的异常

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

非公有的方法通常应该使用断言来检查它们的参数

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

断言一旦失败将会抛出AssertionError

对于有些参数,方法本身没有用到,却被保存起来供以后使用,检验这类参数的有效性尤为重要(如构造器参数的检验)

在方法执行它的计算任务之前,应该先检查它的参数,这一规则也有例外,一个很重要的例外是,在有些情况下,有效性检查工作非常昂贵,或者根本是不切实际的,而且有效性检查已隐含在计算过程中完成,例如Collections.sort(List),其中的某个比较操作就会抛出ClassCastException

有时候,某些计算会隐式地执行必要的有效性检查,但是如果检查不成功,就会抛出错误的异常,这种情况下,应该将计算过程中抛出的异常转换为正确的异常(文档中标明的异常)

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

没有对象的帮助时,虽然另一个类不可能修改对象的内部状态,但是对象很容易在无意识的情况下提供这种帮助,如下类

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

这个类似乎是不可变的,然而因为Date类本身是可变的

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!

为了保护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 IllegalArgumentException(start +" after "+ end);
}

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

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

  • 谨慎地选择方法的名称。命名规范
  • 不要过于追求提供便利的方法。每个方法都应该尽其所能,方法太多会使类难以学习、使用、文档化、测试和维护
  • 避免过长的参数列表。目标是四个参数,或者更少,可以采用Builder模式,允许客户端进行多次“setter”调用

第41条:慎用重载

public class CollectionClassifier {

    public static String classify(Set<?> s){
        return "Set";
    }

    public static String classify(List<?> s){
        return "List";
    }

    public static String classify(Collection<?> s){
        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<?> c : collections) 会导致每次调用的都是classify(Collection<?>)这个重载方法

而被覆盖的方法(override)的选择则是依据被调用方法所在对象的运行时类型

对于重载,安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法

第42条:慎用可变参数

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

假设确定对某个方法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) {}

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

对于一个返回null而不是零长度数组或者集合的方法,几乎每次用到该方法时都需要额外的代码来处理null返回值

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

为了正确地编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释

方法的文档注释应该简洁地描述出它和客户端之间的约定,说明这个方法做了什么,每个参数都应该有一个@param标签,以及一个@return标签(除非返回类型为void),以及对于该方法抛出的每个异常,无论是受检的还是未受检的,都有一个@throws标签,如果方法启动了后台线程,文档中也应该说明这一点

通用程序设计

第45条:将局部变量的作用域最小化

将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性

要使局部变量的作用域最小化,最有利的方法就是在第一次使用它的地方声明

第46条:for-each循环优先于传统的for循环

for-each循环通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能,并且没有性能损失

有三种常见的情况无法使用for-each循环:

  • 1.过滤——如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法
  • 2.转换——如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值
  • 3.平行迭代——如果需要并行地遍历多个集合,就需要显式地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移

第47条:了解和使用类库

通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及其他人的使用经验

程序员应该把时间花在应用程序上,而不是底层的细节上

第48条:如果需要精确的答案,请避免使用float和double

float和double类型主要是为了科学计算和工程计算而设计的,它们执行二进制浮点运算,是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的。因此,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合

System.out.println(1.00 - 0.42);

的结果为:0.5800000000000001

解决这个问题的正确办法是使用BigDecimal、int或者long

BigDecimal a = new BigDecimal(1.00);
BigDecimal b = new BigDecimal(0.42);
System.out.println(a.subtract(b));

// 输出结果为 0.58

使用BigDecimal有两个缺点:与使用基本运算类型相比,这样做很不方便,而且很慢

除了使用BigDecimal之外,还有一种办法是使用int或者long,并自己记录十进制小数点,如果数值范围没有超过9位十进制数字,就可以使用int;如果不超过18位数字,就可以使用long;如果数值可能超过18位数字,就必须使用BigDecimal

第49条:基本类型优先于装箱基本类型

基本类型(int、double、boolean)和装箱基本类型(Integer、Double、Boolean)之间有三个主要区别

  • 第一,基本类型只有值,而装箱基本类型则具有与它们的值不同的同一性
  • 第二,基本类型只有功能完备的值,而每个装箱基本类型还有个非功能值:null
  • 第三,基本类型通常比装箱基本类型更节省时间和空间

当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱

应该使用装箱基本类型的情况:

  • 作为集合中的元素、键和值List<Integer>

第50条:如果其他类型更合适,则尽量避免使用字符串

  • 字符串不适合代替其他的值类型,如int、boolean
  • 字符串不适合代替枚举类型
  • 字符串不适合代替聚集类型

    String compoundKey = className + "#" + i.next();
    // 这种方法有许多缺点,如为了访问单独的域,必须解析该字符串。更好的做法是,简单地编写一个类来描述这个数据集,通常是一个私有的静态成员类
    
  • 字符串也不适合代替能力表(字符串被用于对某种功能进行授权访问)

第51条:当心字符串连接的性能

字符串连接操作符(+)是把多个字符串合并为一个字符串的便利途径,但它不适合运用在大规模的场景中。为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的时间。这是因为两个字符串被连接在一起时,它们的内容都要被拷贝

建议使用StringBuilder替代String

第52条:通过接口引用对象

如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明

如果对象的基本类型是类,不是接口,应该用相关的基类(往往是抽象类)来引用这个对象,而不是用它的实现类

第53条:接口优先于反射机制

核心反射机制java.lang.reflect,提供了“通过程序来访问关于已装载的类的信息”的能力。给定一个Class实例,可以获得Constructor、Method和Field实例

反射机制(reflection)允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在。然而,这种能力也要付出代价:

  • 丧失了编译时类型检查的好处,包括异常检查
  • 执行反射访问所需要的代码非常笨拙和冗长
  • 性能损失,反射方法调用比普通方法调用慢了许多

通常,普通应用程序在运行时不应该以反射方式访问对象

对于有些程序,它们必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类。这种情况,就可以以反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例,如下例

public static void main(String[] args) {
    // Translate the class name into a Class object
    Class<?> cl = null;
    try {
        cl = Class.forName(args[0]);
    } catch (ClassNotFoundException e) {
        System.err.println("Class not found.");
        System.exit(1);
    }

    // Instantiate the class
    Set<String> s = null;
    try {
        s = (Set<String>) cl.newInstance();
    } catch (IllegalAccessException e) {
        System.err.println("Class not accessible.");
        System.exit(1);
    } catch (InstantiationException e) {
        System.err.println("Class not instantiable.");
        System.exit(1);
    }

    // Exercise the set
    s.addAll(Arrays.asList(args).subList(1, args.length));
    System.out.println(s);
}

上述程序创建了一个Set<String>实例,它的类是由第一个命令行参数指定的,该程序把其余的命令行参数插入到这个集合中

这个程序可以很容易地变成一个通用的集合测试器,通过侵入式地操作一个或者多个集合实例,并检查是否遵守Set接口的约定,以此来验证指定的Set实现。绝大多数情况下,使用反射机制时需要的正是这种方法

反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类

第54条:谨慎地使用本地方法

使用本地方法来提高性能的做法不值得提倡

使用本地方法有一些严重的缺点,因为本地语言不是安全的,所以使用本地方法的应用程序也不再能免受内存毁坏错误的影响,在进入和退出本地代码时,需要相关的固定开销

第55条:谨慎地进行优化

要努力编写好的程序而不是快的程序,好的程序体现了信息隐藏的原则:只要有可能,它们就会把设计决策集中在单个模块中

努力避免那些限制性能的设计决策,模块之间交互关系以及模块与外界交互关系的组件都有可能对系统本该达到的性能产生严重的限制

要考虑API设计决策的性能后果,如使公有的类型成为可变的,这可能会导致大量不必要的保护性拷贝

性能剖析工具有助于你决定应该把优化的重心放在哪里,这样的工具可以为你提供运行时的信息,比如每个方法大致上花费了多少时间、它被调用多少次,甚至还可以警告你是否需要改变算法

第56条:遵守普遍接受的命名惯例

标识符类型 例子
com.google.inject
类或者接口 Timer,FutureTask
方法或者域 remove,isDigit,getCrc
常量域 MIN_VALUE
局部变量 i,xref
类型参数 T,E,K,V
展开阅读全文

没有更多推荐了,返回首页