编写高质量Java代码的151个建议(91-110)

序言

声明

因为简书篇幅限制,151个建议只能分开.这里是 [91-110]
本书来源 @Linux公社 的 <<编写高质量Java代码的151个习惯>> 的电子书
作者:秦小波 出版社:北京 机械工业出版社 2011.11
如有侵权请联系 @小猪童鞋QQ聊天链接 删除


@编写高质量Java代码的151个建议(1-40)
@编写高质量Java代码的151个建议(41-70)
@编写高质量Java代码的151个建议(71-90)
@编写高质量Java代码的151个建议(91-110)
@编写高质量Java代码的151个建议(111-124)
@编写高质量Java代码的151个建议(125-135)
@编写高质量Java代码的151个建议(136-151)


致本文读者:

如果小伙伴发现有地方有错误,请联系我 @小猪童鞋QQ聊天链接
欢迎小伙伴和各位大佬们一起学习,可私信也可通过上方QQ链接

我的环境:

eclipse version: 2019-03 (4.11.0) Build id: 20190314-1200
jdk1.8
Lombok.jar 插件 安装指南看这里 @简单粗暴节省JavaBean代码插件 Lombok.jar

建议91:枚举和注解结合使用威力更大

我们知道注解的写法和接口很类似,都采用了关键字interface,而且都不能有实现代码,常量定义默认都是public static final 类型的等,它们的主要不同点是:注解要在interface前加上@字符,而且不能继承,不能实现,这经常会给我们的开发带来些障碍。

我们来分析一下ACL(Access Control List,访问控制列表)设计案例,看看如何避免这些障碍,ACL有三个重要元素:

资源,有哪些信息是要被控制起来的。
权限级别,不同的访问者规划在不同的级别中。
控制器(也叫鉴权人),控制不同的级别访问不同的资源。
  鉴权人是整个ACL的设计核心,我们从最主要的鉴权人开始,代码如下:

interface Identifier{
    //无权访问时的礼貌语
    String REFUSE_WORD  =  "您无权访问";
    //鉴权
    public  boolean identify();
}

这是一个鉴权人接口,定义了一个常量和一个鉴权方法。接下来应该实现该鉴权方法,但问题是我们的权限级别和鉴权方法之间是紧耦合,若分拆成两个类显得有点啰嗦,怎么办?我们可以直接顶一个枚举来实现,代码如下:

enum CommonIdentifier implements Identifier {
    // 权限级别
    Reader, Author, Admin;

    @Override
    public boolean identify() {
        return false;
    }

}

定义了一个通用鉴权者,使用的是枚举类型,并且实现了鉴权者接口。现在就剩下资源定义了,这很容易定义,资源就是我们写的类、方法等,之后再通过配置来决定哪些类、方法允许什么级别的访问,这里的问题是:怎么把资源和权限级别关联起来呢?使用XML配置文件?是个方法,但对我们的示例程序来说显得太繁重了,如果使用注解会更简洁些,不过这需要我们首先定义出权限级别的注解,代码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Access{
    //什么级别可以访问,默认是管理员
    CommonIdentifier level () default CommonIdentifier.Admin;
}

该注解释标注在类上面的,并且会保留到运行期。我们定义一个资源类,代码如下:

@Access(level=CommonIdentifier.Author)
class Foo{
    
}

Foo类只能是作者级别的人访问。场景都定义完毕了,那我们看看如何模拟ACL实现,代码如下:

public static void main(String[] args) {
        // 初始化商业逻辑
        Foo b = new Foo();
        // 获取注解
        Access access = b.getClass().getAnnotation(Access.class);
        // 没有Access注解或者鉴权失败
        if (null == access || !access.level().identify()) {
            // 没有Access注解或者鉴权失败
            System.out.println(access.level().REFUSE_WORD);
        }
    }

看看这段代码,简单,易读,而且如果我们是通过ClassLoader类来解释该注解的,那会使我们的开发更简洁,所有的开发人员只要增加注解即可解决访问控制问题。注意看加粗代码,access是一个注解类型,我们想使用Identifier接口的identity鉴权方法和REFUSE_WORD常量,但注解释不能集成的,那怎么办?此处,可通过枚举类型CommonIdentifier从中间做一个委派动作(Delegate),委派?你可以然identity返回一个对象,或者在Identifier上直接定义一个常量对象,那就是“赤裸裸” 的委派了。

建议92:注意@Override不同版本的区别

@Override注解用于方法的覆写上,它是在编译器有效,也就是Java编译器在编译时会根据注解检查方法是否真的是覆写,如果不是就报错,拒绝编译。该注解可以很大程度地解决我们的误写问题,比如子类和父类的方法名少写一个字符,或者是数字0和字母O为区分出来等,这基本是每个程序员都曾将犯过的错误。在代码中加上@Override注解基本上可以杜绝出现此类问题,但是@Override有个版本问题,我们来看如下代码:

interface Foo {
    public void doSomething();
}

class FooImpl implements Foo{
    @Override
    public void doSomething() {
        
    }
}

这是一个简单的@Override示例,接口中定义了一个doSomething方法,实现类FooImpl实现此方法,并且在方法前加上了@Override注解。这段代码在Java1.6版本上编译没问题,虽然doSomething方法只是实现了接口的定义,严格来说并不是覆写,但@Override出现在这里可减少代码中出现的错误。

可如果在Java1.5版本上编译此段代码可能会出现错误:

The method doSomeThing() of type FooImpl must override a superclass method

注意,这是个错误,不能继续编译,原因是Java1.5版本的@Override是严格遵守覆写的定义:子类方法与父类方法必须具有相同的方法名、输出参数、输出参数(允许子类缩小)、访问权限(允许子类扩大),父类必须是一个类,不能是接口,否则不能算是覆写。而这在Java1.6就开放了很多,实现接口的方法也可以加上@Override注解了,可以避免粗心大意导致方法名称与接口不一致的情况发生。

在多环境部署应用时,需呀考虑@Override在不同版本下代表的意义,如果是Java1.6版本的程序移植到1.5版本环境中,就需要删除实现接口方法上的@Override注解。

第七章 泛型和反射

泛型可以减少强制类型的转换,可以规范集合的元素类型,还可以提高代码的安全性和可读性,正式因为有这些优点,自从Java引入泛型后,项目的编码规则上便多了一条:优先使用泛型。

反射可以“看透” 程序的运行情况,可以让我们在运行期知晓一个类或实例的运行状况,可以动态的加载和调用,虽然有一定的性能忧患,但它带给我们的遍历远远大于其性能缺陷。

建议93:Java的泛型是可以擦除的

Java泛型(Generic) 的引入加强了参数类型的安全性,减少了类型的转换,它与C++中的模板(Temeplates) 比较类似,但是有一点不同的是:Java的泛型在编译器有效,在运行期被删除,也就是说所有的泛型参数类型在编译后会被清除掉,我们来看一个例子,代码如下:

public class Foo {
    //arrayMethod接收数组参数,并进行重载
    public void arrayMethod(String[] intArray) {

    }

    public void arrayMethod(Integer[] intArray) {

    }
    //listMethod接收泛型List参数,并进行重载
    public void listMethod(List<String> stringList) {

    }
    public void listMethod(List<Integer> intList) {
        
    }
}

程序很简单,编写了4个方法,arrayMethod方法接收String数组和Integer数组,这是一个典型的重载,listMethod接收元素类型为String和Integer的list变量。现在的问题是,这段程序是否能编译?如果不能?问题出在什么地方?

事实上,这段程序时无法编译的,编译时报错信息如下:

Erasure of method listMethod(List<String>) is the same as another method in type Foo

这段错误的意思:简单的的说就是方法签名重复,其实就是说listMethod(List<Integer> intList)方法在编译时擦除类型后是listMethod(List<E> intList)与另一个方法重复。这就是Java泛型擦除引起的问题:在编译后所有的泛型类型都会做相应的转化。转换规则如下:

List<String>、List<Integer>、List<T>擦除后的类型为List
List<String>[] 擦除后的类型为List[].
List<? extends E> 、List<? super E> 擦除后的类型为List<E>.
List<T extends Serializable & Cloneable >擦除后的类型为List< Serializable>.
  明白了这些规则,再看如下代码:

