三、类型参数<T>与无界通配符<?>
3-1、区别使用
首先要区分开两种不同的场景:
- 声明一个泛型类或泛型方法。这种情况下要使用类型参数“<T>”
- 使用泛型类或泛型方法。这种情况下要使用无界通配符“<?>”。具体解释,就是对已经存在的泛型,我们不想给她一个具体的类型做为类型参数,我们可以给她一个不确定的类型作为参数,(前提是这个泛型必须已经定义)
<T>的作用是保证所有出现T的地方,指代的都是同一种类型,是一种约束。
<?>的作用是保证能在容器类里面放入各种不同类型的元素(顺便说,?的英文是wildcard:通配符)。通配符?是不能用来声明泛型的。以下声明直接报错:
public class GenericDemo<?> // 报错
<?>的使用例:用<?>声明List容器的变量类型,然后用一个实例对象给它赋值。
List<?> list = new ArrayList<String>();
或者
public static void showObj(List<?> list) {
for (Object object : list) {
System.out.println(object);
}
}
小结(伪代码)
List<T> aa = new ArrayList<T>(); // 正确
List<?> aa = new ArrayList<T>(); // 正确
List<?> aa = new ArrayList<?>(); // 报错,ArrayList里面必须是一种确定类型
List<Object> aa = new ArrayList<Object>(); // 正确
List<?> aa = new ArrayList<Object>(); // 正确
注意 <?> 和 <object>是完全不同的概念。List<?> 表示未知类型的列表,而 List<Object> 表示任意类型的列表——不管是哪一种,都算是已知的。
3-1-附、List<Object> 和 List 之间的区别?
List是原始类型。List<Object>使用了泛型。
- 编译时编译器不会对原始类型进行类型安全检查,但是会检查泛型
- 可以把任何带参数的类型传递给原始类型 List,但却不能把 List< String> 传递给接受 List< Object> 的方法,因为泛型的不可变性,会产生编译错误。(泛型的不可变性参看 6-4 )
3-2、List<?>(以及其他容器类)的坑
使用List<?>这个写法时,通配符会捕获具体的String类型,但编译器不叫它String,而是起个临时的代号,比如”CAP#1“。
以后再也不能往list里存任何元素,包括String。唯一能存的就是null。
在Java集合框架中,对于参数值是未知类型的容器类,只能读取其中元素,不能向其中添加元素。
因为,其类型是未知,所以编译器无法识别添加元素的类型和容器的类型是否兼容,而null不涉及类型问题,所以是唯一的例外
List<?> list = new ArrayList<String>();
list.add("hello"); //ERROR
list.add(111); //ERROR
//argument mismatch; String cannot be converted to CAP#1
//argument mismatch; int cannot be converted to CAP#1
另外如果拿List<?>做参数的话
class Box<T>{
private List<T> item;
public List<T> get(){return item;}
public void set(List<T> t){item=t;}
// 只是把item先用get()方法读出来,然后再用set()方法存回去
public void getSet(Box<?> box){box.set(box.get());}
}
会报错:
error: incompatible types: Object cannot be converted to CAP#1
原因同样是通配符box<?>.set()的参数类型被编译器捕获,命名为CAP#1,和box<?>.get()返回的Object对象无法匹配。
解决方法,是给getSet()方法写一个辅助函数:
class Box<T>{
private List<T> item;
public List<T> get(){return item;}
public void set(List<T> t){item=t;}
//helper()函数辅助getSet()方法存取元素
public void getSet(Box<?> box){helper(box);}
public <V> void helper(Box<V> box){box.set(box.get());}
}
3-3、无界通配符的类型限定
在一些情况下,<?>需要配合extends或者super来使用。
在《Effective Java》中有如下的总结:
泛型-无界通配符-类型限定更通俗地总结一下,就是:
- <? super E> 用于灵活写入,主要目的是统一使用父类的容器,使得对象可以写入父类型的容器。或者用于比较,使得父类型的比较方法可以应用于子类对象。
- <? extends E> 用于灵活读取,使得方法可以读取 E 或 E 的任意子类型的容器对象。
- 如果既是生产又是消费,那使用通配符就没什么意义了,因为需要的是精确的参数类型
这样的话,回过头来看 3-2 一节中的代码,使用了<? extends Fruit>,其目的就是要将Apple放进父类Fruit的容器中。
四、泛型的使用限制以及部分变通方法
4-1、不能实例化类型变量
如T obj = new T (); // 报错, 提示: Type parameter 'T' cannot be instantiated directly
解决方法:使用反射创建泛型实例
public class GenericObj<T> {
private T obj;
public GenericObj(Class<T> c){
try {
obj = c.newInstance(); // 利用反射创建实例
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main (String args[]) {
GenericObj<String> go = new GenericObj<> (String.class);
}
}
Class类本身就是泛型, 而String.class是Class<T>的实例
4-2、不能实例化泛型数组
如T [] arr = new T[3];// 报错, 提示: Type parameter 'T' cannot be instantiated directly
不过只是单纯声明的话是允许的,例如 T [] arr
解决方法一:创建Object类型的数组,然后获取时转型为T类型:
public class GenericArray<T> {
private Object [] arr;
public GenericArray(int n) {
this.arr = new Object[n];
}
public void set (int i, T o) {
this.arr[i] = o;
}
public T get (int i) {
return (T)this.arr[i];
}
}
解决方法二: 使用反射机制中的Array.newInstance方法创建泛型数组
public class GenericArray<T> {
private T [] arr;
public GenericArray(Class<T> type, int n) {
arr = (T[])Array.newInstance(type, n); // 利用反射创建泛型类型的数组
}
public void set (int i, T o) {
this.arr[i] = o;
}
public T get (int i) {
return (T)this.arr[i];
}
}
public class Test {
public static void main (String args[]) {
GenericArray<String> genericArr = new GenericArray<>(String.class, 5);
genericArr.set(0, "abcdefg");
System.out.println(genericArr.get(0));
}
}
解决方法三:可以声明通配类型的数组, 然后做强制转换
Foo<Node> [] f =(Foo<Node> [])new Foo<?> [3];
4-3、不能在泛型类的静态上下文中使用类型变量
public class Foo<T> {
private static T t;
public static T get () { // 报错: 'Foo.this' can not be referenced from a static context
return T;
}
}
原因在于静态变量,不需要创建对象即可调用;而对于泛型类,对象不创建的话无法确定泛型是哪种类型。二者的要求矛盾所以编译禁止通过。
在非泛型类的静态泛型方法中是可以使用类型变量的。
4-4、不能使用基本类型的值作为类型变量的值
Foo<int> node = new Foo<int> (); // 报错
必须使用封装类型
4-5、泛型类不能继承exception
// ERROR:Generic class may not extend java.lang.throwable
public class GenericException<T> extends Exception {
}
扩展Throwable也是不合法的
public class Foo {
public static <T extends Throwable> void doWork () {
try {
// ERROR: Cannot catch type parameters
}catch (T t) {
}
}
}
但是以下写法可以通过
public class Foo {
public static <T extends Throwable> void doWork (T t) throws T {
try {
// ...
}catch (Throwable realCause) {
throw t;
}
}
}
五、泛型与模板模式
5-1、模板模式简要回顾
所谓模板模式,就是把做事情的流程整理好,共通方法提炼出来,由个体实施的方法空出来,谁要实施谁自己填空。
比如,要实现生成、上传合同。先定义好模板(抽象类),就是每一步要干啥,自己能干的自己干,自己干不了的让各个具体需要合同的需求方自己做。
→ 其实这一步就算是在完成骨架类。
public abstract class Abstract合同{
public void createProcess(){
查询数据();
读取合同模板();
替换合同模板中的关键字();
生成合同PDF文件();
签章();
信息入库();
}
protected abstract void 查询数据();
private void 读取合同模板(){...};
protected abstract void 替换合同模板中的关键字();
private void 生成合同PDF文件(){...};
private void 签章(){...};
private void 信息入库(){...};
}
要生成采购合同的时候,只需要完成跟自己业务实际相关的方法即可:
public class 采购合同 extends Abstract合同{
@Override
protected void 查询数据() {
"select A,B,C from T_Contract"
...
}
@Override
protected void 替换合同模板中的关键字() {
strContractContent.replaceAll("#contract_no#","采购合同编号001-01");
...
}
}
5-2、加入泛型
5-1 的例子中,只是简单地用抽象类、抽象方法对于公共行为和不明确的行为进行了隔离。
实际上情况往往要复杂的多。
比如查询出来的数据一定是一个List,要对List做遍历分别生成合同,而每种合同返回的数据类型一定是不一样的。也就是说除了不明确的行为以后,还出现了不明确的对象类型。
这就需要引入泛型来解决问题。比较完整的伪代码如下:
public abstract class Abstract合同<T>{
public void createProcess(){
List<T> contractDatas = 查询数据();
for (T data : contractDatas) {
读取合同模板();
替换合同模板中的关键字(data);
生成合同PDF文件();
签章();
信息入库();
}
}
protected abstract List<T> 查询数据();
private void 读取合同模板(){...};
protected abstract void 替换合同模板中的关键字(T t);
private void 生成合同PDF文件(){...};
private void 签章(){...};
private void 信息入库(){...};
}
5-3、小结
模板模式的特点:
- 抽象类:控制程序总体流程(骨架);实现共通(确定)的方法
- 实现类:继承抽象类,实现具体差异部分的逻辑
模板模式的技术实现:
- 用抽象方法隔离不明确的行为
- 用泛型代表不明确的对象类型
网友评论