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

咱们最大的弱点在于放弃。成功的必然之路就是不断的重来一次。 --达尔文java

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

建议94:不能初始化泛型参数和数组安全

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

建议96:不一样的场景使用不一样的泛型通配符工具

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

建议98:list中泛型顺序为T、?、Objectui

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

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

泛型能够减小将至类型转换,能够规范集合的元素类型,还能够提升代码的安全性和可读性,优先使用泛型。.net

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

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

一、Java泛型的引入增强了参数类型的安全性,减小了类型的转换,Java的泛型在编译器有效,在运行期被删除,也就是说全部的泛型参数类型在编译后会被清除掉,咱们来看一个例子,代码以下:

两个同样的方法冲突了?

这就是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之因此如此处理,有两个缘由:

① 避免JVM的运行负担。

若是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());
    System.out.println(list.getClass()==list2.getClass());
}

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

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

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

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

③ instanceof不容许存在泛型参数

如下代码不能经过编译,缘由同样,泛型类型被擦除了:

建议94:不能初始化泛型参数和数组

泛型类型在编译期被擦除,咱们在类初始化时将没法得到泛型的具体参数,好比这样的代码:

这段代码是编译不过的,由于编译时须要得到T类型,但泛型在编译期类型已经被擦除了。在某些状况下,咱们须要泛型数组,那该如何处理呢?代码以下:

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

    // 构造函数初始化
    public Student() {
        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);
}

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

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

二、变量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类型的集合了。

三、变量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呢?

一、泛型结构只参与 “读” 操做则限定上界(extends关键字),也就是要界定泛型的上界

编译失败,失败的缘由是list中的元素类型不肯定,也就是编译器没法推断出泛型类型究竟是什么,是Integer类型?是Double?仍是Byte?这些都符合extends关键字的定义,因为没法肯定实际的泛型类型,因此编译器拒绝了此类操做。

二、泛型结构只参与“写” 操做则限定下界(使用super关键字),也就是要界定泛型的下界

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

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

协变:窄类型替换宽类型

逆变:宽类型替换窄类型

一、泛型不支持协变,编译不经过,,,窄类型变成宽类型(Integer>>Number)

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

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

二、泛型不支持逆变

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

建议98:list中泛型顺序为T、?、Object

List<T>、List<?>、List<Object>这三者均可以容纳全部的对象,但使用的顺序应该是首选List<T>,次之List<?>,最后选择List<Object>,缘由以下:

一、List<T>是肯定的某一类型

List<T>表示的是list集合中的元素是T类型,具体类型在运行期决定;

List<?>表示的是任意类型,与List<T>类型,而List<Object>表示list集合中的全部元素为Object类型,从字面意义上来分析,List<T>更符合习惯,编译者知道它是某一个类型,只是在运行期肯定而已。

二、List<T>能够进行读写操做,不能进行增长修改操做,由于编译器不知道list中容纳的是什么类型的元素,也就没法校验类型是否安全。

而List<?>读取出的元素都是Object类型的,须要主动转型,因此它常常用于泛型方法的返回值。注意List<?>虽然没法增长,修改元素,可是却能够删除元素,好比执行remove、clear等方法,那是由于它的删除动做与泛型类型无关。

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

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

在Java的泛型中,可使用&符号关联多个上界(extends)并实现多个边界限定,下界(super)没有多重限定的状况。

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

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

package OSChina.Genericity;

import java.util.Arrays;
import java.util.List;

public class GenericFruit {

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

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

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

数组是一个容器,只有确保容器内的全部元素类型与指望的类型有父子关系时才能转换,Object数组只能保证数组内的元素时Object类型,却不能确保它们都是String的父类型或子类,因此类型转换失败。

总而言之,就是数组使用具体类型使用就完了。

 

编写高质量代码:改善Java程序的151个建议@目录

相关文章
相关标签/搜索