聊聊 Java 泛型的使用

1,658 阅读14分钟

在刚开始接触泛型时,我一直有个疑惑,就是为什么我不能为静态字段定义泛型类型,我也尝试过,只是无法通过编译并且会得到“无法从静态上下文中引用非静态‘【类型变量 V”,这时我才有一种模糊的意识,原来泛型信息是属于对象的,而不是类。

public class NoStaticFieldGenerics<V> {

    //  尝试定义静态泛型字段
    public static V staticGenericFiled;
    
    //  上面的错误信息和在静态方法中尝试反问非静态字段或方法是一样的
    public static void staticGetValue() {
        getValue();
    }

    private V value;

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }
}

其实后来想了想很好理解,泛型信息的参数化或实例化基本上是在创建对象才指定的,一方面两者生命周期不同,另一方面就算真的有这种静态泛型字段,那我该听谁的呢?因为我就这么一份,而你们每个具体对象中泛型类型又不相同,所以我认为没有静态的泛型字段确实合情合理。

List<String> strList = new ArrayList<String>();
List<Integer> intList = new ArrayList< Integer >();

开头先讲了一个自己使用泛型的困惑,但是在实际开发中自己使用泛型可并不只有这么一个困惑,而是一个接着一个,这篇内容并不会深入讲解原理,而是讲一些常见的使用和对应使用时的思考,而原理性的内容会放到下一篇着重介绍。

进入正题,下面我们先来看看泛型的一些常见使用:

1.直接使用已有泛型:

List<String> strList = new ArrayList<String>();
strList.add("a");
//  下面这行是无法通过编译的,因为已经声明了这个 List 的泛型类型是 String,所以无法插入其他类型
strList.add(123);

2.泛型类或接口:

当我在使用一个新东西的时候,如果我不知道怎么使用它,那我会先去看看系统或者别人怎么用它,所以这时我直接点开 List 的源码,看到它的声明是什么样的。

//  如果我们不知道如何定义时,可以看看系统或别人是如何做的
public interface List<E> extends Collection<E> {
  ...
}
//  这是我们自定义的用来存放货物的集装箱
public interface Container<GOODS> {
    
    void put(GOODS goods);
}

当然这里的 GOODS 可以是任意的字符,常见的有 T、E、K、V 等,它其实只是一个使用的代号,尽量不要和其他已有类重名就行,哪怕真的乱来写 String 也是可以的,但是这样话,想要使用和泛型类型名称冲突的类就只能使用时写对应类的全路径了。

3.继承或实现已有泛型类或接口:

下面代码中错误写法看上去是我定义了一个装电子产品的集装箱,并且我也声明了对应的泛型,但是为什么系统提示重写方法时会生成一个参数类型是 Object 的方法?我的 EP 类型呢?这种其实错误用法,忘了 Container 本身也有泛型,我们只是在 ElectronicProductContainer 上面声明了泛型,并没有将其应用到 Container 中,所以这时可以先抬头看看上面 List 是如何声明的。

如果说正确写法的话,并不仅限于 List 的那一种,那种的灵活性更大,我可以写代码的时候动态指定我想要使用的类型,比如我想装笔记本电脑,或是装电视进去,当然我也直接指定这次的泛型是什么,就像写法二那样直接指定我们要使用类型。

但不管怎么写,如果我们要继承或实现已有的泛型,那么就必须在父类或接口边上声明这次要使用的泛型类型是什么,这样才不会让泛型信息丢失。

//  错误写法,装电子产品的集装箱
public class ElectronicProductContainer<EP> implements Container {
    
    @Override
    public void put(Object o) {
        
    }
}

//  正确写法一:
public class ElectronicProductContainer<EP> implements Container<EP> {

    @Override
    public void put(EP ep) {
        
    }
}

//  正确写法二:
public class PhoneContainer extends ElectronicProductContainer<Phone> {

    @Override
    public void put(Phone phone) {

    }
}

public abstract class ElectronicProductContainer<EP> implements Container<EP> { }
public class Phone { ... }

4.泛型约束其一:

