【effective Java读书笔记】注解(一)
前言:
为什么许多面试Java都喜欢用这本书上的知识?
原因很简单,读懂这本书,其实考java深入一些就是考源码分析,这本书把jdk优秀的源码许多地方都有所涉及,并形成了自己的总结。我们只需要吸收这部分优秀的代码理念。
阅读本文之前,如果对注解了解较少,可以阅读这篇文章了解基本的概念。
什么情况下我们会自定义注解?书中提及的是工具铁匠,也就是给开发人员编写开发工具的开发人员会用到。但是,这并不妨碍我们学会理解使用它。
一、注解优先于命名模式
我们平时写代码看到注解的人多,关注的可能并不多。
先说命名模式的概念:一种命名方式,通过命名区分方法的作用。
书中提及命名模式的三个缺点:
1、命名错误导致失败,但并不会错误提示。给人造成错误的安全感。通俗点说,也就是,看上去一切正常。
我的理解:
大概,从这我们可以猜测,命名模式的使用,其实无外乎也是通过反射去遍历,根据约束条件获取到所约束的方法的名字,例如Test开头的约定为测试类。然后执行这些方法。
当然,如果我们命名的时候把Test手误打成了Tset,顺序颠倒的命名,那么肯定是找不到这个方法,更不用说执行这个方法了。
假如是个单元测试方法,方法都没执行,也就不存在错误信息报错,那么我们就得到了错误安全感。
2、前面提到的方法的命名模式,类级别的无法确保他们只用于相应的元素上。什么意思呢?
我的理解:也就是如果找到这个需要执行的类,那么其中所有的方法都要执行测试方法、无关名称。原理依然是反射,通过反射执行命名模式约束的类的所有方法。相当于无差别伤害了。
3、没有提供将参数与程序元素关联起来的好方法。
反之也就是注解的三个优点,具体会在后续的例子中提及。
看一个demo:
写一个简单的注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
写一个上述注解的使用类:public class Sample {
@Test
public static void m1(){}
public static void m2(){}
@Test
public static void m3(){
throw new RuntimeException("Boom");
}
public static void m4(){}
@Test
public void m5(){}
public static void m6(){}
@Test
public static void m7(){
throw new RuntimeException("Crash");
}
public static void m8(){}
}
运行使用注解的类的方法:
public class RunTests {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);
for (Method m:testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
}catch(InvocationTargetException wrappedExc){
Throwable exc = wrappedExc.getCause();
System.out.println(m+"failed"+exc);
} catch (Exception e) {
System.out.println("INVALID @Test"+m);
}
}
}
System.out.printf("Passed:%d,Failed:%d%n",passed,tests-passed);
}
}
运行结果如下:
public static void com.annotation.Sample.m3()failedjava.lang.RuntimeException: Boom
INVALID @Testpublic void com.annotation.Sample.m5()
public static void com.annotation.Sample.m7()failedjava.lang.RuntimeException: Crash
Passed:1,Failed:3
结果说明:第1行,第3行是我抛出的异常m3,m7,第2行是无法唤醒这个方法。因为这个方法不是静态的。也就是说其实我们并没有获取到实例对象,只是获取到类。所以只能唤醒静态方法。
回到刚刚说的三个缺点,三个优点:
第一点、错误的安全感。使用@Test如果使用成@Tset那么编译直接报错。问题解决!
第二点、我们可以执行Sample类,唤醒Sample类中所有指定@Test的方法执行。问题解决!
第三点、根据入参约束程序,见下一个举例。
看修改后的demo:
注解稍有不同:添加一个泛型约束的值,主要是后面会根据入参确定值的类型
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
注解的使用类:(运算条件异常ArithmeticException)
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() {
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() {
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {
}
}
运行使用注解的类的方法:
public class RunTests2 {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);
for (Method m:testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed:no exception%n",m);
}catch(InvocationTargetException wrappedExc){
Throwable exc = wrappedExc.getCause();
Class<? extends Exception> excType = m.getAnnotation(ExceptionTest.class).value();
if (excType.isInstance(exc)) {
passed++;
}else {
System.out.printf("Test %s failed:no expected %s,got %s%n",m,excType.getName(),exc);
}
} catch (Exception e) {
System.out.println("INVALID @Test"+m);
}
}
}
System.out.printf("Passed:%d,Failed:%d%n",passed,tests-passed);
}
}
运行结果:
Test public static void com.annotation.Sample2.m3() failed:no exception
Test public static void com.annotation.Sample2.m2() failed:no expected java.lang.ArithmeticException,gotjava.lang.ArrayIndexOutOfBoundsException: 1
Passed:1,Failed:2
结果说明:与改进前代码的差别差异说明:关键代码如下
Class<? extends Exception> excType = m.getAnnotation(ExceptionTest.class).value();
if (excType.isInstance(exc)) {
passed++;
}
取出入参即ArithmeticException.class,用它判断异常。第三个优点到此结束。一点点扩展:注解参数灵活多样,可以是数组,例如:
注解如下:参数为数组
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest2 {
Class<? extends Exception>[] value();
}
注解使用如下:
public class Sample3 {
@ExceptionTest2({IndexOutOfBoundsException.class,NullPointerException.class})
public static void doublyBad() {
List<String> list = new ArrayList<String>();
//在第5个位置添加空
list.addAll(5,null);
}
}
注解执行如下:
public class RunTests3 {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);
for (Method m:testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest2.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed:no exception%n",m);
}catch(InvocationTargetException wrappedExc){
Throwable exc = wrappedExc.getCause();
Class<? extends Exception>[] excType = m.getAnnotation(ExceptionTest2.class).value();
int oldPassed = passed;
for (Class<? extends Exception> e:excType) {
if (e.isInstance(exc)) {
passed++;
System.out.printf("Test %s passed: %s%n",m,exc);
break;
}
}
if (passed==oldPassed) {
System.out.printf("Test %s failed: %s %n",m,exc);
}
}
}
}
System.out.printf("Passed:%d,Failed:%d%n",passed,tests-passed);
}
}
执行结果:
Test public static void com.annotation.Sample3.doublyBad() passed: java.lang.IndexOutOfBoundsException: Index: 5, Size: 0
Passed:1,Failed:0
关键代码如下:获取到入参数组类型,并逐个取出,符合异常类型的通过。Class<? extends Exception>[] excType = m.getAnnotation(ExceptionTest2.class).value();
int oldPassed = passed;
for (Class<? extends Exception> e:excType) {
if (e.isInstance(exc)) {
passed++;
System.out.printf("Test %s passed: %s%n",m,exc);
break;
}
}
二、坚持使用OverRide注解(现代的ide都已经集成此功能)
简单提及一下,OverRide注解是基于方法签名(参数类型、顺序、长度),一致则使用OverRide。
唯一的例外则是集成父类抽象方法,例如抽象父类Person有抽象方法say,子类集成父类则不需要使用OverRide。当然此处现代编译器同样会添加OverRide注解。
父类:
public abstract class Person {
abstract void say();
}
子类:
public class Bob extends Person {
@Override
void say() {
}
}
书中说此种方式可以不强制注解的原因是因为如果不覆盖,编译器也会提示必须对这个抽象方法say具体化。