什么是流?流表示任何有能力产生数据的数据源对象或者是有能力接收数据的接收端对象,它屏蔽了实际的I/O设备中处理数据的细节。

IO流是实现输入输出的基础,它可以很方便地实现数据的输入输出操作,即读写操作。

本片要点

  • 介绍流的定义和基本分类。
  • 介绍文件字符流、字节流、转换流、合并流、打印流等使用。
  • 介绍序列化的意义。
  • 介绍两种自定义序列化方式。

基本分类

  • 根据方向

    • 输入流:数据从外部流向程序,例如从文件中读取数据
    • 输出流:数据从程序流向外部,例如向文件中写数据
  • 根据形式
    • 字符流:字符类文件,【如 txt、 java、 html】,操作16位的字符。
    • 字节流:【图片、视频、音频】 ,操作8位的字节。
  • 根据功能
    • 节点流:直接从/向数据源【如磁盘、网络】进行数据读写
    • 处理流:封装其他的流,来提供增强流的功能。
输入流 输出流
字符流 Reader Writer
字节流 InputStream OutputStream
  • 上面四大基本流都是抽象类,都不能直接创建实例对象。
  • 数据的来源/目的地:磁盘、网络、内存、外部设备。

发展史

  • java1.0版本中,I/O库中与输入有关的所有类都将继承InputStream,与输出有关的所有类继承OutputStream,用以操作二进制数据。

  • java1.1版本对I/O库进行了修改:

    • 在原先的库中新增了新类,如ObjectInputStreamObjectOutputStream
    • 增加了Reader和Writer,提供了兼容Unicode与面向字符的I/O功能。
    • 在Reader和Writer类层次结构中,提供了使字符与字节相互转化的类,OutputStreamWriterInputStreamReader
  • 两个不同的继承层次结构拥有相似的行为,它们都提供了读(read)和写(write)的方法,针对不同的情况,提供的方法也是类似的。

  • java1.4版本的java.nio.*包中引入新的I/O类库,这部分以后再做学习。

文件字符流

  • 文件字符输出流 FileWriter自带缓冲区,数据先写到到缓冲区上,然后从缓冲区写入文件。
  • 文件字符输入流 FileReader:没有缓冲区,可以单个字符的读取,也可以自定义数组缓冲区。

输出的基本结构

在实际应用中,异常处理的方式都需要按照下面的结构进行,本篇为了节约篇幅,之后都将采用向上抛出的方式处理异常。

    //将流对象放在try之外声明,并附为null,保证编译,可以调用close
FileWriter writer = null;
try {
//将流对象放在里面初始化
writer = new FileWriter("D:\\b.txt");
writer.write("abc"); //防止关流失败,没有自动冲刷,导致数据丢失
writer.flush(); } catch (IOException e) {
e.printStackTrace();
} finally {
//判断writer对象是否成功初始化
if(writer!=null) {
//关流,无论成功与否
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
//无论关流成功与否,都是有意义的:标为垃圾对象,强制回收
writer = null;
}
}
}
  • 并不会直接将数据写入文件中,而是先写入缓冲区,待缓冲区满了之后才将缓冲区的数据写入文件。
  • 假设数据写入缓冲区时且缓冲区还没满,数据还没能够写入文件时,程序就已经结束,会导致数据惨死缓冲区,这时需要手动冲刷缓冲区,将缓冲区内的数据冲刷进文件中。writer.flush();
  • 数据写入完毕,释放文件以允许别的流来操作该文件。关闭流可以调用close()方法,值得注意的是,在close执行之前,流会自动进行一次flush的操作以避免数据还残存在缓冲区中,但这并不意味着flush操作是多余的。

流中的异常处理

  • 无论流操作成功与否,关流操作都需要进行,所以需要将关流操作放到finally代码块中
  • 为了让流对象在finally中依然能够使用,所以需要将流对象放在try之外声明并且赋值为null,然后在try之内进行实际的初始化过程。
  • 在关流之前要判断流对象是否初始化成功,实际就是判断流对象是否为nullwriter!=null时才执行关流操作。
  • 关流可能会失败,此时流依然会占用文件,所以需要将流对象置为null,标记为垃圾对象进行强制回收以释放文件。
  • 如果流有缓冲区,为了防止关流失败导致没有进行自动冲刷,所以需要手动冲刷一次,以防止有数据死在缓冲区而产生数据的丢失。

异常处理新方式

