当初学习Java时并没有觉得这个有多重要,又不像C++,我有现成的集合框架可以使用,我管你泛型干吗,(滑稽
现在慢慢的学到了JavaEE的一些知识,所起来,框架中的原理知识除了有Java的反射机制,还大量的用到了泛型的知识,随便点开一个方法的源码,很容易发现有泛型的痕迹。但是仔细一想,这点似乎并没有搞清楚,所以
RT,本次讨论的主要目标,泛型。为了节省时间,一下的研究主要内容来自先驱者的博文指导,所以可以算是转载,侵删
这个探讨的节奏深得我心啊,先说是不是,再问为什么(滑稽
先看一段代码:
/**
* 主要是为了深入了解学习 泛型
* NewPrint类是写的简化输出的工具类
*/
public class TestGeneric extends NewPrint{
List list = new ArrayList();
@Test
public void test(){
list.add("hello");
list.add(100);
for(int i=0;i// 再取第二个值时会出异常 java.lang.ClassCastException
String name = (String)list.get(i);
println("name: "+name);
}
}
}
测试运行时会报出异常java.lang.ClassCastException
,这是类型不匹配的异常。List默认的类型是Object类型的,什么类型的对象都可以往里面装。装入时是Integer
类型的然后强制转为String类型自然会出错
从上面可以看出两个问题:
java.lang.ClassCastException
所以就有了这么 一个需求:如何可以使集合“记住”元素的类型,并在运行时不会出现java.lang.ClassCastException的异常呢?
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
简单说就是将一个类型当作参数传入另一个接口/类/方法的参数
于是将上面代码改为:
public class TestGeneric extends NewPrint{
List newl = new ArrayList();
@Test
public void testNewList(){
newl.add("hello");
// 这里会直接拒绝加入Integer类型的元素
//newl.add(100);
newl.add("scora");
for(int i=0;i// 再取第二个值时会出异常 java.lang.ClassCastException
String name = newl.get(i);
println("name: "+name);
}
}
}
采用泛型写法后,当想插入非String类型的对象时就会直接提示出错,同时当从集合中取值时也没有必要强制类型转换。
可以得知在List中,String是类型实参,也就是说,相应的List接口中肯定含有类型形参。且get()方法的返回结果也直接是此形参类型(也就是对应的传入的类型实参)。
看一看List的源码:
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator iterator();
Object[] toArray();
T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection> c);
boolean addAll(Collection extends E> c);
boolean addAll(int index, Collection extends E> c);
boolean removeAll(Collection> c);
boolean retainAll(Collection> c);
void clear();
boolean equals(Object o);
int hashCode();
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator listIterator();
ListIterator listIterator(int index);
List subList(int fromIndex, int toIndex);
}
在List接口中采用泛型化定义之后,中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。
注意一下这两个常用的方法:
boolean add(E e);
E get(int index);
第一个方法一定需要类型的参数,第二个方法一定会返回一个类型的对象,这也就解释了上面为什么add加入一个非String类型的值会直接提示出错,为什么从集合中取值不再需要强制类型转换。
当然了,这只是List接口的定义,ArrayList实现类既然实现了List,那么一定会重写add()方法和get()方法,所以其也是需要类型的参数的
谈完了什么是泛型,按照我的节奏,我一般都会去想一想使用它的好处都有什么
大概知晓了泛型的知识,来看看我们如何使用泛型:
使用示例:
class A{
private E e;
public A(){ }
public A(E e){
this.e = e;
}
public void setE(E e){
this.e = e;
}
public E getE(){
return e;
}
}
public class MyGenericityClass extends NewPrint{
A a = new A("socra");
@Test
public void testA(){
println(a.getE());
}
}
@Test
public void testGenericityType(){
A str = new A("socra");
A no = new A(10);
println(str.getClass()); // class Genericity.A
println(no.getClass()); // class Genericity.A
System.out.println(str.getClass()==no.getClass()); // true
}
输出结果竟不是预料的,原以为在编译时,编译器会将所有的泛型擦除变成其真实的类型,但现在看来似乎不是这样
由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。
究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦除,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
很奇怪,不是吗?(《Think in Java》P372之后的章节有详述
一个很玄学的东西:Java泛型中的具体类型信息在运行时都被擦除了,而被当作泛型类型的对象去使用。(在泛型代码内部是无法获取任何有关泛型参数类型的信息)
在基于擦除的实现中,泛型类型被当作第二类类型被处理(没有具体化),即不能在某些重要的上下文中使用的类型。泛型类型只有在静态类型检查期间才会出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如,List这样的类型注解被擦除为List,而普通的类型变量类型在未指定边界的情况下将被擦除为Object
那么问题来了,我们是如何得知泛型中参数类型的呢?毕竟我们在运行时还需要检查其类性呢
首先在编写代码时进行检查就容易实现了,编辑器自动检测泛型类型是否一致,下面来看看编译运行时的检测办法:
首先是一点不使用泛型的代码:
public class Test2{
static class A{
private Object obj;
public A(){ }
public A(Object obj){
this.obj = obj;
}
public void setObject(Object obj){
this.obj = obj;
}
public Object getObject(){
return obj;
}
}
public static void main(String[] args){
A a = new A();
a.setObject("socra");
String str = (String)a.getObject();
}
}
接下来看同样操作使用泛型的写法:
public class Test{
static class A{
private E e;
public A(){ }
public A(E e){
this.e = e;
}
public void setE(E e){
this.e = e;
}
public E getE(){
return e;
}
}
public static void main(String[] args){
A a = new A();
a.setE("socra");
String str = a.getE();
}
}
同样反编译查看字节码可以发现:
可以看到两者的字节码是一样的,然后注意到checkcast
这个部分,这是检查类型的语句,事实上这才是关键所在。
编译时擦除了泛型的参数类型信息,在编译时在边界地方开始检查类型,所谓边界就是对象进入和离开的地方。
综上,我们知道为什么泛型可以知道参数类型信息了(先擦除后检查类型,毕竟泛型的主要目的之一就是希望将错误检测移入到编译期
边界可以在泛型的参数类型上设置限制条件,例如class A
,表示的意思就是参数类型必须是B类型或者是继承自B的子类。
看过了上面泛型类的例子,就知道泛型接口就是接口有参数类型
public interface B<E>{
public void setE(E e);
public E getE();
}
class NewB implements B{
// 泛型接口中的泛型对象定义在实现类中
String name ;
@Override
public void setE(String str){
this.name = str;
}
@Override
public String getE(){
return name;
}
}
关注到这个是因为学到了hibernate中的某一个方法,看一看Java中的泛型方法。
/**
* 泛型方法
* 用来声明该方法为泛型方法
* @param t 参数类型对象
* @return
*/
public static T display(T t){
println("hello,这里是泛型方法");
return t;
}
@Test
public void testDisplay(){
String name = "socra";
String name2 = display(name);
println(name2);
}
这里还有dalao提供的进阶版泛型方法,当然了,框架中使用的泛型方法就是这种类型:
/**
* 基于反射的泛型方法
* Class 声明泛型的T的具体类型
* @param t 是泛型T类的需要被代理的对象
* @return 实例化的代理对象
* @throws IllegalAccessException 安全权限异常
* @throws InstantiationException 实例化异常
*/
public T getObject(Class t) throws InstantiationException, IllegalAccessException{
T newt = t.newInstance(); // 基于反射创建对象
return newt;
}
从上面的例子中,可以得知A和A其实还是一种类型,那么能否将这两种类型看作是与A类型有关系的父子类型呢?
这里就需要有一个引用类型,用来在逻辑上表示形如A和A父类的引用类型。这就引出了我们的关注焦点——类型统配符。
java中类型通配符一般是使用?
代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且A
public static void aPrintln(A> a){
println(a.getE());
}
@Test
// 用以测试通配符
public void testWildcard(){
A str = new A("socra");
A no = new A(10);
aPrintln(str); // socra
aPrintln(no); // 10
}
可以看到将A
其实这是泛型边界的定义,上文也有说到,但边界也可用于通配符中
extends T>
,必须是T类或者其子类 super E>
,必须是E类或者是E类的父类不存在的,Java中没有泛型数组这么一说,所有想用到泛型数组的地方都可以使用List
来代替
一不小心怎么研究了这么多,前前后后加上翻书查资料加上做做小实验,4个小时+应该是有的,不敢说翻了个底朝天,掌握大部分应该是有的。
其实很喜欢这种状态。
当然了,对Java掌握的越深越好啊 :-),还是那句话,先狗后人