Java编程思想 -- 泛型概括总结
Posted Y_ZhiWen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java编程思想 -- 泛型概括总结相关的知识,希望对你有一定的参考价值。
在面向对象编程语言中,多态算是一种泛化机制。
例如你可以将方法的参数类型设为基类,那么该方法就可以接受从这个类中导出的任何类作为参数。但是,考虑到除了final类不能扩展,这种灵活性大大降低。
如果方法的参数是一个接口,而不是一个类,这种限制就放松很多,可是有时候,使用了接口,对程序的约束也还是太强了。因为一旦指明了接口,它就要求你的代码必须使用特定的接口。
Java SE5的重大变化之一就是:泛型的概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型。
简单泛型
直接看例子
public class Holder<T>
private T a;
public Holder(T a) this.a = a;
public void set(T a) this.a = a;
public T get() return a;
public static void main(String[] args)
Holder<String> holder = new Holder<String>("String");
String s = holder.get();
// Error
// holder.set(1);
// holder.set(1.1);
在这个例子中可以看出,Holder是持有对象T的类(Holder< T>),在构造器,方法参数,成员变量以及返回值都可以使用对象T的类型。
class I<T>
class K<T>
class KK
private T t;
// Error
// public static class KKK<T>
// public static class KKK
// private static T t;
//
这里可以看到,内部类可以持有对象T的类型,而嵌套类不可以。
泛型接口
直接看例子
public interface GenericsInstance<T>
T next();
void set(T t);
class InnerClass<T>
public static T t;
这里可以看到,GenericsInstance是持有对象T的接口,在其返回值,方法参数,成员变量已经嵌套类都可以使用对象T的类型
泛型方法
可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是。
泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白
定义泛型方法,只需将泛型参数列表置于返回值之前
public class GenericMethods
// 泛型方法
public <T> void f(T t)
System.out.println(t.getClass().getName());
public static void main(String[] args)
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.2);
这个例子中,只有方法f()拥有类型参数T。
可以看下面例子
class T
public <T> T get(T t)
T tt = null;
return tt;
// 可变参数与泛型方法
public static <E> void f(E... es)
for (E e:es)
// do something
注意,当在使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,比如前面的gm.f(1);。因为编译器会为我们找出具体的类型。这称为类型参数推断
匿名内部类
泛型还可以应用于内部类以及匿名内部类
内部类前面例子已经简单描述,这里看一下匿名内部类的例子
public Interface Generator<T>
T next();
class Customer
private static long counter = 1;
private final long id = counter++;
private Customer()
public String toString() return "Customer " + id;
// 匿名内部类与泛型
public static Generator<Customer> generator()
return new Generator<Customer>()
public Customer next()
return new Customer();
擦除的神秘之处
例如,可以声明ArrayList.class,但是不能声明ArrayList< Integer>.class。
public class ErasedTypeEquivalence
public static void main(String[] args)
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
/*Output:
true
*/
上面的c1和c2被认为相同的类型
在泛型代码内部,无法获得任何有关泛型参数类型的信息
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体类型信息都被擦除了,你唯一知道的就是你在使用一个对象。
因此,List< String>和List< Integer>在运行时事实上是相同类型,这两种形式都被擦除成为它们的“原生”类型List
下面看个例子:
public class HasF
public void f()
class Manipulator<T>
private T obj;
public Manipulator(T t)
this.obj = t;
public void manipulate()
// Error
//obj.f();
由于有了擦除,所以上面obj.f()方法不能调用。
为了调用f(),我们必须协助泛型类,给定泛型类的边界,这里使用extends关键字:
// 协助泛型类,给定泛型类的边界
class Manipulator<T extends HasF>
private T obj;
public Manipulator(T t)
this.obj = t;
public void manipulate()
obj.f();
边界< T extends HasF>声明T必须具有类型HasF或者从HasF导出的类型。这样就可以安全调用f()
擦除的问题
擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式
另外,使用泛型并不是强制的
class GenericBase<T>
private T element;
public void set(T arg) element = arg;
public T get() return element;
class Derived1<T> extends GenericBase<T>
class Derived2 extends GenericBase // No warning
// class Derived3 extends GenericBase<?> // Error
// class or interface without bounds
public class ErasureAndInheritance
@SuppressWarnings("unchecked")
public static void main(String[] args)
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj); // Warning here
边界处的动作
public class ArrayMaker<T>
private Class<T> kind;
public ArrayMaker(Class<T> kind)
this.kind = kind;
@SuppressWarnings("unchecked")
T[] create(int size)
// Type safety: Unchecked cast from Object to T[]
return (T[]) Array.newInstance(kind, size);
public static void main(String[] args)
ArrayMaker<String> stringMaker = new ArrayMaker<String>(String.class);
String[] stringArray = stringMaker.create(5);
System.out.println(stringArray);
/*Output:
[null, null, null, null, null]
*/
即使kind被存储为Class< T>,擦除也意味着它实际类型将被存储为Class,没有任何参数,因此,在创建数组时,这不会产生具体的结果,所以必须转型,这将产生一条警告。
注意,对于在泛型中创建数组,使用Array.newInstance()是推荐方式
泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型
擦除的补偿
由于擦除,一下操作将无法操纵
public class Erased<T>
private final int SIZE = 10;
public static void f(Object arg)
if( arg instanceof T ) // Error
T var = new T();// Error
T[] array = new T[SIZE];// Error
T[] array = (T) new Object[SIZE];// Warning
通过引入类型标签对擦除进行补偿,这意味着需要显式地传递类型的Class对象,以便在表达式中使用,如果引入类型标签,就可以转而使用动态的isInstance()。
class Building
class House extends Building
public static class ClassTypeCapture<T>
Class<T> kind;
public ClassTypeCapture(Class<T> kind)
this.kind = kind;
public boolean f(Object arg)
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()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
/*Output:
true
true
false
true
*/
创建类型实例
new T()无法实现,部分原因是因为擦除,另一部分原因是因为编译器不能验证T具有默认(无参)构造器,但是C++却可以实现,因为它是在编译期受到检查
Java中的解决方案是传递一个工厂对象
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);
// Exception
ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class);
第一个可以创建,但是ClassAsFactory<Integer>
失败,因为Integer没有默认的构造器,而是应该使用显示工厂
interface Factory<T>
T create();
class Foo<T>
private T x;
// 构造器:public <F extends Factory<T>> Foo(f factory)
public <F extends Factory<T>> Foo(f factory)
x = factory.create();
// ...
class IntegerFactory implements Factory<Integer>
public Integer create()
return new Integer(0);
class Widget
public static class Factory implements Factory<Widget>
public Widget create()
return new Widget();
public class FactoryConstraint
public static void main(String[] args)
new Foo<Integer>(new IntegerFactory);
new Foo<Widget>(new WidgetFactory);
另一种方式是模板方法设计模式
abstract class GenericWithCreate<T>
final T element;
GenericWithCreate() element = create();
abstract T create();
class X
class Creator extends GenericWithCreate<X>
X create() return new X();
void f()
System.out.println(element.getClass().getSimpleName());
public class GreatorGeneric
public static void main(String[] args)
Creator c = new Creator();
c.f();
泛型数组
看一下例子
class Generic<T>
public class ArrayOfGenericReference
// 泛型数组
static Generic<Integer>[] gia;
编译器将接受这个程序,而不会产生警告,但是,永远都不能创建这个确切类型的数组(包括类型参数),这一点令人困惑。
既然所以数组无论它们持有的类型如何,都具有相同的结构,那么看起来应该能够创建一个Object数组,并将其转型为所希望的数组类型,事实上这可编译,但是不能运行:
publicclassArrayOfGeneric
static Generic<Integer>[] gia;
public static void main(String[] args)
// ClassCaseException
// gia = (Generic<Integer>[])new Object[100];
// 成功创建泛型数组
gia = (Generic<Integer>[])new Generic[100];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
//gia[1] = new Object();
//gia[2] = new Generic<Double>();
成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型
即:gia = (Generic<Integer>[])new Generic[100];
public class GenericArray<T>
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int size)
array = (T[])new Object[size];
public void put(int index, T item)
array[index] = item;
public T get(int index)
return array[index];
public T[] rep() return array;
public static void main(String[] args)
GenericArray<Integer> gai = new GenericArray<Integer>(10);
// ClassCastException
// Integer[] ia = gai.rep();
Object[] oa = gai.rep();
rep()方法返回T[],并将结果作为Integer[]引用来捕获,会产生ClassCastException,这是因为实际运行时类型是Object[]。
因为有了擦除,数组的运行时类型就只能是Object[],如果我们立即将其转型为T[],那么在编译期该数组的实际类型就将丢失,而编译器可能错过某些潜在的错误检查。正因为这样,最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。这一点在集合框架的源码可以见到。
示例:
public class GenericArray<T>
private Object[] array;
public GenericArray(int size)
array = new Object[size];
@SuppressWarnings("unchecked")
public T get(int index) return (T)array[index];
@SuppressWarnings("unchecked")
public T[] rep()
return (T[])array;
public static void main(String[] args)
GenericArray<Integer> gai = new GenericArray<Integer>(10);
for(int i = 0; i < 10; i++)
gai.put(i,i);
for( int i = 0; i < 10; i ++)
System.out.println(gai.get(i)+"");
// ClassCastException
// Integer[] ia = gai.rep();
然而,如果你调用rep(),它还是尝试着将Object[]转型为T[],这仍旧是不正确的,将在编译期产生警告,运行时产生异常。
public class GenericArrayWithTypeToken
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz)
array = (T[]) Array.newInstance(type,sz);
public void put(int index, T item)
array[index] = item;
public void get(int index) return array[index];
public T[] rep() return array;
public static void main(String[] args)
GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class,10);
// work
Integer[] ia = gai.rep();
通过传递类型标记Class<T>
到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组。
边界
因为擦除移除了类型信息,所以可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。但是如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法
interface HasColor int getColor();
class Colored<T extends HasColor>
T item;
Colored(T item) this,item = item;
T getItem() return item;
// 泛型边界
int color() return item.getColor();
class Dimension public int x,y,z;
//T extends Dimension & HasColor:类在前,接口在后
class ColoredDimension<T extends Dimension & HasColor>
T item;
ColoredDimension(T item) this.item = item;
T getItem() return item;
// 泛型边界
int color() return item.getColor();
int getX() return item.x;
int getY() return item.y;
int getZ() return item.z;
interface Weight int weight();
//T extends Dimension & HasColor & Weight:类在前,接口在后
class Solid<T extends Dimension & HasColor & Weight>
T item;
ColoredDimension(T item) this.item = item;
T getItem() return item;
// 泛型边界
int color() return item.getColor();
int getX() return item.x;
int getY() return item.y;
int getZ() return item.z;
int weight() return item.weight();
class Bounded extends Dimension implements HasColor, Weight
public int getColor() return null;
public int weight() return 0;
public class BasicBounds
public static void main(String[] args)
Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
solid.color();
solid.getY();
solid.weight();
可以看到使用<T extends Dimension & HasColor & Weight>
实现多边界,而且类在前,接口在后。
更多层次的情况在书中。
通配符
通配符被限制为单一边界,所以List<? extends A & B>
会报错
看一下例子:数组向导出类型的数组赋予基类型的数组引用
class Fruit
class Apple extends Fruit
class Jonathan extends Apple
class Orange extends Fruit
public class GovariantArrays
public static void main(String[] args)
//
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonahan(); // OK
// 编译器不会报错,运行期报错
// fruit[0] = new Fruit(); // ArrayStoreException
// fruit[1] = new Fruit(); // ArrayStoreException
实际数组类型是Apple[],应该只能放置Apple和Apple的子类型,这在编译期和运行时都可以工作。
但是编译器允许你将Fruit放置到这个数组中,因为它有一个Fruit[]引用,但是运行时数组机制知道它处理的是Apple[],因此会抛出异常
泛型的主要目标之一就是将这种错误检测移入到编译期
因此使用泛型容器来代替数组
public class NonCovariantGenerics
// 编译错误
List<Fruit> flist = new ArrayList<Apple>();
这里要明确一点:Apple的List不是Fruit的List,尽管Apple是一种Fruit类型
那怎么解决这问题呢?
这时候需要在两种类型之间建立某种类型的向上转型关系
public class GenericsAndCovariance
public static void main(String[] args)
List<? extends Fruit> flist = new ArrayList<Apple>();
// 编译错误
flist.add(new Apple());
flist.add(new Fruit());
flist.add(new Object());
// 可以运行
flist.add(null);
Fruit f = flist.get(0);
flist.contains(new Apple()); // 参数类型是Object
flist.indexOf(new Apple()); // 参数类型是Object
flist类型现在是List<? extends Fruit>
,可以将其读作“具有任何从Fruit继承的类型的列表”。但是,这实际上并不意味着这个List将持有任何类型的Fruit。
通配符引用的是明确的类型,因此它意味着“某种flist引用没有指定的具体类型”
因此当你指定一个ArrayList<? extends Fruit>
时,add的参数也变成? extends Fruit。编译器不能了解这里需要Fruit哪个具体子类型,因此它不会接受任何类型的Fruit
但是,在使用contains和indexOf时,类型参数是Object,因此不涉及任何通配符,而编译器也将允许这个调用。这一点在ArrayList等源码可以看到
那应该怎么解决呢??
另一条路:超类型通配符
这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定<? super MyClass>
,甚至或者使用类型参数:<? super T>
(尽管你不能对泛型参数给出一个超类型边界;即不能声明<T super MyClass>
)
因此:
public class SuperTypeWildcards
static void writeTo(List<? super Apple> apples)
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
参数Apple是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的
超类型边界放松了在可以先方法传递的参数上所作的限制,再看一个例子:
public class GenericWriting
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> void writeExact(List<T> list,T item)
list.add(item);
static void f1()
writeExact(apples, new Apple());
// Error
// writeExact(fruit, new Apple());
static <T> void writeExactWildcard(List<? super T> list, T item)
list.add(item);
static void f2()
writeExactWildcard(apples, new Apple());
writeExactWildcard(fruit, new Apple()); // OK
public static void main(String[] args)
f1();
f2();
继续一个例子:
public class GenericReading
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> T readExact(List<T> list)
return list.get(0);
static void f1()
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
// 通过泛型类读取
static class Reader<T>
T readExact(List<T> list) reutrn list.get(0);
static void f2()
Reader<Fruit> fruitReader = new Reader<Fruit>();
Fruit f = fruitReader.readExact(fruit);
// Error
// Fruit f = fruitReader.readExact(apples);
// 改进
static class CovariantReader<T>
T readCovariant(List<? extends T> list)
return list.get(0);
static void f3()
CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
Fruit f = fruitReader.readExact(fruit);
// OK
Fruit a = fruitReader.readExact(apples);
public static void main(String[] args)
f1(); f2(); f3();
上面这例子说明,List<? extends T>
列表中所有对象至少是一个T,并且可能是从T导出的某种对象
问题
实现参数化接口
一个类不能实现同一个泛型接口的两个变体,由于擦除的原因,这两个变体会成为相同的接口
interface Payable<T>
class Employee implements Payable<Employee>
class Hourly extends Employee implements Payable<Hourly> // Error
Hourly不能编译,因为擦除会将Payable<Employee>
和Payable<Hourly>
简化为相同的类Payable,这样,上面的代码就意味着在重复两次实现相同的接口。
十分有趣的是,如果从Payable的两种用法都移除掉泛型参数(就像编译器在擦除阶段所作的那样),这段代码就可以编译。
重载
下面程序是不能编译的
public class UserList<W,T>
void f(List<T> v)
void f(List<W> w)
由于擦除的原因,重载方法将产生相同类型的签名
因此可以改成
public class UserList<W,T>
void f1(List<T> v)
void f2(List<W> w)
基类劫持了接口
class ComparablePet implements Comparable<ComparablePet>
public int compareTo(ComparablePet arg) return 0;
//Error
class Cat extends ComparablePet implements Comparable<Cat>
class Hamster extends ComparablePet implements Comparable<ComparablePet>
public int compareTo(ComparablePet arg) return 0;
可以看到,一旦为Comparable确定了ComparablePet参数,那么其他任何实现类都不能与ComparablePet之外的任何对象比较
以上是关于Java编程思想 -- 泛型概括总结的主要内容,如果未能解决你的问题,请参考以下文章