如果你曾被Java泛型中的T和?搞得头晕眼花,祝贺你,这篇文章就是为你准备的!这是每个Java开发者进阶的必经之路,也是面试中的高频考点。今天,我们用最接地气的方式彻底搞懂它们!
第一章:初识T和?——它们到底是谁?
简单来说,T和?虽然都出目前泛型中,但扮演着完全不同的角色:
T:类型参数 – 就像一个”类型变量”,在定义类、接口或方法时声明,后面可以直接使用
?:通配符 – 表明”未知类型”,只在实例化时使用,不能直接作为类型使用
来看一个生动的例子:
// T 在定义时使用
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// ? 在使用时使用
public void processList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
第二章:T的舞台——定义时的主角
T主要在三种场景下使用:
1. 泛型类:
// T在这里定义
public class Container<T> {
private T item;
public Container(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
// 使用时指定具体类型
Container<String> stringContainer = new Container<>("Hello");
Container<Integer> intContainer = new Container<>(123);
2. 泛型方法:
// <T> 在返回值前声明类型参数
public <T> T getFirst(List<T> list) {
return list.get(0);
}
// 编译器会自动推断类型
String firstString = getFirst(Arrays.asList("A", "B", "C"));
Integer firstInt = getFirst(Arrays.asList(1, 2, 3));
3. 泛型接口:
public interface Repository<T> {
void save(T entity);
T findById(Long id);
}
// 实现时指定具体类型
public class UserRepository implements Repository<User> {
@Override
public void save(User entity) { }
@Override
public User findById(Long id) { return null; }
}
第三章:?的奥秘——使用时的万能牌
通配符?的真正威力在于它的灵活性,主要有三种形式:
1. 无界通配符:<?>
// 可以接受任何类型的List
public void printAll(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// 所有这些调用都是合法的
printAll(Arrays.asList(1, 2, 3));
printAll(Arrays.asList("A", "B", "C"));
2. 上界通配符:<? extends T>
// 只能读,不能写(除了null)
public void processNumbers(List<? extends Number> numbers) {
for (Number number : numbers) {
System.out.println(number.doubleValue());
}
// numbers.add(new Integer(1)); // 编译错误!
}
processNumbers(Arrays.asList(1, 2, 3)); // Integer
processNumbers(Arrays.asList(1.1, 2.2, 3.3)); // Double
3. 下界通配符:<? super T>
// 可以写,读取时只能当做Object
public void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i); // 可以写入Integer
}
// Integer value = list.get(0); // 编译错误!
}
List<Number> numberList = new ArrayList<>();
addNumbers(numberList); // 可以添加Integer到Number列表
第四章:实战对决——PECS原则
记住这个黄金法则:Producer Extends, Consumer Super
当你主要从集合读取数据时(生产者),使用extends
当你主要向集合写入数据时(消费者),使用super
看这个经典例子:
// 正确的做法
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
List<Number> numbers = new ArrayList<>();
List<Integer> integers = Arrays.asList(1, 2, 3);
copy(numbers, integers); // 完美运行!
第五章:面试直通车——高频问题解析
1. List<?> 和 List<Object> 有什么区别?
List<?> 可以接受任何类型的List,但只能读取为Object
List<Object> 只能接受Object类型的List,但可以写入任何Object
2. 什么时候用T,什么时候用??
需要在多个地方引用同一个类型时用T
只关心类型的”范围”而不关心具体类型时用?
3. 下面代码为什么编译错误?
List<? extends Number> list = new ArrayList<Integer>();
list.add(new Integer(1)); // 编译错误!
由于编译器不知道具体的类型是Integer、Double还是其他,为了类型安全,不允许写入。
总结:T是类型代言人,?是类型变色龙
T让你在编码时保持类型一致性,?让你在使用时获得灵活性。掌握它们的关键在于理解:T用于定义时的类型传递,?用于使用时的类型包容。
目前,你是不是对T和?有了全新的认识?下次面试遇到这个问题,信任你必定能对答如流!
思考题:你知道在Spring框架和MyBatis中以及在Android的Java编程中,T和?都是如何被巧妙运用的吗?欢迎在评论区留言讨论!
彩蛋:
1.定义:
JDK5.0后,Java提供了泛型。
泛型是一种在编译时提供类型安全的方式,允许程序员在定义类、接口和方法时使用类型参数。这样,可以在不损失类型安全的情况下,创建可重用的代码。
泛型有两种主要的使用形式:类型参数(如 T)和通配符(如 ?)。
T一般用作类型参数的占位符。列如:泛型方法的语法包括一个包含在尖括号内的类型参数列表,并将它置于方法的返回类型之前。
?通配符主要用于泛型方法的参数和泛型类的字段,以及泛型集合的声明中。
通配符分为3种,分别为:
1)上界通配符:List<? extends Number>
可以读取容器内的元素。由于容器的具体类型未知,如果往容器添加元素,无法确保添加进去的具体数据是该容器具体类型的子类还是父类,因此存在类型不安全问题,所以是不允许往容器里添加数据的。
2)无界通配符:List<?>
3)下界通配符:List<? super Number>
可以读取也可以写入元素。
2.使用范围:
1)T 用作声明类的类型参数、方法(包括静态泛型方法、非静态泛型方法、泛型构造函数,但类型参数仅限于方法内使用)的类型参数。
2)? 通配符用作 参数类型、字段类型、局部变量类型及返回类型。
通配符在PECS(Producer Extends Consumer Super)原则中超级有用,该原则指出当你从一个泛型集合中获取对象时(生产者),应该使用上界通配符,当你向泛型集合中插入对象时(消费者),应该使用下界通配符。
通配符,一般是用于定义一个引用变量,以便实现”多态”调用(非真正意义上的多态)。例如
//以下是正确的代码:
public static void main(String[] args) {
List<String> sList = new ArrayList<String>();
List<Integer> iList = new ArrayList<Integer>();
sList.add("abc");
iList.add(10000);
dump(sList);
dump(iList);
}
public static void dump(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
一般我们使用 ? 的时候并不知道也不关心这个时候的类型,只想使用其通用的方法,而且 ? 通配符是无法作用于声明类的类型参数,一般作用于方法和参数上。而 类型变量 T 在类定义时具有更广泛的应用。
在某些程度的使用上 ? 通配符与 T 参数类型是可以等效的,但是 T 参数类型并不支持下界限制, 即 T super AClass 而 通配符支持下界限制 ? super AClass,即:使用super限定父集的时候,T 不可以,? 可以。
T和?两者都可以通过extends来限定一个类型的子集,但是 T 可以 List<T extends Number & AInterface> 限定为多重限定,? 不可以。
使用T时,Java的类型参数支持多重限定,如 <T extends CharSequence & Comparable<T>,但如果类型参数中包含类,则需要将类参数类型写在最前面。
如果你想写一个通用的方法且该方法的逻辑不关心类型那么就用 ? 通配符来进行适配,如果你需要作用域类型(这可能在操作通用数组类型时更有用)或者声明类的类型参数时请使用 T 类型变量。
//以下是错误的用法(编译错误:通配符不能用在创建对象上):
ArrayList<?> list = new ArrayList<?>();
//以下是错误的用法(List<T>在实例化的时候T要替换成具体的类):
List<T> t = new ArrayList<T>();
类型参数T和通配符?可以混合使用,例如以下为一个接受泛型集合并返回其中最大元素的方法:
public static <T extends Comparable<T>> T max(Collection<? extends T> collection) {
T maxElement = null;
for (T element : collection) {
if (maxElement == null || element.compareTo(maxElement) > 0) {
maxElement = element;
}
}
return maxElement;
}
3.总结:
当对通用的对象类型进行操作时,使用 Object的缺点为:无法对 Object 编译时进行检查,由于 Object 是所有类的父类。
? 表明了集合[所有Java类型,包括String,Integer,Character等系统定义的,或者用户定义的类型]这个整体;而 T 表明了集合[所有Java类型,包括String,Integer,Character等系统定义的,或者用户定义的类型]中的一个成员。
当Integer类是Number类的子类时,List<? extends Integer>是List<? extends Number>的子类,而List<Integer>不是List<Number>类的子类。
可以向 List<Object> 中插入 Object 对象或者其子类对象,但只能向 List<?> 中插入 null 值,由于List<?>无法确定插入的元素的类型,而null是所有类型的成员。
类型参数写成全大写的有意义的单词是更具可读性的方式。