JDK1.7提出了对流进行异常处理的新方式,任何AutoClosable类型的对象都可以用于try-with-resourses语法,实现自动关闭。

要求处理的对象的声明过程必须在try后跟的()中,在try代码块之外。

try(FileWriter writer = new FileWriter("D:\\c.txt")){
writer.write("abc");
}catch (IOException e){
e.printStackTrace();
}

读取的基本结构

    public static void main(String[] args) throws IOException {
FileReader reader = new FileReader("D:\\b.txt");
//定义数组作为缓冲区
char[] cs = new char[5];
//定义一个变量记录每次读取的字符
int hasRead;
//读取到末尾为-1
while ((hasRead = reader.read(cs)) != -1) {
System.out.println(new String(cs, 0, hasRead));
}
reader.close();
}
  • read方法可以传入字符数组,每次读取一个字符数组的长度。
  • 定义变量m记录读取的字符,以达到末尾为终止条件。m!=-1时,终止循环。
  • 读取结束,执行关流操作。

运用输入与输出完成复制效果

运用文件字符输入与输出的小小案例:

public static void copyFile(FileReader reader, FileWriter writer) throws IOException {
//利用字符数组作为缓冲区
char[] cs = new char[5];
//定义变量记录读取到的字符个数
int hasRead;
while((hasRead = reader.read(cs)) != -1){
//将读取到的内容写入新的文件中
writer.write(cs, 0, hasRead)); }
reader.close();
writer.close();
}

文件字节流

  • 文件字节输出流 FileOutputStream 在输出的时候没有缓冲区,所以不需要进行flush操作。
    public static void main(String[] args) throws Exception {
FileOutputStream out = new FileOutputStream("D:\\b.txt");
//写入数据
//字节输出流没有缓冲区
out.write("天乔巴夏".getBytes());
//关流是为了释放文件
out.close();
}
  • 文件字节输入流 FileInputStream,可以定义字节数组作为缓冲区。
    public static void main(String[] args) throws Exception{
FileInputStream in = new FileInputStream("E:\\1myblog\\Node.png");
//1.读取字节
int i;
while((i = in.read()) ! =-1)
System.out.println(i);
//2.定义字节数组作为缓冲区
byte[] bs = new byte[10];
//定义变量记录每次实际读取的字节个数
int len;
while((len = in.read(bs)) != -1){
System.out.println(new String(bs, 0, len));
}
in.close(); }

缓冲流

字符缓冲流

  • BufferedReader:在构建的时候需要传入一个Reader对象,真正读取数据依靠的是传入的这个Reader对象BufferedReadReader对象中获取数据提供缓冲区
    public static void main(String[] args) throws IOException {
//真正读取文件的流是FileReader,它本身并没有缓冲区
FileReader reader = new FileReader("D:\\b.txt");
BufferedReader br = new BufferedReader(reader);
//读取一行
//String str = br.readLine();
//System.out.println(str); //定义一个变量来记录读取的每一行的数据(回车)
String str;
//读取到末尾返回null
while((str = br.readLine())!=null){
System.out.println(str);
}
//关外层流即可
br.close();
}
  • BufferedWriter:提供了一个更大的缓冲区,提供了一个newLine的方法用于换行,以屏蔽不同操作系统的差异性
    public static void main(String[] args) throws Exception {
//真正向文件中写数据的流是FileWriter,本身具有缓冲区
//BufferedWriter 提供了更大的缓冲区
BufferedWriter writer = new BufferedWriter(new FileWriter("E:\\b.txt"));
writer.write("天乔");
//换行: Windows中换行是 \r\n linux中只有\n
//提供newLine() 统一换行
writer.newLine();
writer.write("巴夏");
writer.close();
}

装饰设计模式

缓冲流基于装饰设计模式,即利用同类对象构建本类对象,在本类中进行功能的改变或者增强。

例如,BufferedReader本身就是Reader对象,它接收了一个Reader对象构建自身,自身提供缓冲区其他新增方法,通过减少磁盘读写次数来提高输入和输出的速度。

除此之外,字节流同样也存在缓冲流,分别是BufferedInputStreamBufferedOutputStream

转换流(适配器)

利用转换流可以实现字符流和字节流之间的转换

  • OutputStreamWriter
    public static void main(String[] args) throws Exception {
//在构建转换流时需要传入一个OutputStream 字节流
OutputStreamWriter ow =
new OutputStreamWriter(
new FileOutputStream("D:\\b.txt"),"utf-8");
//给定字符--> OutputStreamWriter转化为字节-->以字节流形式传入文件FileOutputStream
//如果没有指定编码,默认使用当前工程的编码
ow.write("天乔巴夏");
ow.close();
}

