泛型基础

143 阅读10分钟

泛型

1.泛型是什么

用来 规定 一个类、接口或方法所能 接受 的数据的类型.

就像在声明方法时指定参数一样, 我们在声明一个类, 接口或方法时, 也可以指定其"类型参数", 也就是泛型.将类型当作参数传递给类或者方法。

2.泛型的原理

类型擦除:泛型是在编译器这个 层次 实现的。在Java生成 字节码 的时候不包含泛型的类型信息。使用泛型时加上类型参数,编译器会在编译的时候去掉,这个过程叫做类型擦除

原始类型:原始类型(raw type )是擦除了泛型之后字节码中的类型变量。无论何时定义泛型,它的原始类型会自动提供。

public class Test {  
    public void addList (List<String> stringList){
    }  
    public void addList (List<Integer> intList) {
   }  
 } 
//Method addList(List) has the same erasure addList(List) as another method in type Test
//错误原因 这两个方法的签名在进行了类型擦除后均为addList(List<E>)
  • List、List擦除后的类型为List。
  • List[]、List[] 擦除后的类型为 List[]。
  • List<? extends E>、List<? super E>擦除后的类型为List。
  • List<T extends Serialzable & Cloneable>擦除后类型为List。

Pair泛型类

/**
 * Pair
 * @param <T>
 */
class Pair<T> {
    private T value;

    private T getValue() {
        return value;
    }

    private void setValue(T value) {
        this.value = value;
    }
}

Pair的原始类

/**
     * Pair的原始类型
     * @param <T>
     */
    class Pair{
        private Object value;

        private Object getValue() {
            return value;
        }

        private void setValue(Object value) {
            this.value = value;
        }
    }

编译器工作方式:由于泛型类型的擦除,JVM无法访问到泛型的类型信息,所以Java会在编译器通过 先检查代码中泛型的类型,然后再进行擦除,再进行编译。

//本来类型检查就是编译时完成的。new ArrayList()只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,我们使用它的引用Str调用方法,所以Str引用能完成范型的检查
ArrayList<String> str = new ArrayList();

ArrayList string = new Arraylist<String>();//这样就没有效果

 public class Test10 {  
        public static void main(String[] args) {  
              
            //  
            ArrayList<String> arrayList1=new ArrayList();  
            arrayList1.add("1");//编译通过  
            arrayList1.add(1);//编译错误  
            String str1=arrayList1.get(0);//返回类型就是String  
              
            ArrayList arrayList2=new ArrayList<String>();  
            arrayList2.add("1");//编译通过  
            arrayList2.add(1);//编译通过  
            Object object=arrayList2.get(0);//返回类型就是Object  
              ÷
            new ArrayList<String>().add("11");//编译通过  
            new ArrayList<String>().add(22);//编译错误  
            String string=new ArrayList<String>().get(0);//返回类型就是String  
        }  
    } 

通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

ArrayList<Object> arrayList1=new ArrayList<Object>();  
      arrayList1.add(new Object());  
      arrayList1.add(new Object());  
      ArrayList<String> arrayList2=arrayList1;//编译错误  

实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用arrayList2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的。),可是它里面实际上已经被我们存放了Object类型的对象,这样,就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。

ArrayList<String> arrayList1=new ArrayList<String>();  
      arrayList1.add(new String());  
      arrayList1.add(new String());  
      ArrayList<Object> arrayList2=arrayList1;//编译错误  

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

自动类型转换

既然范型变量最后都被替换成了原始类型,那么,为什么我们获取的时候,不需要进行强制类型转换?

以ArrayList的get()方法为例:

public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
    } 

可以看出,在return之前,会根据泛型变量进行强转。

3.泛型的好处

1.提高安全性:将运行期的错误转到编译器。如果对象所赋的值不符合范型要求,编译就会报错。

2.避免强转:加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,使得代码更加优雅

比如我们在使用List时, 如果我们不使用泛型, 当从List中取出元素时, 其类型会是默认的Object, 我们必须将其向下转型为String才能使用

4.泛型中的通配符

4.1作用

规定只允许某一部分类作为泛型

4.2分类

1.无边界通配符(<?>):   无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。

