Java复习——泛型

本文深入探讨Java泛型的使用场景,解释为何引入泛型及其实现机制,包括泛型类、接口、方法的定义,以及通配符、类型限定等高级特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

相关参考:

《Java核心技术精讲》

https://kb.cnblogs.com/page/104265/

泛型

为什么要使用泛型?

设想我们现在需要创建一个基类描述一个平面坐标系。而现实中的这种坐标系有很多种类,可以用整数表示(11,22),也可以用小数表示(11.22,22.11),亦或者可以用字符串表示(“东经112°”,“北纬23.5°”)。这些数据,都需要两个属性组合表示,我们可以分别用数学上常用的x,y代替,但是x,y可以接收多种类型的数据,那么使用Object类来接收是比较合适的:

public class Temp {
    public static void main(String[] args){
        Point point1 = new Point();
        point1.setX(11);
        point1.setY(22);
        System.out.println("x:"+((Integer)point1.getX()));// 为了类型统一,强转
        System.out.println("y:"+((Integer)point1.getY()));// 为了类型统一,强转

        Point point2 = new Point();
        point2.setX(11.22);
        point2.setY(22.11);
        System.out.println("x:"+((Double)point2.getX()));// 为了类型统一,强转
        System.out.println("y:"+((Double)point2.getY()));// 为了类型统一,强转

        Point point3 = new Point();
        point3.setX("东经112°");
        point3.setY("北纬23.5°");
        System.out.println("x:"+((String)point3.getX()));// 为了类型统一,强转
        System.out.println("y:"+((String)point3.getY()));// 为了类型统一,强转
    }
}

class Point {
    private Object x;
    private Object y;

    public Object getX() {
        return x;
    }

    public void setX(Object x) {
        this.x = x;
    }

    public Object getY() {
        return y;
    }

    public void setY(Object y) {
        this.y = y;
    }
}

但是:

public class Temp {
    public static void main(String[] args){
        Point point1 = new Point();
        point1.setX("东经112°");
        point1.setY(22);
        System.out.println("x:"+((Integer)point1.getX()));// 为了类型统一,强转
        System.out.println("y:"+((Integer)point1.getY()));// 为了类型统一,强转
    }
}

class Point {
    private Object x;
    private Object y;

    public Object getX() {
        return x;
    }

    public void setX(Object x) {
        this.x = x;
    }

    public Object getY() {
        return y;
    }

    public void setY(Object y) {
        this.y = y;
    }
}

此时就会发生ClassCastException异常。以上的代码是有安全隐患的,并且编译器并没有在编译的时候发现。所以需要泛型来解决这种问题。泛型相对于依赖程序员来记住对象类型、执行类型转换——这会导致程序运行时的失败,很难调试和解决,而编译器能够帮助程序员在编译时强制进行大量的类型检查,发现其中的错误:

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();// 泛型不能传入int等基本数据类型
        point1.setX(11);
        point1.setY(22);
        System.out.println("x:"+(point1.getX()));// 不需要强制转型了
        System.out.println("y:"+(point1.getY()));// 不需要强制转型了
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();// 泛型不能传入int等基本数据类型
        point1.setX("东经112°");// 此时编译器报错,会提示不能转换
        point1.setY(22);
        System.out.println("x:"+(point1.getX()));// 不需要强制转型了
        System.out.println("y:"+(point1.getY()));// 不需要强制转型了
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

public class Temp {
    public static void main(String[] args){
        // 如果不设置泛型,统一按Object接收
        Point point1 = new Point();
        point1.setX("东经112°");// 此时编译器出现警告信息
        point1.setY(22);
        System.out.println("x:"+(point1.getX()));
        System.out.println("y:"+(point1.getY()));
//        输出:
//        x:东经112°
//        y:22
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

另外泛型只在编译阶段有效:

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();// 泛型不能传入int等基本数据类型
        point1.setX(11);
        point1.setY(22);

        Point<String> point2 = new Point();// 泛型不能传入int等基本数据类型
        point2.setX("东经112°");
        point2.setY("北纬23.5°");

        Class<? extends Point> point1Class = point1.getClass();
        Class<? extends Point> point2Class = point2.getClass();
        System.out.println(point1Class == point2Class);// true
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

在编译过程中,正确检验泛型后,会将泛型的相关信息擦除

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();// 泛型不能传入int等基本数据类型
        point1.setX(11);
        point1.setY(22);

        Point<String> point2 = new Point();// 泛型不能传入int等基本数据类型
        point2.setX("东经112°");
        point2.setY("北纬23.5°");

        boolean b1 = point1 instanceof Point<Integer>;// 不能对确切的泛型类型使用instanceof操作,编译器会报错
        System.out.println(b1);
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

通配符

先看下面的代码:

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();
        point1.setX(11);
        point1.setY(22);
        method(point1);
    }

