JAVA学习14——Junit、反射、注解

0.主要内容

(1)Junit单元测试
(2)反射
(3)注解

1.Junit单元测试

测试分类:
1)黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值;
2)白盒测试:需要写代码,且关注程序具体的执行流程;

步骤:
	1.定义一个测试类(测试用例)
		* 建议:
			* 测试类名:被测试的类名Test	CalculatorTest
			* 包名:xxx.xxx.xx.test		cn.ltcast.test
	2.定义测试方法:可以独立运行
		* 建议:
			* 方法名:test测试的方法名	testAdd()
			* 返回值:void
			* 参数列表:空参
	3.给方法加上@Test,就可以独立运行了
	4.导入junit依赖环境

判定结果:红色代表失败,绿色代表成功。
注意,一般在test方法中,不用System.out输出结果,而是用Assert断言来判断结果和预期结果是否相同。

@Test
public void testAdd() {
	Calculator c = new Calculator();
	int result = c.add(1,2);
	Assert.assertEquals(3,result);//期望值,实际值
}

在测试类中,一般会有两个方法,分别被@Before和@After修饰,它们修饰的方法,一般分别是要申请资源和释放资源的功能的,它们分别会在测试方法之前和之后被自动执行。

@Before
public void init() {
	System.out.println("before");
}

@After
public void close() {
	System.out.println("close");
}

2. 反射

反射,是框架设计的灵魂。框架,是一个半成品软件,可以在框架的基础上进行软件开发,可以简化代码。反射机制,就是将类的各个组成部分封装为其他对象。

反射机制的好处有两个:1)可以在程序运行过程中,操作这些对象;2)可以解耦,提高程序的可扩展性。

在这里插入图片描述

.class:字节码文件。
java文件编译的命令:javac。
源代码阶段,是在硬盘上的;Runtime运行阶段,是在内存上的。

2.1 获取class对象的方式

在三个阶段中,分别有一个获取class对象的方式。
1.class.forName(“全类名”):将字节码文件加载进内存,返回class对象;该方式多用于配置文件,将类名定义在配置文件中,读取文件,加载类。

2.类名.class:通过类名的属性class获取;多用于参数的传递;

3.对象,getClass():getClass()方法在Object类中定义;多用于对象的获取字节码的方式。

Class cls1 = Class.forName("cn.itcast.domain.Person");

Class cls2 = Person.class;

Person p = new Person();
Class cls3 = p.getClass();

结论:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象,都是同一个对象。

2.2 Class对象功能

常用的Class对象中的获取功能:

1)获取成员变量们:
* Field[] getFields()
* Field getField(String name);
* Field[] getDeclaredFields();
* Field getDeclaredField(String name);

2)获取构造方法们;
* Constructor<?>[] getConstructors();
* Constructor<T> getConstructor(类<?>... parameterTypes)
* Constructor<T> getDeclaredConstructor(类<?>... parameterYpes)
* Constructor<?>[] getDeclaredConstructors()

3)获取成员方法们;
* Method[] getMethods(); //获取的所有的方法中,还包括其父类Object的方法。
* Method[] getMethod(String name, 类<?>... paremeterTypes)
* Method[] getDeclaredMethods()
* Method getDeclareMethod(String name, 类<?>... parameterTypes)

4)获取类名;
* String getName()

tips:当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

2.3 Class对象功能——获取成员变量Field

Class personClass = person.class;
Field[] fields = personClass.getFields(); 
//是获取所有public修饰的成员变量

//Field getField(String name); 则是获取指定的public的成员变量

获取成员变量后,我们可以获取这个变量的值,或者改变这个值,用到set函数和get函数。同时,对于Field类型的成员变量,还有一个函数setAccessible,使用来忽略访问权限修饰符的安全检查,这样可以访问私有成员变量的值:

setAccessible(true); //暴力反射
Field a = personClass.getField("a");
Person p = new Person();
Object value = a.get(p);//获取值
a.set(p,"Lucy");//设置值

