跳转至

图片

JavaSE(四)泛型程序设计

一、泛型的基本概念

泛型是 Java 中的一种特性,它允许在定义类、接口和方法时使用类型参数(type parameter),这些类型参数在使用时(即创建对象或调用方法时)被具体的类型(如 IntegerString 等)替换。泛型的主要目的是在编译时提供类型安全检查,避免运行时出现 ClassCastException 异常,同时减少代码中的强制类型转换。

例如,在没有泛型的情况下,使用 ArrayList 存储不同类型的对象,需要进行类型转换:

ArrayList list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0);

而使用泛型后:

ArrayList<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 无需类型转换

二、泛型类

泛型类是带有类型参数的类。定义泛型类的语法是在类名后面加上 <T>T 是类型参数,也可以用其他标识符,如 E 表示元素,K 表示键,V 表示值等)。

示例:

class Box<T> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

使用泛型类时,指定具体的类型:

Box<String> stringBox = new Box<>();
//因为现在有了类型变量,在使用时同样需要跟上<>并在其中填写明确要使用的类型
//这样我们就可以根据不同的类型进行选择了
stringBox.setContent("Java泛型");
String content = stringBox.getContent();

Box<Integer> integerBox = new Box<>();
integerBox.setContent(123);
Integer num = integerBox.getContent();

只不过这里需要注意一下,我们在方法中使用待确定类型的变量时,因为此时并不明确具体是什么类型,那么默认会认为这个变量是一个Object类型的变量,因为无论具体类型是什么,一定是Object类的子类:

图片

三、泛型接口

泛型接口与泛型类类似,在接口名后加上类型参数。

示例:

public interface Study<T> {
    T test();
}

实现泛型接口时,可以指定具体类型:

public class Main {
    public static void main(String[] args) {
        A a = new A();
        Integer i = a.test();
    }

    static class A implements Study<Integer> {   
        //在实现接口或是继承父类时,如果子类是一个普通类,那么可以直接明确对应类型
        @Override
        public Integer test() {
            return null;
        }
    }
}

也可以继续使用泛型:

public class Main {
    public static void main(String[] args) {
        A<String> a = new A<>();
        String i = a.test();
    }

    static class A<T> implements Study<T> {   
        //让子类继续为一个泛型类,那么可以不用明确
        @Override
        public T test() {
            return null;
        }
    }
}

继承也是同样的:

static class A<T> {

}

static class B extends A<String> {

}

四、泛型方法

泛型方法是在方法中使用类型参数,它可以在普通类或泛型类中定义。泛型方法的类型参数声明在方法的返回值类型之前。

示例:

class ArrayUtils {
    public static <T> T getMiddleElement(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[array.length / 2];
    }
}

调用泛型方法:

Integer[] intArray = {1, 2, 3, 4, 5};
Integer middleInt = ArrayUtils.getMiddleElement(intArray);

String[] strArray = {"a", "b", "c"};
String middleStr = ArrayUtils.getMiddleElement(strArray);

五、类型通配符

