抽象类,接口,注解

抽象类,接口,注解

一. 抽象类

如字面意义,就是抽象的类,通过abstract关键字声明某个类或方法是抽象的。

抽象类可以包含抽象方法、域、实例方法等,当然在抽象的类中放置具体的数据和方法似乎有些违背其抽象的定义,但在程序设计时一般建议把最通用的域和方法放到顶层基类中,而不用刻意的考虑是否是抽象类。

抽象类的扩展可以通过两种方式:

  1. 抽象类定义部分抽象方法或不定义任何抽象方法,这样的话子类就必须是抽象类,要负责声明部分抽象方法
  2. 抽象类定义全部的抽象方法,子类就可以是正常类,无需再声明为抽象类。

抽象类不能被实例化,但可以声明抽象类型变量。

类似下列调用a.method();变量a是抽象类型或接口等都是合法的,因为它们不能实例化,所以会去调用子类或实现类对象。

二. 接口

2.1 什么是接口

接口不是类,是对类的一组需求描述,接口定义了一系列行为等,实现此接口的类就必须要包含接口定义的方法。通过implements关键字为类声明要实现哪些接口。

特性:

  1. 接口不是类,接口没有实例,所以不能在接口内引用实例域,也不能通过new去实例化,但可以声明接口变量。
  2. 接口不能包含实例域,但可以定义常量,在Java8以后也可以实现方法。
  3. Java8以后也可以声明静态方法,静态方法有些破坏接口的抽象概念,但并不违法。
  4. 可以通过在接口引入静态方法或直接实现方法来舍掉伴随类,如常用工具类Collections-Collection。
  5. 接口中所有方法自动设置为public,但在类中实现此方法时依然要显示声明public,否则仍为default。
  6. 接口中的域自动设为public static final
  7. 接口和类一样可以通过继承来扩展。
  8. 类可以实现多个接口,为Java带来了极大的灵活性。
  9. 接口的意义之一就是服务于Java的强类型特性,为编译器检查提供遍历。
  10. 可以通过default为接口方法提供一个默认实现,当然每次类实现时会覆盖它。

2.2 解决默认方法冲突

当在接口中将某方法定义为默认,又在超类或其它接口中定义了同样的方法,则遵从以下规则:

  1. 超类有限,超类若提供了具体方法,则相同参数的默认方法会被忽略。
  2. 接口冲突,类实现的多个接口中有同参数同名的方法,其中有默认方法,类会同时得到两个方法,由程序员来实现此方法解决二义性,若没有默认方法,则不存在冲突。

2.3 应用场景

(1)回调

回调是一种常见的设计模式,指定在发生某个事件时采取什么动作,如定时作业,Java中有一个Timer类,可以在一定的时间间隔后发出通告。如果我们构造一个定时器,需要设置一个时间间隔,并告知定时器在到达时间时要做哪些操作。

如何告知定时器执行操作?一些语言可以直接指定一个函数名,定时器会周期的调用它,Java则是采用面向对象的方法,它会将某个类的对象传递给定时器,然后定时器会调用此对象的方法,因为对象相比方法来说可以携带更多的信息,因此相比传递函数传递对象会更灵活。

1
2
3
4
// 通过此接口告知定时器要调用的方法,ActionEvent提供了事件的相关信息
public interface ActionListener {
void actionPerformed(ActionEvent event);
}

(2)比较

当需要对对象数组进行排序时,会要求对象实现了Comparable接口,保证了对象是可比较的,排序实际上就是比较交换。

当如String等Java提供的类或是不应该覆盖 compareTo() 方法的类需要有另外一种排序规则时,可以通过比较器Comparator定义一个排序规则,再通过Arrays等工具类进行排序。

(3)克隆

当要克隆一个对象时,要保证对象实现了Cloneable接口,此接口会要求类提供一个安全的clone方法。对象克隆更多内容可以参考Java对象克隆

2.4 接口和抽象类

区别:

  1. 从设计层面来看抽象类更像是对整个类的抽象,接口则是对行为的约束。
  2. 抽象类受限于类不支持多继承,而接口则可以实现多重继承。

三. 注解

3.1 什么是注解

