目录
1、泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数。
2、静态泛型方法中可以使用自身的方法签名中新定义的类型参数(即泛型方法,后面会说到),而不能使用泛型类中定义的类型参数。
1、定义一个泛型接口时里面的成员变量不可以使用接口定义的泛型(因为在接口中变量默认为public static final)。
2、定义一个接口B去继承一个泛型接口A时,在接口A定义时必须确定泛型接口B中的类型参数。
3、一个类D实现泛型接口A时,在类D定义时需要确定泛型接口A中的类型参数 。
4、一个类E实现泛型接口A时,若是没有确定泛型接口A中的类型参数,则默认为 Object。
5、一个类F实现泛型接口A时,若是没有确定泛型接口A中的类型参数,也可以将F设置成为泛型类(但是两个泛型之间参数类型要一样)。
1、只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。
4、特别注意的是:泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,它们一点关系都没有。
(1)泛型:
泛型(Generics)是 Java 中的一项强大的编程特性,它允许我们在定义类、接口或方法时使用类型参数,从而实现代码的重用性和类型安全性。使用泛型可以使代码更加灵活和通用,减少类型转换和错误,并提高代码的可读性和维护性。
泛型的主要特点如下:
-
参数化类型:通过在定义类、接口或方法时使用类型参数(用尖括号 <> 括起来的类型参数),可以使其成为参数化类型,使得这些类型可以适用于多种具体的数据类型。
-
类型安全性:在使用泛型时,编译器会在编译时对类型进行检查,确保在编译时就能发现类型错误,而不是在运行时抛出异常,从而提高代码的安全性。
-
代码重用性:通过使用泛型,我们可以编写一些通用的代码,使其可以适用于多种不同类型的数据,从而实现代码的重用性。
-
避免类型转换:在没有使用泛型时,我们可能需要进行频繁的类型转换,而使用泛型可以避免这种情况,使代码更加简洁和清晰。
(2)泛型类:
泛型类是指在类的定义中使用了泛型类型参数的类。通过在类名后面使用尖括号(<>)定义一个或多个类型参数,使得类中的字段、方法、构造函数等可以使用这些类型参数来代表具体的数据类型。(类里面某一些变量不知道是什么类型,所以在创建类的对象的时候传入时就可以按照自己的需求去出传入什么样的数据类型!)
格式:
class 类名称 <泛型标识> {
private 泛型标识 /*(成员变量类型)*/ 变量名;
.....
}
}
泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思,文章后面部分会讲解示意。
举例:
public class Generic<T> {
// key 这个成员变量的数据类型为 T, T 的类型由外部传入
private T key;
// 泛型构造方法形参 key 的类型也为 T,T 的类型由外部传入
public Generic(T key) {
this.key = key;
}
// 泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定
public T getKey(){
return key;
}
}
1、泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数。
举例:
public class Test<T> {
public static T one; // 编译错误
public static T show(T one){ // 编译错误
return null;
}
}
原因是泛型类中的类型参数的确定是在创建泛型类对象的时候(例如 ArrayList< Integer >),而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的。
2、静态泛型方法中可以使用自身的方法签名中新定义的类型参数(即泛型方法,后面会说到),而不能使用泛型类中定义的类型参数。
举例:
public class Test2<T> {
// 泛型类定义的类型参数 T 不能在静态方法中使用
public static <E> E show(E one){ // 这是正确的,因为 E 是在静态方法签名中新定义的类型参数
return null;
}
}
3、泛型类不只接受一个类型参数,它还可以接受多个类型参数。
举例:
public class MultiType <E,T> {
E value1;
T value2;
public void setValue1(E value1) {
this.value1 = value1;
}
public void setValue2(T value2) {
this.value2 = value2;
}
public E getValue1(){
return value1;
}
public T getValue2(){
return value2;
}
}
啊!这怎么确定传入的数据哪个是那个,怎么声明对象?
MultiType<String, Integer> obj1 = new MultiType<>();
obj1.setvalue1 = "Hello";
obj1.setvalue2 = 123;
MultiType<Double, String> obj2 = new MultiType<>();
obj2.setvalue1 = 3.14;
obj2.setvalue2 = "World";
注意:在创建泛型类的对象时,必须指定类型参数 T 的具体数据类型,即尖括号 <> 中传入的什么数据类型,T 便会被替换成对应的类型。如果 <> 中什么都不传入,则默认是 < Object >。
(3)泛型接口:
泛型接口是一种在接口中使用泛型类型参数的接口。它类似于泛型类,但是泛型接口是对接口中的方法和成员进行参数化,而不是对类进行参数化。泛型接口中的类型参数,在该接口被继承或者被实现时确定!
格式:
interface 接口名称<类型参数1, 类型参数2, ...> {
// 接口方法和成员的定义
}
1、定义一个泛型接口时里面的成员变量不可以使用接口定义的泛型(因为在接口中变量默认为public static final
)。
举例:
interface A<U, R> {
int n = 10;
U name;// 报错! 接口中的属性默认是静态的,因此不能使用类型参数声明
R get(U u);// 普通方法中,可以使用类型参数
void hi(R r);// 抽象方法中,可以使用类型参数
// 在jdk8 中,可以在接口中使用默认方法, 默认方法可以使用泛型接口的类型参数
default R method(U u) {
return null;
}
}
2、定义一个接口B去继承一个泛型接口A时,在接口A定义时必须确定泛型接口B中的类型参数。
举例:
// 在继承泛型接口时,必须确定泛型接口的类型参数
interface B extends A<String, Double> {
...
}
// 当去实现B接口时,因为B在继承B接口时,指定了类型参数 U 为 String,R 为 Double
// 所以在实现A接口的方法时,使用 String 替换 U,用 Double 替换 R
class C implements B {
@Override
public Double get(String s) {
return null;
}
@Override
public void hi(Double d) {
...
}
}
3、一个类D实现泛型接口A时,在类D定义时需要确定泛型接口A中的类型参数 。
举例:
// 实现接口时,需要指定泛型接口的类型参数
// 给 U 指定 Integer, 给 R 指定了 Float
// 所以,当我们实现A方法时,会使用 Integer 替换 U, 使用 Float 替换 R
class D implements A<Integer, Float> {
@Override
public Float get(Integer integer) {
return null;
}
@Override
public void hi(Float afloat) {
...
}
}
4、一个类E实现泛型接口A时,若是没有确定泛型接口A中的类型参数,则默认为 Object。
举例:
// 实现泛型接口时没有确定类型参数,则默认为 Object
// 建议直接写成 IUsb<Object, Object>
class E implements A {//等价 class CC implements IUsb<Object, Object>
@Override
public Object get(Object o) {
return null;
}
@Override
public void hi(Object o) {
...
}
}
5、一个类F实现泛型接口A时,若是没有确定泛型接口A中的类型参数,也可以将F设置成为泛型类(但是两个泛型之间参数类型要一样)。
举例:
// F 类定义为 泛型类,则不需要确定 接口的类型参数
// 但 F 类定义的类型参数要和接口中类型参数的一致
class F<U, R> implements A<U, R> {
...
}
(4)泛型方法:
泛型方法是一种在方法中使用泛型的特性。它可以在方法的定义中使用泛型类型参数,从而使得方法可以处理不同类型的数据,并且在编译时会进行类型检查,确保类型的安全性。
注意:
当方法的形参不能确定的时候,有两种解决方案,一种是使用类名后面定义的泛型,一种是在方法上自己定义泛型,两者的区别就是第一种在所有的方法里面都可以用,第二种只有在本方法里面可以使用。
格式:
public <类型参数> 返回类型 方法名(类型参数 变量名) {
...
}
1、只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。
举例:
public class Test<U> {
// 该方法只是使用了泛型类定义的类型参数,不是泛型方法
public void testMethod(U u){
System.out.println(u);
}
// <T> 真正声明了下面的方法是一个泛型方法
public <T> T testMethod1(T t){
return t;
}
}
2、泛型方法中可以同时声明多个类型参数。
举例:
public class TestMethod<U> {
public <T, S> T testMethod(T t, S s) {
return null;
}
}
3、泛型方法中也可以使用泛型类中定义的泛型参数。
举例:
public class TestMethod<U> {
public <T> U testMethod(T t, U u) {
return u;
}
}
4、特别注意的是:泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,它们一点关系都没有。
举例:
public class Test<T> {
public void testMethod(T t) {
System.out.println(t);
}
public <T> T testMethod1(T t) {
return t;
}
}
上面代码中,Test< T > 是泛型类,testMethod()
是泛型类中的普通方法,其只是使用的类型参数是泛型类中定义的类型参数。
而 testMethod1()
是一个泛型方法,它使用的类型参数是与方法签名中声明的类型参数。
虽然泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T >,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准(在方法调用时传入什么参数,它的参数就确定了,而不是和类的泛型一样,只是名字相同罢了!)为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
5、泛型方法、泛型接口、泛型类的注意点:
1、泛型类,在创建类的对象的时候确定类型参数的具体类型,
2、泛型方法,在调用方法的时候再确定类型参数的具体类型。
3、泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。
4、当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器
就可以判断出类型参数 T
所代表的具体数据类型。
举例:
public class Demo {
public static void main(String args[]) {
GenericMethod d = new GenericMethod(); // 创建 GenericMethod 对象
String str = d.fun("汤姆"); // 给GenericMethod中的泛型方法传递字符串
int i = d.fun(30); // 给GenericMethod中的泛型方法传递数字,自动装箱
System.out.println(str); // 输出 汤姆
System.out.println(i); // 输出 30
GenericMethod.show("Lin");// 输出: 静态泛型方法 Lin
}
}
class GenericMethod {
// 普通的泛型方法
public <T> T fun(T t) { // 可以接收任意类型的数据
return t;
}
// 静态的泛型方法
public static <E> void show(E one){
System.out.println("静态泛型方法 " + one);
}
}
6、泛型方法中的类型推断
在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。
1、当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。
2、在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类。
public class Test {
// 这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return y;
}
public static void main(String[] args) {
// 一、不显式地指定类型参数
//(1)传入的两个实参都是 Integer,所以泛型方法中的<T> == <Integer>
int i = Test.add(1, 2);
//(2)传入的两个实参一个是 Integer,另一个是 Float,
// 所以<T>取共同父类的最小级,<T> == <Number>
Number f = Test.add(1, 1.2);
// 传入的两个实参一个是 Integer,另一个是 String,
// 所以<T>取共同父类的最小级,<T> == <Object>
Object o = Test.add(1, "asd");
// 二、显式地指定类型参数
//(1)指定了<T> = <Integer>,所以传入的实参只能为 Integer 对象
int a = Test.<Integer>add(1, 2);
//(2)指定了<T> = <Integer>,所以不能传入 Float 对象
int b = Test.<Integer>add(1, 2.2);// 编译错误
//(3)指定<T> = <Number>,所以可以传入 Number 对象
// Integer 和 Float 都是 Number 的子类,因此可以传入两者的对象
Number c = Test.<Number>add(1, 2.2);
}
}
(5)类型擦除
类型擦除是 Java 泛型实现的一种机制。在 Java 中,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除
。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段
。
泛型是在编译期间有效的,而在运行时会进行类型擦除,即将泛型类型信息擦除掉,替换为实际的类型。这是为了兼容 Java 5 之前的版本,因为 Java 5 之前没有泛型机制。
举个例子:
public class GenericType {
public static void main(String[] args) {
ArrayList<String> arrayString = new ArrayList<String>();
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
}
}
在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass()
方法获取它们的类信息并比较,发现结果为true
。
明明我们在 <> 中传入了两种不同的数据类型,按照上文所说的,它们的类型参数 T 不是应该被替换成我们传入的数据类型了吗,那为什么它们的类信息还是相同呢? 这是因为,在编译期间,所有的泛型信息
都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Object>
类型。
类型擦除的过程包括以下几个步骤:
- 编译器在编译泛型代码时,会根据类型参数的实际类型生成相应的类型检查和转换代码。
- 在生成的字节码中,泛型类型参数会被擦除,替换为其上限(对于没有指定上限的类型参数,则被替换为
Object
类型)。 - 在运行时,Java 虚拟机无法得知原始泛型类型的信息,因此泛型类型参数被擦除后,运行时无法区分不同的泛型类型。
由于类型擦除的机制,Java 泛型有以下几个特点:
- 泛型类型参数被擦除后,运行时无法获得泛型的实际类型信息。例如,在一个泛型集合中存放了不同类型的元素,但在运行时无法确定它们的实际类型。
- 泛型类型参数被擦除后,泛型类型变量将被替换为其上限类型。例如,
List<T>
泛型在运行时将被擦除为List<Object>
。 - 泛型类型参数被擦除后,对于泛型类型的方法,其参数和返回值都会被擦除为其上限类型。例如,
T get(int index)
泛型方法在运行时将被擦除为Object get(int index)
。
尽管类型擦除带来了一些限制,但它也为 Java 泛型带来了更好的兼容性和性能。通过类型擦除,Java 泛型可以与之前的非泛型代码兼容,而不会出现运行时错误。同时,类型擦除也避免了因泛型带来的额外开销,使得 Java 泛型在性能上与非泛型代码相当。
1、并不是所有的类型参数被擦除后都以 Object 类进行替换
大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数
(即泛型通配符
,后面我们会详细解释)。
举例:
public class Caculate<T extends Number> {
private T num;
}
反编译:
public class Caculate {
public Caculate() {}// 默认构造器,不用管
private Number num;
}
可以发现,使用到了 extends 语法的类型参数 T 被擦除后会替换为 Number 而不再是 Object。
extends 和 super 是一个限定类型参数边界的语法,extends 限定 T 只能是 Number 或者是 Number 的子类。 也就是说,在创建 Caculate 类对象的时候,尖括号 <> 中只能传入 Number 类或者 Number 的子类的数据类型
,所以在创建 Caculate 类对象时无论传入什么数据类型,Number 都是其父类,于是可以使用 Number 类作为 T 的原始数据类型
,进行类型擦除
并替换。(这一部分涉及到了泛型通配符,在下面还会具体介绍)
2、编译的原理
假如我们定义了一个 ArrayList< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问:
我们定义了 ArrayList< Integer > 泛型集合,而且其泛型信息最终被擦除后就变了 ArrayList< Object > 集合
,那为什么不允许向其中插入 String 对象呢?
其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除
;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。
举个例子!
可以把泛型的类型安全检查机制
和类型擦除
想象成演唱会的验票机制:以 ArrayList< Integer> 泛型集合为例。
1、当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型;
2、当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)。
3、在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。
4、进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)
public class GenericType {
public static void main(String[] args) {
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();// 设置验票系统
arrayInteger.add(111);// 观众进场,验票系统验票,门票会被收走(类型擦除)
Integer n = arrayInteger.get(0);// 获取观众信息,编译器会进行强制类型转换
System.out.println(n);
}
}
擦除 ArrayList< Integer > 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令:
1、对原始方法 get() 的调用,返回的是 Object 类型;
2、将返回的 Object 类型强制转换为 Integer 类型;
总结:
泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在
,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型
(默认是 Object 类,若有 extends 或者 super 则另外分析);
在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换
(从原始类型转换为未擦除前的数据类型)。
(6)泛型通配符:
泛型不具备继承性,但是数据具备继承性。
public class 泛型 {
public static void main(String[] args) {
//创建对象
ArrayList<ye> list1 = new ArrayList<>();
ArrayList<zi> list2 = new ArrayList<>();
ArrayList<su> list3 = new ArrayList<>();
//调用方法
/*method(list1);
method(list2);
method(list3);*/
/*我们发现我们调用list1后list2和3就不可以用啦,出现编译错误!
* */
//数据具备继承性
list1.add(new ye());
list1.add(new zi());
list1.add(new su());
}
public static void method(ArrayList<ye> list){
}
}
class ye{}
class zi extends ye{}
class su extends zi{}
泛型的通配符是用来表示不具体指定泛型类型参数的符号,通常使用 '?' 表示。它在泛型代码中用于表示某种泛型类型的未知类型。
1. 无界通配符 <?>:
表示不确定的类型,可以匹配任意类型的泛型对象。它在一些情况下可以用来接受不同类型的泛型对象,但无法对其进行具体操作,只能使用 'Object' 类的方法和属性。
2. 上界通配符 <?extend T>:
表示泛型类型的上界限定,用来限制通配符可以表示的类型范围。例如,'<?extend Number>' 表示通配符可以是 'Number' 或其子类。上界通配符可以在读取数据时提供更多的灵活性,但无法进行写入操作。
public class GenericType {
public static void main(String[] args) {
ArrayList<Number> list01 = new ArrayList<Integer>();// 编译错误
ArrayList<? extends Number> list02 = new ArrayList<Integer>();// 编译正确
}
}
3. 下界通配符 <?super T>:
表示泛型类型的下界限定,用来限制通配符可以表示的类型范围。例如,'<?super Integer>' 表示通配符可以是 'Integer' 或其父类。下界通配符可以在写入数据时提供更多的灵活性,但在读取数据时只能以 'Object' 类型来处理。