Java泛型

泛型程序设计意味着编写的代码可以被很对不同类型的对象所重用。

简单使用

简单泛型类

一个泛型类是具有一个或多个类型变量的类。

类型变量常使用大写形式,并且一般较短。通常使用E表示集合的元素类型,使用K和V分别表示关键字与值的类型。使用T表示任意类型。

例如:

public class SimpleGenericClass<T> {
    private T first;
    private T second;

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }

    @Override
    public String toString() {
        return "SimpleGenericClass{" +
                "first=" + first +
                ", second=" + second +
                "}";
    }

    public static void main(String[] args) {
        SimpleGenericClass<String> StringGen = new SimpleGenericClass<>();

        StringGen.setFirst("first");
        StringGen.setSecond("second");

        System.out.println(StringGen);
    }
}

泛型方法

类型变量放在修饰符的后面,返回类型的前面。泛型方法可以定义在普通类中,也可以定义在泛型类中。调用泛型方法是时在方法名前的尖括号中放入具体的类型

例如:

public class SimpleGenericMethod {
    public static <T> T getData(T t1, T t2) {
        return t2;
    }

    public static void main(String[] args) {
        System.out.println(SimpleGenericMethod.<String>getData("Str1", "Str2"));
    }
}

类型变量的限定

我们可以对类型变量加一些限定,比如需要实现指定的接口或者继承自指定的类。统一使用extends关键字限定类型变量,如果有多个限定应使用&分隔。如果限定类型使用了类,那他必须放在第一个

例如:

//定义接口interA
interface interA {
    public String Print1();
}

//定义接口interB
interface interB {
    public String Print2();
}

//定义类A,实现了A接口
class A implements interA {
    String str;

    public A(String str) {
        this.str = str;
    }

    public String Print1() {
        return "print1 " + str;
    }
}

//定义类B,实现了B接口
class B implements interB {
    String str;

    public B(String str) {
        this.str = str;
    }

    @Override
    public String Print2() {
        return "print2 " + str;
    }
}

//定义类AB,实现了A接口和B接口
class AB implements interA, interB {
    String str;

    public AB(String str) {
        this.str = str;
    }

    @Override
    public String Print1() {
        return "print1 " + str;
    }

    @Override
    public String Print2() {
        return "print2 " + str;
    }
}

public class GenericRestrict {
    //为类型变量加了限定,只有同时实现了A接口和B接口的类才可以使用该泛型方法
    public static <T extends interA & interB> void fun(T t1) {
        System.out.println(t1.Print1());
        System.out.println(t1.Print2());
    }

    public static void main(String[] args) {
        AB ab = new AB("ab");
        GenericRestrict.<AB>fun(ab);
    }
}

泛型与虚拟机

Java虚拟机并不存在泛型的概念。Java泛型只存在于源码中,编译后的字节码文件中的全部泛型都被替换为原始类型。

类型擦除

对于一个泛型类型,虚拟机都自动提供一个相应的原始类型,擦除类型变量,并替换为限定类型(没有限定就替换为Object)

泛型类型 原始类型
List List
T Object
T extends Person & Comparable Person

翻译泛型

如果虚拟机对返回类型进行了擦除,就需要加上合适的强制类型转换。

类型擦除还会带来一个问题,例如对于一个泛型类:

public class Person<T> {
	private T information; 

	public void setInformation(T information) {
		this.information = information;
	}
	
	public T getInformation() {
		return information;
	}
}

定义一个MyPerson类,继承了Person<String>

public class MyPerson extends Person<String> {

	@Override
	public String getInformation() {
		return super.getInformation();
	}
	
	@Override
	public void setInformation(String information) {
		super.setInformation(information);
	}
}

MyPerson重写了setInformation(String)方法,但是经过虚拟机擦除后,Person类有一个需要Object参数的setInformation方法。显然,MyPerson中的setInformation(String)方法与setInformation(Object)是两个不一样的方法。为了保持泛型类的多态性,编译器会自动生成桥方法。确保MyPerson对象调用正确的方法。

