`

泛型趣谈

阅读更多

Java中的泛型带来了什么好处?规约。就像接口定义一样,可以帮助对于泛型类型和对象的使用上,保证类型的正确性。如果没有泛型的约束,程序员大概需要在代码里面使用大量的类型强制转换语句,而且需要非常清楚没有标注的对象实际类型,这是容易出错的、恼人的。但是话说回来,泛型可不只有规约,还有很多有趣的用法,容我一一道来。

 

泛型擦除

Java的泛型在编译阶段实际上就已经被擦除了(这也是它和C#泛型最本质的区别),也就是说,对于使用泛型的定义,对于编译执行的过程,并没有任何的帮助(有谁能告诉我为什么按着泛型擦除来设计?)。所以,单纯利用泛型的不同来设计接口,会遇到预期之外的问题,比如说:

1
2
3
4
public interface Builder<K,V> {
    public void add(List<K> keyList);
    public void add(List<V> valueList);
}

想这样设计接口?仅仅靠泛型类型的不同来设计重载接口?那是痴人说梦。但是如果代码变成这样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GenericTypes {
 
    public static String method(List<String> list) {
        System.out.println("invoke method(List<String> list)");
        return "";
    }
 
    public static int method(List<Integer> list) {
        System.out.println("invoke method(List<Integer> list)");
        return 1;
    }
 
    public static void main(String[] args) {
        method(new ArrayList<String>());
        method(new ArrayList<Integer>());
    }
}

这个情况就有点特殊了,Sun的Javac编译器居然可以通过编译,而其它不行,这个例子来自IcyFenix的文章,有兴趣不妨移步参阅IcyFenix的文章以及下面的讨论

 

方法泛型

在JDK的java.util.List接口里面,定义了这样一个方法:

1
2
3
public interface List<E> extends Collection<E> {
    <T> T[] toArray(T[] a);
}

事实上,这个方法泛型T表示的是任意类型,它可是和此例中的接口/类泛型E毫不相干啊。

如果我去设计方法,我可能写成这样:

1
<T> T[] toArray();

其实这个T[ ] a参数的作用也容易理解:

  1. 确定了数组类型;
  2. 如果给定的数组a能够容纳得下结果,就会把结果放进a里面(JDK的注释有说明“If the list fits in the specified array, it is returned therein.”),同时也把a返回。

如果没有这个T[ ] a参数的话,光光定义一个方法泛型<T>是没有任何意义的,因为这个T是什么类型完全是无法预料的,例如:

1
2
3
4
5
6
7
8
9
public class Builder {
    public <E> E call(){
        return null;
    }
     
    public static void main(String[] args) {
        String s = new Builder().call(); // ①
        Integer i = new Builder().call(); // ②
        new Builder().<String>call(); // ③
1
2
    }
}

可以看到,call()方法返回的是类型E,这个E其实没有任何约束,它可以表示任何对象,但是代码上不需要强制转换就可以赋给String类型的对象s(①),也可以赋给Integer的对象i(②),甚至,你可以主动告知编译器返回的对象类型(③)。

 

链式调用

看看如下示例代码:

1
2
3
4
5
6
7
8
9
public class Builder<S> {
    public <E> Builder<E> change(S left, E right){
        // 省略实现
    }
     
    public static void main(String[] args) {
        new Builder<String>().change("3", 3).change(3, 3.0f).change(3.0f, 3.0d);
    }
}

同样一个change方法,接收的参数变来变去的,上例中方法参数从String-int变到int-float,再变为float-double,这样的泛型魔法在设计链式调用的方法的时候,特别是定义DSL语法的时候特别有用。

 

使用问号 

其实问号帮助表示的是“通配符类型”,通配符类型 List<?> 与原始类型 List 和具体类型 List<String>都不相同,List<?>表示这个list内的每个元素的类型都相同,但是这种类型具体是什么我们却不知道。注意,List<?>和List<Object>可不相同,由于Object是最高层的超类,List<Object>表示元素可以是任何类型的对象,但是List<?>可不是这个意思。

来看一段有趣的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Wrapper<E> {
    private E e;
    public void put(E e) {
        this.e = e;
    }
     
    public E get(){
        return e;
    }
}
 
public class Builder {
    public void check(Wrapper<?> wrapper){
        System.out.println(wrapper.get()); // ①
        wrapper.put(new Object()); // ② wrong!
        wrapper.put(wrapper.get()); // ③ wrong!
        wrapper.put(null); // ④ right!
    }
}

Wrapper的类定义里面指定了它包装了一个类型为E的对象,但是在另一个使用它的类Builder里面,指定了Wrapper的泛型参数是?,这就意味着这个被包装的对象的类型是完全不可知的:

  • 现在我可以调用Wrapper的get方法把对象取出来看看(①),
  • 但是我不能放任意类型确定的对象进去,Object也不行(②),
  • 即便是从wrapper里面get出来也不行(编译器太不聪明了是吧?③)
  • 可奇葩的是,放一个null是可以被允许的,因为null根本就不是任何一个类型的对象(④,注意,不能放int这类的原语类型,虽然它不是对象,但因为它会被自动装箱成Integer,从而变成具体类型,所以是会失败的)

现在思考一下,如果要表示这个未知对象是某个类的子类,上面代码的Wrapper定义不变,但是check方法写成:

1
public void check(Wrapper<? extends String> wrapper){
1
wrapper.put(new String());
1
}

这样呢?

……

依然报错,因为new String()确实是String的子类(或它自己)的对象,一点都没错,但是它可不见得和同为String子类(或它自己)的“?”属于同一个类型啊!

如果写成这样呢(注意extends变成了super)?

1
public void check(Wrapper<? super String> wrapper){
1
wrapper.put(new String());
1
}

这次对了,为什么呢?

……

因为wrapper要求put的参数“?”必须是String的父类(或它自己),而不管这个类型如何变化,它一定是new String()的父类(或它自己)啊!

 

泛型递归

啥,泛型还能递归?当然能。而且这也是一种好玩的泛型使用:

1
2
3
4
5
6
7
8
class Wrapper<E extends Wrapper<E>> implements Comparable<Wrapper<E>> {
 
    @Override
    public int compareTo(Wrapper<E> wrapper) {
        return 0;
    }
 
}

好玩吧?泛型也能递归。这个例子指的是,一个对象E由包装器Wrapper所包装,但是,E也必须是一个包装器,这正是包装器的递归;同时,包装器也实现了一个比较接口,使得两个包装器可以互相比较大小。

 

别晕!泛型只不过是一个普普通通的语言特性,但是也挺有趣的。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

5
0
分享到:
评论
3 楼 xyw123 2014-06-12  
[list]
[*[flash=200,200][/flash
引用
引用
[u][i][b][u]
引用
引用
[list]
[*][img][list]
[*][*][url][*][*][*][*]
[/url] [*][/list][/img] [/list]
[/u][/b][/i][/u]
]]
[/list]
2 楼 RayChase 2014-01-09  
kidneyball 写道

1. 为什么要按着类型擦除来设计。据我所知,Java1.5引入泛型的最大压力来自于没有泛型的容器API相比起C++的标准模板库来太难用,太多不必要的显式转型,完全违背了DRY原则也缺乏精细的类型检查。但Java与C++不同,C++的对象没有公共父类,不使用泛型根本无法建立一个能存放所有类型的容器,所以必须在费大力气在编译后的运行代码中支持泛型,保留泛型信息自然是顺水推舟。而Java所有对象都有一个共同父类Object,当时已有的容器实现已经在运行期表现良好。所以Sun的考虑是加入一层简单的编译期泛型语法糖进行自动转换和类型检查,而在编译后的字节码中则擦除掉泛型信息,仍然走Object容器的旧路。这种升级方案对jdk的改动是最小的,Runtime根本不用改,改编译器就行了。

谢谢,这一点我补充进去。
1 楼 kidneyball 2014-01-09  
我补充一些:
1. 为什么要按着类型擦除来设计。据我所知,Java1.5引入泛型的最大压力来自于没有泛型的容器API相比起C++的标准模板库来太难用,太多不必要的显式转型,完全违背了DRY原则也缺乏精细的类型检查。但Java与C++不同,C++的对象没有公共父类,不使用泛型根本无法建立一个能存放所有类型的容器,所以必须在费大力气在编译后的运行代码中支持泛型,保留泛型信息自然是顺水推舟。而Java所有对象都有一个共同父类Object,当时已有的容器实现已经在运行期表现良好。所以Sun的考虑是加入一层简单的编译期泛型语法糖进行自动转换和类型检查,而在编译后的字节码中则擦除掉泛型信息,仍然走Object容器的旧路。这种升级方案对jdk的改动是最小的,Runtime根本不用改,改编译器就行了。

2. 关于通配符,它的主要作用是用来在方法接口上接受容器泛型参数,结合这点,一些古怪的行为就很好理解了。容器与泛型通配符结合的效果是: 通配符所指定的继承关系会传递到容器上! 比如说你写了个方法
void foo(List<Number> numbers);
如果你调用 foo(new ArrayList<Integer>())就会出错。因为虽然Integer是Number的子类,但List<Integer>不是List<Number>的子类。

那么如果你的方法希望接受一个以Number的任意一个子类为元素的List,就需要这样写:
void foo(List<? exends Number> numbers);

这时,调用foo(new ArrayList<Integer>())或foo(new ArrayList<Float>())都没有问题。

但新的问题来了,如果你的foo方法是这样写的:
void foo(List<? extends Number> numbers) {
	numbers.add(0.5);  //事实上不可能,会编译出错
}

看上去应该是合法的。但这个方法被另一个人使用时,他就可能会传进来一个List<Integer>。别忘了,在运行期泛型信息被擦除了,因此Java Runtime根本不会进行类型检查,只能把0.5硬塞到这个List<Integer>里(在Runtime看来都是无泛型的List),结果以后读取这个list时,就会拿到个0.5并试图强制转型为Integer,发生转型错误。为了杜绝此类错误,Java作出的限制就是,对使用通配符来指定的泛型方法参数,不允许接受具体的传入值。实际效果就是,除了显式传入null常量,其他情况这个方法就没法用了,编译都过不了。别忘了,泛型检查只会在编译期执行。例如
interface List<E> ... {
	boolean add(E e)
}


当使用List<? extends Number>时,E被指定为<? extends Number>,则所有接受E为参数的方法,除了可以传入null常量外,所有传入具体值的调用都无法通过编译。

相关推荐

    C#泛型类、泛型方法、泛型接口、泛型委托的实例

    本文将深入探讨泛型类、泛型方法、泛型接口和泛型委托,并通过实例来阐述它们的应用。 首先,我们来看泛型类。泛型类是具有一个或多个类型参数的类。类型参数是在定义类时使用的占位符,实际的类型在创建类的实例时...

    泛型dao 泛型dao 泛型dao

    Struts2、Hibernate、Spring整合的泛型DAO (本人评价: 代码开发效率提高30% 代码出错率减少70%) 对于大多数开发人员,系统中的每个 DAO 编写几乎相同的代码到目前为止已经成为一种习惯。虽然所有人都将这种重复...

    【Flutter】Dart 泛型 ( 泛型类 泛型方法 特定类型约束的泛型 ).zip

    【Flutter】Dart 泛型 ( 泛型类 | 泛型方法 | 特定类型约束的泛型 ) https://hanshuliang.blog.csdn.net/article/details/114059611 博客源码快照

    泛型java的泛型知识,非常有用

    Java 泛型是一种强大的语言特性,自JDK 5.0引入以来,极大地提升了代码的类型安全性以及重用性。泛型允许我们在类、接口和方法中使用类型参数,这样在编译时期就能检查类型匹配,减少运行时类型转换异常。 1. **...

    JAVA-泛型课件-泛型课件

    泛型是Java语言的一个重要特性,首次出现在Java SE 1.5版本中。它的引入主要是为了解决在集合操作中类型安全性的问题,并通过引入参数化类型的概念,提高了代码的复用性与可读性。 ### 泛型概念 泛型,即参数化...

    泛型学习和泛型接口和泛型经典示例

    泛型学习和泛型接口和泛型经典示例

    关于java基础的泛型的练习

    Java泛型是Java SE 5.0引入的一个重要特性,它极大地增强了代码的类型安全性和可读性。泛型在编程中的应用广泛,特别是在集合框架中,使得我们可以在编译时就检查类型,避免了不必要的类型转换,并且提高了代码的...

    VC++ 2005:泛型编程

    【VC++ 2005:泛型编程】 泛型编程是C++/CLI中的一种重要特性,允许程序员创建可重用的代码,这些代码能够处理多种数据类型,而无需每次都复制和修改代码。泛型编程的核心思想是参数化类型,即将数据类型作为一个...

    c#泛型类、泛型方法、泛型接口、泛型委托

    泛型主要分为四个关键部分:泛型类、泛型方法、泛型接口和泛型委托。下面将详细介绍这四个方面。 1. 泛型类: 泛型类是具有一个或多个类型参数的类。这些类型参数是占位符,代表一种未知的数据类型,直到在创建类...

    java泛型指南 经典

    ### Java泛型指南经典知识点解析 #### 一、引言 Java 1.5 版本引入了一系列重要的语言特性,其中“泛型”是其中一项关键特性。泛型的引入极大地提高了 Java 语言的类型安全性和代码重用性。本文档旨在深入探讨 ...

    泛型笔记学习2009

    ### 泛型基础知识与应用详解 #### 泛型概述 泛型是Java语言的一个重要特性,它主要解决了在集合框架中频繁出现的类型转换问题,同时也增强了类型安全性。泛型的概念最早出现在Java 5.0版本中,它允许开发者创建能够...

    C#泛型,非泛型实现枚举

    在C#编程中,泛型和枚举是两种非常重要的概念。泛型提供了一种方式,使得代码可以处理多种数据类型,而无需为每种类型重复编写相同逻辑。枚举则是用于定义一组预定义的常量,便于表示一组相关的值。下面我们将详细...

    Generic_2(泛型类-泛型方法-泛型接口-泛型限定(上限)

    泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是...

    gson解析泛型和将泛型转为json字符串

    本文将深入探讨如何使用Gson处理泛型,并将其转换为JSON字符串。 首先,理解泛型在Java中的作用是至关重要的。泛型允许我们在类、接口和方法中使用类型参数,从而提高了代码的类型安全性和重用性。当我们使用Gson与...

    SSH泛型代码实例

    SSH泛型代码实例是关于Java编程中的一种常见技术——Spring、Struts和Hibernate(SSH)框架结合使用泛型的应用示例。泛型是Java SE 5.0引入的一个重要特性,它允许在编译时检查类型安全,并且所有的强制转换都是自动...

    C#泛型集合与非泛型集合

    ### C# 泛型集合与非泛型集合详解 #### 一、概述 在.NET Framework Class Library (FCL) 中,提供了丰富的集合类型,这些集合类型是编程中不可或缺的工具。根据是否支持泛型特性,这些集合大致可以分为两类:泛型...

    java 1.5泛型详解

    Java泛型是自Java 1.5版本引入的一项重要特性,极大地提高了代码的类型安全性和重用性。本文将深入探讨Java泛型的概念、优点、使用方式及其在实际开发中的应用。 **一、泛型的基本概念** 泛型是Java语言中的一种...

    c# 泛型的使用,教你如何用泛型

    7. **泛型接口和泛型类的组合**:你可以在泛型类中实现泛型接口,也可以在非泛型类中实现泛型接口,进一步增强代码的复用性。 8. **泛型和多态**:泛型是多态的一种形式,因为它允许你用一种类型安全的方式处理不同...

Global site tag (gtag.js) - Google Analytics