对于一般的类和方法,我们只能将其用在特定的数据类型。因此JDK1.5引入了泛型,其可将代码适用于多种数据类型
class MyArray {
public Object[] array = new Object[10];
public Object getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,int val) {
this.array[pos] = val;
}
}
public class TestDemo {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.setVal(0,1);
myArray.setVal(1,"hello");//可以存放
String ret = myArray.getPos(1);//编译报错
System.out.println(ret);
}
}
我们发现想要用Object来实现存储不同的数据类型时,虽然可以存储进去,但是当我们想要拿出来的时候,我们并不知道该数组下标所存放的元素类型,并且拿出来的时候即使是一个字符串类型,也需要我们进行强制类型转换,否则就会报错。
因此,泛型的目的就是让容器持有的对象类型让编译器进行检查
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
根据上面的语法规则,我们可以对之前的代码进行改进
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 class TestDemo {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>();
myArray.setVal(0,10);
myArray.setVal(1,12);
int ret = myArray.getPos(1);
System.out.println(ret);
myArray.setVal(2,"hello");//报错
}
}
规则:一般用以下大写字母来表示形参
E 表示 Element
K 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
S, U, V 等等 - 第二、第三、第四个类型
需要注意的是,在Java中不能new泛型类型的数组
T[] t = new T[5];//error
我们可以用以下代码进行替换
T[] array = (T[])new Object[10];
但是上述代码也不是十全十美的,在接下来的内容中将进行讲解
我们用以下语法使用泛型类
泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
例如:
MyArray<Integer> list = new MyArray<Integer>();
需要注意的是:
泛型类的尖括号中需要使用包装类,不能用基本类型
第二个尖括号中的内容可以进行省略
也就是一个泛型类但是没有带着类型,是为了兼容老版本而遗留下来的语法规则,我们自己尽量不要使用
MyArray list = new MyArray();
在字节码文件中,并没有T这类的符号出现,而都是Object,这种编译方式称之为擦除机制
具体内容可以参考下面这篇文章
https://zhuanlan.zhihu.com/p/51452375
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: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer; at test.main(test.java:18)
报错的意思是:我们将Object的数组给Integer数组引用
数组是在运行的时候检测类型的匹配问题,而泛型是在编译时检测
也就是说,返回的Object数组中可能存放任意类型的数据,而把这些数据直接赋值给Integer类型的数组是不安全的,因此编译器报错。
所以,我们之前的定义泛型数组的方法并不是完美的,我们应该用反射的方法来定义
class MyArray<T> {
public T[] array;
public MyArray() {
}
public MyArray(Class<T> clazz, int capacity) {
array = (T[])Array.newInstance(clazz, capacity);
}
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.class,10);
Integer[] integers = myArray1.getArray();
}
class 泛型类名称<类型形参 extends 类型边界> {
…
}
我们用extends关键字来定义泛型的上界,这样的话用户在传泛型参数的时候只能传上界及其子类
public class MyArray<E extends Number> {
...
}
而当我们的类型边界写的是一个接口时,那么用户传的参数必须是实现了该接口的
public class MyArray<E extends Comparable<E>> {
...
}
E必须实现Comparable接口
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { … }
public class Util {
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}
在使用这个方法时,我们可以直接传类型参数,也可以不传,让编译器进行类型推导
Integer[] a = { ... };
swap(a, 1, 2);
Integer[] a = { ... };
Util.<Integer>swap(a, 1, 2);
由于泛型无法解决协变问题,即泛型之间没有父子类关系,泛型就是传什么参数就是什么参数。因此我们用通配符可以实现父子类关系,使参数范围更广
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo {
public static void main(String[] args) {
Message<String> message = new Message() ;
message.setMessage("hello world");
fun(message);
}
public static void fun(Message<String> temp){
System.out.println(temp.getMessage());
}
}
如果我们使用上述代码时,当我们把T改为Integer时,那么fun函数没法进行相对应的调整
因此,这时我们可以使用通配符?
public class TestDemo {
public static void main(String[] args) {
Message<Integer> message = new Message() ;
message.setMessage(55);
fun(message);
}
public static void fun(Message<?> temp){
//temp.setMessage(100); 无法修改
System.out.println(temp.getMessage());
}
}
由于不确定传入通配符的类型,因此无法修改值,只能获取值
和泛型的上界类似
<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类
实例:
class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class Message<T> { // 设置泛型上限
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo {
public static void main(String[] args) {
Message<Apple> message = new Message<>() ;
message.setMessage(new Apple());
fun(message);
Message<Banana> message2 = new Message<>() ;
message2.setMessage(new Banana());
fun(message2);
}
public static void fun(Message<? extends Fruit> temp){
//temp.setMessage(new Banana()); //无法修改
//temp.setMessage(new Apple()); //无法修改
System.out.println(temp.getMessage());
}
}
由于不确定传入类型是fruit的哪个子类,因此无法set
因此,通配符确定上界一般用来读取数据,而不是写入数据
我们用super关键字来描述通配符的下界
<? super 下界>
<? super Integer>//可以传入的实参的类型是Integer或者Integer的父类类型
那么,在刚才的例子中,我们的fun方法有如下特点
public static void fun(Message<? super Fruit> temp){
// 此时可以修改添加的是Fruit 或者Fruit的子类
temp.setMessage(new Apple());//这个是Fruit的子类
temp.setMessage(new Fruit());//这个是Fruit的本身
//Fruit fruit = temp.getMessage(); 不能接收,无法确定是哪个父类
System.out.println(temp.getMessage());//只能直接输出
}
因此,通配符确定下界一般用来写入数据,而不是读取数据
为了支持泛型,Java中的基本类型都有自己对应的包装类
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
手动装箱
int i = 1;
Integer ii = Integer.valueOf(i);
Integer ij = new Integer(i);
自动装箱
int i = 1;
Integer ii = i;
Integer ij = (Integer)i;
手动拆箱
int j = ii.intValue();
自动拆箱
int j = ii;
int k = (int)ii;