effective java

effective java(一):创建和销毁对象

优先考虑静态工厂方法创建对象

它是一个返回类的实例的静态方法:

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.False;
}

使用静态工厂方法建立对象和使用构造方法的对比如下:

静态工厂方法构造方法
命名可以用方法名表达更多信息方法名固定
性能可以把对象缓存好然后返回出来,类似享元模式每次调用必创建新对象
返回类型可以适用多态,具体返回什么类不确定返回类型固定
返回类型2返回的类型可以根据参数不通变化返回类型固定
返回类型3返回的实际类型可以在编写阶段不存在返回的类型在编写阶段必须存在
??类如果不含公有或protected的构造器就不能被子类化
易用性难以在API文档中发现它发现很简单因为方法名固定

静态方法的惯用名称:from(转换)、of(聚合)、valueOf、getInstance、create/newInstance(每次都要创建一个新对象)、getType1/newType1(返回一个实现类为type1的对象)

多参数时的选择:构造器模式

当我们需要使用 很多参数构造一个对象时,通常的选择有以下两种:

1、重叠构造器模式:在内部封装多个构造器互相调用,如提供一个简单的构造器只有一个必要的参数,最后调用其中最复杂的一个构造器,问题在于复杂的构造器难以使用,很容易搞错参数位置,且难以阅读。

2、JavaBeans模式:将对象new出来之后用setter来设置它的属性,这样使用即简单,可读性也好,但是问题在于除了对象使用者以外,其他调用者不知道何时这个对象才算构造完全,才能够投入使用,很容易去使用一个处于不一致状态的对象。

合适的做法是使用构造器模式:

NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

当处理更多的参数时,Builder模式是更好的选择,大致意思是入参构造参数只有一个builder对象,根据这个对象来生成新对象。

合适的单例设计

设计一个单例有以下几种方式:

1、公有静态成员变量:

public static final Class1 INSTANCE = new Class1();

2、静态工厂方法:

private static final Class1 INSTANCE = new Class1();
public Class1 getInstance() {return INSTANCE};

3、包含一个元素的枚举类型

使用单例模式的时候需要注意,有可能其他客户端会利用反射调用私有的构造方法,此时可以在私有构造里抛出异常禁止构造。

如果想要把单例类变成可序列化的,实现Serializable,且一定要重新readResolve方法,把实例域都标记为transient的,否则每次序列化时会重复创建一个新的实例。

三种方法的对比如下:

公有静态成员变量静态工厂方法包含一个元素的枚举类型
易维护要修改成非单例要改API容易修改为非单例容易修改为非单例
扩展性固定可以写一个带泛型的工厂固定
扩展性不能提供可以通过方法引用来提供单例(Supplier)不能提供
安全和序列化需要人为提供措施需要人为提供措施免疫反射攻击、天然提供序列化机制(枚举只序列化name)
扩展性可以继承可以继承不能继承自某类

优先依赖注入来引入资源

静态工具类和单例类因为依赖某些资源,将这些资源设计成一个final成员变量进行默认初始化,这样的做法是不合适的,原因是这些资源很难固定不变,一旦遇到资源变化的情况这种设计就难以变更了,正确的做法是将这些资源作为构造方法的参数传入,这就是依赖注入模式。

使用依赖注入模式可以实现同一个底层资源传入不同的业务对象,它们具有不可变性。

依赖注入的一种变体是在构造方法中传入一个资源工厂,在构造方法中利用资源工厂来构造成员变量。

当需要注入的变量过多时,会影响项目的灵活性和可测试性,解决办法是用依赖注入框架,如spring

避免重复创建对象

一般来说最好能重用单个对象而不是每次都创建新的对象,需要注意的细节点:

1、有静态工厂方法的情况下不要使用构造器,如:

Boolean.valueOf(String)优于new Boolean(String)

2、缓存一些昂贵的对象,如正则的Pattern、数据库的连接对象

3、如果想安全的使用一个不可变的对象,可以考虑使用视图,或者适配器,创建多个视图都是源于同一个源对象

4、避免无意识的自动装箱和拆箱,这会在一定程度上影响性能

避免重复创建对象是重要的,但是不要为了避免重复创建对象牺牲可读性造成过度设计,现代的JVM对于小对象的创建和回收动作都是很廉价的。

不要轻易的维护缓存对象池,这会增加代码的复杂度,除非创建它的代价真的很昂贵

