初识Java【7】—— 泛型

泛型

一、泛型定义

1.泛型引入

在正式讨论泛型之前,我们需要明白泛型存在的意义是什么。这里笔者提出一个问题:能否实现一个类:类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值

看到这个问题,基础好的读者可能会回答:Object定义数组即可。不得不说这是一个非常好的点子,因为我们知道Object类是所有类的父类,哪怕是基本类型也会转成对应的包装类。Talk is cheap, show me the code~

class MyArray {
    public Object[] array = new Object[10];

    public Object getValue(int pos) {
        return this.array[pos];
    }

    public void setValue(int pos, Object val) {
        if(pos < array.length) {
            this.array[pos] = val;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.setValue(0, 1);
        myArray.setValue(1, 2.0);
        myArray.setValue(2, 'a');
        myArray.setValue(3, "abc");
        myArray.setValue(4, new String[2]);

        // 注意下面两行
        String str = (String)myArray.getValue(3);
        String[] arr = (String[])myArray.getValue(4);
    }
}

如上所示,我们可以正常将元素存入MyArray这个类中,但是在取出元素的时候,我们却需要注意强转。这就不太符合我们的使用习惯了,我们能不能不需要强转就获得数据呢?毕竟,现在只是有限个元素,我们能够根据存入的元素去判断强转成什么类型。而且在实际开发中,如果要对这样的类进行抽象,我们还是希望:不同实例能够存放不同类型的数据,同一个实例存放同一个类型的数据。

面对上面两个诉求,各位读者应该能感受到创造泛型的目的了吧。**所谓泛型:就是数据类型的泛化,即数据类型参数化。泛型类相当于一个容器,同一个容器能够持有不同类型的对象,而且这样的容器可以有多个。如果我们让数据类型参数化之后,就能够利用编译器来帮助我们对数据类型做检查。**这一段话并不那么容易理解,下面我们改造一下上方的代码,让各位读者能够对照概念理解。

class MyArray<T> { // 注解1
    
    public T[] array = (T[])new Object[10]; // 注解3

    public T getValue(int pos) {
        return this.array[pos];
    }

    public void setValue(int pos, T val) {
        if(pos < array.length) {
            this.array[pos] = val;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        MyArray<Integer> myArray1 = new MyArray<Integer>(); // 注解2
        myArray1.setValue(0,0);
        myArray1.setValue(1,1);
        System.out.println(myArray1.getValue(1));

        MyArray<String> myArray2 = new MyArray<>();
        myArray2.setValue(0,"abc");
        myArray2.setValue(1,"hello");
        System.out.println(myArray2.getValue(1));
    }
}

接下来咱们就对上面的代码进行一些解释。

  • 注解1:代码是如何实现 数据类型参数化 的呢?

我们能够看到**MyArray这个类名之后多了<T>这样的一个东西**。**<T>是一个占位符,代表了当前这个类是泛型类,而且泛型类中的成员除了能够使用正常的数据类型之外,还能够使用类型参数列表中出现的数据类型,即T类型。**这时数据类型就如同调用方法时传入的一个参数一样,我们就实现了 数据类型参数化 这个目的了。

  • 注解2:如何利用编译器对数据做 类型检查 呢?

**类型检查是针对引用的。**谁是引用,就用这个引用调用泛型方法,然后就会对这个引用调用的方法进行类型检测,这跟它真正引用的对象无关。

  • 注解3:为什么new Object[10]没有被写成new T[10]呢?

**因为Java中是不能够new泛型数组的,数组在 new时不能确定类型,那么就无法在内存开空间,所以编译器直接强制规定无法创建泛型实例。**而接触过擦除机制的读者可能会疑惑,这两种写法有什么区别呢?被擦除之后本质上是相同的,这样写有什么意义呢?我们应该如何创建类型T的数组?实际上,这两种写法都不是真正规范的写法,这个坑我们最后再填。

2.泛型的好处

(1)提升了程序的健壮性和规范性

(2)编译时检查添加元素的类型,提高了安全性

(3)减少了类型转换的次数,提高效率

(4)在类声明时通过一个标识可以表示属性类型、方法的返回值类型参数类型


二、泛型语法

1.基本语法

通过上面的示例,我们已经接触到一些泛型的语法了,接下来我们正式介绍泛型的相关语法。

// 1.泛型类的定义,注意:类型参数列表【必须是引用类型】,不能够是简单类型。
class 泛型类名<类型参数列表> {}

// 2.泛型类的实例化
泛型类名<类型参数列表> 泛型类的引用 = new 泛型类名<类型参数列表>();

// 3.类型推导下的实例化:编译器能够根据上下文推导出类型实参
泛型类名<类型参数列表> 泛型类的引用 = new 泛型类名<>();

// 4.裸类型:跟Object类一样使用了。兼容原有API的产物,免得因为原API是非泛型而报错。
泛型类名 泛型类的引用 = new 泛型类名();

对于类型参数列表这里多说几句,习惯来讲,一些特定的字母,代表的是特定的含义。

比如:T就代表了 Type,即数据类型;E表示ElementKK表示KeyV表示ValueN表示Number

2.泛型上界

(1)泛型上界的产生

为什么我们需要泛型上界呢?因为擦除机制会将所有的泛型都擦成Object类型,**但是有时候Object类型不好进行一些操作,我们需要直接擦成其他的类型。**这就需要用到泛型上界,它的语法形式如下:

class 泛型类名<类型参数 extends 类型边界> {}

笔者举一个简单的示例:

class MyArray<T extends Number> {
    public T[] array = (T[])new Object[10];

