注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它是框架学习和设计者必须掌握的基础
注解基础
注解主要的作用有以下四方面
- 生成文档,通过代码里标识的元数据生成javadoc文档
- 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证
- 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码
- 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例
这么来说是比较抽象的,具体看下注解的常见分类
- Java自带的标准注解,包括
、
@Override
和
@Deprecated
,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查
@SuppressWarnings
- 元注解,元注解是用于定义注解的注解,包括
、
@Retention
、
@Target
、
@Inherited
,
@Documented
用于标明注解被保留的阶段,
@Retention
用于标明注解使用的范围,
@Target
用于标明注解可继承,
@Inherited
用于标明是否生成javadoc文档
@Documented
- 自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解
Java内置注解
class A {
public void test() {
}
}
class B extends A {
/**
* 重载父类的test方法
*/
@Override
public void test() {}
/**
* 被弃用的方法
*/
@Deprecated
public void oldMethod() {}
/**
* 忽略告警
*
* @return
*/
@SuppressWarnings("rawtypes")
public List processList() {
List list = new ArrayList();
return list;
}
}
Java 1.5开始自带的标准注解,包括
@Override
、
@Deprecated
和
@SuppressWarnings
:表示当前的方法定义将覆盖父类中的方法
@Override
:表示代码被弃用,如果使用了被@Deprecated注解的代码则编译器将发出警告
@Deprecated
:表示关闭编译器警告信息
@SuppressWarnings
内置注解 - @Override
先来看一下这个注解类型的定义
/**
* Indicates that a method declaration is intended to override a
* method declaration in a supertype. If a method is annotated with
* this annotation type compilers are required to generate an error
* message unless at least one of the following conditions hold:
*
* <ul><li>
* The method does override or implement a method declared in a
* supertype.
* </li><li>
* The method has a signature that is override-equivalent to that of
* any public method declared in {@linkplain Object}.
* </li></ul>
*
* @author Peter von der Ahé
* @author Joshua Bloch
* @jls 9.6.1.4 @Override
* @since 1.5
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
我们可以看到,这个注解可以被用来修饰方法,并且它只在编译时有效,在编译后的class文件中便不再存在。这个注解的作用我们大家都不陌生,那就是告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,若发现父类中不存在这个方法或是存在的方法签名不同,则会报错
内置注解 - @Deprecated
这个注解的定义如下
/**
* A program element annotated @Deprecated is one that programmers
* are discouraged from using, typically because it is dangerous,
* or because a better alternative exists. Compilers warn when a
* deprecated program element is used or overridden in non-deprecated code.
*
* @author Neal Gafter
* @since 1.5
* @jls 9.6.3.6 @Deprecated
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
我们可以知道,它会被文档化,能够保留到运行时,能够修饰构造方法、属性、局部变量、方法、包、参数、类型。这个注解的作用是告诉编译器被修饰的程序元素已被“废弃”,不再建议用户使用
内置注解 - @SuppressWarnings
这个注解我们也比较常用到,先来看下它的定义
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
/**
* The set of warnings that are to be suppressed by the compiler in the
* annotated element. Duplicate names are permitted. The second and
* successive occurrences of a name are ignored. The presence of
* unrecognized warning names is <i>not</i> an error: Compilers must
* ignore any warning names they do not recognize. They are, however,
* free to emit a warning if an annotation contains an unrecognized
* warning name.
*
* <p> The string {@code "unchecked"} is used to suppress
* unchecked warnings. Compiler vendors should document the
* additional warning names they support in conjunction with this
* annotation type. They are encouraged to cooperate to ensure
* that the same names work across multiple compilers.
* @return the set of warnings to be suppressed
*/
String[] value();
}
它能够修饰的程序元素包括类型、属性、方法、参数、构造器、局部变量,只能存活在源码时,取值为String[]。它的作用是告诉编译器忽略指定的警告信息,它可以取的值如下所示
参数 | 作用 | 原描述 |
---|---|---|
all | 抑制所有警告 | to suppress all warnings |
boxing | 抑制装箱、拆箱操作时候的警告 | to suppress warnings relative to boxing/unboxing operations |
cast | 抑制映射相关的警告 | to suppress warnings relative to cast operations |
dep-ann | 抑制启用注释的警告 | to suppress warnings relative to deprecated annotation |
deprecation | 抑制过期方法警告 | to suppress warnings relative to deprecation |
fallthrough | 抑制确在switch中缺失breaks的警告 | to suppress warnings relative to missing breaks in switch statements |
finally | 抑制finally模块没有返回的警告 | to suppress warnings relative to finally block that don’t return |
hiding | 抑制与隐藏变数的区域变数相关的警告 | to suppress warnings relative to locals that hide variable() |
incomplete-switch | 忽略没有完整的switch语句 | to suppress warnings relative to missing entries in a switch statement (enum case) |
nls | 忽略非nls格式的字符 | to suppress warnings relative to non-nls string literals |
null | 忽略对null的操作 | to suppress warnings relative to null analysis |
rawtype | 使用generics时忽略没有指定相应的类型 | to suppress warnings relative to un-specific types when using |
restriction | 抑制与使用不建议或禁止参照相关的警告 | to suppress warnings relative to usage of discouraged or |
serial | 忽略在serializable类中没有声明serialVersionUID变量 | to suppress warnings relative to missing serialVersionUID field for a serializable class |
static-access | 抑制不正确的静态访问方式警告 | to suppress warnings relative to incorrect static access |
synthetic-access | 抑制子类没有按最优方法访问内部类的警告 | to suppress warnings relative to unoptimized access from inner classes |
unchecked | 抑制没有进行类型检查操作的警告 | to suppress warnings relative to unchecked operations |
unqualified-field-access | 抑制没有权限访问的域的警告 | to suppress warnings relative to field access unqualified |
unused | 抑制没被使用过的代码的警告 | to suppress warnings relative to unused code |
元注解
上述内置注解的定义中使用了一些元注解(注解类型进行注解的注解类),在JDK 1.5中提供了4个标准的元注解:
@Target
,
@Retention
,
@Documented
,
@Inherited
, 在JDK 1.8中提供了两个元注解
@Repeatable
和
@Native
元注解 - @Target
Target注解的作用是:描述注解的使用范围(即:被修饰的注解可以用在什么地方)
Target注解用来说明那些被它所注解的注解类可修饰的对象范围:注解可以用于修饰 packages、types(类、接口、枚举、注解类)、类成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数),在定义注解类时使用了@Target 能够更加清晰的知道它能够被用来修饰哪些对象,它的取值范围定义在ElementType 枚举中
public enum ElementType {
TYPE, // 类、接口、枚举类
FIELD, // 成员变量(包括:枚举常量)
METHOD, // 成员方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类
PACKAGE, // 可用于修饰:包
TYPE_PARAMETER, // 类型参数,JDK 1.8 新增
TYPE_USE // 使用类型的任何地方,JDK 1.8 新增
}
元注解 - @Retention & @RetentionTarget
Reteniton注解的作用是:描述注解保留的时间范围(即:被描述的注解在它所修饰的类中可以被保留到何时)
Reteniton注解用来限定那些被它所注解的注解类在注解到其他类上以后,可被保留到何时,一共有三种策略,定义在RetentionPolicy枚举中
public enum RetentionPolicy {
SOURCE, // 源文件保留
CLASS, // 编译期保留,默认值
RUNTIME // 运行期保留,可通过反射去获取注解信息
}
为了验证应用了这三种策略的注解类有何区别,分别使用三种策略各定义一个注解类做测试
@Retention(RetentionPolicy.SOURCE)
public @interface SourcePolicy {
}
@Retention(RetentionPolicy.CLASS)
public @interface ClassPolicy {
}
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimePolicy {
}
用定义好的三个注解类分别去注解一个方法
public class RetentionTest {
@SourcePolicy
public void sourcePolicy() {
}
@ClassPolicy
public void classPolicy() {
}
@RuntimePolicy
public void runtimePolicy() {
}
}
通过执行
javap -verbose RetentionTest
命令获取到的 RetentionTest 的 class 字节码内容如下
{
public retention.RetentionTest();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public void sourcePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
public void classPolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
RuntimeInvisibleAnnotations:
0: #11()
public void runtimePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 15: 0
RuntimeVisibleAnnotations:
0: #14()
}
从 RetentionTest 的字节码内容我们可以得出以下两点结论
- 编译器并没有记录下 sourcePolicy() 方法的注解信息;
- 编译器分别使用了
和
RuntimeInvisibleAnnotations
属性去记录了
RuntimeVisibleAnnotations
方法 和
classPolicy()
方法 的注解信息;
runtimePolicy()
元注解 - @Documented
Documented 注解的作用是:描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息
以下代码在使用Javadoc工具可以生成
@TestDocAnnotation
注解信息
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface TestDocAnnotation {
public String value() default "default";
}
// 使用
@TestDocAnnotation("myMethodDoc")
public void testDoc() {
}
元注解 - @Inherited
Inherited注解的作用:被它修饰的Annotation将具有继承性。如果某个类使用了被 @Inherited 修饰的 Annotation,则其子类将自动具有该注解
测试下这个注解
- 定义
注解@Inherited
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface TestInheritedAnnotation {
String [] values();
int number();
}
- 使用这个注解
@TestInheritedAnnotation(values = {"vincent"},number = 18)
public class Person {
}
public class Student extends Person {
@Test
public void test(){
Class clazz = Student.class;
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation: annotations){
System.out.println(annotation.toString());
}
}
}
- 输出
@com.vincent.annotationDemo.annotation.annotationImpl.TestInheritedAnnotation(values=[vincent], number=18)
即使Student类没有显示地被注解
@TestInheritedAnnotation
,但是它的父类Person被注解,而且
@TestInheritedAnnotation
被
@Inherited
注解,因此Student类自动有了该注解
元注解 - @Repeatable (Java8)
Repeatable注解的作用:允许在同一申明类型(类,属性,或方法)的多次使用同一个注解
java 8之前也有重复使用注解的解决方案,但可读性不是很好,比如下面的代码
public @interface Authority {
String role();
}
public @interface Authorities {
Authority[] value();
}
public class RepeatAnnotationUseOldVersion {
@Authorities({@Authority(role="Admin"),@Authority(role="Manager")})
public void doSomeThing(){
}
}
由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解
java 8里面的做法
@Repeatable(Authorities.class)
public @interface Authority {
String role();
}
public @interface Authorities {
Authority[] value();
}
public class RepeatAnnotationUseNewVersion {
@Authority(role="Admin")
@Authority(role="Manager")
public void doSomeThing(){ }
}
不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点
元注解 - @Native (Java8)
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可
注解与反射接口
定义注解后,如何获取注解中的内容呢?
反射包java.lang.reflect下的AnnotatedElement接口提供这些方法。注意:只有注解被定义为RUNTIME后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的方法来访问Annotation信息。看下具体的相关接口
判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false。注意:此方法会忽略注解对应的注解容器
boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null
<T extends Annotation> T getAnnotation(Class<T> annotationClass)
返回该程序元素上存在的所有注解,若没有注解,返回长度为0的数组
Annotation[] getAnnotations()
返回该程序元素上存在的、指定类型的注解数组。没有注解对应类型的注解时,返回长度为0的数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。
<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)
方法与
getAnnotationsByType
的区别在于,
getAnnotation
会检测注解对应的重复注解容器。若程序元素为类,当前类上找不到注解,且该注解为可继承的,则会去父类上检测对应的注解
getAnnotationsByType
返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释。如果没有注释直接存在于此元素上,则返回null
<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)
返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释
<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass)
返回直接存在于此元素上的所有注解及注解对应的重复注解容器。与此接口中的其他方法不同,该方法将忽略继承的注解。如果没有注释直接存在于此元素上,则返回长度为零的一个数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响
Annotation[] getDeclaredAnnotations()
自定义注解
- 定义自己的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethodAnnotation {
String title() default "";
String description() default "";
}
- 使用注解
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
public class TestMethodAnnotation {
@Override
@MyMethodAnnotation(title = "toStringMethod", description = "override toString method")
public String toString() {
return "Override toString method";
}
@Deprecated
@MyMethodAnnotation(title = "old static method", description = "deprecated old static method")
public static void oldMethod() {
System.out.println("old method, don't use it.");
}
@SuppressWarnings({"unchecked", "deprecation"})
@MyMethodAnnotation(title = "test method", description = "suppress warning static method")
public static void genericsTest() throws FileNotFoundException {
List l = new ArrayList();
l.add("abc");
oldMethod();
}
}
- 用反射接口获取注解信息
在TestMethodAnnotation中添加Main方法进行测试
public static void main(String[] args) {
try {
// 获取所有methods
Method[] methods = TestMethodAnnotation.class.getClassLoader()
.loadClass(("com.vincent.annotationDemo.TestMethodAnnotation"))
.getMethods();
// 遍历
for (Method method : methods) {
// 方法上是否有MyMethodAnnotation注解
if (method.isAnnotationPresent(MyMethodAnnotation.class)) {
try {
// 获取并遍历方法上的所有注解
for (Annotation anno : method.getDeclaredAnnotations()) {
System.out.println("Annotation in Method '"
+ method + "' : " + anno);
}
// 获取MyMethodAnnotation对象信息
MyMethodAnnotation methodAnno = method
.getAnnotation(MyMethodAnnotation.class);
System.out.println(methodAnno.title());
} catch (Throwable ex) {
ex.printStackTrace();
}
}
}
} catch (SecurityException | ClassNotFoundException e) {
e.printStackTrace();
}
}
- 测试的输出
Annotation in Method 'public java.lang.String com.vincent.annotationDemo.TestMethodAnnotation.toString()' : @com.vincent.annotationDemo.annotation.annotationImpl.MyMethodAnnotation(title=toStringMethod, description=override toString method)
toStringMethod
Annotation in Method 'public static void com.vincent.annotationDemo.TestMethodAnnotation.oldMethod()' : @java.lang.Deprecated()
Annotation in Method 'public static void com.vincent.annotationDemo.TestMethodAnnotation.oldMethod()' : @com.vincent.annotationDemo.annotation.annotationImpl.MyMethodAnnotation(title=old static method, description=deprecated old static method)
old static method
Annotation in Method 'public static void com.vincent.annotationDemo.TestMethodAnnotation.genericsTest() throws java.io.FileNotFoundException' : @com.vincent.annotationDemo.annotation.annotationImpl.MyMethodAnnotation(title=test method, description=suppress warning static method)
test method
深入理解注解
Java 8 - 类型注解
在java 8之前,注解只能是在声明的地方所使用,比如类,方法,属性;
java 8 里面,注解可以应用在任何地方,比如
//创建类实例
new @Interned MyObject();
//类型映射
myString = (@NonNull String) str;
//implements 语句中
class UnmodifiableList<T> implements @Readonly List<@Readonly T> { … }
//throw exception声明
void monitorTemperature() throws @Critical TemperatureException { … }
需要注意的是,类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解
类型注解的作用
Collections.emptyList().add("One");
int i=Integer.parseInt("hello");
System.console().readLine();
上面的代码编译是通过的,但运行是会分别报UnsupportedOperationException; NumberFormatException;NullPointerException异常,这些都是runtime error;
类型注解被用来支持在Java的程序中做强类型检查。配合插件式的check framework,可以在编译的时候检测出runtime error,以提高代码质量。这就是类型注解的作用了
check framework是第三方工具,配合Java的类型注解效果就是1+1>2。它可以嵌入到javac编译器里面,可以配合ant和maven使用, 地址是 http://types.cs.washington.edu/checker-framework/。 check framework可以找到类型注解出现的地方并检查,举个简单的例子
import checkers.nullness.quals.*;
public class GetStarted {
void sample() {
@NonNull Object ref = new Object();
}
}
使用javac编译上面的类
javac -processor checkers.nullness.NullnessChecker GetStarted.java
编译是通过,但如果修改成
@NonNull Object ref = null;
再次编译,则出现
GetStarted.java:5: incompatible types.
found : @Nullable <nulltype>
required: @NonNull Object
@NonNull Object ref = null;
^
1 error
类型注解向下兼容的解决方案
如果你不想使用类型注解检测出来错误,则不需要processor,直接javac GetStarted.java是可以编译通过的,这是在java 8 with Type Annotation Support版本里面可以,但java 5,6,7版本都不行,因为javac编译器不知道@NonNull是什么东西,但check framework 有个向下兼容的解决方案,就是将类型注解 nonnull用 注释起来,比如上面例子修改为
import checkers.nullness.quals.*;
public class GetStarted {
void sample() {
/*@NonNull*/ Object ref = null;
}
}
这样 javac 编译器就会忽略掉注释块,但用check framework里面的javac编译器同样能够检测出nonnull错误。 通过类型注解+check framework我们可以看到,现在runtime error可以在编译时候就能找到
ElementType.TYPE_PARAMETER
ElementType.TYPE_USE
(此类型包括类型声明和类型参数声明,是为了方便设计者进行类型检查)包含了
ElementType.TYPE
(类、接口(包括注解类型)和枚举的声明)和
ElementType.TYPE_PARAMETER
(类型参数声明),不妨再看个例子
// 自定义ElementType.TYPE_PARAMETER注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_PARAMETER)
public @interface MyNotEmpty {
}
// 自定义ElementType.TYPE_USE注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface MyNotNull {
}
// 测试类
public class TypeParameterAndTypeUseAnnotation<@MyNotEmpty T>{
//使用TYPE_PARAMETER类型,会编译不通过
// public @MyNotEmpty T test(@MyNotEmpty T a){
// new ArrayList<@MyNotEmpty String>();
// return a;
// }
//使用TYPE_USE类型,编译通过
public @MyNotNull T test2(@MyNotNull T a){
new ArrayList<@MyNotNull String>();
return a;
}
}
注解支持继承吗?
注解是不支持继承的
不能使用关键字extends来继承某个@interface,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口,虽然反编译后发现注解继承了Annotation接口,即使Java的接口可以实现多继承,但定义注解时依然无法使用extends关键字继承@interface,区别于注解的继承,被注解的子类继承父类注解可以用@Inherited: 如果某个类使用了被@Inherited修饰的Annotation,则其子类将自动具有该注解
继承实现到注解实现 - Junit3到Junit4
一个模块的封装大多数人都是通过继承和组合等模式来实现的,但是如果结合注解将可以极大程度提高实现的优雅度(降低耦合度)。而Junit3 到Junit4的演化就是最好的一个例子
- 被测试类
public class HelloWorld {
public void sayHello(){
System.out.println("hello....");
throw new NumberFormatException();
}
public void sayWorld(){
System.out.println("world....");
}
public String say(){
return "hello world!";
}
}
- Junit 3 实现
通过继承 TestCase来实现,初始化是通过Override父类方法来进行,测试方式通过test的前缀方法获取
public class HelloWorldTest extends TestCase{
private HelloWorld hw;
@Override
protected void setUp() throws Exception {
super.setUp();
hw=new HelloWorld();
}
//1.测试没有返回值
public void testHello(){
try {
hw.sayHello();
} catch (Exception e) {
System.out.println("发生异常.....");
}
}
public void testWorld(){
hw.sayWorld();
}
//2.测试有返回值的方法
// 返回字符串
public void testSay(){
assertEquals("测试失败", hw.say(), "hello world!");
}
//返回对象
public void testObj(){
assertNull("测试对象不为空", null);
assertNotNull("测试对象为空",new String());
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
hw=null;
}
}
- Junit 4 实现
通过定义@Before,@Test,@After 等等注解来实现
public class HelloWorldTest {
private HelloWorld hw;
@Before
public void setUp() {
hw = new HelloWorld();
}
@Test(expected=NumberFormatException.class)
// 1.测试没有返回值,有别于junit3的使用,更加方便
public void testHello() {
hw.sayHello();
}
@Test
public void testWorld() {
hw.sayWorld();
}
@Test
// 2.测试有返回值的方法
// 返回字符串
public void testSay() {
assertEquals("测试失败", hw.say(), "hello world!");
}
@Test
// 返回对象
public void testObj() {
assertNull("测试对象不为空", null);
assertNotNull("测试对象为空", new String());
}
@After
public void tearDown() throws Exception {
hw = null;
}
}
这里我们发现通过注解的方式,我们实现单元测试时将更为优雅