《回炉重造》——泛型

泛型

前言

以前学习到「泛型」的时候,只是浅浅的知道可以限制类型,并没有更深入理解,可以说基础的也没理解到位,只是浮于表面,所以,现在回炉重造,重学泛型!打好基础!

泛型

什么是泛型?

泛型(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 方法

最后的最后

由本人水平所限,难免有错误以及不足之处, 屏幕前的靓仔靓女们 如有发现,恳请指出!

最后,谢谢你看到这里,谢谢你认真对待我的努力,希望这篇博客对你有所帮助!

你轻轻地点了个赞,那将在我的心里世界增添一颗明亮而耀眼的星!

posted @ 2022-05-23 20:57  god23bin  阅读(564)  评论(4编辑  收藏  举报