注解是一种标签,告知工具一些信息,使工具可以在源码层次上操作,或处理编译器放置注解的类文件。在Java中类似修饰符,如public等,可以修饰类或成员方法属性,以及局部变量。

常见的注解如下:

  1. @Test :表示类或方法需要测试,并在测试后删除代码,避免在打包时和其它代码装在一起。
  2. @Override
  3. ……

注解可以包含元素,如@Test(timeout = “10000”),可以被工具读取。

注解通过创建注解接口的形式创建,@interface 标识注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
// @Retention和@Target是元注解,标识@Test,使其只能修饰方法,且在虚拟机加载类文件时仍可以保留下来。
public @interface Test {
Class<? extends Throwable> expected() default Test.None.class;

long timeout() default 0L;//注解可以指定的参数

public static class None extends Throwable {
private static final long serialVersionUID = 1L;

private None() {
}
}
}

3.2 使用场景

基本用处:

  1. 附属文件的自动生成,如部署描述符或bean信息类等
  2. 测试,日志,事务语义等代码的自动生成

如实现一个监听器,一般通过匿名内部类的方式去创建,就可以通过设计一个注解,标识某个方法,当事件方式时就调用此方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    Button button = new Button();
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
doSomeThing();
}
});

//通过注解实现
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
String source();
}

@ActionListenerFor(source = "button")
private static void doSomeThing(){

}


public class ActionListenerInstaller{
/**
* Processes all ActionListenerFor annotations in the given object.
* @param obj an object whose methods may have ActionListenerFor annotations
*/
public static void processAnnotations(Object obj){//枚举出对象接收到的所有方法
try{
Class<?> cl = obj.getClass();
for (Method m : cl.getDeclaredMethods()){//获取每个方法的ActionListenerFor注解对象,并分别处理
ActionListenerFor a = m.getAnnotation(ActionListenerFor.class);//通过AnnotationElement接口所提供方法获得注解对象;方法、构造器、域、类和包都实现了此接口
if (a != null){
Field f = cl.getDeclaredField(a.source());////通过source()获得源成员域,此处只针对成员域,而没有考虑成员变量
f.setAccessible(true);
addListener(f.get(obj), obj, m);//为每个方法添加一个监听器
}
}
}
catch (ReflectiveOperationException e){
e.printStackTrace();
}
}

/**
* Adds an action listener that calls a given method.
* @param source the event source to which an action listener is added
* @param param the implicit parameter of the method that the listener calls
* @param m the method that the listener calls
*/
public static void addListener(Object source, final Object param, final Method m)
throws ReflectiveOperationException{
InvocationHandler handler = new InvocationHandler(){//构造代理,通过反射机制处理注解
public Object invoke(Object proxy, Method mm, Object[] args) throws Throwable{
return m.invoke(param);
}
};

Object listener = Proxy.newProxyInstance(null,
new Class[] { java.awt.event.ActionListener.class }, handler);
Method adder = source.getClass().getMethod("addActionListener", ActionListener.class);
adder.invoke(source, listener);
}
}

注解没有处理机制的话和注释也没有太大的区别,实现注解一般会用到代理机制等,可以参考代理

3.3 注解语法

1
2
3
4
5
6
7
8
9
10
11
12
// 注解结构
modifers @interface AnnotationName {
elementDeclaration1
elementDeclaration2
}

// 元素声明
type elementName();
type elementName() default value;

// 注解调用
@AnnotationName(elementName1 = value1, elementName2 = value2, ...)

elementName1名字为value时可以忽略元素名和等号直接赋值,称为单值注解。所有的注解接口都隐式的扩展自 java.lang.annotation.Annotation 接口,其为正常接口,而非注解。所有的注解接口都无法再扩展,只能扩展自此接口。

注解接口和接口的一个区别就是:注解接口不用提供实现类,而是通过代理机制来生产代理类和对象。

注解元素就是方法声明,其类型只能为:基本类型,String,Class,enum,注解类型,元素为前者的数组。

注解内可以含有:包,类(包括enum),接口(包括注解接口),方法,构造器,实例域(包括enum常量),局部变量,参数变量。

3.4 元注解

