程序员社区

Effective Java学习笔记


title: Effective Java 第二版
date: 2019/06/13 11:07


二、创建和销毁对象

1、静态工厂方法代替构造器

好处:

  1. 具有名字,便于理解这个方法返回的对象有什么特殊含义
  2. 不用每次都创建一个对象
  3. 可以返回任意子类型的对象(配合继承)

可以返回不是public的类的对象,例如:Arrays

可以只修改静态方法的实现,提升性能,构造就需要修改原有的业务代码

  1. 简化构造参数列表(部分参数有默认值)

缺点:

  1. 如果没有构造器的话,那么这个类就不能继承(可以使用复合代替继承)。
  2. 可能会与其它静态方法混淆,因为并不知道这个是用来创建对象的。

2、如果构造器参数过多要考虑使用建造者模式

静态工厂方法和构造器创建对象有一个局限性,就是当构造参数过多的时候,客户端代码不好写且难以阅读。(因为有些参数是可以不传的,但是还是占着构造的参数列表中的位置)

如果javabean模式解决(采用空参构造,然后set参数进去)好处是代码可读性,但也有缺陷,因为它阻止了这个对象不可变的可能,所以还有保证他线程安全。

不可变对象的好处?

  1. 线程安全,不需要担心数据会被其它线程修改
  2. 可以很好的用作Map键值和Set元素

不可变对象最大的缺点就是创建对象的开销,因为每一步操作都会产生一个新的对象。

Integer a = 0; a++ 改变的是a这个对象,而不是a内部的值

建造者模式:

保证了aaa对象的不可变性

public class AAA {

    private final String a;

    private final String b;

    private AAA(Builder builder) {
        this.a = builder.a;
        this.b = builder.b;
    }

    public static class Builder {

        private final String a;

        private String b;

        public Builder(String a) {
            this.a = a;
        }

        public Builder b(String b) {

            this.b = b;
            return this;
        }

        public AAA build() {
            return new AAA(this);
        }
    }

    public static void main(String[] args) {

        Builder builder = new Builder("a");

        AAA aaa = builder.b("b").build();
    }

}

4、将构造器私有,防止客户端代码将不可new的类new了

5、避免创建不必要的对象

1、不要这样写

for(...) {
    String a = new String("xxx");
}
// 这样一次循环创建了2个对象

2、尽量选用小int

3、重用不可变对象

例:不要用Boolean a = new Boolean(),用Boolean a = Boolean.valueof("true")(有缓存)

每次调用都要创建一次对象的那种,如果对象不变的话,可以声明为静态的,在类加载的时候加载(亦可以延迟加载)。

6、消除过期的对象引用

1、自己管理内存,忘记释放

public class AAA {
    
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_NUMBER = 16;

    public AAA() {
        elements = new Object[DEFAULT_NUMBER];
    }

    public void push(Object o) {
        this.ensureCapacity();
        elements[size++] = o;
    }

    /**
     * 当pop的时候,没有将该对象引用清除,导致内存泄漏
     * 
     * @return
     */
    public Object pop() {
        return elements[size--];
    }

    /**
     * 正确版
     * 
     * @return
     */
    public Object pop() {
        Object o = elements[size];
        elements[size] = null;
        size--;
        return o;
    }

