编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议93~97)

  泛型能够减小强制类型的转换,能够规范集合的元素类型,还能够提升代码的安全性和可读性,正式由于有这些优势,自从Java引入泛型后,项目的编码规则上便多了一条:优先使用泛型。java

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

建议93:Java的泛型是能够擦除的

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

 1 public class Foo {
 2     //arrayMethod接收数组参数,并进行重载
 3     public void arrayMethod(String[] intArray) {
 4 
 5     }
 6 
 7     public void arrayMethod(Integer[] intArray) {
 8 
 9     }
10     //listMethod接收泛型List参数,并进行重载
11     public void listMethod(List<String> stringList) {
12 
13     }
14     public void listMethod(List<Integer> intList) {
15         
16     }
17 }

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

  事实上,这段程序时没法编译的,编译时报错信息以下:框架

  

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

  • 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);
    }

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

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泛型是类型擦除的,咱们就能够解释相似以下的问题了:工具

  1. 泛型的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的源代码就清楚了,代码以下: 

 1 public class ArrayList<E> extends AbstractList<E> implements List<E>,
 2         RandomAccess, Cloneable, java.io.Serializable {
 3     // 容纳元素的数组
 4     private transient Object[] elementData;
 5 
 6     // 构造函数
 7     public ArrayList() {
 8         this(10);
 9     }
10 
11     // 得到一个元素
12     public E get(int index) {
13         rangeCheck(index);
14         // 返回前强制类型转换
15         return elementData(index);
16     }
17     /* 其它代码略 */
18 
19 }

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

  在某些状况下,咱们须要泛型数组,那该如何处理呢?代码以下:

 1 class Test<T> {
 2     // 再也不初始化,由构造函数初始化
 3     private T t;
 4     private T[] tArray;
 5     private List<T> list = new ArrayList<T>();
 6 
 7     // 构造函数初始化
 8     public Test() {
 9         try {
10             Class<?> tType = Class.forName("");
11             t = (T) tType.newInstance();
12             tArray = (T[]) Array.newInstance(tType, 5);
13         } catch (Exception e) {
14             e.printStackTrace();
15         }
16     }
17 }

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

  类的成员变量是在类初始化前初始化的,因此要求在初始化前它必须具备明确的类型,不然就只能声明,不能初始化。

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

  Arrays工具类有一个方法asList能够把一个变长参数或数组转变为列表,可是它有一个缺点:它所生成的list长度是不可变的,而这在咱们的项目开发中有时会很不方便。若是你指望生成的列表长度可变,那就须要本身来写一个数组的工具类了,代码以下:

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

  这很简单,与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方法是一个很是好的例子,它实现了把源列表的全部元素拷贝到目标列表中对应的索引位置上,代码以下:

 1     public static <T> void copy(List<? super T> dest, List<? extends T> src) {
 2         int srcSize = src.size();
 3         if (srcSize > dest.size())
 4             throw new IndexOutOfBoundsException("Source does not fit in dest");
 5 
 6         if (srcSize < COPY_THRESHOLD ||
 7             (src instanceof RandomAccess && dest instanceof RandomAccess)) {
 8             for (int i=0; i<srcSize; i++)
 9                 dest.set(i, src.get(i));
10         } else {
11             ListIterator<? super T> di=dest.listIterator();
12             ListIterator<? extends T> si=src.listIterator();
13             for (int i=0; i<srcSize; i++) {
14                 di.next();
15                 di.set(si.next());
16             }
17         }
18     }

  源列表是用来提供数据的,因此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的泛型是不支持协变和逆变的,只是可以实现逆变和协变
相关文章
相关标签/搜索