目录
1、包装类
1.1、基本数据类型和其包装类
1.2、装箱和拆箱
1.2.1、装箱
1.2.2、拆箱
1.2.3、面试题
2、泛型的概念
3、引出泛型
3.1、语法
4、泛型类的使用
4.1、语法
5、裸类型(Raw Type)
6、泛型是如何编译的
6.1、擦除机制
6.2、不能实例化泛型类型数组
7、泛型的上界
7.1、语法
7.2、复杂示例
8、泛型方法
8.1、语法定义
9、强制类型转换数组和数组元素
1、包装类
在Java当中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。
1.1、基本数据类型和其包装类
基本数据类型 | 包装类 |
byte | Byte |
short | Short |
int | integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
✨总结:八种基本数据类型所对应的包装类,除了integer和Character,其余基本类型的包装类都是首字母大写。
举例:当在泛型代码中使用时
import java.util.LinkedList;
public class Test {
public static void main(String[] args) {
// String类型继承自Object,可以在泛型代码中使用
LinkedList<String > linkedList = new LinkedList<>();
linkedList.add("hello");
}
}
基本数据类型(不继承于Object的数据类型,都不能直接出现在泛型代码的<>当中)
🔅 修改:将基本数据类型修改为其所对应的类类型(包装类)。
1.2、装箱和拆箱
装箱 | 把基本数据类型变为对应的包装类型 |
拆箱 | 将引用数据类型拆箱为基本数据类型 |
1.2.1、装箱
public class Test {
public static void main(String[] args) {
int a = 10;
Integer val1 = a;//自动装箱
//因为Integer是一个类,所以它可以直接调用他的方法或者new一个对象用来装箱
Integer val2 = Integer.valueOf(a);//显示装箱
Integer val3 = new Integer(a);//显示装箱
System.out.println(val1);
}
}
对变量val1进行反汇编,来查看自动装箱的过程。
1.2.2、拆箱
📕自动拆箱
public class Test {
public static void main(String[] args) {
Integer val1 = 10;
int a = val1;//自动拆箱
System.out.println(a);
}
}
对上述代码进行反汇编,查看自动拆箱的过程。
📕 显示拆箱
通过上述的反汇编,可以看见拆箱的时候会调用了一些方法,那么在编译过程中,将反汇编中调用的方法直接调用,这样的写法就是显示拆箱。
public class Test {
public static void main(String[] args) {
Integer val1 = 10;
int b = val1.intValue();//显示拆箱
System.out.println(b);
}
}
以Integer包装类为例,类当中有很多装箱和拆箱的方法,那么是不是想要什么类型的数据,就可以拆成什么类型的数据?
答案当然是可以。
public class Test {
public static void main(String[] args) {
Integer val1 = 10;
double d = val1.doubleValue();
System.out.println(d);
short t = val1.shortValue();
System.out.println(t);
}
}
📗Integer类当中的拆箱方法
1.2.3、面试题
下列代码输出的结果是
public class Test {
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
System.out.println(a == b);
}
}
结果为
但是当a 和 b 改为200,就会报错。
上述代码设计到了装箱的代码,装箱会调用Integer类的valueOf方法,那么来看一下这个代码
- 当两个变量值为100,小于127,在缓存数组cache当中同一位置取值,用==比较大小,返回结果为true
- 当两个变量值为200,大于127,在缓存数组cache当中找不到该数字,那么就会返回new 的新的地址。
- 用==比较,比较的是两个数据的存储位置。所以返回false。
❗❗❗【注意事项】
当装箱之后,数据类型,就会变为相应的引用数据类型,引用数据类型比较的时候,使用==
表示的是:比较两个数据存储的位置
2、泛型的概念
一般的类和方法,只能使用具体的类型;要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
泛型是在JDK1.5引入的新语法,通俗讲,泛型:就是适用于许多类型。从代码上将,就是对类实现了参数化。
3、引出泛型
实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据放法返回数组中某个下标的值?
思考:
- 之前学过的数组,只能指定存放指定的元素,例如:int[ ] array = new int[12];String[ ] strs = new String[12];
- 所有类的父类,默认都是Object类。数组是否可以创建为Object?
代码示例:按照上述说法,实现了一个类,包含一个数组成员可以存放任何类型的数据,也可以根据方法返回数组中某个下标的值
当我们知道数组2下标处是double类型的数据,通过这种方式获取数据,编译器报错,这是什么原因呢?
直接原因就是,接收类型和返回值类型不匹配,导致这里出错。说到这里就有人说,向下转型(强转)或者直接用Object类型的变量来接收返回值,不就解决问题了吗?
//方法一:强转(向下转型)
double d = (double)myArray.getPos(2);
//这样接收数据的好处就是得到的数据,由于数据类型固定,可以在接下来的代码中直接参与运算
//方法二:用Object类型的变量来接收
Object d = myArray.getPos(2);
//这样接收数据的好处是,不用管他是那个位置的数据,都可以直接接收;但是缺点就是,得到的数据不能直接参与运算
虽说这两种方法暂时解决了编译器报错的问题,但是又产生了新的问题
- 方法一:现在作为例子的代码只用三个元素,我们想要某个位置的数据的时候,可以知道这个数据是什么类型的数据,可以通过强转来解决这个问题,但是在以后写大型代码的时候,以变量的形式存入数据,数据有成百上千的时候,那个时候我们要获取某个位置的数据的时候,还要像现在一样,去查那个数据是什么类型的数据,再进行强转吗,这样写代码的效率太低了。
- 方法二:这样写,有掩耳盗铃的意味,在代码当中,得到这个数据,就是为了在接下来的操作中能够使用,但是得到的这个数据,并不能直接在接下来的代码当中使用,用来接收的变量的类型是Object,那么这个变量的类型可以是String,也可以是Char,没有办法确定具体的类型,那么就不可能参与到后续的程序中。
通过观察上述代码就产生了两个问题:
- 存放数据的时候,任何类型的数据都可以存放
- 2号下标的元素本身就是double类型的数据,但是用double类型的变量接收,编译器报错。必须得强转
- 在以Object为数组类型的情况下,虽然可以什么类型都可以存放,但是更多的情况下,我们还是希望指定他只能存放一种数据类型。而不是同时持有那么多的类型。所以,这里就要说到泛型。
- 泛型的只要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。
3.1、语法
class 泛型类名称<类型形参列表>{
//这里可以使用类型参数
}
//举例:
class MyArray<T,E,O,N,...>{
}
class 泛型类名称<类型形参列表> extends 继承类 /*这里可以使用类型参数*/{
//这里可以使用类型参数
}
//举例
class ClassName<T1,T2,....Tn> extends ParentClass<T1>{
//可以使用部分类型参数
}
那么上述的代码就可以这样修改
在实例化对象的时候,指定了泛型类的指定参数类型是Interger。所以在编译的时候,写的其他类型的数据,通过编译器检测会报错。
❗❗❗提示:在示例话对象的时候,也可以这样写
当编译器可以根据上下文推导出类型实参时,可以省略实参的填写
MyArray<Integer> myArray = new MyArray<>(); //在后面new的时候,MyArray后面的<>中的内容可以省略
❗❗❗ 注意:
在上述的代码中,有这样一句代码。
public T[] obj = (T[])new Object[3];
这句代码是错的,这样写只是让编译器不会报错。
❗❗❗总结:
- 类名后的<T>代表占位符,表示当前类是一个泛型类。
- 不能new泛型类型的数组
T[] ts = new T[5];//写法是错误的
- 在实例化对象的时候,添加<Integer>表示指定当前类型
MyArray<Integer> myArray = new MyArray<>();
- 在接收读取到数组中某个元素的时候,不需要再进行强制类型转换。
- 当在实例化时,指定当前泛型类的指定参数类型是Integer,在数组中写入其他类型的参数时,编译器就会报错。
4、泛型类的使用
4.1、语法
泛型类<参数类型> 变量名; //定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 示例化一个泛型类对象
示例:
MyArray<Integer> a;
MyArray<Integer> myArray = new MyArray<>();
注意:泛型只能接收类,所有的基本数据类型必须使用包装类!!
5、裸类型(Raw Type)
裸类型是一个泛型类但是没有带着类型实参,这样写,示例化类型对象当中的数组,又可以存放各种类型的元素了
在实例化对象的时候,当前泛型类没有指定参数类型,在理论上,编译器会报错,但是从这里看,编译器并没有报错。
❗❗❗注意:
我们不要自己去使用裸类型,裸类型是为了兼容老版本的API保留的机制。
❗❗❗总结:
- 泛型是将数据类型参数化,进行传递
- 使用<T>表示当前类是一个泛型类。
- 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换
6、泛型是如何编译的
6.1、擦除机制
泛型类(MyArray)源文件在通过编译之后生成字节码文件,字节码文件在JVM上运行,最后生成结果。
我们可以通过命令:javap -c 来查看字节码文件。
被圈出来的部分在MyArray类当中
在编译的过程中,将所有的T替换为Object。这种机制被称为:擦除机制。
泛型是编译期间的一种机制,在运行的时候没有泛型的概念!!!
因为字节码文件是在JVM上运行的,也就是说,JVM没有泛型的概念。
编译的时候就执行了两件事情:编译时自动进行类型检查和转换,编译完成之后就没有泛型了,运行的时候都是Object。
❗❕那么到这里,又产生了一个问题,像下面的代码,在编译完成之后T不是会被擦除为Object吗?为什么还会报错?
这里就涉及到了更深层次的知识,我们只看最终结论就行。
- 在定义泛型数组的时候,我们可以这样写。
对数组的元素进行强制类型转换
在MyArray数组实例化的的同时规定了MyArray类的类型,
❗❗❗在int a = myArray.getPos(1);这句代码中getPos方法返回的是T类型的数据,但是在用int类型的变量来接收的时候,泛型的优点就在这里体现,会自动进行类型检查。当数组是Integer类型的时候,那么用int类型和Integer类型都能接收。
❗❗❗ Java官方在写的时候也是这种思想,以ArrayList为例,他的每个数组都是Object类型的。
在来看ArrayList的get方法
❗❗❗总结:
- 今后我们在定义泛型数组的时候,正确的写法是,下面这种样子。
- 像这样的写法不是正确的,他只是让编译器不报错。这样写会有很大的弊端。
6.2、不能实例化泛型类型数组
- 不能对数组的类型进行强制类型转换
class MyArray1<E>{
public E[] obj = (E[])new Object[3];//泛型类型的数组在示例化的时候,已经
//定义好了是object类型,就不能再强转了。在实例化类的时候,将类指定的类型,传递
//上来,E表示的就是Integer类型。
public E[] getArray(){
return obj;
}
public void setObj(int pos,E val){
obj[pos] = val;
}
}
public class Test {
public static void main(String[] args) {
MyArray1<Integer> myArray1 = new MyArray1<>();
myArray1.setObj(0,10);
myArray1.setObj(1,71);
Integer[] integers = myArray1.getArray();
}
❓❓❓ 这里为什么会报类型转换异常?
- 原因在于定义数组的时候E[] obj = (E[])new Object[3];new的是Object类型的数组,在前面不能再强制类型转换了,数组在开辟空间的时候,已经定义好了类型,那么数组类型就不能改变,只能改变数组当中元素的类型。
❗【举例说明】
- 定义一个int类型的数组,数组空间开辟完成之后,将数组类型转换为short类型,那么该数组的空间大小该怎样计算?
🔆🔆来看调试的结果
数组的类型并没有被强转
那么通过强制类型转换之后,来看结果。
编译器还是会报相同的错误。
- 这就是这种写法的弊端,泛型类当中的数组是Object类型的,那么数组当中的元素,可以是任意类型的,可以是Integer类型的,也可以是Char类型的,也可以是Double类型。
- 当采用上述的强制类型转换(向下转型),编译器还是会报错,编译器认为数组这样强转是不安全的。
- 当在定义数组的时候 ,不确定是数组的类型是什么的时候,可以使用下列的方法来写(采用反射的手段来解决)
❗❗❗正确的方式:通过反射创建 ,指定数组类型
import java.lang.reflect.Array; class MyArray2<E>{ public E[] obj ; //创建一个构造方法 public MyArray2(Class<E> clazz, int capacity) { obj = (E[])Array.newInstance(clazz, capacity); //这里代表的意思是:指定类型的数组,指定new一个什么类型的数组,容量是多大 } public E[] getArray(){ return obj; } public void setObj(int pos,E val){ obj[pos] = val; } } public class Test { public static void main(String[] args) { MyArray2<Integer> myArray2 = new MyArray2<>(Integer.class,10); //这里就相当于我已经指定了数组的类型是Integer myArray2.setObj(0,10); myArray2.setObj(1,71); Integer[] integers = (Integer[])myArray2.getArray(); }
❗❗❗总结:
- 当数组的类型在new的时候已经确定,就不能强制类型转换数组的类型,但是可以修改数组的元素类型。
- 当数组类型不确定的时候,可以使用反射来创建,指定数组的类型。
7、泛型的上界
在定义泛型类时,有时候需要对传入的类型变量做一定的约束,可以通过类型边界类约束。
7.1、语法
class 泛型类名称<类型形参 extends 类型边界>{
.....
}
//举例
public class MyArray<E extends Number>{
...
}
❗❗❗注意:<E extends number>是泛型的上界,他代表:E是Number的子类或者E是Number本身
抽象类Number的直接子类
7.2、复杂示例
class Alg<E extends Comparable<E>>{
...
}
这个示例当中的代码,代表将来指定的参数类型,一定实现了这个接口。
代码示例:找数组中的最大值。
class Alg<E extends Comparable<E>>{
public E findMax(E[] array){
E max = array[0];
for(int i = 1;i < array.length;i++){
if(max.compareTo(array[i])<0){
//比较的是引用类型,所以不能直接用< > =比较
max = array[i];
}
}
return max;
}
}
public class Test {
public static void main1(String[] args) {
Alg<Integer> alg = new Alg<>();
Integer[] array = {1,2,3,4,10,79,25,36};
Integer val = alg.findMax(array);
System.out.println(val);
}
}
8、泛型方法
8.1、语法定义
方法限定符<类型形参列表> 返回值类型 方法名称(形参列表){
...
}
示例:还是上述代码,找数组中的最大值
class Alg2{
public<E extends Comparable<E>> E findMax(E[] array){
//相当于给这个方法指定参数类型为E,并实现Comparable接口,给这个方法做一定的约束
E max = array[0];
for(int i = 1;i < array.length;i++){
if(max.compareTo(array[i])<0){
//实现类Comparable,调用CompareTo方法,进行元素比较
max = array[i];
}
}
return max;
}
}
public class Test {
public static void main1(String[] args) {
Alg2 alg2 = new Alg2();
Integer[] array = {1,2,3,4,10,79,25,36};
Integer val = alg2.findMax(array);
System.out.println(val);
}
}
❓❓那么这里就有一个疑问,从哪里传给findMax方法类型的参数呢?
是从这里
画出来的地方是可以省略的,它可以从findMax方法的参数中去反推,传给E的是Integer类型。
❓上述的写法有些麻烦,每次调用findMax方法的时候,都要实例化该类,能不能有简单一点的方法?
当然是有的
- 在类当中写一个静态方法,调用的时候直接使用类名来调用,不用再实例化Alg3对象。
class Alg3{ public static<E extends Comparable<E>> E findMax(E[] array){ E max = array[0]; for(int i = 1;i < array.length;i++){ if(max.compareTo(array[i])<0){ max = array[i]; } } return max; } } public class Test { public static void main(String[] args) { Integer[] array = {1,2,3,4,10,79,25,36}; Integer val = Alg3.findMax(array); System.out.println(val); }
9、强制类型转换数组和数组元素
在这里再次强调一下,强制类型转换数组类型和数组元素的类型。
- 当实例化数组的时候,数组的类型已经确定,那么数组类型再什么情况下,都不能再强转为其他类型。
- 当创建泛型类的时候,实例化了数组,数组的类型已经固定,想要改变数组的元素的类型,可以通过在泛型类当中定义一个set方法,来修改数组元素的类型。