在泛型中,有时需要处理不确定的类型,这时候就用到类型通配符。类型通配符用 ? 表示。

  • 无界通配符(?:表示未知类型,可以匹配任何类型。例如 List<?> 可以表示 List<String>List<Integer> 等各种泛型列表。
  • 上界通配符(? extends T:表示未知类型,该类型必须是 TT 的子类。例如 List<? extends Number> 可以匹配 List<Integer>List<Double> 等,因为 IntegerDouble 都是 Number 的子类。
  • 下界通配符(? super T:表示未知类型,该类型必须是 TT 的父类。例如 List<? super Integer> 可以匹配 List<Number>List<Object> 等,因为 NumberInteger 的父类,ObjectNumber 的父类。

示例(上界通配符):

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

可以传入 List<Integer>List<Double> 等:

List<Integer> intList = Arrays.asList(1, 2, 3);
double sumInt = sumOfList(intList);

List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
double sumDouble = sumOfList(doubleList);

六、泛型的擦除

Java 中的泛型是伪泛型,在编译期间,泛型的类型信息会被擦除,这就是泛型擦除(Type Erasure)。也就是说,编译后的字节码中不包含泛型的类型参数信息,泛型类、泛型接口和泛型方法都会被擦除为原始类型(Raw Type)。例如,ArrayList<String> 在运行时会被擦除为 ArrayList,类型参数 String 相关的信息会被去掉。

泛型擦除会带来一些限制,比如不能直接创建泛型数组(new T[] 是不允许的),不能在静态方法或静态变量中使用类的泛型参数等。

public abstract class A <T>{
    abstract T test(T t);
}

实际上在Java中并不是真的有泛型类型(为了兼容之前的Java版本)因为所有的对象都是属于一个普通的类型,一个泛型类型编译之后,实际上会直接使用默认的类型:

public abstract class A {
    abstract Object test(Object t);  //默认就是Object
}

当然,如果我们给类型变量设定了上界,那么会从默认类型变成上界定义的类型:

public abstract class A <T extends Number>{   //设定上界为Number
    abstract T test(T t);
}

那么编译之后:

public abstract class A {
    abstract Number test(Number t);  //上界Number,因为现在只可能出现Number的子类
}

因此,泛型其实仅仅是在编译阶段进行类型检查,当程序在运行时,并不会真的去检查对应类型,所以说哪怕是我们不去指定类型也可以直接使用:

public static void main(String[] args) {
    Test test = new Test();    //对于泛型类Test,不指定具体类型也是可以的,默认就是原始类型
}

只不过此时编译器会给出警告,同样的,由于类型擦除,实际上我们在使用时,编译后的代码是进行了强制类型转换的:

public static void main(String[] args) {
    A<String> a = new B();
    String  i = a.test("10");     //因为类型A只有返回值为原始类型Object的方法
}

不过,我们思考一个问题,既然继承泛型类之后可以明确具体类型,那么为什么@Override不会出现错误呢?我们前面说了,重写的条件是需要和父类的返回值类型和形参一致,而泛型默认的原始类型是Object类型,子类明确后变为其他类型,这显然不满足重写的条件,但是为什么依然能编译通过呢?

public class B extends A<String>{
    @Override
    String test(String s) {
        return null;
    }
}

我们来看看编译之后长啥样:

// Compiled from "B.java"
public class com.test.entity.B extends com.test.entity.A<java.lang.String> {
  public com.test.entity.B();
  java.lang.String test(java.lang.String);
  java.lang.Object test(java.lang.Object);   //桥接方法,这才是真正重写的方法,但是使用时会调用上面的方法
}

通过反编译进行观察,实际上是编译器帮助我们生成了一个桥接方法用于支持重写:

public class B extends A {

    public Object test(Object obj) {   //这才是重写的桥接方法
        return this.test((Integer) obj);   //桥接方法调用我们自己写的方法
    }

    public String test(String str) {   //我们自己写的方法
        return null;
    }
}

类型擦除机制其实就是为了方便使用后面集合类(不然每次都要强制类型转换)同时为了向下兼容采取的方案。因此,泛型的使用会有一些限制:

首先,在进行类型判断时,不允许使用泛型,只能使用原始类型:

public static void main(String[] args) {
    Test<String> test = new Test();
    System.out.println(test instanceof Test<String>);
}

只能判断是不是原始类型,里面的具体类型是不支持的:

Test<String> test = new Test<>();
System.out.println(test instanceof Test);   //在进行类型判断时,不允许使用泛型,只能使用原始类型

还有,泛型类型是不支持创建参数化类型数组的,要用只能用原始类型:

public static void main(String[] args) {
    Test[] test = new Test[10];   //同样是因为类型擦除导致的,运行时可不会去检查具体类型是什么
}

只不过只是把它当做泛型类型的数组还是可以用的。

七、泛型的限制

  1. 不能使用基本类型作为泛型参数,必须使用对应的包装类,如 Integer 而不是 int
  2. 运行时类型查询只适用于原始类型,无法查询泛型的具体类型参数,例如 list instanceof ArrayList<String> 是不合法的,只能写成 list instanceof ArrayList
  3. 泛型类的静态成员不能使用类的泛型参数,因为静态成员在类加载时就已经存在,而泛型参数是在创建对象时才确定的。
  4. 不能创建泛型数组,如 T[] array = new T[10] 是不允许的。