Java泛型
# 1. 泛型概述
Java 推出泛型以前,程序员可以构建一个存储任意的数据类型的 Object 集合,而在使用该集合的过程中,需要程序员明确知道存储元素的数据类型,否则很容易引发 ClassCastException 异常。JDK 1.5 后 Java 引入泛型特性,泛型提供了编译时类型安全监测机制,允许我们在编译时检测到非法的类型数据结构。
# 1.1 泛型的定义
泛型编程是一种编程范式,允许程序员编写与具体类型无关的代码。这种做法提高了代码的复用性,并且能够提供更严格的类型检查机制。在Java、C#等现代编程语言中,泛型是一个核心特性,广泛应用于集合框架、算法实现及各种设计模式中。
泛型的本质
泛型的本质是参数化类型(Parameterized Types),也被称为参数化多态
。它允许在定义类、接口和方法时使用类型参数(Type Parameters),这些类型参数在使用时会被实际的类型替换,从而实现代码的通用性和类型安全。
参数化类型的概念
参数化类型是泛型编程的基础。简单来说,就是将类型定义为参数
,使得在定义一个类、接口或方法时不指定具体的数据类型,而是在使用时指定。这类似于定义方法时使用的形参,但用于数据类型。
泛型的关键概念
类型变量(Type Variables):在泛型代码定义中作为占位符使用的符号,例如
T
、E
、K
、V
等。它们在定义泛型时不代表任何具体类型,而是代表将来某个具体类型。类型参数(Type Arguments):在使用泛型时,实际传入的具体类型。例如在
List<String>
中,String
是类型参数,它具体化了类型变量E
。
泛型的应用
泛型可以应用于类、接口和方法中,通过使用类型变量,它们可以变成参数化的。这使得同一个类、接口或方法可以适用于多种数据类型。
泛型类:定义时使用泛型,如
List<E>
,可以存储任意类型的对象,具体类型在使用时确定。泛型接口:类似于泛型类,接口中的方法定义可以使用类型变量,如
Iterable<T>
。泛型方法:在方法定义中使用一个或多个类型变量,这些类型变量可以是方法返回类型、参数类型或方法体内局部变量的类型。
# 1.2 Java 泛型的作用
Java层面:数据存储安全问题:如类型参数,防止不同类型的数据,放入相同的类型里面。
生活层面:物品存储安全问题:如药品标签,防止不同标签的药品,放入相同的标签里面。
1. 数据类型安全检查(编译期)
使用泛型后,能让编译器在编译期间,对传入的类型参数进行检查,判断容器操作是否合法。将运行期的 ClassCastException 等错误,转移到编译时发现。
2. 消除类型强制转换(编译期、手动)
在 JDK1.5 之前,Java 容器对于存入数据是通过将数据类型向上转型为 Object 类型来实现的,因此取出来的时候需要手动的强制转换,麻烦且不安全。加入泛型后,强制转换都是自动的和隐式的,提高代码的重用率、简洁度和优雅度。
3. 复用代码(复用思想)
如果代码中存在对大量的通用类型(如 Object 类或 Compare 接口)的强制类型转换,会产生大量的重复代码,泛型能很好的解决这个问题。使用泛型,通过继承实现通用泛型父类的方式,以达到代码复用的效果。
# 2. Java泛型的特点
# 2.1 类型擦除
Java 选择的泛型实现方式叫做 “ 类型擦除式泛型 ”(Type Erasure Generics)。
类型擦除的过程:
- 编译期:Java 的泛型是在编译期实现的,对传入的类型参数进行安全检查。
- 编译后:编译后的字节码文件中没有泛型,源代码中全部的泛型被替换为裸类型,并且在必要地方插入强制转换的代码。
泛型技术实际上是Java 语言的一颗语法糖,其在Java 语言中实现方法称为类型擦除,是一种伪泛型策略
。
语法糖:计算机术语,指在语言中添加某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。一般语法糖能够简练语言表达的复杂性、增加程序可读性,减少代码出错。
class Demo {
// 定义两个具有不同泛型类型的静态字段
static List<String> listString = new ArrayList<>();
static List<Integer> listInteger = new ArrayList<>();
public static void main(String[] args) throws NoSuchFieldException {
// 比较listString和listInteger在运行时的类信息是否相同
System.out.println("类信息是否相同 = " + (listString.getClass() == listInteger.getClass()));
// 反射获取listString字段的Field对象
Field fieldListStr = Demo.class.getDeclaredField("listString");
// 打印listString的原始(擦除后)类型
System.out.println("擦除后字段类型 = " + fieldListStr.getType());
// 打印listString的泛型类型,展示泛型类型信息
System.out.println("字段泛型类型 = " + fieldListStr.getGenericType());
// 将listString的泛型类型转换为ParameterizedType以访问其实际类型参数
ParameterizedType genericType1 = (ParameterizedType)fieldListStr.getGenericType();
// 打印listString的实际类型参数
System.out.println("字段实际参数的类型 = " + genericType1.getActualTypeArguments()[0]);
System.out.println("-------------------------------");
// 重复listInteger的处理过程
Field fieldListInt = Demo.class.getDeclaredField("listInteger");
System.out.println("擦除后字段类型 = " + fieldListInt.getType());
System.out.println("字段泛型类型 = " + fieldListInt.getGenericType());
ParameterizedType genericType2 = (ParameterizedType)fieldListInt.getGenericType();
System.out.println("字段实际参数的类型 = " + genericType2.getActualTypeArguments()[0]);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
如上述源代码中定义 List<String>
和 List<Integer>
类型,其类的信息相同,在编译后的字节码文件中,通过反射,我们确定这两个字段的字段类型都是 List。实际上JVM看到的只是 List,而由泛型附加的类型信息对JVM是看不到的,将一段有泛型信息的 Java 源代码编译成 Class 文件后,对其反编译形成新源代码,就会发现泛型都不见了。
# 2.2 类型擦除原则
删除泛型说明:意味着在编译过程中,会移除所有的泛型信息,具体表现为将所有的参数化类型(如
List<String>
或Map<K, V>
)中的泛型参数(<String>
,<K, V>
)删除,仅保留原始类型(如List
,Map
)。类型参数替换:
- 如果类型参数没有指定上界(即未通过extends关键字指明边界),则会被替换为
Object
。 - 如果类型参数有限制,即有指定上界,那么类型参数会被替换为它的上界。例如,对于
<T extends Number>
,T
会被替换成Number
。如果有多个上界,如<T extends Number & Comparable>
, 则T
会被替换为它的第一个边界Number
。
- 如果类型参数没有指定上界(即未通过extends关键字指明边界),则会被替换为
保证类型安全:尽管泛型信息在运行时被擦除,但为了保证类型安全,编译器会在必要的位置插入强制类型转换代码。这些类型转换是根据泛型的编译时类型信息自动生成的,确保了运行时的安全性。
保证泛型多态:为了保持泛型代码的多态性,编译器会在类型擦除过程中自动生成所谓的桥接方法(Bridge Methods)。这些方法允许泛型类或泛型方法在继承或实现时能够正确地覆盖或实现泛型接口中的方法。
类型擦除的具体应用场景:
无限制类型参数:当一个类型参数没有任何限制时(即没有使用extends关键字指定上界),它会被替换为
Object
。例如,一个简单的泛型类<T>
中的T
会在运行时被视为Object
。有限制类型参数:当类型参数被限制为某个特定类型或某些类型的共同父类时,这个类型参数会被替换为指定的上界类型。例如,
<T extends Serializable>
中的T
会被替换为Serializable
。
# 2.3 类型擦除带来的影响
类型擦除导致在尝试重载方法时可能会遇到问题。由于泛型信息在编译后被擦除,泛型类型被替换为它们的原始类型(裸类型)或限定边界类型,这就意味着在泛型类型上的重载可能会导致编译器视它们为相同的方法签名。
例子 1:无法根据泛型类型重载
class Demo { // 编译报错
public static Void method(List<String> stringList) {}
public static Void method(List<Integer> integerList) {}
}
2
3
4
在上述例子中,尽管List<String>
和List<Integer>
在源代码中看起来是不同的参数类型,但因为类型擦除的作用,它们都被替换成了原始类型List
。这导致两个方法的参数类型在编译后变得相同,从而违反了Java方法重载的规则(即方法名称相同,但参数类型、个数或顺序至少有一项不同),结果是编译不通过。
例子 2:基于返回类型的方法重载
class Demo { // 在某些编译器中编译通过
public static String method(List<String> stringList) { return ""; }
public static Integer method(List<Integer> integerList) { return 0; }
}
2
3
4
虽然方法的返回值不参与方法的特征签名,即不参与重载选择(相同的特征签名导致无法重载),但是对于 Class 文件,因为返回值不同,使得方法的描述符不同,因而可以合法的存在于同一个 Class文件内。(如引入新的属性 Signature 等,解决伴随泛型而来的参数类型的识别问题)。
上述源代码在 IDEA 编辑器中是不通过的,但是在 JDK 编译器是可以,因为 JDK 编译器是根据方法返回值 + 方法名 + 参数列表来描述每一个方法的。
# 2.4 弱记忆
在Java的泛型实现中,尽管类型擦除机制导致了运行时泛型信息的丢失,JVM为了保持泛型的类型兼容性和提供泛型信息给运行时环境,实际上在Class文件中通过某些机制弱化地保留了泛型信息。这种保留泛型信息的特性被称为“弱记忆”。
Signature属性:保留泛型信息的关键
Signature属性是Class文件中一个重要的属性,用于在字节码层面保留方法和类的泛型签名信息。这包括泛型类型参数、泛型方法的参数类型以及返回类型等泛型相关信息。尽管泛型参数在运行时被擦除为其原生类型或Object,Signature属性确保了泛型的声明信息不会完全丢失,而是以元数据的形式保留下来。
类文件中的泛型信息
通过Signature属性,Java虚拟机和运行时环境能够获取关于泛型声明的详细信息,包括:
- 类或接口的泛型类型参数。
- 方法的泛型类型参数,包括方法参数的泛型类型和返回类型。
- 字段的泛型类型。
总结
Java虚拟机(JVM)通过在Class文件中保留泛型的Signature属性,实现了对泛型信息的“弱记忆”,这保证了即便在类型擦除机制下,泛型的元数据信息仍然可以被保留和使用。这意味着我们可以在运行时通过反射访问这些泛型信息,进行更为动态的操作。例如,框架开发中经常需要根据泛型类型进行反射操作,以实现类型的自动注入、数据绑定等功能。
# 3. 泛型的使用
在使用泛型时,选择合适的类型变量名称可以提高代码的可读性。虽然类型变量的命名是自由的,但以下是一些约定俗成的命名:
类型变量 | 作用 |
---|---|
E | 元素(Element),主要由 Java 集合(Collections)框架使用 |
K V | 键(Key)值(Value),主要用于表示 Java 键值中的 Key Value |
N | 数字(Number),主要用于表示数字 |
T S U V | 表示通用型参数 |
? | 表示不确定的 Java 类型 |
# 3.1 泛型类
泛型类是在类的定义中使用一个或多个类型参数的类。这些类型参数在类实例化时指定,使得相同的类可以用于不同的类型。
类名<T> 对象名 = new 类名<T>();
从JDK 1.7开始,可以使用钻石操作符(<>)简化实例化时的声明。
普通泛型类与多元泛型类
- 普通泛型类:使用单一的类型参数。
- 多元泛型类:使用多个类型参数。
# 3.1.1 泛型类的使用
普通泛型类 VS 多元泛型类【要点:类型变量的个数】
// 普通泛型类
class Person<T>{ // T是type的简称【实例化时,必须指定T的具体类型】
private T name; // 成员变量:属性key的类型T由外部指定
public Person(T name) { // 构造方法:形参key的类型T由外部指定
this.name = name;
}
public T getName(){ // 普通get方法:返回值类型T由外部指定
return name;
}
}
// 多元泛型类
class Person<K,V>{ // 指定了多个泛型类型
public K name; // 类型变量 K 的类型,由外部决定
public V age; // 类型变量 V 的类型,由外部决定
public Person(K name, V age){
this.name = name;
this.age = age;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
类型参数的限制
泛型的类型参数必须是类类型(包括自定义类、接口等),不能是基本数据类型。如果需要使用基本数据类型,可以使用它们的包装类(如Integer
用于int
)。
# 3.1.2 泛型类派生子类注意点
子类也是泛型类时:
- 子类可以拥有自己的泛型参数。
- 如果父类是泛型类,子类在继承父类时需要指明父类的泛型类型,这可以是子类自己的泛型类型参数。
- 对于继承自多元泛型父类的情况,子类需要声明足够的泛型参数以涵盖父类中定义的所有泛型参数,但子类也可以声明更多自己特有的泛型参数。
子类不是泛型类时:
- 即使父类是泛型类,子类在继承时必须为父类的泛型类型提供具体的类型实参,而不是类型参数。
- 这意味着子类成为了一个非泛型类,但是它是从具体化的泛型父类派生出来的。
1. 泛型子类示例:
// 假设Person是一个泛型类
class Child<T, E, K> extends Person<T>{
// Child类是泛型类,并且在继承Person类时,为Person指定了泛型类型T
// 这里的T是Child自己的泛型类型参数
// ...
}
2
3
4
5
6
在这个例子中,Child
类继承自泛型类Person<T>
,并且Child
自身也定义了三个类型参数T, E, K
。在继承Person
时,Child
使用了自己的泛型参数T
作为Person
的类型参数。这是允许的,因为Child
覆盖了Person
的泛型参数。
2. 非泛型子类示例:
// 继续假设Person是一个泛型类
class Child extends Person<String>{
// Child类不是泛型类,但是明确指定了继承自Person<String>
// 即指定了Person的泛型类型为String
// ...
}
2
3
4
5
6
在这个例子中,Child
没有定义自己的泛型参数,而是直接指定了父类Person
的泛型类型为String
。这使得Child
成为一个具体类型的类,而不是泛型类。
# 3.2 泛型接口
泛型类型用于接口的定义中,被称为泛型接口。【泛型接口与泛型类的定义和使用基本相同】。
使用语法:接口名 <类型变量,类型变量 ...> 对象名 = new 接口名<>();
# 3.2.1 泛型接口的使用
interface Person<T>{ // 在接口上定义泛型
public T getName(); // 定义抽象方法,抽象方法的返回值就是泛型类型
}
class PersonImpl<T> implements Person<T>{ // 定义泛型接口的子类
private T name ; // 成员变量:属性name的类型T由外部指定
public PersonImpl(T name){ // 构造方法:形参name的类型T由外部指定
this.name = name; ;
}
public void setName(T name){ // 普通set方法,形参name的类型T由外部指定
this.name = name ;
}
public T getName(){ // 普通get方法,返回值的类型T由外部指定
return this.name;
}
}
public class GenericsDemo{
public static void main(String arsg[]){
Person<String> p = new PersonImpl<String>("刘德华"); // 声明接口对象
System.out.println("name = " + p.getName()); // name = 刘德华
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3.2.2 泛型接口实现类注意点
- 实现接口的类是泛型类时:泛型类的泛型变量要和接口类的一致【对于多元泛型类,至少要包含接口的全部泛型变量】。
- 实现接口的类不是泛型类时:泛型接口必须指明具体的数据类型。
// 续用上例的 Person 接口
// 1. 实现泛型接口的类,是泛型类时:
// 多元泛型类的类型变量集合A ={T、E、K},接口的类型变量有B = {T},集合 A 至少包含 B。
class PersonImpl<T,E,K> implements Person<T>{
// ...
}
// 2. 实现泛型接口的类,不是泛型类时:
// 需要明确实现泛型接口的数据类型。
class PersonImpl implements Person<String>{
// ...
}
2
3
4
5
6
7
8
9
10
11
# 3.3 泛型通配符
# 3.3.1 前言案例
定义了一个泛型类Person
,它接受一个泛型参数T
,用来指定成员变量age
的类型。
// 定义一个泛型类Person,其中T是一个类型参数,用于指定成员变量age的类型
class Person<T> {
public T age;
// 构造方法,接收一个类型为T的参数
public Person(T age) {
this.age = age;
}
}
public class GenericsDemo {
// 定义一个显示Person对象年龄的方法,接收的参数限定为Person<Number>
// 这意味着,只接受Number类型或其子类类型作为T的Person对象
public void showName(Person<Number> p) {
System.out.println("p.age = " + p.age);
}
public static void main(String[] args) {
// 创建两个Person对象,分别指定泛型类型为Integer和Number
Person<Integer> ageInteger = new Person<>(123); // 使用Integer类型
Person<Number> ageNumber = new Person<>(456); // 使用Number类型
GenericsDemo demo = new GenericsDemo();
// 这行可以正常工作,因为ageNumber的类型完全匹配showName方法的参数类型
demo.showName(ageNumber);
// 这行会导致编译错误,因为虽然Integer是Number的子类型,但泛型类型不支持协变
// 即Person<Integer>不被认为是Person<Number>的子类型
// demo.showName(ageInteger); // 编译错误
// 为了解决上述问题,引入泛型通配符,修改showName方法如下
}
// 使用泛型通配符 ? extends Number,使得方法可以接受任何Number及其子类型的Person对象
public void showNameWithWildcard(Person<? extends Number> p) {
System.out.println("p.age with wildcard = " + p.age);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
通过上面这个小案例,泛型通配符横空出世!!!
# 3.3.2 什么是类型通配符
在现实编码中,希望泛型能够处理某一类型范围内的类型参数,比如某个泛型类及它的子类(或泛型类及它的父类),为此 Java 引入了泛型通配符这个概念。
- <?> 无限制通配符。
- <? extends E>
extends
关键字 声明类型的上界
,?替换的参数化类型
可以是E 或者 E 的子类类型
。 - <? super E>
super
关键字 声明类型的下界
,?替换的参数化类型
可以是E 或者 E 的父类类型
。
注意:?类型通配符,一般是代替具体的类型实参【重要的话,自己读三遍哦】
类型通配符用 ?代替,且 ?是类型实参。此处的 ?和 Number、String、Integer 一样,都是一种实际的类型。当具体类型不确定时候,用 ? 代替具体类型实参。
// 接收参数的泛型类Person,其类型实参不具体指定,用?代替。完美解决
public void showName(Person<?> p){
System.out.println("p.age = " + p.age);
}
2
3
4
# 3.4 泛型上下界
Java 选择的泛型实现是类型擦除,类型擦除存在隐含的转换问题,解决办法就是控制参数类型的转换边界。
泛型 上界
语法:类/接口 <? extends 实参类型T>。
泛型的上界是实参类型 T,?
只能接受 T 类型及其子类
。
// 上界是Number类,?只能接受Number类型及其子类(Integer,Double...都是Number的子类)
class Generic<T extends Number>{
public T number; // 定义泛型变量
public Generic(T number){
this.number = number;
}
}
public class GenericsDemo{
public static void main(String args[]){
Generic<Integer> i1 = new Generic<>(1); // Integer的泛型对象
Generic<Double> i2 = new Generic<>(1.1); // Double的泛型对象
Generic<Long> i3 = new Generic<>(1L); // Long的泛型对象
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
泛型 下界
语法:类/接口 <? super 实参类型T>。
泛型的下届是实参类型 T,?
只能接受 T 类型及其父类
。
class Generic<T>{
public T name;
public Generic(T name){
this.name = name;
}
}
public class GenericsDemo{
// 下界是String类,?只能接收String类型及其父类(String类的父类只有Object类)
public static void fun(Generic<? super String> temp){
System.out.print(temp + ", ") ;
}
public static void main(String args[]){
Info<String> i1 = new Generic<String>("happy") ; // String的泛型对象
Info<Object> i2 = new Generic<Object>(new Object()) ; // Object的泛型对象
fun(i1) ;
fun(i2) ;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
泛型多重限制
ava还允许在泛型参数上设置多重边界,使用&
符号连接。这意味着类型参数必须满足所有指定的边界条件。注意,这种多重限定只适用于类型参数而不是通配符
interface A {}
interface B {}
class MultiLimit {
// 方法中的泛型T同时受到了A和B两个接口的限制
public <T extends A & B> void method(T t) {
// T类型的对象t在这里可以使用A和B接口定义的所有方法
}
}
2
3
4
5
6
7
8
9
# 3.5 泛型方法
# 3.5.1 泛型方法与可变参数
泛型类型用于方法的定义中,被称为泛型方法。使用语法:
// 类型变量 T,可以随便写为任意标识,如 T、E、K、V...
修饰符 <T,E, ...> 返回值类型 方法名(形参列表) {
方法体...
}
// 泛型方法
public <T> void getName(){
}
// 泛型方法【可变参数】
public <T> void getName(T... t){
}
// 不是泛型方法【仅返回值和参数列表使用了泛型,其单独出现也不是】
public T getName(T t){
}
2
3
4
5
6
7
8
9
10
11
12
13
笔记
只有在修饰符 与 返回值中间,声明了<类型变量...>,该方法才是泛型方法,才能在方法中使用泛型类型 T。
否则,只是普通方法使用了泛型(返回值是泛型,或者参数是泛型)。
可变参数,即参数列表使用了如 (T... t) 这样的语法。
# 3.5.2 泛型类中的静态泛型方法
注意:静态方法无法访问类上定义的泛型
;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上
。
// 泛型类
class StaticGenerator<T> {
// 泛型类内部,定义普通方法/泛型方法
public void show(T t){} // 正确
public <T> void show(T t){} // 正确
// 泛型类内部,定义静态方法,使用泛型
public static void show(T t){} // 报错
// 泛型类内部,定义泛型静态方法,使用泛型
public static <T> void show(T t){} // 正确
}
2
3
4
5
6
7
8
9
10
静态方法设计的初衷是无须实例化,直接通过“类名.方法名”来调用的方法。
笔记
- 泛型类内部,要使用带类型变量的非泛型方法,要先通过实例化类,才能确定类型变量具体指定的类型参数,因此不能直接通过“类名.方法名”的方式调用静态方法,否则不能确定具体的类型参数。与静态方法的设计初衷相违背。
- 泛型类内部,要使用带类型变量的泛型方法,会在类调用方法时同时确定具体的类型参数(泛型方法声时已定义),因此不用创建类实例,直接通过“类名.方法名”的方式调用静态方法。符合静态方法的设计初衷。
泛型类中,静态方法要使用泛型的话,必须将静态方法定义成泛型方法
泛型方法能使方法独立于类而产生变化。以下是一个基本的指导原则:
- 无论何时,如果你能做到,你就该尽量使用泛型方法。
- 如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个 static 的方法而已,无法访问泛型类型的参数。所以如果 static 方法要使用泛型能力,就必须使其成为泛型方法。
# 3.6 泛型数组
# 3.6.1 泛型类型不能实例化
不能直接创建带泛型的数组对象,但是可以声明带泛型的数组引用
// 不能创建:不能直接创建带泛型的数组对象
List<String>[] ls1 = new ArrayList<String>[10]; //编译错误,非法创建
List<?>[] ls3 = new ArrayList<String>[10]; //编译错误,非法创建
List<String>[] ls2 = new ArrayList<?>[10]; //编译错误,需要强转类型
// 可以声明:可以声明带泛型的数组引用
List<String>[] ls6 = new ArrayList[10]; //正确,但是会有警告
List<String>[] ls4 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告
List<?>[] ls5 = new ArrayList<?>[10]; //正确
// ---- 官网示例演示 --------------------------------
List<String>[] listString = new List<String>[10]; // Not really allowed.
Object[] objects = (Object[]) listString;
List<Integer> listInteger = new ArrayList<Integer>();
listInteger.add(new Integer(3));
objects[0] = listInteger; // Unsound, but passes run time store check
String s = listString[0].get(0); // Run-time error: ClassCastException.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式。 因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
# 3.6.2 正确初始化泛型数组实例
1. 创建一个类型擦除的数组,然后类型转换
// 定义一个泛型类GDemo
class GDemo<T> {
public T t; // 泛型成员变量
}
public class ArrayOfGeneric {
public static void main(String[] args) {
// 创建一个GDemo数组,这个数组在运行时的类型是GDemo[],因为泛型信息被擦除
// 然后将这个数组强制转换为GDemo<Integer>[]类型
// 这种转换是不安全的,因为Java运行时并不知道数组的具体类型是GDemo<Integer>
// 因此,这里会有编译器警告:Unchecked cast
GDemo<Integer>[] gArray = (GDemo<Integer>[])new GDemo[10];
// 直接创建GDemo数组,然后将其赋值给GDemo<Integer>[]类型的变量
// 这同样是不安全的,因为原数组是类型擦除的,没有指定泛型类型
// 这里会有编译器警告:Unchecked assignment
GDemo<Integer>[] gArray1 = new GDemo[10];
// 创建一个GDemo<Integer>的实例并赋值给数组的第一个元素,这是安全的
GDemo<Integer> integerGDemo = new GDemo<>();
gArray[0] = integerGDemo; // 编译正确
// 尝试创建一个GDemo<String>的实例并赋值给GDemo<Integer>[]数组的第二个元素
// 这是不允许的,因为它违反了数组的类型安全,会导致编译错误
// GDemo<String> stringGDemo = new GDemo<>();
// gArray[1] = stringGDemo; // 编译错误
// 正确的做法是创建一个类型兼容的实例
GDemo<Integer> anotherIntegerGDemo = new GDemo<>();
gArray[1] = anotherIntegerGDemo; // 编译正确
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- 该方法仅仅是语法合格,运行时潜在的风险需要我们自己来承担。
- 建议在使用到泛型数组的场景下,应该尽量使用列表集合进行替换。
2. 使用反射
反射创建数组:java.lang.reflect.Array.newInstance(Class<T> componentType
, int length) 方法。
// 定义一个泛型类,其中包含一个泛型成员变量
class Generic<T> {
public T t;
}
// 泛型数组类,利用反射创建泛型数组
public class ArrayWithGeneric<T> {
private T[] array; // 声明一个泛型数组
// 构造方法:通过反射创建泛型数组
public ArrayWithGeneric(Class<T> type, int size) {
// 使用Array.newInstance创建泛型数组
// 这里需要强转,因为Array.newInstance返回的是Object类型
array = (T[]) java.lang.reflect.Array.newInstance(type, size);
}
// 创建方法:返回泛型数组
public T[] create() {
return array;
}
}
public class GenericsDemo {
public static void main(String[] args) {
// 实例化泛型数组类,指定数组元素类型为Integer
ArrayWithGeneric<Integer> arrayGeneric = new ArrayWithGeneric<>(Integer.class, 100);
// 创建数组
Integer[] array = arrayGeneric.create();
// 正确使用:给数组赋值,这是类型安全的
array[0] = 100;
// 编译报错:类型不匹配,无法将String类型赋值给Integer类型数组
// array[0] = "100";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
通过 Array.newInstance(type, size) 创建一个数组,用Class 对象标识数组的类型,
推荐使用的方式创建泛型数组
。
通过反射查看泛型数组的相关信息:
class Demo {
// 声明一个泛型数组类型的成员变量
List<String> list[];
public static void main(String[] args) throws NoSuchFieldException {
// 通过反射获取Demo类中名为"list"的字段
Field listField = Demo.class.getDeclaredField("list");
// 获取字段的泛型类型
Type genericType = listField.getGenericType();
// 打印字段的泛型类型
System.out.println("泛型类型 = " + genericType);
// 打印字段的泛型类型所属的类
System.out.println("泛型类型所属的类 = " + genericType.getClass());
// 检查该泛型类型是否为泛型数组类型
if (genericType instanceof GenericArrayType) {
// 将泛型类型强转为泛型数组类型
GenericArrayType genericArrayType = (GenericArrayType) genericType;
// 获取泛型数组的组件类型(即数组里元素的类型)
Type genericComponentType = genericArrayType.getGenericComponentType();
// 打印数组组件类型的运行时类
System.out.println("数组组件的类型 = " + genericComponentType.getClass());
// 检查组件类型是否为参数化类型(即带有泛型参数的类型)
if (genericComponentType instanceof ParameterizedType) {
// 将组件类型强转为参数化类型
ParameterizedType parameterizedType = (ParameterizedType) genericComponentType;
// 打印声明该类型的类或接口
System.out.println("原始类型 = " + parameterizedType.getRawType());
// 获取泛型类型的实际类型参数(即List中的String)
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 打印实际类型参数
System.out.println("实际类型参数 = " + Arrays.toString(actualTypeArguments));
// 打印拥有该类型的类型(对于嵌套类型有用,这里为null)
System.out.println("所有者类型 = " + parameterizedType.getOwnerType());
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 4. 总结
以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是scholar,一个在互联网行业的小白,立志成为更好的自己。
如果你想了解更多关于scholar (opens new window),可以关注公众号-书生带你学编程,后面文章会首先同步至公众号。