一、概念
没有引入泛型之前
在没有泛型类之前。ArrayList类只维护一个Object引用数组:
public class ArrayList{
private Object[] elementData;
public Object get(int i){...}
public void add(Object o){...}
}
这种方法存在两个问题。获取一个值时必须进行强制类型转换:
ArrayList files = new ArrayList();
String filename = (String)files.get(0);
此外,这里也没有错误检查。可以向数组列表中添加任何类的值:
files.add(new File("./img/a.txt"));
对于这个调用,编译和运行都不会出错。不过,如果你将get的结果强制类型转换为String类型,就会产生错误。显而易见,这是非常危险的。
引入泛型之后
public class ArrayList<T>{
private T[] elementData;
public T get(int i){...}
public void add(T o){...}
}
ArrayList类现在有一个类型参数用来指示元素的类型:
ArrayList<String> files = new ArrayList<String>();//上面的T此时就相当于String
编译器充分利用这个类型信息。调用get的时候,不再需要强制类型转换。编译器知道返回值类型为String,而不是Object:
String filename = files.get(0);
编译器还知道ArrayList的add方法有一个类型为String的参数,这比有一个Object类型的参数要安全得多。现在编译器会检查,防止你插入错误类型的对象。例如,下面的语法:
files.add(new File("./img/a.txt"));
是无法通过编译的。
看到这里你应该对于泛型的作用有了一定得了解,总结一下:
-
在编译的时候检查类型安全
-
所有的强制转换都是自动和隐式的
-
提高代码的重用率(这个在上面没有提到。想一想为什么)
二、泛型的使用方式
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
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);
Java1.7以后,后面的<>中的具体的数据类型可以省略不写(菱形语法)
Generic<Integer> genericInteger = new Generic<>(123456);
注意:
-
泛型的类型参数只能是引用类型,不能是基本数据类型
-
泛型类,如果没有指定具体的数据类型,此时,操作类型是Object
List list = new ArrayList();
从泛型类派生子类
子类也是泛型类,子类和父类的泛型类型要一致
class ChildGeneric<T> extends Generic<T>
//父类
public class Parent<E> {
private E value;
public E getValue() {
return value;
}
public void setValue(E value) {
this.value = value;
}
}
/**
* 泛型类派生子类,子类也是泛型类,那么子类的泛型标识要和父类一致。
* @param <T>
*/
public class ChildFirst<T> extends Parent<T> {
@Override
public T getValue() {
return super.getValue();
}
}
子类不是泛型类,父类要明确泛型的数据类型
class ChildGeneric extends Generic<String>
/**
* 泛型类派生子类,如果子类不是泛型类,那么父类要明确数据类型
*/
public class ChildFirst extends Parent<Sting> {
@Override
public String getValue() {
return super.getValue();
}
}
2.泛型接口:
public interface Generator<T> {
T method();
}
从泛型接口派生子类
实现类也是泛型类,实现类和接口的泛型类型要一致
/**
* 泛型接口
* @param <T>
*/
public interface Generator<T> {
T getKey();
}
/**
* 泛型接口的实现类,是一个泛型类,
* 那么要保证实现接口的泛型类泛型标识包含泛型接口的泛型标识
* @param <T>
* @param <E>
*/
public class Pair<T,E> implements Generator<T> {
private T key;
private E value;
public Pair(T key, E value) {
this.key = key;
this.value = value;
}
@Override
public T getKey() {
return key;
}
public E getValue() {
return value;
}
}
实现类不是泛型类,接口要明确数据类型
/**
* 实现泛型接口的类,不是泛型类,需要明确实现泛型接口的数据类型。
*/
public class Apple implements Generator<String> {
@Override
public String getKey() {
return "hello generic";
}
}
3.泛型方法:
public static <E> void sort(E[] arr){
T small = arr[0];
if(small.compareTo(b) > 0)
}
但是,现在有一个问题。请看sort方法的代码。变量small得类型为T,这意味着他可以是任何类型的变量。如何知道它一定有一个compareTo方法呢?
解决这个问题的办法就是限制变量T只能实现Comparable接口(该接口包含了comparaTo方法 )
public static <E extends Comparable> void sort(E[] arr){...}
注意:Comparable是一个接口,为什么不用implements而是使用extends?我也不知道,就是这样设计的,记住就好了。
三、类型通配符
1.什么是类型通配符
类型通配符一般是使用"?"代替具体的类型实参。
所以,类型通配符是类型实参,而不是类型形参。
使用场景
我们现在有一个方法 print 可以打印 List 集合。当我们集合的泛型是Integer时
public static void main (String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(123);
list.add(456);
print(list);
}
private static void print(List<Integer> context){
System.out.println(context);
}
当我们集合的泛型是String时
public static void main (String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("123");
list.add("456");
print(list);
}
private static void print(List<String> context){
System.out.println(context);
}
发现了没?是不是很麻烦,这个时候使用类型通配符就好多了
public static void main (String[] args) {
ArrayList<String> list = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
list.add("123");
list.add("456");
list2.add(123);
list2.add(345);
print(list);
print(list2);
}
private static void print(List<?> context){
System.out.println(context);
}
2.类型通配符的上限
语法:
- 类/接口<? extends 实参类型>
- 要求该泛型的类型,只能是实参类型,或实参类型的子类类型。
private static void print(List<? extends Vehicle> context){
//表示这个类型必须是Vehicle的子类
System.out.println(context);
}
3.类型通配符的下限
语法:
- 类/接口<? super 实参类型>
- 要求该泛型的类型,只能是实参类型,或实参类型的父类类型。
private static void print(List<? super Car> context){
//表示这个类型必须是Car的父类或者Car本身
System.out.println(context);
}
四、类型擦除
1.概念
泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,我们称之为——类型擦除。
学习类型擦除之前,要明白原始类型这一概念。
无论何时提供一个泛型类型,都会自动提供一个相应的原始类型。例如,现在有一个Pair类
public class Pair<T> {
private T first;
private T second;
public Pair (T first, T second) {
this.first = first;
this.second = second;
}
}
Pair类的原始类型如下:
public class Pair {
private Object first;
private Object second;
public Pair (Object first, Object second) {
this.first = first;
this.second = second;
}
}
Pair或Pair,擦除类型之后,他们都会变成上面的原始类型
2.分类:
无限制类型擦除
有限制类型擦除
擦除方法中类型定义的参数
桥接方法
五、反射与泛型
/**
* 泛型与反射
*/
public class Test {
public static void main(String[] args) throws Exception {
Class<Person> personClass = Person.class;
Constructor<Person> constructor = personClass.getConstructor();
Person person = constructor.newInstance();
}
}
不使用泛型
public class Test {
public static void main(String[] args) throws Exception {
Class personClass = Person.class;
Constructor constructor = personClass.getConstructor();
Person person = (Person) constructor.newInstance();
}
}
参考:
https://blog.csdn.net/Beyondczn/article/details/107093693
https://javaguide.cn/
《Java核心技术》