    public T getValue(int pos) {
        return this.array[pos];
    }

    public void setValue(int pos, T val) {
        if(pos < array.length) {
            this.array[pos] = val;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        MyArray<Integer> myArray1 = new MyArray<>();
        myArray1.setValue(0,0);
        myArray1.setValue(1,1);
        System.out.println(myArray1.getValue(1));

		// error
        MyArray<String> myArray2 = new MyArray<>();
        myArray2.setValue(0,"abc");
        myArray2.setValue(1,"hello");
        System.out.println(myArray2.getValue(1));
    }
}

在这个示例中,**为什么myArray2会报出语法错误呢?**在这个示例中,泛型T只能是Numer类及其子类,而String既不是Number类本身,也不是它的子类。

推广到整个泛型上界则是:类型参数必须是类型边界本身 及 其子类

(2)一个复杂的泛型上界示例

上面这示例比较简单,接下来展示一个比较复杂的示例。在展示这个示例之前,请各位读者先思考一下:如何编写一个泛型类,使得能够找出数组的最大值?

部分读者可能会把代码写成下面这的形式:
在这里插入图片描述

为什么这样直接比较不行呢?因为T[]并不一定是基本类型中的数值类型,直接这样比较会有安全问题,不符合规范。正确的做法其实是要接上Comparble接口类,代码如下所示:

class MyArray<T extends Comparable<T>> {

    public T findMaxValue(T[] array) {
        T maxValue = array[0];
        for (int i = 0; i < array.length; i++) {
            if(array[i].compareTo(maxValue) > 0) {
                maxValue = array[i];
            }
        }
        return maxValue;
    }
}

3.泛型方法

(1)普通泛型成员方法

上面所展示的只是普通的成员方法,如果我们需要写一个泛型方法应该如何改造呢?泛型方法的语法格式如下:

访问修饰限定符 <类型参数列表> 返回值类型 方法名(形参列表)

接下来我们直接将上方的普通成员方法改造成普通泛型方法吧~

class MyArray<T extends Comparable<T>> {

    public <T extends Comparable<T>> T findMaxValue(T[] array) {
        T maxValue = array[0];
        for (int i = 0; i < array.length; i++) {
            if(array[i].compareTo(maxValue) > 0) {
                maxValue = array[i];
            }
        }
        return maxValue;
    }
}

public class Test {
    public static void main(String[] args) {
        Integer[] arr = {1,2,3,21,54,12,65,78};

        MyArray<Integer> myArray = new MyArray<>();
        System.out.println(myArray.findMaxValue(arr));
    }
}
(2)静态泛型成员方法

正常的泛型方法就这么改造完毕了,而对应的当然还有静态类型的泛型成员方法

按照以往的思路,咱们直接给普通泛型成员方法加上static关键字就能改造完成,但是出了一些意外:

在这里插入图片描述

这个报错翻译过来的意思是:“MyArray.this”不能从静态上下文引用。为什么会有这个报错呢?因为静态方法是属于类本身而不是类实例对象的,上图静态方法所使用的T其实就来自于MyArray<T extends Comparable<T>>实例化之后的那个T。因此,如果要写一个静态泛型方法,泛型类上的占位符其实可以省略,因为根本不会用上

那如何是静态泛型方法成为泛型的方法呢?具体语法形式如下:

访问修饰限定符 static<类型参数列表> 返回值类型 方法名(形参列表)

按照语法所说,可以写出如下代码:

class MyArray {

