夯实 Java 基础 - 反射

夯实 Java 基础 - 反射

自嵌套 Fragment 懒加载文章至今已经已经一个星期过去了,说实话最近对于学习的热情有点衰减,也多是本身有点飘了,也有多是现实中的诱惑多了点,可是这是个很差的状态,必须调整本身向着目标继续前进。java

前言

本文将重拾 Java 基础中的反射知识,因为笔者是作移动端 Android 开发的,平常工做中反射用的少的能够拿手指头数过来。如今我所记得的上次使用它应该是在修改 TabLayout 的下划线宽度的时候。此次重拾反射这部分知识的主要缘由实际上是在注解和动态代理。相比反射来讲这二者在咱们平常使用的框架中比较常见,好比 EventBus 原理,ButterKnife 原理,Retrofit原理,甚至 AOP(面向切面编程) 在 Android 中的应用,都有这二者的身影。android

本文其实没有什么养分价值,和其余相关文章同样,本文更多的是在记录反射的 API,这么作更多多是为了之后遗忘了方便查阅。本文将从如下几个方面来记录:编程

  1. 反射是什么? 反射的意义?
  2. 反射的入口 - Class 类
  3. 反射的成员变量获取 - Field 类
  4. 反射的方法获取 - Method 类
  5. 反射的构造方法获取 - Constructor 类
  6. 反射的获取注解相关信息

反射机制

试着解释清楚为何须要反射是并不简单,这里涉及了一些 jvm 类加载机制,那么从先前说的修改 TabLayout 的下划线宽度提及,首先 TabLayout 是存在 SDK 中并非咱们定义的一个类,可是在使用中咱们遇到了要修改其内容的需求,这时候咱们经过反射在程序运行时获取了其内部私有变量 mTabStrip ,并修改了他的 LayoutParams。咱们知道咱们经过 tablayout.mTabStrip 是没法访问的,由于变量是私有的。数组

Field tabStrip = tablayout.getDeclaredField("mTabStrip");
tabStrip.setAccessible(true);
...
do reset padding LayoutParams operation
复制代码

固然这只是我遇到的一个使用场景,作 JavaWeb 的朋友应该对此有更深的了解。框架

那么这里记录下什么是反射的官方说法:jvm

经过反射,咱们能够在运行时得到程序或程序集中每个类型的成员和成员的信息。Java 反射机制能够动态地建立对象并调用其属性,即便这个对象的类型在编译期是未知的。函数

经过 Java 的反射机制咱们能够:post

  1. 在运行时判断任意一个对象所属的类;
  2. 在运行时构造任意一个类的对象;
  3. 在运行时获取任意一个类所具备的成员变量和方法,即便它是私有的;
  4. 在运行时调用任意一个对象的方法;

注意咱们所提到的前提和重点是运行时。学习

反射的入口 - Class 类

如何获取 Class 类

想要经过反射操做一个类的成员或者方法,Java 为咱们提供了一套 API 而这套 API 属于 Class 类,做为一些列反射操做的入口,Java 提供了三个方法或得到某个对象或者类的 Class 类对象:测试

  • 使用 Object 的 getClass 方法,如咱们已知一个类的对象咱们可使用超类的 getClass 方法获取:
TabLayout tabLayout = findViewById(R.id.tabLayout);
Class<?> tabLayoutClass = tabLayout.getClass();
复制代码
  • 咱们也能够经过 XXX.class 的方式直接获取某个类的 Class 对象而无需建立该类的对象。
//对于普通引用数据类型的类咱们能够以下调用
Class<?> tabLayoutClass = TabLayout.class;

//而对于基本数据类型咱们可使用 XXX.TYPE 的方式调用
Class<?> classInt = Integer.TYPE;
复制代码
  • **经过 Class 的静态方法 Class.forName(String name) 方法传入一个类的全量限定名得到建立。**该方法会要求抛出或者捕获ClassNotFoundException异常。
Class c3 = Class.forName("android.support.design.widget.TabLayout");
复制代码

经过反射获取目标的类的成员变量

Class 类能够帮助咱们在只知道一个类的名字的时候,获取该类的成员变量,及时某些成员变量是私有的。咱们假设只知道一个类的名字,并不知道其内部构成,也就是说内部没有 API 列表提供给咱们。Java Class 类提供了4种方法,来获取这些成员变量。

