泛型

1.泛型的由来

       Java泛型是JDK5中引入的新特性,提供了一种编译时安全检测机制,允许在定义类/接口/方法的时候使用类型参数,声明的类型参数在使用时用具体的类型来替换,允许程序员在编译时检测到非法类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

2.泛型的意义

       在没有泛型之前,是要做显式的强制类型转换。针对不同参数得写出几个对应的方法。使用泛型好处是让编译器保留参数类型信息,执行类型检查,执行类型转换操作,同时省掉了显式的强制类型转换,保证了这些类型转换的正确性,保证类型安全,并实现了更为通用的算法。在编译时就能发现插入类型的错误,也不需要手动转换类型。

3.泛型的种类

泛型类

       泛型类声明和非泛型类的声明类似,除了在类名后面添加类型参数声明部分。泛型类的类型参数声明包含一个或多个类型参数,参数之间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符,因为他们接受一个或者多个参数,这些类被称为参数化的类或者参数化的类型。

public class GenericTest{
    public static void main(String[] args) {
        Box name = new Box("corn");
        Box age = new Box(712);
        System.out.println("name class:" + name.getClass());
        System.out.println("age class:" + age.getClass());
        System.out.println(name.getClass() == age.getClass());
    }
 
    static class Box {
        private T data;
        public Box() {}
        public Box(T data) {
            this.data = data;
        }
        public T getData() {
            return data;
        }
    }
}

      在使用泛型类时候,虽然传入了不同的泛型实参,但没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存中只有一个,即为原来的最初类型,在逻辑中可以理解为多个不同的泛型类型。究其原因是泛型只是作用于代码编译阶段,在编译过程中正确检测出了泛型信息,会将泛型的相关信息擦除,即成功编译后的class文件不包含任何泛型信息,泛型信息不会进入运行时阶段。

泛型接口

      在接口处使用泛型,使用比较方便

public class GenericTest{
    public static void main(String args[]) {
        generic i = null;
        i = new genericImpl<>("it");
        System.out.println("Length Of String : " + i.getVar().length());
    }
}
 
interface generic {
    T getVar();
}
 
class genericImpl implements generic {
    private T var;
 
    public genericImpl(T var) {
        this.setVar(var);
    }
 
    public void setVar(T var) {
        this.var = var;
    }
 
    public T getVar() {
        return this.var;
    }
}

泛型方法

      一个基本原则是无论何时,只要你能做到,你就应该尽量使用泛型方法。

public class GenericTest{
    public static void main(String args[]) {
        out("findingsea");
        out(123);
        out(11.11);
        out(true);
    }
 
    public static  void out(T t) {
        System.out.println(t);
    }
}

       可以看到方法的参数彻底泛化了,原来需要自己对类型进行判断和处理,现在交给编译器来做了。这样在定义方法的时候,大大增加了编程的灵活性。

4.类型擦除

      类型擦除指的是通过类型参数合并,编译器只为泛型类型生成一份字节码,将泛型类型实例关联到同一份字节码上。在生成的Java字节码中不包含泛型类型信息的,类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且在必要的时候添加类型检查和类型转换的方法。具体过程为:

      1.检查代码中的泛型类型

      2.将所有泛型参数用它们的原始类型替换

      3.编译代码

      需要注意的是:泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的,但是类型信息已经被擦除了。泛型类并没有自己独有的Class类对象。静态变量是被泛型类的所有实例所共享的。

List list = new ArrayList();
Map map = new HashMap();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
  
/* Output
[E]
[K, V]
*/
      这种代码的输出,我们期待 是得到泛型参数的类型,但是实际上我们只得到了一堆占位符。
public class Main {
    public T[] makeArray() {
        // error: Type parameter 'T' cannot be instantiated directly
        return new T[5];
    }
}
      这种出现错误是因为我们无法在泛型内部创建一个T类型的数组,因为T仅仅是个占位符,并没有真正的类型信息,实际上除了new表达式外,instanceof操作和转型是在泛型内部无法使用的,而这个原因是对编译器对类型信息进行了擦除。
public class Main {
 
    private T t;
 
    public void set(T t) {
        this.t = t;
    }
 
    public T get() {
        return t;
    }
 
    public static void main(String[] args) {
        Main m = new Main();
        m.set("findingsea");
        String s = m.get();
        System.out.println(s);
    }
}
 
/* Output
findingsea
*/
     虽然编译器无法知道T的类型信息,但是编译器可以保证的是:内部的一致性。