编译器自动生成的桥方法如下:

public void setInformation(Object information) {
    setInformation((String) information); 
}

还有一个问题,类似的MyPerson中的setInformation方法也会有两个,但是只是返回值不一样。Java无法通过返回值类型区分不同的方法,但是在虚拟机中实际是通过参数类型和返回值类型来确定一个方法的。因此仍可以利用桥方法实现多态。

public String getInformation() {...}
public Object getInformation() {return getInformation()}

桥方法不仅用于泛型类型,在一个方法覆盖另一个方法时可以指定一个更严格的返回值类型,这时就使用了桥方法保持了多态性。

总结

  • 虚拟机没有泛型,只有普通的类和方法
  • 所有的类型参数都有他们的限定类型替换
  • 桥方法被合成来保持多态
  • 为保持类型安全性,必要时插入强制类型转换

约束与限制

在使用Java泛型时有一些限制,主要是类型擦除引起的

  1. 不能用基本类型实例化类型参数

  2. 运行时类型查询只适用于原始类型

由于虚拟机会进行类型擦除,所以类型查询只能查询到原始类型。在Java中不能使用instanceof查询泛型类型。使用getClass也只会得到原始类型。

  1. 不能创建参数化类型的数组

只是不允许创建这些数组,而声名类型为Pair[]的变量还是合法的,只是不能用new Pair[10]初始化这个变量。可以声明通配符类型的数组,然后进行类型转换

Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];

但是这样做并不安全。

如果需要收集参数化类型对象,只有一种安全有效的方法:使用ArrayList:ArrayList<Pair>

  1. Varargs警告

当使用可变数量的参数化类型参数时,Java虚拟机会自动创建一个参数类型的数组,这违反了前面的规定,但是此时规则有所放松,你只会得到一个警告。

可以采用两种方法抑制这个警告,一是调用方法前增加注解@SuppressWarnings("unchecked")。二是在定义方法前使用注解@SafeVarargs

  1. 不能实例化类型变量

不能使用像new T(...)new T[...]T.class这样的表达式中的类型变量,对于下面的一个类Pair

public Pair<T> {
    T first;
    T second;
}

下面的构造器是非法的

public Pair() {first = new T(); second = new T();}

在Java8之后,最好的方法就是利用Lambda表达式

Pair(T first, T second) {
    this.first = first;
    this.second = second;
}

public static <T> Pair<T> makePair(Supplier<T> constr) {
    //传入T类型的构造函数,创建两个T类型的对象
    //再利用Pair的拷贝构造器构造出一个Pair<T>类型的对象
    return new Pair<>(constr.get(), constr.get());
}

比较老式的做法是使用反射

public static <T> Pair<T> makePair(Class<T> cl) {
    try {
        return new Pair<>(cl.newInstance(), cl.newInstance());}
    }
    catch(Execption e) {
        ...
    }
}
  1. 不能构造泛型数组

考虑下面的例子

public static <T extends Comparable> T[] minmax(T[] a) {
    T[] mm = new T[2];
    ...
}

由于类型擦除,该方法会永远构造Comparable[2]数组。

如果数组仅仅作为一个类的私有域,就可以将这个数组声明为Object[],并且在获取元素是进行类型转换,例如ArrayList类可以这样实现:但是如果返回E[]类型的数组就会有一些问题。

public class ArrayList<E> {
	private E[] elements;
    
    public ArrayList() {
        elements = (E[])new Object[10];
    }
}

最好让用户提供一个数组构造其表达式,例如:

public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a)
    T[] mm = constr.apply(2);
    ...
}

使用反射的话也可以

public static <T extends Comparable> T[] minmax(T... a)
    T[] mm = (T[])Array.newInstance(a.getClass.getComponentType(), 2);
    ...
}
  1. 不能在静态域或方法中引用类型变量

  2. 不能抛出或捕获泛型类的实例

  3. 可以消除对受查异常的检查

  4. 注意擦除后的冲突

通配符

