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

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

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

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

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

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

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

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

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

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

泛型可以减少将至类型转换,可以规范集合的元素类型,还可以提高代码的安全性和可读性,优先使用泛型。

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

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

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

08fa99020f1b1ba1a9ed0849922e18ef518.jpg

两个一样的方法冲突了?

这就是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>.

2、明白了这些规则,再看如下代码:

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

3、Java之所以如此处理,有两个原因:

① 避免JVM的运行负担。

如果JVM把泛型类型延续到运行期,那么JVM就需要进行大量的重构工作了。

② 版本兼容

在编译期擦除可以更好的支持原生类型(Raw Type),在Java1.5或1.6...平台上,即使声明一个List这样的原生类型也是可以正常编译通过的,只是会产生警告信息而已。

4、明白了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,没有任何区别。

fb8c56de350c67a4c03f60d6138be7335f6.jpg

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

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

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

③ instanceof不允许存在泛型参数

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

ef91cee46d51d94d8943de63ad35475f22c.jpg

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

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

35fbaca8183217439a200e784398f936f7d.jpg

这段代码是编译不过的,因为编译时需要获得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);
}

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

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关键字),也就是要界定泛型的上界

4f4936442820b56ab6f265d817544b23640.jpg

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

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

9ffae4b7a80913153254716824d09910929.jpg

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

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

协变:窄类型替换宽类型

逆变:宽类型替换窄类型

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

075d9af7c61dcb45bce74b71e0b24c8b220.jpg

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

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

2、泛型不支持逆变

b619261d69f32f5d1ac6848311de6ffa15b.jpg

457750b51139fd66a291215a776554264b4.jpg

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

建议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类型,从字面意义上来分析,List<T>更符合习惯,编译者知道它是某一个类型,只是在运行期确定而已。

2、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);
        }
    }
}

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

d838dff2cb3408ac76d3e6fb41b00cc32df.jpg

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

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

 

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

转载于:https://my.oschina.net/u/4006148/blog/3079463

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值