    /**
     * 检查数组容量
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }

}

2、缓存

可以使用WeakHashMap代表缓存

三、Object类中的方法

8、equals方法

  1. 自反性:对象必须等于自身
  2. 对称性:a.equals(b) <==> b.equals(a) 充分必要条件
  3. 传递性:a.equals(b), b.equals(c) => a.equals(c)
// equals遇上继承
public class BBB extends AAA {
    
    private String b;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        
        // 如果输入对象和该对象不匹配,返回false【这一步就是为了保证 传递性(害怕不同类型的对象有不同的验证方法,从而违反传递性)】
        if (o == null || getClass() != o.getClass()) return false;
        
        // 调用父类的方法,如果不相同,那么返回false
        if (!super.equals(o)) return false;
        BBB bbb = (BBB) o;
        return Objects.equals(b, bbb.b);
    }

使用getClass() != o.getClass()方式代替instance of有一定的局限性,因为instance of可以判断是否与属于自己的类型和自己的子类型,而getClass() != o.getClass()只能判断是否与自己的类型相同

但是为啥java7默认用的就是getClass() != o.getClass()呢?

源于java.util.Date和java.sql.Timestamp两者的对象违反对称性。

Date date = new Date();
Timestamp t1 = new Timestamp(date.getTime());

System.out.println("Date equals Timestamp ? : " +  date.equals(t1));// true
System.out.println("Timestamp equals Date ? : " +  t1.equals(date);// false

// Date的equals代码,由于使用了instance of,导致了 date.equals(t1) ==> true
public boolean equals(Object obj) {
    return obj instanceof Date && getTime() == ((Date) obj).getTime();
}

// Timestamp的equals代码
public boolean equals(java.lang.Object ts) {
    if (ts instanceof Timestamp) {
        return this.equals((Timestamp)ts);
    } else {
        // 不属于时间戳类型,直接返回false,导致了 t1.equals(date) ==> false
        return false;
    }
}

public boolean equals(Timestamp ts) {
    // 先调用Date类的equals方法,然后再比较纳秒
    if (super.equals(ts)) {
        if  (nanos == ts.nanos) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

如果我们帮他改一改,让它满足对称性:

// Timestamp的equals代码【改后】
public boolean equals(java.lang.Object ts) {
    if (ts instanceof Timestamp) {
        return this.equals((Timestamp)ts);
    } else if (ts instanceof Date) {
        // 如果属于Date类型,直接调用父类equals方法。
        return super(ts);
    }
    else {
        // 不属于时间戳类型,直接返回false,导致了 t1.equals(date) ==> false
        return false;
    }
}

然后就会发现,他不满足传递性:


Date date = new Date();

Timestamp t1 = new Timestamp(date.getTime());

Timestamp t2 = new Timestamp(date.getTime());
t2.setNanos(t2.getNanos() + 1);// 给时间戳增加一纳秒

t1.equals(date);    // true
date.equals(t2);    // true

t1.equals(t2);      // false

总结:由于Date再equals方法中使用了instance of,导致了它与子类之间没有了【对称性】和【传递性】;而且我们也不能改 Timestamp 的equals方法,那样还破坏了Timestamp类的传递性。所以这就是为什么要使用getClass() != o.getClass()方式的原因。

  1. 一致性:如果不可变对象相等,那么就必须始终保持相等
  2. 所有对象不能equals null

基本数据类型除了double、float类型,使用==比较:

public class BBB extends AAA {

    private String b;

    private double c;

    private int d;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if (!super.equals(o)) return false;
        BBB bbb = (BBB) o;
        return Double.compare(bbb.c, c) == 0 &&
                d == bbb.d &&
                Objects.equals(b, bbb.b);
    }

性能:

  1. 先比较最有可能不一致的域
  2. 不要比较冗余域(例如:使用姓名能推算出来的字段)

9、覆盖equals方法的类必须也覆盖hashcode方法

Object规范:

如果两个对象根据equals比较时相等的,那么他们的hashcode方法产生的结果也必须是相等的。


当可变对象插入HashSet中,如果对象中的值改变,不会修改他在HashSet中的位置(可能导致内存泄漏):

public static void main(String[] args) {

    HashSet<BBB> bbbs = new HashSet<>();

    BBB bbb = new BBB();
    bbb.setB("1");
    bbbs.add(bbb);

    bbb.setB("2");
    bbbs.remove(bbb);   
    bbbs.forEach(System.out::println);
}

>> BBB{b='2'}

原因:remove是根据hashcode删除的。

public static void main(String[] args) {

    HashSet<BBB> bbbs = new HashSet<>();

    BBB bbb = new BBB();
    bbb.setB("1");
    bbbs.add(bbb);

    bbb.setB("2");
    bbbs.add(bbb);
    bbbs.forEach(System.out::println);
}

>> BBB{b='2'}
   BBB{b='2'}

原因:插入时,先比较hashcode,再比较equals;2个对象的hashcode已经不同了

12、Comparable接口

public interface Comparable<T> {

    /**
     * 将当前对象和指定对象o相比,如果大于o返回一个正整数,等于o返回0,小于o返回负整数
     */
    public int compareTo(T o);
}

