泛型详解

今天跟着frank大神的blog 传送门学习泛型的知识。

首先看一下下面代码

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
		
System.out.println(l1.getClass() == l2.getClass());

上次在讲解反射机制的时候通过反射举了个例子和这个是有关的。
上面结果输出是true,而造成输出结果的原因是泛型擦除

泛型的概念

泛型就是一句话:将类型参数化
那什么又是类型参数化呢?
举一个例子:

public class Cache {
	Object value;

	public Object getValue() {
		return value;
	}

	public void setValue(Object value) {
		this.value = value;
	}
	
}

假设这个Cache可以存取任何的值,那我们可以这样利用它:

Cache cache = new Cache();
cache.setValue(134);
int value = (int) cache.getValue();
cache.setValue("hello");
String value1 = (String) cache.getValue();

使用起来很简单,我们只需要在get的时候强转就行了。

而泛型可以做到相同的功能,却带来了另外一种体验:

public class Cache<T> {
	T value;

	public Object getValue() {
		return value;
	}

	public void setValue(T value) {
		this.value = value;
	}
	
}

这里就是体现了泛型的作用,它将value这个类型参数化了。再来看看它的使用方法:

Cache<String> cache1 = new Cache<String>();
cache1.setValue("123");
String value2 = cache1.getValue();
		
Cache<Integer> cache2 = new Cache<Integer>();
cache2.setValue(456);
int value3 = cache2.getValue();

看来和上面的相比,它的好处就是不用强制转换类型了。
而当一个new出一个并确定好泛型后,就不能再放置别的类型了,比如上面的cache2,当我们对其setValue(“hello”)的时候,编译都不能通过。

从上可以总结:

  • 相比于直接用Object这样代替一切类型而言,泛型更加符合面向抽象开发的软件编程宗旨,因为泛型可以使具体的数据类别都传入进去。
  • 当泛型的具体类型确定以后,泛型提供了类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译无法通过。
  • 提高了代码可读性

泛型的使用

泛型根据使用情况可以分为三种方法:

  1. 泛型类
  2. 泛型方法
  3. 泛型接口

泛型类
我们可以这样定义一个泛型类:

public class Test<T> {
	T field1;
}

<>尖括号中的T被称作是类型参数,用于指代任意类型,事实上,T只是一种习惯性的写法,我们甚至可以把T换成任意的字母,不过出于规范我们把单个大写字母来表示一些参数类型:

  1. T:表示任何类
  2. E:表示Element,或者Expection
  3. K:代表key
  4. V:代表value
  5. S:代表SubType,后面会讲到

泛型的使用,最前面已经讲解过一个Cache的例子了。

当然泛型不止接受一个类型参数,他还可以接受多个类型参数:

public class MultiType <E,T>{
	E value1;
	T value2;
	
	public E getValue1(){
		return value1;
	}
	
	public T getValue2(){
		return value2;
	}
}

泛型方法

public class Test1 {

	public <T> void testMethod(T t){
		
	}
}

泛型方法和泛型类不同之处在于,泛型方法中<>是写在返回值前的,而< T>中的T不是运行时真正的参数。
当然,声明的类型参数,也可以当成返回值类型返回的。

泛型方法和泛型类共存现象

public class Test1<T>{

	public  void testMethod(T t){
		System.out.println(t.getClass().getName());
	}
	public  <T> T testMethod1(T t){
		return t;
	}
}

有人会问,泛型类中的参数T和泛型方法中的参数T有没有联系?
上述代码中 testMethod只是Test1中的一个普通方法,而testMethod1则是一个泛型方法。
泛型类的类型参数和泛型方法的类型参数没有任何的联系,泛型方法只以自己定义的类型参数为标准。

针对上面,可以写出这样的代码:

Test1<String> t = new Test1();
t.testMethod("generic");
Integer i = t.testMethod1(new Integer(1));

就是我们定义了一个泛型为String的泛型类,里面的泛型方法我们却可以传入Integer~
但是为了避免混淆,我们最好不要再泛型类中那样写,我们可以改成:

public class Test1<T>{

	public  void testMethod(T t){
		System.out.println(t.getClass().getName());
	}
	public  <E> E testMethod1(E e){
		return e;
	}
}

