【从0开始学习Java | 第15篇】泛型
文章目录
- 引言:你是否也遇到过这样的麻烦?
- 一、泛型是什么?一句话说清核心
- 二、为什么需要泛型?3个核心优势
- 三、泛型怎么用?3个核心场景
- 通配符的基本形式
- 通配符的使用场景
- 通配符与泛型方法的区别
- 总结
- 四、泛型的细节和注意事项
- 结语:泛型的本质是“抽象”
引言:你是否也遇到过这样的麻烦?
假设你想写一个“求两个数之和”的函数,一开始可能这么写:
// 整数相加
public int addInt(int a, int b) {return a + b;
}// 浮点数相加
public double addDouble(double a, double b) {return a + b;
}
如果还要支持字符串拼接、自定义对象合并呢?难道要为每种类型都写一个几乎一样的函数?
这就是泛型要解决的核心问题:让代码摆脱“类型绑定”,实现“一次定义,多种类型通用”。今天我们就来聊聊泛型——这个让代码更简洁、更安全的“利器”。
一、泛型是什么?一句话说清核心
泛型(Generics)是编程语言中一种“参数化类型”的机制,简单说就是:在定义类、接口或方法时,不指定具体类型,而是用一个“占位符”代替,等到使用时再传入实际类型。
比如上面的“相加”需求,用泛型可以写成:
// 泛型方法:T是类型占位符,代表“任意类型”
public <T> T add(T a, T b) {// 实际实现需根据类型处理,这里仅示意return ...;
}
调用时传入整数、字符串等类型,编译器会自动“替换”占位符,保证类型匹配。
二、为什么需要泛型?3个核心优势
-
消除重复代码,提高复用性
不用为每种类型写重复逻辑,一套代码适配多种场景。比如Java集合框架(List
、Map
)就是泛型的典型应用:List<String>
存字符串,List<Integer>
存整数,底层逻辑却完全复用。 -
编译时类型检查,减少运行时错误
没有泛型时,集合中可以放任意类型的对象,取出来时需要强制类型转换,容易出现ClassCastException
。有了泛型后,编译器会在编译阶段检查类型是否匹配,提前规避错误:List<String> list = new ArrayList<>(); list.add(123); // 编译报错:不能添加整数到字符串列表
-
代码更清晰,可读性更高
看到List<User>
就知道这个列表存的是用户对象,不用猜类型,也不用看注释。
三、泛型怎么用?3个核心场景
- 泛型方法:在方法上定义类型参数,适用于“单个方法需要适配多种类型”的场景(即方法中的形参类型不确定时)。
方案1: 在方法申明上定义自己的泛型【只能在本方法中使用,其他方法不能使用】
public class MyArrayList {Object[] obj = new Object[10];int size;public <E> boolean add(E e){obj [size]=e;size++;return true;}
}
方案2:在类名后面定义泛型【公共泛型,所有方法都可以用】
public class MyArrayList<E> {Object[] obj = new Object[10];int size;public boolean add(E e){obj [size]=e;size++;return true;}public static <E> void addAll(ArrayList<E>list, E e1,E e2) {list.add(e1);list.add(e2);}
}
- 泛型类/接口:在类或接口定义时声明类型参数,适用于“整个类的逻辑与类型相关”的场景(如集合、容器)。
例 - 泛型类:自定义一个泛型栈:
public class GenericStack<T> {private List<T> elements = new ArrayList<>();public void push(T element) {elements.add(element);}public T pop() {if (elements.isEmpty()) return null;return elements.remove(elements.size() - 1);}
}
// 使用时指定类型,无需强转
GenericStack<String> stringStack = new GenericStack<>();
stringStack.push("hello");
String str = stringStack.pop(); // 直接得到String类型
例子2 - 泛型类:
两种实现方式:
- 实现类给出具体的类型
public class MyArrayList3 implements List<String> {.....
}public class Generics {public static void main(String args[]){MyArrayList3 list3 = new MyArrayList2();}
}
- 实现类延续泛型,创建实现类对象时再确定泛型
public class MyArrayList2<E> implements List<E>{.....
} public class Generics {public static void main(String args[]){MyArrayList2<String> list2 = new MyArrayList2<>();}
}
- 泛型通配符:在Java泛型中,通配符(Wildcard)是一种特殊的语法,用于灵活地表示“未知类型”,解决泛型类型参数的灵活性问题。它的核心作用是在不明确指定具体类型的情况下,限制泛型的范围,让代码更通用。
通配符的基本形式
通配符用 ?
表示,常见的使用场景有三种:
-
无界通配符(Unbounded Wildcard):
?
表示“任意类型”,但无法获取泛型的具体类型信息(编译时会擦除)。示例:
// 可以接收任何泛型类型的List public void printList(List<?> list) {for (Object obj : list) {System.out.println(obj);} }
注意:使用无界通配符的集合不能添加元素(除了
null
),因为编译器不知道具体类型,无法保证类型安全。 -
上界通配符(Upper Bounded Wildcard):
? extends T
表示“T类型或T的子类”,限制了泛型的上限。示例:
// 接收Number及其子类(如Integer、Double)的List public double sum(List<? extends Number> list) {double total = 0;for (Number num : list) {total += num.doubleValue(); // 可调用Number的方法}return total; }
特点:
- 可以读取元素(类型为
T
或其子类,可安全向上转型为T
)。 - 不能添加元素(除了
null
),因为无法确定具体是哪个子类(例如List<? extends Number>
可能是List<Integer>
或List<Double>
,添加Double
到List<Integer>
会出错)。
- 可以读取元素(类型为
-
下界通配符(Lower Bounded Wildcard):
? super T
表示“T类型或T的父类”,限制了泛型的下限。示例:
// 接收Integer及其父类(如Number、Object)的List public void addIntegers(List<? super Integer> list) {list.add(10); // 可以添加Integer(或其子类,如Integer本身)list.add(20); }
特点:
- 可以添加元素(类型为
T
或其子类,因为父类集合可以接收子类对象)。 - 读取元素时,只能确定是
Object
类型(因为父类可能是任意上层类型,无法安全转型为具体类型)。
- 可以添加元素(类型为
通配符的使用场景
- 无界通配符:当代码不依赖泛型的具体类型,仅需要“任意类型的泛型”时(如打印集合)。
- 上界通配符:当需要读取泛型对象,并使用其上层父类的方法时(如计算数值总和)。
- 下界通配符:当需要写入泛型对象,且对象类型是某个类的子类时(如向集合添加特定类型元素)。
通配符与泛型方法的区别
通配符和泛型方法都能实现泛型的灵活性,但适用场景不同:
- 通配符更适合限制泛型范围(如
? extends Number
),简化参数声明。 - 泛型方法(如
<T> void method(List<T> list)
)更适合需要在方法中复用具体类型的场景(如返回与参数同类型的对象)。
总结
通配符通过?
、? extends T
、? super T
三种形式,解决了泛型类型的灵活性问题,核心是在“类型安全”和“通用性”之间找到平衡:
- 上界通配符:只能读,不能写(除
null
)。 - 下界通配符:只能写(特定类型),读时只能作为Object。
- 无界通配符:只读(Object),基本不能写。
四、泛型的细节和注意事项
-
类型擦除:泛型信息仅在编译时存在,运行时会被“擦除”为原始类型(如
List<String>
运行时会变成List
)。因此不能用new T()
、T.class
这样的写法,也不能判断obj instanceof List<String>
【所以,实际上来说,Java中的泛型其实是伪泛型,只是在编译时检查,运行时仍然换回Object类型处理,处理完毕后再次强转回对应的类型输出而已】 -
泛型数组限制:不能直接创建泛型数组(如
new T[10]
不允许),但可以声明泛型数组引用(T[] array
) -
静态方法不能使用类的泛型参数:类的泛型参数属于实例,静态方法需自己声明泛型参数
-
泛型中不能写基本数据类型(在泛型擦除时,基本数据类型无法转换成Object类型)
-
指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类型
-
如果不写泛型,类型默认是Object
-
泛型不具备继承性,但是数据具备继承性
结语:泛型的本质是“抽象”
泛型的核心思想是“对类型进行抽象”——就像函数用参数抽象值,泛型用类型参数抽象具体类型。它让代码从“为具体类型服务”升级为“为一类逻辑服务”,这也是面向对象“抽象”思想的延伸。
如果我的内容对你有帮助,请 点赞 , 评论 , 收藏 。创作不易,大家的支持就是我坚持下去的动力!