Java泛型知识点

1,622 阅读9分钟

一、什么是泛型

泛型就是参数化类型,即我们在定义的时候,将具体的类型进行参数化,在调用或者使用的时候,再传入具体的参数类型,我们可以将泛型用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

二、为什么要使用泛型

泛型在开发过程中经常出现,比如我们一直高频使用的List集合,我们可以这么创建一个 ArrayList 集合。

List<String> stringList = new ArrayList<>();

再看一下List的定义

public interface List<Eextends Collection<E>{...}

我们看到在List接口后面加了 <E> 对类型进行参数化,及参数是可变的,需要我们在使用的时候传入实际的参数,这样的好处是什么?

我们可以看看这么一个例子:

当我们对List 添加泛型后,我们在使用的时候,可以根据需求声明不同类型的实际参数,如果我们传入的String,我们对List集合添加数据的时候,很明显看到当添加的数据不是String类型的时候,编译器会报错,当我们在获取数据的时候,也不需要强制类型转换,会自动返回我们传入实际参数的类型。

如果我们不传入实际类型,会出现什么情况?

我们定义的List集合没有指定明确的参数,在数据添加的时候,可以看到我们可以添加不同类型的参数,编译器也没有报错,在获取数据的时候,返回的是Object类型,所以我们需要对其进行强制转换,而这个过程,很容易就会报ClassCastException异常。

所以,我们使用泛型的好处有:

1、适用于多种数据类型执行相同的代码

2、类型安全:我们使用泛型的时候,指定了特定的参数类型,这样对其类型进行限定,可以在编译期间对我们的传入的参数类型进行判断,增加了类型的安全性。

3、取消强制类型转换:我们指定明确的类型参数后,由于在编译阶段就会对类型进行约束,泛型会自动且隐式的给我们做类型转换,转换成我们指定的类型,我们不再需要关心类型的转换。

三、泛型的使用

泛型可以定义在类、接口、方法上,分别被称为泛型类、泛型接口、泛型方法。

3.1、泛型类

通过 <> 将类型变量T(大写字母都可以,不过常用的就是T,E,K,V等等)括起来,放在类名后面,泛型类运行有多个类型变量。

一个类型变量的泛型类:

/**
 * @author : EvanZch
 *         description: 一个类型变量的泛型类
 **/

public class genericClassTest<T{
    private T mData;

    public genericClassTest() {
    }

    public T getmData() {
        return mData;
    }

    public void setmData(T mData) {
        this.mData = mData;
    }
}

多个类型变量的泛型类:

/**
 * @author : EvanZch
 * description: 两个类型变量的泛型类
 **/

public class genericClassTest1<TK{
  // ...
}

3.2、泛型接口

泛型接口的定义和泛型一致

/**
 * @author : EvanZch
 *         description: 泛型接口
 **/

public interface genericInterface<T{

    getData();

    void set(T data);
}

我们在实现泛型接口可以使用下面两种方式

1、不指定具体的泛型实参

/**
 * @author : EvanZch
 *         description:实现泛型接口,不指定具体类型
 **/

public class genericInterfaceImpl1<Timplements genericInterface<T{

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

    @Override
    public void set(T data) {

    }
}

// 我们在调用的时候,再传入实际类型
genericInterfaceImpl1<String> interfaceImpl1 = new genericInterfaceImpl1();

2、指定具体的泛型实参

/**
 * @author : EvanZch
 *         description:实现泛型接口,指定具体类型
 **/

public class genericInterfaceImpl2  implements genericInterface<String{

    @Override
    public String getData() {
        return null;
    }

    @Override
    public void set(String data) {

    }
}

// 使用时候,直接创建
genericInterfaceImpl2 genericInterfaceImpl2 = new genericInterfaceImpl2();

3.3、泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类泛型类。注意泛型类中定义的普通方法和泛型方法的区别。

我们区别一下普通的方法和泛型方法

我们看到普通方法中也使用了泛型,但是它只是一个普通的方法,只是它的返回值和传入的类型是在前面已经声明过得泛型,所以,这里才可以继续使用 T 这个类型变量。