对于另外两个获取函数,示例代码如下:

Field[] declaredFields = personClass.getDeclaredFields();
//获取所有成员变量,不考虑修饰符

//当我们获取到私有变量,想要访问变量值时,需要暴力反射
Person p = new Person();
Field d = personClass.getDeclaredField("age");
d.setAccessible(true);//暴力反射
Object value = d.get(p);//即可访问

2.4 Class对象功能——获取Constructor

获取构造方法们的介绍。
Constructor的创建对象的方法 :

  • T newInstance(Object… initargs)
Class personClass = Person.class;

//Constructor<T> getConstructor(类<?>... paramTypes)

Constructor constructor = personClass.getConstructor(String.class, int.class);

Object person = constructor.newInstance("张三", 23);
System.out.println(person);

对于空参构造创建对象,有一个更加简洁的替代方法,在Class对象中,直接就有newInstance()这个方法。

Class personClass = Person.class;
Construct constructor1 = personClass.getConstructor();//获取空参构造方法
Object person1 = constructor1.newInstance();//用获取到的空参构造方法,构造一个空参对象person1

//对于空参构造创建对象,直接用Class对象的newInstance()方法即可。
Object o = personClass.newInstance();

2.5 Class对象功能——获取Method

同上面,带Declare的方法可以获取到所有Methods,不带Declare的,只能获取public的Method。

Method对象的方法:

  • Objcet invoke(Object obj, Object… args) //执行指定方法
  • String getName();用来获取方法名
//获取指定名称的方法
Method eat_method = personClass.getMethod("eat");
Person p = new Person();
//执行方法
eat_method.invoke(p);

//当eat方法带参数
Method eat_method2 = personClass.getMethod("eat",String.class);
eat_method2.invoke(p,"饭"); 
String name = method.getName();//获取方法名称

//同样也有暴力反射的方法 
//method.setAccessible(true);

2.6 综合案例

案例需求:写一个“框架”,不能改变该类的任何的代码的前提下,可以帮助我们创建任意类的对象,并且执行其中任意方法。

实现需要用到的两个东西:1)配置文件;2)反射;

步骤:
1)将需要创建的对象的全类名和需要执行的方法定义在配置文件中;
2)在程序中加载读取配置文件;
3)使用反射技术来加载文件进内存;
4)创建对象
5)执行方法

//配置文件 pro.properties,内容如下
className = cn.itcast.domain.Person
methodName = eat;


//测试类
public class ReflectTest {
	public static void main(String[] args) {
		//可以创建任意类的对象,可以执行任意方法

		//1.加载配置文件
		//1,1创建Properties对象
		Properties pro = new Properties();
		//1.2加载配置文件,转换为一个集合
		//1.2.1 获取class目录下的配置文件
		ClassLoader classLoader = ReflectTest.class.getClassLoader();//获取该字节码对应的类的加载器
		InputStream is = classLoader.getResourceAsStream("pro.properties"); 
		//即获取资源的字节流		
		pro.load(is); //完成配置文件加载 

		//2.获取配置文件中定义的数据
		String className = pro.getProperty("className");
		String methodName = pro.getProperty("methodName");

		//加载该类进内存
		Class cls = Class.forName(className);
		//创建对象
		Object obj = cls.newInstance();
		//获取方法对象
		Method method = cls.getMethod(methodName);
		//执行方法
		method.invoke(obj);
		
	}
}

3. 注解

概念:说明程序的,给计算机看的。
注释:用文字描述程序的,给程序员看的。

定义:注解(Annotation),也叫元数据,一种代码级别的说明,时JDK1.5及以后版本引入的一个特性,与类、接口、枚举是同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等前面,用来对这些元素进行说明和注释
使用注解:@注解名称

