java编程思想--15泛型

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

/*
* 存储两个值的对象,第一个与第二个结果的类型在编译时确定,可以是任
* 何类型的对象(基本类型会自动装箱),这样就是一个名副其实的的类模
* 板,只是要存储的结果是两个则可以使用。
* 这里我们只创建到可存储三个返回结果的类,如果需要,可以创建出存储
* 四个、五个返回结果的泛型类
*/
public class TwoValue {
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 extends TwoValue {
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 two = new TwoValue("one",1);
* 但使用该工具类可简化创建的语句:
* TwoValue two = CreateValue.newValue("one",1);
*/
public class CreateMutilValue {
//创建附带两个返回值的对象
public static TwoValue newMutilValue(A a, B b) {
return new TwoValue(a, b);
}

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

public class TestMutilValue {

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

static ThreeValue service2() {
//...
return newMutilValue("two", 2, true);
}

public static void main(String[] args) {
TwoValue twoValue = service1();
ThreeValue threeValue = service2();
System.out.println(twoValue.first);
System.out.println(twoValue.second);
System.out.println(threeValue);
}
}泛型类另一实例——泛型栈
使用泛型创建一个泛型栈,它可以存储各种类型的引用类型对象,在编译时确定类型参数,这样在入栈与出栈时传递的参数类型都已确定,具体实现请参见《栈Stack 》。

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

public interface Generator {
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对象:

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

public class CoffeeGenerator implements Generator, Iterable {
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 {
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 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接口的另一个实现,它负责生成斐波拉契数列:

public class Fibonacci implements Generator {
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 {
private int n;

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

//实现可迭代接口,返回一个Iterator迭代器的实例
public Iterator iterator() {
//匿名类
return new Iterator() {
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 implements Generator {
private Class type;

public BasicGenerator(Class 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.class),注这是一个静态的泛型
* 方法,类型参数T进行了重新的声明,可以与泛型类本身声明的类型参数名一样
*/
public static Generator create(Class type) {
return new BasicGenerator(type);
}

//测试
public static void main(String[] args) {
Generator 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 void f(T x) ;
注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断, 但类型推断只对赋值操作有效,其他时候并不起作用。如果你将一个泛型方法调用的结果(例如newMap())作为参数,传递给另一个方法,这时编译器并不会执行类型推断。在这种情况下,编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变量。下面的例子证明了这一点:

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

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

static void f(Map> map) {
}

public static void main(String[] args) {
//赋值语句能推断出newMap方法中的类型参数类型
Map> 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 Map newMap() {
return new HashMap();
}

static void f(Map> map) {
}

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

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

void h(List map) {
}

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

LimitsOfInference l = new LimitsOfInference();
//l.g(l.newList());//compile-error
//调用非静态泛型方法指定参数类型
l.g(l. newList());
}
}擦除
当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以声明ArrayList.class,但是不能声明ArrayList.class,请考虑下面的情况:

Class c1 = new ArrayList().getClass();
Class c2 = new ArrayList().getClass();
System.out.println(c1 == c2);//trueArrayList和ArrayList是相同的类型,继续请看:

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 {
}

class Particle {
}

public class LostInformation {
public static void main(String[] args) {
List list = new ArrayList();
Map map = new HashMap();
Quark quark = new Quark();
Particle p = new Particle();
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和List在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生”类型,即List。
下面看看C++与java中的模板对比,这样更易理解他们之间的区别。

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

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

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

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

class Manipulator {
private HasF obj;

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

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

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

class GenericBase {
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 {
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 genArr[] = new ArrayList[2]; // 此句无法通过编译,不能创类型参数的数组
T[] array = (T[]) new Object[SIZE]; // Unchecked warning
}
}

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

class Building {
}

class House extends Building {
}

public class ClassTypeCapture {
Class kind;

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

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

public static void main(String[] args) {
ClassTypeCapture ctt1 = new ClassTypeCapture(Building.class);
System.out.println(ctt1.f(new Building()));//true
System.out.println(ctt1.f(new House()));//true
ClassTypeCapture ctt2 = new ClassTypeCapture(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 x;

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

class Employee {
}

public class InstantiateGenericType {
public static void main(String[] args) {
ClassAsFactory fe = new ClassAsFactory(Employee.class);
//ClassAsFactory succeeded
System.out.println("ClassAsFactory succeeded");
try {
ClassAsFactory fi = new ClassAsFactory(Integer.class);
} catch (Exception e) {
//ClassAsFactory failed
System.out.println("ClassAsFactory failed");
}
}
}这可以编译,但是运行时会因ClassAsFactory而失败,因为Integer没有任何默认的构造器。因为这个错误不是在编译期捕获的,所以建议使用显式的工厂,并将限制其类型,使得只能接受实现了这个工厂的类:

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

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

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

class Foo2 {
public final T x;

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

public class FactoryConstraint {
public static void main(String[] args) {
//创建Integer实例
Integer intg = new Foo2(new IntegerFactory()).x;
//创建Widget实例
Widget wg = new Foo2(new Widget.Factory()).x;
}
}另一种方式是模版方法设计模式。在下面的示例中,create ()是模版方法,而create()是在父类中定义的、用来产生子类类型的对象:

abstract class GenericWithCreate {
final T element;

GenericWithCreate() {
element = create();
}

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

class X {
}

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

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

class IntgCreator extends GenericWithCreate {
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 {
private List array = new ArrayList();

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

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

class Generic {
}

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

package generic;

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

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


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

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

public class GenericArray {
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 gai = new GenericArray(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 {
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 gai = new GenericArray2(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 {
private T[] array;

public GenericArrayWithTypeToken(Class 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 gai = new GenericArrayWithTypeToken(
Integer.class, 10);
/*
* 现在返回的是一个真真的Integer数组了,在返回的操作边界插入了强制转换操作:
* (Integer[])gai.rep();,可以看生成的字节码证明,这里之所以能强制转换是
* 因为返回的数组本身是一个Integer数组
*/
Integer[] ia = gai.rep();
}
}类型标记Class被传递到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组。一旦我们获得了实际类型,就可以返回它,并获得想要的结果,就像在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泛型编程时,编译器忽略泛型参数的具体类型,认为使用泛型的类、方法对Object都适用,这在泛型编程中称为类型信息檫除。
例如:
class GenericType{
public static void main(String[] args){
System.out.println(new ArrayList().getClass());
System.out.println(new ArrayList().getClass());
}
}

输出结果为:
java.util.ArrayList
java.util.ArrayList
泛型忽略了集合容器中具体的类型,这就是类型檫除。
但是如果某些泛型的类/方法只想针对某种特定类型获取相关子类应用,这时就必须使用泛型边界来为泛型参数指定限制条件。
例如:
interface HasColor{
java.awt.Color getColor();
}
class Colored{
T item;
Colored(T item){
this.item = item;
}
java.awt.Color color(){
//调用HasColor接口实现类的getColor()方法
return item.getColor();
}
}
class Dimension{
public int x, y, z;
}
Class ColoredDimension{
T item;
ColoredDimension(T item){
this.item = item;
}
T getItem(){
return item;
}
java.awt.Color color(){
//调用HasColor实现类中的getColor()方法
return item.getColor();
}
//获取Dimension类中定义的x,y,z成员变量
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
}
interface Weight{
int weight();
}
class Solid{
T item;
Solide(T item){
this.item = item;
}
T getItem(){
return item;
}
java.awt.Color color(){
//调用HasColor实现类中的getColor()方法
return item.getColor();
}
//获取Dimension类中定义的x,y,z成员变量
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
int weight(){
//调用Weight接口实现类的weight()方法
return item.weight();
}
}
class Bounded extends Dimension implements HasColor, Weight{
public java.awt.Color getColor{
return null;
}
public int weight(){
return 0;
}
}
public class BasicBounds{
public static void main(String[] args){
Solid solid = new Solid(new Bounded());
solid.color();
solid.getX();
solid.getY();
solid.getZ();
solid.weight();
}
}

Java泛型编程中使用extends关键字指定泛型参数类型的上边界(后面还会讲到使用super关键字指定泛型的下边界),即泛型只能适用于extends关键字后面类或接口的子类。
Java泛型编程的边界可以是多个,使用如语法来声明,其中只能有一个是类,并且只能是extends后面的第一个为类,其他的均只能为接口(和类/接口中的extends意义不同)。
使用了泛型边界之后,泛型对象就可以使用边界对象中公共的成员变量和方法。
2.泛型通配符:
泛型初始化过程中,一旦给定了参数类型之后,参数类型就会被限制,无法随着复制的类型而动态改变,如:
class Fruit{
}
class Apple extends Fruit{
}
class Jonathan extends Apple{
}
class Orange extends Fruit{
}
如果使用数组:
public class ConvariantArrays{
Fruit fruit = new Apple[10];
Fruit[0] = new Apple();
Fruit[1] = new Jonathan();
try{
fruit[0] = new Fruit();
}catch(Exception e){
System.out.println(e);
}
try{
fruit[0] = new Orange();
}catch(Exception e){
System.out.println(e);
}
}

编译时没有任何错误,运行时会报如下异常:
java.lang.ArrayStoreException:Fruit
java.lang.ArrayStoreException:Orange
为了使得泛型在编译时就可以进行参数类型检查,我们推荐使用java的集合容器类,如下:
public class NonConvariantGenerics{
List flist = new ArrayList();
}

很不幸的是,这段代码会报编译错误:incompatible types,不兼容的参数类型,集合认为虽然Apple继承自Fruit,但是List的Fruit和List的Apple是不相同的,因为泛型参数在声明时给定之后就被限制了,无法随着具体的初始化实例而动态改变,为解决这个问题,泛型引入了通配符”?”。
对于这个问题的解决,使用通配符如下:
public class NonConvariantGenerics{
List flist = new ArrayList();
}

泛型通配符”?”的意思是任何特定继承Fruit的类,java编译器在编译时会根据具体的类型实例化。
另外,一个比较经典泛型通配符的例子如下:
public class SampleClass < T extendsS> {…}
假如A,B,C,…Z这26个class都实现了S接口。我们使用时需要使用到这26个class类型的泛型参数。那实例化的时候怎么办呢?依次写下
SampleClass a = new SampleClass();
SampleClass a = new SampleClass();

SampleClass a = new SampleClass();
这显然很冗余,还不如使用Object而不使用泛型,使用通配符非常方便:
SampleClass sc = newSampleClass();
3.泛型下边界:
在1中大概了解了泛型上边界,使用extends关键字指定泛型实例化参数只能是指定类的子类,在泛型中还可以指定参数的下边界,是一super关键字可以指定泛型实例化时的参数只能是指定类的父类。
例如:
class Fruit{
}
class Apple extends Fruit{
}
class Jonathan extends Apple{
}
class Orange extends Fruit{
}
public superTypeWildcards{
public static void writeTo(List apples){
apples.add(new Apple());
apples.add(new Jonathan());
}
}

通过? Super限制了List元素只能是Apple的父类。
泛型下边界还可以使用,但是注意不能使用,即super之前的只能是泛型通配符,如:
public class GenericWriting{
static List apples = new ArrayList();
static List fruits = new ArrayList();
static void writeExact(List list, T item){
list.add(item);
}
static void writeWithWildcards(List list, T item){
list.add(item);
}
static void f1(){
writeExact(apples, new Apple());
}
static void f2(){
writeWithWildcards(apples, new Apple());
writeWithWildcards(fruits, new Apple());
}
public static void main(String[] args){
f1();
f2();
}
}

4.无边界的通配符:
泛型的通配符也可以不指定边界,没有边界的通配符意思是不确定参数的类型,编译时泛型檫除类型信息,认为是Object类型。如:
public class UnboundedWildcard{
static List list1;
static List list2;
static List list3;
static void assign1(List list){
list1 = list;
list2 = list;
//list3 = list; //有未检查转换警告
}
static void assign2(List list){
list1 = list;
list2 = list;
list3 = list;
}
static void assign3(List list){
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args){
assign1(new ArrayList());
assign2(new ArrayList());
//assign3(new ArrayList()); //有未检查转换警告
assign1(new ArrayList());
assign2(new ArrayList());
assign3(new ArrayList());
List wildList = new ArrayList();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
}

List和List的区别是:List是一个原始类型的List,它可以存放任何Object类型的对象,不需要编译时类型检查。List等价于List,它不是一个原始类型的List,它存放一些特定类型,只是暂时还不确定是什么类型,需要编译时类型检查。因此List的效率要比List高。
5.实现泛型接口注意事项:
由于泛型在编译过程中檫除了参数类型信息,所以一个类不能实现以泛型参数区别的多个接口,如:
interface Payable{
}
class Employee implements Payable{
}
class Hourly extends Employee implements Payable{
}

类Hourly无法编译,因为由于泛型类型檫除,Payable和Payable在编译时是同一个类型Payable,因此无法同时实现一个接口两次。
6.泛型方法重载注意事项:
由于泛型在编译时将参数类型檫除,因此以参数类型来进行方法重载在泛型中要特别注意,如:
public class GenericMethod{
void f(List v) {
}
void f(List v){
}
}

无法通过编译,因为泛型檫除类型信息,上面两个方法的参数都被看作为Object类型,使用参数类型已经无法区别上面两个方法,因此无法重载。
7.泛型中的自绑定:
通常情况下,一个类无法直接继承一个泛型参数,但是你可以通过继承一个声明泛型参数的类,这就是java泛型编程中的自绑定,如:
class SelfBounded>{
T element;
SelfBounded set(T arg){
Element = arg;
return this;
}
T get(){
return element;
}
}
class A extends SelfBounded{
}
class B extends SelfBounded
{
}
class C extends SelfBounded{
C setAndGet(C arg){
set(arg);
return get();
}
}
public class SelfBounding{
public static void main(String[] args){
A a = new A();
a.set(new A());
a = a.set(new A()).get();
a = a.get();
C c = new C();
C = c.setAndGet(new C());
}
}

泛型的自绑定约束目的是用于强制继承关系,即使用泛型参数的类的基类是相同的,强制所有人使用相同的方式使用参数基类。


本章主题

泛型大家都用过,也尝过甜头。但是是否思考过为什么要有泛型,以及在什么情况下需要使用泛型呢?不说开源项目了,就 Java 本身的代码(java.lang下面的),就大量使用了泛型。学习完本章,希望自己能对泛型有一个清晰的认识,能明白为何使用泛型、怎样正确的使用泛型。
0. 泛型简介 & 与 C++比较
首先来说,泛型实现了 参数化类型 的概念,使代码可以应用于多种类型。泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由 编译器 来保证类型的正确性。泛型在多个编程语言均有涉及,它最初的设计目的是希望类与方法能够具备最广泛的表达能力。那么,如何做到这点呢?正是通过解耦类或方法与所使用的类型之间的约束。
当然,作者也明确指出,和其他语言(比如 C++)对比,使用 Java 泛型机制无法做到的事情,其他语言中的参数化类型机制却能够做到。即使 Java 能做到的,其他语言也能用更优雅的方式实现。那么,学习完 Java 的泛型后,我们可以尝试着去和其他语言对比,看看 Java 在泛型上的不足,以后遇到不必须用 Java 实现的需求,就可以更高效的达到目的。
因为 Java 的设计者曾经说过:设计 Java 的灵感主要来自 C++。所以我们就拿 Java 和 C++做一下对比吧:
首先,了解 C++模板的某些方面,有助于理解泛型的基础。 最终的目的是帮助我们理解 Java 泛型的边界在哪里。理解了边界所在才能成为程序高手,因为只有知道了某个技术不能做什么,才能更好的做到所能做的
第二点,在 Java 社区中,人们普遍对 C++模板有一种误解(what?),会在理解泛型的意图时产生偏差
1. 简单泛型
这个就是最最基本的泛型使用,完全没有难度。所以我就放一个 demo 例子吧:
1 package Chapter15;
2
3 /**
4 * 节点元素 + 下一个节点
5 *
6 * @author niushuai
7 *
8 * @param
9 */
10 class Node {
11 T item;
12 Node next;
13
14 Node() {
15 item = null;
16 next = null;
17 }
18
19 Node(T item, Node next) {
20 this.item = item;
21 this.next = next;
22 }
23
24 boolean end() {
25 return item == null && next == null;
26 }
27 }
28
29 /**
30 * 既然是栈,主要就是入栈和出栈了
31 *
32 * @author niushuai
33 *
34 * @param
35 */
36 public class _01_LinkedStack {
37 // 栈底哨兵
38 private Node top = new Node();
39
40 public void push(T item) {
41 top = new Node(item, top);
42 }
43
44 public T pop() {
45 T result = top.item;
46 if (!top.end()) {
47 top = top.next;
48 }
49
50 return result;
51 }
52
53 public static void main(String[] args) {
54 _01_LinkedStack lss = new _01_LinkedStack();
55 for (String s : "Phasers on stun!".split(" ")) {
56 lss.push(s);
57 }
58
59 String s;
60 while ((s = lss.pop()) != null) {
61 System.out.println(s);
62 }
63 }
64 }
2. 泛型接口
这个也算是简单泛型了,就是对接口使用泛型。我也写了个 demo:
1 import java.util.Iterator;
2
3 public class _05_IterableFibonacci extends _04_Fibonacci implements Iterable {
4 private int n;
5
6 // 要遍历count 次
7 public _05_IterableFibonacci(int count) {
8 n = count;
9 }
10
11 @Override
12 public Iterator iterator() {
13
14 return new Iterator() {
15 @Override
16 public boolean hasNext() {
17 return n > 0;
18 }
19
20 @Override
21 public Integer next() {
22 n--;
23 return _05_IterableFibonacci.this.next();
24 }
25 };
26 }
27
28 public static void main(String[] args) {
29 for (int i : new _05_IterableFibonacci(18)) {
30 System.out.println(i + " ");
31 }
32 }
33 }
3. 泛型方法
首先需要知道的是,可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。泛型方法使得该方法能够独立于类而产生变化。一个基本原则是:
无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。另外,对于一个 static 的方法而言,无法访问泛型类的类型参数,所以,如果 static 方法需要使用泛型能力,就必须使其成为泛型方法。
仔细一想,泛型方法其实用的还是蛮多的。比如经常使用 Guava 的工具类:
1 // 以前初始化
2 Map> map = new HashMap>();
3 // 使用 Guava 后
4 Map> map = Maps.newHashMap();
其实我们去看一下Guava 的代码就知道怎么做的了:
1 public static HashMap newHashMap() {
2 return new HashMap();
3 }
这里需要注意一点: 类型推断只对赋值操作有效,其他时候并不起作用。 如果你将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推导。因为编译器认为:调用泛型方法后,其返回值被赋给一个 Object 类型的变量。这时候解决方法是使用显式的类型说明:
在泛型方法中,可以显式指明类型,不过这种语法很少用到(只有在编写非赋值语句时,才需要显式指明类型)。要显式指明类型,必须在点操作符和方法名之间插入泛型类型。如果是在定义该方法的类的内部,必须在点操作符之前使用 this 关键字,如果是使用 static 方法,必须在点操作符之前加上类名。
写了一个 demo 试验:
1 import java.util.HashMap;
2 import java.util.Map;
3
4 public class _08_ExplicitTypeSpecification {
5
6 // 这里是静态方法的 new
7 static class StaticNew {
8 public static Map map() {
9 return new HashMap();
10 }
11 }
12
13 // 普通实例化的 new
14 class InstanceNew {
15 public Map map() {
16 return new HashMap();
17 }
18 }
19
20 // 类内部的 new
21 public Map map() {
22 return new HashMap();
23 }
24
25 static void test1(Map map) {
26
27 }
28
29 public void main() {
30 // compile error, StaticNew.map() return Map, but test1 requried Map
31 test1(StaticNew.map());
32 // compile ok. 等价于Map map = StaticNew.map();
33 test1(StaticNew. map());
34
35 // compile error, InstanceNew.map() return Map, but test1 requried Map
36 test1(new InstanceNew().map());
37 // compile ok. 等价于Map map2 = new InstanceNew().map();
38 test1(new InstanceNew(). map());
39
40 // compile error, this.map() return Map, but test1 requried Map
41 test1(this.map());
42 // compile ok. 等价于Map map = this.map();
43 test1(this. map());
44 }
45 }
4. 擦除的神秘之处
当开始深入研究泛型时,会发现泛型中有大量的东西初看起来是没有意义的。其中擦除减少了泛型的泛化性, 泛型之所以不是那么“好用”,原因就是擦除。但是,擦除作为泛型实现中的一种折中,所以必须要有所取舍。这种折中会使我们使用泛型的时候很不爽,但是我们能做的就是习惯并了解它为什么是这样。当你在不限制使用 Java 的时候,你就可以用其他语言更优雅的实现你的需求:)
例如,尽管可以声明 ArrayList.class,但是不能声明 ArrayList .claxs,比如下面的例子:
1 import java.util.ArrayList;
2
3 public class _09_ErasedTypeEquivalence {
4 public static void main(String[] args) {
5 Class c1 = new ArrayList().getClass();
6 Class c2 = new ArrayList().getClass();
7
8 System.out.println(c1 == c2);
9
10 // true
11 }
12 }
什么,Integer 和 String 的 List 竟然是相同的??反正刚开始我觉得肯定是不同的啊,因为 c2.add(new Integer(3)) 肯定会报错啊。别急,更崩溃的例子在下面:
1 import java.util.ArrayList;
2 import java.util.Arrays;
3 import java.util.HashMap;
4 import java.util.List;
5 import java.util.Map;
6
7 class Test1 {
8
9 }
10
11 class Test2 {
12
13 }
14
15 class Test3 {
16
17 }
18
19 class Test4 {
20
21 }
22
23 public class _10_LostInformation {
24 public static void main(String[] args) {
25 List list = new ArrayList();
26 Map map = new HashMap();
27 Test3 tt = new Test3();
28 Test4 t4 = new Test4();
29
30 System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
31 System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
32 System.out.println(Arrays.toString(tt.getClass().getTypeParameters()));
33 System.out.println(Arrays.toString(t4.getClass().getTypeParameters()));
34 }
35 }/*output:
36 [E]
37 [K, V]
38 [Q]
39 [POSITION, MOMENT]
40 */
看到输出了没,其中的 E/K/V/Q/POSITION/MOMENT 都是一个类型参数占位符而已。虽然 Class.getTypeParameters() 的文档说“返回一个 TypeVariable 对象数组,表示有泛型声明所声明的类型参数……“,好像暗示我们能获得参数类型的信息。但是,正如你从输出中看到的,你能够发现的只是用作参数占位符的标识符,没有其他有用的信息。所以,事实是: 在泛型代码内部,无法获得任何有关泛型参数类型的信息。
我们可以显式看到类型参数标识符和泛型类型边界(上界下界)信息,但是编译器看不见,所以也就不能创建某个特定实例的实际的类型参数。 Java 泛型是使用擦除来实现的 ,这意味着当你在使用泛型时,任何具体的信息都被擦除了,你唯一知道的就是你在使用一个对象。因此 List 和 List 在运行时事实上都是相同的类型 List 。理解擦除以及应该如何处理它,是学习 Java 泛型面临的最大障碍。
首先我们来看看 C++的方式,毕竟 Java 是受 C++的启发:
1 #include
2 using namespace std;
3
4 template class Manipulator {
5 T obj;
6 public:
7 Manipulator(T x) {
8 obj = x;
9 }
10 void manipulate() {
11 obj.f();
12 }
13 };
14
15 class HasF {
16 public:
17 void f() {
18 cout<<"HasF:f()"<19 }
20 };
21
22 int main(void) {
23 HasF hf;
24 Manipulator manipulator(hf);
25 manipulator.manipulate();
26 }/*output:
27 HasF:f()
28 */
然后我们把这段代码翻译成 Java:
1 /**
2 * 因为擦除效应,Java 编译器无法将manipulate()在 obj 上调用f()这个需求映射到 HasF 有f()这一事实上
3 *
4 * 解决办法是协助泛型类,给定泛型类的边界,这样编译器才不会完全不知所措。
5 *
6 * @author niushuai
7 *
8 * @param
9 */
10
11 class HasF {
12 public void f() {
13 System.out.println("HasF:f()");
14 }
15 }
16
17 class Manipulator {
18 private T obj;
19
20 public Manipulator(T x) {
21 obj = x;
22 }
23
24 public void manipulate() {
25 // 找到 f()这个方法
26 // obj.f();
27 }
28 }
29
30 // 指定了擦除边界,编译器最后知道的就是,这个没有泛型,就是 HasF
31 class ManipulatorNew {
32 private T obj;
33
34 public ManipulatorNew(T x) {
35 obj = x;
36 }
37
38 public void manipulate() {
39 obj.f();
40 }
41 }
42
43 public class _11_Manipulation {
44 public static void main(String[] args) {
45 HasF hf = new HasF();
46 Manipulator manipulator = new Manipulator(hf);
47 manipulator.manipulate();
48 }
49 }
很悲伤的发现,Manipulator 是无法通过编译的,因为编译器不知道 f()是什么鬼,它不会在编译期知道实例化参数中只要有 f()就可以了。而第二个版本因为指定了边界,编译器傻乎乎的将 T extends HasF替换为 HasF,而 HasF 本来就有 f(),所以才能调用 obj.f() 。但是这样的话,泛型没有贡献任何好处,我们自己就可以手工执行擦除,创建出没有泛型的类:
1 class Manipulator3 {
2 private HasF obj;
3 public Manipulator3(HasF x) {
4 obj = x;
5 }
6 publc void manipulate() {
7 obj.f();
8 }
9 }
这么一看,程序因为少了泛型反而简单了不少。那么,这里又扯出另一个问题:什么时候使用泛型更加合适呢?
只有当你希望使用的类型参数比某个具体类型(以及它的子类型)更加泛化时——也就是说,当你希望代码能够跨越多个类工作时,使用泛型才有所帮助。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换要更加复杂。但是,不能因此否认 一定是不合理的。比如当返回值是 T 的时候,泛型就非常方便:
1 class ReturnGenericType {
2 private T obj;
3 public ReturnGenericType(T x) {
4 obj = x;
5 }
6 public T get() {
7 return obj;
8 }
9 }
这时候你要是想手工擦除,就得每个具体类型都写一遍,这样就得不偿失了。
5. 擦除由来 & 问题
没想到 Java 设计擦除的原因是保持兼容性。官方说法是:
假设某个应用程序具有两个类库 X/Y,并且 Y 还要使用类库 Z。随着 Java SE5的出现,这个应用程序和这些类库的创建者最终可能希望迁移到泛型上。但是,迁移是个大工程,不能为了迁移而迁移。所以,为了实现迁移兼容性, 每个类库和应用程序都必须与其他所有的部分是否使用了泛型无关 。这样,它们不能拥有探测其他类库是否使用了泛型的能信。因此,某个特定的类库使用了泛型这样的证据必须被“擦除”。试想,如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到 Java 泛型的开发者们说再见了。正因为类库对于编程语言极其重要,所以这不是一种可以接受的代价。 擦除是否是最佳的或者唯一的迁移途径,还需要时间来检验。
请记住: 无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。 ,比如下面的代码:
1 class Foo {
2 T var;
3 }
4
5 Foo f = new Foo();
//那么,看起来当你创建 Foo 的实例时,class Foo 中的代码应该知道现在工作于 Cat 之上,而泛型语法也仿佛强烈暗示:在整个类中的各个地方,类型 T 都在被替换。但是事实并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它仅仅是一个 Object。”
6. 边界处的动作
这是什么意思呢?核心一句话:
边界就是发生动作的地方:对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。
光说不好理解,我们从例子出来来讲解:
1 import java.util.ArrayList;
2 import java.util.List;
3
4 /**
5 * create 竟然没有任何警告,虽然我们知道 {@code new ArrayList}的{@code }被擦除了
6 *


7 *
8 * 在 运行时,这个类的内部没有任何{@code },但是也不能变为{@code new ArrayList()},编译器会用的啊!!!

9 * 编译器在编译期确保放置到 result 的对象具有 T 类型,所以即使擦除在方法或类内部有关实际类型的信息,

10 * 编译器在编译期也能确保在方法或类中使用的类型的内部一致性。
11 *


12 *
13 * 那么,在运行时没有了类型信息,就需要确定边界:即对象进入和离开方法的地点。这些正是编译器在编译期提前做好的

14 * 编译器会在编译期执行类型检查并插入转型代码的地点。
15 */
16 public class _14_FilledListMaker {
17 List create(T t, int n) {
18 List result = new ArrayList(n);
19 for (int i = 0; i < n; i++) {
20 result.add(t);
21 }
22 return result;
23 }
24
25 public static void main(String[] args) {
26 _14_FilledListMaker stringMaker = new _14_FilledListMaker();
27 List stringList = stringMaker.create("Hello", 9);
28
29 System.out.println(stringList);
30 }
31 }
下面我们就通过编译代码来看看,编译器在编译期是怎么处理类型参数的(不用细看,下面接着看将_12_SimpleHolder泛型化后的结果):
1 public class _12_SimpleHolder {
2 private Object obj;
3
4 public void set(Object obj) {
5 this.obj = obj;
6 }
7
8 public Object get() {
9 return obj;
10 }
11
12 public static void main(String[] args) {
13 _12_SimpleHolder holder = new _12_SimpleHolder();
14 holder.set("item");
15 String s = (String) holder.get();
16 }
17 }
18
19 // 使用 javap -c _12_SimpleHolder.class 得到反编译后的代码:
20 public class Chapter15._12_SimpleHolder {
21 public Chapter15._12_SimpleHolder();
22 Code:
23 0: aload_0
24 1: invokespecial #10 // Method java/lang/Object."":()V
25 4: return
26
27 public void set(java.lang.Object);
28 Code:
29 0: aload_0
30 1: aload_1
31 2: putfield #18 // Field obj:Ljava/lang/Object;
32 5: return
33
34 public java.lang.Object get();
35 Code:
36 0: aload_0
37 1: getfield #18 // Field obj:Ljava/lang/Object;
38 4: areturn
39
40 public static void main(java.lang.String[]);
41 Code:
42 0: new #1 // class Chapter15/_12_SimpleHolder
43 3: dup
44 4: invokespecial #24 // Method "":()V
45 7: astore_1
46 8: aload_1
47 9: ldc #25 // String item
48 11: invokevirtual #27 // Method set:(Ljava/lang/Object;)V
49 14: aload_1
50 15: invokevirtual #29 // Method get:()Ljava/lang/Object;
51 18: checkcast #31 // class java/lang/String
52 21: astore_2
53 22: return
54 }
下面是泛型化后的_12_SimpleHolder:
1 public class _15_GenericHolder {
2 private T obj;
3
4 public void set(T obj) {
5 this.obj = obj;
6 }
7
8 public T get() {
9 return obj;
10 }
11
12 public static void main(String[] args) {
13 _15_GenericHolder holder = new _15_GenericHolder();
14 holder.set("Item");
15 // 这里没有转型了,但是我们知道传递给 set()的值在编译期还是会接受检查
16 String s = holder.get();
17 }
18 }
19
20 // 反编译:
21 public class Chapter15._15_GenericHolder {
22 public Chapter15._15_GenericHolder();
23 Code:
24 0: aload_0
25 1: invokespecial #12 // Method java/lang/Object."":()V
26 4: return
27
28 public void set(T);
29 Code:
30 0: aload_0
31 1: aload_1
32 2: putfield #23 // Field obj:Ljava/lang/Object;
33 5: return
34
35 public T get();
36 Code:
37 0: aload_0
38 1: getfield #23 // Field obj:Ljava/lang/Object;
39 4: areturn
40
41 public static void main(java.lang.String[]);
42 Code:
43 0: new #1 // class Chapter15/_15_GenericHolder
44 3: dup
45 4: invokespecial #30 // Method "":()V
46 7: astore_1
47 8: aload_1
48 9: ldc #31 // String Item
49 11: invokevirtual #33 // Method set:(Ljava/lang/Object;)V
50 14: aload_1
51 15: invokevirtual #35 // Method get:()Ljava/lang/Object;
52 18: checkcast #37 // class java/lang/String
53 21: astore_2
54 22: return
55 }
有没有泛型,产生的字节码竟然是 相同的 。对进入 set()的类型进行检查是不需要的,因为这将由编译器执行。而对从 get()返回的值进行转型仍旧是需要的,但这将由编译器来自动完成。 由于所产生的 get()和 set()字节码相同,所以在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这有助于澄清对擦除的混淆,“边界就是发生动作的地方。”

1. 泛型的主要目的之一,就是用来指定容器持有什么类型的对象,而且由编译器来保证其类型的正确性。由此,与其用Object指定为任何类型,不如暂时不指定类型,而到定义时决定类型,因为前者不会有类型检查。
2. 擦除。List型的对象与List型的对象,在运行时,其类型都被擦除为List。因此,在泛型机制下,无法获得有关泛型参数的运行时信息,像List成为List,普通类型T则成为Object类型。这相比c++的模板机制,有很大的不足之处,弥补方法之一是使用泛型边界如
3. Java泛型没有其它语言的泛型那么有用,原因之一就是使用了擦除。擦除的核心动机是使得泛型化与非泛型化的代码之间能互相调用,是为了兼容。Java的设计者认为这是唯一可靠行的解决方案。如果Jdk1.0就引入了泛型,则不会有这个问题。
4. 擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法完成。而编译器会负责泛型代码之外的类型检查(即边界处),以确保插入容器的对象是正确的类型。
5. 边界可以有多个,如其中只能有一个是类,其它为接口。且第一个为类,后面的为接口。
6. Java的数组不允许放入子类型的对象,而泛型可以。
7. 泛型的一些限制:(1)不能用基本类型作为泛型参数;(2)一个类不能实现一个泛型接口的两种变体,由于擦除泛型无法识别;(3)无法在泛型代码内重载相同个数泛型参数的函数,因为各参数在擦除后没有区别。
8. 因为可向JDK1.5以前的代码传递泛型容器,所以旧式代码仍可能破坏你的容器。JDK1.5的java.util.Collections中有一组有用的工具,它们是静态方法checkedCollection,checkedList,checkMap等等,可以解决此类问题。使旧式代码中插入对象到容器时,也接受类型检查。
9. 使用数组的优势在于效率,如果需要变长度,或者需要一些除存取之外的特别操作,显然应该用容器。
10. Java中返回一个数组比c++中方便,c++中只能返回一个指针。
11. System.arrayCopy比用for循环要快得多。
12. 返回零长度的数组,而不是null是个好习惯。客户省去对null的判断。

一、类型绑定
1、引入
我们重新看上篇写的一个泛型:
class Point {
private T x; // 表示X坐标
private T y; // 表示Y坐标

public void setX(T x) {
this.x = x;
}

public void setY(T y) {
this.y = y;
}

public T getX() {
return this.x;
}

public T getY() {
return this.y;
}
}

//使用
Point p1 = new Point();
p1.setX(new Integer(100));
System.out.println(p1.getX());
首先,我们要知道一点,任何的泛型变量(比如这里的T)都是派生自Object,所以我们在填充泛型变量时,只能使用派生自Object的类,比如String,Integer,Double,等而不能使用原始的变量类型,比如int,double,float等。
然后,问题来了,那在泛型类Point内部,利用泛型定义的变量T x能调用哪些函数呢?
private T x;
当然只能调用Object所具有的函数,因为编译器根本不知道T具体是什么类型,只有在运行时,用户给什么类型,他才知道是什么类型。编译器唯一能确定的是,无论什么类型,都是派生自Object的,所以T肯定是Object的子类,所以T是可以调用Object的方法的。
那么问题又来了,如果我想写一个找到最小值的泛型类;由于不知道用户会传什么类型,所以要写一个接口,让用户实现这个接口来自已对比他所传递的类型的大小。
接口如下:
public interface Comparable{
public boolean compareTo(T i);
}
但如果我们直接利用T的实例来调用compareTo()函数的话,会报错,编译器截图如下:

这是因为,编译器根本无法得知T是继承自Comparable接口的函数。那怎么样才能让编译器知道,T是继承了Comparable接口的类型呢?
这就是类型绑定的作用了。
2、类型绑定:extends
(1)、定义
有时候,你会希望泛型类型只能是某一部分类型,比如操作数据的时候,你会希望是Number或其子类类型。这个想法其实就是给泛型参数添加一个界限。其定义形式为:

此定义表示T应该是BoundingType的子类型(subtype)。T和BoundingType可以是类,也可以是接口。另外注意的是,此处的”extends“表示的子类型,不等同于继承。
一定要非常注意的是,这里的extends不是类继承里的那个extends!两个根本没有任何关联。在这里extends后的BoundingType可以是类,也可以是接口,意思是说,T是在BoundingType基础上创建的,具有BoundingType的功能。目测是JAVA的开发人员不想再引入一个关键字,所以用已有的extends来代替而已。
(2)、实例:绑定接口
同样,我们还使用上面对比大小的接口来做例子
首先,看加上extends限定后的min函数:
public interface Comparable {
public boolean compareTo(T i);
}
//添加上extends Comparable之后,就可以Comparable里的函数了
public static T min(T...a){
T smallest = a[0];
for(T item:a){
if (smallest.compareTo(item)){
smallest = item;
}
}
return smallest;
}
这段代码的意思就是根据传进去的T类型数组a,然后调用其中item的compareTo()函数,跟每一项做对比,最终找到最小值。
从这段代码也可以看出,类型绑定有两个作用:1、对填充的泛型加以限定 2、使用泛型变量T时,可以使用BoundingType内部的函数。
这里有一点非常要注意的是,在这句中smallest.compareTo(item),smallest和item全部都是T类型的,也就是说,compareTo对比的是同一种类型。
然后我们实现一个派生自Comparable接口的类:
public class StringCompare implements Comparable {
private String mStr;

public StringCompare(String string){
this.mStr = string;
}

@Override
public boolean compareTo(StringCompare str) {
if (mStr.length() > str.mStr.length()){
return true;
}
return false;
}
}
在这段代码,大家可能会疑惑为什么把T也填充为StringCompare类型,记得我们上面说的吗:smallest.compareTo(item),smallest和item是同一类型!!所以compareTo的参数必须是与调用者自身是同一类型,所以要把T填充为StringCompare;
在这段代码中compareTo的实现为,对比当前mstr的长度与传进来实例的mstr长度进行比较,如果超过,则返回true,否则返回false;
最后是使用min函数:
StringCompare result = min(new StringCompare("123"),new StringCompare("234"),new StringCompare("59897"));
Log.d(TAG,"min:"+result.mStr);
结果如下:

这里有extends接口,我们开篇说过,extends表示绑定,后面的BindingType即可以是接口,也可以是类,下面我们就再举个绑定类的例子。
源码在文章底部给出
(3)、实例:绑定类
我们假设,我们有很多种类的水果,需要写一个函数,打印出填充进去水果的名字:
为此,我们先建一个基类来设置和提取名字:
class Fruit {
private String name;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后写个泛型函数来提取名字:
public static String getFruitName(T t){
return t.getName();
}
这里泛型函数的用法就出来了,由于我们已知水果都会继承Fruit基类,所以我们利用就可以限定填充的变量必须派生自Fruit的子类。一来,在T中,我们就可以利用Fruit类中方法和函数;二来,如果用户填充进去的类没有派生自Fruit,那编译器就会报错。
然后,我们新建两个类,派生自Fruit,并填充进去它们自己的名字:
class Banana extends Fruit{
public Banana(){
setName("bababa");
}
}
class Apple extends Fruit{
public Apple(){
setName("apple");
}
}
最后调用:
String name_1 = getFruitName(new Banana());
String name_2 = getFruitName(new Apple());
Log.d(TAG,name_1);
Log.d(TAG,name_2);
结果如下:

源码在文章底部给出
(4)、绑定多个限定
上面我们讲了,有关绑定限定的用法,其实我们可以同时绑定多个绑定,用&连接,比如:
public static String getFruitName(T t){
return t.getName();
}
再加深下难度,如果我们有多个泛型,每个泛型都带绑定,那应该是什么样子的呢:
public static T foo(T a, U b){
…………
}
大家应该看得懂,稍微讲一下:这里有两个泛型变量T和U,将T与Comparable & Serializable绑定,将U与Runnable绑定。
好了,这部分就讲完了,下面讲讲有关通配符的用法。
二、通配符
通配符是一个非常令人头疼的一个功能,理解与掌握难度比较大,下面我尽力去讲明白它与泛型变量的区别与用法。
1、引入
重新来看我们上篇用的Point泛型定义:
class Point {
private T x;
private T y;

public Point(){

}
public Point(T x,T y){
this.x = x;
this.y = y;
}

public void setX(T x) {
this.x = x;
}

public void setY(T y) {
this.y = y;
}

public T getX() {
return this.x;
}

public T getY() {
return this.y;
}
}
这段代码很简单,引入了一个泛型变量T,然后是有两个构造函数,最后分别是利用set和get方法来设置和获取x,y的值。这段代码没什么难度,不再细讲。
我们看看下面这段使用的代码:
Point integerPoint = new Point(3,3);
…………
Point floatPoint = new Point(4.3f,4.3f);
…………
Point doublePoint = new Point(4.3d,4.90d);
…………
Point longPoint = new Point(12l,23l);
…………
在这段代码中,我们使用Point生成了四个实例:integerPoint,floatPoint,doublePoint和longPoint;
在这里,我们生成四个实例,就得想四个名字。如果我们想生成十个不同类型的实例呢?那不得想十个名字。
光想名字就是个事,(其实我并不觉得想名字是个什么大事…… T _ T ,没办法,想不出更好的例子了…… )
那有没有一种办法,生成一个变量,可以将不同类型的实例赋值给他呢?
2、无边界通配符:?
(1)、概述
先不讲无边界通配符是什么,同样拿上面的例子来看,如果我们这样实现:
Point point;

point = new Point(3,3);
point = new Point(4.3f,4.3f);
point = new Point(4.3d,4.90d);
point = new Point(12l,23l);
在这里,我们首先,利用下面的代码生成一个point实例,注意到,在填充泛型时,用的是?
Point point;
然后,各种类型的Point实例,都可以赋值给point了:
point = new Point(3,3);
point = new Point(4.3f,4.3f);
point = new Point(4.3d,4.90d);
point = new Point(12l,23l);
这里的?就是无边界通配符。通配符的意义就是它是一个未知的符号,可以是代表任意的类。
所以这里可能大家就明白了,这里不光能将泛型变量T填充为数值类型,其实任意Point实例都是可以传给point的:比如这里的Point(),Point()都是可以的

(2)、?与T的区别
大家可能会有疑问,那无边界通配符?与泛型变量T有什么区别呢?
答案是:他们俩没有任何联系!!!!!
泛型变量T不能在代码用于创建变量,只能在类,接口,函数中声明以后,才能使用。
比如:
public class Box {
public T get(){
…………
};
public void put(T element){
…………
};
}
而无边界通配符?则只能用于填充泛型变量T,表示通配任何类型!!!!再重复一遍:?只能用于填充泛型变量T。它是用来填充T的!!!!只是填充方式的一种!!!
比如:
//无边界通配符填充
Box box;
//其它类型填充
Box stringBox;
(3)、通配符只能用于填充泛型变量T,不能用于定义变量
大家一定要记得,通配符的使用位置只有:
Box box;
box = new Box();
即填充泛型变量T的位置,不能出现在后面String的位置!!!!
下面的第三行,第四行,都是错误的。通配符不能用于定义变量。

再次强调,?只能出现在Box box;中,其它位置都是不对的。
3、通配符?的extends绑定
(1)、概述
从上面我们可以知道通配符?可以代表任意类型,但跟泛型一样,如果不加以限定,在后期的使用中编译器可能不会报错。所以我们同样,要对?加以限定。
绑定的形式,同样是通过extends关键字,意义和使用方法都用泛型变量一致。
同样,以我们上面的Point泛型类为例,因为Point在实例意义中,其中的值是数值才有意义,所以将泛型变量T填充为Object类型、String类型等都是不正确的。
所以我们要对Point point加以限定:只有数值类型才能赋值给point;
我们把代码改成下面的方式:

我们给通配符加上限定: Point point;
此时,最后两行,当将T填充为String和Object时,赋值给point就会报错!
这里虽然是指派生自Number的任意类型,但大家注意到了没: new Point();也是可以成功赋值的,这说明包括边界自身。
再重复一遍:无边界通配符只是泛型T的填充方式,给他加上限定,只是限定了赋值给它(比如这里的point)的实例类型。
如果想从根本上解决乱填充Point的问题,需要从Point泛型类定义时加上:
class Point {
private T x; // 表示X坐标
private T y; // 表示Y坐标

…………
}
(2)注意:利用定义的变量,只可取其中的值,不可修改
看下面的代码:

明显在point.setX(Integer(122));时报编译错误。但point.getX()却不报错。
这是为什么呢?
首先,point的类型是由Point决定的,并不会因为point = new Point(3,3);而改变类型。
即便point = new Point(3,3);之后,point的类型依然是Point,即派生自Number类的未知类型!!!这一点很好理解,如果在point = new Point(3,3);之后,point就变成了Point类型,那后面point = new Point(12l,23l);操作时,肯定会因为类型不匹配而报编译错误了,正因为,point的类型始终是Point,因此能继续被各种类型实例赋值。
回到正题,现在说说为什么不能赋值
正因为point的类型为 Point point,那也就是说,填充Point的泛型变量T的为,这是一个什么类型?未知类型!!!怎么可能能用一个未知类型来设置内部值!这完全是不合理的。
但取值时,正由于泛型变量T被填充为,所以编译器能确定的是T肯定是Number的子类,编译器就会用Number来填充T
也就是说,编译器,只要能确定通配符类型,就会允许,如果无法确定通配符的类型,就会报错。
4、通配符?的super绑定
(1)、概述
如果说 指填充为派生于XXX的任意子类的话,那么则表示填充为任意XXX的父类!
我们先写三个类,Employee,Manager,CEO,分别代表工人,管理者,CEO
其中Manager派生于Employee,CEO派生于Manager,代码如下:
class CEO extends Manager {
}

class Manager extends Employee {
}

class Employee {
}
然后,如果我这样生成一个变量:
List list;
它表示的意思是将泛型T填充为,即任意Manager的父类;也就是说任意将List中的泛型变量T填充为Manager父类的List变量,都可以赋值给list;

从上面的代码中可以看出new ArrayList(),new ArrayList()都是正确的,而new ArrayList()却报错,当然是因为CEO类已经不再是Manager的父类了。所以会报编译错误。
这里还要注意一个地方,从代码中可以看出new ArrayList()是可以成功赋值给 List list的,可见,super关键字也是包括边界的。即边界类型(这里是Manager)组装的实例依然可以成功赋值。
(2)、super通配符实例内容:能存不能取
上面我们讲了,extends通配符,能取不能存,那super通配符情况又怎样呢?我们试试看:

先看存的部分:
List list;
list = new ArrayList();
//存
list.add(new Employee()); //编译错误
list.add(new Manager());
list.add(new CEO());
首先,需要声明的是,与Point point中point的类型是由Point确定的,相同的是list的类型是也是由List ;list的item的类型始终是,即Manager类的任意父类,即可能是Employee或者Object.
大家可能疑惑的地方在于,为什么下面这两个是正确的!而list.add(new Employee()); 却是错误的!
list.add(new Manager());
list.add(new CEO());
因为list里item的类型是,即Manager的任意父类,我们假如是Employee,那下面这段代码大家能理解了吧:
List list = new ArrayList();
list.add(new Manager());
list.add(new CEO());
在这里,正因为Manager和CEO都是Employee的子类,在传进去list.add()后,会被强制转换为Employee!
现在回过头来看这个:
List list;
list = new ArrayList();
//存
list.add(new Employee()); //编译错误
list.add(new Manager());
list.add(new CEO());
编译器无法确定的具体类型,但唯一可以确定的是Manager()、CEO()肯定是的子类,所以肯定是可以add进去的。但Employee不一定是的子类,所以不能确定,不能确定的,肯定是不允许的,所以会报编译错误。
最后再来看看取:

在这段代码中,Object object = list.get(0);是不报错的,而Employee employee = list.get(0);是报错的;
我们知道list中item的类型为,那编译器能肯定的是肯定是Manger的父类;但不能确定,它是Object还是Employee类型。但无论是填充为Object还是Employee,它必然是Object的子类!
所以Object object = list.get(0);是不报错的。因为 list.get(0);肯定是Object的子类;
而编译器无法判断list.get(0)是不是Employee类型的,所以Employee employee = list.get(0);是报错的。
这里虽然看起来是能取的,但取出来一个Object类型,是毫无意义的。所以我们认为super通配符:能存不能取;
5、通配符?总结
总结 ? extends 和 the ? super 通配符的特征,我们可以得出以下结论:
◆ 如果你想从一个数据类型里获取数据,使用 ? extends 通配符(能取不能存)
◆ 如果你想把对象写入一个数据结构里,使用 ? super 通配符(能存不能取)
◆ 如果你既想存,又想取,那就别用通配符。
6、常见问题注意
(1)、Point与Point构造泛型实例的区别
同样以Point泛型类为例:
class Point {
private T x; // 表示X坐标
private T y; // 表示Y坐标

public Point(){

}
public Point(T x,T y){
this.x = x;
this.y = y;
}

public void setX(T x) {
this.x = x;
}

public void setY(T y) {
this.y = y;
}

public T getX() {
return this.x;
}

public T getY() {
return this.y;
}
}
我们来看看下面这种构造Point泛型实例有什么区别:
//使用Point
Point point1 = new Point(new Integer(23),new Integer(23));
Point point2 = new Point(new String(""),new String(""));
//直接使用Point
Point point3 = new Point(new Integer(23),new Integer(23));
Point point4 = new Point(new String(""),new String(""));
上面的四行代码中,point1,point2生成的是Point的实例,填充的是无边界通配符。而point3和point4则非常奇怪,没有了泛型的<>标识,直接使用Point生成的实例,那它填充的是什么呢?
这四行代码在编译和运行时,都没有报错,而且输出结果也一样!
那么问题就来了:
Point point1 = new Point(new Integer(23),new Integer(23));
Point point2 = new Point(new String(""),new String(""));
在上面的代码中,使用了无界通配符,所以能够将各种Point实例赋值给Point point1
而省略了泛型标识的构造方法,依然能将各种Point实例赋值给它:
Point point3 = new Point(new Integer(23),new Integer(23));
Point point4 = new Point(new String(""),new String(""));
这说明:构造泛型实例时,如果省略了填充类型,则默认填充为无边界通配符!
所以下面这两个是对等的:
Point point3 = new Point(new Integer(23),new Integer(23));
Point point3 = new Point(new Integer(23),new Integer(23));
最后重复一遍:构造泛型实例时,如果省略了填充类型,则默认填充为无边界通配符!

泛型(高级)

泛型是提供给javac编译器使用的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,编译器编译带类型说明的集合时会去除掉“类型”信息,使程序运行效率不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样。由于编译生成的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其它类型的数据,例如,用反射得到集合,再调用其add方法即可。
去类型化
            ArrayList arr1 = new ArrayList();
            ArrayList arr2 = new ArrayList();
            System. out.println(arr1.getClass() == arr2.getClass());// true
用反射越过泛型限制
            arr2.getClass().getMethod( "add", Object.class).invoke(arr2, "wanqi");
            System. out.println(arr2);

泛型术语
整个称为ArrayList泛型类型
ArrayList中的E称为类型变量或类型参数
整个ArrayList称为参数化的类型
ArrayList中的Integer称为类型参数的实例或实际类型参数
ArrayList中的<>念着typeof
ArrayList称为原始类型

参数化类型与原始类型的兼容性
参数化类型可以引用一个原始类型的对象,编译报告警告,例如,Collection c = new Vector();//可不可以,不就是编译器一句话的事吗?
原始类型可以引用一个参数化类型的对象,编译报告警告,例如,Collection c = new Vector();//原来的方法接受一个集合参数,新的类型也要能传进去

参数化类型不考虑类型参数的继承关系:
Vector v = new Vector(); //错误!///不写没错,写了就是明知故犯
Vector v = new Vector(); //也错误!

可以使用通配符显示继承关系
限定通配符的上边界:限定通配符总是包括自己。
正确:Vector x = new Vector();
限定通配符的下边界:
正确:Vector x = new Vector();


编译器不允许创建泛型变量的数组。
即在创建数组实例时,数组的元素不能使用参数化的类型,
例如,下面语句有错误:
     Vector vectorList[] = new Vector[10];

泛型中的?通配符
使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量主要用作引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。
?表示的是任意的同一类型参数
Class y = Class.forName("java.lang.String" );
Class x = Class.forName("java.lang.String");//错误
Class x = Class y; //错误

限定通配符的上边界:
正确:Vector x = new Vector();
错误:Vector x = new Vector();
限定通配符的下边界:
正确:Vector x = new Vector();
错误:Vector x = new Vector();

泛型集合类的综合

            HashMap hm = new HashMap();
            hm.put( "wanqi", 18);
            hm.put( "zhanwen", 28);
            Set> set = hm.entrySet();
             for (Map.Entry entry : set) {
                  entry.getKey();
                  entry. getValue();
            }


自定义泛型

定义泛型方法
是否拥有泛型方法,与其所在的类是否泛型没有关系。要定义泛型方法,只需将泛型参数列表置于返回值前。
如:public static  void demo(T a,T b){}

public static void main(String[] args) throws Exception {
             f(1);
             f('a');
             f(1.1);
             f("");
             f(new int[1]);

      }

       public static  void f(T a) {
            System. out.println(a.getClass().getName());
      }
//结果:
//java.lang.Integer
//java.lang.Character
//java.lang.Double
//java.lang.String
//[I

需注意操作:
1,自定义泛型方法,不一定可以想加
       public  T add (T a,T b){
             //return a+b; 未定义方法
             return null ;
      }
2,只有引用类型才能作为泛型方法的实际参数
      swap(new int[3],3,5);语句会报告编译错误。
3,普通方法、构造方法和静态方法中都可以使用泛型。
4,也可以用类型变量表示异常,称为参数化的异常,可以用于方法的throws列表中,但是不能用于catch子句中。
5,在泛型中可以同时有多个类型参数,在定义它们的尖括号中用逗号分,例如:
      public static V getValue(K key) { return map.get(key);}

泛型方法的练习题
1,编写一个泛型方法,自动将Object类型的对象转换成其他类型。
      public static  T auto(Object obj) { return (T) obj; }
2,采用自定泛型方法的方式打印出任意参数化类型的集合中的所有内容。
       public static  void print(Collection cols) {
             for (E obj : cols) {
                  System. out.println(obj);
            }
      }
3,定义一个方法,把任意参数类型的集合中的数据安全地复制到相应类型的数组中。
       public static  void copy(Collection col, T[] arr) {
             for (int i = 0; i < arr.length; i++) {
                   col.add(arr[i]);
            }
      }

类型参数的类型推断
编译器判断范型方法的实际类型参数的过程称为类型推断,类型推断是相对于知觉推断的,其实现方法是一种非常复杂的过程。

根据调用泛型方法时实际传递的参数类型或返回值的类型来推断,具体规则如下:
1,
当某个类型变量只在整个参数列表中的所有参数和返回值中的一处被应用了,那么根据调用方法时该处的实际应用类型来确定,这很容易凭着感觉推断出来,即直接根据调用方法时传递的参数类型或返回值来决定泛型参数的类型
2,
当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型都对应同一种类型来确定,这很容易凭着感觉推断出来
3,
当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型,且没有使用返回值,这时候取多个参数中的最大交集类型,
4,
当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型, 并且使用返回值,这时候优先考虑返回值的类型
5,
参数类型的类型推断具有传递性,下面第一种情况推断实际参数类型为Object,编译没有问题,而第二种情况则根据参数化的Vector类实例将类型变量直接确定为String类型


定义泛型类型
如果类的实例对象中的多处都要用到同一个泛型参数,即这些地方引用的泛型类型要保持同一个实际类型时,这时候就要采用泛型类型的方式进行定义,也就是类级别的泛型

class Demo{
       private T a ;
       public T getA() {
             return a ;
      }
       public void setA(T a) {
             this.a = a;
      }
}

注意:
在对泛型类型进行参数化时,类型参数的实例必须是引用类型,不能是基本类型。
当一个变量被声明为泛型时,只能被实例变量、方法和内部类调用,而不能被静态变量和静态方法调用。因为静态成员是被所有参数化的类所共享的,所以静态成员不应该有类级别的类型参数


通过反射获得泛型的参数化类型

Method getDeclaredMethod(String name, Class... parameterTypes)
          返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
Type[] getGenericParameterTypes()
          按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型的。

接口 ParameterizedType  extends Type
方法摘要
 Type[] getActualTypeArguments()
          返回表示此类型实际类型参数的 Type 对象的数组。
 Type getOwnerType()
          返回 Type 对象,表示此类型是其成员之一的类型。
 Type getRawType()
          返回 Type 对象,表示声明此类型的类或接口。

       private Vector dates = new Vector();

       public void setDates(Vector dates) {
             this.dates = dates;
      }

       public static void main(String[] args) throws Exception {
            Method method = Ts0. class.getMethod("setDates" , Vector.class);
            ParameterizedType pType = (ParameterizedType) method
                        .getGenericParameterTypes()[0];
            System. out.println("setDates(" + ((Class) pType.getRawType()).getName()
                        + "<" + ((Class) (pType.getActualTypeArguments()[0])).getName()
                        + ">)");
      }


泛型类
容器类应该算得上最具重用性的类库之一。先来看一个没有泛型的情况下的容器类如何定义:

public class Container {
private String key;
private String value;

public Container(String k, String v) {
key = k;
value = v;
}

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}
}
Container类保存了一对key-value键值对,但是类型是定死的,也就说如果我想要创建一个键值对是String-Integer类型的,当前这个Container是做不到的,必须再自定义。那么这明显重用性就非常低。

当然,我可以用Object来代替String,并且在Java SE5之前,我们也只能这么做,由于Object是所有类型的基类,所以可以直接转型。但是这样灵活性还是不够,因为还是指定类型了,只不过这次指定的类型层级更高而已,有没有可能不指定类型?有没有可能在运行时才知道具体的类型是什么?

所以,就出现了泛型。

public class Container {
private K key;
private V value;

public Container(K k, V v) {
key = k;
value = v;
}

public K getKey() {
return key;
}

public void setKey(K key) {
this.key = key;
}

public V getValue() {
return value;
}

public void setValue(V value) {
this.value = value;
}
}
在编译期,是无法知道K和V具体是什么类型,只有在运行时才会真正根据类型来构造和分配内存。可以看一下现在Container类对于不同类型的支持情况:

public class Main {

public static void main(String[] args) {
Container c1 = new Container("name", "findingsea");
Container c2 = new Container("age", 24);
Container c3 = new Container(1.1, 2.2);
System.out.println(c1.getKey() + " : " + c1.getValue());
System.out.println(c2.getKey() + " : " + c2.getValue());
System.out.println(c3.getKey() + " : " + c3.getValue());
}
}
输出:

name : findingsea
age : 24
1.1 : 2.2
泛型接口
在泛型接口中,生成器是一个很好的理解,看如下的生成器接口定义:

public interface Generator {
public T next();
}
然后定义一个生成器类来实现这个接口:

public class FruitGenerator implements Generator {

private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
调用:

public class Main {

public static void main(String[] args) {
FruitGenerator generator = new FruitGenerator();
System.out.println(generator.next());
System.out.println(generator.next());
System.out.println(generator.next());
System.out.println(generator.next());
}
}
输出:

Banana
Banana
Pear
Banana
泛型方法
一个基本的原则是:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。下面来看一个简单的泛型方法的定义:

public class Main {

public static void out(T t) {
System.out.println(t);
}

public static void main(String[] args) {
out("findingsea");
out(123);
out(11.11);
out(true);
}
}
可以看到方法的参数彻底泛化了,这个过程涉及到编译器的类型推导和自动打包,也就说原来需要我们自己对类型进行的判断和处理,现在编译器帮我们做了。这样在定义方法的时候不必考虑以后到底需要处理哪些类型的参数,大大增加了编程的灵活性。

再看一个泛型方法和可变参数的例子:

public class Main {

public static void out(T... args) {
for (T t : args) {
System.out.println(t);
}
}

public static void main(String[] args) {
out("findingsea", 123, 11.11, true);
}
}
输出和前一段代码相同,可以看到泛型可以和可变参数非常完美的结合。

.概要

generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods.

泛型能在定义类,接口和方法时将类型(类和接口)作为参数。和方法声明时的形式参数很类似,都是为了在不同的输入时重用相同的代码。不同的是形式参数输入的是值而泛型是类型。

2.为什么使用泛型

1.编译时更加强的(Stronger)类型检测
java编译器对泛型代码进行强类型检测,一旦检测到代码违反类型安全就发出类型错误。
修复编译错误要比运行错误容易,因为运行错误很难找到问题所在。

2.不用类型转换

//没有使用泛型
List list = new ArrayList();
list.add("without generics");
//需要强制类型转换
String s1 = (String) list.get(0);

//使用泛型
List list2 = new ArrayList();
list2.add("generics");
String s2 = list2.get(0);//不需要转换
3.使程序员能实现适用更加普遍的算法

通过使用泛型,使程序员能实现普遍的算法,算法将是能使用于不同类型的,能自定义的,类型安全的,易读的。

如只写一个排序方法,就能够对整形数组、字符串数组甚至支持排序的任何类型的数组进行排序。

3.具体使用

1.泛型(generic type)

A generic type is a generic class or interface that is parameterized over types.

泛型是一种通用的类或接口,通过类型参数化的。

泛型类以下面的格式定义:

class name { /* ... */ }
通过Box类的非泛型版和泛型版来学习一下:

非泛型:

//non-generic class
public class Box {
private Object object;

public Object getObject() {
return object;
}

public void setObject(Object object) {
this.object = object;
}
}
泛型:

//generic class
public class Box {

//T表示Type
private T t;

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}

}
类型参数命名规范按约定的习俗,类型参数的名字都是由一个大写字母构成。

E - Element 元素(used extensively by the Java Collections Framework)
K - Key 键
N - Number 数值
T - Type 类型
V - Value 值
S,U,V etc. - 2nd, 3rd, 4th types 第二种,第三种,第四种类型

调用和实例化泛型类

为了获得泛型类的引用,必须执行泛型类的调用,使用具体的值来代替T,如Integer。

//获得引用
Box integerBox;
//实例化
integerBox = new Box();
泛型类的调用类似于普通的方法调用,只是泛型类传递的是类型参数(type argument)而方法调用传递的是参数(argument)。

多类型参数

泛型类可能有多种类型的参数。如下所示:

public interface Pair{
public K getKey();
public V getValue();
}
public class OrderedPair implements Pair{
private K key;
private V value;
public OrderedPair(K key,V value){
this.key = key;
this.value = value;
}
@Override
public K getKey() {
return key;
}

@Override
public V getValue() {
return value;
}
}
//实例化
Pair p1 = new OrderedPair("Even", 8);
Pair p2 = new OrderedPair("hello", "world");
//Java SE7开始出现的新语法
OrderedPair p1 = new OrderedPair<>("Even", 8);
OrderedPair p2 = new OrderedPair<>("hello", "world");
//参数化的类型
OrderedPair> p = new OrderedPair<>("primes", new Box(...));

你可能感兴趣的:(java编程思想,java,c/c++,测试)