消除过期的对象引用

对于java这种垃圾自动回收的语言,容易在不经意间触发内存泄露,如用数组实现一个栈,当栈增长时指针前移,栈弹出后指针后移,指针后移之后程序使用完毕栈弹出的对象,该对象也不会被垃圾回收器回收,原因是栈中还在引用它,解决办法是当弹栈之后对于以后用不到的位置赋值为null。

类似这种类自己管理内存的情况,应该警惕内存泄露问题。

弱引用的意思是如果没有其他额外引用,则该对象在下一轮就会被垃圾回收期回收,比如使用WeakHashMap

避免使用终结方法finalizer和cleaner

使用终结方法的问题:

1、它们不保证对象会被及时清除,不保证清除优先级、不保证最后一定会被清除

2、终结方法会阻止正常的垃圾回收,导致有严重的性能损失

3、有安全问题,可能会引发终结方法攻击

使用它们的场景:

1、使用资源时忘记调用close方法时,可以用它们当做安全网。虽然不保证及时执行,但总比没有好

2、本地对等体(非java的对象)的回收

try-with-resources 优先于 try-finally

使用try finally的主要劣势在于:在finally中调用close抛出异常时,会抹除第一个异常,给定位带来很大困难,但是使用try-with-resources就不会,在try块中初始化的部分必须实现AutoCloseable接口:

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
}

此时即使readLine和close方法(不可见的)都抛出了异常,两个都会打印在堆栈轨迹中,后一个异常会标记为禁止状态。而且这种方式后面也可以捕捉异常,捕捉到异常后处理错误后的逻辑:

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
} catch (IOException) {
    return defaultVal;
}

effective java(二):Object类的方法

覆盖equals方法

当一个类需要定义一种逻辑相等的概念时,就需要重写equals方法

重写它的时候要遵守以下几个约定:

1、自反性:x.equals(x)必须返回true

2、对称性:对非空的x和y,当y.equals(x)返回true时,x.equals(y)也要返回true

3、传递性:x和y相等,y和z相等,x和z也要相等

4、一致性:属性不改变的情况下多次调用的结果一致

5、非null的值和null比较,结果是false

重写equals的常规步骤:

1、使用==检查对象是否是自己

2、使用instanceof来检查入参是否是正确的类型

3、把入参进行类型转换

4、每个逻辑相等的部分都要分别比较

注意当float和double比较时要用Float.compare方法,double比较时要用Double.compare方法

数组比较可以用Arrays.equals方法,包含null值的比较可以用Objects.equals方法

重写equals的时候一定要注意入参是Object,否则就会重写错方法

重写equals时一定要重写hashCode方法

重写hashCode遵循的原则是:如果两个对象调用equals是相等的,执行hashCode也必须要相等;如果两个对象调用equals是不相等的,那么执行hashCode不相等的几率最高,散列表性能越好

当自定义一个hashCode方法时,如果不太注重性能,可以直接使用Objects类的散列函数:

return Objects.hash(lineNum, prefix, areaCode);

如果要自己写hashCode方法,首先要确定对象中的关键域,这些关键域是决定对象equals的返回结果的域,注意要排除一些衍生域(可以由其他关键域计算出来的那些),然后对每一个关键域都确定它的散列码:

1、如果是基本类型就用对应类型的hashCode方法,如Integer.hashCode(val)

2、如果是对象就调用对象的hashCode方法

3、如果是数组则对每一个元素都当做单独的域来处理,如果数组中没有重要的元素可以用一个非0变量来代替

对于计算出的每个散列码用下列迭代式来计算:(用31是因为它是奇数,乘法溢出不会丢失信息;且31 * i = (i <<5) - i,优化器会做这样的优化,至于使用素数的好处并不明显)

int result = 31 * result + c;

例如下列的hashCode方法:

int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;

当计算一个对象的hashCode代价很昂贵时,可以把散列码当做成员变量缓存在内部,用延迟初始化的方式来加载:

private int hashCode;

@Override
public int hashCode() {
    int result = hashCode;
    if (result == 0) {
        ...
        hashCode = result;
    }
    return result;
}

谨慎覆盖clone方法

实现Cloneable接口表明对象允许克隆,但是并不强制要求重新clone方法,实现该接口后,在类内部调用clone方法就会返回该对象的逐域拷贝(Object的clone方法是受保护的),如果没有实现该接口,调用clone就会抛出CloneNotSupportException

