Junit、反射和注解

510 阅读7分钟

一:Junit

1. 测试分类

- 黑盒测试

  • 不考虑程序内部的逻辑结构和处理过程,只需给定输入观察输出是否符合预期

- 白盒测试

  • 关注程序具体的执行流程,并设计测试用例对程序进行调试,检查是否存在bug

2. Junit使用步骤(白盒测试)

  1. 定义一个测试类(测试用例)
    • 测试包名:test
    • 测试类名:被测试的类名+Test,如CalculatorTest
  2. 定义可以独立运行的测试方法(不需要main方法来调用)
    • 返回值:void
    • 方法名:test+被测试的方法名
    • 参数列表:空
    • 为此方法添加注解@Test 例如:public void testSum(){}
  3. 判断测试结果
    1. 使用Assert.assertEquals(期待值, 实际值)语句
    2. 执行测试方法
    3. 观察控制台的执行结果
  • 自定义方法
    1. 初始化方法(示例命名:init)
      • 为此方法添加注解@Before
      • 用于申请资源,所有测试方法执行前都会自动执行此方法
    2. 释放资源方法(示例命名:close)
      • 为此方法添加注解@After
      • 用于释放资源,所有测试方法执行完毕后就会自动执行此方法

二:反射

1. 反射机制

  • 将类的各个组成部分封装为相应的对象
    • 好处
      1. 可以在程序运行过程中操作这些对象
      2. 解耦,提高程序的可扩展性

2. Java代码在计算机中经历的三个阶段

  1. 源代码阶段:在硬盘上,未加载进内存
  2. 类加载器:可以将.class字节码文件加载进内存 Java代码的三个阶段.jpg

3. 获取Class对象的3种方式

  1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象
    • 多用于配置文件,将类名定义在配置文件中,读取配置文件,加载进内存
  2. 类名.class:通过类名的属性class获取
    • 多用于传递参数时
  3. 对象.getClass():Object类的getClass()方法获取
    • 多用于已有对象后
  • 上述3种方式获取的Class对象相同,说明同一个字节码文件在一次程序运行过程中,只会被加载一次

4. Class对象的功能

  1. 获取成员变量
    • 只能获取public修饰的成员变量
      1. Field[] getFields()
      2. Field getField(String name)
    • 获取成员变量,无修饰符限制
      1. Filed[] getDeclaredFields()
      2. Filed getDeclaredField(String name)
  2. 获取构造方法
    • 只能获取public修饰的构造方法
      1. Constructor<?>[] getConstructors()
      2. Constructor<T>[] getConstructor(Class<?>... parameterTypes)
    • 获取构造方法,无修饰符限制
      1. Constructor<?>[] getDeClaredConstructors()
      2. Constructor<T>[] getClaredConstructor(Class<?>... parameterTypes)
  3. 获取成员方法
    • 只能获取public修饰的成员方法
      1. Method[] getMethods()
      2. Method getMethod(String name, Class<?>... parameterTypes)
    • 获取构造方法,无修饰符限制
      1. Method[] getDeclaredMethods()
      2. Method getDeclaredMethod(String name, Class<?>... parameterTypes)
  4. 获取全类名
    • String getName()

5. 所封装的对象的功能

  • 获取到privatet修饰的成员变量(构造方法/成员方法)后,可以使用setAccessible(true)语句(暴力反射)忽略访问权限修饰符的安全检查
  1. Field:成员变量
    1. 设置值:void set(Object obj, Object value)
    2. 获取值:get get(Object obj)
  2. Constructor:构造方法
    1. 创建对象:newInstance(Object... initargs)
    • 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance()方法
  3. Method:方法对象
    1. 执行此方法:Object invoke(Object obj, Object... args)
    2. 获取方法名称:String getName()

6. 反射案例

  • 需求:写一个简单的框架,在不能改变框架的任何代码的前提下,要求此框架可以帮助我们创建任意类型的对象,并执行其中任意方法
  • 步骤分析
    1. 将需要创建对象的类的全类名(包名.类名)和需要执行的方法先定义在配置文件中
    2. 在程序中加载并读取配置文件
    3. 使用反射加载类文件进内存
    4. 创建对象并执行方法
  • 示例代码
    import java.io.InputStream;
    import java.lang.reflect.Method;
    import java.util.Properties;
    
    /*
        配置文件:pro.properties
        内容:
            className = demo2.Student
            methodName = study
     */
    public class ReflectTest {
        public static void main(String[] args) throws Exception {
            // 获取类加载器
            ClassLoader classLoader = ReflectTest.class.getClassLoader();
            // 获取配置文件的字节输入流
            InputStream inputStream = classLoader.getResourceAsStream("pro.properties");
            // 创建配置文件对象
            Properties properties = new Properties();
            // 加载配置文件的字节输入流
            assert inputStream != null;
            properties.load(inputStream);
            // 读取配置文件获取类名
            String className = properties.getProperty("className");
            // 将类加载进内存
            Class<?> class1 = Class.forName(className);
            // 创建类的对象
            Object instance = class1.newInstance();
            // 读取配置文件获取方法名
            String methodName = properties.getProperty("methodName");
            // 获取方法的对象
            Method method = class1.getMethod(methodName);
            // 执行方法
            method.invoke(instance);
        }
    }
    
  • 好处:改动时只需要更改配置文件,而不需要更改代码,提高了程序的可扩展性

