第十五章:泛型

泛型实现了参数化类型的概念。

简单泛型

容器是出现泛型的重要原因之一。泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。因此我们需要使用类型参数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);
    }
}

  1. 元祖类库:
    一般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;
  }
}

以上元组可以通过继承实现更长的元组。

  1. 一个堆栈类
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 整洁,同时不会删除有用的信息。

泛型会出现的各种问题

  1. 任何基本类型都不能作为类型参数
    解决方法时Java的自动包装机制。但自动包装机制不能作用于数组。
  2. 实现参数化接口
    一个类不能实现同一泛型接口的两种辩题,因为擦除的存在,两个变体会成为相同的接口。
interface Paybale<T> {}
class Employee implements Payable<Employee>{}
class Hourly extends Employee implements Payable<Hourly>{}
//Hourly不能编译,
//但是如果从两个class都移除泛型参数(像编译器擦  除阶段做的那样)就可以编译了。
  1. 转型和警告
    使用带有泛型类型参数的转型和instanceof不会有任何效果,其实只是将Object转型为Object。有时,泛型转型会产生不恰当的警告。可以通过泛型类来转型:
List<Widget> lw = List.class.cast(in.readObject());
  1. 重载
    由于擦除的原因,重载方法将产生相同的类型前面。
//不会被编译
public class UseList<W, T>{
  void f(List<T> v){}
  void f(List<W> v){}
}
  1. 基类劫持了接口
    一旦父类确定了类型参数,子类就不能使用其他类型参数了,即时是类型窄化也不能。

自限定的类型

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

混型

  1. Mixin
  2. 装饰器模式
  3. 动态代理实现方式

潜在类型机制

也叫鸭子类型机制,值要求实现某个方法子集,而不是某个特定类或接口,从而放松了限制,产生更加泛化的代码。java不支持这种机制。

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

  1. 反射
    反射将所有类型检查转移到了运行时。
  2. 使用适配器仿真潜在类型机制
  3. 将函数对象用作策略
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容

  • 2.简单泛型 -********Java泛型的核心概念:告诉编译器想使用什么类型, 然后编译器帮你处理一切细节 2...
    CodingHou阅读 388评论 0 0
  • object 变量可指向任何类的实例,这让你能够创建可对任何数据类型进程处理的类。然而,这种方法存在几个严重的问题...
    CarlDonitz阅读 908评论 0 5
  • 第8章 泛型 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么是自定义的类。但是在集合类的场...
    光剑书架上的书阅读 2,143评论 6 10
  • 本文大量参考Thinking in java(解析,填充)。 定义:多态算是一种泛化机制,解决了一部分可以应用于多...
    谷歌清洁工阅读 456评论 0 2
  • 泛型是Java 1.5引入的新特性。泛型的本质是参数化类型,这种参数类型可以用在类、变量、接口和方法的创建中,分别...
    何时不晚阅读 3,024评论 0 2