一般来说,如果每个域包含一个基本类型的值,或者包含的对象都是不可变的,此时类重写clone方法只需要简单的执行父类的clone方法即可:

@Override
public PhoneNumber clone() {
    return (PhoneNumber) super.clone();
}

clone方法在执行过程中,不能在方法内部调用其他可以被覆盖的方法。

当包含对象域的时候,在写clone方法时就应该先调用父类的clone方法,然后再去深拷贝那些可变的对象。

对于数组来说最好的办法是使用clone方法,除此之外,其他更复杂的对象的克隆更推荐采用拷贝构造器(入参和返回值类型相同的构造器)或者拷贝工厂(入参和返回值类型相同的静态方法)

Comparable接口

当一个数据类内部有明显的内在排序关系时,就可以考虑实现该接口

在判断时注意不要直接使用两个数相减,相减是有可能会出现溢出的,此时应该用一个静态方法compare:

return Integer.compare(o1, o2);

effective java(三):类和接口

使类和成员的可访问性最小化

关于可访问性,几个禁止的点:

1、公有类的实例域不能是公有的,静态域可以有公有的常量

2、公有的常量如果是数组或者对象,那就有被外部类修改的风险

一种方法是将公有变量调整为公有的不可变对象:

private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

还有一种方法是公有方法返回其拷贝:

private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}

公有类不应该直接暴露数据域

有些只表示数据的类没有get和set方法:

class Point {
    public double x;
    public double y;
}

这种类的数据域是直接可以访问的,对于包私有的或者私有的嵌套类,这样定义是可以的,但是对于公有类来说,不应该直接暴露数据域,如果暴露数据域,意味着直接访问其成员变量的代码将散步在各处,将来如果想改变其内部的表示法,将会连锁修改所有引用它的地方,给维护带来很大困难

不可变类:可变性最小化

不可变类是指实例中的所有信息只在创建该实例时就提供,并且在对象的整个生命周期内固定不变,如java中的String、BigInteger、基本类型的包装类等

不可变类遵循的5个规则:

1、不会提供任何修改对象状态的方法

2、保证类不会被拓展(保证子类的改动不会干扰到自己)

3、所有域都是final(当使用延迟初始化时,可以妥协一点,提供一个非final的域,但是它必须是外部不可见的)

4、所有域都是private的(虽然域是public的也可以是不可变类的对象,但是这样会使以后的版本难以改变其表示)

5、如果类中的域有可变对象,不要让外界能访问到它,也不要用外界传入的对象来初始化它

例如自定义一个不可变对象Complex(复数)类,它类似于BigInteger,它的pius方法可以完成复数的相加,但是返回值不会返回它自己,而是返回一个新的new Complex对象。

不可变类应该鼓励客户端尽可能重用现有的实例,如给Complex提供这样的常量(也可以用静态方法返回):

public static final Complex ZERO = new Complex(0,0);

即使多个线程公用上述常量,也不会出现线程安全问题,因为不可变对象本质就是线程安全的

不可变类的缺点是:因为不同的值都需要一个单独的对象,它可能有性能问题。如改变BigInteger的某个位,需要重建整个BigInteger,幸好不可变类可以在内部更加灵活,还可以提供包级配套类(像BigInteger解决该问题一样)来完成这些操作。如果在设计不可变类的时候能预料到这些复杂操作,则可以像上面那样处理,若不能,则提供一个公共的可变配套类,如StringBuilder。

为了保证类不会被拓展,类要设计为final的,还有一种方法可以保证类不会被拓展,那就是将构造方法私有或包私有,提供公有的静态工厂方法,这样外部的类要么不可能继承它(因为构造方法私有),要么包外部的类不可能继承它(因为构造方法包私有),允许多个包私有的实现类,对于包外部的调用方来说其实是final的

最后还有几条原则要说明:

1、除非有理由,否则类应该是不可变的

2、除非有理由,否则域应该是private final的

复合优先于继承

继承是代码重用的有力手段,对于那些在包内部使用继承是很安全的,子类和超类的实现都在同一个程序员的控制之下,但是对于那些跨越包边界的继承则非常危险。继承是打破了封装的危险操作,如果父类改变了,则可能导致自定义的子类出现莫名其妙的错误,而且在自定义子类时,程序员很难穷尽父类中的实现细节,这可能导致在使用新的类时必须穷尽另一个类的细节,违反了封装的原则。

