小白学Java:老师!泛型我懂了!

泛型概述

使用泛型机制编写的程序代码要比哪些杂乱地使用Object变量,然后再进行强制类型转换地代码具有更好的安全性和可读性。

以上摘自《Java核心技术卷一》

在谈泛型的定义之前,我先举一个简单又真实的例子:如果我想定义一个容器,在容器中放同一类的事物,理所当然嘛。但是在没有泛型之前,容器中默认存储的都是Object类型,如果在容器中增加不同类型的元素,都将会被接收,在概念上就不太符合了。关键是放进去不同元素之后,会造成一个很严重的情况:在取出元素并对里面的元素进行对应操作的时候,就需要复杂的转型操作,搞不好还会出错,就像下面这样:

//原生类型
ArrayList cats = new ArrayList();
cats.add(new Dog());
cats.add(new Cat());
for (int i = 0; i < cats.size(); i++) {
//下面语句类型强转会发生ClassCastException异常
((Cat) cats.get(i)).catchMouse();
}

而泛型又是怎么做的呢?通过尖括号<>里的类型参数来指定元素的具体类型。

ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new SkyBarking());
dogs.add(new Snoopy());
dogs.add(new Cat());//编译不通过
//向上转型,另外两个是Dog的子类对象
for(Dog d:dogs){
System.out.println(d);
}

至此,泛型优点显而易见:

  • 可读性:很明显嘛,一看就知道是存着一组Dog对象。
  • 安全性:如果类型不符,编译不会通过,因此不再需要进行强制转换

妙啊,从中我们可以体会泛型的理念:泛型只存在编译器,宁可让错误发生在编译期,也不愿意让程序在运行时出现类型转换异常。 因为bug发生在编译期更容易去找到并修复。 除此之外:

  • 可将子类类型传入父类对象的容器之中,向上转型。
  • 不必纠结对象的类型,可用增强for循环实现遍历。

定义泛型

再次强调,所谓泛型,即参数化类型,就是只有在使用类的时候,才把类型确定下来,相当的灵活。

泛型类的定义

class Element<T>{
private T value;
Element(T value){
this.value = value;
}
public T getvalue() {
return this.value;
}
}
  • 引入类型变量T(按照规范,也可以有多个,用逗号隔开),并用<>扩起,放在类名后面
  • 其实就是可以把T假想成平时熟悉的类型,这里只不过用个符号代替罢了。
Element<String> element = new Element<>("天乔巴夏");
System.out.println(element.getvalue());
  • 使用泛型时,用具体类型(只能是引用类型)替换类型变量T即可。泛型其实可以堪称普通类的工厂。
  • 泛型接口的定义与类定义类似,就暂且不做赘述。

泛型方法的定义

class ParaMethod {
public static <T> T getMiddle(T[] a) {
return a[a.length/2];
}
}
  • 注意该方法并不是在泛型类中所定义,而是在普通类中定义的泛型方法。
  • 类型变量T放在修饰符的后面,返回类型的前面,只是正好我们这边返回类型也是T。
int m = ParaMethod.getMiddle(new Integer[]{1,2,3,4,5});
//返回Integer类型,自动拆箱
System.out.println(m);//3

类型变量的限定

我们上面讲到,泛型拥有足够的灵活性,意味着我传啥类型,运行的时候就是啥类型。但是,实际生活中,我要是想对整数类型进行操作,不想让其他类型混入,怎么办呢?对了,加上类型限定。

public static <T extends Number> T getNum(T num) {
return num;
}
  • 定义格式:修饰符 <T extends 类型上限> 返回类型 方法名 参数列表,如上表示对类型变量的上限进行限定,只有Number及其子类可以传入。
  • 规定类型的上限的数量最多只能有一个。
  • 既然类的定义是这样子,那大胆猜测一下,定义接口上线是不是就应该用implements关键字呢?答案是:否!接口依旧也是extends
public static <T extends Comparable & Serializable> T max(T[] a) {
if (a == null || a.length == 0) return null;
T maximum = a[0];
for (int i = 1; i < a.length; i++) {
if (maximum.compareTo(a[i]) < 0) maximum = a[i];
}
return maximum;
}
  • 需要注意的是:如果允许多个接口作为上限,接口可以用&隔开。
  • 如果规定上限时,接口和类都存在,类需要放在前面,<T extends 类&接口>
  • 没有规定上限的泛型类型可以视为:<T extends Object>

原生类型与向后兼容

使用泛型类而不指定具体类型,这样的泛型类型就叫做原生类型(raw type),用于和早期的Java版本向后兼容,毕竟泛型JDK1.5之后才出呢。其实我们在本篇开头举的例子就包含着原生类型,ArrayList cats = new ArrayList();

ArrayList cats = new ArrayList();//raw type

它大致可以被看成指定泛型类型为Object的类型。

ArrayList<Object> cats = new ArrayList<Object>();

注意:原生类型是不安全的!因为可能会引发类型转换异常,上面已经提到。所以我们在使用过程中,尽量不要使用原生类型。

