【Java 数据结构】泛型
目录
一. 什么是泛型
二. 引出泛型
三. 泛型语法
四. 泛型的使用
五. 泛型是如何编译的
5.1 擦除机制
六. 泛型的继承
6.1 泛型类继承非泛型类
6.2 泛型类继承泛型类
6.2.1 父类的同名传递
6.2 2 父类的异名传递
6.2.3 父类固定类型传递
6.2.4 子类添加参数
七. 泛型的上界
7.1语法
7.2 示例
7.2.1 普通示例
7.2.2 复杂示例
八. 泛型方法
8.1 语法
8.2 非泛型类中的泛型方法
8.3 泛型类中的泛型方法
九. 通配符
9.1 通配符的上界
9.2 通配符的下界
9.3 通配符的限制
一. 什么是泛型
泛型是一种在编程语言中允许代码在定义时使用参数化类型的特性,它使得代码可以处理不同类型的数据,同时保持类型安全,提高代码的复用性和可维护性,比如在Java中可以通过泛型定义一个能存储任意类型数据的集合类。
二. 引出泛型
class MyArray{public Object[] arr=new Object[10];public Object getPos(int pos) {return arr[pos];}public void setArr(Object value,int pos){this.arr[pos]=value;}
}public class Test {public static void main(String[] args) {MyArray array =new MyArray();array.setArr(10,0);//父类的对象赋值给子类时,必须要强转Integer j=(Integer)array.getPos(0);System.out.println(j);}}
这样每次取出的值都要进行强制类型转化,不灵活,所以我们需要一个机制能帮助我们使得代码更灵活
三. 泛型语法
class 泛型类名称 < T >{
}
class 泛型类名称 < T ,E,K ....>{
}
注意:
- 这里的T相当于是个占位符 ,表示当前类是一个泛型类
- T也可与是其他的字母
四. 泛型的使用
注意:
- < >中传入的类型必须是引用/自定义数据类型,不能是基本数据类型
- 在实例化泛型类时,后面< >中可以不写(会发生类型推导)
- 泛型类还可以继承,下面会讲到
- 泛型的优点:数据类型参数化,编译时自动进行类型检查和转化
五. 泛型是如何编译的
5.1 擦除机制
泛型擦除是指在编译阶段,编译器会将泛型类型信息擦除,把泛型参数替换为其限定类型(通常是 Object 类型,如果有指定上限则替换为上限类型),并且在必要的地方插入类型转换以保持类型安全,还可以生成桥接方法以保持多态性,使得运行时的字节码中不再包含泛型的类型信息。
擦除前
public class MyArray <T>{public Object[] array=new Object[10];public void setArray(int pose,T value) {this.array[pose] = value;}public T getArray(int pos) {return (T)this.array[pos];}
}
擦除后
public class MyArray{public Object[] array=new Object[10];public void setArray(int pose,Object value) {this.array[pose] = value;}public Object getArray(int pos) {return this.array[pos];}
}
补充:为什么array数组不能写成泛型数组?
答:数组创建规则:在Java中,数组的创建必须使用具体的类型,例如 int[ ] ,String[ ]等,你不能使用泛型类型T[ ] 直接创建数组,因为编译器不知道 T 具体是什么类型,且因为泛型的擦除机制是在编译时发生的,运行时是没有泛型这个概念的
六. 泛型的继承
6.1 泛型类继承非泛型类
class A {public int i;public int getI() {return i;}
}public class B<T> extends A{public Object[] arr=new Object[10];public void func (int pos,T value){this.arr[pos] =value;}@Overridepublic int getI() {return super.getI();}
}
泛型类继承非泛型类时,重写父类的方法时,父类的方法签名不会改变,也不能将它改为泛型方法
6.2 泛型类继承泛型类
6.2.1 父类的同名传递
class A<T>{public T i;public T getI() {return i;}
}
public class B<T> extends A<T>{public Object[] arr=new Object[10];public void func (int pos,T value){this.arr[pos] =value;}@Overridepublic T getI() {return super.getI();}
}
6.2 2 父类的异名传递
class A<U>{public U i;public U getI() {return i;}
}
public class B<T> extends A<T>{public Object[] arr=new Object[10];public void func (int pos,T value){this.arr[pos] =value;}@Overridepublic T getI() {return super.getI();}
}
这里不是修改父类的定义,而是指定类型参数的对应关系,不影响父类的原始定义,只是子类使用时的类型绑定方式,并且编译后会被类型消除,运行时没有区别(编译器理解U)
6.2.3 父类固定类型传递
class A<U>{public U i;public U getI() {return i;}
}
public class B<T> extends A<String>{public Object[] arr=new Object[10];public void func (int pos,T value){this.arr[pos] =value;}@Overridepublic String getI() {return super.getI();}
}
//编译时生成的桥接方法public Object getI() {getI();}
这是实际上是泛型的桥接方法(为什么重写父类后的方法签名不一致却也能构成重写),下面是关于桥接方法的具体介绍:
在子类重写泛型父类的方法时,尽管在源代码中可以明确地指定重写方法的返回类型为具体的类型参数(如 SubGenericClass 中 getValue 方法返回 Integer ),但由于类型擦除,在字节码层面父类的方法签名会变为返回 Object 类型。为了保证多态性,即根据对象的实际类型来正确调用重写后的方法,就需要桥接方法来进行中间转换。
桥接方法的工作方式
编译器会在子类中生成桥接方法,其方法签名与父类被擦除后的方法签名一致(返回类型为擦除后的类型)。在桥接方法内部,会调用子类中实际重写的方法。如前面例子中, SubGenericClass 的桥接方法 public Object getValue() 会调用子类真正的 public Integer getValue() 方法,这样在运行时,无论通过父类引用还是子类引用调用 getValue 方法,都能正确地根据对象的实际类型来执行子类重写后的方法,实现多态性
6.2.4 子类添加参数
class A<U>{public U i;public U getI() {return i;}
}
public class B<T,K> extends A<T>{public K m;public Object[] arr=new Object[10];public void func (int pos,T value){this.arr[pos] =value;}public K func2(){return m;}@Overridepublic T getI() {return super.getI();}
}
总结:
- 在泛型子类继承泛型父类在签名时,父类的泛型参数要与子类泛型参数的其中一个参数名相同(子类必须为父类的每个参数提供类型),也可以给父类指定具体类型
- 在Java中,会根据子类的参数传递给父类参数,在继承的关系中父类参数的具体类型由子类决定
七. 泛型的上界
泛型的上界是在定义泛型类型时,对泛型参数的类型进行限制,指定其必须是某个类或接口的子类或实现类。
7.1语法
class 泛型类名称 <类型形参 extends 类型边界>{
......
}
7.2 示例
7.2.1 普通示例
7.2.2 复杂示例
类型边界也可以是接口,表示只有实现了该接口的类型才能传入该参数
class 泛型类名称 <类型形参 extends 接口>{
......
}
小测试:写一个泛型类,在其中定义一个方法来求任意类型数组的最大值
public class A <T extends Comparable<T>>{public T Max(T[] arr){T max=arr[0];for (int i = 0; i < arr.length; i++) {if(arr[i].compareTo(max)>0){max=arr[i];}}return max;}}class test {public static void main(String[] args) {Integer[] arr=new Integer[]{1,24,26,75,35,56};A<Integer> a=new A<>();Integer m=a.Max(arr);System.out.println(m);}
}
注意:
- 传入的类型必须是类型边界的子类或者类型边界本身,如果extends后面是接口的话,传入的参数类型必须是实现了该接口的类
- 如果没有指定类型边界则默认为object类
- 泛型没有下界
八. 泛型方法
泛型方法是Java中一种允许在方法中使用泛型类型参数的特性。它可以使方法更具通用性,能够处理不同类型的数据,而不需要为每种数据类型都编写重复的代码。
8.1 语法
方法限定符 <类型形参列表> 返回值类型方法名称(形参列表) {
...
}
8.2 非泛型类中的泛型方法
public class A {public <T> T func(T i){return i;}public static <T> T func(){return null;}public static void main(String[] args) {A a=new A();Integer i=10;Integer b= a.func(i);//类型推导Integer c= a.<Integer>func(i);//非类型推导Integer m=A.<Integer>func();//非类型推导}
}
注意:
- 泛型方法会根据你传入的参数来进行类型推导,推导出第一个<T>中的类型形参
- 如果是无参数的泛型方法就必须要在调用时显式指定类型
8.3 泛型类中的泛型方法
class GenericClass<T> {private T data;public GenericClass(T data) {this.data = data;}// 泛型方法也定义了T作为类型参数,这里会隐藏类的Tpublic <T> void printType(T item) {System.out.println("方法中的T类型: " + item.getClass().getName());// 这里的data是类的成员变量,类型是类定义的TSystem.out.println("类中的T类型: " + data.getClass().getName());}
}public class Main {public static void main(String[] args) {GenericClass<Integer> generic = new GenericClass<>(10);generic.printType("Hello");}
}
注意:
- 当泛型类的参数类型名与泛型方法名一致时,泛型方法会隐藏泛型类的参数类型,即泛型方法中的T是在调用方法时传入的类型参数,而不是泛型类的参数类型
- 为了避免混淆,泛型方法的参数类型名尽量与泛型类的参数类型名不同
九. 通配符
在Java泛型中,通配符 ?是一种用于表示不确定类型的特殊语法,它使得泛型代码更加灵活和通用
9.1 通配符的上界
<? extends 上界>
传入的数据类型必须是上界本身或者上界的子类
import java.util.ArrayList;
import java.util.List;class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}public class UpperBoundWildcardExample {public static double calculateTotalWeight(List<? extends Animal> animals) {double totalWeight = 0.0;// 假设每个动物都有weight属性,这里简单模拟for (Animal animal : animals) {// 可以安全地读取Animal及其子类对象,因为上界是AnimaltotalWeight += 1.0; }return totalWeight;}public static void main(String[] args) {List<Cat> catList = new ArrayList<>();catList.add(new Cat());List<Dog> dogList = new ArrayList<>();dogList.add(new Dog());double catTotalWeight = calculateTotalWeight(catList);double dogTotalWeight = calculateTotalWeight(dogList);System.out.println("猫的总重量: " + catTotalWeight);System.out.println("狗的总重量: " + dogTotalWeight);}
}
注意:使用了通配符的上界,就只能读取数据,不能写入数据,因为我们不能保证传入的是那种参数类型(是上界本身还是上界的子类是不确定的),只要使用了?,编译器在编译时的定义是模糊的(即使传入的是具体的参数类型)
9.2 通配符的下界
<? super 下界>
传入的数据类型必须是上界本身或者上界的父类
import java.util.ArrayList;
import java.util.List;class Fruit {}
class Apple extends Fruit {}
class RedApple extends Apple {}public class LowerBoundWildcardExample {public static void addApple(List<? super Apple> fruitList) {Apple apple = new Apple();// 可以安全地添加Apple及其子类对象到列表中fruitList.add(apple); RedApple redApple = new RedApple();fruitList.add(redApple); }public static void main(String[] args) {List<Apple> appleList = new ArrayList<>();List<Fruit> fruitList = new ArrayList<>();addApple(appleList);addApple(fruitList);System.out.println("苹果列表: " + appleList);System.out.println("水果列表: " + fruitList);}
}
注意:使用了通配符的上界,就只能写入数据,不能读取数据,但是写入数据只能写入下界的子类对象,因为我们不能保证传入的是那种参数类型
9.3 通配符的限制
- 不能用于定义泛型类或方法:
通配符只能在方法的参数、局部变量或表达式中使用,不能用于定义泛型类、泛型接口或泛型方法的类型参数。例如, class MyClass<?> 是不合法的定义。
- 不能直接实例化通配符类型:
List<?> list = new ArrayList<?>(); // 编译错误
List<? extends Number> numbers = new ArrayList<? extends Number>(); // 编译错误List<?> list = new ArrayList<String>(); // 合法
List<? extends Number> numbers = new ArrayList<Integer>(); // 合法
- 方法调用限制:
只是可以调用与元素类型无关的通用方法
List<?> list = new ArrayList<String>();// ✅ 可以调用的方法(不依赖元素类型):list.size(); // 获取大小list.isEmpty(); // 判断空list.clear(); // 清空集合Iterator<?> it = list.iterator(); // 获取迭代器// ❌ 不能调用的方法(依赖元素类型):list.add("hello"); // 编译错误list.add(new Object()); // 编译错误list.remove("hello"); // 编译错误(但实际可以调用,特殊例外)
- 类型引用限制:
不能直接引用通配符类型,?只能存在于<>中
class MyClass<?> { ... } // 编译错误
? myVariable; // 编译错误
- 数组创建限制:
List<?>[] array = new List<?>[10]; // 编译错误