编写高质量代码: 改善Java程序的151个建议 | 第七章 泛型与反射

本文介绍了Java编程中关于泛型和反射的一系列建议,如类型擦除、泛型通配符的使用、反射的注意事项以及动态代理和装饰模式的应用。强调了泛型的安全性和反射的灵活性,同时也提醒了泛型不支持协变和逆变以及反射可能的效率问题。
摘要由CSDN通过智能技术生成

编写高质量代码: 改善Java程序的151个建议

第七章 泛型与反射

在这里插入图片描述

前言

前言 圈主 [Rocky编程日记] 学习 编写高质量代码: 改善Java程序的151个建议 笔记记录。希望我写得笔记你能够喜欢, 希望我写的笔记能够给你提供帮助。同时若笔记中存在不对的地方,那一定是圈主当时的理解还不够, 希望你能够及时指出嗷~

建议93:Java的泛型是类型擦除的
  • 加入泛型优点:加强了参数类型的安全性,减少了类型的转换。Java的泛型在编译期有效,在运行期被删除,也就是说所有的泛型参数类型在编译后都会被清除掉。

    1. 泛型的class对象时是相同的;
    2. 泛型数组初始化时不能声明泛型类型;
    3. instanceof不允许存在泛型参数
    public class Suggest93 {
    
        public void arrayMethod(String[] strArray) {}
        public void arrayMethod(Integer[] strArray) {}
    
        // 'listMethod(List<String>)' clashes with
        // 'listMethod(List<Integer>)';
        // both methods have same erasure
        // public void listMethod(List<String> strArray) {}
        public void listMethod(List<Integer> strArray) {}
    }
    
建议94:不能初始化泛型参数和数组
  • 泛型类型在编译期被擦除,在类初始化时将无法获得泛型的具体参数,所以泛型参数和数组无法初始化,但是ArrayList却可以,因为ArrayList初始化是向上转型变成了Object类型;需要泛型数组解决办法:只声明,不再初始化,由构造函数完成初始化操作
建议95:强制声明泛型的实际类型
  • 无法从代码中推断出泛型类型的情况下,即可强制声明泛型类型;方法:List list2 = ArrayUtils.****asList();在输入前定义这是一个Integer类型的参数
建议96:不同的场景使用不同的泛型通配符
  • Java泛型支持通配符(Wildcard),可以单独使用一个“?”表示任意类,也可以使用extends关键字表示某一个类(接口)的子类型,还可以使用super关键字表示某一个类(接口)的父类型。
    1. 泛型结构只参与“读”操作则限定上界(extends关键字);
    2. 泛型结构只参与“写”操作则限定下界(使用super关键字);
    3. 如果一个泛型结构既用作“读”操作也用作“写”操作则使用确定的泛型类型即可,如List
建议97:警惕泛型是不能协变和逆变的
  • Java的泛型是不支持协变和逆变的,只是能够实现协变和逆变)(协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性。

  • 简单地说,协变是用一个窄类型替换宽类型,而逆变则是用宽类型覆盖窄类型

  • 子类覆写父类返回值类型比父类型变窄,则是协变;子类覆写父类型的参数类型变宽,则是逆变。数组支持协变,泛型不支持协变

    public class Suggest97 {
        public static void main(String[] args) {
            // 数组支持协变
            Number[] n = new Integer[10];
            // 编译不通过, 泛型不支持协变
            // List<Number> ln = new ArrayList<Integer>();
            
            List<? super Integer> li = new ArrayList<Number>();
        }
    }
    
建议98:建议采用的顺序是List,List,List
  • List是确定的某一个类型,编码者知道它是一个类型,只是在运行期才确定而已;
  • List可以进行读写操作,List<?>是只读类型,因为编译器不知道List中容纳的是什么类型的元素,无法增加、修改,但是能删除,List也可以读写操作,只是此时已经失去了泛型存在的意义了
建议99:严格限定泛型类型采用多重界限
  • 使用“&”符号连接多个泛型界限,如:<T extends Staff & Passenger>

    public interface Staff {
    
        int getSalary();
    }
    
    public interface Passenger {
    
        public boolean isStanding();
    }
    
    public class Me implements Staff, Passenger {
        @Override
        public boolean isStanding() {
            return true;
        }
    
        @Override
        public int getSalary() {
            return 2000;
        }
    
        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());
        }
    }
    
    
