IO流目录:
初识IO
IO,即input和output,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。
通过IO可以完成硬盘文件的读和写。
Java 中是通过流处理IO 的,那么什么是流?
流:输入和输出时流动的数据序列(只读或者只写)
当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。
相关的类放在java.io包下
什么是文件:文件就是保存数据的地方
文件流:文件在程序中是以流的形式来操作的
常用的文件操作
File类
创建文件:
File
类实现了Serializable、 Comparable ,说明它是支持序列化和排序的。
File类的构造器:
方法 说明
createNewFile() 当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。
delete() 删除此抽象路径名表示的文件或目录。
exists() 测试此抽象路径名表示的文件或目录是否存在。
getAbsoluteFile() 返回此抽象路径名的绝对路径名形式。
getAbsolutePath() 返回此抽象路径名的绝对路径名字符串。
length() 返回由此抽象路径名表示的文件的长度。
mkdir() 创建此抽象路径名指定的目录。
这里讲三个重要的!即三种用File类创建文件的方法
- new File(String pathname)
传一个路径+文件名
public void createFile(){
String filePath = "d:\\newFile1.txt"; //或者d:/newFile1.txt
File file = new File(filePath);
try {
file.createNewFile(); //创建文件,不写这个不会生成文件
System.out.println("创建成功");
} catch (IOException e) {
e.printStackTrace();
}
}
这里注意:用\的原因是Java中\是转义符所以要用\\
,也可以使用/
- new File(File parent, String child)
根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。相当于是在父目录里生成一个名字为child的文件。这里parent路径不存在就会报错!
File parentFile = new File("d:\\");
String childFile = "newFile2.txt";
File file = new File(parentFile, childFile);
try {
file.createNewFile();
System.out.println("创建成功");
} catch (IOException e) {
e.printStackTrace();
}
- new File(String parent, String child)
根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
String parentPath = "d:\\";
String childPath = "newFile3.txt";
File file = new File(parentPath, childPath);
try {
file.createNewFile();
System.out.println("创建成功");
} catch (IOException e) {
e.printStackTrace();
}
获取文件信息
File file = new File("d:\\newFile1.txt");
System.out.println("文件名:" + file.getName());
System.out.println("文件绝对路径:" + file.getAbsolutePath());
System.out.println("文件父级目录:" + file.getParent());
System.out.println("文件大小(字节):" + file.length());
System.out.println("是否是一个文件:" + file.isFile());
System.out.println("是否是一个目录:" + file.isDirectory());
运行结果:
这里注意length()表示文件大小,不再表示长度!我这里没有在文件里写入东西所以是0。
思考:在文件中写入下图file.length()结果是啥
这里要看文件的编码方式:我的是UTF-8,字母和数字都是占一个字节,一个汉字占三个字节。这里空格还占了一个字节
常用的文件操作
delete();删除此抽象路径名表示的文件或目录。
1、删除文件 (filePath为文件名)
String filePath = "d:/newFile2.txt";
File file = new File(filePath);
if (file.exists()){ //判断文件是否存在
System.out.println(file.delete()?filePath+"删除成功":filePath+"删除失败");
}else
System.out.println("文件不存在。。。");
文件删除失败的原因:文件被占用
2、删除空目录(文件夹)
String filePath = "d:/study007";
File file = new File(filePath);
if (file.exists()){
System.out.println(file.delete()?filePath+"删除成功":filePath+"删除失败");
}else
System.out.println("目录不存在。。。");
这里注意:文件夹需要是空文件夹(或者说是空目录)不然会删除失败!
创建目录:
mkdir(); 创建此抽象路径名指定的目录(一层目录)。
mkdirs();创建多层目录
String filePath = "d:/study007/a/b/c";
File file = new File(filePath);
if (file.exists()){
System.out.println(filePath+"存在。。。");
}else
System.out.println(file.mkdirs()?filePath+"创建成功":filePath+"创建失败");
这里把b、c文件夹删掉,仍然可以创建成功!
想看File方法的图表或者类的继承关系,可在IDEA中点进File类在空白处右击
流的分类
1、IO流的继承体系图
按操作数据单位不同分为:字节流和字符流
按数据流的流向不同分为:输入流和输出流
按流的功能可分为:节点流(Node)和处理流(Filter)
2、输入流和输出流
输入流:数据从数据源(文件)到程序(内存)的路径(Java程序读取文件)
输出流:数据从程序(内存)到数据源(文件)的路径(Java程序将东西写入文件中)
数据保存在文件里也就是在磁盘中,读取数据到Java程序里也就是读取到内存中就叫做输入流。反之就是输出流。
画个图更深的理解一下!
这里人表示Java程序,杯子表示磁盘,胃表示内存,水是存在文件中的数据,水从杯子里进入胃的过程就是输入流,当你不舒服时候把水吐回杯子里的过程就是输出流
Java IO流共涉及四十多个类,实际上都是从以上四个基类派生的!
由这四个类派生的子类名称都是以其父类名作为子类名的后缀
观察源码发现以上四个都是抽象类!不能直接实例化需要实例化其子类使用相应方法!
3、字节流和字符流
字节流和字符流的用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据单元为16位的字符。
字节流和字符流的区别:
- 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件,可以保证文件是无损操作,不会造成文件的损失。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。
- 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。
4、节点流和处理流
节点流:可以从一个特定的数据源(存放数据的地方)读写数据,它是直接操作数据读写的流类。例如FileReader和FileWriter分别对文件进行读和写的,
它是底层的流或者说是低级的流。
因为节点流的灵活性以及效率不好,功能不是很强大。这时Java设计者提供了包装流!
处理流(也叫包装流):对一个已存在的流的 “连接” 和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能。例如BufferedReader
、BufferedWriter
简单来说包装流就是对节点流进行包装,让这个流变得更强大
点进BufferedReader源码发现
说明它可以封装Reader子类的节点流(也就是需要在构造器中传入一个Reader的子类)!它不再局限于特定的数据源,只需要传入相应的节点流即可实现对不同数据源的操作。例如我传入FileReader即可实现对文件的操作
(1)节点流和处理流一览图
(2)节点流和处理流的区别与联系
- 节点流是底层流,直接和数据源相连
- 处理流(包装流)包装节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出
- 包装流对节点流进行包装,使用了修饰器设计模式,不会直接与数据源相连
处理流的功能主要体现在
- 性能的提高:主要以增加缓冲的方式来提高输入输出的效率
- 操作的便捷:处理流可能提供了一系列便捷的方法来输入输出大批量的数据,使用更加灵活方便
下图就很形象地描绘了节点流和处理流,处理流是对节点流的封装,最终的数据处理还是由节点流完成的。
这里模拟一下修饰器模式来帮助理解!
public abstract class Reader_ {
public abstract void read();
}
节点流:
public class FileReader_ extends Reader_{
@Override
public void read() {
System.out.println("读取文件。。。");
}
}
public class StringReader_ extends Reader_ {
@Override
public void read() {
System.out.println("读取字符串。。。");
}
}
包装流:
public class BufferedReader_ extends Reader_ {
private Reader_ reader_;
//可以接收Reader的子类
public BufferedReader_(Reader_ reader_) {
this.reader_ = reader_;
}
@Override
public void read() {
reader_.read();
}
//让方法更加灵活 扩展:批量处理数据
public void read(int num){
for (int i = 0; i < num; i++) {
reader_.read();
}
}
}
测试类
public class Test_ {
public static void main(String[] args) {
BufferedReader_ bufferedReader_ = new BufferedReader_(new FileReader_());
bufferedReader_.read();
BufferedReader_ bufferedReader_1 = new BufferedReader_(new StringReader_());
bufferedReader_1.read(5);
}
}
运行结果:
这样做的优点:
- 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
- 可以通过一种动态的方式来扩展一个对象的功能,通过在实例化对象时选择不同的具体装饰类,从而实现不同的行为。
- 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合 “开闭原则”。
有优点就会有缺点:
修饰器模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
节点流:
InputStream和OutputStream
InputStream:字节输入流,是所有字节输入流类的超类
InputStream常用的子类:
1、FileInputStream:
FileInputStream:文件的输入流,一个非常重要的字节输入流,用于对文件进行读取操作。
读取文件的方法:
举个例子:使用FileInputStream读取a.txt文件(在任意位置创建),在其内写入hello,world!
(1) read();
read(); 从输入流中每次读取一个字节的数据,如果返回-1表示读取完毕
String filePath = "d:/a.txt";
int readDate;
FileInputStream inputStream = null; //提升作用域,否则finally里面取不到inputStream
try {
inputStream = new FileInputStream(filePath);
while ((readDate = inputStream.read()) != -1){
System.out.print((char)readDate);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭文件流释放资源!
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
成功输出
这里解释一下为什么要下图这么写
不定义readData是拿不到读取的完整数据的,只要调用read()方法就会读取,在判断条件里使用read()和在输出里使用read()获取的数据不是一样的,以本题为例,在判断条件里获取到的是h但是输出里获取到的是e直接输出了,h没有输出
思考: 这里我文件里输入的是英文如果我输入汉字会发生什么?
会输出乱码!原因是一个汉字在编码为UTF-8的条件下是占三个字节你只读取了一个字节的内容会导致输出内容乱码。所以建议文本读取使用字符流!
(2) read(byte[] b);
从该输入流中读取最多b.length字节的数据到字节数组。如果返回-1表示读取完毕。如果读取正常返回实际读取的字节数
String filePath = "d:/a.txt";
int len;
byte[] buff = new byte[8]; //一次读取八个字节(提高效率)
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(filePath);
while ((len = inputStream.read(buff)) != -1){
System.out.print(new String(buff , 0 ,len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
运行结果:
输出结果一样但是第二种方法的效率高于第一种!建议使用read(byte[] b)
分析:
把输出改成System.out.println(new String(buff , 0 ,len));
第一次len等于8不等于-1先输出了8个字节,后一次因为不足8个字节len等于实际读取的字节数4。
这里还可以debug一下
最开始数组都是0
下一步
下一步
这里数组4-7元素不为空是因为没有清空数组,0-3是用新的数据覆盖之前的数据,4-7没有被新的元素覆盖。
2、FileOutputStream
FileOutputStream:讲数据写到文件中,如果该文件不存在则创建文件(前提是目录已经存在)
写入文件的方法
(1) write(int b);
这里虽然里面是int类型参数,但实际上传入字节即可(因为char=>int可以进行自动转换)
例:在a.txt文件中写入hello,world!
注:这里为了代码简洁易读把异常捕获省略
String filePath = "d:/a.txt";
FileOutputStream fileOutputStream = new FileOutputStream(filePath);
fileOutputStream.write('A');
fileOutputStream.close();
运行发现自动生成了a.txt并且里面写入了A
(2) write(byte[] b);
可以用来写入字符串(String类有getBytes()方法可以把字符串转换成字节数组)
String filePath = "d:/a.txt";
FileOutputStream fileOutputStream = new FileOutputStream(filePath);
String str = "hello,world!"
fileOutputStream.write(str.getBytes());
fileOutputStream.close();
(3)write(byte[] b, int off, int len) ;
这里off代表偏移量,意思是从off开始,写len个,例如(b,2,3)即从byte[2]开始写入3个字节
String filePath = "d:/a.txt";
FileOutputStream fileOutputStream = new FileOutputStream(filePath);
String str = "hello,world!"
//这里 0和str.length()可以根据需要调节
//fileOutputStream.write(str.getBytes(),2,7);
fileOutputStream.write(str.getBytes(),0,str.length());
fileOutputStream.close();
通过这三个例子可以发现一个问题,就是每次写入都会覆盖掉文件之前的内容,怎样可以不覆盖而是在后面加上呢?
FileOutputStream 的构造器提供给我们一个参数,只要为true就不会覆盖了
String filePath = "d:/a.txt";
FileOutputStream fileOutputStream = new FileOutputStream(filePath,true);
文件输入流输出流实操:
完成图片的拷贝
String inputFilePath = "e:\\IDEA2019.png";
String outputFilePath = "d:\\JavaStudy.jpg";
//字节数组提高读取效率
byte[] buff = new byte[1024];
int len;
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
try {
fileInputStream = new FileInputStream(inputFilePath);
fileOutputStream = new FileOutputStream(outputFilePath);
while ((len = fileInputStream.read(buff)) != -1){
//边读边写
fileOutputStream.write(buff,0,len);//一定要使用这个方法
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileInputStream != null){
fileInputStream.close();
}
if (fileOutputStream != null){
fileOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
这里解释一下:为什么要 边读边写 而不是一起读完一并写入:因为字节型复制文件,底层是创建一个数组长度为1kb–8kb之间,边读边写就可以复制不同大小的文件----无论复制的文件多大-----一边读取一边写入—不用担心创建的byte数组长度不够。所以可以复制任意类型和大小的文件
这里注意一定要使用 write(byte[] b, int off, int len) 这个方法,否则可能会造成文件出现问题!
画个图帮助理解一下
用len可以动态的读取!
补充:
为什么要写if(流 != null) 因为如果某个资源为空,直接就会跳转至finally中,这时如果直接调用close方法,就会报出空指针异常的错误.只有资源不为空时,我们才可以调用close方法
3、FileReader和FileWriter
FileReader
FileReader构造器:
- public FileReader(String fileName)
- public FileReader(File file)
read方法:
read() | 读取单个字符 返回该字符,如果文件末尾返回-1 |
---|---|
read(char[] cbuf) | 批量读取多个字符到数组,返回读取到的字符数,如果文件末尾返回-1 |
例:使用FileReader从 read.txt 读取内容
使用read()方法
String filePath = "d:\\read.txt";
FileReader fileReader = null;
int len;
try {
fileReader = new FileReader(filePath);
while ((len = fileReader.read()) != -1){
//read()返回值为int类型需转换成char输出
System.out.print((char)len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileReader != null){
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用read(char[] cbuf),其余代码同上
char[] buff = new char[8]; //在try外面声明
while ((len = fileReader.read(buff)) != -1){
System.out.print(new String(buff , 0 , len));
}
运行结果:
FileWriter
FileWriter构造器:
- public FileWriter(File file)
- public FileWriter(String fileName)
- public FileWriter(String fileName,boolean append)
- public FileWriter(File file, boolean append)
前两个写入时是覆盖模式,会覆盖掉原来文件里的内容,相当于流的指针指向文件首端。后两个是追加模式,会在原来文件的末尾添加,相当于流的指针在尾端。
这里要根据相应的业务逻辑选择好是要追加还是覆盖!
write方法:
write(int c) | 写入单个字符 |
---|---|
write(char[] cbuf) | 写入指定数组 |
write(char[] cbuf,int off, int len) | 写入指定数组的指定部分 |
write(String str) | 写入整个字符串 |
write(String str,int off, int len) | 写入字符串的指定部分 |
相关API:String 类:toCharArray:将String转换为char[]
注意:FileWriter使用后,必须要关闭(close)或刷新(flush),否则写入不到指定文件!(在内存中)
例:文件中写入信息(没有文件会自动生成文件)
String filePath = "d:/write.txt";
try {
FileWriter fileWriter = new FileWriter(filePath);
fileWriter.write('H');
} catch (IOException e) {
e.printStackTrace();
}
测试不写close 和 flush 发现文件正常生成但是H没有被写入!所以一定要注意关闭(close)或刷新(flush)!
String filePath = "d:/write.txt";
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter(filePath);
fileWriter.write('H');
fileWriter.write(chars);
fileWriter.write("是个码盗~");
fileWriter.write("欢迎看我的博客!",0,7);
fileWriter.write(chars,0,2);
//数据量大时可以使用循环操作
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
运行都被成功写入
有些人这会有疑问不是覆盖吗?怎么都差入了?注意它是先放在了缓冲区里,当close时才开始写入,相当于是一次性写入的!
注:flush() 只是刷新没有关闭,close() 等价于flush()+关闭
处理流:
1、BufferedReader和BufferedWriter
(1)BufferedReader
readLine() :读取一个文本行。
readLine()方法是对FileReader类的扩展。按行读取效率高!该方法如果返回null,说明已经到达文件末尾
应用实例:使用BufferedReader读取文本文件并显示在控制台
String path = "d:/a.txt";
BufferedReader bufferedReader = new BufferedReader(new FileReader(path));
String line = null;
while ((line = bufferedReader.readLine()) !=null){
System.out.println(line);
}
bufferedReader.close();
这里注意:只需关闭外层的BufferedReader,因为底层会自动关闭节点流!
可以通过debug来验证一下
证明了底层将内层的节点流进行了自动关闭!
(2)BufferedWriter
newLine()
newLine():插入一个和系统相关的换行(并不是所有的平台都使用换行符(‘\n’))
应用实例:使用BufferedWriter写入a.txt中
String path = "d:/a.txt";
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(path));
bufferedWriter.write("hello");
//换行
bufferedWriter.newLine();
bufferedWriter.write("欢迎访问我的博客!");
bufferedWriter.newLine();
bufferedWriter.write("一起学习Java");
bufferedWriter.newLine();
//说明:关闭外层流即可 传入的 new FileWriter(path),会在底层关闭
bufferedWriter.close();
运行结果:
这里注意:通过观察BufferedWriter 源码发现没有以追加方式写入,即没有BufferedWriter(new FileWriter(path),true)。
要实现追加须在节点流中添加(不加是覆盖的方式写入):
new BufferedWriter(new FileWriter(path,true));
综合使用
使用BufferedReader和BufferedWriter完成 文本 文件拷贝
找一个.java文件
public static void main(String[] args) throws IOException {
String readPath = "d:/a.java";
String writePath = "d:/b.java";
String line;
BufferedReader bufferedReader = new BufferedReader(new FileReader(readPath));
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(writePath));
while ((line = bufferedReader.readLine() )!= null){
bufferedWriter.write(line);
bufferedWriter.newLine();
}
bufferedReader.close();
bufferedWriter.close();
}
这里做个测试用字符流操作图片、视频等二进制文件
别的代码不变只修改路径文件
String readPath = "d:/IDEA2019.png";
String writePath = "d:/b.png";
发现生成了b.png但是文件无法打开
总结
:用字符流操作二进制文件会造成文件损坏!
2、BufferedInputStream和BufferedOutputStream
(1)BufferedInputStream
BufferedInputStream是字节流
,在创建BufferedInputStream时,会创建一个内部缓冲区数组。
(2)BufferedOutputStream
BufferedOutputStream是字节流,实现缓冲的输出流,可以将多个字节写入底层输出流中,而不必对每次字节写入调用底层系统。
综合使用:
完成图片的拷贝
String inPath = "d:\\up.png";
String outPath = "d:\\1.png";
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream(inPath));
bos = new BufferedOutputStream(new FileOutputStream(outPath));
byte[] buff = new byte[1024];
int num;
//当返回-1时表示读取完毕
while ((num=bis.read(buff)) != -1){
bos.write(buff , 0 , num);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//关闭流,关闭外层流即可,底层会关闭节点流
try {
if (bis != null){
bis.close();
}
if (bos != null){
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
3、对象流-ObjectInputStream和ObjectOutputStream
对象流顾名思义是用来处理对象的
那为什么需要对象处理流呢?来看几个需求
1.将int num=100这个int类型数据保存在文件中,注意不是保存100这个数字,而是要保存int 类型的100,并且能够从文件中直接恢复int 类型的100。(这里你可能会说数字100不就是int类型的吗?你在文件中直接写入100它也可能会是String类型的类型是不确定的!)我们在保存一个数据的值时还希望能把他数据类型保存下来。
2.将Dog dog = new Dog(“旺财”,3);这个dog对象保存在文件中,并且能够从文件中恢复(恢复回来仍然是一个dog对象)。
对于上面这两个需求我们就需要用到序列化和反序列化
序列化和反序列化
序列化就是在保存数据时,保存数据的值 和 数据类型
反序列化就是在恢复数据时,恢复数据的值 和 数据类型(将保存在文件中的值和数据类型重新恢复成对象)
对于要序列化对象的类要去实现Serializable接口或者Externalizable接口
这里ObjectInputStream和ObjectOutputStream提供了对基本类型和对象类型的序列化和反序列化的方法
ObjectOutputStream提供序列化功能
ObjectInputStream提供反序列化功能
ObjectInputStream常用的读取方法:
ObjectOutputStream 常用写入方法与ObjectInputStream的方法对应这里不进行列举了。
使用ObjectOutputStream 序列化 基本数据类型和一个 Dog对象(name,age)并保存在data.txt中
public class ObjectOutputStream_ {
public static void main(String[] args) {
String filePath = "d:\\data.txt";
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeInt(100);
oos.writeBoolean(true);
oos.writeChar('a');
oos.writeDouble(1.9);
oos.writeUTF("是个码盗");
oos.writeObject(new Dog("旺财",3));
} catch (IOException e) {
e.printStackTrace();
}
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Dog implements Serializable {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
使用ObjectInputStream读取data.txt 并反序列化恢复数据
//指定反序列化的文件
String filePath = "d:\\data.txt";
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(filePath));
System.out.println(ois.readInt());
System.out.println(ois.readBoolean());
System.out.println(ois.readChar());
System.out.println(ois.readDouble());
System.out.println(ois.readUTF());
System.out.println(ois.readObject());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
注意这里进行反序列化(读取)的顺序要和反序列化(保存数据)的顺序一致
注意事项和细节说明:
- 序列化对象时,默认将里面所有的属性都进行序列化,但除了static或transient修饰的成员
- 序列化对象时,要求其属性的类型也需要实现序列化接口(基本类型其包装类都默认实现了序列化接口,引用类型需实现序列化接口否则报错!)
- 序列化具备可继承性,也就是说如果父类实现了序列化接口,它的所有子类也默认实现了,无需实现序列化接口!
想了解更多序列化和反序列化 点这里
4、标准输入输出流
类型 | 默认设备 | |
---|---|---|
System.in 标准输入 | inputStream(真正的运行类型是BufferedInputStream) | 键盘 |
System.out 标准输出 | printStream | 显示器 |
System类中的两个成员变量:
public static final InputStream in “标准”输入流。 是从键盘获取数据的
Scanner scanner = new Scanner(System.in);这就是为什么建议用完Scanner后进行关闭实际上传入的是BufferedInputStream
public static final PrintStream out “标准”输出流。
输出语句其本质是IO流操作,把数据输出到控制台
5、转换流-InputStreamReader 和 OutputStreamWriter
转换流解决了编码方式造成的问题(先剧透一下)
作用:把一种字节流转换成字符流
那为什么要把字节流转换成字符流呢?举个例子
创建一个lm.txt文件,我们用BufferedReader读取文件内容并输出在控制台
String path = "d:\\lm.txt";
BufferedReader br = new BufferedReader(new FileReader(path));
System.out.println(br.readLine());
br.close();
//这里省略了异常处理
成功输出了
查看文件编码方式
如果这里不是utf-8呢?是其它编码方式呢?我把它改成国标码ANSI
再次运行程序,发现出现了乱码
这里就会产生个问题,如果你读取的文件它的编码不是utf-8文件,那就会出问题!也就是说转换流的编码必须和文件的一致否则会乱码!代码测试后发现写入文件中也是乱码!
这里我们就要用到转换流解决这个问题了
转换流可以把字节流转换成字符流,而字节流可以指定一个编码方式,我们用字节流指定一个编码后,再把它转换成字符流这个问题就解决了!
(1)InputStreamReader
是Reader的子类,可以将InputStream(字节流)包装成Reader(字符流)
注:这里包装成也可以理解成转换成,包装更合理是因为在底层并没有创建一个新的流,而是用的字节流,在处理时用字符方式处理的
代码实现:
String path = "d:/lm.txt";
//将FileInputStream 转成 InputStreamReader 并指定编码为gbk
InputStreamReader isr = new InputStreamReader(new FileInputStream(path), "gbk");
//处理流包装 InputStreamReader (效率高)
BufferedReader bufferedReader = new BufferedReader(isr);
System.out.println(bufferedReader.readLine());
//关闭外层流
bufferedReader.close();
运行成功,解决了乱码问题!
(2)OutputStreamWriter
Writer的子类,实现将OutputStream(字节流)包装成Writer(字符流)
代码实现:
String path = "d:/cs.txt";
//将FileInputStream 转成 OutputStreamWriter 并指定文件编码为gbk
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(path),"gbk");
//这里直接用节点流操作了
osw.write("hi,是个码盗");
osw.close();
打开文件查看编码:确实为ANSI(gbk)
这里指定编码是啥,文件最后的编码就为啥!
总结:转换流可以用来解决指定编码方式造成的乱码问题。
6、打印流-PrintStream 和 PrintWriter
这里注意:打印流只有输出流没有输入流
(1)PrintStream
查看PrintStream的构造方法
说明打印流不仅仅只能打印在显示器上,也可以打印在文件中!
演示PrintStream,废话不多说上代码
//System.out 本身就是一个字节打印流
PrintStream out = System.out;
//在默认情况下,PrintStream 输出数据的位置是 标准输出 即显示器
out.print("你好,是个码盗");
//换行
out.println();
out.write("一起学Java".getBytes());
//我们可以修改打印流输出的位置,输出到a.txt中
System.setOut(new PrintStream("d:/a.txt"));
System.out.print("打印在文件中");
out.close();
这里解释一下上面的代码:
- 我们点进System.out的源码,发现本身就是PrintStream
- 在默认情况下,PrintStream 输出数据的位置是显示器
- print的底层使用的是writer,所以我们可以直接调用write()进行打印
我们点进print源码
(2)PrintWriter
查看PrintWriter的构造方法
使用方式:
String path = "d:\\b.txt";
// PrintWriter printWriter = new PrintWriter(System.out);默认输出到控制台
PrintWriter printWriter = new PrintWriter(new FileWriter(path));
printWriter.print("一起学io");
//这里一定要关闭!!!不关闭文件没有被写入内容
printWriter.close(); //flush + 关闭流 文件才会被写入
properties
先来看一个需求:
有一个配置文件io.properties内容如下
我们怎么读取到ip、user、pwd的值?‘
1.传统方法实现
BufferedReader br = new BufferedReader(new FileReader("src/io.properties"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
成功读取到了配置文件的内容
这里我们是读取到了一整行数据而不是对应的值!需要做一些修改
因为读取出来的是字符串,可以使用String类的 split方法
//其他不变只修改while里的
String[] split = line.split("=");
System.out.println(split[0]+"值是:" +split[1]);
读取结果
这里传统方法已经解决了,但是对于一些操作,例如:修改,增加以及单独查询某条都是比较麻烦的。查询可以通过判断来实现但是当代文件配置信息特别大时,判断的代码会特别的庞大!这时候就需要properties类来方便的实现
properties类
基本介绍:
专门用于读写配置文件的集合类
配置文件的格式:
键=值
注意:键值对不需要有空格,值不需要用引号引起来,默认类型是String。
常用方法:
- load:加载配置文件的键值对到Properties对象
- list:将数据显示到指定设备
- getProperty(key):根据键获取值
- setProperty(key,value):设置键值对到Properties对象
- store:将properties中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为Unicode码
properties类的使用
1、读取properties文件
加载并读取输出配置文件的键值对中
//1.创建 Properties 对象
Properties properties = new Properties();
//2.加载指定配置文件
properties.load(new FileReader("src/io.properties"));
//3.把键值对显示到 控制台
properties.list(System.out);
运行结果:
根据 key 获取相应的值
//4.根据key 获取相应的值(user配置文件中有)
String user = properties.getProperty("user");
System.out.println("用户名="+user);
//配置文件没有相应的键
String u = properties.getProperty("u", "值不存在");
System.out.println(u);
结果:
getProperty(String key) 和 getProperty(String key, String defaultValue)的区别:
二者都是搜索指定的键对应的值,不同的是当搜索结果不存在时getProperty(String key) 会返回null,getProperty(String key, String defaultValue)会返回你设置的defaultValue。
2、创建新的properties文件
保存键值对到文件中,需要配合使用setProperty方法。需要传入输出流和一个String comments,String comments:注释,用来解释说明保存的文件是做什么用的,这里使用中文,文件会保存它的unicode编码。一般使用""空字符串
创建并添加键值对到新的properties文件中
Properties properties = new Properties();
//创建
properties.setProperty("charset","utf8");
properties.setProperty("username","汤姆");
properties.setProperty("password","123456");
//将键值对存储到文件中 mysql.properties未创建
properties.store(new FileWriter("src/mysql.properties"),null);
运行发现在src目录下创建了mysql.properties
这里我把comments设置不为null,文件上面会出现注释
可以使用Unicode编码转换工具,查看中文内容
这里有个注意点:
用FileWriter保存username=汤姆正常显示汤姆,而用FileOutputStream保存文件中保存的是汤姆的Unicode码。
3、修改properties文件内容
修改键值对就很简单了,还是使用setProperty方法,键为文件里存在的键对值进行修改即可!
比如我要修改刚创建的mysql.properties 中的charset为gbk
properties.setProperty("charset","gbk");
运行刚才代码,修改成功!