【Java】泛型

概述:

在Java5以前,普通的类和方法只能使用特定的类型:基本数据类型或类类型,如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。

Java5的一个重大变化就是引入泛型,泛型实现了参数化类型,使得你编写的组件(通常是集合)可以适用于多种类型。泛型的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。然而很快你就会发现,Java泛型并没有你想的那么完美,甚至存在一些令人迷惑的实现。

泛型类:

促成泛型出现的最主要的动机之一就是为了创建集合类,集合用于存放要使用到的对象。现在一个只能持有单个对象的类:

class Automobile{}

public class Holder1{
    private Automobile a;
    public Holder1(Automobile a) { this.a = a; }
    Automobile get() {return a;}
}

如果没有泛型,那么就必须明确指定其持有的对象的类型,会导致该复用性不高,它无法持有其他类型的对象,我们当然不希望为每个类型都编写一个新类。

在Java5以前,为了解决这个问题,我们可以让这个类直接持有Object类型的对象,这样就可以持有多种不同类型的对象了,但通常而言,我们只会用集合储存同一类型的对象。泛型的主要目的之一就是用来约定集合要储存什么类型的对象,并且通过编译器确保规约得以满足

所以,与其使用Object,我们更希望先指定一个类型占位符,稍等再决定具体使用什么类型。由此我们需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数。

public class GenericHolder<T>{
   private T a;
   public GenericHolder(){}
   public void set(T a){this.a = a; }
   public T get() {return a; }

   public static void main(String[] args){
   //在Java7中右边的尖括号可以为空
   GenericHolder<Automobile> h2 = new GenericHolder<Automobile>();
   GenericHodler<Automobile> h3 = new GenericHolder<>();
   h3.set(new Automobile());//此处有类型校验
   Automobile a = h3.get();//无需类型转换
   //-h3.set("Not an Automobile");//报错
  }
}

元组类库:

有时一个方法需要能返回多个对象,而return语句只能返回单个对象,解决的方法就是创建一个对象,用它来打包想要返回的多个对象。元组的概念正是基于此,元组将一组对象直接打包存储于单一对象中,可以从该对象读取其中的元素,却不允许向其中存储新对象(这个概念也称数据传输对象或信使)

元组可以具有任意长度,元组中的对象可以是不同类型的,我们希望能为每个对象指明类型,这时泛型就派上用场了。例如下面是一个可以存储两个对象的元组:

public class Tuple<A,B>{
  public final A a1;
  public final B a2;
  public Tuple(A a,B b){a1 = a; a2 = b;}
  public String rep() {return a1 + "," + a2; }

  @Override
  public String toString(){
     return "(" + rep() + ")" ;
  }
}

使用final修饰成员变量可以保证其不被修改,如果用户想储存不同的元素,那么就必须创建新的Tuple对象。当然也可以允许用户重新对a1,a2赋值,但无疑前一种形式会更加安全。

利用继承机制可以实现长度更长的元组:

public class Tuple3<A,B,C> extends Tuple2<A,B> {
   public final C a3;
   public Tuple3(A a,B b,C c){
       super(a,b);
       a3 = c;
   }  
@Override
public String rep(){
   return super.rep() + "," + a3;
   }
}

泛型方法:

到目前为止,我们已经研究了参数化整个类,其实还可以参数化类中的方法。类本身是否是泛型,与它的方法是否是泛型并没有什么直接关系。我们应该尽可能使用泛型方法,通常将单个方法泛型化要比将整个类泛型化要更加清晰易懂

要定义泛型方法,请将泛型参数列表放置在返回值之前:

public class GenericMethods{
  public <T> void f(T x){
    System.out.println(x.getClass().getName());
  }
  
  public static void main(String[] args){
  GenericMethods gm = new GenericMethods();
  gm.f("");
  gm.f(1);
  gm.f(1.0);
  gm.f(1.0F);
  gm.f('c');
  gm.f(gm);
  }
}

使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型,这称为类型参数推断,因此,对f()的调用看起来像普通的方法调用,而且像是被重载了无数次一样。

泛型擦除

当你开始深入研究泛型时,你会发现一个残酷的现实:在泛型代码内部,无法获取任何有关泛型参数类型的信息:

class Frob{}
class Fnorkle{}
class Quark{}
class Particle<PISITION,MOMENTUM>{}

public class LostInformation {
   List<Frob> list = new ArrayList<>();
   Map<Frob,Fnorkle> map = new HashMap<>();
   Quark<Fnorkle> quark = new Quark<>();
   Particle<Long,Double> p = new particle<>();

   System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
   System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
   System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
   System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
  } 
}

正如上例中输出所示,你只能看到用作参数占位符的标识符,这并非有用的信息。Java泛型是使用擦除实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象,因此List和List在运行时实际上是相同的类型,它们都被擦除成原生类型List。

再来看一个例子:

class Manipulator<T>{
   private T obj;

   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<>(hf);
      manipulator.manipulate();
   }
}

因为擦除,Java编译器无法将manipulate()方法能调用obj的f()方法这一需求映射到HasF具有f()方法这个事实上。为了调用f(),我们必须协助泛型类,为泛型类给定一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了extends关键字。由于有了边界,下面的代码就能通过编译:

public class Manipulator2<T extends HasF>{
  private T obj;

  Manipulator2(T x){
    obj = x;
  }
  public void manipulate(){
    obj.f();
  }
}