作用分类:
1)编写文档:通过代码里标识的注解生成文档【生成doc文档】
2)代码分析:通过代码里标识的注解对代码进行分析【使用反射】
3)编译检查:通过代码里标识的注解,让编译器能够实现基本的编译检查【Override】

3.1 JDK中预定义的一些注解

@Override
//检测该被注解的标注的方法是不是继承自父类(或者接口)的;

@Deprecated
//该注解标注的内容,表示已经过时

@SuppressWarnings:
压制警告,不让警告显示
一般传递参数all,@SuppressWarnings("all");

3.2 自定义注解

3.2.1 注解的格式和本质

通过其他注解的源代码,我们可以大致知道,注解的格式是:

//新建一个.java文件,名称即为注解名称,往其中添加如下内容
元注解
public @interface 注解名称 {
	属性列表(即抽象成员方法);
}

这样,在同一个包下,就可以使用这个注解了。

为了进一步了解这个注解的原理,我们可以通过反编译的方法,去了解。步骤如下:
1)将创建这个注解的.java进行编译(javac),生成.class字节文件;
2)再将这个字节文件.class进行反编译(javap),就又重写生成了一个.java文件;
新的.java文件内容如下:

public interface MyAnno extends java.lang.annotation.Annotation{}

因此我们可以知道,注解本制上是一个接口,该接口默认继承Annotation接口。

3.2.2 注解的属性

那么,我们就可以理解为,创建该注解(接口),其中添加的内容,同样可以是那些抽象成员方法,我们又把这些方法称之为属性

对于这个属性(接口中的抽象方法),有以下几个要求:
1)属性的返回值只有以下几种类型:基本数据类型、String、枚举、注解、以上类型数组。
2)定义了属性,在使用时需要给属性赋值,例如:

//创建注解的文件
public @interface MyAnno {
	int age();
	int grade(); //在使用时赋值 
	String name() default "Lucy"; //默认赋值
	Person per();//此时Person是枚举类型
	MyAnno2 anno2();//注解类型
	String[] strs();//字符串类型数组
}

//测试文件
@MyAnno(age = 12, grade = 5, per = Person.P1, anno2 = @MyAnno2, strs = {"abc", "bbb"})
public class Worker {

}

注意:1)如果只有一个属性需要赋值,且属性名称是value,则value可以省略,直接在使用中的括号写上一个值就可以了;2)数组赋值时,值使用{}包裹,如果只有一个值,则{}可以省略。

3.2.3 注解的元注解

**元注解:**是用于描述注解的注解。

常用的元注解有以下几个:

@Target:描述注解能够作用的位置
	* ElementType是一个枚举类型,取值:
		1.TYPE:可以作用于类上;
		2.METHOD:可以作用于方法上;
		3.FIELD:可以作用于成员变量上;
		
@Retention:描述注解被保留的阶段;
	@Retention(RetentionPolicy.RUNTIME)
	//当前被描述的注解,会被保留到class字节码文件中,并被JVM读取到;自己定义的注解,一般使用RUNTIME
	@Retention(RetentionPolicy.CLASS)
	//当前被描述的注解,会被保留到class字节码文件中;
	@Retention(RetentionPolicy.SOURCE)
	//当前被描述的注解,不会被保留到class字节码文件中;

@Documented:描述注解是否被抽取到api文档中
@Inherited:描述注解是否被子类继承

接下来,试着使用上面的元注解。

/**
元注解:
	@Target:描述注解能够作用的位置
	@Retention:描述注解被保留的阶段;
	@Documented:描述注解是否被抽取到api文档中
	@Inherited:描述注解是否被子类继承

*
*/

@Target({ElementType.TYPE, ElementType.METHOD}) 
//表示该MyAnno3注解只能作用于类上,且只有一个属性要赋值,value可以省略。
@Retention(RetentionPolicy.RUNTIME)
@Documented
//注解将被抽取到api文档中
@Inherited
//注解将被被注解的类的子类继承
public @interface MyAnno3 {
	
}

