Java——泛型与通配符的详解

定义

对于一般的类和方法,我们只能将其用在特定的数据类型。因此JDK1.5引入了泛型,其可将代码适用于多种数据类型

为何不能用Object

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; 

你可能感兴趣的:(java,java,javascript,开发语言)