大话"爪哇"泛型

1,607 阅读15分钟

  在某个不知名的方位有这么一个叫做爪哇的岛国,得天独厚的自然条件使得该国物产丰富,其中由于盛产著名的爪哇咖啡而闻名世界,每年都吸引大批来自世界各地的游客前往观光学习,观光可以理解,学习这怎么个理解法,带着这个疑问我们继续往下深入,哔哔,开车!

  很有意思的是,在爪哇国中生活的当地居民都使用 Java 语言沟通交流,这种叫做 Java 的语言是由最初爪哇国第一代国王所创造出来的,由此逐渐完善并演变为文化文字、交流语言,一直传承至今。因为有了语言交流,也使得信息传达更加及时,这为该国生产咖啡豆(Java Bean) 的行业提供非常大的帮助,所以该国的咖啡豆出口需求量一直很大,也正因此爪哇岛的国旗是一杯热气腾腾的咖啡:

泛型的出现

  任何样东西的推出都不是一下就完美的,玉石经过巧匠的雕琢后温润有方,上铺的室友任性辞职后,回家继承家产现有车有房,只留下我原地感慨:高级玩家!

  还在公元 JDK 1.4 年代的时候,那个时候还没有泛型的概念,当时的人们都是类似于下面交流:

public static void main(String[] args) {
    List list = new ArrayList();
    list.add("https://www.talkmoney.cn/");
    list.add(5);
    String str = (String)list.get(0);
    Integer num = (Integer)list.get(1);
}

  首先可以看到的是声明了一个集合,然后往集合里放入不同的数据,并且在取出数据的时候还要做强制类型转换,此外人们还会因为存入集合中第几个是什么类型的数据而烦恼,就会造成取出数据强转的时候造成转换异常ClassCastException,这就好比于加工商从种植园来的货车上卸下不同品种的原料,导致最后所烘培加工的咖啡豆并不是所需要的。

  这种情况等到爪哇国第五代君主上任才有所改变,新任君主励精图治并针对此情况颁布泛型这条法规,一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种限制就会对代码的束缚就会很大,事实上,泛型是为了让编写的代码可以被不同类型的对象重用,于是人们从此交流的方式又变为了如此:

public static void main(String[] args) {
    List<String> list = new ArrayList();
    list.add("https://www.talkmoney.cn/");
    list.add("大家好,我是芦苇");
    String url = list.get(0);
    String name = list.get(1);
}

  这时候创建集合并为其指定了 String 类型,也就是说现在只能往集合中存入 String 类型的数据,也正因此,在取出数据的时候也不需要再进行强制转换的操作,从而避免了类型转换异常的风险,而在爪哇国的"宪法"《Thinking in Java》中提出,泛型出现的原因在于:

有许多原因促成泛型的出现,其中最引人注意的一个原因,就是为了创建容器类。

  仔细想想,似乎集合类库和泛型还真的有点配,这里以 Collection 接口为例:

public interface Collection<E> extends Iterable<E> {

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);
    
    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean removeAll(Collection<?> c);
}   

泛型的应用

  前面提到泛型是为了让编写的代码可以被不同类型的对象重用,那么就需要给操作的数据类型指定参数,这种参数类型可以使用在类、方法、集合、接口等,下面就分别来看看。

泛型类

  假如现在我们需要一个可以通过传入不同类型数据的类,通过使用泛型可以这样来编写:

public class GenericClass<T> {
    private T item;
    
    public void setItem(T t) {
        this.item = t;
    }
    
    public void getItem(T t) {
        return this.item;
    }
}

  咋一看可以发现泛型类和普通类差不多,区别在于类名后面多了个类型参数列表,尖括号内的 T 为指定的类型参数,这里不一定非得是 T,可以自己任意命名。指定参数后,可以看到类中的成员,方法及参数都可以使用这个参数类型,并且最后返回的都还是这个指定的参数类型,来看它的使用:

public static void main(String[] args) {
    GenericClass<String> name = new GenericClass<>("芦苇科技");
    GenericClass<Integer> age = new GenericClass<>(5);
}

