1、概述
泛型的概念:
泛型让类、接口或方法在定义时不指定具体类型,而在使用时再确定类型。
换句话说:泛型让代码在“写的时候”更通用,在“用的时候”更安全。
泛型的好处:类型安全、消除强制类型转换
2、泛型类
2.1 泛型类的定义
● 泛型类的定义语法:
class 类名称<泛型标识, 泛型标识, ...> {
private 泛型标识 变量名;
.....
}
● 常用的泛型标识:T、E、K、V...
泛型类定义举例:
package com.itheima.demo2;
/**
* 泛型类的定义
* @param
* T 创建对象的时候指定具体的类型
*/
public class Generic
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
}
2.2 泛型类的使用
● Java1.7以后,后面的<>中的具体的数据类型可以省略不写
类名<具体的数据类型> 对象名 = new 类名<>();
泛型类的使用举例:
Generic
2.3 泛型类的注意事项
①泛型类在创建对象的时候,没有指定类型,将按照Object类型来操作。
Generic generic = new Generic("ABC");
Object key3 = generic.getKey();
System.out.println("key3:" + key3);
②泛型类,不支持基本数据类型。
Generic
③泛型类虽然指定的类型是不同的,但本质上是同一个类型
System.out.println(intGeneric.getClass());
System.out.println(strGeneric.getClass());//他俩输出结果是一样的
2.4 从泛型类派生的子类
●子类也是泛型类的情况下,子类和父类的泛型类型要一致
class ChildGeneric
●子类不是泛型类的情况下,父类要明确泛型的数据类型
class ChildGeneric extends Generic
举例说明 子类是泛型类:
//这是一个父类 是泛型类
public class Parent
private E value;
public E getValue() {
return value;
}
public void setValue(E value) {
this.value = value;
}
}
//若此时定义了一个子类,要继承父类(标识为T),那它的泛型标识也一定是T,要不然报错
public class ChildFirst
@Override
public T getValue() {
return super.getValue();
}
}
举例说明子类不是泛型类:
//子类不是泛型类,父类是泛型类的话,不能写泛型类型,必须指定父类数据类型,否则报错
public class ChildSecond extends Parent
@Override
public Integer getValue() {
return super.getValue();
}
@Override
public void setValue(Integer value)
3、泛型接口
3.1 泛型接口的定义
● 泛型接口的定义语法
interface 接口名称<泛型标识, 泛型标识, ...> {
泛型标识 方法名();
.....
}
3.2 泛型接口的使用
和前面的泛型类很类似
● 实现类不是泛型类的情况下,接口要明确数据类型
//这是一个泛型接口
public interface Generator
T getKey();
}
//这不是一个泛型类,实现了泛型接口
public class Apple implements Generator
@Override
public String getKey() {
return "hello generic";
}
}
● 实现类也是泛型类的情况下,实现类和接口的泛型类型要一致
//这是一个泛型接口
public interface Generator
T getKey();
}
//这是一个泛型类,实现了泛型接口
public class Pair
@Override
public T getKey() {
return null;
}
}
4、泛型方法
泛型方法的定义:泛型方法,是在调用方法的时候指明泛型的具体类型
你可能会问,刚刚讲泛型类的时候不是已经包含了泛型方法吗?
那个其实是属于泛型类的成员方法,不是泛型方法!
泛型方法是我有泛型列表的,而泛型类的成员方法只在参数列表中有泛型标识
4.1 泛型方法
语法
修饰符
方法体...
}
● public 与返回值中间的
举个例子
public class GenericMethodExample {
// 泛型方法:交换任意类型数组中两个元素的位置
public static
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
后面的 T[] array 是使用这个类型参数。
我们也可以在泛型类中使用泛型方法,泛型方法的泛型标识独立于泛型类(即便是相同标识)
public class Box
private T value;
public Box(T value) {
this.value = value;
}
// 泛型方法(与类的泛型参数 T 无关)
public
System.out.println("打印内容:" + element);
}
}
这个方法在 void 前面又声明了一个新的类型参数
请注意,泛型类中的方法不能是 static,如果它要用类的泛型参数
但是泛型方法可以是 static的(泛型方法的类型参数是方法级别的,不依赖于类的泛型参数。)
4.2 泛型方法与可变参数
语法
public
for (E el : e) {
System.out.println(e);
}
}
举个例子
public static
for (int i = 0; i < e.length; i++) {
System.out.println(e[i]);
}
}
Box
Box
4.3 泛型方法的总结
泛型方法能使方法独立于类而产生变化
如果 static 方法要使用泛型能力,就必须使其成为泛型方法
5、类型通配符
类型通配符一般是使用“?”代替具体的类型实参。
所以,类型通配符是类型实参,而不是类型形参。
我来细细讲讲,这里很绕
这个T,是形参,是在定义过程中写的
class Box
private T value;
public T getFirst() { return value; }
public void setFirst(T value) { this.value = value; }
}
当别人使用时,会传入“实参类型” 这里的 Integer、String 就是 类型实参,代替了 T。
Box
Box
这个< ?>是实参,实在使用过程中写的
public static void showBox(Box> box)
这个< ?>仿佛在说“我接收一个 Box,但是我不知道它装的是什么类型”,? 是“实参中的未知者”。
5.1 类型通配符的引出
①我们定义了一个泛型类 叫Box
public class Box
private E first;
public E getFirst() {
return first;
}
public void setFirst(E first) {
this.first = first;
}
}
②又定义了一个静态方法,用于展示box对象的属性
Number是数值类型的共同父类(抽象类)。
public static void showBox(Box
Number first = box.getFirst();
System.out.println(first);
}
在这里,请注意!!
Box
所以:我写如下代码没问题
Box
box1.setFirst(100);
showBox(box1);
但是写如下代码会出问题
Box
box2.setFirst(200);
showBox(box2);//这里会报错
为什么会报错?因为你往showBox中传递的参数是 Box
那怎么办?用类型通配符
public static void showBox(Box> box) {
Object first = box.getFirst();
System.out.println(first);
}
5.2 类型通配符的上限
定义:
类/接口 extends 实参类型>
根据我们刚刚的例子,可以传递Box
但我们设置了类型通配符的上限后(比如上限是Number):
public static void showBox(Box extends Number> box) {
Number first = box.getFirst();
System.out.println(first);
}
代表着,我只可以接收Box的泛型是Number或其子类的对象
注意一点,我们不能在box中add元素,我举例说明:
我们有一个简单的继承结构:
class Animal {}
class Cat extends Animal {}
class MiniCat extends Cat {}
然后我们准备三种容器:
List
List
List
定义一个方法,能接收装各种“猫或猫的子类”的容器:
public static void readCats(List extends Cat> list) {
Cat c = list.get(0); // ✅ 可以安全读取为 Cat
// list.add(new Cat()); // ❌ 不行
// list.add(new MiniCat()); // ❌ 不行
System.out.println(c.getClass().getSimpleName());
}
为什么 add 不行?
假设你是这样调用方法的:readCats(miniCats);
那么 list 实际指向 List
如果我写了add(new Cat()),那肯定不对,因为Cat不是MiniCat类型和其子类
所以编译器为了安全,干脆禁止任何 add(除了 null)。
但你可以 get,因为无论实际是哪种猫,它都至少是个 Cat。
5.3 类型通配符的下限(1)
定义
类/接口 super 实参类型>
注意一点,我们不能在box中get元素,但是add可以:
和上面一样,我们有一个简单的继承结构:
class Animal {}
class Cat extends Animal {}
class MiniCat extends Cat {}
然后我们准备三种容器:
List
List
List
定义一个方法,能接收装各种“猫或猫的父类”的容器:
public static void addCats(List super Cat> list) {
list.add(new Cat()); // ✅ 可以
list.add(new MiniCat()); // ✅ 也可以
// Cat c = list.get(0); // ❌ 不行,只能当 Object
Object obj = list.get(0); // ✅ 只能读成 Object
}
为什么 add 可以?
假设传入的是:addCats(animals);
list 实际是 List
但如果反过来你想 Cat c = list.get(0),编译器拒绝:
——因为它不知道你传进来的是不是 List
安全起见,只能当作 Object 读取。
5.4 类型通配符的下限(2)
这里讲了应用 以TreeSet为例,其中有两个构造方法
我们讲第一个,传构造器的
我们创建了三个类Animal、Cat、MiniCat
// 父类 Animal
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
'}';
}
}
// 子类 Cat,继承自 Animal
public class Cat extends Animal {
public int age;
public Cat(String name, int age) {
super(name);
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
// 子类 MiniCat,继承自 Cat
public class MiniCat extends Cat {
public int level;
public MiniCat(String name, int age, int level) {
super(name, age);
this.level = level;
}
@Override
public String toString() {
return "MiniCat{" +
"level=" + level +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
又定义了三个比较器,用于TreeSet(一个类对应一个)
class Comparator1 implements Comparator
@Override
public int compare(Animal o1, Animal o2) {
return o1.name.compareTo(o2.name);
}
}
class Comparator2 implements Comparator
@Override
public int compare(Cat o1, Cat o2) {
return o1.age - o2.age;
}
}
class Comparator3 implements Comparator
@Override
public int compare(MiniCat o1, MiniCat o2) {
return o1.level - o2.level;
}
}
然后我们应用
public class Test08 {
public static void main(String[] args) {
TreeSet
treeSet.add(new Cat("jerry", 18));
treeSet.add(new Cat("amy", 22));
treeSet.add(new Cat("frank", 25));
treeSet.add(new Cat("jim", 15));
for (Cat cat : treeSet) {
System.out.println(cat);
}
}
}
发现结果可以按照age排序
如果传入父类Animal的比较器呢?
发现也是可以的
如果传入子类minicat的比较器呢
是不可以的!报错!因为只可以传Cat及其父类。从直觉上来说也容易理解,minicat多了一个属性,TreeSet
6、类型擦除
概念:
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的,但是,泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除。我们称之为——类型擦除。
举个例子:
public static void main(String[] args) {
ArrayList
ArrayList
System.out.println(intList.getClass().getSimpleName());
System.out.println(strList.getClass().getSimpleName());
}
运行后发现
他们的类都是一样的,所以这也就解释了,为什么我们编写代码的时候,看似类型不一样,一个是ArrayList
6.1 无限制类型擦除
什么意思呢?当我的泛型标识为T的时候,在编译后,T的类型会被擦除,变成Object
举例说明:
我定义了一个泛型类
public class Erasure
private T key;
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
}
写了一个方法,输出泛型类的属性和类型
import java.lang.reflect.Field;
public class TestErasure {
public static void main(String[] args) {
// 创建泛型对象
Erasure
erasure.setKey(123);
// 利用反射,获取 Erasure 类的字节码文件的 Class 对象
Class extends Erasure> clz = erasure.getClass();
// 获取所有的成员变量
Field[] declaredFields = clz.getDeclaredFields();
// 打印成员变量的名称和类型
for (Field declaredField : declaredFields) {
System.out.println(declaredField.getName() + ":" + declaredField.getType().getSimpleName());
}
}
}
输出:
所以,如果泛型类的泛型列表只有一个T,那类型擦除之后,确实是类型转换成了Object
6.2 有限制类型擦除
什么意思呢?当我的泛型标识为T entends Number的时候,在编译后,T的类型会被擦除,变成Number
举个例子
我定义了一个泛型类
public class Erasure
private T key;
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
}
写了一个方法,输出泛型类的属性和类型
import java.lang.reflect.Field;
public class TestErasure {
public static void main(String[] args) {
// 创建泛型对象
Erasure
erasure.setKey(123);
// 利用反射,获取 Erasure 类的字节码文件的 Class 对象
Class extends Erasure> clz = erasure.getClass();
// 获取所有的成员变量
Field[] declaredFields = clz.getDeclaredFields();
// 打印成员变量的名称和类型
for (Field declaredField : declaredFields) {
System.out.println(declaredField.getName() + ":" + declaredField.getType().getSimpleName());
}
}
}
输出为:
所以,如果泛型类的泛型列表是T extends Number,那类型擦除之后,确实是类型转换成了Number
上面我说是泛型类,泛型方法也一样!!
6.3 桥接方法
桥接方法存在于泛型接口中
可以看见,泛型标识被擦除成了Object,实现类没有泛型标识,所以不变。除此之外,还产生了一个桥接方法,用于保持接口和类的实现关系
为什么会有这个桥接方法?
擦除后,InfoImpl 这个类的 info(Integer) 方法不再匹配 接口里的 info(Object) 方法签名。
也就是说:
接口现在声明的是 Object info(Object var),
而实现类提供的是 Integer info(Integer var)——
两者在 JVM 层面是不同的签名,
如果编译器不处理,多态调用会失效。
举个例子
定义了一个接口
public interface Info
T info(T t);
}
定义了接口的实现类
public class InfoImpl implements Info
@Override
public Integer info(Integer value) {
return value;
}
}
用反射,写一个方法,获取实现类的方法名、方法类型
Class
// 获取所有的方法
Method[] infoImplMethods = infoClass.getDeclaredMethods();
for (Method method : infoImplMethods) {
// 打印方法名称和方法的返回值类型
System.out.println(method.getName() + ":" + method.getReturnType().getSimpleName());
}
运行结果如下
第二个info就是生成的桥接方法
7、泛型与数组
7.1 数组的协变
讲正式内容之前,先讲一下数组的协变
想象你有个动物园系统:
Cat 是 Animal 的子类。
一个 Animal[]数组 可以装各种动物。
那你说:能不能用一个 Cat[] 去当作 Animal[] 使用?
Java 说:可以!
Cat[] cats = new Cat[3];//这是新创建了一个数组,数组引用是放在cats变量里,指向的是实际的地址Cat[3]
Animal[] animals = cats; // ✅ 允许 将刚才的地址给animals,现在,animals和cats都存放着同样的引用,指向实际的地址Cat[3]
这就是 协变 —— Cat 是 Animal 的子类型,所以 Cat[] 也被当作 Animal[] 的子类型。
所以我可以往数组里new一个cat对象
animals[0] = new Cat(); // cats[0] 也跟着变
由于数组是协变的,我也可以放其他Animal子类对象,于是我放一个dog对象
animals[1] = new Dog();
在编译时期确实没问题,编译器会说:“没毛病啊,Dog 是 Animal 嘛。”
但是在运行的时候出问题了,JVM 在运行时检查到这块堆内存的真实类型是 Cat[],
于是报错!
ArrayStoreException: java.lang.Dog
JVM 拒绝往 Cat 数组里塞一只狗。
所以,数组是协变的,这本身就是不合理的,所以在开发中,即使是能用,也尽量少用或不用
7.2 泛型数组的创建
可以声明带泛型的数组引用,但是不能直接创建带泛型的数组对象
举例:
ArrayList
ArrayList
可以通过 java.lang.reflect.Array 的 newInstance(Class
举例:
我创建了一个操作数组的对象
public class Fruit
private T[] array;
public Fruit(Class
// 通过 Array.newInstance 创建泛型数组
array = (T[]) Array.newInstance(clz, length);
}
/**
* 填充数组
* @param index
* @param item
*/
public void put(int index, T item) {
array[index] = item;
}
/**
* 获取数组元素
* @param index
* @return
*/
public T get(int index) {
return array[index];
}
public T[] getArray() {
return array;
}
}
就可以操作它
Fruit
fruit.put(0, "苹果");
fruit.put(1, "西瓜");
fruit.put(2, "香蕉");
System.out.println(Arrays.toString(fruit.getArray()));
输出:
7.3 泛型数组的陷阱(这里没看懂没关系)
先看一个经典的泛型数组陷阱
public static void main(String[] args) {
ArrayList[] list = new ArrayList[5];
ArrayList
ArrayList
intList.add(100);
list[0] = intList; // ⚠️ 这里的赋值虽然能编译,但留下了隐患
String s = listArr[0].get(0); // 💥 运行时出错
System.out.println(s);
}
我讲一下代码执行的流程:
①ArrayList[] list = new ArrayList[5];
声明一个变量 list,它指向 ArrayList 数组 类型的对象
在堆里创建了一个能装 5 个 ArrayList 对象的数组。
编译器和 JVM 都知道:这个数组的类型是“ArrayList 的数组”。
它可以装任何种类的 ArrayList(比如装 ArrayList
②ArrayList
编译器角度:
我要声明一个变量 listArr,它的类型是 ArrayList
但右边其实给我的 list 是个 ArrayList[](原始类型数组),
规范允许:如果你使用了 raw type(比如 ArrayList),编译器允许把它赋给一个带泛型参数的类型,虽然不安全,我就让它过吧,但我得给个 unchecked warning。”
也就是说
list 的静态类型是 ArrayList[],所以它认为:
“list 是一个能装 ArrayList 对象的数组。”
listArr 的静态类型是 ArrayList
“listArr 是一个能装 ArrayList
运行时角度(JVM):
“行,我就让两个变量 listArr 和 list 指向同一块数组内存。”
于是,list和listArr都指向同一个地址
只是编译器以为” listArr 里面的每个元素都是 ArrayList
而运行起来的 JVM 知道:“其实就是普通的 ArrayList 啊。”
③ArrayList
intList.add(100);
list[0] = intList;
讲一下流程:
在栈内创建一个变量(引用变量),名字叫 intList;在堆内创建一个新的 ArrayList 对象,类型是 ArrayList
然后往 intList 所指向的 ArrayList 对象中添加一个元素 100。所以现在堆内的那个 ArrayList 对象状态是 [100]
数组 list 的元素类型是 ArrayList,每个格子(list[0]、list[1] …)存放的不是对象本身,而是对象的引用(地址)。
④String s = listArr[0].get(0);出问题了!
编译时:
编译器看到 listArr 是 ArrayList
它想:“你取出来的肯定是个 String,我就让你赋给 String s 吧。”
运行时:
JVM 去 listArr[0] 看,发现那是 ArrayList
它取出了 Integer 100,尝试给 String s。
就抛出了异常!ClassCastException。