作者:~小明学编程
文章专栏:JavaSE基础
格言:目之所及皆为回忆,心之所想皆为过往
目录
前言
什么是泛型
为什么要引入泛型
使用泛型
裸类型
泛型类的定义
类型擦除
通配符
什么是通配符
通配符的上下界
通配符的使用
泛型中的父子类
前言
今天给大家带来的文章是Java中的泛型,对于泛型的学习主要是为了让我们能够去阅读Java中的底层源码。
什么是泛型
为什么要引入泛型
我们随意的点开了我们ArrayList的源码我们就可以看到形如ArrayList<E>的这种写法,我们可以把我们的数据类型当作参数传递给我们的类中,这样我们new对象的时候就比较的灵活了,就不会拘泥于一种类型了。
例如:
ArrayList<Integer> arrayList1 = new ArrayList<>();
ArrayList<String> arrayList2 = new ArrayList<>();
因为我们有泛型所以我们的ArrayList才能操作我们不同的数据类型,否则的话只能我们的ArrayList的类就只能操作一种类型,这样的话就很鸡肋,不够灵活。
使用泛型
class MyArray<T> {
public T[] object = (T[])new Object[10];
public void set(int pos,T val) {
this.object[pos] = val;
}
public T get(int pos) {
return object[pos];
}
}
这里我们自己定义了一个泛型的类里面有一个数组和两个方法。
public static void main(String[] args) {
MyArray<String> myArray = new MyArray<>();
myArray.set(1,"haha");
String str = myArray.get(1);//这里自动的帮我们将Object的类型转换为String
System.out.println(str);
}
然后我们new了一个对象,我们传入一个String类型,然后将我们数组中的1的下标给设置为"haha"。
public T[] object = (T[])new Object[10];
对于这行代码,写的其实不是很好但是基本实现了我们想要表达的东西,因为我们是Object类型的,所以这里要强转为我们的泛型的类型。
裸类型
前面说到泛型,那么与之对应的就是我们的裸类型
MyArray myArray = new MyArray();
裸类型就是在我们new一个对象的时候不给它传递我们的数据类型,这就会导致我们下面的问题
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.set(0,13);
myArray.set(1,"haha");
String str = (String) myArray.get(1);//这里我们必须进行一个强制类型的转换才能接受
System.out.println(str);
}
因为我们没有传递我们的数据类型,所以在我呢吧返回我们对象里面的数据的时候其实都是Object类型的,所以我们这里要进行一个强制类型的转换。
注意: 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制。
泛型类的定义
上面我们说到泛型可以传入我们的数据类型,但是有时候不是什么类型都能传入的,这个时候就需要我们传入的类型进行一个约束了。
语法
class 泛型类名称<类型形参 extends 类型边界> {
}
其中类型边界表示我们的泛型类可以接受的类型参数的最高的父类。
示例
class C {
}
class D extends C{
}
class E extends D {
}
class Demo<T extends D> {
}
这里我们创建了四个类,其中我们的E继承了D,然后D继承C,然后我们的Demo类接受一个泛型的参数,参数的最高级别为D类,所以
上面这幅图可以清楚的看到我们最高可以接受的参数类型为D类,然而我们的C是我们D的父类,所以这里就给报错了。
提醒
当我们没有指定我们的上界的时候例如
class MyArray<T>
这时候就默认我们的上界为Object,此时我们可以传入任何的类型,同时也正是因为这个原因,所以我们才需要对其上界进行一个限制。
类型擦除
这里我们有必要说的一点就是泛型是作用在编译期间的一种机制,在我们代码的运行期间实际上是没有这么多的类型的,那么到底我们传入的T在运行的时候是什么类型呢?
实际上我们运行期间的类型主要取决于我们的边界。
class MyArray<T> {
public T[] object = (T[])new Object[10];
public void set(int pos,T val) {
this.object[pos] = val;
}
public T get(int pos) {
return object[pos];
}
}
例如我们前面的这个代码我们前面说过没指定边界的话,就默认为Object,所以在代码的运行过程中我们的T就变成了Object。
class Demo<T extends D> {
}
再如上面的这段代码,在我们代码运行的过程中,我们的T就是我们的边界D类型,即使我们传入的参数是C也一样。
这就是我们的类型擦除,也就是在我们运行的时候我们所传的参数类型会被替换为我们的类型上界,同时这个过程是发生在我们的编译期间的。
我们想要实现这样一个功能,有一个类然后类里面有一个方法可以求出我们不同类型数组的的最大值。
写法一:
class Ale<T extends Comparable<T>> {
public T getMax(T[] array) {
T max = array[0];
for (int i = 0; i < array.length; i++) {
if (max.compareTo(array[i])<0) {
max = array[i];
}
}
return max;
}
}
写法一是这样的,我们的T的上界是Comparable,为什么上界要写成这样呢?因为我们下面要对数组里面的元素进行一个比较,如果我们没有Comparable的话我们进行完类型擦除,T将会替换为Object,此时里面没有compareTo方法,无法进行比较。
public static void main(String[] args) {
Ale<Integer> ale = new Ale<>();
System.out.println(ale);
Integer[] array = {9,3,2,65,34,77,457};
Integer a = ale.getMax(array);
System.out.println(a);
System.out.println(Ale1.getMax(array));
}
写法二:
class Ale1 {
public static<T extends Comparable<T>> T getMax(T[] array) {
T max = array[0];
for (int i = 0; i < array.length; i++) {
if (max.compareTo(array[i])<0) {
max = array[i];
}
}
return max;
}
}
第二种就是在我们的静态方法中使用泛型。
通配符
什么是通配符
我们的源码中有很多形如上图的情况问号(?)是什么东西?
问号就是我们Java泛型中的通配符,
class Ale4 {
public static void print2(ArrayList<?> list) {
for (Object x:list) {
System.out.println(x);
}
}
}
形如上面这段代码,当我们传参的时候不知道传入什么的时候我们可以使用通配符?,这样我们就可以接受任何泛型类型的参数了。
class Ale3 {
public static<T> void print1(ArrayList<T> list) {
for (T x:list) {
System.out.println(x);
}
}
}
当然像上面这样不用通配符写也是能够实现我们上述的功能的。
通配符的上下界
上界的写法
<? extends 上界>
上界表示我们传入的类型最高只能是上界,上界的父类就不能传了。
下界的写法
<? super 下界>
下界表示我们传入的数据类型最低要求是下界,或者下界的父类
首先先来看看我们的上界的用法,
class Ale4 {
public static void print2(ArrayList<? extends Number> list) {
for (Object x:list) {
System.out.println(x);
}
}
}
我们通配符和泛型的参数类型一样可以接受上界以内的任何的参数类型,但是我们的泛型会被替换为具体的上界类型而我们的通配符则是对所有对象都适用。
通配符的使用
public static void main10(String[] args) {
ArrayList<? super Number> arrayList = new ArrayList<>();
arrayList.add(new Integer(12));
}
我么的下界适合添加数据,因为因为最低都是Number那么作为我们Number的子类,肯定是能够添加进去的。
public static void main10(String[] args) {
ArrayList<? extends Number> arrayList1 = new ArrayList<>();
arrayList1.get(0);
}
上界比较适合读入数据,因为我们返回的起码是Number,只要接受类型大于Number就行了。
泛型中的父子类
泛型中的父子类是根据我们通配符来决定的,例如
MyArrayList<?> 是 MyArrayList<? extends Number> 的父类型
MyArrayList<? extends Number> 是 MyArrayList<Integer> 的父类型
那么我们就可以写出如下的代码
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>();
MyArray<? extends Number> myArray = myArray1;
}
因为我们的MyArrayList<? extends Number> 是 MyArrayList<Integer> 的父类型所以我们的myArray可以直接引用我们的myArray1。
最后:
这是个人对Java泛型的一些理解,对于泛型的学习可以不用学的很深,只要能看的懂我们的源码就行了,自己写代码的时候就更不要设计到泛型了,除非自己去设计jdk。