当前位置:   article > 正文

数据结构之泛型总结_什么数据有泛型

什么数据有泛型

目录

一、泛型类的引出

二、泛型类的定义和使用

三、泛型的编译

四、泛型的上界

五、泛型方法

六、通配符

1.什么是通配符:

2.通配符可以解决的泛型问题:

3.通配符的上界

4.通配符的下界


一、泛型类的引出

1.什么是泛型

一般的类和方法,只能使用具体的类型 : 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。泛型是在JDK1.5 引入的新的语法,通俗讲,泛型: 就是适用于许多类型 。从代码上讲,就是对类型实现了参数化。
2. 对于泛型的引入和初步认识,我在“数据结构之List”中已经总结过,这里不再重复啰嗦
但是我们要知道一件事情: 泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译 器去做检查。 此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

二、泛型类的定义和使用

1.泛型类的定义

  1. class 泛型类名称<类型形参列表> {
  2. // 这里可以使用类型参数
  3. }
  1. class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
  2. // 这里可以使用类型参数
  3. }

举个例子:

  1. class MyArray<T> {
  2. public T[] array = (T[])new Object[10];//这样写也不好,我们一会儿进行解释
  3. public T getPos(int pos) {
  4. return this.array[pos];
  5. }
  6. public void setVal(int pos,T val) {
  7. this.array[pos] = val;
  8. }
  9. }
  10. public class TestDemo {
  11. public static void main(String[] args) {
  12. MyArray<Integer> myArray = new MyArray<>();//第12行代码
  13. myArray.setVal(0,10);
  14. myArray.setVal(1,12);
  15. int ret = myArray.getPos(1);
  16. System.out.println(ret);
  17. //myArray.setVal(2,"bit");//代码编译报错,此时因为在注释2处指定类当前的类型,此时在注释4处,编译器会在存放元素的时候帮助我们进行类型检查。
  18. }
  19. }

 (1)类名后的 <T> 代表占位符,表示当前类是一个泛型类,类型形参一般使用一个大写字母表示,常用的名称有:

  • E 表示 Element
  • K 表示 Key
  • V 表示 Value
  • N 表示 Number
  • T 表示 Type
  • S, U, V 等等 - 第二、第三、第四个类型

(2)不能new泛型类型的数组

即:T[] ts = new T[5];//error

(3)第12行代码处:类型后加入 <Integer> 指定当前类型。泛型只能接受类,所有的基本数据类型必须使用包装类!所以<int>error!

2.泛型类的使用

  1. 泛型类<类型实参> 变量名; // 定义一个泛型类引用
  2. new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

注意:当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写,如:

MyArray<Integer> list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 String

3.裸类型(Raw Type)

裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型,如:
MyArray list = new MyArray();

注意:我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制

三、泛型的编译

在Powershell窗口中查看“二”中的代码是如何编译的,如图所示:

 通过命令:javap -c 查看字节码文件,所有的T都是Object

在编译的过程当中,将所有的 T 替换为 Object 这种机制,我们称为: 擦除机制 。 Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。(即泛型只存在于编译的时候)

那么现在问题来了,为什么T[] ts = new T[5]; 是不对的,编译的时候替换为Object,不是相当于Object[] ts = new Object[5]吗?

如下代码所示:

  1. class MyArray<T> {
  2. //public T[] objects = new T[10];//error
  3. public T[] objects = (T[])new Object[10];//理论上说这样也是错的
  4. public void set(int pos, T val){
  5. objects[pos] = val;
  6. }
  7. public T get(int pos){
  8. return objects[pos];
  9. }
  10. public T[] getArray(){
  11. return objects;
  12. }
  13. }
  14. public class TestDemo {
  15. public static void main(String[] args) {
  16. MyArray<String> myArray = new MyArray<>();
  17. String[] ret = myArray.getArray();//第20行代码
  18. }
  19. }
编译并运行该代码,输出如下:(编译没有报错,运行时报错了)

 数组和泛型之间的一个重要区别是它们如何强制执行类型检查。具体来说,数组在运行时存储和检查类型信息。然而,泛型在编译时检查类型错误。

也就是说返回的 Object 数组里面,可能存放的是任何的数据类型,可能是 String ,可能是 Person ,运行的时候,直接转给String类型的数组,编译器认为是不安全的。(所以可以把第20行的String[]换成Object[])
我们可以通过反射来new一个泛型数组,我会在后续博客中总结,这里简单的写一下如何new这么的一个泛型数组
  1. import java.lang.reflect.Array;
  2. class MyArray<T> {
  3. public T[] array;
  4. public MyArray() {}
  5. public MyArray(Class<T> clazz, int capacity) {
  6. array = (T[])Array.newInstance(clazz, capacity);
  7. }
  8. public T getPos(int pos) {
  9. return this.array[pos];
  10. }
  11. public void setVal(int pos,T val) {
  12. this.array[pos] = val;
  13. }
  14. public T[] getArray() {
  15. return array;
  16. }
  17. }
  18. public static void main(String[] args) {
  19. MyArray<Integer> myArray1 = new MyArray<>(Integer.class,10);
  20. Integer[] integers = myArray1.getArray();
  21. }

