Java泛型学习记录

泛型学习记录,欢迎纠错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 classextends 动物 {}
static classextends 动物 {}
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的泛型实现机制是怎么样的?


参考文章
图解java泛型的协变和逆变
Java泛型(一)类型擦除
Java泛型(二) 协变与逆变

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值