    public static<T extends Comparable<T>> T findMaxValue(T[] array) {
        T maxValue = array[0];
        for (int i = 0; i < array.length; i++) {
            if(array[i].compareTo(maxValue) > 0) {
                maxValue = array[i];
            }
        }
        return maxValue;
    }
}

public class Test {
    public static void main(String[] args) {
        Integer[] arr = {1,2,3,21,54,12,65,78};
        // 注意:可以省略 <Integer> ,编译器能够推导出数据类型。
        int max = MyArray.<Integer>findMaxValue(arr);
        System.out.println(max);
    }
}

4.通配符

(1)通配符的产生

在泛型中我们会看到**?的使用,这其实是通配符**。通配符是用来解决泛型无法协变的问题。什么是协变呢?所谓协变指的是:如果StudentPerson的子类,那么对应的List<Student>List<Person>的子类。但是泛型无法协变,也就说:在泛型的语法里List<Student>List<Person>不是父子类关系。

无法协变本质上是在表明:类型参数只参与类型的检查,但不参与类型的组成。类型的检查其实对于类加载中的验证阶段,编译阶段则是类加载的解析阶段,运行阶段是 初始化+使用 的阶段。因为擦除机制的存在,所有的类型参数都被擦成了Object,如果这都参与类型组成就没有意义了。

我们可以进行一些实验,证明一下:

// 实验1
public class Test {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass()); // true
    }
}
// 实验2
public class Test {

    public static void main(String[] args) throws Exception {

        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

        // 利用反射能够成功存入字符串
        list.getClass().getMethod("add", Object.class).invoke(list, "asd");

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

}

在没有通配符之前,如果我们要写一个能够让泛型访问的公共方法是做不到的。因为公共方法无法使参数类型一致,编译器会进行类型检查,因此只能拆开成两个方法。

class Message<T> {
    private T message;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }
}

public class Test {
    public static void main(String[] args) {
        Message<String> message1 = new Message<>();
        message1.setMessage("请给我点个赞吧~");
        Test.fun1(message1);

        Message<Integer> message2 = new Message<>();
        message2.setMessage(1);
        Test.fun2(message2);
    }
    
    // 使用通配符
    public static void fun1(Message<?> temp) {
        System.out.println(temp.getMessage());
    }
    
    // 不使用通配符:必须拆成两个方法
    public static void fun1(Message<String> temp) {
        System.out.println(temp.getMessage());
    }
    
    public static void fun2(Message<Integer> temp) {
        System.out.println(temp.getMessage());
    }
}
(2)通配符的上界与下界

上下界的产生也是有原因的,咱们先做一些前置准备:

class Plate<T> {
    private T plate; // 消息

    public T getPlate() {
        return plate;
    }

    public void setPlate(T plate) {
        this.plate = plate;
    }
}

在这里插入图片描述

1)通配符的上界

接收的是 类型边界本身 及 其子类。具体语法:

<? extends 类型边界>

使用示例

public class Test {
    public static void main(String[] args) {
        Plate<String> plate = new Plate<>();
        Plate.setPlate("路哲萧笑欢迎你!");

        fun(plate);
    }
    
    // 不能够使用 setPlate() ,因为 Plate参数化的类型是什么没法确定
    public static void fun(Plate<? extends Fruit> temp) {
        System.out.println(temp.getPlate());
        
        // 必须使用 类型上界 接收
        Fruit fruit = temp.getPlate();
        System.out.println(fruit));
    }
}

在这里插入图片描述

2)通配符的下界

接收的是 类型边界本身 及 其所有的父类。具体语法:

<? super 类型边界>

使用示例,一般用于添加元素

public class Test {
    public static void main(String[] args) {
        Plate<String> plate = new Plate<>();
        Plate.setPlate("路哲萧笑欢迎你!");

        fun(plate);
    }
    
    public static void fun(Plate<? super Fruit> temp) {
        System.out.println(temp.getPlate());
        
        // 直接添加 类型边界本身 及 其子类
        temp.setPlate(new Apple());
        temp.setPlate(new Banana());
        
        //------------------------------
        // error1:不知道获取的是本身还是父类
        Fruit fruit = temp.getPlate();
        System.out.println(fruit));
        
        // error2:不能添加类型边界的父类,向下转型有风险!
        temp.setPlate(new Food());
    }
}

在这里插入图片描述

5.八大注意事项

(1)不能用基本类型实例化类型参数

由于基本类型不继承自Object,也不属于任何类,所以不能转换为Object类型。

List<Integer>  // 正确
List<int>      // error
(2)运行时类型检查只能检查原始类型
Test<String> test = new Test<>();
test instanceof Test<String> // 报错,但可以使用 test instanceof MyInterface
test.getClass() == Test.class // 正确,且为真,反射

因为类型擦除之后,Test<String>只剩下原始类型,泛型信息String不存在了。

