跳至主要內容

⑥JAVA 泛型

holic-x...大约 13 分钟JAVA基础

⑥JAVA 泛型

学习核心

  • 泛型
    • 什么是泛型?有什么作用?
    • 泛型的原理是什么?

学习资料

泛型基础

1.泛型的引入

​ 结合实景案例应用场景引入泛型

需求分析

​ 编写程序,在ArrayList中,添加3个Dog对象。Dog对象含有name和age,并输出name和age (要求使用getXxx())

​ 定义一个ArrayList,将小狗的信息装入,如果这个时候不小心混入一只小猫,则在处理过程中可能需要手动转化

​ 先使用传统的方法来解决->引出泛型

public class DogDemo{
    public static void main(String[] args) {
        // 传统方式引入
        ArrayList dogs = new ArrayList();
        dogs.add(new Dog("小黄",1));
        dogs.add(new Dog("大白",2));
        dogs.add(new Dog("小牛",3));

        // 混入一只cat
        dogs.add(new Cat("花小猫",1,"我是一只猫"));

        // 方式1:遍历数据(由于实体重写了toString,此处打印出来的是对象的toString,不需要额外转化)
        for (int i = 0; i < dogs.size(); i++) {
            System.out.println(dogs.get(i));
        }

        System.out.println("---------------- 分割线 ----------------");
        // 方式2:假设默认dogs里面都是Dog类型,并不知道混进去一只Cat
        for(Object dog : dogs){
            // 向下转型:Object=》Dog
            Dog d = (Dog) dog;
            System.out.println(d);
        }
    }
}


class Dog {

    public String name;
    public int age;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

// 猫定义
class Cat {

    public String name;
    public int age;
    public String descr;

    public Cat(String name, int age, String descr) {
        this.name = name;
        this.age = age;
        this.descr = descr;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", descr='" + descr + '\'' +
                '}';
    }
}

​ 基于上述的内容分析,如果没有指定列表存储的数据类型,则可以加入任何Object,当数据列表中出现了预期之外的数据格式对象,则在遍历处理的时候就会出现问题(需要额外单独处理)

image-20240521110220575

​ 因此基于这种方式衍生两个问题:

  • 安全性:无法对加入到集合ArrayList的数据类型进行约束
  • 遍历的时候需要进行类型转换,如果集合数据量比较大则对效率有影响

​ 因此引入泛型进行约束,进而解决上面的问题

 				// 引用泛型
        ArrayList<Dog> dogs = new ArrayList<>();
        dogs.add(new Dog("小黄", 1));
        dogs.add(new Dog("大白", 2));
        dogs.add(new Dog("小牛", 3));

        // 混入一只cat 此时无法混入,因为dogs约束了添加元素必须为Dog对象
        // dogs.add(new Cat("花小猫", 1, "我是一只猫"));

​ 引入泛型具备的好处:

  • 编译时的强类型检查:编译时检查添加元素的类型,提高安全性
  • 避免类型转化:减少类型转换的次数,提高效率(其分析如下)
    • 不引入泛型:Dog =》转化为Object加入列表=》取出时需要将Object转化为Dog
    • 引入泛型:数据存入和取出都是Dog(不需额外的数据类型转化处理)
  • 泛型编程可以实现通用算法:通过使用泛型可以实现通用算法,用于处理不同类型的集合

​ 泛型的作用:可以在类声明时通过一个标识表示类中某个属性的类型,或者是某个方法的返回类型、或者是参数类型

2.泛型语法

泛型类的语法形式:

class name<T1, T2, ..., Tn> { /* ... */ }

interface 接口<T>{}
class<K,V>{}
// 其中T、K、V不代表值,而是表示类型,可以使任意字母(一般用T,是Type缩写)

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

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

泛型实例化

List<String> strList = new ArrayList<String>();
Iterator<Customer> iterator = customers.iterator();

3.泛型类

1)未应用泛型的类

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

public class Info {
	private Object value;

	public Object getValue() {
		return value;
	}

	public void setValue(Object value) {
		this.value = value;
	}
}

