泛型

1、方法一次返回多个结果的实现方法:

  • 构建一个类
  • 使用泛型,创建多元组
public class TwoTuple<A, B>{
public final A first;
public final B second;
public TwoTuple(A a, B b){
first = a;
second = b;
}
}

public class ThreeTuple<A, B, C> extends TwoTuple<A, B>{
public final C third;
public ThreeTuple(A a, B b, C c){
super(a, b);
third = c;
}
}

  

2、泛型也可以应用于接口,例如生成器。

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

 

3、java泛型的一个局限性,基本类型无法作为参数类型。不过,java SE5具备了自动打包和自动拆包的功能。

4、泛型方法:是否拥有泛型方法,与其所在的类是否是泛型没有关系。注意:如果使用泛型方法可以取代整个类泛型化,那么就应该尽量使用泛型方法。

5、static方法无法使用泛型类的类型参数。也就是说,static想使用泛型能力,那么就必须使其成为泛型方法。要定义泛型方法,只需将泛型参数列表置于返回值之前。

public class GenericMethods{
    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }
}

  

当使用泛型类时,必须在创建对象时指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断。

6、如果你将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推断。在这种情况下,编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变量。要解决这个问题,只能显示的进行类型说明,即必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。

  • 如果是在定义该方法的类内部使用,必须在点操作符之前使用this.
  • 如果是使用static方法,必须在点操作符之前加上类名。

7、泛型方法与可变参数列表能够很好地共存。

8、Class.getTypeParameters()将返回一个TypeVariable对象数组,表示有泛型声明所声明的类型参数。但是,你只能获取用作参数的占位符和标识符,而并非具体的类型。比如

List<Frob> list = new ArrayList<Frob>();

list.getClass().getTypeParameters()只能获得[E],而不是[Frob].

9、java 泛型是通过使用擦除具体类型来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。

  • 泛型类型参数将擦除到它的第一个边界。比如:class Manipulator<T extends HasF>,T 擦除到了HasF。
  • 泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。
  • 诸如List<T>将擦除到List,普通的类型变量在未指定边界的情况下,将擦除到Object。
  • 泛型在编译时检查泛型的类型,因此对于进入set()的类型检查是不需要的。而对从get()返回的值进行转型是必须的,但由于你使用了泛型,转型操作将有编译器自动插入。
  • 擦除的代价是显著的,泛型不能用于显示地引用运行时类型的操作之中,例如转型,instanceof操作和new表达式,因为所有关于参数的类型信息都丢失了。比如Foo<Cat> f = new Foo<Cat>()。class Foo中的代码应该知道工作于Cat上,而泛型语法也在强烈地暗示,在整个类中的各个地方,类型T都在被替换。但是事实却并非如此,它只是个Object.
public class Erased<T>{
	private final int SIZE = 100;
	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];	//Unchecked warning
		}
	}
}

10、泛型擦除的补偿:

  • 对于上面的例子,我们可以通过引入类型标签来对擦除进行补偿。这意味着需要显式地传递你的类型的Class对象。
public class ClassTypeCapture<T>{
	Class<T> kind;
	public ClassTypeCapture(Class<T> kind){
		this.kind = kind;
	}
	public boolean f(Object arg){
		return kind.isInstance(arg);
	}
}
  • 对于无法通过new T()创建类型实例的情况,我们可以通过工厂方法来绕过。最便利的工厂对象就是Class对象,因此如果使用类型标签,那么就可以使用newInstance()来创建这个类型的新对象;
  • 对于无法创建泛型数组,一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList。

11、边界:因为擦除了类型,对于无界泛型,只能调用Object的方法。而假如将泛型参数限制为某个类的子类,那么你就能调用这个类的方法。java泛型通过extends来指定边界。java泛型可以指定一个类,多个接口作为边界。

public class ClassA {
    public int x;
    public int y;
    public int z;

}
public interface InterfaceA {
    void methodA();
}
public interface InterfaceB {
    void methodB();
}
public class GenericClass<T extends ClassA & InterfaceA & InterfaceB> {
    private T item;
    public void methodA(){
        item.methodA();
    }

    public void methodB(){
        item.methodB();
    }

    public int getX(){
        return item.x;
    }
}

注意:extends必须先类,后接口。

