看到这句话的时候证明:此刻你我都在努力
加油陌生人
个人主页: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类,无法确定返回的是哪个父类,所以我们无法接收。