3.3 在程序中使用(解析)注解

解析注解,就是获取注解中定义的属性值。一般,注解就是用来替换配置文件的。

步骤:
1.获取注解定义的位置的对象(Class,Method,Field);
2.获取指定的注解:
getAnnotation(Class)
该方法其实就是在内存中生成了一个该注解接口的子类实现对象。

public class ProImpl implements Pro {
	public String className() {
		return "cn.itcast.annotation.Demo1";
	}
	public String methodName() {
		return "show";
	}
}

3.调用注解中的抽象方法,获取配置的属性值。

示例代码:

//创建注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro {
	String className();
	String methodName();
}

//创建测试类
@Pro(className = "cn.itcast.annotation.Demo1", methodName = "show")
public class ReflectTest {
	public static void main(String[] args) throws Exception {
		//这里,不改变任何代码,实现可以创建任意类的对象,可以执行任意方法
		//1.解析注解
		//1.1获取该类的字节码文件对象
		Class<ReflectTest> reflectTestClass = ReflectTest.class;
		//2.获取上边的注解对象
		//其实相当于就是在内存中生成了一个该接口的子类实现对象,返回对应的值
		/*
			public class ProImpl implements Pro {
				String className() {
					return "cn.itcast.annotation.Demo1";
				}
				public String methodName() {
					return "show";
				}
			}
		*/
		Pro an = reflectTestClass.getAnnotation(Pro.class);
		//3.调用注解对象中定义的抽象方法,获取返回值
		String className = an.className();
		String methodName = an.methodName();
		//4.加载该类进内存
		Class cls = Class.forName(className);
		5.获取方法对象
		Method method = cls.getMethod(methodName);
		6.执行方法
		method.invoke(obj);
	}
}

3.4 综合案例

测试一个小明定义的计算器类,计算器类如下:

public class Calculator {
	public void add() {
		System.out.println("1+0 = " + (1+0));
	}
	public void sub() {
		System.out.println("1-0=" + (1-0));
	}
	public void mul() {
		System.out.println("1*0=" + (1*0));
	}
	public void div() {
		System.out.println("1/0="+(1/0));
	}

	public void show() {
		System.out.println("OK");
	}
}

为了测试更加方便高级,我们写一个测试的框架,通过创建注解来实现,而不是用配置文件来实现。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Check {
} 

这样一来,我们想给计算器类哪个方法做测试,就在哪个方法前面加上注解@Check。

那么创建好注解,在想要测试的方法前添加上注解后,我们开始写我们的框架。

/*
当主方法执行后,会自动执行被检测的所有方法(加了Check注解的方法;
判断方法是否有异常,记录到文件中
*/
public class TestCheck {
	public static void main(String[] args) throws IOException {
		//1.创建计算器对象
		Calculator c = new Calculator();
		//2.获取计算器对象的字节码文件对象
		Class cls = c.getClass();
		//3.获取所有方法
		Method[] methods = cls.getMethods();
		int number = 0;
		BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt"));
		for(Method method : methods)  {
			//4.判断方法上是否有Check注解,有则执行
			if(method.isAnnotationPresent(Check.class)) {
				try {
					method.invoke(c);
				} catch (Exception e) {
					//5.捕获异常
					number++;
					bw.write(method.getName() + "方法出异常了");
					bw.newLine();
					bw.write("异常的名称:"+ e.getCause().getClass().getSimpleName());
					bw.newLine();
					bw.write("异常的原因:"+e.getCause().getMessage());
					bw.newLine();
					bw.write("========================"); 
					bw.newLine();
				}
			}
		}

		bw.write("本次测试一共出现" + number +"次异常”);
		bw.flush();
		bw.close();
	}
}

小结:以后大多数时候,使用的是注解,而不是自定义注解;注解是给编译器用的,也是给解析程序(类似上面的测试框架)用的;注解不是程序的一部分,可以理解为注解就是一个标签。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值