掌握常见流的读写操作、文件复制、对象序列化和反序列化的含义、理解序列化版本号的含义、自定义固定序列化版本号等操作。
文章目录
什么是IO流
IO就是Input、Output,输入输出相对于内存而言,可以把内存作为参照物,从内存中出来就是输出、到内存中去就是输入。输入输出还有一种形象的说法就是读、写。
“流”实际上就是“数据移动”的形象说法。
IO流的分类
- 根据流的方向分为输入流、输出流。相当于内存而言。
- 根据流的数据单位分为字节流、字符流。
1、字节流以字节为单位,一次读取一个字节,属于万能流,任何类型文件都可以读写,但是文件是汉字容易出现乱码,汉字是两个字节,字符流读的时候,会读到一般,如果直接写出,会出现乱码。
2、字符流以字符为单位,一次读取一个字符。读写普通文档,不容易出现乱码,不适合读写图片、声音、视频等文件。 - 根据流的功能分为节点流、处理流(包装流)。
1、节点流直接对目标设备进行操作。
2、处理流(包装流)一般是对节点进行包装,生成更大功能的流。
Java对流的支持
API中java.io下面的流很对,但分了四大类:字节输入流(InputStream)、字节输出流(OutputStream)、字符输入流(Reader)、字符输出流(Writer),这四类没有父类,并且都是抽象类,无法实例化。
在java中以Stream结尾的都是字节流、以Reader\Write r结尾的都是字符流。
所有的流都实现了:java.io.Closeable接口,都是可关闭的,都有close()方法。流毕竟是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费资源。
所有的输出流都实现了:ava.io.Flushable接口,都是可刷新的,都有flush()方法。刷新表示将通道中剩余未输出的数据强行输出完。作用就是清空通道。如果没有flush方法,可能导致文件数据丢失。
掌握常用流
java.io包下 需要掌握的流有16个,文件流四个、转换流(将字节流转换为字符流)两个、缓冲流四个、数据流两个、打印流两个、对象流两个。
文件流
文件流主要包括四个:文件字节输入流(FileInputStream)、文件字节输出流(FileOutputStream)、文件字符输入流(FileReader)、文件字符输出流。
FileInputStream
按照字节方式读取文件,事先创建一个txt文件,然后读取它,使用方法有read()、available()、skip()等方法
代码如下:
注意写代码时候的异常抛出情况、流的关闭
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/*文件字节输入流,万能的,可以读任何类型的文件
字节方式读取文件,完成输入操作,完成读的操作(从硬盘-->内存)
一个字节一个字节读 效率低 内存与硬盘交互太频繁
*/
public class FileInputStreamTest01 {
public static void main(String[] args) {
//初始化为null
FileInputStream fis =null;
//创建文件字节输入流对象
//文件路径: D:\test\temp.txt
try {
fis = new FileInputStream("D:\\test\\temp.txt");
//开始读
/* int b = 0;
while((b=fis.read())!=-1){
System.out.println(b);
}
读出来的是字节
*/
//创建字节数组 往字节数组中读 然后通过构造方法将字节转换为字符串
byte[] bytes = new byte[3];
//读取的字节数
int count =0;
while((count = fis.read(bytes))!=-1){
System.out.print(new String(bytes,0,count));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
//如果流不为空 进行关闭
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
使用available方法获取文件字节数,然后创建字节数组,再在改字节中读,就可以不使用循环,但是不能用于读太大的文件
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/*
提高程序执行效率 在内存中准备byte[] 数组 ,一次可以读取多个字节
int read(byte[] b )
*/
public abstract class FileInputStreamTest02 {
public static void main(String[] args) {
//初始化
FileInputStream file = null;
//创建文件字节输入流对象
try {
file = new FileInputStream("D:\\idea\\Practise\\src\\com\\lic\\File");
//available方法 剩余多少字节没读
System.out.println("总字节数:"+file.available());
//创建字节数组 往该字节中读
byte[] bytes1 = new byte[file.available()]; //不适合读太大的文件 因为byte数组不能太大
file.read(bytes1);
System.out.println(new String(bytes1));
file.skip(3); //跳跃几个字节不读
System.out.println(file.read());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (file != null) {
try {
file.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileOutputStream
FileOutputStream是文件字节输出流,通过write方法在磁盘上写出,可以一个字节一个字节写,也可以字节数组写。文件不存在会新建 存在文件会先将文件清空 再写进去。
注意输出流一定要有flush()方法刷新,将剩余未输出的数据强行输出完。作用就是清空通道。如果没有flush方法,可能导致文件数据丢失。
代码:
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamTest01 {
public static void main(String[] args) {
FileOutputStream file = null;
FileOutputStream file1 = null;
try {
//创建对象
//文件不存在会新建 存在文件会先将文件清空 再写进去
file = new FileOutputStream("test01");
//在文件末尾写入,不会清空原文件
file1 = new FileOutputStream("D:\\idea\\Practise\\src\\com\\lic\\File",true);
//test01文件
//写一个字节数组
byte[] bytes = {97,98,99,100};
file.write(bytes);
//从下标0开始写 写2个字节
file.write(bytes,0,2);
//File文件
// 写一个字符串
String s = "哈哈哈哈哈哈哈";
byte[] bytes1 = s.getBytes();
file1.write(bytes1);
//记得刷新
file.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (file != null) {
try {
file.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
写入的结果:
文件拷贝
文件拷贝的原理就是一边读一边写,文件字节输入流和文件字节输出流联合起来完成文件的复制。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class Copy {
public static void main(String[] args) {
//初始化
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//创建对象
fis = new FileInputStream("D:\\test\\picture\\IMG_1068(20210409-103539).JPG");
fos = new FileOutputStream("D:\\test\\copy\\IMG_1068(20210409-103539).JPG");
//边读边写
int count = 0;
byte[] bytes = new byte[1024*1024]; //一次读取1MB 字节
while((count = fis.read(bytes)) != -1){
fos.write(bytes,0,count);
}
//文件字节输出流刷新
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}//关闭流
finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
完成文件拷贝
FileReader
照葫芦画瓢。
文件字符输入流,代码和文件字节输入流相似,不过FileInputStream是使用byte数组、 FileReader使用char数组,一次读取一个字符。
示例代码:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
/*文件字符输入流
只能读取普通文件
方便 快捷
使用char数组
*/
public class FileReaderTest01 {
public static void main(String[] args) {
//初始化
FileReader fileReader = null;
try {
//创建文件字节输入流
fileReader = new FileReader("D:\\idea\\Practise\\src\\com\\lic\\io\\FileReader");
//开始读 char[]数组
char[] chars = new char[16];
int count = 0 ;
while ((count = fileReader.read(chars)) != -1){
System.out.print(new String(chars,0,count));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileReader != null) {
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
FileWriter
文件字符输出流,方便快捷,可以直接写字符串。
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterTest {
public static void main(String[] args) {
FileWriter fileWriter = null;
try {
// //在文件末尾写入,不会清空原文件
fileWriter = new FileWriter("fileTest",true);
//字符数组
char[] chars = {'中','秋','节','快','乐'};
//开始读 char数组内容
fileWriter.write(chars);
fileWriter.write("\n");
//读字符串内容
fileWriter.write("教师节快乐");
//刷新
fileWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fileWriter != null) {
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
文件拷贝
一边读一边写,char数组,数组下标是-127到128.
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class Copy02 {
public static void main(String[] args) {
FileReader fileReader = null;
FileWriter fileWriter = null;
try {
fileReader = new FileReader("D:\\test\\temp.txt");
fileWriter = new FileWriter("file");
int count = 0;
char[] chars = new char[1024*512]; //一次读取1MB 字节
while ((count = fileReader.read(chars)) != -1){
fileWriter.write(chars,0,count);
}
//刷新
fileWriter.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
缓冲流
缓冲流主要是提高效率,减少物理读取次数。主要有 BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream。
前两个字符缓冲流、后两个字节缓冲流,方法都一样,示例字符缓冲流。
BufferedReader
BufferedReader提供的readLine()方法可以读取一行,采用BufferedReader对Reader进行装饰,BufferedReader先将数据读到缓存里,Java程序再次读取数据时,直接到缓存中读取,减少Java程序物理读取次数,提高性能。
代码:
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
//使用缓冲流 不需要自定义数组 自带缓冲
public class BufferedReaderTest {
public static void main(String[] args) {
BufferedReader bf = null;
try {
//当一个流的构造方法有另一个流 传进来的流叫节点流
//外部负责包装的流 叫包装流/处理流
bf = new BufferedReader(new FileReader("fileTest"));
String s= null;
//读一个文本行
while((s = bf.readLine()) != null){
System.out.println(s);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bf == null) {
try {
//关闭流 只需要关闭包装流 节点流会自动关闭
bf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
BufferedWriter
//带有缓冲的字符输出流
import java.io.*;
//带有缓冲的字符输出流
public class BufferedWriterTest {
public static void main(String[] args) {
BufferedWriter bufferedWriter = null;
try {
bufferedWriter = new BufferedWriter(new FileWriter("BufferedWriter"));
//bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("BufferedWriter")));
bufferedWriter.write("hello");
bufferedWriter.write("\n");
bufferedWriter.write("world");
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (bufferedWriter != null) {
try {
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
转换流
转换流主要有 InputStreamReader和 OutputStreamReader
字节流转换成字符流
InputStreamReader
InputStreamReader是将字节输入流转换成字符输入流
构造BufferedReader对象时候,参数是reader,假设是FileInputStream流,可以通过InputStreamReader将FileInputStream转换成FileReader
bufferedReader = new BufferedReader(
new InputStreamReader(
new FileInputStream("fileTest")));
OutputStreamWriter
OutputStreamWriter是将字节输出流转换成字符输出流
bufferedWriter = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("BufferedWriter")));
数据流
数据专属的流,可以用来加密文件
DataOutputStream
import java.io.*;
/*
数据流
可以将数据连同数据的类型一并写进文件
不是普通文档,(记事本打不开)
*/
public class DataOutputStreamTest {
public static void main(String[] args) {
DataOutputStream dataOutputStream = null;
//构造方法 参数传OutputStream
try {
dataOutputStream = new DataOutputStream(new FileOutputStream("data") );
//写数据 把数据和数据类型一并写进去
dataOutputStream.writeByte(12);
dataOutputStream.writeShort(13);
dataOutputStream.writeInt(14);
dataOutputStream.writeDouble(12.2);
dataOutputStream.writeFloat(12.0f);
dataOutputStream.writeLong(233l);
dataOutputStream.writeChar('c');
dataOutputStream.writeBoolean(true);
//刷新
dataOutputStream.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (dataOutputStream != null) {
try {
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
如果用记事本打开,会乱码。
DataInputStream
DataOutputStream写的文件 只能使用DataInputStream去读。
还要求 读的顺序必须和写的顺序相同 读的时候 必须已知写的什么
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
//DataOutputStream写的文件 只能使用DataInputStream去读
//还要求 读的顺序必须和写的顺序相同 读的时候 必须已知写的什么
public class DataInputStreamTest {
public static void main(String[] args) {
DataInputStream dataInputStream = null;
try {
dataInputStream = new DataInputStream(new FileInputStream("data"));
//开始读
//读的顺序必须和写的顺序相同 读的时候 必须已知写的什么
System.out.println(dataInputStream.readByte());
System.out.println(dataInputStream.readShort());
System.out.println(dataInputStream.readInt());
System.out.println(dataInputStream.readDouble());
System.out.println(dataInputStream.readFloat());
System.out.println(dataInputStream.readLong());
System.out.println(dataInputStream.readChar());
System.out.println(dataInputStream.readBoolean());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (dataInputStream != null) {
try {
dataInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
打印流
打印流主要包含两个:PrintStream和PrintWriter,对应字节流和字符流。
PrintStream
System.out对应的是PrintStream,默认输出到控制台,我们可以重新定向输出。不输出到控制台,输出到文件。
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;
public class PrintStreamTest {
public static void main(String[] args) {
PrintStream printStream = null;
try {
printStream = new PrintStream(new FileOutputStream("print"),true);
//改变输出方向
System.setOut(printStream);
//输出到print文件
System.out.println("日志文件,不会关机消失,写到磁盘中。\n日期:"+
new SimpleDateFormat("yyyy-MM-dd").format(new Date())+
"发生打印事件");
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if (printStream != null) {
printStream.close();
}
}
}
}
运行结果:
PrintWriter
可用来创建一个文件并向文本文件写入数据。可以理解为java中的文件输出。
import java.io.*;
public class PrintWriterTest {
public static void main(String[] args) throws IOException {
PrintWriter p = null;
BufferedReader bf = null;
//创建输出流 写文件
p = new PrintWriter(new FileOutputStream("printWriter"));
//开始写
p.write("hello");
p.write("world");
p.flush();
p.close();
//开始读
//创建输入流 读到控制台
bf = new BufferedReader(new InputStreamReader(new FileInputStream("printWriter")));
String s = null;
while ((s = bf.readLine()) != null){
System.out.println(s);
}
/*
bf = new FileReader("printWriter");
char[] chars = new char[5];
int count = 0;
while ((count = bf.read(chars)) != -1) {
System.out.println(new String(chars,0,count));
}
*/
bf.close();
}
}
对象流
对象流将内存中的Java对象转换成二进制写入磁盘,这个过程称作序列化:Serialize(拆分对象),并且可以从磁盘中读出完整的Java对象,这个过程称作反序列化:DeSerialize(组装对象)。
对象流主要包括:ObjectOutputStream、ObjectInputStream
序列化的实现
参与序列化的类必须实现Serializable接口,这个接口标记这个类是可序列化的。
java虚拟机会默认提供这个类的序列化版本号。
序列化多个对象使用List集合。
写出来的数据相当于加密文件
主类:
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
public class ObjectOutputStreamTest02 {
public static void main(String[] args) throws Exception {
List<User> userList = new ArrayList<>();
userList.add(new User(001,"张三"));
userList.add(new User(002,"张四"));
userList.add(new User(003,"张五"));
userList.add(new User(004,"张六"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("userList"));
oos.writeObject(userList);
//刷新
oos.flush();
//关闭
oos.close();
}
}
User类
import java.io.Serializable;
//实现序列化接口 Serializable
//标志接口 给java虚拟机看 为该类自动生成序列化版本号
public class User implements Serializable {
int id;
String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
运行结果:
这个内容是序列化的结果,等同于乱码。可以根据反序列化恢复文件。
反序列化
通过反序列化可以将序列化到磁盘中的数据恢复到内存中,直接恢复成java对象。
恢复数据相当于解密文件。
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;
public class ObjectInputStreamTest02 {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("userList"));
//反序列化集合
for (User user : (List<User>) ois.readObject()) {
System.out.println(user);
}
//关闭流
ois.close();
}
}
运行结果:
transient关键字
不想让某个字段参与序列化,用transient关键字进行修饰,反序列化出来结果就是null。
public class User implements Serializable {
int id;
String transient name; //不参与序列化
}
序列化版本号
Java语言通过什么区分类?一是比对类名;二是靠序列化版本号比对。
一个类实现Serializable,自动生成序列化版本号,优点是可以区分类;缺点是代码确定后,不能后续修改,如果修改,会重新编译,生成新的序列化版本号,java虚拟机这时候就认为这是全新的类,java认为是不兼容的两个类。
建议序列化版本号手动写出。
//添加一个固定的序列化版本号
private static final long serialVersionUID = 123456789L;
这样,无论后续代码怎样升级,他的序列号是一样的,不会产生兼容的问题
File类
- 在IO包里,唯一表示与文件本身有关的类是File类,进行文件创建、删除操作。
- File对象是文件或目录路径的抽象表示形式,
- File和流没有关系
- 通过File无法完成文件读写。
常用方法
掌握File类的常用方法,学会创建文件、目录,查找文件、目录的绝对路径,获取文件最后修改时间(和Date类关联),获取父目录、子目录等方法。
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/*文件类的常用方法
创建文件、目录
查找文件、目录的绝对路径
获取最后修改时间
获取父目录
*/
public class FileTest01 {
public static void main(String[] args) throws IOException {
//创建file对象
File file = new File("D:\\a");
//如果文件不存在 创建文件
if(!file.exists()){
file.createNewFile();
}
System.out.println(file.exists()); //true
//获取绝对路径
System.out.println("文件的绝对路径:"+file.getAbsolutePath());
//获取文件名
System.out.println("文件名:"+file.getName());
//获取最后修改时间
long l = file.lastModified();
//格式
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
System.out.println("最后修改时间:"+simpleDateFormat.format(new Date(l)));
File file1 = new File("D:\\test\\picture");
//如果不存在 则创建目录
if (!file1.exists()) {
file1.mkdir();
}
//获取父目录
System.out.println("父目录:"+file1.getParent());
//获取目录名
System.out.println("目录名"+file1.getName());
//判断是否是目录
System.out.println("是不是目录:"+file1.isDirectory());
//获取目录下所有子文件
File file2 = new File("D:\\test");
File[] files = file2.listFiles();
for (File f: files
) {
System.out.println("文件名:"+file2.getName()+" 绝对路径:"+file2.getAbsolutePath());
}
}
}
目录文件拷贝
实现目录文件的拷贝,首先确定拷贝源和拷贝目标,拷贝目标到时候会新建。
我们实现一个方法,叫copy方法,传入参数拷贝源和拷贝目标。
在这个方法中,我们分两部分,文件部分、目录部分。
确定了是文件的话,我们采用边读边写,创建字节输入输出流对象,输入流对象就是传的拷贝源。输出流对象比较复杂,是拷贝目标路径返回的字符串连接拷贝源的绝对路径从3下标开始截取的字符串,这样就是目标文件的绝对路径。下来就是边读边写了,创建字节数组,调read方法,开始写。
确定是目录的话,我们需要调用listFiles方法去获取子目录,拷贝目标路径返回的字符串连接拷贝源的绝对路径从3下标开始截取的字符串,这就是目标目录的绝对路径,获取目标目录后,我们就可以新建目录。
因为拷贝源不一定只有一个目录,也有可能目录套目录,所以我们采用递归,递归传入参数,拷贝源,目标目录。
这样,就完整的实现目录文件的拷贝。
import java.io.*;
//目录拷贝 边读边写
public class Copy03 {
public static void main(String[] args) {
//拷贝源
File file1 = new File("D:\\test");
//拷贝目标
File file2 = new File("D:\\ab");
copy(file1,file2);
}
private static void copy(File file1, File file2) {
//如果是文件 递归结束 边读边写
if(file1.isFile()){
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream(file1);
//目标文件的绝对路径 :目标路径+拷贝目标的子目录
String path =( file2.getAbsolutePath().endsWith("\\")? file2.getAbsolutePath():file2.getAbsolutePath()+"\\")
+ file1.getAbsolutePath().substring(3);
out = new FileOutputStream(path);
byte[] bytes = new byte[1024*1024];
int count = 0;
while ((count = in.read(bytes))!=-1){
out.write(bytes,0,count);
}
out.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return;
}
//获取源下面的子目录
File[] files = file1.listFiles();
for (File f: files) {
if(f.isDirectory()){
//目标目录
String s =( file2.getAbsolutePath().endsWith("\\")? file2.getAbsolutePath():file2.getAbsolutePath()+"\\")
+ f.getAbsolutePath().substring(3);
System.out.println("目标文件的绝对路径"+s);
File file = new File(s);
if (!file.exists()){
//新建目录方法名特别注意 mkdirs与mkdir不一样
file.mkdirs();
System.out.println(file.getAbsolutePath());
}
}
copy(f,file2);
}
}
}