注解接口 应用场合 作用
Deprecated 全部 将项标记为过时的
SuppressWarnings 除了包和注解以外的所有情况 阻止某个给定类型的警告信息
Override 方法 检查该方法是否覆盖了某个超类方法
PostConstruct 方法 被标记的方法应该在构造之后或移除之前立即被调用
PreDestroy
Resource 类、接口、方法、域 在类或接口上:标记为在其它地方要用到的资源。在方法或域上:为”注入”标记
Resources 类、接口 一个资源数组
Generated 全部 供代码生成工具使用
Target 注解 指明可以应用这个注解的哪些项
Retention 注解 指明此注解可以保留多久
Documented 注解 指明此注解应该包含在注解项的文档中
Inherited 注解 指明当这个注解应用于一个类时,能够自动被子类继承

用于编译时的注解:@Deprecated@SuppressWarnings@Override@Generated

用于管理资源的注解:@PostConstruct@Resource

3.5 自定义注解

3.6 源码级注解处理

可以通过注解来自动生成一些代码,如实现Java的Bean信息类,可以通过注解@Property来标记属性的获取器和设置器。

1
2
3
4
5
6
7
8
9
@Property
String getTitle(){
return title;
}

@Property(editor="TitlePositionEditor")
void setTitlePosition(int p){
titlePosition = p;
}

为了能自动生成BeanClass信息类,需要完成下列任务:

  1. 编写一个源文件BeanClassBeanInfo.java,继承SimpleBeanInfo,覆盖getPropertyDescriptors()方法。
  2. 对于被注解的方法,去掉get和set前缀,小写化剩余部分可以恢复属性名。
  3. 对于每个属性编写一条用于构建PropertyDescriptor的语句。
  4. 若此属性具有一个编辑器,则要编写一个方法调用setPropertyEditorClass。
  5. 编写代码返回一个包含所有属性描述符的数组。

通过一个实例实现@Property对应的注解处理器BeanInfoAnnotationProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
@SupportedAnnotationTypes("annotation.Property")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BeanInfoAnnotationProcessor extends AbstractProcessor {
/**
* 处理注解声明
* @param annotations 本轮要处理的注解集
* @param rounEnv 包含当前处理轮次的有关信息的RoundEnv引用
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment rounEnv) {
for (TypeElement t : annotations){//迭代遍历注解过的方法
Map<String,Property> props = new LinkedHashMap<>();
String beanClassName = null;
for(Element e : rounEnv.getElementsAnnotatedWith(t)){
String mname = e.getSimpleName().toString();
String[] prefixes = {"get","set","is"};//剥离定义的前缀
boolean found = false;
for(int i = 0; !found && i < prefixes.length;i++){
if(mname.startsWith(prefixes[i])){
found = true;
int start = prefixes[i].length();
String name = Introspector.decapitalize(mname.substring(start));//转换小写,得到属性名
props.put(name,e.getAnnotation(Property.class));
}
}
if(!found){//未找到合法的注释属性
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Property must be applied to getXxx, setXxx, or isXxx method",e);
}else if(beanClassName == null){
beanClassName = ((TypeElement) e.getEnclosingElement()).getQualifiedName().toString();
}
}
try {
if(beanClassName != null){
writeBeanInfoFile(beanClassName,props);
}
}catch (IOException ex){
ex.printStackTrace();
}
}
return true;
}

private void writeBeanInfoFile(String beanClassName, Map<String,Property> props)throws IOException{//编写源文件,生成XxxBeanInfo,processingEnv可以访问各种处理服务
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(beanClassName + "BeanInfo");
PrintWriter out = new PrintWriter(sourceFile.openWriter());
int i = beanClassName.lastIndexOf(".");
if(i > 0){
out.print("package ");
out.print(beanClassName.substring(0,i));
out.println(";");
}
out.print("public class ");
out.print(beanClassName.substring(i + 1));
out.println("BeanInfo extends java.beans.SimpleBeanInfo");
out.println("{");
out.println(" public java.beans.PropertyDescriptor[] getPropertyDescriptors(){");
out.println(" try{");
for(Map.Entry<String,Property> e : props.entrySet()){
out.print(" java.beans.PropertyDescriptor ");
out.print(e.getKey());
out.println("Descriptor");
out.print(" = new java.beans.PropertyDescriptor(\"");
out.print(e.getKey());
out.print("\",");
out.print(beanClassName);
out.println(".class);");
String ed = e.getValue().editor().toString();
if(!ed.equals("")){
out.print(" ");
out.print(e.getKey());
out.print("Descriptor.setPropertyEditorClass(");
out.print(ed);
out.println(".class);");
}
}
out.println(" return new java.beans.PropertyDescriptor[]{");
boolean first = true;
for(String p : props.keySet()){
if(first) first = false;
else out.print(",");
out.println();
out.print(" ");
out.print(p);
out.print("Descriptor");
}
out.println();
out.println(" };");
out.println(" }");
out.println(" catch (java.beans.IntrospectionException e){");
out.println(" e.printStackTrace();");
out.println(" return null;");
out.println(" }");
out.println(" }");
out.println("}");
out.close();
}
}

被注解文件ChartBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ChartBean extends JComponent{
...

/**
* Gets the title property.
* @return the chart title.
*/
@Property
public String getTitle()
{
return title;
}

...
}

