一文了解Java泛型

1. 为什么需要泛型

2. 泛型类型

2.1. 泛型类

2.2. 泛型接口

3. 泛型方法

4. 类型擦除

5. 泛型和继承

6. 类型边界

7. 类型通配符

7.1. 上界通配符

7.2. 下界通配符

7.3. 无界通配符

7.4. 通配符和向上转型

8. 泛型的约束

9. 泛型最佳实践

9.1. 泛型命名

9.2. 使用泛型的建议

10. 小结

11. 参考资料

#1. 为什么需要泛型

JDK5 引入了泛型机制。

为什么需要泛型呢?回答这个问题前,先让我们来看一个示例。

public class NoGenericsDemo {

    public static void main(String[] args) {

        List list = new ArrayList<>();

        list.add("abc");

        list.add(18);

        list.add(new double[] {1.0, 2.0});

        Object obj1 = list.get(0);

        Object obj2 = list.get(1);

        Object obj3 = list.get(2);

        System.out.println("obj1 = [" + obj1 + "]");

        System.out.println("obj2 = [" + obj2 + "]");

        System.out.println("obj3 = [" + obj3 + "]");

        int num1 = (int)list.get(0);

        int num2 = (int)list.get(1);

        int num3 = (int)list.get(2);

        System.out.println("num1 = [" + num1 + "]");

        System.out.println("num2 = [" + num2 + "]");

        System.out.println("num3 = [" + num3 + "]");

    }

}

// Output:

// obj1 = [abc]

// obj2 = [18]

// obj3 = [[D@47089e5f]

// Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

// at io.github.dunwu.javacore.generics.NoGenericsDemo.main(NoGenericsDemo.java:23)

示例说明:

在上面的示例中,List 容器没有指定存储数据类型,这种情况下,可以向 List 添加任意类型数据,编译器不会做类型检查,而是默默的将所有数据都转为 Object。

假设,最初我们希望向 List 存储的是整形数据,假设,某个家伙不小心存入了其他数据类型。当你试图从容器中取整形数据时,由于 List 当成 Object 类型来存储,你不得不使用类型强制转换。在运行时,才会发现 List 中数据不存储一致的问题,这就为程序运行带来了很大的风险(无形伤害最为致命)。

而泛型的出现,解决了类型安全问题。

泛型具有以下优点:

编译时的强类型检查

泛型要求在声明时指定实际数据类型,Java 编译器在编译时会对泛型代码做强类型检查,并在代码违反类型安全时发出告警。早发现,早治理,把隐患扼杀于摇篮,在编译时发现并修复错误所付出的代价远比在运行时小。

避免了类型转换

未使用泛型:

List list = new ArrayList();

list.add("hello");

String s = (String) list.get(0);

使用泛型:

List list = new ArrayList();

list.add("hello");

String s = list.get(0);  // no cast

泛型编程可以实现通用算法

通过使用泛型,程序员可以实现通用算法,这些算法可以处理不同类型的集合,可以自定义,并且类型安全且易于阅读。

#2. 泛型类型

泛型类型是被参数化的类或接口。

#2.1. 泛型类

泛型类的语法形式:

class name { /* ... */ }

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。由尖括号(<>)分隔的类型参数部分跟在类名后面。它指定类型参数(也称为类型变量)T1,T2,...和 Tn。

一般将泛型中的类名称为原型,而将 <> 指定的参数称为类型参数。

未应用泛型的类

在泛型出现之前,如果一个类想持有一个可以为任意类型的数据,只能使用 Object 做类型转换。示例如下:

public class Info {

private Object value;

public Object getValue() {

return value;

}

public void setValue(Object value) {

this.value = value;

}

}

单类型参数的泛型类

public class Info {

    private T value;

    public Info() { }

    public Info(T value) {

        this.value = value;

    }

    public T getValue() {

        return value;

    }

    public void setValue(T value) {

        this.value = value;

    }

    @Override

    public String toString() {

        return "Info{" + "value=" + value + '}';

    }

}

public class GenericsClassDemo01 {

