《回炉重造》——泛型
泛型
前言
以前学习到「泛型」的时候,只是浅浅的知道可以限制类型,并没有更深入理解,可以说基础的也没理解到位,只是浮于表面,所以,现在回炉重造,重学泛型!打好基础!
什么是泛型?
泛型(Generic),Generic 的意思有「一般化的,通用的」。
是 JDK 5 中引入的新特性,它提供编译时的类型安全检测,允许我们在编译时检测到非法的数据类型,本质是 参数化类型。
这里还涉及到一个词「参数化类型」。什么意思呢?
意思就是:把类型参数化(只能感慨中国文化博大精深),即我们可以把类型作为参数,换句话说,就是所操作的数据类型被指定为一个参数。
说到参数,我们也熟悉,你看,方法上的形参、调用方法时的实参,这些都是参数,对吧。
同理,类型,即 Java 中的各种基本的引用类型,当然包含你自己定义的类型,说白了就是各种类(Class),类可以作为参数,就是上面讲的把类型作为参数(好吧,好像讲了一堆废话)。这又涉及到一个词,即「类型参数」。
我们可以看看 ArrayList 的源码,如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
}
ArrayList<E>
中的 <E>
,这里的 E
可以说是一个「类型形参」。
而我们写 ArrayList<String> list = new ArrayList<>()
的时候,给 ArrayList 这个集合指定了一个具体的类型 String
,形参 E
传入的实参就是 String
,也就是说 String
是一个「类型实参」。
简而言之:
ArrayList<E>
中的 E
称为 类型形参;ArrayList<String>
中的 String
称为 类型实参。这两个合起来,就是上面提到的「类型参数」。
为什么会有泛型的出现?
泛型和集合有千丝万缕的关系,我们现在用集合,也是使用「泛型集合」。
List<Integer> list = new ArrayList<>(); // 泛型集合
当然,我们一开始学习的时候,并没有用到泛型,即非泛型集合。
List list = new ArrayList(); // 非泛型集合
在以前没有泛型的情况下,我们看看会出现什么问题。默认 ArrayList 集合中存储的元素类型是 Object
,这样很好,Java 中任何类型的终极父类就是 Object,什么类型的数据都能存储到这个集合中。
比如我可以这样操作(经典案例):
List list = new ArrayList(); // 非泛型集合
list.add("Hello World!"); // 存储 String 类型
list.add(23); // 存储 int 类型,这里会自动装箱为 Integer 类型
list.add(true); // 存储 Boolean 类型
for (Object o : list) { // 用 Object 接收,合情合理
System.out.println(o);
}
我们存储数据之后,后续肯定需要使用它,就需要从集合中取出来,而取出来进一步操作是需要明确具体的数据类型,那么就需要进行强制类型转换。
for (Object o : list) {
String s = (String) o; // 强制类型转换
// 后续操作...
}
此时代码并不会报错,编译也不会有问题,直到我们运行时,就会出现异常——ClassCastException
。
这也是必然的,毕竟我们的集合中还有其他类型的数据,其他类型的数据,再怎样强制类型转换也不可能转成 String 类型,也就会出现异常了。
看到这里,估计有小伙伴要问了,我一个一个强制转换不行吗?我知道存储的是什么数据,到时直接获取相对应的数据进行强转就行了啊。是,没错,你可以一个一个强转,数量少的情况下是可以,但是你数量很多的情况下呢?你怎么办?
所以,泛型出现了,它可以限制下我们在编译期的类型,保证类型是安全的,即运行时不会发生异常的。
List<String> list = new ArrayList<>(); // 泛型集合
list.add("Hello World!"); // 存储 String 类型
list.add("23");
list.add("Coding Coding");
for (String s : list) { // 用 String 接收
System.out.println(s);
// 后续操作...
}
看到这里的小伙伴,可能有这么一个疑惑:那这样为什么不直接使用一个 String 数组呢?这个问题问得好。
数组确实能够存储同一个数据类型的数据,但是对于想无限制存储元素时,数组就有它的缺点,数组长度是固定不可变。总而言之,数组使用起来不方便,所以才有集合的出现,而集合又因为有这种问题,进而出现泛型集合。
这里使用了泛型,那么我们在 add()
的时候,编译期间就会对添加的元素进行类型检查,而且在获取集合元素的时候,也不需要强制类型转换了,直接用指定的类型接收就行了。
使用泛型有什么好处?
- 无需强制类型转换(集合、反射)
- 增加代码可读性,我们可以通过泛型,知道现在操作的是什么数据类型。一句话,给人看的。
- 代码复用,可以根据不同情况传入不同的数据类型,进行不同的操作。
泛型类
定义语法:
class 类名<通配符,通配符,通配符...> {
private 通配符 变量名;
...
}
通配符:T、E、K、V,也就是上面说的类型形参。(这里的通配符,也有人称为泛型标识)
使用语法:
类名<具体的数据类型> 对象名 = new 类名<具体的数据类型>();
类名<具体的数据类型> 对象名 = new 类名<>(); // JDK 7 开始可以省略,人们称为 菱形语法
举个栗子:
/**
定义泛型类
*/
public class Generic<T> {
private T variable;
public void setVariable(T variable) {
return this.variable = variable;
}
public T getVariable() {
return variable;
}
}
测试
Generic<String> g = new Generic<>(); // 指定泛型为 String
g.setVariable("god23bin"); // 正常
g.setVariable(23); // 提示错误,因为这里是 int 型
String var = g.getVariable();
需要注意的点:
-
你使用泛型类时,没有指定数据类型,那么将默认为 Object 类型
-
泛型的类型参数,只能是引用数据类型,不支持基本数据类型。
-
泛型在逻辑上,你操作的是不同的数据类型,但是实际上,还是同样的类型(比如上面例子中的 Generic 类,泛型指定不同的数据类型,逻辑上是不同的,但是实际上还是 Generic 类型,这里就涉及到「类型擦除」)
-
如果有继承:
// 子类如果需要是泛型类,那么其类型参数需要包含父类的类型参数
class ChildGeneric<T> extends Generic<T> {} // OK
class ChildGeneric<T, E> extends Generic<T> {} // OK
// 子类不是泛型类,那么父类的类型参数需要明确
class ChildGeneric extends Generic<String> {}
泛型接口
定义语法:
interface 接口名 <通配符,通配符,通配符...> {
通配符 方法名();
...
}
使用语法:
// 接口实现类是泛型类,那么实现类的类型参数需要包含接口的类型参数
class Demo<T> implements Generic<T> {} // OK
class Demo<T, E> implements Generic<T> {} // OK
// 接口实现类不是泛型类,那么接口类型参数需要明确
class Demo implements Generic<String> {}
泛型方法
之前是在类和接口上定义了泛型,然而有时候,我们并不需要整个类都定义类型,只需要其中某一个方法定义泛型,只关心这一个方法,这时就可以使用把泛型定义在方法上,这样调用泛型方法的时候,才指定具体的类型参数。
定义语法:
访问修饰符 <通配符,通配符,通配符...> 返回值类型 方法名(形参列表) {
// 方法体
}
举个栗子:
public <T, E> void getGeneric() {
// 方法体
}
public <T, E> void getGeneric(Game<T> game) {
// 方法体
}
这里需要注意的是,泛型方法和泛型类中使用了泛型的普通的方法是不一样的。
// 这是泛型类中使用了泛型的普通的方法
public T getVariable() {
return variable;
}
// 这是泛型方法,只有定义了 <T,...> 的方法才是泛型方法
public <T, E> void getGeneric() {
// 方法体
}
而且,如果你在泛型类中定义了泛型方法,那么泛型方法中的 <T,...>
类型形参和泛型类上的类型形参是不一样的,是相互独立的。还有,泛型方法可以定义成静态的,还没完,泛型方法还可以结合可变参数。
举个栗子:
/**
定义泛型类
*/
public class Generic<T> {
private T variable;
public void setVariable(T variable) {
return this.variable = variable;
}
public T getVariable() {
return variable;
}
// 泛型方法,这里的T和类上的T不是同一个T
public <T> T getGeneric(List<T> list) {
return list.get(0);
}
// 静态的泛型方法
public static <T> T getGenericStatic(List<T> list) {
return list.get(0);
}
// 结合可变参数的泛型方法
public static <E> void print(E... e) {
// 这里的参数可以当作数组进行遍历
for (E elem : e) {
System.out.println(elem);
}
}
}
通配符之问号
之前出现的 T,E,K,V
这些,也都是通配符,不过,这些通配符是属于类型形参的通配符。那么类型实参的通配符呢?这就来啦!类型实参通配符:?
。没错,你没看错,就是一个问号。
类型实参的通配符是使用 ?
来代表具体的类型实参的,代表任意类型。
举个例子:
public class Generic<T> {
...
public static void showGame(Games<String> games) { // 要求Games指定的类型为String
String one = games.getOne();
System.out.println(one);
}
}
上面要求 Games 指定的类型为 String。那么我们这样操作:
Generic<String> g = new Generic<>();
Games<String> games = new Games();
g.showGame(games); // OK
Games<Integer> games2 = new Games();
g.showGame(games2); // Error,因为指定了为String
所以使用 ?
通配符
public class Generic<T> {
...
public static void showGame(Games<?> games) { // 使用类型实参通配符 ?
String one = games.getOne();
System.out.println(one);
}
}
通配符上下限
类型通配符的上下限,有的地方也称为上下界,还有称限定通配符的,意思都一样。
上限语法:
类/接口<? extends 实参类型>
这里的 extends 可以这样理解,<? extends A>
,使用的时候,我们传入的实参类型需要小于等于A类,即需要是A的子类或A本身,这样就限制了通配符的上限了,你最高只能是A类。
下限语法:
类/接口<? super 实参类型>
这里的 super 可以这样理解,<? super A>
,使用的时候,我们传入的实参类型需要大于等于A类,即需要是A的父类或A本身,这样就限制了通配符的下限了,你最低只能是A类。
举个例子,这里有 A、B、C 三个类,A 是 B 的父类,B 是 C 的父类。
public class Demo {
public static upperLimit(List<? extends B> list) { // 类型实参通配符上限为B类
// ...
}
public static lowerLimit(List<? super B> list) { // 类型实参通配符下限为B类
// ...
}
}
调用这个方法
List<A> l1 = new ArrayList<>();
List<B> l2 = new ArrayList<>();
List<C> l3 = new ArrayList<>();
Demo.upperLimit(l1); // Error,这里传入 l1,而上面搞了通配符上限,超过了B类,比B类还上
Demo.upperLimit(l2); // OK
Demo.upperLimit(l3); // OK
Demo.lowerLimit(l1); // OK
Demo.lowerLimit(l2); // OK
Demo.lowerLimit(l3); // Error,同理,比B类还下,自然错误,需要比B类上,超过B类才行
需要注意的是,你搞了通配符的上限,在集合中,那么是只能用来读取数据,而不能用来存储数据,这该怎么理解呢?
public class Demo {
public static upperLimit(List<? extends B> list) { // 类型实参通配符上限为B类
list.add(new B()); // Error
list.add(new C()); // Error
// 因为我们使用上限通配符,不知道传入进来的 List 是什么类型的,可能是List<B>,可能是List<C>
// 所以是不能进行存储数据的
}
public static lowerLimit(List<? super B> list) { // 类型实参通配符下限为B类
// ...
}
}
那么下限呢?放心,下限没有这个问题,可以存储数据。
public class Demo {
public static upperLimit(List<? extends B> list) { // 类型实参通配符上限为B类
// ...
}
public static lowerLimit(List<? super B> list) { // 类型实参通配符下限为B类
list.add(new B());
list.add(new C());
// 因为下限通配符,只限定了下限,但是上限是没有限制的,也就是说可以看成上限就是 Object
// 上限是 Object,那么任何类都默认继承 Object,那么自然可以添加 C 类型的数据
// 也就是存储数据的类型是没有限制的。
for (Object o : list) {
System.out.println(o);
}
}
}
类型擦除
泛型的限制,只在编译期存在,一旦在运行了,那么便消失了,即类型被擦除了。
有两种情况:
- 无限制类型擦除
- 有限制类型擦除
无限制:
有限制:
泛型方法上的类型擦除也是同理。还有一个知识点就是,在泛型接口的类型擦除中,会出现一个「桥接方法」,主要是保持接口和类的实现关系。
以上,就是泛型的基本内容了。
面试题
开始回顾八股文!!!
Java 泛型是什么?常用的通配符有哪些?
泛型(Generics)是 JDK5 中引入的一个新特性,它提供了编译时类型安全检测的机制。这个机制可以在编译时就检测到非法的数据类型。本质是一个参数化类型,就是所操作的数据类型可以被指定为一个特定的参数类型。
常用的通配符有 T(Type)、K(Key)、V(Value)、E(Element)、?(未知类型)
Java 的泛型是如何工作的 ? 什么是类型擦除?(泛型擦除是什么?)
Java 的泛型是伪泛型,因为在 Java 运行期间,这些泛型信息都会被擦掉,就是所谓的类型擦除(泛型擦除)。
什么是泛型中的限定通配符和非限定通配符?
限定通配符,顾名思义,就是对类型进行限定,Java 中有两种限定通配符。
一种是 < ? extends T >
,它通过确保类型必须是T的子类来限定上界,即类型必须为T类型或者T子类
另一种是< ? super T >
,它通过确保类型必须是T的父类来限定下届,即类型必须为T类型或者T的父类
< ? >
表示了非限定通配符,因为 < ? > 可以用任意类型来替代。
你的项目中哪里用到了泛型?
- 可用于定义通用返回结果类
CommonResult
通过参数T
可根据具体的返回类型动态指定结果的数据类型 - 用于构建集合工具类。参考
Collections
中的sort
,binarySearch
方法
最后的最后
由本人水平所限,难免有错误以及不足之处, 屏幕前的靓仔靓女们
如有发现,恳请指出!
最后,谢谢你看到这里,谢谢你认真对待我的努力,希望这篇博客对你有所帮助!
你轻轻地点了个赞,那将在我的心里世界增添一颗明亮而耀眼的星!