【黑马java基础】单元测试、反射、注解

一、单元测试

所谓单元测试,就是针对最小的功能单元,编写测试代码对其进行正确性测试。

在这之前是如何进行单元测试的?有什么问题?

  • 只能在main方法中编写测试代码,然后去调用其他方法进行测试;
  • 无法实现自动化测试,一个方法测试失败,可能影响其他方法的测试;
  • 无法得到测试报告,需要程序员自己观察是否测试成功。

为了测试更加方便,有一些第三方的公司或者组织提供了很好用的测试框架,给开发者使用。这里给同学们介绍一种Junit测试框架。

Junit是第三方公司开源出来的,用于对代码进行单元测试的工具(IDEA已经集成了junit框架)。相比于在main方法中测试有如下几个优点:

  • 可以灵活的编写测试代码,可以针对某个方法进行测试,也支持一键完成对方法的自动化测试,且各自独立;
  • 不需要程序员分析测试结果,会自动生成测试报告出来。

案例:使用Junit进行单元测试

在这里插入图片描述

先准备一个类,假设写了一个StringUtil工具类,代码如下

package com.itheima.d1_junit;
/**
 * 字符串工具类
 */
public class StringUtil {
    public static void printNumber(String name){
        if(name == null){
            System.out.println(0);
            return; // 停掉方法
        }
        System.out.println("名字长度是:" + name.length());
    }

    /**
     * 求字符串的最大索引
     */
    public static int getMaxIndex(String data){
        if(data == null) {
            return -1;
        }
        return data.length() - 1;
    }
}

接下来,写一个测试类,测试StringUtil工具类中的方法能否正常使用。

package com.itheima.d1_junit;

import org.junit.*;

/**
 * 测试类
 */
public class StringUtilTest {

    @Test // 测试方法
    public void testPrintNumber(){
        StringUtil.printNumber("admin");
        StringUtil.printNumber(null);
    }

    @Test // 测试方法
    public void testGetMaxIndex(){
        int index1 = StringUtil.getMaxIndex(null);
        System.out.println(index1);

        int index2 = StringUtil.getMaxIndex("admin");
        System.out.println(index2);

        // 断言机制:程序员可以通过预测业务方法的结果。
        Assert.assertEquals("方法内部有bug!", 4, index2);
    }
}

写完代码之后,我们会发现测试方法左边,会有一个绿色的三角形按钮。点击这个按钮,就可以运行测试方法。

在这里插入图片描述

如果测试通过了,下面会显示绿色,哪个测试类通过了,哪个绿;不通过的是红色;

但不是通过了,就代表代码没问题,也有可能是测试不到位。

断言机制:所谓断言就是程序员可以预测程序的运行结果,检查程序的运行结果是否与预期一致。

运行测试方法,结果如下图所示,表示我们预期值与实际值不一致

在这里插入图片描述

Junit框架常见注解

Junit单元测试框架的常用注解(Junit 4.xxxx版本)

注解说明
@Test测试方法
@Before用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次。
@After用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次。
@BeforeClass用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次。
@AfterClass用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次。
package com.itheima.d1_junit;

import org.junit.*;

/**
 * 测试类
 */
public class StringUtilTest {
    @Before
    public void test1(){
        System.out.println("---> test1 Before 执行了---------");
    }

    @BeforeClass
    public static void test11(){
        System.out.println("---> test11 BeforeClass 执行了---------");
    }

    @After
    public void test2(){
        System.out.println("---> test2 After 执行了---------");
    }

    @AfterClass
    public static void test22(){
        System.out.println("---> test22 AfterClass 执行了---------");
    }

    @Test // 测试方法
    public void testPrintNumber(){
        StringUtil.printNumber("admin");
        StringUtil.printNumber(null);
    }

