Java 泛型(上)

为什么需要

场景

开发一个能够存储各种类型对象(比如:String 对象、Integer 对象等)的容器(容器就是存储数据的,可以是对象,可以是数组等等)。

解决方案

在 JDK 1.5 之前

在 JDK 1.5 之前是没有泛型的,最好的办法是开发一个能够存储和检索 Object 类型本身的容器,然后再将该对象用于各种类型时进行类型转换。

  public class ObjectContainer {
      public Object obj;
  }

虽然这个容器会达到预期效果,但就我们的目的而言,它并不是最合适的解决方案。它不是类型安全的(Java 的编译器对于类型转换的错误是检测不到的,在运行时执行到 checkcast这个字节码指令时,如果类型转换错误才会抛出 ClassCastException ),并且要求在检索封装对象时使用显式类型转换(向下转型),因此有可能引发运行时异常。

测试方法

  public static void main(String[] args) {
      ObjectContainer myObj = new ObjectContainer();
      // 这里发生向上转型
      myObj.obj = "Test";       
      // 检索封装对象,发生向下转型
      String myStr = (String) myObj.obj; 
      System.out.println("myStr: " + myStr);
  }

注意:一个面试点,发生向下转型的必要条件是先发生向上转型

从 JDK 1.5 开始

从 JDK 1.5 开始出现了泛型,使用泛型可以很好的解决我们的场景需求。在实例化时为所使用的容器分配一个类型,也称泛型类型,这样就可以创建一个对象来存储所分配类型的对象。泛型类型可以看成一种弱类型(类似于 js 中的 var,定义的时候你可以随便定义,使用的时候就需要给出具体类型),这意味着可以通过执行泛型类型调用分配一个类型,将用分配的具体类型替换泛型类型。然后,所分配的类型将用于限制容器内使用的值,这样就无需进行类型转换,还可以在编译时提供更强的类型检查

  // 根据这个代码来看,泛型类型就是 T。泛型类型也可以称为泛型形参。
  public class ObjectContainer<T>{
      public Object obj;
  }

测试方法

代码里的注释很重要!很重要!很重要!建议多看几遍。

  public static void main(String[] args) {
      // 执行泛型类型调用分配 String 这个具体类型,String 也可以称为泛型实参。
      ObjectContainer<String> myObj = new ObjectContainer<String>();
      myObj.obj = "ypf";
      // 这里不需要类型转型,因为通过执行泛型类型调用分配了 String 这个类型,编译器会帮我们去生成一个 checkcast 字节码指令来做类型转换。也就是说我们以前需要手动去做的事(类型转型),现在编译器帮我们做了。其实泛型也可以看成是 Java 的一种语法糖。
      String myStr = myObj.obj;
      System.out.println("myStr: " + myStr);
  }

从上面的类中我们已经知道了泛型类型就是 T。在这个测试方法中执行泛型类型调用对应的代码就是第 2 行,可以看到我们泛型类型 T 在执行时分配了 String 这个类型。其实执行泛型类型调用就是在写类的时候把 ClassName<T> 写成 ClassName<具体的类型>,也就是说把泛型类型替换成具体的类型。

优点

  • 更强的类型检查,避开运行时可能引发的 ClassCastException ,可以节省时间。
  • 消除了类型转换。
  • 开发泛型算法。(可以多去看看 Java 集合中是怎么利用泛型的)

怎么用

泛型类

  public class GenericClass<T>{ 
      // key 这个成员变量的类型为 T,T 的类型由外部使用时指定。  
      private T key;
  
      public Generic(T key) { 
          this.key = key;
      }
  
      public T getKey(){ 
          return key;
      }
  }

泛型接口

定义一个泛型接口

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

定义实现泛型接口的类

  // 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。
  public class FruitGenerator<T> implements Generator<T>{
      // 使用泛型类型 T。
      @Override
      public T next() {
          return null;
      }
  }
  // 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型。
  public class FruitGenerator implements Generator<String> {
  
      private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
  
      // 泛型类型 T 被替换成分配的类型 String
      @Override
      public String next() {
          Random rand = new Random();
          return fruits[rand.nextInt(3)];
      }
  }