最终与文件接触的是字节流,意味着将传入的字符转换为字节


  • InputStreamReader
    public static void main(String[] args) throws IOException {
//以字节形式FileInputStream读取,经过转换InputStreamReader -->字符
//如果没有指定编码。使用的是默认的工程的编码
InputStreamReader ir =
new InputStreamReader(
new FileInputStream("D:\\b.txt"));
char[] cs = new char[5];
int len;
while((len=ir.read(cs))!=-1){
System.out.println(new String(cs,0,len));
}
ir.close();
}

最初与文件接触的是字节流,意味着将读取的字节转化为字符

适配器设计模式

缓冲流基于适配器设计模式,将某个类的接口转换另一个用户所希望的类的接口,让原本由于接口不兼容而不能在一起工作的类可以在一起进行工作。

OutputStreamWriter为例,构建该转换流时需要传入一个字节流,而写入的数据最开始是由字符形式给定的,也就是说该转换流实现了从字符向字节的转换,让两个不同的类在一起共同办事。

标准流/系统流

程序的所有输入都可以来自于标准输入,所有输出都可以发送到标准输出,所有错误信息都可以发送到标准错误

标准流分类

对象 解释 封装类型
System.in 标准输入流 InputStream
System.out 标准输出流 PrintStream
System.err 标准错误流 PrintStream

可以直接使用System.outSystem.err,但是在读取System.in之前必须对其进行封装,例如我们之前经常会使用的读取输入:Scanner sc = new Scanner(System.in);实际上就封装了System.in对象。

  • 标准流都是字节流
  • 标准流对应的不是类而是对象。
  • 标准流在使用的时候不用关闭。
    /**
* 从控制台获取一行数据
* @throws IOException readLine 可能会抛出异常
*/
public static void getLine() throws IOException {
//获取一行字符数据 -- BufferedReader
//从控制台获取数据 -- System.in
//System是字节流,BufferedReader在构建的时候需要传入字符流
//将字节流转换为字符流
BufferedReader br =
new BufferedReader(
new InputStreamReader(System.in));
//接收标准输入并转换为大写
String str = br.readLine().toUpperCase();
//发送到标准输出
System.out.println(str);
}

通过转换流,将System.in读取的标准输入字节流转化为字符流,发送到标准输出,打印显示。

打印流

打印流只有输出流没有输入流

  • PrintStream: 打印字节流
    public static void main(String[] args) throws IOException {
//创建PrintStream对象
PrintStream p = new PrintStream("D:\\b.txt");
p.write("abc".getBytes());
p.write("def".getBytes());
p.println("abc");
p.println("def");
//如果打印对象,默认调用对象身上的toString方法
p.println(new Object());
p.close();
}
  • PrintWriter:打印字符流
    //将System.out转换为PrintStream
public static void main(String[] args) {
//第二个参数autoFlash设置为true,否则看不到结果
PrintWriter p = new PrintWriter(System.out,true);
p.println("hello,world!");
}

合并流

  • SequenceInputStream用于将多个字节流合并为一个字节流的流。
  • 有两种构建方式:
    • 将多个合并的字节流放入一个Enumeration中来进行。
    • 传入两个InputStream对象。
  • 合并流只有输入流没有输出流。

以第一种构建方式为例,我们之前说过,Enumeration可以通过Vector容器的elements方法创建。

    public static void main(String[] args) throws IOException {
FileInputStream in1 = new FileInputStream("D:\\1.txt");
FileInputStream in2 = new FileInputStream("D:\\a.txt");
FileInputStream in3 = new FileInputStream("D:\\b.txt");
FileInputStream in4 = new FileInputStream("D:\\m.txt"); FileOutputStream out = new FileOutputStream("D:\\union.txt");
//准备一个Vector存储输入流
Vector<InputStream> v = new Vector<>();
v.add(in1);
v.add(in2);
v.add(in3);
v.add(in4); //利用Vector产生Enumeration对象
Enumeration<InputStream> e = v.elements();
//利用迭代器构建合并流
SequenceInputStream s = new SequenceInputStream(e); //读取
byte[] bs = new byte[10];
int len;
while((len = s.read(bs))!=-1){
out.write(bs,0,len);
}
out.close();
s.close();
}

序列化/反序列化流

  • 序列化:将对象转化为字节数组的过程。
  • 反序列化:将字节数组还原回对象的过程。

