Java基础之泛型解析

1,080 阅读9分钟

什么是泛型

泛型就是广泛的类型,同一套代码可以在多种类型中使用,使代码的可重用性更高。泛型是JDK1.5加的新特性。

为什么使用泛型

加入现在有对int类型数值求和的需求,那我们可能会这样写:

public int sumInt(int x, int y){
    return x + y;
}

这样写法没有任何问题,但是如果又来了一个新需求是需要对float类型的数值进行求和,那我们是需要再写一个sumFloat方法吗?

public float sumFloat(float x, float y){
    return x + y;
}

虽然写法上没有什么错误,但是在代码扩展性和优雅方面是不好的。

虽然在求和这个需求上对于不同的类型分别写不同的求和方法,这在功能实现上是没有问题的。再来看看接下来的需求。

将全班同学的姓名全部添加到一个List中,在JDK1.5之前是没有泛型的,实现方式是这样的:

List list = new ArrayList();
list.add("张三");
list.add("李四");
// 不小心填入了一个int型
list.add(10);

// 获取元素
String s0 = (String) list.get(0);
String s1 = (String) list.get(1);
String s2 = (String) list.get(2);

在给List存String类型的时候,如果不小心存入了非String类型的数据,编译器在编译期的时候不会报错,但是在运行期获取到List的元素的时候会报错。上面代码在获取s2时报出java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String的错误,因为存进去的时候是一个int类型,但是取出来的时候需要强转成String类型,但是int不是String的子类,所以会强转失败。

所以,使用泛型有以下几大好处:

  • 一套代码可以多处重用
  • 填写错误编译器可以在编译期就可以暴露出错误
  • 存进去是什么类型,取出来就是什么类型,无需类型强转

泛型可以使用在类、接口和方法上,他们分别称为泛型类、泛型接口和泛型方法,那应该怎么定义泛型呢?

泛型类

定义泛型类和泛型接口是一样的,只需要在类名后面增加<T>,那么这个类就是泛型类了(接口就是泛型接口),T是自己自定义的,可以随便换成其他的。

public class Bag<T> { // 包、袋子
    
}

泛型也可以指定多个类型

public class MyMap<K, V> {
    
}

继承/实现一个泛型类有两种方式,因为泛型类和泛型接口方式基本一样,这里就以泛型类来举例:

  • 子类不确定其泛型

    public class FruitBag<T> extends Bag<T> { // 水果袋
    }
    

    水果袋也没有确定该装哪种水果,因此也需要在FruitBag后面增加<T>。在实例化的时候就需要指明其类型

    FruitBag<Apple> appleBag = new FruitBag<>();
    
  • 子类确定其泛型

    public class FruitBag extends Bag<Apple> { // 水果袋
    }
    

    水果袋子指明了只能装苹果,只需要在父类后面增加<Apple>即可。在实例化的时候和普通的类一样

    FruitBag appleBag = new FruitBag();
    

泛型方法

在方法中含有<T>的方法就是泛型方法(不包含参数中的<T>),要区分泛型方法和普通方法的区别。需要注意的是,声明泛型方法的类不一样是泛型类,泛型类中也可以不声明泛型方法。

接下来看看泛型方法和普通方法的区别:

// 只演示泛型语法,忽略一些可能带来的异常问题
public class Bag<T> {
    private List<T> list = new ArrayList<>();

    public void put(T t) {
        list.add(t);
    }

    public T get(int index) {
        return list.get(index);
    }
    
    public void set(Bag<T> bag) {
    }
}

putget方法中没有<T>,因此是它们都是普通方法,而set方法中虽然有<T>,但是它是在参数中的,因此它依旧是一个普通方法。

public class Bag<T> {
    public <T> T set(T t) {
        return t;
    }
}

上面的set方法才是一个泛型方法,在public后面带有<T>,这里要注意的是,set方法中的TBag<T>T没有关系,它们是独立的类型,只是碰巧都是用T来表示了。

限定类型变量

假设我们现在有一个需求是比较水果的重量,Fruit类有一个weight属性来表示水果的重量,我们要通过比较weight属性来比,因此需要实现Comparable接口并重写其compareTo方法,在该方法中写判断的逻辑。

// Fruit.java
public class Fruit implements Comparable<Fruit> {
    private float weight;
    
    public Fruit(float weight) {
        this.weight = weight;
    }
    
    @Override
    public int compareTo(Fruit o) {
        float result = this.weight - o.weight;
        if (result > 0) {
            return 1;
        } else if (result == 0) {
            return 0;
        } else {
            return -1;
        }
    }
}

// GenericTest.java
public class GenericTest {
    public static void main(String[] args) {
        Fruit a = new Fruit(10);
        Fruit b = new Fruit(11);
        Fruit max = max(a, b); // 返回较重的水果
    }
    
    private static Fruit max(Fruit a, Fruit b) {
        if (a.compareTo(b) >= 0) return a;
        else return b;
    }
}

上面的max方法固定了只能比较Fruit的重量,但是现在如果我想比较苹果、香蕉或者非水果类的重量呢?如果定义成以下泛型:

会提示泛型T没有compareTo方法。这时就需要对泛型进行类型限定了,让泛型T继承自Comparable接口

private static <T extends Comparable<T>> T max(T a, T b) {
    if (a.compareTo(b) >= 0) return a;
    else return b;
}

给泛型增加了类型限定,现在只要是实现了Comparable接口的类都可以作为参数传进来进行比较。Comparable表示绑定类型,T表示绑定类型的子类型,子类型和绑定类型可以是类也可以是接口。

