`
qiemengdao
  • 浏览: 275996 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

Java泛型编程最全总结

阅读更多

由于发到iteye上面格式乱了,需要的朋友可以下载附件。

 

JAVA泛型编程笔记

1介绍

Java泛型编程是JDK1.5版本后引入的。泛型让编程人员能够使用类型抽象,通常用于集合里面。下面是一个不用泛型例子:

 

List myIntList=new LinkedList(); //1
myIntList.add(newInteger(0)); //2
Integer x=(Integer)myIntList.iterator().next(); //3
 

注意第3行代码,但这是让人很不爽的一点,因为程序员肯定知道自己存储在List里面的对象类型是Integer,但是在返回列表中元素时,还是必须强制转换类型,这是为什么呢?原因在于,编译器只能保证迭代器的next()方法返回的是Object类型的对象,为保证Integer变量的类型安全,所以必须强制转换。

这种转换不仅显得混乱,更可能导致类型转换异常ClassCastException,运行时异常往往让人难以检测到。保证列表中的元素为一个特定的数据类型,这样就可以取消类型转换,减少发生错误的机会, 这也是泛型设计的初衷。下面是一个使用了泛型的例子:

 

List<Integer> myIntList=newLinkedList<Integer>(); //1’
myIntList.add(newInteger(0)); //2’
Integerx=myIntList.iterator().next(); //3’
 

       在第1行代码中指定List中存储的对象类型为Integer,这样在获取列表中的对象时,不必强制转换类型了。

 

2定义简单的泛型

下面是一个引用自java.util包中的接口ListIterator的定义,其中用到了泛型技术。

 

public interface List<E> {
	void add(E x);
	Iterator<E> iterator();
}
public interface Iterator<E> {
	E next();
	boolean hasNext();
}
 

       这跟原生类型没有什么区别,只是在接口后面加入了一个尖括号,尖括号里面是一个类型参数(定义时就是一个格式化的类型参数,在调用时会使用一个具体的类型来替换该类型)。

       也许可以这样认为,List<Integer>表示List中的类型参数E会被替换成Integer

 

public interface IntegerList {
	void add(Integer x)
	Iterator<Integer> iterator();
}
 

       类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上,因此泛型类型中的静态变量是所有实例共享的。此外,需要注意的是,一个static方法,无法访问泛型类的类型参数,因为类还没有实例化,所以,若static方法需要使用泛型能力,必须使其成为泛型方法类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法在使用泛型时,任何具体的类型都被擦除,唯一知道的是你在使用一个对象。比如:List<String>List<Integer>在运行事实上是相同的类型。他们都被擦除成他们的原生类型,即List因为编译的时候会有类型擦除,所以不能通过同一个泛型类的实例来区分方法,如下面的例子编译时会出错,因为类型擦除后,两个方法都是List类型的参数,因此并不能根据泛型类的类型来区分方法。

 

/*会导致编译时错误*/ 
 public class Erasure{
            public void test(List<String> ls){
                System.out.println("Sting");
            }
            public void test(List<Integer> li){
                System.out.println("Integer");
            }
  }
 

       那么这就有个问题了,既然在编译的时候会在方法和类中擦除实际类型的信息,那么在返回对象时又是如何知道其具体类型的呢?如List<String>编译后会擦除掉String信息,那么在运行时通过迭代器返回List中的对象时,又是如何知道List中存储的是String类型对象呢?

       擦除在方法体中移除了类型信息,所以在运行时的问题就是边界即对象进入和离开方法的地点,这正是编译器在编译期执行类型检查并插入转型代码的地点。泛型中的所有动作都发生在边界处:对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。

 

3.泛型和子类型

为了彻底理解泛型,这里看个例子:(AppleFruit的子类)

 

List<Apple> apples = new ArrayList<Apple>(); //1
List<Fruit> fruits = apples; //2
 

1行代码显然是对的,但是第2行是否对呢?我们知道Fruit fruit = new Apple(),这样肯定是对的,即苹果肯定是水果,但是第2行在编译的时候会出错。这会让人比较纳闷的是一个苹果是水果,为什么一箱苹果就不是一箱水果了呢?可以这样考虑,我们假定第2行代码没有问题,那么我们可以使用语句fruits.add(new Strawberry())StrawberryFruit的子类)在fruits中加入草莓了,但是这样的话,一个List中装入了各种不同类型的子类水果,这显然是不可以的,因为我们在取出List中的水果对象时,就分不清楚到底该转型为苹果还是草莓了。

通常来说,如果FooBar的子类型,G是一种带泛型的类型,则G<Foo>不是G<Bar>的子类型。这也许是泛型学习里面最让人容易混淆的一点。

 

4.通配符

4.1通配符?

先看一个打印集合中所有元素的代码。

 

//不使用泛型
void printCollection(Collection c) {                
	Iterator i=c.iterator();
	for (k=0;k < c.size();k++) {
		System.out.println(i.next());
	}
}
 

 

//使用泛型
void printCollection(Collection<Object> c) {
for (Object e:c) {
System.out.println(e);
}
}
 

     很容易发现,使用泛型的版本只能接受元素类型为Object类型的集合如ArrayList<Object>();如果是ArrayList<String>,则会编译时出错。因为我们前面说过,Collection<Object>并不是所有集合的超类。而老版本可以打印任何类型的集合,那么如何改造新版本以便它能接受所有类型的集合呢?这个问题可以通过使用通配符来解决。修改后的代码如下所示:

 

//使用通配符?,表示可以接收任何元素类型的集合作为参数
void printCollection(Collection<?> c) {
	for (Object e:c) {
		System.out.println(e);
	}
}
 

这里使用了通配符?指定可以使用任何类型的集合作为参数。读取的元素使用了Object类型来表示,这是安全的,因为所有的类都是Object的子类。这里就又出现了另外一个问题,如下代码所示,如果试图往使用通配符?的集合中加入对象,就会在编译时出现错误。需要注意的是,这里不管加入什么类型的对象都会出错。这是因为通配符?表示该集合存储的元素类型未知,可以是任何类型。往集合中加入元素需要是一个未知元素类型的子类型,正因为该集合存储的元素类型未知,所以我们没法向该集合中添加任何元素。唯一的例外是null,因为null是所有类型的子类型,所以尽管元素类型不知道,但是null一定是它的子类型。

 

Collection<?> c=new ArrayList<String>();
c.add(newObject()); //compile time error,不管加入什么对象都出错,除了null外。
c.add(null); //OK
 

另一方面,我们可以从List<?> lists中获取对象,虽然不知道List中存储的是什么类型,但是可以肯定的是存储的类型一定是Object的子类型,所以可以用Object类型来获取值。如for(Object obj: lists),这是合法的。

4.2边界通配符

   1extends通配符

假定有一个画图的应用,可以画各种形状的图形,如矩形和圆形等。为了在程序里面表示,定义如下的类层次:

 

public abstract class Shape {
	public abstract void draw(Canvas c);
}

public class Circle extends Shape {
	private int x,y,radius;
	public void draw(Canvas c) { ... }
}

public class Rectangle extends Shape
	private int x,y,width,height;
	public void draw(Canvasc) { ... }
}

 

为了画出集合中所有的形状,我们可以定义一个函数,该函数接受带有泛型的集合类对象作为参数。但是不幸的是,我们只能接收元素类型为ShapeList对象,而不能接收类型为List<Cycle>的对象,这在前面已经说过。为了解决这个问题,所以有了边界通配符的概念。这里可以采用public void drawAll(List<? extends Shape> shapes)来满足条件,这样就可以接收元素类型为Shape子类型的列表作为参数了。

 

//原始版本
public void drawAll(List<Shape> shapes) {
	for (Shapes:shapes) {
		s.draw(this);
	}
}
 

 

//使用边界通配符的版本
public void drawAll(List<?exends Shape> shapes) {
	for (Shapes:shapes) {
		s.draw(this);
	}
}
 

这里就又有个问题要注意了,如果我们希望在List<exends Shape> shapes中加入一个矩形对象,如下所示:

shapes.add(0, new Rectangle()); //compile-time error

那么这时会出现一个编译时错误,原因在于:我们只知道shapes中的元素时Shape类型的子类型,具体是什么子类型我们并不清楚,所以我们不能往shapes中加入任何类型的对象。不过我们在取出其中对象时,可以使用Shape类型来取值,因为虽然我们不知道列表中的元素类型具体是什么类型,但是我们肯定的是它一定是Shape类的子类型。

 

2)?super通配符

       这里还有一种边界通配符为?super。比如下面的代码:

 

List<Shape> shapes = new ArrayList<Shape>();
List<? super Cicle> cicleSupers = shapes;
cicleSupers.add(new Cicle()); //OK, subclass of Cicle also OK
cicleSupers.add(new Shape()); //ERROR
 

       这表示cicleSupers列表存储的元素为Cicle的超类,因此我们可以往其中加入Cicle对象或者Cicle的子类对象,但是不能加入Shape对象。这里的原因在于列表cicleSupers存储的元素类型为Cicle的超类,但是具体是Cicle的什么超类并不清楚。但是我们可以确定的是只要是Cicle或者Circle的子类,则一定是与该元素类别兼容。

 

3)边界通配符总结

<!--[if !supportLists]-->l         <!--[endif]-->如果你想从一个数据类型里获取数据,使用 ? extends 通配符

<!--[if !supportLists]-->l         <!--[endif]-->如果你想把对象写入一个数据结构里,使用 ? super 通配符

<!--[if !supportLists]-->l         <!--[endif]-->如果你既想存,又想取,那就别用通配符。

 

5.泛型方法

考虑实现一个方法,该方法拷贝一个数组中的所有对象到集合中。下面是初始的版本:

 

static void fromArrayToCollection(Object[]a, Collection<?> c) {
	for (Object o:a) {
		c.add(o); //compile time error
	}
}
 

可以看到显然会出现编译错误,原因在之前有讲过,因为集合c中的类型未知,所以不能往其中加入任何的对象(当然,null除外)。解决该问题的一种比较好的办法是使用泛型方法,如下所示:

 

static <T> void fromArrayToCollection(T[] a, Collection<T>c){
	for(T o : a) {
		c.add(o);// correct
	}
}
 

注意泛型方法的格式,类型参数<T>需要放在函数返回值之前。然后在参数和返回值中就可以使用泛型参数了。具体一些调用方法的实例如下:

 

Object[] oa = new Object[100];
Collection<Object>co = new ArrayList<Object>();
fromArrayToCollection(oa, co);// T inferred to be Object
String[] sa = new String[100];
Collection<String>cs = new ArrayList<String>();
fromArrayToCollection(sa, cs);// T inferred to be String
fromArrayToCollection(sa, co);// T inferred to be Object
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number>cn = new ArrayList<Number>();
fromArrayToCollection(ia, cn);// T inferred to be Number
fromArrayToCollection(fa, cn);// T inferred to be Number
fromArrayToCollection(na, cn);// T inferred to be Number
fromArrayToCollection(na, co);// T inferred to be Object
fromArrayToCollection(na, cs);// compile-time error
 

注意到我们调用方法时并不需要传递类型参数,系统会自动判断类型参数并调用合适的方法。当然在某些情况下需要指定传递类型参数,比如当存在与泛型方法相同的方法的时候(方法参数类型不一致),如下面的一个例子:

 

public  <T> void go(T t) {
	System.out.println("generic function");
}
public void go(String str) {
	System.out.println("normal function");
}
public static void main(String[] args) {
		FuncGenric fg = new FuncGenric();
		fg.go("haha");//打印normal function
		fg.<String>go("haha");//打印generic function
        fg.go(new Object());//打印generic function
		fg.<Object>go(new Object());//打印generic function
}
 

如例子中所示,当不指定类型参数时,调用的是普通的方法,如果指定了类型参数,则调用泛型方法。可以这样理解,因为泛型方法编译后类型擦除,如果不指定类型参数,则泛型方法此时相当于是public void go(Object t)。而普通的方法接收参数为String类型,因此以String类型的实参调用函数,肯定会调用形参为String的普通方法了。如果是以Object类型的实参调用函数,则会调用泛型方法。

6.其他需要注意的小点

1)方法重载

JAVA里面方法重载是不能通过返回值类型来区分的,比如代码一中一个类中定义两个如下的方法是不容许的。但是当参数为泛型类型时,却是可以的。如下面代码二中所示,虽然形参经过类型擦除后都为List类型,但是返回类型不同,这是可以的。

 

/*代码一:编译时错误*/ 
public class Erasure{
            public void test(int i){
                System.out.println("Sting");
            }
            public int test(int i){
                System.out.println("Integer");
            }
  }
 

 

/*代码二:正确 */
 public class Erasure{
            public void test(List<String> ls){
                System.out.println("Sting");
            }
            public int test(List<Integer> li){
                System.out.println("Integer");
            }
  }
 

 

2)泛型类型是被所有调用共享的

       所有泛型类的实例都共享同一个运行时类,类型参数信息会在编译时被擦除。因此考虑如下代码,虽然ArrayList<String>ArrayList<Integer>类型参数不同,但是他们都共享ArrayList类,所以结果会是true

      

 

List<String>l1 = new ArrayList<String>();
List<Integer>l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass()); //True
 

 

3instanceof

不能对确切的泛型类型使用instanceOf操作。如下面的操作是非法的,编译时会出错。

 

Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>){…}// compile error.如果改成instanceof Collection<?>则不会出错。
 

 

4)泛型数组问题

不能创建一个确切泛型类型的数组。如下面代码会出错。

List<String>[] lsa = new ArrayList<String>[10]; //compile error.

       因为如果可以这样,那么考虑如下代码,会导致运行时错误。

 

List<String>[] lsa = new ArrayList<String>[10]; // 实际上并不允许这样创建数组
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer>li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li;// unsound, but passes run time store check
String s = lsa[1].get(0); //run-time error - ClassCastException
 

       因此只能创建带通配符的泛型数组,如下面例子所示,这回可以通过编译,但是在倒数第二行代码中必须显式的转型才行,即便如此,最后还是会抛出类型转换异常,因为存储在lsa中的是List<Integer>类型的对象,而不是List<String>类型。最后一行代码是正确的,类型匹配,不会抛出异常。

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

  

参考资料:

http://www.aqee.net/java-generics-quick-tutorial/

http://www.infoq.com/cn/articles/cf-java-generics

http://blog.csdn.net/daniel_h1986/article/details/5708605

http://www.cnblogs.com/stephen-liu74/archive/2012/01/20/2228938.html

sun官方文档:generics-tutorial.pdf

 

 

分享到:
评论
6 楼 tuspark 2016-08-14  
总结的不错,只是格式太规范。如果说最全面的泛型内容总结,我推荐这个系列:《站在巨著之上谈泛型》,感觉写的不错,排版也清晰,分析的也很透彻。
5 楼 huihui_0218 2015-07-14  
泛型方法go的调用

fg.<String>go("haha");//打印generic function 

有问题。 应该打印normal function

且上面调用go方法中<String>多余,虽然编译不报错。
4 楼 fantaxy025025 2015-05-24  
楼主总结的不错~赞一个!
3 楼 lijunwyf41 2013-09-26  
public static void main(String[] args) {
FuncGenric fg = new FuncGenric();
fg.go("haha");//打印normal function
fg.<String>go("haha");//打印generic function
        fg.go(new Object());//打印generic function
fg.<Object>go(new Object());//打印generic function
}

这一段代码报错,fg.<String>go这个可以这样写吗?
2 楼 宇宙幻影 2013-09-17  
搂着写的很好
1 楼 sonaive 2012-08-01  
import java.util.*;
import java.lang.reflect.*;
public class GenericsTest {

	/**
	 * 定义了泛型集合,向集合中添加该类型的子类对象依然成功
	 */
	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub
		List<Ax>  list = new ArrayList<Ax>();
		list.add(new Ax());
		list.add(new Bx());
		list.add(new Cx());
		Iterator<Ax> iterator = list.iterator();
		while(iterator.hasNext()) {
			Object obj = iterator.next();
			Method fooMethod = obj.getClass().getMethod("foo");
			fooMethod.invoke(obj);
		}
	}

}
class Ax{
    public void foo() {
    	System.out.println("Ax object is in the list");
    }
}
class Bx extends Ax{
    public void foo() {
    	System.out.println("Bx object is in the list");
    }
}
class Cx extends Ax{
    public void foo() {
    	System.out.println("Cx object is in the list");
    }
}

博主,我用反射的话还是能用分辨出对象的类型并且调用相应的方法。这样做有什么好处呢或者坏处。我不明白为什么要用反射。

相关推荐

    java泛型编程

    ### Java泛型编程详解 #### 一、引言 Java泛型编程是在JDK 1.5版本之后引入的一项重要特性,它允许开发者在编译时检查类型安全,并且所有的强制转换都由编译器自动完成,从而显著减少了运行时出现的`...

    Java泛型编程快速入门

    ### Java泛型编程快速入门详解 #### 一、引言 随着Java的发展,JDK 1.5(即Java 5.0)引入了一系列新特性,其中之一便是泛型(Generics)。泛型的引入极大地提高了Java语言的灵活性和类型安全性。本文将详细介绍...

    java泛型技术之发展

    总结,Java泛型技术的发展极大地提升了Java编程的效率和安全性。它通过编译时检查和类型安全保证,帮助开发者编写出更加健壮和易于维护的代码。理解并熟练掌握Java泛型,是每一个Java开发者必备的技能。

    java 泛型方法使用示例

    下面我们将深入探讨Java泛型方法的概念、语法以及使用示例。 **一、泛型方法概念** 泛型方法是一种具有类型参数的方法,这些类型参数可以在方法声明时指定,并在方法体内部使用。与类的泛型类似,它们提供了编译时...

    泛型编程技术

    总结来说,泛型编程技术是提高代码质量的关键工具,它通过类型参数的使用,增强了代码的灵活性和安全性。理解和掌握泛型编程,对于开发高效、可靠的软件系统至关重要。在实际开发中,合理运用泛型可以避免许多潜在的...

    java 泛型的使用 详细讲解

    ### Java泛型的使用详细讲解 #### 一、引言 在Java开发中,泛型是一种重要的语言特性,它能够帮助开发者在不增加代码量的情况下处理多种数据类型,同时还能保持代码的清晰度和可读性。本文将详细介绍Java泛型的...

    全面总结Java泛型

    Java 泛型是 Java 编程语言中一个强大的特性,它允许在类、接口和方法中使用类型参数,从而增加了代码的复用性和安全性。在本文中,我们将深入探讨 Java 泛型的各种方面。 首先,普通泛型允许我们在定义类时引入一...

    泛型编程 时间测试

    总结而言,泛型编程提供了编写高效、灵活代码的能力,而时间测试则帮助我们了解这些泛型实现的实际性能。通过深入理解泛型的原理和应用,我们可以编写出既安全又高效的代码,并在各种场景下获得理想的时间性能。在...

    全面总结Java泛型--实例

    标题与描述均提到了“全面总结Java泛型--实例”,这表明文章旨在深入解析Java泛型的概念,并通过具体示例来展示其应用。Java泛型是Java编程语言的一个强大特性,它允许在编译时检查类型安全,并且所有的强制转换都是...

    Java泛型类型擦除后的补偿

    总结来说,Java泛型的类型擦除虽然在运行时消除了类型信息,但通过编译时的类型检查、桥接方法、通配符等补偿机制,仍然实现了强大的类型安全和便利性。开发者应理解这些补偿机制,以便更好地利用Java泛型进行类型...

    Java泛型与容器详细笔记.pdf (带书签)

    总结来说,Java泛型与容器详细笔记提供了关于如何使用Java中的泛型和容器类的深入理解,涵盖了Java集合框架的核心组件,泛型的类型安全机制,以及一些提高开发效率的第三方库。文档通过实例代码展示了如何在实际项目...

    java泛型总结.docx

    以下是对Java泛型的详细解释: 1. **泛型类型**:泛型类型允许我们在定义类、接口或集合时引入类型参数。比如`List&lt;String&gt;`就是一个泛型类型,其中String是类型参数,代表了列表中的元素类型。这样,当我们将字符...

    Java深度历险之Java泛型.docx

    ### Java泛型详解 #### 一、Java泛型概述 Java泛型(Generics)是Java SE 5.0引入的一项重要新特性,它允许开发者在定义类、接口或方法时使用类型参数(Type Parameters)。类型参数在使用时可以用具体的类型来...

    java 泛型 demo

    总结起来,Java泛型提供了强大的类型系统工具,让开发者能够编写出更安全、更灵活的代码。通过`GenericsDemo17.java`和`Info.java`这两个文件,你可以深入理解泛型在实际编程中的应用,包括类、方法、边界以及通配符...

    java零基础自学 之 JAVA泛型

    本文将深入探讨Java泛型的基本概念、好处以及与继承的关系。 首先,让我们理解泛型是如何工作的。在Java中,泛型通过对类型系统进行扩展实现,允许我们创建可以接受不同类型参数的类、接口和方法。类型参数,用尖...

Global site tag (gtag.js) - Google Analytics