Java_泛型(7)

引入泛型

JDK 1.5 引入了泛型,在此之前编写可被不同类型重用的代码需要使用 Object

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
  public static void main(String[] args) {
    GenericList list = new GenericList(2);
    list.add(0, 123);
    list.add(1, "abc");
  }
}

class GenericList {
  private Object[] data;

  public GenericList(int capacity) {
    data = new Object[capacity];
  }

  public void add(int index, Object item) {
    data[index] = item;
  }

  public Object get(int index) {
    return data[index];
  }
}

虽然将不同类型的数据保存到了数组中,但将元素取出来使用的时候就可能会出现类型转换的问题

1
2
3
//ClassCastException 
String s = (String) list.get(0); //Integer cannot be cast to String
int i = (int) list.get(1); //String cannot be cast to Integer

通过上面的例子可见,简单通过 Object 类来实现的容器类在取出元素使用的时候需要进行强制类型转换,并且编译器无法对当前的类型转变进行检查,只有运行后报错

其实将泛型引入 Java 的主要原因就是为了创建容器类,泛型将数据类型定义为参数,在创建一个容器类的对象时需要指定其中保存的元素的具体类型,进而可以让编译器在添加元素时就进行类型的检查

也正是为了兼容引入泛型之前的旧的容器类的实现,Java 在泛型的具体实现上做了取舍 (可以参考 RednaxelaFX 的知乎回答 ),采用的是类型擦除的实现方式,即泛型是针对编译时的类型检查,与运行时无关,编译后就会擦除,JVM 看到的还是 Object(如果没有指定边界)

类泛型

类泛型是在类名后定义类型参数的列表,即类型参数可以有多个,例如常见的 HashMap<K, V>,参数名可以自定义,类的成员变量、方法都可以使用,最常见的用途就是实现容器类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
  public static void main(String[] args) {
    NumberList<Integer> iList = new NumberList<>(2);
    iList.add(0, 123);
    //iList.add(0, "123"); //编译报错
    //Long l = (Long) iList.get(0); //编译报错
  }
}
class NumberList<E> {
  public Object[] data;

  public NumberList(int capacity) {
    data = new Object[capacity];
  }

  public void add(int index, E e) {
    data[index] = e;
  }

  public E get(int index) {
    return (E) data[index];
  }
}

JDK 1.7 引入了自动类型推断来简化代码,例如上面的 iList 的创建,如果没有自动类型推断,则需要像下面这样进行创建

1
NumberList<Integer> iList = new NumberList<Integer>(2);

Java 8 进一步增强了泛型的自动类型推断

接口泛型

和类泛型相似,接口泛型也是在接口名后定义类型参数的列表,定义后接口方法就可以使用

在定义接口的实现类时,如果没有设置具体的类型则默认为 Object

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface GenericInterface<T> {
  void func(T t);
}

class GenericImpl1 implements GenericInterface {
  @Override
  public void func(Object o) {
  }
}
class GenericImpl2 implements GenericInterface<String> {
  @Override
  public void func(String s) {
  }
}

接口泛型可以用于定义策略模式的公共策略,例如 Comparator 接口,在运行时传入指定的排序策略

方法泛型

方法泛型要与其所在类的泛型一致,但也可以定义自己的泛型,若与类的泛型同名了,将覆盖掉类的泛型,对于静态方法,则不能使用类的泛型,要想使用就需要自己定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test {
  public static void main(String[] args) {
    Generics<String> generics = new Generics<>();
    generics.func1("abc");
    generics.func2(123);
    generics.func3(3.14);
    Generics.funcS(123);
  }
}
class Generics<T> {
  public void func1(T t) {
    System.out.println(t);
  }
	//定义自己的泛型
  public <Q> void func2(Q q) {
    System.out.println(q);
  }
	//定义与类泛型同名的泛型
  public <T> void func3(T t) {
    System.out.println(t);
  }
  //静态方法定义自己的泛型
  public static <T> void funcS(T t) {
    System.out.println(t);
  }
}

泛型上界

可以通过 extends 指定泛型的上界,即传入的具体类型不能大于上界,但可以是其子类型

上界可以是一个具体的类,类型擦除后就不会转换为 Object,而是指定的上界类

可以通过泛型上界来实现一个求和方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Test {
  public static void main(String[] args) {
    add(1, 1);   //2.0
    add(1, 1.1); //2.1
    add(1.1, 1); //2.1
  }
  public static <T extends Number, E extends Number> void add(T t, E e) {
    System.out.println(t.doubleValue() + e.doubleValue());
  }
}

上界可以是一个接口,例如常见的 Comparable,实现找出数组中的最大元素的 max 方法

