Java IO
I : Input 硬盘 -> 内存
O:Output 内存 -> 硬盘
1.1 IO流的分类
1、按照流的方向进行分类,以内存为参照物。
->内存:叫做输入,即读; 内存->:叫做输出,即写。
2、按照读取数据的方式不同进行分类
有的流是按照字节的方式读取数据,一次读取一个字节 (byte) ,等同于一次读取8个二进制位。 这种流是万能的,什么类型的文件都能读,包括文本文件、图片、声音文件….
- 第一次读:一个字节,正好读到 ‘h’
- 第二次读:一个字节,读到 ‘中’ 的一半
- 第三次读:一个字节,读到 ‘中’ 的另一半
有的流是按照字符的方式读取,一次读取一个字符,这种流是为了方便读取普通文本而存在的, 这种流不能读取图片、声音、视频等文件,只能读取文本文件,甚至连word文件都无法读取。
- 第一次读:读到 ‘a’ 字符(’a’ 字符在windows系统中占1个字节)
- 第二次读:读到 ‘中’ 字符(’中’ 字符在windows系统中占用2个字节)
Java IO流的四大家族:
以 Stream
结尾的都是字节流java.io.InputStream
java.io.OutputStream
以 Reader
或 Writer
结尾的都是字符流java.io.Reader
java.io.Writer
这四个流都是抽象类 abstract class
, 所有的流都实现了 java.io.Closeable
接口,都有 close()
方法。流毕竟是一个管道,这是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费很多资源。
所有的输出流都实现了 java.io.Flushable
接口,都有 flush()
方法。输出流在最终输出的时候,一定要记得调用 flush
方法,将管道中的剩余数据输出,以清空管道,否则可能导致丢失数据。
1.2 文件流
java.io.FileInputStream
java.io.FileOutputStream
java.io.FileReader
java.io.FileWriter
1.2.1 FileInputStream
int read() 从输入流中读取一个字节,返回读取的字节码值
try (FileInputStream fis = new FileInputStream(
"/Users/fangshiqi/dev/java-io/src/main/resources/file/test.txt")) {
// 标准写法,当读到-1时,表示已经读到文件末尾
int readData = 0;
while ((readData = fis.read()) != -1) {
System.out.println(readData);
}
} catch (IOException e) {
e.printStackTrace();
}
以上程序的缺点:
- 一次读一个字节,这样内存和硬盘交互太频繁,基本上时间/资源都耗费在IO上;
int read(byte[] b) 从输入流中将最多b.length个字节的数据读如一个byte数组中,返回读取到的字节数
// 在idea中,默认当前路径是工程的根目录
String filePath = "./file/test.txt";
try (FileInputStream fis = new FileInputStream(filePath)) {
byte[] bytes = new byte[5];
int n = 0;
// 将读到的byte数组转换为字符串
// 返回-1表示没读到
while ((n = fis.read(bytes)) != -1) {
// 读取多少个字节,转换多少个
System.out.print(new String(bytes, 0, n));
}
} catch (Exception e) {
e.printStackTrace();
}
int available() 返回文件剩余还没读的字节数量;
public static void available() {
try(FileInputStream fis = new FileInputStream(filePath)) {
System.out.println("文件总字节数: " + fis.available());
} catch (IOException e) {
e.printStackTrace();
}
}
long skip(int n) 跳过几个字节不读取,返回实际跳过的字节数;
/**
* long skip(int n) 跳过几个字节不读取
*/
public static void skip() {
try (FileInputStream fis = new FileInputStream(filePath)) {
long skip = fis.skip(3);
System.out.println("跳过3" + skip + "个字节,
"还剩" + fis.available() + "个字节");
} catch (IOException e) {
e.printStackTrace();
}
}
1.2.2 FileOutputStream
文件字节输出流,负责写文件,内存 —-> 硬盘
void write(byte[] b) 将b.length个字节从指定byte数组写入次文件输出流中。
// ./file/out.txt 文件在不存在时会自动新建,但如果文件夹都没有就会报错
// java.io.FileNotFoundException
// 并且该方式会将原文件清空,然后重新写入
try (FileOutputStream fos = new FileOutputStream("./file1/out1.txt")) {
String message = "hello world";
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
fos.write(bytes);
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
void write(byte[] b, int start, int end) 将指定的byte数组中从 start 到 end 的字节写入文件输出流中。 FileOutputStream(String name) 创建一个向具有指定 name 的文件中写入数据的文件输出流,当指定的文件不存在时会自动创建该文件,但如果指定的目录不存在则会报 java.io.FileNotFoundException 异常。 FileOutputStream(String name, boolean append) 创建一个向具有指定 name 的文件中写入数据的输出文件流
- append 为 true 时表示以追加的方式在文件的末尾写入;
FileOutputStream(File file, boolean append) 创建一个向指定 File 对象表示的文件中写入数据的文件输出流
1.2.3 综合案例—-拷贝文件
/**
* 拷贝文件,从{@code src}->{@code dst}
* <p>目标文件如果不存在则会自动创建,但其目录必须存在
*
* @param src 原文件路径
* @param dst 目标文件路径
*/
public static void copyFile(String src, String dst) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 创建一个输入流对象
fis = new FileInputStream(src);
fos = new FileOutputStream(dst);
byte[] bytes = new byte[1024 * 1024]; // 一次拷贝1M
int readCount = 0;
while ((readCount = fis.read(bytes)) != -1) {
fos.write(bytes, 0, readCount);
}
// 刷新输出流
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 分开try, 如果一起try,其中一个流的异常会影响到另一个流的关闭.
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.2.4 FileReader
文件字符输入流,只能读取普通文本。
在读取文本内容时,比较方便、快捷。
public static void readByte() {
// 创建文件字符输入流
try (FileReader reader = new FileReader("file/reader.txt")) {
// 开始读
char[] chars = new char[4]; // 一次读取4个字符
int readCount = 0;
while ((readCount = reader.read(chars))!= -1) {
System.out.println(new String(chars, 0, readCount));
}
} catch (IOException e) {
e.printStackTrace();
}
}
1.2.5 FileWriter
void write(char[] chars)
public static void writerBytes() {
try(FileWriter writer = new FileWriter("./file/writer.txt")) {
// 开始写
char[] chars = {'中', '华', '有', '为'};
writer.write(chars);
// 刷新
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
void write(String str)
public static void writerString() {
try(FileWriter writer = new FileWriter("./file/writer.txt")) {
// 开始写
String message = "尔奉尔禄,民脂民膏";
writer.write(message);
// 刷新
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
1.3 缓冲流
1.3.1 BufferedReader
- 节点流:当一个流的构造方法中需要另一个流时,这个被传进来的流称为节点流
- 包装流:包装流也即处理流,外部负责包装的这个流叫做包装流
对于包装流来说,只需要关闭最外层的流即可,里面的节点流会自动关闭。
使用 BufferedReader
可以每次读一行数据
public static void readLine() {
BufferedReader br = null;
try {
FileReader reader = new FileReader("./file/reader.txt");
br = new BufferedReader(reader);
// 当一个流的构造方法中需要一个流时,这个被传进来的流称为节点流
// 外部负责包装的这个流,叫做包装流,也即处理流
// 像当前程序,FileReader被称为节点流,BufferedReader就是包装流/处理流
String line = null;
// 当读到最后时,会返回null
while ((line = br.readLine()) != null) {
// 注意这里并不会读到换行符
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭流
// 对于包装流来说,只需要关闭最外层的流即可
// 里面的节点流会自动关闭
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.3.2 BufferedWriter
/**
* 通过使用 {@link OutputStreamWriter} 将 字节流 转换为 字符流
*
* @throws IOException io异常
*/
public static void write() throws IOException {
FileOutputStream fos = new FileOutputStream("out.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos);
BufferedWriter bufferedWriter = new BufferedWriter(osw);
bufferedWriter.write("fuck this world");
bufferedWriter.flush();
}
1.3.3 使用转换流
BufferedReader(Reader reader) 构造器只能传字符流,不能传字节流,若需要传字节流,需要使用转换流进行转换
stream -> reader
/**
* 通过转换流来使用 {@link java.io.InputStream}
*/
public static void readStream() throws IOException {
// 字节流
// 第二个参数设置为true表示追加
FileInputStream fis = new FileInputStream("./file/reader.txt");
// 使用转换流进行转换
InputStreamReader reader = new InputStreamReader(fis);
// 读取字节流的数据
BufferedReader br = new BufferedReader(reader);
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}
1.4 数据流
1.4.1 DataOutputStream
- 数据专属流,他是一个包装流
- 这个流可以将数据连同数据的类型,一并写入文件
这个文件不是普通文本文档,该文件用记事本打不开
public static void output() throws IOException {
// 创建数据专属的字节输出流
// 数据专属流还是一个包装流
final DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));
// 写数据
byte b = 100;
short s = 200;
int i = 300;
long l = 400L;
float f = 3.0F;
double d = 3.14;
char c = 'a';
// 写入
// 将数据于数据类型一并写入文件当中
dos.writeByte(b);
dos.writeShort(s);
dos.writeInt(i);
dos.writeLong(l);
dos.writeFloat(f);
dos.writeDouble(d);
dos.writeChar(c);
dos.writeBoolean(true);
dos.flush(); // 刷新
dos.close(); // 关闭
}
1.4.2 DataInputStream
java.io.DataInputStream
数据字节输入流java.io.DataOutputStream
写的文件只能由 {@link java.io.DataInputStream} 去读并且读的顺序需要和写入的顺序一致才能正常读到数据
/**
* {@link java.io.DataInputStream} 数据字节输入流
* <p> {@link DataOutputStream} 写的文件只能由 {@link java.io.DataInputStream} 去读
* <p> 并且读的顺序需要和写入的顺序一致才能正常读到数据
*/
public static void input() throws IOException {
final DataInputStream data = new DataInputStream(new FileInputStream("data"));
// 开始读
// 读之前需要知道写入的顺序
// 读的顺序要和写的顺序一致
byte b = data.readByte();
short s = data.readShort();
int i = data.readInt();
long l = data.readLong();
float f = data.readFloat();
double d = data.readDouble();
boolean bool = data.readBoolean();
char c = data.readChar();
System.out.println(b + " " + s + " " + i + " " + l + " " + f + " " + d +
" " + bool + " " + c);
data.close();
}
1.5 标准流
1.5.1 PrintStream/PringWriter
获取标准输出流 (字节的方式)
// 获取标准输出流
PrintStream ps = System.out;
改变输出目标
/**
* 改变输出目标
*/
public static void out() throws FileNotFoundException {
// 标准输出流不再指向控制台
// 而是指向log.txt
// 这是日志框架的实现原理
PrintStream ps = new PrintStream(new FileOutputStream("log.txt"));
System.setOut(ps);
System.out.println("hello world");
}
1.6 序列化与对象流
序列化(serialize): Java对象存储到文件中。将 Java 对象的状态保存下来的过程
反序列化(deserialize):将硬盘上的数据重新恢复到内存当中,恢复成 Java 对象
1.6.1 Serializable接口
Serializable
接口起到一个标志的作用,- Java虚拟机看到这个类实现了该接口, 会为该类自动生成一个序列化版本号
序列化版本号的作用:
用于区分类的。 当源代码改动之后,重新便衣后生成了全新的字节码文件
并且class文件再次运行的时候,Java虚拟机生成的序列化版本号也会发生相应的改变
这时若用改动之后的类去读取未改动之前所保存的文件内容,会出现序列化版本不一致的错误.
Java语言中是采用什么机制来区分类的?
第一:首先通过类名进行比对,如果类名不一样,肯定不是同一个类 第二:如果类名一致,会通过序列化版本号进行区分。 也就是说,当实现了
Serializable
接口后,如果对代码进行后续的修改, 编译后会生成全新的序列化版本号码,此时Java虚拟机会认为这是一个全新的类。
最佳实践:**
凡是一个类实现了 Serializable
接口,建议给该类提供一个固定不变的序列化版本号。
这样,以后这个类即使代码修改了,但是版本号不变,JVM会认为是同一个类。
// 手动写明序列化版本号
private static final long serialVersionUID = -1214344247376595885L;
1.6.2 transient 关键字
1.6.3 ObjectInputStream
public static void readObject() throws IOException, ClassNotFoundException {
final FileInputStream fs = new FileInputStream("./sebuntin");
final ObjectInputStream objectInputStream = new ObjectInputStream(fs);
final Object object = objectInputStream.readObject();
if (object instanceof Student) {
Student s = (Student) object;
System.out.println(s.toString());
}
其中 Student
类实现了 Serializable
接口
public class Student implements Serializable {
// 手动写明序列化版本号
private static final long serialVersionUID = -1214344247376595885L;
private int no;
private String name;
// transient关键字表示password不参与序列化
private transient String password;
...
}
1.6.4 ObjectOutputStream
public static void writeObject() throws IOException {
final Student sebuntin = new Student(23, "sebuntin", "123456");
final ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./sebuntin"));
objectOutputStream.writeObject(sebuntin);
objectOutputStream.flush();
objectOutputStream.close();
}
1.7 File类
⚠️注意:File
并不是一个流,所以通过 File
是无法完成文件的读和写的
关于File类的解释: 1、在Java中File是一个文件或目录路径名的抽象表示形式。
C:\Drivers
这是一个File
对象C:\Drivers\Lan\Realtek\Readme.txt
也是File
对象 2、File类和四大家族没有关系,File类不能完成文件的读和写。
1.7.1 构造方法
File(File parent, String child) 根据 parent 抽象路径名和child路径名字符串创建一个新
File
实例 File(String pathname) 通过将给定路径字符床转换为抽象路径名来创建一个新File
实例File(String parent, String child) 根据 parent 路径名字符串和 child 路径名字符串创建一个新
File
实例File(URI uri) 通过将给定的 file:URI 转换为一个抽象路径名来创建一个新的
File
实例
1.7.2 实例方法
boolean exists() 判断文件或目录是否存在
public static void test01() {
// 创建一个file对象
File f1 = new File("./file");
// 判断文件或目录是否存在
System.out.println(f1.exists());
}
boolean createNewFile() 创建文件,如果文件或目录不存在,则会以文件的形式创建。
boolean mkdir() 创建目录,如果文件或目录不存在,则会以目录的形式创建。
boolean mkdirs() 以多重目录形式创建。
String getParent() 获取文件的父路径(以String的形式返回)。
File getParentFile() 获取文件的父路径(以File的形式返回)
File getAbsolutePath() 获取文件或目录的绝对路径。
boolean isDirectory() 判断是否为目录
boolean isFile() 判断该抽象路径名是否为文件
long lastModified() 获取文件最后一次修改时间,返回的数值的单位为毫秒(从1970年到现在的总毫秒数)
案例:io+properties联合应用
/**
* Properties 是一个map集合,key和value都是String类型
* 想将一个userinfo文件中的数据加载到Properties对象当中
*/
public static void createProperties() throws IOException {
final FileReader reader = new FileReader("file/userinfo");
// 新建一个map集合
Properties properties = new Properties();
// 调用load方法, 将文件中的数据加载到内存中
// 文件中的数据顺着管道加载到map集合中
// 其中=左边作key,右边作为value
properties.load(reader);
System.out.println(properties.getProperty("username"));
System.out.println(properties.getProperty("age"));
}
其中 userinfo文件内容为
username=sebuntin
age=23
需要了解的是: Java规范中有要求,属性配置文件建议以
.properties
结尾,但这不是必须的 这种以.properties
结尾的文件在 Java 中被称为:属性配置文件。 其中Properties
类是专门存放属性配置文件内容的一个类在properties文件中,key于value可以使用
=
隔开,也可以使用:
隔开在properties文件中,key重复时,value会自动覆盖。 在properties文件中,# 为注释符 在properties文件中,=两边不要有空格。