Java学习——泛型

简介:

1.为什么使用泛型以及使用Object实现泛型

考虑您要设计下面的两个类别(两个很无聊的类别,但足以说明需求):

BooleanFoo.java
public class BooleanFoo {
	private Boolean foo;
	public void setFoo(Boolean foo) {
		this.foo = foo;
	}
	public Boolean getFoo() {
		return foo;
	}
}
IntegerFoo.java
public class IntegerFoo {
	private Integer foo;
	public void setFoo(Integer foo) {
		this.foo = foo;
	}
	public Integer getFoo() {
		return foo;
	}
}

类别定义时逻辑完全一样,只是当中宣告的成员型态不同,有点小聪明的程序设计人员会将第一个类的内容复制至另一个档案中,然后用编辑器「取代」功能一次取 代所有的型态名称(即将Boolean取代为Integer)。

OK!是有些小聪明,但还是不太聪明,如果类别中的逻辑要修改,您要修改两个档案,泛型(Generics)的需求就在此产生,当您定义类 别时,发现到好几个类别的逻辑其实都相同,就只是当中所涉及的型态不一样时,使用复制、贴上、取代的功能来撰写程序只是让您增加不必要的档案管理困扰,有 没有办法只写一个档案就好,毕竟它们的逻辑是相同的。

别忘了,Java中所有的类别都扩充自Object,这样写会比较好:

ObjectFoo.java
public class ObjectFoo {
	private Object foo;
	public void setFoo(Object foo) {
		this.foo = foo;
	}
	public Object getFoo() {
		return foo;
	}
}

由于Java中所有定义的类别,都以Object为最上层的父类别,所以用它来实现泛型(Generics)功能是一个不错的考虑,大部份的人都这么作,您只要撰写这么一个类别,然后可以如下的使用它:

ObjectFoo foo1 = new ObjectFoo();
ObjectFoo foo2 = new ObjectFoo();

foo1.setFoo(new Boolean(true));
// 记得转换接口
Boolean b = (Boolean) foo1.getFoo();

// 记得转换接口
foo2.setFoo(new Integer(10));
Integer i = (Integer) foo2.getFoo();

看来还不错,但是由于传回的是Object,您必须转换它的接口,问题出在这边,粗心的程序设计人员往往会忘了要作这个动作,或者是转换接口时用错了型态(像是该用Boolean却用了Integer),例如:

ObjectFoo foo1 = new ObjectFoo();
foo1.setFoo(new Boolean(true));
String s = (String) foo1.getFoo();

要命的是,语法上是可以的,所以编译器检查不出错误,真正的错误要在执行时期才会发生,这时恼人的ClassCastException就会出来搞怪,在使用Object设计泛型程序时,程序人员要再细心一些、小心一些。

2.Generic泛型的提出

在J2SE 5.0之后,提出了针对泛型(Generics)设计的解决方案,要定义一个简单的泛型类别是简单的,直接来看个例子:

GenericFoo.java
public class GenericFoo<T> {
	private T foo;
	public void setFoo(T foo) {
		this.foo = foo;
	}
	public T getFoo() {
		return foo;
	}
}

<T> 用来宣告一个型态持有者(Holder)T,之后您可以用 T 作为型态代表来宣告变量(参考)名称,然后您可以像下面的程序来使用这个类别:

GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();
GenericFoo<Integer> foo2 = new GenericFoo<Integer>();

foo1.setFoo(new Boolean(true));
Boolean b = foo1.getFoo();

foo2.setFoo(new Integer(10));
Integer i = foo2.getFoo();

不同的地方在于,在宣告与配置对象时,您可以一并指定泛型类别中真正的型态,这将用来取代定义时所使用的 T,而这次您可以看到,接口转换不再需要了,所定义出来的泛型类别在使用时多了一层安全性,至少可以省去恼人的ClassCastException 发生,编译器可以帮您作第一层防线,例如下面的程序会被检查出错误:

GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();

foo1.setFoo(new Boolean(true));
Integer b = foo1.getFoo();

foo1.getFoo()传回的是Boolean,您要将它指定给Integer,这显然语法上不合,编译器这时可以派上用场:

Test.java:7: incompatible types
found : java.lang.Boolean
required: java.lang.Integer
Integer b = foo1.getFoo();

如果使用泛型类别,但宣告时不一并指定型态呢?那么预设会使用Object,不过您就要自己转换对象的接口型态了,但编译器会提出警讯,告诉您这可能是不安全的操作:

Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

回过头来看看下面的宣告:

GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();
GenericFoo<Integer> foo2 = new GenericFoo<Integer>();