四、泛型的上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
  1. class 泛型类名称<类型形参 extends 类型边界> {
  2. ...
  3. }

例如:

  1. class MyArray<T extends Number> {//T只能是Number或者Number的子类
  2. public T[] objects = (T[])new Object[10];
  3. public void set(int pos, T val){
  4. objects[pos] = val;
  5. }
  6. public T get(int pos){
  7. return objects[pos];
  8. }
  9. public T[] getArray(){
  10. return objects;
  11. }
  12. }
  13. public class TestDemo {
  14. public static void main(String[] args) {
  15. MyArray<String> myArray = new MyArray<>();//第18行代码
  16. MyArray<Integer> myArray2 = new MyArray<>();
  17. }
  18. }

第18行代码从编译开始就出错了,因为String 不是 Number 的子类型,但是Integer是。

注意:

(1)泛型只有上界,没有下界

(2)没有指定边界时,默认边界为Object

  1. class MyArray<T>{//默认边界为Object
  2. ......
  3. }

一个比较复杂的例子:写一个泛型类,求出数组当中的最大值

  1. class Alg<T extends Comparable<T>>{//此时传入的T一定要实现该接口 此处不是继承
  2. public T findMax(T[] array){
  3. T max = array[0];
  4. for(int i = 1; i < array.length; i++){
  5. if(max.compareTo(array[i]) < 0){//①max<array[i]//error 泛型传入的类型均为类类型,不能直接用><=比较
  6. //②也不能用equals方法,它只能比较true和false,比较不了大小关系。两个引用比较大小,要用CompareTo方法
  7. max = array[i];
  8. }
  9. }
  10. return max;
  11. }
  12. }
  13. public class TestDemo {
  14. public static void main(String[] args) {
  15. Alg<Integer> alg = new Alg<>();
  16. Integer[] array = {1,22,3,4};
  17. System.out.println(alg.findMax(array));
  18. }
  19. }

编译并运行该代码,输出如下:

22

注意Integer是实现了Comparable<>接口的,如图所示:

该代码有一个不好的地方,我们每次想调用findMax方法时,必须要new一个对象;我们对其进行优化一下

五、泛型方法

1.

  1. 方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) {
  2. ...
  3. }

优化如下:

  1. class Alg2/*<T extends Comparable<T>>*/{// "/* */"的这一部分写和不写都是一样的
  2. public static<T extends Comparable<T>> T findMax(T[] array){//我们发现当我们加一个static后,整个代码都报错了,这是因为加了static后,我们的方法不再依赖
  3. //于对象,我们在使用该方法时不再new对象,但是我们的泛型传参在new对象时传参,所以如果加了一个static的
  4. //的话,相当于没有传参;因此我们在static处加上<T>,但是要用compareTo方法,还是要实现Comparable接口
  5. T max = array[0];
  6. for(int i = 1; i < array.length; i++){
  7. if(max.compareTo(array[i]) < 0){
  8. max = array[i];
  9. }
  10. }
  11. return max;
  12. }
  13. }
  14. public class TestDemo {
  15. public static void main(String[] args) {
  16. Integer[] array = {1,22,3,4};
  17. System.out.println(alg./*<Integer>*/findMax(array));//"/* */"的这一部分写和不写都是一样的,它会通过array的类型来推导方法中的T是什么类型【类型推导】
  18. }
  19. }

2.泛型中的父子类关系

  1. public class MyArrayList<E> {
  2. ...
  3. }
  4. // MyArrayList<Object> Object不是 MyArrayList<Number> 中的Number的父类类型
  5. // MyArrayList<Number> Number也不是 MyArrayList<Integer> 的Integer父类类型

因为在编译的时候它们都被擦除了

举个例子:

  1. class Alg<T extends Comparable<T>>{
  2. public T findMax(T[] array){
  3. T max = array[0];
  4. for(int i = 1; i < array.length; i++){
  5. if(max.compareTo(array[i]) < 0){
  6. max = array[i];
  7. }
  8. }
  9. return max;
  10. }
  11. }
  12. public class TestDemo {
  13. public static void main(String[] args) {
  14. Alg<Integer> alg1 = new Alg<>();
  15. Alg<Integer> alg2 = new Alg<>();
  16. System.out.println(alg1);
  17. System.out.println(alg2);
  18. }
  19. }

编译并运行该代码,输出如下:

我们发现打印的内容中不存在<>,这说明它们被擦除了

六、通配符

1.什么是通配符:

? 用于在泛型的使用,即为通配符

2.通配符可以解决的泛型问题:

通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Student Person 的子类,那么 List<Student> 也应该是 List<Person> 的子类。但是泛型是不支持这样的父子类关系的

泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围( 或者我们可以这样理解:泛型T就像是个变量,等着你将来传一个具体的类型,而通配符则是一种规定, 规定你能传哪些参数 ),如下代码所示:
  1. class Alg3{
  2. public static<T> void print(ArrayList<T> list){
  3. for(T x:list){
  4. System.out.println(x);
  5. }
  6. }
  7. //此时代码的参数是T,此时的T一定是将来指定的一个泛型参数
  8. public static<T> void print2(ArrayList<?> list){
  9. for(Object x:list){
  10. System.out.println(x);
  11. }
  12. }
  13. //代码中使用了统配符,和代码1相比,此时传入printList2的,具体是什么数据类型,我们是不清楚的。这就是通配符。
  14. }

3.通配符的上界

(Ⅰ)语法

与泛型上界类似,<? extends 上界>;如:<? extends Number>//可以传入的实参类型是Number或者Number的子类

举个例子:假设有如下关系:

Animal
Cat extends Animal
Dog extends Animal   根据该关系,写一个方法,打印一个存储了 Animal 或者 Animal 子类的 list
(1)
  1. public static void print(List<Animal> list) {
  2. ......
  3. }
这样不可以解决问题,因为 print 的参数类型是 List<Animal> list ,就不能接收 List<Cat> list
(2)
  1. public static <T extends Animal> void print2(List<T> list) {
  2. for (T animal : list) {
  3. System.out.println(animal);
  4. }
  5. }
此时 T 类型是 Animal 的子类或者自己。该方法可以实现
(3)
  1. public static void print3(List<? extends Animal> list) {
  2. for (Animal ani : list) {
  3. System.out.println(ani);//传过来是谁,就调用谁的toString方法 (发生了向上转型)
  4. }
  5. }
通配符实现,该方法也可以达到效果

区别如下:

①对于泛型实现的print2方法, <T extends Animal> 对T进行了限制,只能是Animal的子类 比如:传入Cat,那么类型就是Cat

②对于通配符实现的print3方法,首先不用再static后使用尖括号,其次相当于对Animal进行了规定,允许你传入Animal 的子类。具体哪个子类,此时并不清楚。比如:传入了Cat,实际上声明的类型是Animal,使用多态才能调用Cat的toString方法

(Ⅱ)通配符上界的父子类关系

// 需要使用通配符来确定父子类型
MyArrayList<? extends Number> 是 MyArrayList <Integer>或者 MyArrayList<Double>的父类类型
MyArrayList<?> 是 MyArrayList<? extends Number> 的父类型

(Ⅲ)通配符的上界的特点

通配符的上界不适合写入数据

  1. public class TestDemo {
  2. public static void main(String[] args) {
  3. ArrayList<Integer> arrayList1 = new ArrayList<>();
  4. ArrayList<Double> arrayList2 = new ArrayList<>();
  5. List<? extends Number> list = arrayList1;
  6. list.add(0,1);
  7. list.add(1,10.9);
  8. }
  9. }

这样子写编译时是不会通过的,因为此时list的引用的子类对象有很多,再添加的时候,任何子类型都可以,为了安全,java不让这样进行添加操作。

(添加任何类型的数据都不可以,无法确定到底是哪种类型。)

但是通配符的上界适合读取数据:

  1. public class TestDemo {
  2. public static void main(String[] args) {
  3. ArrayList<Integer> arrayList1 = new ArrayList<>();
  4. ArrayList<Double> arrayList2 = new ArrayList<>();
  5. List<? extends Number> list = arrayList1;
  6. Number o = list.get(0);
  7. //Integer i = list.get(1);//error 类型太多,不一定是Integer(你怎么知道,获取的就是Integer呢?),所以读取时一定要用Number类型来接收
  8. }
  9. }

4.通配符的下界

(Ⅰ)语法

  1. <? super 下界>
  2. <? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型

(Ⅱ)通配符下界的父子类关系

MyArrayList<? super Integer> 是 MyArrayList<Integer>的父类类型
MyArrayList<?> 是 MyArrayList<? super Integer>的父类类型
(Ⅲ)通配符的下界的特点
  1. public class TestDemo {
  2. public static void main(String[] args) {
  3. ArrayList<? super Person> arrayList1 = new ArrayList<>();
  4. //ArrayList<? super Person> arrayList2 = new ArrayList<>(student);//error arrayList2只能引用Person或者Person父类类型的list
  5. arrayList1.add(new Person());//为添加元素的时候,我们知道list引用的对象肯定是Person或者Person的父类的集合,我们能够确定此时存储元素的最小粒度比Person小的都可以。
  6. //Student s = arrayList1.get(0);//error 读取的时候,我们不知道是读取到的是哪个子类
  7. Object s2 = arrayList1.get(0);//可以
  8. //Person s3 = arrayList1.get(0);//error
  9. }
  10. }

通配符的下界适合写入数据,不适合读取数据

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/541826
推荐阅读
相关标签
  

闽ICP备14008679号