Java篇 - 泛型的真谛

目录:

  1. 什么是泛型
  2. 泛型类
  3. 泛型接口
  4. 泛型通配符
  5. 泛型方法
  6. 泛型通配符的上下边界
  7. 泛型数组
  8. 泛型的类型擦除

 

 

1. 什么是泛型

 

  • 1.1 泛型的概念

泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,在用到的时候在指定具体的类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

泛型思想早在C++语言的模板(Templates)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父 类和类型强制转换两个特点的配合来实现类型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于 java.lang.Object,那Object转型为任何对象成都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个 Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多 ClassCastException的风险就会被转嫁到程序运行期之中。

 

  • 1.2 Java的伪泛型

 泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中 (Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,List<int>与 List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型。

Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList<int>与 ArrayList<String>就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。

 

  • 1.3 泛型的意义

使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类来说尤其有用。

泛型程序设计(Generic Programming)意味着编写的代码可以被很多不同类型的对象所重用。

 

 

2. 泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

 

  • 2.1 语法
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  }
}

 

  • 2.2 使用
public class TestGeneric {

    private static final class MyData<T> {

        private final T key;

        MyData(T key) {
            this.key = key;
        }

        public T getKey() {
            return key;
        }
    }

    public static void main(String[] args) {
        MyData<String> myData = new MyData<>("kuang");
        // 打印输出kuang
        System.out.println(myData.getKey());
    }
}

 

  • 2.3 注意事项

(1) 泛型的类型参数只能是类类型,不能是基本数据类型,如int, short, byte, long, char。

(2) 不能对确切的泛型类型使用instanceof操作,否则编译时会出错。

        // 不能是基本数据类型,否则编译期会报错
        MyData<int> myData1 = new MyData<int>(1);
        // 编译期会报 Illegal generic type for instanceof
        System.out.println(myData instanceof MyData<String>);

 

 

3. 泛型接口

泛型接口与泛型类的定义及使用基本相同,泛型接口常被用在各种类的生产器中。

 

  • 3.1 语法
public interface 接口名<T> {
    public T method();
}

 

  • 3.2 使用
    private interface Factory<T> {

        T newInstance();
    }

    private static final class Coder {

        final String name;
        final int age;
        final String lang;

        public Coder(String name, int age, String lang) {
            this.name = name;
            this.age = age;
            this.lang = lang;
        }
    }

    private static final class CoderFactory implements Factory<Coder> {

        @Override
        public Coder newInstance() {
            return new Coder("Kuang", 28, "Java");
        }
    }

    public static void main(String[] args) {
        Factory factory = new CoderFactory();
        Object object = factory.newInstance();
        Coder coder = (Coder) object;
        // 执行输出 Kuang - 28 - Java
        System.out.println(coder.name + " - " + coder.age + " - " + coder.lang);
    }

 

 

4. 泛型通配符

 

  • 4.1 通配符的概念和例子

我们知道IngeterNumber的一个子类,那么问题来了,在使用MyData<Number>作为形参的方法中,能否使用MyData<Ingeter>的实例传入呢?在逻辑上类似于MyData<Number>MyData<Ingeter>是否可以看成具有父子关系的泛型类型呢?

    private static void printMyDataKey(MyData<Number> myData) {
        System.out.println(myData.getKey());
    }

    public static void main(String[] args) {
        MyData<Integer> myData = new MyData<>(1);
        MyData<Number> myData1 = new MyData<>(2);
        // 这里将编译不过
        printMyDataKey(myData);
        printMyDataKey(myData1);
    }

改成这样就能执行了:

    private static void printMyDataKey(MyData<? extends Number> myData) {
        System.out.println(myData.getKey());
    }

类型通配符一般是使用'?'代替具体的类型实参,注意了,此处'?'是类型实参,而不是类型形参。

再直白点的意思就是,此处的'?'和Number、String、Integer一样都是一种实际的类型,可以把'?'看成所有类型的父类,是一种真实的类型。

