【java核心技术卷一】泛型程序设计
2015-12-03 16:37
267 查看
泛型程序设计
为什么要使用泛型程序设计?
泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。 例如
ArrayList<T>,其中
T可以为任意对象,这也就是说该
ArrayList的操作都是以
T为基本单位的,
T称为类型参数(type parameters)。
ArrayList类有一个类型参数用来指示元素的类型
[code]ArrayList<T> list = new ArrayList<T>();
在使用时一看就知道
list包含的是
T类型对象
在J***A SE7及以后的版本,构造函数中可以省略泛型类型
[code]ArrayList<T> list = new ArrayList<>();
省略的类型可以从变量的类型推断得出
泛型程序设计使在对
list中的对象进行使用时,可以避免使用错误的方法或参数,让程序具有更好额可读性和安全性。
定义简单泛型类
一个泛型类(generic class)就是具有一个或者多个类型变量的类。对于这个类来说,我们只关注泛型,而不会为数据储存的细节烦恼。[code]public class Pair<T>{ private T first; private T second; public Pair(){ first = null; second = null; } public Pair(T first , T second){ this.first = first; this.second = second; } public T getFirst(){return first;} public T getSecond(){return second;} public void setFirst(T newValue){first =newValue;} public void setSecond(T newValue){second = newValue;} }
pair类引入一个类型变量
T,用尖括号
<>括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以定义
Pair类,其中第一个域和第二个域使用不同的类型
[code]public class Pair <T,U> {...}
在java中,类型变量使用大写形式且较短为常见。在java库中,使用变量
E表示集合的元素类型,
K和
V分别表示表的关键字(key)和值(Value)的类型。
T以及附近
U、
S表示任意类型
泛型方法
定义一个带有类型参数的简单方法[code]class ArrayAlg{ public static <T> T getMiddle(T...a){ return a[a.length/2]; } }
这个方法使在普通类中定义的,而不是在泛型类中定义的,但这是一个泛型方法。注意,类型变量放在修饰符的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:
[code]String middle = ArrayAlg.<String>getMiddle("aa","bb","cc");
在这种情况下(也是大多数情况下),方法调用中可以省略
<String>类型参数。编译器有足够的信息能够推断出所调用的方法。它用
String[]与泛型类型
T[]进行匹配并推断出
T一定是
String。也就是说,可以调用
[code]String middle = ArrayAlg.getMiddle("aa","bb","cc");
几乎在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会给出错误提示:
[code]double middle = ArrayAlg.getMiddle(3.14 , 123 , 0);
错误消息会以晦涩的方法指出:解释这句代码有两种方法,而且这两种方法都是合法的。简单地说,编译器会自动打包参数为一个
Double和两个
Integer对象,而后寻找这些类的共同超类型。事实上,找到2个这样的超类:
Number和
Comparable接口,其本身也是一个泛型类型。这种情况下的措施就是将所有参数改为
Double类型
类型变量的限定
有时候,类或方法需要对类型变量加以束缚。例如求数组中最小元素[code]class Test{ public static <T> T min(T[] a){ if(a ==null||a.length==0){ return null; }else{ T smallest = a[0]; for(int i = 1 ; i<a.length ;i++ ){ if(smallest.compareTo(a[i]>0))smallest = a[i]; return smallest; } } } }
但是变量
smallest的类型为
T,这意味着它可以是任何一类的对象,如何确信
T所属的类有
compareTo方法?
解决问题的方法是将
T限制为实现了
Comparable接口的类。可以通过对类型变量
T设置限定实现这一点
[code]public static <T extends Comparable> T min(T[] a){...}
现在,泛型的
min方法只能被实现了
Comparable接口的类的数组调用。
但为什么实现接口的关键字是
extends而不是
implements?毕竟
Comparable是一个接口
[code]<T extends BoundingType>
表示
T应该是绑定类型的子类型(subType)。
T和绑定类型可以是累,也可以是接口。选择
extends是因为更接近子类的概念。
一个类型变量或通配符可以有多个限定,例如
[code]T extends Comparable & Serializable
限定类型用
&分隔,而类型变量用
,分隔。
Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。
[code]public class A <T extends List&Comparable&Serializable> implements Serializable{}
约束与局限性
不能使用基本类型实例化类型参数没有
Pair<double>,只有
Pair<Double>。原因是类型擦除,擦除之后
Object类型不能储存
double的值。而当包装器类型不能接受替换时,可以使用独立的类和方法处理
运行时类型查询只适用于原始类型
所有类型查询只能产生原始类型。
例如
[code]if(a instanceof Pair<String>) //ERROR if(a instanceof Pair<T> )//ERROR Pair<String> p = (Pair<String>) a;//WARNING
无论何时使用
instance或涉及泛型类型的强制类型转换表达式都会看到一个警告。
同样,getClass总是返回原始类型
[code]Pair<String> a = ...; Pair<Double> b = ...; if(a.getClass()==b.getClass()) //always true
因为
getClass的返回结果都是
Pair.class
不能创建参数化的数组
例如
[code]Pair<String>[] table = new Pair<String>[10];//ERROR
擦除之后,table的类型是Pair[],可以转换为Object[]:
[code]Object[] objarray = table;
数组会记住它的元素类型,如果试图储存其他类型的元素,就会跑出一个
ArrayStoreException异常
[code]objarray[0] = "hello" //ERROR component type is Pair
Varargs警告
向参数个数可变的方法传递一个泛型类型的实例。例如下面这个方法
[code]public static <T> void addAll(Collection<T> coll,T ... ts){ for(t:ts) coll.add(t); }
实际上参数
ts是一个数组,包含提供的所有实参
[code]Collection <Pair<String>> table = ...; Pair<String> pair1 = ...; Pair<String> pair2 = ...; addAll(table,pair1,pair2);
为了调用这个方法,JVM必须建立一个
Pair<String>数组,但这违反了前面的规则。但你只会得到一个警告。
可以使用
@SuppressWarnings("unchecked")或者在Java SE7中使用
@SafeVarargs来标注
addAll方法
不能实例化类型变量
不能使用
new T(...),
new T[...]或者
T.class这样的表达式中的类型变量。例如
[code]public Pair(){first = new T();} //ERROR
因为在类型擦除后会使得
first为
Object,而本意肯定并非如此。不过可以使用烦着调用
Class.newInstance方法来构造泛型对象。但非下面这样
[code]first = T.class.newInstance(); //ERROR
表达式是不合法的,必须像下面这样
[code]public static <T> Pair<T> makePair(Class<T> cl){ try{ return new Pair<>(cl.newInstance(),cl.newInstance()); }catch(Exception e){ return null; } }
这个方法可以按照下列方式调用
[code]Pair<String> p = Pair.makePair(String.class);
注意,
Class类本身是泛型。例如,
String.class是一个也是唯一
Class<String>的实例。因此,
makePair可以判断
pair的类型。
不能构造一个泛型数组,类型参数会让这个方法永远构造
Object[]数组。
如果数组仅仅作为一个类的私有实例,就可以将这个数组声明为
Object[],并在获取元素时进行类型转换。
泛型类的静态上下文中类型变量无效
不能在静态实例域或方法中引用类型变量
[code]public class Pair<T>{ private T pair1; //ERROR public T getPair1(){ //ERROR return pair1; } }
不能抛出或捕获泛型类的实例
不能抛出或捕获泛型类对象,甚至泛型类扩展
Throwable都是不合法的
catch子句中不能使用类型变量
[code]public static <T extends Throwable> void do(Class<T > t){ try{ ... }catch(T e){ //ERROR ... } }
不过,在异常规范中使用类型变量是允许的
[code]public static <T extends Throwable> void do(Class<T > t) throws T{ //OK try{ ... }catch(Throwable cause){ t.initCause(cause); throw t; } }
擦除后的冲突
要想支持擦除的转换,就需要强行限制一个类或者类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化
泛型类型的继承规则
永远可以将一个参数化类型转换为一个原始类型。例如
Student是
Person的一个子类,但是
Pair<Student>并不是
Pair<Person>的子类,他们抽象化后都是
Pair<T>
通配符类型
[code]Pair<? extends Employee>
表示任何泛型
Pair类型,它的类型参数是
Employee的子类,如
Pair<Manager>,但不是
Pair<String>
假如要编写一个打印雇员的方法,例如
[code]public static void printBuddies(Pair<Employee> p){ System.out.print(p.getFirst.getName()); }
正如泛型类型的继承规则,不能将
Pair<Manager>传递给这个方法,这一点很受限,但是可以用通配符解决
[code]public static void printBuddies(Pair<? extends Employee> p )
类型
Pair<Manager>是
Pair<? extends Employee>的子类型
通配符的超类型限定
通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定(supertype bound):[code]? super Manager
这个通配符限制为
Manager的所有超类型。
带有超类型限定的通配符,可以为方法提供参数,但不能使用返回值。例如,
Pair<? super Manager>有方法
[code]void setFirst(? super Manager)
[code]? super Manager getFirst()
编译器不知道
setFirst方法的确切类型,但是可以用任意
Manager对象调用它,而不能用
Employee对象调用。然而,如果调用
getFirst,返回的对象类型就不会得到保证,只能把它赋给一个
Object
直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取
无限定通配符
无限制的通配符Pair<?>看起来好像与原始的
Pair类型一样,实际上有很大不同。类型
Pair<?>有方法如下所示:
[code]? getFirst() void setFirst(?)
getFirst()的返回值只能赋给一个
Object,
setFirst方法不能被调用,甚至不能用
Object调用
Pair<?>和
Pair本质的不同在于:可以用任意
Object对象调用原始的
Pair类的
setObject方法
可以调用
setFirst(null)
为什么要使用这样的类型?例如,下面这个方法用来测试一个
pair是否包含一个
null引用,它不需要实际的类型。
[code]public static boolean hasNulls(Pair<?> p){ return p.getFirst()==null||p.getSecond()==null; }
通过将
hasNulls转换成泛型方法,可以避免使用通配符类型
[code]public static <T> boolean hasNulls(Pair<T> p)
但是,带有通配符的版本可读性更强
通配符捕获
一个交换pair元素的方法
[code]public static void swap(Pair<?> P)
通配符不是类型变量,因此,不能再编写代码中使用“?”作为一种类型。也就是说下列代码是非法的
[code]? t = p.getFirst() //ERROR p.setFirst(p.getSecond()) p.setSecond(t)
在交换的时候必须临时保存第一个元素。不过我们可以写一个辅助方法
swapHelper
[code]public static <T> void swapHelper(Pair<T> p){ T t = p.getFirst(); p.setFirst(p.getSecond()); p.setSecond(t); }
swapHelper是一个泛型方法,而
swap不是,它具有固定的
Pair<?>类型参数
现在可以用
swap调用
swapHepler
[code]public static void swap{swapHepler(p);}
这种情况下,
swapHepler方法的参数
T捕获通配符。他不知道是哪种类型的通配符,但是,这是明确的类型,而且
<T>swapHepler的定义只有在
T指出类型时才有明确的含义
反射和泛型
现在,Class类是泛型的。例如,
String.class实际上是一个也是唯一一个Class`类的对象。
类型参数十分有用,这是因为它允许
Class<T>方法的返回类型更加具有针对性。下面
Class<T>中的方法就是用了类型参数
[code]T newInstance() T cast(Object obj) T[] getEnumConstants() Class<? super T> getSuperClass() Constructor<T> getConstructor(Class ... param) Constructor<T> getDeclaredConstructor(Class ... param)
newInstance方法返回一个实例,这个实例所属的类由默认的构造器获得。他的返回类型目前声明为
T,其类型与
Class<T>描述的类相同,这样就免除了类型转换。
如果给定的类型确实是
T的一个子类型,
cast方法就会返回一个现在声明为类型
T的对象,否则,抛出一个
BadCastException异常
如果这个类不是
enum类或者类型
T的枚举值的数组,
getEnumConstans方法将返回
null
最后,
getConstructor与
getdeclaredConstructor方法返回一个
Constructor<T>对象。
Constructor类也已经变成泛型,以便
newInstance方法有一个正确的返回类型
相关文章推荐
- java编译错误 程序包javax.servlet不存在javax.servlet.*
- SpringDataJpa增删改查
- Eclipse : 找不到或无法加载主类
- Spring MVC+hibernate+Spring的框架搭建中遇到的问题总结
- Mac OS 上设置 JAVA_HOME
- 从小白慢慢往上爬的历程--几种简单的流程控制语句
- Java中的instanceof关键字
- java项目打包成可执行jar文件
- 三、RSA
- 查看java内存情况命令
- JavaEE_Mybatis_SpringMVC_Mybatis_lesson13_多对多关联映射(ResultMap)
- spring mybatis mvc cache 缓存 二级缓存
- Eclipse开发工具组合键介绍
- 【Java基础】Java IO流的总结
- 在Eclipse中创建Maven多模块工程的例子
- spring restful 入门例子
- Struts2 框架的简单实现——轻松理解Struts原理
- java 线程池的用法
- Spring @Resource、@Autowired、@Qualifier的注解注入及区别
- 二、3DES