当前位置:   article > 正文

java数据结构——泛型篇_java泛型方法在字节码是什么样的

java泛型方法在字节码是什么样的

目录

一、泛型的概念

(一)什么是泛型

(二)为何引入泛型

(三)泛型在集合中的使用

二、泛型的使用(以下用代码进行讲述)

(一)泛型类

(二)泛型方法

(1)普通泛型方法

(2)静态泛型方法

(三)泛型接口

(四)泛型上界及其擦除机制

(1)泛型上界

(2)java泛型擦除机制

三、通配符

(一)通配符的上界

 (二)通配符的下界

 四、泛型总结    


一、泛型的概念

(一)什么是泛型

    在类和方法的使用中,一般只能使用具体的类型,基本类型亦或是自定义的类。但想要其适用于多种类型的代码编写。因此产生了泛型来处理这种刻板的限制对代码的束缚。简单的来讲,泛型就是适用于很多类型。从代码上讲,就是对类型实现了参数化。

(二)为何引入泛型

    想要在数据结构集合中适用于多种数据类型,但又不是所有的类型都能拿来存储,而是选取自己想要的数据类型进行存储,因此引入泛型,便能很好地处理这种情况。同时泛型的引入,能有效的检查编译中存在的错误数据,更好的体现了java语言的安全性。

  1. public class MyArrayList {
  2. private final int capacity = 10;
  3. private int usedSize;
  4. //Object[]数组能存储任何类型的元素
  5. private Object[] elem;
  6. public MyArrayList() {
  7. elem = new Object[capacity];
  8. }
  9. //增添元素
  10. public void add(Object val) {
  11. if(usedSize != capacity) {
  12. elem[usedSize++] = val;
  13. } else {
  14. System.out.println("容量溢出");
  15. }
  16. }
  17. //获取元素
  18. public Object get(int pos) {
  19. return elem[pos];
  20. }
  21. public static void main(String[] args) {
  22. //在自定义的集合中能够存储任何类型的元素,显然不是程序员想要的结果
  23. MyArrayList myArrayList = new MyArrayList();
  24. myArrayList.add("a");//字符串型
  25. myArrayList.add(1);//整型
  26. myArrayList.add(2.0);//double型
  27. //每次读取元素的时候都要强制类型转换
  28. String s = (String) myArrayList.get(0);
  29. Integer a = (Integer) myArrayList.get(1);
  30. }
  31. }

(三)泛型在集合中的使用

语法:

  1. class 泛型类名称<类型形参列表> {
  2. // 这里可以使用类型参数
  3. }
  4. class ClassName<T1, T2, ..., Tn> {}
  5. class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
  6. // 这里可以使用类型参数
  7. }
  8. class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
  9. // 可以只使用部分类型参数
  10. }

    为使上述问题得到解决,使用泛型对该自定义集合类进行改进。

  1. public class MyArrayList1<T> {
  2. private final int capacity = 10;
  3. private int usedSize;
  4. private T[] elem;
  5. public MyArrayList1() {
  6. //报错,不能创建一个泛型数组
  7. //elem = new T[capacity];
  8. //不报错,但是存在问题,此问题留到下文讲擦除机制时讲
  9. elem = (T[]) new Object[capacity];
  10. }
  11. //真正正确的创建泛型数组的方式--运用反射
  12. public MyArrayList1(Class<T> clazz,int capacity) {
  13. elem = (T[]) Array.newInstance(clazz,capacity);
  14. }
  15. //增添数据
  16. public void add(T val) {
  17. if(usedSize != capacity) {
  18. elem[usedSize++] = val;
  19. } else {
  20. System.out.println("容量溢出");
  21. }
  22. }
  23. //读取数据
  24. public T get(int pos) {
  25. return elem[pos];
  26. }
  27. public static void main(String[] args) {
  28. //使用泛型后自动进行编译检查和类型转换
  29. MyArrayList1<String> myArrayList = new MyArrayList1<String>();
  30. myArrayList.add("a");
  31. //报错
  32. //myArrayList.add(1);
  33. //myArrayList.add(2.0);
  34. //读取元素时不需要强制类型转换
  35. String s = myArrayList.get(0);
  36. }
  37. }

注意:

泛型实现数据类型参数化,传入的数据类型必须是基本数据类型的包装类。

