Java-泛型
1. 泛型与Object对象造型的优势
1.1. 泛型的本质
-
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
-
这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法
-
泛型是JavaSE1.5的新特性
- 《Java 核心技术》中对泛型的定义是: “泛型” 意味着编写的代码可以被不同类型的对象所重用。
- “泛型”,顾名思义,“泛指的类型”。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来 约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如 Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束 集合中只存放Integer类型的元素,如
List<Integer> list = new ArrayList<>();
1.2. 参数化类型
-
泛型类型的“类型参数”,它们也被称为参数化类型(parameterizedtype)或参量多态(parametricpolymorphism)
1.3. 泛型的好处
-
Java语言引入泛型的好处是安全简单。可以将运行时类型相关错误提前到编译时错误。
说明:
-
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
-
在JavaSE1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”(Java中的所有类型都是Object类的子类),“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
- 以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而 这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向上转型为 Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
-
1.4. 泛型使用对比
1.4.1. 没有泛型之前,保存“通用数据”的方法
- 没有泛型之前,我们可能通常使用如下代码来保存“通用数据”
public class SimpleGen { private Object obj; public SimpleGen(Object obj) { this.obj = obj; } public Object getObj() { return obj; } public void setObj(Object obj) { this.obj = obj; } public String showType() { //反射,obj的对象类型名称 return obj.getClass().getName(); /*return String.valueOf(obj.getClass());*/ } public static void main(String[] args) {
SimpleGen simpleGen1 = new SimpleGen("张三"); System.out.println("simpleGen1的属性obj的类型为:" + simpleGen1.showType()); String obj1 = (String) simpleGen1.getObj(); System.out.println("simpleGen1的obj的值为:" + obj1);
SimpleGen simpleGen2 = new SimpleGen(123); System.out.println("simpleGen2的属性obj的类型为:" + simpleGen2.showType()); Integer obj2 = (Integer) simpleGen2.getObj(); System.out.println("simpleGen2的obj的值为:" + obj2);
//运行异常 java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Double /*Double obj3 = (Double) simpleGen2.getObj();*/ } } /*simpleGen1的属性obj的类型为:java.lang.String simpleGen1的obj的值为:张三 simpleGen2的属性obj的类型为:java.lang.Integer simpleGen2的obj的值为:123*/
1.4.2. 有了泛型之后
- 有了泛型之后,我们可以在编译阶段就发现异常的类型问题:
public class GenericSimpleGen<T> { private T obj; public GenericSimpleGen(T obj) { this.obj = obj; } public T getObj() { return obj; } public void setObj(T obj) { this.obj = obj; } public String showType() { return obj.getClass().getName(); } public static void main(String[] args) { //确定泛型类型为String后,类中凡是T的地方均由String代替,即构造方法只 能提供String对象,Set/Get对应的类型也只能是String,无需类型转换 GenericSimpleGen<String> g1 = new GenericSimpleGen<>("张三"); System.out.println(g1.showType()); String strObj = g1.getObj(); System.out.println(strObj); //确定泛型类型为Integer后,类中凡是T的地方均由Integer代替,即构造方法 只能提供Integer对象,Set/Get对应的类型也只能是Integer,无需类型转换 GenericSimpleGen<Integer> g2 = new GenericSimpleGen<>(123); System.out.println(g2.showType()); Integer intObj = g2.getObj(); System.out.println(intObj); } } /*java.lang.String 张三 java.lang.Integer 123*/
2. 泛型类
泛型类,是在实例化类的时候指明泛型的具体类型
2.1. 语法
权限修饰符 class 类名<泛型标识:标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
}
java 中泛型标记符(可以随便写任意标识号):一般规范如下
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型
例如:jdk中的ArrayList类
public class ArrayList<E> extends AbstractList<E>
2.2. 使用泛型
ArrayList<String> list = new ArrayList<>();
//此时,变量E的值就是String类型,那么我们的类型就可以理解为:String
ArrayList<Integer> list = new ArrayList<>();
//此时,变量E的值就是Integer类型,那么我们的类型就可以理解为:Integer
2.3. 例子
/**
* 泛型类
* <p>
* 泛型类型只是标识,定义时没有确定具体类型,使用者来确定具体类型
*
* @param <MVP> 第一个泛型类型
* @param <MVC> 第二个泛型类型
* @param <T> 第三个泛型类型
* 多个泛型之间用逗号隔开
* 一般习惯 泛型的类型用T V K E来表示
*/
public class Generic<MVP, MVC, T> {
private MVP name;
private MVC age;
private T result;
public Generic(MVP name, MVC age, T result) {
this.name = name;
this.age = age;
this.result = result;
}
public MVP getName() {
return name;
}
public void setName(MVP name) {
this.name = name;
}
public MVC getAge() {
return age;
}
public void setAge(MVC age) {
this.age = age;
}
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
public static void main(String[] args) {
//使用泛型,泛型的类型是引用类型和String,其他的基本类型不能使用
Generic<String, Integer, Boolean> generic = new Generic<>("carat", 17, true);
//不用强转,直接能够确定类型
String name = generic.getName();
Integer age = generic.getAge();
Boolean result = generic.getResult();
System.out.println("name=" + name + ",age=" + age + ",result=" + result);
}
}
/*name=carat,age=17,result=true*/
3. 泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型。
3.1. 语法
3.2.注意事项:
- public 与返回值中间的<T,E..>可以理解为声明此方法为泛型方法;
- 与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T,E,K,V等形式的参数常于表示泛型;
- 在泛型类中使用泛型的方法无需再声明泛型;
- 是否拥有泛型方法,与其所在的类是否泛型类没有关系。要定义泛型方法,只需将泛型参数列表置于返回值前。
3.3. 例子
public class GenericMethod {
public <T> void show1(T t) {
System.out.println(t.getClass().getName());
}
//泛型方法中泛型类型 可以用在形参列表中,也可以用在返回值中
public <T, E> T show2(E e, T t) {
/*return (T)e;*/
return null;
}
public static void main(String[] args) {
GenericMethod genericMethod = new GenericMethod();
//泛型方法在使用中,不用指定类型,实参已经具有了类型,所以方法的泛型类型也就确定了
genericMethod.show1("carat");
Integer integer = genericMethod.show2(17526, 9588);
/* System.out.println(integer.getClass().getName());*/
System.out.println(integer);
}
}
/*
java.lang.String
null
*/
4. 泛型接口
4.1. 语法
例如:
public interface MyGenericInterface<E>{
void add(E e);
E getE();
}
4.2. 说明
泛型接口的声明同泛型类的声明类似,在接口名之后,用尖括号将所有类型参数括起来。
4.3. 使用格式
1、定义实现类时确定泛型的类型
例如:
public class MyImp1 implements MyGenericInterface<String> {
@Override
public void add(String e) {
// 省略...
}
@Override
public String getE() {
return null;
}
}
此时,泛型E的值就是String类型。
2、始终不确定泛型的类型,直到创建对象时,确定泛型的类型
例如:
public class MyImp2<E> implements MyGenericInterface<E> {
@Override
public void add(E e) {
// 省略...
}
@Override
public E getE() {
return null;
}
}
确定泛型:
/*
* 使用
*/
public class GenericInterface {
public static void main(String[] args) {
MyImp2<String> my = new MyImp2<String>();
my.add("aa");
}
}
5. 泛型不支持协变
5.1. Java中的数组是协变的
-
main方法中的第一行,创建了一个 Apple 数组并把它赋给 Fruit 数组的引用。 这是有意义的,Apple 是 Fruit 的子类,一个 Apple 对象也是一种 Fruit 对象, 所以一个 Apple 数组也是一种 Fruit 的数组。这称作数组的协变。
-
尽管 Apple[] 可以 “向上转型” 为 Fruit[],但数组元素的实际类型还是 Apple,我们只能向数组中放入 Apple或者 Apple 的子类。在下面的代码中, 向数组中放入了 Fruit 对象和 Orange 对象。对于编译器来说,这是可以通过编译的,但是在运行时期,JVM 能够知道数组的实际类型是 Apple[],所以当其它对象加入数组的时候就会抛出异常。
5.2. 泛型不支持协变
泛型设计的目的之一是要使运行时期的类型相关错误在编译期就能发现,看看用泛型 容器类来代替数组会发生什么
import java.util.ArrayList;
class Fruit {
}
class Apple extends Fruit {
}
class Hong extends Apple {
}
class Orange extends Fruit {
}
public class ArrayTest1 {
public static void main(String[] args) {
//xie1();
}
private static void xie2() {
//ArrayList<Fruit> fruits = new ArrayList<Apple>(); //编译不通过
}
private static void xie1() {
//数组的协变
Fruit[] fruits = new Apple[17];
//fruits数组实际上是 Apple的数组,存储Apple对象或者Apple的子类对象是可以的
fruits[0] = new Apple();
fruits[1] = new Hong();
//fruits数组实际上是 Apple的数组,运行时添加Fruit对象是不允许的,编译通过 ,运行报错
fruits[2] = new Fruit();
/*向下转型要强转
Apple a = new Fruit();*/
//编译通过,运行报错: java.lang.ArrayStoreException: com.tjetc.generic.Orange
fruits[3] = new Orange();
}
}
当涉及到泛型时,尽管 Apple 是 Fruit 的子类型,但是 ArrayList <Apple>不是 ArrayList<Fruit> 的子类型,泛型不支持协变。
6. 泛型通配符
泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用? , ?表示未知通配符。
?代表可以接收任意类型
6.1. 通配符基本使用
-
读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型是什么,它包含的都是Object。
-
写入list中的元素时,不行。因为我们不知道的元素类型,我们不能向其中添加对象。
唯一的例外是null,它是所有类型的成员。
-
调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object。
举个例子:
import java.util.ArrayList;
import java.util.List;
public class Test3 {
public static void main(String[] args) {
//List<?> 问号表示任何类型
List<?> list4 = new ArrayList<Number>(); //new ArrayList<Integer>();
list4 = new ArrayList<Integer>();
list4 = new ArrayList<String>();
//?表示任何类型,如果?表示Integer,add(3)允许的话就能解释的通,但是?表示String就解释不通,所以出现了解释的矛盾
List<?> list1 = new ArrayList<>();
// 因此除了null任何类型的数据都不能添加
/*list1.add(3);
list1.add("str");*/
//null可以添加进去的,所有引用类型的值都可以赋值为null
list1.add(null);
read(list1);
System.out.println("============================");
//String
List<String> list2 = new ArrayList<>();
list2.add("jack");
list2.add("jim");
read(list2);
System.out.println("============================");
//Integer
List<Integer> list3 = new ArrayList<>();
list3.add(1);
list3.add(100);
read(list3);
}
/**
* 读取list集合中每一个元素并打印
*
* @param list
*/
/*static void read(List<?> list) {
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
System.out.println(o);
}
}*/
static void read(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
}
/*
null
============================
jack
jim
============================
1
100
*/
6.2. 通配符高级使用----受限泛型
之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在 JAVA的泛型中可以指定一个泛型的上限和下限。
比如:现已知Object类,String 类,Number类,Integer类,其中Number是 Integer的父类
package com.tjetc.generic;
import java.util.ArrayList;
import java.util.List;
public class TestShangXiaXian {
/**
* 泛型上限
*
* @param list List集合泛型的类型 是Number或者Number的子类
*/
private static void getElement1(List<? extends Number> list) {
}
/**
* 泛型下限
*
* @param list List集合泛型的类型 是Number或者Number的父类(包括Object)
*/
private static void getElement2(List<? super Number> list) {
}
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<Number> list3 = new ArrayList<>();
List<Object> list4 = new ArrayList<>();
getElement1(list1);
//getElement1(list2); //报错,Number 的子类没有String类
getElement1(list3);
//getElement1(list4); //报错 ,Number的子类没有Object类
//getElement2(list1); //报错 Number的父类没有Integer类
//getElement2(list2); //报错 Number的父类中没有String类
getElement2(list3);
getElement2(list4);
}
}
6.3. 课堂案例
6.3.1. 泛型的上限
6.3.2. 泛型的下限
public class Person<T> {
private T val;
public Person(T val) {
this.val = val;
}
public T getVal() {
return val;
}
public void setVal(T val) {
this.val = val;
}
@Override
public String toString() {
return "Person{" +
"val=" + val +
'}';
}
//Person类型的泛型是Number或Number的子类
public static void show(Person<? extends Number> p) {
System.out.println(p);
}
//Person类型的泛型是Number或Number的父类
public static void show1(Person<? super Number> p) {
System.out.println(p);
}
public static void main(String[] args) {
Person<Integer> p1 = new Person<>(3);
Person<Double> p2 = new Person<>(3.14);
Person<String> p3 = new Person<>("hello");
Person<Object> p4 = new Person<>(null);
show(p1);
show(p2);
//p3、p4是Person类型,但不是Number或Number的子类,报错
/*show(p3);
show(p4);*/
//Integre、Double、String 不是Number类型,也不是Number的父类
/*show1(p1);
show1(p2);
show1(p3);*/
show1(p4);
}
}
/*
Person{val=3}
Person{val=3.14}
Person{val=null}
*/
7.类型擦除
7.1概念
- 泛型是java1.5版本才引进的概念,在这之前没有泛型,但是,泛型代码能够很好的和之前版本的代码兼容。
- 原因是泛型只存在代码编译阶段,在进入JVM 之前,泛型相关的信息会被擦除掉,我们称之为--类型擦除
7.2.证明Java类型的类型擦除
- 这个例子里,定义了两个List集合,一个是List泛型类型的,只能存储整数;一个 是List泛型类型的,只能存储字符串。
- 通过list1对象和list2对象的 getClass()方法获取他们的类的信息,结果发现结果为true。
- 说明泛型类型String和 Integer都被擦除掉了,只剩下原始类型。
import java.util.ArrayList; import java.util.List; public class Test1 { public static void main(String[] args) { List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = new ArrayList<String>(); /*list1.add("carat"); list2.add(17526);*/ System.out.println(list1.getClass() == list2.getClass()); } } //true
7.3. 类型擦除后保留的原始类型
- 原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。
- 无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
7.3.1. 原始类型Object
- 因为在Pair中,T是一个无限定的类型变量,所以用Object代替,其结果就是一个普通的类,如同泛型加入Java之前就已经实现了。
- 在程序中可以包含不同类型的Pair, 如Pair或Pair,但是擦除类型后他们就成了原始的Pair类型,原始类型都是Object。
//原始类型是Object public class Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } public static void main(String[] args) { Pair p = new Pair(); //调用setter方法能看到参数类型是 Object:setValue(Object) p.setValue(null); } }
7.3.2. 如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换
- 限定
/** * 反省类 * * @param <T> T extends Comparable 表示类型是Comparable 或者 表示类型是Comparable的子类 */ //原始类型是Comparable public class Pair1<T extends Comparable> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } public static void main(String[] args) { Pair1 p = new Pair1(); //调用setter方法能看到参数类型是 Comparable:setValue(Comparable) p.setValue(null); } }
7.4. Java中类型擦除的限制
7.4.1. 无法利用同一泛型类的实例区分方法签名
import java.util.List;
//虽然泛型实例不同,但是由于类型擦除后是同一字节码(类型擦拭之后,就都是 List了),因此不能用于区分方法签名
public class Erasure {
//'test(List<String=)'与'test(List<Integer>)'冲突;这两种方法具有相同的擦除
public void test(List<String> list) {
}
/*public void test(List<Integer> list){
}*/
}
7.4.2. 泛型类的静态变量是共享的
class GT<T> {
//静态变量
public static int var = 0;
public void nothing(T t) { }
}
public class StaticTest {
public static void main(String[] args) {
GT<Integer> gti = new GT<>();
//var变量赋值1
gti.var = 1;
GT<String> gts = new GT<>();
gts.var = 2;
//泛型参数不同,但是共享 类的静态成员
System.out.println(gti.var);//2
}
}
7.5. Java中类型擦除的特征
-
所有泛型类的类型参数在编译时都会被擦除,虚拟机运行时中没有泛型,只有普通类和普通方法,从这一点上来说,Java中的泛型从某种程度上是一种语法糖
-
Java泛型不支持基本类型
-
在泛型代码内部,无法获得任何有关泛型参数类型的信息,如果传入的类型参数为T,那么在泛型代码内部你不知道T有什么方法,属性,关于T的一切信息都丢失了
-
创建泛型对象时请指明类型,让编译器尽早的做参数检查
-
不要忽略编译器的警告信息,那意味着潜在的ClassCastException等着你
-
Java的泛型类型不能用于new构建对象(也不能用于初始化数组).泛型不能用于显性地引用运行时类型的操作之中。
-
例如转型,instanceof和new操作(包括new一个对象,new一个数组)。
-
因为所有关于参数的类型信息都在运行时丢失了,所以任何在运行时需要获取类型信息的操作都无法进行工作。
-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南