Java泛型(结合阿里一道面试编程题)

Java基础扫盲 专栏收录该内容
1 篇文章 0 订阅

目录

1 前言

2 初识泛型

2.1 提升代码通用性

2.2 提供编译时类型安全检测

3 三种泛型

3.1 泛型接口

3.2 泛型类

3.3 泛型方法

4 补充事项

4.1 泛型通配符

4.2 泛型限定符

4.3 运行时去泛型化


1 前言

        面试阿里时,有一道编程题如下:

请编程实现冒泡排序的工具类,该方法支持对实现了Comparable接口的对象进行排序。

        这道题就需要用到泛型的相关知识。虽然平时写代码经常使用Java集合,不可避免会用到泛型,但细想确实没有好好学习或者总结过泛型,于是就“蛋生”了这篇博客……

2 初识泛型

        泛型(JDK5引入),字面意思是将类型泛化。在类、接口、方法定义的时侯,无需强制指定成员变量、输入参数或返回值的类型,由程序员在具体调用的时候来指定。好处有二:一是增加了代码的通用性,二是泛型可以提供编译时的类型安全检测

2.1 提升代码通用性

        我们在比较Java基础数据类型(如int、float、long)的大小时,可以使用“>”、“<”、“==”等运算符,然而当我们需要给String、Date或者自定义类型比较大小时,就需要使用另一套规则来衡量。

        回到前言中的编程题,写一个冒泡排序并不难,整数版冒泡排序的Java代码如下。

public static void bubbleSort(int[] arr) {
    boolean sortFlag = true;
    // 长度为n的数组至多需要扫描n-1轮
    // 若某一轮扫描结束发现sortFlag为false,则表示数组已经有序
    for (int i = 0; sortFlag && i < arr.length - 1; i++) {
        // 每一轮扫描先乐观地认为没有进行元素交换
        sortFlag = false;
        for (int j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                sortFlag = true;
            }
        }
    }
}

        问题来了,如果传入的是float类型怎么办?再重载一个?那如果还要对“学生”、“大炮”等自定义类排序怎么办?

public static void bubbleSort(float[] arr) {
    // 省略……
}

public static void bubbleSort(Student[] arr) {
    // 省略……
}

public static void bubbleSort(DaPao[] arr) {
    // 省略……
}

        请注意,题目要求是实现一个工具类,这样每次对新的类型进行冒泡排序都需要在工具类中重载新的方法是不能接受的!好在,题目也提示我们了只需要对实现的Comparable接口的对象进行排序。

        下面来先看一下JDK中Comparable<T>接口的定义

public interface Comparable<T> {
    /**
     * 将该对象与另一个指定对象进行比较排序
     *
     * @param   o 被比较的对象
     * @return  当该对象小于、等于、大于指定对象时分别返回负整数、零、正整数
     *         
     * @throws NullPointerException 传入对象为null将抛出空指针异常
     * @throws ClassCastException 若传入对象的类型不能与调用对象进行比较将抛出异常
     */
    public int compareTo(T o);
}

        这个compareTo方法就是用来替代“>”运算符的。因为我们并不知道用户传入的是什么类型的对象数组,那这个对象能不能用“>”来比较大小就更不知道了。为了保证排序方法的通用性,就可以使用compareTo来比较对象的大小了,具体比较的规则交给了该对象的实现类。是不是通用性极大地提高了,这才配将它放在工具类中。

// <T extends Comparable> 表明传入的数组中的对象必须是Comparable的子类
public static <T extends Comparable> void bubbleSort(T[] arr) {
    boolean sortFlag = true;
    for (int i = 0; sortFlag && i < arr.length - 1; i++) {
        sortFlag = false;
        for (int j = 0; j < arr.length - i - 1; j++) {
            // 既然每个元素都是Comparable的子类,就可以调用compareTo方法比较大小了
            if (arr[j].compareTo(arr[j + 1]) > 0) {
                T temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                sortFlag = true;
            }
        }
    }
}

        看上面代码有点疑惑的小伙伴不用着急,看完第3节的泛型方法,再回过头来瞅瞅这个应该就很简单了。  

