概述
注解(Annotation)在JDK1.5之后增加的一个新特性,注解的引入意义很大,有很多非常有名的框架,比如Hibernate、Spring,Android新推出的AAC/Room等框架中都大量使用注解。注解作为程序的元数据嵌入到程序。注解可以被解析工具或编译工具解析,此处注意注解不同于注释(comment)。
当一个接口直接继承java.lang.annotation.Annotation接口时,仍是接口,而并非注解。要想自定义注解类型,只能通过@interface关键字的方式,其实通过该方式会隐含地继承.Annotation接口。
API 摘要
所有与Annotation相关的API摘要如下:
注解类型(Annotation Types) API
| 注解类型 | 含义 |
|---|---|
| Documented | 表示含有该注解类型的元素(带有注释的)会通过javadoc或类似工具进行文档化 |
| Inherited | 表示注解类型能被自动继承 |
| Retention | 表示注解类型的存活时长 |
| Target | 表示注解类型所适用的程序元素的种类 |
1. Documented
源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
表示拥有该注解的元素可通过javadoc此类的工具进行文档化。该类型应用于注解那些影响客户使用带注释(comment)的元素声明的类型。如果类型声明是用Documented来注解的,这种类型的注解被作为被标注的程序成员的公共API。
例如,上面源码@Retention的定义中有一行@Documented,意思是指当前注解的元素会被javadoc工具进行文档化,那么在查看Java API文档时可查看当该注解元素。
2. Inherited
源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
@Inherited:表示该注解类型被自动继承,如果用户在当前类中查询这个元注解类型并且当前类的声明中不包含这个元注解类型,那么也将自动查询当前类的父类是否存在Inherited元注解,这个动作将被重复执行知道这个标注类型被找到,或者是查询到顶层的父类。@Inherited对字段,子类对于继承的方法才能通过反射获取注解,但是对于子类重写父类的方法是无法获取父类的注解的。,如果要查看子类中父类的字段,需要从子类中通过Clazz.getSuper()获取到父类,在获取对应的字段及其注解
3. Retention
源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
@Retention:表示该注解类型的注解保留的时长。当注解类型声明中没有@Retention元注解,则默认保留策略为RetentionPolicy.CLASS。关于保留策略(RetentionPolicy)是枚举类型,共定义3种保留策略,如下表:
| RetentionPolicy | 含义 |
|---|---|
| SOURCE | 仅存在Java源文件,经过编译器后便丢弃相应的注解 |
| CLASS | 存在Java源文件,以及经编译器后生成的Class字节码文件,但在运行时VM不再保留注释 |
| RUNTIME | 存在源文件、编译生成的Class字节码文件,以及保留在运行时VM中,可通过反射性地读取注解 |
例如,上面源码@Retention的定义中有一行@Retention(RetentionPolicy.RUNTIME),意思是指当前注解的保留策略为RUNTIME,即存在Java源文件,也存在经过编译器编译后的生成的Class字节码文件,同时在运行时虚拟机(VM)中也保留该注解,可通过反射机制获取当前注解内容。
4. Target
源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
@Target:表示该注解类型的所使用的程序元素类型。当注解类型声明中没有@Target元注解,则默认为可适用所有的程序元素。如果存在指定的@Target元注解,则编译器强制实施相应的使用限制。关于程序元素(ElementType)是枚举类型,共定义8种程序元素,如下表:
| ElementType | 含义 |
|---|---|
| ANNOTATION_TYPE | 注解类型声明 |
| CONSTRUCTOR | 构造方法声明 |
| FIELD | 字段声明(包括枚举常量) |
| LOCAL_VARIABLE | 局部变量声明 |
| METHOD | 方法声明 |
| PACKAGE | 包声明 |
| PARAMETER | 参数声明 |
| TYPE | 类、接口(包括注解类型)或枚举声明 |
例如,上面源码@Target的定义中有一行@Target(ElementType.ANNOTATION_TYPE),意思是指当前注解的元素类型是注解类型。
枚举(Enum) API
| 枚举 | 含义 |
|---|---|
| ElementType | 程序元素类型,用于Target注解类型 |
| RetentionPolicy | 注解保留策略,用于Retention注解类型 |
异常和错误 API
| 异常/错误 | 含义 |
|---|---|
| AnnotationTypeMismatchException | 当注解经过编译(或序列化)后,注解类型改变的情况下,程序视图访问该注解所对应的元素,则抛出此异常 |
| IncompleteAnnotationException | 当注解经过编译(或序列化)后,将其添加到注解类型定义的情况下,程序视图访问该注解所对应的元素,则抛出此异常。 |
| AnnotationFormatError | 当注解解析器试图从类文件中读取注解并确定注解出现异常时,抛出该错误 |
内建注解
Java提供了多种内建的注解,下面接下几个比较常用的注解:@Override、@Deprecated、@SuppressWarnings这3个注解。
- @Override(覆写)
源码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
用途:用于告知编译器,我们需要覆写超类的当前方法。如果某个方法带有该注解但并没有覆写超类相应的方法,则编译器会生成一条错误信息。
注解类型分析:@Override可适用元素为方法,仅仅保留在java源文件中。
- @Deprecated(不赞成使用)
源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
用途:用于告知编译器,某一程序元素(比如方法,成员变量)不建议使用时,应该使用这个注解。Java在javadoc中推荐使用该注解,一般应该提供为什么该方法不推荐使用以及相应替代方法。
注解类型分析: @Deprecated可适合用于除注解类型声明之外的所有元素,保留时长为运行时VM。
- @SuppressWarnings(压制警告)
源码:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
用于:用于告知编译器忽略特定的警告信息,例在泛型中使用原生数据类型。
注解类型分析: @SuppressWarnings可适合用于除注解类型声明和包名之外的所有元素,仅仅保留在java源文件中。
该注解有方法value(),可支持多个字符串参数,例如:
@SupressWarning(value={"uncheck","deprecation"})
前面讲的@Override,@Deprecated都是无需参数的,而压制警告是需要带有参数的,可用参数如下:
| 参数 | 含义 |
|---|---|
| deprecation | 使用了过时的类或方法时的警告 |
| unchecked | 执行了未检查的转换时的警告 |
| fallthrough | 当Switch程序块进入进入下一个case而没有Break时的警告 |
| path | 在类路径、源文件路径等有不存在路径时的警告 |
| serial | 当可序列化的类缺少serialVersionUID定义时的警告 |
| finally | 任意finally子句不能正常完成时的警告 |
| all | 以上所有情况的警告 |
3.4 对比
3种内建注解的对比:
| 内建注解 | Target | Retention |
|---|---|---|
| Override | METHOD | SOURCE |
| SuppressWarnings | 除ANNOTATION_TYPE和PACKAGE外的所有 | SOURCE |
| Deprecated | 除ANNOTATION_TYPE外的所有 | RUNTIME |
注解解析
接下来,通过反射技术来解析自定义注解@AuthorAnno,关于反射类位于包java.lang.reflect,其中有一个接口AnnotatedElement,该接口定义了注释相关的几个核心方法,如下:
| 返回值 | 方法 | 解释 |
|---|---|---|
| T | getAnnotation(Class annotationClass) | 当存在该元素的指定类型注解,则返回相应注释,否则返回null |
| Annotation[] | getAnnotations() | 返回此元素上存在的所有注解 |
| Annotation[] | getDeclaredAnnotations() | 返回直接存在于此元素上的所有注解。 |
| boolean | isAnnotationPresent(Class annotationClass) | 当存在该元素的指定类型注解,则返回true,否则返回false |
注解实战
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Age {
int value() default 0;//如果不添加默认值的话,在使用的时候就必须要设置默认值
}
@IntDef({1,5,10})//建议值
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})//可以作用于变量,方法,参数列表
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Level {
int value();//没有设置默认值,所以使用的时候需要设置值
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Gender {
Type value() default Type.Male;
enum Type {
Male("男"),
Female("女"),
Other("中性");
private String genderStr;
Type(String arg0) {
this.genderStr = arg0;
}
@Override
public String toString() {
return genderStr;
}
}
}
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Job {
String name();
boolean isManager();
}
@Documented
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
String value() default "none";
}
使用注解:
public abstract class Person {
@Name()
protected String mName;
@Age(value = 10)
protected int mAge;
@Gender(Gender.Type.Female)
protected String mGender;
@Job (name = "Android程序员", isManager = false)
protected String mJob;
@Level(1)
protected int mLevel;
public String getName() {
return mName;
}
public void setName(@NonNull String name) {
mName = name;
}
public @Level(1) int getLevel() {
return mLevel;
}
public void setLevel(@Level(1) int level, @Age int age) {
mLevel = level;
}
public @Age int getAge() {
return mAge;
}
public void setAge(@Age int age) {
mAge = age;
}
@Age
public @Gender @Name String getGender(@Age(2) int age) {
return mGender;
}
public @Gender @Name @Job(name = "abcd", isManager = true) abstract String getAllInfo(@Level(10) @Age(30)int level);
public void setGender(String gender) {
mGender = gender;
}
public void setJob(String job) {
mJob = job;
}
public @Job(name = "anndroid", isManager = false) String getJob(@Level(10) int level) {
return mJob;
}
@Override
public String toString() {
return "name=" + mName + " age=" + mAge + " gender=" + mGender;
}
}
public class ChinesePersion extends Person {
@Override
public String getGender(int age) {
return super.getGender(age);
}
@Override
public String getAllInfo(int level) {
return null;
}
}
测试注解:
public class TestAnnotation {
public static void main(String[] args){
analyzeClass(Person.class);
System.out.println("\n");
analyzeClass(ChinesePersion.class);
}
public static void analyzeClass(Class cla){
System.out.println("##################AnalyzeClass " + cla.getName() + " ######################");
displayFileds(cla);
displayMedthods(cla);
}
private static void displayMedthods(Class cla) {
Method[] methods = cla.getMethods();
for(Method method : methods){
Annotation[] methodAnnotations = method.getAnnotations();
Annotation[][] paramAnotations = method.getParameterAnnotations();
if(methodAnnotations.length == 0 && paramAnotations.length == 0){
continue;
}
System.out.println( "============================Analyze methodName=" + method.getName() + "================");
displayMethodAnotations(method);
displayParamAnotations(method);
}
}
//打印字段注解
private static void displayFileds(Class cla) {
System.out.println( "====================DisplayFields========================");
//只能获取当前类定义的字段,父类的不能获取,如果要获取父类字段的需要Clazz.getSuper()获取到父类,在获取其对应字段
Field fields[] = cla.getDeclaredFields();
for(Field field : fields){
if(field.isAnnotationPresent(Name.class)){
Name name = field.getAnnotation(Name.class);
System.out.println("name=" + name.value());
}
if(field.isAnnotationPresent(Age.class)){
Age age = field.getAnnotation(Age.class);
System.out.println("age=" + age.value());
}
if(field.isAnnotationPresent(Gender.class)){
Gender gender = field.getAnnotation(Gender.class);
System.out.println("gender=" + gender.value().toString());
}
if(field.isAnnotationPresent(Level.class)){
Level level = field.getAnnotation(Level.class);
System.out.println("level=" + level);
}
if(field.isAnnotationPresent(Job.class)){
Job job = field.getAnnotation(Job.class);
System.out.println("job name=" + job.name() + " isManager=" + job.isManager());
}
}
}
//打印参数注解
private static void displayParamAnotations(Method method) {
Annotation[][] paramAnotations = method.getParameterAnnotations();
if(paramAnotations != null && paramAnotations.length > 0){
for(Annotation[] annotations : paramAnotations){
if(annotations == null || annotations.length == 0){
continue;
}
for(Annotation annotation : annotations){
System.out.println("*****Param Annotation=" + annotation);
}
}
}
}
//打印方法注解,只能获取到继承父类的方法的注解,不能获取到重写父类的方法的注解
private static void displayMethodAnotations(Method method) {
Annotation[] anotations = method.getAnnotations();
if(anotations != null && anotations.length > 0){
for(Annotation annotation: anotations){
if(annotation instanceof Job){
Job job = (Job) annotation;
System.out.println("-----Method Annotation:job name=" + job.name() + " isManager=" + job.isManager());
}
if(annotation instanceof Name){
Name name = (Name) annotation;
System.out.println("-----Method Annotation:name=" + name.value());
}
if(annotation instanceof Age){
Age age = (Age) annotation;
System.out.println("-----Method Annotation:age=" + age.value());
}
if(annotation instanceof Gender){
Gender gender = (Gender) annotation;
System.out.println("-----Method Annotation:gender=" + gender.value().toString());
}
if(annotation instanceof Level){
Level level = (Level) annotation;
System.out.println("-----Method Annotation:level=" + level);
}
}
}
}
}
输出:
##################AnalyzeClass com.example.administrator.mannotation.Person ######################
====================DisplayFields========================
name=none
age=10
gender=女
job name=Android程序员 isManager=false
level=@com.example.administrator.mannotation.Level(value=1)
============================Analyze methodName=setName================
============================Analyze methodName=getLevel================
-----Method Annotation:level=@com.example.administrator.mannotation.Level(value=1)
============================Analyze methodName=setLevel================
*****Param Annotation=@com.example.administrator.mannotation.Level(value=1)
*****Param Annotation=@com.example.administrator.mannotation.Age(value=0)
============================Analyze methodName=getAge================
-----Method Annotation:age=0
============================Analyze methodName=setAge================
*****Param Annotation=@com.example.administrator.mannotation.Age(value=0)
============================Analyze methodName=getGender================
-----Method Annotation:age=0
-----Method Annotation:gender=男
-----Method Annotation:name=none
*****Param Annotation=@com.example.administrator.mannotation.Age(value=2)
============================Analyze methodName=getAllInfo================
-----Method Annotation:gender=男
-----Method Annotation:name=none
-----Method Annotation:job name=abcd isManager=true
*****Param Annotation=@com.example.administrator.mannotation.Level(value=10)
*****Param Annotation=@com.example.administrator.mannotation.Age(value=30)
============================Analyze methodName=setGender================
============================Analyze methodName=setJob================
============================Analyze methodName=getJob================
-----Method Annotation:job name=anndroid isManager=false
*****Param Annotation=@com.example.administrator.mannotation.Level(value=10)
============================Analyze methodName=wait================
============================Analyze methodName=wait================
============================Analyze methodName=equals================
##################AnalyzeClass com.example.administrator.mannotation.ChinesePersion ######################
====================DisplayFields========================
============================Analyze methodName=getGender================
============================Analyze methodName=getAllInfo================
============================Analyze methodName=setName================
============================Analyze methodName=getLevel================
-----Method Annotation:level=@com.example.administrator.mannotation.Level(value=1)
============================Analyze methodName=setLevel================
*****Param Annotation=@com.example.administrator.mannotation.Level(value=1)
*****Param Annotation=@com.example.administrator.mannotation.Age(value=0)
============================Analyze methodName=getAge================
-----Method Annotation:age=0
============================Analyze methodName=setAge================
*****Param Annotation=@com.example.administrator.mannotation.Age(value=0)
============================Analyze methodName=setGender================
============================Analyze methodName=setJob================
============================Analyze methodName=getJob================
-----Method Annotation:job name=anndroid isManager=false
*****Param Annotation=@com.example.administrator.mannotation.Level(value=10)
============================Analyze methodName=wait================
============================Analyze methodName=wait================
============================Analyze methodName=equals================
查看类的注解
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
String value() default "none";
}
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Name
public @interface Age {
int value() default 0;//如果不添加默认值的话,在使用的时候就必须要设置默认值
}
@Name
public abstract class Person {
@Name()
protected String mName;
@Age(value = 10)
protected int mAge;
@Gender(Gender.Type.Female)
protected String mGender;
@Job (name = "Android程序员", isManager = false)
protected String mJob;
@Level(1)
protected int mLevel;
public String getName() {
return mName;
}
public void setName(@NonNull String name) {
mName = name;
}
public @Level(1) int getLevel() {
return mLevel;
}
public void setLevel(@Level(1) int level, @Age int age) {
mLevel = level;
}
public @Age int getAge() {
return mAge;
}
public void setAge(@Age int age) {
mAge = age;
}
@Age
public @Gender @Name String getGender(@Age(2) int age) {
return mGender;
}
public @Gender @Name @Job(name = "abcd", isManager = true) abstract String getAllInfo(@Level(10) @Age(30)int level);
public void setGender(String gender) {
mGender = gender;
}
public void setJob(String job) {
mJob = job;
}
public @Job(name = "anndroid", isManager = false) String getJob(@Level(10) int level) {
return mJob;
}
@Override
public String toString() {
return "name=" + mName + " age=" + mAge + " gender=" + mGender;
}
}
@Age
public class ChinesePersion extends Person {
@Override
public String getGender(int age) {
return super.getGender(age);
}
@Override
public String getAllInfo(int level) {
return null;
}
}
public class TestAnnotation {
public static void main(String[] args){
displayClassAnnotation(Name.class);
System.out.println("\n");
displayClassAnnotation(Age.class);
System.out.println("\n");
displayClassAnnotation(Person.class);
System.out.println("\n");
displayClassAnnotation(ChinesePersion.class);
}
private static void displayClassAnnotation(Class cla) {
//getAnnotations能拿到所有的注解包括父类,getDeclaredAnnotations只能拿到当前直接类的注解
Annotation annotations[] = cla.getAnnotations();
for(Annotation annotation : annotations){
System.out.println(annotation);
}
}
}
输出
@java.lang.annotation.Documented()
@java.lang.annotation.Inherited()
@java.lang.annotation.Target(value=[FIELD, METHOD, ANNOTATION_TYPE, TYPE])
@java.lang.annotation.Retention(value=RUNTIME)
@java.lang.annotation.Target(value=[FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE])
@java.lang.annotation.Retention(value=RUNTIME)
@java.lang.annotation.Inherited()
@com.example.administrator.mannotation.Name(value=none)
@com.example.administrator.mannotation.Name(value=none)
@com.example.administrator.mannotation.Name(value=none)
@com.example.administrator.mannotation.Age(value=0)
用注解(IntDef/StringDef)替代枚举
public class TestAnnotation1 {
public static final int ONE = 1;
public static final int TWO = 2;
public static final int THREE = 3;
public static final String STR1 = "s1";
public static final String STR2 = "s2";
public static final String STR3 = "s3";
@IntDef({ONE, TWO, THREE})
@interface IntDataType{
}
@StringDef({STR1, STR2, STR3})
@interface StrDataType{
}
static class Data {
private int mDataI;
public String mDataS;
public void setDataI(@IntDataType int dataI) {
mDataI = dataI;
}
public void setDataS(@StrDataType String dataS) {
mDataS = dataS;
}
}
static void main(String[] args){
Data data = new Data();
data.setDataI(TestAnnotation1.ONE);//编译正确
data.setDataS(TestAnnotation1.STR3);//编译正确
data.setDataI(2);//编译报错
data.setDataS("s1");//编译报错
}
}
注解限制输入
在Java自带注解中,可以通过@IntRange,@FloatRange限制输入参数,
private int mAge;
public void setAge(@IntRange(from = 0, to = 100) int age){
mAge = age;
}
private double mPrice;
public void setAge(@FloatRange(from = 10.0f, to = 100.0f) double price){
mPrice = price;
}
private void test(){
setAge(-1);//编译报错
setAge(10);//编译通过
setPrice(101);//编译报错
setPrice(100.0);//编译通过
}
但是,如果代码如下,则编译正常:因为注解的归根结底只是起注释作用,无法去动态判定变量的值
private int mAge;
public void setAge(@IntRange(from = 0, to = 100) int age){
mAge = age;
}
private void test(){
setAge(calAge());//编译通过
}
private int calAge() {
return -100;
}
所以正确的使用姿势是:在方法体内也要做参数判断
private int mAge;
public void setAge(@IntRange(from = 0, to = 100) int age){
if(age < 0 || age > 100){
return;
}
mAge = age;
}
private void test(){
setAge(calAge());//编译通过
}
private int calAge() {
return -100;
}
Android 注解替代findViewById
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IdInject {
int value();
}
public class MainActivity extends AppCompatActivity {
private LinearLayout mRootLy;
@IdInject(R.id.annotation_btn)
private Button mAnnotationBtn;
@IdInject(R.id.rootly)
private LinearLayout mLinearLayout;
@IdInject(R.id.dyn_view)
private Button mDynAnnotationBtn;
...
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
testAnnotation();
}
private void testAnnotation() {
IdInjectHelper.inject(this);//通过注解获取view
mAnnotationBtn.setText("这是个注解的button");
Button button = new Button(this);
button.setId(R.id.dyn_view);//需要使用自定义viewID
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
button.setLayoutParams(params);
mLinearLayout.addView(button);
IdInjectHelper.inject(this, button.getId());//通过注解获取动态view
mDynAnnotationBtn.setText("这是个动态的注解button");
}
}
自定义viewID,为给view.setID()时候用:
<resources>
<item name="dyn_btn" type="id"></item>
<item name="dyn_view" type="id"></item>
</resources>
public class IdInjectHelper {
//获取所有注解字段对应view
public static void inject(Activity activity){
Class cla = activity.getClass();
Field fields[] = cla.getDeclaredFields();//获取到所有的字段
for(Field field : fields){
if(field.isAnnotationPresent(IdInject.class)){//获取指定注解的字段
try {
IdInject inject = field.getAnnotation(IdInject.class);
View view = activity.findViewById(inject.value());
field.setAccessible(true);
field.set(activity, view);//给对应view赋值
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
//动态获取某个id的字段
public static void inject(MainActivity activity, int id) {
Class cla = activity.getClass();
Field fields[] = cla.getDeclaredFields();
for(Field field : fields){
if(field.isAnnotationPresent(IdInject.class)){
try {
IdInject inject = field.getAnnotation(IdInject.class);
if(id == inject.value()){
View view = activity.findViewById(inject.value());
field.setAccessible(true);
field.set(activity, view);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}

本文深入讲解Java注解的原理及应用,包括注解类型、元注解、内置注解及其实战案例。此外还介绍了如何利用注解简化Android开发。
791

被折叠的 条评论
为什么被折叠?