先来看看没有任何约束的情况:

//  这是一开始的我们刚刚声明好的泛型类,我们希望这个泛型类里面只能存放电子相关的类型
public class ElectronicProductContainer<EP> implements Container<EP> {

    @Override
    public void put(EP ep) {
        //  这里的 ep 只能调用 Object 相关的方法
    }
}

//  前两个的使用情况是我们期待的,放手机和 PC 的集装箱
ElectronicProductContainer<Phone> phoneEPContainer = new ElectronicProductContainer<>();
ElectronicProductContainer<PersonalComputer> pcEPContainer = new ElectronicProductContainer<>();
//  不小心乱入了军火???是谁再乱来???想要搞死我???
//  有内鬼终止交易
ElectronicProductContainer<Weapon> weaponEPContainer = new ElectronicProductContainer<>();

所以有时我们希望对声明的泛型做出一些约束,比如只能放某些类型的数据,或希望可以调到它一些比较具体的方法,而不只有 Object 的方法,就像上面例子的中的代码,当我们试图调用 put(EP ep) 里参数 ep 的方法时,发现只有 Object 方法,这其实也对,因为泛型信息在运行时被擦除了,所以并不知道确切的类型信息,为了能让我们这么方便的使用其实也是因为编译器帮我们做了很多处理,这里不细说,先来看看,如果想要约束存放类型或想要调用某个类或接口的方法时,该如何来处理,具体例子如下:

//  对于上面的类我要做出一些微小调整
//  把 <EP> 改为了 <EP extends ElectronicProduct>
public class ElectronicProductContainer<EP extends ElectronicProduct> implements Container<EP> {

    @Override
    public void put(EP ep) {
        //  这里也可以调用 ElectronicProduct 的方法了
        System.out.println(ep.productName());
        System.out.println(ep.productPrice());
    }
}

public interface ElectronicProduct {

    String productName();
    String productDate();
    float productPrice();
}

//  前面两行依然正常,因为 Phone 和 PersonalComputer 都实现了 ElectronicProduct 接口
ElectronicProductContainer<Phone> phoneEPContainer = new ElectronicProductContainer<>();
ElectronicProductContainer<PersonalComputer> pcEPContainer = new ElectronicProductContainer<>();
//  第三行没法通过编译,因为并不符合我们声明的约束条件
//  除非我们中出了卧底,把约束条件悄悄告诉了敌人
ElectronicProductContainer<Weapon> weaponEPContainer = new ElectronicProductContainer<>();

可以理解为我对 EP 这个泛型类型做了约束,我告诉编译器它是什么,这个泛型可以被实例化成什么,这里的 EP 只能被实例化成 ElectronicProduct 以及它的子类,当然,extends 后面可以跟着接口也可以跟着类,并且还可以同时存在多个不同约束,不过本着 Java 类单继承的特性,所以 extends 后面只能跟一个类,不过接口可以跟多个,如果有多个约束,要用 & 隔开,就像这样 T extends A & B & C。

5.泛型约束其二:

再讲下面类型约束前,先来讲讲数组,《Effective Java》中讲到数组与泛型两个重要的不同,一个是数组是协变类型的,意思就是说,如果 Sub 是 Super 子类,那么 Sub[] 是 Super[] 的子类型,而泛型是不可变,对于任意两种类型 T1 和 T2 ,List 与 List 并不能认为谁是谁的子类型,谁是谁的父类型,听上去是不是感觉泛型好菜啊,这都不行,不过这里看文字理解起来比较晦涩,不如直接看代码:

//  由于数组是协变的,所以 Long[] 可以赋值给 Object[] 
//  这样是不安全的,但也是可以编译通过的,然后我们在不知不觉中在 Long[] 中插入了一个 String,
//  但只有在运行时才能发现错误,系统会抛出 java.lang.ArrayStoreException
Object[] oArr = new Long[2];
oArr[0] = 1;
oArr[1] = "1";