边界声明T必须是HasF类型或其子类。如果情况确实如此,就可以安全地在obj上调用f()方法。泛型类型参数会擦除到它第一个边界(可能有多个边界,稍后你将看到)。我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例,T擦除到了HasF,就像在类的声明中用HasF替换了T一样。如果我们愿意,完全可以把上例的T替换成HashF,效果也是一样的,那么泛型的意义又何在呢?

这提出了很重要的一点:泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”,代码能跨多个类工作时才有用。因此,使用类型参数通常比简单的声明类更加复杂。但是不能因此认为使用形式就是有缺陷的。你必须查看所有的代码,从而确定代码是否复杂到必须使用泛型的程度。

有关泛型擦除的困惑,其实是 Java 为实现泛型的一种妥协,因为泛型并不是 Java 语言出现时就有的。擦除减少了泛型的泛化性,泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, List 这样的类型注解会被擦除为 List,普通的类型变量在未指定边界的情况下会被擦除为 Object

在 Java5 以前编写的类库是没有使用泛型的,而作者可能打算重新用泛型编写,或者根本不打算这样做。Java 设计者们既要保证旧代码和类文件依然合法,还得考虑当某个类库变为泛型时,不会破坏依赖于它的代码和应用。Java 设计者们最终认为泛型是唯一可行的解决方案,擦除使得向泛型的迁移成为可能,为了实现非泛型的代码和泛型代码共存,必须将某个类库使用了泛型这样的“证据”擦除。

基于上述观点,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而言。因为擦除,我们无法在运行时知道确切的类型,为了补偿擦除带来的弊端,我们可以为所需的类型显示传递一个 Class 对象,以在类型表达式中使用它。

class Building{}
class House extends Building{}

public class ClassTypeCapture<T>{
    Class<T> kind;
    
    public ClassTypeCapture(Class<T> kind){
         this.kind = kind;
    }
    public boolean f(Object arg){
         return kind.isInstance(arg);
    }
    public static void main(String[] args){
         ClassTypeCapture<Building> ctt1 = 
                 new ClassTypeCapture<>(Building.class);
         System.out.println(ctt1.f(new Building()));
         System.out.println(ctt1.f(new House()));
         ClassTypeCapture<House> ctt2 =
                 new ClassTypeCapture<>(House.class);
         System.out.println(ctt2.f(new Building()));
         System.out.println(ctt2.f(new House())); 
    }
}

边界和通配符

由于擦除会删除类型消息,因此唯一可用于无限制泛型参数的方法是那些Object可用的方法。边界允许我们对泛型使用的参数类型施以类型,将参数限制为某类型的子集,那么就可以调用孩子集中的方法。为了应用约束,Java泛型使用了extends关键字。

class Coord{
    public int x,y,z;
 }
 interface Weight{
     int weight();
 }
 class Solid<T extends Coord & Weight>{
      T item;
      Solid(T item){
        this.item = item;
      }
      T getItem(){
        return item;
      }
      int getX(){
        return item.x;
      }
      int getY(){
        return item.y;
      }
      int getZ(){
        return item.z
      }
      int weight(){
        return item.weight();
      }
}
class Bounded extends Coord implements weight{
     @Override
     public int weight(){
        return 0;
     }
}
public class BasicBounds{
    public static void main(String[] args){
          Solid<Bounded> solid = new Solid<>(new Bounded());
          solid.getY();
          solid.weight();
     }
}
          

引用通配符可以在泛型实例时更加灵活地控制,也可以在方法中控制方法地参数,具体语法如下:

  1. ?extends T:表示T或T的子类
  2. ?super T:表示T或T的父类
  3. ?:表示可以是任意类型

值得注意的问题:

在这里主要阐述在使用Java泛型时出现的各类问题:

1. 任何基本数据类型不能作为类型参数
Java 泛型的限制之一是不能将基本类型用作类型参数。因此,不能创建 ArrayList 之类的东西。 解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 ArrayList,并将基本类型 int 应用于这个集合,那么你将发现自动装箱机制将自动地实现 int 到 Integer 的双向转换,这几乎就像是有一个 ArrayList 一样。

2. 实现参数化接口
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。下面是产生这种冲突的情况:

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

Hourly 不能编译,因为擦除会将 Payable 和 Payable 简化为相同的类 Payable,这样,上面的代码就意味着在重复两次地实现相同的接口。十分有趣的是,如果从 Payable 的两种用法中都移除掉泛型参数(就像编译器在擦除阶段所做的那样)这段代码就可以编译。

3. 转型和警告
使用带有泛型类型参数的转型不会有任何效果,例如:

class Storage<T>{
     private Object obj;
     Storage(){
     obj = new Object();
     }
     @SuppressWarnings("unchecked")
     public T pop(){
         return (T)obj;
     }
}

public class GenericCast{
   public static void main(String[] args){
       Storage<String> storage = new Storage<>();
       System.out.println(storage.pop());
       }
}

如果没有 @SuppressWarnings 注解,编译器将对 pop() 产生 “unchecked cast” 警告。由于擦除的原因,编译器无法知道这个转型是否是安全的,并且 pop() 方法实际上并没有执行任何转型。 这是因为,T 被擦除到它的第一个边界,默认情况下是 Object,因此 pop() 实际上只是将 Object 转型为 Object

4. 重载
下面的程序是不能编译的,因为擦除,所以重载方法产生了相同的类型签名。

public class UseList<W,T>{
     void f(List<T> v){}
     void f(List<W> v){}
}

添加链接描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值