目录
一、单元测试
所谓单元测试,就是针对最小的功能单元,编写测试代码对其进行正确性测试。
在这之前是如何进行单元测试的?有什么问题?
- 只能在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类的字节码代码有三种写法。
- Class c1 = 类名.class
- 调用Class提供方法:public static Class forName(String package);
- 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);
}
}
}
}