    public static void method(Point<Integer> point){// 只接受Point<Integer>类型
        System.out.println("x = "+point.getX());
        System.out.println("y = "+point.getY());
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

如果此时把泛型为String的对象传入method(),编译器会报错(类型不匹配),那如果将method()重载呢?

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();
        point1.setX(11);
        point1.setY(22);
        method(point1);
    }

    public static void method(Point<Integer> point){// 编译器报错:提示该方法已定义
        System.out.println("x = "+point.getX());
        System.out.println("y = "+point.getY());
    }

    public static void method(Point<String> point){// 只接受Point<Integer>类型
        System.out.println("x = "+point.getX());
        System.out.println("y = "+point.getY());
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

因为泛型擦除后,两个方法是一模一样的,所以方法的重载观察的不是泛型类型,而是类的名称,或者是数据的类型

那如果方法的形参不设置泛型呢,是可以解决问题的:

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();
        point1.setX(11);
        point1.setY(22);
        method(point1);

        Point<String> point2 = new Point();
        point2.setX("balaba");
        point2.setY("balala");
        method(point2);
    }

    public static void method(Point point){// 编译器报错:提示该方法已定义
        System.out.println("x = "+point.getX());
        System.out.println("y = "+point.getY());
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

但是存在新的问题:

package com.example.wudongjiang.eventbusdemo;

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();
        point1.setX(11);
        point1.setY(22);
        method(point1);
//        输出:
//        x = heihei
//        y = 22
    }

    public static void method(Point point){
        point.setX("heihei");
        System.out.println("x = "+point.getX());
        System.out.println("y = "+point.getY());
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

这就失去了泛型存在的意义!好比你今天买了瓶可乐喝,但是你喝完后,想整蛊下你的朋友,于是把墨汁和酱油参和进去了,明显属于造假!所以用户使用一个产品时,应该只能使用,不能修改。

要想解决上面的问题,就必须找到一种方法:此方法可以接收任意类型的泛型,但是不能修改,只能够输出,这就需要引用通配符"?"

package com.example.wudongjiang.eventbusdemo;

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();
        point1.setX(11);
        point1.setY(22);
        method(point1);
//        输出:
//        x = heihei
//        y = 22
    }

    public static void method(Point<?> point){// 编译器报错:提示该方法已定义
//        point.setX("heihei");// 编译器报错,不能修改
        System.out.println("x = "+point.getX());
        System.out.println("y = "+point.getY());
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

对于通配符"?"表示任意类型,可以当成Object理解。但是要注意:

package com.example.wudongjiang.eventbusdemo;

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();
        point1.setX(11);
        point1.setY(22);
        method(point1);
        Point<Number> point2 = point1;//报错,这样是无法接收的,
//        输出:
//        x = heihei
//        y = 22
    }

