美文网首页
第十五章:泛型

第十五章:泛型

作者: MAXPUP | 来源:发表于2017-12-07 16:56 被阅读0次

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

简单泛型

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

相关文章

  • 《JAVA编程思想》学习笔记:第15章(泛型)

    第十五章、泛型 泛型(generics)的概念是Java SE5的重大变化之一。泛型实现了参数化类型(parame...

  • effective java 第五章 (笔记)

    第5章 泛型 java 1.5增加了泛型。 *** 第23条:请不要在新代码中使用原生态类型 *** 泛型类和接口...

  • 泛型 & 注解 & Log4J日志组件

    掌握的知识 : 基本用法、泛型擦除、泛型类/泛型方法/泛型接口、泛型关键字、反射泛型(案例) 泛型 概述 : 泛型...

  • 【泛型】通配符与嵌套

    上一篇 【泛型】泛型的作用与定义 1 泛型分类 泛型可以分成泛型类、泛型方法和泛型接口 1.1 泛型类 一个泛型类...

  • 泛型的使用

    泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法 泛型类 泛型接口 泛型通配符 泛型方法 静态方法与...

  • Java 泛型

    泛型类 例如 泛型接口 例如 泛型通配符 泛型方法 类中的泛型方法 泛型方法与可变参数 静态方法与泛型 泛型上下边...

  • 第8章 泛型程序设计

    第8章 泛型程序设计 使用泛型要比直接使用Object,让后进行强制类型转换要安全 8.1 为什么要使用泛型程序设...

  • 探秘 Java 中的泛型(Generic)

    本文包括:JDK5之前集合对象使用问题泛型的出现泛型应用泛型典型应用自定义泛型——泛型方法自定义泛型——泛型类泛型...

  • Web笔记-基础加强

    泛型高级应用 自定义泛型方法 自定义泛型类 泛型通配符? 泛型的上下限 泛型的定义者和泛型的使用者 泛型的定义者:...

  • Kotlin学习 8 -- 泛型的高级特性

    本篇文章主要介绍以下几个知识点:对泛型进行实化泛型实化的应用泛型的协变泛型的逆变内容参考自第一行代码第3版 1. ...

网友评论

      本文标题:第十五章:泛型

      本文链接:https://www.haomeiwen.com/subject/lallixtx.html