这本书的目的是帮助编写清晰正确,可用的,健壮性灵活性高和可维护的代码,而非针对高性能。主要从对象,类,接口,泛型,枚举,流,并发和序列化等方面介绍。
from,of,valueOf,instance,getIntance,create,newInstance
等方法名来命名。new String("bikini")
(应该直接使用"bikini"
),计算时包装类型和基本类型的来回转换,大对象的重复创建等都是资源的浪费。应尽量使用基本类型的对象参与运算,复用不可变(或在使用时不会改变的)大对象,常用对象使用池化技术等技巧来避免对象的创建。pop
时应释放弹出的数组元素的引用,否则会导致内存泄漏。public class Stack {
private Object[] elements;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private int size;
public Stack{
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
Object result = elements[--size];
//释放引用
elements[size] = null;
return result;
}
.......
}
因为Java是有垃圾回收机制的,通常不需要手动将对象置为null。但是如果对象内部管理自己的内存分配,则需要手动释放元素的引用,(如上面的例子,只有数组将元素置空了,元素对应的对象才能被回收)否则会导致内存泄漏。此外,缓存,监听器和其他回调函数也可能导致内存泄漏,可借助性能分析工具来分析这类问题。
AutoClosable
接口的对象回收速度快。Finalizer存在finalizer攻击,可通过增加一个空实现的finalize方法解决。在Java中,可使用cleaner来实现native方法分配的堆外内存的回收(DirectByteBuffer)。总的来说,还是要慎用。关于Java的引用,可参考:
Java Reference详解try-with-resources
的书写更加简洁优雅,同时可解决try-finally
资源关闭失败的情况。使用try-with-resources
的资源必须实现AutoClosable
接口。Object类是所有类的父类,它定义了一些非final的方法,如equals
,hashCode
,toString
等,这些方法有它们自己的规定。此外,Comparable.compareTo
方法虽然不是Object的方法,但是它也经常出现在任何同类对象中用来比较两个对象,以下也会讨论。
java.util.regex.Pattern
就没必要。
3.父类重写的equals方法已经合适使用了,就不需要子类再重写。如AbstractMap
。
4.如果类是private或者package-private的访问权限,则没有必要暴露equals方法,也就不用重写。
所以value classes(也就是需要比较类对象内容的类)适合重写equals方法,重写equals方法应该满足以下特性:
1.自反性,即x.equals(x)==true
。
2.对称性,即如果x.equals(y)==y.equals(x)
。
3.传递性,即如果x.equals(y)==y.equals(z)==true
,则x.equals(z)==true
。
4.一致性,即如果x.equals(y)==true
,x,y值不改变的话,x.equals(y)==true
一直成立。
5.如果x是不为null的引用对象,则x.equals(null)==false
。
一般重写equals方法时需要先对比引用,再对比类型,然后挨个对比成员变量的值是否相等。如AbstractList.equals
://AbstractList.equals
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;
ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return !(e1.hasNext() || e2.hasNext());
}
AbstractList.hashCode
:public int hashCode() {
int hashCode = 1;
for (E e : this)
//使用31这个质数尽可能保证了运算值的唯一性
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}
println,printf,log输出
以及debug的时候都会默认调用对象的toString方法,所以重写toString可使代码更易懂,同时toString方法返回信息应该包含对象中所有有意义的信息,并且尽量格式化,方便阅读。Cloneable
接口才能重写clone方法,clone方法的规定需要保证以下成立:x.clone() != x;
x.clone().getClass() == x.getClass();
//非必须
x.clone().equals(x);
使用clone方法需要注意: 1.不可变的类不应该提供clone方法。 2.clone方法的功能就像是构造方法,应该确保不会对原来的对象造成影响。 3.对于final成员变量,将其作为可变对象进行clone是不太合适的,可能需要去掉final修饰。 4.对于复杂对象的clone,应该先调用super.clone,然后在组装变量之间的关联关系,如HashTable中的Entry链表引用. 5.对象克隆的更好方法其实是提供copy 构造方法或者 copy工厂方法,如:
//copy constructor
public static Yum(Yum yum){
.....
}
//copy factory
public static Yum newInstance(Yum yum){
.....
}
因为他们不依赖危险的克隆机制,也没有文档克隆要求,和final fields的使用不冲突,同时不需要抛出检查异常和强转。
对于数组,是适合使用clone的,因为它的运行时类型为Object[],不需要进行强转。如ArrayList.copy
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
TreeSet,TreeMap
。此外,在compareTo
实现中,不要使用<,>
来做比较,而应该继续调用比较对象(基础对象就用包装类的compare方法)的compare或compareTo方法。private
,package-private
,protected
,public
四种。使用原则是尽量减少成员的访问权限。如public类的实例变量基本不能为public的,也不应该定义public static final修饰的数组和集合变量,或对外返回这样的变量,都是线程不安全的。静态内部类
,成员内部类
,匿名内部类
和局部内部类
。
1.静态内部类相当于外部类的一个静态成员,它的创建不依赖于外部类,可访问外部类的所有静态成员。可作为一个公有的帮助类
,如外部类的枚举类(Calculator.Operation.PLUS)。私有静态内部类可表示外部类的组成。如HashMap中的Node
类,因为它并不持有外部类的引用,所以用静态实现更节省内存空间。
2.成员内部类依赖于外部类的创建,可以调用外部类的任何成员。通常作为外部类的一个扩展类使用,如集合类中的Iterator
实现类。
3.匿名内部类没有名字,是一个类的引用。之前匿名内部类可用来作为接口或抽象类的实现传入方法,但自从Java8引入Lambda表达式,Lambda表达式更适合这种场景。此外,匿名内部类可作为静态工厂方法的实例返回。
4.局部内部类使用频率最低,可定义在方法和代码块中。可参考:
详解内部类 Java5之后,泛型成为Java语言的一部分。没有泛型前,操作集合中的元素必须进行强转,而类型转换异常只能在运行期才能发现。泛型可以告诉编译器集合中每个元素是什么类型的,从而可以在编译期就发现了类型转换的错误。泛型使得程序更加安全,简洁明了。
Set
定义集合,这又回到了没有泛型的时代了。而应该使用Set<Object>
,Set<?>
,Set<T>
等来定义集合。关于泛型类型的获取,可参考:
java Type 详解
Java泛型的学习和使用 @SuppressWarnings
注解抑制警告,同时说明原因,并尽量缩小该注解的作用范围。Stack<E>
类。
2.使用E[]来做成员变量,只有创建泛型数组的时候强转为E[],其他添加和获取操作不用进行强转。即 E[] elements = (E[])new Object[16];
推荐使用第二种方式,因为它更加易读,简洁,只在创建数组时进行了一次强转。 此外,可以使用<E extends T>可使泛型元素获得原来T的功能。 总之,使用泛型类型的参数可尽量避免运行时的类型强转。
PECS原则
可获得最大的灵活性。PECS是指当参数是作为生产者时,使用<? extends E>
,当参数作为消费者时,使用<? super E>
。如向Stack中添加元素push
(向stack中生产元素),使用<? extends E>;从Stack中获取元素pop
(从stack中消费元素),使用<? super E>。即生产上边界类型消费下边界类型的原则。//保证放进去的元素都有E的特性
public void pushAll(Iterable<? extends E> src){
for (E e : src) {
push(e);
}
}
//保证取出来的元素至多有E的特性
public void popAll(Iterable<? super E> dst){
while (! isEmpty()){
dst.add(pop());
}
}
Arrays.asList(T... a)
,EnumSet.of(E first, E... rest)
中的实现。如果类型安全,使用@SafeVarags
注解这类方法。Class object
作为key,来存储更多参数类型的容器。如Map<Class<?>,Object> map
。public static final
定义的(枚举对象可以是枚举类的子类实现)。同时枚举类构造方法是私有的,外界没有办法创建枚举实例,Enum类序列化相关方法会抛出异常,也就无法通过序列化创建出新的枚举对象。所以枚举对象是天然的不可变单例对象。
枚举类的好处是易读,安全和可提供更多的功能。如何使用枚举类:
1.枚举类应该是public
的类,如果它和使用者紧密相关,那么应该是使用者的成员类。
2.如果每个枚举对象都有各自特有的行为,最好是定义一个抽象方法,让枚举对象各自实现这个抽象方法。如:public enum Operation {
PLUS{
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS{
@Override
public double apply(double x, double y) {
return x-y;
}
};
//由子类实现
public abstract double apply(double x,double y);
}
3.当由某个特征获取枚举对象时,可返回Optional<T>对象,由客户端判断是否能获取到枚举对象。 4.当编译期可确定常量的集合内容时,都可以使用枚举类来实现。
public enum Ensemble {
SOLO(1),
DUET(2);
private int numberOfMusicians;
Ensemble(int size){
this.numberOfMusicians = size;
}
//正确使用
public int numberOfMusicians1(){
return numberOfMusicians;
}
//错误使用
public int numberOfMusicians(){
return ordinal() + 1;
}
}
Enum类中ordinal
的设计是用来比较枚举对象的大小和EnumSet,EnumMap中使用的。
EnumMap<...,EnumMap<...>>
。通用特性
,使用接口声明会更加明确,统一。如Serializable
接口统一表示Java的原生序列化标志。(argument) -> (body)
,而方法引用指的是目标引用::方法
的形式。如下://Lambda 表达式形式
Comparator c = (p1, p2) -> p1.getAge().compareTo(p2.getAge());
//方法引用
Comparator c = Comparator.comparing(Person::getAge);
使用方法引用大多时候会显得更加简洁易懂。但是当Lambda 表达式形式比方法引用更简洁的时候,使用前者。
java.util.function
包中提供了表示生产者(Supplier<T>),消费者(Consumer<T>),预测(Predicate<T>)等众多函数接口,优先使用这些函数接口。另外,含有基本类型参数的函数接口不要传入包装类型参数。自定义的函数接口使用@FunctionalInterface
注解。1.代码块可读写作用域内访问的任何局部变量。Lambda只能读用final修饰
或者effectively final
(A variable or parameter whose value is never changed after it is initialized is effectively final.)的变量,并且不能修改任何局部变量。
2.代码块可使用return
,break
,continue
或者抛异常
;Lambda不能做这些事情。
如果需要上述这些代码块操作的,则不适合使用streams。streams适合做的事情为:
1.统一的流中元素转换
2.按照条件过滤一些元素
3.用简单的操作(如求最小值)处理元素
4.把元素放入集合容器中,如分组
5.查询满足某些条件的元素集合
其实也就是map-reduce
操作适合流式处理。
forEach
做流展示的运算。在做流的collect
操作时,可通过Collectors
实现一系列的集合操作,如toList
,toMap
,groupingBy
等。parallel
方法实现并行计算,但是对于limit
,collect
方法并不适合使用并行计算,因为这些操作可能导致错误的结果或灾难。而对于reduce
或者有预操作的方法,比如min
,max
,count
,anyMatch
这些是适合使用并行计算的。同时集合类ArrayList
,HashMap
,ConcurrentHashMap
,arrays
是适合做并行计算,因为他们的数据结构主要由数组构成,可简单切分成小块数据并行运算,同时局部的数组数据引用通常在内存中是连续的。此外,只有当数据量很大时,使用cpu核数相同的线程才可能达到接近线性的速度,如机器学习和大数据处理适合使用流的并行计算。
可参考:
什么是函数式编程思维?
JavaLambdaInternals
lambda表达式原理篇(译)
浅析 Java Stream 实现原理
java8 函数式编程Stream 概念深入理解 Stream 运行原理 Stream设计思路 clone方法
,后者可能会导致安全问题。printf
设计的,对打印和反射都很有帮助。
3.每次调用可变参数都会造成数组的分配和初始化。如果是为了性能考虑,可重载多参数方法,直到超过3个参数再使用可变参数,EnumSet
的静态创建方法就是使用这种策略。 public static <E extends Enum<E>> EnumSet<E> of(E e) {
....
}
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2) {
....
}
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3) {
....
}
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4) {
....
}
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4,E e5)
{
....
}
public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {
....
}
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
//返回空数组
public Object[] getObject(){
...
return Collections.emptyList();
}
//返回空集合
public List<Object> getObject(){
...
return list.toArray(EMPTY_OBJECT_ARRAY);
}
empty
,get
,orElse
,orElseThrow
等。但是:
1.不要再返回Optional的方法中返回null。
2.Optional<T>中T 不应该为容器类型:比如collections,maps,streams,arrays和optionals,不应该在包着一层Optional。
3.Optional<T>中T 不应该为包装类型,如Long等。
4.几乎也不把Optional<T> 用在集合和数组的key,value或者element上。
5.Optional<T>也不适合当成员变量。
6.严格考虑性能的方法,还是返回null或者抛异常吧。 public static void main(String[] args) {
Long sum = 0L;
for (long i = 0;i<Integer.MAX_VALUE;i++){
//不停的拆箱封箱
sum += i;
}
System.out.println(sum);
}
2.原始类型有默认值,而包装类型初始值为null,进行运算时可能会报NullPointerException。 3.原始类型可使用==比较,包装类型的==总是false。
StringBuilder
代替,它的复杂度为线性的。或者使用字符数组,或者只调用一次连接字符串。 try {
... //调用其他低级方法
}catch (LowerLevelException e){
throw new HigherLevelException(...);
}
lock splitting,lock stripping,nonblocking(无锁)技术
(我的理解lock splitting/lock stripping,就是类似ConcurrentHashMap1.7中的分段锁,ConcurrentHashMap1.8中的多线程预分配帮助扩容以及disruptor中的队列预分配一样的技术,《Java Concurrency in Practice 》好像有讨论,欢迎小伙伴指出我这里是不是有问题。。) synchronized (obj){
while (<condition does not hold>){
obj.wait();//不满足条件时释放锁,唤醒时重新再获得锁
}
... //满足条件时的操作
}
这样子保证了只有条件满足时,才能执行操作。否则可能由于notifyAll/notify唤醒了不该唤醒的线程等导致条件不满足就被唤醒了,而执行错误操作。
不可变
(String/Long/BigInteger),无条件线程安全
(AtomicLong/ConcurrentHashMap),有条件线程安全
(Collections.synchronized包装类需要额外保证自身迭代器的同步,否则可能fail-fast),线程不安全
(ArrayList/HashMap),违反线程安全的
(修改静态变量时没有同步)。
有条件线程安全需要写明什么时候需要额外同步,且应该获取什么锁进行同步。
无条件线程安全的类可以在同步方法上使用不可变私有对象锁代替类锁,可保护子类或客户端的同步方法。同时增加自身后续实现的灵活性。首先介绍下Java原生序列化的原理:
Java原生序列化使用ObjectInputStream.readObject
和ObjectOutputStream.writeObject
来序列化和反序列化对象。对象必须实现Serializable
接口,同时对象也可以自实现序列化方法readObject
和反序列化方法writeObject
定义对象成员变量的序列化和反序列化方式。此外对象还可实现writeReplace
,readResolve
,serialVersionUID
。
writeReplace
主要用来定义对象具体序列化的内容,可对原来按照readObject
方法序列化的内容进行修改替换。
readResolve
主要用来定义对象反序列化时的内容,可对原来按照writeObject
方法反序列化的内容进行修改替换。
serialVersionUID
是类的序列化版本号,保证能将二进制流反序列化为内存中存在的类的对象。如果不主动生成的话,在序列化反序列化过程中根据类信息动态生成,耗时且类结构不灵活。
Serializable
接口
why:
1.因为一旦使用了,以后就不好去掉了。本来就不建议使用原生序列化方式。
2.增加产生bug和安全漏洞的风险。
3.增加新版本发布的测试负担。readObject
和writeObject
方法,因为Java实现的太笨重了,序列化所有东西,并且耗时不可控,可能导致安全和异常等问题。自己实现相对会减少这些风险。readObject
方法
其实还是降低安全风险等问题,如变量的完整校验,不要将序列化方法重写,交给子类不可控等问题。枚举
而不是readResolve
方法
why:
枚举类对象的序列化和反序列化方式是Java语言规范的,不是由用户实现的。枚举类对象是天生的单例对象。
自实现readResolve
方法实现单例的话,需要做很多额外工作,如将所有成员变量用transient
修饰等,此外仍然会拥有Java序列化的缺点。writeReplace
方法,好处是安全。关于原生序列化的优化,其实都不用看。。。
因为生产上一般是不会使用原生序列化的,性能差又不安全又不易读....有关序列化的知识和框架选择,可参考: 序列化和反序列化 分布式服务序列化框架的使用与选型