Java 泛型使用

泛型是Java中一项十分重要的特性,在Java 5版本被引入,在日常的编程过程中,有很多依赖泛型的场景,尤其是在集合容器类的使用过程中,更是离不开泛型的影子。

泛型的作用

泛型提供的功能有:参数化类型,以及编译期类型检查。

1 参数化类型

在方法的定义中,方法的参数称为形参,在实际调用方法时传递实参。泛型的使用中,可以将类型定义为一个参数,在实际使用时再传递具体类型。将泛型这种使用方式称之为参数化类型。

在集合类的使用中,若不使用泛型,则需要对每一种元素类型设计相同的集合操作,例如:

class ListInteger{
    //...
}
class ListDouble{
    //...
}

通过泛型的使用,可以避免这种重复定义的现象,定义一套集合操作,来应对所有元素类型,例如:

class List<E>{
    //...
}

在使用中传递不同的元素类型给List即可。

这里使用的字符E并无特殊含义,只是为了便于理解而已。泛型中通常使用的字符及表示意义为:
K: 键值对中的key
V: 键值对中的value
E: 集合中的element
T: 类的类型type

2 编译期类型检查

对于集合ArrayList而言,若不指定具体元素类型,则使用过程中可能出现以下情况:

List list = new ArrayList();
list.add("abc");
list.add(123);

for (Object obj : list) {
    String e = (String) obj;//ClassCastException
}

这段代码在编译期没问题,运行时会报出java.lang.ClassCastException

这种对集合的使用方式存在两个问题:一是add添加元素时,因为元素声明为Object类型,任意类型元素都可以添加到集合中,所以在添加元素时需要使用者自己注意选择的元素类型;二是get取元素时需要强制类型转换,需要开发人员记住操作的元素类型,否则可能抛出ClassCastException异常。

在声明集合时指定元素类型则可以避免以上两种问题:

List<String> list = new ArrayList<String>();
list.add("abc");
//list.add(123); compile error

for (String obj : list) {
    String e = obj;
}

通过泛型的使用,指定集合元素的类型,则可以在编译期就进行元素类型检查,并且get获取元素时无需进行强制类型转换。

这里称获取元素无需进行强制类型转换,其实并不准确,严格来讲,使用泛型在进行获取元素操作时,进行的是隐式类型转换,所以仍然存在强制类型转换的操作。

ArrayList中的隐式类型转换:

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

泛型的使用

泛型可以应用于定义泛型类、泛型接口和泛型方法。

1 泛型类

泛型类的定义方式较为简单,通过将类型抽象为参数,附加在类名称后,即可完成泛型类的定义,示例:

public class Test {
    public static void main(String[] args) {
        User<Integer> user = new User<>();
        user.setAttribute(123);
//        user.setAttribute("abc");compile error
        Integer attribute = user.getAttribute();
    }
}

class User<T> {
    private T attribute;

    public User() {
    }

    public T getAttribute() {
        return this.attribute;
    }

    public void setAttribute(T attribute) {
        this.attribute = attribute;
    }
}

通过使用泛型类,可以在编译期进行参数类型检查,并且使用时无需进行强制类型转换。

2 泛型接口

泛型接口的使用与泛型类较为相似,在接口名称后添加表示类型的字符即可,示例:

interface Person<T> {
    T getAttribute();

    void setAttribute(T attribute);
}
3 泛型方法

在前面的泛型类中定义的如下方法:

    public T getAttribute() {
        return this.attribute;
    }

    public void setAttribute(T attribute) {
        this.attribute = attribute;
    }

虽然使用了参数化类型,但是并不算是泛型方法,因为这些方法中使用的参数类型是泛型类定义的。泛型方法中定义了自己使用的类型,示例:

public <T> void genericsMethod(T parameter){
    //...
}

泛型与继承

在泛型的使用中,关于继承方面需要注意,示例:

public class Test {
    public static void main(String[] args) {
        A<Number> aNumber = new A<>();
        A<Integer> aInteger = new A<>();
//        aNumber = aInteger; compile error
        System.out.println(aNumber.getClass() == aInteger.getClass()); // true
    }
    static class A<T>{}
}

虽然IntegerNumber的子类型,但是A<Integer>并不是A<Number>的子类型。

事实上,编译器会在编译阶段进行类型检查后,会擦除泛型的类型信息,也就是说在运行期A<Integer>A<Number>是同一个类。

对于泛型容器类List<E>,在进行泛型擦除后,记录的元素类型为其声明的最左边父类型,此处即为Object类型,示例:

public class Test {
    public static void main(String[] args) throws Exception {
        List<Integer> integers = new ArrayList<>();
        integers.getClass().getDeclaredMethod("add", Object.class).invoke(integers, "abc");
    }
}

代码在编译期和运行期都没问题,在编译生成的.class文件中,Integer元素类型被擦除后,容器的元素类型记录为Object类型。


泛型使用中的继承定义方式如下:

public class Test {
    public static void main(String[] args) {
        A<Integer> a = new A<>();
        B<Integer> b = new B<>();
        a = b;
    }
}
class A<T>{}
class B<T> extends A<T>{}

在继承关系中使用同一个参数类型,以此实现泛型类的继承。在JDKArrayList<E>List<E>Collection<E>采用的就是这种方式。

但是这种继承方式依然不能满足前面提到的使用场景,例如如下使用List方式:

public class Test {
    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();
//        numberList = integerList; compile error
    }
}

虽然IntegerNumber的子类型,但List<Integer>却不是List<Number>的子类型,问题与前面的示例中相同。

通配符