    @Test // 测试方法
    public void testGetMaxIndex(){
        int index1 = StringUtil.getMaxIndex(null);
        System.out.println(index1);

        int index2 = StringUtil.getMaxIndex("admin");
        System.out.println(index2);

        // 断言机制:程序员可以通过预测业务方法的结果。
        Assert.assertEquals("方法内部有bug!", 4, index2);
    }
}

执行结果:

在这里插入图片描述

开始执行的方法:初始化资源。

执行完之后的方法:释放资源。

比如说测试方法要用通信管道,每个测试方法都要拿到自己独立的通信管道,这种情况下,通信管道就要在@Before中进行初始化,在@After中进行释放,因为每个测试方法都要用自己的通信管道

private Socket socket;

@Before
public void test1(){
    socket = new Socket();  //进行连接
}

@After
public void test2(){
    socket.close();    //进行资源释放
}

比如说这些测试方法公用一个测试管道,就用@BeforeClass和@AfterClass

private static void Socket socket;

@BeforeClass
public static void test11(){
    socket = new Socket();
}

@AfterClass
public static void test22(){
    socket.close();
}

Junit单元测试框架的常用注解(Junit 5.xxxx版本)

注解说明
@Test测试方法
@BeforeEach用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次。
@AfterEach用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次。
@BeforeAll用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次。
@AfterAll用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次。

二、反射

接下来我们学习的反射、动态代理、注解等知识点,在以后开发中极少用到,这些技术都是以后学习框架、或者做框架的底层源码。

2.1 认识反射

反射指的是允许以编程方式访问已加载类的成分(成员变量、方法、构造器等)。

反射技术,指的是加载类的字节码到内存,并以编程的方法解刨出类中的各个成分(成员变量、方法、构造器等)。

反射有啥用呢?其实反射是用来写框架用的,但是现阶段同学们对框架还没有太多感觉。为了方便理解,我给同学们看一个我们见过的例子:平时我们用IDEA开发程序时,用对象调用方法,IDEA会有代码提示,idea会将这个对象能调用的方法都给你列举出来,供你选择,如果下图所示

在这里插入图片描述

问题是IDEA怎么知道这个对象有这些方法可以调用呢? 原因是对象能调用的方法全都来自于类,IDEA通过反射技术就可以获取到类中有哪些方法,并且把方法的名称以提示框的形式显示出来,所以你能看到这些提示了。

那记事本写代码为什么没有提示呢? 因为技术本软件没有利用反射技术开发这种代码提示的功能。

因为反射获取的是类的信息,那么反射的第一步首先获取到类才行。由于Java的设计原则是万物皆对象,获取到的类其实也是以对象的形式体现的,叫字节码对象,用Class类来表示。获取到字节码对象之后,再通过字节码对象就可以获取到类的组成成分了,这些组成成分其实也是对象,其中每一个成员变量用Field类的对象来表示每一个成员方法用Method类的对象来表示每一个构造器用Constructor类的对象来表示

如下图所示:

在这里插入图片描述
在这里插入图片描述

2.2 获取类

在这里插入图片描述

反射的第一步:是将字节码加载到内存,我们需要获取到的字节码对象。

在这里插入图片描述

比如有一个Student类,获取Student类的字节码代码有三种写法。

  1. Class c1 = 类名.class
  2. 调用Class提供方法:public static Class forName(String package);
  3. Object提供的方法:public Class getClass(); Class c3 = 对象.getClass();

不管用哪一种方式,获取到的字节码对象其实是同一个。

package com.itheima.d2_reflect;

/**
 * 目标:获取Class对象。
 */
public class Test1Class {
    public static void main(String[] args) throws Exception {
        //第一种方式
        Class c1 = Student.class;
        System.out.println(c1.getName()); // 全类名
        System.out.println(c1.getSimpleName()); // 简名:Student

        //第二种方式
        Class c2 = Class.forName("com.itheima.d2_reflect.Student");
        System.out.println(c1 == c2);

        //第三种方式
        Student s = new Student();
        Class c3 = s.getClass();
        System.out.println(c3 == c2);
    }
}