//  一般来说我们的泛型会这样写
List<String> strList= new ArrayList<String>();
//  Java 1.7 中,泛型加入了有限的类型推断,所以后面的 <String> 可以简化为 <>
List<String> strListNew= new ArrayList<>();
//  让我们回到老版本的写法,来看看下面这样的写法
//  泛型是不可变的因此不支持这样的写法,所以这里直接无法通过编译
List<ElectronicProduct> electronicProducts = new ArrayList<Phone>();

数组的协变特性看上去还蛮不错的,不过实际用上去会出现类型安全问题,尤其是当我不小心往 Long 数组中插入了一个 String 元素时,那么代码会在运行时抛出 ArrayStoreException 异常,因为具体类型是在运行时才知道的,而编译时,只看到 Object[] 类型,也就导致了运行时才发现这个问题,但泛型由于不可变特性,两种类型并不相等,所以不能进行赋值,也就无法通过编译,所以书中也说,与其说是泛型有缺陷,不如说数组才是有缺陷的,因为泛型更早的帮助我们完成了类型安全检查,避免了某些错误在运行时才暴露。

我们真的没法让 List electronicProducts = new ArrayList(); 它通过编译吗?其实稍作修改就可以,我们通过泛型中的有限通配符类型可以做到,不过虽然可以通过编译了,但是其它的操作也被受限了,有一种我变强了也变秃了的感觉,为什么会如此,那它这样的写法岂不是没什么用了?别急,一点点来看,先看为什么会约束你插入数据,因为这样声明的你的赋值内容是不可控的,只要是 ElectronicProduct 以及子类的 List 都可以进行赋值,就像下面代码块中最后四行的代码一样,假如它能通过编译,那我先赋值了 Phone 类型的列表并插入数据,然后别人又操作这个列表并赋值了 PC 类型的列表给它,最后我还试图拿出来我刚才插入的 Phone 数据,那肯定是有问题,所以为了防止这种情况出现,直接在插入数据时产生编译时错误就可以避免这个问题在运行时才被发现了。

//  虽然可以通过编译了,但是它的操作也被受限了
List<? extends ElectronicProduct> electronicProducts = new ArrayList<Phone>();
//  但你在试图插入数据的时候会发现这时无法通过编译的
electronicProducts.add(new Phone());

//  哪怕改成这样也没法插入
List<? extends Phone> electronicProducts = new ArrayList<Phone>();
//  这里依然无法编译
electronicProducts.add(new Phone());

//  比如这样的代码就存在风险
List<? extends ElectronicProduct> electronicProducts = new ArrayList<Phone>();
electronicProducts.add(new Phone());
//  别人修改了引用
electronicProducts = new ArrayList<PersonalComputer>();
//  我在使用使用时以为还是存储的手机,这时候肯定会就会出错了
Phone phone = electronicProducts.get(0);

那既然 ? extends XXX 不是这么用的,那么它的意义是什么呢?让我们来看看下面这段代码就知道它的用途是什么了,? extends XXX 确实放宽了对泛型类型的要求,如果某个方法的参数是 List eps,那么就它只能接收这一种类型的泛型列表,现在它可以接收任何 ElectronicProduct 或子类的 List,并打印出它们的电子产品信息,也就是说,这样写可以让使用更灵活,“虽然我不知道这个列表的具体信息,但是我知道它应该是 ElectronicProduct 或它的子类型的列表,这样我就可以进行产品信息打印了”,所以这么看 ? extends XXX 还是有它实际上的作用的,而且结合上面和下面代码我们可以出它的并不适合数据插入,因为存在风险,而更适合数据返回,也就是泛型类型当做参数的方法它都不能使用,而是只能使用泛型类型当做返回值的情况,比如 List 中 add 的参数是泛型类型,get 的返回值是泛型类型。

public static void main(String[] args) {
        List<Phone> phones = new ArrayList<>();
        List<PersonalComputer> pcs = new ArrayList<>();
        printEPInfo(phones);
        printEPInfo(pcs);
}

//  打印电子设备相关信息
public static void printEPInfo(List<? extends ElectronicProduct> eps) {
    for (ElectronicProduct ep : eps) {
        System.out.println(ep.productName());
        System.out.println(ep.productPrice());
    }
}

