EFFECT JAVA笔记

EFFECT JAVA笔记

一、创建和销毁对象

1.考虑使用静态方法代替构造器

实现:
私有化构造方法,提供静态的方法以提供实例(newInstance)
优点 :
1. 构造器的参数本身没有确切地描述正被返回的对象. 而提供的静态方法有自己的名称,更加容易阅读.
2. 不必在每次调用的时候都创建一个新的对象. 如果程序经常请求创建相同的对象 , 那这种方式极易提升性能. (单例模式)
3. 返回原返回类型的任何子类型的对象. (工厂模式)
4. 创建参数类型实例的时候,使代码变得更加简洁.(使用泛型)
缺点 :
1. 类如果不含共有的或者受保护的构造器, 就不能被子类化.
2. 与其他静态方法实际没有任何区别,不能像构造方法那样被标识出来
例:

    //使用泛型使代码变得更加简洁
    //通常做法
    Map> map=new HashMap();
    //改造方法
    public static  HashMap newInstance(){
        return new HashMap();
    }
    //改造后用法
    Map> map=newInstance();
    //Guava提供了一系列集合静态工具,可以直接使用.此处只是举例.

2.遇到多个构造器参数时要考虑用构建器

*场景:初始化对象实例具有多个构建参数,且存在可选不可选参数.
一般情况:
1. 提供多个构造器,每个构造器都具有所有的必填参数和相应的可选参数
2. 提供一个必填参数构成的构造器,使用set方法设置可选参数
* 第一种方法会使得构造方法过多,不利于阅读及使用.
* 第二种情况构造的过程被分到了多个步骤中,在构造过程中对象本身处于不一致的状态.需要程序员自己保证线程安全.且使用这种方法把类限制为可变状态,无法提供不可变的类.
使用构建器
构建器不直接生成对象,利用所有必填参数生成一个构造对象(builder),在builder上调用类似setter的方法设置可选属性,最后利用无参的build方法构造出原有对象.好处有:
* 灵活:可以利用一个builder构建多个对象
* 安全:在参数齐了之后才开始构建对象
不足之处
* 为了创建对象必须要先创建构造器(创建对象开销)
* 书写起来比构造方法更加冗长

public class Student {
    private int id;
    private String name;
    private int age;
    private int field1;
    private int field2;
    private int field3;

    private Student(Builder builder) {
        int id = builder.id;
        String name = builder.name;
        int age = builder.age;
        int field1 = builder.field1;
        int field2 = builder.field2;
        int field3 = builder.field3;
    }

    //构建器
    public static class Builder {
        //必填参数
        private int id;
        private String name;
        private int age;
        ///选填参数/
        private int field1;
        private int field2;
        private int field3;