子类型和绑定类型都可以是一个或者多个,但是绑定类型的类只能有一个且只能放在第一个,多个的话后面只能是接口,多个绑定类型用&连接。

public class A {}

public interface B {}

private static void <K, T extends A & B> void min(T a, T b){
    
}

min方法定义了两个子类型,其中子类型T继承类A & 接口B,需要注意的是子类型K只是一个普通的类型,并没有继承。

泛型的约束与局限性

泛型也有一些约束和限制。

  • 泛型不能使用基本类型

不能使用基本类型,应该使用类类型和基本类型的包装类型

  • 运行时类型判断不能携带泛型

  • 不能使用static修饰泛型

因为泛型是在对象创建的时候才确定,而对象创建会先执行静态修饰的部分然后才是构造器等,所以在对象初始化之前就已经执行了static部分,所以虚拟机就不知道泛型具体是指什么类型了。

  • 不能初始化泛型数组

```java
public class Fruit implements Comparable<Fruit> {}
public class Apple extends Fruit {}
public class Bag<T extends Fruit> {}
public class FruitBag<T extends Fruit> extends Bag<T> {}
public class AppleBag extends FruitBag<Apple> {}
```
  • 不能实例化泛型变量

  • 泛型类不能继承ExceptionThrowable或其子类

  • 不能捕获泛型

根据上面的代码应该可以大致得出:泛型类型不能被捕获,但是可以抛出泛型类型异常。

泛型的继承规则

现有FruitApple两个类,Apple派生自FruitAppleFruit的子类,但是List<Apple>却不是List<Fruit>的子类。但是泛型类可以继承或实现其它泛型类,如:

public class ArrayList<E> extends AbstractList<E> implements List<E>

泛型通配符

泛型有2中通配符,一个是上界通配符? extends Class,另一种是下界通配符? super Class

  • ? extends Class

    上界通配符,表示传入的类型必须是Class的子类或者是Class本身。举个例子:

    public class Bag<T> {
        private T t;
    
        public T get() {
            return t;
        }
    
        public void set(T t) {
            this.t = t;
        }
    }
    
    public class Fruit {}
    
    public class Apple extends Fruit {}
    

定义了三个类,一个泛型类Bag<T>,一个水果类Fruit,一个苹果类AppleApple继承Fruit。使用上界通配符定义了一个包对象,代表包里面可以装水果,只要是水果都可以装,然后有创建了一个水果对象和苹果对象。但是在调用bag.set方法的时候编译器不通过,但是在bag.get的时候可以返回一个Fruit对象。这是为什么呢?

因为? extends Fruit已经确定了上界是Fruitset的时候可以传入Fruit对象,也可以传入Apple对象,这样编译器就不确定具体是哪个类的对象了,而泛型的意义就是要确定某一种类型,这与泛型的设计初衷相违背了;get的时候编译器可以明确它取出来就是一个Fruit。所以不允许set,但是可以get

主要用于安全地访问数据,可以访问数据但是不能写入数据。

  • ? super Class

    下界通配符,表示传入的类型必须是Class本身或者是Class的父类。类的定义还是和上面一样,这里再增加一个Apple的子类GreenApple

    public class GreenApple extends Apple {}
    

现在的bag对象用的是下界通配符,表示包里只能装Apple以及Apple的父类。创建了一个水果对象,一个苹果对象,一个青苹果对象。在分别调用bag.set时,设置fruit的时候报错,添加applegreenApple就可正常,bag.get返回的却是 Object

因为? super Apple已经确定了下界是Apple,只有AppleApple的子类才能安全的转成Apple,因此能设置下界类和下界类的子类(下界类此处表示的是Apple)。而其父类可以有多个但是并不能安全的转换成Apple,因此它只能set下界类本身及其子类。在get获取的时候虚拟机并不知道具体是什么父类,但是有一点很明确的是,最终父类一定是Object

无限定通配符

还有一种通配符叫做无限定通配符,用?表示,它表示没有限制,可以看成任意类型,并没有什么实质性的作用。

虚拟机是如何实现泛型的

泛型是从JDK1.5才有的,那么为了兼容以前的JDK,Java采用了泛型擦除来兼容。那什么是泛型擦除呢?简单的来说就是Java代码在编译之后是不会保留泛型的类型的。比如List<String>在编译后,字节码中变成了List,再也没有了泛型类型了。也就是说在运行时,List<String>List<Double>其实是一种类型。写个代码验证一下:

public static void main(String[] args) {
    List<String> listString = new ArrayList<>();
    List<Double> listDouble = new ArrayList<>();
    System.out.println(listString.getClass());
    System.out.println(listDouble.getClass());
}

// 输出
class java.util.ArrayList
class java.util.ArrayList

虽然定义的是两个类型不同的List,但是获取到的类型都是ArrayList,这很好地 证明了泛型擦除的机制。

通过使用ASM Bytecode Viewer插件将代码转成字节码来看,

从椭圆形框中可以看出,左边Java代码中的第10行和第11行,泛型类型都变成了Object,而不是定义时候的类型。

< T extends X> 与 <? extends X>的区别

  • 前者可用于定义泛型类,而后者不行

    < T extends X><? extends X>
  • 前者在运行时确定了具体的一种类型,该类型是X或者X的子类中的某一个,而后者是X后者X的子类都可以

    class Bag<T> {}
    class Food {}
    class Fruit extends Food {}
    class Apple extends Fruit {}
    
    < T extends X><? extends X>
  • 前者在声明泛型类、泛型接口、泛型方法时使用,后者在声明变量、方法形参时使用