2)单类型参数的泛型类

class Info<T>{
    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 + '}';
    }
}

/**
 * 单类型参数的泛型类 demo
 */
public class SingleGenericClass {
    public static void main(String[] args) {
        Info<Integer> info = new Info<>();
        info.setValue(10);
        System.out.println(info.getValue());

        Info<String> info2 = new Info<>();
        info2.setValue("xyz");
        System.out.println(info2.getValue());
    }
}

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

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

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());
}

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

3)多类型参数的泛型类

class MyMap<K,V> {
    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 MuiltGenericClass {
    public static void main(String[] args) {
        MyMap<Integer, String> map = new MyMap<>(1, "one");
        System.out.println(map);
    }
}

// 测试结果
MyMap{key=1, value=one}

4)泛型类的类型嵌套

/**
 * 泛型类的类型嵌套
 */
public class NestGenericClass {
    public static void main(String[] args) {
        Info<Integer> info = new Info<>(10);
        MyMap<String,Info<Integer>> myMap = new MyMap<String,Info<Integer>>("hello",info);
        System.out.println(myMap);
    }
}

// 输出结果
MyMap{key=hello, value=Info{value=10}}

4.泛型接口

泛型接口定义语法

public interface Content<T> {
    T text();
}

泛型接口实现方式

  • 实现接口的子类明确声明泛型类型
  • 实现接口的子类不明确声明泛型类型
// 泛型接口定义
interface Content<T> {
    T text();
}

// 泛型接口实现1:子类明确声明泛型类型
class SubContent1<String> implements Content<String> {
    private String text;
    public SubContent1(String text) {
        this.text = text;
    }
    @Override
    public String text() {
        return text;
    }
}


// 泛型接口实现2:子类不明确声明泛型类型
class SubContent2<T> implements Content<T> {
    private T text;
    public SubContent2(T text) {
        this.text = text;
    }
    @Override
    public T text() {
        return text;
    }
}

/**
 * 泛型接口
 */
public class GenericInterface {
    public static void main(String[] args) {
        // 子类明确声明泛型类型
        SubContent1 subContent1 = new SubContent1("Hello");
        System.out.println(subContent1.text());

        // 子类不明确声明泛型类型
        SubContent2<Integer> subContent2 = new SubContent2<Integer>(10);
        System.out.println(subContent2.text());
    }
}

// 响应结果
Hello
10

5.泛型方法

​ 泛型方法是引入其自己的类型参数的方法。泛型方法可以是普通方法、静态方法以及构造方法。是否拥有泛型方法,与其所在的类是否是泛型没有关系。

泛型方法语法形式如下

public <T> T func(T obj) {}

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

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

class GenericMethod{
    public static <T> 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

可变参数泛型方法

// 可变参数泛型方法
 class GenericVarargsMethod{
    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<T>();
        Collections.addAll(result, args);
        return result;
    }

    public static void main(String[] args) {
        List<String> ls = makeList("A");
        System.out.println(ls);
        ls = makeList("A", "B", "C");
        System.out.println(ls);
    }
}

// output
[A]
[A, B, C]

6.泛型擦除

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

类型擦除做了如下工作:

  • 把泛型中的所有类型参数替换为 Object,如果指定类型边界,则使用类型边界来替换。因此,生成的字节码仅包含普通的类,接口和方法。
  • 擦除出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get()List<String> 就变成了 List。如有必要,插入类型转换以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。类型擦除确保不为参数化类型创建新类;因此,泛型不会产生运行时开销。

案例分析

public class GenericsErasureTypeDemo {
    public static void main(String[] args) {
        List<Object> list1 = new ArrayList<Object>();
        List<String> list2 = new ArrayList<String>();
        System.out.println(list1.getClass());
        System.out.println(list2.getClass());
    }
}

// output
class java.util.ArrayList
class java.util.ArrayList

​ 虽然指定了不同的参数,但是list1、list2的类信息是一样的,这是因为使用泛型时,任何具体的类型信息都被擦除了,也就意味着:ArrayList<Object>ArrayList<String> 在运行时,JVM 将它们视为同一类型

