泛型
1.什么是泛型?
在我们以往敲代码、定义类或者方法的时候,只能使用具体的类型,比如基本类型、自定义类型。如果我要编写一个可以应用于多种类型的代码,那么以往的方式就显得无力了。让下面的例子来引入泛型:
实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,换言之,就是能让这个数组能灵活的存不同类型的元素,也可以根据成员方法返回数组中某个下标的值。
class Test{
public Object[] array = new Object[10];
public Object getVal(int index){
return this.array[index];
}
public void setVal(int index,Object val){
this.array[index] = val;
}
}
public class Main {
public static void main(String[] args) {
Test test = new Test();
test.setVal(0,100);
test.setVal(1,30.3);
test.setVal(2,"Hello World");
}
}
我们可以很快的想到借助Object类型来完成这种操作。array确实是存了很多的类型,我们现在尝试读取元素:
public class Main {
public static void main(String[] args) {
Test test = new Test();
test.setVal(0,100);
test.setVal(1,30.3);
test.setVal(2,"Hello World");
int a = (int)test.getVal(0);
double d = (double)test.getVal(1);
String str = (String) test.getVal(2);
System.out.println(a);
System.out.println(d);
System.out.println(str);
}
}
结果:
100
30.3
Hello World
由于getVal方法返回的是Object类型,所以要强制类型转换。但是这种方法有一个缺陷,就是我们必须得提前知道我们读取的那个下标的元素类型,这显然是非常麻烦的。
虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型,并且是我们想让它存什么类型它就存什么类型,而不用定义多的数组。这就要用到泛型,那么泛型就是类型的参数化。
2.泛型的语法
什么是类型参数化?就是把类型当成参数,让我们指定其类型。
class 类名称 <T> {
// 这里可以使用类型参数
//比如:
<T> a = ....;
}
//new一个泛型类:
类名称<T> = new 类名称<>; //后一个<>里的T可以省略。
<>里面是 类型 形参列表,可以写任意的字母,代表占位符,就类似于方法里形参变量。
我们对上面案例的代码进行优化:
class Test<T>{
public T[] array = (T[]) new Object[10];//T[] array = (T[]) new Object[10] 写法是很危险的。
public T getVal(int index){
return this.array[index];
}
public void setVal(int index,T val){
this.array[index] = val;
}
}
public class Main {
public static void main(String[] args) {
//我想让数组存 字符串 类型
Test<String> test = new Test<>();// 可以推导出实例化需要的类型实参为 String,所以后面不用写(下同)
test.setVal(0,"Hello");
test.setVal(1,"World");
System.out.println(test.getVal(0));
System.out.println(test.getVal(1));
//我想让数组存 整数
Test<Integer> test1 = new Test<>();
test1.setVal(0,100);
test1.setVal(1,200);
System.out.println(test1.getVal(0));
System.out.println(test1.getVal(1));
}
}
结果:
Hello
World
100
200
上述代码就体现了泛型应用所带来的优点,我们只需传入想要的类型数组就存什么类型,并且不需要强制类型转换,应用到其它地方也是相同的道理。
补充:
1.泛型也可以如下定义:
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
3.擦除机制
泛型是怎么编译的?我们看看上面代码的汇编:
这里借助了jclasslib bytecode viewer这个插件,可以用来看汇编代码:
可以看到,在编译的过程当中,JVM将所有的T替换为Object,这种机制就叫做擦除机制。
上面提到T[] array = (T[]) new Object[10] 写法是很危险的,为什么?:
像上面的代码编译器并没有报错,我们直接执行:
原因是在编译的时候,getArray()方法的返回值类型被擦除成Object类型了,返回的Object数组里面,可能存放的是任何的数据类型,可能是Double,可能是Person,运行的时候,直接转给String类型的数组,编译器认为是不安全的。这种写法编译器不显示报错,容易写出BUG,所以不推荐这种写法。
4.泛型的上界
语法:
class 泛型类名称<类型形参 extends 类型边界> {
...
}
这里先举一个案例来更好的理解泛型的上界。
问题:实现一个泛型类,提供一个方法,用来求出传入的数组中的最大值。
上面求数组最大值的逻辑是没问题的,但是为什么会报错呢?因为这里没有把类型定死,数组里的元素可能是一个个对象,而对象之间的不能用大于小于号来比较,那我们就用compareTo()方法来比较。
为什么又报错误了?这是因为T这个类型可能没有实现Comparable接口,也可能实现了,编译器不知道你到实没实现,T的范围太大了,所以得给它一个约束。
<T extends Comparable<T>>表示传进来的类型必须实现Comparable接口或者是该接口;如果extends后面是一个类,则表示传进来的类型必须是该类的子类或者该类,不然报错。这就给T加上了一种界限,这就叫上界。
注意,这里上界不管是类还是接口都是使用extends,没有implements。
补充:上面介绍过的擦除机制会把T擦成Object类型,而当给T加了上界后,它就擦成了上界的类型,这里就是Comparable类型。
5.泛型方法
我们还是根据上面的案例来引入泛型方法。
就上面的求数组最大值案例来说,其实现方法有点不够好,每次求最大值的时候我们还要new一个Test对象,这也太麻烦啦。欸,想到这是不是就想到了static了,我们加个static不就行啦?
唉我去,怎么报错了?当我们调用getMax静态方法的时候,开始加载Test类,然而这时就上面代码而言是没有机会将T的类型确定的。想一下我们没有加static的时候,是先new一个Test对象,顺便传了<Integer>进去确定了T,然后再调用getMax方法。而现在我们是直接类名.getMax,就没有机会传入<Integer>,所以报错了。
正确的写法:
class Test {
//将泛型类型参数写在方法上
public static <T extends Comparable<T>> T getMax(T[] arr){
T max = arr[0];
for (int i = 1; i < arr.length; i++) {
if(max.compareTo(arr[i]) < 0){
max = arr[i];
}
}
return max;
}
}
public class Main {
public static void main(String[] args) {
Integer[] arr = {1,2,3,4,5,6,10};
int max = Test.<Integer>getMax(arr);//这里的<Integer>可以省略:Test.getMax(arr)
System.out.println(max);
}
}
结果:
10
泛型方法语法:
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
把类型形参列表放在方法上面,那这就是泛型方法。
补充:
不止是静态方法可以这么写,普通方法也可以,上面的代码也可以写成:
class Test {
public <T extends Comparable<T>> T getMax(T[] arr){
T max = arr[0];
for (int i = 1; i < arr.length; i++) {
if(max.compareTo(arr[i]) < 0){
max = arr[i];
}
}
return max;
}
}
通配符
1.什么是通配符?
我们现在已经熟悉了泛型的使用、以及其应用场景,那么通配符与泛型是什么关系呢?通配符的使用场景又是什么呢?
我们实现一个类,里面一个成员变量、两个方法,分别用来存取变量。Main里用一个方法来打印变量:
class Test<T>{
private T message;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Main{
public static void main(String[] args) {
Test<String> test = new Test<>();
test.setMessage("Hello");
print(test);
}
//打印
public static void print(Test<String> test){
System.out.println(test.getMessage());
}
}
结果:
Hello
上面的代码你是否能看出一些端倪?那就是print方法写得不好,如果我传入的类型不为String的时候就会报错。
那怎么解决呢?用方法重载?
方法重载是不行的!!!泛型只是类型参数化,描述的是泛型类里面成员的类型,而这里的Test<String> test 和 Test<Integer> test的类型是Test类型,也就是说,<String> <Integer>并不参与类型的组成,在编译的时候就会把<String> <Integer>擦除掉。
那么正确的方法就是要用到通配符:
public class Main{
public static void main(String[] args) {
Test<String> test = new Test<>();
test.setMessage("Hello");
print(test);
Test<Integer> test1 = new Test<>();
test1.setMessage(10);
print(test1);
}
//打印 用到了 ? 通配符
public static void print(Test<?> test){
System.out.println(test.getMessage());
}
}
结果:
Hello
10
这里的?就是通配符,是用来代表任意类型的。那么通配符的应用场景:方法形参表里出现泛型类的时候就会用到通配符,前提条件是这个方法不在这个泛型类里。
补充:通配符是不能修改数据的:
原因是没法确定message具体类型,所以无法修改。
2.通配符上界
通配符上界与泛型的上界是一样的道理,它的语法:
<? extends 上界>
<? extends Father>//可以传入的实参类型是Father或者Father的子类
表示只能接收Father或者Father的子类,Father就是通配符的上界。
class Father{
}
class Son1 extends Father{
}
class Son2 extends Son1{
}
class Test<T>{
private T message;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Main{
public static void main(String[] args) {
//传入 Test<Father>
Test<Father> test = new Test<>();
test.setMessage(new Father());
print(test);
//传入 Testt<Son1>
Test<Son1> test1 = new Test<>();
test1.setMessage(new Son1());
print(test1);
//传入 Test<Son2>
Test<Son2> test2 = new Test<>();
test2.setMessage(new Son2());
print(test2);
}
//打印
public static void print(Test<? extends Father> test){
System.out.println(test.getMessage());
}
}
结果:
demo6.Father@5cad8086
demo6.Son1@6e0be858
demo6.Son2@61bbe9ba
通配符上界也是不能修改(set)数据的,因为test接收的是Father和它的子类,此时存储的元素应该是哪个子类无法确定。(如果能修改的话,你想一想,message的类型本来是Son,而你set一个Father类型,这是不是向下转型了,要知道向下转型是非常危险的。)
3.通配符下界
通配符的下界与上界类似:
语法:
<? super 下界>
<? super Son>//代表 可以传入的实参的类型是Son或者Son的父类类型
这里的Son就是通配符的下界。(下界下界,就表示你能传入的最低要求,这样更易理解);看看下面的案例:
class Father{
}
class Son1 extends Father{
}
class Son2 extends Son1{
}
class Test<T>{
private T message;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Main{
public static void main(String[] args) {
Test<Father> test = new Test<>();
test.setMessage(new Father());
print(test);
Test<Son1> test1 = new Test<>();
test1.setMessage(new Son1());
print(test1);
Test<Son2> test2 = new Test<>();
test2.setMessage(new Son2());
print(test2);//错误的
}
//这里变为<? super Son1>
public static void print(Test<? super Son1> test){
System.out.println(test.getMessage());
}
}
这里Father、Son1能传,而Son2不能传,就是因为Son2不是Son1的父类。
那么通配符的下界能否修改message呢?
这里就与通配符的上界有区别了,通配符的下界能提交下界(Son1)的子类,不能提交下界(Son1)的父类。
解释:如果当message的类型是Son1或者Son1的父类的时候,我传入了Son1的子类,这时message不管是Son1或者Son1的父类,这里都是向上转型,所以可以编译。
还要需要注意的是通配符的下界对数据(这里就是message)的读取是有要求的:
当没有用变量接收的时候可以编译通过,而当用变量接收的时候就会报错,原因也是与向下转型有关,大家可以动动脑筋想一想。