阅读 527

java 基础 泛型

泛型是什么?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入 集合中,避免了在运行时出现ClassCastException。

优点

泛型的引入可以解决之前的集合类框架在使用过程中通常会出现的运行时刻类型错误,因为编译器可以在编译时刻就发现很多明显的错误。

类型擦除

泛型实在编译器层次来实现的

在生成的java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉,这个就叫类型擦除。

如在代码中定义的List(Object)和List(String)等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的,java编译器会在编译是尽可能发现出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。

泛型的奇怪特性都与类型擦除有关

  1. 泛型类并没有自己独有的Class对象,比如并不存在List(String).class,z只有List.class
  2. 静态变量是被泛型类的所有实例被共享的。对于声明为MyClass(T)的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass(String)还是new MyClass(Integer)创建的对象,都是共享一个静态变量。
  3. 泛型的类型参数不能用在java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException(String)和MyException(Integer)的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。

类型擦除过程

找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用上界。把代码中的类型参数都替换成具体类。同时去掉同时出现的类型声明,即去掉<>的内容。比如Tget()方法声明就变成了Object get();List(String)就变成了List。接下来就可能需要生辰改一些桥接方法。就是由于擦除了类型之后的类可能缺少默写某些必须的方法。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。

实例分析

了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的List(Object)和List(String)为例来具体分析:

public void insert(List(Object) list) {    
    for (Object obj : list) {        
        System.out.println(obj);    
    }    
    list.add(1); //这个操作在当前方法的上下文是合法的。 
}
public void test() {    
    List(String) strs = new ArrayList(String)();    
    inspect(strs); //编译错误 
}复制代码

这段代码中,insert方法接受List(Object)作为参数,当在test方法中试图传入List(String)的时候,会出现编译错误。假设这样的做法是允许的,那么在insert方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List(String)的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。

泛型类和泛型方法

泛型类

public class Som<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}
复制代码

Som就是一个泛型类,value的类型是T,而T是参数化的。如果有多个类型参数,使用分号隔开,如<U,V>。在编译期,是无法知道U和V具体是什么类型,只有在运行时才会真正根据类型来构造和分配内存。

public class Main {
    public static void main(String[] args) {
        Son<String, String> c1 = new Son<String, String>("name", "findingsea");
        Son<String, Integer> c2 = new Son<String, Integer>("age", 24);
        Son<Double, Double> c3 = new Son<Double, Double>(1.1, 2.2);
        System.out.println(c1.getKey() + " : " + c1.getValue());
        System.out.println(c2.getKey() + " : " + c2.getValue());
        System.out.println(c3.getKey() + " : " + c3.getValue());
    }
}输出:

name : findingsea
age : 24
1.1 : 2.2复制代码

可以看一下现在Som类对于不同类型的支持情况:
使用泛型类:

Som<String> som = new Som<>();
som.setValue("Hi");
//som.setValue(123);编译不通过
String str = som.getValue();
复制代码

在使用中指定具体的类型实参。

泛型接口

public interface Generator<T> {
    public T next();
}
然后定义一个生成器类来实现这个接口:

public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}
调用:

public class Main {

    public static void main(String[] args) {
        FruitGenerator generator = new FruitGenerator();
        System.out.println(generator.next());
        System.out.println(generator.next());
        System.out.println(generator.next());
        System.out.println(generator.next());
    }
}
输出:

Apple
Banana
Pear
Pear复制代码

泛型方法

在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型

如果你定义了一个泛型(类、接口),那么Java规定,你不能在所有的静态方法、静态初块等所有静态内容中使用泛型的类型参数。例如:

public class A<T> {
    public static void func(T t) {
    //报错,编译不通过
    }
}复制代码

泛型的通配符

泛型的通配符增强了方法的灵活性但也容易让人困惑。

Java中有无限定通配符<?>,上界限定通配符<? extends E>,下界限定通配符<? super E>这三种通配符。

无限定通配符

需求:打印List中的元素。List是一个泛型类,有List<String>,List<Number>,List<Object>等可能。使用List<?>通配符,可以匹配任意List泛型。
代码如下:

public static void printList(List<?> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}
复制代码

看起来很简单,但是此时的list是无法进行add操作的,因为List的类型是未知的。这就是<?>的只读性,稍后会有介绍。

在使用泛型类的时候,既可以指定一个具体的类型,如List(String)就声明了具体的类型是String;也可以用通配符?来表示未知类型,如List<?>就声明了List中包含的元素类型是未知的。 通配符所代表的其实是一组类型,但具体的类型是未知的。List<?>所声明的就是所有类型都是可以的。但是List<?>并不等同于List(Object)。List(Object)实际上确定了List中包含的是Object及其子类,在使用的时候都可以通过Object来进行引用。而List<?>则其中所包含的元素类型是不确定。其中可能包含的是String,也可能是 Integer。如果它包含了String的话,往里面添加Integer类型的元素就是错误的。正因为类型未知,就不能通过new ArrayList(?)()的方法来创建一个新的ArrayList对象。因为编译器无法知道具体的类型是什么。但是对于 List(?)中的元素确总是可以用Object来引用的,因为虽然类型未知,但肯定是Object及其子类。

有限通配符

同样是一个打印List元素的例子,但是只接受类型参数是Number及其子类。

public static void printList(List<? extends Number> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}
复制代码

和<?>一样,<? extends E>也具有只读性。

通配符<?>和<? extends E>具有只读性,即可以对其进行读取操作但是无法进行写入。

public static void printList(List<?> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
    //一下操作不可以
    list.add(1);
    list.add("123");
}
复制代码

原因在于:?就是表示类型完全无知,? extends E表示是E的某个子类型,但不知道具体子类型,如果允许写入,Java就无法确保类型安全性。假设我们允许写入,如果我们传入的参数是List<Integer>,此时进行add操作,可以添加任何类型元素,就无法保证List<Integer>的类型安全了。

因为对于List(?)中的元素只能用Object来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。 如List(? extends Number)说明List中可能包含的元素类型是Number及其子类。而List(? super Number)则说明List中包含的是Number及其父类。当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List(? extends Number)的时候,就可以使用Number类的intValue等方法。

超类型

超类型通配符允许写入,例子如下:

public static void printList(List<? super String> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
    list.add("123");
    list.add("456");
}
复制代码

这个很好理解,list的参数类型是String的上界,必然可以添加String类型的元素。

泛型与数组

Java不能创建泛型数组,以Som泛型类为例,以下代码编译报错:

Som<String> [] soms = new Som<String>[8];
复制代码

原因是像Integer[]和Number[]之间有继承关系,而List<Integer>和List<Number>没有,如果允许泛型数组,那么编译时无法发现,运行时也不是立即就能发现的问题会出现。参看以下代码:

Som<Integer>[] soms = new Som<Integer>[3];
Object[] objs = soms;
objs[0] = new Som<String>();
复制代码

那我们怎么存放泛型对象呢?可以使用原生数组或者泛型容器。

泛型的命名规范

为了更好地去理解泛型,我们也需要去理解java泛型的命名规范。为了与java关键字区别开来,java泛型参数只是使用一个大写字母来定义。各种常用泛型参数的意义如下:
E — Element,常用在java Collection里,如:List(E),Iterator(E),Set(E)
K,V — Key,Value,代表Map的键值对
N — Number,数字
T — Type,类型,如String,Integer等等





关注下面的标签,发现更多相似文章
评论