1
2
3
4
5
6
7
8
public static <T extends Comparable> T max(T[] arr) {
  T max = arr[0];
  for (int i = 0; i < arr.length; i++) {
    if (arr[i].compareTo(max) > 0)
      max = arr[i];
  }
  return max;
}

但是 Comparable 接口本身也定义有泛型,所以 max 方法完整的定义如下

1
2
3
4
//即表示 T 类型必须要实现 Comparable 接口,并且 T 类型的对象可以比较大小
public static <T extends Comparable<T>> T max(T[] arr) {
	//...
}

上界还可以是另一个类型参数,例如要给上面 NumberList 类定义一个将另一个 list 中的元素全部插入到当前 list 中的 addAll 方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
  public static void main(String[] args) {
    NumberList<Number> nList = new NumberList<>(1);
    NumberList<Integer> iList = new NumberList<>(1);
    iList.add(0, 123);
    //当前nList中元素的类型是Number,要addAll的iList的元素类型为Integer
    //因为Integer是Number的子类,所以编译类型检查通过
    nList.addAll(iList);
  }
}
class NumberList<E> {
	//...
  public int size() {
    return data.length;
  }
  //限定另一个list中元素的类型T,是当前list中元素类型的子类(类型相同也行,即小于等于的关系)
  public <T extends E> void addAll(NumberList<T> list) {
    for (int i = 0; i < list.size(); i++)
      add(i, list.get(i));
  }
}

泛型通配符

上面 addAll 方法定义的泛型,可以通过泛型通配符来简化

1
2
3
public void addAll(NumberList<? extends E> list) {
  //...
}

即 addAll 方法不用再定义其自己的泛型 T 了,<? extends E> 表示 E 或 E 的子类,对应的 <?> 就表示无界,通配符存在只读不能写的问题

1
2
3
4
5
6
7
public class Test {
  public static void main(String[] args) {
    NumberList<Integer> iList = new NumberList<>(1);
    NumberList<? extends Number> nList = iList;
    nList.add(0, 123); //编译报错
  }
}

再比如实现一个交换元素位置的静态方法

1
2
3
4
5
public static <T>void swap(NumberList<T> list, int i, int j) {
  T tmp = list.get(i);
  list.add(i, list.get(j));
  list.add(j, tmp);
}

因为 swap 方法不关心传入的 NumberList 中元素的类型是什么,所以可以采用无界通配符 <?> 来进行简化,但会因为通配符的只读不能写出现编译报错

1
2
3
4
5
public static void swap(NumberList<?> list, int i, int j) {
  Object tmp = list.get(i);
  list.add(i, list.get(j)); //编译报错
  list.add(j, tmp);         //编译报错
}

可以采用在方法外面再包一层的方式来解决

1
2
3
4
5
6
7
8
private static <T>void swapInner(NumberList<T> list, int i, int j) {
  T tmp = list.get(i);
  list.add(i, list.get(j));
  list.add(j, tmp);
}
public static void swap(NumberList<?> list, int i, int j) {
  swapInner(list, i, j);
}

至于 <? super E> 限制上界的通配符,常用于写入和比较,例如 Collections 的 sort 方法

1
2
3
public static <T extends Comparable<? super T>> void sort(List<T> list) {
  list.sort(null);
}

因为只能通过 extends 指定泛型的上界,所以没有 <? super E> 通配符对应的泛型实现方式

<? super E> 的具体内容,等以后研究明白了再说吧 ……

泛型的类型擦除

Java 的泛型是在编译器层面实现的,在泛型中指定的类型参数,编译后会被去掉,即称为类型擦除

1
2
3
4
5
6
7
8
public class Test {
  public static void main(String[] args) {
    ArrayList<String> list1 = new ArrayList<>();
    ArrayList<Integer> list2 = new ArrayList<>();
    System.out.println(list1.getClass().getName()); //java.util.ArrayList
    System.out.println(list2.getClass().getName()); //java.util.ArrayList
  }
}

指定的泛型<String><Integer> 都被擦除了,JVM 可见的只是 ArrayList

通过反射绕过编译时泛型的类型检查,添加不同类型的元素到 ArrayList<Integer>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Test {
  public static void main(String[] args) throws Exception {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(123);

    list.getClass().getMethod("add", Object.class)
      .invoke(list, "abc");

    for (int i = 0; i < list.size(); i++) {
      System.out.println(list.get(i));
    }
    //123
    //abc
  }
}

原始类型 raw type 指的是在编译时进行类型擦除之后的类型,对于没有限制边界的泛型,擦除后的原始类型为 Object,有指定边界则为边界的类型

上面的 ArrayList<Integer> 在类型擦除后,原始类型为 Object,所以可以插入 String 类型的元素

至于泛型方法的重写设计到的协变、桥方法等内容,等以后再说吧 ……


以上内容是玉山整理的笔记,如有错误还请指出