2.2 提供编译时类型安全检测

         相信很多小伙伴在使用List的时候都是先进行如下声明:

List<String> list1 = new ArrayList<>();
// 但其实这样也可以,默认可以存放所有的Object
List list2 = new ArrayList();

list1.add("this is ok"); 
list1.add(0);   // 编译不通过
        
list2.add("this is ok too");
list2.add(0);           // 编译通过
list2.add(student);     // 编译通过

        这样就能将很多异常在运行前确定下来,也避免了写如下的代码:

for (Object item : list2) {
    if (item instanceof Student) {
        // 这里需要类型强转才能调用Student类中的方法,因为Object对象是没有getName()的
        System.out.println("Hello, " + ((Student) item).getName());
    }
}

3 三种泛型

3.1 泛型接口

        上面提到的Comparable接口就是一个泛型接口,此外JDK中还提供了很多泛型接口,如Comparator<T>以及JDK8中著名的四大函数式接口(Function<T, R>Comsumer<T>Supplier<T>Predicate<T>)。以Function接口为例,看下泛型定义方式。

// 声明泛型:在接口名后用尖括号包裹泛型标识,多个可用逗号隔开
// 接口上声明过的标识可以在方法中使用

@FunctionalInterface
public interface Function<T, R> {
    /**
     * 在给定的参数上执行该函数
     *
     * @param t 函数参数
     * @return 函数执行结果
     */
    R apply(T t);
}

        实现类在实现泛型接口时可以传入具体的类型,比如我们实现一个方法:将字符串去除前后空格后再反转。

// 实现时分别指定两个泛型都是String类型
class StringOperation implements Function<String, String> {
    @Override
    public String apply(String s) {
        if(s == null) return s;
        return new StringBuilder(s.trim()).reverse().toString();
    }
}

        也可以直接继承并在调用时再指定,JDK中的集合类一般也是这么做的。

// 将类型指定推迟到实例化操作时
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

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

3.2 泛型类

        在3.1节中我们已经看到了编写泛型类的一种方式,即通过实现一个泛型接口或继承一个泛型类。那么,泛型类当然也可以没有实现任何泛型接口或继承任何泛型类,就自己声明就好了。

// 和泛型接口声明方法类似
class Point<T> {
    // 已声明的泛型标识T可以用于成员变量
    T x;
    T y;
    
    // T也可以用于参数
    public Point(T x, T y) {
        this.x = x;
        this.y = y;
    }

    public static void main(String[] args) {
        // 整数坐标
        Point<Integer> p1 = new Point<>(1, 2);
        // 浮点型坐标
        Point<Float> p2 = new Point<>(1.0f, 2.0f);
    }
}

3.3 泛型方法

        泛型方法相对比较难理解,首先大家经常容易误认为泛型类中的方法就是泛型方法,或者认为包含了T、E、K、V等泛型标识的方法就是泛型方法。其实,泛型方法与泛型类或泛型接口关系不大,甚至可以用于替代泛型类,从而避免一些麻烦。

        泛型方法定义时需要在修饰符(如:public、private、static、final等)后,方法返回类型前声明方法中持有的类型变量。上一段JDK中Arrays.class中的代码!

// 这就是一个普通的工具类    
public class Arrays {

    // <T>表名了该方法是一个泛型方法,且持有泛型T。这样参数列表中就可以使用T了
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        // 具体实现可以暂时忽略
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
}

        同样,可以在泛型方法中持有多个泛型变量 ,像这样:

// 仅供演示,这个方法好像没啥用
public <K, V> V getValue(K key, V val) {
    // do something
    System.out.println(key.toString());
    return val;
}