这样的情况下,复合就可以解决前面提到的问题,也就是在新的类中增加一个私有域,它引用现有类的一个实例,新类的每个方法都可以调用域实例中对应的方法,并返回结果,这就是转发,新类中的方法被称为转发方法。这样得到的新类非常稳固,它不依赖于现有类的实现细节。这样的类也被称为包装类,也就是装饰者模式。

包装类的缺点是写转发方法比较繁琐,但是可以用转发类来简化,还有一个缺点在于SELF问题。

谨慎使用继承,对继承做好文档说明

父类的文档必须精确地描述覆盖每个方法带来的影响,对于每个public和protected的方法和构造器都必须指明它调用了哪些可覆盖(public或protected)的方法,是以什么顺序调用的,每个调用结果是怎么影响后续处理的。

如果方法调用了可覆盖的方法,就应该在方法注释上加上@implSpec,也就是实现要求。例如remove方法的实现要求就应该提醒:由集合的iterator方法返回的迭代器没有实现remove方法,该实现会抛出异常。

好的API文档应该说明它做了什么工作,而不是描述它如何做到的,而上述的说明破坏了这个准则,这就是继承破坏了封装性带来的后果。

继承过程中需要注意的点:

构造器不能调用可被覆盖的方法。因为超类的构造器在子类的构造器前运行,子类覆盖的方法可能在子类构造器调用前被调用,这可能会使程序失败。同样的道理,clone方法(子类clone前被调用)和readObject方法(反序列化前被执行)也是一样。

做好继承是很难的,父类发布后,文档建成后就必须要一直遵守。所以非必要时要禁止其他类继承本类,可以用之前的final和私有化构造器来实现。

接口优于抽象类

接口是允许多个实现的最佳途径,是定义mixin(混合类型)的理想选择,mixin的意思就是类可以实现某个类型,表面它提供某种能力,如Comparable代表提供了对比的能力,它允许任选的功能,可以混合到类型的主要功能中。

接口使用起来比抽象类更加简单,它不会破坏类本身的体系,可以很容易的实现和扩展。如果用抽象类来代替接口的功能,将使得所有的子类都被迫实现这些功能。

包装类结合接口可以很安全的增强类的功能。

接口配合骨架实现类可以完成强大的功能,骨架实现类就是一个提供基本功能实现的抽象类。无论是骨架实现类还是接口的默认方法,都需要提供文档进一步说明。

谨慎添加默认方法

接口中的默认方法最好是在一开始就设计好的,如果在开发的过程中想要给接口添加一个默认方法,就意味着实现接口的类都可以调用它,可能会产生一些错误,因为不是所有类都能一定适应这个新方法。如子类中对元素状态进行了定义,但是接口中的默认方法对此就一无所知。所以要谨慎设计接口,尽量不要给现有的接口添加默认方法。

使用常量:不要使用常量接口

不包含任何方法,只包含静态final域的接口被称为常量接口,实现这些接口可能会让其他类使用这些不必要的常量,未来某个类不需要这些常量的时候,也不能将它取消掉。

使用常量可以定义一个final类,也可以把常用的常量添加到某个类中,如Integer中的MIN_VALUE,还可以用枚举。

使用常量时可以使用静态导入,这样就可以避免类名修饰常量名。

4种内部类

4种内部类:静态成员类、非静态成员类、匿名类和局部类

静态成员类相当于一个单独的类,它只是被声明在某个类的内部,它的常见用法是作为公有的辅助类,只有和外部类一起使用才有意义,如Map.Entry、Calculator.Operation

非静态成员类和静态成员类的区别:

非静态成员类内部隐含外部类的实例,相当于关联了一个外部类this的引用,当非静态成员类的实例创建出来的时候,它和外围实例的关联关系也随之被创建,这种关联关系会消耗非静态成员类的空间,增加构造的时间开销。这个引用很隐蔽,可能导致外围类不会被回收,导致内存泄漏。

如果成员类不要求访问外围实例,就要始终用静态成员类。

匿名类是一个只有声明的时候实例化的类,出现在非静态的环境中,它可以访问外围实例。无法声明一个匿名类实现多个接口或者拓展一个类。除了从超类型中继承之外,不能调用任何成员。它应该尽可能简短,否则会影响可读性。

局部类使用较少。属于方法内部的类,需要只在一个地方创建实例时就使用匿名类,否则就用局部类。