泛型接口
和泛型类差不多:

public interface InterfaceTest<T> {
}

通配符

除了<T>外,还有<?>,?通常被称为通配符。
为什么已经有了T,还要引进?这样的概念呢?

class Base{}

class Sub extends Base{}

Sub sub = new Sub();
Base base = sub;		

上面代码显示,Base是Sub的父类,它们是继承的关系,所以Sub的实例可以给Base引用赋值,那么:

List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;

这样的代码是合理、成立的吗?

答案是否定的,这样的代码不能通过编译。
因为虽然Sub虽然是Base的子类,但是不代表List< Sub>是List< Base>的子类呀。

实际开发中我们有时候会有这样的需求,而通配符的出现就是为了解决这个问题的。

通配符的出现是为了指定泛型中的类型范围。

通配符有3中类型:

  1. <?>被称作无限定的通配符
  2. < ? extends T>被称为有上限的通配符
  3. < ? super T>被称为有下限的通配符

无限定通配符<?>
无限定通配符经常与容器类 配合使用,它其中的?代表的是未知类型,所以涉及到了?操作时,一定与具体的类型无关。

public void testWildCards(Collection<?> collection){
}

这里代码表示,testWildCards无需关注collection的真实类型,因为其类型参数是位置的,所以你只能调用collection中与类型无关的方法。
在这里插入图片描述
我们可以知道当?修饰了Collection的参数时,Collection就丧失了add()的方法。编译器不能通过。

我们再来看看代码:

List<?> wildlist = new ArrayList<String>();
wildlist.add(123);// 编译不通过

通常认为<?>提供了只读功能,它删减了增删改具体数据的能力,只保留与具体类型无关的功能。比如它修饰容器时只关心容器的大小、元素的数量。
那<?>的作用这么弱,为什么我们还要引用它呢?

可能是为了提高代码的可读性吧。我们看到这种代码,就会建立起简洁的形象。

< ? extends T>

<?>中的?代表未知,但是我们的确需要对类型描述的再具体一点,对?描述的范围进行缩小,比如说它是A类或者A类的子类都行。
public void testSub(Collection<? extends Base> para){
 }

上面代码中,para这个Collection接受的是Base或者Base的子类。
但是它仍然丧失了写操作的能力,也就是说

para.add(new Sub());
para.add(new Base());

编译这样编译仍然不能通过。但是我们至少搞清楚了它要表示的范围。

< ? super T>
这个和前面的相对应,代表?是T或者T的超类(即父类)。
神奇的在于,它有一定的写操作能力。

public void testSuper(Collection<? super Sub> para){ 

             para.add(new Sub());//编译通过
             para.add(new Base());//编译不通过 
 } 

通配符与类型参数的区别 一般而言,通配符能干的事情都可以用类型参数替换。
比如

 public void testWildCards(Collection<?> collection){
 }

可以被类型参数替换

public <T> void test(Collection<T> collection){
}

值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。

public <T> void test(Collection<T> collection){
	collection.add((T)new Integer(12));
	collection.add((T)"123");
}

需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明:

public class Test2 <T,E extends T>{
	T value1;
	E value2;
}

E 类型是 T 类型的子类,显然这种情况类型参数更适合。
有一种情况是,通配符和类型参数一起使用:

public <T> void test(T t,Collection<? extends T> collection){
}

类型擦除

之前在讲反射的时候提过一句话:
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
		
System.out.println(l1.getClass() == l2.getClass());

最后得到的结果都是是 List.class,他们的String、Integer都被擦除了。

那Stirng、Integer会怎么办?答案是泛型转译。

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.test.Erasure

Class的类型仍然是Erasure而不是Erasure< T>,那我们再看看泛型类中T在运行时是什么类型:

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>,则类型参数就被替换成类型上限。

类型擦除带来的局限性
类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。
在这里插入图片描述
上述就是局限性的体现。
但是我们可以通过反射机制来解决这个问题。

泛型值得注意的地方

  1. 泛型不接受8中基础数据类型
  2. Java 不能创建具体类型的泛型数组
    比如:
List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];

上述代码不能通过编译,但是通配符却可以解决:

List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];

借助于无限定通配符却可以,前面讲过 ?代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作,这个在前面通配符的内容小节中已经讲过。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值