//响应结果:
com.itheima.d2_reflect.Student
Student
true
true

2.3 获取类的构造器

在这里插入图片描述

反射的第一步是先得到类对象,然后从类对象中获取类的成分对象,并对其进行操作。

  • Class类中用于获取构造器的方法:
方法说明
Constructor<?>[] getConstructors()返回所有构造器对象的数组(只能拿public的)
Constructor<?>[] getDeclaredConstructors()返回所有构造器对象的数组,存在就能拿到
Constructor getConstructor(Class<?>… parameterTypes)返回单个构造器对象(只能拿public的)
Constructor getDeclaredConstructor(Class<?>… parameterTypes)返回单个构造器对象,存在就能拿到

具体代码如下:

package com.itheima.d2_reflect;

import org.junit.Test;

import java.lang.reflect.Constructor;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 目标:掌握获取类的构造器,并对其进行操作。
 */
public class Test2Constructor {
    @Test
    public void testGetConstructors(){
        // 1、反射第一步:必须先得到这个类的Class对象
        Class c = Cat.class;
        // 2、获取类的全部构造器
        // Constructor[] constructors = c.getConstructors();
        Constructor[] constructors = c.getDeclaredConstructors();
        // 3、遍历数组中的每个构造器对象
        for (Constructor constructor : constructors) {
            System.out.println(constructor.getName() + "--->"
            + constructor.getParameterCount());
        }
    }
//
com.itheima.d2_reflect.Cat--->0
com.itheima.d2_reflect.Cat--->2    
    
    
    

    @Test
    public void testGetConstructor() throws Exception {
        // 1、反射第一步:必须先得到这个类的Class对象
        Class c = Cat.class;
        // 2、获取类的某个构造器:无参数构造器
        Constructor constructor1 = c.getDeclaredConstructor();
        System.out.println(constructor1.getName() + "--->"
                + constructor1.getParameterCount());
        constructor1.setAccessible(true); // 禁止检查访问权限
        Cat cat = (Cat) constructor1.newInstance();
        System.out.println(cat);

        AtomicInteger a;


        // 3、获取有参数构造器
        Constructor constructor2 =
                c.getDeclaredConstructor(String.class, int.class);  //一定要带.class,带了才是String类型的意思
        System.out.println(constructor2.getName() + "--->"
                + constructor2.getParameterCount());
        constructor2.setAccessible(true); // 禁止检查访问权限
        Cat cat2 = (Cat) constructor2.newInstance("叮当猫", 3);
        System.out.println(cat2);

    }
}

获取类构造器的作用是初始化一个对象返回。

  • Constructor类中用于创建对象的方法
符号说明
T newInstance(Object… initargs)根据指定的构造器创建对象
public void setAccessible(boolean flag)设置为true,表示取消访问检查,进行暴力反射

反射可能会破坏封装性

2.4 获取类的成员变量

在这里插入图片描述

  • Class类中用于获取成员变量的方法
方法说明
Field[] getFields()返回所有成员变量对象的数组(只能拿public的)
Field[] getDeclaredFields()返回所有成员变量对象的数组,存在就能拿到
Field getField(String name)返回单个成员变量对象(只能拿public的)
Field getDeclaredField(String name)返回单个成员变量对象,存在就能拿到

具体代码如下:

package com.itheima.d2_reflect;

import org.junit.Test;

import java.lang.reflect.Field;

/**
 * 目标:掌握获取类的成员变量,并对其进行操作。
 */
