泛型

泛型实质上就是使程序员定义安全的类型。在没有出现泛型之前,Java 也提供了对 Object 类型的引用 “任意化” 操作,这种 “任意化” 操作就是对 Object 类型引用进行向下转型及向上转型操作,但某些强制类型转换的错误也许不会被编译器捕捉,而在运行后出现异常,可见强制类型转换存在安全隐患,因此在此提供了泛型机制。本节就来探讨泛型机制。

回顾向上转型与向下转型

在介绍泛型之前,先来看一个例子。在项目中创建 Test 类,在该类中将基本类型向上转型为 Object 类型,具体代码如下:

public class Test {
    private Object b;   // 定义 Object 类型成员变量
    public Object getB() { // 设置相应的 getXXX() 方法
        return b;
    }
    public void setB(Object b) { // 设置相应的 setXXX() 方法
        this.b = b;
    }
    public static void main(String[] args) {
        Test t = new Test();
        t.setB(Boolean.valueOf(true)); // 向上转型操作
        System.out.print(t.getB());
        t.setB(Float.valueOf("12.3"));
        Float f = (Float)t.getB();  // 向下转型操作
        System.out.print(f);
    }
}
java

运行结果如下:

     true
     12.3
bash

在本实例中,Test 类中定义了私有的成员变量 b,它的类型为 Object 类型,同时为其定义了相应的 setXXX() 与 getXXX() 方法。在类的主方法中,将 Boolean.valueOf(true) 作为 setB() 方法的参数,由于 setB() 方法的参数类型为 Object 类型,这样就实现了向上转型操作。同时,在调用 getB() 方法时,将 getB() 方法返回的 Object 对象以相应的类型进行返回,这个就是向下转型操作,问题通常就会出现在这里。因为向上转型是安全的,而如果进行向下转型操作时用错了类型,或者并没有执行该操作,就会出现异常,例如以下代码:

t.setB(Float.valueOf("12.3"));
Integer f = (Integer) t.getB();
System.out.println(f);
java

该段并不存在语法错误,因此可以被编译器接受,但在执行时会出现 ClassCastException 异常。这样看来,向下转型操作通常会出现问题,而泛型机制有效地解决了这一问题。

定义泛型类

Object 类为最上层的父类,很多程序员为了使程序更为通用,设计程序时通常使传入的值与返回的值都以 Object 类型为主。当需要使用这些实例时,必须正确地将该实例转换为原来的类型,否则在运行时将会发生 ClassCastException 异常。

为了提前预防这种问题,Java 提供了泛型机制。其语法如下:

     类名<T>
bash

其中,T 是泛型的名称,代表某一种类型。开发者在创建该类对象时需要指定 T 代表哪种具体的类型。如果不指定具体类型,T 则采用 Object 类型。

【例13.6】创建带泛型的图书类(实例位置:资源包\TM\sl\13\6)

为 Book 图书类创建泛型 T,用 T 声明一个成员变量 bookInfo。创建不同的图书对象,分别将 bookInfo 的类型指定为字符串、浮点数和布尔值类型。

public class Book<T> {								// 定义带泛型的Book<T>类
	private T bookInfo;								// 类型形参:书籍信息
	public Book(T bookInfo) {							// 参数为类型形参的构造方法
		this.bookInfo = bookInfo;						// 为书籍信息赋值
	}
	public T getBookInfo() {							// 获取书籍信息的值
		return bookInfo;
	}
	public static void main(String[] args) {
		// 创建参数为String类型的书名对象
		Book<String> bookName = new Book<String>("《Java从入门到精通》");
		// 创建参数为String类型的作者对象
		Book<String> bookAuthor = new Book<String>("明日科技");
		// 创建参数为Double类型的价格对象
		Book<Double> bookPrice = new Book<Double>(69.8);
		// 创建参数为Boolean类型的附赠源码
		Book<Boolean> hasSource = new Book<Boolean>(true);
		// 控制台输出书名、作者、价格和是否附赠光盘
		System.out.println("书名:" + bookName.getBookInfo());
		System.out.println("作者:" + bookAuthor.getBookInfo());
		System.out.println("价格:" + bookPrice.getBookInfo());
		System.out.println("是否附赠源码?" + hasSource.getBookInfo());
	}
}
java

运行结果如下:

     书名:《Java从入门到精通》
     作者:明日科技
     价格:69.8
     是否附赠源码?true
bash

从这个实例中可以看出,使用泛型定义的类在声明该类对象时可以根据不同的需求指定 <T> 真正的类型,而在使用类中的方法传递或返回数据类型时将不再需要进行类型转换操作,而是使用在声明泛型类对象时 “< >” 符号中设置的数据类型。

使用泛型这种形式将不会发生 ClassCastException 异常,因为在编译器中就可以检查类型匹配是否正确。

如果不按照泛型指定的类型进行赋值,就会发生编译错误。例如,将泛型指定为 Double 类型的值赋值给 Integer 类型时,就会出现如图13.1所示的错误。

image 2024 03 05 11 49 36 740
Figure 1. 图13.1 不按照泛型指定的类型进行赋值引起的编译错误

泛型的常规用法

定义泛型类时声明多个类型

在定义泛型类时,可以声明多个类型。语法如下:

class MyClass<T1,T2>{ }
java

其中,T1 和 T2 为可能被定义的类型。

这样,在实例化指定类型的对象时就可以指定多个类型。例如:

MyClass <Boolean,Float> m = new MyClass<Boolean, Float>();
java

定义泛型类时声明数组类型

定义泛型类时也可以声明数组类型,下面的实例在定义泛型时便声明了数组类型。

【例13.7】定义泛型数组(实例位置:资源包\TM\sl\13\7)

在项目中创建 ArrayClass 类,在该类中定义用于声明数据类型的泛型类。

public class ArrayClass<T> {
	private T[] array; // 定义泛型数组

	public T[] getArray() {
		return array;
	}

	public void setArray(T[] array) {
		this.array = array;
	}

	public static void main(String[] args) {
		ArrayClass<String> demo = new ArrayClass<String>();
		String value[] = { "成员1", "成员2", "成员3", "成员4", "成员5" };
		demo.setArray(value);
		String array[] = demo.getArray();
		for (int i = 0; i < array.length; i++) {
			System.out.println(array[i]);
		}
	}
}
java

运行结果如下:

     成员1
     成员2
     成员3
     成员4
     成员5
bash

可见,可以在使用泛型机制时声明一个数组,但是不可以使用泛型来建立数组的实例。例如,图13.2中显示的代码就是错误的。

image 2024 03 05 11 52 51 726
Figure 2. 图13.2 泛型不可以创建数组实例

集合类声明容器的元素

JDK 中的集合接口、集合类都被定义了泛型,其中 List<E> 的泛型 E 实际上就是 element 元素的首字母,Map<K,V> 的泛型 K 和 V 就是 key 键和 value 值的首字母。常用的被泛型化的集合类如表13.2所示。

image 2024 03 05 13 19 32 627
Figure 3. 表13.2 常用的被泛型化的集合类

下面的实例演示了这些集合的使用方式。

【例13.8】使用泛型约束集合的元素类型(实例位置:资源包\TM\sl\13\8)

在项目中创建 AnyClass 类,在该类中使用泛型实例化常用集合类。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class AnyClass {
	public static void main(String[] args) {
		// 定义ArrayList容器,设置容器内的值类型为Integer
		ArrayList<Integer> a = new ArrayList<Integer>();
		a.add(1); // 为容器添加新值
		for (int i = 0; i < a.size(); i++) {
			// 根据容器的长度,循环显示容器内的值
			System.out.println("获取ArrayList容器的值:" + a.get(i));
		}
		// 定义HashMap容器,设置容器的键名与键值类型分别为Integer与String型
		Map<Integer, String> m = new HashMap<Integer, String>();
		for (int i = 0; i < 5; i++) {
			m.put(i, "成员" + i); // 为容器填充键名与键值
		}
		for (int i = 0; i < m.size(); i++) {
			// 根据键名获取键值
			System.out.println("获取Map容器的值" + m.get(i));
		}
	}
}
java

运行结果如下:

     获取ArrayList容器的成员值:1
     获取Map容器的值成员0
     获取Map容器的值成员1
     获取Map容器的值成员2
     获取Map容器的值成员3
     获取Map容器的值成员4
bash

泛型的高级用法

泛型的高级用法包括限制泛型可用类型和使用类型通配符等。

限制泛型可用类型

默认可以使用任何类型来实例化一个泛型类对象,但 Java 中也对泛型类实例的类型进行了限制。语法如下:

class 类名称<T extends anyClass>
bash

其中,anyClass 指某个接口或类。

使用泛型限制后,泛型类的类型必须实现或继承 anyClass 这个接口或类。无论 anyClass 是接口还是类,在进行泛型限制时都必须使用 extends 关键字。

【例13.9】限制泛型的类型必须为 List 的子类(实例位置:资源包\TM\sl\13\9)

在项目中创建 LimitClass 类,在该类中限制泛型类型。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

public class LimitClass<T extends List> { // 限制泛型的类型
	public static void main(String[] args) {
		// 可以实例化已经实现List接口的类
		LimitClass<ArrayList> l1 = new LimitClass<ArrayList>();
		LimitClass<LinkedList> l2 = new LimitClass<LinkedList>();
		// 这句是错误的,因为HashMap没有实现List()接口
		LimitClass<HashMap> l3 = new LimitClass<HashMap>();
	}
}
java

在上面这个实例中,设置泛型类型必须实现 List 接口。例如,ArrayList 类和 LinkedList 类都实现了 List 接口,而 HashMap 类没有实现 List 接口,因此在这里不能实例化 HashMap 类型的泛型对象。

当没有使用 extends 关键字限制泛型类型时,默认 Object 类下的所有子类都可以实例化泛型类对象。图13.3显示的两个语句是等价的。

image 2024 03 05 13 35 12 306
Figure 4. 图13.3 两个等价的泛型类

使用类型通配符

在泛型机制中,提供了类型通配符,其主要作用是在创建一个泛型类对象时限制这个泛型类的类型实现或继承某个接口或类的子类。要声明这样一个对象可以使用 “?” 通配符来表示,同时使用 extends 关键字来对泛型加以限制。使用泛型类型通配符的语法如下:

     泛型类名称<? extends List> a=null;
bash

其中,<? extends List> 表示类型未知,当需要使用该泛型对象时,可以单独对其进行实例化。例如:

A<? extends List> a = null;
a = new A<ArrayList>();
a = new A<LinkedList>();
java

如果实例化没有实现 List 接口的泛型对象,编译器将会报错。例如,实例化 HashMap 对象时,编译器将会报错,因为 HashMap 类没有实现 List 接口。

除了可以实例化一个限制泛型类型的实例,还可以将该实例放置在方法的参数中。例如:

public void doSomething(A<? extends List> a){}
java

在上述代码中,定义方式有效地限制了传入 doSomething() 方法的参数类型。

如果使用 A<?> 这种形式实例化泛型类对象,则默认表示可以将 A 指定为实例化 Object 及以下的子类类型。例如:

List<String> l1 = new ArrayList<String>();
l1.add("成员");
List<?> l2 = l1;
List<?> l3 = new LinkedList<Integer>();
System.out.println(l2.get(0));
java

在上面的例子中,List<?> 类型的对象可以接受 String 类型的 ArrayList 集合,也可以接受 Integer 类型的 LinkedList 集合。也许有的读者会有疑问,“List<?> l2 =l1” 语句与 “List l2 = l1” 语句存在何种本质区别?这里需要注意的是,对于使用通配符的对象不能向其中加入新的信息,只能从其中获取或删除信息。例如:

l1.set(0, "成员改变");   // 没有使用通配符的对象调用set()方法
l2.set(0, "成员改变");   // 使用通配符的对象调用 set() 方法时,该方法不能被调用
l3.set(0,1);
l2.get(0);         // 可以使用l2的实例获取集合中的值
l2.remove(0);      // 根据键名删除集合中的值
java

从上述代码中可以看出,由于对象 l1 是没有使用 A<?> 这种形式初始化出来的对象,因此它可以调用 set() 方法改变集合中的值,但 l2 与 l3 对象则是通过使用通配符的方式创建出来的,因此不能改变集合中的值。

泛型类型限制除了可以向下限制,还可以进行向上限制,只要在定义时使用 super 关键字即可。例如,像 “A<? super List> a = null;” 这样定义后,对象 a 只接受 List 接口或上层父类类型,如 “a =new A<Object>();”。

继承泛型类与实现泛型接口

定义为泛型的类和接口也可以被继承与实现。例如,让 SubClass 类继承 ExtendClass 的泛型,代码如下:

class ExtendClass<T1>{}
class SubClass<T1,T2,T3> extends ExtendClass<T1>{}
java

如果在 SubClass 类继承 ExtendClass 类时保留父类的泛型类型,需要在继承时指明,如果没有指明,直接使用 “extends ExtendsClass” 语句进行继承操作,则 SubClass 类中的 T1、T2 和 T3 都会自动变为 Object 类型,所以在一般情况下都将父类的泛型类型进行保留。

定义为泛型的接口也可以被实现。例如,让 SubClass 类实现 SomeInterface 接口,并继承接口的泛型,代码如下:

     interface SomeInterface<T1>{ }
     class SubClass<T1,T2,T3> implements SomeInterface<T1>{ }
java

泛型总结

下面总结泛型的使用方法:

  • 泛型的类型参数只能是类类型,不可以是简单类型,如 A<int> 这种泛型定义就是错误的。

  • 泛型的类型个数可以是多个。

  • 可以使用 extends 关键字限制泛型的类型。

  • 可以使用通配符限制泛型的类型。

编程训练(答案位置:资源包\TM\sl\13\编程训练)

【训练3】模拟银行存钱 使用泛型类模拟场景:赵四刚刚(通过 Date 类获取当前时间)在中国建设银行向账号为 “6666 7777 8888 9996 789” 的银行卡上存入“¥8,888.00”,存入后卡上余额还有“¥18,888.88”。现要将“银行名称”、“存款时间”、“户名”、“卡号”、“币种”、“存款金额”、“账户余额”等信息通过泛型类BankList<T>在控制台上进行输出。

【训练4】输出NBA球队信息 定义泛型类(Miami<T>),再创建两个类(Detroit类和Philadelphia类)继承该泛型类,输出NBA中夺冠次数为3次的球队及夺冠年份:迈阿密热火队(2006年、2012年、2013年),底特律活塞队(1989年、1990年、2004年),费城76人队(1955年、1967年、1983年)。