LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

编写高质量代码:改善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

VectorArrayList 不同的地方是它提供了递增步长,其值代表的是每次数组拓长时要增加的长度,不设置此值则是容量翻倍(默认是不设置递增步长的,可以通过构造函数来设置递增步长)。其他集合类的扩容方式与此相似,如 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对象不可更改

ArraysasList 返回的 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 类的 m1m2 都是调用 m 方法,但是 m1 打印 truem2 打印 false

建议118:不推荐覆写start方法

Thread 的多线程类是在 start 方法里面调用本地方法 start0 实现的。

建议123:volatile不能保证数据同步

volatile 不能保证数据是同步的,只能保证线程能够获得最新值。

建议125:优先选择线程池

线程有五个状态:新建状态(New)、可运行状态(Runnable,也叫做运行状态)、阻塞状态(Blocked)、等待状态(Waiting)、结束状态(Terminated),线程的状态只能由新建转变为了运行态后才可能被阻塞或等待,最后终结,本末倒置会抛出 IllegalThreadStateException