Java泛型学习
此篇博客用sout代替System.out.pringln();
概述
Java集合有个缺点——当我们把一个对象“丢进”集合里后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。
Java集合之所以被设计成这样,是因为设计集合的程序员不会知道我们用它来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:
集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。
编译时不检查类型可能引发的异常:
main(){
List strList=new ArrayList();
strList.add("aaa");strList.add("aaa");strList.add("aaa");
strList.add(5);//1
for(int i=0;i<strList.size();i++){
String str =(String)strList.get(i);//2
}
}
上面程序创建了一个List集合,而且只希望该List集合保存字符串对象——但我们没有办法进行任何限制,如果程序在①处“不小心”把一个Integer对象“丢进”了List集合中,这将导致程序在②处引发ClassCastException异常,因为程序试图把一个Integer对象转换为String类型。
手动实现编译时检查类型:
class StrList{
private List strList=new ArrayList();
public boolean add(String ele){
return strList.add(ele);
}
public String get(int index){
return (String)strList.get(index);
}
public int size(){
return strList.size();
}
}
public static void main(String[] args) {
StrList strList=new StrList();
strList.add("aaa");
strList.add("bbb");
strList.add("ccc");
strList.add(5);
System.out.println(strList);
for (int i = 0; i <strList.size() ; i++) {
String str=strList.get(i);
}
}
这种做法虽然有效,但局限性非常明显——程序员需要定义大量的List子类,这是一件让人沮丧的事情。从Java 5以后,Java引入了“参数化类型(parameterized type)”的概念,允许我们在创建集合时指定集合元素的类型,正如List< String>,这表明该List只能保存字符串类型的对象。Java的参数化类型被称为泛型(Generic)。
使用泛型
List<String> stringList =new ArrayList<String>();//1
上面程序成功创建了一个特殊的List集合:strList,这个List集合只能保存字符串对象,不能保存其他类型的对象。创建这种特殊集合的方法是:在集合接口、类后增加尖括号,尖括号里放一个数据类型,即表明这个集合接口、集合类只能保存特定类型的对象。注意①处的类型声明,它指定strList不是一个任意的List,而是一个String类型的List,写作:List
泛型的菱形语法
在Java 7以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这显得有些多余了。例如如下两条语句:
List<String> strList=new ArrayList<String>();
Map<String,Integer> scores=new HashMap<String,Integer>();
上面两条语句中后面的方括号里的字完全是多余的,在Java 7以前这是必需的,不能省略。从Java7开始,Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。即上面两条语句可以改写为如下形式:
List
Map<String , Integer> scores=new HashMap<>();
深入泛型
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java 5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参,这就是在前面程序中看到的List
定义泛型接口、类
Java 5改写后List接口、Iterator接口、Map的代码片段:
public interface List<E>{
void add(E,x);
Iterator<E> iterator();//1
}
public interface Iterator<E>{
E next();
boolean hasNest();
}
public interface Map<K,V>{
Set<K> keySet();//2
V put(K key,V value);
}
允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。除此之外,我们发现①②处方法声明返回值类型是Iterator< E>、Set< K>,这表明Set
通过上面介绍可以发现,我们可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)。下面自定义一个Apple类,这个Apple类就可以包含一个泛型声明。
class Apple<T>{
private T info;
public Apple(){}
public Apple(T info){
this.info=info;
}
public void setInfo(T info){
this.info=info;
}
public T getInfo(){
return this.info;
}
}
public static void main(String[] args) {
Apple<String> apple=new Apple<>("苹果");
System.out.println(apple.getInfo());
Apple<Double> apple1=new Apple<>(5.67);
System.out.println(apple.getInfo());
}
上面程序定义了一个带泛型声明的Apple< T>类(不要理会这个类型形参是否具有实际意义),使用Apple
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如,为Apple< T>类定义构造器,其构造器名依然是Apple,而不是Apple< T>!调用该构造器时却可以使用Apple< T>的形式,当然应该为T形参传入实际的类型参数。Java 7提供了菱形语法,允许省略<>中的类型实参。
从泛型类派生子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,但需要指出的是,当使用这些接口、父类时不能再包含类型形参。例如,下面代码就是错误的。
public class A extends Apple<T>{}
如果想从Apple类派生一个子类,则可以改为如下代码:
public class A extends Apple<String>
并不存在泛型类
可以把ArrayList< String>类当成ArrayList的子类,事实上,ArrayList< String>类也确实像一种特殊的ArrayList类,这个ArrayList< String>对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList< String>生成新的class文件,而且也不会把ArrayList< String>当成新类来处理。
List<String> l1 =new ArrayList<>();
List<Integer l2=nwq ArrayList<>();
sout(l1.getClass()==l2.getClass());
结果为true。因为不管泛型的实际类型参数是什么,他在运行时总有同样的类。
不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。例如:
public class R<T>{
//下列代码错误,不能再静态Field(成员变量)生命中使用类型形参。
static T info;
T age;
public void foo(T msg){}
//下面代码错误,不能再静态方法声明中使用类型形参
public static void bar(T msg){}
}
由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。(instanceof主要用来判断一个类是否实现了某个接口,或者判断一个实例对象是否属于一个类。)
类型通配符
当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。例如下列代码
public void test(List c){//泛型警告
for(int i=0;i<c.size();i++){
System.out.println(c.get(i));
}
}
此处使用List接口时没有传入实际类型参数,这将引起泛型警告。修改后:
public void test(List<Object> c){
for(int i=0;i<c.size();i++){
System.out.println(c.get(i));
}
}
List<String> str=new ArrayList<>();
test(str);//编译错误,List<String>对象不能被当成List<Object>对象使用,也就是说, List<String>类并不是List<Object>类的子类。
如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G< Foo>并不是G< Bar>的子类型!它与我们的习惯看法不同。
为了表示各种泛型List的父类,我们需要使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是未知类型元素的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。所以改写后的代码如下:
public void test(List<?> c){
for(int i=0;i<c.size();i++){
System.out.println(c.get(i));
}
}
List< ?>,这种写法可以适应于任何支持泛型声明的接口和类,比如写成Set< ?>、Collection< ?>、Map< ? , ?>等。
但这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中。
List<?> c=new ArrayList<String>():
c.add(new Object());//编译错误
因为我们不知道上面程序中c集合里元素的类型,所以不能向其中添加对象。根据前面的List
通配符上限
实际上,我们需要一种泛型表示方法,它可以表示所有Shape泛型List的父类。为了满足这种需求, Java泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下:
List<? extends Shape>
List<? extends Shape>是受限制通配符的例子,此处的问号(?)代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape的子类型(也可以是Shape本身),因此我们把Shape称为这个通配符的上限(upper bound)。
形参上限
public class Apple<T extends Number>{
T col;
}
上面程序定义了一个Apple泛型类,该Apple类的类型形参的上限是Number类,这表明使用Apple类时为T形参传入的实际类型参数只能是Number或Number类的子类。
泛型方法
格式:
修饰符 <T,S> 返回值类型 方法名 (形参列表){
//方法体。。。
}
示例:
public class GenericMethodTest{
static <T> void fromArrayToCollection(T[] a,Collection<T> c){
for(T t:a){
c.add(t);
}
}
main(){
Object [] object=new Object[100];
Collection<Object> collection =new ArrayList<>();
fromArrayToCollection(object,collection);
}
}
上面程序定义了一个泛型方法,该泛型方法中定义了一个T类型形参,这个T类型形参就可以在该方法内当成普通类型使用。与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。