    public static void main(String[] args) {

        Info info = new Info<>();

        info.setValue(10);

        System.out.println(info.getValue());

        Info info2 = new Info<>();

        info2.setValue("xyz");

        System.out.println(info2.getValue());

    }

}

// Output:

// 10

// xyz

在上面的例子中,在初始化一个泛型类时,使用 <> 指定了内部具体类型,在编译时就会根据这个类型做强类型检查。

实际上,不使用 <> 指定内部具体类型,语法上也是支持的(不推荐这么做),如下所示:

public static void main(String[] args) {

    Info info = new Info();

    info.setValue(10);

    System.out.println(info.getValue());

    info.setValue("abc");

    System.out.println(info.getValue());

}

示例说明:

上面的例子,不会产生编译错误,也能正常运行。但这样的调用就失去泛型类型的优势。

多个类型参数的泛型类

public class MyMap {

    private K key;

    private V value;

    public MyMap(K key, V value) {

        this.key = key;

        this.value = value;

    }

    @Override

    public String toString() {

        return "MyMap{" + "key=" + key + ", value=" + value + '}';

    }

}

public class GenericsClassDemo02 {

    public static void main(String[] args) {

        MyMap map = new MyMap<>(1, "one");

        System.out.println(map);

    }

}

// Output:

// MyMap{key=1, value=one}

泛型类的类型嵌套

public class GenericsClassDemo03 {

    public static void main(String[] args) {

        Info info = new Info("Hello");

        MyMap> map = new MyMap<>(1, info);

        System.out.println(map);

    }

}

// Output:

// MyMap{key=1, value=Info{value=Hello}}

#2.2. 泛型接口

接口也可以声明泛型。

泛型接口语法形式:

public interface Content {

    T text();

}

泛型接口有两种实现方式:

实现接口的子类明确声明泛型类型

public class GenericsInterfaceDemo01 implements Content {

    private int text;

    public GenericsInterfaceDemo01(int text) {

        this.text = text;

    }

    @Override

    public Integer text() { return text; }

    public static void main(String[] args) {

        GenericsInterfaceDemo01 demo = new GenericsInterfaceDemo01(10);

        System.out.print(demo.text());

    }

}

// Output:

// 10

实现接口的子类不明确声明泛型类型

public class GenericsInterfaceDemo02 implements Content {

    private T text;

    public GenericsInterfaceDemo02(T text) {

        this.text = text;

    }

    @Override

    public T text() { return text; }

    public static void main(String[] args) {

        GenericsInterfaceDemo02 gen = new GenericsInterfaceDemo02<>("ABC");

        System.out.print(gen.text());

    }

}

// Output:

// ABC

#3. 泛型方法

泛型方法是引入其自己的类型参数的方法。泛型方法可以是普通方法、静态方法以及构造方法。

泛型方法语法形式如下:

public T func(T obj) {}

是否拥有泛型方法,与其所在的类是否是泛型没有关系。

泛型方法的语法包括一个类型参数列表,在尖括号内,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际类型参数的占位符。

使用泛型方法的时候,通常不必指明类型参数,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。类型推断只对赋值操作有效,其他时候并不起作用。如果将一个返回类型为T的泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行推断。编译器会认为:调用泛型方法后,其返回值被赋给一个 Object 类型的变量。

public class GenericsMethodDemo01 {

    public static void printClass(T obj) {

        System.out.println(obj.getClass().toString());

    }

    public static void main(String[] args) {

        printClass("abc");

        printClass(10);

    }

}

// Output:

// class java.lang.String

// class java.lang.Integer

泛型方法中也可以使用可变参数列表

public class GenericVarargsMethodDemo {

    public static List makeList(T... args) {

        List result = new ArrayList();

        Collections.addAll(result, args);

        return result;

    }

    public static void main(String[] args) {

        List ls = makeList("A");

        System.out.println(ls);

        ls = makeList("A", "B", "C");

        System.out.println(ls);

    }

}

// Output:

// [A]

// [A, B, C]

#4. 类型擦除

Java 语言引入泛型是为了在编译时提供更严格的类型检查,并支持泛型编程。不同于 C++ 的模板机制,Java 泛型是使用类型擦除来实现的,使用泛型时,任何具体的类型信息都被擦除了。

