文章目录
1、Junit单元测试
1.1 前言
JUnit是一个开源的java语言的单元测试框架,专门针对java语言设计, 使用最广泛, JUnit是标准的单元测试架构。
java单元测试是最小的功能单元测试代码, 单元测试就是针对单个java方法的测试。java程序的最小功能单元是方法。
使用main()
方法来进行测试不能把测试代码和源代码分离出来,效率低下。而使用Junit单元测试可以确保单个方法正常运行,如果修改了方法代码,只需要保证对应的单元(方法)测试通过就OK了,同时可以自动化所有的测试并获得报告。
Junit的特点是可以使用断言(Assertion)测试期望结果,可以方便地组织和运行测试,可以方便地查看测试结果,更可以方便地继承到maven中。
1.2 测试前提
测试类的使用目录必须是如下, 测试类规定标准是在test目录中进行测试。
├── src
│ ├── main
│ │ └── **.java
│ └── test
│ └── **Test.java
1.3 断言
断言的使用, 必须先引入必须的包: IDE自动创建的会自动引入。下面用一个例子说明断言地基本使用:
首先,在src/main中定义一个待测试的计算机类 Calculator.java。
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
}
然后,然后在src/test中定义测试类 CalculatorTest.java。
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
public class CalculatorTest {
public CalculatorTest() {
System.out.println("This is CalculatorTest's Constructor");
}
@Before
public void shows() {
System.out.println("initing...");
}
@After
public void showe() {
System.out.println("closing...");
}
@Test
public void add() {
System.out.println("testing...");
assertEquals(3, new Calculator().add(1, 2));
}
@Test
public void sub() {
System.out.println("testing...");
assertEquals(3, new Calculator().sub(1, 1));
}
}
这里用到了几个注解,解释如下:
@Before
:在@Test
之前运行,用于方法前初始化测试资源;@After
:在@Test
之后运行,用于方法后释放测试资源;@BeforeClass
:初始化非常耗时的资源, 例如创建数据库;@AfterClass
:清理@BeforeClass创建的资源, 例如创建数据库
单个@Test
方法执行前会创建新的XxxTest
实例, 实例变量的状态不会传递给下一个@Test
方法, 单个@Test
方法执行前后会执行@Before
和@After
方法。所以,被@Test
注解的就是一个测试单元。
单独测试add()
方法,输出如下:
This is CalculatorTest's Constructor
initing...
testing...
closing...
单独测试sub()
方法,输出如下:
This is CalculatorTest's Constructor
initing...
testing...
closing...
java.lang.AssertionError:
Expected :3
Actual :0
可以看出sub()
存在问题,这里先说明一下断言的常用方法:
assertEquals(100, x)
: 断言相等assertArrayEquals({1, 2, 3}, x)
: 断言数组相等assertEquals(3.1416, x, 0.0001)
: 浮点数组断言相等assertNull(x)
: 断言为nullassertTrue(x > 0)
: 断言为trueassertFalse(x < 0)
: 断言为false;assertNotEquals
: 断言不相等assertNotNull
: 断言不为null
代码中的sub()
方法用到了 assertEquals(100, x)
方法,因为1-1≠3,所以检测到了错误并进行输出。
这里介绍常用的,其他更详细的使用方法请阅读API。
2、反射
2.1 前言
在Java中,万事万物皆对象。也就是说,在开发Java代码中,最常见的就是建立对象,然后通过对象进行成员变量设置、访问以及成员方法的调用等。
然而,在Java中,还有一个更牛逼哄哄的机制,叫做反射,一听到这个词就开始牛逼了,但是这里的“反射”是什么意思呢?首先先说明我个人的理解,就是和“通过类生成对象,然后通过对象去操作”的思想相反,是一种只关心如何使用类的一种机制。举个例子,我想乘坐一种交通工具从广州到北京,那么,先通过飞机类生成飞机对象,然后调用飞机的飞行方法。而反射就是,我不管你坐飞机、坐车还是坐驴,我只按照一种公式来,就是“通过一个类,调用这个类的方法把你载到北京去”。所以,代码关注的是对类的操作,而不是对类的对象的操作。
2.2 类对象和类的对象
乍一看这两个词好像一个意思,但是要理解Java的反射机制,这两个词是不一样的。“类的对象”很好理解,就是通过一个类的构造函数创造出来的对象。而“类对象”则是“把类当成对象”,也就是类对象了。下面会多处用到这几个词,很绕口,但是要仔细区分理解清楚。
2.3 类类型和类对象
既然把类当成对象了,那这个类对象又是谁创造出来的呢?没错,有一个叫做类类型的类,叫做Class类
,这里的C是大写的。而且每个类的对象都有一个getClass()
方法,这个方法就是问这个对象的老子(类)是谁,下面演示一下:
//定义一个Person类
public class Person{
//类内容
}
//定义一个测试类
public class Demo{
public static void main(String[] args) {
Person p = new Person();
Class c = p.getClass();
System.out.println(c);
}
}
输出如下:
class cn.test.ReflectDemo.Person
前面的class
表示它是一个类对象,后面接着包名.类名
。
2.4 什么是Class类对象
Java弄出这么一个机制,把类当成对象,叫做类对象,这个类对象的类型是类类型(很绕口,但是还是不难理解。)到底是干嘛用的呢?其实反射机制大多用在框架上,理解反射对后面学习各种Java框架很有用处。
首先,我们复习一下Java代码经历的三个阶段:源代码、类对象、运行时阶段。
当我们写好xx.java
源代码,通过javac
就把源代码编译成字节码文件xx.class
,JVM通过类加载器ClassLoader
把xx.class
文件加载成一个个Class类对象
,然后通过类对象创建一个个的类的对象。一个类不论加载多少次也只生成一个Class类对象
。当然,除了让JVM来生成Class类对象
,我们自己也可以生成类对象,有三种方式生成Class类对象
,演示如下:
//定义一个Person类
public class Person{
//类内容
}
//定义一个测试类
public class Demo{
public static void main(String[] args) throws Exception {
Person p = new Person("张三",19);
//第一种,通过对象生成Class类对象
Class c1 = p.getClass();
System.out.println("c1 = " + c1);
//第二种,通过类本身生成Class类对象
Class c2 = Person.class;
System.out.println("c2 = " + c2);
//第三种,通过forName("类的全名")方法生成类对象
Class c3 = Class.forName("cn.test.ReflectDemo.Person");
System.out.println("c2 = " + c2);
//判断三种方法生成的Class对象是否一致
System.out.println(c1 == c2);
System.out.println(c1 == c3);
}
}
输出如下:
c1 = class cn.test.ReflectDemo.Person
c2 = class cn.test.ReflectDemo.Person
c2 = class cn.test.ReflectDemo.Person
true
true
可见,三种方法生成的Class类对象都是一样的。再次强调,类也是一个对象,叫做Class类对象。如果还是不懂什么叫Class类对象,那我说一个“Student类对象”,你就明白什么意思了,即类也是一种对象,是Class类的对象。
2.5 什么是反射
只要明白了什么是Class类对象,懂得了类本身也是一个对象。现在就好解释什么是反射了。官方定义是:JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
现在来看这个定义就不难理解了,所谓反射,就是事先并不知道类和类的内容,要再运行状态中动态获取类的信息。那怎样才叫动态加载一个类呢,想象一下,当我们使用面向对象思想的时候,都是通过一个个的对象去完成一个个任务,那如果我在运行的时候要加一个类或者换掉一个类呢,难不成要程序重新编译?对于项目很大的代码,这是不现实的,比如QQ、微信的升级,都是在线下载一些更新包,而不是重新编译一个软件叫你重新下载软件。下面先通过一个例子演示,然后进一步说明什么是反射,以及如何使用反射。
项目结构如下:
├── src
│ ├── cn.test.ReflectDemo
│ │ └── Demo01.java
│ │ └── Person.java
│ ├── pro.properties
pro.properties 配置文件内容如下:
className=cn.test.ReflectDemo.Person
methodName=sleep
proName=id
Person.java 文件内容 如下:
public class Person {
private String name;
private int age;
public int id;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() {
}
//省略set、get方法
public void sleep() {
System.out.println("Person Sleeping...");
}
public void eat() {
System.out.println("Person Eating...");
}
}
Demo01.java 文件内容如下:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Properties;
public class Demo01 {
public static void main(String[] args) throws Exception {
//获得类加载器ClassLoader
ClassLoader classLoader = Demo01.class.getClassLoader();
//建立属性对象
Properties proFile = new Properties();
//获取 pro.properties 输入流并加载到 pro 对象种
proFile.load(classLoader.getResourceAsStream("pro.properties"));
//获取 pro.properties 配置文件信息
String className = proFile.getProperty("className");
String methodName = proFile.getProperty("methodName");
String proName = proFile.getProperty("proName");
//获取 Class 类对象
Class cls = Class.forName(className);
//通过 Class 类对象获得 Constructor 类对象
Constructor cst = cls.getConstructor(String.class, int.class);
//通过 Class 类对象获得 Method 类对象
Method m = cls.getMethod(methodName);
//通过 Class 类对象获得 Field 类对象
Field pro = cls.getField(proName);
/**
* 通过 Constructor 类对象获得 Object 类对象
* 即 Class 类对象对应什么类,就创建该类对象的对象
* 此处 Class 类对象是 Person 类,所以创建的是Person类对象
*/
Object o = cst.newInstance("学哥斌", 18);
/**
* 调用 Method 类对象的 invoke 方法
* 输入值是通过 Constructor 类对象获得的 Object 类对象
* 根据配置文件,此处输入的是 Person 类对象
* 即:调用 Person 类对象的 m 方法
* 此处的 m 方法是 sleep 方法
*/
m.invoke(o);
/**
* 调用 Field 类对象的 get 方法
* 输入值是通过 Constructor 类对象获得的 Object 类对象
* 根据配置文件,此处输入的是 Person 类对象
* 即:访问 Person 类对象的 pro 成员变量
* 此处的 pro 成员变量是 id
*/
System.out.println(pro.get(o));
}
}
运行后输出如下:
Person Sleeping...
0
这时候我把配置文件修改成如下:
className=cn.test.ReflectDemo.Person
methodName=eat
proName=id
运行后输出如下:
Person Eating...
0
好了,代码演示完了,你会注意到一个地方,就是我完全没有改变源代码,只是修改了配置文件,就改变了输出。这就是反射的,一种只在乎如何使用类=的机制。我的源代码都是在操作如何加载一个类,访问加载到的类的属性并且调用加载到的类的方法,我事先完全不知道加载到的类有什么属性,方法内容是什么,甚至不知道这个类是谁。
随便百度一下就能看到一句话:“Java反射是框架设计的灵魂”。现在你能体会到这句话的含义吧。我们使用框架时,一千行代码可能就50行你自己写的,剩下950行都是框架生成,框架生成代码除了固定格式,还有就是通过配置文件来加载了。Java的反射机制就是把类的各个组成部分封装为其他对象,然后可以在程序运行过程中操作这些对象。还有一个好处就是可以解耦,无需一更新就重新编译,提高程序的可拓展性。
2.6 反射的总结
通过上面的一系列讲解,下面来总结一下反射的内容。
2.6.1 获取Class类对象的方式
Class.forName("全类名")
:将字节码文件加载进内存,返回Class对象;类名.class
:通过类名的属性class获取;对象.getClass()
:getClass()方法在Object类中定义着。
第一种获取方式多用于配置文件,将类名定义在配置文件中,读取文件、加载类;第二种多用于参数的传递;第三种多用于对象的获取字节码的方式。
注意:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
2.6.2 Class类对象的功能
获取成员变量:
Field[] getFields()
:获取所有public修饰的成员变量
Field getField(String name)
:获取指定名称的 public修饰的成员变量
Field[] getDeclaredFields()
:获取所有的成员变量,不考虑修饰符
Field getDeclaredField(String name)
:获取指定名称的成员变量,不考虑修饰符
获取构造方法:
Constructor<?>[] getConstructors()
Constructor<T> getConstructor(Class<?>... parameterTypes)
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
Constructor<?>[] getDeclaredConstructors()
注意,使用了private,记得加一句:AccessibleObject.setAccessible(pro, true);
获取成员方法:
Method[] getMethods()
Method getMethod(String name, Class<?>... parameterTypes)
Method[] getDeclaredMethods()
Method getDeclaredMethod(String name, Class<?>... parameterTypes)
获取全类名:
String getName()
注意,调用getDeclaredxxx
方法后,还要调用返回的对象的setAccessible(true)
方法,才能执行set和get。
3、注解
3.1 什么是注解
注释大家都知道,就是代码中给程序员看的,代码编译的时候不会编译进去,更不会执行。那么注释又是啥呢?其实注释也是一种说明作用的,但是这个说明不是给程序员看的,而是给计算机看的。
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
3.2 注解的作用
看了上面对注解的描述,貌似还是云里雾里,这里说一下注解的作用,就能彻底理解什么是注解了。第一节的Junit单元测试还记得吧,那些@Test
、@Before
、@After
等就叫做注解。程序员要是不看API,谁也不能第一眼就知道这货是干嘛的,所以说明根本不是给程序员看的,而是给计算机看的。注解主要分为三类:自定义注解、JDK内置注解、还有第三方框架提供的注解。
自定义注解、JDK内置注解、还有第三方框架提供的注解。JDK内置注解,比如@Override
检验方法重载,@Deprecated
标识方法过期等。第三方框架定义的注解比如SpringMVC的@Controller
等。
实际项目开发重,注解常常出现在类、方法、成员变量、形参位置上。当然还有上面Junit单元测试的注解。
3.3 注解的本质
注解的定义格式如下:
public @interface 注解名称{
//属性列表;
}
可以通过编译+反编译,得到如下:
import java.lang.annotation.Annotation;
public interface 注解名称 extends Annotation{
//属性列表
}
可以看出,@interface
变成了interface
,而且自动继承了Annotation
。看得出注解其实本质上是一个接口。但是既然要这种写法,应该是跟接口有些区别,还是按照一种新的东西对待。
3.4 注解的原理
既然注解是给计算机看的,那么就得有一个第三方程序专门来解析注解。只要用到注解,必然有一种三角关系:定义注解、使用注解、解析注解。
3.4.1 定义注解
定义注解前先看Java官方是如何定义一个注解的,@Override
的源码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
可以看到@Override
注解上面还有注解,注解的注解称之为元注解,主要有三个:@Documented
、@Target
、@Retention
。
@Documented
用于制作文档,不是很重要,忽略,有需要再自己查API。
@Target
用于限定注解的位置,比如类、成员方法或成员变量等。
@Retention
用于限定注解的保留策略,保留策略有三种:SOURCE\CLASS\RUNTIME,顾名思义,就是说被这个元注解修饰的注解能保留到哪个阶段。
但是还注意到一点,元注解貌似有输入值,为了探索这个是怎么定义的,进入@Retention
源码查看一下,得到下面源代码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
上面提到过,注解是一个接口,既然是接口,就可以定义抽象方法,而接口的方法修饰符默认是public abstract
。其实注解括号里是一个省略写法,@Retention(value=RetentionPolicy.RUNTIME)
,你可能会懵逼,给方法赋值是什么操作?可能是低层又有什么机制在操作,这里就不管了。看到这么多与接口不太一样的东西,所以还是不要把注解等同于接口,最多类比来看代。
继续深入源码,可以看出RetentionPolicy
是一个枚举:
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
分析了注解的源码,了解了元注解,我们来有样学样,自定义一个注解:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnn {
String getValue() default "default value";
}
注解使用了RetentionPolicy.RUNTIME
,说明这个注解能保留到运行阶段。
注解里的成员方法一般不叫方法,叫属性。“给属性赋值”总比“给方法赋值”好理解。属性的数据类型有限制。只能是:八种基本数据类型、String、枚举、Class、注解类型以及这几个对应的一维数组。如果注解的属性只有一个,且叫value,那么使用该注解时,可以不用指定属性名,因为默认就是给value赋值。比如上面的@Target(ElementType.ANNOTATION_TYPE)
不用看源码我都能猜到只有一个属性,且叫做value。但是注解的属性如果有多个,无论是否叫value,都必须写明属性的对应关系。
3.4.1 使用注解
定义一个类,来使用自定义的注解:
@MyAnn(getValue = "annotation on class")
public class Student {
@MyAnn(getValue = "annotation on field")
public String name;
@MyAnn(getValue = "annotation on method")
public void say() {
System.out.println("hello");
}
@MyAnn()
public void defaultMethod() {
System.out.println("DefaultMethod...");
}
}
3.4.2 解析注解
既然自定义了注解,肯定要写一个程序专门来解析这个注解,否则这个注解跟注释没两样。
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ParseAnn {
public static void main(String[] args) throws Exception {
// 通过类名获取 Class 类对象
Class StuClass = Student.class;
// 获取类上面的注解
Annotation annotationOnClass = StuClass.getAnnotation(MyAnn.class);
System.out.println(annotationOnClass);
// 获取成员变量上面的注解
Field name = StuClass.getField("name");
MyAnn annotationOnField = name.getAnnotation(MyAnn.class);
System.out.println(annotationOnField);
// 获取成员方法上面的注解
Method say = StuClass.getMethod("say");
MyAnn annotationOnMethod = say.getAnnotation(MyAnn.class);
System.out.println(annotationOnMethod);
// 获取 defaultMethod 方法上面的注解
Method defaultMethod = StuClass.getMethod("defaultMethod");
MyAnn annotationOnDefaultMethod = defaultMethod.getAnnotation(MyAnn.class);
System.out.println(annotationOnDefaultMethod);
}
}
输出如下:
@cn.test.Annotation.MyAnn(getValue="annotation on class")
@cn.test.Annotation.MyAnn(getValue="annotation on field")
@cn.test.Annotation.MyAnn(getValue="annotation on method")
@cn.test.Annotation.MyAnn(getValue="default value")
这里用到了Java的反射机制来获取自定义的注解,毕竟反射就是获取类、接口等的,而注解又类似于接口。这里解析到了注解内容后仅仅只是打印出来,一般的注解解析程序都是对被注解的地方进行特殊处理,比如Junit的@Before
、@Test
和@After
。
3.5 注解的总结
注解就像是一个标签,给计算机看的,是程序判断执行的依据。比如,注解解析程序读到@Test
就知道这个方法是待测试的方法,而@Before
的方法就要在测试方法之前执行;
注解需要三要素:定义、使用和解析。(一般我们都是使用。)
注解又分为自定义注解、JDK内置注解和第三方注解。我们只要使用就可以,定义和读取都交给别人。
大多数情况下,我们只负责使用注解,框架会将注解的解析程序隐藏起来,除非阅读源码,否则根本看不到。平时用注解多了,不见定义和读取过程,就会忘了注解是如何作用了。如果你忘了,就收藏这篇,到时候翻出来继续看。