compare约定与equals约定差不多;强烈建议(o1.compareTo(o2) == 0) == o1.equals(o2),否则可能会破坏依赖于比较关系的类(TreeSet、TreeMap)

// 此处的范性表示当前类的类型和TreeSetTest类型比较
@Data
@ToString
@AllArgsConstructor
public class TreeSetTest implements Comparable<TreeSetTest> {

    private int a;

    private int b;

    @Override
    public int compareTo(TreeSetTest o) {
        return Integer.compare(this.getA(), o.getA());
    }


    public static void main(String[] args) {

        TreeSetTest o1 = new TreeSetTest(1, 2);
        TreeSetTest o2 = new TreeSetTest(1, 3);
        TreeSetTest o3 = new TreeSetTest(2, 2);

        TreeSet<TreeSetTest> treeSetTests = new TreeSet<>();
        treeSetTests.add(o1);
        treeSetTests.add(o2);
        treeSetTests.add(o3);

        treeSetTests.forEach(System.out::println);
    }

}

>> TreeSetTest(a=1, b=2)
   TreeSetTest(a=2, b=2)

缺少了一个。

其它:如果不实现Comparable接口,会抛出:TreeSetTest cannot be cast to java.lang.Comparable异常

由于compare约定中只要求了返回值的符号,而没有约定返回值的大小,所以如果比较的值是int型,可以使用return o1.a - o2.a这种方式提高速度。但是要注意,两者不能相差超过Integer.MAX_VALUE

四、类和接口

13、将类和成员的可访问性最小化

封装:模块隐藏所有实现细节,模块之间通过他们的API通信

好处:

  1. 解除模块之间的耦合关系;可以并行开发
  2. 提高模块的可重用性

Java通过访问控制技术实现:

1)对于类和接口上面的修饰符,只有两种访问级别:包级私有(没有修饰符)和公有的(public修饰)。

  • 包级私有:以后的发行版可以对其进行修改、替换、删除。
  • 公有:永远支持他

如果一个包级私有的类有且仅有一个类使用,可以考虑将该类变成私有内部类。

内部类的访问修饰符:

  1. public:所有地方都可以通过new A().new B()的方式访问到。
  2. private:只有类本身可以使用。
  3. protected:和不写修饰符的含义一样,为包级私有;子类无法继承内部类。

2)对于成员(方法、变量/常量、内部类)有四中访问级别。

  1. 子类对成员的访问修饰,不得低于父类的。
  2. 变量或常量指向的是一个可变对象的引用时,它的修饰符不能是公有的。否则就放弃了对他们的值进行限制的能力。

上面的第二条对于静态的同样使用,常量类中的常量除外,要尽量不要让常量中引用的是可变对象。

如果常量中存的是List、Map,可以使用枚举或不可变类的封装。

// 不可变List
ImmutableList.of(".pdf", ".doc", ".docx", ".txt", ".ini");

// 不可变Map
ImmutableMap.Builder<Integer, String> reviewPointShapeMapBuilder = ImmutableMap.builder();
reviewPointShapeMapBuilder.put(200201, "differentExtent");
reviewPointShapeMap = reviewPointShapeMapBuilder.build();

Builder的实现,上面有讲