class GenericTest
    public List fillList(T t, int size) {
        List list = new ArrayList();
        for (int i = 0; i < size; i++) {
            list.add(t);
        }
        return list;
    }
 
 
    public static void main(String[] args) throws Exception{
 
        GenericTest m = new GenericTest();
        List list1 = m.fillList("findingsea", 5);
        System.out.println(list1.toString());
    }
}

     输出为[findingsea, findingsea, findingsea, findingsea, findingsea],同样保证了一致性。

5.类型擦除带来的问题以及解决

public static  void main(String[] args) { 
        ArrayList arrayList=new ArrayList(); 
        arrayList.add("123"); 
        arrayList.add(123);//编译错误 
    }

      泛型变量Integer会被擦除为Object,为什么ArrayList不能存别的类型,在类型擦除的情况下,如何保证我们加入的是泛型变量指定的类型?因为Java编译器是先检查代码中泛型的类型,再进行类型擦除,然后进行编译。

      泛型中的引用传递问题:

ArrayList arrayList1=new ArrayList(); 
        arrayList1.add("1");//编译通过 
        arrayList1.add(1);//编译错误 
        String str1=arrayList1.get(0);//返回类型就是String 
ArrayList arrayList2=new ArrayList();
arrayList2.add("1");//编译通过
arrayList2.add(1);//编译通过
Object object=arrayList2.get(0);//返回类型就是Object
       类型检查是针对引用的,谁是引用,就对这个引用调用的方法进行类型检测,而无关它真正引用的对象。  
ArrayList arrayList1=new ArrayList(); 
          arrayList1.add(new Object()); 
          arrayList1.add(new Object()); 
          ArrayList arrayList2=arrayList1;//编译错误
ArrayList arrayList1=new ArrayList(); 
          arrayList1.add(new String()); 
          arrayList1.add(new String()); 
          ArrayList arrayList2=arrayList1;//编译错误 
          
  

以上两种情况都会出错,因为泛型就是为了解决类型转换问题,但是这种强转违背了泛型设计的初衷,所以在Java中是禁止这种行为的。

6.通配符

     类型通配符一般是使用?来代替具体的类型实参。这个地方是类型实参,而不是类型形参。通配符有三种。

无限定通配符:形式

public class GenericTest{
    public static void main(String[] args) throws Exception{
        List listInteger =new ArrayList();
        List listString =new ArrayList();
        printCollection(listInteger);
        printCollection(listString);
    }
    public static void printCollection(Collection collection){
        for(Object obj:collection){
            System.out.println(obj);
        }
    }
}

上述代码中,如果Collection中是Object,会出现编译失败的情况。使用?通配符可以引用其他各种参数化的类型,通配符定义的变量的主要用作引用,可以调用与参数化无关的方法,比如Collection.size()。不能调用与参数化有关的方法,比如Collection.add(),因为程序调用这个方法时候传入的参数不知道是什么类型的。

通配符上限:

     Vector x = new Vector<类型2>();

      类型1指定一个数据类型,那么类型2就只能是类型1或者是类型1的子类。例:Vector x = new Vector();

通配符下限:

      Vector x = new Vector<类型2>();

     类型1指定一个数据类型,那么类型2就只能是类型1或者是类型1的父类。例:Vector x = new Vector();

通配符使用总结:

     如果你想从一个数据类型中获取数据,使用? extends,如果你想把对象写进一个数据结构里,使用? super通配符。

7.当泛型遇上重载

public static void method(List list) { 
    System.out.println("invoke method(List list)"); 
} 
   
public static void method(List list) { 
    System.out.println("invoke method(List list)"); 
}
      这段代码无法编译,因为类型擦除使得两个方法特征签名变得一模一样。

class Pair { 
    private T value; 
    public T getValue() { 
        return value; 
    } 
    public void setValue(T value) { 
        this.value = value; 
    } 
} 
//子类继承
class DateInter extends Pair { 
    @Override 
    public void setValue(Date value) { 
        super.setValue(value); 
    } 
    @Override 
    public Date getValue() { 
        return super.getValue(); 
    } 
} 
public static void main(String[] args) throws ClassNotFoundException { 
        DateInter dateInter=new DateInter(); 
        dateInter.setValue(new Date());                 
                dateInter.setValue(new Object());//编译错误 
 }

如果是重载,那么子类的两个重载方法,一个是Object类型,一个是Date类型。但是没有出现这样子类继承父类的Object类型参数的方法。所以这种情况下是重写而不是重载。

      原因是:虚拟机不能将泛型类型变成Date,只能类型擦除,将类型变为原始类型Object。

8.JVM如何理解泛型类

class SonBox extends Box{
    public void setData(String data){....}
}

