编写高质量代码:改善Java程序的151个建议
建议6:覆写变长方法也循规蹈矩
子类 Override 父类必须符合开闭原则:
- 重写方法不能缩小访问权限。
- 参数列表必须与被重写方法相同。
- 返回类型必须与被重写方法的相同或是其子类。
但是重写可以混合变长和数组,编写代码只会有 Warning,调用时候父类和子类形式却可以不一样:
class Parent {
public void name(int... is) {
System.out.println("parent");
}
}
class Child extends Parent {
@Override
public void name(int[] is) {
System.out.println("child");
}
}
父类可以变长参数调用和数组方式,此时以变长参数调用会自动向上转为数组:
Parent p = new Child();
p.name(10);
子类却只能数组方式调用:
int[] is = {}
new Child().name(is);
建议7:警惕自增的陷阱
int count = 1;
for (int i = 0; i < 10; i++) {
count = count++;
}
System.out.println(count) // 1
建议8:不要让旧语法困扰你
该书的说法是错误的,Java 的标签是用于循环块前面,为了可以更快 break/continue label,而非 goto 作用。
建议11:养成良好习惯,显式声明UID
一个实现 Serializable 的类,假如不显示如下声明:
private static final long serialVersionUID = XXXXXL;
编译时会自动生成 serialVersionUID
,假如版本不一致但是代码可以兼容,也会抛出 InvalidClassException
。
建议17:慎用动态编译
从 Java 6 版本开始支持动态编译,简单的示例,首先需要一个将字符串转为 Java 对象的类:
class JavaFileObjectFromString extends SimpleJavaFileObject {
final String code;
public JavaFileObjectFromString(String name, String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}
之后就是使用 Java API 来动态编译和反射调用:
String className = "DynamicClass";
String methodName = "dynamicMethod";
String sourceText = "public class " + className + " { public String " + methodName + "(String name) { return \"" + methodName + "(\" + name + \")\"; }}";
System.out.println(sourceText);
JavaCompiler compiler = new ToolProvider().getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
JavaFileObject javaFileObject = new JavaFileObjectFromString(className, sourceText);
List<JavaFileObject> javaFileObjects = Arrays.asList(javaFileObject);
List<String> options = new ArrayList<>();
options.addAll(Arrays.asList("-d", "./"));
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options, null, javaFileObjects);
if (task.call()) {
Object dynamicObject = Class.forName(className).newInstance();
Class<? extends Object> dynamicClass = dynamicObject.getClass();
Method dynamicMethod = dynamicClass.getMethod(methodName, String.class);
String result = (String)dynamicMethod.invoke(dynamicObject, "parameter");
System.out.println(result);
}
建议28:优先使用整型池
装箱动作是通过 valueOf
方法实现,对于 Integer
在数区间 [-128, 127] 有实现缓存:
public static Integer valueOf(int i) {
final int offset = 128;
if (i >= -128 && i <= 127) {
return IntegerCache.cache[i + offset];
}
return new Integer(i);
}
建议31:在接口中不要存在实现代码
虽然 Java 8 之后的 interface
可以有 default
来实现代码,此前,则需要一些技巧:
interface Dog {
public static final Man owner = new Man() {
public void play() {}
};
}
interface Man {
public void play();
}
建议32:静态变量一定要先声明后赋值
JVM 初始化变量是先声明后赋值:
class StaticBlockFirst {
static { i = 100; }
public static int i = 1;
} // i = 1
class StaticBlockLast {
public static int i = 1;
static { i = 100; }
} // i = 100
建议37:构造代码块会想你所想
构造代码块会在每个构造函数内首先执行,如果遇到 this
关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块。
建议38:使用静态内部类提高封装性
只有在是静态内部类的情况下才能把 static
修复符放在类前,其他任何时候 static
都是不能修饰类的。静态内部类虽然存在于外部类内,而且编译后的类文件名也包含外部类(格式是:外部类+$+内部类),但是它可以脱离外部类存在。
静态内部类与普通内部类区别:
- 在普通内部类中,我们可以直接访问外部类的属性、方法,即使是
private
类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性(如果是private
权限也能访问,这是由其代码位置所决定的),其他则不能访问。 - 普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生同死,一起声明,一起被垃圾回收器回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。
- 普通内部类不能声明
static
的方法和变量,注意这里说的是变量,常量(也就是final static
修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。
建议63:在明确的场景下,为集合指定初始容量
ArrayList
是一个大小可变的数组,但它在底层使用的是数组存储,当存储数据大于数组长度,就会扩容 1.5 倍,ArrayList
默认数组长度是 10,为了避免多次扩容产生消耗,可以进行有参 new
。
Vector
与 ArrayList
不同的地方是它提供了递增步长,其值代表的是每次数组拓长时要增加的长度,不设置此值则是容量翻倍(默认是不设置递增步长的,可以通过构造函数来设置递增步长)。其他集合类的扩容方式与此相似,如 HashMap
是按照倍数增加的,Stack
继承自 Vector
,所采用的也是与其相同的扩容原则。
建议65:避开基本类型数组转换列表陷阱
Arrays.asList
方法输入的是一个泛型变长参数,我们知道基本类型是不能泛型化的,也就是说 8 个基本类型不能作为泛型参数,要想作为泛型参数就必须使用其所对应的包装类型。当传入基本类型数组时候,会直接被当成一个 T 的类型。
Integer[] box = {1, 2, 3};
int[] unbox = {1, 2, 3};
List boxList = Arrays.asList(box); // length = 3
List unboxList = Arrays.asList(unbox); // length = 1
System.out.println(boxList.get(0).getClass()); // class java.lang.Integer
System.out.println(unboxList.get(0).getClass()); // class [I
建议66:asList方法产生的List对象不可更改
Arrays
的 asList
返回的 ArrayList
对象是 Arrays
工具类的一个内置类,而非 java.util.ArrayList
。
建议69:列表相等只需关心元素数据
List
的子类都是继承 AbastractList
抽象类,他们的 equals
方法是在 AbstractList
中定义的,只要所有的元素相等,并且长度也相等就表明两个 List
是相等的,与具体的容器类型无关。
建议70:子列表只是原列表的一个视图
List
接口提供了 subList
方法,是由 AbstractList
实现的,返回的的是通过内部 offset
来控制列表起始位置的视图,所有的修改动作直接作用于原列表。
建议72:生成子列表后不要再操作原列表
生成子列表后,subList
并未锁定原表。但是修改表后会因为计数器修改问题导致抛出 ConcurrentModifcationException
。
建议79:集合中的哈希码不要重复
HashMap
底层是用数组存储 Entry
对象,获取对象是先用 hash
(使用到哈希码)和 indexFor
函数来计算 Entry
对象所在数组的位置:
static int hash(int hashCode) {
hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12);
return hashCode ^ (hashCode >>> 7) ^ (hashCode >>> 4);
}
static int indexFor(int hash, int length) {
return hash & (length - 1);
}
获取的 Entry
对象是一个单向链表,除了保存键值对还有一个 next
变量指向下个 Entry
对象。HashMap
的存储主线还是数组,遇到哈希冲突的时候则使用链表解决。如果哈希码相同,它的查找效率就与 LinkedList
差不多了。
建议84:使用构造函数协助描述枚举项
enum Role {
Admin("admin", new Lifetime(), new Scope()),
User("user", new Lifetime(), new Scope());
static class Lifetime {}
static class Scope {}
private String name;
private Lifetime lifetime;
private Scope scope;
Role(String name, Lifetime lifetime, Scope scope) {
this.name = name;
this.lifetime = lifetime;
this.scope = scope;
}
public String getName() { return name; }
public Lifetime getLifetime() { return lifetime; }
public Scope getScope() { return scope; }
}
建议85:小心switch带来的空值异常
目前 Java 中的 switch
语句只能判断基本类型,但是遇到枚举类型,编译器会调用 Enum.ordinal()
做转换,如果该枚举为 null
,就会出现空指针异常。
建议88:用枚举实现工厂方法模式更简洁
enum Factory {
Ford {
public Car create() {
return new Ford();
}
};
interface Car {};
class Ford implements Car {};
public abstract Car create();
}
建议89:枚举项的数量限制在64个以内
使用 EnumSet
时候,当枚举项数目不大于 64 时,返回 java.util.RegularEnumSet
,大于 64 时,则返回 java.util.JumboEnumSet
。
建议93:Java的泛型是类型擦除的
- 泛型的 class 对象是相同的
- 可以声明一个带有泛型参数的数组,但是不能直接初始化该数组
- instanceof 不允许存在泛型参数
建议94:不能初始化泛型参数和数组
class Klazz<T> {
private T t = new T(); // error
private List<T> = new ArrayList<T>(); // ok
}
在编译期,T
类型会直接被擦除,容器里面的类型会变成 Object
,根据之前的 T
做强制转换。
建议95:强制声明泛型的实际类型
有一个泛型工具类:
class ArrayListUtils {
@SafeVarargs
public static<T> List<T> asList(T... t) {
List<T> list = new ArrayList<T>();
Collections.addAll(list, t);
return list;
}
}
当参数为空时候,可以在调用 asList
时指定其类型:
List<String> l = ArrayListUtils.<String>asList();
建议97:警惕泛型是不能协变和逆变的
协变是用一个窄类型替换宽类型,而逆变则是用宽类型覆盖窄类型。Java 的泛型是不支持协变和逆变的,只是能够实现协变和逆变。
List<? extends Number> numbers = new ArrayList<Integer>();
List<? super Integer> integers = new ArrayList<Number>();
建议98:建议采用的顺序是List<T>、List<?>、List<Object>
List<?>
是只读类型的,不能进行增加、修改操作,可以理解为 List<? extends Object>
建议103:反射访问属性或方法时将Accessible设置为true
通过反射方式执行方法时,必须在 invoke
之前检查 Accessible
属性,但方法对象的 Accessible
属性并不是用来决定是否可访问的,而是指是否更容易获得,是否进行安全检查,如果不需要则直接执行,这就可以大幅度地提升系统性能。
建议106:动态代理可以使代理模式更加灵活
Java 提供了 java.lang.reflect.Proxy
用于实现动态代理:
interface Subject {
public void request();
}
class ComputerScience implements Subject {
public void request() {
}
}
class SubjectHandler implements InvocationHandler {
private Subject subject;
public SubjectHandler(Subject subject) {
this.subject = subject;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Todo");
Object result = method.invoke(subject, args);
System.out.println("Done");
return result;
}
}
调用:
Subject subject = new ComputerScience();
InvocationHandler handler = new SubjectHandler(subject);
Class<?> klazz = subject.getClass();
Subject proxy = (Subject)Proxy.newProxyInstance(klazz.getClassLoader(), klazz.getInterfaces(), handler);
proxy.request();
建议113:不要在finally块中处理返回值
public static int doStuff(int n) {
try {
if (n < 0) {
throw new Exception();
} else {
return n;
}
} catch (Exception e) {
throw e;
} finally {
return -1;
}
}
finally
里面的 return
会覆盖代码的正常 return
值,而且会屏蔽掉抛出的异常:
doStuff(-1) // -1
doStuff(100) // -1
建议115:使用Throwable获得栈信息
代码植入修改:
class Invoker {
public static void m1() {
System.out.println(m());
}
public static void m2() {
System.out.println(m());
}
private static boolean m() {
StackTraceElement[] stackTraceElements = new Throwable().getStackTrace();
for (StackTraceElement stackTraceElement: stackTraceElements) {
if (stackTraceElement.getMethodName().equals("m1")) {
return true;
}
}
return false;
}
}
Invoker
类的 m1
和 m2
都是调用 m
方法,但是 m1
打印 true
,m2
打印 false
。
建议118:不推荐覆写start方法
Thread
的多线程类是在 start
方法里面调用本地方法 start0
实现的。
建议123:volatile不能保证数据同步
volatile
不能保证数据是同步的,只能保证线程能够获得最新值。
建议125:优先选择线程池
线程有五个状态:新建状态(New)、可运行状态(Runnable,也叫做运行状态)、阻塞状态(Blocked)、等待状态(Waiting)、结束状态(Terminated),线程的状态只能由新建转变为了运行态后才可能被阻塞或等待,最后终结,本末倒置会抛出 IllegalThreadStateException
。