方法名称 返回值类型 是否包含得到继承的属性 是否能够得到私有属性
getField() Field YES NO
getDeclaredField() Field NO YES
getFields() Field[] YES NO
getDeclaredFields() Field[] NO YES
  • 测试 getField() / getDeclaredField()
Class<TopStudent> topStudentClass = TopStudent.class;
  
  //id public 属性定义在父类 Student 中
  Field id = topStudentClass.getField("id");
  //grade public 属性定义在 TopStudent 中
  Field grade = topStudentClass.getField("grade");
  //isReal private 属性定义在 TopStudent 中 没法得到私有属性将抛出NoSuchFieldException: isReal
  Field isReal = topStudentClass.getField("isReal");

  //id public 属性定义在父类 Student 中 没法得到 public 父类属性 java.lang.NoSuchFieldException: id
  Field  declaredId = topStudentClass.getDeclaredField("id");
  //grade public 属性定义在 TopStudent 中
  Field declaredGrade = topStudentClass.getDeclaredField("grade");
  //isReal private 属性定义在 TopStudent 中
  Field declaredIsReal = topStudentClass.getDeclaredField("isReal");
复制代码
  • 测试 getFields() / getDeclaredFields()
Field[] fields = topStudentClass.getFields();
  Field[] declaredFields = topStudentClass.getDeclaredFields();

  //fields = [public int Reflection.TopStudent.grade, public int Reflection.Student.id]
  System.out.println("fields = " + Arrays.toString(fields));

  // grade  id
  for (Field field : fields) {
      System.out.println("" + field.getName());
  }

  //declaredFields = [private boolean Reflection.TopStudent.isReal, public int Reflection.TopStudent.grade]
  System.out.println("declaredFields = " + Arrays.toString(declaredFields));
  //isReal grade
  for (Field field : declaredFields) {
      System.out.println("" + field.getName());
  }
复制代码

事实上咱们经过反射获取到属性之后,下一步多是要获取或者修改该属性的值,Field 类也为咱们准备了相应的 set 和 get 方法。同时 set 方法做用于私有属性的时候将抛出 IllegalAccessException异常。此时咱们须要经过。

同时若是咱们预先不知道该属性的类型的时候咱们也能够经过 getType/getGenericType 来获取该属性的类型,后者在属性为泛型表示的属性时获取泛型的通用符号若是不是则返回值与 getType 内容相同。

TopStudent topStudent = topStudentClass.newInstance();

  Field grade = topStudentClass.getDeclaredField("grade");
  
  grade.set(topStudent, 4);
  //Can not set int field Reflection.TopStudent.grade to java.lang.Float
 // grade.set(topStudent,4.0f);
  System.out.println("grade = " + grade.get(topStudent));

  Class<?> type = grade.getType();
  Type genericType = grade.getGenericType();
  System.out.println("type = " + type);
  System.out.println("genericType = " + genericType);
  //若是咱们知道对应的变量为基本类型变量的某个类型可使用 setXXX 的等价方法
   grade.setInt(topStudent,4);
  //Can not set int field Reflection.TopStudent.grade to (float)4.0
  //grade.setFloat(topStudent,4);


 //再给私有属性设置值的时候要记得设置 isAccessible 为 true
  Field isReal = topStudentClass.getDeclaredField("isReal");
  isReal.setAccessible(true);
  // 若是不设置isReal.setAccessible(true);
  // 将会抛出 can not access a member of class Reflection.TopStudent with modifiers "private"异常
  isReal.set(topStudent, true);
  boolean isRealValue = (boolean) isReal.get(topStudent);
  System.out.println("isRealValue = " + isRealValue);

  int gradeValue = grade.getInt(topStudent);
  System.out.println("gradeValue  " + gradeValue);
复制代码

自动装箱致使的 IllegalArgumentException 异常

值得注意的是咱们当我反射的类某个属性为基本数据类型的包装类的时候,咱们没法使用 setXXX 直接设置该数值,将抛出java.lang.IllegalArgumentException ,使用 set(Object obj,Object val) 则能够直接运行,缘由在于 setInt 等方法没法为咱们作自动装箱的操做,然后者则能够:

