本文是小编在回顾JAVA泛型时,发现JAVA泛型的内容比我想象中的丰富,有许多需要注意的内容,本文从基础概念到使用和编写泛型,再到通配符,与反射的使用一起说清楚,让新手也能看得懂
编辑
一.泛型的概念
1.什么是泛型?
泛型就是参数化类型(Parameterized Type) ,即在定义数据结构或方法时不指定 具体的数据类型,而是用一个占位符(如
T
、E
、K
、V
等)来代表类型 ,等到使用时再指定具体的类型。
2.什么是参数化类型?
参数化类型就是把类型作为参数传递给类、接口或方法,使得这些结构在处理不同数据类型时更加灵活。
例如下面的 List就是把String或者Integer就是作为类型参数传入的
ini
List<String> list1 = new ArrayList<>(); // 参数化为 String 类型
List<Integer> list2 = new ArrayList<>(); // 参数化为 Integer 类型
3.为什么需要泛型?
泛型的存在可以保证类型安全 ,避免强制类型转换等问题
为什么能保证类型安全?什么是类型安全??
类型安全是指程序在处理数据类型时发生了不匹配、错误转换、或意外行为,导致程序崩溃或逻辑出错。
举个例子,我写一个ArrayList
typescript
public class ArrayList {
private Object[] array;
private int size;
public void add(Object e) {...}
public void remove(int index) {...}
public Object get(int index) {...}
}
这里面的ArrayList的类型是Object,那么当我们往容器里面放置数据就容易出现 类型安全问题
vbnet
ArrayList list = new ArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String first = (String) list.get(0);
list.add(new Integer(123));
// ERROR: ClassCastException:
String second = (String) list.get(1);
当程序运行时就会报CastException [注意这里不是编译时异常,而是运行时异常,会存在很大的安全隐患]
使用
Object
的代码,类型错误是在运行时才会抛出异常-ClassCastException!
使用泛型就能避免类型安全问题了!
为了解决这个问题,我们需要一个指定数据类型的ArrayList,让程序能够检查数据类型是否正确和 匹配!
例如,单独为ArrayList写一个String版本
arduino
public class StringArrayList {
private String[] array;
private int size;
public void add(String e) {...}
public void remove(int index) {...}
public String get(int index) {...}
}
那么此时如果往这种List里面放非String类型的数据就会狠狠报错了,会在编译时检查报错,而不是等到运行的时候才报错
ini
StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译错误: 不允许放入非String类型:
list.add(new Integer(123));
但是我们Java实际会有几百上千种类,我们不可能写这么多的类,那么此时,泛型就应声而出了!
它提供了一种模板,让类型作为参数传入进来,在编译的时候能检查数据类型,保证类型安全!
arduino
public class ArrayList<T> {
private T[] array;
private int size;
public void add(T e) {...}
public void remove(int index) {...}
public T get(int index) {...}
}
// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();
实现了一次编写模拟,创建任意类型的ArrayList
ini
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // ❌ 编译阶段就报错,防止错误
String s = list.get(0); // ✔️ 无需强转,类型安全
或者其他类的功能!
这样一来,既实现了编写一次,万能匹配,又通过编译器保证了类型安全:这就是泛型。
并且泛型能够自动为我们参数获取进行自动强制数据转换
没有泛型时
ini
ArrayList list = new ArrayList();
list.add("Hello");
list.add(123); // 没有报错,但取出时强转会出错
String s1=(String)list.get(0);//不会报错,但是会需要强制类型转换,要手动
String s = (String) list.get(1); // ❌ 运行时报 ClassCastException
有泛型时
4.泛型概念总结
泛型就是 Java 提供的一种机制:让类或方法的类型参数化 ,以实现 代码复用 + 类型安全 ,并在编译阶段提前发现类型错误。
二.泛型的使用
1.泛型类
泛型类概念
泛型类 是指在定义类的时候,通过类型参数来表示类中某些属性或方法的参数/返回值的类型,使类可以用于多种类型的数据。比如说我们的各自集合容器,List,Set,Map等就是用的泛型。
定义的基本格式
csharp
public class 类名<T> {
// T 可以出现在属性、方法参数、返回值等地方
}
举个栗子:
csharp
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
这就是一个泛型类的例子
- 一般来说我们称呼尖括号<>中的T为泛型标识,也被叫做类型参数
- 类型参数的设置是任意的,都行。
常见的泛型标识:
编辑
- 泛型的类型参数的位置可以在非静态方法的形参(包括非静态成员变量和构造器),非静态成员属性类型,非静态的成员方法的返回值
泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数
泛型类的类型参数 T
是 在实例化对象时才确定 的,而静态成员是 属于类本身(而非实例) 的,在类加载时就已经存在,那时泛型参数还没有被传入,因此静态的方法或者变量无法得知这个类型参数是什么。
举个例子就是
csharp
public class Box<T> {
private T value; // ✅ 合法,T 是实例相关的
public void set(T value) { // ✅ 合法
this.value = value;
}
public T get() { // ✅ 合法
return value;
}
// ❌ 错误:静态变量不能使用 T
// private static T staticValue;
// ❌ 错误:静态方法不能使用 T
// public static void print(T data) {
// System.out.println(data);
// }
}
会报Cannot make a static reference to the non-static type parameter T
但是泛型类中的泛型方法或者方向变量可以使用自己方法签名中的参数类型
举个例子
csharp
public class Box<T> {
public static <E> void print(E data) {
System.out.println(data);
}
}
泛型类可以接受多个类型参数
csharp
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public void setKey(K key) {
this.key = key;
}
public void setValue(V value) {
this.value = value;
}
}
泛型类的实际使用
当我们创建泛型对象的时候需要指定泛型的类型,但是如果不指定,其实会默认是object
csharp
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
ini
Box<String> stringBox = new Box<>();//确定泛型是String
Box objectBox=new Box<>();//默认是Box<Object>
stringBox.set("Hello");
String value = stringBox.get(); // ✅ 不需要强转
如果指定类型参数是String,那么泛型类中的T全部替换成String,并且泛型的特性可以使得往集合里面添加的数据类型必须与指定的相同,不然在编译时就会报错,这就是泛型类型的安全检测机制的实现!
泛型接口
泛型接口的概念
泛型接口 指的是:在定义接口时引入一个或多个类型参数,使得接口的方法在实现时可以操作不同的数据类型,而不用重复定义多个版本的接口。
泛型接口的基本格式
csharp
public interface 接口名<T> {
T methodName(T param);
}
举个栗子
csharp
public interface Converter<T> {
T convert(T input);
}
值得注意的是泛型接口的类型参数,是在接口被继承或者实现的时候才被确定的!
csharp
public interface MyInterface<T> {
// ❌ 错误:接口的字段是 static 的,不能使用 T
// T value = null; // 编译错误
// ✅ 正确:抽象方法可以使用 T
T process(T input);
// ✅ 正确:默认方法可以使用 T
default void show(T input) {
System.out.println("Input: " + input);
}
// ✅ 正确:静态方法不能使用接口的 T,但可以自定义新的泛型
static <E> void print(E data) {
System.out.println("Static Print: " + data);
}
}
泛型接口类型参数的确定
泛型接口类型参数只有在被继承或者实现时确定
1.实现时确定参数类型
typescript
public interface Converter<T> {
T convert(T input);
}
// 实现类指定具体类型:String
public class StringConverter implements Converter<String> {
public String convert(String input) {
return input.toUpperCase();
}
}
2.实现时保留泛型
csharp
public class GenericConverter<T> implements Converter<T> {
public T convert(T input) {
return input; // 假设不做处理
}
}
3.继承的时候保留类型参数
csharp
interface Parent<T> {
void set(T value);
}
interface Child<T> extends Parent<T> {
T get();
}
4.继承时添加类型参数
csharp
interface Parent<T> {
void set(T value);
}
// 子接口增加新的泛型参数 U,同时保留 T
interface Child<T, U> extends Parent<T> {
U get();
}
5.继承时指定类型参数
csharp
interface Parent<T> {
void set(T value);
}
interface StringChild extends Parent<String> {
String get();
}
泛型方法
泛型方法的概念
泛型方法是在方法定义时引入自己的类型参数 的方法,适用于任何类(不论是否是泛型类)。
也就是说,不依赖于类是否是泛型,方法本身就可以是泛型的。
同时方法必须要在返回值前面声明一个来声明它是一个泛型方法。
并且泛型方法如果在方向类中,泛型类的类型参数标识 和泛型方法中的类型参数的标识都是T的话,泛型方法以自己的类型参数为准,与泛型类的类型参数是独立的!
typescript
public <T> T methodName(T param) {
// 方法体
}
必须有泛型标识的方法才是泛型方法,仅仅使用了泛型或者使用了泛型类中的泛型并不是泛型方法
typescript
// 泛型类 Box<T>
public class Box<T> {
private T value;
// ✅ 使用类的泛型参数 T ------ 不是泛型方法
public void setValue(T value) {
this.value = value;
}
// ✅ 使用类的泛型参数 T ------ 不是泛型方法
public T getValue() {
return value;
}
// ✅ 独立声明 <E> ------ 是泛型方法
public <E> void show(E element) {
System.out.println("Show: " + element);
}
// ✅ 泛型方法,返回类型也是泛型(独立的 <U>)
public <U> U echo(U input) {
return input;
}
// ❌ 错误用法(演示):静态方法不能用类的 T
/*
public static void printValue(T value) {
// 编译错误:静态方法不能访问类的泛型参数 T
}
*/
// ✅ 正确:静态方法需自己声明泛型
public static <K> void print(K item) {
System.out.println("Static Print: " + item);
}
}
另外值得注意的是:如果泛型类的泛型标识和泛型方法的泛型标识相同,那么他们之间没有任何关系
csharp
public class Container<T> {
private T value;
public Container(T value) {
this.value = value;
}
public T getValue() {
return value;
}
// 泛型方法,声明了新的 <T>,与类的 <T> 无关
public <T> void printType(T input) {
System.out.println("Method T: " + input.getClass().getName());
System.out.println("Class T: " + value.getClass().getName());
}
}
泛型方法的使用
值得记住的是只有在泛型方法的返回值前加等泛型标识,那它才是泛型方法
举个例子
php
public class Utils {
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
ini
String[] arr = {"a", "b", "c"};
Utils.swap(arr, 0, 2); // 交换 a 和 c
System.out.println(Arrays.toString(arr)); // 输出:[c, b, a]
在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。
当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。
在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类
typescript
public class GenericUtils {
// 泛型方法:返回任意类型的值
public static <T> T identity(T value) {
System.out.println("T 的类型是: " + value.getClass().getSimpleName());
return value;
}
// 泛型方法:从两个值中选择一个
public static <T> T choose(T a, T b) {
System.out.println("choose() 中 T 推断为: " + a.getClass().getSimpleName() + " 和 " + b.getClass().getSimpleName());
return a;
}
public static void main(String[] args) {
// ✅ 自动类型推断:T 是 String
String str = identity("Hello");
// ✅ 显式指定类型参数:T 是 Integer
Integer num = GenericUtils.<Integer>identity(123);
// ✅ 类型推断:两个 Integer 推断为 Integer
Integer a = choose(10, 20);
// ✅ 类型推断:Integer 和 Double 推断为 Number(共同父类)
Number n = choose(10, 3.14); // 推断为 Number
// ✅ 类型推断:String 和 Integer 推断为 Object
Object obj = choose("Test", 100);
// ❌ 错误:T 被推断为 Object,不能赋值给 String(编译错误)
// String wrong = choose("abc", 123); // 编译错误
// ✅ 显式指定为 Object 也可以
Object obj2 = GenericUtils.<Object>choose("abc", 123);
}
}
类型擦除
类型擦除是Java泛型实现的一种方式。简单来说,Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的,使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉,这个过程就称为类型擦除。
这意味着,对于JVM(Java虚拟机)来说,
List<String>
、List<Integer>
和List
在运行时是相同的类型。JVM看到的只是原始类型(Raw Type)。
在java代码编译后是不存在泛型的,每个使用的泛型会被转换为其类型上界,就好像List和List对java虚拟机来说都是List
csharp
public class Main {
public static void main(String[] args) {
Pair<String> p1 = new Pair<>("Hello", "world");
Pair<Integer> p2 = new Pair<>(123, 456);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
System.out.println(c1==c2); // true
System.out.println(c1==Pair.class); // true
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
那问题来了,为什么泛型信息被擦除了,还能保证只能往定义为List的集合里面不饿能加List呢?
其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除
;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。
通俗易懂的例子: 你有一个贴了"苹果专用"标签的篮子。 你在放东西的时候,管家(编译器)会检查,确保你只放苹果。 但是,这个篮子本身(在JVM看来)其实是个普通篮子,啥都能放。 当你从篮子里取苹果时,管家因为记得你贴了"苹果专用",所以它会确保递给你的是一个苹果(如果不是,那之前就该报错了;如果是从一个来源不明的普通篮子硬说是苹果篮子,则取出时管家可能会进行一次确认,这就是强制转换)。
三.泛型通配符
什么是泛型通配符?
泛型通配符,用问号
?
表示,它代表未知的类型 。通配符可以用作类型参数,主要用在方法参数、字段类型或局部变量的类型声明中,但不能用于定义泛型类或泛型方法的类型参数本身(即不能写class MyClass<?>
或public <?> void myMethod()
)
为什么需要泛型通配符
为了解答这个问题,我们需要先了解 泛型的不变性
泛型不变性
泛型的不变性 指的是类似于
List<String>
不是List<Object>
的子类型(即使String
是Object
的子类型)。这种特性称为泛型的不变性 (Invariance)。
为什么会有这种特性呢
给个例子就能懂了
ini
假设 List<String> 是 List<Object> 的子类 (这是错误的假设)
List<String> stringList = new ArrayList<>();
List<Object> objectList = stringList; // 假设这行编译通过
objectList.add(123); // 向一个声称是字符串列表的列表中添加了整数!
String s = stringList.get(0); // ClassCastException!
如果不存在泛型不变性,就会有很大的漏洞
因此,为了使得泛型能够接受一个范围的数据类型,java引入了泛型通配符
泛型通配符的分类
泛型通配符有 3 种形式:
1.无界通配符 :?
2.上界通配符:? extend Type
3.下界通配符:?super Type
无界通配符 :?
含义 :
List<?>
表示 "一个未知类型的列表" 或 "任何类型的列表"。它不关心列表中元素的具体类型。 ?表示的是任意一种数据类型,但是值得注意的是,虽然Object本身是一种数据结构,但是不能代表任意一种数据结构,理论上List<?>是List的父类使用场景 :当你编写的方法只使用
Object
类的功能,或者方法本身不依赖于列表元素的具体类型时。操作限制:
- 读取 (Get) :你可以从
List<?>
中读取元素,但编译器只能保证它们是Object
类型。- 写入 (Add/Put) :你不能 向
List<?>
中添加任何元素(除了null
,因为null
可以是任何类型的成员)。这是因为编译器不知道?
到底代表什么类型,无法保证你添加的元素符合列表的实际类型。
typescriptpublic class WildcardExamples { // 可以打印任何类型的列表 public static void printList(List<?> list) { for (Object elem : list) { // 读取元素时,只能安全地当作 Object System.out.print(elem + " "); } System.out.println(); // list.add("hello"); // 编译错误! 不知道 ? 是什么类型 // list.add(123); // 编译错误! list.add(null); // 合法,但通常没意义 } public static void main(String[] args) { List<String> stringList = Arrays.asList("Apple", "Banana"); List<Integer> integerList = Arrays.asList(1, 2, 3); List<Object> objectList = Arrays.asList(new Object(), "Test"); printList(stringList); // 输出: Apple Banana printList(integerList); // 输出: 1 2 3 printList(objectList); // 输出: java.lang.Object@... Test } }
上界通配符 ? extend Type
含义 :
List<? extends Type>
表示 "一个持有Type
类型或其子类型 的元素的列表"。Type
是这个未知类型的上限。使用场景 :当你需要从一个泛型结构中读取 数据,并且你知道这些数据至少是
Type
类型或其子类型时。这通常用于"生产者" (Producer) 的场景------你从集合中获取数据。操作限制:
读取 (Get) :你可以从
List<? extends Type>
中读取元素,编译器保证它们至少是Type
类型。所以你可以安全地将取出的元素赋值给Type
类型的引用。写入 (Add/Put) :你不能 向
List<? extends Type>
中添加任何有意义的元素(除了null
)。
原因 :编译器只知道列表中的元素是
Type
的某个子类型,但不知道具体是哪个子类型。
- 例如,如果
list
是List<? extends Number>
,它可能是List<Integer>
,也可能是List<Double>
。- 如果你试图
list.add(new Integer(1))
,而这个list
实际上是List<Double>
,那么就会破坏类型安全。- 如果你试图
list.add(new Double(1.0))
,而这个list
实际上是List<Integer>
,同样会破坏类型安全。- 所以编译器干脆禁止添加任何非
null
的元素。
typescriptpublic class UpperBoundWildcard { // 计算列表中所有数字的总和 (作为 double 返回) public static double sumOfList(List<? extends Number> list) { double sum = 0.0; for (Number n : list) { // 读取时,可以安全地视为 Number sum += n.doubleValue(); } // list.add(Integer.valueOf(1)); // 编译错误! // list.add(Double.valueOf(1.0)); // 编译错误! return sum; } public static void main(String[] args) { List<Integer> integerList = Arrays.asList(1, 2, 3); List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3); // List<String> stringList = Arrays.asList("a", "b"); // 不能传递给 sumOfList System.out.println("Sum of integers: " + sumOfList(integerList)); // 输出: Sum of integers: 6.0 System.out.println("Sum of doubles: " + sumOfList(doubleList)); // 输出: Sum of doubles: 6.6 } }
下界通配符 ? super Type
含义 :
List<? super Type>
表示 "一个持有Type
类型或其父类型 (一直到Object
) 的元素的列表"。Type
是这个未知类型的下限。使用场景 :当你需要向一个泛型结构中写入 数据,并且这个结构需要能够接受
Type
类型及其子类型的实例时。这通常用于"消费者" (Consumer) 的场景------你向集合中添加数据。操作限制:
写入 (Add/Put) :你可以安全地向
List<? super Type>
中添加Type
类型及其子类型的实例。
原因 :无论
?
实际代表的是Type
还是Type
的某个父类型 (例如Object
或Number
,如果Type
是Integer
),它总能安全地容纳一个Type
的实例或其子类型的实例。
- 例如,如果
list
是List<? super Integer>
,它可以是List<Integer>
、List<Number>
或List<Object>
。- 你向其中添加
new Integer(1)
总是安全的。- 你向其中添加
new SubInteger()
(假设SubInteger extends Integer
) 也是安全的。读取 (Get) :当你从
List<? super Type>
中读取元素时,编译器只能保证它们是Object
类型。原因 :如果
list
是List<? super Integer>
,它可能是List<Integer>
(此时读取得到Integer
),也可能是List<Number>
(此时读取得到Number
),甚至可能是List<Object>
(此时读取得到Object
)。编译器无法确定一个比Object
更具体的共同类型,所以只能安全地返回Object
。
scsspublic class LowerBoundWildcard { // 向列表中添加指定数量的整数 public static void addNumbers(List<? super Integer> list, int count) { for (int i = 1; i <= count; i++) { list.add(i); // 可以安全地添加 Integer 实例 // list.add(new Object()); // 编译错误! Object 不是 Integer 的子类 } } public static void printObjects(List<? super Integer> list) { for (Object obj : list) { // 读取时只能保证是 Object System.out.print(obj + " "); } System.out.println(); } public static void main(String[] args) { List<Integer> integerList = new ArrayList<>(); List<Number> numberList = new ArrayList<>(); List<Object> objectList = new ArrayList<>(); addNumbers(integerList, 3); // [1, 2, 3] addNumbers(numberList, 2); // [1, 2] (Integer 可以赋给 Number) addNumbers(objectList, 4); // [1, 2, 3, 4] (Integer 可以赋给 Object) System.out.print("Integer List: "); printObjects(integerList); System.out.print("Number List: "); printObjects(numberList); System.out.print("Object List: "); printObjects(objectList); // 读取的例子 // Number n = numberList.get(0); // 编译错误! get() 返回的是 Object Object o = numberList.get(0); // 合法 System.out.println("First element of numberList (as Object): " + o); } }
四.PECS 原则 (Producer Extends, Consumer Super)
Producer Extends (PE) : 如果你的参数化类型代表一个生产者 (即你只需要从集合中读取/获取数据),那么使用
? extends T
。
- 例如,
copy(List<? extends T> src, List<? super T> dest)
中的src
是生产者。Consumer Super (CS) : 如果你的参数化类型代表一个消费者 (即你只需要向集合中写入/添加数据),那么使用
? super T
。
- 例如,
copy(List<? extends T> src, List<? super T> dest)
中的dest
是消费者五.总结
Java泛型提供了参数化类型 ,旨在实现代码复用 和类型安全 。它们允许类、接口和方法操作多种数据类型,并在编译时 检测错误,从而避免运行时
ClassCastException
。核心概念包括泛型类 、泛型接口 、泛型方法 ,以及类型擦除 (编译时将泛型类型转换为其上界,没有上界就是T)。通配符 (?
、? extends Type
、? super Type
)解决了泛型不变性问题,实现了灵活的类型关系,这可以用PECS原则(生产者使用extends,消费者使用super)。写这个查了很多资料,也是一种沉淀,学了很多框架和中间件回来后发现java的一个语法都这么巧妙,真的受益匪浅!