范型
一次问题引发的学习
范型很大程度帮助集合,JAVA SE 1.5开始引入,之前的集合不记得元素类型。这样的会引发的问题是:
- 一个全是String元素的集合,我可以放入Integer元素,
健壮性
不行 - 取出元素后需要类型转换,代码
臃肿
,并且转换时容易引发异常
List<String>可以理解为ListString,看作List的子类型
List是范型接口,T是类型参数形参
,创建对象时候,必须传入类型参数实参
。
声明类型,创建对象,都需要传入类型参数实参。构造函数还是一样的,并没有<>,但是调用的时候,需要加上<>,JAVA SE 7支持菱形语法了。
- 范型(类型参数)
- 范型声明可以用在
类
接口
方法
上 - 当在类上使用范型声明后,类里面可以把这个类型参数当作普通的类型使用
public class Apple<T>{
T info;
public Apple(T info){
this.info = info;
}
public void setInfo(T info){
this.info = info;
}
public T getInfo(){
return info;
}
}
继承或者实现一个范型声明的类或者接口时,需要指定类型参数的实参。
- 错误写法
public class A extends Apple<T>
- 正确写法
public class A extends Apple<String>
这个时候,覆盖父类的正确写法是
public String getInfo(){
return "子类: " + super.getInfo();
}
/**
* public Object getInfo(){
* return "子类:" + super.getInfo();
* }
*/
如果写成下面的,这个时候,T被当作Object。
public class A extends Apple
覆盖父类的写法应该是
public String getInfo(){
return "子类: " + super.getInfo().toString();
}
无论传入的类型实参是什么,范型类是一种类型,在内存空间中只占一块地方
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
a.getClass() == b.getClass() //true
所以不能用类型参数来声明静态
变量,不能用在静态
方法的参数声明中使用。
T info;
//static T info; 错误写法
public void setInfo(T info){
this.info = info;
}
//public static void setInfo(T info){ 错误写法
//this.info = info;
//}
instanceof后面的错误写法
a instanceof java.util.ArrayList<String>
Foo是Bar的子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型,但是G<Foo>不是G<Bar>的子类型。
以下代码的问题是会引起范型警告
public void test(List c){
for(int i=0; i<c.size(); i++){
System.out.println(c.get(i));
}
}
下面代码的问题是无法传入List<String>类型的对象,因为List<String>不是List<Object>的子类
public void test(List<Object> c){
for(int i=0; i<c.size(); i++){
System.out.println(c.get(i));
}
}
看一下数组,不存在如上问题,其实是早期设计的缺陷
public class ArrayErr {
public static void main(String[] args) {
Integer[] ia = new Integer[5];
Number[] na = ia;
//ia[0] = 0.5; //编译报错
na[0] = 0.5; //编译正常 运行时报错 java.lang.ArrayStoreException 不安全的设计
}
}
范型设计时进行了改进,如下代码导致编译错误。
List<Integer> iList = new ArrayList<Integer>();
List<Number> nList = iList; //编译错误
使用类型通配符List<?>,表示所有List范型类的父类
List<?> c = new ArrayList<>();
c.add(new Object()); //引起编译错误 因为不知道c集合中元素的类型 唯一例外的是null,null是所有引用类型的实例。
可以get元素,拿出来的是Object。
Shape是父类,有抽象方法,Rectangle和Circle是子类。
如果使用不加限制的通配符?,那么取出元素后,都得转换成Shape再调用draw
public class Canvas {
public void drawAll(List<?> shapes) {
for (Object object : shapes) {
Shape shape = (Shape) object;
shape.draw(this);
}
}
使用带限制的类型通配符,那么<? extends Shape>,取出来之后直接调用draw。
- 范型声明也是可以加类型上限的
范型方法声明
修饰符 <参数类型> 返回类型 方法名(参数列表)
static <T> void copyArrayToCollection(T[] a, Collection<T> c);
Collection<T>类型T需要完全吻合,T[]可以传入T的子类。
public class ErrorTest {
static <T> void test(Collection<T> from, Collection<T> to) {
for (T ele : from) {
to.add(ele);
}
}
public static void main(String[] args) {
List<Object> ao = new ArrayList<>();
List<String> as = new ArrayList<>();
//下面代码将产生编译错误
test(as, ao);
}
}
public class RightTest {
static <T> void test(Collection<? extends T> from, Collection<T> to) {
for (T ele : from) {
to.add(ele);
}
}
public static void main(String[] args) {
List<Object> ao = new ArrayList<>();
List<String> as = new ArrayList<>();
//下面代码完全正常
test(as, ao);
}
}
范型方法与通配符的区别
public interface Collection<E>
{
//类型通配符写法
boolean containAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
//范型方法写法
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
}
什么时候使用范型方法而不用通配符?
形参a或返回值的类型依赖于形参b的类型 b的声明不应该使用通配符
java 的Collections.copy()同时使用范型方法和通配符
public class Collections{
public static <T> void copy(List<T> dest, List<? extends T> src){...}
}
范型构造器
class Foo {
public <T> Foo(T t) {
System.out.println(t);
}
}
public class GenericConstructor {
public static void main(String[] args) {
new Foo("疯狂Java讲义");
new Foo(200);
//显式指定
new <String>Foo("疯狂Java讲义");
//显式指定String 传入Double 出错
//new <String> Foo(12.3);
}
}
class MyClass<E> {
public <T> MyClass(T t) {
System.out.println("t参数的值为: " + t);
}
}
public class GenericDiamondTest {
public static void main(String[] args) {
MyClass<String> mc1 = new MyClass<>(5);
MyClass<String> mc2 = new <Integer> MyClass<String>(5);
//不能使用菱形语法
//MyClass<String> mc3 = new <Integer> MyClass<>(5);
}
}
从src集合往dest集合copy元素,src中的元素类型是dest元素类型的子类,这个时候如果要求返回最后加入的元素,使用通配符上限的方法,会造成类型丢失。
使用通配符下限的方式改写copy方法
public class MyUtils {
public static <T> T copy(Collection<? super T> dest, Collection<T> src) {
T last = null;
for (T ele : src) {
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String[] args) {
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
Integer last = copy(ln, li); //准确知道最后一个被复制的元素是Integer类型
}
}
java集合框架中使用通配符下限的例子
TreeSet(Comparator<? super E> c)
public interface Comparator<T>{
int compare(T fst, T snd);
}
方法重载
public static <T> void copy(Collection<T> dest, Collection<? extends T> src){...}
public static <T> T copy(Collection<? super T> dest, Collection<T> src) {...}
List<Number> ln = new ArrayList<>();
List<Integer> ln = new ArrayList<>();
copy(ln, li); //编译错误 分不清调用哪个方法
java8增强的范型推断能力
class MyUtil<E> {
public static <Z> MyUtil<Z> nil() {
return null;
}
public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail) {
return null;
}
E head() {
return null;
}
}
public class InferenceTest {
public static void main(String[] args) {
MyUtil<String> ls = MyUtil.nil(); //通过赋值的目标参数推断类型参数为String
MyUtil<String> mu = MyUtil.<String>nil(); //显式指定
MyUtil.cons(42, MyUtil.nil()); //根据cons方法所需的参数类型来推断
MyUtil.cons(42, MyUtil.<Integer>nil());//显式指定
//类型推断不是万能的 编译错误
//String s = MyUtil.nil().head();//为什么错?答:万一String是Z的父类呢
String s = MyUtil.<String>nil().head();
}
}
- 类型擦除和转换
class Apple<T extends Number>{
T size;
public Apple(){}
public Apple(T size){
this.size = size;
}
public T getSize() {
return size;
}
public void setSize(T size) {
this.size = size;
}
}
public class ErasureTest {
public static void main(String[] args) {
Apple<Integer> a = new Apple<>(6);
Integer as = a.getSize();
//尖括号的类型信息丢失
Apple b = a;
Number size1 = b.getSize();
//编译出错
//Integer size2 = b.getSize();
}
}
//引发ClassCastException异常
public class ErasureTest2 {
public static void main(String[] args) {
Object o = 6;
//System.out.println((String)o);
List list1 = new ArrayList();
list1.add(6);
list1.add(9);
//System.out.println((String)list.get(0));
/
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
List list = li;
//下面代码引起"未经检查的转换"
List<String> ls = list;
//只要访问ls里面的元素 将引发ClassCastException
//System.out.println(ls.get(0));
}
}
范型设计的重要原则
一段代码在编译时没有提出
[unchecked] 未经检查的转换
警告,则运行时不会引发ClassCastException异常。
- 允许声明List<String>[],不能创建ArrayList<String>[]
public class GenericArrayTest {
public static void main(String[] args) {
//下面代码实际上是不允许的
List<String>[] lsa = new List<String>[10];
//将lsa向上转型为Object[]类型的变量
Object[] oa = (Object[]) lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
//将List<Integer>对象作为oa的第二个元素 代码没有任何警告
oa[1] = li;
//下面代码不会有任何警告 但将引发ClassCastException
lsa[1].get(0); //违背java范型设计原则
}
}
//下面代码编译时有警告
List<String>[] lsa = new ArrayList[10];
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(new Integer(3));
oa[1] = li;
String s = lsa[1].get(0); //ClassCastException异常
java允许创建无上限的通配符范型数组,如new ArrayList<?>[10]
//没有unchecked警告
List<?>[] lsa = new ArrayList<?>[10];
List<Integer> li = new ArrayList<>();
li.add(3);
//lsa[1] = li; //List<?>使用通配符不能加向其中添加元素 这块为什么可以? List<Integer>是List<?>子类
Object[] oa = lsa;
oa[1] = li;
//String s = (String)lsa[1].get(0); //ClassCastException
//这个错误类似与 Object a = 6; String s = (String) a;类似 不是范型的原因
//程序应该通过instancof运算符保证数据类型
Object target = lsa[1].get(0);
if (target instanceof String) {
//下面代码安全了
String s = (String) target;
}
创建元素类型是类型变量的数组对象也会导致编译错误
<T> T[] makeArray(Collection<T> coll){
//下面代码将导致编译错误
return new T[coll.size()];
}
类型变量在运行时并不存在,编译器无法确定实际类型!!!
-Xlint:unchecked
打印范型警告详细信息