1. 泛型
1. 概念
一个类或接口,它的声明有一个或多个类型参数( type parameters ),被称之为泛型类或泛型接口。
在没有泛型的时候,对于一个类中有不知类型的变量通常要对其进行强转,变成Object类型,例如:
public class Cache {
Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
上舒代码中,geValue和setValue的返回值,参数对应都是Object,现在Cache类可以缓存任何类型的实例,如下
Cache cache = new Cache();
cache.setValue(134);
int value = (int) cache.getValue();
cache.setValue("hello");
String value1 = (String) cache.getValue();
只要对需要存入的类型进行强转就行,这是在Java5之前没有泛型时的做法。
现在引入泛型,所谓的泛型就是对需要传入的类型进行参数化。普通的函数参数如下,test方法的输入参数只能是int,那么我们可以对输入的这个类型参数化用E表示,相当于输入的类型也是个变量,但是这个类型变量定了以后不能改变,那么可以传入任何类型的类作为输入类型。
public void test(int i){
System.out.println(i);
}
//这就是泛型化后的
public void set(E i){
System.out.println(i)
}
上面的Cache类使用泛型后:
/**
* @author: qujundong
* @date: 2020/11/28 下午5:12
* @description:
*/
public class Cache<T> {
private T value;
public void setValue(T value){
this.value = value;
}
public T getValue(){
return value;
}
public static void main(String[] args) {
Cache<Integer> cache = new Cache();
cache.setValue(134);
int value = (int) cache.getValue();
System.out.println(value);
}
}
这个的好处就是不需要强转了,同时也更加安全,因为我们在声明这个Cache类的时候给定需要的类型,并且在后续的时候用的都是这个。
2. 泛型类
我们可以这样定义一个泛型类。
public class Test<T> {
T field1;
}
尖括号 <>
中的 T 被称作是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。
public class Test<Hello> {
Hello field1;
}
但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:
- T 代表一般的任何类。
- E 代表 Element 的意思,或者 Exception 异常的意思。
- K 代表 Key 的意思。
- V 代表 Value 的意思,通常与 K 一起配合使用。
- S 代表 Subtype 的意思,文章后面部分会讲解示意。
如果一个类被 <T>
的形式定义,那么它就被称为是泛型类。例如:
Cache<Integer> cache1 = new Cache<>();
Cache<String> cache2 = new Cache<>();
泛型可以接受多个泛型
/**
* @author: qujundong
* @date: 2020/11/28 下午5:23
* @description:
*/
public class CacheMultiType<E, T> {
private E value1;
private T value2;
public void setValue1(E value){
this.value1 = value;
}
public void setVaule2(T value){
this.value2 = value;
}
public static void main(String[] args) {
CacheMultiType<String, Integer> cache = new CacheMultiType<>();
cache.setValue1(new String("ssss"));
cache.setVaule2(new Integer(3));
}
}
2. 泛型方法
下面是泛型方法和泛型类结合的使用,从中可以看出来,泛型类和泛型方法表示符号如果同名,并不会发生冲突,但是为了易读性,通常不设置为相同的。
* @author: qujundong
* @date: 2020/11/28 下午5:26
* @description:
*/
public class CacheMethod<T> {
public <T> T testOneType(T value) {
System.out.println("one type");
return null;
}
public <T, E> E testTwoTypes(T value){
System.out.println("two types");
return null;
}
public static void main(String[] args) {
CacheMethod<Constant> cache = new CacheMethod();
cache.testOneType(new String("ss"));
cache.testOneType(123);
cache.testTwoTypes(new Date());
}
3. 泛型接口和泛型类相似
public interface<T> ICache{
public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型
}
// 实现类直接指定泛型类型,后续使用不需要声明
class Cache implements ICache<String>{ // 定义泛型接口的子类
private String var ; // 定义属性
public Cache(String var){ // 通过构造方法设置属性内容
this.setVar(var) ;
}
public void setVar(String var){
this.var = var ;
}
public String getVar(){
return this.var ;
}
}
public class GenericsDemo25{
public static void main(String arsg[]){
Info i = null; // 声明接口对象
i = new Cache("...") ; // 通过子类实例化对象
System.out.println("内容:" + i.getVar()) ;
}
}
// 生成对象的时候需要声明泛型
class Cache<T> implements ICache<T>{ // 定义泛型接口的子类
private T var ; // 定义属性
public Cache(T var){ // 通过构造方法设置属性内容
this.setVar(var) ;
}
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
};
public class GenericsDemo24{
public static void main(String arsg[]){
Info<String> i = null; // 声明接口对象
i = new Cache<String>("...") ; // 通过子类实例化对象
System.out.println("内容:" + i.getVar()) ;
}
}
2. 通配符
1. 无限制通配符
通配符在使用的时候用<?>表示,在我理解,泛型可以认为是一个实参,确定了以后不能改变,对应的类只能是对应的泛型及其子类,而通配符可以了解为“形参”,因为我们在写代码的时候并没有确定对应的参数,所以无法添加,每个通配符可以表示任何类型,在读取数据的时候非常方便。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author: qujundong
* @date: 2020/11/28 下午5:51
* @description:
*/
public class CacheWildCard{
public void readCache(List<?> value){
for(Object v : value){
System.out.println(v);
}
}
public void writeCache(List<?> value){
//无法添加,因为我们根本不知道?的类型,都没办法new对象
value.add(null);
}
public static void main(String[] args) {
Integer[] array = {1, 2, 3};
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(array));
CacheWildCard cache = new CacheWildCard();
cache.readCache(list);
cache.writeCache(list);
cache.writeCache(list);
System.out.println(list);
}
}
/*
1
2
3
[1, 2, 3, null, null]
*/
从上述代码中其实就可以看出为什么通配符无法添加数据,我们在读数据的时候都是用Object类型来便利的,那说明我们根本无法确定List<?>中?的类型,这个时候new的对象都不知道是什么,所以通配符无法添加数据。但是我们可以在list中添加null,因为添加null不需要确认类型。
2. 上界通配符
上界通配符表示为<? extends T>,表示可以接收T及其子类的对象。这里要声明一些,即使声明了上界,依然无法通过编译器添加元素
/**
* @author: qujundong
* @date: 2020/11/28 下午6:08
* @description:
*/
public class UpperCacheWildCard {
public void readCache(List<? extends Person> value){
for(Person v : value){
System.out.println(v);
}
}
public void writeCache(List<? extends Person> value){
//无法添加
// value.add(new Person(1, "name", LocalDate.now()));
}
public static void main(String[] args) {
ArrayList<Person> list = new ArrayList<>();
list.add(new Person(1, "name", LocalDate.now()));
UpperCacheWildCard cache = new UpperCacheWildCard();
cache.readCache(list);
}
}
为什么无法上界通配符无法添加数据其实很容易想清楚,如果代码中Person不是类,而是个接口,那么我们就无法生产Person实例
3. 下界通配符
下界通配符表示成<? supper T>,与无界通配符和上界通配符不同,有一定的添加数据能力,可以添加类型T的数据。
package item26;
import Entity.Man;
import Entity.Person;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* @author: qujundong
* @date: 2020/11/28 下午6:27
* @description:
*/
public class CacheLowerCacheWilderCard {
public void readCache(List<? super Man> value){
for(Object v : value){
System.out.println(v);
}
}
public void writeCache(List<? super Man> value){
value.add(new Man(1, "ss", LocalDate.now(), true));
//编译无法通过
//value.add(new Person(2, "dd", LocalDate.now()));
}
public static void main(String[] args) {
ArrayList<Man> list = new ArrayList<>();
list.add(new Man(234, "sssdd", LocalDate.now(), false));
CacheLowerCacheWilderCard cache = new CacheLowerCacheWilderCard();
cache.readCache(list);
cache.writeCache(list);
System.out.println(list);
}
}
Person{id=234, name='sssdd', localDate=2020-11-28} Man{sex=false}
[Person{id=234, name='sssdd', localDate=2020-11-28} Man{sex=false}, Person{id=1, name='ss', localDate=2020-11-28} Man{sex=true}]
下界通配符之所以可以添加数据,是因为下界Man肯定是一个类,那么可以通过new Man()生成实例。
3. 有界泛型
可以参照通配符,对泛型加上界限 ,例如
对于泛型加上下界线会报错,不太懂,以后再搞
package item26;
import Entity.Man;
import Entity.Person;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* @author: qujundong
* @date: 2020/11/28 下午6:48
* @description:
*/
public class CacheLowerAndUpperGeneric {
public <T, E extends T> void readUpperValue(List<E> values, T flag){
for(T t : values){
System.out.println(t);
}
}
public <T, E extends T> void writeUpperValue(List<E> values, T flag){
values.add((E)new Man(234, "sssdd", LocalDate.now(), false));
values.add((E)new String("好奇怪"));
}
//这里编译无法通过,会报错
// public <T, E super T> void readLowerValue(List<E> values, T flag){
// for(T t : values){
// System.out.println(t);
// }
// }
// public <T, E super T> void writeLowerValue(List<E> values, T flag){
// values.add((E)new Man(234, "sssdd", LocalDate.now(), false));
// values.add((E)new String("好奇怪"));
// }
public static void main(String[] args) {
CacheLowerAndUpperGeneric cache = new CacheLowerAndUpperGeneric();
ArrayList<Person> list1 = new ArrayList<>();
Person p1 = new Person(123, "ds", LocalDate.now());
Person p2 = new Man(234, "sssdd", LocalDate.now(), false);
list1.add(p1);
cache.readUpperValue(list1, p1);
cache.writeUpperValue(list1, p1);
System.out.println(list1);
}
}
Person{id=123, name='ds', localDate=2020-11-28}
Person{id=234, name='sssdd', localDate=2020-11-28} Man{sex=false}
[Person{id=123, name='ds', localDate=2020-11-28}, Person{id=234, name='sssdd',
localDate=2020-11-28} Man{sex=false}, Person{id=234, name='sssdd',
localDate=2020-11-28} Man{sex=false}, 好奇怪]
4. 类型擦除
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除,泛型擦除的目的是为了不同版本的java可以兼容。
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
打印的结果为 true 是因为 List<String>
和 List<Integer>
在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了。
public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
}
Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
打印的结果是
erasure class is:com.frank.test.Erasure
Class 的类型仍然是 Erasure 并不是 Erasure<T>
这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型。
Field[] fs = eclz.getDeclaredFields();
for ( Field f:fs) {
System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}
打印结果是
Field name object type:java.lang.Object
那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?
这种说法,不完全正确。
我们更改一下代码。
public class Erasure <T extends String>{
// public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
}
现在再看测试结果:
Field name object type:java.lang.String
我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>
则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>
则类型参数就被替换成类型上限。
所以,在反射中。
public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
public void add(T object){
}
}
add() 这个方法对应的 Method 的签名应该是 Object.class。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
System.out.println(" method:"+m.toString());
}
打印结果是
method:public void com.frank.test.Erasure.add(java.lang.Object)
也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod("add",Object.class)
否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。