1.什么是泛型?
我们在java编程的时候使用到泛型可能就是在集合中和其他的一些框架中使用到过泛型,对泛型的了解不是很深,那么泛型到底是什么呢?
泛型,即参数化类型。泛型就类似于我们没有给它指定一个固定的参数类型,它是一个可变的参数类型,在我们调用或者使用它的时候才会传入具体的类型。
泛型的本质是为了参数化类型,我们在使用泛型的时候,操作的数据类型被指定为一个参数,这个参数类型可以在类、接口和方法中,分别被称为泛型类、泛型接口和泛型方法。
2.泛型的特性
它的特性就是类型擦除,Java中的泛型基本上都是在编译器这个层次来实现的,生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的 List和 List等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。
我们通过一个例子来看一下:
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
//在编译之后的程序会采取去泛型化的措施。也就是说java中的泛型,只在编译阶段有效。在编译过程中,正确校验泛型结果后
//会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
//泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
if (classStringArrayList.equals(classIntegerArrayList)) {
System.out.println("泛型测试,泛型相同");
}
测试结果:
所以通过这个例子我们也可以验证泛型在逻辑上看是不同的类型,但是实际上都是相同的类型。
3.泛型类
定义泛型类也是比较简单的,我们只需要在类名的后面加上这个T你可以改成E、V等字母。
我们首先创建一个Generic泛型类:
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
使用泛型,这里可以分为2种,一种是传入实参类型,另外一种是不传入实参类型。
传入实参类型:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
Generic<String> genericString = new Generic<String>("key_value");
不传入实参类型:
Generic generic = new Generic("111111");
Generic generic1 = new Generic(44444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
测试结果:
注意事项:
1.泛型的类型参数只能是类类型,不能是简单类型。
2.不能对确切的泛型类型使用instanceof操作。
4.泛型接口
定义一个泛型接口和定义一个泛型类基本是一样的,都是在接口名后面加上。
定义一个泛型接口:
public interface Generator<T> {
public T next();
}
定义了一个接口过后我们来实现这个接口,实现接口又分为了未传入泛型实参和传入泛型实参。
未传入泛型实参:
public class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
未传入泛型实参需要在声明类的时候将也加在后面,不然编译会报错。
传入泛型实参时:
public class FruitGenerator implements Generator<String>{
private String[] fruits = new String[] {"Apple","Banana","Pear"};
@Override
public String next() {
Random random = new Random();
return fruits[random.nextInt(3)];
}
}
测试,使用传入泛型参数测试:
FruitGenerator fruitGenerator = new FruitGenerator();
System.out.println(fruitGenerator.next());
测试结果:
5.泛型通配符
我们定义一个方法,参数使用通配符,使用通配符就是就是将字母换成了问号,没有一个具体的类型:
public static void showKeyValue(Generic<?> obj) {
System.out.println("泛型测试-通配符,key is " + obj.getKey());
}
然后实例化2个泛型对象:
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
测试:
showKeyValue(gNumber);
showKeyValue(gInteger);
结果:
说明没有问题,它会自动兼容不同的类型。
但是,如果我们不使用通配符那么会怎么样呢?我们还是使用刚才的例子看一下,将方法中参数的<?>换成:
public static void showKeyValue(Generic<Number> obj) {
System.out.println("泛型测试,key is " + obj.getKey());
}
同样实例化2个泛型对象:
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
测试结果:
我们发现泛型类为Integer的报错了,由此可以得出结论:同一种泛型可以对应多个版本,它的参数类型是不确定的。不同版本的泛型类实例是不兼容的。
6.泛型方法
泛型方法相比较于泛型类有点复杂,这里需要注意的是我们不要将泛型通配符方法、泛型类参数方法和泛型方法弄混了,比如方法的参数为Generic<?> obj,这并不是一个泛型方法,而是使用的泛型通配符。
接下来,我们通过一个例子来看一下什么是泛型方法:
public class GenericFruit {
static class Fruit{
@Override
public String toString() {
return "fruit";
}
}
static class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
static class Person {
@Override
public String toString() {
return "Person";
}
}
static class GenerateTest<T> {
public void show_1(T t) {
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型
public <E> void show_3(E t) {
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型
public <T> void show_2(T t) {
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
//因为Apple是Fruit的子类所以不会报错
generateTest.show_1(apple);
//会报错,因为泛型类型实参指定的friut,而传入的参数是person
// generateTest.show_1(person);
//使用这两个方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
上面代码中的注释讲解了泛型方法的使用,需要注意的是如果泛型类的实参和泛型方法传入的实参类型不一样或者类型不是父子关系则会报错。
7.泛型上下边界
上界通配符: <? extends Fruit>,什么是上边界,顾名思义这个泛型的类型参数最大边界为Fruit,如果是Fruit的子类都可以通过编译,如果是Fruit的父类则不可以。
下界通配符:<? super Fruit>,什么是下边界,顾名思义这个泛型的类型参数最小边界为
Fruit,如果是Fruit的父类都可以通过编译,如果是Fruit的子类则不可以。
我们通过一个方法来验证,我们还是使用泛型类Generic作为参数,前面的代码中有这个类代码:
public static void showKeyValue1(Generic<? extends Number> obj) {
System.out.println("泛型测试-上下边界,key is " + obj.getKey());
}
定义4个泛型类:
Generic<String> generic4 = new Generic<String>("111111");
Generic<Integer> generic5 = new Generic<Integer>(22222);
Generic<Float> generic6 = new Generic<Float>(2.4f);
Generic<Double> generic7 = new Generic<Double>(2.56);
调用方法测试:
// showKeyValue1(generic4);//会报错,因为String 类型并不是Number类型的子类
showKeyValue1(generic5);
showKeyValue1(generic6);
showKeyValue1(generic7);
测试结果:
其中5,6,7都可以运行,但是4中会在编译时报错,因为它的上边界为Number,但是String并不是Number的子类,所以会报错。