美文网首页规范
Effective Java学习笔记

Effective Java学习笔记

作者: 想54256 | 来源:发表于2019-10-26 17:05 被阅读0次

    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版的看不进去。。。

    相关文章

      网友评论

        本文标题:Effective Java学习笔记

        本文链接:https://www.haomeiwen.com/subject/quhuvctx.html