序列化的意义

对象序列化的目标是将对象保存在磁盘中,或允许在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种流,都可以将这种二进制流恢复为原来的Java对象。

让某个对象支持序列化的方法很简单,让它实现Serializable接口即可:

public interface Serializable {
}

这个接口没有任何的方法声明,只是一个标记接口,表明实现该接口的类是可序列化的。

我们通常在Web开发的时候,JavaBean可能会作为参数或返回在远程方法调用中,如果对象不可序列化会出错,因此,JavaBean需要实现Serializable接口。

序列化对象

创建一个Person类。

//必须实现Serializable接口
class Person implements Serializable {
//序列化ID serialVersionUID
private static final long serialVersionUID = 6402392549803169300L;
private String name;
private int age; public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
}
}

创建序列化流,将对象转化为字节,并写入"D:\1.data"。

public class ObjectOutputStreamDemo {
public static void main(String[] args) throws IOException {
Person p = new Person();
p.setAge(18);
p.setName("Niu");
//创建序列化流
//真正将数据写出的流是FileOutputStream
//ObjectOutputStream将对象转化为字节
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\1.data"));
out.writeObject(p);
out.close();
}
}

创建反序列化流,将从"D:\1.data"中读取的字节转化为对象。

    public static void main(String[] args) throws IOException, ClassNotFoundException {
//创建反序列化流
//真正读取文件的是FileInputStream
//ObjectInputStream将读取的字节转化为对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\1.data"));
//读取数据必须进行数据类型的强制转换
Person p = (Person)in.readObject();
in.close();
System.out.println(p.getName());//Niu
System.out.println(p.getAge());//18 }

需要注意的是:

  • 如果一个对象要想被序列化,那么对应的类必须实现接口serializable,该接口没有任何方法,仅仅作为标记使用。
  • statictransient修饰的属性不会进行序列化。如果属性的类型没有实现serializable接口但是也没有用这两者修饰,会抛出NotSerializableException
  • 在对象序列化的时候,版本号会随着对象一起序列化出去,在反序列化的时候,对象中的版本号和类中的版本号进行比较,如果版本号一致,则允许反序列化。如果不一致,则抛出InvalidClassException
  • 集合允许被整体序列化 ,集合及其中元素会一起序列化出去。
  • 如果对象的成员变量是引用类型,这个引用类型也需要是可序列化的。
  • 当一个可序列化类存在父类时,这些父类要么有无参构造器,要么是需要可序列化的,否则将抛出InvalidClassException的异常。

关于版本号

  • 一个类如果允许被序列化,那么这个类中会产生一个版本号 serialVersonUID

    • 如果没有手动指定版本号,那么在编译的时候自动根据当前类中的属性和方法计算一个版本号,也就意味着一旦类中的属性发生改变,就会重新计算新的,导致前后不一致。
    • 但是,手动指定版本号的好处就是,不需要再计算版本号。
  • 版本号的意义在于防止类产生改动导致已经序列化出去的对象无法反序列化回来。版本号必须用static final修饰,本身必须是long类型。

自定义序列化的两种方法

Serializable自定义

// 实现writeObject和readObject两个方法
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable { private String name;
private int age; // 将name的值反转后写入二进制流
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
} // 将读取的字符串反转后赋给name
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}
}

还有一种更加彻底的自定义机制,直接将序列化对象替换成其他的对象,需要定义writeReplace

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable { private String name;
private int age; private Object writeReplace(){
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}

Externalizable自定义

Externalizable实现了Seriablizable接口,并规定了两个方法:

public interface Externalizable extends java.io.Serializable {

    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

实现该接口,并给出两个方法的实现,也可以实现自定义序列化。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Externalizable { String name;
int age; @Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
} @Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}
}

参考阅读


写在最后:如果本文有叙述错误之处,还望评论区批评指正,共同进步。

参考资料:《Java 编程思想》、《Java语言程序设计》、《大话设计模式》、《疯狂Java讲义》

