前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊聊JDK泛型那些事儿

聊聊JDK泛型那些事儿

作者头像
孟君
发布2019-08-28 15:13:29
3360
发布2019-08-28 15:13:29
举报

在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。 

泛型的好处在编译的时候检查类型安全,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会

本篇博文将从如下几个方面入手,简述一下Java泛型的一些事儿:

  1. 泛型的修饰范围
  2. 使用&实现多重限制
  3. 类型擦除
  4. <? super T>, <? extends T>, <?>通配符的使用

一、泛型的修饰范围

泛型可以修饰接口,修改类,修饰方法。下面给出几个例子:

1.1 泛型接口示例

代码语言:javascript
复制
public interface List<E> extends Collection<E> {
   ...
  
   Iterator<E> iterator();
  
   boolean add(E e);
  
   boolean addAll(Collection<? extends E> c);
  
   ...
}

1.2 泛型类示例

代码语言:javascript
复制
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    ...
    public Iterator<E> iterator() {
        return new Itr();
    }

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    ...

    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
    
    ...
}

1.3 泛型方法

泛型方法语法:

[访问权限修饰符][static][final]<类型参数列表>返回值类型 方法名([形式参数列表]) ,其中,[]内的内容是可选的。

下面举几个Collections工具类中的几个泛型方法的例子:

代码语言:javascript
复制
public static <T> void sort(List<T> list, Comparator<? super T> c) {
      Object[] a = list.toArray();
      Arrays.sort(a, (Comparator)c);
      ListIterator i = list.listIterator();
      for (int j=0; j<a.length; j++) {
          i.next();
          i.set(a[j]);
      }
  }
代码语言:javascript
复制
 public static <T extends Comparable<? super T>> void sort(List<T> list) {
      Object[] a = list.toArray();
      Arrays.sort(a);
      ListIterator<T> i = list.listIterator();
      for (int j=0; j<a.length; j++) {
          i.next();
          i.set((T)a[j]);
      }
  }
代码语言:javascript
复制
public static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp) {
        if (comp==null)
            return (T)min((Collection<SelfComparable>) (Collection) coll);

          Iterator<? extends T> i = coll.iterator();
          T candidate = i.next();
        
                while(i.hasNext()) {
              T next = i.next();
              if (comp.compare(next, candidate) < 0)
            candidate = next;
          }
      return candidate;
}

二、使用&实现多重限制

如果一个类型有多个限制条件,可以使用&实现多重限制

代码语言:javascript
复制
    public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll) {
        Iterator<? extends T> i = coll.iterator();
        T candidate = i.next();

        while (i.hasNext()) {
            T next = i.next();
            if (next.compareTo(candidate) < 0)
                candidate = next;
        }
        return candidate;
    }
代码语言:javascript
复制
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {
        Iterator<? extends T> i = coll.iterator();
        T candidate = i.next();

        while (i.hasNext()) {
            T next = i.next();
            if (next.compareTo(candidate) > 0)
                candidate = next;
        }
        return candidate;
 }

三、类型擦除

类型擦除(type erasure)。 Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除

比如:

代码语言:javascript
复制
import java.util.List;

public class TypeErasureTest {

  public void test(List<String> ls) {
    System.out
        .println("Mthod test(List<String> ls) is calling.");

  }

  public void test(List<Integer> ls) {
    System.out.println("Mthod test(List<Integer> ls) is calling.");
  }
}

这段代码无法编译通过

在编译后泛型类型是会被擦除的,在这个重载的例子中,因为参数List<Integer>和 List<String>编译之后都被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两个方法的特征签名一样,这样两个相同的方法将不能满足重载,最后导致编译失败。

在编译后所有的泛型类型都会做相应的转化:

  • List<String>, List<Integer>, List<T>擦除后的类型为List
  • List<String>[] 擦除后的类型为List[]
  • List<? extends E>, List<? super E> 擦除后的类型为List<E>
  • List<T extends Serialiable & Clonable> 擦除后为List<Serialiable>

四、<? super T> , <? extends T> , <?> 通配符的使用

Java泛型支持通配符, 可以单独使用 '?' 来表示任意类型, 也可以使用extends关键字表示某一个类或接口的子类, 也可以使用super关键字表示某一个类,接口的父类型。

接下来,我们给出一些例子,并小结一下什么时候该使用extends 和 super。

4.1 <? super T>使“读”受到限制

先来看一个例子,比如,要实例化一个List<? super Integer>的numberList ,我们可以使用Integer, NumberObject来完成。