        //构建器的构造函数 里面放有必填参数
        public Builder(int id, String name, int age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        public Builder field1(int field1) {
            this.field1 = field1;
            return this;
        }

        public Builder field2(int field2) {
            this.field2 = field2;
            return this;
        }

        public Builder field3(int field3) {
            this.field3 = field3;
            return this;
        }

        //构建对象
        public Student build() {
            return new Student(this);
        }
    }
}
//使用lombok的@Build标签可以直接实现,不用自己去写冗长的代码

在使用的时候:
Student stu=new Student.Builder(1,"test",18).field1(10).field2(11).build();

3.使用私有构造器或者枚举类型强化Singleton属性

单例模式
最佳单例模式的实现 : 枚举Enum单例模式
不再详细叙述;

4.通过私有构造器强化不可实例化的能力

场景:需要编写只包含静态方法和静态域的类
类的静态方法调用, 无需实例化构造器
做法
* 私有化构造函数
* 在私有构造函数中抛出异常,防止反射调用构造方法.
-错误做法:写成抽象类,会误导其他人,认为此类是为了被继承/实现而存在的-

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

可重用的变量, 可以设置为静态变量, 并且通过静态代码块来初始化, 达到避免重复创建对象的目的

private static final Persion PERSION;
static{
    PERSION = new Persion();
}
//有时候这种做法会要求我们维护自己的对象池

6.消除过期对象引用

场景1:在使用数组时,先增加里面的元素再减少,如果在减少时仅仅只是把标记位置的数字减小并未清除减少位置所引用的对象,可能会引起内存泄漏.
解决方法:清空过期引用, 常用方式 object = null;
场景2:将对象放入缓存中而不考虑清理
解决方法:若内存泄漏来源于缓存 , 建议 WeakReference包裹之;(例:WeakHashMap)
场景3:在提供一个可以注册回调函数的api时,除非你采取了某些动作,否则回调的匿名函数会积聚
解决方法:回调以及监听持有的匿名类, 建议也用WeakRefence包裹.

7.避免使用终结方法

定义:终结方法是finalize(),Java中所有类都从Object类中继承finalize()方法.
运行时机:finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的.
缺陷:
1. 不能保证会被及时的执行,从对象到达不可达到终结方法执行,这个时间是任意长的,注重时间的任务不应由终结方法执行.
2. 在某些情况下,为类增加终结方法可能会任意延迟其实例的回收过程.
3. 在java规范中甚至不保证终结方法会被执行,也就是说 完全有可能到程序终止的时候某些对象上的终结方法根本没有被执行.
4. 使用终结方法有一个非常严重的性能损失.创建和销毁一个简单的对象提供终结方法可能会慢数百倍.
结论:不应当依赖终结方法来更新重要的持久态
解决方案:使用try{}finally{}代码块 , 释放资源.
正确用法:
1. 对象忘记调用显式终止方法时充当安全网的作用.
2. 终结本地对等体.普通的方法通过本地方法(native method)委托给一个本地对象进行执行,这个本地对象就是本地对等体,他不受jvm管理,不会被回收,在本地对等体不持有关键资源的情况下很适合使用终结方法将其回收.
注意事项:若子类覆盖实现了超类的终结方法,必须需要手动调用超类的终结方法.

二、对所有对象都通用的方法

8.覆盖equals时请遵守通用约定

明确概念:
1. 类的每一个实例本质上唯一.
2. 超类已经覆盖了equals,从超类继承过来的行为对于子类也同样适用.
3. 若类是私有的,且可以确定其equals方法永远不会被调用时,应当覆盖其equals方法以防止其被意外调用.

//3.确保equals不被意外调用
@override
public boolean equals(Object obj){
    throw new AssertionError();
}

重写时机:判断比较引用对象,在逻辑上是否相等,而非是否指向同一个对象.
euqals性质:
1. 自反性 :x.equals(x)必须返回为true.
2. 对称性 :x.equals(y)若为ture,则y.equals(x)必须为true
3. 传递性 :x.equals(y)为ture, y.equals(z)为true,则x.equals(z)为true
4. 一致性 :对于非null引用,x.equals(y)若为ture,只要比较中所用到的信息没有被修改,则x.equals(y)恒返回true
5. 非空性 :被比较对象一旦为空,立即返回false(防止抛出NP异常)
实现equals()方法 :
1. 使用==操作符检查”参数是否为这个对象的引用”
2. 使用instanceof操作符检查”参数是否为正确的类型”
3. 把参数转换成正确的类型
4. 对于该类中的每个”关键”域,检查参数中的域是否与该对象中对应的域相匹配
5. 实现完成后,是否满足一致性,对称性,传递性
注意事项:
1. 覆盖equals时总要覆盖hashCode(见第9条)
2. 不要企图让equals过于智能(不要过度寻找等价关系)
3. 不要将equals方法中的Object类型参数换成其他的类型(会变成重载)

9.覆盖equals时总要覆盖hashCode

原因:
1. Object规范中要求,相等的对象必须拥有相等的散列码(hashcode)
2. HashMap与HashSet等集合类会依赖于对象的hashcode来储存
优化:若一个类是不可变类,且计算散列码开销较大,则应当考虑将缓存 码储存在对象中.

//延迟初始化(lazily initialize) 散列码
private volatile int hashCode;

@Override
public int hashCode() {
    int result = hashCode;
    if (result == 0) {
        result = 17;
        result = 31 * result + param;
        hashCode = result;
    }
    return result;
}

10.始终要覆盖toString()

原因:
1. toString() 返回值中包含的所有信息 , 提供一种编程式的访问途径.
2. 不重写的话, 打印的值是 “object ” + “@” + 散列码的无符号十六进制表示法

11.谨慎的覆盖clone

背景: Cloneable接口没有任何方法,其作用是Object受保护的clone()方法在进行clone操作时校验,若当前类没有实现Cloneable,则抛出异常(此种接口使用行为不值得效仿); clone操作将会对对象所有的域进行逐级拷贝.
谨慎使用的原因:
若Cloneable对某个类起到作用,则类和他所有的超类都必须遵守一个相当复杂的,不可实施的,基本没有文档说明的协议,从而获得一个语言之外的机制——无需使用构造器就可以创建对象.
浅层clone

//需要注意的是简单clone不会拷贝引用对象的内部属性
@Override
protected Object clone() throws CloneNotSupportedException {
    return super.clone();
}

深层clone

//深层clone需要你手动的clone每一个引用类型对象
public class Student implements Cloneable {
        private String name;
        private int age;
        private Bag bag;

        @Override
        protected Student clone() throws CloneNotSupportedException {
            Student stu = (Student) super.clone();
            stu.bag = bag.clone();
            return stu;
        }
    }

    class Bag implements Cloneable {
        private int width;
        private String logo;

