1、说一说你对泛型的理解
参考答案
Java集合有个缺点—把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。
Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:
- 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
- 由于把对象“丢进”集合时,集合丢失了对象的状态信息,只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。
从Java 5开始,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。例如 List<String>
,表明该List只能保存字符串类型的对象。泛型可以在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率。
有了泛型以后,程序再也不能“不小心”地把其他对象“丢进”集合中。而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。
2、简述泛型擦除
泛型擦除指的是在 Java 泛型中,编译器在编译时会将泛型类型参数的具体类型擦除掉,转而使用 Object 类型来替代泛型类型参数,以实现运行时的兼容性。
泛型擦除是 Java 泛型实现中的一种类型伪装,它使得 Java 泛型能够向后兼容 JDK5 以前的版本。具体来说,泛型擦除主要包括以下两个方面:
- 类型的擦除:Java 编译器会将泛型类型参数擦除掉,用 Object 类型替代。例如,泛型类 Box 的类型参数会被擦除,Box 将被转换成 Box。
- 类型参数的替换:Java 编译器会将泛型类型参数在运行时替换为具体类型。例如,一个泛型方法定义为 public void print(T t),其中 T 为类型参数,在运行时,实参类型将被替换成具体的类型,例如 print("Hello") 将会被转换成 print((Object)"Hello")。
泛型擦除的主要优点是保证了编译器能够正确编译泛型代码,同时支持向后兼容性。但它也导致了一些问题,例如运行时不能获取泛型参数的具体类型,因此不能进行强制类型转换和类型检查;另外,也不能创建泛型类型参数的实例。
需要注意的是,Java 泛型擦除是在编译阶段实现的,因此仍然可以通过使用反射等机制获取泛型类型参数的具体类型信息。此外,在某些场景下,Java 泛型擦除可能会导致类型安全的问题。因此,在使用 Java 泛型时需要注意理解泛型擦除的特点及其对代码的影响,并尽可能避免出现潜在的类型安全问题。
- 定义:泛型擦除是指在继承(实现)或使用时没有指定具体的类型
- 特点:一旦擦除之后按Object处理 - 依然存在警告,加上Object可以去除,但是有些画蛇添足 - 不完全等同于Object,编译不会类型检查
参考答案: 在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个
List<String>
类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。上述规则即为泛型擦除,可以通过下面代码进一步理解泛型擦除:
List<String> list1 = ...; List list2 = list1; // list2将元素当做Object处理
扩展阅读
从逻辑上来看,
List<String>
是List的子类,如果直接把一个List对象赋给一个>List<String>
对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List对象赋给一个List<String>
对象,编译器仅仅提示“未经检查的转换”。上述规则叫做泛型转换,可以通过下面代码进一步理解泛型转换:
List list1 = ...; List<String> list2 = list1; // 编译时警告“未经检查的转换”
3、List<? super T>和List<? extends T>有什么区别?
参考答案
- ? 是类型通配符,List<?> 可以表示各种泛型List的父类,意思是元素类型未知的List;
- List<? super T> 用于设定类型通配符的下限,此处 ? 代表一个未知的类型,但它必须是T的父类型;
- List<? extends T> 用于设定类型通配符的上限,此处 ? 代表一个未知的类型,但它必须是T的子类型。
扩展阅读
在Java的早期设计中,允许把Integer[]数组赋值给Number[]变量,此时如果试图把一个Double对象保存到该Number[]数组中,编译可以通过,但在运行时抛出ArrayStoreException异常。这显然是一种不安全的设计,因此Java在泛型设计时进行了改进,它不再允许把 List 对象赋值给 List 变量。
数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型,但G 不是 G 的子类型。Foo[]自动向上转型为Bar[]的方式被称为型变,也就是说,Java的数组支持型变,但Java集合并不支持型变。Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。
4、泛型的使用方式有哪几种?
泛型本质上是提供类型的“类型参数”,也就是参数化类型。我们可以为类、接口或方法指定一个类型参数,通过这个参数限制操作的数据类型,从而保证类型转换的绝对安全。
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
1. 泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
2. 泛型接口:
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
3. 泛型方法:
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
注意:
public static < E > void printArray( E[] inputArray )
一般被称为静态泛型方法;在java中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的
5、泛型的高级用法
1. 限制泛型可用类型
在 Java 中默认可以使用任何类型来实例化一个泛型类对象。当然也可以对泛型类实例的类型进行限制,语法格式如下:
class 类名称<T extends anyClass>
其中,anyClass 指某个接口或类。使用泛型限制后,泛型类的类型必须实现或继承 anyClass 这个接口或类。无论 anyClass 是接口还是类,在进行泛型限制时都必须使用 extends 关键字。
例如,在下面的示例代码中创建了一个 ListClass 类,并对该类的类型限制为只能是实现 List 接口的类。
// 限制ListClass的泛型类型必须实现List接口
public class ListClass<T extends List> {
public static void main(String[] args) {
// 实例化使用ArrayList的泛型类ListClass,正确
ListClass<ArrayList> lc1 = new ListClass<ArrayList>();
// 实例化使用LinkedList的泛型类LlstClass,正确
ListClass<LinkedList> lc2 = new ListClass<LinkedList>();
// 实例化使用HashMap的泛型类ListClass,错误,因为HasMap没有实现List接口
// ListClass<HashMap> lc3=new ListClass<HashMap>();
}
}
在上述代码中,定义 ListClass 类时设置泛型类型必须实现 List 接口。例如,ArrayList 和 LinkedList 都实现了 List 接口,所以可以实例化 ListClass 类。而 HashMap 没有实现 List 接口,所以在实例化 ListClass 类时会报错。
当没有使用 extends 关键字限制泛型类型时,其实是默认使用 Object 类作为泛型类型。因此,Object 类下的所有子类都可以实例化泛型类对象,如图1所示的这两种情况。
2. 使用类型通配符
泛型还支持使用类型通配符,它的作用是在创建一个泛型类对象时限制这个泛型类的类型必须实现或继承某个接口或类。
使用泛型类型通配符的语法格式如下:
泛型类名称<? extends List>a = null;
其中,“<? extends List>”作为一个整体表示类型未知,当需要使用泛型对象时,可以单独实例化。
例如,下面的示例代码演示了类型通配符的使用。
A<? extends List>a = null;
a = new A<ArrayList> (); // 正确
b = new A<LinkedList> (); // 正确
c = new A<HashMap> (); // 错误
在上述代码中,同样由于 HashMap 类没有实现 List 接口,所以在编译时会报错。
3. 继承泛型类和实现泛型接口
定义为泛型的类和接口也可以被继承和实现。例如下面的示例代码演示了如何继承泛型类。
public class FatherClass<T1>{}
public class SonClass<T1,T2,T3> extents FatherClass<T1>{}
如果要在 SonClass 类继承 FatherClass 类时保留父类的泛型类型,需要在继承时指定,否则直接使用 extends FatherClass 语句进行继承操作,此时 T1、T2 和 T3 都会自动变为 Object,所以一般情况下都将父类的泛型类型保留。
下面的示例代码演示了如何在泛型中实现接口。
interface interface1<T1>{}
interface SubClass<T1,T2,T3> implements Interface1<T2>{}
6、项目中哪里用到了泛型?
- 自定义接口通用返回结果 CommonResult 通过参数 T 可根据具体的返回类型动态指定结果的数据类型
- 定义 Excel 处理类 ExcelUtil 用于动态指定 Excel 导出的数据类型
- 构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。
- .....