建议100:数组的真实类型必须是泛型类型的子类型
  • 有可能会抛出ClassCastException异常,toArray方法返回后会进行一次类型转换,Object数组转换成了String数组。由于我们无法在运行期获得泛型类型的参数,因此就需要调用者主动传入T参数类型

    public class Suggest100 {
    
        public static <T> T[] toArray(List<T> list) {
            T[] t = (T[]) new Object[list.size()];
            for (int i = 0; i < list.size(); i++) {
                t[i] = list.get(i);
            }
            return t;
        }
    
        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 code.rocky.chapter07.Suggest100.main(Suggest100.java:18)
    
    
  • 为什么是 main 方法 抛出异常, 而不是toArray 方法?

    其实,是在toArray方法中进行的类型向下转换,而不是main方法中。那为什么异常会在main方法中抛出,应该在toArray方法的“T[t =(T[])new Object[list.size()]”这段代码才对呀?那是因为泛型是类型擦除的,toArray方法经过编译后与如下代码相同:

    public static Object toArray(List list)
    
建议101:注意Class类的特殊性
  • Java处理的基本机制:先把Java源文件编译成后缀为class的字节码文件,然后再通过ClassLoader机制把这些类文件加载到内存中,最后生成实例执行。

  • Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象。

  • Class类是“类中类”,具有特殊性:

    1. 无构造函数,不能实例化,Class对象是在加载类时由Java虚拟机通过调用类加载器中的defineClass方法自动构建的;

    2. 可以描述基本类型,8个基本类型在JVM中并不是一个对象,一般存在于栈内存中,但是Class类仍然可以描述它们,例如可以使用int.class表示int类型的类对象;

    3. 其对象都是单例模式,一个Class的实例对象描述一个类,并且只描述一个类,反过来也成立,一个类只有一个Class实例对象。Class类是Java的反射入口,只有在获得了一个类的描述对象后才能动态地加载、调用,一般获得一个Class对象有三种途径:1、类属性方式,如String.class;2、对象的getClass方法,如new String().getClass();

      3、forName方法重载,如Class.forName(“java.lang.String”)。获得了Class对象后,就可以通过getAnnotation()获得注解,通过个体Methods()获得方法,通过getConstructors()获得构造函数等

建议102:适时选择getDeclaredXXX和getXXX
  • getMethod方法获得的是所有public访问级别的方法,包括从父类继承的方法,而getDeclaredMethod获得的是自身类的所有方法,包括公用方法、私有方法等,而且不受限于访问权限。
  • Java之所以这样处理,是因为反射本意只是正常代码逻辑的一种补充,而不是让正常代码逻辑产生翻天覆地的改动,所以public的属性和方法最容易获取,私有属性和方法也可以获取,但要限定本类。
  • 如果需要列出所有继承自父类的方法,需要先获得父类,然后调用getDeclaredMethods方法,之后持续递归。
建议103:反射访问属性或方法是将Accessible设置为true
  • 通过反射方式执行方法时,必须在invoke之前检查Accessible属性。而Accessible属性并不是我们语法层级理解的访问权限,而是指是否更容易获得,是否进行安全检查。Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度地提升系统性能。经过测试,在大量的反射情况下,设置Accessible为true可以提升性能20倍以上
建议104:使用forName动态加载类文件
  • forName只是加载类,并不执行任何代码)(动态加载(Dynamic Loading)是指在程序运行时加载需要的类库文件,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时再决定是否要加载一个类,然后在JVM中加载并初始化。

  • 动态加载通常是通过Class.forName(String)实现。一个对象的生成必然会经过一下两个步骤

    1. 加载到内存中生成Class的实例对象;
    2. 通过new关键字生成实例对象;
  • **动态加载的意义:**加载一个类即表示要初始化该类的static变量,特别是static代码块,在这里我们可以做大量的工作,比如注册自己,初始化环境等,这才是我们重点关注的逻辑

    public class Test01 {
    
        static {
            System.out.println("Do something");
        }
    }
    
    public class Suggest104 {
        public static void main(String[] args) throws ClassNotFoundException {
            // 动态加载
            Class.forName("code.rocky.chapter07.Test01");
        }
    }
    
建议105:动态加载不适合数组
  • 通过反射操作数组使用Array类,不要采用通用的反射处理API

  • 如果forName要加载一个类,那它首先必须是一个类–8个基本类型排除在外,不是具体的类;

    public class Suggest105 {
        public static void main(String[] args) throws ClassNotFoundException {
            String[] strs = new  String[10];
            Class.forName("java.lang.String[]");
        }
    }
    
    Exception in thread "main" java.lang.ClassNotFoundException: java/lang/String[]
    	at java.lang.Class.forName0(Native Method)
    	at java.lang.Class.forName(Class.java:264)
    	at code.rocky.chapter07.Suggest105.main(Suggest105.java:6)
    
    
    元素类型编译后的类型
    byte[][B
    char[][C
    Double[][D
    Float[][F
    Int[][I
    Long[][J
    Short[][S
    Boolean[Z
    引用类型(如String[])[L 引用类型(如: [java.lang.String)
  • 其次,它必须具有可追索的类路径,否则会报ClassNotFoundException异常。在Java中,数组是一个非常特殊的类,虽然是一个类,但没有定义类路径。作为forName参数时会抛出ClassNotFoundException异常,原因是:数组虽然是一个类,在声明时可以定义为String[],但编译器编译后会为不同的数组类型生成不同的类,所以要想动态创建和访问数组,基本的反射是无法实现的。

     // 动态创建数组
            String[] str1 = (String[]) Array.newInstance(String.class, 8);
            // 创建一个多维数组
            int[][] ints = (int[][]) Array.newInstance(int.class,2,3);
    
建议106:动态代理可以使代理模式更加灵活
  • Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发。静态代理是通过代理主题角色(Proxy)和具体主题角色(Real Subject)共同实现抽象主题角色(Subject)的逻辑的,只是代理主题角色把相关的执行逻辑委托给了具体主题角色而已。动态代理需要实现InvocationHandler接口,必须要实现invoke方法,该方法完成了对真实方法的调用。
建议107:使用反射增加装饰模式的普适性
  • 装饰模式(Decorator Pattern)的定义是“动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比于生成子类更为灵活”。比较通用的装饰模式,只需要定义被装饰的类及装饰类即可,装饰行为由动态代理实现,实现了对装饰类和被装饰类的完全解耦,提供了系统的扩展性。

    public interface Animal {
    
        public void doStuff();
    
    }
    
    public class Rat implements Animal {
        @Override
        public void doStuff() {
            System.out.println("Jerry will play with Tom.");
        }
    }
    
    public interface Feature {
    
        public void load();
    }
    
    public class FlyFeature implements Feature {
        @Override
        public void load() {
            System.out.println("增加一只翅膀...");
        }
    }
    
    public class DigFeature implements Feature {
        @Override
        public void load() {
            System.out.println("增加钻地能力");
        }
    }
    
    public class DecorateAnimal implements Animal {
    
        // 被包装的动物
        private Animal animal;
    
        private Class<? extends Feature> clz;
    
        public DecorateAnimal(Animal _animal, Class<? extends Feature> _clz) {
            animal = _animal;
            clz    = _clz;
        }
    
        @Override
        public void doStuff() {
            InvocationHandler handler = new InvocationHandler() {
    
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Object obj = null;
                    if (Modifier.isPublic(method.getModifiers())) {
                        obj = method.invoke(clz.newInstance(), args);
                    }
                    animal.doStuff();
                    return obj;
                }
            };
            // 当前加载器
            ClassLoader c1 = getClass().getClassLoader();
            // 动态代理, 由 Handler 决定如何包装
            Feature proxy = (Feature) Proxy.newProxyInstance(c1, clz.getInterfaces(), handler);
            proxy.load();
        }
    
        public static void main(String[] args) {
            Animal Jerry = new Rat();
            // 增加一只翅膀
            Jerry = new DecorateAnimal(Jerry, FlyFeature.class);
            // 增加钻地能力
            Jerry = new DecorateAnimal(Jerry, DigFeature.class);
            Jerry.doStuff();
        }
    }
    
建议108:反射让模板方法模式更强大
  • 决定使用模板方法模式时,请尝试使用反射方式实现,它会让你的程序更灵活、更强大)(模板方法模式(Template Method Pattern)的定义是:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。简单说,就是父类定义抽象模板作为骨架,其中包括基本方法(是由子类实现的方法,并且在模板方法被调用)和模板方法(实现对基本方法的调度,完成固定的逻辑),它使用了简单的继承和覆写机制。使用反射后,不需要定义任何抽象方法,只需定义一个基本方法鉴别器即可加载复合规则的基本方法
建议109:不需要太多关注反射效率
  • 反射效率低是个真命题,但因为这一点而不使用它就是个假命题)(反射效率相对于正常的代码执行确实低很多(经测试,相差15倍左右),但是它是一个非常有效的运行期工具类。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值