泛型不知道咋写,可以参考jdk List 和 Map的写法 (或ArrayList, HashMap)
总结
- 泛型相比 Object 使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力
- 泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过
- 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换
通配符的出现是为了指定泛型中的类型范围。
- 当 <?>存在时,Collection 对象丧失了 add() 方法的功能
- <?>代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以,这时用<? extends T>
- 通配符与类型参数的区别 一般而言,通配符能干的事情都可以用类型参数替换。
比如 public void testWildCards(Collection<?> collection){}
可以被
public
取代。
先看个例子
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;
}
}
————————————————
public class Cache<T> {
T value;
public Object getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
泛型除了可以将类型参数化外,而参数一旦确定好,如果类似不匹配,编译器就不通过。上面代码显示,无法将一个 String 对象设置到 cache2 中,因为泛型让它只接受 Integer 的类型。
所以,综合上面信息,我们可以得到下面的结论。
- 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
- 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
- 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache
这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。
通配符
除了用
可能有同学会想,已经有了
class Base{}
class Sub extends Base{}
Sub sub = new Sub();
Base base = sub;
上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么
List lsub = new ArrayList<>();
List
最后一行代码成立吗?编译会通过吗?
答案是否定的。
编译器不会让它通过的。Sub 是 Base 的子类,不代表 List和 List
但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
所以,通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式。
- <?>被称作无限定的通配符。
- <? extends T>被称作有上限的通配符。
- <? super T>被称作有下限的通配符。
<?>
我们可以看到,当 <?>存在时,Collection 对象丧失了 add() 方法的功能,编译器不通过。
我们再看代码。
List<?> wildlist = new ArrayList<String>();
wildlist.add(123);// 编译不通过
有人说,<?>提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空?我想这种需求还是很常见的吧。
有同学可能会想,<?>既然作用这么渺小,那么为什么还要引用它呢?
个人认为,提高了代码的可读性,程序员看到这段代码时,就能够迅速对此建立极简洁的印象,能够快速推断源码作者的意图。
<? extends T>
<?>代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以。
<? extends T> 代表类型 T 及 T 的子类。
public void testSub(Collection<? extends Base> para){ }
上面代码中,para 这个 Collection 接受 Base 及 Base 的子类的类型。 但是,它仍然丧失了写操作的能力。也就是说 (为什么?)
para.add(new Sub()); para.add(new Base());
仍然编译不通过。
没有关系,我们不知道具体类型,但是我们至少清楚了类型的范围。
<? super T>
<? super T> 这个和<? extends T>相对应,代表 T 及 T 的超类。 java public void testSuper(Collection<? super Sub> para){ }
<? super T>
神奇的地方在于,它拥有一定程度的写操作的能力。
public void testSuper(Collection<? super Sub> para){
para.add(new Sub());//编译通过
para.add(new Base());//编译不通过
}
通配符与类型参数的区别 一般而言,通配符能干的事情都可以用类型参数替换。
比如 public void testWildCards(Collection<?> collection){}
可以被
public
取代。
值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。
public
collection.add((T)new Integer(12));
collection.add((T)”123”);
}
需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。
public class Test2
T value1;
E value2;
}
public
}
E 类型是 T 类型的子类,显然这种情况类型参数更适合。
有一种情况是,通配符和类型参数一起使用。
public
}
如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。
public T test1(T t){
return value1;
}
类型擦除
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码
List
List
System.out.println(l1.getClass() == l2.getClass());
打印的结果为 true 是因为 List
泛型信息被擦除了。
可能同学会问,那么类型 String 和 Integer 怎么办?
答案是泛型转译。
public class Erasure
T object;
public Erasure(T object) {<br /> this.object = object;<br /> }<br /> <br />}
Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。
Erasure
Class eclz = erasure.getClass();
System.out.println(“erasure class is:”+eclz.getName());
打印的结果是
erasure class is:com.frank.test.Erasure
Class 的类型仍然是 Erasure 并不是 Erasure
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
// public class Erasure
T object;
public Erasure(T object) {<br /> this.object = object;<br /> }<br /> <br />}
现在再看测试结果:
Field name object type:java.lang.String
1
我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如
所以,在反射中。
public class Erasure
T object;
public Erasure(T object) {<br /> this.object = object;<br /> }<br /> <br /> public void add(T object){<br /> <br /> }<br /> <br />}
add() 这个方法对应的 Method 的签名应该是 Object.class。
Erasure
Class eclz = erasure.getClass();
System.out.println(“erasure class is:”+eclz.getName());
Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
System.out.println(“ method:”+m.toString());
}
打印结果是
method:public void com.frank.test.Erasure.add(java.lang.Object)
1
也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod(“add”,Object.class)否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。
类型擦除带来的局限性
类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。
理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如
正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。
public interface List
boolean add(E e);
}
上面是 List 和其中的 add() 方法的源码定义。
因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于
boolean add(Object obj);
1
那么,利用反射,我们绕过编译器去调用 add 方法。
public class ToolTest {
public static void main(String[] args) {<br /> List<Integer> ls = new ArrayList<>();<br /> ls.add(23);<br />// ls.add("text");<br /> try {<br /> Method method = ls.getClass().getDeclaredMethod("add",Object.class);<br /> <br /> <br /> method.invoke(ls,"test");<br /> method.invoke(ls,42.9f);<br /> } catch (NoSuchMethodException e) {<br /> // TODO Auto-generated catch block<br /> e.printStackTrace();<br /> } catch (SecurityException e) {<br /> // TODO Auto-generated catch block<br /> e.printStackTrace();<br /> } catch (IllegalAccessException e) {<br /> // TODO Auto-generated catch block<br /> e.printStackTrace();<br /> } catch (IllegalArgumentException e) {<br /> // TODO Auto-generated catch block<br /> e.printStackTrace();<br /> } catch (InvocationTargetException e) {<br /> // TODO Auto-generated catch block<br /> e.printStackTrace();<br /> }<br /> <br /> for ( Object o: ls){<br /> System.out.println(o);<br /> }<br /> <br /> }
}
打印结果是:
23
test
42.9
可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。
泛型中值得注意的地方
泛型类或者泛型方法中,不接受 8 种基本数据类型。
所以,你没有办法进行这样的编码。
List
List
需要使用它们对应的包装类。
List
List
对泛型方法的困惑
public
return null;
}
有的同学可能对于连续的两个 T 感到困惑,其实
你可以相像一下,如果 test() 这样被调用
test(“123”);
那么实际上相当于
public String test(String t);
Java 不能创建具体类型的泛型数组
这句话可能难以理解,代码说明。
List
List
这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。
List