一、包装类
1、什么是包装类
将基础类型包装成的类就是包装类。由于基础类型不是继承 Object 类的类,所以在泛型不能直接支持基础类型,为了解决这个问题,就需要把基础类型转换为对应的包装类。
基础类型 | 包装类 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
2、装箱和拆箱
装箱:基础类型转换为包装类。
拆箱:包装类转换为基础类型。
以下为过时的写法:
3、自动装箱和拆箱
我们现在用的都是自动装箱、拆箱,第一种方式是最好的。
用 javap -v 命令反汇编字节码文件,可以看到编译器会自动添加 valueOf (装箱)和 intValue(拆箱) 方法。
4、包装类的常量缓冲池
同是比较有相同内容的对象的地址,结果却不同:
这是因为 Integer 有一个常量池机制,当加载 Integer 类时,就会自动把 -128 ~ 127 范围的对象(常用的数值)放到常量池中:
因此,i8、i9 引用都是存放的常量池中 127 对象的地址,而 i10、i11 引用分别存放不同的堆中新创建的对象的地址:
注:128 并没有超过 Integer 的取值范围,它的范围大概是 -21亿 ~ 21 亿。
包装类 | 缓冲值范围 |
Byte | -128 ~ 127 |
Short | -128 ~ 127 |
Integer | -128 ~ 127 |
Long | -128 ~ 127 |
Float | 无 |
Double | 无 |
Character | 0 ~ 127 |
Boolean | true 和 false |
二、泛型
1、什么是泛型
一般的类和方法只能固定接收、返回一种类型的数据,但有时我们希望接收、返回很多种类型的数据。比如写一个加法器,里面的数据可以是整型、浮点型,如果使用方法重载,方法里的步骤相同,只是数据类型不同,造成代码的冗余。使用泛型可以解决这个问题,相当于将数据类型参数化,可以给同一个类或方法灵活地指定任意类型。
2、使用 Object 类
如果我们需要实现一个数组类,这个类中的数组成员可以存放任意类型的元素,并且有 set、get 指定位置的元素的方法。可以使用 Object 类,因为它是任意类的父亲,通过任意类型向上转型为 Object 类实现:
但是这种方法有个缺点,一是获取值时需要手动强制类型转换;二是无法确定一种类型,即让类或方法在使用时持有一种类型,而不是同时拥有任意类型。而泛型,不需要手动强制类型转换,并且在创建对象时就将某一种类型作为参数传入,指定其类型,编译器对其进行类型检查。既能让类指定不同的类型,又能给对象指定一种类型。
3、泛型的使用
将上面的类改写成泛型:
- <T> 表示该类为泛型类,类型形参常用的名称有:
- MyArray 中仍用 Object[] 类型创建对象,因为 T 只是泛型类的标志,它并没有实际的构造函数。
- 在定义引用时,已确定为 String 类, 因此 new 对象时可根据上下文推导类型,省略为 <> 。
- 优点:编译器自动进行类型转换、类型检查;一份代码支持多种类型。
4、裸类型
泛型参数不指定,默认 T 为 Object,它只是为了兼容老版本,不建议使用:
我们学习泛型,重点不在定义泛型类,而在泛型类的实例化,后续使用 java 中的集合类时,会经常用到泛型的实例化。
5、如何编译泛型——擦除机制
编译器先将泛型类型擦除,再替换为 Object 或边界类型。set 中把指定类型转为 Object (向上转型),get 中强制转换 Object 为指定类型(针对泛型类)。
最后针对子类重写泛型父类的方法,避免没有真正覆盖父类方法,编译器会自动生成桥接方法。
擦除前:
public class Node<T> {
T data;
public void setData(T data) {
this.data = data;
}
}
public class StringNode extends Node<String> {
@Override
public void setData(String data) {
super.setData(data);
}
}
擦除后:
public class Node {
Object data;
public void setData(Object data) {
this.data = data;
}
}
public class StringNode extends Node {
// 形参中,子类的 String 与父类替换后的 Object 不一致
// 未成功覆盖
@Override
public void setData(String data) {
super.setData(data);
}
}
自动生成桥接方法:
public class StringNode extends Node {
// 子类与父类的方法签名一致
@Override
public void setData(Object data) {
setData((String) data);
}
}
6、泛型上界
对泛型的类型范围的上界进行约束:
在实现计算器时,使用泛型上界,可以限制类型为数字。若没有 extends,默认为 extends Object。
7、泛型方法
将泛型写到方法名后,返回值类型前,让返回值类型、参数列表都能使用该泛型:
泛型方法的使用,类型推导见上文 “泛型的使用” :
8、通配符
使泛型类引用能接收所有类型的泛型类对象:
三、总结
泛型的重点在泛型类对象怎么实例化,为后续集合类的使用做铺垫。而怎么定义泛型类、怎么定义泛型方法、擦除机制、泛型上界、通配符等相比较下就不那么重要了。
四、泛型数组的创建(补充)
出错代码:
4.1、原因
java 不支持创建泛型数组,因为泛型数组会破坏数组的类型安全性。
① 前言,数组的类型检查机制:
public class ArrayRuntimeCheck {
public static void main(String[] args) {
Integer[] intArray = new Integer[5];
try {
Object[] objArray = intArray;
objArray[0] = "Hello";
} catch (ArrayStoreException e) {
System.out.println("捕获到 ArrayStoreException: " + e.getMessage());
}
}
}
当尝试将 String 类型的元素赋值给数组时,Java 运行时会立即检查数组的实际类型(Integer[]),发现类型不兼容,马上抛出 ArrayStoreException
。阻止了不兼容类型的元素被存储到数组中,保障了类型安全。
② 如果允许创建泛型数组。由于泛型的擦除机制,List<Interger>[] 在编译后变成 List<Object>[],即当作 List<Object>[] 处理。如果将 String 类型数据赋值给泛型数组,在运行时不兼容的数据可以顺利存入数组,直到后续尝试从数组中取出元素并当作 List<Integer>
处理时,才才会抛出 ClassCastException,破坏了数组元素类型的一致性。
② 数组是协变的,即如果Sub
是Super
的子类,那么Sub[]
也是Super[]
的子类。例如:
Number[] numbers = new Integer[10];
但是泛型是不可变的,List<Integer>
并不是 List<Number>
的子类。数组和泛型存在冲突。
4.2、解决
4.2.1、使用通配符数组
可以创建通配符数组,然后将其转换为泛型数组。不过这种方法需要使用@SuppressWarnings("unchecked")
注解来抑制编译警告,因为这种转换在运行时是不安全的。这种转换需要开发者自己保证类型的安全性,因为在运行时 Java 无法进行严格的类型检查。
@SuppressWarnings("unchecked")
// 等效于 (Node<K, V>[])new Node[10]
public Node<K, V>[] arr = (Node<K, V>[])new Node<?, ?>[10];
4.2.2、使用 ArryList 代替数组
public ArrayList<Node<K, V>> arr = new ArrayList<>();