2.固定上边界通配符(<? extend E>):   使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。

要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类.

3.固定下边界通配符(<? super E>):   使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据。

要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界.

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}
public static void main(String[] args) {
    List<Object> list1 = new ArrayList<>();
    addNumbers(list1);
    System.out.println(list1);
    List<Number> list2 = new ArrayList<>();
    addNumbers(list2);
    System.out.println(list2);
    List<Double> list3 = new ArrayList<>();
 // addNumbers(list3); // 编译报错 
}
  • List、List擦除后的类型为List。
  • List[]、List[] 擦除后的类型为 List[]。
  • List<? extends E>、List<? super E>擦除后的类型为List。
  • List<T extends Serialzable & Cloneable>擦除后类型为List。

Pair泛型类

/**
 * Pair
 * @param <T>
 */
class Pair<T> {
    private T value;

    private T getValue() {
        return value;
    }

    private void setValue(T value) {
        this.value = value;
    }
}

Pair的原始类

/**
     * Pair的原始类型
     * @param <T>
     */
    class Pair{
        private Object value;

        private Object getValue() {
            return value;
        }

        private void setValue(Object value) {
            this.value = value;
        }
    }

编译器工作方式:由于泛型类型的擦除,JVM无法访问到泛型的类型信息,所以Java会在编译器通过 先检查代码中泛型的类型,然后再进行擦除,再进行编译。

//本来类型检查就是编译时完成的。new ArrayList()只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,我们使用它的引用Str调用方法,所以Str引用能完成范型的检查
ArrayList<String> str = new ArrayList();

ArrayList string = new Arraylist<String>();//这样就没有效果

 public class Test10 {  
        public static void main(String[] args) {  
              
            //  
            ArrayList<String> arrayList1=new ArrayList();  
            arrayList1.add("1");//编译通过  
            arrayList1.add(1);//编译错误  
            String str1=arrayList1.get(0);//返回类型就是String  
              
            ArrayList arrayList2=new ArrayList<String>();  
            arrayList2.add("1");//编译通过  
            arrayList2.add(1);//编译通过  
            Object object=arrayList2.get(0);//返回类型就是Object  
              ÷
            new ArrayList<String>().add("11");//编译通过  
            new ArrayList<String>().add(22);//编译错误  
            String string=new ArrayList<String>().get(0);//返回类型就是String  
        }  
    } 

通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

ArrayList<Object> arrayList1=new ArrayList<Object>();  
      arrayList1.add(new Object());  
      arrayList1.add(new Object());  
      ArrayList<String> arrayList2=arrayList1;//编译错误  

实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用arrayList2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的。),可是它里面实际上已经被我们存放了Object类型的对象,这样,就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。

ArrayList<String> arrayList1=new ArrayList<String>();  
      arrayList1.add(new String());  
      arrayList1.add(new String());  
      ArrayList<Object> arrayList2=arrayList1;//编译错误  

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

自动类型转换

既然范型变量最后都被替换成了原始类型,那么,为什么我们获取的时候,不需要进行强制类型转换?

以ArrayList的get()方法为例:

public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
    } 

可以看出,在return之前,会根据泛型变量进行强转。

3.泛型的好处

1.提高安全性:将运行期的错误转到编译器。如果对象所赋的值不符合范型要求,编译就会报错。

2.避免强转:加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,使得代码更加优雅

比如我们在使用List时, 如果我们不使用泛型, 当从List中取出元素时, 其类型会是默认的Object, 我们必须将其向下转型为String才能使用

4.泛型中的通配符

4.1作用

规定只允许某一部分类作为泛型

4.2分类

1.无边界通配符(<?>):   无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。

2.固定上边界通配符(<? extend E>):   使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。

要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类.

3.固定下边界通配符(<? super E>):   使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据。

要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界.


public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}
public static void main(String[] args) {
    List<Object> list1 = new ArrayList<>();
    addNumbers(list1);
    System.out.println(list1);
    List<Number> list2 = new ArrayList<>();
    addNumbers(list2);
    System.out.println(list2);
    List<Double> list3 = new ArrayList<>();
 // addNumbers(list3); // 编译报错 
}