目录
前言
在泛型出现之前,Java容器类(如 ArrayLIst )只能存储 Object 类型的对象
这导致了两个主要问题:
1、类型不安全:编译器无法检查类型是否正确
2、需要强制类型转换:每次取出元素都需要手动转换类型
// 泛型出现前的代码示例 ArrayList list = new ArrayList(); list.add("Hello"); list.add(123); // 可以添加任何类型 String str = (String) list.get(0); // 需要强制转换 String str2 = (String) list.get(1); // 运行时异常:ClassCastException
为了实现保持向后兼容的前提下引入类型安全机制,Java设计师们选择了类型擦除的方式来实现泛型
一、概述
泛型的概念是 JDK1.5 引入的
主要目的是为了解决 类型安全性 和 代码复用 的问题
它具有一个强大的特性:
允许我们在定义类、接口和方法时使用参数化类型
二、定义
// 定义一个简单的泛型类
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 使用示例
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello World");
String content = stringBox.getContent(); // 不需要强制转换
三、使用
大概了解之后,我们结合前面学习的集合来具体理解:
泛型中常见的类型参数命名约定:
K - Key(键)
V - Value(值)
E - Element(元素)
T - Type(类型)
N - Number(数字)
①Collection示例
Collection是一个泛型接口
泛型参数是E,add方法的参数类型也是E
//Collection接口定义,其是一个泛型接口
public interface Collection<E> {
//省略...
boolean add(E e);
}
案例:
public static void main(String[] args) {
Collection<String> c = new ArrayList<String>();
c.add("hello1");
c.add("hello2");
c.add("hello3");
//编译报错,add(E e) 已经变为 add(String e)
//int类型的数据1,是添加不到集合中去的
//c.add(1);
for(String str : c) {
System.out.println(str);
}
}
可以看出,传入泛型参数后,add方法只能接收String类型的参数,其他类型的数据无法添加到集合中,同时在遍历集合的时候,也不需要我们做类型转换了,直接使用String类型变量接收就可以了,JVM会自动转换
Collection<String> c = new ArrayList<String>();
可简写为菱形泛型形式:
Collection<String> c = new ArrayList<>();
②Map示例
//Map接口也是泛型接口
public interface Map<K,V> {
//省略...
V put(K key, V value);
Set<Map.Entry<K, V>> entrySet();
}
public static void main(String[] args) {
Map<Integer,String> map = new HashMap<>();
//根据泛型类型的指定,put方法中的key只能是Integer类型,value只能是String类型
map.put(1,"hello1");
map.put(2,"hello2");
map.put(3,"hello3");
map.put(4,"hello4");
//根据上面列出的源码可知,当前指定Map的泛型类型为:Map<Integer,String> map
//entrySet方法返回的类型就应该是Set<Map.Entry<Integer, String>>
Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
for(Map.Entry entry:entrySet){
System.out.println(entry.getKey()+" : "+entry.getValue());
}
}
四、自定义泛型
Java中泛型分三种使用情况:
- 泛型类
- 泛型接口
- 泛型方法
①泛型类
如果泛型参数定义在类上面
那么这个类就是一个泛型类
定义格式:
[修饰符] class 类名<泛型类型名1,泛型类型名2,...> {
0个或多个数据成员;
0个或多个构造方法;
0个或多个成员方法;
}
//注意:之前用确定数据类型的地方,现在使用自定义泛型类型名替代
示例(JDK中的 HashSet):
泛型类实例化对象格式:
泛型类名<具体类型1,具体类型2,...> 对象名 = new 泛型类名<>(实参列表);
案例展示:
定义一个泛型类Circle
包含 x,y 坐标和 radius 半径
基础泛型类:
//自定义泛型类:圆
//class 类名<泛型类型1,泛型类型2,...>
// 泛型类型名字可以自行定义
public class Circle<T, E> {
//原来具体数据类型的地方,使用泛型类型名替换即可
private T x;
private T y;
private E radius;
//无参构造器没有任何改变
public Circle() {}
//原来具体数据类型的地方,使用泛型类型名替换即可
public Circle(T x, T y, E radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public T getY() {
return y;
}
public void setY(T y) {
this.y = y;
}
public E getRadius() {
return radius;
}
public void setRadius(E radius) {
this.radius = radius;
}
@Override
public String toString() {
return "Circle [x=" + x + ", y=" + y + ", radius=" + radius + "]";
}
}
测试类:
import com.briup.chap08.bean.Circle;
public class TestGenericsClass {
public static void main(String[] args) {
//实例化泛型类对象:
// 泛型类<具体类型1,具体类型2,...> 对象 = new 泛型类<>(实参s);
//1.实例化具体类对象,2种泛型设置为Integer和Double
// 注意,泛型类可以是任意引用类型
Circle<Integer, Double> c1 = new Circle<>(2,3,2.5);
int x = c1.getX();
double r = c1.getRadius();
System.out.println("x: " + x + " radius: " + r);
System.out.println("------------------");
//2.实例化具体类对象,2种泛型设置为Double和Integer
Circle<Double, Integer> c2 = new Circle<>(2.0,3.0,2);
double x2 = c2.getX();
int r2 = c2.getRadius();
System.out.println("x2: " + x2 + " r2: " + r2);
}
}
//运行结果:
x: 2 radius: 2.5
------------------
x2: 2.0 r2: 2
②泛型接口
如果泛型参数定义在接口上面,那么这个接口就是一个泛型接口
定义格式:
[修饰符] interface 接口名<泛型类型名1,泛型类型名2,...> { }
示例(JDK中的 Set 泛型接口):
// 定义泛型接口
public interface Container<T> {
void add(T item);
T get(int index);
int size();
}
// 实现泛型接口
public class SimpleContainer<T> implements Container<T> {
private List<T> items = new ArrayList<>();
@Override
public void add(T item) {
items.add(item);
}
@Override
public T get(int index) {
return items.get(index);
}
@Override
public int size() {
return items.size();
}
}
③泛型方法
如果泛型参数定义在方法上面,那么这个方法就是泛型方法
定义格式:
[修饰符] <泛型类型名> 返回值类型 方法名(形式参数列表){
方法具体实现;
}
调用格式:
类对象.泛型方法(实参列表);
类名.static泛型方法(实参列表);
注意!
泛型方法调用时不需要额外指定泛型类型,系统会自动识别泛型类型
案例展示(接上面 Circle,补充泛型方法disp() 和 static show() ):
public class Circle<T,E> {
//省略...
//泛型类中定义 泛型方法
public <F> void disp(F f) {
System.out.println("in 泛型方法disp, f: " + f);
}
// 下面写法虽然不会报错,不建议大家这样写
// 因为泛型方法上的T 会和 泛型类上的T 产生歧义
public static <T> void show(T t) {
System.out.println("in 泛型static方法show, t: " + t);
}
}
测试类:
public static void main(String[] args) {
Circle<Integer,Integer> c = new Circle<>();
//public <F> void disp(F f);
//调用时系统自动识别泛型方法类型
c.disp(1); //Integer
c.disp(2.3); //Double
c.disp("hello");//String
c.disp('h'); //Character
System.out.println("--------------");
//public static <T> void show(T t);
//通过类名可以直接调用,不需要额外指定泛型类型
Circle.show(2.3);
Circle.show(2);
Circle.show("hello");
}
//运行结果:
in 泛型方法disp, f: 1
in 泛型方法disp, f: 2.3
in 泛型方法disp, f: hello
in 泛型方法disp, f: h
--------------
in 泛型static方法show, t: 2.3
in 泛型static方法show, t: 2
in 泛型static方法show, t: hello
五、通配符
通配符可以匹配所有的泛型类型
主要分为三种:
- 无界通配符 ?
- 上界通配符 ?super T
- 下界通配符 ?extends T
①无界通配符
观察以下代码:
public void test1(Collection<Integer> c) {
}
public void test2(Collection<String> c) {
}
public void test3(Collection<Double> c) {
}
可以发现他们只能接受一种类型的集合对象
原因是由于泛型的类型之间没有多态
所以 = 两边的泛型类型必须一致
这种情况就可以使用通配符 ?来表示泛型的父类型:
public void test(Collection<?> c) { }
public static void main(String[] args){ Test t = new Test(); t.test(new ArrayList<String>()); t.test(new ArrayList<Integer>()); t.test(new ArrayList<Double>()); t.test(new ArrayList<任意引用类型>()); }
②上界通配符
格式:
类型名<? extends 类型 > 对象名称
意义:
只能接收该类型及其子类
public static void main(String[] args) {
List<? extends Number> list;
//list可以指向泛型是Number或者Number【子】类型的集合对象
list = new ArrayList<Number>();
list = new ArrayList<Integer>();
list = new ArrayList<Double>();
//编译报错,因为String不是Number类型,也不是Number的子类型
//list = new ArrayList<String>();
}
能表示数字的类型都是Number类型的子类型,例如 Byte Short Integer Long 等
③下界通配符
格式:
类型名<? super 类型 > 对象名称
意义:
只能接收该类型及其父类型
public static void main(String[] args) {
List<? super Number> list;
//list可以指向泛型是Number或者Number【父】类型的集合对象
list = new ArrayList<Number>();
list = new ArrayList<Serializable>();
list = new ArrayList<Object>();
//编译报错,因为String不是Number类型,也不是Number的父类型
//list = new ArrayList<String>();
//编译报错,因为Integer不是Number类型,也不是Number的父类型
//list = new ArrayList<Integer>();
}
④泛型中 extends 和 super 对比:
extends 可以定义泛型的 上限
这就表示将来泛型所接收的类型 最大 是什么类型
可以是这个 最大类型 或者它的 子类型
super 可以定义泛型的 下限
这就表示将来泛型所接收的类型 最小 是什么类型
可以是这个 最小类型 或者它的 父类型
六、类型擦除
泛型类型仅存在于编译期间
编译后的字节码和运行时不包含泛型信息
所有的泛型类型映射到同一份字节码
直接看代码就懂了:
// 编写时的代码
List<String> stringList = new ArrayList<String>();
List<Integer> intList = new ArrayList<Integer>();
// 编译后实际变成(简化理解)
List rawList1 = new ArrayList(); // 泛型信息被"擦除"
List rawList2 = new ArrayList(); // 泛型信息被"擦除"
为什么要这么做呢?
历史原因:
Java 5 引入泛型时,已经存在大量 没有泛型 的老代码和类库
为了让新代码能和老代码兼容,Java 设计者采用了"类型擦除"方案。
注意!
泛型信息被擦除后,会转换为以下类型:
①无界泛型 ——> Object
(无界通配符同理)
// 擦除前
public class Box<T> {
private T content;
public T getContent() { return content; }
}
// 擦除后(概念上)
public class Box {
private Object content; // T 被替换为 Object
public Object getContent() { return content; }
}
②有界泛型 ——> 边界类型
(通配符同理,自动去掉 ?extends 和 ?super)
// 擦除前
public class NumberBox<T extends Number> {
private T number;
public T getNumber() { return number; }
public double getValue() { return number.doubleValue(); } // 可以调用 Number 方法
}
// 擦除后(概念上)
public class NumberBox {
private Number number; // T 被替换为 Number
public Number getNumber() { return number; }
public double getValue() { return number.doubleValue(); }
}
多重边界的话会变成第一个边界:
// 擦除前
public class ComparableSerializableBox<T extends Comparable<T> & Serializable> {
private T value;
public T getValue() { return value; }
}
// 擦除后(概念上)
public class ComparableSerializableBox {
private Comparable value; // 第一个边界类型
public Comparable getValue() { return value; }
}
③泛型方法(T ——> Object)
// 擦除前
public static <T> T getFirst(List<T> list) {
return list.get(0);
}
// 擦除后(概念上)
public static Object getFirst(List list) { // T 被替换为 Object
return list.get(0);
}
注意数组:
// 擦除前
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int size) {
array = (T[]) new Object[size]; // 这里有陷阱!
}
}
// 擦除后,实际创建的是 Object[] 数组
七、易踩坑的点
1、= 两边的泛型类型必须一致
//编译通过
//父类型的引用,指向子类对象
Object o = new Integer(1);
//编译通过
//Object[]类型兼容所有的【引用】类型数组
//arr可以指向任意 引用类型 数组对象
Object[] arr = new Integer[1];
//编译失败
//注意,这个编译报错,类型不兼容
//int[] 是基本类型数组
Object[] arr = new int[1];
//编译失败
//错误信息:ArrayList<Integer>无法转为ArrayList<Object>
//在编译期间,ArrayList<Integer>和ArrayList<Object>是俩个不同的类型,并且没有子父类型的关系
ArrayList<Object> list = new ArrayList<Integer>();
虽然 Integer 是 Object 的子类型
但是 ArrayList<Integer> 和 ArrayList<Object> 之间没有子父类型的关系
它们就是两个不同的类型
所以
Object o = new Integer(1); 编译通过
ArrayListlist<Objcet> = new ArrayList<Integer>();编译报错
也就是说
两个类型,如果是当做泛型的指定类型的时候,就没有多态的特点了
2、使用通配符后不能添加数据
但是可以添加 null
Collection<?> c;
c = new ArrayList<String>();
//编译报错
//因为变量c所声明的类型是Collection,同时泛型类型是通配符(?)
//那么编译器也不知道这个?将来会是什么类型,因为这个?只是一个通配符
//所以,编译器不允许使用变量c来向集合中添加新数据。
c.add("hello");
//编译通过
//有一个值是可以添加到集合中的,null
//集合中一定存的是引用类型,null是所有引用类型共同的一个值,所以一定可以添加进去。
c.add(null);
虽然不可以添加元素了,但是可以遍历集合取出数据:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("hello1");
list.add("hello2");
list.add("hello3");
list.add("hello4");
Collection<?> c = list;
//编译报错
//c.add("hello5");
for(Object obj : c) {
System.out.println(obj);
}
}
3、泛型数组创建的限制
为什么不能直接创建泛型数组?
// 这些都是非法的,编译器会报错
// T[] array = new T[10]; // 错误
// List<String>[] lists = new List<String>[5]; // 错误
// List<T>[] genericArray = new List<T>[5]; // 错误
根本原因在于:
类型擦除导致运行时无法知道具体的类型参数
类型擦除与数组创建的冲突:
public class GenericArrayProblem<T> {
// 假设这能编译通过
// T[] array = new T[10]; // 如果 T = String,应该创建 String[]
// 但是在运行时,JVM 看到的是:
// Object[] array = new Object[10]; // 因为 T 被擦除为 Object
// 这会导致类型安全问题
public void demonstrateProblem() {
// 如果允许这样做,会出现问题:
// String[] stringArray = (String[]) new Object[10]; // ClassCastException
}
}
那么如何正确创建泛型数组?
①使用 Object[] 并进行类型转换:
public class GenericArrayWithObject<T> {
private Object[] array;
private Class<T> type;
public GenericArrayWithObject(Class<T> type, int size) {
this.array = new Object[size];
this.type = type;
}
@SuppressWarnings("unchecked")
public void set(int index, T value) {
// 运行时检查类型
if (value != null && !type.isInstance(value)) {
throw new ClassCastException("Expected " + type.getName() +
", but got " + value.getClass().getName());
}
array[index] = value;
}
@SuppressWarnings("unchecked")
public T get(int index) {
return (T) array[index]; // 类型转换
}
public int size() {
return array.length;
}
}
②使用通配符创建数组:
public class WildcardArrayCreation {
// 可以创建无界通配符数组,但有限制
public void createWildcardArray() {
// 直接创建仍然不行
// List<?>[] array = new List<?>[10]; // 编译错误
// 但可以通过类型转换创建
List<?>[] array = (List<?>[]) new List[10]; // 未检查转换警告
// 使用示例
array[0] = new ArrayList<String>();
array[1] = new ArrayList<Integer>();
// 但不能添加元素(除了 null)
// array[0].add("Hello"); // 编译错误
array[0].add(null); // 只能添加 null
}
}
③通过反射创建泛型数组(这个后面遇到再说)
4、泛型方法调用陷阱
public class GenericMethodTrap {
// 错误示例:泛型方法参数推断问题
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void demonstrateTrap() {
// 当传递 null 时,编译器无法推断类型
// swap(null, 0, 1); // 编译错误:无法推断类型
// 解决方案1:显式指定类型参数
swap((String[]) null, 0, 1);
// 解决方案2:使用具体类型
String[] strings = null;
swap(strings, 0, 1);
}
// 错误示例:泛型集合不支持协变(协变:类型安全前提下,子类型替换父类型)
public static void covarianceTrap() {
// 这样做是不安全的
// List<Object> objectList = new ArrayList<String>(); // 编译错误
// 但是这样是可以的(使用通配符)
List<? extends Object> objectList = new ArrayList<String>();
// 注意:使用 extends 通配符后不能添加元素(除了 null)
// objectList.add("Hello"); // 编译错误
objectList.add(null); // 只能添加 null
}
}