那么,类型擦除做了什么呢?它做了以下工作:

把泛型中的所有类型参数替换为 Object,如果指定类型边界,则使用类型边界来替换。因此,生成的字节码仅包含普通的类,接口和方法。

擦除出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get() ;List 就变成了 List。如有必要,插入类型转换以保持类型安全。

生成桥接方法以保留扩展泛型类型中的多态性。类型擦除确保不为参数化类型创建新类;因此,泛型不会产生运行时开销。

让我们来看一个示例:

public class GenericsErasureTypeDemo {

    public static void main(String[] args) {

        List list1 = new ArrayList();

        List list2 = new ArrayList();

        System.out.println(list1.getClass());

        System.out.println(list2.getClass());

    }

}

// Output:

// class java.util.ArrayList

// class java.util.ArrayList

示例说明:

上面的例子中,虽然指定了不同的类型参数,但是 list1 和 list2 的类信息却是一样的。

这是因为:使用泛型时,任何具体的类型信息都被擦除了。这意味着:ArrayList 和 ArrayList 在运行时,JVM 将它们视为同一类型。

Java 泛型的实现方式不太优雅,但这是因为泛型是在 JDK5 时引入的,为了兼容老代码,必须在设计上做一定的折中。

#5. 泛型和继承

泛型不能用于显式地引用运行时类型的操作之中,例如:转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了。当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。

正是由于泛型时基于类型擦除实现的,所以,泛型类型无法向上转型。

向上转型是指用子类实例去初始化父类,这是面向对象中多态的重要表现。

img

Integer 继承了 Object;ArrayList 继承了 List;但是 List 却并非继承了 List

这是因为,泛型类并没有自己独有的 Class 类对象。比如:并不存在 List.class 或是 List.class,Java 编译器会将二者都视为 List.class。

List list = new ArrayList<>();

List list2 = list; // Erorr

#6. 类型边界

有时您可能希望限制可在参数化类型中用作类型参数的类型。类型边界可以对泛型的类型参数设置限制条件。例如,对数字进行操作的方法可能只想接受 Number 或其子类的实例。

要声明有界类型参数,请列出类型参数的名称,然后是 extends 关键字,后跟其限制类或接口。

类型边界的语法形式如下:

示例:

public class GenericsExtendsDemo01 {

    static > T max(T x, T y, T z) {

        T max = x; // 假设x是初始最大值

        if (y.compareTo(max) > 0) {

            max = y; //y 更大

        }

        if (z.compareTo(max) > 0) {

            max = z; // 现在 z 更大

        }

        return max; // 返回最大对象

    }

    public static void main(String[] args) {

        System.out.println(max(3, 4, 5));

        System.out.println(max(6.6, 8.8, 7.7));

        System.out.println(max("pear", "apple", "orange"));

    }

}

// Output:

// 5

// 8.8

// pear

示例说明:

上面的示例声明了一个泛型方法,类型参数 T extends Comparable 表明传入方法中的类型必须实现了 Comparable 接口。

类型边界可以设置多个,语法形式如下:

注意:extends 关键字后面的第一个类型参数可以是类或接口,其他类型参数只能是接口。

示例:

public class GenericsExtendsDemo02 {

    static class A { /* ... */ }

    interface B { /* ... */ }

    interface C { /* ... */ }

    static class D1 { /* ... */ }

    static class D2 { /* ... */ } // 编译报错

    static class E extends A implements B, C { /* ... */ }

    public static void main(String[] args) {

        D1 demo1 = new D1<>();

        System.out.println(demo1.getClass().toString());

        D1 demo2 = new D1<>(); // 编译报错

    }

}

#7. 类型通配符

类型通配符一般是使用 ? 代替具体的类型参数。例如 List 在逻辑上是 List ,List 等所有 List<具体类型实参> 的父类。

#7.1. 上界通配符

可以使用**上界通配符**来缩小类型参数的类型范围。

它的语法形式为:

public class GenericsUpperBoundedWildcardDemo {

    public static double sumOfList(List list) {

        double s = 0.0;

        for (Number n : list) {

            s += n.doubleValue();

        }

        return s;

    }

