Java-数据结构-包装类与泛型
一、包装类
Java的包装类指的是将基本数据类型(如int、float、boolean等)封装成对象的类。Java中的8个基本数据类型(byte、short、int、long、float、double、char、boolean)都有对应的包装类。
① 基本数据类型对应的包装类
基本数据类型 | 包装类 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
只需要记住两个特殊的:Integer和Character,其余的基本类型的包装类都是首字母大写。
② 装箱和拆箱
装箱和拆箱的这种说法看似非常高级,但其实本质上就是基本数据类型和其对应的包装类之间发生的相互转换~
📚 我们来看一段代码体会一下装箱和拆箱:
public class Main {
public static void main(String[] args) {
int a = 10;
// 此为装箱操作,创建一个 Integer 类型对象
// 将 a 转换成 Integer 型,或者将 a 的值放入 new Integer() 中
Integer A1 = Integer.valueOf(a);
Integer A2 = new Integer(a);
// 此为拆箱操作,将值从 Integer 对象中取出,放入对应的基本数据类型中去
int b = A1.intValue();
}
}
由此大家应该能大致理解装箱和拆箱的具体行为了,而其实这么写只是为了方便大家理解,但是由于这种繁琐的拆箱和装箱过程,会带来不少的代码量,所以为了减少开发者的负担,现在java提供了自动机制,也就是说以上的写法已经算是被淘汰了:
我们可以看到,其中代码几乎都显示了黄色背景,而代码背后为黄色背景可能代表以下情况:
代码被标记为警告
详细解释:当 IDEA 检测到代码可能存在潜在的问题,但这些问题不会导致编译错误或程序崩溃时,会将代码背景标记为黄色。
使用了已被标记为过时的方法或类
详细解释:当你使用的 API 的方法或类在更新的版本中被认为是不推荐使用的,IDEA 会将其背景标记为黄色。这是为了提醒开发者这些代码在未来版本中可能会被移除或者行为会发生改变,应该考虑使用替代的方法。
并且在A2的那段代码,甚至new Integer还被划了横线,这也代表代码不推荐使用~所以上面那段代码只是为了大家更好的理解装箱和拆箱,并不需要在实际开发中使用~
③ 自动装箱和自动拆箱
public class Main {
public static void main(String[] args) {
int a = 10;
Integer A1 = a; //自动装箱
int b = A1; //自动拆箱
}
}
📚 让我们看一段比较特殊的代码:
public class Main {
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a == b);
System.out.println(c == d);
}
}
大家觉得这段代码输出的结果会是什么呢?不妨大胆猜想一下~ 在进行猜想之前,我们首先要知道," == "所代表的含义是什么:引用是否指向同一个对象,而并非比较值。
输出结果:
欸?既然不是比较值,为何还会出现true呢?其实这是因为,Integer 里面的一些常用数值,都被放到了常量池中,这个范围是 -128~127 ,所以在使用 -128~127 范围内的数时,就不会认为是创建了新的变量,而是直接从常量池中取出这个值,而 a 和 b 的值相等,并且都是从常量池中取出使用的,所以两者其实指向的就是同一个对象~
二、泛型
① 泛型的定义
在Java语言中,想要写一个类/一个方法,往往都要明确说明里面的成员/参数类型,但是也有些情况,希望一个类/一个方法,能够同时给出多种的类型提供支持。
虽然我们之前学习过的方法重载是能够提供这样的支持,但是重载出的多个方法,内容大差不差,重复性过大,所以泛型也就应运而生~泛型可以同时支持多种类型
📕 通俗易懂的讲,泛型:就是适用于许多许多类型。
📕 再从代码上讲,泛型:就是对类型实现了参数化。
② 引出泛型
📚 让我们先来看一个小题:
要求实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值。
那么在使用泛型之前,思考一下,用我们之前所学习过的内容,是否能够解决这个问题呢?
有些聪明的小伙伴或许此时已经想到了:使用 Object类 呀,所有类的父类。那就借着这个思路,我们来敲一下代码:
class MyArray {
public Object[] array = new Object[10];
public Object getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,Object val) {
this.array[pos] = val;
}
}
public class Main {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.setVal(0,10);
myArray.setVal(1,"hello");
String ret = myArray.getPos(1);
System.out.println(ret);
}
}
这是因为我们使用getPos()取出的是一个Object,而用String接收Object,肯定是会有问题的,所以再接收变量时,我们还需要手动将getPos()的返回值强制转换成对应的类型。
而这种转换,会使代码的实现和数据的使用都变得更为麻烦,并且代码量也会随之增多,所以就有了泛型的使用。
③ 泛型的语法
📕 基础写法:
class 泛型类名称<类型形参列表>{
// 这里可以使用类型参数
}
class ClassName<T1,T2,T3,...,Tn>{
}
📕 其他写法:
class 泛型类名称<类型形参列表> extends 继承类 /*这里可以使用类型参数*/{
// 这里可以使用类型参数
}
class ClassName<T1,T2,T3,...,Tn> extend ParentClass<T1>{
// 可以只使⽤部分类型参数
}
那么知道了泛型的语法,让我们用泛型来实现刚刚我们所说的问题吧~
class MyArray<T>{
public T[] array = (T[]) new Object[10];
public T get(int index){
return (T)array[index];
}
public void set(int index,T val){
array[index] = val;
}
}
让我们仔细观察一下两者之间的区别
📚 主要的区别就在于这么几点:
📕 使用泛型,类名后加<T>,代表占位符,表示当前类是一个泛型类
📕 使用泛型,创建类时需要在<>中写入指定数组的类型
📕 使用泛型可以省去传回数据时的强制转换步骤
并且当我们使用泛型类,创建新的类时,传入的基本数据类型必须改写成其对应的包装类,比如int就必须写成Integer,double必须写成Double:
这是因为:泛型在Java底层其实也是被当作 "Object" 来处理的,只不过编译器自动加了类型转换,而 int 是没法与 Object 进行类型转换的,所以必须转换成对应的 Integer 包装类。
还有定义泛型类时在<>中写入的 T 也不是固定的,我们也可以写其他的字母或词,但是对于这个填入词在Java也是有默认规范的:
📚 类型形参一般使用一个大写字母表示,常用的名称有:
📕 E表示Element
📕 K表示Key
📕 V表示Value
📕 N表示Number
📕 T表示Type
📕 S,U,V等等-第二、第三、第四个类型
📚 泛型的优点:
📕 泛型能够将数据类型参数化,进行传递
📕 代码重用,一份代码,支持多种类型
📕 数据类型参数化,编译时自动进行类型检查和转换
④ 泛型类的擦除机制
那么在代码真正的跑起来之前,泛型的编译是如何进行的呢?
其实Java的泛型在本质上还是通过 Object 来实现的,所以在编译时,会经过一个核心机制,就是"类型擦除"。
📚 类型擦除的概念:
在编译时,Java 编译器会将泛型类型信息从代码中移除,这个过程就叫做类型擦除。擦除后,泛型类型会被替换为其边界类型(通常是Object)或者指定的类型。
📚 类型擦除的过程:
1. 将泛型参数替换成其边界或Object
2. 在必要的地方插入类型转换以保持类型安全
3. 生成桥接方法以保持多态性
⑤ 泛型的上界
泛型的上界所描述的是,在使用泛型,创建泛型实例时,传入的参数要满足什么条件~
class 泛型类名称<类型形参 extends 类型边界> {
...
}
这里的 "类型边界" 就像 "父类" 一样,后续创建的泛型实例的类型参数必须是它的子类。
由于 String 并不是 Number 的子类,所以就会报错~
(注:没有指定类型边界E,可以视为EextendsObject)
⑥ 泛型方法
📚 泛型方法的语法:
方法限定符 <类型形参列表> 返回值类型 方法名称 (形参列表) {
...
}
class MyArray<T>{
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}
public class Main {
public static void main(String[] args) {
Integer[] arr1 = {1,2,3};
String[] arr2 = {"c","a","b"};
MyArray.swap(arr1,0,2);
MyArray.swap(arr2,1,2);
for(int i = 0;i < arr1.length;i++){
System.out.print(arr1[i] + " ");
}
System.out.println();
for (int i = 0; i < arr2.length; i++) {
System.out.print(arr2[i] + " ");
}
}
}
⑦ 通配符
以上所学到的知识大部分都是定义泛型类的时候所涉及到的,而通配符则是针对使用泛型类(实例化泛型类)所涉及到的。
📚 我们先来看一段代码示例:
我们可以看到 a1 和 a2 由于泛型参数类型相同,于是可以进行 a1 = a2 的操作,但是 a3 的泛型参数类型与 a1 和 a2 并不相同,所以就发生了报错。
那么我们能否创建一个能指向多种不同泛型参数类型的对象呢?这就要用到我们的通配符了~
当我们将 String 改写成 ? 时,代码便不再报错了,这就是通配符。
而有些时候我们不希望通配符能够指向所有泛型参数类型对象,而是希望它只能指向一部分,所以通配符也是可以对其限制上界的,并且通配符也能对其限制下界
📚 通配符上界:
<? extends 上界>
<? extends Number>//可以传⼊的实参类型是Number或者Number的⼦类
当我们对 a4 的通配符进行了上界Number的限制后,它便无法指向String,所以会报错。
📚 通配符下界:
<? super 下界>
<? super Integer>//代表可以传⼊的实参的类型是Integer或者Integer的⽗类类型
那么对于包装类与泛型这一块知识,就为大家分享到这里啦,作者能力有限,如果有什么讲的不清楚或者有错的地方,还请大家多多在评论区指出,我也会虚心学习的!那么我们下期再见哦~