代码语言:javascript
复制
List<? super Integer> numberList = new ArrayList<Integer>();  
List<? super Integer> numberList = new ArrayList<Number>();   
List<? super Integer> numberList = new ArrayList<Object>();

从这个例子中可以看出,numberList可能是指向List<Integer>, 可能指向List<Number>, 也可能指向List<Object>, 这样多可能性将会限制“读”操作。

因为:

  • 我们并不能保证读到的是Integer,因为numberList可能指向List<Number>或者List<Object>。
  • 我们并不能保证读到的是Number,因为numberList可能指向List<Object>。
  • 唯一能保证的的就是我们将得到一个Object或者是Object的子类的一个实例,但是我们并不知道具体的子类是什么。

如有有这样的一个get方法,使用了List<? super T>:

代码语言:javascript
复制
private static <T> T get(List<? super T> list, int index) {
    
 }

那么,我们怎么知道list中存放的内容是什么类型呢? 我们只能知道是T或者T的父类,仅此而已。但T的父类具体是什么,不得而知了。

“读”操作不能使用<? super T>, 而应该使用<? extends T>,

让我们来看看java.util.Collections类中的一些方法吧。

代码语言:javascript
复制
    /**
     * Gets the ith element from the given list by repositioning the specified
     * list listIterator.
     */
    private static <T> T get(ListIterator<? extends T> i, int index) {
        T obj = null;
        int pos = i.nextIndex();
        if (pos <= index) {
            do {
                obj = i.next();
            } while (pos++ < index);
        } else {
            do {
                obj = i.previous();
            } while (--pos > index);
        }
        return obj;
    }

4.2 <? extedns T>使“写”受到限制

一个List<? extends Number>的numberList可能指向List<Number>, 可能指向List<Integer>, 也可能指向List<Object>。

代码语言:javascript
复制
List<? extends Number> numberList = new ArrayList<Number>();
List<? extends Number> numberList = new ArrayList<Integer>();
List<? extends Number> numberList = new ArrayList<Double>();

这种多可能性将让<? extends T> 的“写”操作受到限制。

因为,

  • 我们不能添加一个Integer类型的值,因为numberList可能指向List<Double>
  • 我们不能添加一个Double类型的值,因为numberList可能指向的是List<Integer>
  • 我们不能添加一个Number类型的值,因为numberList可能指向的是List<Integer>

我们不能添加任何对象到List<? extends T>, 那是因为我们并不能保证实际指向的是什么类型的List,所以也就不能保证想要添加的对象是List所允许的类型。

唯一能保证的是只能读取并得到一个T或者是T的子类。

上面的分析,我们可以得出一个结论, 那就是<? extends T> 不适合“写”操作,<? super T> 不适合“读”操作

其实,

Collections中的copy方法很好的使用<? extends T> 和 <? super T>的经典案例。

另外还有一个PECS原则供参考: PECS原则-->

在 Collections#copy方法中,src (the producing list)使用extends, 而 desc (the consuming list) 使用super. 代码如下:

代码语言:javascript
复制
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

4.3 通配符<?>的使用

List<?>表示的是任意类型。因为编译器不知道List中容纳的是什么类型的元素,所以不能对其进行增加,修改的操作。 但是,List<?>拥有删除的功能,因为这些功能与泛型类型没有关系。

所以,List<?>适合用于与泛型类型无关的方法,比如remove, shuffle等。

我们来看看Collections中的几个方法吧:

代码语言:javascript
复制
public static void shuffle(List<?> list, Random rnd) {
        int size = list.size();
        if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
            for (int i=size; i>1; i--)
                swap(list, i-1, rnd.nextInt(i));
        } else {
            Object arr[] = list.toArray();

            // Shuffle array
            for (int i=size; i>1; i--)
                swap(arr, i-1, rnd.nextInt(i));

            // Dump array back into list
            ListIterator it = list.listIterator();
            for (int i=0; i<arr.length; i++) {
                it.next();
                it.set(arr[i]);
            }
        }
    }
代码语言:javascript
复制
    public static void rotate(List<?> list, int distance) {
        if (list instanceof RandomAccess || list.size() < ROTATE_THRESHOLD)
            rotate1(list, distance);
        else
            rotate2(list, distance);
    }

通配符使用小结

  • 只用于“读”功能时,泛型结构使用<? extends T>
  • 只用于“写”功能时,泛型结构使用<? super T>
  • 如果既用于“写”,又用于“读”操作,那么直接使用<T>.
  • 如果操作与泛型类型无关,那么使用<?>
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 孟君的编程札记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.1 泛型接口示例
  • 1.3 泛型方法
  • 4.3 通配符<?>的使用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档