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