**Java总结篇系列:Java泛型
一. 泛型概念的提出(为什么须要泛型)?
首先,咱们看下上面这段简短的代码:
1 public class GenericTest { 2 3 public static void main(String[] args) { 4 List list = new ArrayList(); 5 list.add("qqyumidi"); 6 list.add("corn"); 7 list.add(100); 8 9 for (int i = 0; i < list.size(); i++) {10 String name = (String) list.get(i); // 111 System.out.println("name:" + name);12 }13 }14 }
定义了一个List类型的汇合,先向其中退出了两个字符串类型的值,随后退出一个Integer类型的值。这是齐全容许的,因为此时list默认的类型为Object类型。在之后的循环中,因为遗记了之前在list中也退出了Integer类型的值或其余编码起因,很容易呈现相似于//1中的谬误。因为编译阶段失常,而运行时会呈现“java.lang.ClassCastException”异样。因而,导致此类谬误编码过程中不易发现。
在如上的编码过程中,咱们发现次要存在两个问题:
1.当咱们将一个对象放入汇合中,汇合不会记住此对象的类型,当再次从汇合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其自身类型。
2.因而,//1处取出汇合元素时须要人为的强制类型转化到具体的指标类型,且很容易呈现“java.lang.ClassCastException”异样。
那么有没有什么方法能够使汇合可能记住汇合内元素各类型,且可能达到只有编译时不呈现问题,运行时就不会呈现“java.lang.ClassCastException”异样呢?答案就是应用泛型。
二.什么是泛型?
泛型,即“参数化类型”。一提到参数,最相熟的就是定义方法时无形参,而后调用此办法时传递实参。那么参数化类型怎么了解呢?顾名思义,就是将类型由原来的具体的类型参数化,相似于办法中的变量参数,此时类型也定义成参数模式(能够称之为类型形参),而后在应用/调用时传入具体的类型(类型实参)。
看着如同有点简单,首先咱们看下下面那个例子采纳泛型的写法。
1 public class GenericTest { 2 3 public static void main(String[] args) { 4 / 5 List list = new ArrayList(); 6 list.add("qqyumidi"); 7 list.add("corn"); 8 list.add(100); 9 /10 11 List<String> list = new ArrayList<String>();12 list.add("qqyumidi");13 list.add("corn");14 //list.add(100); // 1 提醒编译谬误15 16 for (int i = 0; i < list.size(); i++) {17 String name = list.get(i); // 218 System.out.println("name:" + name);19 }20 }21 }
采纳泛型写法后,在//1处想退出一个Integer类型的对象时会呈现编译谬误,通过List<String>,间接限定了list汇合中只能含有String类型的元素,从而在//2处毋庸进行强制类型转换,因为此时,汇合可能记住元素的类型信息,编译器曾经可能确认它是String类型了。
联合下面的泛型定义,咱们晓得在List<String>中,String是类型实参,也就是说,相应的List接口中必定含有类型形参。且get()办法的返回后果也间接是此形参类型(也就是对应的传入的类型实参)。上面就来看看List接口的的具体定义:
1 public interface List<E> extends Collection<E> { 2 3 int size(); 4 5 boolean isEmpty(); 6 7 boolean contains(Object o); 8 9 Iterator<E> iterator();10 11 Object[] toArray();12 13 <T> T[] toArray(T[] a);14 15 boolean add(E e);16 17 boolean remove(Object o);18 19 boolean containsAll(Collection<?> c);20 21 boolean addAll(Collection<? extends E> c);22 23 boolean addAll(int index, Collection<? extends E> c);24 25 boolean removeAll(Collection<?> c);26 27 boolean retainAll(Collection<?> c);28 29 void clear();30 31 boolean equals(Object o);32 33 int hashCode();34 35 E get(int index);36 37 E set(int index, E element);38 39 void add(int index, E element);40 41 E remove(int index);42 43 int indexOf(Object o);44 45 int lastIndexOf(Object o);46 47 ListIterator<E> listIterator();48 49 ListIterator<E> listIterator(int index);50 51 List<E> subList(int fromIndex, int toIndex);52 }
咱们能够看到,在List接口中采纳泛型化定义之后,<E>中的E示意类型形参,能够接管具体的类型实参,并且此接口定义中,但凡呈现E的中央均示意雷同的承受自内部的类型实参。
天然的,ArrayList作为List接口的实现类,其定义模式是:
1 public class ArrayList<E> extends AbstractList<E> 2 implements List<E>, RandomAccess, Cloneable, java.io.Serializable { 3 4 public boolean add(E e) { 5 ensureCapacityInternal(size + 1); // Increments modCount!! 6 elementData[size++] = e; 7 return true; 8 } 9 10 public E get(int index) {11 rangeCheck(index);12 checkForComodification();13 return ArrayList.this.elementData(offset + index);14 }15 16 //...省略掉其余具体的定义过程17 18 }
由此,咱们从源代码角度明确了为什么//1处退出Integer类型对象编译谬误,且//2处get()到的类型间接就是String类型了。
三.自定义泛型接口、泛型类和泛型办法
从下面的内容中,大家曾经明确了泛型的具体运作过程。也晓得了接口、类和办法也都能够应用泛型去定义,以及相应的应用。是的,在具体应用时,能够分为泛型接口、泛型类和泛型办法。
自定义泛型接口、泛型类和泛型办法与上述Java源码中的List、ArrayList相似。如下,咱们看一个最简略的泛型类和办法定义:
1 public class GenericTest { 2 3 public static void main(String[] args) { 4 5 Box<String> name = new Box<String>("corn"); 6 System.out.println("name:" + name.getData()); 7 } 8 9 }10 11 class Box<T> {12 13 private T data;14 15 public Box() {16 17 }18 19 public Box(T data) {20 this.data = data;21 }22 23 public T getData() {24 return data;25 }26 27 }
在泛型接口、泛型类和泛型办法的定义过程中,咱们常见的如T、E、K、V等模式的参数罕用于示意泛型形参,因为接管来自内部应用时候传入的类型实参。那么对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?
1 public class GenericTest { 2 3 public static void main(String[] args) { 4 5 Box<String> name = new Box<String>("corn"); 6 Box<Integer> age = new Box<Integer>(712); 7 8 System.out.println("name class:" + name.getClass()); // com.qqyumidi.Box 9 System.out.println("age class:" + age.getClass()); // com.qqyumidi.Box10 System.out.println(name.getClass() == age.getClass()); // true11 12 }13 14 }
由此,咱们发现,在应用泛型类时,尽管传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最根本的类型(本实例中为Box),当然,在逻辑上咱们能够了解成多个不同的泛型类型。
究其原因,在于Java中的泛型这一概念提出的目标,导致其只是作用于代码编译阶段,在编译过程中,对于正确测验泛型后果后,会将泛型的相干信息擦出,也就是说,胜利编译过后的class文件中是不蕴含任何泛型信息的。泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是雷同的根本类型。
四.类型通配符
接着下面的论断,咱们晓得,Box<Number>和Box<Integer>实际上都是Box类型,当初须要持续探讨一个问题,那么在逻辑上,相似于Box<Number>和Box<Integer>是否能够看成具备父子关系的泛型类型呢?
为了弄清这个问题,咱们持续看下上面这个例子:
1 public class GenericTest { 2 3 public static void main(String[] args) { 4 5 Box<Number> name = new Box<Number>(99); 6 Box<Integer> age = new Box<Integer>(712); 7 8 getData(name); 9 10 //The method getData(Box<Number>) in the type GenericTest is 11 //not applicable for the arguments (Box<Integer>)12 getData(age); // 113 14 }15 16 public static void getData(Box<Number> data){17 System.out.println("data :" + data.getData());18 }19 20 }
咱们发现,在代码//1处呈现了谬误提示信息:The method getData(Box<Number>) in the t ype GenericTest is not applicable for the arguments (Box<Integer>)。显然,通过提示信息,咱们晓得Box<Number>在逻辑上不能视为Box<Integer>的父类。那么,起因何在呢?
1 public class GenericTest { 2 3 public static void main(String[] args) { 4 5 Box<Integer> a = new Box<Integer>(712); 6 Box<Number> b = a; // 1 7 Box<Float> f = new Box<Float>(3.14f); 8 b.setData(f); // 2 9 10 }11 12 public static void getData(Box<Number> data) {13 System.out.println("data :" + data.getData());14 }15 16 }17 18 class Box<T> {19 20 private T data;21 22 public Box() {23 24 }25 26 public Box(T data) {27 setData(data);28 }29 30 public T getData() {31 return data;32 }33 34 public void setData(T data) {35 this.data = data;36 }37 38 }
这个例子中,显然//1和//2处必定会呈现谬误提醒的。在此咱们能够应用反证法来进行阐明。
假如Box<Number>在逻辑上能够视为Box<Integer>的父类,那么//1和//2处将不会有谬误提醒了,那么问题就进去了,通过getData()办法取出数据时到底是什么类型呢?Integer? Float? 还是Number?且因为在编程过程中的程序不可控性,导致在必要的时候必须要进行类型判断,且进行强制类型转换。显然,这与泛型的理念矛盾,因而,在逻辑上Box<Number>不能视为Box<Integer>的父类。
好,那咱们回过头来持续看“类型通配符”中的第一个例子,咱们晓得其具体的谬误提醒的深层次起因了。那么如何解决呢?总部能再定义一个新的函数吧。这和Java中的多态理念显然是违反的,因而,咱们须要一个在逻辑上能够用来示意同时是Box<Integer>和Box<Number>的父类的一个援用类型,由此,类型通配符应运而生。
类型通配符个别是应用 ? 代替具体的类型实参。留神了,此处是类型实参,而不是类型形参!且Box<?>在逻辑上是Box<Integer>、Box<Number>...等所有Box<具体类型实参>的父类。由此,咱们仍然能够定义泛型办法,来实现此类需要。
1 public class GenericTest { 2 3 public static void main(String[] args) { 4 5 Box<String> name = new Box<String>("corn"); 6 Box<Integer> age = new Box<Integer>(712); 7 Box<Number> number = new Box<Number>(314); 8 9 getData(name);10 getData(age);11 getData(number);12 }13 14 public static void getData(Box<?> data) {15 System.out.println("data :" + data.getData());16 }17 18 }
有时候,咱们还可能听到类型通配符下限和类型通配符上限。具体有是怎么样的呢?
在下面的例子中,如果须要定义一个性能相似于getData()的办法,但对类型实参又有进一步的限度:只能是Number类及其子类。此时,须要用到类型通配符下限。
1 public class GenericTest { 2 3 public static void main(String[] args) { 4 5 Box<String> name = new Box<String>("corn"); 6 Box<Integer> age = new Box<Integer>(712); 7 Box<Number> number = new Box<Number>(314); 8 9 getData(name);10 getData(age);11 getData(number);12 13 //getUpperNumberData(name); // 114 getUpperNumberData(age); // 215 getUpperNumberData(number); // 316 }17 18 public static void getData(Box<?> data) {19 System.out.println("data :" + data.getData());20 }21 22 public static void getUpperNumberData(Box<? extends Number> data){23 System.out.println("data :" + data.getData());24 }25 26 }
此时,显然,在代码//1处调用将呈现谬误提醒,而//2 //3处调用失常。
类型通配符下限通过形如Box<? extends Number>模式定义,绝对应的,类型通配符上限为Box<? super Number>模式,其含意与类型通配符下限正好相同,在此不作过多论述了。
五.话外篇
本文中的例子次要是为了论述泛型中的一些思维而简略举出的,并不一定有着理论的可用性。另外,一提到泛型,置信大家用到最多的就是在汇合中,其实,在理论的编程过程中,本人能够应用泛型去简化开发,且能很好的保障代码品质。并且还要留神的一点是,Java中没有所谓的泛型数组一说。**