        @Override
        protected Bag clone() throws CloneNotSupportedException {
            return (Bag) super.clone();
        }
    }
//现在编码过程中需要进行数据属性拷贝时可以使用spring的BeanUtil类实现

12.考虑实现Comparable接口

原因:
1. 由于Java语言对Comparable接口的良好支持,不但允许进行简单的比较,而且允进行顺序比较,与euqals方法具有相似的特征,是一个泛型接口,类实现Comparable接口就代表其实例具有排序关系.
2. 一旦实现Comparable,就可以跟许多泛型算法及依赖该接口的集合实现进行协作(例如:Arrays.sort())

三、类和接口

13.使类和成员可访问最小化

实现方法:
信息隐藏和封装 : 模块之间通过API进行通信(一个模块不需要知道其他模块内部工作的情况)
理由:实现模块间的解耦,使得这些模块可以被独立的开发测试优化使用和理解.
Java提供了访问控制(private default protected public)进行支持.
原则:
1. 尽可能地使每个类或者成员不被外界访问
2. 实例域决不能是公有的.(见14条)

14.在公有类中使用访问方法而非公有域

即:公有类永远都不应该暴露可变的域,如果类可以在它所在的包的外部进行访问,就提供访问方法.简单来说就是封装.
原因:若实例中的成员变量直接暴露出去的话,我们就无法再不改变api的情况下改变数据表示法,也无法强加任何约束条件.

15.使可变性最小化

即创建不可变类
不可变类指的是实例不能被修改的类,每个实例所拥有的信息必须在创建该实例时提供,并且在对象的生命周期内固定不变.
创建不可变类的规则:
1. 不要提供任何会修改对象状态的方法
2. 保证类不会被拓展, 也就是防止子类化, 常用做法是使用final修饰
3. 使所有的域都是final
4. 使所有的域都成为private
5. 确保对于任何可变组件的互斥访问
6. 没有set方法 , 内部域均为final private

优点
1. 简单,只有一种状态,且能保证在生命周期内不会发生改变.
2. 线程安全, 不存在同步,因为是不可变对象
缺点: 代价高, 对于每个不同的值都需要一个单独的对象
另:不可变类一般使用静态工厂且私有化其构造器(见第1条)

16.复合优先于继承

继承的缺点:与方法调用不同的是, 继承打破了封装性.(子类依赖于超类,若超类由api提供在升级过程中发生变动,子类就可能会遭受破坏)
注意:构造器决不能调用可被覆盖的方法.(超类方法)
复合:使用装饰者模式来替代继承(使新的类型持有原有对象)

17.要么为继承而设计,并提供文档说明,要么就禁止继承

第16条提醒我们外来的子类有多么危险,对于为了继承而设计的类来说,我们需要写出非常明确的文档说明其实可以被覆盖的,方法是有自用性的.
自用性的影响:超类依赖自己内部可以被覆盖的方法进行实现,若此时该方法被子类覆盖,则可能导致超类方法功能发生改变.例如:AbstractCollection类,当你覆盖其itertator方法时,会影响到其remove方法的行为.
注意事项:为了继承而设计的类,唯一的测试方法是编写子类.

18.接口优于抽象类

优势
1. 现有的类可以很容易被更新,以实现新的接口
2. 接口使定义mixin(混合类型)的理想选择
3. 接口允许我们构造非层次结构的类型框架,避免了类层次的臃肿和类臃肿(功能单一化,并且可以互相组合,举例作曲家歌唱家).

mixin(混合类型):类除了实现它的基本类型外,还可以实现这个mixin类型,以表明它提供了某些可选择的行为.例如:Comparable接口,这个功能可以被选择实现到任何类中,表明这个类提供的实例之间可以互相比较,排序.

19.接口只用于定义类型

接口应该只能被用来定义类型 , 不应该被用来导出常量.
常量接口(一个接口中只有定义的final常量)是对接口的不良使用.因为接口代表着承诺,假若你实现了这个常量接口,之后你的类不再需要使用这些常量了你也必须实现这个常量接口以保证兼容性.
程序的常量最好是保存在枚举类中或者是不可实例化的工具类中

20.类层次优于标签类

标签类

public class Figure {
    enum Shape {
        RECTANGLE , CIRCLE
    }
    final Shape shape;