12、通配符

  • extends通配符
public class Fruit {}
public class Apple extends Fruit{}
public class CovariantArrays {
    public static void main(String[] args){
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();  // OK
        fruits[1] = new Fruit();    //编译时正常,运行时报java.lang.ArrayStoreException
    }
}

编译时认为fruits是Fruit数组类型的引用,所以放Fruit对象,编译不报错,但是运行机制知道这是Apple[],放入异构类型数据就会抛出异常。
其实向上转型并不适合这里,我们应该是将一个数组赋值给另一个数组。但是,当我们想像如下代码那么执行时:

List<Fruit> fruits = new ArrayList<Apple>();

这段代码会报类型不匹配,报错原因其实是不能把Apple泛型赋给Fruit泛型。
我们可以通过extends通配符来使得子类性的list引用,赋值给基类的list引用。

List<? extends Fruit> fruitList = new ArrayList<Apple>();
// fruitList.add(new Fruit());
// fruitList.add(new Apple());
Fruit fruit = fruitList.get(0);

List<? extends Fruit> fruitList指的是fruitList存的是Fruit子类型对象,但是具体是哪个子类型,编译器是无从得知的,所以就可能出现上面Fruit[]数组的情况(fruitList其实被赋值ArrayList<Apple>,但是你却能拿着fruitList往里面放Orange)。为了防止这种情况发生,编译器只能禁止你往fruitList中放入任何对象(即使是Object也不行)。
但是,从fruitList中取出对象却是允许的,因为你知道在这个List中的任何对象至少是Fruit类型,因此编译器允许你这么做。

  • super通配符
public class Fruit {}
public class Apple extends Fruit{}
public class GreenApple extends Apple{}
public class Orange extends Fruit{}
public class SuperTypeWildcards{
	public static void writeTo(List<? super Apple> apples){
		apples.add(new Apple()); //Right
		apples.add(new GreenApple()); // Right
		// apples.add(new Fruit());  ERROR
		
	}
}

通配符super,我们知道apples是Apple类型的引用,Apple是引用类型的下界(可以是Fruit的引用,Apple的引用等),而我们知道Fruit的引用是可以指向它的子类对象的,因此当你往数组中放入GreenApple对象,是可行的。而当我们要放入Fruit的对象时,编译器报错,因为编译器无法确定Fruit引用具体是哪种子类型(可能是Orange),所以认为是不安全的。

  • 无界通配符<?>
public class UnboundedWildCards {
    static List list1;
    static List<?> list2;
    static List<? extends Object> list3;

    static Map<String, ?> map1;
    static Map<?, ?> map2;


    static void assignList(List<?> list){
        list1 = list;
        list2 = list;
        list3 = list;
    }

    static void assignMap(Map<?, ?> map){
        map2 = map;
    }
    
    
//    报both method has same erasure
//    static void assignMap(Map<String, ?> map){
//        map1 = map;
//    }


    public static void main(String[] args){
        assignList(new ArrayList());
        assignList(new ArrayList<String>());
        
        assignMap(new HashMap<String, String>());
    }
}
  • 无界通配符<?>看起来意味着“任何事物”,实际上,它是在声明"我想用java的泛型来编写这段代码,并且泛型可以持有任何类型"。另外,Map<?, ?>中,你还能看到,当你在处理多个泛型参数时,有时允许一个参数是任何类型。
  • 由于泛型参数将擦除到它的第一个边界,因此List看起来等价于List<Object>,而List实际上表示“持有任何Object类型的原生List”,而List<?>表示“具有某种特定类型的非原生List”。
  • 只要使用了原生类型,都会放弃编译期检查。当set一个原生类型时,类型都胡向上转型为Object。而get也是获取一个Object。


13、泛型使用中的问题

  • 任何基本类型都不能作为类型参数

解决之道是使用基本类型的包装类以及Java SE5的自动包装机制。
注意:自动包装机制不能应用于数组。

public <T> T[] parse(T[] arrays){
        return arrays;
}

public void check(){
        Integer[] integers = new Integer[10];
        parse(integers);
        
//        no instance of type T exist so that T[] confirms to int[]
//        int[] ints = parse(integers);  
}
  • 实现参数化接口

一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。

