泛型详解

✨hello,愿意点进来的小伙伴们,你们好呐!
✨ 🐻🐻系列专栏:【数据结构】
🐲🐲本篇内容:泛型的细谈,欢迎大佬们指点
🐯🐯作者简介:一名现大二的三非编程小白

前言

Java编程思想一书中,有对泛型的介绍—一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
所以在JDK1.5后就引入了泛型这个语法,通俗的来说。泛型:就是实现了类型参数化,可以适用于许许多多的类型。

引出泛型

在这里插入图片描述

我们会发现,以上代码任何类型都可以存放,在1下标元素是字符串的情况下,也必须强制类型转换才不会编译错误。在这里插入图片描述
这样子的代码有一个很不好的地方,就是他任何类型数据都可以存放,然而我们更多情况下只希望他可以持有一种类型的数据类型,而不是同时持有这么多类型。所以泛型的主要目的是:指定一个容器,想让他持有什么类型的对象。让编译器去检查,我们可以把类型当成参数去进行传递,想要什么类型就传递什么类型。

泛型的语法

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

在这里插入图片描述

  1. < T > – 是一个占位符 -》 表示当前类是一个泛型类,给T传递一个类型,类中的T都是这种类型。
  2. 泛型中不可以new泛型数组:T] ts = new T[5];
    想要创建泛型数组可以先new一个Object,然后进行强转:T[] array = (T[])new Object[10];

泛型的意义:

1. 在存放元素的时候,编译器会根据传入的泛型类型,来对类型进行检查,若类型不匹配则编译不通过。
2. 在取出元素时,编译器会自动帮你进行类型的转换,就不需要强制类型转换了,只在编译期间完成,运行时就没有泛型这个概念了。
3. 泛型主要用于编译的时候,是编译期间的一种机制,这种机制 -》 擦除机制。
4. <> 中一定是类的类型,不可以是基本数据类型。
5. 编译的时候会进行自动类型检查和转换。
6. 在泛型<> 中没有传输类型参数的时候这种叫做裸类型,就和普通的类一样了,都是建议不要去使用裸类型,裸类型是为了兼容老版本的API而保留的机制

泛型是如何编译的

擦除机制

擦除机制就是在编译时把代码中所有泛型都给擦除成Object类型。
关于擦除机制我们可以通过 javap-c 查看字节码文件可以看出:

在这里插入图片描述
在编译的过程当中,将所有的T替换为Object。Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。

为什么不能实例化泛型数组

了解了擦除机制后,我们来研究一下前文提到过的:不能实例化泛型数组。

public class Test05 {
    public static void main(String[] args) {
        MyArray1<Integer> a = new MyArray1<>();
        Integer[] t = a.getT();
    }
}
class MyArray1<T>{
    public T[] t = (T[])new Object[10];
    public T getPos(int pos) {
        return this.t[pos];
    }
    public void setT(int pos,T val) {
        this.t[pos] = val;
    }
    public T[] getT() {
        return t;
    }
}

在这里插入图片描述
为什么在这里会有异常出现呢?
因为Java中数组和泛型都可以进行类型检查,但是数组在运行时存储和检查类型信息。然而,泛型在编译时检查类型错误。
通俗讲就是:返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Person,运行的时候,直接转给Intefer类型的数组,编译器认为是不安全的。所以运行时就会有错误。

泛型的上界

在这里插入图片描述
泛型的上界:- 》 T一定是实现 了C omparable 接口的,就是 T一定是Comparable的子类,或者是Comparable本身。
在这里插入图片描述
我们要创建Alg类的对象,编译器会自动检测 传进去的参数类型有没有实现 Comparable 接口。
在上面代码中,因为Person没有实现Comparable接口,所以会报错。
所以我们就必须 让Person实现了Comparable接口。并且重写了compareTo
在这里插入图片描述

接下来我再来举一个例子:
在这里插入图片描述
在这里插入图片描述

当一个类的泛型所继承的类或者实现接口,那么我们在创建该类的对象的时候我们要传入泛型所继承的类或者实现的接口 的子类或者本身。
因为泛型的擦除机制,在泛型有继承关系的时候,会在编译的时候将泛型擦除成父类,也称为边界类。
没有实现继承与实现就默认上届为:Object

泛型方法

在这里插入图片描述

方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表){}

泛型方法有一些坑点:

坑点1:泛型方法加static

在这里插入图片描述

在没有加static之前,T是创建对象的时候传入类型的。static方法是通过类名调用的,在没有创建对象传入类型的时候,方法在加载时候没办法确定泛型类型,所以会导致编译不通过。
然后我们可以这样子解决:

在这里插入图片描述
在static后面加入泛型的声明。

然后调用该方法:
在这里插入图片描述
我们可以看到在调用该方法的时候,并没有尖括号指定类型,因为编译器会根据你传入的数据自动检测给你补充传入类型。比如说上述代码,编译器就会自动补充泛型类型:Integrt

2:我们也可以不要在创建类对象的时候传入泛型类型,可以在方法声明中加入泛型类型。

在这里插入图片描述
3:泛型方法传入参数可以省略

在这里插入图片描述

泛型的通配符 : ?

泛型通配符时泛型运用起来变得更加的灵活,可以扩充参数类型的范围,接下来我们来看一个例子来了解通配符的好处。

class Message<T> {
 	private T message ;
 	public T getMessage() {
	 	return message;
 	}
	public void setMessage(T message) {
		this.message = message;
	} 
}
public class TestDemo {
 	public static void main(String[] args) {
 		Message<String> message = new Message() ; 
		 message.setMessage("比特就业课欢迎您");
 		fun(message);
	 }
 	public static void fun(Message<String> temp){
		System.out.println(temp.getMessage());
	}
}

以上程序会有一个问题,如果现在泛型的类型设置的不是String,而是Integer,那我们还要重新创建一个fun方法,这样子就会比较的麻烦。所以我们可以创建一个可以接收所有泛型类型,但是又不能让用户去随意去修改,这种情况下就需要通配符啦。

使用通配符

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

由于通配符可以接收任意类型的参数,所以编译器不确定性类型,所以没办法修改里面的内容。

然后我们又会有一个疑问,通配符导致类型的范围太大了,有没有上下界呢?

通配符上界:

<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类

接下来来举个例子就可以更深刻的了解到了。

在这里插入图片描述
Food 为最高父类。

class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class Message<T> { // 设置泛型上限
	private T message ;
	public T getMessage() {
		return message;
 	}
	public void setMessage(T message) {
		this.message = message;
	}
}
 

public 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());
	}
	// 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
 	public static void fun(Message<? extends Fruit> temp){
 		//temp.setMessage(new Banana()); //仍然无法修改!
 		//temp.setMessage(new Apple()); //仍然无法修改!
		System.out.println(temp.getMessage());
	}
}

因为通配符设置的是上限,所以传进去的参数类型只能是Fruit的子类或者Fruit本身,然后我们再想要修改Message里面的值时,因为编译器不清楚传入的到底是什么类型,所以就无法修改其内容。
但是我们可以取出其内容,因为里面的内容都是Fruit或者其父类,编译器会自动给我们强制类型转换。

通配符下界:

在这里插入图片描述

<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型

代表传入的参数类型只能是 Integer 或者 Integer的父类

class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Message<T> {
	private T message ;
	public T getMessage() {
		return message;
	}
	public void setMessage(T message) {
		this.message = message;
 	}
}

public class TestDemo {
	public static void main(String[] args) {
		Message<Fruit> message = new Message<>() ;
 		message.setMessage(new Fruit());
 		fun(message);
 		Message<Food> message2 = new Message<>() ;
 		message2.setMessage(new Food());
 		fun(message2);
 	}
	//temp 接收Fruit及其子类的一个Message
 	public static void fun(Message<? super Fruit> temp){
 	// 此时可以修改!!添加的是Fruit 或者Fruit的子类
 		temp.setMessage(new Apple());//这个是Fruit的子类
 		temp.setMessage(new Fruit());//这个是Fruit的本身
 		//Fruit fruit = temp.getMessage(); 不能接收,这里无法确定是哪个父类
 		System.out.println(temp.getMessage());//只能直接输出
	}
}

因为泛型类型设置的是下限,所以存进去的类型只能是Furit或者Furit的父类,因此我们不能将里面的与元素给取出来,因为编译器不知道取出的究竟是什么类型的元素,无法进行强制类型转换。
但是我们可以将类型的子类元素存进去,因为无论如何也都是该类型的子类,编译器可以自动类型转换。## 标题

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无满*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值