`
lgl669
  • 浏览: 173496 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

浅谈Java泛型编程

    博客分类:
  • java
阅读更多

浅谈Java泛型编程

1 引言在JDK 1.5中,几个新的特征被引入Java语言。其中之一就是泛型(generics)。泛型(generics,genericity)又称为“参数类型化(parameterized type)”或“模板(templates)”,是和继承(inheritance)不同而互补的一种组件复用机制。继承和泛型的不同之处在于——在一个系统中,继承层次是垂直方向,从抽象到具体,而泛型是水平方向上的。当运用继承,不同的类型将拥有相同的接口,并获得了多态性;当运用泛型,将拥有许多不同的类型,并得以相同的算法作用在它们身上。因此,一般说来,当类型与实现方法无关时,使用泛型;否则,用继承。

泛型技术最直接联想到的用途就是建立容器类型。下面是一个没有使用泛型技术的例子: List myIntList = new LinkedList();// 1 myIntLikst.add(new Integer(0));// 2 Integer x = (Integer)myIntList.iterator().next();// 3 显然,程序员知道究竟是什么具体类型被放进了myIntList中。但是,第3行的类型转换(cast)是必不可少的。因为编译器仅仅能保证iterator返回的是Object类型。要想保证将这个值传给一个Integer类型变量是安全的,就必须类型转换。除了使代码显得有些混乱外,类型转换更带来了运行时错误的可能性。因为程序员难免会犯错误。使用了泛型技术,程序员就可以确切地表达他们的意图,并且把myIntList限制为包含一种具体类型。下面就是前一个例子采用了泛型的代码段: List<Integer> myIntList = new LinkedList<Integer>();// 1 myIntLikst.add(new Integer(0));// 2 Integer x = myIntList.iterator().next();// 3 List<Integer>指出了这不是一个随意的List,而是一个Integer的List。我们说List是一个带有类型参数的泛型接口,在这里就是指Integer。现在,我们在第1行里使用Integer作为类型参数,而不是在第3行里做类型转换。这样,在编译时刻,编译器就能够检查程序的正确性——无论何时何地,编译器都将保证myIntList的正确使用。相反地,类型转换仅仅告诉我们——在这里,程序员认为这样做是对的。采用泛型可以增强代码可读性和健壮性(robustness)。

 

 

2 定义泛型 public interface List<E> {    void add(E x);    Iterator<E> iterator(); } public interface Interator<E> {    E next();    boolean hasNext(); } 这是一段Collection里代码,一个完整的泛型定义。尖括号里的E就是形式类型参数(formal type parameters)。在泛型定义中,类型参数的用法就像一般具体类型那样。在引言中,我们看到初始化了一个泛型List——List<Integer>。在这里,类型参数被赋于实际类型参数(actual type argument)Integer。你可以想象List<Integer>将获得这样的代码: public interface List {    void add(Integer x);    Iterator< Integer > iterator(); } 和C++中对模板的处理有很大的不同,这里没有第2份副本。Java采用的是拭去法(erasure)而C++采用的是膨胀法(expansion)。一个泛型定义只被编译一次,只生成一个文件,就像一般的class和interface一样。形式类型参数可以不止1个,如: class Bar < E, D> { …… }

3 通配符 3.1 泛型和子类下面的这段代码合法么? List<String> ls = new ArrayList<String> ();// 1 List<Object> lo = ls;// 2 假设这两行代码是正确的,那么下面的操作: lo.add(new Object());// 3 String str = ls.get(0);// 4 将导致运行时刻错误。通过别名lo存取ls时,我们可以插入任意类型的对象——ls就不再仅仅持有String了。 Java编译器消除了这种错误发生的可能性。第2行将导致编译时刻错误。一般地说,如果Foo是Bar的子类,G定义为某种泛型,那么G<Foo>不是G<Bar>的子类。

 

 

 

3.2 通配符如果,我们试图使用泛型的方法编写一个打印Collection内所有元素的函数,要怎么做? void printCollection (Collection<Objcet> c) {    for (Objcet obj : c) {// jdk 1.5中新增的语法,见5.1        System.out.println(obj);    } } 显然这样是不行的,因为通过3.1我们可以知道——Collection<Object>不是任何Collection的父类。那么,所有Collection的父类是什么?Collection<?>——未知类型的Collection(collection of unknown),一个元素可以匹配为任意类型的Collection。“?”被称作通配类型。上述的代码,可以改写成这样: void printCollection(Collection<?> c) {    for (Object obj : c) {        System.out.println(obj);    } } 现在,我们可以使用任意类型的Collection作为参数了。注意,在printCollection内,用Objcet类型访问c的元素是安全的,因为任何一种具体类型都是Object的子类。但是这样的操作是错误的: List<?> list = new ArrayList<String>(); list.add(…);// compile-time error! 因为list被定义为List<?>,“?”指代了一个未知类型。list.add(…)无法保证插入的对象类型就是list实际包含的类型。唯一的例外就是null——null可以是任意类型的值。但是,通过一个List<?>引用,调用get()函数是可以的——即不会修改Collection的函数,就像printCollection里那样。尽管不能确定具体的类型,但是都是Object的子类。

 

3.3 受限通配符现在要创建一个简单的作图程序。我们定义了接口Shape: public abstract class Shape {    public abstract void draw(); } 然后定义了2个子类: public class Circle extends Shape {    …….    public void draw() { … } } public class Rectangle extends Shape {    ……    public void draw() {……} } 很自然地,我们也会设计这样一个函数: void drawAll (List<…> shapes) {    for (Shape s : shapes) {        s.draw();    } } 尖括号里应该填写什么了?显然,List<Shape>是行不通的,这在3.1里已经说明了。List<?>可以,但是不好,因为如果这样使用: List<Object> list = new ArrayList<Object>();// 1 list.add(new Object());// 2 drawAll(list);// 3 编译器认为没有问题,但是运行时刻肯定报错。在drawAll里,我们实际需要的是Shape的子类,但是List<?>无法在编译时刻保证这一点。这里的解决方案是受限通配符(bounded wildcard)。这样做: void drawAll(List<? extends Shape> shapes) { .. … } 如果,再像前一个例子的第3行那样使用的话,编译器会报错。因为编译器要求shapes的每一个元素的实际类型都是Shape的子类。同使用一般通配符一样,shapes.add(…)是不允许的,因为,编译器只能保证插入的是Shape的子类对象,而不能肯定与Collection实际包含的类型是匹配的。

4 泛型函数考虑设计这样一个函数——把一个数组中的对象依次插入一个Collection中。我们首先这样尝试: void addFromArray(Object[] a, Collection<?> c) {    for (Object o : a) {        c.add(o);// compile-time error!    } } 从前面的介绍中,可以明确这样是不行的。当然Collection<Object>同样是错误的。解决这类问题的方法就是使用泛型函数: static <T> void addFromArray(T[] a, Collection<T> c) {    for (T o : a) {        c.add(o);    } } 但是必须注意,当我们执行addFromArray时,编译器将根据参数的类型检查是否安全: addFromArray(new String[10], new ArrayList<String>());// OK! addFromArray(new String[10], new ArrayList<Object>());// OK! addFromArray(new Object[10], new ArrayList<String>());// compile-time error! addFromArray(new String[10], new ArrayList<Integer>());// compile-time error! 第3,4行的错误是很容易理解的,无论是把一个Object类型对象插入String的List还是把一个String插入Integer的List都是不安全的。不过,如果这样的代码是没有问题的: <T> void foo(T t1, T t2) {    System.out.println(t1.getClass());    System.out.println(t2.getClass()); } foo(new Object(), new String());// 显示 class java.lang.Objectclass.lang.String foo(new Integer(), new String();// 显示 class java.lang.Integerclass.lang.String foo(new Object[10], new ArrayList<String>()); // 显示 class [Ljava.lang.Object;class.util.ArrayList foo(new String[10], new ArrayList<Integer>()); // 显示 class [Ljava.lang.String;class.util.ArrayList 至于每一种调用T究竟是匹配了哪种类型。注意:这不是C++。经过编译,foo只生成一段代码,T就是Object。编译器只是在恰当的地方做了恰当的类型转换。

4.1 泛型函数和通配符的选择什么时候应当使用泛型函数,什么时候应当使用通配符呢?先看一段来自Collection里的代码: interface Collection<E> {    public boolean containsAll(Collection<?> c);    public boolean addAll(Collection<? extends E> c); } 我们也可以用泛型函数改写: interface Collection<E> {    public <T> boolean containsAll(Collection<T> c);    public <T extends E> boolean addAll(Collection<T> c); } 在containsAll和addAll中,类型参数T仅仅被使用了一次。函数返回值并不依赖于类型参数。这就告诉我们,类型参数是被用于实现多态的;它的作用仅仅是允许不同的实际类型在不同的场合下可以被使用。如果是这种情况的话,应当使用通配符。通配符用来实现弹性的子类化——就像这里试图表达的那样。泛型函数允许类型参数用来表达函数以及它的返回值和一个或多个类型参数之间的依赖性。如果,不存在这样的依赖性的话,泛型函数就不应当被使用。泛型函数和通配符有时是可以一起使用的,如: class Collections {    public static <T> void copy(List<T> dest, List<? extends T> src) { … } } 注意两个参数之间的类型依赖性。src内包含的对象必须满足is-a T,只有这样才能够被安全的插入dest,因为dest包含的对象是T类型的。当然这样也可以的: public static <T, S extends T> void copy(List<T> dest, List<S> src) { … } 但是推荐第一种用法。因为T同时对dest和src起作用,而S仅仅作用于src,没有其他的什么依赖于它——这种情况下,用通配符取代S比较好。用通配符更加清晰、明了。

5 其他 5.1 增强型for(Enhanced for,foreach)增强型for也是JDK 1.5新引入的Java语法。与传统的for相比,具有代码清晰,安全的优点。 List<Integer> list= new ArrayList<Integer>(); int result = 0; for (Integer i : list) {    result += i.intValue(); } 相当于: for (Iterator iter = list.iterator(); iter.hasNext();) {    result += ((Integer)i.next()).intValue(); } 同样也可以作用于数组: Integer[] ia = new Integer[10]; int result = 0; for (Integer i : ia) {    result += i.intValue(); }

5.2 通配符和重载,泛型函数和重载 void foo(List<String> ls) {    System.out.print(“foo(List<String> ls)”); } void foo(List<Object> lo) {    System.out.print(“foo(List<Object> lo)”); } void foo(List<?> l) {    System.out.print(“foo(List<?> l)”); }

foo(new ArrayList<String>()); foo(new ArrayList<Object>()); foo(new ArrayList<Integer>()); 编译并运行这段代码,我们能看到什么?……编译错误——“hava the same erasure”。注意,Java针对泛型采取的是拭去法,不论是List<String>,List<Object>还是List<?>,编译生成的都是同一段代码,而且这段代码和非泛型的List在本质上是一样的。可以这样认为,Java编译器对泛型的处理只是替我们在适当的地方加上了类型转换而已。所以以上3个foo函数不构成重载。类似的代码在C++中是可行的,因为C++采用的是膨胀法。针对不同的具体类型,生成不同的副本,List<String>和List<Object>是2个不同类型(STL里没有Object类型,String应为std::string),因此foo满足重载的条件。这种用法称为“显式特化”(explicit specialization definition)。

再看一下下面这段代码: void foo(String s) {    System.out.println(“foo(String s)”); } void <T> foo(T t) {    System.out.println(“foo(T t)”); } foo(“Test”); foo(new Integer(1)); 编译并运行这段代码,我们能看到什么?……编译错误?不是! foo(String s) foo(T t)。正是预期的输出。现在,修改一下: void foo(Object o) {    System.out.println(“foo(Object o)”); } void <T> foo(T t) {    System.out.println(“foo(T t)”); } 不用尝试任何例子,因为这已经无法通过编译了: name clash: foo(java.lang.Object o) and <T>foo<T> hava the same erasure 拭去法是这样处理泛型的: l一个参数化类型擦拭后应该除去参数(List<T> è List) l一个未受限的类型参数擦拭后成为Object l一个受限的类型参数擦拭后成为bound的类型

但是需要注意以下的代码: class Foo<E> { public void test1(List<E> list) { … };// List<E>擦拭后èList    public <T> void test2(T t) { … }// T擦拭后èObject } class Bar<E, F> extends Foo<F> {    public void test1(List<E> list) { … }// compile-time error    public void test2(Object o) { … }// compile-time error } 注意不是是覆盖(override)……

5.3 数组 List<String>[] list = new ArrayList<String>[10]; 似乎是正确的……编译时错误! List<String>[] list = new ArrayList<String>[10];// 1 Object o = list;// 2 Object[] oa = (Object[])o;// 3 oa[1] = new ArrayList<Integer>();// 4 String s = list[1].get();// 5 如果第1行是正确的话,那么第5行就会出现运行时错误,因为2—5行的语法都是没有问题的。泛型数组只能这样用: List<?>[] list = new ArrayList<?>[10]; 这解决问题了么?没有。因为错误还是无法避免,除了第5行必须一个显式的类型转换。 List<String>[] list = new ArrayList<String>[10];// 1 Object o = list;// 2 Object[] oa = (Object[])o;// 3 oa [1] = new ArrayList<Integer>();// 4 String s = (String)list[1].get();// 5 explicit cast

5.4 新建参数类型的对象 <T> static void foo(T t) {    //…..    T tt = new T();// compile-time error } 又是一个和C++模版的不同之处。Java采取的是拭去法!所以,试图新建一个参数类型对象的话,应当这样: <T> static void foo (T t, Class<T> klass) {// JDK 1.5中,Class类用泛型改写了    // …..    try {        T tt = klass.getInstance();    } catch (…) {    } }

 

参考资料: [1] Generics in the Java Programming Language http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf [2] Forthcoming Java Programming Language Features http://java.sun.com/j2se/1.5/pdf/Tiger-lang.pdf [3] 侯捷·Java泛型技术之发展·程序员,2002年第8,9期 [4] 紫云英·漫谈面向对象程序设计方法·程序员,2002年第3期

分享到:
评论
1 楼 dallarwww 2011-03-03  
您好,您在第4段第三句话“当然Collection<Object>同样是错误的。”是什么意思?我做了个实验,也没有出现错误。程序如下:
public static void copy(Object[] objects,Collection<Object> c)
{
for(Object object:objects)
{
c.add(object);
System.out.println(object);
}
}
没有出现编译错误。用main函数执行也没有错误。main函数如下:
public static void main(String[] args)
{
List<Object> list=new ArrayList<Object>();
Object[] objects=new Object[5];
for(int i=0;i<5;i++)
{
objects[i]=i;
}
copy(objects,list);
}
望指教!谢谢!辛苦!

相关推荐

    浅谈Java泛型让声明方法返回子类型的方法

    Java泛型是Java编程语言中的一个强大特性,它允许在代码中使用类型参数,从而提高了代码的重用性和安全性。在处理集合时,泛型能够确保集合中的元素都是同一种类型,避免了运行时的类型转换。在某些情况下,泛型还...

    浅谈java中定义泛型类和定义泛型方法的写法

    在Java编程语言中,泛型是一种强大的特性,它允许我们在编写代码时指定类型参数,从而提高了代码的灵活性和可重用性。本文将深入探讨如何在Java中定义泛型类和泛型方法。 首先,我们来看一下泛型方法的定义。在Java...

    浅谈Java模型以外的类型策略

    本文主要探讨了静态类型和动态类型这两种主要的类型模型,以及它们在Java语言及其以外的编程环境中的应用和影响。 静态类型语言如Java,其特点在于编译时进行严格的类型检查,这确保了程序在运行前就捕获了许多潜在...

    java之浅谈深说--教你如何成长

    ### Java之浅谈深说——教你如何成长为Java编程高手 在IT行业中,Java作为一种广泛使用的编程语言,其重要性不言而喻。对于希望成为Java编程高手的学习者来说,掌握正确的学习路径至关重要。本文将根据提供的标题、...

    浅谈Java向下转型的意义

    浅谈Java向下转型的意义 Java中向下转型是指将父类引用转换为子类对象的过程。向下转型的意义在于能够将父类对象转换为子类对象,以便在实际开发中满足不同的需求。 在Java中,向下转型是通过强制类型转换实现的。...

    浅谈Java中强制类型转换的问题

    在Java编程语言中,强制类型转换是将一个数据类型转换为另一个不兼容的数据类型的过程。这种转换通常是必要的,因为Java是一种静态类型的语言,它要求在编译时就确定变量的类型。然而,有时我们可能需要将一个对象从...

    浅谈Java与C#的一些细微差别

    Java 和 C# 作为两种广泛使用的面向对象编程语言,在语法和数据类型上有许多共同之处,但也存在一些微妙的差异。以下是一些关键的区别点: 1. **字符串定义**: - 在Java中,字符串通常使用`String`(首字母大写)...

    浅谈在Java中使用Callable、Future进行并行编程

    在Java编程中,进行并行计算以提升程序性能是一个重要的技术。传统的并行编程方式包括继承Thread类或实现Runnable接口,但这些方法在任务完成后无法直接获取执行结果,需要通过复杂的线程同步机制来实现。从Java 1.5...

    浅谈JavaAPI 中 &lt;E&gt; 与 &lt;T&gt; 的含义

    泛型是Java编程语言的一个重要特性,它允许在类、接口和方法中定义类型参数,从而增加了代码的类型安全性和重用性。下面将详细解释这两个符号的含义和它们在不同场景下的应用。 首先,`&lt;E&gt;` 通常代表 "Element" 的...

    java基础PPT

    11. **JNI与JVM原理**:浅谈Java Native Interface(JNI),用于在Java程序中调用本地(非Java)代码,以及JVM的工作原理,包括类加载、内存管理和垃圾收集。 12. **案例分析**:可能包含一些简单的编程实例,帮助...

    java程序员初学20道题

    浅谈JDBC的概念理解与学习 JDBC(Java Database Connectivity)是Java中用来对数据库进行统一访问、管理的一种机制。通过JDBC,开发人员可以使用标准Java API来连接不同的数据库管理系统。 - **JDBC驱动模型**:...

    java高手的文章合集1/3

    这个合集以“由一个简单的程序谈起”为主题,通过一系列文章深入浅出地介绍了Java语言的核心概念和技术。以下是这些文章可能涉及的重要知识点: 1. **Java入门**:文章可能从一个简单的“Hello, World!”程序开始,...

    浅谈Scala模式匹配

    在 Scala 中,模式匹配可以与其他语言特性结合使用,例如,函数式编程、泛型编程等。例如,可以使用模式匹配来处理列表中的每个元素,而不需要使用循环语句。 模式匹配的返回值是由第一个匹配的模式中的代码块决定...

    浅谈箭头函数写法在ReactJs中的使用

    在ReactJs中,箭头函数是一种简洁且易读的函数定义方式,尤其对于有Java等面向对象编程背景的开发者来说,它们提供了更直观的语法。然而,直接在React组件内部使用箭头函数可能会遇到编译错误,因为默认情况下,...

    2020年百度、阿里、腾讯、字节跳动Android高频面试题解析.pdf

    首先,Java 基础部分深入浅出地介绍了数据类型,包括基本数据类型(如整型、浮点型、字符型和布尔型)以及引用数据类型。String 类是面试中常见的重点,涉及到字符串的创建、拼接、比较和不可变性等特性。运算章节...

Global site tag (gtag.js) - Google Analytics