三:注解

1. 作用

  1. 编译检查
    • 通过代码里标识的注解让编译器能够实现基本的编译检查
  2. 编写文档
    • 通过代码里标识的注解生成doc文档
  3. 代码分析
    • 通过代码里标识的注解对代码进行分析

2. JDK中预定义的注解

  1. @Override
    • 表示该注解标识的方法覆盖重写父类(接口)
  2. @Deprected
    • 表示该注解标识的内容已过时(虽然仍然可以使用,但是已经有了更好的替代方式)
  3. @SuppressWarnings
    • 压制所有类型的警告:@SuppressWarnings("all")

3. 自定义注解

  1. 格式
    元注解
    public @interface 注解名称{
        属性列表
    }
    
  2. 本质
    • 注解本质上就是一个接口,默认继承Annotation接口
    • public interface 注解名称 extends java.lang.annotation.Annotation {}
  3. 属性
    • 也就是接口中的抽象方法
    • 要求
      1. 属性的返回值只有以下取值
        • 基本数据类型
        • String
        • 枚举
        • 注解
        • 以上类型的数组
      2. 如果定义了属性,在使用时需要给属性赋值(一般情况下)
        • 如果定义属性时,使用default关键字给属性赋予默认初始值,那么给不给属性赋值都可以
        • 如果只有一个属性需要赋值,并且该属性名为value,那么在赋值时可以省略value=,直接赋值即可
        • 为数组赋值时,多个值需要使用{}包裹,如果数组中只有一个值,那么{}可以省略
        • 示例代码
          public @interface AnnotationTest {
              public abstract String[] value();
          }
          
          @AnnotationTest("zs")
          public class Test {
          }
          
  4. 元注解
    • 概念:用于描述注解的注解
    • 分类
      1. @Target描述注解能够作用的位置
        public @interface Target {
            ElementType[] value();
        }
        
      • 其中枚举类ElementType的取值有TYPE(类),FIELD(成员变量),METHOD(方法)等取值,例如@Target({ElementType.TYPE, ElementType.METHOD})
      1. @Retention描述注解被保留的阶段
        public @interface Retention {
            RetentionPolicy value();
        }
        
      • 其中枚举类RetentionPolicy的取值有SOURCE(源代码阶段),CLASS(类对象阶段),RUNTIME(运行时阶段),一般自定义注解使用此语句@Retention(RetentionPolicy.RUNTIME),表示当前被描述的注解会保留到class字节码文件中,并被JVM读取到
      1. @Documented描述注解是否会被抽取到api文档中
      2. @Inherited描述注解是否会被子类继承
  5. 解析注解
    • 在程序中解析(使用)注解,就是获取注解中定义的属性值
    • 注解多用于替换配置文件
    • 步骤:
      1. 获取注解作用的位置(CLASS/FIELD/METHOD)的字节码文件对象
      2. 获取注解对象
      3. 调用注解对象中定义的抽象方法,获取返回值
    • 示例代码
      • 利用注解实现反射案例的需求
        import java.lang.reflect.Method;
        
        @TestAnnotation(className = "demo3.Demo1", methodName = "method1")
        public class Test {
            public static void main(String[] args) throws Exception {
                // 获取该类的字节码文件对象
                Class<Test> testClass = Test.class;
                // 获取注解对象
                TestAnnotation annotation = testClass.getAnnotation(TestAnnotation.class);
                // 调用注解对象中定义的抽象方法,获取返回值
                String className = annotation.className();
                String methodName = annotation.methodName();
                // 加载该类进内存
                Class<?> aClass = Class.forName(className);
                // 创建该类的对象
                Object instance = aClass.newInstance();
                // 获取方法对象
                Method method = aClass.getMethod(methodName);
                // 执行方法
                method.invoke(instance);
                }
        }