泛型方法

当我们只想在某个方法中使用泛型而并不需要给整个类加个泛型时,可以使用泛型方法来指定一个泛型类型。泛型方法的泛型类型完全独立于类,也就是说可以与泛型类中声明的 T 不是同一种类型。通过下面的代码来验证这个结论。

  public class GenericTest<T> {
      public T t;
  
      public static <T> T genericMethod(Class<T> clazz)throws InstantiationException ,IllegalAccessException{
          T instance = clazz.newInstance();
          return instance;
      }
  
      public static void main(String[] args) throws IllegalAccessException, InstantiationException {
          GenericTest g = new GenericTest();
          g.t = "666";
          System.out.println(g.t);
          GenericTest g2 = genericMethod(GenericTest.class);
          System.out.println(g2);
      }
  }

输出结果

从输出结果可以看出 GenericTest 类的泛型类型 T 为 String,genericMethod 方法的泛型类型 T 为 GenericTest 。

泛型方法和可变参数灵活使用

通过泛型方法和可变参数,我们可以 new 出任何类型的数组。这样我就很方便创建一个数组,其实在底层实现上是编译器帮我们去 new 数组这个操作了。

  public class GenericTest<T> {
      // 巧妙利用语言的特性。多去了解一下语言的特性,利用起来。
      public static <T> T[] of(T... array){
          return array;
      }
  
      public static void main(String[] args)  {
          Integer[] numArr = of(1,2,3,4);
          String[] strArr = of("y", "p", "f");
          System.out.println(Arrays.toString(numArr));
          System.out.println(Arrays.toString(strArr));
      }
  }

泛型通配符

泛型类型是没有继承关系的。

  public class GenericTest<T> {
      public T t;
  
      public GenericTest(T t) {
          this.t = t;
      }
  
      public static void showKeyValue(GenericTest<Number> obj){
          System.out.println(obj.toString());
      }
  
      public static void main(String[] args)  {
          GenericTest<Integer> gInteger = new GenericTest<Integer>(123);
          GenericTest<Number> gNumber = new GenericTest<Number>(456);
          showKeyValue(gNumber);
          // 下面这个会报错,编译不通过。因为泛型没有继承这个说法。
          // GenericTest<Integer> 和 GenericTest<Number> 经过泛型擦除后都变为 GenericTest。
  //      showKeyValue(gInteger);
      }
  }

解决方案

当操作类型时,不需要使用类型的具体功能时,只使用 Object 类中的功能。那么可以用 ? 通配符来表未知类型。?和 Number、String、Integer 一样都是一种被分配的具体类型,可以把?看成所有类型的父类来理解(也可以把这个看成 Java 语言的一种规范)。

  public class GenericTest<T> {
      public T t;
  
      public GenericTest(T t) {
          this.t = t;
      }
  
      // 使用泛型通配符,可以接收任意类型的泛型类。
      public static void showKeyValue(GenericTest<?> obj){
          System.out.println(obj.toString());
      }
  
      public static void main(String[] args)  {
          GenericTest<Integer> gInteger = new GenericTest<Integer>(123);
          GenericTest<Number> gNumber = new GenericTest<Number>(456);
          showKeyValue(gNumber);
          showKeyValue(gInteger);
      }
  }

泛型上下边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

  • 为泛型类型添加上边界,即传入的类型实参必须是指定类型的子类型。
  • 为泛型类型添加下边界,即传入的类型实参必须是指定类型的父类型。

泛型上下边界这块具体怎么使用在下次分析时介绍。

注意事项

泛型类型不可以是基本类型,只能是类。

泛型类型没有继承关系。

不能对确切的泛型类型使用 instanceof 操作。

不可以创建一个确切的泛型类型的数组,但是可以声明泛型数组。

底层实现

下次分析时从字节码角度来讲解。

参考链接:https://blog.csdn.net/s10461/article/details/53941091

原文发布于微信公众号 - Java知其所以然(gh_37a1335e2608)

原文发表时间:2019-01-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券