`

泛型 —— Java编程思想

    博客分类:
  • Java
阅读更多

泛型类

有时方法调用需返回多个对象,你应该经常需要这样的功能吧。可是return语句只允许返回单个对象,因此,解决办法就是创建一个对象,用它来持有想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。可是有了泛型,我们就能够一次性地解决该问题。同时,我们在编译期就能确保类型安全。这些携带多个返回结果的对象我们称之为容器,它是将一组返回结果对象直接打包存储于其中的一个单一对象中,这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。(这个概念也称为数据传送对象,或信使。)容器中的对象可以是任意不同的类型。不过,我们希望能够为每一个对象指明其类型,并且从容器中读取出来时,能够得到正确的类型,所以使用泛型是一个好的主意。

/*
 * 存储两个值的对象,第一个与第二个结果的类型在编译时确定,可以是任
 * 何类型的对象(基本类型会自动装箱),这样就是一个名副其实的的类模
 * 板,只是要存储的结果是两个则可以使用。
 * 这里我们只创建到可存储三个返回结果的类,如果需要,可以创建出存储
 * 四个、五个返回结果的泛型类
 */
public class TwoValue<A, B> {
	public final A first;//存储第一个返回结果
	public final B second;//存储第二个返回结果

	public TwoValue(A a, B b) {
		first = a;
		second = b;
	}

	public String toString() {
		return "(" + first + ", " + second + ")";
	}
}

第一次阅读上面的代码时,你也许会想,这不是违反了Java编程的安全性原则吗?first和 second应该声明为private,然后提供getFirst()和getSecond()之类的访问方法才对呀?让我们仔细看看这个例子中的安全性:客户端程序可以读取first和second对象,然后可以随心所欲地使用这两个对象。但是,它们却无法将其他值赋予first或second。因为 final声明为你买了相同的安全保险,而且这种格式更简洁明了。
还有另一种设计考虑,即你确实希望允许客户端程序员改变first或second所引用的对象。然而,采用以上的形式无疑是更安全的做法,这样的话,如果程序员想要使用具有不同返回结果的容器时,就强制要求他们另外创建一个新的TwoReturn对象。

/*
 * 能存储三个返回结果的对象
 */
public class ThreeValue<A, B, C> extends TwoValue<A, B> {
	public final C third;

	public ThreeValue(A a, B b, C c) {
		super(a, b);
		third = c;
	}

	public String toString() {
		return "(" + first + ", " + second + ", " + third + ")";
	}
}
/*
 * 用来简化泛型实例的创建过程,如原来要创建TwoValue需要这写:
 * TwoValue<String, Integer> two = new TwoValue<String, Integer>("one",1);
 * 但使用该工具类可简化创建的语句:
 * TwoValue<String, Integer> two = CreateValue.newValue("one",1);
 */
public class CreateMutilValue {
	//创建附带两个返回值的对象
	public static <A, B> TwoValue<A, B> newMutilValue(A a, B b) {
		return new TwoValue<A, B>(a, b);
	}

	//创建附带三个返回值的对象
	public static <A, B, C> ThreeValue<A, B, C> newMutilValue(A a, B b, C c) {
		return new ThreeValue<A, B, C>(a, b, c);
	}
}
import static generic.CreateMutilValue.newMutilValue;//静态导入

public class TestMutilValue {

	//模拟业务方法
	static TwoValue<String, Integer> service1() {
		//...这里为业务逻辑
		/*
		 * 当业务逻辑处理完后返回结果,但返回结果有两个值,因此我们创建
		 * TwoValue<A, B>泛型对象来存储它们后返回该实例对象,外界可
		 * 以通过使用TwoValue<A, B>实例的first与second属性来获取
		 * 第一个与第二个返回结果
		 */
		return newMutilValue("one", 1);
	}

	static ThreeValue<String, Integer, Boolean> service2() {
		//...
		return newMutilValue("two", 2, true);
	}

	public static void main(String[] args) {
		TwoValue<String, Integer> twoValue = service1();
		ThreeValue<String, Integer, Boolean> threeValue = service2();
		System.out.println(twoValue.first);
		System.out.println(twoValue.second);
		System.out.println(threeValue);
	}
}

泛型类另一实例——泛型栈

使用泛型创建一个泛型栈,它可以存储各种类型的引用类型对象,在编译时确定类型参数,这样在入栈与出栈时传递的参数类型都已确定,具体实现请参见《栈Stack 》。

泛型接口

泛型也可以应用于接口。例如生成器(generator),这是一种专门负责创建对象的类。实际上,这是工厂方法设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。也就是说,生成器无需额外的信息就知道如何创建新对象。一般而言,一个生成器只定义一个方法,该方法用以产生新的对象。在这里,就是next()方法

public interface Generator<T> {
	T next();
}

方法next()的返回类型是参数化的T。正如你所见到的,接口使用泛型与类使用泛型没什么区别。
为了演示如何实现Generator接口,我们还需要一些别的类。例如,Coffee类层次结构如下:

public class Coffee {
	private static long counter;//编号,初始值为0
	private final long id = counter++;

	public String toString() {
		return getClass().getSimpleName() + " " + id;
	}
}

class Mocha extends Coffee {
}

class Latte extends Coffee {
}

class Breve extends Coffee {
}

现在,我们可以编写一个类,实现Generator<Coffee>接口,它能够随机生成不同类型的Coffee对象:

import java.util.Iterator;
import java.util.Random;

public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> {
	private Class[] types = { Latte.class, Mocha.class, Breve.class, };
	private static Random rand = new Random(47);

	public CoffeeGenerator() {
	}

	// 为了使用For循环对该类实例进行迭代时次数控制:
	private int size = 0;

	public CoffeeGenerator(int sz) {
		size = sz;
	}

	public Coffee next() {
		try {
			//随机返回一个Coffee实例
			return (Coffee) types[rand.nextInt(types.length)].newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	//实现Iterator接口,这里属于黑箱迭代子模式
	private class CoffeeIterator implements Iterator<Coffee> {
		int count = size;

		public boolean hasNext() {
			return count > 0;
		}

		public Coffee next() {
			count--;
			//调用外部类的next方法,所以前面要明确指定外部类类型,
			//不然的话就是调用内部类自身的方法了
			return CoffeeGenerator.this.next();
		}

		public void remove() { // Not implemented
			throw new UnsupportedOperationException();
		}
	};

	//向外界提供迭代器的实现,这样可以用在foreach循环语句中
	public Iterator<Coffee> iterator() {
		return new CoffeeIterator();
	}

	public static void main(String[] args) {
		CoffeeGenerator gen = new CoffeeGenerator();
		//不使用迭代接口Iterator进行迭代时,由程序外部自己控制迭代过程
		for (int i = 0; i < 5; i++) {
			/*
			 * 某次输出:
			 * Breve 0
			 * Breve 1
			 * Mocha 2
			 * Breve 3
			 * Mocha 4
			 */
			System.out.println(gen.next());
		}

		/*
		 * 使用增加for循环,CoffeeGenerator实现了Iterable接口,所以它可以在
		 * 循环语句中使用。使用迭代子模式时迭代过程由迭代器来控制
		 */
		for (Coffee c : new CoffeeGenerator(5)) {
			/*
			 * 某次输出:
			 * Breve 5
			 * Mocha 6
			 * Breve 7
			 * Latte 8
			 * Mocha 9
			 */
			System.out.println(c);
		}
	}
}

参数化的Generator接口确保next()的返回值是参数的类型。CoffeeGenerator同时还实现了Iterable接口,所以它可以在循环语句中使用。不过,它还需要一个“末端哨兵”来判断何时停止,这正是由第二个构造器的传进的参数。

斐波拉契数列

下面的类是Generator<T>接口的另一个实现,它负责生成斐波拉契数列:

public class Fibonacci implements Generator<Integer> {
	protected int count = 0;//计数器

	public Integer next() {
		return fib(count++);//自动装箱
	}

	//递归求某数的斐波拉契
	private int fib(int n) {
		if (n < 2) {
			return 1;
		}
		return fib(n - 2) + fib(n - 1);
	}

	public static void main(String[] args) {
		Fibonacci gen = new Fibonacci();
		//输出0-9的斐波拉契
		for (int i = 0; i < 10; i++) {
			/*
			 * Output: 1(0) 1(1) 2(2) 3(3) 5(4) 8(5) 13(6) 21(7) 34(8) 55(9)
			 */
			System.out.print(gen.next() + "(" + gen.getIndex() + ")" + " ");
		}
	}

	public int getIndex() {
		return count - 1;
	}
}

如果还想更进一步,编写一个实现了Iterable的Fibonacci生成器。我们的一个选择是重写这个类,令其实现Iterable接口。不过,有时你并不是总能拥有源代码的控制权(比如这里的Fibonacci类是别人提供的一个class,我们根本没有源码时),并且,除非必须这么做,否则,我们也不愿意重写一个类。而且我们还有另一种选择,就是创建一个适配器(adapter)来实现所需的接口。有多种方法可以实现适配器。例如,我们这里通过继承来创建适配器类:

package generic;

import java.util.Iterator;

/*
 * 通过继承的方式把Fibonacci类适配成可在foreach语句中使用的类,Fibonacci本身就具
 * 备了迭代能力,提供了迭代接口next()方法,现在只需把这个方法适配成Iterable接口即可
 * 在foreach中进行迭代。这里的Iterable就是Target角色,Fibonacci就是Adaptee角色,
 * 而IterableFibonacci当然就是适配器Adapter。
 * 这样原本Fibonacci不能与foreach一起工作,但现在却可以了。
 */
public class IterableFibonacci extends Fibonacci implements Iterable<Integer> {
	private int n;

	public IterableFibonacci(int count) {
		n = count;
	}

	//实现可迭代接口,返回一个Iterator迭代器的实例
	public Iterator<Integer> iterator() {
		//匿名类
		return new Iterator<Integer>() {
			public boolean hasNext() {
				return n > 0;
			}

			public Integer next() {
				n--;
				//这里借助于Fibonacci的迭代方法完成迭代过程,注,这里的
				//IterableFibonacci.this.next()方法实质是Fibonacci自己的方法,由
				//IterableFibonacci继承而来
				return IterableFibonacci.this.next();
			}

			public void remove() { // Not implemented
				throw new UnsupportedOperationException();
			}
		};
	}

	public static void main(String[] args) {
		IterableFibonacci it = new IterableFibonacci(10);
		for (int i : it) {
			/* 
			 * Output: 
			 * 1(0) 1(1) 2(2) 3(3) 5(4) 8(5) 13(6) 21(7) 34(8) 55(9) 
			 */
			System.out.print(i + "(" + it.getIndex() + ")" + " ");
		}
	}
} 

一个通用的生成器——Generator

下面的程序可以为任何类构造一个Generator,只要该类具有默认的构造器。为了减少类型声明,它提供了一个泛型方法,用以生成BasicGenerator:

import java.util.Date;

/*
 * 通用生成器,它实例了泛型接口,可以根据指定的Class创建出相应的实例
 */
public class BasicGenerator<T> implements Generator<T> {
	private Class<T> type;

	public BasicGenerator(Class<T> type) {
		this.type = type;
	}

	public T next() {
		try {
			/*
			 * 注,
			 * (1)、要创建的类必须声明为public(因为BasicGenerator与要处理的类在不同
			 * 的包中,所以该类必须声明为public)
			 * (2)、要创建的类必须具备默认的构造器(无参数的构造器)
			 */
			return type.newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/*
	 * 根据给定的类型创建默认的生成器。外界只需执行BasicGenerator.create(MyType.class)
	 * 而不必执行麻烦的new BasicGenerator<MyType>(MyType.class),注这是一个静态的泛型
	 * 方法,类型参数T进行了重新的声明,可以与泛型类本身声明的类型参数名一样
	 */
	public static <T> Generator<T> create(Class<T> type) {
		return new BasicGenerator<T>(type);
	}

	//测试
	public static void main(String[] args) {
		Generator<Date> gen = BasicGenerator.create(Date.class);
		for (int i = 0; i < 5; i++) {
			Date date = gen.next();
			System.out.println(date);
		}
	}
}

泛型方法

可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为static泛型方法,而不能引用类中定义的类型参数。要定义泛型方法,只需将泛型参数列表置于返回值之前:public static <T> void f(T x) ;
注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断, 但类型推断只对赋值操作有效,其他时候并不起作用。如果你将一个泛型方法调用的结果(例如newMap())作为参数,传递给另一个方法,这时编译器并不会执行类型推断。在这种情况下,编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变量。下面的例子证明了这一点:

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LimitsOfInference {
	/*
	 * 外界创建一个Map对象时只需执行Map<String, List<String>> map = newMap();类
	 * 似的语句,而不必麻烦地使用Map<String, List<String>> map = new Map<String, 
	 * List<String>>();,所以可以试着把这些创建集合的代码集中封装到一个公共类中,省去创
	 * 建时指定类型,它可根据赋值语句前部分声明来推导出类型参数
	 */
	static <K, V> Map<K, V> newMap() {
		//编译时会根据赋值语句来推断 K,V 的参数类型
		return new HashMap<K, V>();
	}

	static void f(Map<String, List<String>> map) {
	}

	public static void main(String[] args) {
		//赋值语句能推断出newMap方法中的类型参数类型
		Map<String, List<String>> map = newMap();
		// Does not compile,因为类型推断只发生在赋值语句时
		//f(newMap());
	}
}

泛型方法显式的指定参数类型

在泛型方法中,可以显式地指明类型,不过这种语法很少使用。要显式地指明类型,必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。如果是在 定义该方法的类的内部,必须在点操作符之前使用this关键字,如果是使用static的方法,必须在点操作符之前加上类名。使用这种语法,可以解决 LimitsOfInference中的问题(不过,只有在编写非赋值语句时,我们才需要这样的额外的指定类型):

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LimitsOfInference {
	//静态的泛型方法
	static <K, V> Map<K, V> newMap() {
		return new HashMap<K, V>();
	}

	static void f(Map<String, List<String>> map) {
	}

	//非静态泛型方法
	<E> List<E> newList() {
		return new ArrayList<E>();
	}

	void g(List<String> map) {
		//h(newList());//compile-error
		//如果是在定义该方法的类的内部,必须在点操作符之前使用this关键字,然后在点后明确指定类型参数
		h(this.<String> newList());
	}

	void h(List<String> map) {
	}

	public static void main(String[] args) {
		//f(newMap());//compile-error
		// 调用static的泛型方法时指定参数类型时,必须在点操作符之前加上类名,参数类型放在点后
		f(LimitsOfInference.<String, List<String>> newMap());

		LimitsOfInference l = new LimitsOfInference();
		//l.g(l.newList());//compile-error
		//调用非静态泛型方法指定参数类型
		l.g(l.<String> newList());
	}
}

擦除

当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以声明ArrayList.class,但是不能声明ArrayList<Integer>.class,请考虑下面的情况:

		Class c1 = new ArrayList<String>().getClass();
		Class c2 = new ArrayList<Integer>().getClass();
		System.out.println(c1 == c2);//true

ArrayList<String>和ArrayList<Integer>是相同的类型,继续请看:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class Frob {
}

class Fnorkle {
}

class Quark<Q> {
}

class Particle<POSITION, MOMENTUM> {
}

public class LostInformation {
	public static void main(String[] args) {
		List<Frob> list = new ArrayList<Frob>();
		Map<Frob, Fnorkle> map = new HashMap<Frob, Fnorkle>();
		Quark<Fnorkle> quark = new Quark<Fnorkle>();
		Particle<Long, Double> p = new Particle<Long, Double>();
		System.out.println(Arrays.toString(list.getClass().getTypeParameters()));//[E]
		System.out.println(Arrays.toString(map.getClass().getTypeParameters()));//[K, V]
		System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));//[Q]
		System.out.println(Arrays.toString(p.getClass().getTypeParameters()));//[POSITION, MOMENTUM]
	}
}

根据JDK文档的描述,Class.getTypeParameters()将“返回一个TypeVariable对象数组,表示有泛型声明所声明的类型参数……”这好像是在暗示你可能发现参数类型的信息,但是,正如你从输出中所看到的,你能够发现输出的只是用作参数占位符的标识符,这并非有用的信息。因此,残酷的现实是:在泛型代码内部,无法获得任何有关泛型参数类型的信息
因此,你可以知道诸如类型参数标识符(像上面程序所输出的)和泛型类型边界这类的信息——你却无法知道用来创建某个特定实例的实际的类型参数,这与C++不同。
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此 List<String>和List<Integer>在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生”类型,即List。
下面看看C++与java中的模板对比,这样更易理解他们之间的区别。

C++的方式
下面是C++模板,用于参数化类型的语法与java十分相似,因为Java是受C++的启发:

#include <iostream>
using namespace std;
template<class T> class Manipulator {
  T obj;
public:
  Manipulator(T x) { obj = x; }
  void manipulate() { obj.f(); }//C++泛型可以调用类型参数对象的方法,java是不可以的
};

class HasF {
public:
  void f() { cout << "HasF::f()" << endl; }
};

int main() {
  HasF hf;
  Manipulator<HasF> manipulator(hf);
  manipulator.manipulate();
} /* Output:
HasF::f()
///:~

Manipulator类存储了一个类型T的对象,有意思的地方是manipulate()方法,它在obj上调用方法f()。它怎么能知道类型参数T有f()方法的呢?当你实例化这个模版时,C++编译器将进行检查,因此在Manipulator<HasF>被实例化(C++编译后还会存在类型参数的信息,但Java在编译时就擦除掉了)的这一刻,它看 到HasF拥有一个方法f()。如果没有这个方法,就会得到一个编译期错误,这样类型安全就得到了保障。
Java泛型就不同了。下面是HasF的Java版本:

public class HasF {
	public void f() {
		System.out.println("HasF.f()");
	}
}

 

class Manipulator<T> {
	private T obj;

	public Manipulator(T x) {
		obj = x;
	}

	public void manipulate() {
		//! Error: cannot find symbol: method f():
		obj.f();
	}
}

public class Manipulation {
	public static void main(String[] args) {
		HasF hf = new HasF();
		Manipulator<HasF> manipulator = new Manipulator<HasF>(hf);
		manipulator.manipulate();
	}
}

由于有了擦除,Java编译器无法在obj上调用f(),因为运行时无法将f()映射到HasF上。为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。这里重用了extends关键字。由于有了边界,下面的代码就可以编译了:
class Manipulator<T extends HasF>
边界<T extends HasF>声明T必须是类型HasF或者是HasF的子类,这样就可以安全地在obj上调用f()了。
泛型类型参数将擦除到它的第一个边界(它可能会有多个边界,稍候你就会看到),在编译时,编译器实际上会把类型参数替换为它的擦除边界类型,就像上面的示例一样,T擦除到了HasF,就好像在类的声明中用HasF替换了T一样。
上面的class Manipulator<T extends HasF>类在编译时进行了擦除,擦除后成了没有泛型的类,就好像是:

class Manipulator {
	private HasF obj;

	public Manipulator(HasF x) {
		obj = x;
	}

	public void manipulate() {
		obj.f();
	}
}

擦除只因为兼容性

擦除这不是一个语言特性。它是Java的泛型实现中的一种折中,因为泛型不是Java语言出现时就有的组成部分,所以这种折中是必需的。
如果泛型在Java 1.0中就已经是其一部分了,那么这个特性将不会使用擦除来实现——它将使用具体化,使类型参数保持为第一类实体,因此你就能够在类型参数上执行基于类型的语言操作和反射操作。
在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如,诸如List<T>这样的类型注解将被擦除为List,而普通的类型变量在未指定边界的情况下将被擦除为Object。
Java使用擦除来实现泛型的真真动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”。因此Java泛型不仅必须支持向后兼容性,即现有的代码和类文件仍旧合法,并且继续保持其之前的含义;而且还要支持迁移兼容性,使得类库变为泛型的,并且当某个类库变为泛型时,不会破坏依赖于它的代码和应用程序。在决定这就是目标之后,设计者们决策认为擦除是唯一可行的解决方案。通过允许非泛型代码与泛型代码共存,擦除使得这种向着泛型的迁移成为可能。

擦除的问题

因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直到客户端使用用泛型重写这些代码。这是一个时间性的问题,因为它不会突然间破坏所有现有的代码
擦除的代价是显著的。泛型不能用于显式地引用于运行时类型的操作之中,例如转型、instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了,无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。因此,如果你编写了下面这样的代码段:
class Foo<T>{T var;}
那么,看起来当你在创建Foo的实例时:
Foo<Cat> f = new Foo<Cat>();
class Foo中的代码应该知道现在工作于Cat之上,而泛型语法也在强烈暗示:在整个类中的各个地方,类型T都在被替换 。但是事实并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object 。”
另外,擦除和迁移兼容性意味着,使用泛型并不是强制的,所以你可以这样:

class GenericBase<T> {
	private T element;

	public void set(T arg) {
		element = arg;
	}

	public T get() {
		return element;
	}
}

@SuppressWarnings("unchecked")
class Derived2 extends GenericBase {//子类没有泛化也可,但会警告
} //warning

public class ErasureAndInheritance {
	public static void main(String[] args) {
		Derived2 d2 = new Derived2();
		//由于类型的擦除,返回的为Object类型
		Object obj = d2.get();
		d2.set("str"); // Warning here!
		System.out.println(d2.get());
	}
}

边界处的动作

泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型

擦除的补偿

擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作:

public class Erased<T> {
	private static final int SIZE = 100;

	public void f(Object arg) {
		//if(arg instanceof T) {}          // Error
		//T var = new T();                 // Error
		//T[] array = new T[SIZE];         // Error
		//ArrayList<String> genArr[] = new ArrayList<String>[2]; // 此句无法通过编译,不能创类型参数的数组
		T[] array = (T[]) new Object[SIZE]; // Unchecked warning
	}
}

 

示例中对使用instanceof的尝试最终失败了,因为其类型信息已经被擦除了。如果引入类型标签,就可以转而使用动态的isInstance():

class Building {
}

class House extends Building {
}

public class ClassTypeCapture<T> {
	Class<T> kind;

	public ClassTypeCapture(Class<T> kind) {
		this.kind = kind;
	}

	public boolean f(Object arg) {
		//判定指定的 Object 是否与此 Class 所表示的对象赋值兼容。此方法
		//是 Java 语言 instanceof 运算符的动态等效方法
		return kind.isInstance(arg);
	}

	public static void main(String[] args) {
		ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
		System.out.println(ctt1.f(new Building()));//true
		System.out.println(ctt1.f(new House()));//true
		ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
		System.out.println(ctt2.f(new Building()));//false
		System.out.println(ctt2.f(new House()));//true
	}
}

创建类型实例

在Erased中对创建一个new T()的尝试将无法实现,部分原因是因为擦除,而另一部分原因是因为编译器不能验证T具有默认(无参)构造器。Java中的解决方案是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象就是Class对象,因此如果使用类型标签,那么你就可以使用newInstance()来创建这个类型的新对象:

class ClassAsFactory<T> {
	T x;

	public ClassAsFactory(Class<T> kind) {
		try {
			x = kind.newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

class Employee {
}

public class InstantiateGenericType {
	public static void main(String[] args) {
		ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class);
		//ClassAsFactory<Employee> succeeded
		System.out.println("ClassAsFactory<Employee> succeeded");
		try {
			ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class);
		} catch (Exception e) {
			//ClassAsFactory<Integer> failed
			System.out.println("ClassAsFactory<Integer> failed");
		}
	}
}

这可以编译,但是运行时会因ClassAsFactory<Integer>而失败,因为Integer没有任何默认的构造器。因为这个错误不是在编译期捕获的,所以建议使用显式的工厂,并将限制其类型,使得只能接受实现了这个工厂的类:

//工厂泛型接口,创建某个T的实例前需实现创建它的工厂
interface FactoryI<T> {
	//工厂方法,实例创建接口,因为不同的类可能具有不同的创建方式
	T create();
}

//Integer实例工厂,实现了FactoryI泛型接口
class IntegerFactory implements FactoryI<Integer> {
	public Integer create() {
		//Integer没有默认的构造函数,创建时需传入参数
		return new Integer(0);
	}
}

class Widget {
	//这里巧妙地使用了静态的内部类来充当Widget的工厂类,很适合使用内部类~~!
	public static class Factory implements FactoryI<Widget> {
		public Widget create() {
			return new Widget();
		}
	}
}

class Foo2<T> {
	public final T x;

	//只接收实现了FactoryI泛型接口的工厂实例
	public <F extends FactoryI<T>> Foo2(F factory) {
		//调用工厂方法
		x = factory.create();
	}
}

public class FactoryConstraint {
	public static void main(String[] args) {
		//创建Integer实例
		Integer intg = new Foo2<Integer>(new IntegerFactory()).x;
		//创建Widget实例
		Widget wg = new Foo2<Widget>(new Widget.Factory()).x;
	}
}

另一种方式是模版方法设计模式。在下面的示例中,create ()是模版方法,而create()是在父类中定义的、用来产生子类类型的对象:

abstract class GenericWithCreate<T> {
	final T element;

	GenericWithCreate() {
		element = create();
	}

	//模板方法
	abstract T create();
}

class X {
}

class XCreator extends GenericWithCreate<X> {
	X create() {
		return new X();
	}

	void f() {
		System.out.println(element.getClass().getSimpleName());
	}
}

class IntgCreator extends GenericWithCreate<Integer> {
	Integer create() {
		return new Integer(0);
	}

	void f() {
		System.out.println(element.getClass().getSimpleName());
	}
}

public class CreatorGeneric {
	public static void main(String[] args) {
		XCreator xc = new XCreator();
		xc.f();//X
		X x = xc.element;
		IntgCreator ic = new IntgCreator();
		ic.f();//Integer
		Integer intg = ic.element;
	}
}

泛型数组

正如你在Erased中所见,不能创建泛型数组。一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList:

import java.util.ArrayList;
import java.util.List;

public class ListOfGenerics<T> {
	private List<T> array = new ArrayList<T>();

	public void add(T item) {
		array.add(item);
	}

	public T get(int index) {
		return array.get(index);
	}
}

有时,你仍旧希望创建泛型类型的数组(例如,ArrayList内部使用的是数组 E[] elementData;),这是可以的,你可以定义一个数组引用,例如:

class Generic<T> {
}

public class ArrayOfGenericReference {
	static Generic<Integer>[] gia;
}

编译器将接受这个程序,而不会产生任何警告。但是,永远都不能创建这个确切类型的数组(包括类型参数),因此这有一点令人困惑。
如果我们创建一个Object数组,并将其转型为所希望的数组类型。事实上这可以编译,但是不能运行,它将产生ClassCase-Exception:

package generic;

public class ArrayOfGeneric {
	static final int SIZE = 100;
	static Generic<Integer>[] gia;

	public static void main(String[] args) {
		// Compiles; produces ClassCastException:
		//! gia = (Generic<Integer>[])new Object[SIZE];
		// Runtime type is the raw (erased) type:
		gia = (Generic<Integer>[]) new Generic[SIZE];
		// 编译通不过,因为不能创建泛型数组
		//! gia = new Generic<Integer>[SIZE];
		System.out.println(gia.getClass().getSimpleName());
		gia[0] = new Generic<Integer>();
		//! gia[1] = new Object(); // Compile-time error
		// Discovers type mismatch at compile time:
		//! gia[2] = new Generic<Double>();
	}
}

 

上面(gia = (Generic<Integer>[])new Object[SIZE]; )的问题在于数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的,因此,即使gia已经被转型为 Generic<Integer>[],但是这个信息只存在于编译期(并且如果没有@Suppress Warnings注解,你将得到有关这个转型的警告)。在运行时,它仍旧是Object数组,因而引发问题。成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。

让我们看一个更复杂的示例。考虑一个简单的泛型数组包装器:

public class GenericArray<T> {
	private T[] array;

	public GenericArray(int sz) {
		/*
		 * 这里这样做只是为了通过编译器的类型检查,在编译时会擦除掉T,使用
		 * Object替换T,所以在运行时是正确的,这里按常理构造一个Object数
		 * 组后强制转换成其他任何类型时一定会报错,如:String[] strArr
		 * =(String[]) new Object[sz];运行时肯定会报ClassCastException
		 */
		array = (T[]) new Object[sz]; //外界不管怎样转型,实质都为Object   
	}

	public void put(int index, T item) {
		array[index] = item;
	}

	public T get(int index) {
		return array[index];
	}

	// 注,这里返回的其实还是Object数据,这里由于编译时擦除引起
	public T[] rep() {
		return array;
	}

	public static void main(String[] args) {
		GenericArray<Integer> gai = new GenericArray<Integer>(10);
		// This causes a ClassCastException:
		//! Integer[] ia = gai.rep();
		// This is OK:
		Object[] oa = gai.rep();
		System.out.println(oa.getClass());
	}
}

与前面相同,我们并不能声明T[] array = new T[sz],因此我们创建了一个对象数组,然后将其转型。rep()方法将返回T[],它在main()中将用于gai,因此应该是Integer[],但是如果调用它,并尝试着将结果赋值给Integer[]引用,就会得到ClassCastException,这还是因为实际的运行时类型是Object[]。
因为有了擦除,数组的运行时类型就只能是Object[]。如果我们立即将其转型为T[],那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。正因为这样,最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。让我们看看这是如何作用于GenericArray.java示例的:

public class GenericArray2<T> {
	private Object[] array;

	public GenericArray2(int sz) {
		array = new Object[sz];//外界不管怎样转型,实质都为Object
	}

	public void put(int index, T item) {
		array[index] = item;
	}

	public T get(int index) {
		return (T) array[index];/*转型*/
	}

	public T[] rep() {
		//Object数组转换Object数组当然没有问题,编译与运行时都没有问题,但运行时返回的为Object数组
		return (T[]) array; // Warning: unchecked cast
	}

	public static void main(String[] args) {
		GenericArray2<Integer> gai = new GenericArray2<Integer>(10);
		for (int i = 0; i < 10; i++) {
			gai.put(i, i);
		}
		for (int i = 0; i < 10; i++) {
			Integer intg = gai.get(i);
			//0 1 2 3 4 5 6 7 8 9
			System.out.print(intg + " ");
		}
		System.out.println();
		try {
			Integer[] ia = gai.rep();//只能通过编译,运行就会抛异常
		} catch (Exception e) {
			//java.lang.ClassCastException: [Ljava.lang.Object;
			System.out.println(e);
		}
	}
}

初看起来,这好像没多大变化,只是转型挪了地方。但是,现在的内部表示是Object[]而不是T[]。当get()被调用时,它将对象转型为T,这实际上是正确的类型,因此这是安全的。然而,如果你调用rep(), 它还是尝试着将Object[]转型为T[],这仍旧是不正确的,将在编译期产生警告,在运行时产生异常。因此,没有任何方式可以推翻底层的数组类型,它只能是Object[]。在内部将array当作Object[]而不是T[]处理的优势是:我们不太可能忘记这个数组的运行时类型,从而意外地引入缺陷。

从上面两个程序可以看出在创建数组时都是直接采用new Object[sz];方式来创建的,所以在外界使用一个非Object数组变量来引用该Object数组会出错误运行时转型异常,为了外界使用真真类型来引用数组,则应该使用Array.newInstance方式动态地创建数组,所以,应该传递一个类型标记。在这种情况下,GenericArray看起来会像下面这样:

import java.lang.reflect.Array;

public class GenericArrayWithTypeToken<T> {
	private T[] array;

	public GenericArrayWithTypeToken(Class<T> type, int sz) {
		//这里运行时实质上是转换成了Object类型,向上转换是可以的
		array = (T[]) Array.newInstance(type, sz);
	}

	public void put(int index, T item) {
		array[index] = item;
	}

	public T get(int index) {
		return array[index];
	}

	//这里实质上返回的还是Object数组,只是在返回赋值给引用变量时编译时会插入强制转型字节码:
	public T[] rep() {
		return array;
	}

	public static void main(String[] args) {
		GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(
				Integer.class, 10);
		/*
		 * 现在返回的是一个真真的Integer数组了,在返回的操作边界插入了强制转换操作:
		 * (Integer[])gai.rep();,可以看生成的字节码证明,这里之所以能强制转换是
		 * 因为返回的数组本身是一个Integer数组
		 */
		Integer[] ia = gai.rep();
	}
}

类型标记Class<T>被传递到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组。一旦我们获得了实际类型,就可以返回它,并获得想要的结果,就像在main()中看到的那样。该数组的运行时类型是确切的类型T[]。

遗憾的是,如果查看Java SE5标准类库中的源代码,你就会看到从Object数组到参数化类型的转型遍及各处。例如,下面是经过整理和简化之后的从Collection中复制ArrayList的构造器:

public ArrayList(Collection c) {
        size = c.size();       
        elementData = (E[]) new Object[size];
        //...
}

Neal Gafter(Java SE5的领导开发者之一)在他的博客中指出,在重写Java类库时,他十分懒散,而我们不应该像他那样。Neal还指出,在不破坏现有接口的情况下,他将无法修改某些Java类库代码。因此,即使在Java类库源代码中出现了某些惯用法,也不能表示这就是正确的解决之道。当查看类库代码时,你不能认为它就是应该在自己的代码中遵循的示例。

分享到:
评论

相关推荐

    侯捷-java编程思想.pdf

    《侯捷-Java编程思想》是一本深受Java开发者喜爱的经典著作,尽管是繁体版本,但其中也包含英文内容,方便不同语言背景的读者理解。这本书深入浅出地讲解了Java编程的核心概念和技术,旨在帮助读者掌握Java编程的...

    Java编程思想全面的答案

    《Java编程思想全面的答案》这份资源集合了众多Java编程思想的学习习题解答,旨在帮助学习者深入理解并掌握Java编程的核心概念。以下是对这个主题的详细解析: Java编程思想是学习Java编程的基础,它涵盖了面向对象...

    java 编程思想 英文版 第四版(非扫描pdf)

    《Thinking in Java》是Bruce Eckel创作的一部Java编程经典著作,英文版的第四版更是深受全球程序员喜爱的参考资料。这本书全面深入地介绍了Java语言,不仅适合初学者,也对有经验的程序员提供了宝贵的洞见。它以...

    java编程第4版完整版,完整书签,扫描但文字可搜索复制

    《Java编程思想》第四版详细阐述了泛型的概念、泛型类、泛型方法以及通配符的使用,使得读者能够编写更加安全、灵活的代码。 #### 集合框架 Java的集合框架是用于存储和操作数据的高效工具。本书深入讲解了集合...

    Java编程思想及Struts2

    描述中提到的“Struts2技术内幕——深入解析Struts2架构设计与实现原理及Java编程思想”,表明了文档内容会涉及对Struts2框架架构层面的深入探讨,包括它的核心组件、工作流程、请求处理机制以及与其他Java技术的...

    JAVA编程思想习题及答案

    《JAVA编程思想习题及答案》是一份针对Java编程学习者的宝贵资源,它涵盖了广泛的Java编程概念和技术,旨在帮助读者深化理解并提升实践能力。这份资料包含了一系列的习题和对应的解答,对于初学者和进阶者都是极好的...

    java 编程思想

    《Java编程思想》是 Bruce Eckel 的经典著作,这本书深入浅出地介绍了Java语言的核心概念和技术,对于初学者和有经验的程序员来说都是极好的学习资源。PDF版使得读者可以随时随地电子阅读,方便快捷。 首先,Java...

    晾晾多年珍藏 Java编程思想.chm

    《Java编程思想》是 Bruce Eckel 的经典著作,这本书深入浅出地讲解了Java语言的核心概念和编程技术,是许多Java开发者入门和进阶的重要参考书籍。从标题来看,我们聚焦的是Java编程这一主题,而描述中提到的链接...

    面向对象程序设计——javaPPT课件(第二版)

    这篇“面向对象程序设计——Java PPT课件(第二版)”由西安电子科技大学出版社出版,提供了深入浅出的Java编程知识体系,适合初学者及有一定经验的开发者学习和参考。 1. **面向对象概念**:课件可能涵盖了面向...

    java编程的逻辑(高清百度盘)

    ### Java编程的逻辑 #### 一、概述 《Java编程的逻辑》由马俊昌撰写,本书主要聚焦于Java编程的核心概念与技术要点,适用于Java 5及更高版本的开发者。书中不仅涵盖了Java语言的基础知识,还深入探讨了近年来Java...

    Java编程思想笔记(全)

    ### Java编程思想笔记知识点概述 #### 第 1 章 对象导论 在这一章节中,主要介绍了Java中的基本概念——对象。对象是面向对象编程的核心,它封装了数据和行为。本章首先解释了对象的概念,接着讨论了如何创建对象...

    JAVA编程思想

    《JAVA编程思想》是Java开发领域的一本经典著作,它深入浅出地讲解了Java语言的核心概念、语法以及编程范式。这本书对于理解和掌握Java编程的精髓至关重要,无论是初学者还是经验丰富的开发者,都能从中受益匪浅。...

    java变成思想第四版中文版 练习题答案

    《Java编程思想》是 Bruce Eckel 的经典著作,第四版更是深受广大Java程序员喜爱。这本书深入浅出地讲解了Java语言的核心概念和技术,包括面向对象编程、类与对象、继承与多态、接口、异常处理、集合框架、泛型、...

    java编程思想

    《Java编程思想》是Java开发领域的一本经典著作,作者是Bruce Eckel。这本书深入浅出地介绍了Java语言的核心概念和编程技术,旨在帮助开发者理解并掌握Java编程的精髓。通过对这本书的学习,开发者可以对Java有全面...

    Thinking in Java——自己手写的代码

    《Thinking in Java》是Bruce Eckel的经典编程教材,它深入浅出地介绍了Java语言的核心概念和技术。这本书以其详尽的示例和丰富的实践性而受到广大程序员的喜爱。在这个压缩包中,你可能找到了作者自己手写的代码...

    Java编程思想里的泛型实现一个堆栈类 分享

    在Java编程中,泛型是一种强大的工具,它允许我们在编写代码时指定容器(如堆栈、队列等)可以容纳的数据类型,从而提高了代码的可读性和安全性。在这个例子中,我们将探讨如何使用泛型实现一个堆栈类——`...

    java面向对象编程思想2版本+源代码 (孙卫琴)

    《Java面向对象编程思想》是孙卫琴老师的一本经典教程,主要针对Java语言的核心特性——面向对象编程进行深入解析。这本书的第二版本基于2017年的最新技术和最佳实践进行了更新,旨在帮助读者掌握Java编程的核心技巧...

Global site tag (gtag.js) - Google Analytics