如果理解了上面所说的,那么 ? super XXX 就好理解好多,经历刚才我想把 Phone 列表赋值给 ElectronicProduct 的冲动后,那我们能不能反过来呢?当然可以,这时就要配合 ? super XXX 使用了,才能避免编译时错误,因为它的作用也是放宽限制,只要是 Phone 本身或它的超类的列表都是赋值给它,不过这时,编译器只知道我可以给它赋值它本身及以上的列表类型,那么不论我怎么往列表中插入数据都是没问题,大不了可以先上转型,但如果是取出数据就要谨慎了,就拿下面代码中的例子来说 ElectronicProduct 列表中可以存了它的别的子类,这时我还试图取出数据时肯定是不安全的,取出什么类型并不能得到保障。

//  直接写依然无法通过编译
List<Phone> electronicProducts = new ArrayList<ElectronicProduct>();

//  配合 ? super XXX 就可以通过以编译了
List<? super Phone> electronicProducts = new ArrayList<ElectronicProduct>();
electronicProducts.add(new Phone());
System.out.println(electronicProducts.get(0));
//  这里编译出错
Phone phone = electronicProducts.get(0);

//  被向上转型存入列表
List<ElectronicProduct> electronicProducts = new ArrayList<ElectronicProduct>();
electronicProducts.add(new Phone());
electronicProducts.add(new PersonalComputer());

//  ? super XXX 举个例子的使用场景
public <EP> void addEPToList(EP ep, List<? super EP> eps) {
        eps.add(ep);
}

? super XXX 是不是看着和 ? extends XXX 看着是正好相反,? super XXX 更好的使用场景是我只使用泛型类型作为参数的方法从而放宽限制,? extends XXX 更好的使用场景是我只使用泛型类型返回值的方法从而放宽限制,前者更适合当一个输入者,不断往里面放东西,后者更适合当一个输出者,你要是吧,给你给你。当然和泛型无关的方法大家都可以调用的。

还有更宽松的限制符就是直接使用 ?,来看看下面的代码,这时插入和取出都会遇到编译时错误,只可以使用一些和泛型类型无关的方法。

List<?> electronicProducts = new ArrayList<ElectronicProduct>();
//  不行
electronicProducts.add(new Phone());
//  还是不行
Phone phone = electronicProducts.get(0);
electronicProducts.size();

public void test(List<?> list) {
    //  比如拿个大小
    list.size();
}

6.泛型方法:

说了那么多,还没提到泛型方法,上面的那些代码中的方法其实都只是使用到了声明的泛型类型而已,并没有真正涉及到泛型方法,而且内容开头的疑惑也提到了泛型信息是属于对象的,而不是类,那我们来看看泛型方法如何定义,其实泛型方法的泛型类型是需要单独定义的,所以下面的例子中,NoStaticFieldGenerics 的 V 和 void print(V v) 中的 V 虽然名字一样,但是确实不同的,泛型方法需要单独声明它的泛型类型,而且如果真的和外面的泛型类型重名的话,会覆盖掉外面的泛型类型,就像是局部变量和对象字段重名时,局部变量优先级更高是一样的,不过既然泛型方法的泛型类型是需要单独声明的,那么它也就和对象无关了,只和具体调用时的信息有关,既然和对象无关,那么虽然静态泛型字段不存在,静态泛型方法还是可以定义和使用的。

public class NoStaticFieldGenerics<V> {

    private V value;

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }

    //  这里重名的泛型类型会覆盖掉类边上的泛型类型
    //  所以不建议这么写
    public <V> void print(V v) {
        System.out.println(v);
    }

     //  这样写才正常
     public static  <E> void staticPrint(E v) {
        System.out.println(v);
    }
}

//  具体泛型类型为 String
NoStaticFieldGenerics<String> n = new NoStaticFieldGenerics<>();
//  传入 Integer,输出 1
n.print(1);

写到这里差多把泛型的基本使用都覆盖到了,希望我把这些内容清楚地传达给了你。