目录
一、什么是泛型
泛型是Java SE 1.5的新特性,可以适应不同的很多很多类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
二、为什么使用泛型
1.使用泛型能写出更加灵活通用的代码
2.泛型将代码安全性检查提前到编译期
使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException转移到编译时
//没有泛型的情况下使用集合
public static void noGeneric() {
ArrayList names = new ArrayList();
names.add("张三");
names.add("李四");
names.add(123); //编译正常
}
//有泛型的情况下使用集合
public static void useGeneric() {
ArrayList<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
names.add(123); //编译不通过
}
3.泛型能够省去类型强制转换
在JDK1.5之前,Java容器都是通过将类型向上转型为Object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,使得代码更加优雅。
//没有泛型的情况下使用集合
public static void noGeneric() {
ArrayList names = new ArrayList();
names.add("张三");
names.add("李四");
names.add(123);
String name = (String) names.get(2); //需强制转换
}
//有泛型的情况下使用集合
public static void useGeneric() {
ArrayList<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
String name = names.get(1); //无需强制转换
}
而且第一种方法在运行时才会抛出异常
三、如何使用泛型
1.泛型类
定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔
public class GenericClass<ab,a,c> {
}
当然,这个后面的参数类型也是有规范的,不能像我上面一样乱写,通常类型参数我们都使用大写的单个字母表示:
T:任意类型 type
E:集合中元素的类型 element
K:key-value形式 key
V: key-value形式 value
我们写个案例试一下:
public class GenericClass<T> {
private T value;
public GenericClass(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
GenericClass<String> name = new GenericClass<>("张三");
System.out.println(name.getValue());
GenericClass<Integer> number = new GenericClass<>(123);
System.out.println(name.getValue());
打印结果是张三 123是没有问题的。
2.泛型接口
泛型接口的定义和泛型类的定义相类似
public interface GenericInterface<T> {
void show(T value);
}
我们还是实现String和Integer的实现类:
public class StringShowImpl implements GenericInterface<String> {
@Override
public void show(String value) {
System.out.println(value);
}
}
public class NumberShowImpl implements GenericInterface<Integer> {
@Override
public void show(Integer value) {
System.out.println(value);
}
}
此时如果我们像下面这样写是不允许的,因为前后定义的泛型类型不一致:
GenericInterface<String> genericInterface = new NumberShowImpl(); //编译错误
或者干脆不指定类型,那么new什么类型都是可以的:
GenericInterface g1 = new NumberShowImpl();
GenericInterface g2 = new StringShowImpl();
3.泛型方法
泛型方法可以定义在泛型类中,也可以定义在普通类中。
如果我们一个方法的参数可能是不同类型的,那如果不用泛型的话我们可能只能用重载的方式:
public class GenericFun {
public void show(String value) { }
public void show(Integer value) { }
}
但是如果我们用泛型的话,一个函数就搞定了,
定义泛型方法,在返回值前面,修饰符后面,添加尖括号,并在其中填写类型参数:
public class GenericFun {
public static <T> void show(T value) { }
}
如果可以定义泛型方法,那就尽量定义泛型方法,也就是说如果泛型方法能解决问题,就尽量不要定义泛型类了。
下面是调用:
GenericFun.show(123);
//GenericFun.<Integer>show(123); 可省略
但是如果我们show()中的业务需求是参数必须都是数值类型呢?
如果你写show("张三")程序编译也是可以的,但是在处理业务的时候肯定会出现类似上面的强转异常,这是我们就需要对这个泛型方法的参数类型进行限定了,
你打开Integer,Double等数值包装类的源码:
public final class Integer extends Number implements Comparable<Integer> {
public final class Double extends Number implements Comparable<Double> {
就会发现他们都是继承于Number这个类的,所以我们就可以这样来限定:
public class GenericFun {
/**
* T extends Number:表示T所绑定的类型必须是Number的子类
**/
public static <T extends Number> void show(T value) { }
}
这个限定类型可以是接口,也可以是类,而且可以限定多个,可以用&符号来连接,
而且如果限定既有类又有接口的时候,类必须写在前边,什么意思呢?
我们还是看Integer,Double的源码发现他们还实现了Comparable接口,那我们再加一个接口的限定:
public static <T extends Number & Comparator> void show(T value) { }
其中我们就是多个限定,而且类放在了接口前面,如果不按照这样的规则,编译就会出错。
这样一限定的话,你传进来的参数既要是Number的子类,还需要实现Comparator接口。
四、泛型是怎么实现的
说了一些基本用法,我们来看看java是如果实现的泛型。
我们都知道我们的类都必须编译成class文件jvm才能运行,那么我们定义的泛型类,它编译后的class文件中的泛型参数是什么类型呢?
就用我们上面定义的泛型类GenericClass<T>来举例,难道它编译后的class文件是GenericClass<T>.class么,类中的变量value又要编译成什么类型呢?对jvm来说,根本没有泛型这个概念,只有具体类型,那么java底层是怎么实现的呢?
类型擦除
说到这里,我们就不得不提到类型擦除的概念了,一听名字也很好理解,就是编译的时候把类型擦掉呗,但是单纯的擦掉肯定是不行的,我们来看看java是怎么擦除的:
泛型类编译时:
1:去掉类名后的尖括号和尖括号中的类型参数,剩下的名字就是对应的类名
2:对于类中使用了类型参数变量的类型,如果没有类型限定的情况下,使用Object替换,如果有类型限定,使用限定的类型替换,如果有多个类型限定,使用第一个限定的类型来替换。
那么按照这个规则,我们上面的泛型类实际编译效果是什么呢?
public class GenericClass {
private Object value;
public GenericClass(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
口说无凭,我们来看一下编译后的字节码文件:
Compiled from "GenericClass.java"
public class com.example.demo.test1.GenericClass<T> {
public com.example.demo.test1.GenericClass(T);
descriptor: (Ljava/lang/Object;)V
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #2 // Field value:Ljava/lang/Object;
9: return
public T getValue();
descriptor: ()Ljava/lang/Object;
Code:
0: aload_0
1: getfield #2 // Field value:Ljava/lang/Object;
4: areturn
public void setValue(T);
descriptor: (Ljava/lang/Object;)V
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field value:Ljava/lang/Object;
5: return
}
你问我看的懂么?不好意思,我也看不太懂哈哈,那我们把泛型类的类型限定一下,再看看其中的变化:
GenericClass<T extends Number>
Compiled from "GenericClass.java"
public class com.example.demo.test1.GenericClass<T extends java.lang.Number> {
public com.example.demo.test1.GenericClass(T);
descriptor: (Ljava/lang/Number;)V
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #2 // Field value:Ljava/lang/Number;
9: return
public T getValue();
descriptor: ()Ljava/lang/Number;
Code:
0: aload_0
1: getfield #2 // Field value:Ljava/lang/Number;
4: areturn
public void setValue(T);
descriptor: (Ljava/lang/Number;)V
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field value:Ljava/lang/Number;
5: return
}
这下能看出区别了吧,给注释的地方是jvm给的,并不是我写的,其中已经很明确的表明了返回的类型,由Object变成了Number,可以印证我们上述的观点。
我们通过一个更简单的小案例也可以看出:
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
这个案例的结果是true,也就是说ArrayList<String>和ArrayList<Integer>是相同的类型。
public class GenericDemo {
public static void main(String[] args) {
GenericClass<String> genericClass = new GenericClass<>("张三");
//genericClass.setValue(123); //编译报错
String value = genericClass.getValue();
}
}
我们setValue的时候,编译会直接报红线,那我们getValue()的时候,怎么没要求我们强制转换呢?
Object object = new Object();
String s = (String) object;
我们如果单独这么写,程序是一定要求我们强制转换的呀!,这里是怎么回事?
我们来看一下我们这个GenericDemo类的字节码文件:
Compiled from "GenericDemo.java"
public class com.example.demo.test1.GenericDemo {
public com.example.demo.test1.GenericDemo();
descriptor: ()V
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
Code:
0: new #2 // class com/example/demo/test1/GenericClass
3: dup
4: ldc #3 // String 张三
6: invokespecial #4 // Method com/example/demo/test1/GenericClass."<init>":(Ljava/lang/Object;)V
9: astore_1
10: aload_1
11: invokevirtual #5 // Method com/example/demo/test1/GenericClass.getValue:()Ljava/lang/Object;
14: checkcast #6 // class java/lang/String
17: astore_2
18: return
}
其中有个 大家应该有注意到,在编译的时候jvm已经帮我们做了这一步。
五、泛型使用注意事项
1.不能用基本类型数据实例化类型参数
GenericClass<Integer> g1 = new GenericClass<>(123);
GenericClass<int> g2 = new GenericClass<>(123);//编译错误
上文我们又说到,泛型在编译时实际是类型之间的转换,Integer转Object,Object转Integer,如果允许使用基本类型,就存在基本数据类型与类类型之间转换的问题。
2.运行时类型检查不适用于泛型
GenericClass<Integer> g1 = new GenericClass<>(123);
if(g1 instanceof GenericClass<Integer>) {} //编译错误
上文已经说过JVM是没有泛型的概念的,也就是说并不存在GenericClass<Integer>这个类型,所以比较类型只能比较 g1 instanceof GenericClass。
3.不能实例化泛型类型的数组
GenericClass[] g1 = new GenericClass[5];
GenericClass<String>[] g2 = null;
GenericClass<String>[] g3 = new GenericClass<>[5]; //编译错误
这里涉及到一个泛型设计的原则:如果一段代码在编译时没有提出“未经检查的警告”,则程序在运行时不会引ClassCastException异常。意思就是如果编译时没警告,那么运行时就不会抛出类型转换的异常。
Object[] objects;
GenericClass<String>[] g = new GenericClass<>[5]; //这里编译出错,我们假设是允许的
objects = g;
objects[0] = "123";
objects[1] = 123;
上述案例第二行我们假设是允许这样实例化的,objects=g,那么这两个对象就是一个引用了,我们往objects中分别放入String类型的值和Integer类型的值,那么此时编译不会报错,但是运行时由于g做了泛型限定String,那么一定会抛出类型转换异常,就会违背泛型设计的原则,所以这种情况是不允许的。
那么我们使用下面的定义方式是可以的:
GenericClass<String>[] g = new GenericClass[5];
g[0] = new GenericClass<String>();
g[0] = new GenericClass<Integer>(); //编译报错
4.不能实例化类型参数
public static <T> void show(T value) {
value = new T(); //编译报错
}
我们在上文的泛型方法中实例化一下类型参数,编译时不通过的。
因为我们说过类型擦除,其实这里就是new一个Object,那我们要想这样实现的话就需要类型限定什么我们就new什么,比如类型限定String,那么这里就自动new String(),限定Integer,就自动new Integer(),但是java目前是无法实现的,既然实现不了,那就不能用了。
5.静态方法不能使用类上下文中定义的类型参数
public class GenericClass<T> {
public void test1(T value) {}
public static void test2(T value){} //编译报错
}
静态方法中是不允许使用泛型类定义的类型参数的,如果想使用,那么只能吧静态方法也定义为泛型方法:
public static <T> void test2(T value){}
此时静态方法中的T和泛型类中的T是没有半毛钱的关系的,并不是指一个。
6.泛型在异常中的使用
public class GenericException<T> extends Throwable { } //编译报错
泛型类是不允许继承异常基类的。
public class GenericException<T> {
public void test(){
try {
} catch (T e) { //编译报错
}
}
}
public class GenericException<T extends Throwable> {
public void test(){
try {
} catch (T e) { //编译报错
}
}
}
以上都是规定不允许的,那我们想对异常使用异常该怎么办呢?
public class GenericException<T extends Throwable> {
public void test(T e) throws Throwable {
throw e;
}
}
没错,我们对泛型异常进行抛出是允许的。
7.类型擦除冲突
public class GenericClass<T> {
public boolean equals(T obj) { //编译报错
return super.equals(obj);
}
}
上述案例中我们用类型参数重载类的equals()方法, 编译是报错的,就是因为我们类型擦除的时候参数会变成Object obj,这就和原来已有的equals()方法完全一模一样了,是不允许的,这就是类型擦除冲突。
那么我们可以怎么改呢,我们只需要将类限定为Object的子类就可以了:
public class GenericClass<T extends Number> {
public boolean equals(T obj) {
return super.equals(obj);
}
}
8.另一个泛型原则
另一个泛型原则:想要支持类型擦除的转换,就需要强行限制一个类或类型参数不能同时成为两个接口类型的子类,并且这两个接口是同一个接口的不同参数化。
什么意思呢?我们通过一个案例讲解一下:
interface Show<T> {
void show(T value);
}
public class BaseShowImpl implements Show<String> {
@Override
public void show(String value) {
System.out.println(value);
}
}
public class SubShowImpl extends BaseShowImpl implements Show<Long> {} //编译报错
SubShowImpl这里编译错误并不会因为没有实现方法,而是因为上面的原则,SubShowImpl既继承了BaseShowImpl,又实现了Show<Long>,这两个类的类型参数一个是String,一个是Long,这样是不允许的。
六、类型通配符
我们这里简单介绍一个需要类型通配符需求的案例:
public class Animal {
private String type;
public Animal(String type) {
this.type = type;
}
public void show() {
System.out.println("type:" + type);
}
}
public class Dog extends Animal {
public Dog(String type) {
super(type);
}
}
public class Cat extends Animal {
public Cat(String type) {
super(type);
}
}
public class BigCat extends Cat {
public BigCat(String type) {
super(type);
}
}
这里简单过一下就是定义了一个Animal类,然后两个子类,一个Dog,一个Cat,然后还写了一个Cat的子类BigCat。
我们测试类中先实例化一些数据出来准备测试:
public class GenericDemo {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal("动物"));
List<Cat> cats = new ArrayList<>();
cats.add(new Cat("布偶猫"));
cats.add(new Cat("英短猫"));
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog("金毛"));
dogs.add(new Dog("拉布拉多"));
List<BigCat> bigCats = new ArrayList<>();
bigCats.add(new BigCat("大布偶猫"));
bigCats.add(new BigCat("大英短猫"));
List<String> strList = new ArrayList<>();
strList.add("xxx");
}
}
那么我们现在如果有一个需求:定义一个方法,传入一个List对象,其中保存Animal类或其子类,遍历集合,拿出对象,调用show()方法。
我们先不用泛型来实现以下,很简单嘛:
public static void showAnimalNoGeneric(List list) {
for (int index = 0; index < list.size(); index ++) {
Animal animal = (Animal) list.get(index);
animal.show();
}
}
但是这样有什么问题呢?如果我把strList传入这个方法:
是的,马上就会报错,这也就是我们需要泛型的原因之一:代码安全检查。
下面我们用泛型实现:
public static void showAnimalUseGeneric(List<Animal> list) {
for (Animal animal : list) {
animal.show();
}
}
应该是这样没问题吧?但是当我们使用时:
showAnimalUseGeneric(animals);
showAnimalUseGeneric(cats); //编译报错
showAnimalUseGeneric(dogs); //编译报错
showAnimalUseGeneric(bigCats); //编译报错
下面三个居然编译都不通过,这就是泛型子类型的问题了,我们先讲一下这个问题:
Cat[] catArr = new Cat[5];
Animal[] animalsArr = catArr;
animalsArr[0] = new Animal("XXX");
这段代码中如果没有第二行,直接把Animal对象存入Cat数组,显然是不行的,不能向下转型,就算强转了,也会报错,但是因为我们加了第二行,就会给编译器一个错觉,认为Cat[]数组就是Animal[]数组,所以编译不会出错。
这个数组设计的一个不合理的地方,所以在泛型设计中避免了这个错误,这就是泛型子类型问题。
类型通配符
那我们类型参数既然不能写Animal,那应该写什么呢?这时候通配符就该出场了,这样写是没有任何问题的:
/**使用泛型
* 类型通配符
* 获取对象:只能获取到Object类型
* 存储对象:禁止
**/
public static void showAnimalUseGeneric(List<?> list) {
for (int index = 0; index < list.size(); index ++) {
Animal animal = (Animal) list.get(index);
animal.show();
}
}
但是这样仍然存在问题,因为此时也没有对类型参数加以限制,当你传入strList的时候编译没问题,运行同样会报错。
泛型上下边界
这里就又涉及到新的知识点,泛型上下边界。
/**使用泛型
* 类型通配符上限
* 获取对象:获取到Animal类型,可以限定类型参数
* 存储对象:禁止
**/
public static void showAnimalUseGenericOne(List<? extends Animal> list) {
for (int index = 0; index < list.size(); index ++) {
Animal animal = list.get(index);
animal.show();
}
}
List<? extends Animal> list:这个就是泛型上限,其中限定的类型只能有一个,表示 ? 能匹配的类型要么是指定的类或者接口,要么是其子类或者子接口。
此时再传入strList就直接编译错误了。
有上限就有下限,我们看看泛型下限:
/**使用泛型
* 类型通配符下限
* 获取对象:获取到Object类型,可以限定类型参数
* 存储对象:只能存储限定的下限指定的类型
**/
public static void showAnimalUseGenericTwo(List<? super BigCat> list) {
for (int index = 0; index < list.size(); index ++) {
Animal animal = (Animal) list.get(index);
animal.show();
}
}
List<? super Animal> list:这个就是泛型下限,其中限定的类型只能有一个,表示 ? 能匹配的类型要么是指定的类或者接口,要么是其父类或者父接口。
按照下限的说法,如果我们闲的BigCat的话,那么Dog是不符合的:
showAnimalUseGenericTwo(animals);
showAnimalUseGenericTwo(cats);
showAnimalUseGenericTwo(dogs); //编译报错
showAnimalUseGenericTwo(bigCats);
泛型的知识点大概就到这里了!
欢迎访问我的个人小站哦:我爱吃土豆