GenericFoo< Boolean>宣告的foo1与GenericFoo< Integer>宣告的foo2是相同的类型吗?答案是否定的,基本上它们分属于两个不同类别的类型,即「相当于」下面两个类型(只是个比喻):

public class GenericFooBoolean {
	private Boolean foo;
	public void setFoo(Boolean foo) {
		this.foo = foo;
	}
	public Boolean getFoo() {
		return foo;
	}
}

以及:

public class GenericFooInteger {
	private Integer foo;
	public void setFoo(Integer foo) {
		this.foo = foo;
	}
	public Integer getFoo() {
		return foo;
	}
}

所以您不可以将 foo1 指定给 foo2,或是将 foo2 指定给 foo1,编译器会回报以下错误:

incompatible types
found : GenericFoo<java.lang.Integer>
required: GenericFoo<java.lang.Boolean>
foo1 = foo2;
3.使用泛型的几个例子

您可以在定义泛型类别时,宣告多个类型持有者,例如:

GenericFoo.java
public class GenericFoo< T1, T2> {
	private T1 foo1;
	private T2 foo2;
	public void setFoo1(T1 foo1) {
		this.foo1 = foo1;
	}
	public T1 getFoo1() {
		return foo1;
	}
	public void setFoo2(T2 foo2) {
		this.foo2 = foo2;
	}
	public T2 getFoo2() {
		return foo2;
	}
}

您可以如下使用GenericFoo类别,分别以Integer与Boolean取代T1与T2:

GenericFoo<Integer, Boolean> foo =new GenericFoo<Integer, Boolean>();

如果是数组的话,可以像这样:

GenericFoo.java
public class GenericFoo<T> {
	private T[] fooArray;
	public void setFooArray(T[] fooArray) {
		this.fooArray = fooArray;
	}
	public T[] getFooArray() {
		return fooArray;
	}
}

您可以像下面的方式来使用它:

String[] strs = {"caterpillar", "momor", "bush"};

GenericFoo<String> foo = new GenericFoo<String>();
foo.setFooArray(strs);
strs = foo.getFooArray();

来改写一下 Object 类别 中的 SimpleCollection

SimpleCollection.java
public class SimpleCollection<T> {
	private T[] objArr;
	private int index = 0;
	public SimpleCollection() {
		objArr = (T[]) new Object[10]; // 预设10个对象空间
	}
	public SimpleCollection(int capacity) {
		objArr = (T[]) new Object[capacity];
	}
	public void add(T t) {
		objArr[index] = t;
		index++;
	}
	public int getLength() {
		return index;
	}
	public T get(int i) {
		return (T) objArr[i];
	}
}

现在您可以直接使用它来当作特定类型对象的容器,例如:

Test.java
public class Test {
	public static void main(String[] args) {
		SimpleCollection<Integer> c =
		new SimpleCollection<Integer>();
		for(int i = 0; i < 10; i++) {
			c.add(new Integer(i));
		}
		for(int i = 0; i < 10; i++) {
			Integer k = c.get(i);
		}
	}
}

另一个SimpleCollection的写法也可以如下,作用是一样的:

SimpleCollection.java
public class SimpleCollection<T> {
	private Object[] objArr;
	private int index = 0;
	public SimpleCollection() {
		objArr = new Object[10]; // 预设10个对象空间
	}
	public SimpleCollection(int capacity) {
		objArr = new Object[capacity];
	}
	public void add(T t) {
		objArr[index] = t;
		index++;
	}
	public int getLength() {
		return index;
	}
	public T get(int i) {
		return (T) objArr[i];
	}
}

如果您已经定义了一个泛型类别,想要用这个类别来于另一个泛型类别中宣告成员的话要如何作?举个实例,假设您已经定义了下面的类别:

GenericFoo.java
public class GenericFoo<T> {
	private T foo;
	public void setFoo(T foo) {
		this.foo = foo;
	}
	public T getFoo() {
		return foo;
	}
}

您想要写一个包装类别(Wrapper),这个类别必须也具有GenericFoo的泛型功能,您可以这么写:

WrapperFoo.java
public class WrapperFoo<T> {
	private GenericFoo<T> foo;
	public void setFoo(GenericFoo<T> foo) {
		this.foo = foo;
	}
	public GenericFoo<T> getFoo() {
		return foo;
	}
}

这么一来,您就可以保留型态持有者 T 的功能,一个使用的例子如下:

GenericFoo<Integer> foo = new GenericFoo<Integer>();
foo.setFoo(new Integer(10));

WrapperFoo<Integer> wrapper = new WrapperFoo<Integer>();
wrapper.setFoo(foo);

4.形态通配符?Extends

假设您撰写了一个泛型类别:

GenericFoo.java
public class GenericFoo<T> {
	private T foo;
	public void setFoo(T foo) {
		this.foo = foo;
	}
	public T getFoo() {
		return foo;
	}
}

分别使用下面的程序宣告了foo1与foo2两个参考名称:

GenericFoo<Integer> foo1 = null;
GenericFoo<Boolean> foo2 = null;

那么 foo1 就只接受GenericFoo<Integer>的实例,而foo2只接受GenericFoo<Boolean>的实例。

现在您有这么一个需求,您希望有一个参考名称foo可以接受所有下面的实例(List、Map或List接口以及其实接口的相关类别,在J2SE 5.0中已经针对泛型功能作了改写,在这边仍请将之当作界面就好,这是为了简化说明的考虑):

foo = new GenericFoo<ArrayList>();
foo = new GenericFoo<LinkedList>();

简单的说,实例化型态持有者时,它必须是实作List的类别或其子类别,要宣告这么一个参考名称,您可以使用 '?' 通配字符,并使用"extends"关键词限定型态持有者的型态,例如:

GenericFoo<? extends List> foo = null;
foo = new GenericFoo<ArrayList>();
.....
foo = new GenericFoo<LinkedList>();
....

如果指定了不是实作List的类别或其子类别,则编译器会回报错误,例如:

GenericFoo<? extends List> foo = new GenericFoo<HashMap>();

上面这段程序编译器会回报以下的错误:

incompatible types
found : GenericFoo<java.util.HashMap>
required: GenericFoo<? extends java.util.List>
GenericFoo<? extends List> foo = new GenericFoo<HashMap>();

这样的限定是很有用的,例如如果您想要自订一个showFoo()方法,方法的内容实作是针对List而制定的,例如:

public void showFoo(GenericFoo foo) {
// 针对List而制定的内容
}

您当然不希望任何的型态都可以传入showFoo()方法中,您可以使用以下的方式来限定,例如:

public void showFoo(GenericFoo<? extends List> foo) {
// 
}

这么一来,如果有粗心的程序设计人员传入了您不想要的型态,例如GenericFoo<Boolean>型态的实例,则编译器都会告诉它这是不可行的,在宣告名称时如果指定了<?>而 不使用"extends",则预设是允许Object及其下的子类,也就是所有的Java对象了,那为什么不直接使用GenericFoo宣告就好了,何 必要用GenericFoo<?>来宣告?使用通配字符有点要注意的是,透过使用通配字符宣告的名称所参考的对象,您没办法再对它加入新的资 讯,您只能取得它的信息或是移除它的信息,例如:

GenericFoo<String> foo = new GenericFoo<String>();
foo.setFoo("caterpillar");
GenericFoo<?> immutableFoo = foo;

// 可以取得信息
System.out.println(immutableFoo.getFoo());

// 可透过immutableFoo来移去foo所参考实例内的信息
immutableFoo.setFoo(null);

// 不可透过immutableFoo来设定新的信息给foo所参考的实例
// 所以下面这行无法通过编译
//?immutableFoo.setFoo("良葛格");

所以使用<?>或是<? extends SomeClass>的宣告方式,意味着您只能透过该名称来取得所参考实例的信息,或者是移除某些信息,但不能增加它的信息,因为只知道当中放置的 是SomeClass的子类,但不确定是什么类的实例,编译器不让您加入对象,理由是,如果可以加入对象的话,那么您就得记得取回的对象实例是什么形态, 然后转换为原来的型态方可进行操作,这就失去了使用泛型的意义。

事实上,GenericFoo<?> immutableFoo相当于GenericFoo immutableFoo。

除了可以向下限制,您也可以向上限制,只要使用"super"关键词,例如:

GenericFoo<? super StringBuilder> foo;

如此,foo就只接受 StringBuilder 及其上层的父类型态之对象

5.限制泛型可用类型,只让部分类型有作为holder的资格。

在定义泛型类别时,预设您可以使用任何的型态来实例化泛型类别中的型态持有者,但假设您想要限制使用泛型类别时,只能用某个特定型态或其子类别才能实例化型态持有者的话呢?

您可以在定义型态持有者时,一并使用"extends"指定这个型态持有者必须是扩充某个类型,举个实例来说:

ListGenericFoo.java
import java.util.List;
public class ListGenericFoo<T extends List> {
	private T[] fooArray;
	public void setFooArray(T[] fooArray) {
		this.fooArray = fooArray;
	}
	public T[] getFooArray() {
		return fooArray;
	}
}

ListGenericFoo在宣告类型持有者时,一并指定这个持有者必须扩充自List接口(interface),在限定持有者时,无论是要限定的对 象是接口或类别,都是使用"extends"关键词。

