包装类
java中,由于基本数据类型不是Object,但为了将基本数据类型当作对象处理,Java给每个基本类型都对应了一个包装类型。
基本数据类型和对应包装类
基本数据类型 | 包装类 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
boolean | Boolean |
float | Float |
double | Double |
char | Character |
规律:除了Integer和Character,其它基本数据类型的包装类都是首字母大写
装箱和拆箱
int i = 10;
//(显式)装箱操作,新建一个Integer对象,将i的值放在对象的某个属性当中
Integer ii = Integer.valueOf(i);
Integer ij = new Integer(i);
//(显式)拆箱操作,将Integer从包装中取出,放到一个基本数据类型中
int j = ii.intValue();
自动装箱和自动拆箱
可以看到,使用过程中,显式地装箱和拆箱带来了不少代码量,所以为了减少给开发者带来的负担,Java提供了自动机制。
int i = 10;
Integer ii = i;//自动装箱
Integer ij = (Integer)i;//自动装箱
int j = ii;//自动拆箱
int k = (int)ij;//自动拆箱
什么是泛型(也算是一种语法)
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写出可以应用于多种类型的代码,这种刻板的的限制对代码的约束就很大。
我们便引入了泛型:就是适用于多种类型。从代码上讲,就是对类型实现了参数化。
引出泛型
实现一个类,类中包含一个数组成员,是的数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值。
思路:所有类的父类都是Object类,数组是否可以创建为Object类型的呢?
引入代码:
class MyArrays { private Object[] array = new Object[10]; public void setVal(int pos, Object val) { this.array[pos] = val; } public Object getPos(int pos) { return this.array[pos]; } } public class Test1 { public static void main(String[] args) { MyArrays myarrays = new MyArrays(); myarrays.setVal(0,10); myarrays.setVal(1,"hello"); //编译错误 String ret = myarrays.getPos(1); } }
问题:在上述代码中:
1.任何数据的类型都可以存放
2.但在取出时会发生编译错误,因而需要强制类型转换。
虽然在这种情况下,当前数组的任何数据都可以存放,但是在更多的情况下,我们还是希望它只能够持有一种数据类型。而不是同时存放这么这么多的类型。
所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去检查(比如持有int类的,就不能放入String类型的数据),此时就需要把类型,作为参数传递。需要什么类型,就传入什么类型。
语法
class 泛型类名称 <类型形参列表> { //这里可以使用类型参数 } class ClassName<T1, T2, ...Tn> { } class 泛型类名称<类型形参列表> extends 继承类/*这里可以适用类型参数*/ { //这里可以使用类型参数 } class ClassName<T1, T2,.. Tn>extends ParentClass<T1> { //可以使用部分类型参数 }
将上述代码改造如下:
class MyArrays<T> { private T[] array = (T[])new Object[10];//1 public void setVal(int pos, T val) { this.array[pos] = val; } public T getPos(int pos) { return this.array[pos]; } } public class Test1 { public static void main(String[] args) { MyArrays<Integer> myArrays = new MyArrays<>();//2 myArrays.setVal(0,1); myArrays.setVal(1,2); int ret = myArrays.getPos(0);//3 System.out.println(ret); //myArrays.setVal(2,"hh");//4 } }
代码解释:
1.类名后的<T>表示占位符,表示当前类是一个泛型类
了解:类型形参一般使用一个大写字母表示,常用的名称有:
E代表Element
K表示Key
V表示Value
N表示Number
T表示Type
2.注释一处,不能new泛型类型的数组
T[] ts = new T[10];//是不对的
泛型是编译时存在的,当程序运行起来到JVM后,就无泛型这个概念。
3.注释2处,类型后加入<Integer>指定当前类型
4.注释3处,不需要进行强制类型转换
5.注释4处,代码编译报错,此时因为已经指定好当前类型了,编译器会在存放元素的时候帮助我们进行类型检查。
泛型类的使用
语法
泛型类<类型实参> 变量名;//定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参);//实例化一个泛型类对象
示例
MyArrays<Integer> array = new MyArray<Integer>();
注意:泛型只能接受类,所有的基本数据类型必须使用包装类!
类型推导
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
//可以推导出实例化所需要的类型为Integer MyArrays<Integer> array = new MyArray<>();
泛型是如何编译的
擦除机制
那么,泛型到底是怎么编译的?这个问题,也是曾经的面试问题。泛型本质是一个非常难的语法,要能理解还需要长时间的打磨。
在编译当中,会有一种将所有的T替换成Object这种机制,我们称为:擦除机制。
Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。
提出问题:
类型擦除,一定是将T变成Object吗
不一定。之前泛型类中的类型参数部分如果没有指定上限,如<T>则会被转换成普通的Object类型。如果指定了上限如<T extends String>则类型参数就被替换成类型上限。
为什么不能实例化泛型类型数组
代码:
public 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 static void main(String[] args) { MyArray<Integer> myArray1 = new MyArray<>(); Integer[] strings = myArray1.getArray(); } } /*Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Integer; ([Ljava.lang.Object; and [Ljava.lang.Integer; are in module java.base of loader 'bootstrap')at Demo2.MyArray.main(MyArray.java:21)*/
原因想必大家知道:替换后的方法为将Object[]分配给Integer[]引用,程序报错。
通俗讲就是:返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Person,运行的时候,直接转给了Integer类型的数组,编译器认为是不安全的。
正确的方式:
import java.lang.reflect.Array; public 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; } /** * 通过反射创建,指定类型的数组 * @param clazz * @param capacity * */ public MyArray(Class<T> clazz, int capacity) { array = (T[]) Array.newInstance(clazz, capacity); } public T[] getArray() { return array; } public static void main(String[] args) { MyArray<Integer> myArray1 = new MyArray<>(Integer.class, 10); Integer[] strings = myArray1.getArray(); } }
泛型的上界
在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
语法
class 泛型类名称 <类型形参 extends 类型边界> { }
示例
public class MyArray<E extends Number>{ ... }
只接受Number的子类型作为E的类型实参。
MyArray<Integer>|1;//正常,因为Integer是Number的子类型 MyArray<String>|2;//编译错误,因为String不是Number的子类型
如果没有指定边界E,可以视为E extends Object。
复杂示例
写一个泛型类,求一个数组中的最大值:
初始版代码:
class Alg<T> { public T findMaxValue(T[] array) { T max = array[0]; for (int i = 0; i < array.length; i++) { if (max < array[i]) { max = array[i]; } } return max; } } public class Test2 { public static void main(String[] args) { Alg<Integer> a = new Alg<>(); Integer[] array = {1, 2, 3, 4, 213, 321313, 6}; int max = a.findMaxValue(array); } }
这样写一定是不可行的,因为我们知道T是引用类型,最终会被擦除为Object类型,所以我们不能单纯地使用比较符号来比较这种类型。
那么该怎么比较呢?
这个问题可以看作如何约束这个T是可以比较大小的:这时我们就需要用到泛型的上界。
因为T是Object的子类,Object中有Comparable,所以考虑让T实现Comparable来利用相应方法进行比较:
class Alg<T extends Comparable<T>> { public T findMaxValue(T[] array) { T max = array[0]; for (int i = 0; i < array.length; i++) { if (max.compareTo(array[i]) < 0) { max = array[i]; } } return max; } } public class Test2 { public static void main(String[] args) { Alg<Integer> a = new Alg<>(); Integer[] array = {1, 2, 3, 4, 213, 321313, 6}; int max = a.findMaxValue(array); System.out.println(max); } }
这样就可以实现大小比较啦!