// 测试 自动拆箱装箱
  Field code = topStudentClass.getField("code");
  //装箱成功
  code.set(topStudent,100);
  //没法自动装箱 Can not set java.lang.Integer field Reflection.Student.code to (int)200
  code.setInt(topStudent,200);
  int codeVal = (int) code.get(topStudent);
  System.out.println("codeVal = " + codeVal);
复制代码

对于 final 修饰的变量改如何修改

从代码编写角度来看,若是咱们将一个成员变量定义为 final 表明咱们不但愿有人能够修改它的值,可是实际的需求谁又能考虑到这里多呢?好在经过反射咱们也能够修改某个 final 成员变量。固然须要注意的地方比较多。

对于 Java 基本数据类型 以及用使用 String str = "111" 赋值的成员变量,在编译期 JVM 对其作了内联优化,能够简单的理解为编译后就写死在.class 文件中了,咱们并不能修改为功 final 的成员变量。

对于非上述两种状况是能够修改为功的

//测试 set final

public class Student {
    public final int id  = 30;
    public final Integer cod  = 90;
    public static final int ID = 1000;
    ...
}

Field id = topStudentClass.getField("id");
// 若是不设置 setAccessible(true) 将抛出 IllegalAccessException
// 设置 settAccessible 将绕过检查
id.setAccessible(true);
id.set(topStudent,100);
int idVal = (int) id.get(topStudent);
System.out.println("idVal = " + idVal);//100
System.out.println("idVal = " + topStudent.id);//30 修改失败

Field code = topStudentClass.getField("code");
code.setAccessible(true);
code.set(topStudent,100);
int codeVal = (int) code.get(topStudent);
System.out.println("codeVal = " + codeVal);//100
System.out.println("codeVal = " + topStudent.code);//100 修改为功


Field ID = topStudentClass.getField("ID");

//即便设置了setAccessible(true) 也会抛出 IllegalAccessException
//ID.setAccessible(true);

//经过反射将对应成员变量的 final 修饰去掉
Field modifiersField = Field.class.getDeclaredField("modifiers"); 
modifiersField.setAccessible(true);
modifiersField.setInt(ID, ID.getModifiers() & ~Modifier.FINAL); 

ID.set(topStudent,100);
int IDVal = (int) id.get(topStudent);
System.out.println("IDVal = " + IDVal);//100
System.out.println("IDVal = " + TopStudent.ID);//1000 修改失败
复制代码

结论,没事不要乱改 final 万一给后人给本身挖了个大坑埋了呢。

经过反射获取目标类的成员方法

获取目标类的成员方法

除了经过 Class 获取成员变量,经过反射也能够获取一个类的全部成员方法。与后去成员变量同样,获取 成员方法也有 4 个方法:

方法名称 返回值类型 是否包含得到父类方法 是否能够得到私有方法
getMethod() Method YES NO
getDeclaredMethod() Method NO YES
getMethods() Method[] YES NO
getDeclaredMethods() Method[] NO YES

假设咱们设置两个类它们以下:

public class Student {
    .....
    private String name;
    .....
    
    public String getName() {
        System.out.println("我是 Student 的  public 方法");
        return name;
    }

    private void testPrivate(){
        System.out.println("我是 Student 的 private 方法");
    }
}

public class TopStudent extends Student {
    private boolean isReal;
    public int grade;

    public boolean isReal() {
        System.out.println("我是 TopStudent 的 public 方法");
        return isReal;
    }

    private void testSelfPrivate(){
        System.out.println("我是 TopStudent 的 private 方法");
    }
}

复制代码

咱们尝试用 getMethods()/getDeclaredMethods() 获取 TopStudent 类所包含的方法:

private void testGetMethod() {
   Class<TopStudent> topStudentClass = TopStudent.class;
   Method[] methods = topStudentClass.getMethods();
   Method[] declaredMethods = topStudentClass.getDeclaredMethods();

   for (Method method: methods) {
       System.out.println(method.getName());
   }

   System.out.println("---------");

   for (Method method: declaredMethods) {
       System.out.println(method.getName());
   }
}
复制代码