public interface Payable<T> {}
public class Employee implements Payable<Employee> {}
// public class Hourly extends Employee implements Payable<Hourly>{}

Hourly 不能编译通过,因为擦除会将Payable<Employee>和Payable<Hourly>简化为相同的类Payable,这样,Hourly就相当于重复两次地实现相同的接口。

  • 转型和警告

使用带有泛型类型参数的转型或instanceof不会有任何效果。

  • 重载
public class UserList<W, V> {
    void f(List<W> wList){}
    void f(List<V> vList){}
    
}

这段代码是无法编译的,由于类型擦除原因,重载的方法将产生相同的类型签名。
对于类型擦除后,参数列表无法区分的,只能通过参数名不同来区分。

14、自限定类型

  • 自限定
public class BasicHolder<T> {
    private T element;
    public void set (T arg){
        element = arg;
    }
    public T get(){
        return element;
    }
    public void f(){
        System.out.println(element.getClass().getSimpleName());
    }
}
public class SubType extends BasicHolder<SubType> {

    public static void main(String[] args){
        SubType subTypeHolder = new SubType();
        subTypeHolder.set(new SubType());
        SubType subType = subTypeHolder.get();
    }
}

本来BasicHolder能放任意一个类型,而SubType继承于BasicHolder,也能放任何类型。但是通过继承BasicHolder<SubType>,则将SubType限定为只能放SubType类型。
从这个角度出发,我们可以在泛型基类中书写一些功能模板,然后再使用明确类继承泛型基类,并指定基类的明确类型,则能实现共用。这就是自限定类型的本质:基类用导出类替代其参数,泛型基类变成了一种其所有导出类的公共功能的模板,但是这些功能对于其所有参数和返回值,都使用导出类型。

class SelfBounded<T extends SelfBounded<T>>{
	T element;
	SelfBounded<T> set(T arg){
		element = arg;
		return this;
	}
	T get() {return element;}
}
class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{} // Also OK

SelfBounded强制限定泛型参数为自己的子类。

  • 参数协变

自限定类型的价值在于它们可以产生斜变参数类型。

public interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
    void set(T arg);
}
public interface Setter extends SelfBoundSetter<Setter> {
}
public class SelfBoundingAndCovariantArguments {
    void test(Setter s1, Setter s2, SelfBoundSetter sbs){
        s1.set(s2);
//        errors: set(Setter) in SelfBoundSetter<Setter>
//        cannot be applied to (SelfBoundSetter)
//        s1.set(sbs);
    }
}

通过泛型,我们可以使得参数类型随子类型发生变化。

15、动态类型安全
由于java SE5之前的代码存在原生类型(比如List),而我们是有可能向这些代码传递泛型容器的,所以旧式代码仍旧有可能破坏你的容器。(对于旧式原生类型代码,放入泛型时不会报错,只有取出来的时候才会报错)。对此,Java SE5提供了java.util.Collections一组工具,可以使用checkedList(),checkedMap()等检查往容器中放入的参数是否正确,在类型放入的源头就进行检查。

public class Pet {}
public class Cat extends Pet{}
public class Dog extends Pet {}
public class CheckedList {
    static void oldStyleMethod(List probablyDogs){
        probablyDogs.add(new Cat());
    }

    public static void main(String[] args){
        List<Dog> dogs1 = new ArrayList<Dog>();
        oldStyleMethod(dogs1);  // Quietly accepts a cat

        List<Dog> dogs2 = Collections.checkedList(new ArrayList<Dog>(), Dog.class);
        try{
            oldStyleMethod(dogs2);
        }catch (Exception e){
            System.out.println(e);
        }

        List<Pet> pets = Collections.checkedList(new ArrayList<Pet>(), Pet.class);
        pets.add(new Dog());
        pets.add(new Cat());


    }
}
/* Output
java.lang.ClassCastException: Attempt to insert class com.yanguang.Cat element into collection with element type class com.yanguang.Dog
*/

16、异常
由于擦除的原因,将泛型应用于异常是非常受限的。catch语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。

