一、泛型概念
(1)什么是泛型
什么是泛型???
泛型(Generic),是一种参数化数据类型,它允许我们在编写程序代码的时候不用具体指定需要什么数据类型,而是等到具体使用的时候,将数据类型以参数的形式传递给程序,这就是泛型程序设计。
泛型不是Java中特有的概念,它是一种程序设计思想,在很多编程语言都具有泛型,例如:Golang、Java、Python等等语言都有泛型。
(2)为什么需要泛型
我们来看看下面这个栗子
假设现在有个需求,通过集合保存元素,需要保存String字符串类型和Integer类型,在不考虑泛型的情况下,我们可能会写出下面这样的代码:
String字符串集合
// 字符串集合
class StringBox {
private String[] box = new String[10];
private int index = 0;
public boolean add(String e) {
if (index < box.length) {
box[index++] = e;
return true;
}
return false;
}
}
Integer整数集合
// 整数集合
class IntegerBox {
private Integer[] box = new Integer[10];
private int index = 0;
public boolean add(Integer e) {
if (index < box.length) {
box[index++] = e;
return true;
}
return false;
}
}
从上面两段代码可以看出,我们两个集合类,除了数据类型不同,其余的代码都是类似的。对于这种情况,我们可能会想着,能不能将两个类变成一个类,从而减少重复代码呢???
这时候,我们可能会想到这种方式,我们可以将数据类型用Object替换,这样就可以实现通过一个类保存不同的数据类型了,于是,就写出来下面这样的代码:
// 通用集合
class ObjectBox {
private Object[] box = new Object[10];
private int index = 0;
public boolean add(Object e) {
if (index < box.length) {
box[index++] = e;
return true;
}
return false;
}
public void print() {
for (int i = 0; i < index; i++) {
String str = (String)box[i];
// 将大写字母转换为小写字母
System.out.println(str.toLowerCase());
}
}
}
这时候,我们写完代码,心里面可能会觉得,我们的代码瞬间提升了一个Level,于是信心满满的在需要使用集合的地方调用我们写的通用集合方法,调用代码如下:
public class WhyUseGeneric {
public static void main(String[] args) {
// 使用集合
ObjectBox objectBox = new ObjectBox();
objectBox.add(520);
objectBox.add("LOVE");
// 打印集合中的元素
objectBox.print();
}
}
运行上面的代码之后,居然发现报错了,如下所示:
哎呀,出现数据类型转换失败啦,为什么呢???因为我们的集合中,不仅保存了String类型,还保存了Integer类型,所以在使用的时候,就会发生类型转换失败。
我们应该怎么解决这个问题???
我们可以在使用之前进行类型判断,如果是我们需要的类型,我们就进行强制类型转换,这样在运行过程中,不会发生错误,修改代码如下:
public void printNew() {
for (int i = 0; i < index; i++) {
// 判断一下,是不是String类型,不是String类型不处理
if (box[i] instanceof String) {
String str = (String)box[i];
// 将大写字母转换为小写字母
System.out.println(str.toLowerCase());
}
}
}
再次运行程序,这次就可以运行成功了。
通过上面的案例,我们可以发现,如果不使用泛型,我们如果要实现某个功能,针对不同的数据类型,就需要编写很多类似的代码;如果要减少重复代码,可以通过Object类型保存元素,但是需要在使用数据的时候,进行强制类型转换,否则程序报错。
为了解决上面两种情况,于是提出了泛型程序设计:
泛型可以使用同一套代码,实现不同数据类型的操作,并且在程序运行过程中,不需要我们进行强制类型转换。
本篇文章后面,我会介绍泛型的具体使用,请继续往后看。
二、泛型的优缺点
(1)优点
- 减少重复代码,提高代码的复用性。
- 可以不用强制类型转换。
- 提供了编译期间的类型安全检查机制。
(2)缺点
- 泛型的数据类型不能是基本数据类型。
- 方法重载中不能使用相同参数列表的泛型。
- 泛型的类型检查是编译期间的,通过反射机制可以绕过类型检查机制。
三、泛型的使用
(1)泛型类
泛型使用在一个类上面,这个类就称为:泛型类。
如何定义一个泛型类???
// 定义一个泛型类
// 语法格式:类名称<T>
public class GenericClassDemo<T> {
}
语法格式:
访问修饰符 类名称<T> {}
- 类名称后面使用左右尖括号【<>】标识
- 左右尖括号【<>】里面,使用一个泛型标识,标识可以是任意字符,一般常用的是:T、K、V、E等。
泛型类定义之后,我们就可以在类里面使用这个泛型。下面给出一个案例代码:
// 定义一个泛型类
// 语法格式:类名称<T>
public class GenericClassDemo<T> {
// 定义泛型变量
private T data;
// 定义方法
public void setData(T data) {
this.data = data;
}
public T getData() {
return this.data;
}
}
泛型类的使用
public class GenericDemo {
public static void main(String[] args) {
// 泛型类的使用
// 保存String类型
GenericClassDemo<String> generic = new GenericClassDemo<>();
generic.setData("泛型类的使用");
// 输出结果
System.out.println(generic.getData());
// 保存Integer类型
GenericClassDemo<Integer> generic2 = new GenericClassDemo<>();
generic2.setData(520);
System.out.println(generic2.getData());
}
}
看完泛型类的使用后,是不是觉得代码写的很丝滑呢???即可以减少重复代码,又可以避免强制类型转换。
泛型类的继承使用
如果一个类,继承了泛型类,那么子类有两种处理泛型的方式:
- 第一种方式:子类给父类中的泛型参数指定具体的数据类型。
- 第二种方式:子类也定义为泛型类。
- 第一种方式:子类给父类中的泛型参数指定具体的数据类型
我们在定义子类的时候,可以给父类传递具体的泛型类型,告诉父类泛型应该使用哪个具体的数据类型。
// 子类给父类指定具体的数据类型
public class GenericExtendsDemo extends GenericClassDemo<String> {
}
class GenericClassDemo<T> {
private T data;
public void setData(T data) {
this.data = data;
}
public T getData() {
return this.data;
}
}
- 第二种方式:子类也定义为泛型类
如果子类没有指定父类的数据类型,那么子类就需要声明为泛型类(注意:子类的泛型参数标识符必须和父类的泛型参数标识符一致,否则编译报错)。
// 子类也定义为泛型类
public class GenericExtendsDemo<T> extends GenericClassDemo<T> {
}
class GenericClassDemo<T> {
private T data;
public void setData(T data) {
this.data = data;
}
public T getData() {
return this.data;
}
}
如果子类不是泛型类,那么父类也不能使用指定【<T>】修饰,可以不写,不写就默认是【Object】数据类型。
(2)泛型接口
和泛型类基本类似,只不过是定义在接口上面。
泛型接口:泛型声明在接口上面,就叫做泛型接口。
案例代码如下所示(很简单,看一眼就会了):
// 定义泛型接口
public interface GenericInterfaceDemo<E> {
// 定义方法
void setData(E data);
E getData();
}
泛型接口的实现
和泛型类的继承是类似的,也有两种方式:
- 第一种方式:实现类指定具体的数据类型。
- 第二种方式:实现类定义为泛型类(注意:实现类的泛型参数标识符必须和接口的泛型参数标识符一致,否则编译报错)。
下面给出两种方式的演示代码,大致看看就可以啦,非常简单滴。
- 第一种方式:实现类指定具体的数据类型
// 泛型实现类指定具体的数据类型
public class SubGenericInterface02 implements GenericInterfaceDemo<String> {
@Override
public void setData(String data) {
}
@Override
public String getData() {
return null;
}
}
- 第二种方式:实现类定义为泛型类
// 泛型实现类
public class SubGenericInterface<T> implements GenericInterfaceDemo<T> {
@Override
public void setData(T data) {
}
@Override
public T getData() {
return null;
}
}
以上就是泛型接口的定义和使用。
(3)泛型方法
在前面,我们定义泛型类的时候,我们可以发现一个问题,就是我们在类中定义了一个包含泛型参数标识【T】的方法,如下所示:
// 定义一个泛型类
// 语法格式:类名称<T>
public class GenericClassDemo<T> {
// 定义泛型变量
private T data;
// 定义方法
public void setData(T data) {
this.data = data;
}
public T getData() {
return this.data;
}
}
很多人可能会认为上面的【setData()、getData()】就是泛型方法,其实严格来说那个不是泛型方法,那只是类中一个使用到了泛型参数的普通方法而已。
那什么是泛型方法呢???
什么是泛型方法???
泛型方法是在方法调用的时候,将泛型的数据类型作为参数传递给方法,而不是通过泛型类进行参数传递。
换句话说,泛型方法不需要依赖泛型类,它可以单独在一个普通的类中使用泛型参数。
通过泛型方法的概念,我们就可以知道,泛型方法是可以定义在普通的类里面的,它只需要在调用方法的时候进行参数传递,而不需要借助泛型类进行参数传递。
如何定义一个泛型方法呢???
泛型方法定义语法格式:
修饰符 <T> 返回值类型 方法名称(T data,......) {}
- 【<T>】在方法的访问修饰符和返回值之间,采用【<T>】标识这个方法是泛型方法,它的参数类型在调用时候决定。
- 【T data】方法参数,在调用方法的时候传递。
上面就是泛型方法的定义格式,我们来看下代码案例:
public class GenericMethodDemo {
public static void main(String[] args) {
// 泛型方法调用
GenericMethod generic = new GenericMethod();
// 调用
generic.method(520);
// 带有返回值的泛型方法
String ret = generic.method02("面向对象编程");
System.out.println("返回值:" + ret);
// 调用泛型静态方法
String staticRet = GenericMethod.method03("Static Generic Method.");
System.out.println(staticRet);
}
}
class GenericMethod {
// 定义泛型方法
public <T> void method(T data) {
System.out.println("这是定义的一个泛型方法......");
System.out.println("方法参数值: "+data);
}
// 返回值也是泛型的泛型方法
public <T> T method02(T data) {
System.out.println("这是定义的一个具有泛型返回值的泛型方法......");
return data;
}
/** 泛型静态方法 */
public static <T> T method03(T data) {
System.out.println("这是泛型静态方法......");
return data;
}
}
程序运行结果如下所示:
静态方法使用泛型的要求
- 如果一个静态方法使用了泛型参数,那么这个静态方法必须声明为泛型方法,否则编译不通过。
class GenericStaticMethod<T> {
// 普通方法中使用泛型参数
public void method(T data) {
System.out.println("普通方法中,使用泛型参数......");
}
// 静态方法中使用泛型参数
public static void method02(T data) {
// 编译不通过,必须声明静态的泛型方法
}
public static <T> void method03(T data) {
// 静态的泛型方法
}
}
静态方法如果不定义为泛型方法,并且还使用泛型类中的泛型参数,则会编译不通过。
注意:静态方法,是不能访问【泛型类】中的【泛型参数标识符】。
泛型类和泛型方法同时出现的情况
- 当我们在使用泛型类的时候,如果还需要在类中定义泛型方法,那该怎么办呢???
解决办法:
- 可以使用不同的泛型标识符,比如:泛型类中使用【T】,而泛型方法中则使用【E】。
其实泛型方法依旧可以使用和泛型类一样的泛型参数标识符【T】,只不过为了区分一下,一般情况下,我们都尽量不使用相同的泛型标识符。
案例代码如下所示:
// 定义泛型类
class GenericClassAndMethod<T> {
// 定义泛型方法
public <T> void method(T data) {
System.out.println("这是泛型方法......");
System.out.println("参数类型:" + data.getClass());
}
// 使用不同的泛型标识符
public <E> void method02(E data) {
System.out.println("这是泛型方法......");
System.out.println("参数类型:" + data.getClass());
}
}
以上,就是泛型方法的定义和使用,是不是So easy~~~。
(4)泛型通配符
泛型通配符
- Java中通过使用【?】来表示泛型通配符,含义是:未知的类型。
- 【?】泛型通配符是一个无界的泛型,就是可以为任意的数据类型。
这里我个人是不太理解的,泛型都已经是参数化类型,那按理来说,泛型就应该是未知的类型,可以传入任意的数据类型,为什么还需要额外定义一个泛型通配符呢???
(5)泛型上界
泛型的上界:
- 泛型的上界,是指规定传入的数据类型只能是某个具体数据类型及其子类型。
泛型上界定义格式:
上界语法格式:
- <T extends XXX>
- T:表示泛型参数
- XXX:表示具体的数据类型,比如:String、Integer、等等。
下面给出一个泛型上界的案例代码:
public class GenericExample {
public static void main(String[] args) {
// 使用泛型上界
GenericExtends<Integer> generic = new GenericExtends<>();
// 如果传递一个不是Number的子类,则编译不通过
GenericExtends<String> generic2 = new GenericExtends<String>();
}
}
// 定义一个泛型上界的泛型类
class GenericExtends<T extends Number> {
}
上面的代码会出现编译不通过,因为在使用的时候,传入的数据类型不在泛型上界范围里面。
上面编译错误的大致意思是:类型参数String不在范围里面,应该要继承Number类型的。
(6)泛型下界
既然可以限制泛型的上界,当然也可以限制泛型的下界啦。
泛型下界:
- 泛型的下界,是指规定传入的数据类型只能是某个具体数据类型及其父类型。
泛型上界语法格式:
语法格式:
- <XXX super T>
- XXX:是指具体的数据类型,比如:String、Integer、等等。
- T:是指泛型参数。
案例代码如下所示:
class GenericSupers {
// 泛型下界
public void method(List<? super Integer> list) {
}
}
我发现,泛型下界的不能定义在类或者接口上面,只能用在方法上面。
(7)泛型擦除
什么是泛型擦除???
泛型是只能在编译期间生效的,到了程序运行期间的时候,程序不认识那些泛型参数,所以在编译期间,泛型的那些参数都会被擦除,也就是被替换为具体的数据类型。
- 如果没有指定泛型的上下界,那么默认情况下,所有的泛型参数,在泛型擦除后将会是Object类型。
- 如果指定上界类型,那么泛型擦除之后,数据类型将和泛型上界的数据类型相同。
- 举个栗子:List<? extends String>,这个泛型擦除之后,数据类型将变成String类型。
- 如果指定下界类型,那么泛型擦除之后,数据类型将和泛型下界的数据类型相同。
- 举个栗子:List<? super Student>,这个泛型擦除之后,数据类型将变成Student类型。
- 默认情况下,所有泛型参数,在泛型擦除之后都会变成Object类型
public class GenericEraseDemo {
public static void main(String[] args) {
// 创建String类型
GenericEraseClass<String> stringGeneric = new GenericEraseClass<>();
// 创建Integer类型
GenericEraseClass<Integer> integerGeneric = new GenericEraseClass<>();
// 输出两个数据类型
Class stringClazz = stringGeneric.getClass();
System.out.println(stringClazz);
Class integerClazz = integerGeneric.getClass();
System.out.println(integerClazz);
System.out.println("===============================");
// 利用反射查看两个类中的参数data
Field[] fields = stringClazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("GenericEraseClass<String>中data的参数类型: "+field.getType());
}
fields = integerClazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("GenericEraseClass<Integer>中data参数类型: "+field.getType());
}
}
}
class GenericEraseClass<T> {
private T data; // 定义一个泛型变量
}
查看上面代码运行结果,发现两个不太类型的泛型参数,居然都输出了相同的数据类型,都是【Object】类型。
- 泛型上界,在泛型擦除之后会变成和上界相同的数据类型
public class GenericEraseDemo {
public static void main(String[] args) {
// 创建String类型
GenericEraseClass<Long> stringGeneric = new GenericEraseClass<>();
// 创建Integer类型
GenericEraseClass<Integer> integerGeneric = new GenericEraseClass<>();
// 输出两个数据类型
Class stringClazz = stringGeneric.getClass();
Class integerClazz = integerGeneric.getClass();
// 利用反射查看两个类中的参数data
Field[] fields = stringClazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("GenericEraseClass<Long>中data的参数类型: "+field.getType());
}
fields = integerClazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("GenericEraseClass<Integer>中data参数类型: "+field.getType());
}
}
}
// 定义泛型上界
class GenericEraseClass<T extends Number> {
private T data; // 定义一个泛型变量
}
上面定义了一个以【Number】类型的泛型上界,运行结果后,发现泛型擦除之后,对应的变量会变成【Number】的数据类型。
- 泛型下界,在泛型擦除之后会变成和下界相同的数据类型
这里通过反射机制,我不知道怎么查看擦除后的类型,所以这里我通过查看字节码的方式,来查看泛型擦除之后的类型。
查看字节码文件内容的命令:
- javap -verbose XXXX.class
从字节码文件里面,可以看出,泛型擦除之后,方法的参数类型变成和泛型下界的数据类型相同,都是Integer类型。
(8)泛型擦除带来的问题
- 泛型擦除是Jdk1.5之后出现的新特性,但是为了让之前的Jdk版本也可以运行泛型程序,所以就提出了泛型擦除,将其数据类型全变成了Java中的原始类型,从而实现兼容性。(因为如果不进行擦除,那么Jdk就需要额外添加处理泛型参数的代码,这样就导致Jdk1.5之前的版本,无法识别泛型参数了,因此也就不能兼容Jdk1.5之前的版本,所以泛型擦除就出来了)
- 泛型擦除只能在编译期间,对数据类型进行限制,但是通过反射机制,可以绕过泛型的限制。
- 泛型参数不能是基本数据类型,因为擦除之后会变成Object类型,而基本数据类型不能转换成Object,因为泛型参数只能是引用数据类型。
- 泛型类中的静态方法和属性不能使用泛型类中的参数类型(因为静态方法加载在类实例化之前)。
- catch代码块中,不能使用泛型(因为两个catch中使用相同的异常类型,但是泛型参数不同,那么泛型擦除后,就会导致两个catch块一致,从而编译错误)。
(9)泛型数组
- Java中不支持直接通过new创建一个泛型的对象或者数组(编译会报错)。
那如果要创建泛型数组呢???
有下面几种方式,创建泛型数组的方式:
- new后面不指定泛型参数。
- 通过java.lang.reflect.Array类中的newInstance()方法创建泛型数组。
创建泛型数组
代码如下所示:
public class GenericDemo<T> {
private T[] data;
// 方式一:new后面不加泛型类型<T>
private List<T>[] list = new LinkedList[10];
// 方式二:通过Array类的newInstance()方法
public GenericDemo(Class<T> clazz, int length) {
data = (T[]) Array.newInstance(clazz, length);
}
}
以上,就是Java中泛型相关的知识点,如果有哪里写的不正确的地方,希望大家纠正一下。