【注意】

  • 当泛型类中已经声明的泛型标识,非静态泛型方法中是可以直接使用的
  • 静态泛型方法不能直接使用类泛型标识,需要主动声明   
class Example<T> {
    // 这不是一个泛型方法,也不是一个静态方法,可以直接使用T
    public T getValue(T a) {
        return a;
    }

    // 这是一个静态泛型方法,需要主动声明T,才可以使用T
    public static <T> T getValueStatic(T a) {
        return a;
    }

    // 这是一个静态泛型方法,有必要的话可以继续声明其他泛型标识
    public static <T, K, V> T getValueStatic2(T a, K key) {
        V val;
        return a;
    }
}

        看到这里,细心的小伙伴一定有疑问,那就是“?”是干嘛用的?莫着急 ,请继续看第4节。

4 补充事项

4.1 泛型通配符

        下面是JDK8中ArrayList.class中的一个方法。这个方法中的“?”表示可以接收任意类型的Collection作为入参,若写成 public boolean removeAll(Collection<String> c)则表示仅能接收String类型的集合。

// JDK 1.8 源码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{   
    // 移除当前list中所有被包含在集合c中的元素
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
}

// example
public static void main(String[] args) {
    // 为什么需要定义成“?”,而不是E呢
    List list = new ArrayList();
    list.add("1");
    list.add(2);
    
    Set<String> set = new HashSet<String>(){{
        add("1");
        add("2");
    }};
    
    // 因为定义为 “?” ,故此处编译不会报错
    list.removeAll(set); 
    System.out.println(list);  // 输出 [2]
}

4.2 泛型限定符

        还记得前言中的编程题吗?在泛型方法中我们使用了<T extends Comparable>,表示我们对泛型T还有额外的要求:T必须是Comparable的子类。于此对应的还有<T super Comparable>,表明T必须是Comparable的父类。

public static <T extends Comparable> void bubbleSort(T[] arr) {
    // 省略……
}

  【提示】   

  •  ? extends ClassA:表示可以传入ClassA及其子类;
  •  ? super ClassB:表示可以传入ClassB及其父类。

        下面说明举一组比较难以理解的例子,吃透他!


//假设有ClassA、ClassB、ClassC,他们关系如下:
//    ClassB extends ClassA 且 ClassC extends ClassB

// 例1
List<? extends ClassA> listA = new ArrayList<ClassA>(){{add new ClassA();}};  // 正确
List<? extends ClassB> listB = new ArrayList<ClassB>();  // 正确
List<? super ClassB> listB2 = new ArrayList<ClassC>();  // 正确

// 例2: 不能执行add()操作,因为不确定该容器到底存放的是ClassA的哪一个子类
listA.add(new ClassA()); // 错误,参数不匹配
listA.add(new ClassB()); // 错误,参数不匹配

// 例3: 可以执行get()操作
ClassA classA = listA.get(0);

// 例4:listB2容器声明时存放的是ClassB及其父类,可能是ClassA,也可能只是Object
// 由于这种不确定性,不能直接存放ClassA对象
listB2.add(new ClassB()); // 正确
listB2.add(new ClassC()); // 正确
listB2.add(new ClassA()); // 错误 

// 例5:注意只能用Object去接收返回值
Object obj = listB2.get(0);

         最后,提一下PECS(Producer Extends Consumer Super)原则,根据上面5个小例子也不难看出extends适合做查询操作,而super更适合执行添加操作。

4.3 运行时去泛型化

         运行时会进行泛型擦除。JVM并不知道泛型的存在,泛型在编译阶段就已经被处理成普通的类和方法。可以参考下面这个例子:

List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();

System.out.println(list1.getClass() == list2.getClass());   // true
System.out.println(list1.getClass());   // class java.util.ArrayList

5 参考链接

java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一

super T>,终于搞清楚了!" data-link-title="困扰多年的Java泛型 extends T> super T>,终于搞清楚了!">困扰多年的Java泛型 extends T> super T>,终于搞清楚了!

  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值