当你觉得这条路很难走的时候,一定是上坡路
目录
1.初识泛型
1.1 什么是泛型
1.2泛型类语法
1.2.1泛型类定义
1.2.2泛型类使用语法
1.2.3泛型类的使用
1.2.4裸类型
2.泛型如何编译
2.1擦除机制
3.泛型的上界
3.1语法
3.2示范
4.泛型方法
4.1 语法
4.2示例
4.2.1使用示例-可以类型推导
4.2.2使用示例-不使用类型推导
5.通配符
5.1通配符的作用
5.2子通配符
5.2.1通配符上界
5.2.2通配符的下界
6.包装类
6.1基本类型对应的包装类
6.2装箱和拆箱
6.2.1装箱
6.2.2拆箱
6.2.3自动装箱和自动拆箱
1.初识泛型
1.1 什么是泛型
《Java编程思想》这本书中对泛型的介绍中有这样一段话:一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大
如下代码:
class Array {
public int[] arr = new int[10];
public void setArr(int val,int pos) {
this.arr[pos] = val;
}
public int getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array array = new Array();
array.setArr(1,0);
int ret = array.getArr(0);
System.out.println(ret);
}
}
一般在方法的形参和返回值都会用到具体的类型,这样就限制了代码。如:我们实例化了一个Array 类型的对象,当我们用这个对象调用 setArr 方法设置数组值时,因为形参是整型那么只能传入整型的实参。当我们用这个对象调用 getArr 方法去获取数组某个位置值时,因为返回值是整型所以只能返回整型的值,那么这种刻板的限制对代码的束缚就会很大
为了避免如上的限制在 JDK1.5 版本中引入了泛型。
泛型就是参数化类型 ,说起参数大家第一时间就会想起方法的形参,当我们要调用方法时就需要传入实参,那什么叫做参数化类型呢?顾名思义也就是根据实参的类型来决定形参的类型
那我们现在需要将上述代码数组中可以存放任何类型的数据,那需要如何进行修改代码呢?
思路:所有类的父类,默认为Object类。所以类型都有包装类,那么它们的包装类都继承了Object类,那么我们就可以将数组设置为 Object 类。所以的子类都都可以用父类接收,那么我们将数组设置为 Object 类型之后就可以接收任何类型的值
class Array {
public Object[] arr = new Object[10];
public void setArr(Object val,int pos) {
this.arr[pos] = val;
}
public Object getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array array = new Array();
array.setArr(1,0);
array.setArr("obj",1);
//int ret = (int)array.getArr(0);
}
}
将数组设置为 Object 类型之后,就可以将数组中的不同位置放不同类型的值,但是当我们获取一个数组中指定位置的值时,需要强制类型转换。在大多数的情况下我们想要的是它只能够持有一种数据类型,而不是同时持有这么多类型。泛型的主要目的就是指定当前的容器,要持有什么类型的对象,让编译器去做检查。此时就需要把类型,作为参数传递。需要什么类型,就传入什么类型。
1.2泛型类语法
1.2.1泛型类定义
class 泛型类名称<类型形参列表> {
}
class Array<T> {
}
注:一个泛型的类型形参列表可以指定多个类型
类型形参一般使用一个大写字母表示,类型形参命名:
- E 表示 Element
- K 表示 Key
- V 表示 Value
- N 表示 Number
- T 表示 Type
- S, U, V 等等 - 第二、第三、第四个类型
1.2.2泛型类使用语法
泛型类<类型实参> 变量名 = new 泛型类<类型实参>(构造方法实参);
泛型类<类型实参> 变量名:定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参):实例化一个泛型类对象
Array<Integer> array = new Array<Integer>();
类型推导:
Array<Integer> array = new Array<>();
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
1.2.3泛型类的使用
class Array<T> {
public T[] arr = (T[])(new Object[10]);
public void setArr(T val,int pos) {
this.arr[pos] = val;
}
public T getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array<Integer> array = new Array<Integer>();
array.setArr(1,0);
array.setArr(1,1);
Integer ret = array.getArr(0);
System.out.println(ret);
}
}
- <T>:占位符,相当于当前类是一个泛型类
- 不能 new 泛型类型的数组
- 类型后加入<Integer>指定当前类型
- 不需要进行强制类型转换
- 编译器会在存放元素的时候帮助我们进行类型检查
- 泛型只能接受类,所有的基本数据类型必须使用包装类
泛型存在的意义:
- 存数据是会进行类型检查
- 取数据的时候会自动帮你类型转换,也就不需要我们手动强制类型转换了
1.2.4裸类型
裸类型:在定义和实例化泛型类的时候不给类型实参
泛型类 变量名 = new 泛型类(构造方法实参);
Array array = new Array();
裸类型存在的意义:是为了兼容老版本的 API 保留的机制
2.泛型如何编译
2.1擦除机制
class Array<T> {
public T[] arr = (T[])(new Object[10]);
public void setArr(T val,int pos) {
this.arr[pos] = val;
}
public T getArr(int pos) {
return this.arr[pos];
}
}
接下来我们就通过DOS窗口查看一下上述这个泛型类的字节码文件:
我们在Dos窗口中用命令 javap -c 查看字节码文件,此时所有的 T 都是 Object 。那么在编译的过程当中,将所有的 T 替换为 Object 这种机制,我们称为:擦除机制。
Java的泛型机制是在编译级别实现的,编译器生成的字节码在运行期间并不包含泛型的类型信息
问题:为什么不能 new 泛型类型的数组呢?
答:T[] arr = new T[],在编译过程当中不会将 new 的这个 T 擦除为Object,Java 规定擦除机制只针对定义变量时的类型和返回值的返回类型,不会擦除实例化时的泛型,所以 new 一个 T 数组,不会将 T 擦除为 Object 时,那么编译器就不认识 T,那么编译器在编译期间就会报错。
3.泛型的上界
泛型的上界:在定义泛型的时候,有时候需要给传过来的类型做一个约束,此时就可以用泛型的上界来进行约束。比如:我们希望传过来的类型是继承 School 类,那我们就可以用泛型的上界来约束,如果传过来的类型没有继承 School 这个类,则会报错
3.1语法
class 泛型类名称<类型形参 extends 类型上界> {
}
3.2示范
class Student<T extends School> {
}
此时只接受 School 的子类型作为 T 的类型实参,在编译时期擦除时会被擦除为其父类也就是擦除为School
注:如果没有指定泛型类型的上界,默认视为 T extends Object
public class MyArray<E extends Comparable<E>> {
}
此时传过来的实参类型必须是实现了 Comparable 接口
4.泛型方法
4.1 语法
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) {
}
4.2示例
public class Test {
public static <T> void swap(T[] array,int i,int j) {
T t = array[i];
array[i] = array[j];
array[j] = t;
}
}
4.2.1使用示例-可以类型推导
public static void main(String[] args) {
Integer[] arr = {1,2,3,4,5,6};
swap(arr,0,1);
}
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
4.2.2使用示例-不使用类型推导
public static void main(String[] args) {
Integer[] arr = {1,2,3,4,5,6};
Test.<Integer>swap(arr,0,1);
}
手动将类型实参传给类型形参
注: 给泛型传类型的时候必须传类,不能传基本数据类型但是可以传它们的包装类
5.通配符
通配符:?用于在泛型的使用,这个?就是通配符
5.1通配符的作用
class Array<T> {
public T[] arr = (T[])(new Object[10]);
public void setArr(T val,int pos) {
this.arr[pos] = val;
}
public T getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array<Integer> array = new Array<Integer>();
fun(array);
}
public static void fun(Array<Integer> arr) {
arr.setArr(1,0);
System.out.println(arr.getArr(0));
}
}
在主函数中实例化了一个 Array 泛型类,传的类型是 Integer。在 fun 方法形参中只能接收 Array 类的类型为 Integer 的对象,调用 fun 方法可以使用类型推导,如果传入的不是 Array 类的类型为Integer 的对象则会报错。Array 是一个泛型类,实例化的时候可以传入不同类型的实参,那么 fun这样就有了一定的局限性
那么此时我们就可以将fun方法形参中的Array<>里面类型设置为通配符,这样就可以接收 Array 类的任何类型的对象了
class Array<T> {
public T[] arr = (T[])(new Object[10]);
public void setArr(T val,int pos) {
this.arr[pos] = val;
}
public T getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array<Integer> array = new Array<Integer>();
array.setArr(1,0);
fun(array);
}
public static void fun(Array<?> arr) {
System.out.println(arr.getArr(0));
}
}
使用通配符"?"说明它可以接收任意类型的Array对象,但是由于不确定类型,所以无法修改
5.2子通配符
在通配符的基础上又产生了两个子通配符:
- ?extends 类:设置泛型上界
- ?super 类:设置泛型下界
5.2.1通配符上界
①语法
<? extends 上界>
<? extends Number>
传入的实参类型必须是 Number 本身类或者是 Number 的子类
②示例
class Fruits {
}
class Apple extends Fruits {
}
class Message<T> {
private T message;
public void setMessage(T message) {
this.message = message;
}
public T getMessage() {
return message;
}
}
public class Main {
public static void main(String[] args) {
Message<Apple> message = new Message<>();
message.setMessage(new Apple());
fun(message);
}
public static void fun(Message<? extends Fruits> tmp) {
//tmp.setMessage(new Apple());
Fruits fruits = tmp.getMessage();
}
}
fun 形参类型传过来的实参类型必须是 Fruits 类型 或者是 Fruits 子类类型。在fun里面用通配符接收类型,那么我们也就不清楚是具体的哪种类型,只知道是Fruits 类型 或者是 Fruits 子类类型,那么我们就无法调用 setMessage 去设置 message 的值,因为子类类型无法存储父类对象。我们知道主函数传给fun方法的对象类型要么是 Fruits 类型 要么是 Fruits 子类类型,那么我们就可以调用 getMessage 去获取 message 的值,用Fruits 接收即可。
通配符的上界,不能进行写入数据,只能进行读取数据
5.2.2通配符的下界
①语法
<? super 下界>
<? super Integer>
传过来的类型必须是 Integer 本身类 或者是 Integer 的父类
②示例
class Fruits {
}
class Apple extends Fruits {
}
class Message<T> {
private T message;
public void setMessage(T message) {
this.message = message;
}
public T getMessage() {
return message;
}
}
public class Main {
public static void main(String[] args) {
Message<Fruits> message = new Message<>();
message.setMessage(new Fruits());
fun(message);
}
public static void fun(Message<? super Fruits> tmp) {
tmp.setMessage(new Apple());
//Fruits fruits = tmp.getMessage();
}
}
fun 形参类型传过来的实参类型必须是 Fruits 类型 或者是 Fruits类型的父类,那我们在fun方法中通过 setMessage 去设置 message 的值时,我们知道主函数传给 fun 方法的对象类型最低也得是Fruits 方法,通过 setMessage 去设置 message 值是我们可以设置为 Fruits 子类对象,因为 Fruits子类肯定也是Fruits父类的子类,所以可以设置 message 的值。但是不能通过 getMessge 去获取messge的值,原因就在于我们只知道主函数传给fun实参类型是 Fruits 类型 或者是 Fruits类型的父类,如果传过来的实参类型是 Fruits 父类那我们去获取message的值时我们就不知道用什么接收了。
通配符的下界,不能进行读取数据,只能写入数据
6.包装类
在 Java 中,基本类型是不会继承 Object 类的,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型
6.1基本类型对应的包装类
基本数据类型 | 包装类 |
---|---|
byte | Byte |
shout | Shout |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
6.2装箱和拆箱
6.2.1装箱
int a = 10;
Integer i = Integer.valueOf(a);
Integer.valueof:主要将一个基本数据类型的值存放到 Integer 类的对象里面
上述代码装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
Integer 类中的 valueOf 方法中,如果你存储的值在 -128(IntegerCache.low:-128)到127(IntegerCache:127)范围中就存储到 i +(-IntegerCache.low)这个位置的数组当中。如果超出了这个范围就 new 一个 Integer 对象,通过构造方法存储在 value 属性中
6.2.2拆箱
int a = 10;
Integer i = Integer.valueOf(a);//装箱
int b = i.intValue();//拆箱
拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中
6.2.3自动装箱和自动拆箱
从上述手动装箱和拆箱中我们可以看出手动装箱和拆箱给开发者带来了不少代码量,为了减少开发者的负担,Java提供了自动装箱拆箱机制
public class Demo {
public static void main(String[] args) {
int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer)i; // 自动装箱
int j = ii; // 自动拆箱
int k = (int)ii; // 自动拆箱
}
}
通过字节码文件可以看出在编译时期会自动的装箱拆箱,还原为装箱和拆箱