什么是IO
IO顾名思义就是Input和Output.主要是以程序作为参照物来定义.
- 研究的是数据从内存中(程序)输出(Output)到节点中(文件、网络等)的方式
- 研究的是数据从节点中读取入内存(程序中的)方式
Java中对于IO的分类
所谓分类就是根据特点可以对事物进行归类.而对IO进行分类, 也就是以IO类的特点进行分类.先看如下图
流的分类按流方向划分
可以划分为输入和输出流
按流操作的数据单位划分
可以划分为字节流和字符流。
字节流能操作读写任意类型文件, 字符流只能操作文本文件
按流的角色划分
可以分为节点流和处理流。
- 字节流和字符流都是节点流。
- 处理流建立在节点流之上, 是对节点流的包装,进一步争强其功能.(比如有字节转字符流的包装流等)
四大IO抽象类和常用的节点流处理流
抽象类 | 节点流 | 处理流(提供的是缓冲功能) |
---|---|---|
InputStream | FileInputPutStream | BufferedInputStream |
OutputSteam | FileOutputStream | BufferedOutputStream |
Reader | FileReader | BufferedReader |
Writer | FileWriter | BufferedWriter |
- 实际应用中,我们的流肯定要和节点(文件、网络等统称为几点)发送关系,所以和节点直接建立练习的流称为节点流,节点流主要和文件节点建立链接.
- 再按流处理数据的单位分为字节流和字符流。接着再按数据的方向分为字节输入输出流,字符输入输出流。
- 最后由于节点流都只实现了最小的核心功能。而此时就诞生了处理流, 可以节点流的基础上进一步包装,所以其是依赖于节点流,但是功能更加的强大.
FileInputStream
其常用的API
- read() 读取一个字节
- read(byte[]) 读取多个字节, 具体长度为min(byte.length, 文件剩余的字节大小),并将这些字节存入byte数组
- read(byte[], off, len) 读取多个字节, 将这些字节存入byte数组.存入位置为off,长度为min(len, 文件剩余的字节)
示例代码
/**
* read(buffer, off, len)
*/
@Test
public void testInputStreamWithBytesArray2() {
File file = new File("1.txt");
// System.out.println(file.getAbsolutePath());
InputStream is = null;
try {
is = new FileInputStream(file);
int len = 3;
int off = 0;
int totalLength = 0;
byte[] buffer = new byte[100];
while ((len = is.read(buffer, off, len)) != -1) {
off += len;
System.out.print("len = " + len);
}
for (int i = 0; i < off; i++) {
System.out.print((char)buffer[i]);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is!=null) {
try {
is.close();
}catch (IOException e) {
}
}
}
}
@Test
public void testInputStreamWithBytesArray() {
File file = new File("1.txt");
// System.out.println(file.getAbsolutePath());
InputStream is = null;
try {
is = new FileInputStream(file);
int len = -1;
byte[] buffer = new byte[5];
while ((len = is.read(buffer)) != -1) {
System.out.print("len = " + len);
for (int i = 0; i < len; i++) {
System.out.print((char)buffer[i]);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is!=null) {
try {
is.close();
}catch (IOException e) {
}
}
}
}
@Test
public void testInputStreamWithSingleByte() {
File file = new File("1.txt");
// System.out.println(file.getAbsolutePath());
InputStream is = null;
try {
is = new FileInputStream(file);
int len = -1;
while ((len = is.read()) != -1) {
System.out.print((char)len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is!=null) {
try {
is.close();
}catch (IOException e) {
}
}
}
}
源码解析
read()和read0
read(byte b[], int off, int len)
readBytes(byte b[], int off, int len)
跟踪FileInputStream起代码可见其调用的是native的read0方法。证明该方法是一个系统调用, 频繁调用的话效率和性能会很低。
而后两者是对native类型的方法readBytes(byte[], off, len)的一层包装,这个方法也是一个系统调用,但它可以次调用读取多个
这就降低了系统调用带来的性能开销, 提高了程序性能
文件字节输入流总结
- 使用FileInputStream按字节读取文件, 当读取到文件尾部会返回-1.
- 如果读取结束, 需要调用close方法关闭文件资源.
- read()方法本质上是调用系统的read0.read(byte[])和read(byte[], off, len)本质上是对native方法readBytes(byte[], off, len)的封装
- 如果读取的文件不存在会抛出FileNotFoundException异常.关闭文件资源可能抛出IOException异常.
FileOutputStream
- write(int) 写一个字节到对应的文件
- write(byte[]) 写一个字节数组到对应的文件
- write(byte[], off, len) 将一个字节数组从off位置开始, 取len长度,写到对应的文件
示例代码
@Test
public void testOutputStream() {
File file = new File("testOutput.txt");
OutputStream os = null;
try {
os = new FileOutputStream(file, true);
os.write("I love Beijing".getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
}finally {
if (null != os) {
try {
os.close();
}catch (IOException e) {
}
}
}
}
文件的写操作和读差不多,其接口使用就不一一列举。直接总结
文件字节输出流总结
- 文件字节输出流的常用的三个接口是write(int)、write(byte[])、write(byte[], off, len).这三个接口具有可能抛出IOException
- write(int)接口底层调用的是native方法write(int b, boolean append).而write(byte[])、write(byte[],off,len)底层调用都是native方法writeBytes(byte[], off, len).所以使用后两者写入字节数据的性能会更高
- 文件字节输出流操作完后,要调用close方法进行关闭.该方法也可能抛出IOException
- 创建输出流的时候,如果关联的文件不存在,并不会抛出FileNotFoundException
FileReader
FileReader接口和FileInputStream基本一样
示例代码
/**
*
* FileReader继承自InputStreamReader
*/
@Test
public void testFileReader() {
File file = new File("2.txt");
FileReader fr = null;
try {
fr = new FileReader(file);
int len;
char[] buf = new char[100];
while ((len = fr.read(buf)) != -1) {
String str = new String(buf, 0, len);
System.out.print(str);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (null != fr) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FileWriter
FileWriter接口和FileInputStream基本一样
示例代码
@Test
public void testFileReaderAndWriter() {
FileReader fr = null;
FileWriter fw = null;
try {
fr = new FileReader("2.txt");
fw = new FileWriter("2_copy.txt");
int len;
char[] buf = new char[100];
while ((len = fr.read(buf)) != -1) {
fw.write(buf, 0, len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (null != fw)
fw.close();
if (null != fr)
fr.close();
}catch (IOException e) {
}
}
}
BufferedReader和BufferedWriter
- BufferedReader、BufferedWriter的接口和FileReader、FileWriter基本一样.
- BufferedReader有一个能直接按行读取的接口readLine().如果读到行尾该接口返回null
示例代码
@Test
public void testBufferedReaderAndWriter() throws IOException {
FileReader fr = new FileReader("2.txt");
FileWriter fw = new FileWriter("bufferedWriterCopy3.txt");
BufferedReader br = new BufferedReader(fr);
BufferedWriter bw = new BufferedWriter(fw);
int len;
char[] buf = new char[100];
// 方式一
// while ((len = br.read(buf)) !=-1) {
// bw.write(buf, 0, len);
// }
// 方式二
String line;
while ((line = br.readLine()) != null) {
bw.write(line+"\n");
}
bw.close();
br.close();
fw.close();
fr.close();
}
转换流(处理流二)
转换流主要实现了字节流和字符流之间的转换.主要涉及到以下两个流。对于程序来说,输入输出都是字符串表示。只有从磁盘刚读出后者要写入磁盘才会转成字节
涉及的流
- InputStreamReader
将字节输入流转成字符输入流 - OutputStreamWriter
实现字符的输出流转换为字节的输出流
编码解码
- 编码过程: 字符串、字符数组 转换成 字节数组
- 解码过程: 字节数组 转换成 字符串、字符数组
转换流使用步骤
- 创建对应的字节流(要读取就创建输入字节流, 要写入就创建输出字节流).
- 创建对应的转换流,将字节流转换成字符流,并指定对应要编码或解码的字符集
- 调用read或者write方法进行读取和写入.
常见的编码表名称
常见的编码集
示例代码
将GBK编码的文件读取出来, 并转换成UTF-8编码写出到新文件
@Test
public void testISRAndOSW() throws IOException {
// 读取出来的时候也是用字节
FileInputStream fis = new FileInputStream("test_gbk.txt");
InputStreamReader isr = new InputStreamReader(fis, "GBK");
// 写入的时候是用字节
FileOutputStream fos= new FileOutputStream("test_utf8.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
int len;
char[] buf = new char[100];
while ((len = isr.read(buf)) != -1) {
String line = new String(buf, 0, len);
System.out.print(line);
osw.write(buf, 0, len);
}
isr.close();
fis.close();
osw.close();
fos.close();
}
注意
- 面向程序的始终是字符串,输出到节点的始终是字节信息。
- 从节点到程序,是先经过字节流再转为字符流
- 从程序到节点,是先通过字符流再转为字节流
标准输入输出流(处理流三)
略
打印流(处理流四)
PrintStream\PrintWriter
都是输出流
打印流也是处理流, 其争强了字节输出流的功能. System.out是PrintStream的实例, 其关联的是键盘节点
-
提供了8大基本基本数据类型的打印, Object对象的打印和, 字符串数组的打印。
-
分别重载了print和println().后者功能是多一个换行.
-
PrintStream和PrintWriter的输出不会抛异常
-
PrintStream和PrintWriter有自动flush功能
print和println
示例代码
使用打印流PrintStream包装字节输出流,提供各种数据类型的便利输出方法
/**
* PrintStream
*
*/
@Test
public void testPrintStream() throws FileNotFoundException {
// 创建一个PrintStream对象,将其关联到printStream.txt节点
FileOutputStream fos = new FileOutputStream("printStream.txt");
PrintStream ps = new PrintStream(fos, true);
if (ps != null) {
System.setOut(ps);
}
System.out.println("我爱北京!!!!");
}
运行后创建的文件
System.out重定向示例
数据流(处理流五)
数据流主要用来序列化java字符串和基本数据类型
为什么不直接使用字节流和字符流读写基本数据类型
因为如果你想使用FileOutputStream或者FileWriter来将java中的数据输出, 对于FileOutputStream需要将所有的基本数据类型转为字节或者字节数组。而FileWriter你需要将所有基本数据类型转为字符串。实现起来就十分麻烦。
数据流的特点
- 数据流不保证线程安全
- 数据输入流的方法基本上都是readXxx
- 数据输出流的方法基本上都是writerXxx
DataInputStream中的方法
DataInputStream中的方法
示例代码
@Test
public void testDataOutputStream() throws IOException {
String name = "张三";
int age = 24;
char gender = '男';
double salary = 10000.0;
FileOutputStream fos = new FileOutputStream("test_dataOutputStream.txt");
DataOutputStream dos = new DataOutputStream(fos);
dos.writeUTF(name);
dos.writeInt(age);
dos.writeChar(gender);
dos.writeDouble(salary);
}
test_dataOutputStream.txt文件
使用文本打开是乱码
之所以乱码, 是因为我们写入到文件中本质上也是字节, 但系统的文本编辑器并不知道java存储这些数据的志节规则,比如int它是序列化为几个字节.所以会乱码, 这时候如果我们还还原数据需要使用数据输入流来读出。
DataOutputStream中的方法
将上述的方法的read改为相应的write即可.其提供了将基本类型数据写入字节流的功能, 可以用数据输入流再读出.
使用数据输入流来读取上面生成的文件.读取的顺序要和写入的顺序一样.
示例代码
@Test
public void testDataInputStream() throws IOException {
FileInputStream fis = new FileInputStream("test_dataOutputStream.txt");
DataInputStream dis = new DataInputStream(fis);
String name = dis.readUTF();
int age = dis.readInt();
char gender = dis.readChar();
double salary = dis.readDouble();
System.out.println("name = " + name);
System.out.println("age = " + age);
System.out.println("gender = " + gender);
System.out.println("salary = " + salary);
}
输出
DataOutputStream
总结数据流
- 数据流是用来处理Java中String和基本数据类型的读写
- 数据流输出的字节流数据并不能用文本编辑器直接打开解析, 因为它有自己的存储规则.有点像序列化.
- 数据输入流读取数据, 需要按照数据输出流写入的顺序读取。
对象流(处理流六)
对象流是java中用来序列化对象和反序列化对象的流.其对应的类是ObjectOutputStream和ObjectInputStream.
序列化反序列化概念
- 序列化对象就是将对象转成二进制数据进行传输或者存储.
- 反序列化对象就是将二进制数据转换成对象在程序中使用.
对象流和数据流的异同
- 数据流的相同点就是都是用来序列化数据, 并持久化存储。
- 数据流的不同是其是用来序列化对象,数据流是用来序列化基本数据类型和字符串
序列化步骤
- 要序列化的对象需要实现Serializable接口
- 标记版本(定义final long serialVersionUID)
- 创建对象流包装字节流, 使其能处理对象.(需要先创建字节流关联相应节点.)
- 调用writeObject()方法序列化对象
public class TestObjectOutputStreamAndObjectInputStream {
@Test
public void testObjectSerialization() throws IOException {
Student stu = new Student("苏轼", 20, '男', 10000.0);
FileOutputStream fos = new FileOutputStream("stu.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(stu);
oos.close(); // 记得关闭流
fos.close();
/**
* 流的操作步骤
* 1.创建节点流和相应的节点关联
* 2.根据需要的功能的创建处理流, 对节点流进行争强.
* 3.进行输入输出操作
* 4.关闭流
*/
}
@Test
public void testObjectDeserialize() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream("stu.dat");
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
System.out.println(o);
ois.close();
fis.close();
}
}
class Student implements Serializable {
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
", salary=" + salary +
'}';
}
public Student(String name, int age, char gender, double salary) {
this.name = name;
this.age = age;
this.gender = gender;
this.salary = salary;
}
private String name;
private int age;
private char gender;
private double salary;
// 代码过长省略了getter和setter
transient
可以使用transient关键字声明对象中哪些字段不被序列化,以提高序列化和反序列化效率.
serialVersionUID
在对象进行序列化或反序列化操作的时候,要考虑JDK版本的问 题,如果序列化的JDK版本和反序列化的JDK版本不统一则就有可能造成异常。所以在序列化操作中引入了一个serialVersionUID的常量,可以通过此常量来验证版本的一致性,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现版本不一致的异常。
但是在进行序列化或反序列化操作的时候,对于不同的JDK版本,实际上会出现版本的兼容问题。
Externalizable接口
作用
既然序列化可以使用Serializable接口, 为什么还有Externalizable接口?该接口的功能其实等效于 Serializable + transient.
两个方法
- writeExternal(ObjectOutput out) 用来定制要序列化哪些属性
- readExternal(ObjectInput in) 用来定制要反序列化哪些属性
示例代码,将Student类替换成实现Externalziable接口, 并实现上述两个方法。然后重新运行代码。
class Student implements Externalizable {
private static final long serialVersionUID = 1L;
public Student() {
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
out.writeChar(gender);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = in.readUTF();
this.age = in.readInt();
this.gender = in.readChar();
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
", salary=" + salary +
'}';
}
public Student(String name, int age, char gender, double salary) {
this.name = name;
this.age = age;
this.gender = gender;
this.salary = salary;
}
private String name;
private int age;
private char gender;
private transient double salary;
}
控制台输出
由于我们没有定制salary的序列化,所以反序列化出来的时候改字段默认值就是0.0
salary没有序列化
流的使用总结
- 创建节点流和相应的节点关联
- 根据需要的功能的创建处理流, 对节点流进行增强.
- 进行输入输出操作
- 关闭流
- 关闭流是有顺序要求, 要先关闭处理流在关闭对应的节点流.
- 处理流关闭时, 其实也会取关闭对应的节点流, 所以关闭节点流也可以不写.
网友评论