从打印结果能够看出 getMethods() 方法获取的 method 包含全部父类和当前类的全部 Public 方法,而 getDeclaredMethods() 获取的 method 仅包含当前类的全部方法。咦好像没有办法获取父类的 private 方法的途径,什么? 子类根本就没法继承父类的私有方法好伐。

/*topStudentClass.getMethods 获取的 method 包含全部父类和当前类的全部 Public 方法*/

public boolean Reflection.TopStudent.isReal()
public java.lang.String Reflection.Student.getName()
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()

/*topStudentClass.getDeclaredMethods 获取的 method 仅包含当前类的全部方法*/

boolean Reflection.TopStudent.isReal()
private void Reflection.TopStudent.testSelfPrivate()方法*/

复制代码

一样咱们还能够获取某个类的单个方法经过 Class 提供给咱们的 getMethod 和 getDeclaredMethod 这两个方法都带有两个参数,第一个参数为方法名 "name",第二个参数为对应方法须要 传入的参数的Class 对象 即 "Class<?>... parameterTypes"。当咱们尝试获取一个并不存在的方法时,将会抛出NoSuchMethodException 异常。

咱们为 TopStudent 添加两个方法用于测试

public void testParams(String p1,int p2){}

public void testParams(double p){}
复制代码
try {
       // getMethod 能够正常获取本身以及父类的公有方法
       Method isRealMethod = topStudentClass.getMethod("isReal");
       Method getNameMethod = topStudentClass.getMethod("getName");

       // 尝试获取私有方法将抛出 java.lang.NoSuchMethodException 异常
       Method testSelfPrivateMethod = topStudentClass.getMethod("testSelfPrivate");
       Method testPrivateMethod = topStudentClass.getMethod("testPrivate");


       //尝试获取父类的方法 将抛出 NoSuchMethodException 异常
       Method getNameDeclareMethod = topStudentClass.getDeclaredMethod("getName");

       // getDeclaredMethod 能够获取私有方法将抛出 以及公有方法
       Method isRealDeclareMethod = topStudentClass.getDeclaredMethod("isReal");
       Method testSelfPrivateDeclareMethod = topStudentClass.getDeclaredMethod("testSelfPrivate");
        
        //重载方法的测试
        Method testParams1 = topStudentClass.getMethod("testParams", double.class);
       Method testParams2 = topStudentClass.getMethod("testParams", String.class, int.class);
       //获取并不存在的重载方法 将抛出 java.lang.NoSuchMethodException
       Method testParams3 = topStudentClass.getMethod("testParams");

   } catch (NoSuchMethodException e) {
       e.printStackTrace();
   }
复制代码

调用目标类的成员方法

因为咱们上文说过了 getMethod 和 getDeclaredMethod 方法的区别了,为了咱们正常获取对应的方法去掉用,咱们须要使用对应的方法。

咱们获取到了指定了 Class 的成员方法后能够经过 Method 的

Object invoke(Object obj, Object... args)

方法来调用指定类的对象的方法。第一个参数为该类的对象,第二个可变参数为该方法的参数,而返回值即所调用的方法的返回值,一般须要咱们强转为指定参数类型。而咱们还能够经过 Method 的 getReturnType 方法来获取返回值类型。

另外还须要注意的是,私有成员方法和私有变量同样,获取能够,可是当咱们须要访问修改的时候,必需要绕过权限检查即设置:method.setAccessible(true)

下面咱们来一个例子:

//为 TopStudent 添加 testParams 测重载方法
public String testParams(int p) {
   System.out.println("我是 TopStudent 的 testParams(int p) 方法 ," + " p = " + p);
   return String.valueOf(p * 100);
}

