一、字节流与字符流
- 输入流:用于读取数据 – 将数据写入内存进行展示,即将数据从其他设备读取到内存中的流。
- 输出流:用于数据保存 – 将数据写入磁盘,可持久化存储,即将数据从内存中写出到其他设备上的流。
在字节流(以字节为单位)中,输出数据使用OutStream
类完成,输入使用的是InputStream
类完成。(所有字节流的父类)
在字符流(以字符为单位)中,输出数据使用Writer
类完成,输入使用Reader
完成。(所有字符流的父类)
其中,字节流主要操作byte
类型数据,以byte
数组为准。
如果想对文件进行读写,首先需要创建一个文件对象,如下:
1 | public class FileDemo01 { |
从上面代码段可以看出,File
接收的参数是文件路径,返回的是File
对象。但是,直接打印File
时,返回的是pathname
,即为传入的参数。所以,在File
类中,重写了toString
方法。后面当我们拿到File
对象后,就可以进行后续对当前文件的一系列操作了。
File
对象常见的方法:
1 | public class FileDemo01 { |
二、字节流的读写操作
1.字节流的输出
1 | <details> |
1 | public class FileDemo05 { |
2.字节流的读取
字节流的读取
1 | public class FileDemo05 { |
当一次读取一个字节时,length
中存放的是对应字节的ASCII
码数值;当一次读取多个字符时,length
中记录的是数组的有效长度。
对上图中一次读取多个字节的思考:
上图右边读取多个字符中,在读取时,若没有指定数组的有效长度,会出现重复的情况。例如:这里我们期望读取到是ABCDE
,但最终的情况会是ABCDEDED
。这里设定的缓冲区读取的数组长度为2。
第一次读取时:缓冲区数组对应的内容是{A,B}
(这里对应的其实是ASCII
码)。len = 2.
第二次读取时:读取到缓冲区对应的内容是{C,D}
,len = 2.
第三次读取时:读取到缓冲区对应内容,只有E,但是原来的数组中存储的是{C,D}
,故将E覆盖C。所以此时缓冲区对应的内容是{E,D}
,len = 1.故此轮输出是ED
。
注意:又因为没有返回-1,所以继续读取。
第四次读取时:文件中没有有效内容,返回-1。但数组中是{E,D}
,所以再次打印时,还是ED
。
因此在读取操作时,需要注意两点:
未读取到有效内容,返回-1时,则停止读取。
while(len != -1){}
读取时,指定数组的有效长度,而数组的有效长度,又可以通过
len
进行指定。new String(bytes,0,len)
3.案例 — 统计并打印指定文件夹.java
文件
思路:
- 一个文件夹下可能既包含文件,又包含文件夹。所以采取的方式是:遇到文件夹就继续进入,遇到文件则判断文件是否是
.java
文件,故采用递归的方式。 - 由于我们需要过滤出
.java
文件,所以我们有两种方法:- 对文件名进行判断
.endWith(".java")
。 - 实现
FileFilter
接口,重写其中的accept
方法。
- 对文件名进行判断
accept接口实现的方式
1 | public class FileFilterImpl implements FileFilter { |
主方法的实现
1 | public class FindJavaDemo05 { |
对File[] files = dir.listFiles(new FileFilterImpl())
的思考:
其中接口实现accept
方法,用于过滤满足方法体条件的文件对象。
首先dir.listFiles
遍历出每一个File
对象,每一个都作为FileFilterImpl
中accpet
方法的参数进行传入。
然后,调用accpet
函数,对当前传入的参数(File
对象)进行判断操作。如果满足方法体条件,返回true
,否则返回false
。
最后,当.listFiles
接收到true
时,就将当前的File
对象添加入File[]
数组中,否则就不加入。
三、字符流的读写操作
字符流以字符为单位,专门用于处理文本文件。若用字节流读取中文字符时,可能不会显示完整的字符,因为一个中文字符可能占用多个字节存储。
同样在字符输出流中,同样有Reader
和Writer
两种读取和写入的抽象类。
Reader
用于读取字符流的所有类的超类,可以读取字符信息到内存中。其中,字符输入流的基本共性功能方法有:
public void close()
:关闭此流并释放与此流相关联的任何系统资源。public void read()
:从输入流读取一个字符。public void read(char[] cbuf)
:从输入流中读取一些字符,并将它们存储到字符数组cbuf
中。
Writer
用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。以下为字节输出流的基本共性功能方法。
void write(int c)
:写入单个字符。void write(char[] cbuf)
:写入字符数组。abstract void write(char[] cbuf,int off,int len)
:写入字符数组的某一部分,off
数组的开始索引,len
写的字符个数。void write(String str)
:写入字符串。void write(String str,int off,int len)
:写入字符串的某一部分,off
数组的开始索引,len
写的字符个数。void flush()
:刷新该流的缓冲。void close()
:关闭此流,再次此前先进行刷新。
以上可对OutStream
和InStream
进行类比。
1.FileReader类
注意:读取文件时,构造时使用系统默认的字符编码和默认字节缓冲区。
- 字符编码:字节与字符的对应规则。
idea
中默认的是UTF-8
- 字节缓冲区:一个字节数组,用来临时存储字节数据。
构造方法
FileWriter(File file)
FileWriter(String name)
具体使用方法等同FileInputStream
,字节输入流。
2.FileWriter
其构造时使用系统默认的字符编码和默认字节缓冲区。
构造方法
FileWriter(File file)
:创建一个新的FileWriter
,给定要读取的File
对象。FileWriter(String fileName)
:同上,传入的参数是文件的名称(路径)。
其使用方法同字节流的使用,需要注意的一点是:在写出数据以后,如果未调用close
方法,数据只是保存到了缓冲区,并未写出到文件中。因此,字节流在操作时本身不会用到缓冲区(内存),是文件本身操作的。
字符流在操作时使用了缓冲区,通过缓冲区再操作文件。
因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。当既想写出数据,又想继续使用流,就需要flush
方法。
flush
:刷新缓冲区,流对象可以继续使用。close
:先刷新缓冲区,然后通知系统释放资源。流对象不可以再使用了。
输出同字节输出流。
1.写出字符数组
string.toCharArray()
2.写出字符串
writer(String str),writer(String str,int off,int len)
四、属性集
java.util.Properties
继承于Hashtable
,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java
类使用,比如获取系统属性时,System.getProperties
方法就是返回一个Properties
对象。
4.1 Properties类
构造方法
public Properties()
:创建一个空的属性列表。
基本的存储方法
public Object setProperty(String key, String value)
: 保存一对属性。public String getProperty(String key)
:使用此属性列表中指定的键搜索属性值。public Set<String> stringPropertyNames()
:所有键的名称的集合。
通常在不知道键值的情况下,获取其键值的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14public static void main(String[] args) throws FileNotFoundException {
// 创建属性集对象
Properties properties = new Properties();
// 添加键值对元素
properties.setProperty("filename", "a.txt");
properties.setProperty("length", "209385038");
properties.setProperty("location", "D:\\a.txt");
// 遍历属性集,获取所有键的集合
Set<String> strings = properties.stringPropertyNames();
// 打印键值对
for (String key : strings ) {
System.out.println(key+" -- "+properties.getProperty(key));
}
}
4.2 与流相关的方法
public void load(InputStream inStream)
: 从字节输入流中读取键值对。
参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式:
1 | filename=a.txt |
文本中的数据必须是键值对格式,可以冒号,逗号,等号等符号分隔。
加载代码示例:
1 | public class ProDemo2 { |
五、缓冲流
缓冲流,也是高效流,是对4个基本的FileXxx
流的增强,所以对应的也是4个流。同理按照数据类型分类:
- 字节缓冲流:
BufferedInputStream
、BufferedOutputStream
- 字符缓冲流:
BufferedReader
、BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO
次数,从而提高读写的效率。
5.1 字节缓冲流
1. 构造方法
public BufferedInputStream(InputStream in)
:创建一个新的缓冲输入流,传递字节输入流。public BufferedOutputStream(OutputStream out)
: 创建一个新的缓冲输出流,传递字节输出流。
2.用例
1 | public class BufferedDemo { |
5.2 字符缓冲流
1.构造方法
public BufferedReader(Reader in)
:创建一个新的缓冲输入流。public BufferedWriter(Writer out)
: 创建一个新的缓冲输出流。
1 | // 创建字符缓冲输入流 |
2.特有方法
字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。
BufferedReader
:public String readLine()
: 读一行文字。BufferedWriter
:public void newLine()
: 写一行分隔符,由系统属性定义符号。
readLine
方法演示:
1 | public class BufferedReaderDemo { |
newLine
方法演示:
1 | public class BufferedWriterDemo throws IOException { |
3.原理
由上图可以看到,当我们使用普通的字节输出流时,由OS
进行内存到硬盘的读写时,字符是一个一个读取,这样增加了文件从内存(其中经过缓存)读取到磁盘次数,从而增加了读取的时间;当我们使用缓冲流时,每次操作系统从内存读取到缓冲区的数据就是多个字符,然后再读取到硬盘,由于每次读取的数量多了,那么总的读取次数就肯定减少了,所以就能够减少我们读写的时间。
六、转换流
6.1 字符编码和字符集
- 字符编码
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。
编码:字符->字节
解码:字节->字符
- 字符编码
Character Encoding
: 就是一套自然语言的字符与二进制数之间的对应规则。- 编码表:生活中文字和计算机中二进制的对应规则
图片转换流图解-桥梁
- 字符集
- 字符集
Charset
:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。
计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII
字符集、GBK
字符集、Unicode
字符集等。
当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。
6.2 InputStreamReader类
转换流java.io.InputStreamReader
,是Reader
的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
1.构造方法
InputStreamReader(InputStream in)
: 创建一个使用默认字符集的字符流。InputStreamReader(InputStream in, String charsetName)
: 创建一个指定字符集的字符流。
构造举例,代码如下:
1 | InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt")); |
2.指定编码读取
1 | public class ReaderDemo2 { |
6.3 OutputStreamWriter类
转换流java.io.OutputStreamWriter
,是Writer
的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
1.构造方法
OutputStreamWriter(OutputStream in)
: 创建一个使用默认字符集的字符流。OutputStreamWriter(OutputStream in, String charsetName)
: 创建一个指定字符集的字符流。
构造举例,代码如下:
1 | OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt")); |
2.指定编码输出
1 | public class OutputDemo { |
6.4 字符流转换实例
案例:
输入条件:给定文件中,输入文件指定为gbk
格式。
输出条件:将文件以utf-8
格式进行输出。
1 | public class TransDemo{ |
七.序列化
Java
提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据
、对象的类型
和对象中存储的属性
等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
同样,该字节序列还可以从文件中读取回来,重构对象,对其进行反序列化操作。对象的数据
、对象的类型
和对象中存储的数据
信息,都可以用来在内存中创建对象。
序列化:将对象转换为字节。
反序列化:字节重构为对象。
7.1 ObjectOutputStream类
java.io.ObjectOutputStream
类,将Java
对象的原始数据类型写出到文件,实现对象的持久存储。
构造方法:
public ObjectOutputStream(OutputStream out)
: 创建一个指定OutputStream
的ObjectOutputStream
。
举例:
1 | FileOutputStream fileOut = new FileOutputStream("employee.txt"); |
7.2 序列化操作
- 一个对象要想序列化,必须满足两个条件:
- 该类必须实现
java.io.Serializable
接口,Serializable
是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
。 - 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用
transient
关键字修饰。
- 该类必须实现
1 | public class Employee implements Serializable { |
写出对象方法
public final void writeObject (Object obj)
:将指定的对象写出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class SerializeDemo {
public static void main(String[] args) {
Employee employee = new Employee();
employee.setAge(21);
employee.setAddress("江苏");
employee.setName("Tom");
ObjectOutputStream out = null;
try {
// 创建序列化对象
out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
// 写出对象
out.writeObject(employee);
// 释放资源
out.close();
System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化
} catch (IOException e) {
e.printStackTrace();
}
}
}
7.3 反序列化操作
ObjectInputStream
反序列化流,将之前使用ObjectOutputStream
序列化的原始数据恢复为对象。
构造方法:
public ObjectInputStream(InputStream in)
: 创建一个指定InputStream
的ObjectInputStream
。
7.3.1 反序列化操作一
通过查找一个对象的class
文件,即可将其进行反序列化操作,调用ObjectInputStream
读取对象的方法:
public final Object readObject ()
: 读取一个对象。
1 | public class SerializeDemo01 { |
由于JVM
可以反序列化对象,它必须是能够找到class
文件的类。如果找不到该类的class
文件,则抛出一个ClassNotFoundException
异常。
7.3.2 反序列化操作二
当JVM
反序列化对象时,能够找到class
文件,但是class
文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException
异常。发生这个异常的原因如下:
- 该类的序列版本号与从流中读取的类描述符的版本号不匹配
- 该类包含未知数据类型
- 该类没有可访问的无参数构造方法
Serializable
接口给需要序列化的类,提供了一个序列号版本号。serialVersionUID
该版本号的目的在于验证序列化的对象和对应类是否版本匹配。
1 | // 加入序列版本号 |
7.4 序列化案例
- 将存有多个自定义对象的集合序列化操作,保存到自定义文件中。
- 反序列化此文件,并遍历集合,打印对象信息。
分析:
- 将若干学生对象,保存到集合中。
- 把集合进行序列化。
- 反序列化读取时,只需要读取一次,转换为集合类型。
- 遍历集合,打印所有的学生信息。
首先创建一个student
类,再创建集合类的对象,序列化和反序列化的对象都是这个集合对象。
1 | public class DemoSerialize { |
八、打印流
print
和println
方法来自于java.io.PrintStream
类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方法。
8.1 PrintStream类
构造方法:
public PrintStream(String fileName)
: 使用指定的文件名创建一个新的打印流。
1 | PrintStream ps = new PrintStream("ps.txt"); |
改变打印流的流向:
System.out
就是PrintStream
类型的,只不过它的流向是系统规定的,打印在控制台上。通过PrintStream
类改变打印流流向。
1 | public class PrintStreamDemo { |