(3)不能使用静态的类型变量字段
private static T singleInstance; // 错误
(4)不能实例化 类型变量 和 泛型数组
T t1 = new T(); // 错误
T[] t2 = new T[2]; // 错误
(5)不能创建参数化类型数组
public class Test<T> {
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}

public class MainTest {
    
    public static void main(String[] args) {
    	Test[] p = new Test[10]; // 这种可以编译通过,但是不安全。
		Test<?>[] p = new Test<?>[10]; // 与上面效果一样
    }

    // error
    public static void main(String[] args) {
        Test<Long>[] p = new Test<Long>[10]; // 假设这里可以运行
        Object[] o = p;
        o[0] = new Test<String>(); // 数组中会存储一个不是Pair<Long>类型的数据
    }
}
(6)异常类型不能用泛型类型
(7)关于继承1 —— 类型擦除与多态的冲突和解决方法
class MyTest<T> {
    public void test(T obj) {
        // do something
    }
}

public class Test extends MyTest<String> {
    @Override
    public void test(String obj) {
        // do something
    }
}

注意,在泛型擦除后,子类Test中的test方法和父类中的test方法并不属于重载关系,不具有多态性,是重写。

// 泛型在编译期,虽然你看着擦除之后是Object 和 String,但是其实都是Object
// 父类方法
public void test(Object obj) { ... }

// 子类方法
public void test(String obj) { ... }

如果子类重写了,直接调用子类的,子类没有重写,调用自动生成的桥方法桥方法是由于泛型类型擦除而自动生成的方法,它用于连接原始类型和泛型类型之间的关系

public void test(Object obj) {
  test((String)obj);
}

注意:如果是常规的两个方法,它们的方法签名是一样的,虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过 参数类型 和 返回类型 来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来”不合法”的事情,然后交给虚拟器去区别。

(8)关于继承2 —— 泛型中参数化类型不考虑继承关系
// 错误1:ArrayList<Object> list2 = new ArrayList<String>();
ArrayList<Object> list1 = new ArrayList<Object>();  
list1.add(new Object());  
list1.add(new Object());  
ArrayList<String> list2 = list1; // 编译错误

我们先假设第四行代码编译正确。当我们使用list2调用get()方法取值的时候,返回的都是String类型的数据(上面提到了,类型检测是根据引用来决定的),可是它实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。

// 错误2:ArrayList<String> list2 = new ArrayList<Object>();
ArrayList<String> list1 = new ArrayList<String>();  
list1.add(new String());  
list1.add(new String());

ArrayList<Object> list2 = list1; //编译错误

这样的情况比第一种情况好的多,最起码,在我们用list2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的一个原因就是去解决类型转换的问题。我们使用了泛型,到头来还是要自己强转,那就违背了泛型设计的初衷。所以Java不允许这么干。同样,如果又用list2往里面add()新的对象,那么取出来的时候,我们怎么知道我取出来的到底是String类型的,还是Object类型的呢?

三、擦除机制

在探究擦除机制之前,我们先做好前置的准备工作。

首先,下载好插件jclasslib

在这里插入图片描述

其次,重新Build编译 ,生成最新的字节码文件擦除机制发生在编译期,而不是运行期。

在这里插入图片描述

最后,在选中某个类之后,在View中点击Show Bytecode with Jclasslib就能够看到字节码了

在这里插入图片描述

通过对比方法与字节码的信息,我们会发现:擦除机制就是——所有的T都被擦除Ljava/lang/Object

在这里插入图片描述

用代码解释的话,如下所示:

// ------------编译前-------------
public class Test<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}

// ------------编译后-------------
public class Test {
    private Object value;
    public Object getValue() {
        return value;
    }
    public void setValue(Object value) {
        this.value = value;
    }
}

最后我们填上最初T[] a = (T[]) new Object[N];的坑。真正正确的写法需要用到反射:

class MyArray<T> {
	public T[] array;
	
	public MyArray() {}
	
	public MyArray(Class<T> clazz, int capacity) {
		array = (T[]) Array.newInstance(clazz, capacity);
	}
	
	public T getValue(int pos) {
		return this.array[pos];
	}
	
	public void setValue(int pos,T val) {
		this.array[pos] = val;
	}
	
	public T[] getArray() {
		return array;
	}
}

public class Test {
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray<>(Integer.class,10);
        Integer[] integers = myArray.getArray();
    }
}

结语

写到这里,我们总算是理顺了泛型的知识啦。这些知识还是有点难理解的,主要是一些语法规则我们需要去遵守,大家可以通过看源码的方式对照学习!

最后,如果你觉得本文对你有帮助的话,就请点个赞支持一下博主吧!如果文中有任何不对或者疑惑的地方,希望不吝赐教。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值