目录

泛型

Generics

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

泛型可以用于类、接口、方法,通过使用泛型可以使代码更简单、安全。然而 Java 中的泛型使用了类型擦除,所以只是伪泛型。

Java 泛型的参数只可以代表类,不能代表个别对象。由于Java泛型的类型参数之实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型,而且无法直接使用基本值类型作为泛型类型参数。Java编译程序在编译泛型时会自动加入类型转换的编码,故运行速度不会因为使用泛型而加快。

由于运行时会消除泛型的对象实例类型信息等缺陷经常被人诟病,Java及JVM的开发方面也尝试解决这个问题,例如:Java通过在生成字节码时添加类型推导辅助信息,从而可以通过反射接口获得部分泛型信息;通过改进泛型在JVM的实现,使其支持基本值类型泛型和直接获得泛型信息等。

协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。

只读数据类型(源)是协变的;只写数据类型(汇/sink)是逆变的。可读可写类型应是“不变”的。

Java 把数组类型处理为协变,在Java中,String[]是Object[]的子类型。

返回值的协变, Java 允许返回值协变。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class AnimalShelter {
    Animal getAnimalForAdoption() {
      ...
    }

    void putAnimal(Animal animal) {
      ...
    }
}

class CatShelter extends AnimalShelter {
    Cat getAnimalForAdoption() {
        return new Cat();
    }
}

泛型方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class GenericMethod {
    public <K,V> void f(K k,V v) {
        System.out.println(k.getClass().getSimpleName());
        System.out.println(v.getClass().getSimpleName());
    }

    public static void main(String[] args) {
        GenericMethod gm = new GenericMethod();
        gm.f(new Integer(0),new String("generic"));
    }
}

定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • ? - 表示不确定的 java 类型

泛型类 与 泛型接口

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

通配符

  • <T>:泛型标识符,用于泛型定义(类、接口、方法等)时。

  • <?>:通配符,用于泛型实例化时。

  • 向上转型:子类型通过类型转换成为父类型(隐式的)。

  • 向下转型:父类型通过类型转换成为子类型(显式的,有风险需谨慎 ClassCastException)。

  • <? extends T> 上边界通配符 因为可以确定父类型,所以可以以父类型去获取数据(向上转型)。但是不能写入数据。

  • <? super T> 下边界通配符 因为可以确定最小类型,所以可以以最小类型去写入数据(向上转型)。而不能获取数据。

  • <?> 无边界通配符 等同于 上边界通配符<? extends Object>,所以可以以Object类去获取数据,但意义不大。

PECS 原则

“Producer Extends, Consumer Super”

  • “Producer Extends” – 如果你需要一个只读List,用它来produce T,那么使用 <? extends T>
  • “Consumer Super” – 如果你需要一个只写List,用它来consume T,那么使用 <? super T>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }
1
2
3
4
    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        return removeIf(filter, 0, size);
    }
1
2
3
4
5
6
7
// 两者结合起来一起用
public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++)
            dest.set(i, src.get(i));
    }
}

类型擦除

简单的说就是,类型参数只存在于编译期,在运行时,Java 的虚拟机 ( JVM ) 并不知道泛型的存在。

1
2
3
4
5
  public static void main(String[] args) {
    Class c1 = new ArrayList<String>().getClass();
    Class c2 = new ArrayList<Integer>().getClass();
    System.out.println(c1 == c2);
  }

上面的代码输出是 true。这说明在 JVM 看来它们是同一个类。而在 C++、C# 这些支持真泛型的语言中,它们就是不同的类。

之所以取出来自动就是传入的参数类型,这是因为编译器在编译生成的字节码文件中插入了类型转换的代码,不需要我们手动转型了。如果参数类型有边界那么就擦除到它的第一个边界

类型擦除导致泛型丧失了一些功能,任何在运行期需要知道确切类型的代码都无法工作。

1
2
3
4
5
6
7
8
9
public class Erased<T> {
    private final int SIZE = 100;
    public static void f(Object arg) {
    if(arg instanceof T) {} // Error
    T var = new T(); // Error
    T[] array = new T[SIZE]; // Error
    T[] array = (T)new Object[SIZE]; // Unchecked warning
    }
}

通过 new T() 创建对象是不行的,一是由于类型擦除,二是由于编译器不知道 T 是否有默认的构造器。一种解决的办法是传递一个工厂对象并且通过它创建新的实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface FactoryI<T> {
    T create();
}
class Foo2<T> {
    private T x;
    public <F extends FactoryI<T>> Foo2(F factory) {
    x = factory.create();
    }
    // ...
}

另一种解决的方法是利用模板设计模式:

1
2
3
4
5
abstract class GenericWithCreate<T> {
    final T element;
    GenericWithCreate() { element = create(); }
    abstract T create();
}

泛型与数组

数组与容器差异:

  • 数组创建后大小便固定,但效率更高
  • 数组能追踪它内部保存的元素的具体类型,插入的元素类型会在编译期得到检查
  • 数组可以持有原始类型 ( int,float等 ),不过有了自动装箱,容器类看上去也能持有原始类型了

如果要创建一个泛型数组,应该是这样: Generic<Integer> ga = new Generic<Integer>[]。不过行代码会报错,也就是说不能直接创建泛型数组。 那么如果要使用泛型数组怎么办?

  • 一种方案是使用 ArrayList。
  • 不能直接创建,但可以定义泛型数组的引用。成功创建泛型数组的唯一方式是创建一个类型擦除的数组,然后转型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class ArrayOfGeneric {
    static final int SIZE = 100;
    static Generic<Integer>[] gia;
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        gia = (Generic<Integer>[])new Generic[SIZE];
        System.out.println(gia.getClass().getSimpleName());
        gia[0] = new Generic<Integer>();
        Generic<Integer> g = gia[0];
    }
} 

元素的类型是泛型类 rep() 调用会报 ClassCastException

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class GenericArray<T> {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArray(int sz) {
        array = (T[])new Object[sz];   // 创建泛型数组
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { return array[index]; }
    // Method that exposes the underlying representation:
    public T[] rep() { return array; }     //返回数组 会报错
    public static void main(String[] args) {
        GenericArray<Integer> gai =
        new GenericArray<Integer>(10);
        // This causes a ClassCastException:
        //! Integer[] ia = gai.rep();
        // This is OK:
        Object[] oa = gai.rep();
    }
}

推荐的方案: 使用类型标识

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class GenericArrayWithTypeToken<T> {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz) {
        array = (T[])Array.newInstance(type, sz);
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { return array[index]; }
    // Expose the underlying representation:
    public T[] rep() { return array; }
    public static void main(String[] args) {
        GenericArrayWithTypeToken<Integer> gai =
        new GenericArrayWithTypeToken<Integer>(
        Integer.class, 10);
        // This now works:
        Integer[] ia = gai.rep();
    }
}