11.1 泛型类
11.1.1 一个令程序员纠结的问题
在给大家讲泛型之前,我先讲一个例子将这个概念引入。
假如说你是JDK的开发者,你正在编写集合类。考虑到用户在集合中存入的元素的类是完全不确定的。不过还好,我们还有多态性。我们可以把集合类中的字段的类型都声明为Object,让所有类的元素都能存入。这样这个问题就轻松解决了。
但是麻烦到了用户身上:
- import java.util.*;
- public class GenericDemo {
- public static void main(String[] args) throws Exception {
- ArrayList al = new ArrayList();
-
- al.add(new MyClass()); //加入多个MyClass对象
- al.add(new MyClass());
- al.add(new MyClass());
- al.add(new MyClass());
- al.add(5); //加入一个Integer
-
- Iterator i = al.iterator();
-
- while(i.hasNext()){
- //为了调用MyClass的method()方法,我们需要将迭代器中的元素向下转换为MyClass类
- MyClass mc = (MyClass)i.next();
-
- mc.method(); //调用method()方法
- }
- }
- }
- class MyClass{ //自定义类
- public void method(){ //自定义方法
- System.out.println("method runs");
- }
- }
在这个程序中,我在ArrayList中添加了多个MyClass类对象,以及一个int。在对这个ArrayList迭代的时候,我想要调用MyClass的method()方法。所以说我需要将迭代器中的下一个元素向下转换成MyClass类型(见面向对象上)。
结果:
很容易发现,由于我在ArrayList中添加了四个MyClass对象,一个Integer对象,在第五次循环迭代时出现了异常。原因显而易见:Integer怎么能够被向下转换为MyClass!可以看出,这是用户没有检查传入的类型而导致的问题。
从中可以发现多态性的两个缺点:
- 即使用户没有检查类型,不会有任何提示;
- 有向下转换的麻烦。
那么大家回忆一下,为什么要使用多态这门技术。就是因为写这个泛型类的时候不知道用户会存哪类的对象,所以说必须要用Object类来吸收。如果我们在类上面定义一个占位符,然后把这些不确定类型的值的类型定义为这个占位符,然后让用户去告诉我这个占位符究竟是什么,那这个问题不就是解决了吗?
Java的泛型技术和上述的猜想差不多。但是有一个小区别:用户指定这些占位符的类型时,类中的占位符不会是“变成”用户指定的类型,而是会被java编译器视为用户指定的类型。到了运行时期,这些类型就都没了。详情见泛型擦除。
通过这种方式,我们可以在编译时期对不正确的类型报出错误。这样就保障了类型安全。因此,Java泛型技术的定义是:对编译器类型的指定是否安全的一个检查。(感谢@DeathWolf96 的纠正)
11.1.2 泛型类
当一个类中的引用数据类型不明确,需要由用户指定,并由编译器检查,可以使这个在这个类上定义泛型。这里说的泛型,就是刚才说的“占位符”。
泛型类定义方法如下:
- 若干个修饰符 class 类名<泛型类型1,泛型类型2,泛型类型3....泛型类型n>{
- //一些代码
- }
一般来讲,泛型类型的命名是一个大写字母。
在一个有泛型的类中,泛型可以像普通的类型一样的使用。你可以用它作为字段的类型、用它作为方法的返回值、用它作为构造方法的参数....
我们写一个类。这个类可以存储一个对象,并拥有获取这个对象以及设置这个对象的方法。考虑到,这个对象的类型不确定,需要由实例化者指定,应当使用泛型。
- class ObjectTool<T> { // 在ObjectTool类上面定义一个叫做T的泛型
- private T t; // 一个类型是T的字段
- public void set(T t) { //接收一个T类型的参数
- this.t = t;
- }
-
- public T get() { //一个返回T的方法
- return t;
- }
- }
其实,大家可以理解为类中的“T”就是一个占位符。当用户实例化时指定这个占位符的类型,使用“T”的变量就被约束成了用户指定的类型。
实例化一个泛型类的语法如下:
- 类名<类型1,类型2,类型3,....,类型n> 实例名 = new 类名<类型1,类型2,类型3,....,类型n>(构造方法参数);
不过,可以发现在等号的右边,泛型的类型又要指定一次,看上去重复很严重。因此,在Java7当中,增加了一个语法糖。上面的实例化语法可以被简化为:
- 类名<类型1,类型2,类型3,....,类型n> 实例名 = new 类名<>(构造方法参数);
Java会自动用过左边的泛型,推断出右边的泛型。“<>”很像一个菱形,所以这个语法糖被称为“菱形语法”
好的!现在我们想要实例化我们刚才写的ObjectTool类。我想要存入的对象的类型是String,我实例化的时候就可以指定。
- ObjectTool<String> tool = new ObjectTool<>();
在我们执行这条语句时,tool实例中的所有“T”约束成了String。
如果实例化一个泛型类时没有指定泛型类型,就像是我们以前学的实例化方法,会出现警告。这个类中的泛型会统统变成Object。现在大家可以理解为什么上一章我们学集合类时出现那么多警告。
- public class GenericDemo {
- public static void main(String[] args){
- ObjectTool<String> tool = new ObjectTool<>();
-
- tool.set("abc");
-
- System.out.println(tool.get());
- }
- }
- class ObjectTool<T> { // 在ObjectTool类上面定义一个叫做T的泛型
- private T t; // 一个类型是T的字段
- public void set(T t) { //接收一个T类型的参数
- this.t = t;
- }
-
- public T get() { //一个返回T的方法
- return t;
- }
- }
好的!现在我们了解了泛型类,以及实例化泛型类的语法。现在我们来解决我们刚开始提到的集合类的问题吧。
11.1.13 解决问题
我们又变回了JDK程序员。我们在开发集合类的时候,发现集合类中的某些变量的类型不确定,如果定义为Object会有安全隐患。我们可以在集合类上面加上一个泛型。
如果有同学有兴趣查阅API,可以发现ArrayList是这样定义的:
- class ArrayList<E> {
- //某些代码
- }
其中,E代表要存储的对象的类型。
同时,Iterator也有一个泛型。这个泛型代表Iterator中的元素的类型。
好的,现在我们通过实例化泛型的方法,使用ArrayList。
- import java.util.ArrayList;
- import java.util.Iterator;
- public class GenericDemo {
- public static void main(String[] args) {
- //将ArrayList中的泛型指定为MyClass。这样ArrayList中存储的对象必须都是MyClass
- ArrayList<MyClass> al = new ArrayList<>();
-
- al.add(new MyClass());
- al.add(new MyClass());
- al.add(new MyClass());
- al.add(new MyClass());
-
- //下面这句话引发编译时错误
- //al.add(5);
-
- Iterator<MyClass> i = al.iterator();
-
- while(i.hasNext()){
- //不需要向下转换了!
- MyClass mc = i.next();
-
- mc.method();
- }
- }
- }
- class MyClass {
- public void method() {
- System.out.println("method runs");
- }
- }
可以发现,有了泛型,我们把上面说的两个问题解决了。
- 成功地将运行时异常转换成编译时异常;
- 不再需要向下转换,很方便。
而且,上一章一直困扰我们的警告消失了。
本章小结
- 在我们编写集合类时,发现某些变量的类型不确定,需要由用户决定;然而,我们只能把这些变量声明为Object
- 这导致了使用集合类的用户发现两个问题:①很容易发生运行时异常 ②需要向下转换
- 如果我们能让用户去指定类中不确定的类型,如果指定之后还使用错误的类型,会导致编译时期的错误,这样也许会解决这些问题
- Java的泛型机制基本上实现了上述猜想。但是,并不是说类中的所有泛型都会变成用户指定的类型,而是会被编译器视为用户指定的类型
- 泛型类是有泛型的类。定义泛型类的语法是:“若干个修饰符 class 类名<泛型1,泛型2,...泛型n>”
- 实例化泛型类时,需要指定泛型类的类型。语法为“类名<类型1,类型2,...类型n> 实例名 = new 类名<>(构造方法参数)”
- 如果实例化泛型类没有指定泛型类型,出现警告;所有的泛型变成Object
- 使用占位符代替不确定的类型,解决了上述两个问题
11.2 泛型方法
11.2.1 泛型方法概述
在上一节当中,泛型的声明是在类上面的。这使这个泛型再这个类当中有效。如果仅仅是一个方法中的某个引用数据类型未知的话,可以把泛型加在方法上。
泛型方法的声明语法如下:
- 若干个修饰符 <泛型类型1,泛型类型2,....泛型类型3> 返回值类型 方法名(参数列表){
- //一些代码
- }
可以发现,泛型的声明方法和泛型类是差不多的。不过值得注意的一点是泛型的声明必须要在修饰符后,返回值前。
11.2.2 使用泛型方法
泛型方法和泛型类有一个很大的区别:那就是泛型类实例化时必须显式的指定每一个泛型的类型;然而,泛型方法中的泛型不需要显式的指定类型。那么用户是如何指定这个类型的呢?
泛型方法的泛型的引用数据类型由使用这个泛型的参数指定。之后编译器会将这个泛型视为这个参数的类型,以保障类型安全。
假如说我们有这样的一个方法,可以打印指定的对象的类名。考虑到指定的对象的类型不确定,应当使用泛型方法暂时代替。
- public static <T> void printClassName(T t) {
- System.out.println(t.getClass());
- }
注:getClass()方法是Object类中的方法。用于获取对象的类。
调用这个方法时,不需要指定“T”是什么类型。由于这个方法的参数是以“T”作为类型的,这个参数你传入什么对象,“T”就会被编译器视为这个参数的对象的类型。
- public class GenericMethodDemo {
- public static void main(String[] args) {
- GenericMethodDemo demo = new GenericMethodDemo();
- // 调用方法时无需指定泛型的类型。你传入什么参数,泛型就成为这个参数的类
- demo.printClassName(5);
- demo.printClassName("abc");
- demo.printClassName(new Object());
- demo.printClassName(3.14);
- }
- public <T> void printClassName(T t) {
- System.out.println(t.getClass());
- }
- }
大家看主方法:我第一次传入的是一个int(Integer),所以第一行打印结果就是Integer类;第二次传入String,第二行打印的就是String类.... 以此类推。
可以发现:方法中的泛型是随着参数而改变的。只要你的某个参数用了泛型作为类型声明,编译器会将泛型的类型视为参数传入的类型。
11.2.3 静态方法泛型
静态方法使用泛型稍微有些特殊。
上一节中我们讲过了,泛型类的泛型是在实例化时显示的指定类型的。这说明每一个对象都有自己的泛型类型。由于静态方法比对象更加早进入内存,静态方法无法使用类的泛型。
不过还好,静态方法可以使用方法上的泛型。例如拿上面的程序来讲,我们可以把printClass()方法改成静态的:
- public class GenericMethodDemo {
- public static void main(String[] args) {
-
- printClassName(5);
- printClassName("abc");
- printClassName(new Object());
- printClassName(3.14);
- }
- public static <T> void printClassName(T t) {
- System.out.println(t.getClass());
- }
- }
本章小结
- 若泛型只需在方法中使用,可以在方法上添加泛型
- 泛型要在方法上声明的话,放在修饰符后面、返回值前面
- 泛型方法在被调用时无需显式指定方法的泛型的类型,而是通过使用这个泛型的参数来决定
- 静态方法不能使用类泛型,但是可以使用方法泛型
11.3 泛型接口
11.3.1 泛型接口概述
我们已经学会了在类上、在方法上定义泛型。有的时候我们在写接口的时候也会遇到引用数据类型不明确的时候。因此,泛型也可以定义在接口上。
泛型接口的定义方式如下:
- interface 接口<泛型1,泛型2,泛型3.....,泛型n> extends 父接口1,父接口2,父接口3....,父接口n {
- //一些代码
- }
例如,我写一个这样的接口:
- interface MyInterface<T> {
- void method(T t); //一个无返回值,要求一个类型为T的参数的方法
- T method2(); //一个返回T类型,不要求参数的方法
- }
值得注意的是,泛型接口的泛型不能用作这个接口的字段的类型:
- interface MyInterface<T> {
- T t;
- }
这是因为,接口中的字段都是固定的public static final。然而,类中的静态成员不能使用类上的泛型。
好的,现在一个简单的泛型接口已经定义完了。接口不能够被实例化,那么接口的泛型是什么时候被确定呢?
11.3.2 实现一个泛型接口
一个接口的泛型类型的确认时机其被实现时。当一个类实现一个泛型接口,必须指定这个接口中的泛型的类型。若不,所有泛型视为Object。
- interface MyInterface<T> {
- void method(T t);
- T method2();
- }
- class ImplementingClass implements MyInterface<String> { //传入String
- public void method(String t) { //复写method()方法
- System.out.println(t);
- }
- public String method2() { //复写method2()方法
- return "hello";
- }
- }
ImplementingClass类在实现MyInterface的时候,为该接口的泛型类型指定为String。因此,其复写接口中的抽象方法时,方法中的“T”全部被编译器视为String。
还有另一种情况:实现类也不确定给接口的泛型传入哪个类型。需要由实例化者指定。可以在类上建立一个泛型,然后把这个泛型传入接口的泛型。
- interface MyInterface<T> {
- void method(T t);
- T method2();
- }
- //实现类也不知道给泛型传入什么类型。在类上面定义一个泛型,将这个泛型传入
- class ImplementingClass<T> implements MyInterface<T> {
- public void method(T t) {
- System.out.println(t);
- }
- public T method2() {
- return null;
- }
- }
实现类定义泛型T,给接口传入T。因此类中复写的方法中的“T”不变。
本章小结
- 接口中的引用数据类型不确定时,可以在接口上定义泛型
- 要实现一个泛型接口,必须给这个接口的泛型指定一个类型;若不,这个接口中的所有泛型变成Object
- 如果实现时也不知道传入什么,可以在类上定义一个泛型,将这个泛型传入
11.4 泛型通配符以及限定
11.4.1 无界通配符
请观察下列代码:
- public static void printList(List<Object> list) {
- Iterator<Object> i = list.iterator();
-
- while(i.hasNext()){
- System.out.println(i.next());
- }
- }
假设我定义一个方法,这个方法可以打印一个List中的所有元素。这个时候许多人会陷入一个误区:只要将List中的泛型定义为Object类,那么List<String>、List<Integer>....都可以接收了。
其实不是这样的。在Java的泛型机制中,如果A类是B类的父类,XXX<A>不是XXX<B>的父类。因此,这个方法的功能很有限:只能打印List<Object>的元素。
那么,我这个方法究竟该如何写呢?我们可以不给List和Iterator加上泛型,不过会引起警告。这里就涉及到泛型类型不确定的问题。遇到这种问题,应当使用泛型通配符“?”当做类型传入。
- public static void printList(List<?> list) { //接收List<?>
- Iterator<?> i = list.iterator();
-
- while(i.hasNext()){
- System.out.println(i.next());
- }
- }
- import java.util.ArrayList;
- import java.util.Iterator;
- import java.util.List;
- public class WildCardDemo {
- public static void main(String[] args) {
- List<Integer> list = new ArrayList<>();
- list.add(5);
- list.add(6);
-
- List<String> list2 = new ArrayList<>();
- list2.add("abc");
- list2.add("def");
-
- List<Object> list3 = new ArrayList<>();
- list3.add(new Object());
- list3.add(new Object());
-
- printList(list); //传入List<Integer>
- printList(list2); //传入List<String>
- printList(list3); //传入List<Object>
- }
- public static void printList(List<?> list) { //接收List<?>
- Iterator<?> i = list.iterator();
-
- while(i.hasNext()){
- System.out.println(i.next());
- }
- }
- }
可以看出,使用泛型通配符作为参数中的List的泛型类型可以增加方法的扩展性。
学生提问:泛型方法也可以做到呀,那么泛型方法和泛型通配符有什么区别?
复制代码
- public static void printList(List<?> list) //使用泛型通配符
- public static <T> void printList(List<T> list) //使用泛型方法
答:从上面我举得例子来讲,使用泛型方法也是可以的。但是泛型通配符和泛型方法有一个逻辑层面上的区别:那么就是泛型通配符是永远不确定的;泛型方法是等到方法被调用时,类型就会被确定。由于我们这个方法的参数是不确定的,应当使用通配符。
可以看出来,上面使用通配符接收任意类型进来。由于没有类型的限制,这种通配符叫做无界通配符。
11.4.2 泛型限定
为了方便讲解,我们先定义一个体系:
- class Human { //人类
- String name;
- int age;
- public Human(String name, int age) { //构造方法
- this.name = name;
- this.age = age;
- }
- public void introduce() { //自我介绍方法
- System.out.println("我的名字是" + name + ",我" + age + "岁了");
- }
- }
- class Student extends Human { //学生类
- public Student(String name, int age) {
- super(name, age);
- }
- public void study() { //学习方法
- System.out.println("学习中");
- }
- }
- class Teacher extends Human { //老师类
- public Teacher(String name, int age) {
- super(name, age);
- }
- public void teach() { //教课方法
- System.out.println("教课中");
- }
- }
现在,我的需求改了。我现在有一个集合,这个List可以装Human类。我希望我上面写的printList()方法不仅仅可以遍历List中的元素,还可以调用元素中的introduce()方法。
但是,泛型通配符是所有类型都支持的。所以说如果我要调用introduce()方法,我必须要让传入进来的参数的泛型类型都是Human或Human的子类。因此,Java为我们提供泛型限定机制。
我们把刚才写的方法更改成:
- public static void printList(List<? extends Human> list) {
- Iterator<? extends Human> i = list.iterator();
- while (i.hasNext()) {
- i.next().introduce();
- }
- }
- import java.util.ArrayList;
- import java.util.Iterator;
- import java.util.List;
- public class WildCardDemo {
- public static void main(String[] args) {
- List<Human> list1 = new ArrayList<>(); //List<Human>
- list1.add(new Human("张三",15));
- list1.add(new Human("李四",17));
- list1.add(new Human("王五",19));
-
- List<Student> list2 = new ArrayList<>(); //List<Student>
- list2.add(new Student("小王",13));
- list2.add(new Student("小李",14));
-
- List<String> list3 = new ArrayList<>(); //List<String>
- list3.add("abc");
- list3.add("def");
-
- printList(list1);
- printList(list2);
- // 由于String不是Human子类,下面代码报错
- // printList(list3);
- }
- public static void printList(List<? extends Human> list) {
- Iterator<? extends Human> i = list.iterator();
-
- while (i.hasNext()) {
- i.next().introduce();
- }
- }
- }
请大家注意看主方法:我有三个List,一个是List<Human>,一个是List<Student>,最后一个是List<String>。由于printList()方法已经限定其通配符必须是Human或Human子类,String不是Human子类,所以说最后一个List不能最为printList()的参数。
这样就完成了泛型的限定上限。当然了,既然可以限定上限,是否可以限定下限?
- <泛型类型或通配符 super 类>
本章小结
- 当要指定的泛型类型不确定时,可以使用泛型通配符"?"表示
- 泛型通配符是永远不确定的,泛型方法是刚开始不确定,等到方法被调用就会确定
- 如果希望一个泛型通配符或者泛型类型必须是某个类或者某个类的子类,可以限定上限
- 限定上限的语法是<泛型类型或通配符 extends 类>
- 如果希望限定下限,<泛型类型或通配符 super 类>
11.5 泛型擦除
11.5.1 泛型擦除概述
不仅仅是Java,其他的主流语言也有泛型技术的提供。不过Java的泛型和其他的泛型稍微有一些区别:Java的泛型是伪泛型。
那么什么是伪泛型呢?为了示范,我们写一个程序。需求是提供一个用于存储String的ArrayList,在迭代时打印通过字符串的length()方法来获取每一个元素的长度。
- import java.util.ArrayList;
- import java.util.Iterator;
- public class GenericErasureDemo {
- public static void main(String[] args) {
- ArrayList<String> al = new ArrayList<>();
- al.add("java");
- al.add("c++");
- al.add("c#");
- Iterator<String> i = al.iterator();
- while (i.hasNext()) {
- System.out.println("长度为:"+i.next().length());
- }
- }
- }
这个程序并不是很难理解。但是我想要让大家回忆一下,没有学泛型的时候该如何写。
如果没有对ArrayList进行泛型指定,那么其中的字段都会变成默认的Object类,并给出警告。在迭代时,由于length()方法是String的特有方法,需要对i.next()进行向下转换,也就是(String)i.next()。
如果使用反编译软件把这个程序的字节码文件(.class文件)反编译成源文件(.java),会看到这番场景:
- import java.io.PrintStream;
- import java.util.ArrayList;
- import java.util.Iterator;
- class GenericErasure
- {
- public static void main(String[] paramArrayOfString)
- {
- ArrayList localArrayList = new ArrayList();
-
- localArrayList.add("java");
- localArrayList.add("c++");
- localArrayList.add("c#");
-
- Iterator localIterator = localArrayList.iterator();
- while (localIterator.hasNext()) {
- System.out.println(((String)localIterator.next()).length());
- }
- }
- }
- 多了一个"import java.io.PrintStream"(无关紧要);
- 实例名改变了(无关紧要);
- ArrayList以及Itereator的泛型消失;
- 迭代中使用String的length()方法时,向下转换;
通过这个实例,我们可以观察到一点:Java中的泛型只存在于编译时期,运行时期带泛型的类型会统统变回没有泛型指定的类型。这种类型叫做原始类型(raw type)。例如:List<Integer>的原始类型是List。
因此:
- ArrayList<Integer> al = new ArrayList<>();
- ArrayList<String> al2 = new ArrayList<>();
- System.out.println(al.getClass() == al2.getClass()); //通过getClass()方法比较两个实例类是否相等
按理说,两个ArrayList的实例有着不同的泛型类型,但是对两个实例的getClass()方法比较的值是true。这更加证明了:泛型只存在在编译时期。
本章小结
- Java的泛型特点是伪泛型
- 伪泛型指:泛型只存在于编译时期,运行时期这些泛型类中的泛型都会变成Object
- 从泛型类型转转变成原始类型的过程叫做泛型擦除
[groupid=546]Command Block Logic[/groupid]