可以解决当具体类型不确定的时候,这个通配符就是'?'。当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能,那么可以用'?'通配符来表未知类型。

 

  • 4.2 ?和T的区别与联系

?和T都表示不确定的类型,但是两者还是有区别的。

(1) T是行参,?是实参。

(2) 如果是T的话,函数里面可以对T进行操作。

    public static void printElement(ArrayList<?> al) {
        Iterator<?> it = al.iterator();
        while (it.hasNext()) {
            System.out.println(it.next().toString());
        }
    }

使用T:

    public static <T> void printElement(ArrayList<T> al) {
        Iterator<T> it = al.iterator();
        while (it.hasNext()) {
            T next = it.next();
            System.out.println(next.toString());
        }
    }

 

 

5. 泛型方法

上面的代码里也看到了泛型方法了,泛型方法,是在调用方法的时候指明泛型的具体类型。

 

  • 5.1 语法
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}

 

  • 5.2 使用
    private static final class MyData<T> {

        private final T key;

        MyData(T key) {
            this.key = key;
        }

        public T getKey() {
            return key;
        }
    }

    private static final class MyMethod {

        <T> void printMyDataKey(MyData<T> myData) {
            T key = myData.getKey();
            System.out.println(key);
        }

        <T> void printMyDataKeys(MyData<T>... myDatas) {
            for (MyData<T> myData : myDatas) {
                System.out.println(myData.getKey());
            }
        }
    }

    private static void printMyDataKey(MyData<? extends Number> myData) {
        System.out.println(myData.getKey());
    }

上面的例子可以看到普通泛型方法,静态泛型方法和可变参数泛型方法。

 

 

 

6. 泛型通配符的上下边界

 

  • ? 通配符类型。
  • <? extends T> 表示类型的上界,表示参数化类型的可能是T 或是 T的子类,最多为T,所以可以以T类型安全取出,但是不能存。
  • <? super T> 表示类型下界(Java Core中叫超类型限定),表示参数化类型是此类型的超类型(父类型),直至Object,最少为T(包括T及其T的子类),可以以T类型安全存进去。

首先,泛型的出现是为了安全,所有与泛型相关的异常都应该在编译期间发现,因此为了泛型的绝对安全,java在设计时做了相关的限制:List<? extends E>表示该list集合中存放的都是E的子类型(包括E自身),由于E的子类型可能有很多,但是我们存放元素时实际上只能存放其中的一种子类型(这是为了泛型安全,因为其会在编译期间生成桥接方法<Bridge Methods>该方法中会出现强制转换,若出现多种子类型,则会强制转换失败)。


例如:

List<? extends Number> list = new ArrayList<Number>();
list.add(4.0); // 编译错误
list.add(3); // 编译错误


上例中添加的元素类型不只一种,这样编译器强制转换会失败,为了安全,Java只能将其设计成不能添加元素。虽然List<? extends E>不能添加元素,但是由于其中的元素都有一个共性:有共同的父类,因此我们在获取元素时可以将他们统一强制转换为E类型,我们称之为get原则。

对于List<? super E>其list中存放的都是E的父类型元素(包括E),我们在向其添加元素时,只能向其添加E的子类型元素(包括E类型),因为只能存放一种类型,E类型及其子类型是安全的,这样在编译期间将其强制转换为E类型时是类型安全的,因此可以添加元素。


例如: 

List<? super Number> list = new ArrayList<Number>();
list.add(2.0);
list.add(3.0);

// 不能取,只能存,下面会编译错误
// Number number = list.get(0);

但是,由于该集合中的元素都是E的父类型(包括E),其中的元素类型众多,在获取元素时我们无法判断是哪一种类型,故设计成不能获取元素,我们称之为put原则。

