写点什么

Java 程序经验小结:编程更好的使用泛型以替代原生态类型

发布于: 2021 年 01 月 23 日
Java 程序经验小结:编程更好的使用泛型以替代原生态类型

1、写在开头

Java1.5 发行版增加了泛型(Generic)。

泛型出现前,集合读取的每个对象都必须进行转换,如果不小心插入类型错误对的对象,运行时的转换处理会报错。

泛型出现后,我们通过泛型可以告诉编译器每个集合可以接受哪些对象类型,让编译器自动为集合的元素插入进行转化,并且在编译时告知我们是否插入了类型错误的对象。

2、一些泛型的专业术语

  1. 泛型类或泛型接口声明中具有一个或多个类型参数(type parameter)的类或者接口,统称为泛型。eg,jdk1.5 之后,List 接口只有单个类型参数 E,表示列表的元素类型,所以他的接口名称应该是 List<E>,但是人们常常把它简称为 List。

  2. 参数化的类型(parameterized type),构成格式是:类或接口的名称 + 尖括号(<>)将泛型形式参数的实际类型参数列表括起来。

  3. 每个泛型都定义类一个 原生态类型(raw type),即不带任何实际类型参数的泛型名称。eg,List<E> 对应的原生态类型是 List。原生态类型就相当于从类型声明中删除了泛型信息。

3、泛型助于在编译期及早发现错误

使用泛型进行编码,有两个好处:

  • 优点 1:让编写代码时在编译期及早发现错误,并且助于定位报错位置

  • 优点 2:集合使用泛型,从集合中遍历元素时不需要再进行手工转换了(编译器替我们完成隐式转换,并确保过程不会失败,无论我们使用的是否 for-each 循环)


下面我们通过一个例子阐述清楚,代码如下:


/**   * @exception ClassCastException   */  private static void testGenericeBeforejdk5() {    Collection stamps = new ArrayList();    stamps.add(new Coin());    for (Iterator i = stamps.iterator(); i.hasNext();){      Stamp next = (Stamp)i.next(); //ClassCastException    }  }  private static void testGenericeAfterjdk5() {    Collection<Stamp> stamps = new ArrayList();    stamps.add(new Coin());//编译器告诉我们错误  }    // 两个测试内部类  static class Stamp{}  static class Coin{}
复制代码


  • testGenericeBeforejdk5()方法里,我们希望 stamps 集合只会存放 Stamp 类元素,但是编码时还是不小心把一个 coin 放进了这个集合。那么程序是不会在编译时告诉程序员这个问题的,而是等到代码真正运行时,出现了异常。

运行异常:

 	Exception in thread "main" java.lang.ClassCastException:   effectivejava.no23.TestGeneric$Coin cannot be cast to effectivejava.no23.TestGeneric$Stamp    at effectivejava.no23.TestGeneric.testGenericeBeforejdk5(TestGeneric.java:26)    at effectivejava.no23.TestGeneric.main(TestGeneric.java:14)
复制代码


  • testGenericeAfterjdk5()方法里,我们使用了泛型定义了集合的参数类型。通过这条声明,编译器知道 stamps 集合应该只包含 Stamp 实例,并给以保证。因此在代码开发时,我们不小心将一个 coin 实例放进 stamps 集合时,编译器会及时提醒我们并产生一条编译错误信息,准确告知程序员哪里出现错误。

编译报错:


Error:(20, 28) java: 不兼容的类型: effectivejava.no23.TestGeneric.Coin无法转换为effectivejava.no23.TestGeneric.Stamp
复制代码


通过比较,我们还能发现,集合使用泛型,从集合中遍历元素时不需要再进行手工转换了。


4 、原生类型与泛型类型的区别

其一、使用原生态类型,会失掉泛型在安全性和其他表述性方面的优势。

  • 为什么继续允许使用原生态类型呢?Java 平台发展至今,已经存在大量的没有使用泛型的 Java 代码了,人们认为让所有这些代码保持合法,且能够与泛型的代码互用,为了这个“移植兼容性”(Migration Compatibility)需求,促成了支持原生态类型的决定。


其二、原生态类型 List 和 参数化类型 List<Object>有区别。

  • 原生态类型 List,逃避了泛型检查,List<Object>则明确告知编译器:它能够持有任意类型的对象。eg,List<String>可以传递给 List,但不能传递给 List<Object>。

  • 泛型有子类型化(subtyping)的规则。List<String> 是原生态类型 List 的一个子类型,但不是 List<Object> 的子类型。


下面通过一个例子解读两者的区别:


 private static void testSubTyping() {    List<String> strings = new ArrayList<String>();    unsafeAdd(strings, new Integer(110));    String s = strings.get(0); // exception while run the method;  }    /**   * 方法使用了原生态类型,所以可以编译。   */  private static void unsafeAdd(List list, Object o) {    list.add(0);  }    /**   * 方法使用了List<Object>替代原生态类型,所以编译不会通过。   */  private static void unsafeAddV2(List<Object> list, Object o) {    list.add(0);  }
复制代码


结论:使用 List 这样的原生态类型会丢掉类型安全性,但是使用 List<Object> 这样的参数化类型则不会。


5、泛型的不推荐使用场景

不要在新代码中使用原生态类型,这条规则有两个小小的例外,原因是:泛型信息可以在运行时被编译器擦除了


  • 在类文字(class literal)中必须使用原生态类型,规范不允许使用参数化类型(但允许数组类型和基本类型)[JLS,15.8.2]


ClassLiteral: 

    TypeName {[ ]} . class 

    NumericType {[ ]} . class 

    boolean {[ ]} . class 

    void . class


class literal is an expression consisting of the name of a class, interface, array, or primitive type, or the pseudo-type void, followed by a '.' and the token class.

https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.8.2


  • instanceof 操作符对“无限制通配符”的参数化类型是无效非法的。,由于泛型信息在运行中被擦除了,这种情况下,尖括号(<>)和问号(?)显得多余了。


  private static void testInstanceOfInvalidOnGeneric(Object o) {    if (o instanceof Set){      //一旦确定o是个Set,必须转换为通配符类型Set<?>,而不是原生态类型Set。这样能避免后续代码出现运行时异常。      Set<?> set = (Set<?>) o;    }  }
复制代码


6、总结


7、延伸阅读

《源码系列》

JDK之Object 类

JDK之BigDecimal 类

JDK之String 类

JDK之Lambda表达式


《经典书籍》

Java并发编程实战:第1章 多线程安全性与风险

Java并发编程实战:第2章 影响线程安全性的原子性和加锁机制

Java并发编程实战:第3章 助于线程安全的三剑客:final & volatile & 线程封闭


《服务端技术栈》

《Docker 核心设计理念

《Kafka史上最强原理总结》

《HTTP的前世今生》


《算法系列》

读懂排序算法(一):冒泡&直接插入&选择比较

《读懂排序算法(二):希尔排序算法》

《读懂排序算法(三):堆排序算法》

《读懂排序算法(四):归并算法》

《读懂排序算法(五):快速排序算法》

《读懂排序算法(六):二分查找算法》


《设计模式》

设计模式之六大设计原则

设计模式之创建型(1):单例模式

设计模式之创建型(2):工厂方法模式

设计模式之创建型(3):原型模式

设计模式之创建型(4):建造者模式


发布于: 2021 年 01 月 23 日阅读数: 34
用户头像

Diligence is the mother of success. 2018.03.28 加入

公众号:后台技术汇 笔者主要从事Java后台开发,喜欢技术交流与分享,保持饥渴,一起进步!

评论

发布
暂无评论
Java 程序经验小结:编程更好的使用泛型以替代原生态类型