title: 常用的IO流
date: 2022-09-06
sidebar: ‘auto’
tags:
- java基础
categories: - 学习小记
序言:
这期笔记和心得是认真看完了韩顺平老师的课之后写下的。重新学了一下这个IO流,完全地巩固了我的对常用的IO流用法和一些底层知识基础,老师讲的还是很到位的,以前不懂得很模糊的知识点现在一下子就通了。其实核心就围绕着两个流,字符流和字节流。视频大概学习了两遍,第一遍是过了一遍,第一遍的时候我已经是基本把以前不懂得问题都可以梳理了,第二遍是想记录一下里面的笔记。不禁感慨,真的是铁打的Java基础,有了一定的基础去学习其他的或者是炒回锅肉都香点易熟点,当然我觉得还是让我收获最大的就是学会去用类比的方式去理解以下比较难理解的和易混淆的知识点。
这个笔记大概分为以下几个部分:
1.明白流的概念,如何区分各个流的作用
2.从整体上熟悉各个流的结构
3.学会以类比的方法去理解各个流的作用以及相关的代码和图片的展示
明白流的概念,如何区分各个流的作用
什么是IO流?对数据进行输入输出的流成为IO流。
当然这里我们不要这么官方的回答,我们要更能明白输入流和输出流。就需要用到类比的一个方法。如图
我们平时所说的 输入输出流是相对于Java文件(内存) 来说的。
其实我们可以理解为:输入流就是将文件内容输入到我们的Java程序中,输出流就是从我们的Java程序中输出到文件中。
类比为人喝水这个情形。
此外,IO流中主要的两个流分别是字节流(InputStream
、OutputStream
)和字符流(Reader
、Writer
);
如何区分其作用呢?
简单笼统地来说就是Stream
字节流是处理字节数据的(word文档,doc,pdf,声音音频此类等等),而Reader
和Writer
是处理字符数据的(UTF-8中的一个汉字占三个字节)。
从整体上熟悉各个流的结构(需要熟悉记忆这个图的相关的关系)
先看InputStream
的常用的子类的和其自身的结构
从图中或者点进去看InputStream
都可以知道该类是基类,也就是我们所说的抽象类。大体上来说,InputStream
有几个比较常用的子类。就如上图;简单讲以下,FileInputStream
类处理的是文件的一个输入流;ObjectIputStream
类处理的是对象的一个输入流;BufferedInputStream
就是一个缓冲流。
OutputStream
常用的子类的和其自身的结构
这里的结构就不一一赘述,下面会详细讲各个子类的具体功能。(这里的printStream
打印流只有输出打印流,没有输入打印流)
Reader
常用的子类的和其自身的结构
这里的Reader
也是一个基类(抽象类),也是需要他的子类实现该类(指的是Reader
)的方法才行。Reader
就是字符流的代表,其中子类为BufferedReader
是缓冲字符流;InputStreamReader
是输入字符流。
Writer
常用的子类的和其自身的结构
同理,Writer
类比Reader
就行了。(这里多了一个PrintWriter
输出字符流)
下面就用代码来展示具体的各个流的作用吧。
关于文件和目录的基本操作和各个流的作用
我们直奔主题:
如何去创建一个文件呢?
这里参考API文档中File
的构造器,有三种方式提供。(我个人喜欢第一种写法,次之第二种)
// 第一种 直接写文件的绝对路径
File file01 = new File("D:\\1.txt");
// 第二种 File(String parent,String child)
String path = "D:\\";
File file02 = new File(path,"2.txt");
// 第三种 File(File parent,String child)
File file03 = new File("D:\\","3.txt");
/*
这里补充一个知识点就是:
这里创建了文件不代表你真的在你的电脑的磁盘中生成相应的文件,他们只是在你的Java程序中生成了文件,但是还没有真正地落地在磁盘中。
此时需要加上一段代码才能真正生成对应的文件。
*/
file01.creatNewFile();
file02.creatNewFile();
file03.creatNewFile();
上面中其实也可以用一个类比的方法;就像是一个母亲怀着孩子(这就类似于Java程序中生成一个文件的操作),但是还没分娩出来之前,你的孩子都还没有占有这个地球的空间资源。就需要一个医生接生这些,让这个孩子落地(这就是类似于creatNewFile()
方法),一旦这个方法实行了,这个孩子就真正落地,就会占有这个地球的空间的一个资源了(类似于你可以再磁盘时看到这个文件的存在了)。
如何去创建目录呢?
这里就不过多的赘述,直接给代码展示就好了。注意区分mkdir
和mkdirs
的区别,前者是创建单级的目录的,后者是创建多级的目录。(我个人写代码的时候即使用mkdir
创建多级目录,虽没有报错,但是没有生成多级目录。)
// 创建一级目录
@Test
public void test() throw IOException{
File file = new File("D:\\IO_test\\");
if (!file.getParentFile().exists()) {
file.mkdir();
}
file.createNewFile();
System.out.println("运行成功");
}
// 创建多级目录
@Test
public void test1() {
File file = new File("D:\\IO_test\\test\\a\\b");
if (file.exists()) {
System.out.println("目录已存在");
} else {
System.out.println("目录不存在,但是已创建");
file.mkdirs();
}
}
// 这里补充一下删除目录操作和文件的操作(注意,一定是空目录才能删除)
@Test
public void test2() {
File file = new File("D:\\2.txt");
if (file.exists()) {
file.delete();
System.out.println("成功删除");
} else {
System.out.println("文件不存在");
}
}
完成了上面的文件和目录的操作之后我们可以进入核心主题了。
InputStream
和 OutputStream
由上面我们得知,InputStream
和OutputStream
是抽象类,无法直接new一个对象出来,但是我们可以通过多态的方式生成子类对象。
下面我们就来看看子类的用法
FileInputStream
和 FileOutputStream
认识一个类首先得认识这个类的构造方法和普通方法的使用
这里参考API文档:
FileInputStream
和 FileOutputStream
构造方法:
这里需要注意 FileOutputStream
构造器中有一个 Boolean append
这个形参,意思是 是否需要覆盖你原先文本的一个boolean
值,如果为 true
则在原文的文件的内容末尾进行追加内容;如果为 false
,则就是对原来的文件的内容进行覆盖处理。
FileInputStream
和 FileOutputStream
常用的方法:
点开read()
方法来看可以知道其返回值为-1,则我们可以利用这个特性来构造一个while循环来不断读取文本内容,直至读取到当值为-1时候退出循环结束。(下面有代码展示)
其中,红色框住的read()
方法是一个字节一个字节这样读取的文件的信息;黄色框住的read()
方法是一个字节数组这样读取的信息,效率相比高许多。(writer类比去理解就是了,不多赘述)
下面我就用比较常用的几个方法来演示一下FileInputStream
和 OutputStream
的用法(因为防止代码的很长不方便阅读,就不利用try-catch
的方法处理有异常的代码,而是直接采用抛出异常的方式去写)
(需求:将D:\1.txt文件中的信息取出然后存入到D:\\3.txt
这个文件中)
// 效果图(1)
@Test
public void test3() throws IOException {
FileInputStream fileInputStream = new FileInputStream(new File("D:\\1.txt"));
FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\3.txt"));
int readLen = 0;
byte[] bytes = new byte[1024]; // 这里有个小细节,假如你中括号里写的假如是3,则一次bytes字节数组只读三个字节
while ((readLen = fileInputStream.read(bytes)) != -1) {
// 这里也要记住,必须要用这种方式读取,否则读出的数据是正确的
System.out.print(new String(bytes, 0, readLen));
fileOutputStream.write(bytes, 0, readLen);
}
// 这里很重要!!!如果不关闭就会造成资源浪费
fileInputStream.close();
// 假如不关闭输出流的资源,就不会在文档上显示相应的文字
fileOutputStream.close();
System.out.println();
System.out.println("输出流已关闭");
System.out.println("输入流已关闭");
}
// 效果图 (2)
// 这里是追加信息的代码展示
@Test
public void test3() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\3.txt"),true);
String msg = " i love u";
fileOutputStream.write(msg.getBytes());
fileOutputStream.close();
System.out.println("输出流已关闭");
}
效果图(1)如下:
效果图(2)如下:
ObjectInputStream
和 ObjectOutputStream
我们都知道之前我们存入的数据都是只是存入值进入文件而已,而数据类型就没有存入到文件中。比如:你存入100在文件中,你无法证明这个100是int
类型的,还是String
类型的。这里就需要我们引入对象输入流和对象输出流,而这俩个流就涉及到一个重要的知识,序列化和反序列化。简单来说其概念为:
序列化: 就是将数值和数据类型存入文件中。
反序列化: 就是将文件中的数值和数据类型返回到Java程序中
补充一下序列化和反序列化的知识
1.需要让某个对象支持序列化,就必须让其类是可序列化的,为了让某个类可序列化,该类必须要实现两个接口之一;
Serializable
//这是个标记的接口 (建议 因为是标记接口 没有方法)
Externalizable
// 这个也是继承Serializable
这个接口的
下面就用一个例子来展示:(需求:将Dog类的信息存入到文件中)
// 新建一个类
public class Dog implements Serializable {
private String name;
private int age;
public Dog(String name,int age){
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
@Test
public void test13() throws IOException, ClassNotFoundException {
// 序列化后,保存的文件格式,不是存文本的,而是按照他的格式来存,所以这个要存入的文件的后缀名就显得不那么重要了
ObjectOutputStream ops = new ObjectOutputStream(new FileOutputStream("D:\\data.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\data.txt"));
// 序列化
ops.writeInt(100);
ops.writeBoolean(true);
ops.writeChar('a');
ops.writeDouble(7.7);
ops.writeUTF("hero生仔");
ops.writeObject(new Dog("旺财", 3));
// 反序列化
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());
Object dog = ois.readObject();
System.out.println("运行类型:"+dog.getClass());
System.out.println("dog的信息:"+dog);
ois.close();
ops.close();
System.out.println("数据序列化完毕");
}
/*输出的信息为:
100
true
a
7.7
hero生仔
运行类型:class 刘东生.IO流.Dog
dog的信息:Dog{name='旺财', age=3}
数据序列化完毕
进程已结束,退出代码0
*/
2.序列化和反序列化的细节知识:
- 首先,你序列化的时候的顺序是怎么样的,反序列化的时候顺序就怎么样,如上述代码序列化的时候先
int
后Boolean
,那么你反序列化的时候也就是应该int
和Boolean
- 其次,类中被
static
和transient
修饰的属性,就不会被序列化。 - 序列化的类的属性也是需要序列化的。(
String
和Integer
底层都是实现了序列化Serializable
接口的)
缓冲字节流BufferedInputStream
和 BufferedOutputStream
BufferedInputStream
和 BufferedOutputStream
虽然都是InputStream
的子类,但是这个缓冲流有点特殊,他是一个缓冲流。我们可以看看BufferedStream
的源码。
从图中我们可以看出该类的构造器的一个形参是InputStream
,这里就代表着改类的实参可以写入任何一个 InputStream
的一个子类。(这里就是涉及到多态的知识,不懂得自行去复习Java三大特性)
同理 BufferedOutputStream
也是如此。该构造器的实参可以写入任何一个 OutputStream
的子类。
(需求:将D:\\a7e3c7139fc9c3623ee166497978103.jpg照片存入到D:\\照片这个文件夹中)
@Test
public void test12() throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\a7e3c7139fc9c3623ee166497978103.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\照片"));
int len = 0;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.close();
bis.close();
}
需要注意的是这里只需要关闭外层的流就可以了(即bos
和 bis
这两个流),外层流底层调用的也是关闭相应的子类流的操作。(意思就是,你 bos.close
之后,在底层会调用FileOutputStream
这个流的close()
方法)
InputReader
和 OutputWriter
我们都知道,在UTF
-8中,一个汉字占用三个字节,如果用字节流的方式去读取,要用上这个格式(就是在输出文本的时候 System.out.print(new String(bytes, 0, readLen));
这样才能读取到正确的文字,如果不是这样读取的话就会出现中文字符的乱码),其次我们可以利用字符流来读取纯文本的文件。
我们来看看该类的一些构造方法和常用的方法
参考API文档:(这里以FileReader
和 FileWriter
为例子)
FileReader
和 FileWriter
同理Reader的也是如此。
也许有人会有疑问这里为什么不是FileReader
和FileWriter
的方法呀,因为这两个方法继承的是OutputStreamWriter
和InputStreamReader
两个类重写的方法都是父类的方法,所以没啥区别。
(需求:将D:\test.txt 信息输出在显示台,且在该路径下保存 " ,我会唱挑rap和打篮球"字段。)
@Test
public void test8() throws IOException {
char[] chars = new char[1024];
int len = 0;
FileReader fileReader = new FileReader("D:\\test.txt");
while ((len=fileReader.read(chars))!=-1){
System.out.print(new String(chars,0,len));
}
fileReader.close();
FileWriter fileWriter = new FileWriter("D:\\test.txt",true);
String hobby = " ,我会唱挑rap和打篮球";
fileWriter.write(hobby);
fileWriter.close();
}
效果图如下:
缓冲字符流BufferedReader
和 BufferedWriter
这个类也是用到了修饰器设计模式。我们来看他的源码。
也就是说,这两个类的实参都是可以写入Reader
和Writer
的所有子类。基本的内容和缓冲输入流和缓冲输出流都一样的功能了。
(需求:将D:\test.txt 的内容拷贝到(以追加的形式)D:\1.txt )
@Test
public void test11() throws IOException {
BufferedReader brd = new BufferedReader(new FileReader("D:\\test.txt"));
BufferedWriter bwt = new BufferedWriter(new FileWriter("D:\\1.txt", true));
int len = 0;
char[] chars = new char[1024];
while ((len = brd.read(chars)) != -1) {
bwt.write(chars);
}
brd.close();
bwt.close();
}
效果图:
printWriter
和 printStream
(打印流)
/**
* 标准输入输出流
*
* System.in 标准输入 (键盘) new Scanner(System.in)
* System.in 编译类型 InputStream
* System.in 运行类型 BufferedInputStream
*
*
* 同理 System.out 标准输出(其实就是显示器) System.out.print
* public final static PrintStream out = null;
* class java.io.PrintStream
* 运行类型和编译类型都是PrintStream
*/
@Test
public void test14() {
// 源码中 public final static InputStream in = null;
System.out.println((System.in).getClass());
// 源码中 public final static PrintStream out = null;
System.out.println((System.out).getClass());
}
Properties
类处理配置文件
我们首先看Properties
类的API文档
这里我们就来举个例子吧:
// 配置文件信息为
ip=123.123.123.12
user=root
pwd=123456
@Test
public void test17() throws IOException {
Properties properties = new Properties();
// 加载配置文件
properties.load(new FileReader("src/刘东生/IO流/mysql.properties"));
String ip = properties.getProperty("ip");
System.out.println(ip);
String user = properties.getProperty("user");
System.out.println(user);
String pwd = properties.getProperty("pwd");
System.out.println(pwd);
}
/*输出为:
123.123.123.12
root
123456
*/
/*进行对配置文件的修改操作和字符集添加操作*/
@Test
public void test17() throws IOException {
Properties properties = new Properties();
properties.setProperty("charset","utf8");
// 保存的是中文的Unicode的码
// 还需要注意的是 假如键值对中的键有这个user,就是修改 否则就是新创建这个键值对
properties.setProperty("user","汤姆");
// 将k-v存储到文件中
properties.store(new FileWriter("src/刘东生/IO流/mysql.properties"),null);
System.out.println("保存成功");
}
// 此时配置文件的信息为:
user=汤姆
charset=utf8
补充一个修饰器设计模式的简单例子
什么是修饰器设计模式:装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。 这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。 这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。(来自谷歌搜索)
这里模仿的是BufferedWriter
类的写的一个简易的修饰器设计模式,目的是为了理解这个设计模式。
Writer_
类
// 抽象类 (基类)
public abstract class Writer_ {
public void read(){
System.out.println("这是父类的Writer方法");
};
}
FileWriter_
类
public class FileWriter_ extends Writer_{
public void read() {
System.out.println("这是在FileWriter_中的方法");
}
}
BufferedWriter_
类
public class BufferedWriter_ extends Writer_ {
private Writer_ writer_;
// 构造方法
public BufferedWriter_(Writer_ writer_) {
this.writer_ = writer_;
}
// 这里印证了 “用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。”
public void readFile(int num) {
for (int i = 0; i < num; i++) {
/**
* 这里传入的writer_是Writer子类的对象,
* 他们重写了父类中read()的方法,
* 那读取的理所当然是子类的read()的方法
*/
writer_.read();
}
}
}
Test
类
public class Test {
public static void main(String[] args) {
BufferedWriter_ bufferedWriter_ = new BufferedWriter_(new FileWriter_());
bufferedWriter_.readFile(10);
/**
* 这里读取的的是父类的方法
* 原因是:这里复习一遍继承多态的知识
* 因为你bufferedWriter_继承的是Writer这个基类,然而你在BufferedWriter_这个类中没有重写read()方法,
* 通过构造器传入的FileWriter对象依然调取的是父类中read()的方法
*/
bufferedWriter_.read();
}
}
/*输出内容为:
这是在FileWriter_中的方法
这是在FileWriter_中的方法
这是在FileWriter_中的方法
这是在FileWriter_中的方法
这是在FileWriter_中的方法
这是在FileWriter_中的方法
这是在FileWriter_中的方法
这是在FileWriter_中的方法
这是在FileWriter_中的方法
这是在FileWriter_中的方法
这是父类的Writer方法
*/