您使用"extends"限定型态持有者必须是实作List的类别或其子类别,例如LinkedList与ArrayList,下面的程序是合法的:

ListGenericFoo<LinkedList> foo1 =new ListGenericFoo<LinkedList>();
ListGenericFoo<ArrayList> foo2 =new ListGenericFoo<ArrayList>();

但是如果不是List的类别或是其子类别,就会发生编译错误,例如下面的程序通不过编译:

ListGenericFoo<HashMap> foo3 =new ListGenericFoo<HashMap>();

编译器会回报以下错误讯息:

type parameter java.util.HashMap is not within its bound
ListGenericFoo<HashMap> foo3 = new ListGenericFoo<HashMap>();

HashMap并没有实作List界面,所以无法用来实例化型态持有者,事实上,当您没有使用extends关键词限定型态持有者时,预设则是Object下的所有子类别都可以实例化型态持有者,即只写<T>时就相当于<T extends Object>。

6.扩充泛型类别、实作泛型接口

您可以扩充一个泛型类别,保留其型态持有者,并新增自己的型态持有者,例如先写一个父类别:

GenericFoo.java
public class GenericFoo<T1, T2> {
	private T1 foo1;
	private T2 foo2;
	public void setFoo1(T1 foo1) {
		this.foo1 = foo1;
	}
	public T1 getFoo1() {
		return foo1;
	}
	public void setFoo2(T2 foo2) {
		this.foo2 = foo2;
	}
	public T2 getFoo2() {
		return foo2;
	}
}

再来写一个子类别扩充上面的父类别:

SubGenericFoo.java
public class SubGenericFoo<T1, T2, T3>
extends GenericFoo<T1, T2> {
	private T3 foo3;
	public void setFoo3(T3 foo3) {
		this.foo3 = foo3;
	}
	public T3 getFoo3() {
		return foo3;
	}
}

如果决定要保留型态持有者,则父类别上宣告的型态持有者数目必须齐全,也就是说上式中,T1与T2都要出现,如果不保留型态持有者,则继承下来的T1与 T2自动变为Object,建议当然是父类别的型态持有者都保留

接口实作也是类似,例如先定义一个接口:

IFoo.java
public interface IFoo<T1, T2> {
	public void setFoo1(T1 foo1);
	public void setFoo2(T2 foo2);
	public T1 getFoo1();
	public T2 getFoo2();
}

实作时如下,保留所有的型态持有者:

GenericFoo.java
 

}


本文转自ZH奶酪博客园博客,原文链接:http://www.cnblogs.com/CheeseZH/archive/2012/12/05/2802841.html,如需转载请自行联系原作者

相关文章
|
5天前
|
Java
JAVA难点包括异常处理、多线程、泛型和反射,以及复杂的分布式系统知识
JAVA难点包括异常处理、多线程、泛型和反射,以及复杂的分布式系统知识。入坑JAVA因它的面向对象特性、平台无关性、强大的标准库和活跃的社区支持。
21 2
|
8天前
|
算法 Java 大数据
Java从入门到精通学习报告
Java从入门到精通学习报告
17 1
|
8天前
|
消息中间件 监控 安全
【JAVAEE学习】探究Java中多线程的使用和重点及考点
【JAVAEE学习】探究Java中多线程的使用和重点及考点
|
9天前
|
Java
【专栏】如何在 Java 8 中使用 Streams?结合多种案例剖析学习!
【4月更文挑战第28天】Java 8 的 Streams 提供了一种处理数据集合的新方式,增强了代码的可读性和可维护性。本文介绍了 Streams 的基本概念,如从数据源创建 Stream,以及中间和终端操作。通过过滤、映射、归并、排序、分组等案例,展示了 Streams 的使用,包括并行 Streams 提高效率。学习 Streams 可以提升代码质量和效率,文章鼓励读者在实际开发中探索更多 Streams 功能。
|
9天前
|
存储 监控 Java
Java 零基础入门学习(小白也能看懂!)四
Java 零基础入门学习(小白也能看懂!)四
|
9天前
|
存储 安全 Java
Java 零基础入门学习(小白也能看懂!)三
Java 零基础入门学习(小白也能看懂!)三
|
9天前
|
存储 机器学习/深度学习 Java
Java 零基础入门学习(小白也能看懂!)二
Java 零基础入门学习(小白也能看懂!)二
|
9天前
|
Java 编译器 程序员
Java 零基础入门学习(小白也能看懂!)一
Java 零基础入门学习(小白也能看懂!)一
|
9天前
|
Java
Java学习——类与对象(二)
Java学习——类与对象(二)
|
9天前
|
Java C++
Java学习——类与对象(一)
Java学习——类与对象(一)