当然,还可以将List变成私有的,然后使用get方法返回这个List的一个clone(return list.clone()

14、公有类的变量访问使用get/set方法

15、不可变类设计

规定:

  1. 保证类不被继承(final修饰或构造器私有),防止恶意子类假装对象的状态已经改变
  2. 所有的参数都使用final修饰,防止修改
  3. 所有的参数都是私有的,如果参数指向的是可变对象,有可能被恶意修改。

优点:

  1. 整个生命周期不会发生变化
  2. 线程安全,可以随意共享对象。因为根本无法修改这个变量
  3. 内部的信息也可以共享:
final int[] mag;

// 取反,mag这个数组被两个BigInteger对象公用了
public BigInteger negate() {
    return new BigInteger(this.mag, -this.signum);
}

缺点:

每做一次修改就是一个新的值,造成性能开销。

为了解决这个问题,javaer为BigInteger类提供了一个包级私有可变配套类MutableBigInteger加快计算。

其它:

可以将常用的对象缓存起来,提供一些静态工厂直接获取对象。

hashcode方法可以在第一次调用的时候计算出来,然后缓存起来。

在写一个类的时候,要尽可能的限制它的可变性,除非必须可变,这样可以降低出错。

16、复合优先于继承

谨慎继承不是自己的类

public class InstrumentedHashSet<E> extends HashSet<E> {

    // 统计这个set被插入的元素个数
    private int addCount = 0;

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public static void main(String[] args) {

        List<Integer> integers = Arrays.asList(1, 2, 4, 3);
        InstrumentedHashSet<Integer> set = new InstrumentedHashSet<>();
        set.addAll(integers);

        System.out.println(set.addCount);
    }
}

>> 8

由于这个类不是我们写的,我们并不知道父类的addAll方法调用了add方法;所以导致结果不对。
当然我们可以根据测试的结果修改我们的代码,但是,如果有一天父类修改了addAll方法的实现,他并不会管我们这个实现类,从而再次导致错误。

还有,如果我们有一个需求:要在add元素到set中之前做判断;我们可以通过重写add、addAll方法来实现,如果日后父类又多加了一个addxxx的方法,如果客户端通过这个方法添加元素,就不受我们的控制,从而导致错误。

上面的问题都是由于重写导致的,其实,如果我们继承之后新增方法也会导致问题,如果我们新增的方法和父类后来添加的方法,**方法签名一样,返回值类型不同**,会导致子类编译不通过。

所以如果要继承不是自己的类,优先考虑使用复合。

复合的方式有一个缺点,就是不能用作回调,想想@Async为啥this.xxx()调用不好使。

只有当子类真的是父类的子类型(is-a关系),才应该使用继承。

18、使用接口优先于抽象类

骨架类:

例如AbstractList、AbstractSet、AbstractMap等都是骨架类

// 其中List继承了Collection接口,AbstractCollection实现了Collection接口
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

实现了一个接口(规范相同行为),继承了一个抽象类(实现相同的部分)

19、不要将接口用作储存常量的地方

接口是用来被实现的,而不是让你存放常量的,而且如果使用接口存放常量,客户端类编译过程中会将常量直接编译进去,从而导致,修改了常量(替换了接口的class文件)但是实际执行还是原来的常量。

22、内部类

内部类有四种:

1、静态内部类

可以把它看作是一个普通的类,只是被声明到了一个类的内部。

通常作为公有的辅助类,例如:Calc.Optional.PLUS 表示计算器类的加法操作

2、非静态内部类

主要目的:为外围类提供服务。例如Map.Entry<K,V>

非静态内部类的实例隐含着一个与外围实例之间的关联。这个关联关系消耗了空间和构造的时间开销。

非静态内部类可以使用外围类的所有成员和方法。

public class NestTest {

    private String a;

    public class A {
        public void test() {
            String a = NestTest.this.a;
        }
    }
}

非静态内部类中不能有static方法。

由于它的每一个实例都有一个额外的引用指向外围类的实例,所以如果可以不用到外围类的实例信息(非静态成员、方法),那么就要考虑使用静态内部类了。

3、匿名内部类

4、局部类

五、泛型

25、列表优先于数组

// 合法,运行时会报错(运行时对类型检查)
Object[] objects = new Long[3];
objects[1] = "xxx";

// 不合法,编译时报错(编译时对类型进行检查)
ArrayList<Long> longs = new ArrayList<>();
longs.add("xxx")

使用列表可以在编译的时候就提醒我们报错。

注意:Java不能创建泛型数组(new List<E>[]、new E[])

这是Java初期的设计缺陷导致的,由于初期Java不支持泛型,为了不修改JVM代码,所以将Java中的泛型在运行时进行擦除

而Java对数组中的类型在运行时检查,而泛型在运行时已经被擦除掉了。

假设Java支持泛型数组

// 创建一个List<String>[]
List<String>[] lists = new ArrayList<String>[3];

// Object[]是List<String>[]的父类型,所以可以直接赋值
Object[] objects = lists;

// 由于对数组中的东西是运行时检查的,但是运行时List<String>已经被擦除成List了,Arrays.asList(123)也被擦除成List,所以不会报错
objects[0] = Arrays.asList(123);

// 当获取数组中集合中的字符串的时候,由于编译器会自动在取出的时候加上强转,所以会抛出类型转换失败异常。
System.out.println(lists[0].get(0));

为了防止这种情况的发生,Java直接产生编译时错误

从而会导致,可变参数方法使用泛型会提示警告。

public static void main(String[] args) {
    func(Collections.singletonList(231));

}

// 禁止警告
@SafeVarargs
private static void func(List<Integer>... args) {
    
}

27、使用泛型方法

泛型方法的泛型可以通过返回值的泛型和入参的泛型来确定。

28、使用有限制的通配符

List<String>不是List<Object>的子类型,所以无法强转

interface Stack<E> {
    void push(E e);

    void pushAll(Iterable<E> es);

    E pop();
}

class StackImpl<E> implements Stack<E> {

    @Override
    public void push(E e) {

    }

    @Override
    public void pushAll(Iterable<E> es) {
        es.forEach(this::push);
    }

    @Override
    public E pop() {
        return null;
    }
}

public static void main(String[] args) {

    Stack<Number> stack = new StackImpl<>();
    stack.push(123);

    // 这句话会报编译时错误,类型不正确,因为Iterable<Integer>不是Iterable<Number>的子类
    stack.pushAll(Arrays.asList(1, 2, 3).iterator());
}

解决办法,可以将上面代码中的所有<E>改成<? extends E>

假如新增一个popAll方法,将所有元素放入给定的集合中:

@Override
public void popAll(Collection<E> es) {
    while (栈不为空) {
        es.add(this.pop());
    }
}

// 这样也会报编译时错误,因为List<Number>不是List<Object>的子类型
public static void main(String[] args) {   
    Stack<Number> stack = new StackImpl<>();
    List<Object> objects = new ArrayList<>();
    stack.popAll(objects);
}

解决办法,可以将代码中的所有<E>改成<? super E>

为啥? extends Fruit不能add元素

public class ListTest {

    // 这里传入的List是【Fruit的子类】集合,但是编译器并不知道?代表的是哪个子类,所以无法add元素
    public void func2 (List<? extends Fruit> list) {
        // list.add(new Fruit());   报错
        Fruit fruit = list.get(0);
    }

    // 这里传入的List是【Apple的超类】集合,所以编译器知道Apple和它的子类都可以被添加到这个集合中
    public void func3(List<? super Apple> list) {
        list.add(new Apple());
        list.add(new RedFuShi());
        // list.add(new Fruit());   报错
        // list.add(new Orange());  报错

        // 由于编译器并不知道List中Apple的超类到底是谁,所以使用Object声明
        Object object = list.get(0);
    }
}

class Fruit {}
class Apple extends Fruit {}
class RedFuShi extends Apple {}
class Orange extends Fruit {}

6、枚举和注解

30、枚举

枚举其实就是用来存放一组固定常量的(例如:季节就可以是一个枚举,因为它是一种东西,而且里面包含的常量也是固定的)。

枚举的高级用法:

/**
 * 计算器的加减乘除枚举
 */
public enum Operation {

    PLUS, MINUS, TIMES, DIVIDE;

    public double apply(double x, double y) {
        switch (this) {
            case PLUS:
                return x + y;
            case MINUS:
                return x - y;
            case TIMES:
                return x * y;
            case DIVIDE:
                return x / y;
            default:
                throw new RuntimeException();
        }
    }
}

上面的代码有一个问题,那就是如果最后不向外抛出异常,就没办法编译通过,但从逻辑上我们可以知道,上面的代码永远不会抛出异常。所以可以使用下面的方式优化:

public enum Operation {

    PLUS("+") {
        @Override
        double apply(double x, double y) {
            return x + y;
        }
    }, MINUS("-") {
        @Override
        double apply(double x, double y) {
            return x - y;
        }
    }, TIMES("*") {
        @Override
        double apply(double x, double y) {
            return x * y;
        }
    }, DIVIDE("/") {
        @Override
        double apply(double x, double y) {
            return x / y;
        }
    };

    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    // 最好把这个抽象方法抽取出来作为一个接口
    // 实现接口的方法,可以由枚举类自己实现,也可以让枚举常量(对象)自己实现
    abstract double apply(double x, double y);

    @Override
    public String toString() {
        return symbol;
    }
}


// 每个枚举天生就带有一个valueOf方法,通过枚举常量值来获取枚举对象的。
public static void main(String[] args) {
    Operation plus = Operation.valueOf("PLUS");
    System.out.println("plus = " + plus);
}

>> plus = +

如果有多个枚举常量共享想通过的行为,可以考虑使用策略枚举

所谓策略枚举就是使用了策略模式的枚举,例如要获取每天的工资:

public enum PayrollDay {

    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    private static final int HOURS_PER_SHIFT = 8;//正常工作时数

    /**
     * 工资计算
     *
     * @param hoursWorked 工作时间(小时)
     * @param payRate     每小时工资
     * @return
     */
    double pay(double hoursWorked, double payRate) {

        //基本工资,注这里使用的是double,真实应用中请不要使用
        double basePay = hoursWorked * payRate;

        //加班工资,为正常工资的1.5倍
        double overtimePay;

        switch (this) {
            case SATURDAY:
            case SUNDAY://双休日加班工资
                overtimePay = hoursWorked * payRate / 2;
                break;
            default: //正常工作日加班工资
                overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
                break;
        }

        return basePay + overtimePay;//基本工资+加班工资

    }
}

上面的代码没错也很简单,但是如果新加一天,然后忘了修改下面的pay方法,就会导致新加的那天可能是要加班工资,但是还按照平时工资来计算的(虽然一般不可能发生),策略枚举就是为了解决这个问题的:

// 将策略(计算方式)通过构造传入
public enum PayrollDay {

    MONDAY(PayType.WEEKDAY),
    TUESDAY(PayType.WEEKDAY),
    WEDNESDAY(PayType.WEEKDAY),
    THURSDAY(PayType.WEEKDAY),
    FRIDAY(PayType.WEEKDAY),
    SATURDAY(PayType.WEEKEND),
    SUNDAY(PayType.WEEKEND);

    private final PayType payType;

    PayrollDay(PayType payType) {
        this.payType = payType;
    }

    double pay(double hoursWorked, double payRate) {
        return payType.pay(hoursWorked, payRate);
    }

    //the strategy enum type  
    private enum PayType {
        WEEKDAY {
            double overtimePay(double hours, double payRate) {
                return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            double overtimePay(double hours, double payRate) {
                return hours * payRate / 2;
            }
        };

        private static final int HOURS_PER_SHIFT = 8;

        abstract double overtimePay(double hoursWorked, double payRate);

        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked, payRate);
        }
    }
}