二、泛型的使用(以下用代码进行讲述)

(一)泛型类

语法:

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

代码:

  1. //指定一个泛型类引用泛型类的对象
  2. ArrayList<String> list = new ArrayList<String>();//后面的String可以省略
  3. //自定义一个交换数据类
  4. class Swap<T> {}
  5. public static void main(String[] args) {
  6. Swap<String> swap = new Swap<>();
  7. }

(二)泛型方法

(1)普通泛型方法

语法:

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

代码:

  1. //定义交换两个数据值的类
  2. class Swap<T> {
  3. //普通泛型方法
  4. //(形式一)
  5. public void swap(T[] array, int i, int j) {
  6. T tmp = array[i];
  7. array[i] = array[j];
  8. array[j] = tmp;
  9. }
  10. //(形式二)
  11. public <T> void swap(T[] array, int i, int j) {
  12. T tmp = array[i];
  13. array[i] = array[j];
  14. array[j] = tmp;
  15. }
  16. }

(2)静态泛型方法

语法:

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

代码:

  1. //泛型的静态方法
  2. class Swap {
  3. public static<T> void swap(T[] array, int i, int j) {
  4. T tmp = array[i];
  5. array[i] = array[j];
  6. array[j] = tmp;
  7. }
  8. }

注:泛型静态方法不依赖于对象,所以不在class类后面添加泛型

(三)泛型接口

语法:

interface 泛型接口名<类型形参列表> {....}

代码:

  1. //在此用Comparable接口来举例
  2. //此接口适用于任何类型的比较
  3. interface Comparable<T> {
  4.         public int compareTo(T o);
  5. }

(四)泛型上界及其擦除机制

(1)泛型上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

语法:

class 泛型类名称<类型形参 extends 类型边界> { ... }

代码:

  1. //泛型上界为Number
  2. class MyArray<E extends Number> {
  3. public static void main(String[] args) {
  4. //创建对象时只能是Number或Number的子类
  5. MyArray<Integer> myArray1 = new MyArray<Integer>();
  6. MyArray<Double> myArray2 = new MyArray<Double>();
  7. //报错,String不是Number的子类
  8. MyArray<String> myArray = new MyArray<String>();
  9. }
  10. }

特殊实例:

  1. //自定义一个比较类
  2. //泛型方法
  3. class Alg1<T extends Comparable<T>> {
  4. public T findMax(T[] array) {
  5. T max = array[0];
  6. for (T elem: array) {
  7. if(elem.compareTo(max) > 0)
  8. max = elem;
  9. }
  10. return max;
  11. }
  12. }

注意:

    该代码块的上界是Comparable接口,当构造该类对象时,必须是实现了Comparable接口,基本数据类型的包装类都是实现了Comparable接口。

(2)java泛型擦除机制

    擦除机制是Java5用来实现泛型的技术。一般来说,在运行时阶段,Java编译器先执行类型检查,然后执行擦除或删除泛型信息。而具体化的泛型,与此正好相反。基于具体化泛型系统的类实现在运行时作为顶级实体,它在运行时保留了类型参数,而这给基于类型和反射性的语言提供了确定的操作。

通过查看编译生成的字节码文件,更好的理解java泛型擦除机制

(a)未定义上界类型,最终擦除到Object

源代码:

 生成的字节码文件:

 (b)定义上界类型为Number,最终擦除到Number

源代码:

 生成的字节码文件:

注意: 

不能创建泛型数组,会存在隐患,以下用代码进行演示。

  1. class MyArray<T> {
  2. private final int capacity = 10;
  3. private int usedSize;
  4. private T[] elem;
  5. public MyArrayList1() {
  6. //报错,不能创建一个泛型数组
  7. //elem = new T[capacity];
  8. //不报错,但是存在问题,编译的时候,替换为Object[]
  9. elem = (T[]) new Object[capacity];
  10. }
  11. //真正正确的创建泛型数组的方式--运用反射
  12. public MyArrayList1(Class<T> clazz,int capacity) {
  13. elem = (T[]) Array.newInstance(clazz,capacity);
  14. }
  15. public T getPos(int pos) {
  16. return this.array[pos];
  17. }
  18. public void setVal(int pos,T val) {
  19. this.array[pos] = val;
  20. }
  21. //编译时返回的类型是Object[]
  22. public T[] getArray() {
  23. return array;
  24. }
  25. public static void main(String[] args) {
  26. MyArray<Integer> myArray1 = new MyArray<>();
  27. //使用getArray()方法,返回的是bject[]类型的数组,不是不能直接用Integer[]来接受的
  28. //数组和泛型的区别是数组是在运行时检查错误,而泛型是在编译时检查错误,为此该语句段未报错
  29. Integer[] strings = myArray1.getArray();
  30. }
  31. }

