泛型实现了参数化类型的概念。
简单泛型
容器是出现泛型的重要原因之一。泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。因此我们需要使用类型参数T达到需要时决定什么类型的目的。
泛型的核心概念就是告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
class Holder<T>{
private T value;
public Holder(){}
public Holder(T val){ value = val;}
public void set(T val){value = val;}
public T get(){ return value;}
public boolean equals(Object obj){
return value.equals(obj);
}
}
- 元祖类库:
一般return语句只允许返回单个对象,若想返回多个对象,需要创建一个对象,用它来持有想要返回的多个对象。但我们可以使用元组,将一组对象直接打包存储于其中的一个单一对象,这个容器是只读的。元祖一般可以具有任意长度,任意类型。元祖隐含的保持了其中元素的次序。
public class TwoTuple<A,B>{
//使用final保证了只读不能改
public final A first;
public final B second;
public TwoTuple(A a, B b){
first = a;
second = b;
}
}
以上元组可以通过继承实现更长的元组。
- 一个堆栈类
public class LinkedStack<T>{
private static class Node<U>{
U item;
Node<U> next;
Node(U item, Node<U> next){
this.item = item;
this.next = next;
}
boolean end() { return item == null && next == null}
}
private Node<T> top = new Node<T>(): // 结束标志
//push
public void push(T item){
top = new Node<T>(item, top);
}
//pop
public T pop(){
T result = top.item;
if(!top.end()){
top = top.next;
}
return result;
}
}
泛型接口
public interface Generator<T>{ T next();}
基本类型无法作为类型参数,但是java具备了自动打包和自动拆包的功能。
泛型方法
泛型方法使得该方法能够独立于类而产生变化。此外,对于一个static的方法而言,无法访问泛型类的类型参数,因此,static需要变成泛型方法才能使用泛型能力。
public <T> void f(T X){}
public static void main(String[] args){
obj.f("");
obj.f(1);
}
当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法时编译器会自动找出具体的类型,这称为类型参数推断。
显示的类型说明:
f(New.<Person, List<Pet>>map());
如果在定义该方法的内部,必须在点操作符之前使用this关键字。
可变参数与泛型方法能够很好的共存。
public static <T> List<T> makeList(T ...args){
List<T> result = new ArrayList<T>();
for(T item : args){
result.add(item);
}
return result;
}
总之,使用泛型可以使方法或者类脱离类型的限制,操作更加灵活。
擦除
Class.getTypeParameters()将返回一个TypeVarible对象数组,表示有泛型声明所声明的类型参数。然而实际只返回参数占位符。重要的是:在泛型代码内部,无法获知任何有关泛型参数类型的信息。
java泛型是使用擦除来实现的,在使用泛型是,任何具体的类型信息都被擦除了,唯一知道的就是在使用对象。
public Manipulator<T>{
private T obj;
public Manipulator(T x){ obj = x;}
//ERROR: cannot find symbol: method f();
public void manipulate() { 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编译器无法将manipulate()必须能够在obj上调用f()映射到Hasf拥有f()这一事实。为了达到目的,必须给定泛型的边界,比如extends关键字。
```java
public Manipulator<T extends Hasf>{
private T obj;
public Manipulator(T x){ obj = x;}
//ERROR: cannot find symbol: method f();
public void manipulate() { obj.f();}
}
泛型类型参数将擦除到它的第一个边界。实际上类的声明中用Hasf替换了T。实际上和向上转型一样了。但是如果方法有返回值的话还是有好处的。
这是为了迁移兼容性的折中。
对于在泛型中创建数组,使用Array.newInstance(Class<t>type,int size)是推荐的方式。
擦除的补偿
因为擦除,任何在运行时需要知道的确切类型信息的操作都将无法工作:
public class Erased<T>{
private final in SIZE = 100;
public stativ 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]; // Unchecked warning
}
}
如果引入类型标签,就可以转而使用动态的isInstance():
public class ClassTypeCapture<T>{
Class<T> kind;
...
public boolean f(Object arg){
return kind.isInstance(arg);
}
T x;
public ClassAsFactory(Class<T> kind ){
try{
x = kind.newInstance();//或许会因为没有默认构造器失败,一般还是显示的工厂对象
}catch....
}
}
**模板方法设计模式:
abstruct class GenericWithCreate<T>{
final T element;
GenericWithCreate{ element = create();}
abstruct T create();
}
class X {}
class Creator extends GenericWithCreate<X>{
X create() { return new X();}
void f() { print(element.getClass().getSimpleName()); }
}
泛型数组:
不能创建泛型数组,一般使用ArrayList代替。即时创建一个Object数组,然后将其转型,这可以编译,但是不能运行,将产生ClassCastException。因为数组创建时类型被确定,编译器知道其类型,但运行时,依旧是Object数组。成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。
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 = GenericArray<Integer>(10);
//This causes a ClassCastException
// ! Integer[] ia = gai.rep();
//This is OK:
Object[] oa = gai.rep();
}
}
因为有了擦除,数组的运行时类型就只能是Object[],如果我们立即将其转型为T[],那么编译器该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。因此,最好在集合内部使用Object[],然后当你使用数组元素是,添加一个队T 的转型。
public class GenericArray2<T>{
private Object[] array;
@SuppressWarnings("unchecked")
public GenericArray(int size){
array = new Object[size];
}
public void put(int index, T item){
array[index] = item;
}
public T get(int index){ return (T) array[index];}
public T[] rep(){ return (T[])array;}// WANNING: unchecked cast
}
这种方法调用rep()时依然会编译器产生警告,运行时产生异常,没有任何方式可以推翻底层的数组类型,只能是Object[],这种方法的优势是我们不大可能忘记这个数组的运行时类型。实际上很多源码都是这么写的。
边界
边界可以使你在用于泛型的参数类型上设置限制条件。
通配符
泛型和数组不一样,泛型没有内建的协变类型(向上转型)。意思是,如果将子类数组赋值给父类数组是允许的,并且向子类数组添加其他子类在编译期是允许的,而运行时会抛出异常ArrayStoreException。而对于泛型,不能将一个子类的容器赋值给父类的容器。如果要在两个类型之间建立某种类型的向上转型关系,需要用到通配符。然而一旦执行泛型的向上转型,就会失去向List添加任何类型对象的机会。
public static void main(String[] args){
List<? extends Fruit> list = new ArrayList<Apple>;
//Compile Error: cannot add any object
//list.add(new Fruit());
//list.add(new Apple());
//list.add(new Orange());
//但可以这样:
List<? extends Fruit> list1= new Arrays.asList(new Apple());
Apple a = list1.get(0); //no warnning
list1.indexOf(new Apple()); //参数是Obeject
list1.contains(new Appel()); //参数是Obeject
}
因为add接受一个泛型化参数,然而编译器面对"? extends Fruit"参数时并不知道需要Fruit的哪个子类型,因此它不会接受任何类型的Fruit。而上述两个可以使用的方法接受的参数为Object,不涉及任何通配符,所以是安全的。
逆变:超类型通配符<? super MyClass>,可以声明通配符是某个特定类的任何基类来界定。对于这样的Collection,能够保证是特定类的父类容器,因此添加该类或该类的子类是安全的。
static void writeTo(List<? super Apple> apples){
apples.add(new Apple());
apples.add(new Jonathan());
//apples.add(new Fruit()) //Errors!!!
}
总之,读的时候的通配符extends,写的时候的通配符super。
无界通配符:?声明使用java泛型写这段代码,但并不是要使用原生类型,当前情况,泛型参数可以持有任何类型。
public class UnboundedWildcards1{
static List list1;
static List<?> list2;
static List<? extends Object> list3;
static void assign1(List list){
list1 = list;
list2 = list;
//list3 = list; // Warning:unchecked conversion
//Found:list, Required: List<? extends Object>
}
static void assign2(List<?> list){
list1 = list;
list2 = list;
list3 = list;
}
stativ void assign3(List<? extends Object> list){
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args){
assign1(new ArrayList());
assign2(new ArrayList());
// assign3(new ArrayList()); // warnning:Unchecked conversion. Found:ArrayList Required: List<? extends Object>
assign1(new ArrayList<String>());
assign2(new ArrayList<String>());
assign3(new ArrayList<String>());
//both forms are acceptable as List<?>:
List<?> wildList = new ArrayList();
wildList = new ArrayList<String>();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
}
编译器在处理List<?>和List<? extends Object>是不同的。实际上,List<?>等价于List<Object>,前者表示“具有某种特定类型的非原生List,只是我们不知道那种类型是什么”,后者表示“持有任何Object类型的原生List”。
当你在处理多个泛型参数是,有时允许一个参数是任何类型,同时为其他参数确定某种特定类型的这种能力会十分有用。比如Map<?,?>,Map<?, String>。
下面一个例子体会一下通配符的使用:
public class Wildcards{
// 原生类型参数
static void rawArgs(Holder holder, Object arg){
//holder.set(arg);//Warning: Unchecked call to set(T) as a member of the raw type holder
//can't do this: don't have any 'T'
// T t = holder.get();
//ok, but type information has been lost:
Object obj = holder.get();
}
//无界通配符,与原生函数一样,不过是报错而不是警告
static void unboundedArg(Holder<?> holder, Object arg){
//holder.set(arg);//Error: set(capture of ?) in Holder<capture of ?> cannot be applied to (Object)
// holder.set(new Wildcards()); // Same error
//can't do this: don't have any 'T'
// T t = holder.get();
//ok, but type information has been lost:
Object obj = holder.get();
}
static <T> T exact1(Holder<T> holder){
T t = holder.get();
return t;
}
static <T> T exact2(Holder<T> holder, T arg){
holder.set(arg);
T t = holder.get();
return t;
}
//extends通配符
static <T> T wildSubtype(Holder<? extends T>holder, T arg){
//holder.set(arg); // Error: set(capture of ? extends T) in Holder<capture of ? extends T> cannot be applied to (T)
T t = holder.get();
return t;
}
//super通配符
static <T> T sildSuperType(Holder<? super T> holder, T arg){
holder.set(arg);
// T t = holder.get(); //Error:
// Incompatible types: found Object, required T
//ok, but type information has been lost:
Object obj = holder.get();
}
}
捕获转换:
通常情况下,使用原生类型和<?>并没有什么区别,但是有一种情况特别需要使用<?>而不是原生类型,即捕获转换。
public class CaptureConversion{
static <T> void f1(Holder<T> holder){
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder){
f1(holder);
}
//@SuppressWarnings("unchecked")
public static void main(String[] args){
Holder raw = new Holder<Integer>(1);
f1(raw);//Unchecked invocation f1(Holder) of the generic method f1(Holder<T>) of type
f2(raw);
Holder rawBasic = new Holder();
rawBasic.set(new Object());//Type safety: The method set(Object) belongs to the raw type Holder. References to generic type Holder<T> should be parameterized
f2(rawBasic);//No warnings
//Upcast to Holder<?>, still figures out:
Holder<?> wildcarded = new Holder<Double>(1.0);
f2(wildcarded);
}
}
参数类型在调用f2()的过程中被捕获,因此它可以在对f1()的调用中被使用。
捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。
注意:不能从f2()中返回T,因为T对于f2()来说是未知的。
一般来说,带有通配符的 API 比带有泛型方法的 API 更简单,在更复杂的方法声明中类型名称的增多会降低声明的可读性。因为在需要时始终可以通过专有的捕获转换来恢复名称,这个方法可以保持 API 整洁,同时不会删除有用的信息。
泛型会出现的各种问题
- 任何基本类型都不能作为类型参数
解决方法时Java的自动包装机制。但自动包装机制不能作用于数组。 - 实现参数化接口
一个类不能实现同一泛型接口的两种辩题,因为擦除的存在,两个变体会成为相同的接口。
interface Paybale<T> {}
class Employee implements Payable<Employee>{}
class Hourly extends Employee implements Payable<Hourly>{}
//Hourly不能编译,
//但是如果从两个class都移除泛型参数(像编译器擦 除阶段做的那样)就可以编译了。
- 转型和警告
使用带有泛型类型参数的转型和instanceof不会有任何效果,其实只是将Object转型为Object。有时,泛型转型会产生不恰当的警告。可以通过泛型类来转型:
List<Widget> lw = List.class.cast(in.readObject());
- 重载
由于擦除的原因,重载方法将产生相同的类型前面。
//不会被编译
public class UseList<W, T>{
void f(List<T> v){}
void f(List<W> v){}
}
- 基类劫持了接口
一旦父类确定了类型参数,子类就不能使用其他类型参数了,即时是类型窄化也不能。
自限定的类型
class SelfBounded<T extends SelfBounded<T>>{}
这强调了当extends关键字用于边界和用来创建子类明显是不同的。
古怪的循环泛型(CRG):类古怪地出现在它自己的基类中。
class GenericType<T>{}
public class CRGeneric extends GenericType<CRGeneric>()
eg:
public class BasicHolder<T>{
T element;
void set(T arg){ element = arg;}
T get() { return element;}
void f(){ print(element.getClass().getSimpleName());}
}
在一个古怪的循环泛型中使用BasicHolder:
class Subtype extends BasicHolder<Subtype>{}
public class CRGWithBasicHolder(String[] args){
Subtype st1 = new Subtype(), st2 = new Subtype();
st1.set(st2);
Subtype st3 = st1.get();
st1.f();
}
本质:基类用导出类代替其参数,意味着泛型基类编程了所有导出类的公告模板。重点是,在所产生的类中将使用确切类型而不是基类型,这里,set()和get()都是确切的Subtype。
自限定:
class A extends SelfBounded<A> {}
自限定强制泛型当做其自己的边界参数来使用。它可以保证类型参数必须与正在被定义的类相同。
public class SelfBounded<T extends SelfBounded<T>>{
.....
}
class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{} // 也可以哦
class D{}
// class E extends SlefBounded<D>{}
//!!!!!ERROR: D is not within its bound
//但是可以这样:
class F extends SelfBounded{}
可以将自限定用于泛型方法,防止方法应用于自限定参数之外的任何事物之上。
static <T extends SelfBounded<T>> T f(T arg){}
还可以产生协变参数类型——方法参数类型会随子类而变化。
interface Ggetter<T extends Ggetter<T>>{
T get();
}
interface Getter extends Ggetter<Getter>{}
...
void test(Getter g){
Getter result = g.get();
Ggetter gg = g.get(); //对基类也有效
}
而对set来说:
interface SelfBoundSetter <T extends SelfBoundSetter<T>>{
void set(T arg);
}
interface Setter extends SelfBoundSetter<Setter>{}
...
void test(Setter s1, Setter s2, SelfBoundSetter sbs){
s1.set(s2);
// s1.set(sbs) //!!ERROR:类型不匹配
}
动态类型安全
java.util.Collections可以使用一系列静态方法进行类型检查:checkedCollectio。。。。对老版本代码很有好处。这些方法返回一个容器,该容器在添加item的时候会对item进行类型检查。
异常
类型参数可能会在throws语句中用到,这就是参数化所抛出的异常。
interface Processor<T,E extends Exception>{
void process(List<T> result) throws E;
}
混型
潜在类型机制
也叫鸭子类型机制,值要求实现某个方法子集,而不是某个特定类或接口,从而放松了限制,产生更加泛化的代码。java不支持这种机制。
对缺乏潜在类型机制的补偿
- 反射
反射将所有类型检查转移到了运行时。 - 使用适配器仿真潜在类型机制
- 将函数对象用作策略