JVM并不知道泛型,所有的泛型在编译阶段就已经处理成了普通类和方法。无论我们如何定义一个泛型类型,相应的都会有一个原始类型被自动提供。原始类型的名字就是擦除类型参数的泛型类型的名字。

     如果泛型类型的类型变量没有限定(),那么就用Object作为原始类型。

     如果有限定(),那么就用XClass为原始类型。

     如果有多个限定(),我们就用第一个边界的类型变量XClass1类作为原始类型。

     继承泛型类的多态麻烦,本来想覆盖父类中setData这个方法,但是事实上Box已经被擦除成了Box了,他的setData方法变成了setData(Object obj),这个时候自然无法覆盖父类的方法了。

     此时编译器会在调用一个桥方法,bridge method,而且桥方法调用的实际是子类字节setData(String)方法,即多态中方法覆盖是可以的。即JVM利用很巧妙的方法来避免了类型擦除和多态之间的冲突。

      引入桥方法,是编译器自动生成,而不是需要程序员自己去写代码的。但是另一个问题是,在getData中自己定义的为String getData(),编译器的桥方法为Object getData(),这两个方法的签名是一样的,编译器需要怎么去区别?

      方法签名只有方法名加上参数列表,我们绝对不能编写出方法签名相同的一个方法,JVM会用参数类型和返回类型来确定一个方法。 一旦编译器通过某种方式自己编译出方法签名一样的两个方法(只能编译器自己来创造这种奇迹,我们程序员却不能人为的编写这种代码)。JVM还是能够分清楚这些方法的,前提是需要返回类型不一样。 

      在JVM中不存在泛型,只有普通类和方法,而在编译阶段,所有泛型类的类型参数都会被Object或者限定的边界来替换。在继承泛型类型的时候,桥的生成是为了避免类型擦除带来的多态问题。

9.泛型类型中的方法冲突

public class Box{ 
      public boolean equals(T value){ 
            return (data.equals(value)); 
      } 
}

上述代码会报错,编译器显示方法冲突。子类方法要覆盖父类的方法必须要和父类方法有同样的签名,即方法名加上参数列表,必须保证子类访问权限大于父类访问权限,编译器发现equals方法,第一反应是没有覆盖父类Object的equals方法,如果编译器将这个方法覆盖的话,equals(T)变为equals(Object),基于开始没有确定覆写这个方法,编译器会出错。

10.注意事项

        1.Java中没有泛型数组的说法;

Box[] stringBoxes=new Box[10];
Box[] intBoxes=new Box[10];
  这种方法会指定编译器错误。假设泛型存在,就会有
Object[0]=stringBoxes[0]; Ok
Object[1]=intBoxes[0]; Ok

每次调用Object[]的元素都可能得到不同的结果,也许是字符串,也许是整型,这个是JVM无法预料的结果。即数组必须要牢记元素类型,但是泛型做不到这点。

 

      还是因为类型擦除,对于泛型而言,擦除降低了效率。如果要收集参数化类型对象,可以直接使用ArrayList。

      2.一个类不能实现同一个泛型接口的两种变体,因为类型擦除会使得两个变体变成相同接口。

      3.泛型通配符和自定义T类型的区别:

void test(List list);表示List里面装的某一类型,void test(List list);表示里面是任一类型。即通配符是调用与参数类型无关的方法,而T类型调用于参数类型相关的方法。前者里list中的元素都要一样,但后者只需要list中放入的是A的字类型就可以。

      4.当泛型中包含静态变量

public class StaticTest{
    public static void main(String[] args){
        GT gti = new GT();
        gti.var=1;
        GT gts = new GT();
        gts.var=2;
        System.out.println(gti.var);
    }
}
class GT{
    public static int var=0;
    public void nothing(T x){}
}

这个答案是2,因为经过类型擦除,所有泛型实例都关联到一份字节码上,泛型类所有静态变量共享。

      5.不能在catch中使用泛型变量

public static  void doWork(Class t){ 
        try{ 
            ... 
        }catch(T e){ //编译错误 
            ... 
        } 
   }
      因为泛型信息在编译的时候已经变成了原始类型,也就是说上面的T会变成原始的Throwable。
public static  void doWork(Class t){ 
        try{ 
            ... 
        }catch(T e){ //编译错误 
            ... 
        }catch(IndexOutOfBounds e){ 
        }                          
 }
        这种情况也会出错,因为类型捕获一定要子类在前,父类在后。Java为了避免异常捕获原则,禁止在catch字句中使用泛型变量。



你可能感兴趣的:(泛型)