对未知枚举的操作,可以这样写:

private static <T extends Enum<T> & Operate> void test(Class<T> c) {
    // 返回此枚举类的元素,如果此Class对象不表示枚举类型,则返回null。
    T[] enumConstants = c.getEnumConstants();
}

七、方法

38、在方法执行前,对参数进行检查

对于公有方法,应该在注释上明确写出方法参数的限制,并使用@throws表明违反参数值限制会抛出的异常。

/**
 * xxx
 * 
 * @param bigInteger xxx不能小于等于0
 * @return xxx
 * @throws RuntimeException 如果xxx小于等于0将会抛出该异常
 */
public BigInteger mod(BigInteger bigInteger) {
    if (bigInteger.signum() <= 0) {
        throw new RuntimeException();
    }
    ...
}

对于非公有方法,这些方法是我们使用的,推荐使用断言assert:

private void func(int a) {
    assert a < 0;
    ...
}

构造器的参数也要进行检查。

39、必要时进行保护性copy

class Period {

    private final Date startTime;

    private final Date endTime;

    public Period(Date startTime, Date endTime) {
        if (startTime.compareTo(endTime) > 0) {
            throw new RuntimeException("xxx");
        }

        this.startTime = startTime;
        this.endTime = endTime;
    }
}

public static void main(String[] args) {
    Date startTime = new Date();
    Date endTime = new Date();

    Period period = new Period(startTime, endTime);

    // 由于Date类不是不可变对象,所以客户端如果这样恶意操作,将会导致时间不对
    endTime.setTime(123L);
}