17、潜在类型机制
要编写能够尽可能广泛地应用的代码,我们需要各种途径来放松对我们代码将要作用的类型所作的限制,同时不丢失静态类型检查的好处。而泛型满足这个特点。但是泛型的擦除会要求你指定某个边界类型,以安全地调用代码中的泛型对象上的具体方法。这种限制导致它们必须继承特定的类,或者实现特定的接口。在某些情况下,限定边界的泛型可能会和指定类或接口没有任何区别。
泛型代码典型的使用是在泛型类型上调用少量方法,而具有潜在类型机制的语言只要求实现某个方法子集,而不是某个特定类或接口。因此,实际上一段代码可以声明为:“我不关心你的类型,只要你可以speak()和sit()即可。”由于不要求具体类型,因此代码就更加泛化了。支持潜在类型机制的有python和C++。而java不支持潜在类型。

18、对缺乏潜在类型机制的补偿

  • 反射:通过类的.class对象,你可以假设class对象上有任何方法,并且调用它们,从而达到只关注方法,而不需要关注继承类或实现接口。但是反射将所有的类型检查都放到运行时,因此在许多情况下,并不是我们想要的。
  • 将一个方法应用于序列:假设有一个apply方法,它能够将任何方法应用于某个序列中的对象。怎么实现呢,我们可以用反射加可变参数实现。
public class Apply {
    public static <T, S extends Iterable<? extends T>>
    void apply(S seq, Method f, Object... args){
        try{
            for(T t : seq){
                f.invoke(t, args);
            }
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}
public class Shape {
    public void rotate(){
        System.out.println(this + " rotate.");
    }
    public void resize(int newSize){
        System.out.println(this + " resize " + newSize);
    }
}
public class ApplyTest {
    public static void main(String[] args)throws Exception{
        List<Shape> shapes = new ArrayList<>();
        for(int i = 0; i < 5; i++){
            shapes.add(new Shape());
        }
        Apply.apply(shapes, Shape.class.getMethod("rotate"));
        Apply.apply(shapes, Shape.class.getMethod("resize", int.class), 5);
    }
}

这种解决方法是碰巧在Java中内建了一个由Java容器类库使用的Iterable接口,正由于此,apply()方法可以接受任何实现了Iterable接口的事物。

  • 假如我们并未碰巧拥有正确的接口

现在我们要写一个方法,能为某个类填充集合。我们只知道方法接受一个序列,某个类,还有填充的数量。这次我们需要一个Addable接口,但是这个接口并没有现成的。我们可能会这么编写代码:

public class Fill{
	public static<T> void fill(Collection<T> collection, 
		Class<? extends T> classToken, int size){
		for(int i = 0; i < size; i++){
			collection.add(classToken.newInstance());
		}
	}
}
public class Contract{}
public class FillTest{
	public static void main(String[] args){
		List<Contract> contracts = new ArrayList<Contract>();
		Fill.fill(contracts, Contract.class, 3);
		SimpleQueue<Contract> contractQueue = new SimpleQueue<Contract>();
		// Fill.fill(contractQueue, Contract.class, 3);
	}
}

上述代码,因为Java设计者没有预见到对"Addable"接口的需要,所以我们被限制在Collection继承层次结构中,即使SimpleQueue有一个add方法,它也不能工作。

  • 用适配器仿真潜在类型

我们可以从我们的接口中编写泛化代码,并为想使用我们泛化代码的类实现适配器类,来达到仿真潜在类型的目的。

interface Addable<T> {void add(T t);}
public class Fill2{
	public static <T> void fill(Addable<T> addable,
	Class<? extends T>classToken, int size){
		for(int i = 0; i < size; i++){
			addable.add(classToken.newInstance());
		}
	}
}
public class Coffee{}

现在我们要让Coffee使用Fill2填充它的序列对象,编写适配器类

public class AddableCollectionAdapter<T> implements Addable<T>{
	private Collection<T> c;
	public AddableCollectionAdapter(Collection<T> c){
		this.c = c;
	}
	pubic void add(T item){
		c.add(item);
	}
}

public class Fill2Test{
	List<Coffee> carrier = new ArrayList<Coffee>();
	Fill2.fill(
		new AddableCollectionAdapter<Coffee>(carrier),
		Coffee.class, 4);
	for(Coffee coffee : carrier){
		System.out.println(coffee);
	}
}
posted @ 2017-06-17 12:42  kejicjk  阅读(210)  评论(0)    收藏  举报