【用Java学习数据结构系列】泛型上界与通配符上界

看到这句话的时候证明:此刻你我都在努力

加油陌生人

个人主页:Gu Gu Study**
专栏:用Java学习数据结构系列
喜欢的一句话: 常常会回顾努力的自己,所以要为自己的努力留下足迹
喜欢的话可以点个赞谢谢了。
作者:小闭**


目录

前言

泛型的概念

泛型的擦除机制

泛型的上界

通配符上界

我们实现一下场景一:

场景二:

泛型上界与通配符上界的区别

泛型上界:

通配符上界:

通配符下界


前言

本系列准备已经结束,反射,lambda表达示,之类知识了。本系列属于数据结构初阶,进阶的敬请期待。本文章主要是讲泛型的进一步认识,以及更加底层的String类的认识。

之前也写过一篇泛型初阶的一篇文章,大家如果没看过可以再看看。

这篇文章已经讲了:包装类,简单的编译器推导,泛型的基本使用,以及泛型上界。

泛型的概念

泛型是Java中一种强大的特性,它允许程序员在编写代码时指定类型参数,从而使得代码更加灵活和可重用。泛型提供了一种方式,使得编译器可以在编译时检查类型安全,避免了类型转换的错误和运行时的类型检查。

通俗来说: 就是适用于许多许多类型 ,从代码上讲,就是对类型实现了参数化。

语法:

class 泛型类名称<类型形参列表> {

// 这里可以使用类型参数

}

简单示例泛型的简单使用:

class MyArray<T> {  //注释1
    public Object[] array = new Object[10];
    public T getPos(int pos) {
        return (T)this.array[pos];
    }
    public void setVal(int pos,T val) {
        this.array[pos] = val;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray<>();//注释2
        myArray.setVal(0,10);
        myArray.setVal(1,12);//注释3
        int ret = myArray.getPos(1);
        System.out.println(ret);
        myArray.setVal(2,"bit");//注释4 此处是错误的
    }
}

代码注释处解析:

**注释一:**我们在类名后加了<T>,这里的作用就是泛型得基本用法,相当于这个T就代表一个类,但具体是哪个类我,还需要在创建这个类对象的时候,我们指定哪个类,才会知道。

**注释二:**这里我们创建对象时(new一个对象时)我们同样在类名后加了<Integer>,这就是我们指定T就是Integer类,则在我们创建的类对象时 T 就是Integer。

**注释三:**因为我们指定T为Intege类型,则在使用setVal(1,12);方法时我们可以直接传参12,然后jJVM就会进行自动拆包了。

**注释四:**首先说明这里的使用是编译器是会报错的,因为我们前面已经指定T为Integer了,这是传入一个String显然是不对的。所以编译器是会报错的。要想传入String储存到数组中,我们就需要在创建一个MyArray<String>对象。


泛型的擦除机制

关于泛型的擦除机制,它是Java泛型实现的一个核心概念。在Java中,泛型的类型参数在编译期间会被替换为其边界或Object类型,这个过程被称为类型擦除。这意味着在运行时,泛型的类型参数实际上是被"擦除"了,泛型代码在运行时无需知晓具体的类型参数。例如,如果有一个泛型类`List<T>`,在运行时,无论`T`是什么类型,`List<T>`都会被当作`List<Object>`来处理。

这种机制的主要目的是为了向后兼容Java的旧版本,同时减少代码重复,使得代码更加简洁 。但是,这也带来了一些限制和挑战,比如不能在运行时获取泛型参数的具体类型,泛型数组的创建受到限制等。

尽管如此,通过一些技巧和设计模式,可以在一定程度上绕过这些限制,让代码更加灵活和可扩展。

类型擦除机制也意味着,在编译过程中,所有的泛型类型参数`T`都会被替换为`Object`,这就是我们通常所说的泛型擦除。由于被编译器当作`Object`类型处理,我们可以通过反射set任意类型的参数。但是,这种擦除也导致了一些问题,比如在泛型类中不能直接调用泛型参数的具体方法,因为这在编译时是未知的。解决这个问题的一种方法是给泛型参数一个边界,这样编译器就能知道泛型参数至少具有哪些方法。

**总结:**泛型的擦除机制是Java泛型实现的关键部分,它允许泛型代码在运行时以一种类型安全的方式处理不确定的类型,但同时也带来了一些限制和挑战。