实际上,我们采用extends,super来扩展泛型的目的是为了弥补例如List<E>只能存放一种特定类型数据的不足,将其扩展为List<? extends E> 使其可以接收E的子类型中的任何一种类型元素,这样使它的使用范围更广,List<? super E>同理。

 

  • 6.1 extends使用
    static class Food {
    }

    static class Fruit extends Food {
    }

    static class Apple extends Fruit {
    }

    static class RedApple extends Apple {
    }

    public static void main(String[] args) {
        /**
         * ? extends Fruit:
         * 具有任何从Fruit继承类型的列表,编译器无法确定List所持有的类型,所以无法安全的向其中添加对象。
         * 可以添加null, 因为null 可以表示任何类型。
         * 所以List的add方法不能添加任何有意义的元素,但是可以接受现有的子类型List<Apple> 赋值。
         */
        List<? extends Fruit> flist = new ArrayList<Apple>();
        // 下面三行都会编译出错
//        flist.add(new Apple());
//        flist.add(new RedApple());
//        flist.add(new Object());
        // 可以编译通过
        flist.add(null);

        // 由于,其中放置是从Fruit中继承的类型,所以可以安全地取出Fruit类型。可以取出来(null可以表示任何类型)
        Fruit fruit = flist.get(0);
        Apple apple = (Apple) flist.get(0);

        // 在使用Collection中的contains方法时,接受Object 参数类型,可以不涉及任何通配符,编译器也允许这么调用。
        flist.contains(new Fruit());
        flist.contains(new Apple());
    }

 

  • 6.2 super的使用
    public static void main(String[] args) {
        /**
         * List<? super Fruit> 表示具有任何Fruit超类型的列表,列表的类型至少是一个 Fruit 类型,
         * 因此可以安全的向其中添加Fruit 及其子类型。
         *
         * 由于List<? super Fruit>中的类型可能是任何Fruit 的超类型,无法赋值为Fruit的子类型Apple的List<Apple>.
         */
        List<? super Fruit> flist = new ArrayList<Fruit>();
        flist.add(new Fruit());
        flist.add(new Apple());
        flist.add(new RedApple());

        // compile error:
//        List<? super Fruit> flist1 = new ArrayList<Apple>();

        // 因为,List<? super Fruit>中的类型可能是任何Fruit 的超类型,
        // 所以编译器无法确定get返回的对象类型是Fruit, 还是Fruit的父类Food 或 Object.compile error:
//        Fruit item = flist.get(0);
    }

 

 

7. 泛型数组

java中是"不能创建一个确切的泛型类型的数组"的。

也就是说下面的这个例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];  

而使用通配符创建泛型数组是可以的,如下面这个例子:

List<?>[] ls = new ArrayList<?>[10]; 

这样也是可以的:

List<String>[] ls = new ArrayList[10];

看看sun文档提供一个例子:

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Unsound, but passes run time store check    
String s = lsa[1].get(0); // Run-time error: ClassCastException.

这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。

而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。

下面采用通配符的方式是被允许的: 数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Correct.    
Integer i = (Integer) lsa[1].get(0); // OK

 

 

8. 泛型的类型擦除

Java的泛型是伪泛型,为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)。

Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉,这个过程就称为类型擦除。

如在代码中定义的List<object>和List<String>等类型,在编译后都会编程List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。

   public static void main(String[] args) {
        ArrayList<String> arrayList1 = new ArrayList<String>();
        arrayList1.add("abc");
        ArrayList<Integer> arrayList2 = new ArrayList<Integer>();
        arrayList2.add(123);
        System.out.println(arrayList1.getClass() == arrayList2.getClass());
    }

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型,只能存储字符串。一个是ArrayList<Integer>泛型类型,只能存储整型。最后,我们通过arrayList1对象和arrayList2对象的getClass方法获取它们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。

再来看一个例子:

    public static void main(String[] args) throws IllegalArgumentException, SecurityException, 
            IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        ArrayList<Integer> arrayList3 = new ArrayList<Integer>();
        // 这样调用add方法只能存储整形,因为泛型类型的实例为Integer
        arrayList3.add(1);
        arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
        for (int i = 0; i < arrayList3.size(); i++) {
            System.out.println(arrayList3.get(i));
        }
    }

 

在程序中定义了一个ArrayList泛型类型实例化为Integer的对象,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了原始类型。

 

 

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值