目录
1、泛型是什么——引出泛型
2、泛型的使用
2.1、语法
2.2泛型类的使用
2.3、裸类型
3、泛型如何编译
3.1、擦除机制
3.2、为什么不能实例化泛型类型数组
4、泛型的上界
5、泛型方法
5.1、语法
5.2、举例
6、通配符
6.1、什么是通配符
6.2、统配符解决了什么问题
6.3、通配符上界
6.4、通配符的下界
7、包装类
7.1、基本数据类型对应的包装类
7.2、装箱和拆箱
7.3、自动装箱和自动拆箱
7.4、关于装箱和拆箱的面试题
1、泛型是什么——引出泛型
设想现在有一个场景,要求我们实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值。
思路:
我们现在无法确定他的数据类型是什么,所以我们可以直接使用他们所有的类父类,也就是Object类,来实现:
代码如下:
/**
* Created with IntelliJ IDEA.
* Description:
* User:龙宝
* Date:2023-11-26
* Time:9:40
*/
class Arr {
//数组
public Object[] arr = new Object[10];
//获取数据
public Object getPos(int pos) {
return arr[pos];
}
//设置某位置上的数据
public void setVal(int pos,Object val){
this.arr[pos] = val;
}
}
public class test1 {
public static void main(String[] args) {
Arr arr = new Arr();
arr.setVal(0,10);
arr.setVal(1,"hhh");
Integer ret1 = (Integer) arr.getPos(0);
String ret2 = (String) arr.getPos(1);
}
}
我们会看到上述的代码,在一个数组中,我们既可以存放int类型的数据,也可以存放字符串了,也就是说可以存放任意类型的数据~
但是我们会发现一个点,我们在获取数据时,每次都需要我们进行一次类型强转,不强转就是编译报错:
那在这种情况下,虽然说,可以去存放多种类型的数据在一个数组中,但更多情况下,我们还是希望他只能够持有一种数据类型,而不是同时持有这么多类型。
以上,就引出了泛型,通过上述,我们大概也能猜到了,泛型是在干什么?
泛型的主要目的,就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时就需要我们把类型,作为参数传递,需要什么类型,我们就传入什么类型。
2、泛型的使用
2.1、语法
//语法:
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
//例1:
class ClassName<T1, T2, ..., Tn> {
}
//语法:
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
//例2:
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
上述语法,大致意思就是,我们把参数的类型也当做参数使用尖括号传过去,并且我们这个类还能去继承一个父类,这个父类也可以是带有泛型的~
使用泛型将目录1中的代码进行改写:
/**
* Created with IntelliJ IDEA.
* Description:
* User:龙宝
* Date:2023-11-26
* Time:9:40
*/
class Arr2<T> {
//数组
public T[] arr = (T[]) new Object[10];
//获取数据
public T getPos(int pos) {
return arr[pos];
}
//设置某位置上的数据
public void setVal(int pos,T val){
this.arr[pos] = val;
}
}
public class test2 {
public static void main(String[] args) {
Arr2<Integer> arr2 = new Arr2<>();
arr2.setVal(0,10);
Integer ret = arr2.getPos(0);
System.out.println(ret);
}
}
上述代码说明:
- 类名后<T>代表的是占位符,表示当前类是一个泛型类
- 上面的代码new一个数组时,不能new泛型类型的数组,会报错,所以我这里是new一个Object数组,再强转了一下~
- main函数中new Arr2时,需要指定当前类型
- 这种方式,我们在获取数据时无需强转了
- 此时我们指定arr2这个数组是Integer类型,我们插入其他类型的数据时,会编译报错,我们重新new 一个Arr2即可~
2.2泛型类的使用
上面main函数中,其实就是在使用泛型类了~
总结上面的语法:
泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
说明:
- 上面两个语法,我在上面的main函数中,只使用了一个,因为第二个尖括号那里,编译时会推导出来他的类型,可以不传这个参数~
实例,下面两种方式都可以:
Arr2<Integer> arr2 = new Arr2<>();
Arr2<Integer> arr3 = new Arr2<Integer>();
2.3、裸类型
上面2.1中的代码,我们在main函数中使用时,不传类型这个参数,也不会报错:
这种就是裸类型,就是我们在使用时,没有给他传类型这个参数。
一般不建议使用这个方式,这种方式编译不会报错是因为,要兼容老版本的API。
3、泛型如何编译
3.1、擦除机制
所谓的擦除机制,其实就是java在编译的过程当中,将所有的T替换为Object~
所以说,java的泛型机制是在编译级别实现的,我们可以去看看我们上述代码中的:
3.2、为什么不能实例化泛型类型数组
看下面一段代码:
/**
* Created with IntelliJ IDEA.
* Description:
* User:龙宝
* Date:2023-11-26
* Time:11:29
*/
class MyArray<T> {
public T[] array = (T[])new Object[10];
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,T val) {
this.array[pos] = val;
}
public T[] getArray() {
return array;
}
}
public class test3 {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>();
Integer[] arr = myArray.getArray();
}
}
看着代码,好像没什么问题,我们运行看看:
报错,显示泛型类型数组实例化这一行不能强转类型转换,为什么呢?
上面我们说了,擦除机制,在编译时,把所有的T替换成了Object,也就是说,编译会认为,你想要把一个Object的数组给了Integer类型的数组,编译器认为是不安全的~
正确方式:
import java.lang.reflect.Array;
/**
* Created with IntelliJ IDEA.
* Description:
* User:龙宝
* Date:2023-11-26
* Time:11:29
*/
class MyArray<T> {
public T[] array = (T[])new Object[10];
public MyArray(){
}
public MyArray(Class<T> clazz, int capacity) {
array = (T[]) Array.newInstance(clazz, capacity);
}
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,T val) {
this.array[pos] = val;
}
public T[] getArray() {
return array;
}
}
public class test3 {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>(Integer.class,10);
Integer[] arr = myArray.getArray();
}
}
上述,是通过反射创建并指定了类型的数组~
4、泛型的上界
在定义泛型类型时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
4.1、语法
class 泛型类名称<类型形参 extends 类型边界> {
...
}
举例:
public class MyArray<E extends Number> {
//...
}
上述代码意思:只接受Number的子类型作为E的类型实参~
当没有指定类型的上边界时,可以视为E extends Object
举例2:写一个泛型类,找出数组当中的最大值,只要这个T,实现了Comparable接口就行
/**
* Created with IntelliJ IDEA.
* Description:
* User:龙宝
* Date:2023-11-26
* Time:11:45
*/
class Alg<T extends Comparable<T>> {
public T findMaxVal(T[] array) {
T maxVal = array[0];
for (int i = 1; i < array.length; i++) {
if(array[i].compareTo(maxVal) > 0) {
maxVal = array[i];
}
}
return maxVal;
}
}
public class test4 {
public static void main(String[] args) {
Alg<Integer> a1 = new Alg<>();
Integer[] arr = {2,4,5,9,10,1};
Integer ret = a1.findMaxVal(arr);
}
}
5、泛型方法
上面我们一直在给一个类传一个参数过去,那方法中,泛型方法,如何使用呢?
5.1、语法
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
5.2、举例
例1:
public class Util {
//静态的泛型方法 需要在static后用<>声明泛型类型参数
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}
使用上面的方法:
//使用1:
Integer[] a = { ... };
swap(a, 0, 9);
String[] b = { ... };
swap(b, 0, 9);
//使用2:
Integer[] a = { ... };
Util.<Integer>swap(a, 0, 9);
String[] b = { ... };
Util.<String>swap(b, 0, 9);
6、通配符
6.1、什么是通配符
?用于在泛型的使用,即为通配符。具体继续往下看~
6.2、统配符解决了什么问题
泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围。
案例:
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());
}
}
上述代码的问题:
这里我直接给他指定了一个数据类型,那也就是说,如果main函数中,我设的是Integer类型,就无法调用这个方法了:
编译报错了~
解决方案:使用通配符
public static void main(String[] args) {
Message<Integer> message = new Message() ;
message.setMessage(111);
fun(message);
}
public static void fun(Message<?> temp){
System.out.println(temp.getMessage());
}
这种方式也有缺点的,因为我们无法确定这个方法过来的是什么数据类型,所以无法进行修改~
所以为了解决这个问题,就引出了通配符的上、下界:
6.3、通配符上界
语法:
<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类
举例:
代码:
/**
* Created with IntelliJ IDEA.
* Description:
* User:龙宝
* Date:2023-11-26
* Time:12:10
*/
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 test5 {
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()); //仍然无法修改!
Fruit b = temp.getMessage();
System.out.println(b);
}
}
通配符的上界的主要作用:
更方便我们去接收数据~(不能进行写入数据,只能进行读取数据)~
6.4、通配符的下界
语法:
<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
举例:
在上述代码下,增加:
/**
* Created with IntelliJ IDEA.
* Description:
* User:龙宝
* Date:2023-11-26
* Time:12:10
*/
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 test5 {
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);
Message<Fruit> message3 = new Message<>();
message3.setMessage(new Fruit());
fun2(message3);
}
// 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
public static void fun(Message<? extends Fruit> temp){
//temp.setMessage(new Banana()); //仍然无法修改!
//temp.setMessage(new Apple()); //仍然无法修改!
Fruit b = temp.getMessage();
System.out.println(b);
}
public static void fun2(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());//只能直接输出
}
}
通配符的下界的主要作用:
不能进行读取数据,只能写入数据~
7、包装类
7.1、基本数据类型对应的包装类
7.2、装箱和拆箱
int i = 10;
// 装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
Integer ii = Integer.valueOf(i);//方法1
Integer ij = new Integer(i);//方法2
// 拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中
int j = ii.intValue();
7.3、自动装箱和自动拆箱
int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer)i; // 自动装箱
int j = ii; // 自动拆箱
int k = (int)ii; // 自动拆箱
本质:
- 自动装箱其实就是调用Integer.valueOf()这个方法
- 自动拆箱就是调用intValue()这个方法
7.4、关于装箱和拆箱的面试题
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);
}
结果:
为什么会这样?
我们进入Integer这个类中,来看:
先进入Integer这个类中,然后:
上面我们会看到这个valueOf()方法,我们来看看这个方法内部是怎么实现的:
他是判断我们这个数字的大小,是不是在这个[IntegerCache.low , IntegerCache.high]区间中,如果在,他就会返回IntegerCache.cache这个数组中的一个值,这个值位置:用我们给的这个数字加上刚才判断中的那个区间的左边界值的相反数。
我们再来看看这个IntegerCache长什么样:
那就是说,他其实是有一个Cache数组,如下:
数组的长度是256,存放的数值是-128到127之间的,当我们来了一个要装箱的数字,如果这个数字在这个数组中,那就返回这个数组的所在位置,就有了我们上述的127和127两次装箱,引用所指向的地址相等了,如果数字不在这个数组上,就会去new一个对象了,每次new出来的地址,是不相等的,所以128和128两次装箱的引用指向的地址就不相等了~
下期见~~~