7.泛型和继承

泛型不能用于显式地引用运行时类型的操作之中,例如:转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了。当在编写泛型代码时,必须时刻提醒自己,只是看起来好像拥有有关参数的类型信息而已。正是由于泛型时基于类型擦除实现的,所以,泛型类型无法自动向上转型(向上转型是指用子类实例去初始化父类,这是面向对象中多态的重要表现),但可借助通配符完成

image-20240521120357691

Integer 继承了 ObjectArrayList 继承了 List;但是 List<Interger> 却并非继承了 List<Object>。这是因为,泛型类并没有自己独有的 Class 类对象。比如:并不存在 List<Object>.class 或是 List<Interger>.class,Java 编译器会将二者都视为 List.class

List<Integer> list = new ArrayList<>();
List<Object> list2 = list; // Erorr

8.类型边界

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

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

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

<T extends XXX>
// 类型边界案例1:类型参数 T extends Comparable<T> 表明传入方法中的类型要实现Comparable接口
class GenericExtendDemo1{
    static <T extends Comparable<T>> 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
// 类型边界案例2
class GenericExtendDemo2{
    static class A { /* ... */ }
    interface B { /* ... */ }
    interface C { /* ... */ }
    static class D1 <T extends A & B & C> { /* ... */ }
    static class D2 <T extends B & A & C> { /* ... */ } // 编译报错
    static class E extends A implements B, C { /* ... */ }

    public static void main(String[] args) {
        D1<E> demo1 = new D1<>();
        System.out.println(demo1.getClass().toString());
        // D1<String> demo2 = new D1<>(); // 编译报错
    }
}

9.类型通配符

通配符基本概念

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

  • 其中上界通配符和下界通配符不能同时使用

上界通配符:<? extends Number>,使用上界通配符来缩小类型参数的类型范围

public class GenericsUpperBoundedWildcardDemo {
    public static double sumOfList(List<? extends Number> list) {
        double s = 0.0;
        for (Number n : list) {
            s += n.doubleValue();
        }
        return s;
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        System.out.println("sum = " + sumOfList(li));
    }
}
// Output:
// sum = 6.0

下界通配符:<? super Number>,使用下界通配符来将未知类型限制为该类型的特定类型或超类类型

public class GenericsLowerBoundedWildcardDemo {
    public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);
        }
    }

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        addNumbers(list);
        System.out.println(Arrays.deepToString(list.toArray()));
    }
}
// Output:
// [1, 2, 3, 4, 5]

无界通配符:<?>

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

  • 可以使用 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<Integer> li = Arrays.asList(1, 2, 3);
        List<String> ls = Arrays.asList("one", "two", "three");
        printList(li);
        printList(ls);
    }
}
// Output:
// 1 2 3
// one two three

通配符和向上转型

泛型不能向上转型。但是可以通过使用通配符来向上转型

public class GenericsWildcardDemo {
    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        List<Number> numList = intList;  // Error

        List<? extends Integer> intList2 = new ArrayList<>();
        List<? extends Number> numList2 = intList2;  // OK
    }
}

10.泛型约束(todo)

Pair<int, char> p = new Pair<>(8, 'a');  // 编译错误
public static <E> void append(List<E> list) {
    E elem = new E();  // 编译错误
    list.add(elem);
}
public class MobileDevice<T> {
    private static T os; // error

    // ...
}
public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // 编译错误
        // ...
    }
}
List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // 编译错误
List<Integer>[] arrayOfLists = new List<Integer>[2];  // 编译错误
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }    // 编译错误

// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // 编译错误
public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   // compile-time error
        // ...
    }
}
public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { } // 编译错误
}

11.泛型实践

泛型命名

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

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

使用泛型的建议

  • 消除类型检查告警
  • List 优先于数组
  • 优先考虑使用泛型来提高代码通用性
  • 优先考虑泛型方法来限定泛型的范围
  • 利用有限制通配符来提升 API 的灵活性
  • 优先考虑类型安全的异构容器
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3