我们只需要修改一下构造,就可以防止这种情况:

this.startTime = new Date(startTime.getTime());
this.endTime = new Date(endTime.getTime());

这里我们重新new了一个Date对象,而不是使用clone()方法,原因是因为Date类可以被继承,防止子类恶意重写clone()方法。

40、

1、方法名称要易于理解,同一个包内风格要一致。

2、参数列表不要过长

解决办法:

  1. 把方法拆分,一个方法只需要少量参数
  2. 创建辅助类(通常是静态内部类),代表一种东西(例如:一个方法有6个参数,但是有4个是和狗相关的,那么就可以建一个静态内部类来表示他)
  3. 创建一个类,把所有参数set进去。

41、慎重使用重载,尤其是方法参数个数相同的时候

覆盖是在运行时进行的。

重载是在编译期进行选择的。

自动装箱和泛型的出现 破坏了List接口

E remove(int index);

boolean remove(Object o);

如果List的泛型是Integer类型的,那么调用remove方法就不知道调用哪个了。

42、当返回值是数组或者集合的时候,不要返回null

if (list == null) {
    return new ArrayList(0);
}

八、通用程序设计

45、局部变量作用域最小化

用到的时候再声明

48、如果需要精确的答案,不要用double和float

用Big浮点或int/long型代替

