浅析面向对象之泛型

66 阅读4分钟

文章目录


1. 引入

泛型最早出现在JDK1.5中,在此之前定义List、Set等容器时,如果明确知道要存储什么类型的数据,那么直接指定容器的类型即可。而大多数情况下并不知道未来会存放哪种类型数据,而且希望它可以根据具体存储的数据类型确定,而不是只能存储一种类型。

我们知道,Object是所有类的超类,那么直接将容器定义成Object类型,那么不就可以在后面存放任何类型的数据了吗?实际上,JDK1.5之前的容器定义中就是这样做的。但是这样做有一些不足之处:

  • 任何类型的数据都可以添加到容器中,会有类型不安全问题
  • 如果保存的不是Object类型,而是其他类型,那么每次读取时都需要进行强转,不免过于繁琐

而泛型的引入巧妙的解决了上述的问题,而且使得代码更加的简洁与健壮。


2. 概念

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者某个方法的返回值及参数类型。而这个类型参数只有该类被使用时才被真正确定,例如继承或实现该接口、类实例化等操作。

JDK1.5之后所有的集合类都被定义为泛型形式,只有在使用该类时才需要指定具体的类型。例如ArrayList的定义为:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

HashMap的定义如下:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {}

那么在实际使用集合时需要指定具体的类型,表示我们希望集合应该存放什么类型的数据。例如创建只存放String类型的List:

List<String> list = new ArrayList<>();

或者String-Integer键值对类型的Map:

Map<String, Integer> map = new HashMap<>();

3. 自定义泛型

除了直接使用JDK为我们提供的多种泛型容器外,我们同样可以根据自己的需要自定义泛型结构,常用来定义的结构有:

  • 泛型类
  • 泛型接口
  • 泛型方法

下面通过一个例子来看一下如何自定义泛型结构。假设我们需要一个类做到使用数组实现栈的功能,但是未来操作的具体类型数据而未知的,那么泛型就是一个很好的工具。具体的定义如下:

class Array2Stack<T>{
      private T[] array;
      private int size;

      public Array2Stack() {
      }

      public Array2Stack(T[] array) {
          this.array = array;
          this.size = 0;
      }


      // 入栈
      public void push(T x){
          if (this.size == this.array.length){
              throw new ArrayIndexOutOfBoundsException("the stack is full!");
          }

          this.array[size++] = x;
      }
      // 出栈
      public <T> T pop(){
          if (isEmpty()){
              throw new ArrayIndexOutOfBoundsException("the stack is empty");
          }

          return this.array[--size];
      }
      // 取栈顶元素
      public <T> T top(){
          if (isEmpty()){
              return null;
          }
          return this.array[size];
      }

      // 判空
      public boolean isEmpty(){
          boolean flag = this.size == 0 ? true : false;
          return flag;
      }
  }

上面的代码中不仅定义了泛型类,同样定义了泛型方法,其中使用T表示泛型。当然使用其他任何的字母都是可以的,这里只是一种约定,将来在使用时,具体的额类型会替换掉T。

那么在使用该类时,就需要为它指定具体的类型,例如实现Integer类型的栈:

public static void main(String[] args) {
      int size = 10;
      Integer [] array = new Integer[size];
      Array2Stack<Integer> stack = new Array2Stack<>(array);

      // 入栈
      stack.push(1);
      stack.push(2);
      stack.push(3);

      // 出栈
      while(!stack.isEmpty()){
          System.out.println(stack.pop()); // 3, 2, 1
      }
}

或者指定栈类型为String:

public static void main(String[] args) {
      String[] arr = new String[size];
      Array2Stack<String> stack = new Array2Stack<>(arr);
      stack.push("Forlogen");
      stack.push("kobe");

      while(!stack.isEmpty()){
          System.out.println(stack.pop()); // kobe, Forlogen
      }
  }

不管是指定为什么类型,使用时一定要指定一个具体的类型。对于int、float、boolean的基本类型来说,实例化泛型类时要使用对应的包装类,不能直接使用基本类型。

在自定义泛型结构时需要注意以下几点:

  • 泛型不同的引用之间不能相互赋值

  • 泛型如果不指定将被擦除,泛型对应的类型均按照Object处理,但不等价于Object

  • 异常类不能是泛型的

  • 如果存在继承关系,假设父类有泛型,那么子类可以选择保留泛型也可以选择指定泛型类型,还可以增加自己的泛型。例如父类定义如下:

    class Person<T1, T2>(){}
    

    那么子类在继承父类时,下面的定义都是合法的:

    // 不保留任何泛型
    class Student<E1, E2> extends Person{}
    
    // 指定具体类型,同时创建自己的泛型
    class Student<E1, E2> extends Person<Integer, String>{}
    
    // 保留全部泛型的同时,创建自己的泛型
    class Student<T1, T2,E1, E2> extends Person<T1, T2>{}
    
    // 部分保留泛型的同时,创建自己的泛型
    class Student<T2, E1, E2> extends Person<Integer, T2>
    

4. 通配符

在使用泛型类的过程中,通常写作如下的形式:

Map<String> map = new HashMap<>();

如果单独定义Map变量map时不指定具体的类型,写成下面的形式是不合法的:

Map<> map = null;

但如果就是想定义一个Map类型的变量,而不指定具体的类型,那么就需要使用通配符。通配符使用?表示,例如定义成Map<?> map = null;就是一种合法的写法。但是使用通配符定义的Map或是List,不能向其中添加任何类型的元素,因为此时并不知道它可以接受什么类型的数据;但可以从中取值,取到的值都是Object类型。

既然不能添加元素,那么通配符有啥用呢?虽然单纯的使用?看起来很鸡肋,但是使用有限制的通配符就很有用了。例如:

  • 通配符指定上限:extends,使用时指定的类型必须是继承某个类或是实现某个接口
  • 通配符指定下限:super,使用时指定的类型不能小于操作的类

比如说:

  • <? extends Number>只允许Number即它的子类的引用调用
  • <? super Number>只允许泛型为Number及Number父类的引用调用
  • <? extends Comparable>只允许泛型为实现Comparable接口的实现类的引用调用