通配符号?是一种实参类型,表示类型不确定的意思,或者表示任意一种类型,选择?作为类型的目的是为了匹配更大范围的类型,所以这里?是一种具体的类型。

这里称?类型不确定,又称?是一种具体的类型,这种说法是相对于前面的类型参数T而言的,T表示类型形参,使用时被替代为传入的具体类型,而?就是一种具体类型,不会被别的具体类型替代。

在前面有关泛型的继承关系中,遇到List<Integer>不是List<Number>的子类型问题,可以使用通配符号?表示具体类型,这样则可以匹配任意的参数类型,示例:

public class Test {
    public static void main(String[] args) {
        List<?> numberList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();
        numberList = integerList; 
    }
}

既然?可以表示所有类型,当然也可以表示Integer类型,所以代码可以编译通过。

在平常的使用中,类型的选择范围并非如此随意,更多时候在定义泛型类、接口或方法时,限定了能够使用的类型范围。

1 限定上界

使用extends关键字限定参数类型能够选择的上界,示例:

public class Test {
    public static void main(String[] args) {
        GenericsClass<Integer> integerObj = new GenericsClass<>();
//        GenericsClass<String> stringObj = new GenericsClass<>(); compile error
        
        Test.genericsMethod1(new ArrayList<Integer>());
//        Test.genericsMethod1(new ArrayList<String>()); compile error

        Test.genericsMethod2(new ArrayList<Integer>());
//        Test.genericsMethod2(new ArrayList<String>()); compile error
    }
    static class GenericsClass<T extends Number>{
        //...
    }
    static <T extends Number> void genericsMethod1(List<T> list) {
//        list.add(1); compile error
    }
    static void genericsMethod2(List<? extends Number> list) {
//        list.add(1); compile error
    }
}

GenericsClass类中通过<T extends Number>限定参数类型为Number的子类型,genericsMethod1、genericsMethod2同样使用extends关键字限定类型上界。

genericsMethod1genericsMethod2分别使用了T?作为参数类型符号,在限定类型范围上,两者作用相同。不同之外在于,使用T表示类型形参,在genericsMethod1方法体内可以引用T类型相关的操作,但是?则无法引用。

这里需要注意一点,若使用具有上界的泛型来作为集合的元素类型时,因为此时无法确定集合的元素类型,所以无法向集合中添加元素,示例:

    static <T extends Number> void genericsMethod1(List<T> list) {
//        list.add(1); compile error
    }
    static void genericsMethod2(List<? extends Number> list) {
//        list.add(1); compile error
    }
2 限定下界

使用super关键字限定参数类型能够选择的下界,示例:

public class Test {
    public static void main(String[] args) {
        Test.genericsMethod2(new ArrayList<Integer>());
//        Test.genericsMethod2(new ArrayList<String>()); compile error
    }
//    static class GenericsClass<? super Integer>{ compile error
//        //...
//    }
//    static <T super Integer> void genericsMethod1(List<T> list) { compile error
//        //...
//    }
    static void genericsMethod2(List<? super Integer> list) {
        list.add(1); 
    }
}

由示例可知,<? super Integer>的形式限定元素的下界为Integer类型,则此时可以对集合进行添加Integer元素操作。

由示例同样可知,使用super关键字限定参数类型下界,与使用extends关键字限定参数类型的上界有所不同,最大的区别就是:类型形参T不能与super关键字配合使用。若可以配合使用,则会存在以下问题:

  • <T extends Integer>表示T类为Integer的子类型,则T类型属性可以访问Integer类型中的部分属性;<T super Integer>的描述表示T类为Integer的父类,则T类型属性不确定其父类为何类,也可能为Serializable,那么此时将不具备任何属性,因为不确定,所以无法进行操作;

  • <T extends Integer>在编译时进行类型擦除后,则T属性将默认为extends继承的父类中最左边一个,这里即为Integer;而<T super Integer>描述的类,在进行类型擦除后将无法确定其类型。

根据以上两点,在类的描述中,不能使用<T super Integer>的形式限定参数类型的下界。

通配符的上下界使用有PECS(producer extends, consumer super)原则,producer可以根据上界进行元素读取,但是不确定类型,所以无法添加元素;consumer可以根据下界进行元素添加,但是不确定类型,所以无法读取元素。

泛型数组

在普通数组的使用中,存在如下的情况:

public class Test {
    public static void main(String[] args) {
        Integer[] integers = new Integer[5];
        Object[] objects = integers;
        objects[0] = "abc";
    }
}

这段代码在编译期是没问题的,在运行时会报出ArrayStoreException异常。这种情况称之为数组的协变(covariant),即S类型为T类型的子类型,则S类型数组为T类型数组的子类型。

为了避免这种协变的情况发生,Java禁止创建具体类型的泛型数组,否则对于泛型数组有如下情况,示例来源Java 指导手册

// Not really allowed.
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;

// Run-time error: ClassCastException.
String s = lsa[1].get(0);

如果Java中允许创建具体类型的泛型数组,则以上代码在编译期通过类型检查,在运行期获取元素时会报出ClassCastException异常,即无法通过泛型元素的隐式类型转换。

Java虽然禁止创建具体类型的泛型数组,但并不禁止创建通配符形式的数组,如下所示,示例来源Java 指导手册

// OK, array of unbounded wildcard type.
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);

虽然发生运行期错误,但是因为通配符的使用,所以在获取元素时,需要进行显示类型转换,也就是将元素的类型操作交给开发人员进行控制。

参考

Type Parameters
Difference between <? super T> and <? extends T> in Java
The Java™ Tutorials

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容