public class Test3Field {
    @Test
    public void testGetFields() throws Exception {
        // 1、反射第一步:必须是先得到类的Class对象
        Class c = Cat.class;
        // 2、获取类的全部成员变量。
        Field[] fields = c.getDeclaredFields();
        // 3、遍历这个成员变量数组
        for (Field field : fields) {
            System.out.println(field.getName() +  "---> "+ field.getType());
        }
        // 4、定位某个成员变量
        Field fName = c.getDeclaredField("name");
        System.out.println(fName.getName() + "--->" + fName.getType());

        Field fAge = c.getDeclaredField("age");
        System.out.println(fAge.getName() + "--->" + fAge.getType());

        // 赋值
        Cat cat = new Cat();
        fName.setAccessible(true); // 禁止访问控制权限
        fName.set(cat, "卡菲猫");
        System.out.println(cat);

        // 取值
        String name = (String) fName.get(cat);
        System.out.println(name);
    }
}

获取成员变量的作用依然是在某个对象中取值、赋值。

  • Field类中用于取值、赋值的方法
符号说明
void set(Object obj, Object value):赋值
Object get(Object obj)获取值。

2.5 获取类的成员方法

在这里插入图片描述

  • Class类中用于获取成员方法的方法
方法说明
Method[] getMethods()返回所有成员方法对象的数组(只能拿public的)
Method[] getDeclaredMethods()返回所有成员方法对象的数组,存在就能拿到
Method getMethod(String name, Class<?>… parameterTypes)返回单个成员方法对象(只能拿public的)
Method getDeclaredMethod(String name, Class<?>… parameterTypes)返回单个成员方法对象,存在就能拿到

具体代码如下:

package com.itheima.d2_reflect;

import org.junit.Test;

import java.lang.reflect.Method;

/**
 * 目标:掌握获取类的成员方法,并对其进行操作。
 */
public class Test4Method {
    @Test
    public void testGetMethods() throws Exception {
        //  1、反射第一步:先得到Class对象。
        Class c = Cat.class;
        // 2、获取类的全部成员方法。
        Method[] methods = c.getDeclaredMethods();
        // 3、遍历这个数组中的每个方法对象
        for (Method method : methods) {
            System.out.println(method.getName() + "--->"
                    + method.getParameterCount() + "---->"
                    + method.getReturnType());
        }
        //  4、获取某个方法对象
        Method run = c.getDeclaredMethod("run"); // 拿run方法,无参数的
        System.out.println(run.getName() + "--->"
                + run.getParameterCount() + "---->"
                + run.getReturnType());

        Method eat = c.getDeclaredMethod("eat", String.class);
        System.out.println(eat.getName() + "--->"
                + eat.getParameterCount() + "---->"
                + eat.getReturnType());

        Cat cat = new Cat();
        run.setAccessible(true); // 禁止检查访问权限
        Object rs = run.invoke(cat); // 调用无参数的run方法,用cat对象触发调用的。
        System.out.println(rs);

        eat.setAccessible(true); // 禁止检查访问权限
        String rs2 = (String) eat.invoke(cat, "鱼儿");
        System.out.println(rs2);
    }
}

获取成员方法的作用依然是在某个对象中进行执行此方法

  • Method类用于触发执行的方法
符号说明
Object invoke(Object obj, Object… args)运行方法 参数一:用obj对象调用该方法 参数二:调用方法的传递的参数(如果没有就不写) 返回值:方法的返回值(如果没有就不写)

2.6 作用、应用场景

反射的作用:

  • 基本作用:可以得到一个类的全部成分然后操作
  • 可以破坏封装性
  • 最重要的是:适合做java的框架,基本上主流的框架都会基于反射设计出一些通用的功能

案例:使用反射做一个简易版框架

三、注解

3.1 注解概述(Annotation)

先来认识一下什么是注解?

Java注解是代码中的特殊标记,比如@Override、@Test等,作用是:让其他程序根据注解信息决定怎么执行该程序。

比如:Junit框架的@Test注解可以用在方法上,用来标记这个方法是测试方法,被@Test标记的方法能够被Junit框架执行。

再比如:@Override注解可以用在方法上,用来标记这个方法是重写方法,被@Override注解标记的方法能够被IDEA识别进行语法检查。