    public static void main(String[] args) {

        List li = Arrays.asList(1, 2, 3);

        System.out.println("sum = " + sumOfList(li));

    }

}

// Output:

// sum = 6.0

#7.2. 下界通配符

**下界通配符**将未知类型限制为该类型的特定类型或超类类型。

注意:上界通配符和下界通配符不能同时使用。

它的语法形式为:

public class GenericsLowerBoundedWildcardDemo {

    public static void addNumbers(List list) {

        for (int i = 1; i <= 5; i++) {

            list.add(i);

        }

    }

    public static void main(String[] args) {

        List list = new ArrayList<>();

        addNumbers(list);

        System.out.println(Arrays.deepToString(list.toArray()));

    }

}

// Output:

// [1, 2, 3, 4, 5]

#7.3. 无界通配符

无界通配符有两种应用场景:

可以使用 Object 类中提供的功能来实现的方法。

使用不依赖于类型参数的泛型类中的方法。

语法形式:

public class GenericsUnboundedWildcardDemo {

    public static void printList(List list) {

        for (Object elem : list) {

            System.out.print(elem + " ");

        }

        System.out.println();

    }

    public static void main(String[] args) {

        List li = Arrays.asList(1, 2, 3);

        List ls = Arrays.asList("one", "two", "three");

        printList(li);

        printList(ls);

    }

}

// Output:

// 1 2 3

// one two three

#7.4. 通配符和向上转型

前面,我们提到:泛型不能向上转型。但是,我们可以通过使用通配符来向上转型。

public class GenericsWildcardDemo {

    public static void main(String[] args) {

        List intList = new ArrayList<>();

        List numList = intList;  // Error

        List intList2 = new ArrayList<>();

        List numList2 = intList2;  // OK

    }

}

扩展阅读:Oracle 泛型文档(opens new window)

#8. 泛型的约束

泛型类型的类型参数不能是值类型(opens new window)

Pair p = new Pair<>(8, 'a');  // 编译错误

不能创建类型参数的实例(opens new window)

public static void append(List list) {

    E elem = new E();  // 编译错误

    list.add(elem);

}

不能声明类型为类型参数的静态成员(opens new window)

public class MobileDevice {

    private static T os; // error

    // ...

}

类型参数不能使用类型转换或 instanceof(opens new window)

public static void rtti(List list) {

    if (list instanceof ArrayList) {  // 编译错误

        // ...

    }

}

List li = new ArrayList<>();

List  ln = (List) li;  // 编译错误

不能创建类型参数的数组(opens new window)

List[] arrayOfLists = new List[2];  // 编译错误

不能创建、catch 或 throw 参数化类型对象(opens new window)

// Extends Throwable indirectly

class MathException extends Exception { /* ... */ }    // 编译错误

// Extends Throwable directly

class QueueFullException extends Throwable { /* ... */ // 编译错误

public static void execute(List jobs) {

    try {

        for (J job : jobs)

            // ...

    } catch (T e) {  // compile-time error

        // ...

    }

}

仅仅是泛型类相同,而类型参数不同的方法不能重载(opens new window)

public class Example {

    public void print(Set strSet) { }

    public void print(Set intSet) { } // 编译错误

}

#9. 泛型最佳实践

#9.1. 泛型命名

泛型一些约定俗成的命名:

E - Element

K - Key

N - Number

T - Type

V - Value

S,U,V etc. - 2nd, 3rd, 4th types

#9.2. 使用泛型的建议

消除类型检查告警

List 优先于数组

优先考虑使用泛型来提高代码通用性

优先考虑泛型方法来限定泛型的范围

利用有限制通配符来提升 API 的灵活性

优先考虑类型安全的异构容器

#10. 小结

img

#11. 参考资料

Java 编程思想(opens new window)

Java 核心技术(卷 1)(opens new window)

Effective java(opens new window)

Oracle 泛型文档(opens new window)

Java 泛型详解(opens new window)

帮助我们改善此页面! (opens new window)

上次更新: 5 days ago

你可能感兴趣的:(一文了解Java泛型)