    public Figure() {
        this.shape = Shape.CIRCLE;
    }
}

让一个类型既可以表示圆也可以表示长方形,把两个类型的属性放在一个类当中,通过标签进行区分.缺点很明显: 冗余, 容易出错, 效率低下.

类层次:简单来说就是子类型化,上面的例子的替代方案是将圆与长方形子类型化,继承/实现Figure.

21.用函数对象表示策略

java可以通过对象引用实现类似于其他语言的函数指针的功能.
基本操作:
1. 调用对象的方法通常是执行该对象的某项操作,我们可以定义一个对象,让他的方法执行其他对象上的操作.
2. 如果一个类仅仅导出这样的一个方法,他的实例就等同于一个指向该方法的指针.
例:

class StringLengthComparator{
    public int compare(String str1,String str2){
        return str1.length()-str2.length();
    }
}

StringLengthComparator实例是用于字符串比较操作的具体策略:使用字符串长度对String进行比较.
但是将StringLengthComparator实例传递给客户端并不好,因为客户端无法使用StringLengthComparator以外的其他策略 所以需要额外定义一个策略接口.

public interface Comparator{
    public int compare(T t1,T t2);
}

客户端通过这个策略接口去使用不同的策略.
策略实例应实现这个接口

class StringLengthComparator implements Comparator<String>{
    public int compare(String str1,String str2){
        return str1.length()-str2.length();
    }
}

Comparator是java.util定义的一个接口,在集合操作中可以通过这个策略进行一系列集合操作,例如排序.

StringLengthComparator stringLengthComparator =new StringLengthComparator();
Arrays.sort(stringArray, stringLengthComparator);

优化:由于策略是无状态的,可以将其设置为单例以减小实例创建开销.
注意事项:当一个策略只被使用一次时,通常我们习惯于使用匿名类来声明个实例化.

Arrays.sort(stringArray, new Comparator<String>(){
        public int compare(String str1,String str2){
            return str1.length()-str2.length();
        }
    });

22.优先考虑静态成员类

嵌套类:定义在一个类内部的类,有四种类型:静态成员类,非静态成员类,匿名类,局部类.
静态成员类:最正常的一个内部类,与正常类没有区别,只是恰巧定义在了别的类内部.他可以访问外围类的所有成员,包括私有.静态成员类是外围类的一个静态成员,他也遵循着可访问性规则,如果定义为private则只有在外围类的内部可以被访问.
常见用法:作为公有类的辅助类,例如Map中的Entry
非静态成员类:与静态成员类相比,最大的区别是少了staic关键字,但是区别却很大,他不独立与静态成员类,每一个非静态成员类的实例都必须包含一个外围实例,他可以访问外围实例的方法,或者利用修饰过this的构造获得外围实例的引用.在没有外围实例的情况下无法创建非静态成员类.随着非静态成员类的实例被创建时,其与外围实例类的关系随之被确定下来且以后不可修改.
常见用法:用于构建adapter适配器.它允许将外部类实例看做另一个不相关的实例.例如 Map中的集合视图(Values类 KeySet类和EntrySet类)和他的迭代器(Iterator)
匿名类:不同于其他语法单元,匿名类没有名称,他不是一个外围类的一个成员,不与其他成员一起呗声明,而是在使用的时候同时被声明和实例化.匿名类可以被声明在代码中任何允许存在表达式的地方,当且仅当匿名类出现在非静态环境中时,他才拥有外围实例.无论是否存在于静态环境中,他都不可能拥有静态成员.
常见用法:1.用于动态的创建函数对象(21条)2.创建过程对象(例如Thread)3.静态工厂方法内部(构建多个不同方法的实例用于取用)
局部类:几乎不会使用,在声明方法的局部变量的地方可以任意使用,和匿名类的区别是他有名字,可以被重复使用,不能太长否则会影响可读性.

四、泛型

23.不要在新代码中使用原生态类型

泛型的定义
声明中具有一个或者多个 类型 参数的类或者接口,就是泛型.
例如List接口在java1.5版本之后就只有单个类型参数E,表示元素类型.即List
原生态类型
每一个泛型的类或者接口,都定义着一个原生态类型,即不带任何实际类型参数的泛型名称 List对应的原生态类型为List
不要再新代码中使用原生态类型的原因
程序出错应该尽快的发现,最好是在编译时期就发现.如果使用原生态类型会导致在填入泛型参数时可以放入任意类型参数而不会出错,在取用时如果需要转换成特定类型就会抛出错误.
泛型在安全性和表述性上具有优势 之所以提供原生态类型是为了对之前版本的兼容.
通过使用原生态类型可以将泛型的安全性去除,如下图
EFFECT JAVA笔记_第1张图片
这时候进行编译的时候会提示你使用了不安全的操作.
但是,有时候我们希望写一些通用的方法, 比如统计两个Set的元素总数.这时候我们并不关心两个Set里存的类型参数,即使不一致也不会影响到统计结果.这时候我们可以使用无限制通配符类型

24.消除非受检警告

场景:在出现泛型和非泛型互相转换时,就会产生非受检警告,它本身并不影响正确的程序的执行,但是有可能会发生类型转换错误的情况。
解决方案:简单的场景泛型两侧一致,复杂的场景,尤其涉及到数组的情况下,在自己保证转换的安全的前提下使用@SuppressWarning(“unchecked”)来消除.注意要在尽可能小的范围内使用.

25.列表优先于数组

数组是协变的:如果Sub是Super的子类,那么Sub[]是Super[]的子类.
泛型是不可变的:List与List没有关系.
数组的协变性这可能会导致如下的情况:
EFFECT JAVA笔记_第2张图片
通过数组协变得特性我们可以轻易的把一个String塞进Long数组里而编译器不会抛出任何编译时错误.数组是有缺陷的.
由于数组和泛型列表的本质区别,导致两者不能很好地混用:创建泛型数组是非法的.如List[];
实际上在程序中我们可以使用泛型数组,如E[] item=(E[])lsit.toArray;编译器会抛出警告,但是这样的程序是可以运行的,但是不安全,实际上使用的是Object[].只能在特殊情况下使用.(见26条)

26.优先考虑泛型

在某些基础类中,如List相关的类.底层数据储存总要使用数组.在这种情况下会遇到不得不使用一些方法混用泛型和数组.在创建泛型数组时我们无法创建不指定类型的数组E[] item=new E[1];这种是不允许的,于是我们使用Object进行替换,再转换成E[]类型:E[] item=(E[])new Object[1];
使用泛型比使用需要在客户端代码中进行转换的类型更加安全,也更加容易.

27.优先考虑泛型方法

类似23条中的统计set中的数量,在合并两个set时,不使用泛型时无法对传入数组中数据类型做出限制.如果使用泛型则可以保障其中的数据类型,且可以对传入参数做出规范:
EFFECT JAVA笔记_第3张图片
这个方法保证了传入的两个Set参数中的数据类型必须一致,且告知返回值Set中储存的数据.
这种泛型使用方法常使用在静态函数中.
递归类型限制
虽然很少见,但通过某个包含该类型参数本身的表达式来限制类型参数是被允许的.举个例子 Camparable接口.通过集成该接口来表明类的队形是可以进行比较的.那我们如何约束进入比较方法的对象是实现了Camparable接口的呢?
比如我们现在有一个函数,用来找出一个继承了Camparable接口的对象列表中最大的那个对象.

public static > T max(List list);

这种递归式限制并不常见,如果想要使用请参考第28条,利用有限制的通配符来提高Api的灵活性

28.利用有限制的通配符来提高API的灵活性

有限制通配符号分为两种

public static  super T>> T max(List extends T> list);

对于传入参数T而言 他是生产者,负责提供T的实例进行操作.
T的camparable消费T的实例,用于比较.

29.优先考虑类型安全的异构容器

异构容器
所有键都不是同类型的 , 即 Class

五、枚举和注解

30.用enum枚举类型代替int常量

枚举示例:
EFFECT JAVA笔记_第4张图片
枚举的本质是int
枚举类型为类型安全的枚举模式
策略枚举 : 利用抽象方法, 避免switch过于复杂,冗余
EFFECT JAVA笔记_第5张图片

31.用实例代替序数

enum虽然自带序数,可以简单的通过ordinal()方法取到,但是如果常量进行重新排序就会十分困难,且无法给某个int值添加常量.
结论是不要根据枚举的序号导出他的值,而是给每个枚举值一个序号的属性:
EFFECT JAVA笔记_第6张图片

32.用EnumSet替代位域

位域
EFFECT JAVA笔记_第7张图片
像这种将2的不同的倍数赋予每个常量,这种方法让你用OR位运算将几个常量合并在一个集合中,称作位域;
text.applyStyle(STYLE_BOLD | STYLE_ITALIC);
EnumSet
但是位域如果将组合的常量打印出来将非常难以识别,推荐使用EnumSet来代替位域, EnumSet在底层批处理运算上均使用的位运算,比得上位域效率,识别上比位域更简短,更清楚,也更加安全.

33.用EnumMap替代序数索引

与31原因类似,不要使用序数组建索引,而是要使用Map进行索引.数组不支持泛型,序数不能保证正确的int顺序.

34.用接口模拟可伸缩的枚举

虽然无法编写可扩展的枚举类型, 却可以通过编写接口以及实现该接口的基础枚举类型.
即:让枚举类实现不同的接口来对枚举的功能进行扩展

35.注解优先于命名模式

使用明明模式用于反射调用十分危险,你不能保证你的方法名称拼写正确无误,也无法保证是否会影响到其他不知情的方法,比如巧合的匹配上了你的命名规则.
使用注解模式能够很好地保障作用域,并且可以对注解设置多重限制,即使错误的使用如果不满足注解的限制的话将会失效.

36.坚持使用override注解

应该在你想覆盖超类声明的每一个方法上使用注解,这可以检查你重写是否成功,是否存在由于参数类型错误变成重载.

37.用标记接口定义类型

所谓的标记接口是不含有任何方法声明的接口,他是一个声明,声明某个类具有某种属性,比如Serializable接口.
标记接口对比注解的优势:
1.标记接口定义的类型是由被标记类的实例实现的,标记注解则没有定义这种类型.
2.标记接口能够被更精确的锁定,注解可能作用于多个地方,而标记接口只能被类实现.并且标记接口可以有继承结构.

六、方法

38.检查参数的有效性