运行结果:

 问题原因:

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

    返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Double等等类型,运行的时候,直接转给Integer类型的数组,编译器认为是不安全的。

    另外,即使对返回的数组进行强制类型转换为(Integer[])也不能改变其内部元素是其他的数据类型,运行时程序任然会报错。

正确的处理方法是运用反射来创建数组

  1. public class MyArrayList1<T> {
  2. private final int capacity = 10;
  3. private int usedSize;
  4. private T[] elem;
  5. //运用反射创建泛型数组
  6. public MyArrayList1(Class<T> clazz,int capacity) {
  7. elem = (T[]) Array.newInstance(clazz,capacity);
  8. }
  9. }

三、通配符

    通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Cat 是Animal 的子类,那么 List<Cat> 也应 该是 List<Animal> 的子类。但是经过泛型的擦除机制,最终两个都擦除到object,所以泛型是不支持这样的父子类关系的。

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

(一)通配符的上界

语法:

<? extends 上界 >
<? extends Animal > // 可以传入的实参类型是Animal或者Animal的子类

①搞清父子类关系:

<? extends Animal> 是 <Animal>及<Animal子类>的父类

<?> 是 <? extends Animal>的父类

  1. //构成父子类关系
  2. class Animal {
  3. }
  4. class Dog extends Animal {
  5. }
  6. class Dubianquan extends Dog {
  7. }

②父类引用子类对象:

  1. public class Zoon {
  2. public static void main(String[] args) {
  3. //<? extends Animalr> 是 <Animal>及<Animal子类>的父类
  4. ArrayList<? extends Animal> arrayList1 = new ArrayList<Animal>();
  5. ArrayList<? extends Animal> arrayList2 = new ArrayList<Dog>();
  6. ArrayList<? extends Animal> arrayList3 = new ArrayList<Dubianquan>();
  7. //<?> 是 <? extends Animalr>的父类
  8. ArrayList<?> arrayList = arrayList1;
  9. }
  10. }

③注意:

    通配符的上界只适合于读取数据,不适用于写入数据。因为通配符上界引用的是<Animal及其子类>的对象,但是不能确定到底是哪个子类,不能存储确定的子类类型,java在编译时会自动查错。

  1. class Animal {
  2. public String toString() {
  3. return "Animal :>";
  4. }
  5. }
  6. class Cat extends Animal {
  7. @Override
  8. public String toString() {
  9. return "Cat :>";
  10. }
  11. }
  12. class Dog extends Animal {
  13. @Override
  14. public String toString() {
  15. return "Dog :>";
  16. }
  17. }
  18. public class Zoon {
  19. //通配符的上界是不适用于写入的,只适合于读取
  20. public static void main0(String[] args) {
  21. ArrayList<? extends Animal> arrayList = new ArrayList<>();
  22. //报错
  23. //arrayList.add(new Animal());
  24. //arrayList.add(new Dog());
  25. //但是可以读取数据,用泛型的上界来读取子类的数据,属于向上转型
  26. Animal animal = arrayList.get(0);
  27. //也可以用Object,因为他是所有类的父类
  28. Object o = arrayList.get(0);
  29. }
  30. }

 (二)通配符的下界

语法:

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

①搞清父子类关系:

<? super Animal> 是 <Animal>及<Animal父类>的父类

<?> 是 <? super Animal>的父类

  1. //构成父子类关系
  2. class Animal {
  3. }
  4. class Dog extends Animal {
  5. }
  6. class Dubianquan extends Dog {
  7. }

