一、序列化
1.序列化概述
在实际开发中,经常需要将对象的信息保存到磁盘中便于检索,但通过前面输入输出流的方法逐一对对象的属性信息进行操作,很繁琐并容易出错,而序列化提供了轻松解决这个问题的快捷方法。
简单地说,序列化就是将对象的状态存储到特定存储介质中的过程,也就是将对象状态转换为可保持或传输格式的过程,在序列化过程中,会将对象的共有成员、私有成员包括雷明,转换为字节流,然后再把字节流写入数据流,存储到存储介质中,这里说的存储介质通常指的是文件。
使用序列化的意义在于,将对象序列化后,可以将其转换为字节序列,这样字节序列就可以被保存在磁盘上,也可以借助网络进行传输,同时序列化后的对象时二进制状态,这样实现了平台无关性。
2.使用序列化保存对象信息
序列化机制允许将实现了序列化的Java对象转化为字节序列,这个过程需要借助I/O流实现。
Java中只有实现了java.io.Serializable接口的类的对象才能被序列化,Serializable表示可串行的、可序列化的,所有序列化有时也称作串行化。JDK中如String类、包装类、Date类等都实现了Serializable接口。
实例:
1)引入java.io.Serializable,创建一个Student类,实现Serializable接口
package cn.serializableDemo; import java.io.Serializable; public class Student implements Serializable { private String name; private int age; private String gender; private transient String password; public Student(String name, int age,String gender){ this.name=name; this.age=age; this.gender=gender; } public Student(String name, int age,String gender,String password){ this.name=name; this.age=age; this.gender=gender; this.password=password; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
2)引入相关类包,创建对象输出流,调用writeObject()方法将对象写入文件。最后关闭输出流
package cn.serializableDemo; import java.io.*; public class SerializableObj { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = null; ObjectInputStream ois=null; try { // 创建ObjectOutputStream输出流 oos = new ObjectOutputStream(new FileOutputStream("d:\javaWork\stu.txt")); Student stu = new Student("安娜", 30, "女","aaaa"); System.out.println("姓名为:"+stu.getName()); System.out.println("年龄为:"+stu.getAge()); System.out.println("性别为:"+stu.getGender()); System.out.println("密码为:"+stu.getPassword()); // 对象序列化,写入输出流 oos.writeObject(stu); //创建ObjectInputStream输入流 ois=new ObjectInputStream(new FileInputStream("d:\javaWork\stu.txt")); //反序列化,强转类型 Student stu1=(Student)ois.readObject(); //输出生成后对象信息 System.out.println("姓名为:"+stu1.getName()); System.out.println("年龄为:"+stu1.getAge()); System.out.println("性别为:"+stu1.getGender()); System.out.println("密码为:"+stu1.getPassword()); } catch (IOException ex) { ex.printStackTrace(); } finally { try { if (oos != null) { oos.close(); } if (ois != null) { ois.close(); } }catch (IOException ex) { ex.printStackTrace(); } } } }
上面例子可以将一个学生的信息保存到文件中,如果要保存多个学生信息,可以使用集合先保存多个学生信息,再将集合添加到输出流。关键代码如下:
//主要代码 // 创建ObjectOutputStream输出流 oos = new ObjectOutputStream(new FileOutputStream("d:\javaWork\stu.txt")); Student stu = new Student("安娜", 30, "女","aaaa"); Student stu1 = new Student("李白", 18, "男","123456");
List<Student> list = new ArrayList<Student>(); list.add(stu); list.add(stu1); // 对象序列化,写入输出流 oos.writeObject(list); //创建ObjectInputStream输入流 ois=new ObjectInputStream(new FileInputStream("d:\javaWork\stu.txt")); //反序列化,强转类型 List listStu=(ArrayList)ois.readObject(); //输出生成后对象信息 Iterator iterator = listStu.iterator(); while(iterator.hasNext()){ Student student = (Student)iterator.next(); System.out.println("姓名为:"+student.getName()); System.out.println("年龄为:"+student.getAge()); System.out.println("性别为:"+student.getGender()); System.out.println("密码为:"+student.getPassword()); }
注意一个问题,实例化对象时是有给密码属性赋值的,但序列化时并没有保存该对象的密码属性值。原因是被声明为transient的password属性不会被序列化。
在对象序列化时需要注意:
- 序列化之后保存的是类的信息
- 被声明为transient的属性不会被序列化(出于安全考虑),这就是transient关键字的作用
- 被声明为static的属性不会被序列化,这个问题可以这么理解,序列化保存的是对象的状态,但是static修饰的变量是属于类的而不是属于变量的,因此序列化的时候不会序列化它
- 如果向文件中使用序列化写入多个对象,则反序列化恢复对象时必须按照写入的顺序读取。
- 如果一个可序列化的类有多级父类,则这些父类要么也是可序列化的,要么有无参数的构造器,否则会抛出异常
3.使用反序列化获取对象信息
反序列化就是从特定存储介质中读取数据并重新构成对象的过程。大致分为2步:
1)创建一个对象输入流(ObjectInputStream),它可以板状一个其他类型的输入流,如FileInputStream
2)通过对象输入流的readObject()方法读取对象,该方法返回一个Object类型的对象,需要进行强制转换成真实的对象类型
例子可参考对象序列化的实例。
4.对象引用的序列化
如果一个类的成员中包含其他类的对象,如班级类中包含学生类型的对象,那么要序列化班级类时,则必须保证班级类和学生类都是可序列化的。即当需要序列化某个特定对象时,它的各个成员对象也必须是可序列化的。
序列化的算法规则如下:
- 所有保存到磁盘的对象都有一个序列号
- 当程序试图序列化一个对象时,将会检查是否已经被序列化,只有序列化后的对象才能被转换为字节码序列输出
- 如果对象已经被序列化,则程序直接输出一个序列化编号,而不再重新序列化
二、Java中的反射机制
1.反射概述
Java反射机制时Java特性之一,反射机制是构建框架技术的基础所在。
反射机制是指在运行状态中,动态的获取信息以及动态调用对象方法的功能。
Java反射有3个动态性质:
- 运行时生成对象实例
- 运行期间调用方法
- 运行时更改属性
由Java程序的执行过程可知,要想Java程序能够运行,Java类必须被Java虚拟机加载,运行的程序都是在编译时就已经加载了所需的类;
Java反射机制在编译时并不确定那个类被加载了,而是在程序运行时才加载、探知、使用,这样的特点就是反射。如上图,Class对象的由来是将class文件读入内存,并为之创建一个Class对象。如使用MyEclipse时代码的自动提示共嗯那个,就是利用了Java的反射原理。
使用Java反射机制可以实现以下功能:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的方法和属性
- 在运行时调用任意一个对象的方法
2.Java中反射常用API
使用Java反射基数常用的类:
- Class类:反射的核心类,反射所有的操作都是围绕该类来生成的,它可以获取类的属性、方法等内容信息
- Field类:表示类的属性,可以获取和设置类中属性的值
- Method类:表示类的方法,它可以用来获取类中方法的信息,或者执行方法
- Constructor类:表示类的构造方法
一般情况下我们使用反射获取一个对象的步骤:
- 获取类的 Class 对象实例
Class clz = Class.forName("com.zhenai.api.Apple");
- 根据 Class 对象实例获取 Constructor 对象
Constructor appleConstructor = clz.getConstructor();
- 使用 Constructor 对象的 newInstance 方法获取反射类对象
Object appleObj = appleConstructor.newInstance();
而如果要调用某一个方法,则需要经过下面的步骤:
- 获取方法的 Method 对象
Method setPriceMethod = clz.getMethod("setPrice", int.class);
- 利用 invoke 方法调用方法
setPriceMethod.invoke(appleObj, 10);
在 JDK 中,反射相关的 API 可以分为下面几个方面:获取反射的 Class 对象、通过反射创建类对象、通过反射获取类属性方法及构造器。
package cn.UtilTest; import java.lang.reflect.*; public class Phone { private int price; public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public static void main(String[] args) { try{ //正常创建对象 Phone phone = new Phone(); phone.setPrice(5000); System.out.println("手机价格:"+phone.getPrice()); System.out.println("----------------------------"); //使用反射创建对象并调用方法 //获取类的Class对象实例 Class clz = Class.forName("cn.UtilTest.Phone"); //根据 Class 对象实例获取 Constructor 对象 Constructor phoneConstructor = clz.getConstructor(); //使用 Constructor 对象的 newInstance 方法获取反射类对象 Object phoneObject = phoneConstructor.newInstance(); //获取方法的 Method 对象 Method setPriceMethod = clz.getMethod("setPrice", int.class); Method setPriceMethod1 = clz.getMethod("getPrice"); //利用 invoke 方法调用方法 setPriceMethod.invoke(phoneObject,5000); System.out.println("手机价格:"+setPriceMethod1.invoke(phoneObject)); }catch (Exception e){ e.printStackTrace(); } } }
3.反射的应用
- 获取类的信息
分为2步,首先获取Class对象,然后通过Class对象获取信息。
1)获取Class对象
每个类被加载后,系统就会为该类生成一个对应的Class对象,通过Class对象就可以访问Java虚拟机中类的信息。获取Class对象通常有3种方式:
-
- 调用对象的getClass()方法
属于java.lang.Object类中的一个方法,所有对象都可以调用。例如:Student stu = new Student(); Class clz = stu.getClass(); 其中clz为Class对象 - 调用类的class属性
调用某个类的class属性可获取该类对应的Class对象,这种方式需要在编译器就知道类的名称。例如:Class clz = Student.class; Student为真实的学生类 - 使用Class类的forNmae()静态方法
该方法需要传入字符串作为参数,其值是某个类的全名,即在类名前加上完整的包名。例如:Class clz = Class.forName("com.yu.reflection.Student");
- 调用对象的getClass()方法
后2种方式都是直接根据类来获取该类的Class对象,相比之下调用某个类的class属性来获取该类对应的Class对象这种方式更有优势,原因如下:
-
- 代码更安全,程序在编译阶段就可以检查需要访问的Class对象是否存在;
- 程序性能更高,因为这种方式无需调用方法,所以性能更好;
2)从Class对象获取信息
在获得了某个类的Class对象后,就可以调用Class对象的方法来获取该类的详细信息。Class类提供了大量实例方法:
方法名 | 说明 |
访问Class对应的类包含的构造方法: | |
Constructor getConstructor(Class[] params) | 返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。构造方法的参数类型和顺序与params所指定的参数类型、顺序匹配 |
Constructor[] getConstructor() | 返回一个包含某些 Constructor 对象的数组,这些对象反映此 Class 对象所表示的类的所有公共构造方法。 |
Constructor getDeclaredConstructor(Class[] params) | 返回一个 Constructor 对象,它反映此 Class 对象所包含的类的指定公共构造方法。与构造方法的访问级别无关 |
Constructor getDeclaredConstructor() | 返回一个 Constructor 对象,它反映此 Class 对象所包含的类的所有公共构造方法。与构造方法的访问级别无关 |
访问Class对应的类所包含的属性: | |
Filed gtField(String name) | 返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段。 |
Filed[] gtFields() | 返回一个包含某些 Field 对象的数组,这些对象反映此 Class 对象所表示的类或接口的所有可访问public字段。 |
Filed gtDeclaredField(String name) | 返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段。与字段的访问级别无关 |
Filed[] gtDeclaredFields() | 返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段。与字段的访问级别无关 |
访问Class对应的类所包含的方法: | |
Method getMethod(String name,Class[] params) | 返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。 |
Method[] getMethods() | 如果此 Class 对象表示某一方法中的一个本地或匿名类,则返回 Method 对象,它表示底层类的所有public方法。 |
Method getDeclaredMethod(String name,Class[] params) | 返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的指定方法。与方法的访问级别无关 |
Method[] getDeclaredMethods() | 返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。与方法的访问级别无关 |
访问Class对应的类所包含的注解: | |
<A extends Annotation>A getAnnotation(Class<A>annotationClass) | |
Annotation[] getAnnotations() | |
Annotation[] getDeclaredAnnotations() | 返回直接存在于此元素上的所有注释。 |
访问Class对应的类的其他信息: | |
Class[] getDeclaredClasses() | 返回 Class 对象的一个数组,这些对象反映声明为此 Class 对象所表示的类的全部内部类。 |
Class[] getDeclaringClasses() | 如果此 Class 对象所表示的类或接口是另一个类的成员,则返回的 Class 对象表示该对象的声明类所在的外部类。 |
Class[] getInterfaces() | 确定此对象所表示的类或接口实现的接口。 |
int getModifiers() | 返回此类或接口以整数编码的 Java 语言修饰符。 |
Package getPackage() | 获取此类的包。 |
String getName() | 以 String 的形式返回此 Class 对象所表示的实体(类、接口、数组类、基本类型或 void)名称。 |
String getSimpleName() | 返回源代码中给出的底层类的简称。 |
Class getSuperclass () | 返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的超类的 Class 。 |
- 创建对象
- 获取该类的Class对象
- 利用Class对象的getConstructor()方法来获取指定构造方法
- 调用Constructor的newInstance()方法创建对象
package cn; import java.lang.reflect.Constructor; import java.util.Date; /* * 利用反射机制创建对象 */ public class ReflectionDemo { public static void main(String[] args) { try{ //获取Date对应的Class对象 Class date = Date.class; //获取Date中一个长整型参数的构造方法 Constructor constructor = date.getConstructor(long.class); //调用Constructor的newInstance()方法创建对象 Date da = (Date)constructor.newInstance(1987); System.out.println(da.toString()); }catch (Exception e){ e.printStackTrace(); } } }
- 访问类的属性
通过Field对象可以对属性进行取值或赋值操作
方法名 | 说明 |
Xxx getXxx(Object obj) | 该方法中Xxx对应8个基本数据类型,obj为该属性所在的对昂 |
Object get(Object obj) | 得到引用类型属性值 |
void setXxx(Object obj,Xxx val) | 将obj对象的改属性设置成val值,此处的Xxx对应8个基本数据类型 |
void set(Object obj,Object val) | 将obj对象的改属性设置成val值,针对引用类型赋值 |
void setAccessible(boolean flag) | 对获取到的属性设置访问权限,参数为true,可对私有属性取值和赋值 |
package cn.ReflectionDemo; import java.lang.reflect.Field; import java.lang.reflect.Method; /* * 访问学生类的私有属性并赋值 */ public class Student { //私有属性 private String name; private int age; //成员方法 private String printString(){ return "name is "+name+",age is "+age; } public static void main(String[] args) { try{ Student p = new Student(); Class cla = p.getClass(); Field nameField = cla.getDeclaredField("name"); //此时用getField()会报错 nameField.setAccessible(true); //设置通过反射访问该Field时取消权限校验 nameField.set(p,"Jack"); Field ageField = cla.getDeclaredField("age"); ageField.setAccessible(true); //设置通过反射访问该Field时取消权限校验 ageField.setInt(p,25); Method printStringMethod = cla.getDeclaredMethod("printString"); printStringMethod.setAccessible(true); ////设置通过反射调用该方法时取消权限校验 Object object = printStringMethod.invoke(p); System.out.println(object); }catch (Exception e){ e.printStackTrace(); } } }
- 访问类的方法
在Method类中包含一个invoke()方法:Object invoke(Object obj, Object args) 其中obj是执行该方法的对象,args是执行该方法时传入的参数
- 使用数组类动态创建和访问数组
在java.lang.reflect包下还提供了一个Array类,此Array类的对象可以代表所有的数组。程序可以通过Array类来动态的创建数组、操作数组元素等。
package cn.ReflectionDemo; import java.lang.reflect.Array; /* * 使用Array动态创建和访问数组 */ public class ReflectArray { public static void main(String[] args) { Object arr = Array.newInstance(String.class,10); Array.set(arr,5,"Jack"); Array.set(arr,6,"Tom"); Object arr5 = Array.get(arr,5); Object arr6 = Array.get(arr,6); System.out.println("arr[5]="+arr5+" "+"arr[6]="+arr6); } }
4.使用反射需注意的问题
使用反射虽然会很大程度上提高代码的灵活性,但是不能滥用反射,因为通过反射创建对象时性能稍微要低一些。
实际上,只有当程序需要动态创建某个类的对象时才会考虑使用反射,通常在开发通用性比较强的框架、基础平台时可能会大量使用反射,因为很多Java框架中都需要根据配置问价信息来创建Java对象,从配置文件读取的只是某个类的字符串类名,程序需要根据字符串来创建对应的实例,就必须使用反射。
在实际开发中,没有必要使用反射来访问以之类的方法和属性,只有当程序需要动态创建某个类的对象时才会考虑使用。