Java泛型相关知识点汇总

本文主要汇总Java泛型相关知识点


泛型的作用

使用泛型能写出更加灵活通用的代码

泛型的设计主要参照了C++的模板,旨在能让人写出更加通用化,更加灵活的代码。

泛型将代码安全性检查提前到编译期

泛型被加入Java语法中,还有一个最大的原因:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException转移到编译时。

  • 比如:
    1
    2
    List dogs =new ArrayList();
    dogs.add(new Cat());
    在没有泛型之前,这种代码除非运行,否则你永远找不到它的错误。但是加入泛型后
    1
    2
    List<Dog> dogs=new ArrayList<>();
    dogs.add(new Cat());//Error Compile
    会在编译的时候就检查出来。

泛型能够省去类型强制转换

在JDK1.5之前,Java容器都是通过将类型向上转型为Object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。

1
Dog dog=(Dog)dogs.get(1);

加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,使得代码更加优雅。

泛型的通配符

上界通配符<? extends T>

1
2
3
4
5
6
7
8
9
class Fruit{}
class Apple extends Fruit{}
class Plate<T>{
T item;
public Plate(T t){
item = t;
}
}
Plate<? extend Fruit>是Plate< Fruit >和Plate< Apple >的基类。

下界通配符<? super T>

1
2
3
4
5
6
7
8
9
10
class Fruit{}
class Apple extends Fruit{}
class Plate<T>{
T item;
public Plate(T t){
item = t;
}
}
Plate<? super Apple>包含Plate< Fruit >和Plate< Apple >的基类。
Plate<? super Fruit>只包含Plate< Fruit >。

<?>无限通配符

无界通配符 意味着可以使用任何对象,因此使用它类似于使用原生类型。但它是有作用的,原生类型可以持有任何类型,而无界通配符修饰的容器持有的是某种具体的类型。举个例子,在List类型的引用中,不能向其中添加Object, 而List类型的引用就可以添加Object类型的变量。

注意:List与List并不等同,List是List的子类。还有不能往List<?> list里添加任意对象,除了null。

泛型的擦除

  • 泛型是为了将具体的类型作为参数传递给方法,类,接口。擦除是在代码运行过程中将具体的类型都抹除。
  • 泛型仅仅是在编译的时候帮你做了编译时类型检查,成功编译后所生成的.class文件还是一模一样的,这便是擦除。

泛型语法

类型边界

泛型在最终会擦除为Object类型。这样导致的是在编写泛型代码的时候,对泛型元素的操作只能使用Object自带的一些方法,怎么可以使用其他类型的方法呢?

答案是extend,泛型重载了extend关键字,可以通过extend关键字指定最终擦除所替代的类型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Node<T extend People>{

private T obj;

public T get(){

return obj;
}

public void set(T obj){
this.obj=obj;
}

public void playName(){
System.out.println(obj.getName());
}
}

泛型与向上转型

概念

协变:子类能向父类转换 Animal a1=new Cat();
  • 示例1
    1
    2
    3
    4
    5
    public static void error(){
    Object[] nums=new Integer[3];
    nums[0]=3.2;
    nums[1]="string"; //运行时报错,nums运行时类型是Integer[]
    }
  • 示例2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static  void playFruit(List < ? extends Fruit> list){
    //do somthing
    }
    public static void main(String[] args) {
    List<Apple> apples=new ArrayList<>();
    List<Orange> oranges=new ArrayList<>();
    List<Food> foods =new ArrayList<>();
    playFruit(apples);
    playFruit(oranges);
    //playFruit(foods); 编译错误
    }
逆变: 父类能向子类转换 Cat a2=(Cat)a1;
  • 示例1

    1
    2
    Object obj="test";
    String str=(String)obj;//属于强者转换
  • 示例2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public  static  void playFruitBase(List < ? super  Fruit> list){
    //..
    }
    public static void main(String[] args) {
    List<Apple> apples=new ArrayList<>();
    List<Food> foods =new ArrayList<>();
    List<Object> objects=new ArrayList<>();
    playFruitBase(foods);
    playFruitBase(objects);
    //playFruitBase(apples); 编译错误
    }
    public static void playFruitBase(List < ? super Fruit> list){
    Object obj=list.get(0);
    }
  • 不变: 两者均不能转变

实现

  • 可使用extend,super关键字与通配符?

extend还被用来指定擦除到的具体类型,比如,表示在运行时将E替换为Fruit,注意E表示的是一个具体的类型,但是这里的extend和通配符连续使用<? extend Fruit>这里通配符?表示一个通用类型,它所表示的泛型在编译的时候,被指定的具体的类型必须是Fruit的子类

泛型的阴暗角落

通过擦除而实现的泛型,有些时候会有很多让人难以理解的规则,但是了解了泛型的真正实现又会觉得这样做还是比较合情合理。下面分析一下关于泛型在应用中有哪些奇怪的现象:

  • 擦除的地点—边界
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    static <T> T[] toArray(T... args) {

    return args;
    }

    static <T> T[] pickTwo(T a, T b, T c) {
    switch(ThreadLocalRandom.current().nextInt(3)) {
    case 0: return toArray(a, b);
    case 1: return toArray(a, c);
    case 2: return toArray(b, c);
    }
    throw new AssertionError(); // Can't get here
    }

    public static void main(String[] args) {

    String[] attributes = pickTwo("Good", "Fast", "Cheap");
    }
    运行的时候却会类型转换错误:Exception in thread “main” java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;

问题就在于可变参数那里,使用可变参数编译器会自动把我们的参数包装为一个数组传递给对应的方法,而这个数组的包装在泛型中,会最终翻译为new Object,那么toArray接受的实际类型是一个Object[],当然不能强制转换为String[]

  • 基类劫持

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public interface Playable<T>  {
    T play();
    }

    public class Base implements Playable<Integer> {
    @Override
    public Integer play() {
    return 4;
    }
    }

    public class Derived extend Base implements Playable<String>{
    ...
    }

    结果是:Derived类会在编译器会报错。

  • 自限定类型
    自限定类型简单点说就是将泛型的类型限制为自己以及自己的子类。最常见的在于实现Compareable接口的时候:

    1
    2
    public class Student implements Comparable<Student>{
    }

    问题点:假如Pen和Cup都继承于Enum,但是按道理来说笔和杯子之间相互比较是没有意义的,也就是说在Enum中compareTo(Enum o)方法中的Enum这个限定词太宽泛,这个时候有两种思路:

  • 子类分别自己实现Comparable接口,这样就可以规定更详细的参数类型,但是由于前面所说,会出现基类劫持的问题

  • 修改父类的代码,让父类不实现Comparable接口,让每个子类自己实现即可,但是这样会有大量一模一样的代码,只是传入的参数类型不同而已。

而更好的解决方案便是使用泛型的自限定类型:

1
2
3
4
5
6
7
public abstract class Enum<E extend Enum<E>> implements Comparable<E>,Serializable{
@Override
public int compareTo(E o) {
return 0;
}

}

泛型的自限定类型比起传统的自限定类型有个更大的优点就是它能使泛型的参数也变成协变的。

这样每个子类只用在集成的时候指定类型

1
2
public class Pen extends Enum<Pen>{}
public class Cup extends Cup<Cup>{}

注意:自限定类型一般用在继承体系中,需要参数协变的时候