从 Java 5以后,Java 引入了“参数化类型(parameterized type)”的概念,允许程序在创建集合时,指定集合元素的类型,例如List<String>,这表明该 List 只能保存字符串类型的对象。Java 的参数化类型被称为 泛型(Generic)

使用泛型

通过在泛型类型后增加一对尖括号,尖括号中放入一个类型,例如

List<String> list = new ArrayList<String>();
Map<String,Integer> map = new HashMap<String,Integer>();

从 java 7 开始,Java 允许在构造器后不需要带完整的泛型类型,只需要给出一对尖括号<> 即可,可以通过定义变量推断尖括号里应该是什么泛型类型

List<String> list = new ArrayList<>();
Map<String,Integer> map = new HashMap<>();

集合声明泛型后,则只能存放该泛型类型的元素

List<String> list = new ArrayList<>();
Map<String,Integer> map = new HashMap<>();

list.add("Java");
list.add(new Object()); // 报错,参数不匹配
map.put("Java",60);
map.put("C#",new Object()); // 报错,参数不匹配

深入泛型

编写泛型类

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(泛型)将在声明变量、创建对象、调用方法时动态地指定

public class Apple<T> {
    // 使用T类型定义变量
    private T info;

    public Apple() {
    }

    public Apple(T info) {
        this.info = info;
    }

    public void setInfo(T info) {
        this.info = info;
    }
    public T getInfo(){
        return this.info;
    }
}
public class GenericTest {
    public static void main(String[] args) {
        // 由于传给T形参地类型是String,所以构造器参数只能是String
        Apple<String> a1 = new Apple<>("红苹果");
        // 由于传给T形参地类型是Double,所以构造器参数只能是Double 或 double
        Apple<Double> a2 = new Apple<>(20.5);
        System.out.println("a1:" + a1.getInfo());
        // 通过 该方法参数只能是String
        a1.setInfo("青苹果");
        System.out.println("a1:" + a1.getInfo());
        System.out.println("a2:" + a2.getInfo());
    }
}

输出

a1:红苹果
a1:青苹果
a2:20.5

从泛型派生子类

当创建了带泛型声明地接口、父类之后,可以为该接口创建实现类,或从该父类派生子类

子类或实现类不能再包含泛型类形参

// 定义类A 继承 Apple类,Apple 类不能跟泛型形参
public class A extends Apple<T> {}

但是可将子类的泛型形参传入给父类

public class A<T> extends Apple<T> {}

同理也可以

public class A extends Apple<String> {}
public class A extends Apple<String> {
    // 重新父类方法,因为传入给父类形参是String类型 所以返回值也要是String
    @Override
    public String getInfo() {
        return "子类" + super.getInfo();
    }

    public static void main(String[] args) {
        A a = new A();
        a.setInfo("Java");
        System.out.println(a.getInfo());
    }
}

输出

子类Java

注意,泛型类例如ArrayList<String> 并不是ArrayList的子类,并不会生成class文件,instanceof 运算符后也不能使用泛型

类型通配符

类型通配符是一个问号(?),将一个问好作为类型实参传给List 集合,写作:List<?>,这个问好(?)被被称为通配符,它的元素类型可以匹配任何类型

public class GenericTest {
    // 不论使用任何类型的List调用此方法,程序都可以访问集合中的元素,其类型是Object
    public static void test(List<?> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        list1.add("Java");
        list1.add("C#");
        list1.add("PHP");
        test(list1);
        List<Double> list2 = new ArrayList<>();
        list2.add(20.5);
        list2.add(21.2);
        test(list2);
    }
}

由于无法确定集合中元素的类型,这种带通配符的List 仅仅表示它是各种泛型List 的父类,并不能把元素加入其中。

List<?> list = new ArrayList<>();
// 下面程序将会引起编译错误
list.add("ad");

设定类型通配符的上限

有时候,我们希望泛型类型只代表某一类泛型类型的父类,我们可以通过<? extends 类名> 指定泛型类型的上限

public abstract class Shape {
    public abstract void draw(Canvas c);
}
public class Circle extends Shape {
    @Override
    public void draw(Canvas c) {
        System.out.println("在画布" + c + "上画了个圆");
    }
}
public class Rectangle extends Shape{
    @Override
    public void draw(Canvas c) {
        System.out.println("在画布" + c + "上画了个矩形");
    }
}
public class Canvas {
    public void drawAll(List<? extends Shape> shapes){
        for(Shape shape : shapes){
            shape.draw(this);
        }
    }
}
public class GenericExtendsTest {
    public static void main(String[] args) {
        List<Circle> circles = new ArrayList<>();
        List<Rectangle> rectangles = new ArrayList<>();
        Canvas canvas = new Canvas();
        canvas.drawAll(circles);
        canvas.drawAll(rectangles);
        
        List<String> s = new ArrayList<>();
        // 引发编译错误,因为String不属于Shape 或 其子类
        canvas.drawAll(s);
    }
}

对于更广泛的泛型来说,指定通配符上限就是为了支持类型型变。比如 XY 的子类,这样I<X> 就相当于I<? extends Y> 的子类,可以将I<X> 赋值给A<? extends Y> 类型的变量,这种型变方式被称为协变

对于协变的泛型而言,它只能调用泛型类型作为返回值类型的方法;而不能调用泛型类型作为参数的方法。口诀:协变只出不进