在这里插入图片描述

  • 注解不光可以用在方法上,还可以用在类上、变量上、构造器上等位置。

上面我们说的@Test注解、@Overide注解是别人定义好给我们用的,将来如果需要自己去开发框架,就需要我们自己定义注解。

接着我们学习自定义注解

自定义注解的格式如下图所示

在这里插入图片描述

比如:现在我们自定义一个MyTest注解

public @interface MyTest{
    String aaa();
    boolean bbb() default true;	//default true 表示默认值为true,使用时可以不赋值。
    String[] ccc();
}

定义好MyTest注解之后,我们可以使用MyTest注解在类上、方法上等位置做标记。注意使用注解时需要加@符号,如下

@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{
    @MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})
    public void test1(){
        
    }
}

注意:注解的属性名如何是value的话,并且只有value没有默认值,使用注解时value名称可以省略。比如现在重新定义一个MyTest2注解

public @interface MyTest2{
    String value(); //特殊属性
    int age() default 10;
}

定义好MyTest2注解后,再将@MyTest2标记在类上,此时value属性名可以省略,代码如下

@MyTest2("孙悟空") //等价于 @MyTest2(value="孙悟空")
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{
    @MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})
    public void test1(){
        
    }
}

到这里关于定义注解的格式、以及使用注解的格式就学习完了。

注解本质是什么呢?

想要搞清楚注解本质是什么东西,我们可以把注解的字节码进行反编译,使用XJad工具进行反编译。经过对MyTest1注解字节码反编译我们会发现:

1.MyTest1注解本质上是接口,每一个注解接口都继承子Annotation接口
2.MyTest1注解中的属性本质上是抽象方法
3.@MyTest1实际上是作为MyTest接口的实现类对象
4.@MyTest1(aaa="孙悟空",bbb=false,ccc={"Python","前端","Java"})里面的属性值,可以通过调用aaa()bbb()ccc()方法获取到。 【别着急,继续往下看,再解析注解时会用到】

在这里插入图片描述

3.2 元注解

元注解是修饰注解的注解。我们看一个例子

在这里插入图片描述

接下来分别看一下@Target注解和@Retention注解有什么作用,如下图所示

@Target是用来声明注解只能用在那些位置,比如:类上、方法上、成员变量上等
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期

在这里插入图片描述

  • @Target元注解的使用:比如定义一个MyTest3注解,并添加@Target注解用来声明MyTest3的使用位置
@Target(ElementType.TYPE)	//声明@MyTest3注解只能用在类上
public @interface MyTest3{
    
}

接下来,我们把@MyTest3用在类上观察是否有错,再把@MyTest3用在方法上、变量上再观察是否有错

在这里插入图片描述

如果我们定义MyTest3注解时,使用@Target注解属性值写成下面样子

//声明@MyTest3注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})	
public @interface MyTest3{
    
}

此时再观察,@MyTest用在类上、方法上、变量上是否有错

在这里插入图片描述

到这里@Target元注解的使用就演示完毕了。

  • @Retetion元注解的使用:定义MyTest3注解时,给MyTest3注解添加@Retetion注解来声明MyTest3注解保留的时期
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
	@Retetion(RetetionPloicy.SOURCE): 注解保留到源代码时期、字节码中就没有了
	@Retetion(RetetionPloicy.CLASS): 注解保留到字节码中、运行时注解就没有了
	@Retetion(RetetionPloicy.RUNTIME):注解保留到运行时期
	【自己写代码时,比较常用的是保留到运行时期】
//声明 @MyTest3 注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})	
//控制使用了 @MyTest3 注解的代码中,@MyTest3 保留到运行时期
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest3{
    
}

3.3 注解的解析

各位小伙伴,通过前面的学习我们能够自己定义注解,也能够把自己定义的注解标记在类上或者方法上等位置,但是总感觉有点别扭,给类、方法、变量等加上注解后,我们也没有干什么呀!!!

