通过之前的学习,读者可以了解到,把一个对象存入集合后,再次取出该对象时,该对象的编译类型就变成了Object类型(尽管其在运行时类型没有改变)。集合设计成这样,提高了它的通用性,但是也带来了一些类型不安全和繁琐的问题,例如,集合可以同时存储任何类型的对象,通常对取出之后的对象都需要强制类型转换,而且如果不知道实际参数类型的情况,也无法进行强制类型转换。为了解决这些问题,从JDK 5版本开始引入了泛型,本章将围绕泛型的相关内容进行讲解。
1. 泛型基础
1.1. 泛型的概念
泛型是在JDK 5中引入的一个新特性,其本质是参数化类型,也就是将具体的类型形参化,参数化的类型(可以称之为类型形参)在使用或者调用时传入具体的类型(类型实参),类似于调用方法时传入实参才确定方法形参的具体值。泛型的声明由一对尖括号和类型形参组成,类型形参定义在尖括号中间,定义类、接口和方法时使用泛型声明,定义出的类、接口和方法分别称为泛型类、泛型接口和泛型方法。
1.2. 泛型的定义
使用泛型编程,会在使用或者调用时传入具体的类型时才确定最终的数据类型,所以集合需要存储什么类型的数据,在创建集合时传入对应的类型即可。
定义泛型时类型形参由一对尖括号(<>)包含在中间,使用或者调用泛型时,需要将类型实参写在尖括号(<>)之间。
JDK 5之后的类库中很多重要的类和接口都引入了泛型,例如集合体系中的类和接口。下面分别演示未引入泛型和使用泛型编程的区别,体验泛型具体有什么好处。
(1)未引入泛型前
public class TestDemo {
@Test
public void test(){
// 创建一个只保存Integer类型的List集合
List intList = new ArrayList();
intList.add(1);
intList.add(2);
//因为失误存放了Integer类型之外的字符串数据
intList.add("3");
for (int i = 0; i < intList.size(); i++) {
/*因为List里面默认取出的全部Object对象,所以使用之前需要进行强
* 制类型转换。集合内最后一个元素进行转换时候将出现类型转换异常
* */
Integer num=(Integer)intList.get(i);
}
}
}
(2)引入泛型后
public class TestDemo {
@Test
public void test(){
// 创建一个只保存Integer类型的List集合
List<Integer> intList = new ArrayList<Integer>();
intList.add(1);
intList.add(2);
//下面代码将出现编译时异常
intList.add("3");
for (int i = 0; i < intList.size(); i++) {
//下面的代码无需强制类型转换
Integer num=intList.get(i);
}
}
}
1.3. 泛型的好处
使用泛型的好处如下:
-
提高类型的安全性
使用泛型后,将类型的检查从运行期提前到编译期,编译期的类型检查,可以更早、更容易的找出因为类型限制而导致的类型转换异常,从而提高程序的可靠性。
-
消除强制类型转换
使用泛型后,程序会记住当前的类型形参,从而无需对传入的实参值进行强制类型转换。使得代码更加清晰和筒洁,可读性更高。
-
提高代码复用性
使用泛型后,可以更好的将程序中通用的代码提取出来,在使用时传入不同类型的参数,避免了多次编写相同功能的代码,以提高代码的复用性。
-
拥有更高的运行效率
使用泛型之前,传入的实际参数值作为Object类型传递时,需要进行封箱和拆箱操作,会消耗程序的一定的开销。使用泛型后,类型形参中都需要使用引用数据类型,即传入的实际参数的类型都是对应引用数据类型,避免了封箱和拆箱操作,降低了程序运行的开销,提高了程序运行的效率。
2. 泛型类
2.1. 泛型类的语法格式
定义类时,在类名后加上尖括号包含类型形参,定义的这个类就是泛型类。创建泛型类的实例对象时传入不同的类型实参,从而可以动态生成无数个该泛型类的子类。在JDK类包中泛型类的最典型应用就是各种容器类,如ArrayList、HashMap等。定义泛型类的格式具体如下。
public class 类名<类型形参变量>{
}
上述语法格式中,类名<类型形参变量>是一个整体的数据类型,通常称为泛型类型;类型形参变量,没有特定的意义,可以是任意一个字母,但是为了提高可读性,建议使用有意义的字母。一般情况下使用较多的字母及意义如下所示。
-
E:表示Element(元素),常用在java Collection里使用,如 List<E>,Iterator<E>,Set<E>。
-
K,V:表示Key,Value(Map的键值对)。
-
N:表示Number(数字)。
-
T:表示Type(类型),如String,Integer等。
2.2. 泛型类的定义与创建
定义:定义泛型类时,类的构造方法名称还是类的名称,类型形参变量可以用于属性的类型、方法的返回值类型和方法的参数类型。
创建:创建泛型类的对象时,不强制要求传入类型实参,如果传入类型实参,类型形参会根据传入的类型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入类型实参的话,在泛型类中使用类型形参的方法或成员变量定义的类型可以为任何的类型。
(1)定义泛型类Goods,声明私有变量info,定义构造方法,与getter/setter方法。
/**
* 定义泛型类Goods
* @param <T>
*/
public class Goods<T> {
// 类型形参变量作用于属性的类型
private T info ;
//无参构造方法
public Goods(){
}
// 类型形参变量作用于构造方法的参数类型
public Goods(T info) {
this.info = info;
}
// 类型形参变量作用于方法的参数类型
public void setInfo(T info){
this.info = info ;
}
// 类型形参变量作用于方法的返回值类型
public T getInfo(){
return this.info ;
}
}
(2)定义测试类,创建Goods对象,分别调用setInfo()方法和getInfo()方法。
public class TestDemo{
@Test
public void test(){
Goods goods = new Goods();
goods.setInfo("电脑");
System.out.println(goods.getInfo() + ":" + goods.getInfo().getClass());
goods.setInfo(200);
System.out.println(goods.getInfo() + ":" + goods.getInfo().getClass());
}
@Test
public void test2(){
Goods<String> goods = new Goods<>();
goods.setInfo("电脑");
System.out.println(goods.getInfo() + ":" + goods.getInfo().getClass());
}
@Test
public void test3(){
Goods<Integer> goods= new Goods<>();
goods.setInfo(200);
System.out.println(goods.getInfo() + ":" + goods.getInfo().getClass());
}
}
2.3. 泛型类的练习
定义一个泛型类Point<T>,其中包含x和y两个类型为T的成员,定义类的带参构造方法,为x和y定义setter和getter,定义show方法输出坐标。
编写测试方法,创建Point<Integer>对象和Point<Double>对象。
-
Point<T>类
public class Point<T> {
//泛型成员
private T x;
private T y;
//构造方法
public Point(T x,T y){
this.x = x;
this.y = y;
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public T getY() {
return y;
}
public void setY(T y) {
this.y = y;
}
//输出坐标
public void show(){
System.out.println("x坐标是:" + x + ",y坐标是:" + y);
}
}
-
测试类
public class Test {
public static void main(String[] args) {
//Integer型
Point<Integer> p1 = new Point(1,2);
p1.show();
//Double型
Point<Double> p2 = new Point(1.1,2.2);
p2.show();
}
}
3. 泛型接口
3.1. 泛型接口的语法格式
定义泛型接口和定义泛型类的语法格式类似,在接口名称后面加上尖括号包含类型形参即可。集合相关的接口中很多接口也都是泛型接口,如Collection、List等。定义泛型接口的基本语法格式如下所示:
public interface 接口名称<类型形参变量>{
}
3.2. 泛型接口的应用
泛型接口可以有两种类方式实现,第一种是使用非泛型类实现泛型接口,第二种是使用泛型类实现泛型接口。
(1)使用非泛型类实现泛型接口
当使用非泛型类实现接口时,需要明确接口的泛型类型,也就是需要将类型实参传入到接口中。此时实现类重写接口中使用泛型的地方,都需要将类型形参替换成传入的类型实参,这样可以直接使用泛型接口的类型实参,具体代码如下所示。
-
定义一个泛型接口
public interface Student<T> {
public abstract void show(T t);
}
-
定义泛型接口的实现类,在泛型接口后指定类型实参以明确接口的泛型类型。
public class StudentImpl implements Student<String>{
@Override
public void show(String s) {
System.out.println(s);
}
}
-
定义测试类,创建Student对象时,传入的类型实参必须是String类型,否则编译异常。
public class TestDemo {
@Test
public void test(){
Student<String> stu = new StudentImpl();
stu.show("你好,我是张三");
}
}
(2)使用泛型类实现泛型接口
当使用泛型类实现泛型接口时,需要将泛型的声明加在实现类中,并且泛型类和泛型接口使用的都是同一个类型形参变量,否则会出现编译异常。具体代码如下所示。
-
定义一个泛型接口
public interface Student<T> {
public abstract void show(T t);
}
-
定义泛型接口的实现类,使用泛型类实现泛型接口
public class StudentImpl<T> implements Student<T> {
@Override
public void show(T t) {
System.out.println(t);
}
}
-
定义测试类,创建Student对象时,传入不同的类型实参,并分别调用show()方法进行输出验证。
public class TestDemo {
@Test
public void test(){
Student<String> stu1 = new StudentImpl<>();
stu1.show("你好,我是张三");
Student<Integer> stu2 = new StudentImpl<>();
stu2.show(20);
}
}
4. 泛型方法
4.1. 泛型方法的语法格式
泛型方法是将类型形参的声明放在修饰符和返回类型之间的方法。在Java程序中,定义泛型方法常用的格式如下所示:
public [static] [final] <类型形参> 返回值类型 方法名 (形式参数列表){
}
定义泛型方法注意事项如下所示:
-
访问权限修饰符(包括private、public、protected)、static和 final都必须写在类型形参列表的前面。
-
返回值类型必须写在类型形参列表的后面。
-
泛型方法可以在泛型类中,也可以在普通类中。
-
泛型类中的任何方法本质上都是泛型方法,所以在实际使用中很少会在泛型类中再用上面的形式来定义泛型方法。
-
类型形参可以用在方法体中修饰局部变量,也可以修饰方法的返回值。
-
泛型方法可以是实例方法(没有用static修饰,也叫非静态方法)也可以是静态方法。
4.2. 泛型方法的应用
如果泛型方法是实例方法,则需要使用对象名进行调用;如果泛型方法是静态方法,可以使用类名进行调用,泛型方法的两种使用方式。
-
方式一
对象名|类名.<类型实参> 方法名(类型实参列表);
-
方式二
对象名|类名.方法名(类型实参列表);
两种调用泛型方法的差别在于,方法名之前是否显式地指定了类型实参。调用时是否需要显式地指定了类型实参,要根据泛型方法的声明形式,以及调用时编译器能否从实际参数表中获得足够的类型信息决定,如果编译器能够根据实际参数推断出参数类型,就可以不指定类型实参,反之则需要指定类型实参。
4.3. 泛型方法的练习
下面通过一个案例,演示泛型方法的定义与使用,具体代码如下所示。
(1)定义Student类,在类中定义一个静态泛型方法和一个普通泛型方法。
public class Student {
// 静态泛型方法
public static <T> void show(T t) {
System.out.println(t + ":" + t.getClass());
}
// 普通泛型方法
public <T> void study(T t) {
System.out.println(t + ":" + t.getClass());
}
}
(2)定义测试方法,调用方法测试结果。
public class TestDemo {
@Test
public void test(){
//静态方法
Student.show("你好,我是张三"); // 使用方式一调用静态的泛型方法
Student.<String>show("你好,我是张三"); // 使用方式二调用静态的泛型方法
//普通方法
Student stu = new Student();
stu.study("好好学习"); // 使用方式一调用普通的泛型方法
stu.<String>study("好好学习"); // 使用方式二调用普通的泛型方法
}
}
从运行结果可以得出,泛型方法可以在非泛型类中定义,并且在调用泛型方法的时候确定泛型的具体类型 。上述结果中虽然使用方式一和方式二的输出结果一致,但是方式一隐式的传入类型实参,不能直观的查看到调用的方法是泛型方法,不利于代码的阅读和维护,通常建议使用第二种方式调用泛型方法。
5. 类型通配符
类型通配符使用一个问号(?)表示,类型通配符可以匹配任何类型的类型实参。
下面使用一个案例演示类型通配符的使用。
(1)定义泛型类Student,声明私有变量info,定义有参构造方法和getter方法。
/**
* 定义泛型类Student
* @param <T>
*/
public class Student<T> {
private T info;
public Student(T info) {
this.info = info;
}
public T getInfo() {
return info;
}
}
(2)定义测试方法,创建Student对象,分别传入String类型和Integer类型的类型实参,进行测试。
public class TestDemo {
@Test
public void test(){
// 创建student对象,传入String类型的类型实参
Student<?> student = new Student<String>("张三");
System.out.println( student.getInfo()+":"+student.getInfo().getClass());
// 创建student对象,传入Integer类型的类型实参
student =new Student<Integer>(20);
System.out.println( student.getInfo()+":"+student.getInfo().getClass());
}
}
-
不使用通配符的情况一
如果创建Student对象时,不使用类型通配符,而是使用指定的类型实参,会出现编译异常,具体如下图所示。
-
不使用通配符的情况二
使用Object代替类型通配符?接收所有的类型,也会出现编译异常,具体如下图所示。