  1. 校验,如果传入参数不满足某些类型返回特定值或者抛出错误.
  2. 断言,使用assert进行断言,如果不满足断言则直接抛出运行时异常.
  3. 注解,例如非空参数可以@NonNull Class

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

对于构造器的每个可变参数进行保护性拷贝是必要的.

public class Period {

    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end =  new Date(end.getTime());
    }

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

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

40.谨慎设计方法签名

  1. 谨慎的选择方法的名称. 遵循标准的命名习惯
  2. 不要过于追求提供便利的方法. 当一项操作被经常用到的时候, 才考虑为他提供快捷方式
  3. 避免过长的参数列表
    • 把方法分解成多个方法
    • 增加辅助类,来保存参数的分组
    • 从对象构建到方法调用都采用Builder模式, 最好定义一个对象来表示所有的参数
  4. 参数选择 :
    • 参数类型有限选择接口来输入,而非类, 避免拷贝操作
    • 对于boolean值参数, 可以使用两个元素的枚举类型

41.慎用重载

案例:
EFFECT JAVA笔记_第8张图片
这个结果将会是三个Collection,而不是我们期望的Set,List, Collection.
因为方法中classify(c)被重载了,而要调用哪个方法在编译期就已经确定了.
最佳修正方案 : 使用单个方法来替代多个重载的方法, 利用 Class instanceof ?进行区分.

42.慎用可变参数

可变参数 : 申明两个参数, 一个是指定类型的正常参数, 另一个是这种类型的varargs参数

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

要注意的是,使用可变参数时会伴随着一次数组的分配和初始化,确定自己的系统可以承受这种成本.假设大部分方法只调用三个以内的参数时可以采用将0-3个参数写成4个方法再写一个由3个参数和一个可变参数的方法,这样可以避免大量数组初始化,从而提升性能.

43.返回0长度的数组或者集合,而非null

使用Arrays or Collections , 避免返回null . 会导致NullPointException.

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

编码习惯.javadoc能利用你的注释生成API文档.让程序功能更加清晰.

七、通用程序设计

45.将局部变量的作用域最小化

在第一次声明的地方, 使局部变量的作用域最小化.在需要时再进行扩展.

46.for-each循环优先于传统的for循环

for-each循环在简洁性和预防Bug方面有着传统的for循环无法比拟的优势,并且没有性能损失,应该尽可能地多使用它.
但有3类情况无法使用:
1. 过滤,遍历集合并且删除选定的元素,就需要使用显式的迭代器,以便可以调用其remove方法;
2. 转换,如果需要遍历列表或数组,并取代其部分或全部元素,就需要知道下标以便设定;
3. 平行迭代,同时遍历多个集合,需要显式控制迭代器或者索引。

47.了解和使用类库

Random类库去随机数的调用, 以及java标准类库.例如Math相关的库.

48.如果需要精确答案, 避免使用float和double

浮点类型只能对能被1/(2^n)的小数相加能得出的小数进行精确表示.
BigDicemal欢迎您.

49.基本类型优先于装箱基本类型

基本类型 : 例如 : int , double ,boolean
装箱基本类型 : 例如 : Integer, Double , Boolean
区别 : 基本类型 比 装箱基本类型 更节省时间和空间
装箱基本类型 不能使用 == 比较
装箱基本类型 应用场景 :
作为集合中的元素 \ 键 \ 值

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

字符串不是基本数据类型,本质是一个char数组,且是final类型,拥有着一些缺点
* 不适合替代基本类型
* 不适合替代枚举类型
* 不适合替代聚集类型(使用字符串+分割符号进行分割不同数据是不明智的)
* 不适合代表能力表(代表某种授权/能力,某种空间分割等)

51.注意字符串连接的性能

使用StringBuilder替代String

52.通过接口引用对象

多态的实现,例如List list = new Vector();

53.接口优先于反射

反射强大且脆弱,在非必要时避免使用
反射的缺点 :
1. 缺乏编译时的类型检查
2. 反射代码冗长
3. 性能损失

54.谨慎的使用本地方法

本地方法本身是必要的,他为java语言提供了访问特定平台机制的能力.但是不要刻意使用本地方法来提高性能.其一是因为jvm越来越快,没有必要了.其次是本地方法是不安全的.再者书写本地方法再调用也难以调试,且调用本地方法是需要固定开销的.可能反而拖累性能.

55.谨慎的进行优化

首先明确一点:优化弊大于利
不要因为性能牺牲合理结构:编写好的程序而不是快的.
努力避免限制性能的设计
考虑API设计决策的性能后果:注意,api一旦确定之后很难改变

56.遵守普遍规定的命名惯例

包,类,方法,变量,常量,类型参数(泛型)

八、异常

57.只针对异常情况才使用异常

不要使用异常进行逻辑控制!!!

58.对可恢复的情况使用受检异常,对编程错误使用运行时异常

可抛出结构一共有三种
受检异常:期望调用者可以适当的恢复.也就是用于被外层捕捉的异常.
运行时异常:前提违规,用户没有遵守API约定.
错误:通常用于 资源不足,约束失败,或者其他使程序无法正常运行的情况.

注意:你所实现的所有的未受检的抛出结构都应该是运行时异常.

59.避免不必要的使用受检的异常

受检异常十分强大,他逼迫程序员不得不在外层处理这种可能出现的种种异常,或者让他们传播出去,无论哪种方式,都给程序员添加了不可忽视的负担.
过分的使用受检异常使得api使用起来非常不便,避免使用不必要的受检异常.
解决方法:封装返回结果 把结果码和结果一起返回由调用方判断.

60.优先使用标准异常

注重代码重用性,增加代码可读性,且能使你的代码易于学习和使用.
例如
1. illegalargumentexception:非null的参数值不正确
2. illegalstateexception:对于方法调用而言,对象的状态值不合适
3. nullpointerexception:在禁止使用null的情况下参数值为null
4. indexoutofboundsexception:数组下标越界
5. concurrentmodificationexception:在禁止并发修改的情况下检测到对象的并发修改
6. unsupportedoperationexception:对象不支持用户的请求方法

61.抛出与抽象相对应的异常

有时候抛出的异常与程序所执行的任务没有明显的关系,这种情况往往令人不知所措,如果仅使用异常传播机制会出现这种现象,所以应当对异常做适度的抽象及转译,在某一层捕获下层异常之后进行转译成其他类型异常抛出.

62.每个方法抛出的异常都要有文档

记录可能抛出的每个异常及抛出异常的条件.

63.在细节消息中包含能捕获的失败的信息

为异常增加消息细节方便后续定位查询.

64.努力使失败保持原子性

调用失败方法的对象应当保持被调用之前的状态.

65.不要忽略异常

不要忽略异常!异常是API设计者给调用者的警告!

九、并发

66.同步访问共享的可变数据

synchronized : 可以保证在同一时刻,只有一个线程可以执行某一个方法, 或者某一个代码块.
多线程访问:可能会导致同一对象状态发生变化.
同步 :可以使多线程看到由同一个锁保护的之前所有的修改效果.并且其他线程可以感知到修改
java语言保证读写一个变量是原子的,除非为long和double型.
你可能听说过,为了提高性能,在读或写原子数据的时候,应该避免使用同步,这个建议是非常危险而错误的.为了在线程间进行可靠的通信,也为了互斥访问,同步是必要的.原子数据保证了你在读取数据时不会看到任意的数值,但是不能保证一个线程写入的值对其他的线程可见.
读和写的操作方法都需要同步,如果任何一个没有做同步,同步就不会完全起作用.
需要注意的是long与double这两个变量java不保证其原子性,需要使用AtomicLong, AtomicDouble.
对方法加synchronized和对变量加volatile关键字,使用volatile关键字需要注意它只保证了同步性,但是并不保证多线程下的互斥性.也就是说对于一个long型的++操作必须考虑同步加锁机制,如加synchronized关键字或使用线程安全的AtomicLong类型,要么不共享可变的数据,要么共享不可变的数据,也就是把可变数据限制在单个线程中.
安全发布对象的方法有:将他保存在静态域中,作为类初始化的一部分;可以保存在volatile域中、final域中或者其他通过正常锁定访问的域中;或者把它放到并发的集合中。
不要使用 Thread.stop() , 要阻止一个线程妨碍另一个线程 ,正确做法如下 :

public class StopThread {

