1.Properties类
- 该类是一个Map集合。之前学习的集合都是将数据存储到内存中,但是这个集合类它可以和IO流结合,直接把集合中的数据保存在硬盘的文件中,或者直接从硬盘的文件中加载数据保存在集合中;
- 这个集合中的键和值都是String类型的字符串;
- 这个集合类没有泛型;
1.1 特有函数(功能)
-
Object
setProperty(String key, String value)
添加元素,等同于Map中的put功能。
返回:属性列表中指定键的旧值,如果没有值,则为 null。 -
String
getProperty(String key)
根据键找值,等同于Map中的 get(Object key)。
返回:属性列表中具有指定键值的值。
Set stringPropertyNames() 获取所有键的集合,等同于Map中的keySet方法。
//遍历演示
public class PropertiesDemo2 {
public static void main(String[] args) {
//创建集合对象
Properties prop = new Properties();
//向集合中添加数据
prop.setProperty("张三","北京");
prop.setProperty("李四","上海");
prop.setProperty("王五","南京");
prop.setProperty("田七","杭州");
//使用对象调用stringPropertyNames()函数获取所有的键集合
Set<String> keys = prop.stringPropertyNames();
//遍历集合
for (Iterator<String> it = keys.iterator(); it.hasNext();) {
String key = it.next();
//根据key获得value
String value = prop.getProperty(key);
System.out.println(key+"---"+value);
}
}
}
1.2 Properties与流相关的操作
Properties类与流相互操作的主要方法如下所示:
1.把集合中的数据通过流保存到文件中。
public static void method_1() throws IOException {
//创建集合对象
Properties prop = new Properties();
//向集合中添加数据
prop.setProperty("张三","北京");
prop.setProperty("李四","上海");
prop.setProperty("王五","南京");
prop.setProperty("田七","杭州");
//创建输出流对象
FileOutputStream fos = new FileOutputStream("D:\\person.txt");
//使用集合对象prop调用store()函数向文件中添加数据
/*
* 如果在这里使用了输出流,那么不用关闭资源,在Properties类的store函数中
* 已经帮助我们关闭了
*/
prop.store(fos, "person");
}
上述代码运行的结果是:
说明:
1)上述结果中的 # 表示注释,#号后面的内容如果从硬盘文件中加载到内存中是不会被读取的;
2)在Java中汉字字符都是以gbk来保存的,而当你使用字节流FileOutputStream 进行写数据的时候底层使用的是ISO-8859-1来进行解码
的时候,所以保存在硬盘文件中都看不懂;
向硬盘中写数据使用字符流,不使用字节流,因为字符流Writer底层使用的编码表是GBK。
//把Properties集合中的数据写到本地文件中
public static void method_1() throws IOException {
//创建集合对象
Properties prop = new Properties();
//向集合中添加数据
prop.setProperty("张三","北京");
prop.setProperty("李四","上海");
prop.setProperty("王五","南京");
prop.setProperty("田七","杭州");
//使用集合对象prop调用store()函数向文件中添加数据
/*
* 如果在这里使用了输出流,那么不用关闭资源,在Properties类的store函数中
* 已经帮助我们关闭了
*/
prop.store(new FileWriter("D:\\person.txt"), "person");
}
结果如下:
2.从流中把数据加载到集合中。
public static void main(String[] args) throws IOException {
Properties prop = new Properties();
//使用集合对象prop调用load()函数从硬盘上加载数据
//prop.load(new FileInputStream("D:\\person.txt"));
prop.load(new FileReader("D:\\person.txt"));
//遍历集合
Set<String> keys = prop.stringPropertyNames();
for (Iterator<String> it = keys.iterator(); it.hasNext();) {
String key = it.next();
//根据key键获得value
String value = prop.getProperty(key);
System.out.println(key+"---"+value);
}
}
注意:
1)如果将内存中的数据写到硬盘上的文件中使用字节输出流,那么最好从硬盘中向内存中读取也使用字节输入流;
2)如果将内存中的数据写到硬盘上的文件中使用字符输出流,那么最好从硬盘中向内存中读取也使用字符输入流;
两个流一定要对应。
2.序列化流与反序列化流
2.1 序列化
序列化和反序列化流:它们的功能是可以把程序中new出来的对象保存到持久设备上,或者从持久设备上读取被保存的对象。
1.序列化
序列化:把创建出来的对象(new出来的对象),以及对象中的成员变量的数据转化为字节数据,写到流中,然后存储到硬盘的文件中。
使用场景:
在创建对象并给所创建的对象赋予了值后,当前创建出来的对象是存放在堆内存中的,当JVM停止后,堆中的对象也被释放了,如果下一次想要继续使用之前的对象,需要再次创建对象并赋值。
然而使用序列化对象,就可以把创建出来的对象及对象中数据存放到硬盘的文件中,下次使用的时候不用在重新赋值,而是直接读取使用即可。
public class Student implements Serializable {
//属性
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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 class ObjectOutputStreamDemo {
public static void main(String[] args) throws IOException {
//创建学生对象
Student s = new Student("黑旋风",18);
//把创建出来的学生对象持久化保存在硬盘中
//创建序列化对象 创建输出流对象并关联目标文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\s.txt"));
//使用序列化对象中的方法持久化学生对象
oos.writeObject(s);
//关闭资源
oos.close();
}
}
注意,如果类没有继承Serializable接口
分析异常:
上述异常表示对象需要实现序列化接口时,而对象没有实现序列化接口,就会抛出该异常。
换句话说当我们需要把Java中的一个对象(例如Student类的对象)持久的保存在硬盘上的文件中的时候,这时这个对象所属的类需要实现一个接口
,如果对象所属的类不实现该接口,就报上述的异常。
那么这个接口到底叫什么呢?其实在序列化类ObjectOutputStream的API中已经有所描述,如下图
接口:序列化接口。Serializable:
它是一个标记性接口。这个接口中没有任何的方法,这种接口称为标记型接口!它仅仅是一个标识。是一个标记接口为了启动一个序列化功能。只有具备了这个接口标识的类才能通过Java中的序列化和反序列化流操作这个对象。
注意:只要一个类实现了Serializable接口,那么都会给每个实现类分配一个序列版本号作为唯一标识。
小结:
当对创建的对象进行序列化操作时,必须保证对象所属的类要实现序列化接口Serializable。
2.反序列化
反序列化:可以把序列化后的对象(硬盘上的文件中的对象数据),读取到内存中,然后就可以直接使用对象。这样做的好处是不用再一次创建对象了,直接反序列化就可以了。
反序列化对象所属的类是ObjectInputStream类:
public class ObjectInputStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//创建反序列化对象,指定一个字节输入流用来读取持久文件
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\s.txt"));
//使用反序列化对象ois调用函数进行读取数据
Student s = (Student) ois.readObject();
System.out.println(s.getName()+"==="+s.getAge());
//关闭资源
ois.close();
}
}
我们对Student类做了一些简单的修改,无关紧要的修改。例如给Student类添加一个属性字段或者函数都可以,再次反序列化,就出问题了,报如下图所示的异常:
通过以上发生的异常我们发现是由于readObject()函数发生的异常,所以我们接下来会查看readObject()函数。
接下来查看InvalidClassException 异常类:
问题一:什么是该类的序列版本号呢?
类要进行序列化操作时,需要实现Serializable接口。(Serializable接口也称为标记接口),实现了标记接口的类,该类会存在一个标记值。这个标记值就是该类的序列版本号。这个版本号和该类相关联。
说明:也就是说只要一个类实现Serializable接口,那么在编译源文件时,生成的class文件中就会生成一个和该类相关联的序列版本号。
问题二:这个版本号有什么作用呢?
在序列化时,这个版本号会随着对象一起被序列化到本地文件中。在反序列化的时候,使用流从硬盘上会读取之前序列化的文件,那么jvm会拿着使用流读取到的序列化的版本号和本地硬盘上即class文件中的序列化版本号进行匹配,如果不匹配就会抛异常。Java.io.InvalidClassException。
所以可以理解这个序列化版本号serialVersionUID是用来验证的,防止反序列化的对象和本地的类不匹配。
问题三:这个序列化版本号是如何生成的呢?
由于实现类已经实现了Serializable接口,那么只要重新编译源文件的时候,编译器就会根据类的各个方面(如类的成员变量、成员函数、修饰符、函数返回值类型等)计算成为该类的默认 serialVersionUID 值(版本号)
。
serialVersionUID 称为序列版本编号(标记值)。
通过以上分析,可以得到一个结论:
如果可以保证反序列化对象和序列化对象的标记值相同,就可以避免异常的发生。
那么我们如何做才能保证反序列化对象和序列化对象的标记值相同呢?
我们修改Student类是无关紧要的。在我们修改Student类的时候,我们不希望它抛异常。我们可以给类定义一个默认的版本号,即给Student类添加标记值也就是版本号serialVersionUID。这样一来,添加的标记值即版本号会随着对象的序列化持久保存。无论是序列化,还是反序列化,都不会再根据类的各个方面计算版本号了。序列化和反序列化的版本号会永远一致,所以不会抛出异常,这样就可以避免InvalidClassException异常的发生了。
但是,这样一来,类的安全问题,只能自己来维护。
因为已经将类的对象序列化之后,由于类中已经显示定义了版本号,那么反序列化的时候即使修改了Student类,也不会报异常了。
注意:
在使用序列化操作时,不是所有的成员的值都可以进行序列化操作:
- 静态成员的值不会进行序列化操作;
- 瞬态成员的值也不会进行序列化操作;
瞬态成员:在进行序列化操作时,如果希望某些成员不被序列化,而该成员又不能是静态成员(不希望随着类加载而存在,和对象有关系),就使用关键字transient(表示瞬时的意思)把成员变为瞬态成员。在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
代码如下所示:
说明:由于name和age属性分别被静态和瞬态修饰了,所以他们的值都不能被序列化到硬盘上,所以反序列化都是默认值。
总结,记住:
1、当一个对象需要被序列化 或 反序列化的时候对象所属的类需要实现Serializable接口。
2、被序列化的类中需要添加一个serialVersionUID。
序列化的细节:
序列化的时候,只能把对象在堆中的所有数据持久保存到持久设备上。静态的成员变量不会被序列化。
有时我们在序列化的时候某些非静态成员变量也不想被序列化的时候,我们可以使用瞬态关键字(transient)修饰。
3.打印流
打印流:用来向文件、控制台、网络中打印数据的流。
打印流有两种:
- PrintStream:打印字节数据
- PrintWriter:打印字符数据
1.PrintStream
public static void main(String[] args) throws FileNotFoundException {
System.out.print("hello world");
/*
* System表示一个系统类
* System.out的返回值是一个打印流PrintStream
* 这个打印流的目的地就是控制台
*/
PrintStream ps = System.out;
//使用PrintStream打印流对象调用函数输出数据
ps.print("java");
ps.print("锁哥");
//向文件中打印数据
PrintStream ps2 = new PrintStream("E:\\ps.txt");
ps2.print(true);//true
ps2.println("hello");
ps2.println("world");
ps2.println("java");
// 释放资源
ps.close();
}
2.PrintWriter
输出结果:
pw.txt文件中没有任何数据,因为没有刷新流中的数据。
开启自动刷新的打印流:
上述代码中已经开启了自动刷新功能,可是依然没有将数据写到文件中,为什么?
原因:PrintWriter流只有在调用println()、printf()、format()三个方法中任意一个方法时,才会自动刷新
总结:
在使用打印流的时候,需要启动的自动刷新之后,才能针对println() , format(), printf():完成刷新的功能。
如果及时启动自动刷新,但是没有调用上述的这三个方法,依然不会刷新。
其实在上述代码中直接使用字节打印流PrintWriter的对象pw调用close()或者flush()函数都会将数据写到文件中;
打印流特点:
A:只有输出流,没有输入流。
B:既可以输出到文件,又可以输出到其它流中。
C:会把一切数据当成字符串来输出。
D:打印流打印char数组时会输出数组中的内容,打印其它数组会打印地址值。
E:PrintStream自带自动刷新,PrintWriter需要启动自动刷新。