try {
       Class<TopStudent> topStudentClass = TopStudent.class;
       TopStudent topStudent = topStudentClass.newInstance();
       
       //调用 public 方法
       Method isRealDeclareMethod = topStudentClass.getDeclaredMethod("isReal");
       isRealDeclareMethod.invoke(topStudent);

       //调用私有方法必须绕过权限检查 即须要设置对应的 Method 对象的 setAccessible 属性为 true
       Method testSelfPrivateDeclareMethod = topStudentClass.getDeclaredMethod("testSelfPrivate");
       testSelfPrivateDeclareMethod.setAccessible(true);
       testSelfPrivateDeclareMethod.invoke(topStudent);
       
       Method testParams1 = topStudentClass.getMethod("testParams", double.class);

       //传入错误的参数类型将会抛出 IllegalArgumentException 异常
       //testParams1.invoke(topStudent,"200");
       testParams1.invoke(topStudent, 200);

       Method testParams2 = topStudentClass.getMethod("testParams", String.class, int.class);
       testParams2.invoke(topStudent, "测试", 200);

       Method testParams3 = topStudentClass.getMethod("testParams", int.class);
       Class<?> returnType = testParams3.getReturnType();
        //returnType = class java.lang.String
       System.out.println("returnType = " + returnType);
       String result = (String) testParams3.invoke(topStudent, 200);//result = 20000

       System.out.println("result = " + result);
       
  } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
       e.printStackTrace();
   }
复制代码

经过反射获取目标类的构造函数

经过反射获取构造函数的方法一样有4个,分别为

方法名称 返回值类型 是否能够得到私有方法
getConstructor() Constructor<?> NO
getDeclaredConstructor() Constructor<?> YES
getConstructors() Constructor<?>[] NO
getDeclaredConstructors() Constructor<?>[] YES

对于构造方法的获取这里没有指出是否能够得到父类的构造方法,由于 java 规定,子类没法继承父类的构造方法。而对于访问修饰符的限制,这里跟上文的普通成员函数没有什么区别。

如:

Class<TopStudent> topStudentClass = TopStudent.class;

   Constructor<?>[] constructors = topStudentClass.getConstructors();
   for (Constructor constructor: constructors) {
       System.out.println("constructor = " + constructor);
   }

   Constructor<?>[] declaredConstructors = topStudentClass.getDeclaredConstructors();
   for (Constructor constructor: declaredConstructors) {
       System.out.println("constructor = " + constructor);
   }
   
   
   try {
       Constructor<TopStudent> isRealConstructor = topStudentClass.getConstructor(boolean.class);
       System.out.println("isRealConstructor = " + isRealConstructor);

       Constructor<TopStudent> gradeConstructor = topStudentClass.getDeclaredConstructor(int.class);
       System.out.println("gradeConstructor = " + gradeConstructor);

       TopStudent topStudent = isRealConstructor.newInstance(false);
       System.out.println("topStudent.isReal = " + topStudent.isReal()); 
    }catch (NoSuchMethodException) {
        e.printStackTrace();
    }
       
复制代码

运行结果

constructor = public Reflection.TopStudent(boolean,int)
constructor = public Reflection.TopStudent(boolean)

constructor = public Reflection.TopStudent(boolean,int)
constructor = private Reflection.TopStudent(int)
constructor = public Reflection.TopStudent(boolean)

isRealConstructor = public Reflection.TopStudent(boolean)
gradeConstructor = private Reflection.TopStudent(int)

复制代码

而咱们以前说过经过 Class.newInstance() 能够建立一个类的对象,可是若是一个类并无提供空参数的构造方法,那么这个方法将抛出 InstantiationException 异常。此时咱们就能够经过获取其余参数构造函数的方法来得到对应的 Constructor 对象来调用 Constructor.newInstance(Object... obj)

此方法接受对应的构造函数的参数类型的对象,若是传递的参数个数以及类型错误将抛出IllegalArgumentException,相似于 invoke 方法所抛出的异常。

try {

        // 若是没有空构造函数,将抛出 InstantiationException 异常
        //  TopStudent topStudent = topStudentClass.newInstance();
       TopStudent topStudent = isRealConstructor.newInstance(false);
       System.out.println("topStudent.isReal = " + topStudent.isReal());

       //调用私有构造函数的时候必须把对应的 Constructor 设置为  setAccessible(true)
       gradeConstructor.setAccessible(true);
       TopStudent topStudent1 = gradeConstructor.newInstance(1000);
       System.out.println("topStudent.grade = " + topStudent1.grade);
       //传入错误的参数的个数的时候将会抛出 java.lang.IllegalArgumentException
       TopStudent errorInstance = isRealConstructor.newInstance(false, 100);


   } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
       e.printStackTrace();
   }