    public static boolean stopRequested;

    private static synchronized void requestStop() {
        stopRequested = true;
    }

    public static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public void stopThread() throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested()) {
                    i++;
                }
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

使用volatile优化之后可以写成(volatile保证其每次读取都是从)

public class StopThread {

    public static volatile boolean stopRequested;

    public void stopThread() throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                }
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

67.避免过度的同步

自己编写线程互动容易出现死锁,脏数据等情况.
共享数据类型,需要使用线程安全的数据结构 , 例如 : CopyOnWriteArrayList
或者直接使用Collections.synchronizedList()
Map可以使用CurrentHashMap

68.executor和task优先于线程

尽量避免自己编写工作队列, 尽量不要直接使用线程, 使用java提供的executor线程池

69.并发工具优先于wait和notify

  • Executor Framework:任务执行工具框架
  • 并发集合:CurrentHashMap等
  • 同步器 : CountDownLatch 和 Semaphore
    注意
  • 调用wait方法,应当用循环之内调用 , 永远不要在循环外调用
  • 优先使用notifyAll , 当只有一个线程被唤醒才使用notify

70.线程安全文档化

线程安全是分级别的 并不是有synchronized就是安全的 没有就不是,也会存在一些中间态.
不可变的:没有synchronized也安全的,比如String
无条件线程安全的:同步做的足够的
有条件的线程安全:依赖外部的同步 ,比如依赖外部的迭代器.
非线程安全的:类实例是可变的,为了并发使用它们必须由客户端进行同步.
线程对立的:这个类不能安全的被多个线程并发使用,即使客户端进行同步.
所以要在文档中标明类的安全级别及使用注意事项

71.慎用延迟初始化

延迟初始化在单例的饿汉模式中有所使用,在第一次调用时再进行初始化.
但是在多线程中就会出现严重的问题,可能会存在多个线程同时对其进行初始化工作,这就会导致可能初始化多份乃至导致严重的bug
延迟初始化属于 ”除非必要,否则不要这么做“ 的技能.需要使用的时候也要关注线程问题.

72.不要依赖于线程调度器

线程调度器是依赖一定的策略,来决定哪些线程会运行.看起来是很有用的工具,但是良好的程序不应当依赖于这种策略细节.
任何依赖于线程调度器来达到正确性或者性能要求的程序,都很有可能是不可移植的.

73.避免使用线程组

ThreadGroup 已经为过时的API, 没必要继续使用

十、序列化

74.谨慎的实现Serializable接口

将一个对象编码成一个字节流 , 称为对象序列化.

Serializable接口 实现的代价:
* 一旦一个类被发布, 就大大降低了”改变这个类的实现”的灵活性 ; 序列化类的唯一标识符 是 序列版本 UID .
* 它增加了出现bug和安全漏洞的可能性
* 随着类发行新的版本 , 相关的测试负担也增加了
要点 :
* 为了继承而设计的类应该尽可能少地去实现Serializable.
* 对于为继承而设计的不可序列化的类 , 你应该考虑提供一种无参构造器.
* 内部类不应该实现Serializable.

75.考虑使用自定义的序列化形式

由于实现序列化接口的api很难被变更,难以被舍弃.所以可能需要我们提供自定义的序列化形式.
注意:序列化类时给其属性增加@serial注解告诉javdoc
如果一个对象的物理表示法等同于其逻辑内容,使用默认的序列化形式是合适的.(比如抽象出来的猫狗DTO,不一致是比如内部形成链表的类)
如果出现不合适的,依然使用默认序列化形式的话,可能会出现以下问题
* 使这个类的导出API永远地束缚在该类的内部表示法上
* 它会消耗过多的空间
* 它会消耗过多的时间
* 它会引起栈溢出
* 如果在读取整个对象状态的任何其他方法上强制任何同步, 则也必须在对象序列化上强制这种同步

在必要时可以考虑重写其readObject和writeObject方法
从技术角度说,不调用默认的readObject和writeObject是允许的,但不推荐.
注意2
无论哪一种形式的序列化 请为每个可序列化的类声明一个显式的序列版本UID
这样可以避免序列版本UID成为潜在的不兼容根源.

76.保护性编写readObject方法

readObject相当于一个隐藏的一个类的构造器.构造器是需要保证他的数据的有效性.这样也能防止外部出现序列化攻击.

77.对于实例控制,枚举类型优先于readResolve

只要一个类实现了序列化,那么他将不会是单例了,因为可以通过序列化方法实现多例,于是存在了readResolve ,readResolve会在反序列化完成时调用,你能使用系统中的单例去替换他,从而保证系统定义的单例的性质.
当然 枚举单例将更加容易被使用.

78.考虑使用序列化代理代替序列化实例

序列化会增加出错和出现安全性问题的可能性,有一种方法可以极大的减少这些风险,就是序列化代理模式.
做法
* 为可序列化的类设计一个私有的静态嵌套类,精准的表示外围实例的逻辑状态,称之为序列化代理.
* 序列化代理私有静态类有一个单独的构造器,其参数就是那个外围类,这个构造器只从它的参数中复制数据.
* 将writeReplace方法提供到外围类中,这个方法提供了一个序列化代理类.

private Object writeReplace(){
    return new SerializationProxy(this);
}

writeReplace放啊在序列化之前,将外围的实例转化成了它的序列化代理.
这样一来,序列化系统永远不会产生外围类的序列化实例,为了防止攻击者伪造,覆盖readObject直接返回异常.然后再SerializationProxy中华提供一个readResolve方法,它返回一个逻辑上相当于外围类的实例.这个方法使得系统在反序列化时将序列化代理转变回外围类的实例.

你可能感兴趣的:(java基本功)