编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议98~101)

建议98:建议的采用顺序是List中泛型顺序依次为T、?、Object

  List<T>、List<?>、List<Object>这三者均可以容纳全部的对象,但使用的顺序应该是首选List<T>,次之List<?>,最后选择List<Object>,缘由以下:java

(1)、List<T>是肯定的某一个类型数组

  List<T>表示的是List集合中的元素都为T类型,具体类型在运行期决定;List<?>表示的是任意类型,与List<T>相似,而List<Object>则表示List集合中的全部元素为Object类型,由于Object是全部类的父类,因此List<Object>也能够容纳全部的类类型,从这一字面意义上分析,List<T>更符合习惯:编码者知道它是某一个类型,只是在运行期才肯定而已。安全

(2)List<T>能够进行读写操做ide

  List<T>能够进行诸如add,remove等操做,由于它的类型是固定的T类型,在编码期不须要进行任何的转型操做。函数

  List<T>是只读类型的,不能进行增长、修改操做,由于编译器不知道List中容纳的是什么类型的元素,也就没法校验类型是否安全了,并且List<?>读取出的元素都是Object类型的,须要主动转型,因此它常常用于泛型方法的返回值。注意List<?>虽然没法增长,修改元素,可是却能够删除元素,好比执行remove、clear等方法,那是由于它的删除动做与泛型类型无关。编码

  List<Object> 也能够读写操做,可是它执行写入操做时须要向上转型(Up cast),在读取数据的时候须要向下转型,而此时已经失去了泛型存在的意义了。spa

  打个比方,有一个篮子用来容纳物品,好比西瓜,番茄等.List<?>的意思是说,“嘿,我这里有一个篮子,能够容纳固定类别的东西,好比西瓜,番茄等”。List<?>的意思是说:“嘿,我有一个篮子,我能够容纳任何东西,只要是你想获得的”。而List<Object>就更有意思了,它说" 嘿,我也有一个篮子,我能够容纳全部物质,只要你认为是物质的东西均可以容纳进来 "。code

  推而广之,Dao<T>应该比Dao<?>、Dao<Object>更先采用,Desc<Person>则比Desc<?>、Desc<Object>更优先采用。对象

建议99:严格限定泛型类型采用多重界限

  从哲学来讲,很难描述一个具体的人,你能够描述他的长相、性格、工做等,可是人都是由多重身份的,估计只有使用多个And(与操做)将全部的描述串联起来才能描述一个完整的人,好比我,上班时我是一个职员,下班了坐公交车我是一个乘客,回家了我是父母的孩子,是儿子的父亲......角色时刻在变换。那若是咱们要使用Java程序来对一类人进行管理,该如何作呢?好比在公交车费优惠系统中,对部分人员(如工资低于2500元的上班族而且是站立的乘客)车费打8折,该如何实现呢?blog

  注意这里的类型参数有两个限制条件:一个为上班族;二为乘客。具体到咱们的程序中就应该是一个泛型参数具备两个上界(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),并且让泛型很“汗颜” :已经使用了泛型限定参数的边界了,还要进行泛型类型判断。事实上,使用多重边界能够很方便的解决此问题,并且很是优雅,建议你们 在开发中考虑使用多重限定。

建议100:数组的真实类型必须是泛型类型的子类型

  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数组,这段异常包含了两个问题:

  • 为何Object数组不能向下转型为String数组:数组是一个容器,只有确保容器内的全部元素类型与指望的类型有父子关系时才能转换,Object数组只能保证数组内的元素时Object类型,却不能确保它们都是String的父类型或子类,因此类型转换失败。
  • 为何是main方法抛出异常,而不是toArray方法:其实,是在toArray方法中进行的类型向下转换,而不是main方法中。那为何异常会在main方法中抛出,应该在toArray方法的“ T[] t = (T[]) new Object[list.size()];”这段代码才对呀?那是由于泛型是类型擦除的,toArray方法通过编译后与以下代码相同:  
    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),只能是泛型类型的子类型(固然包括自身类型),不然就会出现类型转换异常。

建议101:注意Class类的特殊性

  Java语言是先把Java源文件编译成后缀为class的字节码文件,而后再经过ClassLoader机制把这些类文件加载到内存中,最后生成实例执行的,这是Java处理的基本机制,可是加载到内存中的数据的如何描述一个类的呢?好比在Dog.class文件中定义一个Dog类,那它在内存中是如何展示的呢?

  Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象,好比Dog.class文件加载到内存中后就会有一个class的实例对象描述之。由于是Class类是“类中类”,也就有预示着它有不少特殊的地方:

  1. 无构造函数:Java中的类通常都有构造函数,用于建立实例对象,可是Class类却没有构造函数,不能实例化,Class对象是在加载类时由Java虚拟机经过调用类加载器中的difineClass方法自动构造的。
  2. 能够描述基本类型:虽然8个基本类型在JVM中并非一个对象,它们通常存在于栈内存中,可是Class类仍然能够描述它们,例如可使用int.class表示int类型的类对象。
  3. 其对象都是单例模式:一个Class的实例对象描述一个类,而且只描述一个类,反过来也成立。一个类只有一个Class实例对象,以下代码返回的结果都为true: 
        // 类的属性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对象有三种途径:

  1. 类属性方式:如String.class
  2. 对象的getClass方法,如new String().getClass()
  3. forName方法加载:如Class.forName(" java.lang.String")

   得到了Class对象后,就能够经过getAnnotations()得到注解,经过getMethods()得到方法,经过getConstructors()得到构造函数等,这位后续的反射代码铺平了道路。

相关文章
相关标签/搜索