通配泛型

我们通过下面几个例子,来详细总结通配类型出现的意义,以及具体的用法。

非受限通配

如果我想定义一个方法,让它接收一个集合,不关注集合中元素的类型,并把集合中的元素打印出来,应该怎么办呢?

上面谈到泛型,你可能会这样写,让方法接收一个Object的集合,这样子你传进来啥我都接,完成之后美滋滋,一调试就不对了:

public static void print(ArrayList<Object> arrayList){
//错误!:arrayList.add(5);
for(int i = 0;i< arrayList.size();i++){
System.out.println(arrayList.get(i));
}
}
ArrayList<Integer> arr = new ArrayList<>();
print(arr);

究其原因:Integer是Object的子类的确没错,但是ArrayList并不是ArrayList的子类型。那可咋办啊?这时非受限通配符它来了……

public static void print(ArrayList<?> arrayList)
  • 定义格式:?表示接收所有的类型,可以看成是? extends Object,这个就是我们即将要说的受限通配的格式了,非受限通配就是以Object为上限的通配,可不是嘛。
  • 使用通配符?时,由于类型的不确定,你不能够调用与对象类型相关的方法,就像上面的arrayList.add(5);就是错误的。

受限通配

如果我想定义一个方法,让它接收一个整数类型的集合,应该怎么办呢?

public static void operate(ArrayList<Number> list){
/*operate a List of Number*/
}
/* 调用方法 */
ArrayList<Integer> arr = new ArrayList<>();
operate(arr);

上面的这个错误,想必你不会再犯,因为ArrayList并不是ArrayList的子类型。那这个时候又咋办啊?这时受限通配符它来了……

public static void operate(ArrayList<? extends Number> list){
/*operate a List of Number*/
}
  • 形式:?extends T ,表示T或者T的子类型。

下限通配

说完了上面两个,第三个我就不卖关子了,直接写上它的定义格式:? super T,表示T或者T的父类型。

public static <T> void show(ArrayList<T> arr1,ArrayList<? super T>arr2){
System.out.println(arr1.get(0)+","+arr2.get(0));
}
ArrayList<Number> arr1 = new ArrayList<>();
ArrayList<Integer> arr2 = new ArrayList<>();
//编译出错
show(arr1,arr2);

以上将会编译错误,因为限定show方法中第二个参数的类型必须时第一个参数类型或者其父类。

泛型的擦除和限制

类型擦除

  • 泛型的相关信息可被编译器使用,但是这些信息在运行时是不可用的。

  • 泛型仅仅存在于编译,一但编译器确认泛型类型的安全性,就会将它转换原生类型。

  • 当编译泛型类、接口或方法时,编译器会用Object代替泛型类型。以上面的例子举例:

Element<String> element = new Element<>("天乔巴夏");
System.out.println(element.getvalue());

将会变成:

Element element = new Element("天乔巴夏");
System.out.println((String)element.getvalue());
  • 当一个泛型受限时,编译器会用其首限类型替换它。
public static void operate(ArrayList<? extends Number> list){
/*operate a List of Number*/
}

将变成下面这样:

public static void operate(ArrayList<Number> list){
/*operate a List of Number*/
}
  • 不管实际的具体类型是什么,泛型类总是被它的所有实例所共享
ArrayList<Number> arr1 = new ArrayList<>();
ArrayList<Integer> arr2 = new ArrayList<>();
System.out.println(arr1 instanceof ArrayList);//true
System.out.println(arr2 instanceof ArrayList);//true

可以看到,虽然ArrayList<Number>ArrayList<Integer>是两种类型,但是由于泛型在编译器进行类型擦除,它们在运行时会被加载进同一个类,即ArrayList类。所以下面这句将会编译出错。

System.out.println(arr1 instanceof ArrayList<Number>);//编译出错

类型擦除造成的限制

  • 不能使用泛型类型参数创建实例
T t = new T();//错误
  • 不能使用泛型类型参数创建数组
//错误:E[] elements = new E[5];
E[] elements = (E[])new Object[5];
//可以通过类型转换规避限制,但仍会导致一个unchecked cast警告,编译器不能够确保在运行时类型转换能否成功。
  • 不允许使用泛型类创建泛型数组
ArrayList<String>[] list = new ArrayList<String>[5];//错误
  • 上面说到,泛型类的所有实例具有相同的运行时类,所以泛型类的静态变量和方法时被它的实例们所共享的,所以下面三种做法都不行。
class Test<T> {
public static void m(T o1) {//错误
}
public static T o1;//错误
static {
T o2;//错误
}
}
  • 异常类不能是泛型的。因为如果异常类可以是泛型的,那么想要捕获异常,JVM需要检查try子句抛出的异常是否和catch子句中的异常类型匹配,但是泛型类型擦除的存在,运行时的类型并不能够知道,所以没什么道理。

本文若有叙述不当之处,还望评论区批评指正。

参考资料:

《Java核心结束卷一》、《Java语言程序设计与数据结构》

泛型就这么简单

