一:泛型是什么?
泛型就是给类或方法加上一个“类型标签”。
没有泛型之前,集合里可以放任意类型,取出来必须强转,容易报错。
有了泛型,编译器帮我们检查类型,放错类型直接编译报错,取出来直接是目标类型,不用强转。
一句话:泛型是把“运行时的ClassCastException”提前到“编译时报错”。
为什么要用泛型?
-
类型安全——放错类型编译期就报错,不会等到线上崩了才发现
-
消除强转——取出来直接用,不用写
(String)list.get(0) -
通用代码——写一个类/方法,支持所有类型(比如
List<T>、Box<T>)
二:泛型的基本语法
1. 泛型类
// 定义:T是类型参数,用的时候指定
public class Box<T> {
private T item;
public void set(T item) {
this.item = item;
}
public T get() {
return item;
}
}
// 使用
Box<String> box = new Box<>(); // JDK1.7以后右边可以省略类型
box.set("苹果");
String str = box.get(); // 不用强转
记住:
-
T是Type,E是Element,K是Key,V是Value——约定俗成,不是强制 -
创建对象用
new Box<>()——菱形语法
2. 泛型方法
public class Util {
// 错误:E没有声明
public static void method(E e) {}
// 正确:<E>写在返回值前面
public static <E> void method(E e) {}
// 有返回值的泛型方法
public static <E> E getLast(E[] arr) {
return arr[arr.length - 1];
}
}
// 调用
String last = Util.getLast(new String[]{"a", "b", "c"});
死规矩:
方法里用到的泛型字母,要么来自类,要么自己在返回值前面声明。
3. 泛型接口
// 定义
public interface List<E> {
boolean add(E e);
E get(int index);
}
// 实现方式1:实现类指定具体类型
public class StringList implements List<String> {
private String[] arr;
public boolean add(String e) { ... }
public String get(int index) { ... }
}
// 实现方式2:实现类继续泛型
public class ArrayList<E> implements List<E> {
private Object[] arr;
public boolean add(E e) { ... }
public E get(int index) { ... }
}
三:泛型通配符
List<Object> 和 List<?> 有什么区别?
List<Object> | List<?> | |
|---|---|---|
| 含义 | 这个List明确是Object类型 | 这个List是某种类型,但我不知道具体是什么 |
| 可以存什么? | 可以存任何类型(String、Integer...) | 不能存任何东西(除了null) |
| 可以取什么? | 取出来是Object | 取出来是Object |
代码演示:
// List<Object>
List<Object> list1 = new ArrayList<>();
list1.add("字符串"); //
list1.add(123); //
Object obj = list1.get(0); // 取出来是Object
// List<?>
List<?> list2 = new ArrayList<String>();
list2.add("abc"); // 编译报错 不能放
list2.add(null); // 唯一能放的是null
Object obj2 = list2.get(0); // 可以取,但类型是Object
为什么不能放?
因为
list2可能是List<String>,也可能是List<Integer>。
编译器不敢让你放任何东西,万一是List<Integer>,你放String就崩了。
<? extends T> 和 <? super T> 是什么?
这是泛型的上限和下限。
<? extends T> —— 上限通配符(T或T的子类)
// 这个list里的元素,都是Number或Number的子类 List<? extends Number> list = new ArrayList<Integer>(); // Integer是Number子类 // 只能读,不能写 Number n = list.get(0); // 可以读,因为一定是Number子类 list.add(123); // 不能写 万一是List<Double>呢?
场景: 方法只读数据,不修改数据。
<? super T> —— 下限通配符(T或T的父类)
// 这个list里的元素,都是Integer或Integer的父类 List<? super Integer> list = new ArrayList<Number>(); // Number是Integer父类 // 可以写,读出来是Object list.add(123); // 可以写,Integer一定能放进去 Object obj = list.get(0); // 读出来只能是Object,不知道具体类型
场景: 方法只写数据,不关心读出来的具体类型。
PECS原则:Producer Extends, Consumer Super。
生产者用extends,消费者用super。
什么意思?
-
你从里面取数据(它是生产者)→
<? extends T> -
你往里面放数据(它是消费者)→
<? super T>
四:泛型擦除
Java的泛型是真正的泛型吗?
你答:
不是。Java的泛型是伪泛型,或者说编译期泛型。
泛型信息只在编译期存在,编译成字节码时会被擦除,所有类型参数替换为Object或上限,并自动插入强转代码。
代码证明:
ArrayList<String> list1 = new ArrayList<>(); ArrayList<Integer> list2 = new ArrayList<>(); System.out.println(list1.getClass() == list2.getClass()); // true // 运行时都是ArrayList.class,泛型信息没了
泛型擦除带来什么问题?
1. 不能用基本类型
List<int> list = new ArrayList<>(); // 编译报错 List<Integer> list = new ArrayList<>(); // 必须用包装类
擦除后变成Object,int不是Object的子类。
2. 不能重载签名一样的方法
public void method(List<String> list) {}
public void method(List<Integer> list) {} // 编译报错
擦除后都是
method(List list),签名冲突。
3. 不能通过反射获取泛型具体类型(正常途径)
List<String> list = new ArrayList<>(); // 运行时不知道这个list原本是String类型
但可以通过父类的泛型获取(TypeReference)
五:项目里怎么用的
场景1:统一返回结果(最常用)
public class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 200;
r.msg = "成功";
r.data = data;
return r;
}
}
// 调用
Result<User> r1 = Result.success(new User());
Result<List<User>> r2 = Result.success(userList);
Result<Void> r3 = Result.success(null);
为什么T不用Object?
答: 用Object的话,前端调用要强转,而且文档生成工具(Swagger)能识别Result<User>里的User,识别不了Result里的Object。
场景2:泛型BaseDao/BaseService
public class BaseService<T> {
@Autowired
private BaseMapper<T> mapper;
public T getById(Long id) {
return mapper.selectById(id);
}
public int save(T entity) {
return mapper.insert(entity);
}
}
@Service
public class UserService extends BaseService<User> {
// 继承getById和save,泛型自动变成User
}
场景3:泛型工具方法
// 把数组转成List
public static <T> List<T> arrayToList(T[] arr) {
List<T> list = new ArrayList<>();
for (T t : arr) {
list.add(t);
}
return list;
}
// 获取集合第一个元素
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
六:
Q1:什么是泛型?为什么要用泛型?
泛型是JDK1.5引入的特性,把类型参数化。
主要作用是类型安全和消除强制转换。
没有泛型的话,集合里能放任意对象,取出来必须强转,容易出现ClassCastException。
泛型让编译器在编译期就检查类型,放错类型直接报错,取出来直接用。
Q2:泛型类、泛型方法、泛型接口的区别?
泛型类:类名后面带
<T>,整个类都可以使用这个T泛型方法:返回值前面带
<T>,T只在当前方法有效泛型接口:和泛型类类似,实现类可以指定具体类型,也可以继续泛型
Q3:List<T>、List<Object>、List<?>的区别?
List<T>:正在定义泛型时使用,T是类型参数
List<Object>:具体类型是Object,可以放任何对象
List<?>:通配符,表示未知类型,只能读不能写
Q4:什么是泛型擦除?
Java的泛型是编译期实现的,编译后会擦除类型信息。
比如List<String>编译成List,所有类型参数替换为Object,并自动插入强转。
所以运行时无法获取泛型的具体类型。
Q5:可以声明泛型数组吗?
// 不能直接创建 T[] arr = new T[10]; // 编译报错 // 只能通过反射或强转 T[] arr = (T[]) new Object[10];
因为泛型擦除后,T[]变成Object[],存的时候可能类型错误。
1676

被折叠的 条评论
为什么被折叠?