泛型方法

  泛型方法顾名思义就是使用泛型的方法,它有自己的类型参数,同样它的参数也是在尖括号间,并且位于方法返回值之前,此外,泛型方法可以存在于泛型类或者普通类中,如下:

// 泛型类
public class GenericClass<T> {
    private T item;
    
    public void setItem(T t) {
        this.item = t;
    }
    // 只是用到了泛型并没有声明类型参数,所以不要混淆此为泛型方法
    public void getItem(T t) {
        return this.item;
    }
    
    // 泛型方法
    public <T> void GenericMethod(T t) {
        System.out.println(t);
    }
}

  这里需要注意的是,泛型方法中的类型 T 和泛型类中的类型 T 是两码事,虽然它们都是 T,但其实它们之间互相不影响,此外类型参数可以有多个。

泛型接口

  泛型接口的定义基本和泛型类的定义是差不多的,类型参数写在接口名后,如下:

public interface GenericInterface<T> {
    public void doSomething(T t);
}

  在使用中,实现类在实现泛型接口时需要传入具体的参数类型,那么之后使用到泛型的地方都会被替换传入的参数类型,如下:

public class Generic implements GenericInterface<String> {
    @override
    public void doSomething(String s) {
     	......   
    }
}

泛型通配符

  除去在前面介绍完的泛型类、方法等之外,泛型还有其他方面的应用,例如有时候希望传入的类型参数有一个限定的范围内,这一点爪哇国显然也早就料到,解决方案便是泛型通配符,那么泛型通配符主要有以下三种类型:

  • 上界通配符 <? extends T>
  • 下界通配符 <? super T>
  • 无界通配符 <?>

  在了解通配符之前,首先我们得要对类型信息有个基本的了解。

public class Coffee {}

// 卡布奇诺咖啡
public class Cappuccino extends Coffee {}

// 第一种创建方式
Cappuccino cappuccino = new Cappuccino();
// 第二种创建方式
Coffee cappuccino = new Cappuccino();

  上面这个类中有一个 Coffee 类,Coffee 类是 Cappuccino 类的父类,往下便是两种创建类对象的方式,第一种方式很常见,创建 cappuccino 对象并指向 Cappuccino 类对象,这样在 JVM 编译时还是运行时的类型信息都是 Cappuccino 类型。那么第二种方式呢,我们使用 Coffee 类型的变量指向 Cappuccino 类型对象,这在 Java 的语法中称为向上转型,在 Java 中可以把子类对象赋值给父类对象。运行时类型信息可以让我们在程序运行时发现并使用类型信息,而所有的类型转换都是在运行时识别对象的类型,所以在编译时的类型是 Coffee 类型,而在运行时 JVM 通过初始化对象发现它指向 Cappuccino 类型对象,所以运行时它是 Cappuccino 类型。


上界通配符 <? extends T>

  大概理清了下类型信息后,前面我们也提到过有时候希望传入的类型参数有一个限定的范围内,那么在前面的基础上定义一个 Cup 类用来装载咖啡:

public class Cup<T> {
    private List<T> list;
    public void add(T t) {list.add(t);}
    public T get(int index) {return list.get(index);}
}

  这里这个 Cup 类是一个泛型类,并且指明类型参数 T,代表着我们可以传入任何类型,假如目前需要一个装咖啡的杯子,理论上既然是装咖啡的杯子,那么就可以装上卡布奇诺,因为卡布奇诺也是属于咖啡的一种啊对吧,如下:

Cup<Coffee> cup = new Cup<Cappuccino>();	// compiler error

  然而我们会发现代码在编译时会发生错误,虽然知道 Coffee 类和 Cappuccino 类存在继承关系,但是在泛型中是不支持这样的写法,解决办法就是通过上界通配符来处理:

Cup<? extends Coffee> cup = new Cup<Cappuccino>();

  在类型参数列表中使用 extends 表示在泛型中必须是 Coffee 或者是其子类,当然在类型参数列表中还可以有多个参数,可以用逗号对它们进行隔开。通过上界通配符解决了这个问题,但是使用上界通配符会使得无法往其中存放任何对象,却可以从中取出对象

Cup<? extends Coffee> cup = new Cup<Cappuccino>();
cup.add(new Cappuccino()); 			  // compiler error
Coffee coffee = cup.get(0);		          // compiler success

  出现这种情况的原因,我们知道一个 Cup<? extends Coffee> 的引用,可能指向 Coffee 类对象,也可以指向其他 Coffee 类的子类对象,这样的话 JVM 在编译时并不能确定具体是什么类型,而为了安全考虑,就直接一棒子打死。而从中取出数据,最后都能通过向上转型使用 Coffee 类型变量去引用它。所以,当我们使用上界通配符 <? extends T> 的时候,需要注意的是不能往其中插入,但是可以读取;

下界通配符 <? super T>

  下界通配符 <? super T> 的特性则刚好与上界通配符 <? extends T> 相反,即只能往其中存入 T 或者其子类的数据,但是在读取的时候就会受到限制

Cup<? super Cappuccino> cup = new Cup<Coffee>();         // compiler success
Cup<? super Cappuccino> cup = new Cup<Object>(); 	 // compiler success
Object object = cup.get(0);

  对于 cup 来说,它指向的具体类型可以是 Cappuccino 类的任何父类,虽然无法知道具体是哪个类型,但是它都可以转化为 T 类型,所以可以往其中插入 T 及其子类的数据,而在读取方面,因为里面存储的都是T 及其基类,无法转型为任何一种类型,只有通过 Object 基类去取出数据。

无界通配符 <?>

  如果单独使用 则使用的是无界通配符,如果不确定实际要操作的类型参数,则可以使用该通配符,它可以持有任何类型,这里需要注意的是,我们很容易将 和 搞混,例如 List 和 List 咋看之下似乎很是相像,实际上却不是这样的,List 是一个未知类型的 List,而 List 则是任意类型的 List,我们可以将 List 赋值给 List<?>,却不能把 List 赋值给 List,要搞清楚这一点:

List<Object> objectList = new ArrayList<>();
List<?> anyTypeList;
List<String> stringList = new ArrayList<>();

anyTypeList = stringList;	       // compiler success
objectList = (List<Object>)stringList; // compiler error

通配符小结

  通过前面的了解,无界通配符 <?> 用于表示不确定限定范围的场景下,而对于使用上界通配符 <? extends T> 和下界通配符 <? super T> 也知道它们的使用和受限制的地方:

  • 上界通配符 <? extends T> 不能插入存储数据,却可以读取数据,适合需要内容读取的场景;
  • 下界通配符 <? super T> 则可以插入 T 类型及其子类对象,而只能通过 Object 变量去取出数据,适合需要内容插入的场景;

  实际上,这种情况又被叫做 PECS 原则,PECS 的全称是 Producer Extends Consumer Super。Producer Extends 说明的是当需要获取内容资源去生产时,此时的场景角色是生产者,可以使用上界通配符 <? extends T> 更好地读取数据;Consumer Super 则指的是当我们需要插入内容资源以待消费,此时的场景角色是消费者,可以使用下界通配符 <? super T> 更好地插入数据。具体的选择可以根据自己的实际场景需要灵活选择。说到生产者消费者,感觉又回到初识多线程那会儿,时间就这样悄悄地溜走,捉也捉不住。

泛型的神秘之处

关于泛型机制

  在金庸老爷子描绘的江湖世界中,里面有种武功绝学叫做乾坤大挪移,只要习得此功可以直接施展对方武功,哪怕是现学现用都过之而不及,听起来泛型似乎也差不多。那么先从一个例子说起:

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> a = new ArrayList<String>();
        ArrayList<Integer> b = new ArrayList<Integer>();
        System.out.println(a.getclass() == b.getclass());
    }
}
// 运行结果:true

  尽管这是两个不同类型的集合数组,当获取它们的类信息并比较的时候,此时的两者之间的类型竟然是一样的,究其原因这是 Java 泛型擦除的机制,首先需要明白的是,泛型是体现在编译的时候实现,之后在生成的字节码文件和运行时是不包含泛型信息的,所以类型信息是会被擦除掉的,只留下原始类型,这个过程就是泛型擦除的机制,之所以擦除是为了兼容之前原有的代码。此外,关于原始类型下面再展开叙述。

  上面提到泛型被擦除后只保留原始类型,那么这个原始类型啥东东,如下:

// 擦除前
public class GenericClass<T> {
    private T data;
    
    public T getData() {return data;}
    ......
}

// 擦除后
public class GenericClass {
    private Object data;
    
    public Object getData() {return data;}
    ......
}

  对于没有指定界限类型参数,在被类型擦除之后会被替换为 Object,这是相对于无界类型参数而言,如上述所言,如果没有指定限制范围,那么类型参数就会被替换为 Object,那如果要让类型限定在一个范围内的情况呢?

// 擦除前
public class GenericClass<T extends Serializable> {
  	private T data;
    public T getData() {return data;}
    ......
}

// 擦除后
public class GenericClass {
    private Serializable data;
    public Serializable getData() {return data;}
    ......
}

  那么此时的情况是有界类型参数,类型擦除后会替换为指定的边界,当然这里的边界可以指定多个,如果在有多个边界的情况下,那么类型参数也只是会擦除第一个边界。

泛型的本质

  说到这里,我们可能就会存在这样一个疑问,既然说泛型在类编译的时候会被擦除,那么在运行时是如何做到插入读取类型一致的呢?换句话来说就是,泛型类型参数被擦除为原始类型 Object,那么按理来说是可以插入其他类型的数据的啊!

  事实上,JVM 虽然在对类进行编译的时候将类型参数进行擦除,但是它会保证使用到泛型类型参数的一致性。我们知道,在类还处于编译阶段的时候,此时的类型参数还可以获取的到,编译器可以做类型检查,举例来说就是,一个 ArrayList 被声明为 Integer 的时候,当往该 ArrayList 中插入数据的时候会对其进行判断,以此保证数据类型的一致性。而当类型参数被擦除后,为了能够保证从中读取数据的类型是原来指定的类型参数,JVM 默默地帮我们进行类型转换,以此将类型参数还原成我们指定的那个它~

泛型的限制

  由于 Java 泛型擦除机制,指定的类型参数会被擦除,所以对于以下的一些操作将是不允许的:

  • 不能使用类型参数进行创建实例;
  • 不能使用类型参数进行 instanceof;
public class GenericClass<T> {
    public static void method(Object obj) {
        if (obj instanceof T) { // compiler error
            ......
        }
        T t = new T(); // compiler error
    }
}
  • 泛型指定的类型参数不能是基本类型
ArrayList<int> list = new ArrayList<int>(); // compiler error
/**
 *	可以使用基本数据类型的包装类
 */
ArrayList<Integer> list = new ArrayList<Integer>(); // compiler success
  • 不能创建类型参数的数组
ArrayList<Integer>[] list = new ArrayList<Integer>(3);

结语

  可以看到,爪哇国为了能让国民安居乐业,也是下了一番苦心,这些年来随着“咖啡市场"的供不应求,很多人也都加入了进来,在文章的开头中也提到,每年都吸引大批来自世界各地的游客前往观光学习,未来到底怎么样谁也不知道,只是很多时候就像一座围城,有的人出来,有的人进去。用他们的一句话来说:”唉啥,混口饭吃!“。

  到这里,本文已经进入尾声,关于泛型这一方面还有许多未能详细记录,希望也能在这里起到个抛砖引玉的作用,由于本人水平有限还请批评指正。


参考:


内推信息

  • 我们正在招募小伙伴,有兴趣的小伙伴可以把简历发到 app@talkmoney.cn,备注:来自掘金社区
  • 详情可以戳这里--> 广州芦苇信息科技