https://www.programcreek.com/category/java-2/generics-java-2/

小白学Java:老师!泛型我懂了!的更多相关文章

  1. 小白学Java:包装类

    目录 小白学Java:包装类 包装类的继承关系 创建包装类实例 自动装箱与拆箱 自动装箱 自动拆箱 包装类型的比较 "=="比较 equals比较 自动装箱与拆箱引发的弊端 自动装 ...

  2. 小白学Java:迭代器原来是这么回事

    目录 小白学Java:迭代器原来是这么回事 迭代器概述 迭代器设计模式 Iterator定义的方法 迭代器:统一方式 Iterator的总结 小白学Java:迭代器原来是这么回事 前文传送门:Enum ...

  3. 小白学Java:奇怪的RandomAccess

    目录 小白学Java:奇怪的RandomAccess RandomAccess是个啥 forLoop与Iterator的区别 判断是否为RandomAccess 小白学Java:奇怪的RandomAc ...

  4. 小白学Java:内部类

    目录 小白学Java:内部类 内部类的分类 成员内部类 局部内部类 静态内部类 匿名内部类 内部类的继承 内部类有啥用 小白学Java:内部类 内部类是封装的一种形式,是定义在类或接口中的类. 内部类 ...

  5. 小白学Java:File类

    目录 小白学Java:File类 不同风格的分隔符 绝对与相对路径 File类常用方法 常用构造器 创建方法 判断方法 获取方法 命名方法 删除方法 小白学Java:File类 我们可以知道,存储在程 ...

  6. 小白学Java:I/O流

    目录 小白学Java:I/O流 基本分类 发展史 文件字符流 输出的基本结构 流中的异常处理 异常处理新方式 读取的基本结构 运用输入与输出 文件字节流 缓冲流 字符缓冲流 装饰设计模式 转换流(适配 ...

  7. 小白学Java:RandomAccessFile

    目录 小白学Java:RandomAccessFile 概述 继承与实现 构造器 模式设置 文件指针 操作数据 读取数据 read(byte b[])与read() 追加数据 插入数据 小白学Java ...

  8. 【aliyun】学java,看这里,不迷茫!1460道Java热门问题

    阿里极客公益活动: 或许你挑灯夜战只为一道难题 或许你百思不解只求一个答案 或许你绞尽脑汁只因一种未知 那么他们来了,阿里系技术专家来云栖问答为你解答技术难题了 他们用户自己手中的技术来帮助用户成长 ...

  9. 教妹学 Java:晦涩难懂的泛型

    00.故事的起源 “二哥,要不我上大学的时候也学习编程吧?”有一天,三妹突发奇想地问我. “你确定要做一名程序媛吗?” “我觉得女生做程序员,有着天大的优势,尤其是我这种长相甜美的.”三妹开始认真了起 ...

随机推荐

  1. jq添加插入删除元素

    https://www.cnblogs.com/sandraryan/ append() - 在被选元素的结尾插入内容 <body> <div class="wrap&qu ...

  2. [C++] 自动关闭右下角弹窗

    最近腾讯.迅雷等各种客户端,都越发喜欢在屏幕的右下角弹框了. 有骨气的人当然可以把这些软件卸载了事,但是这些客户端在某些情况下却又还是有用的.怎么办呢? 作为码农,自己实现一个自动关闭右下角弹窗的程序 ...

  3. 2006年NOIP普及组复赛题解

    题目涉及算法: 明明的随机数:简单模拟: 开心的金明:01背包: Jam的计数法:模拟: 数列:二进制. 明明的随机数 题目链接:https://www.luogu.org/problem/P1059 ...

  4. AIM Tech Round (Div. 2)

    A. Save Luke 题意:给一个人的长度d,然后给一个区间长度0~L,给你两个子弹的速度v1,v2,两颗子弹从0和L向中间射去(其实不是子弹,是一种电影里面那种绞牙机之类的东西就是一个人被困在里 ...

  5. Codeforces Round #200 (Div. 1 + Div. 2)

    A. Magnets 模拟. B. Simple Molecules 设12.13.23边的条数,列出三个等式,解即可. C. Rational Resistance 题目每次扩展的电阻之一是1Ω的, ...

  6. 总结thinkphp快捷查询getBy、getField、getFieldBy用法及场景

    thinkphp作为国内现阶段最成熟的框架:没有之一: 不得不说是有好些特别方便的方法的: 然而如果初接触thinkphp的时候难免会被搞的有点迷茫: for example这些: getBy get ...

  7. H5 拖拽元素

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. 【踩坑记录】vue单个组件内<style lang="stylus" type="text/stylus" scoped>部分渲染失效

    vue组件化应用,近期写的单个组件里有一个的渲染部分样式渲染不上去 因为同结构的其他组件均没有问题,所以排除是.vue文件结构的问题,应该是<style>内部的问题 <style l ...

  9. H3C通过端口ID决定端口角色

  10. vue2.x+elelmentUI@3.5 表格

    <template> <section> <el-row> <el-col :span="16"> <!--表单--> ...