标签:
List<T>、List<?>、List<Object>这三者都可以容纳所有的对象,但使用的顺序应该是首选List<T>,次之List<?>,最后选择List<Object>,原因如下:
(1)、List<T>是确定的某一个类型
List<T>表示的是List集合中的元素都为T类型,具体类型在运行期决定;List<?>表示的是任意类型,与List<T>类似,而List<Object>则表示List集合中的所有元素为Object类型,因为Object是所有类的父类,所以List<Object>也可以容纳所有的类类型,从这一字面意义上分析,List<T>更符合习惯:编码者知道它是某一个类型,只是在运行期才确定而已。
(2)List<T>可以进行读写操作
List<T>可以进行诸如add,remove等操作,因为它的类型是固定的T类型,在编码期不需要进行任何的转型操作。
List<T>是只读类型的,不能进行增加、修改操作,因为编译器不知道List中容纳的是什么类型的元素,也就无法校验类型是否安全了,而且List<?>读取出的元素都是Object类型的,需要主动转型,所以它经常用于泛型方法的返回值。注意List<?>虽然无法增加,修改元素,但是却可以删除元素,比如执行remove、clear等方法,那是因为它的删除动作与泛型类型无关。
List<Object> 也可以读写操作,但是它执行写入操作时需要向上转型(Up cast),在读取数据的时候需要向下转型,而此时已经失去了泛型存在的意义了。
打个比方,有一个篮子用来容纳物品,比如西瓜,番茄等.List<?>的意思是说,“嘿,我这里有一个篮子,可以容纳固定类别的东西,比如西瓜,番茄等”。List<?>的意思是说:“嘿,我有一个篮子,我可以容纳任何东西,只要是你想得到的”。而List<Object>就更有意思了,它说" 嘿,我也有一个篮子,我可以容纳所有物质,只要你认为是物质的东西都可以容纳进来 "。
推而广之,Dao<T>应该比Dao<?>、Dao<Object>更先采用,Desc<Person>则比Desc<?>、Desc<Object>更优先采用。
从哲学来说,很难描述一个具体的人,你可以描述他的长相、性格、工作等,但是人都是由多重身份的,估计只有使用多个And(与操作)将所有的描述串联起来才能描述一个完整的人,比如我,上班时我是一个职员,下班了坐公交车我是一个乘客,回家了我是父母的孩子,是儿子的父亲......角色时刻在变换。那如果我们要使用Java程序来对一类人进行管理,该如何做呢?比如在公交车费优惠系统中,对部分人员(如工资低于2500元的上班族并且是站立的乘客)车费打8折,该如何实现呢?
注意这里的类型参数有两个限制条件:一个为上班族;二为乘客。具体到我们的程序中就应该是一个泛型参数具有两个上界(Upper Bound),首先定义两个接口及实现类,代码如下:
1 interface Staff { 2 // 工资 3 public int getSalary(); 4 } 5 6 interface Passenger { 7 // 是否是站立状态 8 public boolean isStanding(); 9 } 10 //定义我这个类型的人 11 class Me implements Staff, Passenger { 12 13 @Override 14 public boolean isStanding() { 15 return true; 16 } 17 18 @Override 19 public int getSalary() { 20 return 2000; 21 } 22 23 }
"Me"这种类型的人物有很多,比如系统分析师也是一个职员,也坐公交车,但他的工资实现就和我不同,再比如Boss级的人物,偶尔也坐公交车,对大老板来说他也只是一个职员,他的实现类也不同,也就是说如果我们使用“T extends Me”是限定不了需求对象的,那该怎么办呢?可以考虑使用多重限定,代码如下:
public class Client99 { //工资低于2500的并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t) { if (t.getSalary() < 2500 && t.isStanding()) { System.out.println(" 恭喜您,您的车票打八折!"); } } public static void main(String[] args) { discount(new Me()); } }
使用“&”符号设定多重边界,指定泛型类型T必须是Staff和Passenger的共有子类型,此时变量t就具有了所有限定的方法和属性,要再进行判断就一如反掌了。在Java的泛型中,可以使用"&"符号关联多个上界并实现多个边界限定,而且只有上界才有此限定,下界没有多重限定的情况。想想你就会明白:多个下界,编码者可自行推断出具体的类型,比如“? super Integer” 和 “? extends Double”,可以更细化为Number类型了,或者Object类型了,无需编译器推断了。
为什么要说明多重边界?是因为编码者太少使用它了,比如一个判断用户权限的方法,使用的是策略模式(Strategy Pattern) ,示意代码如下:
1 class UserHandler<T extends User> { 2 // 判断用户是否有权限执行操作 3 public boolean permit(T user, List<Job> jobs) { 4 List<Class<?>> iList = Arrays.asList(user.getClass().getInterfaces()); 5 // 判断 是否是管理员 6 if (iList.indexOf(Admin.class) > -1) { 7 Admin admin = (Admin) user; 8 // 判断管理员是否有此权限 9 } else { 10 // 判断普通用户是否有此权限 11 } 12 return false; 13 } 14 } 15 16 class User {} 17 18 class Job {} 19 20 class Admin extends User {}
此处进行了一次泛型参数类别判断,这里不仅仅违背了单一职责原则(Single Responsibility Principle),而且让泛型很“汗颜” :已经使用了泛型限定参数的边界了,还要进行泛型类型判断。事实上,使用多重边界可以很方便的解决此问题,而且非常优雅,建议大家 在开发中考虑使用多重限定。
List接口的toArray方法可以把一个集合转化为数组,但是使用不方便,toArray()方法返回的是一个Object数组,所以需要自行转变。toArray(T[] a)虽然返回的是T类型的数组,但是还需要传入一个T类型的数组,这也挺麻烦的,我们期望输入的是一个泛型化的List,这样就能转化为泛型数组了,来看看能不能实现,代码如下:
public static <T> T[] toArray(List<T> list) { T[] t = (T[]) new Object[list.size()]; for (int i = 0, n = list.size(); i < n; i++) { t[i] = list.get(i); } return t; }
上面要输出的参数类型定义为Object数组,然后转型为T类型数组,之后遍历List赋值给数组的每个元素,这与ArrayList的toArray方法很类似(注意只是类似),客户端的调用如下:
public static void main(String[] args) { List<String> list = Arrays.asList("A","B"); for(String str :toArray(list)){ System.out.println(str); } }
编译没有任何问题,运行后出现如下异常:
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
at com.study.advice100.Client100.main(Client100.java:16)
类型转换异常,也就是说不能把一个Object数组转换为String数组,这段异常包含了两个问题:
public static Object[] toArrayTwo(List list) { // 此处的强制类型转换没必要存在,只是为了与源代码对比 Object[] t = (Object[]) new Object[list.size()]; for (int i = 0, n = list.size(); i < n; i++) { t[i] = list.get(i); } return t; } public static void main(String[] args) { List<String> list = Arrays.asList("A", "B"); for (String str : (String [])toArrayTwo(list)) { System.out.println(str); } }
阅读完此段代码后就很清楚了:toArray方法返回后进行一次类型转换,Object数组转换成了String数组,于是就报ClassCastException异常了。
Object数组不能转为String数组,T类型又无法在运行期获得,那该如何解决这个问题呢?其实,要想把一个Object数组转换为String数组,只要Object数组的实际类型也就是String就可以了,例如:
// objArray的实际类型和表面类型都是String数组 Object[] objArray = { "A", "B" }; // 抛出ClassCastException String[] strArray = (String[]) objArray; String[] ss = { "A", "B" }; //objs的真实类型是String数组,显示类型为Object数组 Object objs[] =ss; //顺利转换为String数组 String strs[]=(String[])objs;
明白了这个问题,我们就把泛型数组声明为泛型的子类型吧!代码如下:
public static <T> T[] toArray(List<T> list,Class<T> tClass) { //声明并初始化一个T类型的数组 T[] t = (T[])Array.newInstance(tClass, list.size()); for (int i = 0, n = list.size(); i < n; i++) { t[i] = list.get(i); } return t; }
通过反射类Array声明了一个T类型的数组,由于我们无法在运行期获得泛型类型的参数,因此就需要调用者主动传入T参数类型。此时,客户端再调用就不会出现任何异常了。
在这里我们看到,当一个泛型类(特别是泛型集合)转变为泛型数组时,泛型数组的真实类型不能是泛型的父类型(比如顶层类Object),只能是泛型类型的子类型(当然包括自身类型),否则就会出现类型转换异常。
Java语言是先把Java源文件编译成后缀为class的字节码文件,然后再通过ClassLoader机制把这些类文件加载到内存中,最后生成实例执行的,这是Java处理的基本机制,但是加载到内存中的数据的如何描述一个类的呢?比如在Dog.class文件中定义一个Dog类,那它在内存中是如何展现的呢?
Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象,比如Dog.class文件加载到内存中后就会有一个class的实例对象描述之。因为是Class类是“类中类”,也就有预示着它有很多特殊的地方:
// 类的属性class所引用的对象与实例对象的getClass返回值相同 boolean b1=String.class.equals(new String().getClass()); boolean b2="ABC".getClass().equals(String.class); // class实例对象不区分泛型 boolean b3=ArrayList.class.equals(new ArrayList<String>().getClass());
Class类是Java的反射入口,只有在获得了一个类的描述对象后才能动态的加载、调用,一般获得一个Class对象有三种途径:
获得了Class对象后,就可以通过getAnnotations()获得注解,通过getMethods()获得方法,通过getConstructors()获得构造函数等,这位后续的反射代码铺平了道路。
编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议98~101)
标签:
原文地址:http://www.cnblogs.com/selene/p/5923005.html