泛型(了解掌握)
在之前的学习中,我们曾自己实现过一个顺序表,如果想要温习的同学可以直接来我这篇博客进行翻阅:附上博客链接:
点击此处进入博客
之前我们对于顺序表的实现也是只能插入整形,不能插入其他类型的数据,那么现在假如我们想要实现一个顺序表是可以插入任何数据的,那么该怎样进行实现呢?来看我们的代码:我们只实现两个方法来表达我们想说的意思
class MyArrayList1 {
//定义一个Object类型的数组,用于存储所有类型的数据.
//Object类是所有类的父类,即使这个类他不继承Object类
public Object[] elem;
public int usedSize;
//实例化的时候生成一个大小为10的数组
public MyArrayList1() {
this.elem = new Object[10];
}
//数据的类型也替换成Object,这样才能保证插入所有类型数据
public void add(Object data) {
this.elem[this.usedSize] = data;
usedSize++;
}
public Object get(int pos) {
return this.elem[pos];
}
}
public class TestDemo {
public static void main(String[] args) {
MyArrayList1 myArrayList = new MyArrayList1();
//什么类型都可以放到数组里
myArrayList.add(19);
myArrayList.add(20);
myArrayList.add("sdsd");
//缺点:取数据的时候需要强制类型转换
int a=(int)myArrayList.get(1);
System.out.println(a);
}
}
可以看到只需要修改数据类型为Object,便可以完成对顺序表的操作,但是这样的操作有一个缺点,就是插入数据的时候可以随便插入,但是获取数据的时候却必须进行强制类型转换
,所以就会显的非常的麻烦,此时为了简化就需要用到泛型.
下面先来看下泛型是如何解决我们上述顺序表的,来看源码(并仔细看注释
):
//类这块的泛型的字母可以是任意的
//T是类型变量(Type Variable),变量名一般要大写
//T在定义时是形参,代表的意思是MyArrayList最终传入的类型,但现在还不知道
class MyArrayList<T> {
//定义一个T类型的数组,此时并不知道到底是什么类型的数组
public T[] elem;
public int usedSize;
public MyArrayList() {
/*此处为什么要用强制类型转换:我来解释下:
首先这么写是因为泛型类的原因,当我们强转其为T类型的数组时,此时我们并不知道这个数组强转后到底是什么数组,因为
T此时并没有给定一个合适的引用类型,而数组的类型是由后续我们填入的引用类型来决定的,这就提供类一个通用的数组模板,且后期
不需要进行强制类型转换
*/
this.elem = (T[]) new Object[10];
/*为什么不直接定义一个T类型的数组,因为此时发生了泛型的擦除机制,即将泛型擦除为Object,从而此时的泛型具有了Object的特质
所以此时的this.elem=new T[10];就等价于this.elem=new Object[10];
注意我们并不能直接写成this.elem=new T[10]这样的形式,原因是T只是在编译的时候被擦除为Object,具有了Object的特质,并不是T直接就等价于Object
而当我们是想要一个非Object类型的通用的数组,且后期不需要进行强制类型转换,此时才需要写成 this.elem = (T[]) new Object[10]这种形式.
并且只有当父类赋给子类的时候才进行强制类型转换,子类给父类不需要进行强制类型转换,因为发生了向上转型.
/*
此时大家还是会有疑问,此时创建数组可否换一个写法:如下所示:
this.elem = (T[]) new Integer[10];
此时我们会发现编译器会报出一个ArrayStoreException异常,原因是T与Object其实在这里是绑定的,
举个例子,假如我们此时T处为String类型的话,(T[]) new Object[10];就等价于(String[]) new Object[10],
此时String是Object类的子类,此时便会创造出一个String类型的数组,
如果此时是(T[]) new Integer[10];这段代码的话,就等价于(String[]) new Integer[10],此时String并不是Integer的子类
那么最终便会抛出ArrayStoreException异常。
*/
}
//插入数据,插入的数据类型由T处的数据类型决定
public void add(T data) {
this.elem[this.usedSize] = data;
usedSize++;
}
//根据T处的数据类型来返回相应的值
public T get(int pos) {
return this.elem[pos];
}
}
public class TestMain {
public static void main1(String[] args) {
//T为String类型
MyArrayList<String> myArrayList = new MyArrayList<>();
myArrayList.add("sss");
myArrayList.add("ddd");
myArrayList.add("fff");
String str = myArrayList.get(1);
System.out.println(str);
//T为整数类型
MyArrayList<Integer> myArrayList2 = new MyArrayList<>();
myArrayList2.add(1);
myArrayList2.add(2);
int val = myArrayList2.get(1);
System.out.println(val);
//T为浮点数类型
MyArrayList<Double> myArrayList3 = new MyArrayList<>();
}
}
可以看到此时不再进行强转类型转换来获取顺序表中的数据,而是直接通过下标便可以获取,这就是泛型的厉害之处
从而引出泛型的两个意义
:
1、自动进行类型的检查
为什么可以自动进行类型的检查,是这样的:T处的数据类型可以有很多种,例如简单数据类型的包装类,引用类型以及自定义数据类型,假如此时我们在泛型类内部
定义一个数组的话,这个数组的类型可以跟T处定义的类型有关,并且后期往数组里面插入数据的时候会自动检查插入的数据是否跟T中的数据类型匹配,如果匹配,那么就直接插入,这样子我们就制作出来了一个通用的顺序表
2、自动进行类型的转换
紧接着上面,根据之前我们写通用顺序表的写法,是直接定义一个Object类型的数组,这样就会导致
最终我们在获取顺序表中的某个值的时候必须进行强制类型转换(具体可参照上面的diamagnetic),而当我们使用了泛型之后,便不再存在这样的问题,因为泛型会帮我们自动进行类型的转换。例如String str = myArrayList.get(1)这段代码,他就自动进行了类型转换
同时通过初始化数组的时候的T【】这样的强转方式,我们引出一道面试题目:
泛型是怎么编译的?
答:这涉及到了泛型的擦除机制,进行类型擦除,编译的时候都会把泛型擦除为Object
,并不是我们所理解的替换为Object
,从而此时的泛型具有了Object的特质
泛型的注意事项
1:泛型只存在于编译时期
,只是编译时期的一种机制,1.即运行期间没有泛型的概念。
2:简单类型不能做泛型类型的参数,例如下面的int就不能做参数,尖括号中只能是引用类型
,而像java当中的八种基本数据类型
就不能放在尖括号里面,此时放入的应该是这八种基本类型所对应的包装类
,因为包装类是引用类型
MyArrayList myArrayList1 = new MyArrayList<>();
2:泛型在编译的时候 并不会进行指定类型的替换 而是拿着指定的类型进行检查
, 也就是说在编译的时候 ,拿着你指定的类型进行类型检查 ,记住我并没有说是替换
例如下面的代码:
MyArrayList myArrayList = new MyArrayList<>();
myArrayList.add(“sss”);
此时插入的时候会拿着String这个类型进行检查,如果插入的是字符串,就不会报错,否则便报错。此时并不是说我把泛型里面的参数替换成了String。
3:编译的时候 会进行类型擦除,编译的时候都会把泛型擦除为Object,并不是我们所理解的替换为Object
,从而此时的泛型具有了Object的特质
这样就很好解释下面的代码为什么我们可以在<>中放入很多不同的引用类型,例如String,Interger,Double,这些,因为在编译的时候已经将泛型T擦除为Object类型,而Object是所有类的父类,所以就可以放入许多引用类型以及包装类(包装类本质上也是引用类型
)
4:不能new 泛型类型的数组
this.elem = new T[10];
因为T的类型不能确定,编译和运行时候都不知道T的类型,所以不能new出来一个泛型类型的数组,只能强转。
5:所以T就是个模板,里面可以放不同的引用类型。
同时T代表占位符
,表示当前的类是一个泛型类
,泛型的标志就是尖括号<>
尖括号和T的作用就是帮助我们进行类型的检查与类型的转换,例如在插入数据的时候判断是否类型符合.
6:泛型类可以一次有多个类型变量,用逗号分割,例如<T,E>
再次证明泛型是编译时期的一种机制
package Genetic;
class Person{
}
public class TestMain {
public static void main(String[] args) {
Person person = new Person();
//输出结果为Genetic.Person@4554617c
System.out.println(person);
MyArrayList<String> myArrayList1 = new MyArrayList<>();
//输出结果为Genetic.MyArrayList@74a14482
System.out.println(myArrayList1);
MyArrayList<Integer> myArrayList2 = new MyArrayList<>();
MyArrayList<Double> myArrayList3 = new MyArrayList<>();
//输出结果为Genetic.MyArrayList@1540e19d
System.out.println(myArrayList2);
//输出结果为Genetic.MyArrayList@677327b6
System.out.println(myArrayList3);
}
}
此时我们在Person类中没有重写toString方法,所以最终我们的输出结果的组成形式为包名+类+@+存储对象地址的哈希值
.当然我们可以看到当我们使用泛型的时候,运行后
打印的值中是没有泛型中的值的,说明泛型类型的参数不参与类型的组成
,更加验证了泛型只存在于编译时期这一观点
.