通过编译生成对应ChartBeanBeanInfo.java等文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>javac  -encoding UTF-8 annotation/BeanInfoAnnotationProcessor.java
>javac -XprintRounds -processor annotation.BeanInfoAnnotationProcessor annotation/ChartBean.java

循环 1:
输入文件: {annotation.ChartBean}
注释: [java.lang.Override, annotation.Property]
最后一个循环: false
循环 2:
输入文件: {annotation.ChartBeanBeanInfo}
注释: []
最后一个循环: false
循环 3:
输入文件: {}
注释: []
最后一个循环: true

注解会未标注的方法创建ChartBeanBeanInfo文件,并实现getPropertyDescriptors()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package annotation;
public class ChartBeanBeanInfo extends java.beans.SimpleBeanInfo
{
public java.beans.PropertyDescriptor[] getPropertyDescriptors(){
try{
java.beans.PropertyDescriptor titleDescriptor
= new java.beans.PropertyDescriptor("title",annotation.ChartBean.class);
return new java.beans.PropertyDescriptor[]{

titleDescriptor
};
}
catch (java.beans.IntrospectionException e){
e.printStackTrace();
return null;
}
}
}

3.7 字节码级注解处理

字节码级注解处理会一直存在于类文件中。

假如某类的hacode()方法有以下注解:

1
2
3
4
5
@Override
@LogEntry(logger = "global")
public int hashCode() {
return super.hashCode();
}

当此方法被调用时,会打印类似日志消息:

1
2
Aug 17, 2004 9:32:59 PM Item hashCode
FINER: ENTRY

为了实现此注解功能,需要完成下列任务:

  1. 加载类文件中的字节码。
  2. 定位所有的方法。
  3. 对于每个方法,检查其是不是有一个@LogEntry注解。
  4. 如果有,则在方法开始部分添加以下字节码。
1
2
3
4
5
ldc loggerName
invokestatic java/util/logging/Logger.getLogger:(Ljava/lang/String;)Ljava/util/logging/Logger;
ldc className
ldc methodName
invokevirtual java/util/logging/Logger.entering:(Ljava/lang/String;Ljava/lang/String;)V

BCEL即字节码工程类库,即可以处理类文件的特殊类库。ASM则是一个java字节码操纵框架,可以用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。

1
2
3
4
5
6
7
8
9
10
javac annotation/set/Item.java
javac -classpath .:asm-6.2.jar annotation/bytecodeAnnotations/EntryLogger
javap -c Item.java
javac -classpath .:asm-6.2.jar annotation/bytecodeAnnotations/EntryLogger set.Item

javac -classpath .:asm-6.2.jar annotation/bytecodeAnnotations/EntryLoggerAgent
jar

javac annotation/set/SetTest.java
java -javaagent:annotation/bytecodeAnnotations/EntryLoggingAgent.jar=set.Item -classpath .:asm-6.2.jar set.SetTest

在加载时修改字节码,Java5以后可以通过代理的方式,通过监视程序运行,检验后调用类转换器修改字节码,将结果直接返回给JVM加载,即即使字节码修改


参考:

🔗《Java语言规范》

🔗《Java核心技术 卷Ⅰ》

🔗《Java核心技术 卷Ⅱ》