49、尽量使用基本类型

但是类中的全局变量(例如POJO),要使用包装类型,因为基本数据类型默认值是0(比如,数据库中存的是null,但是实体却告诉我值是0,这显然是不正确的)

九、异常

57、不要使用异常做流程控制

还有我们设计的API也不要强迫客户端来使用异常来做控制流:

// 如果Iterator类没有提供hasNext()方法,那么我们只能使用下面这种方式来进行书写
Iterator<Integer> iterator = Arrays.asList(1, 2, 3).iterator();
try {
    while (true) {
        System.out.println(iterator.next());
    }
} catch (Exception e) {
    e.printStackTrace();
}

所以我们的Api可以采用以下两种做法:

  1. 提供状态测试方法:例如hasNext()方法
  2. 可识别的返回值:一般用于,对象被高并发访问时

58、Api中使用异常

受检异常:期望Api调用者能够适当的恢复。

运行时异常:Api调用者没有遵守Api的规范。

错误:JVM使用

60、优先使用Java提供的运行时异常

异常 使用场合
IllegalArgumentException 客户端传递的参数不合适
IllegalStateException 传递的对象状态不合适(iterator.remove().remove())
NullPointerException 传递的参数为空
IndexOutOfBoundsException 传递的参数对数组的操作下标越界了
ConcurrentModificationException 禁止并发的时候,对对象进行并发修改了
UnsupportedOperationException 对象不支持用户请求的方法(不可变List的set方法)

62、异常转译

如果不能阻止或处理底层的异常,要使用异常转译。除非底层方法抛出的异常恰好能用于高层。

63、异常的消息中包含失败的信息(参数等)

64、如果抛出异常了,要将之前的修改全部恢复(原子性)

可以通过调整计算处理的顺序解决

十、并发

67、避免在synchronized代码块中调用外面的方法;不要过度同步

Demo看书,里面有笔记

68、使用线程池不要直接使用线程

69、使用并发工具不要使用wait、notify

wait、notify代码过于复杂,容易写错

// 使用ConcurrentHashMap模拟String.intern()方法
private static final Map<String, String> map = new ConcurrentHashMap<>();

public static String intern(String s) {
    String s1 = map.putIfAbsent(s, s);
    return s1 == null ? s : s1;
}

// 使用双重检查锁进行优化(节省时间)
public static String intern(String s) {
    String s1 = map.get(s);
    if (s1 == null) {
        s1 = map.putIfAbsent(s, s);
        if (s1 == null) {
            s1 = s;
        }
    }
    return s1;
}

第三版新增

42、lambda表达式代替匿名类

编译器是通过泛型来推导lambda表达式中的类型的

// 有了lambda表达式,就可以将上面的枚举进行优化
public enum Operation {

    PLUS("+", (x, y) -> x + y);
    ...

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
}

如果代码很简单,可以使用lambda表达式(少于3行)

lambda无法获得自身的引用(this,虽然他是一个匿名类)

43、优先使用方法引用(::)

44、Stream流只支持int、double、long

不能处理char型

pdf版的看不进去。。。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » Effective Java学习笔记

一个分享Java & Python知识的社区