泛型的上界

什么是泛型上界呢?

泛型上界(Bounded Type Parameters) 是泛型编程中的一个概念,它允许我们为泛型类型参数指定一个边界,即限制泛型参数必须是某个类或接口的子类或实现。这样做可以提供更多的类型安全,并允许在泛型代码中使用更具体的操作。

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。 就如上面所说给泛型一个边界,这样编译器就能知道泛型参数至少具有哪些方法。

代码语法:

class 类名 <形参类型 extends 另一个类>{
//代码
}

例如:

public class Myarr< E extends Number >{
//代码
}

代码解析:

这里当我们要创建Myarr这个类对象时,指定 E 的类型时,那么这时我们指定的类只能是Number类或它的子类作为类型实参。如果不是那么编译器就会报错。

**扩展:**那么我们如果我们没有定义上界,而是public class Myarr< E >时,我们就可以看做E extends Object;

通配符上界

我们上面说过当我们使用泛型类时吗,我们需要指定一个类作为泛型参数类。那么这时我们就会产生两个场景。

场景一:

那么如果我们有一个方法,是获取各个泛型类对象里的元素 ,这时我们定义这个方法时的形参类型到底怎么确定呢?如果我们指定一个类 就会使得形参类型定死,无法实现获取各种泛型类里面的元素。

场景二:

如果我们现在要创建一个泛型类,但是暂时还不想示例化 ,只是定义一个null的泛型类,后面才随机实例化范围内的泛型类对象。

为了实现上面两个场景,就有了通配符上界

class Food {
    public void show(){
        System.out.println("食物");
    }
}
class Fruit extends Food {
    public void show(){
        System.out.println("水果");
    }

}
class Apple extends Fruit {
    public void show(){
        System.out.println("苹果");
    }
}
class Banana extends Fruit {
    public void show(){
        System.out.println("香蕉");
    }
}

class Message<T> { // 设置泛型
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}

如上当我们有以上的类时。

我们实现一下场景一:

class TestDemo {
    public static void main(String[] args) {
        Message<? extends Fruit> message;//这时还不知道示例化苹果还是香蕉 
        
        message== new Message<Apple>() ;//过一会知道了,这时才指定类型,当然也可以是其它情况
        
        message.setMessage(new Apple());
        
       
       
    }
    
}

如上:我们在想创建对象时还没知到message对象中 T 为什么类(对象) ,那么这时我们就使用 通配符上界Message<? extends Fruit>进行暂时限定泛型类的范围。到了后面我们知道了,这里的知道可以是 if 判断得出 ,或是返回值判断知道,并不像代码中的一样是我们后面主观指定了Apple。

场景二:

class TestDemo {
    public static void main(String[] args) {
        Message<Apple> message = new Message<>() ;
        message.setMessage(new Apple());
        fun(message);
        Message<Banana> message2 = new Message<>() ;
        message2.setMessage(new Banana());
        fun(message2);
    }
    
    // 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
    public static void fun(Message<? extends Fruit> temp){
//temp.setMessage(new Banana()); //仍然无法修改!
//temp.setMessage(new Apple()); //仍然无法修改!
        temp.getMessage().show();
        System.out.println(temp.getMessage());

    }
}

如上:我们的fun函数,我们可以根据不同的传值,就可以得出不同的对象(限定范围内的),只需要在实参里使用通配符上界的语法。

注意:因为我们这里Message的指定类型是Fruit的子类或其本身,所以这里我们可以是直接用Fruit来直接 接受这个指定的类型的,顶多也就是向上转型,但是我们是无法对temp进行设置类的,因为这时我们传入实参的时候已经确定了的,T已经确定了一个类,而我们这时也就无法对其进行设置。

简单来说: **通配符的上界,不能进行写入数据,只能进行读取数据。**这与通配符下界是完全相反的。下文还会给大家介绍通配符下界。

public static void fun(Message<? extends Fruit> temp){
//temp.setMessage(new Banana()); //仍然无法修改!
//temp.setMessage(new Apple()); //仍然无法修改!
    Fruit b = temp.getMessage();
    System.out.println(b);
}

泛型上界与通配符上界的区别

在Java中,泛型上界和通配符上界是两个不同的概念,它们在泛型编程中扮演着不同的角色。下面分别解释它们的含义和区别:

