泛型学习记录,欢迎纠错o(╥﹏╥)o
文章目录
1 什么是泛型(Generic Type)
“参数化类型” 就是所操作的数据类型被指定为一个参数,然后在使用/调用时传入具体的类型。
2 泛型怎么用
2.1 泛型类
class A<T>{
T t;
public void set(T t){ this.t = t; }
public T get(){ return t; }
}
//泛型类的使用
A<String> a = new A<>();
2.2 泛型接口
interface a<T>{
void set(T t);
T get();
}
//泛型接口的实现1
class A implements a<String> {
@Override
public void set(String s) {}
@Override
public String get() { return null; }
}
//泛型接口的实现2
class AA<T> implements a<T> {
T t;
@Override
public void set(T t) { this.t = t}
@Override
public T get() { return t; }
}
2.3 泛型方法
<T> T make(T t){ //定义的<T>只在该方法有效,并且该T只有在该方法被调用才确定类型
//dosomething
return t;
}
3 什么时候使用泛型
3.1 类型检查和自动转型
class A<T>{
T t;
public void set(T t){ this.t = t; }
public T get(){ return t; }
}
A<String> a = new A<>();
a.set("aaa");
a.set(1); //报错!!! -> 这里进行了类型检查
String s = a.get(); //自动转型
3.2 类型约束
class Aa{}
class A<T extends Object & Serializable>{}
A<String> a = new A<>();
A<Aa> aa = new A<>(); //报错!!!-> 限制A不单要继承Object还需要可序列化
//限制传入参数是同样的类型
<G> void get(List<G> list1, List<G> list2)
4 声明static就不能用泛型了吗
记住:泛型是使用的时候才确定类型,所以一开始就需要确定类型的地方就不能用泛型了。
class a{
static class A<T> {
//报错 -> '...A.this' cannot be referenced from a static context
static T t;
//报错 -> '...A.this' cannot be referenced from a static context
static {
T tt;
}
//报错 -> '...A.this' cannot be referenced from a static context
static void set(T t){}
static <G> void get(G g){}
}
}
4.1 静态变量 不能是泛型
静态变量:是随着类加载时被完成初始化的,它在内存中仅有一个,且JVM也只会为它分配一次内存,同时类所有的实例都共享静态变量,可以直接通过类名来访问它。
实例变量:它是伴随着实例的,每创建一个实例就会产生一个实例变量,它与该实例同生共死。
由于静态变量在类加载就已经初始化了,所以必须是明确的类型。无法等到实例创建才确定类型。
4.2 静态内部类 可以使用泛型
静态内部类跟普通类差不多,都是需要实例化的。在实例的时候给泛型指定具体类型就可以了。
4.3 静态代码块 不能使用泛型
跟静态变量一样,静态代码块只在类加载的时候会调一次。实例化是不会调用的,所有无法给泛型指定类型。
4.4 静态方法 可以使用自身定义的泛型,而不能使用类定义的泛型
方法中声明的泛型,是在调用的时候才确定泛型的类型。所有静态方法是可以使用自身定义的泛型。
但是静态方法不能使用类中声明的泛型,因为静态方法会随着类的定义而被分配和装载入内存中,无法访问实例方法以及实例变量。
5 泛型的 协变(Covariant) 与 逆变(Contravariant) 以及 不变(Invariance)
协变与逆变用来描述类型转换(type transformation)后的继承关系:A、B表示类型;f(·)表示类型转换;A<=B表示A为B的子类。
那么则存在:
f(·)是协变性:当A<=B ,f(A)<=f(B)成立
f(·)是逆变性:当A<=B ,f(A)>=f(B)成立
f(·)是不变性:当A<=B ,f(A) 和f(B)不存在继承关系
//定义几个类
static class 生物 {}
static class 动物 extends 生物 {}
static class 猪 extends 动物 {}
static class 人 extends 动物 {}
static class 赛亚人 extends 人 {}
static class 橡胶人 extends 人 {}
5.1 泛型具有不变性
//报错 -> Required type: ArrayList<动物> Provided: ArrayList<人>
ArrayList<动物> a = new ArrayList<人>();
//报错 -> Required type: ArrayList<人> Provided: ArrayList<动物>
ArrayList<人> b = new ArrayList<动物>();
当人<=动物,f(人) 和f(动物)不存在继承关系
Why? 因为类型擦除。在运行时泛型信息会被擦除掉。擦除掉后,就不知道它到底是人还是动物了。
5.2 让泛型具有协变性
先来看看数组的协变性吧!
//数组是具有协变性的,因为数组的类型不会被擦除
//这样会导致 array[1] = 1 可以编译通过,因为int是Object的子类。但这样的操作是有问题的,而这个问题在代码执行到这里的时候才会出现。
Object[] array = new String[5];
array[0] = "llk";
array[1] = 1; //抛异常:java.lang.Integer cannot be stored in an array of type java.lang.String[]
再看看下边的代码
ArrayList<? extends 动物> list = new ArrayList<人>();
list.add(new 动物()); //报错 -> Required type: capture of ? extends 动物 Provided: 动物
list.add(new 人()); //报错 -> Required type: capture of ? extends 动物 Provided: 人
list.add(new 猪()); //报错 -> Required type: capture of ? extends 动物 Provided: 猪
动物 a = list.get(0);
生物 b = list.get(1);
人 c = list.get(2); //报错 -> Required type: 人 Provided: capture of ? extends 动物
使用上限通配符(?extends Class)
当人<=动物,f(人) <=f(动物)成立
为什么无法往list里边添加任何数据呢?
?extends 动物,限制了list只能存放继承自动物的子类,由于无法确定是哪个子类(有可能是人也有可能是猪),这种情况为了保证安全直接报错提醒。防止出现像上边数组的问题,那是非常危险的。
那list存放的是子类为什么 人 c = list.get(2) 会报错?
由于无法确定里边存是什么子类,所以只能读取出动物以及动物的父类。
使用了上限通配符后,list相当于变成了只读。
5.3 让泛型具有逆变性
ArrayList<? super 人> list = new ArrayList<动物>();
list.add(new 赛亚人());
list.add(new 橡胶人());
list.add(new 人());
list.add(new 猪()); //报错 -> Required type: capture of ? super 人 Provided: 猪
list.add(new 动物()); //报错 -> Required type: capture of ? super 人 Provided: 动物
人 a = list.get(0); //报错 -> Required type: 人 Provided: capture of ? super 人
Object b = list.get(1);
使用下限通配符(?super Class)
当人<=动物,f(人) >=f(动物)成立
list能添加数据了而且也是new ArrayList<动物>,为什么不能添加动物类了呢?
? super 人,限制了list只支持添加人以及人的子类,但是list里边存的却是人以及人的父类。
为什么取值只能返回Object?
由于无法确定存的是哪个父类,所有取值是只能返回万物的鼻祖Object。
使用了下限通配符后,该list相当于变成了 只写。
5.4 小结
?extends T 存放的类型一定为T及其子类,但是获取要用T或者其父类引用。转型一致性
?super T 存放的类型一定为T的父类,但添加一定为T和其子类对象。转型一致性
?extends T 进行add(T子类)编译出错:因为无法确定到底是哪个子类
?super T get()对象,都是Object类型,因为T的最上层父类是Object,想要向下转型只能强转。
6 什么是类型擦除
泛型信息只存在于代码编译阶段,在运行时泛型相关的信息会被擦除掉。
//java中
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass()); //输出true
//class中
//在运行时所有的<>以及里边的内容都会被擦除掉,
ArrayList var1 = new ArrayList();
ArrayList var2 = new ArrayList();
System.out.println(var1.getClass() == var2.getClass());
7 为什么Java使用擦除法实现泛型
7.1 来看看c++的泛型
//c++模板
class Test {
public:
void dosomething() {}
};
template<class T> class AClass {
T t;
public:
AClass(T tt) {
t = tt;
}
void dosomething() {
t.dosomething();
}
};
int main(int argc, const char * argv[]) {
Test test;
AClass<Test> aClass(test);
aClass.dosomething();
return 0;
}
c++编译器用实参来为我们推断模板实参,并为我们实例化一个特定版本的代码。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。被编译器生成的版本通常称为模板的实例。在编译时,编译器会将T替换成Test并生成模板实例。
而java的泛型就不太一样。Java采用的是擦除法实现的伪泛型。
7.2 为什么Java使用擦除法实现泛型
主要原因:兼容旧版本
因为Java最开始的版本是不支持泛型的,Java5才支持。为了兼容旧版让旧版Jdk编译出来的class也能在新版本虚拟机上继续使用。
7.3 那类型擦除会引发什么问题
7.3.1 在运行时无法获取泛型的类型
泛型在运行时会被擦除,但是在字节码里泛型的定义是不会被擦除的。所有可以通过获取类信息的方法来获取泛型信息。
class A<T, K> { }
//栗子1
//报错:Cannot select from parameterized type
System.out.println("A Info -> " + A<String, Integer>.class);
//栗子2
//输出:A info -> class com.llk.kt.A
System.out.println("A info -> " + A.class);
//栗子3 泛型会在运行时被擦除,所有打印跟栗子2一样。
//输出:A info -> class com.llk.kt.A
System.out.println("A info -> " + new A<String, Integer>().getClass());
//栗子4
Type type = new A<String, Integer>(){}.getClass().getGenericSuperclass();
//输出:A info -> com.llk.kt.A<java.lang.String, java.lang.Integer>
System.out.println("A info -> " + type);
ParameterizedType pt = (ParameterizedType) type;
//输出:A generic info -> [class java.lang.String, class java.lang.Integer]
System.out.println("A generic info -> " + Arrays.toString(pt.getActualTypeArguments()));
为什么栗子4就可以获取到泛型信息?
因为加了{}这个语法糖后java会帮我们创建一个匿名类并且实例化它。我们通过这个匿名类就能拿到泛型信息。
//javac 编译后,发现除了Test.class、Test$A.class文件外,还多出了Test$1.class文件
//Test$1.class文件
class Test$1 extends A<String, Integer> {
Test$1(Test var1) {
this.this$0 = var1;
}
}
//Test.class文件
System.out.println("A Info -> " + Test.A.class);
PrintStream var10000 = System.out;
Test.A var10001 = new Test.A();
var10000.println("A Info -> " + var10001.getClass());
//没错这里是创建了匿名类Test$1,然后通过匿名类获取类中的泛型信息。因为class中的泛型信息是不会被擦除的。
Type var1 = (new Test.A<String, Integer>() {}).getClass().getGenericSuperclass();
System.out.println("A info -> " + var1);
ParameterizedType var2 = (ParameterizedType)var1;
System.out.println("A generic info -> " + Arrays.toString(var2.getActualTypeArguments()));
实际应用例子:Gson库的TypeToken
//为了强制让你加上{},还特意将构造函数设置为protected
//如果忘记加{},就会报错:'TypeToken()' has protected access in 'com.google.gson.reflect.TypeToken'
TypeToken<A<String, Integer>> typeToken = new TypeToken<A<String, Integer>>(){};
System.out.println("A Info -> " + typeToken.getType());
//TypeToken源码
public class TypeToken<T> {
...
final Type type;
protected TypeToken() {
this.type = getSuperclassTypeParameter(getClass());
...
}
static Type getSuperclassTypeParameter(Class<?> subclass) {
Type superclass = subclass.getGenericSuperclass();
if (superclass instanceof Class) {
throw new RuntimeException("Missing type parameter.");
}
ParameterizedType parameterized = (ParameterizedType) superclass;
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
}
}
7.3.2 导致泛型具有不变性
因为类型擦除,导致泛型失去类型的协变性与逆变性。
解决方法:通过使用上限通配符以及下限通配符实现。
8 泛型面试问题整理(持续完善中)
8.1 下列哪些是错误的
//反正运行时泛型都会被擦除,String放哪里都没关系。在运行时,A、B、C都是一样的。
//这个没问题,在实例化的时候给泛型指定类型。
A、ArrayList list = new ArrayList<String>();
//Java7的类型推断,可以省略右边的<String>
B、ArrayList<String> list2 = new ArrayList();
//add啥都不会报错,因为里边的泛型被实例化成了Object
//list4.add("1");
//list4.add(1);
C、ArrayList list3 = new ArrayList();
//报错,原因是泛型具有不变性。
//如果想要不报错,用上限通配符让泛型支持协变就ok了 -> ArrayList<? extends Object> list4 = ...
D、ArrayList<Object> list4 = new ArrayList<String>();
//报错,原因是泛型具有不变性。
//如果想要不报错,用下限通配符让泛型支持逆变就ok了 -> ArrayList<? super String> list5 = ...
E、ArrayList<String> list5 = new ArrayList<Object>();
8.2 使用泛型实现lru缓存
LinkedHashMap实现:使用泛型实现(LRU)缓存
大佬实现:具有泛型和O(1)操作的Java中的LRU缓存
8.+ 相关面试题的文章
Java高级面试 —— Java的泛型实现机制是怎么样的?