泛型
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);
}
}

浙公网安备 33010602011771号