Java基础,泛型的正确使用

227 阅读4分钟

一、泛型基础使用

你有写过这样的类吗?

class StorageString {
    String date;

    private void print() {
        System.out.println(date);
    }
}

class StorageInt {
    int date;

    private void print() {
        System.out.println(date);
    }
}

StorageString storageStr = new StorageString<>(); 
storageStr.date = "泛型"; 
storageStr.print();

StorageInt storageInt = new StorageInt<>(); 
storageInt.date = 2; 
storageInt.print();

或者这样的方法?

public void storageString(String str){
    StorageString storageStr = new StorageString();
    storageStr.date = msg;
    storageStr.print();
}

public void storageInt(int d){
    StorageInt storageInt = new StorageInt();
    storageInt.date = d;
    storageInt.print();
}

因为所需处理对象的类型不同,而不停的重载类或方法,但逻辑相同。泛型就是为了解决这类问题而存在的,我们就使用泛型进行简化如下:

泛型类:

//泛型定义名称(T)随便取,26个字母都可以。
class StorageData<T> {
    T date;

    private void print() {
        System.out.println(date);
    }
}

StorageData<String> storageData = new StorageData<>();
storageData.date = "泛型";
storageData.print();

StorageData<Int> storageData = new StorageData<>();
storageData.date = 2;
storageData.print();

泛型方法:

public <T>void storageData(T d){
    StorageData<T> storageStr = new StorageData();
    storageStr.date = d;
    storageStr.print();
}

泛型还可以使用extends关键字给它设置限制,比如:

public <T extends Number>void storageData(T d){
    StorageData<T> storageStr = new StorageData();
    storageStr.date = d;
    storageStr.print();
}

这里只对方法举例,类同样适用,T extends Number表示对泛型T设置了上限,传入的参数对象类型将必须是Number的派生类。

还没有完呢!泛型是否可以同时设置多个呢?不管是在方法上还是类中都是可以的。

public <T,D,K>void storageData(T t,D d,K k){
    
}

甚至泛型的限制也是可以设置多个。

public <T extends String & Test>void storageData(T d){
    StorageData<T> storageStr = new StorageData();
    storageStr.date = d;
    storageStr.print();
}

public interface Test{

}

不过需要注意,根据java语法,指定多个界限时,只能有一个是类且必须在第一位,其它接口无限制。上面String是类,所以只能有一个且在第一位,Test是接口,像这样的后面指定多少个都没有限制。

二、 泛型通配符

先对接下来的例子类结构做一个说明,有一个Earth(地球)类,地球上有Biology(生物)类,派生的Animal(动物)类,再由Animal派生的Hippo(河马)类。大概如下:

public class Earth<T> implements Star<T> {


    @Override
    public void setBiology(T t) {

    }

    @Override
    public T getBiology() {
        return null;
    }
}

public class Biology {

}

public class Animal extends Biology{

}

public class Hippo extends Animal {
}

看两个泛型方法例子:(注意earth1和earth2对象的泛型)

image.png

image.png 两种泛型方法,被分别调用时都没有编译异常,但如果是下面这种泛型方法,就会存在编译器报错。

image.png 嗯?我们看下报错内容。

image.png 原因:没有类型变量的实例存在,使Earth<Animal>符合Earth<Biology>。

按正常的理解,Earth<Animal>肯定是能够符合Earth<Biology>参数限制的,因为Animal是Biology的派生类,那为什么这里编译器报错呢?我猜测是当一个类作为另一个类的泛型,且另一个类又作为一个泛型的限制类;简单的理解,当一个类不是直接作为泛型的限制类时,这个类的派生关系将无法体现出来。所以这里编译器识别不了。带着这个猜想,我们再看一个例子。

image.png 报同样的错,Biology类也不是直接作为参数类型,那对这个猜想是不是可以有一个更准确的定义,当一个类不是直接作为参数对象或泛型限制类,而是间接作为参数对象或泛型限制类的泛型,处在第二层指定泛型;此时此类的派生关系将无法被编译器识别。至于编译器为什么要这么设定,需要深入研究!那有解决办法没有呢?这里就是通配符出场的时候了。

image.png 这种形式也是可以的,但是当我们需要对Earth对象的操作跟泛型相关时,就无法进行了。 引出通配符的两种写法,?extends Class / ? super Class 。 我们先看第一种形式extends(上界通配符),注意earth1和earth2对象的泛型。

image.png 调用不报错,可是对Earth对象set值又报错了,这里原因也是因为编译器原因,这里没有得知准确的官方解释,只需要记住上界通配符用来安全的取值。

第二种形式super(下界通配符),注意earth1和earth2对象的泛型。

image.png 同样调用不报错,对Earth对象set值也不报错,但是仔细观察会发现get值只能接受Object对象,这里就需要记住下界通配符用来安全的赋值。

上界通配符用来安全的取值:知道最大安全取。

下界通配符用来安全的赋值:知道最小安全赋。

image.png

最后,我们同时编写两种通配符的方法。

image.png

image.png

需要注意,不管是通配符定义的上界/下界的差异,还是界限类的不同都不能触发方法重载的规则。