设定类型通配符的下限

除了可以指定通配符的上限以外,也允许指定通配符的下限,通过用<? super 类型> 的方式来指定,与上限相反,比如 XY 的子类,程序可以将I<Y> 赋值给I<? super X> 类型的变量,这种型变方式被称为逆变

对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种你变得泛型集合只能向其中添加元素。 口诀:逆变只进不出

案例:实现将src 集合 复制到 dest 集合,因为 dest 集合可以保存src 集合中的元素,因此 dest 集合的元素类型 是 src 的父类

public class MyUtils {
    /**
     * 实现将`src` 集合 复制到 `dest` 集合
     * @param dest T类型为下限的集合
     * @param src T类型集合
     * @return 最后添加的元素
     * @param <T>
     */
    public static <T> T copy(Collection<? super T> dest, Collection<T> src) {
        T last = null;
        for (T ele : src) {
            last = ele;
            dest.add(ele);
        }
        return last;
    }

    public static void main(String[] args) {
        List<Number> numbers = new ArrayList<>();
        List<Integer> integers = new ArrayList<>();
        integers.add(5);

        Integer last = copy(numbers, integers);
        System.out.println("last:" + last);
        System.out.println("numbers:" + numbers);
    }
}

输出

last:5
numbers:[5]

设定泛型形参的上限

Java 泛型不仅允许在使用通配符形参时设定上限,并且可以在定义泛型形参时设定上限

例如

public class Apple<T extends Number> {

也可以设置多个

// T类型必须是Number 类或其子类,并且实现Serializable接口
public class Apple<T extends Number & java.io.Serializable> {

泛型方法

当定义类、接口时没有使用泛型形参,但定义方法时想自己定义泛型形参,我们可以通过泛型方法来实现

定义泛型方法

语法格式如下

修饰符 <T, S> 返回值类型 方法名(形参列表)
{
	// 方法体...
}

上面设定通配符下限的案例就是泛型方法

public static <T> T copy(Collection<? super T> dest, Collection<T> src) {
    T last = null;
    for (T ele : src) {
        last = ele;
        dest.add(ele);
    }
    return last;
}

同样也可以设定泛型形参的上限和设定类型通配符的上限,例如 Java 的 Collection 接口中有两个方法定义

boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);

可以改为

<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);

更改上述Canvas

原类

public class Canvas {
    public void drawAll(List<? extends Shape> shapes) {
        for (Shape shape : shapes) {
            shape.draw(this);
        }
    }
}

改为

public class Canvas {
    public <T extends Shape> void drawAll(List<T> shapes) {
        for (Shape shape : shapes) {
            shape.draw(this);
        }
    }
}

泛型构造器

如泛型方法一样,构造器也可以声明泛型形参

public class Foo {
    public <T> Foo(T t) {
        System.out.println(t);
    }

    public static void main(String[] args) {
        new Foo("Java");
        new Foo(200);
        // 指定泛型类型为String
        new <String>Foo("C#");
        // 因为指定了泛型类型为String类型,所以下行代码传入数值类型会报错
        new <String>Foo(20);
    }
}

如果是泛型类,并且显示指定了泛型构造器的泛型类型时,则泛型类后面不可以再省略泛型类型,如下

public class MyClass<E> {
    public <T> MyClass(T t) {
        System.out.println(t);
    }

    public static void main(String[] args) {
        // E形参String类型,T形参Integer类型
        // 不显式指定泛型构造器的泛型形参类型时,后面可以用<>省略
        MyClass<String> myClass1 = new MyClass<>(20);
        // 显式指定泛型构造器的泛型形参类型时,后面<>中也要显示指定类的泛型形参类型
        MyClass<String> myClass2 = new <Integer>MyClass<String>(20);
        // 显式指定泛型构造器的泛型形参类型时,MyClass后面不可以用<>省略,下方代码报错
        MyClass<String> myClass4 = new <Integer>MyClass<>(20);
    }

}

擦除和转换

定义变量时,如果没有为泛型类指定实际的类型,此时被称为 raw type(原始类型) ,默认声明该泛型形参指定的是第一个上限类型。例如把一个List<String> 类型的对象赋值给List,则该List 对集合元素的类型检查变成了泛型参数的上限(即Object

public class Cat<T extends Number> {
    T weight;

    public Cat(T weight) {
        this.weight = weight;
    }

    public T getWeight() {
        return weight;
    }

    public void setWeight(T weight) {
        this.weight = weight;
    }

}
public class ErasureTest {
    public static void main(String[] args) {
        // cat 泛型形参为Integer类型
        Cat<Integer> cat = new Cat<>(9);
        // c的泛型形参类型为Number类型
        Cat c = cat;
        Integer weight_int = cat.getWeight();
        Number weight_num = c.getWeight();

        // 下方代码报错,因为c的泛型形参是Number类型
        weight_int= c.getWeight();
    }
}

从逻辑上来看,如果直接把List 转换为List<String> 对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List 对象赋给一个List<String> 对象

public class ErasureTest2 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(20);
        list.add(1);
        list.add(21);
        List<String> stringList = list;
        System.out.println(stringList.get(0));
    }
}

运行输出

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at GenericDemo.ErasureTest2.main(ErasureTest2.java:13)

虽然程序编译没问题,但运行时当集合试当将元素当String取出来时还是会报错