public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("abc");
        String str = list.get(0);
    }

进过编译后的擦除处理,上面的代码和下面的程序时一致的:

public static void main(String[] args) {
        List list = new ArrayList();
        list.add("abc");
        String str = (String) list.get(0);
    }

Java编译后字节码中已经没有泛型的任何信息了,也就是说一个泛型类和一个普通类在经过编译后都指向了同一字节码,比如Foo<T>类,经过编译后将只有一份Foo.class类,不管是Foo<String>还是Foo<Integer>引用的都是同一字节码。Java之所以如此处理,有两个原因:

避免JVM的大换血。C++泛型生命期延续到了运行期,而Java是在编译期擦除掉的,我们想想,如果JVM也把泛型类型延续到运行期,那么JVM就需要进行大量的重构工作了。
版本兼容:在编译期擦除可以更好的支持原生类型(Raw Type),在Java1.5或1.6...平台上,即使声明一个List这样的原生类型也是可以正常编译通过的,只是会产生警告信息而已。
  明白了Java泛型是类型擦除的,我们就可以解释类似如下的问题了:

泛型的class对象是相同的:每个类都有一个class属性,泛型化不会改变class属性的返回值,例如:

public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        List<Integer> list2 = new ArrayList<Integer>();
        System.out.println(list.getClass()==list2.getClass());
    }

以上代码返回true,原因很简单,List<String>和List<Integer>擦除后的类型都是List,没有任何区别。

2.泛型数组初始化时不能声明泛型,如下代码编译时通不过:

List<String>[] listArray = new List<String>[];

原因很简单,可以声明一个带有泛型参数的数组,但不能初始化该数组,因为执行了类型擦除操作,List<Object>[]与List<String>[] 就是同一回事了,编译器拒绝如此声明。

3.instanceof不允许存在泛型参数

以下代码不能通过编译,原因一样,泛型类型被擦除了:

List<String> list = new ArrayList<String>();
System.out.println(list instanceof List<String>);
建议94:不能初始化泛型参数和数组

泛型类型在编译期被擦除,我们在类初始化时将无法获得泛型的具体参数,比如这样的代码:

class Test<T> {
    private T t = new T();
    private T[] tArray = new T[5];
    private List<T> list = new ArrayList<T>();
}

这段代码有神么问题呢?t、tArray、list都是类变量,都是通过new声明了一个类型,看起来非常相似啊!但这段代码是编译不过的,因为编译器在编译时需要获得T类型,但泛型在编译期类型已经被擦除了,所有new T()和 new T[5]都会报错(有人可能会有疑问,泛型类型可以擦除为顶级Object,那T类型擦除成Object不就可以编译了吗?这样也不行,泛型只是Java语言的一部分,Java语言毕竟是一个强类型、编译型的安全语言,要确保运行期的稳定性和安全性就必须要求在编译器上严格检查)。可为什么new ArrayList<T>()却不会报错呢?

这是因为ArrayList表面是泛型,其实已经在编译期转为Object了,我们来看一下ArrayList的源代码就清楚了,代码如下:

public class ArrayList<E> extends AbstractList<E> implements List<E>,
        RandomAccess, Cloneable, java.io.Serializable {
    // 容纳元素的数组
    private transient Object[] elementData;

    // 构造函数
    public ArrayList() {
        this(10);
    }

    // 获得一个元素
    public E get(int index) {
        rangeCheck(index);
        // 返回前强制类型转换
        return elementData(index);
    }
    /* 其它代码略 */

}

注意看elementData的定义,它容纳了ArrayList的所有元素,其类型是Object数组,因为Object是所有类的父类,数组又允许协变(Covariant),因此elementData数组可以容纳所有的实例对象。元素加入时向上转型为Object类型(E类型转换为Object),取出时向下转型为E类型,如此处理而已。

在某些情况下,我们需要泛型数组,那该如何处理呢?代码如下:

class Test<T> {
    // 不再初始化,由构造函数初始化
    private T t;
    private T[] tArray;
    private List<T> list = new ArrayList<T>();

