------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
一、反射的基本描述
Java反射机制是在运行状态中,对于任意一个类(class文件),都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象方法的功能称为java语言的反射机制。动态获取类中信息,就是java反射。可以理解为对类的解剖。如果想要对指定名称的字节码文件进行加载并获取其中的内容并调用,这时就使用到了反射技术。
简单一句话就是:反射就是把java类中的各种成分映射成相应的java类。
二、反射的基石---Class类
1、了解Class类
Java程序中把各个对象共性的东西向上抽取得到了Object类,同理各个java类属于同一类事物,我们向上抽取就能得到描述这类事物的Java类名就是Class。比如:描述很多人的时候我们会定义一个Person类,那么描述很多java类的时候我们就用Class类来描述众多的class类。Class是Java程序中各个Java类的总称;它是反射的基石,通过Class类来使用反射。
一个类被类加载器加载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码,不同的类的字节码是不同的,所以它们在内存中的内容是不同的,这一个个的空间可分别用一个个的对象来表示,这些对象显然具有相同的类型,这个类型就是Class类型。
用于描述字节码的类就是Class类,创建对象,可以提取字节码文件中的内容,如字段、构造函数、一般函数。该类就可以获取字节码文件中的所有内容,那么反射就是依靠该类完成的。想要对一个类文件进行解剖,只要获取到该类的字节码文件对象即可。java类的Class类提供一系列的方法来获得其中的变量,方法,构造函数,修饰符,包等信息,这些信息就是用相应类的实例对象来表示,它们就是Field(字段、成员变量)、Method(一般方法)、Constructor(构造函数)、Package(包)等等。
2、获取字节码对应的实例对象(Class类型)的三种方法
(1)、Class clazz=对象.getClass();
Class clazz=new Person().getClass();
这种方法是调用了Object类中的getClass()方法。这种方法每次都需要知道具体的类并且创建该类对象,以及调用getClass()方法。非常的繁琐和不利于后期程序的扩展性。
(2)、Class clazz=类名.class;
Class clazz=System.class;
这种方法相对简单只需要用到类名就可以了,因为任何数据类型都具备一个静态的属性.class来获取其对应的Class对象,但是还是要明确用到类中的静态成员。
(3)、Class clazz=Class.forName("包名.类名");
String className="cn.itheima.Person";Class clazz=Class.forName(className);
这种方法是最为常用的方法,该方法不需要创建对象,也不用调用对象的方法,只需要通过给定的类的字符串名称就可以获取该类。更为方便,扩展性更强。这种方法在以后的开发中非常的常见应该重点掌握。
注:1.这三种方法获取的字节码对象都是相同的,只存在一份。
2.int.class==Integer.TYPE ,boolean、byte、char、short、int、long、float 和 double都有对应的装饰类.TYPE。
3.所有具有相同元素类型和维数的数组都是一个 Class 对象。
4.基本的 Java 类型(boolean、byte、char、short、int、long、float和double)和关键字void也表示Class对象。
3、Class类中的常用方法
通过查看API帮助文档发现:Class类没有构造函数,所以不可以直接new创建对象,说明内部一定有可以返回本类对象的静态方法,这个方法就是forName()方法。方然上面的三种方法都可以获取到Class对象的。既然有了Class对象那么我们就要用其方法。
1 static Class<?> forName(String className);//返回本类Class对象 2 T newInstance();//创建此Class对象的无参 构造函数 3 boolean isPrimitive();//判断指定的Class对象是否是一个基本类型 4 boolean isArray();//判断此Class对象是否是一个数组类 5 Class<? super T> getSuperclass();//返回此Class类所表示的实体的父类Class 6 String getName();//返回Class对象所表示的实体String名称 7 String getSimpleName();//返回源代码中给出的底层类的简称 8 Package getPackage();//获取此类的包 9 int getModifiers();//返回此类或者接口的修饰符10 ClassLoader getClassLoader();//获取此类的类加载器11 Class<?>[] getClassess();//返回类中成员所有公共类和接口12 Class<?>[] getDeclaredClasses();//获取类的成员的所有类和接口13 Constructor<T> getConstructor(Class<?>...参数类型);//获取类中指定参数的公共(Public)构造函数14 Constructor<?>[] getConstructors();//返回类中所有公有构造函数15 Constructor<T> getDeclaredConstructor(Class<?>...参数类型);//获取带有指定参数列表(公共、保护、默认及私有)的构造函数16 Costructor<?>[] getDeclaredConstructors()//获取类中所有的构造函数17 Field getField(String name);//获取类中指定公有成员变量。name表示的是指定的所需成员变量名18 Field[] getFields();//获取类中所有公有的成员变量19 Field getDeclaredField(String name);//获取类中指定的成员变量20 Field[] getDeclaredFields();//获取类中所有成员变量21 Method getMethod(String name,Class<?>...参数类型);//获取类中指定公有成员方法,name代表方法名22 Method[] getMethods();//获取类中所有公有的成员方法23 Method getDeclaredMethod(String name,Class<?>...参数类型);//获取类中指定的成员方法24 Method[] getDeclaredMethods();//获取类中所有的成员方法。包括公共、保护、默认和私有方法,但不包括继承的方法。25 Class<?>[] getInterfaces();//获取全部接口26 InputStream getResourceAsStream(String name);//查找指定名称的资源
需求:用Class方法创建一个无参构造函数
//Person p=new Person();Person p=(Person) Class.forName("com.itheima.Person").newInstance();
三、获取Class中的构造函数(Constructor类)
以前我们创建对象先根据被new的类的名称找寻该类的字节码文件,并加载进内存,并创建该字节码文件对象,并接着创建该字节文件的对应的对象。如:Person p=new Person()。现在我们可以通过反射Class.forName(“类名”).getInstance()来创建无参的构造函数,但是这种方法有一个弊端,就是:假如类中没有无参构造函数怎么办或者创建指定的构造函数又该怎么办?这时候Class类中的newInstance都无法完成。这时候就必须先获取这个构造函数——Constructor。然后通过Constructor中的newInstance(Object ...参数列表)的方法创建指定的构造函数。
那么首先了解一下Constructor类的继承体系,以方便我们一下更好的学习。
java.lang.Object
|---java.lang.reflect.Array
|---java.lang.reflect.AccessibleObject
|---- java.lang.reflect.Constructor<T>
|---- java.lang.reflect.Field
|---- java.lang.reflect.Method
这里我们需要知道AccessibleObject类中有void setAccessible(boolean flag) 方法,当flag为true的时候可以对私有成员的访问取消权限检查,暴力访问。
1、Constructor类中的方法
1 String getName();//以字符串形式返回构造方法的名称2 T newInstance(Object...initargs);//创建实例对象3 void setAccessbie();//暴力访问私有构造方法4 Class<?>[] getParamterTypes();//获取对象所表示构造方法的形参类型5 int getModifiers();//获取构造方法的修饰符的整数形式
2、用Constructor对象创建对象的步骤
(1)、首先获取Class字节码对象
Class clazz=Class.forName(String className);
(2)、获取到字节码对象后调用其getDeclaredConstructor()方法获取Constructor对象
Constructor con=clazz.getDeclaredConstructor(String.class,int.class);
(3)、通过获取到的Constructor对象中的newInstance(Class<?>...参数类型)方法得到指定的构造函数
Object obj=con.newInstance("黑马",88);
注:
(1)、创建实例时newInstance方法中的参数列表必须与获取Constructor的方法getConstructor方法中的参数列表一致。
(2)、newInstance()构造出一个实例对象,每调用一次就构造一个对象。
(3)、利用Constructor类来创建类实例的好处是可以指定构造函数,而Class类只能利用无参构造函数创建类实例对象。
3、Constructor综合练习
(1)、需求:获取Person类中的构造函数
Person.java文件
1 package com.itheima; 2 public class Person 3 { 4 private String name; 5 private int age; 6 public static String country="CN"; 7 public Person(){ 8 System.out.println("Person run"); 9 }10 public Person(String name,int age){11 this.name=name;12 this.age=age;13 System.out.println("name="+name+",age="+age);14 }15 private Person(String name){16 this.name=name;17 System.out.println("Person run"+name);18 }19 private void method(String name,int age){20 System.out.println("name="+name+"..method run.."+"age="+age);21 }22 public void show(){23 System.out.println("Person show");24 }25 public static void staticMethod(){26 System.out.println("staticMethod run");27 }28 }
ConstructorDemo.java文件
1 mport java.lang.reflect.*; 2 import com.itheima.Person; 3 public class ConstructorDemo 4 { 5 public static void main(String[] args) throws Exception 6 { 7 String className="com.itheima.Person"; 8 constructor(className); 9 }10 public static void constructor(String className)throws Exception{11 //获取Class字节码文件对象12 Class clazz=Class.forName(className);13 14 //获取所有公有的构造函数:getConstructors()15 printLine("所有公共构造函数如下:");16 Constructor[] con1=clazz.getConstructors();17 printArray(con1);18 19 //获取所有的构造函数:getDeclaredConstructors()20 printLine("所有构造函数如下:");21 Constructor[] con2=clazz.getDeclaredConstructors();22 printArray(con2);23 24 //获取指定的构造函数25 printLine("指定公有构造函数如下:");26 Constructor con3=clazz.getConstructor(String.class,int.class);27 System.out.println(con3);28 }29 public static void printArray(Object[] arr){30 for (Object o:arr )31 {32 System.out.println(o);33 }34 }35 public static void printLine(String str){36 System.out.println();37 System.out.println("--"+str+"------------------"); 38 }39 }
运行结果:
(2)、需求:创建Person实例对象
ConstructorDemo.java文件
1 import java.lang.reflect.*; 2 import com.itheima.Person; 3 public class ConstructorDemo 4 { 5 public static void main(String[] args) throws Exception 6 { 7 String className="com.itheima.Person"; 8 createObj(className); 9 }10 public static void createObj(String className)throws Exception{ 11 Class clazz=Class.forName(className);12 13 //以前我们创建对象的方式:new Person(),现在我们用反射创建对象。14 15 //反射第一种方法,直接用字节码对象的newInstance()创建空参实例对象 16 Person p=(Person)Class.forName(className).newInstance();17 18 //第二种方法:获取Constructor对象,通过Constructor对象的newInstance()创建实例对象19 //newInstance()方法中的参数列表必须与获取Constructor的方法getConstructor()方法中的参数列表一致20 Constructor con= Class.forName(className).getConstructor();21 Person p1=(Person)con.newInstance();22 23 //上面的两种方法貌似都可以创建出来Person的实例对象,那么思考一下:如果创建指定参数的对象呢?24 //那么用字节码对象创建空参数的对象就无法完成了,这时候我们必须使用获取Constructor对象来创建对象了25 //Person p2=new Person("黑马",88);26 Constructor c=clazz.getConstructor(String.class,int.class);27 Person p2=(Person)c.newInstance("黑马",88);28 }29 }
运行结果:
四、获取Class的字段(Field类)
Field类代表某个类中一个成员变量.
1、Field类中的方法
1 Object get(Object obj);//获取指定对象上的字段值2 int getModifiers();//获取字段上的修饰符3 String getName();//获取字段的名称4 void set(Object obj,Object value);//将指定对象上的字段设置为指定的新值5 Class<?> getType();//获取字段自身的类型6 void setAccessible(true);//私有字段进行取消权限检查的能力,暴力访问
2、获取并修改指定成员变量的值的步骤
(1)、首先需要创建对象,因为获取和修改成员变量的值都需要明确修改的是哪个对象身上的
(2)、获取字节码对象,用其getDeclaredField(String name)方法来获取Field对象
(3)、通过Field对象的set(Object obj,Object value)来设置指定对象上的指定成员变量。
在设置之前一般都需要暴力访问,因为成员变量一般都是私有的。
(4)、通过Field对象中的get(Object obj)获取其身上的字段值。
3、Field综合练习
(1)、需求:获取Person类中的成员变量并修改
注:Person类因为和Constructor练习中的Person类一样,所以这里就不单独贴了。
FieldDemo.java文件
1 import java.lang.reflect.*; 2 import com.itheima.Person; 3 public class FieldDemo 4 { 5 public static void main(String[] args) throws Exception 6 { 7 String className="com.itheima.Person"; 8 field(className); 9 }10 public static void field(String className)throws Exception{11 //获取Class字节码文件对象12 Class clazz=Class.forName(className);13 14 //获取所有的成员变量15 printLine("所有成员变量如下:");16 Field[] f1=clazz.getDeclaredFields();17 printArray(f1);18 19 //获取指定成员变量20 printLine("指定成员变量如下:");21 Field f2=clazz.getDeclaredField("name");22 System.out.println(f2);23 24 //为对象的属性赋值,首先要有对象25 Person p=(Person)clazz.getConstructor(String.class,int.class).newInstance("黑马",88);26 //修改成员变量name的值27 Field f3=clazz.getDeclaredField("name");28 //取消访问检查,暴力访问私有成员变量29 f3.setAccessible(true);30 f3.set(p,"小明");31 //获取某对象的属性值 32 System.out.println("name="+f3.get(p));33 34 }35 public static void printArray(Object[] arr){36 for (Object o:arr )37 {38 System.out.println(o);39 }40 }41 public static void printLine(String str){42 System.out.println();43 System.out.println("--"+str+"------------------"); 44 }45 }
运行结果:
(2)、需求:将任意一个对象中的所有String类型的成员变量所对应的字符串内容中的"a"改成"b"。
1 import java.lang.reflect.*; 2 class Person 3 { 4 private String s1="com.itheima"; 5 private String s2="basketball"; 6 private String s3="我爱黑马"; 7 } 8 class FieldTest 9 {10 public static void main(String[] args) throws Exception11 {12 Class clazz=Class.forName("Person");
//创建对象13 Person p=(Person)clazz.newInstance();
//获取全部成员变量14 Field[] field=clazz.getDeclaredFields();
//遍历Field数组15 for(Field f:field){16 if(f.getType()==String.class){//如果字段的类型和String类型的字节码相同,则视为同一类型17 f.setAccessible(true);//暴力访问18 String oldValue=(String)f.get(p);19 f.set(d,oldValue.replace('a','b'));//String类型的成员变量中含有a的字符换成b字符20 System.out.println(f.get(p));21 }22 }23 }24 }
运行结果:
五、获取Class中的成员方法(Method类)
1 Object invoke(Object obj,Object...args);//调用方法
注:
(1)、如果传递给Method对象的invoke()方法的第一个参数为null,说明Method对象对应的是一个静态方法。
(2)、jdk1.5以上如果调用的这个方法没有参数列表Object...args填写null编译的时候会提示警告。
所以这里的Object...args可以用new Object[0],来抑制jvm编译时期的警告提示。
2、Method综合练习
(1)、获取Person类中的成员方法并调用
//Person类接Constructor里的Person类
1 import java.lang.reflect.*; 2 import com.itheima.Person; 3 public class MethodDemo 4 { 5 public static void main(String[] args) throws Exception 6 { 7 String className="com.itheima.Person"; 8 method(className); 9 }10 public static void method(String className)throws Exception{11 //获取Class字节码文件对象12 Class clazz=Class.forName(className);13 14 //获取所有的成员方法15 printLine("所有成员方法如下:");16 Method[] m1=clazz.getDeclaredMethods();17 printArray(m1);18 19 //获取指定成员方法20 printLine("指定成员方法如下:");21 Method m2=clazz.getDeclaredMethod("method",String.class,int.class);22 System.out.println(m2);23 24 //创建对象调用方法,Person p=new Person().method(String name,int age);25 System.out.println("hahhaha");26 Person p=(Person)clazz.newInstance();27 Method m3=clazz.getDeclaredMethod("method",String.class,int.class); 28 m3.setAccessible(true);29 m3.invoke(p,"黑马",66);30 31 //调用静态方法的时候不需要对象,因为静态优先于对象存在32 //Person.staticMethod();33 Method m4=clazz.getMethod("staticMethod",new Class[0]);34 m4.invoke(null,new Object[0]);35 /*36 警告: 最后一个参数使用了不准确的变量类型的 varargs 方法的非 varargs 调用;37 [javac] 对于 varargs 调用,应使用 java.lang.Object38 [javac] 对于非 varargs 调用,应使用 java.lang.Object[],这样也可以抑制此警告39 在jdk1.4下可以编译通过,但在1.5就不行。40 在调用方法的时候,如果方法的参数列表为null的时候就会有此提示。这是java的友好提示。41 解决方法:就是null的位置上创建空的数组对象即可。如:invoke(null,new Object[0]); 42 43 */44 45 }46 public static void printArray(Object[] arr){47 for (Object o:arr )48 {49 System.out.println(o);50 }51 }52 public static void printLine(String str){53 System.out.println();54 System.out.println("--"+str+"------------------"); 55 }56 }
此时会出现下面的问题:
启动Java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用这个main方法时,如何为invoke方法传递参数呢?按jdk1.5的语法,整个数组是一个参数,而按jdk1.4的语法,数组中的每个元素对应一个参数,当把一个字符串数组作为参数传递给invoke方法时,javac会到底按照哪种语法进行处理呢?jdk1.5肯定要兼容jdk1.4的语法,会按jdk1.4的语法进行处理,即把数组打散成为若干个单独的参数。所以,在给main方法传递参数时,不能使用代码mainMethod.invoke(null,new String[]{“xxx”}),javac只把它当作jdk1.4的语法进行理解,而不把它当作jdk1.5的语法解释,因此会出现参数类型不对的问题。
解决办法:
mainMethod.invoke(null,new Object[]{new String[]{"xxx"}});
mainMethod.invoke(null,(Object)new String[]{"xxx"});
这两种方式编译器会作特殊处理,编译时不把参数当作数组看待,也就不会数组打散成若干个参数了。
1 import java.lang.reflect.*; 2 class Demo 3 { 4 public static void main(String[] args){ 5 for(String s:args){ 6 System.out.println(s); 7 } 8 } 9 }10 class MainDemo 11 {12 public static void main(String[] args) throws Exception13 {14 //一般调用Demo类中的main()方法15 Demo.main(new String[]{"abc","123","我爱黑马"});16 System.out.println("------------------------");17 18 //通过反射方式根据用户提供的类名,去执行该类中的main方法。19 String className=args[0];20 Class clazz=Class.forName(className);21 Method m=clazz.getMethod("main",String[].class);22 //第一种方式:将数组打包,编译器拆包后就是一个String[]类型的整体23 m.invoke(null,new Object[]{new String[]{"abc","123","我爱黑马"}});24 //第二种方式:强制转换为父类Object,不用拆包 25 m.invoke(null,(Object)new String[]{"abc","123","我爱黑马"});26 }27 }
1 Array.getLength(Object obj);//获取数组的长度2 Array.get(Object obj,int x);//获取数组中的元素
代码演示:
1 import java.lang.reflect.*; 2 public class ArrayReflectDemo { 3 public static void main(String[] args)throws Exception{ 4 int[] arr1=new int[]{4,12,3,5,485,78}; 5 int[] arr2=new int[5]; 6 int[][] arr3=new int[3][4]; 7 String[] arr4=new String[]{"abc","haha","nihao"}; 8 System.out.println(arr1.getClass()==arr2.getClass());//true 9 // System.out.println(arr1.getClass()==arr3.getClass());//编译直接报错,arr1是数组类型,arr3可以看作Object[]类型10 // System.out.println(arr1.getClass()==arr4.getClass());//arr1是Object类型,arr4是Object[]类型11 System.out.println(arr1.getClass().getSuperclass());12 System.out.println(arr2.getClass().getSuperclass());13 System.out.println(arr3.getClass().getSuperclass());14 System.out.println(arr4.getClass().getSuperclass());15 Object obj1=arr1;16 Object obj2=arr2;17 Object[] obj3=arr3;18 Object[] obj4=arr4;//String数组中的元素属于Object19 // Object[] obj5=arr2;//int不是对象,int[]和String[]是对象.20 printArray(arr1);21 printArray(234343);22 }
//打印对象,如果是数组的遍历打印元素,如果不是直接打印23 public static void printArray(Object obj){24 if(obj.getClass().isArray()){//判断是否是数组25 int len=Array.getLength(obj);//获取数组的长度,得到循环的条件26 for(int x=0;x<len;x++){27 System.out.println(Array.get(obj, x));//获取数组元素28 }29 }else30 System.out.println(obj);31 }32 }
七、反射的作用----实现框架功能
什么是框架,例如,我们要写程序扫描.java文件中的注解,要解决哪些问题:读取每一样,在每一个中查找@,找到的@再去查询一个列表,如果@后的内容出现在了列表中,就说明这是一个我能处理和想处理的注解,否则,就说明它不是一个注解或者说至少不是一个我感兴趣和能处理的注解。接着就编写处理这个注解的相关代码。现在sun提供了一个apt框架,它会完成所有前期工作,只需要我们提供能够处理的注解列表,以及处理这些注解的代码。Apt框架找到我们感兴趣的注解后通知或调用我们的处理代码去处理。框架与框架要解决的核心问题
1、通过反射读取配置文件信息。
思路:
(1)、右击项目File命名一个配置文件:config.properties,然后写入配置信息。键值对:className=java.util.ArrayList。
(2)、 将文件读取到读取流中,要写出配置文件的绝对路径.
(3)、用Properties类的load()方法将流中的数据存入集合。
(4)、关闭流:关闭的是读取流,因为流中的数据已经加载进内存。
(5)、通过getProperty()方法获取className,即配置的值,也就是某个类名。
(6)、用反射的方式,创建对象newInstance()。
代码实现:
Person.java文件
1 package ReflectDemo; 2 3 public class Person { 4 private String name; 5 private int age; 6 public Person(String name, int age) { 7 this.name = name; 8 this.age = age; 9 }10 public String getName() {11 return name;12 }13 public void setName(String name) {14 this.name = name;15 }16 public int getAge() {17 return age;18 }19 public void setAge(int age) {20 this.age = age;21 }22 //复写中hashCode方法23 public int hashCode(){24 25 return name.hashCode()+age*37;26 }27 //复写了Object中equalss方法28 public boolean equals(Object obj){29 if(!(obj instanceof Person))30 throw new RuntimeException("类型转换异常");31 Person p=(Person)obj;32 return this.name.equals(p.name)&& this.age==p.age;33 }34 @Override35 public String toString() {36 return "Person =[age=" + age + ", name=" + name + "]";37 }38 }
RflectDemo.java文件
1 package com.itheima; 2 import java.util.*; 3 import java.io.*; 4 import java.lang.reflect.*; 5 public class ReflectDemo { 6 public static void main(String[] args)throws Exception { 7 //文件关联流对象,用字节流读取配置文件信息 8 FileInputStream in=new FileInputStream("config.properties"); 9 //创建Properties对象,用来操作配置文件信息10 Properties pro=new Properties();11 //把流中的数据加载到集合中12 pro.load(in);13 in.close();14 //获取集合中的键当做className15 String className=pro.getProperty("className");16 //用反射创建ArrayList对象用来存储Person对象17 Collection<Person> con=(ArrayList)Class.forName(className).newInstance();18 con.add(new Person("黑马",120));19 con.add(new Person("小明",50));20 con.add(new Person("王五",12));21 con.add(new Person("逗逗",40));22 con.add(new Person("赵四",80));23 con.add(new Person("王五",12));//ArrayList集合的特点是元素可重复,所以这个视为不相同的Person对象24 //遍历集合中的元素25 for(Person p:con){26 System.out.println(p);27 } 28 29 30 //原运行程序31 Collection<Person> collection=new HashSet<Person>();32 collection.add(new Person("黑马",120));33 collection.add(new Person("小明",50));34 collection.add(new Person("王五",12));35 collection.add(new Person("逗逗",40));36 collection.add(new Person("赵四",80));37 collection.add(new Person("王五",12));//因为Person复写了hashCode和equals方法,所以视为重复元素38 for(Person pe:collection){39 System.out.println(pe);40 }41 }42 43 }
config.properties文件
1 className=java.util.ArrayList
2、类加载器
类加载器是将.class的文件加载进内存,也可将普通文件中的信息加载进内存。
资源文件的加载:是使用类加载器。
(1)、由类加载器ClassLoader来加载进内存,即用getClassLoader()方法获取类加载器,然后用类加载器的getResourceAsStream(String name)方法,将配置文件(资源文件)加载进内存。利用类加载器来加载配置文件,需把配置文件放置的包名一起写上。这种方式只有读取功能。
(2)、Class类也提供getResourceAsStream方法来加载资源文件,其实它内部就是调用了ClassLoader的方法。这时,配置文件是相对类文件的当前目录的,也就是说用这种方法,配置文件前面可以省略包名。
如:类名.class.getResourceAsStream(“资源文件名”)
注:菜鸟日记,如有错误欢迎大神指正,欢迎探讨!!
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------