Java泛型详解
来源:互联网 发布:宜信大数据 编辑:程序博客网 时间:2024/05/04 16:17
为什么要来再详究一遍泛型
当初学习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<list.size();i++){ // 再取第二个值时会出异常 java.lang.ClassCastException String name = (String)list.get(i); println("name: "+name); } }}
测试运行时会报出异常java.lang.ClassCastException
,这是类型不匹配的异常。List默认的类型是Object类型的,什么类型的对象都可以往里面装。装入时是Integer
类型的然后强制转为String类型自然会出错
从上面可以看出两个问题:
- 当一个对象放入集合中时,集合并不会记住这个对象本来的类型;当该对象从集合中取出时,它的编译类型就变成了Object类型,但运行时还是会按照其本来的类型运算(这就是为什么编译时不会报错,允许强制转换,运行时却出异常的原因)
- 当从一个集合中取出对象时,因为可能不知道其真实类型而去强制转换,这是很容易触发
java.lang.ClassCastException
所以就有了这么 一个需求:如何可以使集合“记住”元素的类型,并在运行时不会出现java.lang.ClassCastException的异常呢?
什么是泛型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
简单说就是将一个类型当作参数传入另一个接口/类/方法的参数
于是将上面代码改为:
public class TestGeneric extends NewPrint{ List<String> newl = new ArrayList<String>(); @Test public void testNewList(){ newl.add("hello"); // 这里会直接拒绝加入Integer类型的元素 //newl.add(100); newl.add("scora"); for(int i=0;i<newl.size();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<E> iterator(); Object[] toArray(); <T> 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<E> listIterator(); ListIterator<E> listIterator(int index); List<E> subList(int fromIndex, int toIndex);}
在List接口中采用泛型化定义之后,中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。
注意一下这两个常用的方法:
boolean add(E e);
E get(int index);
第一个方法一定需要类型的参数,第二个方法一定会返回一个类型的对象,这也就解释了上面为什么add加入一个非String类型的值会直接提示出错,为什么从集合中取值不再需要强制类型转换。
当然了,这只是List接口的定义,ArrayList实现类既然实现了List,那么一定会重写add()方法和get()方法,所以其也是需要类型的参数的
使用泛型的好处
谈完了什么是泛型,按照我的节奏,我一般都会去想一想使用它的好处都有什么
- 类型安全:在使用时对一个对象进行了限制,只有约定类型的对象才能继续,编译器在编译时期也可以进行类型检查
- 避免强制类型转换:因为前面已经约束了类型,所以在使用时就已知了类型,便省去了类型转换的过程,使得代码更加可读,也减少了出错的机会
- 潜在的性能收益:泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。
泛型类、泛型接口和泛型方法
大概知晓了泛型的知识,来看看我们如何使用泛型:
泛型类
使用示例:
class A<E>{ 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<String> a = new A<String>("socra"); @Test public void testA(){ println(a.getE()); }}
对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?
@Test public void testGenericityType(){ A<String> str = new A<String>("socra"); A<Integer> no = new A<Integer>(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<E>{ 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<String> a = new A<String>(); a.setE("socra"); String str = a.getE(); }}
同样反编译查看字节码可以发现:
可以看到两者的字节码是一样的,然后注意到checkcast
这个部分,这是检查类型的语句,事实上这才是关键所在。
编译时擦除了泛型的参数类型信息,在编译时在边界地方开始检查类型,所谓边界就是对象进入和离开的地方。
- 在实例一中,会在强制转型的地方开始检测参数类型;
- 在实例二中,会在调用getObject()方法处检查参数类型
综上,我们知道为什么泛型可以知道参数类型信息了(先擦除后检查类型,毕竟泛型的主要目的之一就是希望将错误检测移入到编译期
泛型中的边界
边界可以在泛型的参数类型上设置限制条件,例如class A<T extends B>
,表示的意思就是参数类型必须是B类型或者是继承自B的子类。
泛型接口
看过了上面泛型类的例子,就知道泛型接口就是接口有参数类型
public interface B<E>{ public void setE(E e); public E getE();}class NewB implements B<String>{ // 泛型接口中的泛型对象定义在实现类中 String name ; @Override public void setE(String str){ this.name = str; } @Override public String getE(){ return name; }}
注意接口声明的小细节
- 接口的默认访问修饰符是protected
- 接口中的属性只能是static或final修饰的已知类型的对象,同时接口中不允许声明构造方法
- 泛型对象需要在实现类中定义
泛型方法
关注到这个是因为学到了hibernate中的某一个方法,看一看Java中的泛型方法。
/** * 泛型方法 * <T> 用来声明该方法为泛型方法 * @param t 参数类型对象 * @return */ public static <T> T display(T t){ println("hello,这里是泛型方法"); return t; } @Test public void testDisplay(){ String name = "socra"; String name2 = display(name); println(name2); }
这里还有dalao提供的进阶版泛型方法,当然了,框架中使用的泛型方法就是这种类型:
/** * 基于反射的泛型方法 * Class<T> 声明泛型的T的具体类型 * @param t 是泛型T类的需要被代理的对象 * @return 实例化的代理对象 * @throws IllegalAccessException 安全权限异常 * @throws InstantiationException 实例化异常 */ public <T> T getObject(Class<T> 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<String> str = new A<String>("socra"); A<Integer> no = new A<Integer>(10); aPrintln(str); // socra aPrintln(no); // 10 }
可以看到将A
通配符的上下界
其实这是泛型边界的定义,上文也有说到,但边界也可用于通配符中
- 类型通配符上界:
<? extends T>
,必须是T类或者其子类 - 类型通配符下界:
<? super E>
,必须是E类或者是E类的父类
泛型数组?
不存在的,Java中没有泛型数组这么一说,所有想用到泛型数组的地方都可以使用List<E>
来代替
话尾
一不小心怎么研究了这么多,前前后后加上翻书查资料加上做做小实验,4个小时+应该是有的,不敢说翻了个底朝天,掌握大部分应该是有的。
其实很喜欢这种状态。
当然了,对Java掌握的越深越好啊 :-),还是那句话,先狗后人
- Java泛型详解
- java泛型详解
- java泛型详解
- Java泛型详解
- Java泛型详解
- java泛型详解
- Java泛型详解
- java泛型详解
- java 泛型详解
- java 泛型详解
- Java 泛型详解
- java 泛型详解
- java 泛型详解
- java泛型详解
- java 泛型详解
- Java泛型详解
- java 泛型详解
- java 泛型详解
- CF Round #412( Div.2) Success Rate
- 卷积算子计算方法(卷积运算)
- exploit-db上的WordPress和JoomlaP相关
- CCF NOI1121 逆波兰表达式
- UVA, 10104 Euclid Problem
- Java泛型详解
- Unity笔记-打飞碟游戏
- 学习
- 对Mac下Python添加路径PATH的总结
- 补二:5-5
- Java 使用 FTP 实现大文件上传下载
- Storm学习笔记(一)
- 基于 Apache 的服务器Subversion安装与配置
- 最大子矩阵和