    // 构造函数初始化
    public Test() {
        try {
            Class<?> tType = Class.forName("");
            t = (T) tType.newInstance();
            tArray = (T[]) Array.newInstance(tType, 5);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

此时,运行就没有什么问题了,剩下的问题就是怎么在运行期获得T的类型,也就是tType参数,一般情况下泛型类型是无法获取的,不过,在客户端调用时多传输一个T类型的class就会解决问题。

类的成员变量是在类初始化前初始化的,所以要求在初始化前它必须具有明确的类型,否则就只能声明,不能初始化。

建议95:强制声明泛型的实际类型

Arrays工具类有一个方法asList可以把一个变长参数或数组转变为列表,但是它有一个缺点:它所生成的list长度是不可变的,而这在我们的项目开发中有时会很不方便。如果你期望生成的列表长度可变,那就需要自己来写一个数组的工具类了,代码如下:

class ArrayUtils {
    // 把一个变长参数转化为列表,并且长度可变
    public static <T> List<T> asList(T... t) {
        List<T> list = new ArrayList<T>();
        Collections.addAll(list, t);
        return list;
    }
}

这很简单,与Arrays.asList的调用方式相同,我们传入一个泛型对象,然后返回相应的List,代码如下:

public static void main(String[] args) {
        // 正常用法
        List<String> list1 = ArrayUtils.asList("A", "B");
        // 参数为空
        List list2 = ArrayUtils.asList();
        // 参数为整型和浮点型的混合
        List list3 = ArrayUtils.asList(1, 2, 3.1);
    }

这里有三个变量需要说明:

(1)、变量list1:变量list1是一个常规用法,没有任何问题,泛型实际参数类型是String,返回结果就是一个容纳String元素的List对象。

(2)、变量list2:变量list2它容纳的是什么元素呢?我们无法从代码中推断出list2列表到底容纳的是什么元素(因为它传递的参数是空,编译器也不知道泛型的实际参数类型是什么),不过,编译器会很聪明地推断出最顶层类Object就是其泛型类型,也就是说list2的完整定义如下:

List<Object> list2 = ArrayUtils.asList();

如此一来,编译器就不会给出" unchecked "警告了。现在新的问题又出现了:如果期望list2是一个Integer类型的列表,而不是Object列表,因为后续的逻辑会把Integer类型加入到list2中,那该如何处理呢?

强制类型转换(把asList强制转换成List<Integer>)?行不通,虽然Java泛型是编译期擦出的,但是List<Object>和List<Integer>没有继承关系,不能强制转换。

重新声明一个List<Integer>,然后读取List<Object>元素,一个一个地向下转型过去?麻烦,而且效率又低。

最好的解决办法是强制声明泛型类型,代码如下:

List<Integer> intList = ArrayUtils.<Integer>asList();

就这么简单,asList方法要求的是一个泛型参数,那我们就在输入前定义这是一个Integer类型的参数,当然,输出也是Integer类型的集合了。

(3)、变量list3:变量list3有两种类型的元素:整数类型和浮点类型,那它生成的List泛型化参数应该是什么呢?是Integer和Float的父类Number?你太高看编译器了,它不会如此推断的,当它发现多个元素的实际类型不一致时就会直接确认泛型类型是Object,而不会去追索元素的公共父类是什么,但是对于list3,我们更期望它的泛型参数是Number,都是数字嘛,参照list2变量,代码修改如下:

List<Number> list3 = ArrayUtils.<Number>asList(1, 2, 3.1);

Number是Integer和Float的父类,先把三个输入参数、输出参数同类型,问题是我们要在什么时候明确泛型类型呢?一句话:无法从代码中推断出泛型的情况下,即可强制声明泛型类型。

建议96:不同的场景使用不同的泛型通配符

Java泛型支持通配符(Wildcard),可以单独使用一个“?”表示任意类,也可以使用extends关键字表示某一个类(接口)的子类型,还可以使用super关键字表示某一个类(接口)的父类型,但问题是什么时候该用extends,什么该用super呢?

(1)、泛型结构只参与 “读” 操作则限定上界(extends关键字)

阅读如下代码,想想看我们的业务逻辑操作是否还能继续:

public static <E> void read(List<? super E> list) {
        for (Object obj : list) {
            // 业务逻辑操作
        }
    }

从List列表中读取元素的操作(比如一个数字列表中的求和计算),你觉得方法read能继续写下去吗?

答案是:不能,我们不知道list到底存放的是什么元素,只能推断出E类型是父类,但问题是E类型的父类又是什么呢?无法再推断,只有运行期才知道,那么编码器就无法操作了。当然,你可以把它当做是Object类来处理,需要时再转换成E类型---这完全违背了泛型的初衷。在这种情况下,“读” 操作如果期望从List集合中读取数据就需要使用extends关键字了,也就是要界定泛型的上界,代码如下:

public static <E> void read(List<? extends E> list) {
        for (E e : list) {
            // 业务逻辑操作
        }
    }

此时,已经推断出List集合中取出的元素时E类型的元素。具体是什么类型的元素就要等到运行期才确定了,但它一定是一个确定的类型,比如read(Arrays.asList("A"))调用该方法时,可以推断出List中的元素类型是String,之后就可以对List中的元素进行操作了。如加入到另外的List<E>中,或者作为Map<E,V>的键等。

(2)、泛型结构只参与“写” 操作则限定下界(使用super关键字)

先看如下代码能否编译:

public static <E> void write(List<? extends Number> list){
        //加入一个元素
        list.add(123);
    }

编译失败,失败的原因是list中的元素类型不确定,也就是编译器无法推断出泛型类型到底是什么,是Integer类型?是Double?还是Byte?这些都符合extends关键字的定义,由于无法确定实际的泛型类型,所以编译器拒绝了此类操作。

在此种情况下,只有一个元素时可以add进去的:null值,这是因为null是一个万用类型,它可以是所有类的实例对象,所以可以加入到任何列表中。

Object是否可以?不可以,因为它不是Number子类,而且即使把List变量修改为List<? extends Object> 类型也不能加入,原因很简单,编译器无法推断出泛型类型,加什么元素都是无效的。

在这种“写”的操作的情况下,使用super关键字限定泛型的下界才是正道,代码如下:

public static <E> void write(List<? super Number> list){
        //加入元素
        list.add(123);
        list.add(3.14);
    }

甭管它是Integer的123,还是浮点数3.14,都可以加入到list列表中,因为它们都是Number的类型,这就保证了泛型类的可靠性。

对于是要限定上界还是限定下界,JDK的Collections.copy方法是一个非常好的例子,它实现了把源列表的所有元素拷贝到目标列表中对应的索引位置上,代码如下:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

源列表是用来提供数据的,所以src变量需要界定上界,要有extends关键字。目标列表是用来写数据的,所以dest变量需要界定下界,带有super关键字。

如果一个泛型结构既用作 “读” 操作又用作“写操作”,那该如何进行限定呢?不限定,使用确定的泛型类型即可,如List<E>.

建议97:警惕泛型是不能协变和逆变的

什么叫协变和逆变?

在编程语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性,简单的说,协变是一个窄类型替换宽类型,而逆变则是用宽类型覆盖窄类型。其实,在Java中协变和逆变我们已经用了很久了,只是我们没发觉而已,看如下代码:

class Base {
    public Number doStuff() {
        return 0;
    }
}

class Sub extends Base {
    @Override
    public Integer doStuff() {
        return 0;
    }
}

子类的doStuff方法返回值的类型比父类方法要窄,此时doStuff方法就是一个协变方法,同时根据Java的覆写定义来看,这又属于覆写。那逆变是怎么回事呢?代码如下:

class Base {
    public void doStuff(Integer i) {
        
    }
}

class Sub extends Base {
    @Override
    public void doStuff(Number n) {
      
    }
}

子类的doStuff方法的参数类型比父类要宽,此时就是一个逆变方法,子类扩大了父类方法的输入参数,但根据覆写的定义来看,doStuff不属于覆写,只是重载而已。由于此时的doStuff方法已经与父类没有任何关系了,只是子类独立扩展出的一个行为,所以是否声明为doStuff方法名意义不大,逆变已经不具有特别的意义了,我们重点关注一下协变,先看如下代码是否是协变:

   public static void main(String[] args) {
        Base base = new Sub();
    }

base变量是否发生了协变?是的,发生了协变,base变量是Base类型,它是父类,而其赋值却是在子类实例,也就是用窄类型覆盖了宽类型。这也叫多态,两者同含义。

说了这么多,下面再再来想想泛型是否支持协变和逆变呢,答案是:泛型既不支持协变,也不支持逆变。为什么会不支持呢?

(1)、泛型不支持协变:数组和泛型很相似,一个是中括号,一个是尖括号,那我们就以数组为参照对象,看如下代码:

public static void main(String[] args) {
        //数组支持协变
        Number [] n = new Integer[10];
        //编译不通过,泛型不支持协变
        List<Number> list = new ArrayList<Integer>();
    }

ArrayList是List的子类型,Integer是Number的子类型,里氏替换原则在此行不通了,原因就是Java为了保证运行期的安全性,必须保证泛型参数的类型是固定的,所以它不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。

泛型不支持协变,但可以使用通配符模拟协变,代码如下:

    //Number子类型(包括Number类型) 都可以是泛型参数类型
        List<? extends Number> list = new ArrayList<Integer>();

" ? extends Number " 表示的意思是,允许Number的所有子类(包括自身) 作为泛型参数类型,但在运行期只能是一个具体类型,或者是Integer类型,或者是Double类型,或者是Number类型,也就是说通配符只在编码期有效,运行期则必须是一个确定的类型。

(2)、泛型不支持逆变

java虽然允许逆变存在,但在对类型赋值上是不允许逆变的,你不能把一个父类实例对象赋给一个子类类型变量,泛型自然也不允许此种情况发生了。但是它可以使用super关键字来模拟实现,代码如下:

 //Integer的父类型(包括Integer)都可以是泛型参数类型
        List<? super Integer> list = new ArrayList<Number>();

" ? super Integer " 的意思是可以把所有的Integer父类型(自身、父类或接口) 作为泛型参数,这里看着就像是把一个Number类型的ArrayList赋值给了Integer类型的List,其外观类似于使用一个宽类型覆盖一个窄类型,它模拟了逆变的实现。

泛型既不支持协变,也不支持逆变,带有泛型参数的子类型定义与我们经常使用的类类型也不相同,其基本类型关系如下表所示:

泛型通配符QA 问 回答
Integer是Number的子类型? 正确
ArrayList<Integer> 是List<Integer> 的子类型 正确
Integer[]是 Number[]的子类型? 正确
List<Integer> 是 List<Number> 的子类型? 错误
List<Integer> 是 List<? extends Integer> 的子类型? 错误
List<Integer> 是 List<? super Integer> 的子类型? 错误
Java的泛型是不支持协变和逆变的,只是能够实现逆变和协变 Over
建议98:建议的采用顺序是List中泛型顺序依次为T、?、Object

List<T>、List<?>、List<Object>这三者都可以容纳所有的对象,但使用的顺序应该是首选List<T>,次之List<?>,最后选择List<Object>,原因如下:

(1)、List<T>是确定的某一个类型

List<T>表示的是List集合中的元素都为T类型,具体类型在运行期决定;List<?>表示的是任意类型,与List<T>类似,而List<Object>则表示List集合中的所有元素为Object类型,因为Object是所有类的父类,所以List<Object>也可以容纳所有的类类型,从这一字面意义上分析,List<T>更符合习惯:编码者知道它是某一个类型,只是在运行期才确定而已。

(2)List<T>可以进行读写操作

List<T>可以进行诸如add,remove等操作,因为它的类型是固定的T类型,在编码期不需要进行任何的转型操作。

List<T>是只读类型的,不能进行增加、修改操作,因为编译器不知道List中容纳的是什么类型的元素,也就无法校验类型是否安全了,而且List<?>读取出的元素都是Object类型的,需要主动转型,所以它经常用于泛型方法的返回值。注意List<?>虽然无法增加,修改元素,但是却可以删除元素,比如执行remove、clear等方法,那是因为它的删除动作与泛型类型无关。

List<Object> 也可以读写操作,但是它执行写入操作时需要向上转型(Up cast),在读取数据的时候需要向下转型,而此时已经失去了泛型存在的意义了。

打个比方,有一个篮子用来容纳物品,比如西瓜,番茄等.List<?>的意思是说,“嘿,我这里有一个篮子,可以容纳固定类别的东西,比如西瓜,番茄等”。List<?>的意思是说:“嘿,我有一个篮子,我可以容纳任何东西,只要是你想得到的”。而List<Object>就更有意思了,它说" 嘿,我也有一个篮子,我可以容纳所有物质,只要你认为是物质的东西都可以容纳进来 "。

推而广之,Dao<T>应该比Dao<?>、Dao<Object>更先采用,Desc<Person>则比Desc<?>、Desc<Object>更优先采用。

建议99:严格限定泛型类型采用多重界限

从哲学来说,很难描述一个具体的人,你可以描述他的长相、性格、工作等,但是人都是由多重身份的,估计只有使用多个And(与操作)将所有的描述串联起来才能描述一个完整的人,比如我,上班时我是一个职员,下班了坐公交车我是一个乘客,回家了我是父母的孩子,是儿子的父亲......角色时刻在变换。那如果我们要使用Java程序来对一类人进行管理,该如何做呢?比如在公交车费优惠系统中,对部分人员(如工资低于2500元的上班族并且是站立的乘客)车费打8折,该如何实现呢?

注意这里的类型参数有两个限制条件:一个为上班族;二为乘客。具体到我们的程序中就应该是一个泛型参数具有两个上界(Upper Bound),首先定义两个接口及实现类,代码如下:

interface Staff {
    // 工资
    public int getSalary();
}

interface Passenger {
    // 是否是站立状态
    public boolean isStanding();
}
//定义我这个类型的人
class Me implements Staff, Passenger {

    @Override
    public boolean isStanding() {
        return true;
    }

    @Override
    public int getSalary() {
        return 2000;
    }
}

"Me"这种类型的人物有很多,比如系统分析师也是一个职员,也坐公交车,但他的工资实现就和我不同,再比如Boss级的人物,偶尔也坐公交车,对大老板来说他也只是一个职员,他的实现类也不同,也就是说如果我们使用“T extends Me”是限定不了需求对象的,那该怎么办呢?可以考虑使用多重限定,代码如下:

public class Test99 {
    // 工资低于2500的并且站立的乘客车票打8折
    public static <T extends Staff & Passenger> void discount(T t) {
        if (t.getSalary() < 2500 && t.isStanding()) {
            System.out.println(" 恭喜您,您的车票打八折!");
        }
    }

    public static void main(String[] args) {
        discount(new Me());
    }
}

使用“&”符号设定多重边界,指定泛型类型T必须是Staff和Passenger的共有子类型,此时变量t就具有了所有限定的方法和属性,要再进行判断就一如反掌了。在Java的泛型中,可以使用"&"符号关联多个上界并实现多个边界限定,而且只有上界才有此限定,下界没有多重限定的情况。想想你就会明白:多个下界,编码者可自行推断出具体的类型,比如“? super Integer” 和 “? extends Double”,可以更细化为Number类型了,或者Object类型了,无需编译器推断了。

为什么要说明多重边界?是因为编码者太少使用它了,比如一个判断用户权限的方法,使用的是策略模式(Strategy Pattern) ,示意代码如下:

class UserHandler<T extends User> {
    // 判断用户是否有权限执行操作
    public boolean permit(T user, List<Job> jobs) {
        List<Class<?>> iList = Arrays.asList(user.getClass().getInterfaces());
        // 判断 是否是管理员
        if (iList.indexOf(Admin.class) > -1) {
            Admin admin = (Admin) user;
            // 判断管理员是否有此权限
        } else {
            // 判断普通用户是否有此权限
        }
        return false;
    }
}

class User {}

class Job {}

class Admin extends User {}

此处进行了一次泛型参数类别判断,这里不仅仅违背了单一职责原则(Single Responsibility Principle),而且让泛型很“汗颜” :已经使用了泛型限定参数的边界了,还要进行泛型类型判断。事实上,使用多重边界可以很方便的解决此问题,而且非常优雅,建议大家 在开发中考虑使用多重限定。

建议100:数组的真实类型必须是泛型类型的子类型

List接口的toArray方法可以把一个集合转化为数组,但是使用不方便,toArray()方法返回的是一个Object数组,所以需要自行转变。toArray(T[] a)虽然返回的是T类型的数组,但是还需要传入一个T类型的数组,这也挺麻烦的,我们期望输入的是一个泛型化的List,这样就能转化为泛型数组了,来看看能不能实现,代码如下:

public static <T> T[] toArray(List<T> list) {
        T[] t = (T[]) new Object[list.size()];
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

上面要输出的参数类型定义为Object数组,然后转型为T类型数组,之后遍历List赋值给数组的每个元素,这与ArrayList的toArray方法很类似(注意只是类似),客户端的调用如下:

public static void main(String[] args) {
        List<String> list = Arrays.asList("A","B");
        for(String str :toArray(list)){
            System.out.println(str);
        }
    }

编译没有任何问题,运行后出现如下异常:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    at com.study.advice100.Client100.main(Client100.java:16)

类型转换异常,也就是说不能把一个Object数组转换为String数组,这段异常包含了两个问题:

为什么Object数组不能向下转型为String数组:数组是一个容器,只有确保容器内的所有元素类型与期望的类型有父子关系时才能转换,Object数组只能保证数组内的元素时Object类型,却不能确保它们都是String的父类型或子类,所以类型转换失败。
为什么是main方法抛出异常,而不是toArray方法:其实,是在toArray方法中进行的类型向下转换,而不是main方法中。那为什么异常会在main方法中抛出,应该在toArray方法的“ T[] t = (T[]) new Object[list.size()];”这段代码才对呀?那是因为泛型是类型擦除的,toArray方法经过编译后与如下代码相同:

public static Object[] toArrayTwo(List list) {
        // 此处的强制类型转换没必要存在,只是为了与源代码对比
        Object[] t = (Object[]) new Object[list.size()];
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B");
        for (String str : (String [])toArrayTwo(list)) {
            System.out.println(str);
        }
    }

阅读完此段代码后就很清楚了:toArray方法返回后进行一次类型转换,Object数组转换成了String数组,于是就报ClassCastException异常了。

Object数组不能转为String数组,T类型又无法在运行期获得,那该如何解决这个问题呢?其实,要想把一个Object数组转换为String数组,只要Object数组的实际类型也就是String就可以了,例如:

// objArray的实际类型和表面类型都是String数组
        Object[] objArray = { "A", "B" };
        // 抛出ClassCastException
        String[] strArray = (String[]) objArray;

        String[] ss = { "A", "B" };
        //objs的真实类型是String数组,显示类型为Object数组
        Object objs[] =ss;
        //顺利转换为String数组
        String strs[]=(String[])objs;

明白了这个问题,我们就把泛型数组声明为泛型的子类型吧!代码如下:

public static <T> T[] toArray(List<T> list,Class<T> tClass) {
        //声明并初始化一个T类型的数组
        T[] t = (T[])Array.newInstance(tClass, list.size());
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

通过反射类Array声明了一个T类型的数组,由于我们无法在运行期获得泛型类型的参数,因此就需要调用者主动传入T参数类型。此时,客户端再调用就不会出现任何异常了。

在这里我们看到,当一个泛型类(特别是泛型集合)转变为泛型数组时,泛型数组的真实类型不能是泛型的父类型(比如顶层类Object),只能是泛型类型的子类型(当然包括自身类型),否则就会出现类型转换异常。

建议101:注意Class类的特殊性

Java语言是先把Java源文件编译成后缀为class的字节码文件,然后再通过ClassLoader机制把这些类文件加载到内存中,最后生成实例执行的,这是Java处理的基本机制,但是加载到内存中的数据的如何描述一个类的呢?比如在Dog.class文件中定义一个Dog类,那它在内存中是如何展现的呢?

Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象,比如Dog.class文件加载到内存中后就会有一个class的实例对象描述之。因为是Class类是“类中类”,也就有预示着它有很多特殊的地方:

无构造函数:Java中的类一般都有构造函数,用于创建实例对象,但是Class类却没有构造函数,不能实例化,Class对象是在加载类时由Java虚拟机通过调用类加载器中的difineClass方法自动构造的。
可以描述基本类型:虽然8个基本类型在JVM中并不是一个对象,它们一般存在于栈内存中,但是Class类仍然可以描述它们,例如可以使用int.class表示int类型的类对象。

    // 类的属性class所引用的对象与实例对象的getClass返回值相同
        boolean b1=String.class.equals(new String().getClass());
        boolean b2="ABC".getClass().equals(String.class);
        // class实例对象不区分泛型
        boolean b3=ArrayList.class.equals(new ArrayList<String>().getClass());

Class类是Java的反射入口,只有在获得了一个类的描述对象后才能动态的加载、调用,一般获得一个Class对象有三种途径:

类属性方式:如String.class
对象的getClass方法,如new String().getClass()
forName方法加载:如Class.forName(" java.lang.String")
  获得了Class对象后,就可以通过getAnnotations()获得注解,通过getMethods()获得方法,通过getConstructors()获得构造函数等,这位后续的反射代码铺平了道路。

建议102:适时选择getDeclaredXXX和getXXX

Java的Class类提供了很多的getDeclaredXXX方法和getXXX方法,例如getDeclaredMethod和getMethod成对出现,getDeclaredConstructors和getConstructors也是成对出现,那这两者之间有什么差别呢?看如下代码:

public class Test102 {

    public static void main(String[] args) throws NoSuchMethodException, SecurityException {
        // 方法名称
        String methodName = "doStuff";
        Method m1 = Foo.class.getDeclaredMethod(methodName);
        Method m2 = Foo.class.getMethod(methodName);
    }

    // 静态内部类
    static class Foo {
        void doStuff() {
        }
    }
}

此段代码运行后输出如下:

Exception in thread "main" java.lang.NoSuchMethodException: cn.icanci.test_151.Test102$Foo.doStuff()
    at java.lang.Class.getMethod(Unknown Source)
    at cn.icanci.test_151.Test102.main(Test102.java:20)

该异常是说m2变量的getMethod方法没有找到doStuff方法,明明有这个方法呀,为什么没有找到呢?这是因为getMethod方法获得的是所有public访问级别的方法,包括从父类继承的方法,而getDeclaredMethod获得的是自身类的方法,包括公用的(public)方法、私有(private)方法,而且不受限于访问权限。

其它的getDeclaredConstructors和getConstructors、getDeclaredFileds和getFields等于此相似。Java之所以如此处理,是因为反射本意只是正常代码逻辑的一种补充,而不是让正常代码逻辑发生翻天覆地的变化,所以public的属性和方法最容易获取,私有属性和方法也可以获取,但要限定本类。

那么问题来了:如果需要列出所有继承自父类的方法,该如何实现呢?简单,先获得父类,然后使用getDeclaredMethods,之后持续递归即可。

建议103:反射访问属性或方法时将Accessible设置为true

Java中通过反射执行一个方法的过程如下:获取一个方法对象,然后根据isAccessible返回值确定是否能够执行,如果返回值为false则需要调用setAccessible(true),最后再调用invoke执行方法,具体如下:

Method method= ...;
        //检查是否可以访问
        if(!method.isAccessible()){
            method.setAccessible(true);
        }
        //执行方法
        method.invoke(obj, args);

此段代码已经成了习惯用法:通过反射方法执行方法时,必须在invoke之前检查Accessible属性。这是一个好习惯,也确实该如此,但方法对象的Accessible属性并不是用来决定是否可以访问的,看如下代码:

public class Foo {
    public final void doStuff(){
        System.out.println("Do Stuff...");
    }
}

定义一个public类的public方法,这是一个没有任何限制的方法,按照我们对Java语言的理解,此时doStuff方法可以被任何一个类访问。我们编写一个客户端类来检查该方法是否可以反射执行:

public static void main(String[] args) throws NoSuchMethodException,
            SecurityException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException {
        // 反射获取方法
        Method m = Foo.class.getMethod("doStuff");
        // 打印是否可以访问
        System.out.println("Accessible:" + m.isAccessible());
        // 执行方法
        m.invoke(new Foo());
    }

很简单的反射操作,获得一个方法,然后检查是否可以访问,最后执行方法输出。让我们来猜想一下结果:因为Foo类是public的,方法也是public的,全部都是最开放的访问权限Accessible也应该等于true。但是运行结果却是:

Accessible:false
Do Stuff...

为什么Accessible属性会等于false?而且等于false还能执行?这是因为Accessible的属性并不是我们语法层级理解的访问权限,而是指是否更容易获得,是否进行安全检查。

我们知道,动态修改一个类或执行方法时都会受到Java安全体制的制约,而安全的处理是非常耗资源的(性能非常低),因此对于运行期要执行的方法或要修改的属性就提供了Accessible可选项:由开发者决定是否要逃避安全体系的检查。

阅读源代码是最好的理解方式,我们来看AccessibleObject类的源代码,它提供了取消默认访问控制检查的功能。首先查看isAccessible方法,代码如下:

public class AccessibleObject implements AnnotatedElement {
      //定义反射的默认操作权限suppressAccessChecks
      static final private java.security.Permission ACCESS_PERMISSION =
        new ReflectPermission("suppressAccessChecks");
      //是否重置了安全检查,默认为false
      boolean override;
      //构造函数
      protected AccessibleObject() {}
      //是否可以快速获取,默认是不能
      public boolean isAccessible() {
        return override;
    }
}

AccessibleObject是Filed、Method、Constructor的父类,决定其是否可以快速访问而不进行访问控制检查,在AccessibleObject类中是以override变量保存该值的,但是具体是否快速执行时在Method的invoke方法中决定的,源码如下:

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        //见擦汗是否可以快速获取,其值是父类AccessibleObject的override变量
        if (!override) {
          //不能快速获取,执行安全检查   
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass(1);

                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        //直接执行方法
        return ma.invoke(obj, args);
    }

看了这段代码,大家就清楚了:Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度的提升系统性能了(当然了,取消了安全检查,也可以运行private方法、访问private属性的)。经过测试,在大量的反射情况下,设置Accessible为true可以提高性能20倍左右。

AccessibleObject的其它两个子类Field和Constructor与Method的情形类似:Accessible属性决定Field和Constructor是否受访问控制检查。我们在设置Field或执行Constructor时,务必要设置Accessible为true,这并不仅仅是因为操作习惯的问题,还是为我们的系统性能考虑。

建议104:使用forName动态加载类文件

动态加载(Dynamic Loading)是指在程序运行时加载需要的类库文件,对Java程序来说,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时再决定是否需要加载一个类,比如从Web上接收一个String参数作为类名,然后在JVM中加载并初始化,这就是动态加载,此动态加载通常是通过Class.forName(String)实现的,只是这个forName方法到底是什么意思呢?

我们知道一个类文件只有在被加载到内存中才可能生成实例对象,也就是说一个对象的生成必然会经过两个步骤:

加载到内存中生成Class的实例对象
通过new关键字生成实例对象
  如果我们使用的是import关键字产生的依赖包,JVM在启动时会自动加载所有的依赖包的类文件,这没有什么问题,如果好动态加载类文件,就要使用forName的方法了,但问题是我们为什么要使用forName方法动态加载一个类文件呢?那是因为我们不知道生成的实例对象是什么类型(如果知道就不用动态加载),而且方法和属性都不可访问呀。问题又来了:动态加载的意义在什么地方呢?

意义在于:加载一个类即表示要初始化该类的static变量,特别是static代码块,在这里我们可以做大量的工作,比如注册自己,初始化环境等,这才是我们要重点关注的逻辑,例如如下代码:

package com.study.advice103;
public class Client103 {
    public static void main(String[] args) throws ClassNotFoundException {
        //动态加载
        Class.forName("com.study.advice103.Utils");
    }
}
class Utils{
    //静态代码块
    static{
        System.out.println("Do Something.....");
    }
}

注意看Client103类,我们并没有对Utils做任何初始化,只是通过forName方法加载了Utils类,但是却产生了一个“Do Something.....”的输出,这就是因为Utils类加载后,JVM会自动初始化其static变量和static静态代码块,这是类加载机制所决定的。

对于动态加载,最经典的应用是数据库驱动程序的加载片段,代码如下:

  //加载驱动
        Class.forName("com.mysql..jdbc.Driver");
        String url="jdbc:mysql://localhost:3306/db?user=&password=";
        Connection conn =DriverManager.getConnection(url);
        Statement stmt =conn.createStatement();

在没有Hibernate和Ibatis等ORM框架的情况下,基本上每个系统都会有这么一个JDBC链接类,然后提供诸如Query、Delete等的方法,大家有没有想过为什么要加上forName这句话呢?没有任何的输出呀,要它干什么用呢?事实上非常有用,我们看一下Driver的源码:

public class Driver extends NonRegisteringDriver
    implements java.sql.Driver
{
  //构造函数
    public Driver()
        throws SQLException
    {
    }
   //静态代码块
    static 
    {
        try
        {
           //把自己注册到DriverManager中
            DriverManager.registerDriver(new Driver());
        }
        catch(SQLException E)
        {
           //异常处理
            throw new RuntimeException("Can't register driver!");
        }
    }
}

该程序的逻辑是这样的:数据库驱动程序已经由NonRegisteringDriver实现了,Driver类只是负责把自己注册到DriverManager中。当程序动态加载该驱动时,也就是执行到Class.forName("com.mysql..jdbc.Driver")时,Driver类会被加载到内存中,于是static代码块开始执行,也就是把自己注册到DriverManager中。

需要说明的是,forName只是把一个类加载到内存中,并不保证由此产生一个实例对象,也不会执行任何方法,之所以会初始化static代码,那是由类加载机制所决定的,而不是forName方法决定的。也就是说,如果没有static属性或static代码块,forName就是加载类,没有任何的执行行为。

注意:forName只是加载类,并不执行任何代码。

建议105:动态加载不适合数组

上一个建议解释了为什么要用forName,本建议就来说说那些地方不适合动态加载。如果forName要加载一个类,那它首先必须是一个类___8个基本类型排除在外,它们不是一个具体的类;其次,它必须具有可追溯的类路径,否则就会报ClassNotFoundException。

在Java中,数组是一个非常特殊的类,虽然它是一个类,但没有定义类类路径,例如这样的代码:

public static void main(String[] args) throws ClassNotFoundException {
        String [] strs =  new String[10];
        Class.forName("java.lang.String[]");
    }

String []是一个类型声明,它作为forName的参数应该也是可行的吧!但是非常遗憾,其运行结果如下:

Exception in thread "main" java.lang.ClassNotFoundException: java/lang/String[]
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:186)

产生ClassNotFoundException异常的原因是数组算是一个类,在声明时可以定义为String[],但编译器编译后为不同的数组类型生成不同的类,具体如下表所示:

数组编译对应关系表 ~~~
元素类型 编译之后的类型
byte[] [B
char[] [C
Double[] [D
Float[] [F
Int[] [I
Long[] [J
Short[] [S
Boolean[] [Z
引用类型(如String[]) [L引用类型(如:[Ljava.lang.String;)

在编码期,我们可以声明一个变量为String[],但是经过编译后就成为了[Ljava.lang.String。明白了这一点,再根据以上的表格可知,动态加载一个对象数组只要加载编译后的数组对象就可以了,代码如下:

 //加载一个数组
        Class.forName("[Ljava.lang.String;");
        //加载一个Long数组
        Class.forName("[J");

虽然以上代码可以加载一个数组类,但这是没有任何意义的,因为它不能产生一个数组对象,也就是说以上代码只是把一个String类型的数组类和Long类型的数组类加载到了内存中(如果内存中没有改类的话),并不能通过newInstance方法生成一个实例对象,因为它没有定义数组的长度,在Java中数组是定长的,没有长度的数组是不允许存在的。

既然反射不能定义一个数组,那问题就来了:如何动态加载一个数组呢?比如依据输入动态生成一个数组。其实可以使用Array数组反射类动态加载,代码如下:

  // 动态创建数组
        String[] strs = (String[]) Array.newInstance(String.class, 8);
        // 创建一个多维数组
        int[][] ints = (int[][]) Array.newInstance(int.class, 2, 3);

因为数组比较特殊,要想动态创建和访问数组,基本的反射是无法实现的,“上帝对你关闭一扇门,同时会为你打开一扇窗。”,于是Java就专门定义了一个Array数组反射工具类来实现动态探知数组的功能。

注意:通过反射操作数组使用Array类,不要采用通用的反射处理API。

建议106:动态代理可以使代理模式更加灵活

Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发。我们知道一个静态代理是通过主题角色(Proxy)和具体主题角色(Real Subject)共同实现主题角色(Subject)的逻辑的,只是代理角色把相关的执行逻辑委托给了具体角色而已,一个简单的静态代理如下所示:

interface Subject {
    // 定义一个方法
    public void request();
}

// 具体主题角色
class RealSubject implements Subject {
    // 实现方法
    @Override
    public void request() {
        // 实现具体业务逻辑
    }

}

class Proxy implements Subject {
    // 要代理那个实现类
    private Subject subject = null;

    // 默认被代理者
    public Proxy() {
        subject = new RealSubject();
    }

    // 通过构造函数传递被代理者
    public Proxy(Subject _subject) {
        subject = _subject;
    }

    @Override
    public void request() {
        before();
        subject.request();
        after();
    }

    // 预处理
    private void after() {
        // doSomething
    }

    // 善后处理
    private void before() {
        // doSomething
    }
}

这是一个简单的静态代理。Java还提供了java.lang.reflect.Proxy用于实现动态代理:只要提供一个抽象主题角色和具体主题角色,就可以动态实现其逻辑的,其实例代码如下:

interface Subject {
    // 定义一个方法
    public void request();
}

// 具体主题角色
class RealSubject implements Subject {
    // 实现方法
    @Override
    public void request() {
        // 实现具体业务逻辑
    }

}

class SubjectHandler implements InvocationHandler {
    // 被代理的对象
    private Subject subject;

    public SubjectHandler(Subject _subject) {
        subject = _subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // 预处理
        System.out.println("预处理...");
        //直接调用被代理的方法
        Object obj = method.invoke(subject, args);
        // 后处理
        System.out.println("后处理...");
        return obj;
    }

}

注意这里没有代理主题角色,取而代之的是SubjectHandler 作为主要的逻辑委托处理,其中invoke方法是接口InvocationHandler定义必须实现的,它完成了对真实方法的调用。

我们来详细解释一下InvocationHandler接口,动态代理是根据被代理的接口生成的所有方法的,也就是说给定一个或多个接口,动态代理会宣称“我已经实现该接口下的所有方法了”,那大家想想看,动态代理是怎么才能实现接口中的方法呢?在默认情况下所有方法的返回值都是空的,是的,虽然代理已经实现了它,但是没有任何的逻辑含义,那怎么办?好办,通过InvocationHandler接口的实现类来实现,所有的方法都是由该Handler进行处理的,即所有被代理的方法都由InvocationHandler接管实际的处理任务。

我们开看看动态代理的场景,代码如下:

public static void main(String[] args) {
        //具体主题角色,也就是被代理类
        Subject subject = new RealSubject();
        //代理实例的处理Handler
        InvocationHandler handler =new SubjectHandler(subject);
        //当前加载器
        ClassLoader cl = subject.getClass().getClassLoader();
        //动态代理
        Subject proxy = (Subject) Proxy.newProxyInstance(cl,subject.getClass().getInterfaces(),handler);
        //执行具体主题角色方法
        proxy.request();
    }

此时就实现了,不用显式创建代理类即实现代理的功能,例如可以在被代理的角色执行前进行权限判断,或者执行后进行数据校验。

动态代理很容易实现通用的代理类,只要在InvocationHandler的invoke方法中读取持久化的数据即可实现,而且还能实现动态切入的效果,这也是AOP(Aspect Oriented Programming)变成理念。

建议107:使用反射增加装饰模式的普适性

装饰模式(Decorator Pattern)的定义是“动态的给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比于生成子类更为灵活”,不过,使用Java的动态代理也可以实现装饰模式的效果,而且其灵活性、适应性都会更强。

我们以卡通片《猫和老鼠》(Tom and Jerry)为例,看看如何包装小Jerry让它更强大。首先定义Jerry的类:老鼠(Rat类),代码如下:

interface Animal{
    public void doStuff();
}

class Rat implements Animal{
    @Override
    public void doStuff() {
        System.out.println("Jerry will play with Tom ......");
    }
    
}

接下来,我们要给Jerry增加一些能力,比如飞行,钻地等能力,当然使用继承也很容易实现,但我们这里只是临时的为Rat类增加这些能力,使用装饰模式更符合此处的场景,首先定义装饰类,代码如下:

//定义某种能力
interface Feature{
    //加载特性
    public void load();
}
//飞行能力
class FlyFeature implements Feature{

    @Override
    public void load() {
        System.out.println("增加一对翅膀...");
    }
}
//钻地能力
class DigFeature implements Feature{
    @Override
    public void load() {
        System.out.println("增加钻地能力...");
    }
    
}

此处定义了两种能力:一种是飞行,另一种是钻地,我们如果把这两种属性赋予到Jerry身上,那就需要一个包装动作类了,代码如下:

class DecorateAnimal implements Animal {
    // 被包装的动物
    private Animal animal;
    // 使用哪一个包装器
    private Class<? extends Feature> clz;

    public DecorateAnimal(Animal _animal, Class<? extends Feature> _clz) {
        animal = _animal;
        clz = _clz;
    }

    @Override
    public void doStuff() {
        InvocationHandler handler = new InvocationHandler() {
            // 具体包装行为
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {
                Object obj = null;
                if (Modifier.isPublic(method.getModifiers())) {
                    obj = method.invoke(clz.newInstance(), args);
                }
                animal.doStuff();
                return obj;
            }
        };
        //当前加载器
        ClassLoader cl = getClass().getClassLoader();
        //动态代理,又handler决定如何包装
        Feature proxy = (Feature) Proxy.newProxyInstance(cl, clz.getInterfaces(), handler);
        proxy.load();
    }

}

注意看doStuff方法,一个装饰类型必然是抽象构建(Component)的子类型,它必须实现doStuff方法,此处的doStuff方法委托给了动态代理执行,并且在动态代理的控制器Handler中还设置了决定装饰方式和行为的条件(即代码中InvocationHandler匿名类中的if判断语句),当然,此处也可以通过读取持久化数据的方式进行判断,这样就更加灵活了。

抽象构建有了,装饰类也有了,装饰动作类也完成了,那我们就可以编写客户端进行调用了,代码如下:

public static void main(String[] args) {
        //定义Jerry这只老鼠
        Animal jerry = new Rat();
        //为Jerry增加飞行能力
        jerry = new DecorateAnimal(jerry, FlyFeature.class);
        //jerry增加挖掘能力
        jerry = new DecorateAnimal(jerry, DigFeature.class);
        //Jerry开始戏弄毛了
        jerry.doStuff();
    }

此类代码只一个比较通用的装饰模式,只需要定义被装饰的类及装饰类即可,装饰行为由动态代理实现,实现了对装饰类和被装饰类的完全解耦,提供了系统的扩展性。

建议108:反射让模板方法模式更强大

模板方法模式(Template Method Pattern)的定义是:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。简单的说,就是父类定义抽象模板作为骨架,其中包括基本方法(是由子类实现的方法,并且在模板方法中被调用)和模板方法(实现对基本方法的调度,完成固定的逻辑),它是用了简单的继承和覆写机制,我么来看一个基本的例子。

我们经常会开发一些测试或演示程序,期望系统在启动时自动初始化,以方便测试或讲解,一般的做法是写一个SQL文件,在系统启动前自动导入,不过,这样不仅麻烦而且容易出错,于是我们就手写了一个自动初始化数据的框架:在系统(或容器)自动启动时自行初始化数据。但问题是每个应用程序要初始化的内容我们并不知道,只能由实现者自行编写,那我们就必须给作者预留接口,此时就得考虑使用模板方法模式了,代码如下:

public abstract class AbsPopulator {
    // 模板方法
    public final void dataInitialing() throws Exception {
        // 调用基本方法
        doInit();
    }

    // 基本方法
    protected abstract void doInit();
}

这里定义了一个抽象模板类AbsPopulator,它负责数据初始化,但是具体要初始化哪些数据则是由doInit方法决定的,这是一个抽象方法,子类必须实现,我们来看一个用户表数据的加载:

public class UserPopulator extends AbsPopulator{
    @Override
    protected void doInit() {
        //初始化用户表,如创建、加载数据等
    }

}

该系统在启动时查找所有的AbsPopulator实现类,然后dataInitialing实现数据的初始化。那大家可能要想了,怎么让容器指导这个AbsPopulator类呢?很简单,如果是使用Spring作为Ioc容器的项目,直接在dataInitialing方法上加上@PostConstruct注解,Spring容器启动完毕后自动运行dataInitialing方法。具体大家看spring的相关知识,这里不再赘述。

现在问题是:初始化一张User表需要非常多的操作,比如先建表,然后筛选数据,之后插入,最后校验,如果把这些都放入到一个doInit方法里会非常庞大(即使提炼出多个方法承担不同的责任,代码的可读性依然很差),那该如何做呢?又或者doInit是没有任何的也无意义的,是否可以起一个优雅而又动听的名字呢?

答案是我们可以使用反射增强模板方法模式,使模板方法实现对一批固定的规则的基本方法的调用。代码是最好的交流语言,我们看看怎么改造AbsPopulator类,代码如下:

public abstract class AbsPopulator {
    // 模板方法
    public final void dataInitialing() throws Exception {
        // 获得所有的public方法
        Method[] methods = getClass().getMethods();
        for (Method m : methods) {
            // 判断是否是数据初始化方法
            if (isInitDataMethod(m)) {
                m.invoke(this);
            }
        }
    }

    // 判断是否是数据初始化方法,基本方法鉴定器
    private boolean isInitDataMethod(Method m) {
        return m.getName().startsWith("init")// init开始
                && Modifier.isPublic(m.getModifiers())// 公开方法
                && m.getReturnType().equals(Void.TYPE)// 返回值是void
                && !m.isVarArgs()// 输出参数为空
                && !Modifier.isAbstract(m.getModifiers());// 不能是抽象方法
    }
}

在一般的模板方法模式中,抽象模板(这里是AbsPopulator类)需要定义一系列的基本方法,一般都是protected访问级别的,并且是抽象方法,这标志着子类必须实现这些基本方法,这对子类来说既是一个约束也是一个负担。但是使用了反射后,不需要定义任何抽象方法,只需要定义一个基本方法鉴定器(例子中的isInitDataMethod)即可加载符合规则的基本方法。鉴别器在此处的作用是鉴别子类方法中哪些是基本方法,模板方法(例子中的dataInitaling)则需要基本方法鉴定器返回的结果通过反射执行相应的方法。

此时,如果需要进行大量的初始化工作,子类的实现就非常简单了,代码如下:

public class UserPopulator extends AbsPopulator {

    public void initUser() {
        /* 初始化用户表,如创建、加载数据等 */
    }

    public void initPassword() {
        /* 初始化密码 */
    }

    public void initJobs() {
        /* 初始化工作任务 */
    }
}

UserPopulator类中的方法只要符合基本方法鉴别器条件即会被模板方法调用,方法的数据量也不再受父类的约束,实现了子类灵活定义基本方法、父类批量调用的功能,并且缩减了子类的代码量。

如果大家熟悉JUnit的话,就会看出此处的实现与JUnit非常相似,JUnit4之前要求测试的方法名必须是以test开头的,并且无返回值、无参数,而且是public修饰,其实现的原理与此非常类似,大家有兴趣可以看看Junit的源码。

建议109:不需要太多关注反射效率

反射的效率是一个老生常谈的问题,有"经验" 的开发人员经常会使用这句话恐吓新人:反射的效率是非常低的,不到万不得已就不要使用。事实上,这句话前半句是对的,后半句是错的。

反射的效率相对于正常的代码执行确实低很多,但它是一个非常有效的运行期工具类,只要代码结构清晰、可读性好那就先开发起来,等到进行性能测试时证明此处性能确实有问题再修改也不迟(一般情况下,反射并不是性能的终极杀手,而代码结构混乱、可读性差则可能会埋下性能隐患)。我们看这样一个例子,在运行期获得泛型类的泛型,代码如下:

class Utils {
    // 获得一个泛型类的实际泛型类型
    public static <T> Class<T> getGenricClassType(Class clz) {
        Type type = clz.getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) type;
            Type[] types = pt.getActualTypeArguments();
            if (types.length > 0 && types[0] instanceof Class) {
                // 若有多个泛型参数,依据位置索引返回
                return (Class<T>) types[0];
            }
        }
        return (Class<T>) Object.class;
    }
}

前面我们讲过,Java泛型只存在于编译器,那为什么这个工具类可以取得运行期的泛型类型呢?那是因为该工具只支持继承的泛型类,如果是在Java编译时已经确定了泛型类的类型参数,那当然可以通过泛型类获得了。例如有这样一个泛型类:

abstract class BaseDao<T>{
    //获得T运行期的类型
    private Class<T> clz = Utils.getGenricClassType(getClass());
    //根据主键获得一条记录
    public void get(long id){
        session.get(clz,id);
    }
}
//操作user表
class UserDao extends BaseDao<String>{
    
}

对于UserDao类,编译器编译时已经明确了其参数类型是String,因此可以通过反射的方式来获取其类型,这也是getGenricClassType方法使用的场景。

BaseDao和UserDao是ORM中的常客,BaseDao实现对数据库的基本操作,比如增删改查,而UserDao则是一个比较具体的数据库操作,其作用是对User表进行操作,如果BaseDao能够提供足够多的基本方法,比如单表的增删改查,哪些与UserDao类似的BaseDao子类就可以省却大量的开发工作。但问题是持久层的session对象(这里模拟的是Hibernate  Session)需要明确一个具体的类型才能操作,比如get查询,需要获得两个参数:实体类类型(用于确定映射的数据表)和主键,主键好办,问题是实体类类型怎么获得呢?

子类进行传递?麻烦,而且也容易产生错误。

读取配置问题?可行,但效率不高。

最好的办法就是父类泛型化,子类明确泛型参数,然后通过反射读取相应的类型即可,于是就有了我们代码中clz变量:通过反射获得泛型类型。如此实现后,UserDao可不用定义任何方法,继承过来的父类操作方法已经满足基本需求了,这样的代码结构清晰,可读性又好。

想想看,如果考虑反射效率问题,没有clz变量,不使用反射,每个BaseDao的子类都要实现一个查询操作,代码将会大量重复,违反了" Don't Repeat Yourself " 这条最基本的编码规则,这会致使项目重构、优化难度加大,代码的复杂度也会提高很多。

对于反射效率的问题,不要做任何的提前优化和预期,这基本上是杞人忧天,很少有项目是因为反射问题引起系统效率故障的(除非是拷贝的垃圾代码),而且根据二八原则,80%的性能消耗在20%的代码上,这20%的代码才是我们关注的重点,不要单单把反射作为重点关注对象。

注意:反射效率低是个真命题,但因为这一点而不使用它就是个假命题。

第八章 异常

不管人类的思维有多么缜密,也存在" 智者千虑必有一失 "的缺憾。无论计算机技术怎么发展,也不可能穷尽所有的场景___这个世界是不完美的,也是有缺陷的。完美的世界只存在于理想中。

对于软件帝国的缔造者来说,程序也是不完美的,异常情况会随时出现,我们需要它为我们描述例外事件,需要它处理非预期的情景,需要它帮我们建立“完美世界”。

建议110:提倡异常封装

Java语言的异常处理机制可以去确保程序的健壮性,提高系统的可用率,但是Java API提供的异常都是比较低级的(这里的低级是指 " 低级别的 " 异常),只有开发人员才能看的懂,才明白发生了什么问题。而对于终端用户来说,这些异常基本上就是天书,与业务无关,是纯计算机语言的描述,那该怎么办?这就需要我们对异常进行封装了。异常封装有三方面的优点:

(1)、提高系统的友好性

例如,打开一个文件,如果文件不存在,则回报FileNotFoundException异常,如果该方法的编写者不做任何处理,直接抛到上层,则会降低系统的友好性,代码如下所示:

public static void doStuff() throws FileNotFoundException {
        InputStream is = new FileInputStream("无效文件.txt");
        /* 文件操作 */
    }

此时doStuff的友好性极差,出现异常时(如果文件不存在),该方法直接把FileNotFoundException异常抛到上层应用中(或者是最终用户),而上层应用(或用户要么自己处理),要么接着抛,最终的结果就是让用户面对着" 天书 " 式的文字发呆,用户不知道这是什么问题,只是知道系统告诉他" 哦,我出错了,什么错误?你自己看着办吧 "。

解决办法就是封装异常,可以把异常的阅读者分为两类:开发人员和用户。开发人员查找问题,需要打印出堆栈信息,而用户则需要了解具体的业务原因,比如文件太大、不能同时编写文件等,代码如下:

public static void doStuff2() throws MyBussinessException{
        try {
            InputStream is = new FileInputStream("无效文件.txt");
        } catch (FileNotFoundException e) {
            //方便开发人员和维护人员而设置的异常信息
            e.printStackTrace();
            //抛出业务异常
            throw new MyBussinessException();
        }
        /* 文件操作 */
    }

(2)、提高系统的可维护性

看如下代码:

public  void doStuff3(){
        try{
            //doSomething
        }catch(Exception e){
            e.printStackTrace();
        }
        
    }

这是大家很容易犯的错误,抛出异常是吧?分类处理多麻烦,就写一个catch块来处理所有的异常吧,而且还信誓旦旦的说" JVM会打印出栈中的错误信息 ",虽然这没错,但是该信息只有开发人员自己看的懂,维护人员看到这段异常时基本上无法处理,因为需要到代码逻辑中去分析问题。

正确的做法是对异常进行分类处理,并进行封装输出,代码如下:

public  void doStuff4(){
        try{
            //doSomething
        }catch(FileNotFoundException e){
            log.info("文件未找到,使用默认配置文件....");
            e.printStackTrace();
        }catch(SecurityException e1){
            log.info(" 无权访问,可能原因是......");
            e1.printStackTrace();
        }
    }

如此包装后,维护人员看到这样的异常就有了初步的判断,或者检查配置,或者初始化环境,不需要直接到代码层级去分析了。

(3)、解决Java异常机制自身的缺陷

Java中的异常一次只能抛出一个,比如doStuff方法有两个逻辑代码片段,如果在第一个逻辑片段中抛出异常,则第二个逻辑片段就不再执行了,也就无法抛出第二个异常了,现在的问题是:如何才能一次抛出两个(或多个)异常呢?

其实,使用自行封装的异常可以解决该问题,代码如下:

class MyException extends Exception {
    // 容纳所有的异常
    private List<Throwable> causes = new ArrayList<Throwable>();

    // 构造函数,传递一个异常列表
    public MyException(List<? extends Throwable> _causes) {
        causes.addAll(_causes);
    }

    // 读取所有的异常
    public List<Throwable> getExceptions() {
        return causes;
    }
}

MyException异常只是一个异常容器,可以容纳多个异常,但它本身并不代表任何异常含义,它所解决的是一次抛出多个异常的问题,具体调用如下:

public void doStuff() throws MyException {
        List<Throwable> list = new ArrayList<Throwable>();
        // 第一个逻辑片段
        try {
            // Do Something
        } catch (Exception e) {
            list.add(e);
        }
        // 第二个逻辑片段
        try {
            // Do Something
        } catch (Exception e) {
            list.add(e);
        }
        // 检查是否有必要抛出异常
        if (list.size() > 0) {
            throw new MyException(list);
        }
    }

这样一来,DoStuff方法的调用者就可以一次获得多个异常了,也能够为用户提供完整的例外情况说明。可能有人会问:这种情况会出现吗?怎么回要求一个方法抛出多个异常呢?

绝对有可能出现,例如Web界面注册时,展现层依次把User对象传递到逻辑层,Register方法需要对各个Field进行校验并注册,例如用户名不能重复,密码必须符合密码策略等,不要出现用户第一次提交时系统显示" 用户名重复 ",在用户修改用户名再次提交后,系统又提示" 密码长度小于6位 " 的情况,这种操作模式下的用户体验非常糟糕,最好的解决办法就是异常封装,建立异常容器,一次性地对User对象进行校验,然后返回所有的异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容