复制代码

经过 Class 作类型判断

在不使用反射的时候,咱们会用 instanceof 关键字来判断是否为某个类的实例。当咱们经过上面的方法获取了一个对象的 Class 对象,也能够Class对象的 isInstance() 方法来判断是否为某个类的实例,它是一个 Native方法,使用方法以下:

try {
       Class<Student> studentClass = Student.class;
       //java 中内部类的全路径命名是 OuterClassName$InnerClassName
       Class<?> tearcherClass = Class.forName("ReflectionTest$Teacher");

       Teacher teacher = new Teacher();
       //tearcherClass.isInstance(teacher) true 
       System.out.println("tearcherClass.isInstance(teacher)" + tearcherClass.isInstance(teacher));
   } catch (ClassNotFoundException e) {
       e.printStackTrace();
   }
复制代码

经过反射获取注解相关信息

开头提到,学习反射可能更大目的不是应用于平常开发,而是为了学习一些三方库的源码,而这些源码中,反射每每都是伴随着注解一块使用的,这篇文章咱们暂时不拿注解展开说,只简单的说下Annotation 相关的反射 API:

首先在 java.lang.reflect 包下有一个跟提取注解很是相关的接口,它就是 AnnotatedElement 接口,那么实现该接口的对象有哪些呢?其实它包含了上述咱们所说的 ClassConstructor、 FieldMethod几个类。其实了解注解能够修饰哪些成员的朋友对此并不难理解,注解能够修饰一个类,一个方法,一个成员,因此当咱们须要自定义注解的时候,若是拿到对应的成员或者类的注解即是关键。

AnnotatedElement 接口定义了一下几个方法:

方法名 参数 返回值 做用
isAnnotationPresent 注解修饰的元素的 Class boolean 检测该元素是否被参数对应注解修饰
getAnnotation 注解修饰的元素的 Class Annotation 返回注解对象
getAnnotations Annotation[] 返回该程序元素上存在的全部注解(若是没有注解存在于此元素上,则返回长度为零的一个数组。)
       
getDeclaredAnnotations Annotation[] 返回直接存在于此元素上的全部注解。与此接口中的其余方法不一样,该方法将忽略继承的注解。(若是没有注解直接存在于此元素上,则返回长度为零的一个数组。)

下面咱们经过一个简单的例子来了解下如何经过反射获取注解:

假设咱们有一个这样的自定义注解:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
protected @interface FruitName {
   String value();
}
复制代码

且咱们定义了一个 Apple:

public class Apple {
   @FruitName(value = "Apple")
   public String name;
}
复制代码

并有下列方法用来查看注解信息:

public static void getFruitInfo(Class<?> clazz) {
   try {
       String fruitName = "水果名称:";

       Field field = clazz.getDeclaredField("name");
       java.lang.annotation.Annotation[] annotations = field.getAnnotations();
       System.out.println("annotations = " + Arrays.toString(annotations));
       if (field.isAnnotationPresent(FruitName.class)) {
           FruitName fruitNameAnno = field.getAnnotation(FruitName.class);
           fruitName = fruitName + fruitNameAnno.value();
           System.out.println(fruitName);
       }
   } catch (NoSuchFieldException e) {
       e.printStackTrace();
   }
}
复制代码

获得打印结果为

annotations = [@Annotation$FruitName(value=Apple)]
水果名称:Apple
复制代码

总结

本文分析了一些常见的反射 API 的使用。这些并非所有的 API。网上也有不少其余的反射的讲解也都不错。本文出发点想要不同凡响,可是写着写着就"同流合污"了。 原本想说明反射中的泛型擦除,也想加上动态代理,虽然这两个知识点和反射有着很大的联系,可是两个均可独立成文。因此仅当此篇是一份学习记录,方便之后查阅吧。最后放出我所查看过一些不错的反射文章。

一些不错的反射介绍文章:

深刻解析Java反射(1) - 基础 Java 反射由浅入深 | 进阶必备 JAVA反射与注解 反射技术在android中的应用 细说反射,Java 和 Android 开发者必须跨越的坎 Thinking in JavaJava 核心技术卷 I

相关文章
相关标签/搜索