我相信很多人跟我一样还未学习过范型的概念就开始使用范型的实例,最典型的就是集合框架。为了进一步深入了解范型,这一次通过几个简单的例子来说明范型的注意事项。
一.没有范型的世界
所有的java类都派生自java.lang.Object ,这意味着所有的java对象都可以转换成Object,听起来似乎很美妙,但事实并非如此。举个例子,假设现在需要一伙人去排队,要求只有学生可以参与进来,但是如果对于这个队伍没有条件限定的话,那就意味着我们不想要的一些群体也会进入大军之中,这不利于管理。再如下面一段没有使用范型的代码:
List string=new ArrayList();
string.add("我是第一个元素");
string.add("我是第二个元素");
当要从中获取一个成员时,得到的却是java.lang.Object的一个实例,那么如果我们要使用String类型的对象还要进行强转型,这无疑增加了代码的冗杂度。不过幸好,我们有了范型。
二. 范型类型简介
像方法一样,范型类型也可以接受参数,声明范型类型要使用尖括号将类型变量列表括起来。例如声明一个List对象:
List<E> mylist;
为了将范型类型实例化,要在声明它的时候传递相同的参数列表,例如,为了创建一个使用String的ArrayList,要将String放在一对尖括号中进行传递:
List<String> string=new ArrayList<String>();
不过,java7版本之后,可以在参数化类的构造器中显式的传入参数,上面一段代码可以改为如下的表达方式:
List<String> string=new ArrayList< >();
毫无疑问范型类型也可以指定Object类型,但是范型类型却不可以是java.lang.Throwable的直接或间接子类,因为它在运行时抛出异常,因此无法查看在编译时会抛出什么异常。
下面一段代码就对使用了范型与未使用范型的List进行了比较:
import java.util.ArrayList;
import java.util.List;
public class GenericListTest {
public static void main(String args[]){
//没有使用范型的队列
List stringList=new ArrayList();
stringList.add("元素1");
stringList.add("元素2");
//需要强转型
String s1=(String)stringList.get(0);
System.out.println(s1);
//使用范型的队列
List<String> list=new ArrayList<String>();
list.add("元素3");
list.add("元素4");
//不需要强转型
String s2=list.get(0);
System.out.println(s2);
}
}
提示:使用范型类型时,是在编译时进行类型检查的。
这里值得关注的地方在于,范型类型本身也是一个类型,它还可以用作类型变量。
如下的一段代码演示:
import java.util.ArrayList;
import java.util.List;
public class ListOfList {
public static void main(String args[]){
List<String> list=new ArrayList<>();
list.add("我是list中的对象");
//list中的元素
System.out.println(list.get(0));
//创建封装了list的List对象
List<List<String>> listOflist=new ArrayList<>();
listOflist.add(list);
//获取list当中的元素
String s=listOflist.get(0).get(0);
System.out.println(s);
}
}
范型类型可以接受不止一个类型变量,那就是我们非常熟悉的Map集合,相信大家也已经相当熟悉了吧
三. 使用“?”通配符
前面所提到的,如果声明一个List<aType>,那么List将使用aType的实例,并且可以保存以下任意一种类型:
1. aType的一个实例
2. aType的子类的一个实例,如果aType是类的话
3. 实现aType的类的一个实例,如果aType是接口的话
但是,注意范型类型本身也是一个java类型,如果你不理解的话,下面这段代码将会让你更加了解这句话的含义:
import java.util.ArrayList;
import java.util.List;
public class AllowedTypeTest {
public static void main(String args[]){
List<String> myList=new ArrayList<String>();
doIt(myList);
}
private static void doIt(List<Object> l){
}
}
编译产生了错误,按理说,String是Object的一个子类,但是进行了范型类型封装之后List<String>就不再是List<Object>的一个实例了。
为了解决这个问题,我们就要用到通配符“?”,如下:
import java.util.ArrayList;
import java.util.List;
public class WildCardTest {
private static void doIt(List<?> l){
for(Object element:l){
System.out.print(element);
}
}
public static void main(String args[]){
//字符型List
List<String> stringList=new ArrayList<String>();
stringList.add("Hello");
stringList.add("World");
doIt(stringList);
System.out.println();
//整型List
List<Integer> intList=new ArrayList<Integer>();
intList.add(100);
intList.add(200);
doIt(intList);
}
}
代码中doIt方法里面的List<?>表示的是任意类型的一个List,但要注意的是,在声明或者创建一个范型类型时使用通配符是不合法的,如:
List<?> list=new ArrayList<?>();
编译会出错。
四. 在方法中使用有界通配符
通过上面的描述,你已经知道了可以使用“?”通配符来解决类型匹配的问题,但是我们在实际中碰到的问题并非这么简单。如果我们想定义一个Number实现子类的List,虽然我们可以用通配符来解决List<Integer>与List<Double>类型不匹配的问题,但是无疑的,我们还可以放入很多我们并不需要的实例如List<String>,这样程序的类型安全性根本就没有保障了,那么我们就必须需要一种规则来保证我们所定义的List实例是属于Number的实现子类的,在这里我们就要用到有界通配符。
如下演示:
import java.util.ArrayList;
import java.util.List;
public class BoundedWildcardTest {
public static void main(String args[]){
List<Integer> intList=new ArrayList<>();
intList.add(3);
intList.add(30);
System.out.println(getAverage(intList));
List<Double> doubleList=new ArrayList<>();
doubleList.add(3.0);
doubleList.add(30.0);
System.out.println(getAverage(doubleList));
}
private static double getAverage(List<? extends Number> list){
double total=0.0;
for(Number number:list){
total+=number.doubleValue();
}
return total/list.size();
}
}
五.编写范型类型
编写范型类型与其他类型并无太大差异,只是在类中的某处声明需要用到的类型变量列表。下面就是范型类的一个简单示例:
public class MyGeneric<E> {
E a;
public MyGeneric(E a){
this.a=a;
}
public E get(){
return a;
}
public void set(E b){
this.a=b;
}
}
范型为我们的编码过程做出了巨大的贡献,提供了更加严格的类型检查,并且在取得元素时无需再进行类型转换,真乃编码之一大利器也。