在之前使用泛型时,如果给定了类型,那就固定了。Java中还允许参数类型变化,就需要用到通配符类型。

例如:

Pair<? extends Employee>

表示一系列的泛型Pair类型,但类型参数必须是Employee或其子类,比如Pair<Employee>Pair<Manager>。但是Pair<String>并不属于这种类型。

泛型协变与逆变

Java支持向上转型(协变)和向下转型(逆变)。例如:

Employee manager = new Manager();

但是泛型却和想象中的可能不同,下面这样写是错误的。

List<Employee> manager = new ArrayList<Manager>(); //error,无法通过编译

为了获得泛型类的“协变”,可以将引用类型设置为 ? extends 类型

为了获得泛型类的“逆变”,可以将引用类型设置为 ? super 类型

如果将引用的泛型设为<? extends Apple>,此时这个引用可以接受Apple及其子类的容器

如果将引用的泛型设为<? super Apple>,此时这个引用可以接受Apple及其父类的容器

读写限制

虽然使用通配符可以实现协变逆变,但是也带来了一些影响,主要是读写操作的限制。

这里所谓的读指的是get之类的操作,将泛型类型作为函数的返回值。

写指的是set之类的操作,将泛型类型作为函数的参数。

  • 对于类型为List<? super Apple>,合法的行为是将something extends Apple类型赋值给? super Apple。而? super Apple类型只能赋值给Object

  • 对于类型为List<? extends Apple>,合法的行为是将? extends Apple赋值给something super Apple。而只能将null类型赋值给? extends Apple

Java中top type为Object,bottom type为null。

带通配符的引用之间赋值必须相容

  • 使用通配符的引用,可以把这种引用看作一个范围,比如List<?>看作从nullObject的范围。而如果通配符带了边界,就只是将这个范围缩小了。
  • 两种引用List(raw type)和List<?>(unbounded type)之间相互赋值,编译器不会有警告。
  • 带有通配符的引用,只能够赋值给List(raw type)或者相容的带通配符的引用。不能赋值给带有具体类型的引用

自己的疑惑与思考

  1. 对通配符的理解

    首先类型变量(T)指代某一个类型。不确定是哪种类型,但是只是某一个。

    通配符指代一系列的类型。表示了继承树上某一个范围内的类型。

  2. 对代码的理解

    class MyList<T> {
        T first;
    
        public T getFirst() {
            return first;
        }
    
        public void setFirst(T first) {
            this.first = first;
        }
    
        public static void main(String[] args) {
            MyList<? extends Integer> l1;
            MyList<Integer> l2 = new MyList<Integer>();
            l2.setFirst(111);
            
            l1 = l2;
            //l2 = l1;	error
    
            Integer num = l1.getFirst();
        }
    }
    

    其中的l1 = l2;OK

    此处l1被定义为了MyList<? extends Integer>类型。按照我的理解l1就是被定义为了一系列的类型,例如

    MyList<Integer>MyList<Class1ExtendsInteger>MyList<Class2ExtendsClass1>……,这里l1是MyList<Integer>类型的变量,所以可以被l1引用。

    但是如果想执行 l2 = l1;ERROR

    由于l1的类型是MyList<Integer>MyList<Class1ExtendsInteger>MyList<Class2ExtendsClass1>……的,l2只是MyList<Integer>类型,无法引用MyList<Class1ExtendsInteger>MyList<Class2ExtendsClass1>这样类型的变量(应该知道IntegerClass1ExtendsInteger有继承关系,但是MyList<Integer>MyList<Class1ExtendsInteger>是没有继承关系的)。所以编译不会通过。

    而对于Integer num = l1.getFirst();OK

    为什么可以这样赋值呢?l1getFirst()返回值类型为<? extends Integer>,表示了一些列的类型:IntegerClass1ExtendsIntegerClass2ExtendsClass1……与上面不同的是这些类型之前是确实存在继承关系的,所以这一系列的对象都可以被Integer类型的变量引用。

参考资料

Java泛型 通配符详解

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » Java泛型