一、包装类与基本类型
包装类,就是基本数据类型对应的类类型。我们已知Java中有8种基本数据类型,这些基本数据类型中除了int
对应的包装类Integer
、char
对应的包装类Character
其它包装类均为基本类型首字母大写。
装箱/装包: 把基本类型变为对应的包装类型
拆箱/拆包: 将包装类型拆箱为基本数据类型
//装箱/装包:把基本类型变为对应的包装类型 int a=10; Integer val0 = (Integer)a;//自动装箱 Integer val1 = a;//自动装箱-->底层就是调用的是以下两个方法: Integer val2 = Integer.valueOf(a);//手动装箱 Integer val3 = new Integer(a); //手动装箱 //拆箱/拆包:将包装类型拆箱为基本数据类型 Integer value1 = 10;//这里存在自动装箱 int z = (int)value1;//自动拆箱 int a = value1; //自动拆箱-->底层调用了以下方法: int b = value1.intValue();//手动拆箱 double c = value1.doubleValue();//包装类的权限更大,可以拆成其它类型
一道有坑的面试题:
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
、false
.其实这道题就考察了装箱的底层原理,我们找到装箱源码:
二、泛型
1、什么是泛型
一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。
2、引出泛型
需求: 实现一个类,类中包含一个数组成员可以存放任意类型的数据。
根据我们已知的知识,Object类默认为所有类的父类(包括八大基本类型的包装类),所以我们可以创建一个Object
类型的数组用来容纳各种类型的数据。具体实现如下:
class MyArrays { public Object[] obj = new Object[5]; public Object getPos(int pos) { return obj[pos]; } public void setPos(Object val,int pos) { obj[pos] = val; } } public class TestDemo { public static void main(String[] args) { MyArrays myArrays = new MyArrays(); myArrays.setPos(6,0); myArrays.setPos("world",1); myArrays.setPos(10.5,2); int a = (int)myArrays.getPos(0); String str = (String)myArrays.getPos(1); double d = (double)myArrays.getPos(2); } }
上面的Object
数组确实可以实现什么类型的数据都能存储,但是也有一个问题:对于使用者来说,他们可能并不知道Object数组中每个下标存的到底是什么类型的数据,所以这就给使用时带来了麻烦。
结论:
所以更多情况下,我们还是希望他只能够持有一种数据类型,而不是同时持有这么多类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象,让编译器去做检查。此时,就需要把类型,作为参数传递,需要什么类型,就传入什么类型。
3、泛型类的定义和使用
泛型语法
//泛型类的定义 //概念化 class 泛型类名称<类型形参列表> { // 这里可以使用类型参数 } ↆ //具体化 class ClassName<T1, T2, ..., Tn> { // 这里可以使用类型参数 } //泛型类的使用 泛型类<类型实参> 变量名 = new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
按照上述规则,将上面引入改写:
class MyArray<T> { // 泛型数组(底层为Object) public Object[] obj = new Object[5]; public void setObj(int pos,T val) { obj[pos]=val; } public T getPos(int pos) { return (T)obj[pos]; } } //测试 public class TestDemo { public static void main1(String[] args) { MyArray<Integer> myArray = new MyArray<>(); //可以推导出实例化需要的类型实参为 Integer,所以这里的<>中内容可省略 myArray.setObj(0,1); myArray.setObj(1,2); myArray.setObj(2,3); int a = myArray.getPos(0); //myArray.setObj(4,"hello");//编译器会对类型检查,导致此处编译报错 } }
注解:
类名后的 < T > 代表占位符,表示当前类是一个泛型类,T是类型形参(引用类型),一般使用一个大写字母表示,常用的名称有:
(1) E 表示 Element
(2) K 表示 Key
(3) V 表示 Value
(4) N 表示 Number
(5) T 表示 Type
(6) S, U, V 等等 - 第二、第三、第四个类型
测试代码中,类型后加入 < Integer > 指定当前容器类型为Integer.
int a = myArray.getPos(0);不需要进行强制类型转换.
myArray.setObj(4,"hello");编译报错,是因为上面已指定类当前的类型为Integer,编译器会帮我们进行类型检查.
泛型只能接受类,所有的基本数据类型必须使用包装类!
MyArray<Integer> myArray = new MyArray<>();当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写.
错误定义方式T[] ts = new T[5];不能new泛型类型的数组.
4、擦除机制
在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制。Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。对于泛型的编译这里我们就点到为止,目前先了解泛型有这样一个机制,想要真正理解泛型是需要时间和知识的打磨的!
5、泛型的上界
语法
class 泛型类名称<类型形参 extends 类型边界> { ... }
例如:
//代表将来指定的参数E类型一定是实现了Comparable这个接口的 class Alg<E extends Comparable<E>> { } //E是Number的子类或者E是Number本身 class Alg2<E extends Number> { }
使用场景: 实现一个泛型类,包含一个求数组最大值的方法。
分析: 由于类型形参E是一个引用类型,我们知道引用类型数值比较是不能直接使用大于小于号的,而是需要使用compareTo()比较方法,使用此方法又需要实现Comparable接口,而此时如果不使用泛型上界的话,最终E会被擦除为Object导致不能使用compareTo()方法,所以需要使用泛型上界E extends Comparable<E>
class Alg<E extends Comparable<E>> { //如果不使用泛型上界会导致E被擦除为Object,后面 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 TestDemo { Alg<Integer> alg = new Alg<>(); Integer[] array = {5,2,7,100,3,6,9}; int max = alg.findMax(array); System.out.println(max); }
6、泛型方法
语法
方法限定符 [static] <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
使用泛型方法改写上述求最大值泛型类:
class Alg2 { //这里将其改写成静态的方法 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 TestDemo { public static void main(String[] args) { Integer[] array1 = {1,2,3,4,5}; Integer[] array2 = {6,7,8,9,10}; int max2 = Alg2.<Integer>findMax(array2); //可以使用类型推导省略<> int max1 = Alg2.findMax(array1); System.out.println("array1的最大值为:"+max1); System.out.println("array2的最大值为:"+max2); }
7、通配符
?
用于在泛型的使用,即为通配符
在"?"的基础上又产生了两个子通配符:
(1)? extends 类
:设置通配符上限
<? extends 上界> <? extends Number>//可以传入的实参类型是Number或者Number的子类
(2)? super 类
:设置通配符下限
<? super 下界> <? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
特别注意: 使用 ?
通配符时,无法对泛型类型进行具体化操作,即不能将具体类型赋值给 ?
。因此,在使用 ? 通配符的地方,只能进行读取操作(获取或比较),而不能进行写入操作(添加或修改)。这是为了保证类型安全性。
总结
到这里本文就结束了,或许读完本文你对泛型的理解还是萌萌懂懂,不过这都没关系,本篇文章的主要目的是能够对泛型在语法层面有一个基本的认知,想要真正理解泛型是需要时间和知识的打磨的!
到此断断续续的JavaSE也就告一段落了,总的来说Java的语法没有那么多弯弯绕绕,重点在于理解“面向对象”的概念。当然了JavaSE对于整个编程星球来说也只是一块敲门砖。路漫漫其修远兮,吾将上下而求索,我们下期再见喽!