详解Java中的IO输入输出流!的更多相关文章

  1. 详解Java中的clone方法

    详解Java中的clone方法 参考:http://blog.csdn.net/zhangjg_blog/article/details/18369201/ 所谓的复制对象,首先要分配一个和源对象同样 ...

  2. 详解Java中的字符串

    字符串常量池详解 在深入学习字符串类之前, 我们先搞懂JVM是怎样处理新生字符串的. 当你知道字符串的初始化细节后, 再去写String s = "hello"或String s ...

  3. 【Java学习笔记之三十三】详解Java中try,catch,finally的用法及分析

    这一篇我们将会介绍java中try,catch,finally的用法 以下先给出try,catch用法: try { //需要被检测的异常代码 } catch(Exception e) { //异常处 ...

  4. 详解Java中的Object.getClass()方法

    详解Object.getClass()方法,这个方法的返回值是Class类型,Class c = obj.getClass(); 通过对象c,我们可以获取该对象的所有成员方法,每个成员方法都是一个Me ...

  5. 干货——详解Java中的关键字

    在平时编码中,我们可能只注意了这些static,final,volatile等关键字的使用,忽略了他们的细节,更深层次的意义. 本文总结了Java中所有常见的关键字以及一些例子. static 关键字 ...

  6. 详解Java中格式化日期的DateFormat与SimpleDateFormat类

    DateFormat其本身是一个抽象类,SimpleDateFormat 类是DateFormat类的子类,一般情况下来讲DateFormat类很少会直接使用,而都使用SimpleDateFormat ...

  7. 详解Java中的clone方法:原型模式

    转:http://developer.51cto.com/art/201506/478985.htm clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象.所谓的 ...

  8. 详解Java中的访问控制修饰符(public, protected, default, private)

    Java中的访问控制修饰符已经困惑笔者多时,其中较复杂的情况一直不能理解透彻.今天下定决心,系统.全面地研究Java中的访问控制修饰符的所有方面,并整理成这篇文章,希望有同样疑惑的读者读完后能有所收获 ...

  9. java中String是对象还是类?详解java中的String

    有很多人搞不懂对象和类的定义.比如说java中String到底是对象还是类呢? 有人说String 既可以说是类,也可以说是对象. 其实他这么说也没问题, 类和对象其实都是一个抽象的概念. 我们可以把 ...

随机推荐

  1. C#实现SM2国密加密

    本文主要讲解"国密加密算法"SM系列的C#实现方法,不涉及具体的算法剖析,在网络上找到的java实现方法比较少,切在跨语言加密解密上会存在一些问题,所以整理此文志之.JAVA实现参 ...

  2. Jmeter监控插件

    Jmeter-Plugins支持CPU.Memory.Swap.Disk和Network的监控,在测试过程中更加方便进行结果收集和统计分析. 一.准备工作: 1.下载Jmeter-Plugins插件, ...

  3. 深度分析:那些Java中你一定遇到过的问题,一次性帮你搞定!深度分析:那些Java中你一定遇到过的问题,一次性帮你搞定!

    1.java中==和equals和hashCode的区别 基本数据类型的比较的值相等.类的比较的内存的地址,即是否是同一个对象,在不覆盖equals的情况下,同比较内存地址,原实现也为 == ,如St ...

  4. 仅一年工作经验成功跳槽字节跳动,腾讯并拿到字节的offer,全靠这份面经!

    前言 前几天由于工作的原因一直没怎么看私信,昨天在整理私信的时候看到了一个粉丝给我疯狂私信想要我帮忙整理一份大厂面经,说自己工作也有几年了想跳槽冲刺一下,但是不知道该怎么做好前期准备.我看到这个粉丝也 ...

  5. [COCI2016-2017#1] Mag

    [COCI2016-2017#1] Mag 题解 题目TP门 题目描述 你将获得一棵由无向边连接的树.树上每个节点都有一个魔力值. 我们定义,一条路径的魔力值为路径上所有节点魔力值的乘积除以路径上的节 ...

  6. 如何用pdfFactory新建打印机并设置属性

    今天我们来讲一讲,在pdfFactory中如何去修改PDF文件打印页面的页边距.页面大小.页面清晰度等属性参数. pdfFactory是一款Windows平台上的虚拟打印机,在没有打印机可以安装的情况 ...

  7. 【flask-migrate】:ERROR [root] Error: Target database is not up to date.

    问题:flask-migrate数据迁移添加新的表,执行python manager.py db migrate 出现Target database is not up to date 原因: 1. ...

  8. 精尽 MyBatis 源码分析 - SqlSession 会话与 SQL 执行入口

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  9. PC 端轮播图的实现

    <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8& ...

  10. Python实现树莓派摄像头持续录像并传送到主机

    关于树莓派,想必从事嵌入式开发的开发者都有听过,树莓派原名为Raspberry Pi,也就是它的英文读法,树莓派诞生于英国,由"Raspberry Pi 基金会"这个慈善组织注册开 ...