接下来,我们就要做点什么。我们可以通过反射技术把类上、方法上、变量上的注解对象获取出来,然后通过调用方法就可以获取注解上的属性值了。我们把获取类上、方法上、变量上等位置注解及注解属性值的过程称为解析注解。

解析注解套路如下

1.如果注解在类上,先获取类的字节码对象,再获取类上的注解
2.如果注解在方法上,先获取方法对象,再获取方法上的注解
3.如果注解在成员变量上,先获取成员变量对象,再获取变量上的注解
总之:注解在谁身上,就先获取谁,再用谁获取谁身上的注解

在这里插入图片描述

解析来看一个案例,来演示解析注解的代码编写

在这里插入图片描述

按照需求要求一步一步完成

① 先定义一个MyTest4注解

//声明@MyTest4注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})	
//控制使用了@MyTest4注解的代码中,@MyTest4保留到运行时期
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest4{
    String value();
    double aaa() default 100;
    String[] bbb();
}

② 定义有一个类Demo

@MyTest4(value="蜘蛛侠",aaa=99.9, bbb={"至尊宝","黑马"})
public class Demo{
    @MyTest4(value="孙悟空",aaa=199.9, bbb={"紫霞","牛夫人"})
    public void test1(){
        
    }
}

③ 写一个测试类AnnotationTest3解析Demo类上的MyTest4注解

public class AnnotationTest3{
    @Test
    public void parseClass(){
        //1.先获取Class对象
        Class c = Demo.class;
        
        //2.解析Demo类上的注解
        if(c.isAnnotationPresent(MyTest4.class)){
            //获取类上的MyTest4注解
            MyTest4 myTest4 = (MyTest4)c.getDeclaredAnnotation(MyTest4.class);
            //获取MyTests4注解的属性值
            System.out.println(myTest4.value());
            System.out.println(myTest4.aaa());
            System.out.println(myTest4.bbb());
        }
    }
    
    @Test
    public void parseMethods(){
        //1.先获取Class对象
        Class c = Demo.class;
        
        //2.解析Demo类中test1方法上的注解MyTest4注解
        Method m = c.getDeclaredMethod("test1");
        if(m.isAnnotationPresent(MyTest4.class)){
            //获取方法上的MyTest4注解
            MyTest4 myTest4 = (MyTest4)m.getDeclaredAnnotation(MyTest4.class);
            //获取MyTests4注解的属性值
            System.out.println(myTest4.value());
            System.out.println(myTest4.aaa());
            System.out.println(myTest4.bbb());
        }
    }
}

3.4 注解的应用场景

各位同学,关于注解的定义、使用、解析注解就已经学习完了。接下来,我们再学习一下注解的应用场景,注解是用来写框架的,比如现在我们要模拟Junit写一个测试框架,要求有@MyTest注解的方法可以被框架执行,没有@MyTest注解的方法不能被框架执行。

第一步:先定义一个MyTest注解

@Target(ElementType.METHOD)	
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest{
    
}

第二步:写一个测试类AnnotationTest4,在类中定义几个被@MyTest注解标记的方法

public class AnnotationTest4{
    @MyTest
    public void test1(){
        System.out.println("=====test1====");
    }
    
    @MyTest
    public void test2(){
        System.out.println("=====test2====");
    }
    

    public void test3(){
        System.out.println("=====test2====");
    }
    
    public static void main(String[] args){
        AnnotationTest4 a = new AnnotationTest4();
        
        //1.先获取Class对象
        Class c = AnnotationTest4.class;
        
        //2.解析AnnotationTest4类中所有的方法对象
        Method[] methods = c.getDeclaredMethods();
        for(Method m: methods){
            //3.判断方法上是否有MyTest注解,有就执行该方法
            if(m.isAnnotationPresent(MyTest.class)){
            	m.invoke(a);
        	}
        }
    }
}
  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值