而下面这个泛型方法,首先通过 <E> 标识了它是一个泛型方法,返回值类型和传入的类型一致,通过泛型进行参数化了。

四、泛型类型变量的限制

我们使用泛型的时候,对类型进行参数化,使用的可以传入任意的类型,但是在实际使用过程中,我们可能需要对类型进行限制,在进行类型擦除的时候,会转换成我们限定的类型,比如我们要比较两个字符的大小,需要用到 compareTo 方法

int compareTo(Object o) 或 int compareTo(String anotherString)

返回值是整型,它是先比较对应字符的大小(ASCII码顺序),如果第一个字符和参数的第一个字符不等,结束比较,返回他们之间的差值,如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至比较的字符或被比较的字符有一方结束。

  • 如果参数字符串等于此字符串,则返回值 0;
  • 如果此字符串小于字符串参数,则返回一个小于 0 的值;
  • 如果此字符串大于字符串参数,则返回一个大于 0 的值。

可以看到如果我们直接这么定义,会报错,因为T为任意类型,但是 compareTo 方法定义在Comparable 接口中,我们需要限定传入的类型必须实现了 Comparable 接口

public interface Comparable<T{
    public int compareTo(T o);
}

我们可以这么写:

这里我们对类型对进行了限定,使用了通配符和指明了上界 ? extends X ,这样就限制了我们传入的参数类型必须是实现了Comparable接口,我们在调用的时候,编译器就会进行判断,我们传入的参数是否实现了Comparable接口,如果没有就会报错。

Integer 实现了 Comparable 接口

public final class Integer extends Number implements Comparable

五、泛型通配符

上面类型变量的限制中,我们使用了通配符 ? ,通配符的有三种使用方式。
<? extends X> : 类型的上界限定,参数类型是X的子类。
<? super X> :类型的下界限定,参数类型是X的超类。

<?> : 无界限定。

为了说明他们的区别,我们先新建People类、Man类、Son类,其中Man继承至PeopleSon 继承至 Man

People类:

public class People {
}

Man 类:

public class Man extends People {
}

Son 类:

public class Son extends Man {
}

再创建一个泛型类 Test<T>

public class Test<T{

    private T data;

    public T getData() {
        return data;
    }

    public void setData(T mData) {
        this.data = mData;
    }
}

查看继承关系:

很明显,我们可以看到People类和Man类是具有继承关系的,但是 Test<People>Test<Man> 之间却没有任何关系。

我们进行如下操作

可以看到set,get都没问题,因为我们将泛型直接指定为 People,而 ManSon 都是people的子类,所以我们都能set进去。

1、`? extends X` : 上界限定通配符

我在再使用 ? extends X ,指明类型的上界限定为 X,表示传入方法的类型参数必须是其本身或子类。

但是对于泛型类来说,如果内部提供了get\set方法,那么set不允许调用,即类型的上界只读。

我们可以看到,我们通过 <? extends People> 指明了类型参数上界为 People,那我们执行取操作的时候,即调用get的时候,编译器知道返回的肯定是个 x(不管是x还是x的子类),但是我们进行设置的时候,编译器只知道我们传入的是 x ,至于具体是 x 的那个子类并不知道,所以没有办法进行set操作。

总结:上不存。

主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。

2、`? super X` : 下界限定通配符

使用 ? super X ,指明类型的下界限定为 X,表示传入方法的类型参数必须是其本身或其超类。

但是对于泛型类来说,如果内部提供了get\set方法,那么set只能传入X的子类及其本身,get返回的类型是Object。

? super X 表示类型的下界,类型参数是X的超类(包括X本身),那么可以肯定的说,get方法返回的一定是个X的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是X和X的子类可以安全的转型为X。

总结:下不取

主要用于安全地写入数据,可以写入X及其子类型。

3、`?` 无限定通配符

表示类型无限制,和 T 的区别

ArrayList al=new ArrayList(); 指定集合元素只能是T类型

ArrayList al=new ArrayList(); 集合元素可以是任意类型,这种没有意义,一般是方法中,只是为了说明用法。

总结

泛型在java语言中的一种语法糖的存在,java中泛型的实现为类型擦除,是一种伪泛型。