复制代码
变长参数列表也可以使用泛型参数:
public static List toList(T… args) {
List l = new ArrayList(args.length);
for (T e : args) {
l.add(e);
}
return l;
}
复制代码
当调用一个可变参数方法时,会创建一个数组来存放可变参数,若参数的类型是泛型的,那么将创建泛型的数组,但Java不是允许直接使用泛型创建数组吗?这里java做了一些妥协允许为可变参数创建一个泛型数组。
但可变参数列表的入参是可以为不同类型的,所以有时编译也无法决定泛型可变参数的具体类型,只能选择一个最通用的类型。
public class Test {
public static void main(String[] args) {
System.out.println(toArray(Integer.valueOf(11), Double.valueOf(13)).getClass());
}
public static T[] toArray(T… args) {
return args;
}
}
复制代码
输出:
class [Ljava.lang.Number;
2.1、继承时指定类型
在继承一个泛型类或实现一个泛型接口时需要指定具体类型,指定了具体的类型后对子类而言它的父类或实现的接口就是参数化类型的,通过Class的getGenericSuperclass获取父类的类型时返回的类型为ParameterizedType的。
public class Holder {
private T val;
public Holder(T val) {
this.val = val;
}
public T getVal() {
return val;
}
public void setVal(T val) {
this.val = val;
}
}
class Apple {
public void show() {
System.out.println(getClass().getSimpleName());
}
}
public class AppleHolder extends Holder {
public AppleHolder(Apple apple) {
super(apple);
}
public static void main(String[] args) {
AppleHolder appleHolder = new AppleHolder(new Apple());
Apple apple = appleHolder.getVal();
apple.show();
System.out.println(appleHolder.getClass().getGenericSuperclass() instanceof ParameterizedType);
}
}
复制代码
输出:
Apple
true
2.2、继承时不指定类型
若继承类或实现接口时未指定类型,则对子类而言父类或接口的就是一个普通的类或接口,而其类型参数被擦除为Object,通过Class的getGenericSuperclass返回的类型是Class的。
public class CommonHolder extends Holder {
public CommonHolder(Object val) {
super(val);
}
public static void main(String[] args) {
System.out.println(CommonHolder.class.getGenericSuperclass() instanceof Class);
}
}
复制代码
输出:
true
2.3、指定为子类中的类型参数
也可以将子类中声明的类型参数给到父类,后面为子类指定类型时父类也获得同样的类型。对子类而言它的父类仍是参数化类型的,通过Class的getGenericSuperclass的返回类型仍是ParameterizedType的。
public class CommonHolder extends Holder {
public CommonHolder(T val) {
super(val);
}
public static void main(String[] args) {
System.out.println(CommonHolder.class.getGenericSuperclass() instanceof ParameterizedType);
}
}
复制代码
输出:
true
由于类型擦除,对于类型参数我们是无法直接使用到具体的属性或方法的。如下面的调用会编译失败:
import java.sql.DriverManager;
import java.util.*;;
public class Test {
public T val;
public void show() {
// 编译时失败
val.show();
}
public static class Apple {
public void show() {
}
}
public static void main(String[] args) throws ClassNotFoundException {
Test t = new Test<>();
t.show();
}
}
复制代码
上面例子中即使我们知道val的类型后面会是时Show,但因为类型擦除后无法保证这样做的安全性,所以编译器禁止这样的用法。
不过可以通过extends显示的声明类型参数的上界,若没有声明那么上界就是Object。声明类上界后,在使用该泛型类时指定的类型只能为上界或其子类。
public class Show {
public void show() {}
}
public class Test {
public T val;
public void show() {
// 可以调用
val.show();
}
public static void main(String[] args) {
Test t = new Test<>();
t.show();
}
}
复制代码
在没有声明上界时默认上界为Object,所有我们可以在没有声明上界的情况下调用Object的方法。
public class Test {
public T val;
public void show() {
val.getClass();
val.toString();
val.hashCode();
}
}
复制代码
// 继承关系:Drink -> Juice -> AppleJuice
public class Drink {}
public class Juice extends Drink {}
public class AppleJuice extends Juice {}
public class Bottle {
private T drink;
public Bottle(T drink) {
drink = drink;
}
public T getDrink() {
return drink;
}
public void setDrink(T drink) {
drink = drink;
}
}
复制代码
对于普通的类,同一个类的对象之间是可以互相赋值的,也可以将子类对象赋值给父类对象。
Juice juice = new Juice();
juice = new AppleJuice();
复制代码
但对于泛型类只要指定的类型参数不同,,即使他们是同一个泛型类,它们也是不同的参数化类型,互相直接时不能赋值的:
// Error
Bottle b1 = new Bottle(new AppleJuice());
复制代码
虽然在类型擦除后他们都是Bottle,但在编译时编译器在泛型类的边界插入的类型处理代码是不同的,显然不能用处理AppleJuice的代码去处理其他类型,所以在编译器角度它们是不同的类型,编译时会报错。
为了解决的类型参数有继承关系的泛型实例之间的赋值问题,java提供了通配符。
4.1、上界通配符
在定义泛型变量时可以使用extends关键指定类型的上界,从而使声明的变量可以被赋值为类型参数为上界类及其子类的泛参数化类型,当然前提是泛型类是相同的或父子类。
Bottle<? extends Juice> b = new Bottle(new AppleJuice());
复制代码
声明上界为Juice的b可以被赋值为Bottle。但使用上界通配符后泛型实例的使用也受到了一定限制。
虽然使用了extends通配符,但编译器任然不知道b的具体类型是AppleJuice还是OrangeJuice的子类,所以编译器无法保证参数类型有类型参数的方法的入参的安全性,例如:
Bottle<? extends Juice> bottle = new Bottle(new AppleJuice());
// error
bottle.setDrink(new OrangeJuice());
复制代码
setDrink的定义为:
void setDrink(T drink)
复制代码
那么显然bottle变量的实际类型为Bottle,所以setDrink会编译为:
setDrink((AppleJuice) val)
复制代码
显然同级类型之间强制类型转化时不安全的,所以使用上界通配符声明的实例是不允许调用参数有类型参带的方法的。但入参为null时可以的,因为null并没有具体的类型。但返回是安全的,将子类赋给父类是安全的,所以返回类型类型参数的方法不受影响。
Bottle<? extends Juice> b = new Bottle(new AppleJuice());
Juice juice = b.getDrink();
复制代码
4.2、下界通配符
使用super关键字指定下界的泛型变量,指定了下界的变量只能赋值为类型参数为指定的下界或下界的父类的类型。
Bottle<? super Juice> b = new Bottle(new AppleJuice());
复制代码
在编译时入参会被转换为实际的类型Drink:
setDrink((Drink) val)
复制代码
用父类型来操作子类型是安全的,所以下界通配符声明的实例使用入参带类型参数的方法是安全的。但由于不能将父类赋值给子类,所以下界通配符声明的实例不能将返回类型为参数类型的方法的返回值赋给其他变量。
Bottle<? super Juice> b = new Bottle(new AppleJuice());
// Error
Drink drink = b.getDrink();
复制代码
4.3、无界通配符
参数类型指定为?号,表示任意类型都可以。
Bottle<?> b = new Bottle<>(new AppleJuice());
Drink drink = (Drink) b.getDrink();
// ERROR
b.setDrink(new AppleJuice());
复制代码
使用
【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】 浏览器打开:qq.cn.hn/FTf 免费领取
无界通配符看起来和原始生类型没有什么区别,但无界通配符的意义在于在我们明确知道这里使用任意类型,并且无界通配符会进行类型检查,因为无界通配符不知道确切的类型所以无法保证安全性,所无界通配符的变量不能调用入参类型为类型参数的方法。
使用泛型时指定的类型只在编译期生效,在编译后会将所有的类型参数擦除到它的第一个边界,未指定边界的情况下擦除为Object。
因为类型擦除,类型参数在运行时已经不存在,所以不能在运行时显式的使用泛型的类型操作,如instanceof、new、T.class等,但前置类型转换时可以的:
public class Test {
Class<?> type;
public Test(Class<?> type) {
this.type = type;
}
public T[] newArray(int size) {
return (T[]) Array.newInstance(type, size);
}
public static void main(String[] args) {
Test t = new Test<>(String.class);
String[] strArr = t.newArray(10);
}
}
复制代码
虽然我们可以指定不同类型参数然,但在擦除后这些都指向同一个类型。如List和List的Class都是同一个即List.Class。
因为在编译时擦除了具体的类型信息,为类保证运行时正确的类型行为,编译器在编译时对泛型‘边界’,即对类中有泛型入参和放回的方法做了类型检查和插入强制转型代码,调用方法时对入参进行类型转换,返回时对返回值进行转换。
6.1、指定类型信息
因为类型擦除,我们无法在运行时获取具体的参数类型信息,若需要具体的类型信息可以显示的传递类型的Class对象。
public class Test {
private Class kind;
public T val;
public Test(Class kind) {
this.kind = kind;
}
public boolean isType(Object o) {
return kind.isInstance(o);
}
}
复制代码
6.2、能使用泛型方法就不使用泛型类
如果使用泛型方法可以取代泛型类,那么应该尽量使用泛型方法替换类的泛型类。
6.3、尽量使用参数化泛型:
如果一个类或接口是泛型的那么应该尽量使用其参数化的类型,这样编译器在编译时会为我们做一些类型的检查,避免在运行时报错。
若确是没有具体的类型也建议使用通配符,如List<?>。使用通配符可以在编译时进行检查,并阻止我们调用有类型参数的方法。
直接使用泛型的原始类型时有风险的,原始类型在编译时并不会进行类型检查,且类型参数被擦除为Object,Object可以接受任意类型的实例,若给到类的是不同类型的实例,那么在类中操作这些实例是有一定安全隐患且这些隐患可能在运时才暴露出来。而java之所以支直接使用泛型的原始类型只是为了兼容性Java5之前的代码。
6.4、不要将参数化类型赋给原始类型使用
为了兼容性,java没有禁止将参数化类型的变量转为原始类型,若这样做了只是在编译时产生告警。但将参数化类型赋给原始类型后,编译器不会再对原始类型实例的操作进行类型检查,这可能会造成运行时的错误。
class Calculator {
public int intAdd(T v1, T v2) {
return ((Number) v1).intValue() + ((Number) v1).intValue();
}
}
public class Test {
public static void main(String[] args) {
Calculator intCal= new Calculator<>();
Calculator cal = intCal;