一、什么是泛型
泛型就是参数化类型,即我们在定义的时候,将具体的类型进行参数化,在调用或者使用的时候,再传入具体的参数类型,我们可以将泛型用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
二、为什么要使用泛型
泛型在开发过程中经常出现,比如我们一直高频使用的List
集合,我们可以这么创建一个 ArrayList
集合。
List<String> stringList = new ArrayList<>();
再看一下List
的定义
public interface List<E> extends Collection<E>{...}
我们看到在List
接口后面加了 <E>
对类型进行参数化,及参数是可变的,需要我们在使用的时候传入实际的参数,这样的好处是什么?
我们可以看看这么一个例子:
image当我们对List
添加泛型后,我们在使用的时候,可以根据需求声明不同类型的实际参数,如果我们传入的String,我们对List集合添加数据的时候,很明显看到当添加的数据不是String类型的时候,编译器会报错,当我们在获取数据的时候,也不需要强制类型转换,会自动返回我们传入实际参数的类型。
如果我们不传入实际类型,会出现什么情况?
image我们定义的List集合没有指定明确的参数,在数据添加的时候,可以看到我们可以添加不同类型的参数,编译器也没有报错,在获取数据的时候,返回的是Object类型,所以我们需要对其进行强制转换,而这个过程,很容易就会报ClassCastException
异常。
所以,我们使用泛型的好处有:
1、适用于多种数据类型执行相同的代码
2、类型安全:我们使用泛型的时候,指定了特定的参数类型,这样对其类型进行限定,可以在编译期间对我们的传入的参数类型进行判断,增加了类型的安全性。
3、取消强制类型转换:我们指定明确的类型参数后,由于在编译阶段就会对类型进行约束,泛型会自动且隐式的给我们做类型转换,转换成我们指定的类型,我们不再需要关心类型的转换。
三、泛型的使用
泛型可以定义在类、接口、方法上,分别被称为泛型类、泛型接口、泛型方法。
3.1、泛型类
通过 <>
将类型变量T(大写字母都可以,不过常用的就是T,E,K,V等等)括起来,放在类名后面,泛型类运行有多个类型变量。
一个类型变量的泛型类:
/**
* @author : EvanZch
* description: 一个类型变量的泛型类
**/
public class genericClassTest<T> {
private T mData;
public genericClassTest() {
}
public T getmData() {
return mData;
}
public void setmData(T mData) {
this.mData = mData;
}
}
多个类型变量的泛型类:
/**
* @author : EvanZch
* description: 两个类型变量的泛型类
**/
public class genericClassTest1<T, K> {
// ...
}
3.2、泛型接口
泛型接口的定义和泛型一致
/**
* @author : EvanZch
* description: 泛型接口
**/
public interface genericInterface<T> {
T getData();
void set(T data);
}
我们在实现泛型接口可以使用下面两种方式
1、不指定具体的泛型实参
/**
* @author : EvanZch
* description:实现泛型接口,不指定具体类型
**/
public class genericInterfaceImpl1<T> implements genericInterface<T> {
@Override
public T getData() {
return null;
}
@Override
public void set(T data) {
}
}
// 我们在调用的时候,再传入实际类型
genericInterfaceImpl1<String> interfaceImpl1 = new genericInterfaceImpl1();
2、指定具体的泛型实参
/**
* @author : EvanZch
* description:实现泛型接口,指定具体类型
**/
public class genericInterfaceImpl2 implements genericInterface<String> {
@Override
public String getData() {
return null;
}
@Override
public void set(String data) {
}
}
// 使用时候,直接创建
genericInterfaceImpl2 genericInterfaceImpl2 = new genericInterfaceImpl2();
3.3、泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。注意泛型类中定义的普通方法和泛型方法的区别。
image我们区别一下普通的方法和泛型方法
image我们看到普通方法中也使用了泛型,但是它只是一个普通的方法,只是它的返回值和传入的类型是在前面已经声明过得泛型,所以,这里才可以继续使用 T 这个类型变量。
而下面这个泛型方法,首先通过 <E>
标识了它是一个泛型方法,返回值类型和传入的类型一致,通过泛型进行参数化了。
四、泛型类型变量的限制
我们使用泛型的时候,对类型进行参数化,使用的可以传入任意的类型,但是在实际使用过程中,我们可能需要对类型进行限制,在进行类型擦除的时候,会转换成我们限定的类型,比如我们要比较两个字符的大小,需要用到 compareTo
方法
imageint compareTo(Object o) 或 int compareTo(String anotherString)
返回值是整型,它是先比较对应字符的大小(ASCII码顺序),如果第一个字符和参数的第一个字符不等,结束比较,返回他们之间的差值,如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至比较的字符或被比较的字符有一方结束。
- 如果参数字符串等于此字符串,则返回值 0;
- 如果此字符串小于字符串参数,则返回一个小于 0 的值;
- 如果此字符串大于字符串参数,则返回一个大于 0 的值。
可以看到如果我们直接这么定义,会报错,因为T为任意类型,但是 compareTo
方法定义在Comparable
接口中,我们需要限定传入的类型必须实现了 Comparable
接口
public interface Comparable<T> {
public int compareTo(T o);
}
我们可以这么写:
image这里我们对类型对进行了限定,使用了通配符和指明了上界 ? extends X
,这样就限制了我们传入的参数类型必须是实现了Comparable
接口,我们在调用的时候,编译器就会进行判断,我们传入的参数是否实现了Comparable
接口,如果没有就会报错。
imageInteger 实现了 Comparable 接口
public final class Integer extends Number implements Comparable<Integer>
五、泛型通配符
上面类型变量的限制中,我们使用了通配符 ?
,通配符的有三种使用方式。
<? extends X>
: 类型的上界限定,参数类型是X的子类。
<? super X>
:类型的下界限定,参数类型是X的超类。
<?>
: 无界限定。
为了说明他们的区别,我们先新建People
类、Man
类、Son
类,其中Man
继承至People
,Son
继承至 Man
People
类:
public class People {
}
Man
类:
public class Man extends People {
}
Son
类:
public class Son extends Man {
}
再创建一个泛型类 Test<T>
:
public class Test<T> {
private T data;
public T getData() {
return data;
}
public void setData(T mData) {
this.data = mData;
}
}
查看继承关系:
image很明显,我们可以看到People
类和Man
类是具有继承关系的,但是 Test<People>
和 Test<Man>
之间却没有任何关系。
我们进行如下操作
image可以看到set,get都没问题,因为我们将泛型直接指定为 People
,而 Man
和 Son
都是people
的子类,所以我们都能set
进去。
1、? extends X
: 上界限定通配符
我在再使用 ? extends X
,指明类型的上界限定为 X,表示传入方法的类型参数必须是其本身或子类。
但是对于泛型类来说,如果内部提供了get\set方法,那么set不允许调用,即类型的上界只读。
image我们可以看到,我们通过 <? extends People>
指明了类型参数上界为 People
,那我们执行取操作的时候,即调用get
的时候,编译器知道返回的肯定是个 x(不管是x还是x的子类),但是我们进行设置的时候,编译器只知道我们传入的是 x ,至于具体是 x 的那个子类并不知道,所以没有办法进行set操作。
总结:上不存。
主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。
2、? super X
: 下界限定通配符
使用 ? super X
,指明类型的下界限定为 X,表示传入方法的类型参数必须是其本身或其超类。
但是对于泛型类来说,如果内部提供了get\set方法,那么set只能传入X的子类及其本身,get返回的类型是Object。
image? super X 表示类型的下界,类型参数是X的超类(包括X本身),那么可以肯定的说,get方法返回的一定是个X的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是X和X的子类可以安全的转型为X。
总结:下不取
主要用于安全地写入数据,可以写入X及其子类型。
3、<?>
无限定通配符
表示类型无限制,和 T 的区别
ArrayList<T> al=new ArrayList<T>(); 指定集合元素只能是T类型
ArrayList<?> al=new ArrayList<?>(); 集合元素可以是任意类型,这种没有意义,一般是方法中,只是为了说明用法。
总结
泛型在java语言中的一种语法糖的存在,java中泛型的实现为类型擦除,是一种伪泛型。
网友评论