泛型上界:

泛型上界是在声明泛型类型时指定的,用来限制泛型类型参数的类型范围。

它通常用在泛型类的声明中,例如 class Box<T extends Number> 表示 T 必须是 Number 或其子类的类型。

泛型上界是静态的,即在编译时就已经确定的。

通配符上界:

通配符上界是在实例化泛型类或使用泛型方法时使用的,用来指定通配符的类型范围。

它通常用在泛型的实例化和传递参数时,例如 List<? extends Number> 表示这个列表可以包含 Number 类型及其所有子类型的元素。

通配符上界是动态的,即在运行时可以确定具体类型。

具体区别

  • 使用场景不同
    • 泛型上界是在定义泛型类或接口时使用的,用来限制类型参数的类型。
    • 通配符上界是在实例化泛型类或调用泛型方法时使用的,用来指定具体的类型范围。
  • 类型安全
    • 泛型上界提供了编译时的类型安全检查,确保类型参数不会超出指定的范围。
    • 通配符上界则提供了运行时的类型安全,允许在运行时确定具体的类型。
  • 协变与逆变
    • 泛型上界可以是协变的(extends),也可以是逆变的(super),这取决于泛型参数是用作输入还是输出。
    • 通配符上界通常是协变的,表示可以接收更具体的类型。
  • 类型擦除
    • 泛型上界在编译时会进行类型擦除,泛型类型参数会被替换为其上界。
    • 通配符上界在运行时不会进行类型擦除,它们用于保持泛型的灵活性。
  • 实例化
    • 泛型上界在定义泛型类时实例化,不需要显式指定。
    • 通配符上界在创建泛型实例时显式指定。

**通俗来说:**泛型上界是在定义泛型时用来限制类型参数的,而通配符上界是在实例化泛型时用来指定具体类型范围的。

通配符下界

通配符下届与上界相似,只是指定的范围有所不一样。还有就是跟上面说的与通配符的性质是相反的。

通配符的下界,只能进行写入数据,不能进行读取数据。

代码还是类似举例:

class Food {
    public void show(){
        System.out.println("食物");
    }
}
class Fruit extends Food {
    public void show(){
        System.out.println("水果");
    }

}
class Apple extends Fruit {
    public void show(){
        System.out.println("苹果");
    }
}
class Banana extends Fruit {
    public void show(){
        System.out.println("香蕉");
    }
}

class Plate<T> {
    private T plate ;
    
    public T getPlate() {
        return plate;
    }
    
    public void setPlate(T plate) {
        this.plate = plate;
    }
}

class TestDemo {
    public static void main(String[] args) {
        Plate<Fruit> plate1 = new Plate<>();
        plate1.setPlate(new Fruit());
        通配符的下界,不能进行读取数据,只能写入数据。
        fun(plate1);
        Plate<Food> plate2 = new Plate<>();
        plate2.setPlate(new Food());
        fun(plate2);
    }

    public static void fun(Plate<? super Fruit> temp) {
// 此时可以修改!!添加的是Fruit 或者Fruit的子类
        temp.setPlate(new Apple());//这个是Fruit的子类
        temp.setPlate(new Fruit());//这个是Fruit的本身
//Fruit fruit = temp.getPlate(); 不能接收,这里无法确定是哪个父类
        System.out.println(temp.getPlate());//只能直接输出
    }
}

无法接受的原因:因为在实参传入进去时T也是被确定为Fruit的父类或Fruit类,无法确定返回的是哪个父类,所以我们无法接收。

相关推荐
生命几十年3万天2 分钟前
java的threadlocal为何内存泄漏
java
caridle14 分钟前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
萧鼎17 分钟前
Python并发编程库:Asyncio的异步编程实战
开发语言·数据库·python·异步
学地理的小胖砸18 分钟前
【一些关于Python的信息和帮助】
开发语言·python
疯一样的码农18 分钟前
Python 继承、多态、封装、抽象
开发语言·python
^velpro^19 分钟前
数据库连接池的创建
java·开发语言·数据库
苹果醋323 分钟前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花27 分钟前
【JAVA基础】Java集合基础
java·开发语言·windows
小松学前端30 分钟前
第六章 7.0 LinkList
java·开发语言·网络
Wx-bishekaifayuan37 分钟前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava