首页 > 编程笔记 > Java笔记 阅读:1

Java泛型的用法(非常详细)

泛型实质上就是使程序员定义安全的类型。

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

回顾向上转型与向下转型

在介绍泛型之前,先来看一个例子。在项目中创建 Test 类,在该类中将基本类型向上转型为 Object 类型,具体代码如下:
public class Test {
    private Object b;
    public Object getB() {
        return b;
    }
    public void setB(Object b) {
        this.b = b;
    }
    public static void main(String[] args) {
        Test t = new Test();
        t.setB(Boolean.valueOf(true)); // 向上转型操作
        System.out.println(t.getB());
        t.setB(Float.valueOf("12.3")); // 向下转型操作
        Float f = (Float) t.getB();
        System.out.println(f);
    }
}
运行结果如下:

true
12.3

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

Java定义泛型类

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

为了提前预防这种问题,Java 提供了泛型机制。其语法如下:
类名<T>
其中,T 是泛型的名称,代表某一种类型。开发者在创建该类对象时需要指定 T 代表哪种具体的类型。如果不指定具体类型,T 则采用 Object 类型。

【实例】创建带泛型的 Book 图书类,为 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>(49.9);
        //创建参数为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基础教程(从入门到精通)》
作者:严长生
价格:49.9
是否附赠源码?true

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

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

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


图 1 不按照泛型指定的类型进行赋值引起的编译错误

Java泛型的常规用法

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

在定义泛型类时,可以声明多个类型。语法如下:
class MyClass<T1,T2>{ }
其中,T1 和 T2 为可能被定义的类型。

这样,在实例化指定类型的对象时就可以指定多个类型。例如:
MyClass<Boolean,Float> m = new MyClass<Boolean,Float>();

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

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

【实例】定义泛型数组。在项目中创建 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]);
        }
    }
}
运行结果如下:

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

可见,可以在使用泛型机制时声明一个数组,但是不可以使用泛型来建立数组的实例。例如,下面的代码就是错误的:
public class ArrayClass<T> {
    private T[] array = new T[10]; //错误
}

3) 集合类声明容器的元素

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

常用的被泛型化的集合类如下表所示:

表:常用的被泛型化的集合类
集合类 泛型定义
ArrayList ArrayList<E>
HashMap HashMap<K,V>
HashSet HashSet<E>

【实例】使用泛型约束集合的元素类型。在项目中创建 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));
        }
    }
}
运行结果如下:

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

Java泛型的高级用法

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

1) 限制泛型可用类型

默认可以使用任何类型来实例化一个泛型类对象,但 Java 中也对泛型类实例的类型进行了限制。语法如下:
class 类名<T extends anyClass>
其中,anyClass 指某个接口或类。

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

【实例】限制泛型的类型必须为 List 的子类,在项目中创建 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>();
    }
}
在上面这个实例中,设置泛型类型必须实现 List 接口。例如,ArrayList 类和 LinkedList 类都实现了 List 接口,而 HashMap 类没有实现 List 接口,因此在这里不能实例化 HashMap 类型的泛型对象。

当没有使用 extends 关键字限制泛型类型时,默认 Object 类下的所有子类都可以实例化泛型对象。下面的两个语句是等价的:
public class a<T>{
    //...
}
public class a<T extends Object >{
    //...
}

2) 使用类型通配符

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

使用泛型类型通配符的语法如下:
泛型类名称<? extends List> a = null;
其中,<? extends List> 表示类型未知,当需要使用该泛型对象时,可以单独对其进行实例化。例如:
A<? extends List> a = null;
a = new A<ArrayList>();
a = new A<LinkedList>();
如果实例化没有实现 List 接口的泛型对象,编译器将会报错。例如,实例化 HashMap 对象时,编译器将会报错,因为 HashMap 类没有实现 List 接口。

除了可以实例化一个限制泛型类型的实例,还可以将该实例放置在方法的参数中。例如:
public void doSomething(A<? extends List> a){}
在上述代码中,定义方式有效地限制了传入 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)); //获取集合中第一个值
在上面的例子中,List<?> 类型的对象可以接受 String 类型的 ArrayList 集合,也可以接受 Integer 类型的 LinkedList 集合。

也许有的读者会有疑问,“List<?> l2 = l1”语句与“List l2 = l1”语句存在何种本质区别?这里需要注意的是,对于使用通配符的对象不能向其中加入新的信息,只能从其中获取或删除信息。例如:
l1.set(0,"成员改变"); //没有使用通配符的对象调用set()方法时,该方法能被调用
//l2.set(0, 1); //使用通配符的对象调用set()方法时,该方法不能被调用
//l3.set(0, 1);
l2.get(0); //可以使用l2的实例获取集合中的值
l2.remove(0); //根据键名删除集合中的值
从上述代码中可以看出,由于对象 l1 是没有使用 A<? >这种形式初始化出来的对象,因此它可以调用 set() 方法来改变集合中的值,但 l2 与 l3 对象则是通过使用通配符的方式创建出来的,因此不能改变集合中的值。

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

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

定义为泛型的类和接口也可以被继承与实现。例如,让 SubClass 类继承 ExtendClass 的泛型,代码如下:
class ExtendClass<T1>{}
class SubClass<T1,T2,T3> extends ExtendClass<T1>{}

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

定义为泛型的接口也可以被实现。例如,让 SubClass 类实现 SomeInterface 接口,并继承接口的泛型,代码如下:
interface SomeInterface<T1>{}
class SubClass<T1,T2,T3> implements SomeInterface<T1>{}

总结

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

相关文章