    public static void method(Point<?> point){
//        point.setX("heihei");// 编译器报错,不能修改
        System.out.println("x = "+point.getX());
        System.out.println("y = "+point.getY());
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

因为Number要比Integer范围大,可以这样理解:Point point1 = new Point();表示我拥有了一筐的苹果,但是Point point2 = point1不代表我这个箩筐装的是各种各样的水果

设置泛型的上限

? extends 父类:如? extends Number,代表只能是Number或者Number的子类。

public class Temp {
    public static void main(String[] args){
        Point<Integer> point1 = new Point();
        point1.setX(11);
        point1.setY(22);
        method(point1);
//        输出:
//        x = 11
//        y = 22
    }

    public static void method(Point<?> point){//
//        point.setX("heihei");// 编译器报错,不能修改
        System.out.println("x = "+point.getX());
        System.out.println("y = "+point.getY());
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T extends Number> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

设置泛型的下限

? super 子类:如 ? super String,代表只能是String或者是String的父类。

public class Temp {
    public static void main(String[] args){
        Point<String> point1 = new Point();
        point1.setX("东经112°");
        point1.setY("北纬23.5°");
        method(point1);
    }

    public static void method(Point<? super String> point){
        System.out.println("x = "+point.getX());
        System.out.println("y = "+point.getY());
    }
}

/**
 * T可以是任意标示,但是命名应该有意义
 * @param <T>
 */
class Point<T> {
    private T x;
    private T y;

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

泛型接口

方式一:在子类上继续定义泛型,同时泛型在此接口上继续使用

public class Temp {
    public static void main(String[] args){
        new MessageImpl<String>().showText("hello");
    }
}

interface Message<T>{
    void showText(T msg);
}

/**
 * 在子类上继续定义泛型,同时泛型在此接口上继续使用
 *
 * 如果不在子类上定义泛型(class MessageImpl implements Message<T>),编译器会报错:不能解析符号T
 * @param <T>
 */
class MessageImpl<T> implements Message<T>{
    @Override
    public void showText(T msg) {
        System.out.println("showText() "+msg);
    }
}

方式二:在子类设置具体类型

public class Temp {
    public static void main(String[] args){
        new MessageImpl().showText("hello");
    }
}

interface Message<T>{
    void showText(T msg);
}

/**
 * 在子类设置具体类型
 */
class MessageImpl implements Message<String>{
    @Override
    public void showText(String msg) {
        System.out.println("showText() "+msg);
    }
}

泛型方法

public class Temp {
    public static void main(String[] args){
        String[] arrs = new String[]{"1","11","111"};
        print(arrs);
    }

    public static<T> void print(T...arr){
        for (int i = 0; i < arr.length;i++){
            System.out.print(arr[i]+" , ");
        }
    }
}

定义泛型方法时,必须在返回值前边加一个,泛型类中的使用了泛型的成员方法并不是泛型方法。

静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 :

class Point<T>{
    public static<C> void print(C...arr){// 不能使用T
		......
    }
}

在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的上添加上下边界,即在泛型声明的时候添加:

public static<T> void method(Point<T extends String> point){// 编译器报错

}

public static<T extends String> void method(Point<T> point){// 正确做法

}

在日后的编码中,应该尽量使用泛型方法:不应该为使用泛型方法,而将整个类泛型化

泛型类型的子类型

如果类型Father是类型Son的子类型,那ClassName 和 ClassName之间是什么关系?答案就是没有任何关系

List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers;// 编译器报错

如果上面的操作是成立的话,就有可能出现下面这种情况:

List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers;// 编译器报错
numbers.add(new Double(11.3));

相当于一箱苹果是一箱水果,但是后面又往一箱水果里面参杂了一些橘子~这显然是不行的

数组和泛型类型上用法的不一致

对于泛型类型,它们和类型的子类型之间是没什么关系的。而对于数组,它们和子类型是相关的:如果类型A是类型B的子类型,那么A[]是B[]的子类型:

Integer[] integers = new Integer[10];
Number[] numbers = integers;

Integer[] integers = new Integer[10];
Number[] numbers = integers;
numbers[0] = new Double(11.1);// java.lang.ArrayStoreException: java.lang.Double

这样写真的可以编译,但是在运行时抛出ArrayStoreException异常。因为数组的这特点,在存储数据的操作上,Java运行时需要检查类型的兼容性。这种检查,很显然,会带来一定的性能问题,你需要明白这一点。

泛型出现后,数组的这个个性已经不再有使用上的必要了,实际上是应该避免使用。

如果你想从一个数据类型里获取数据(get),使用 ? extends 通配符

原因非常的简单,你可以这样想:这个? extends T通配符告诉编译器我们在处理一个类型T的子类型,但我们不知道这个子类型究竟是什么。因为没法确定,为了保证类型安全,我们就不允许往里面加入任何这种类型的数据。另一方面,因为我们知道,不论它是什么类型,它总是类型T的子类型,当我们在读取数据时,能确保得到的数据是一个T类型的实例:

如果你想把对象写入一个数据结构里(add),使用 ? super 通配符

? super T 我们不知道究竟是什么超类,但我们知道T和任何T的子类都跟它的类型兼容。既然这个未知的类型即是T,也是T子类的超类,我们就可以写入。但是如果我们想往里面加入T的超类,编译器就会警告你。因为我们不知道它是怎样的超类,所有这样的实例就不允许加入。

从这种形式的类型里获取数据又是怎么样的呢?结果表明,你只能取出Object实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个Object,因为Object是任何Java类型的超类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值