②父类引用子类对象:

  1. public class Zoon {
  2. public static void main(String[] args) {
  3. //<? super Dog> 下界是Dog,引用的是<Dog及其父类>的对象
  4. //因为<? super Dog>的上界没有限制,所以一直可以到Object。
  5. //因此<? super Dog>是<任一Dog及其父类>的父类
  6. //符合父类引用子类对象的规则
  7. ArrayList<? super Dog> arrayList1 = new ArrayList<Animal>();
  8. ArrayList<? super Dog> arrayList2 = new ArrayList<Dog>();
  9. //报错,下界是Dog,不能引用下界以下的子类
  10. // ArrayList<? super Dog> arrayList3 = new ArrayList<Dubianquan>();
  11. //<?> 是 <? super Dog> 的父类
  12. ArrayList<?> arrayList = arrayList1;
  13. }
  14. }

③注意:

     通配符的下界只适合于写入数据,不适用于读取数据。因为通配符下界引用的是<Dog及其父类>的对象,但是不能确定读取到的是哪个父类类型,所以引用的类型不能确定,也就不能读取,但是Object是所有类的父类,非要读取的话可以用Object来接受。因为下界存储的内容都是下界以下的数据类型,所以适合写入数据。

  1. class Animal {
  2. @Override
  3. public String toString() {
  4. return "Animal{}";
  5. }
  6. }
  7. class Dog extends Animal {
  8. @Override
  9. public String toString() {
  10. return "Dog{}";
  11. }
  12. }
  13. class Dubianquan extends Dog {
  14. @Override
  15. public String toString() {
  16. return "Dubianquan{}";
  17. }
  18. }
  19. public class Zoon {
  20. public static void main(String[] args) {
  21. ArrayList<? super Dog> arrayList1 = new ArrayList<Animal>();
  22. ArrayList<? super Dog> arrayList2 = new ArrayList<Dog>();
  23. //报错,存储的数据是Dog及其子类的数据类型
  24. // arrayList2.add(new Animal());
  25. //添加的元素 是Dog或者Dog的子类
  26. arrayList2.add(new Dog());
  27. arrayList2.add(new Dubianquan());
  28. //ArrayList<? super Dog> arrayList2引用的是Dog及其父类对象
  29. //编译器会考虑到ArrayList<? super Dog> arrayList2引用的对象如果是 new ArrayList<Animal>();
  30. //那么它存储的数据可能会有Animal类型的数据,那么就不能用Dog来接受Animal类型的数据。
  31. // Dog dog = arrayList2.get(0);//报错
  32. // Animal animal = arrayList2.get(0);//报错 原理同上
  33. //但是Object是所有类的父类,可以使用其来读取数据
  34. Object o = arrayList2.get(0);
  35. }
  36. }

④拓展(运用通配符上下界存储并读取数据,实现多态)

  1. class Animal {
  2. @Override
  3. public String toString() {
  4. return "Animal{}";
  5. }
  6. }
  7. class Dog extends Animal {
  8. @Override
  9. public String toString() {
  10. return "Dog{}";
  11. }
  12. }
  13. class Dubianquan extends Dog {
  14. @Override
  15. public String toString() {
  16. return "Dubianquan{}";
  17. }
  18. }
  19. public class Zoon {
  20. public static void main(String[] args) {
  21. //通配符下界存储数据
  22. ArrayList<? super Dog> arrayList1 = new ArrayList<Dog>();
  23. arrayList1.add(new Dog());
  24. arrayList1.add(new Dubianquan());
  25. //要用通配符下界读取数据,就要用Object来接受
  26. for (Object O: arrayList1) {
  27. System.out.println(O);
  28. }
  29. //利用通配符上界来读取数据
  30. ArrayList<? extends Animal> arrayList = (ArrayList<? extends Animal>) arrayList1;
  31. //因为上述存储的数据类型都是Animal的子类,所以可以用Animal来接受
  32. for (Animal a: arrayList) {
  33. System.out.println(a);
  34. }
  35. }
  36. }

结果显示:

 四、泛型总结    

①归根结底java语言的安全性能:使得程序具有更好的可读性和安全性。泛型是编译时检查错误,java的数组是运行时检查错误。因此不能用泛型直接创建数组,要用反射来创建泛型数组。

②泛型有上界但是没有下界

③类型构成的父子类关系,对泛型来说,泛型因为有擦除机制,所以没有父子类关系。而通配符就是解决此类问题。

④搞清通配符上下界的父子类关系,通配符上界适合读取数据,通配符下界适合写入数据。通配符的解决了泛型不能构成父子类的关系,因此可以通过泛型来实现多态读取和写入数据。

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

闽ICP备14008679号