Java中的泛型

219 阅读7分钟

开始

Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。作者印象中第一次接触泛型的时候应该是在学习集合的时候,今天再详细的的讨论下泛型。

package com.wlee.test;

import java.util.ArrayList;
import java.util.List;

public class GenericTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("123");
        list.add("abc");
        list.add(123);	//这里明显会报错
    }
}

上面很简单的例子,这里可以看出来在代码编写阶段就已经报错了,不能往 String 类型的集合中添加 int 类型的数据。那可不可以往 List 集合中添加多个类型数据呢,那肯定是可以的,其实我们可以把 List 集合当成普通的类也是没问题的:

package com.wlee.test;

import java.util.ArrayList;
import java.util.List;

public class GenericTest {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("123");
        list.add("abc");
        list.add(123);	//这里就没有问题了
    }
}

以上代码,就可以得知不定义泛型也是可以往集合中添加数据的,所以说泛型只是一种类型的规范,在代码编写阶段起一种限制

下面我们通过例子来介绍泛型背后数据是什么类型:

package com.wlee.test;

public class BasePojo<T> {
    T val;

    public void setVal(T val) {
        this.val = val;
    }

    public T getVal() {
        return val;
    }
}

上面定义了一个泛型的类,然后我们通过反射获取属性和 getValue 方法返回的数据类型:

package com.wlee.test;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class GenericTest {

    public static void main(String[] args) {
        //实例对象 并 赋值
        BasePojo<String> basePojo = new BasePojo<String>();
        basePojo.setVal("WorkerLee");

        try {
            //获取属性上的泛型类型
            Field fieldVal = basePojo.getClass().getDeclaredField("val");
            Class<?> type = fieldVal.getType();
            String typeName = type.getTypeName();
            System.out.println("type:" + typeName);

            //获取方法上的泛型类型
            Method getVal = basePojo.getClass().getDeclaredMethod("getVal");
            Object objInvoke = getVal.invoke(basePojo);
            String methodName = objInvoke.getClass().getName();
            System.out.println("methodName" + methodName);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:

type:java.lang.Object
methodNamejava.lang.String

从结果上看到通过反射获取到的属性是 Object 类型的,在方法中返回的是 String 类型,其实它在 getVal 方法里面实际是做了个强转的动作,将 Object 类型的 val 强转成 String 类型。说白了这里就是咱们经常说“装箱,拆箱”那些事。

泛型只是为了约束我们规范代码,而对于编译完之后的 class 交给虚拟机后,对于虚拟机它是没有泛型的说法的,所有的泛型在它看来都是 Object 类型。其实很好理解,我把上面定义的 BasePojo 稍微修改:

package com.wlee.test;

public class BasePojo<T extends String> {
    T val;

    public void setVal(T val) {
        this.val = val;
    }

    public T getVal() {
        return val;
    }
}

这里我们将泛型加了个关键字 extends,extends 是约束了泛型是向下继承的,最后我们通过反射获取 val 的类型是 String 类型的,加上了 extends 关键字其实就是约束泛型是属于哪一类的。所以我们在编写代码的时候如果没有向下兼容类型,会警告报错。

//这里用Long肯定就直接报错了
BasePojo<Long> basePojo = new BasePojo<Long>();

既然泛型其实对于 JVM 来说都是 Object 类型的,咱们直接将类型定义成 Object 不就行了,这种做法是没问题的。但是您拿到 Object 类型值之后不是还得自己进行强转,定义了泛型减少了我们的强转工作,而将这些工作交给了虚拟机岂不是美滋滋。那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。

泛型用在哪

常见的泛型主要有作用在普通类上面,作用在抽象类、接口、静态或非静态方法上:。

方法上面的泛型:

public <T> T getData() {
    return null;
}

类上面的泛型:

public class Result<T> {
	public String code;
    public String msg;
    public T data;
}

抽象类或接口上的泛型:

//抽象类泛型
public abstract class TestService<T> {
    public List<T> resultList;
}
//接口泛型
public interface TestService<T> {
    public T delete();
}

//二级抽象类
public abstract class TestService<K extends Common, V> implements Base<K, V> {
}
//二级接口
public interface TestService<K extends Common, V> extends Base<K, V> {
}

多元泛型:

public interface TestService<K, V> {
    public void setKey(K k);

    public V getVal();
}

泛型中通配符

我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V,? 等等。本质上这些通配符没太大区别,只不过是编码时起一种代码约定的作用。一般情况下 T,E,K,V,?是这样约定的:

  • ?表示不确定的 Java 类型
  • T (type) 表示具体的一个 Java 类型,而且 T 完全可以用 A-Z 之间的任何一个字母代替
  • K V (key value) 分别代表 Java 键值中的 Key Value
  • E (element) 代表 Element

?无界通配符

通配符其实在声明局部变量时是没有太多的意义,但是当你为一个方法声明一个参数时,它是挺重要的。举个例子吧,比如有一个父类 Animal 和 两个子类 Bird 和 Dog:

public class Animal {
}

public class Bird extends Animal {
}

public class Dog extends Animal {
}


public class GenericTest {

    //通配符用来定义变量时是没有太多意义
    //List<Animal> listAnimals1;
    //List<? extends Animal> listAnimals2;
    
    //主要按一下两个方法 test1 和 test2
    public static void test1(List<? extends Animal> animals) {
        //方法内容略
    }

    public static void test2(List<Animal> animals) {
        //方法内容略
    }
    
    public static void main(String[] args) {
        List<Bird> birds = new ArrayList<Bird>();
		
		//调用test1不报错
        GenericTest.test1(birds);

		//调用test2肯定是报错的
        GenericTest.test2(birds);	//报错
	}
}

以上代码很好理解,test2 方法要求需要一个 Animal 的 List,传递 Bird 的 List 肯定报错。所以,对于不确定或者不关心实际要操作的类型,可以使用无界通配符(就是那个问号),表示可以持有任何类型。像 test1 方法中,限定了上届,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 test2 就不行。

上界通配符与下界通配符

上界通配符与下界通配符其实很好理解。

上界通配符,其实就是用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类,用来限定泛型的上界。

下界通配符,就是用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object,用来限定泛型的下界。

代码示例:

//水果类 继承 食物
public class Fruit extends Food {

}

//桔子类 继承 水果
public class Orange extends Fruit {

}

//定义了一个 篮子 类
public class Basket<T>{

    private T item;

    public void set(T item) {
		this.item = item;
	}

    public T get() {
		return item;
	}
}

现在我们定义一个"Fruit Basket 水果篮子",按照现实逻辑,Orange Basket 当然是 Fruit Basket 的一种。 按照 Java 中父类和子类的使用规范,子类 Orange Basket 当然可以赋值给父类 Fruit Basket。代码就是这样的:

Basket<Fruit> fruitBasket = new Basket<Orange>();	//ide报错,类型无法转换

以上代码肯定是报错的,其实很好理解,Orange 和 Fruit 是父子关系,但是相应的 Basket 却不是父子关系。那怎么解决这个问题?使用上界通配符来处理这种关系:

Basket<? extends Fruit> fruitBasket = new Basket<Orange>();

反过来思考,把 Fruit Basket 赋值给 Food Basket 是否可以?使用下界通配符来处理这种关系:

Basket<? super Fruit> orangeBasket = new Basket<Food>();

?和 T 的区别

简单总结下:T 是一个“确定的”类型,通常用于泛型类和泛型方法的定义。? 是一个“不确定”的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。通过 T 来 确保 泛型参数的一致性,类型参数可以多重限定而通配符不行,通配符可以使用超类限定而类型参数不行。