Java——IO流

IO流在整个Java的体系中都是十分重要的,最近用来觉得一些东西比较生疏,就回过头梳理一下整个知识体系加深印象。

概念

首先我们来讲一些概念性的东西,从整体上感受一下这个流程:IO就是Input和Output。一个是in,一个是out。对应的:一个是进,一个是出。

我总是很喜欢用参照系这个概念,IO的参照系就是我们的应用程序,这里可以理解成我们编写的项目工程。

那么In是什么,In就是向我们的程序中读入,我们叫Read。Out是什么,Out就是我们从程序写出,我们叫Write。

很好理解吧?那么我们就可以想象一下整个数据走过的流程了:从数据源源设备我们把数据读入到程序内,经过我们程序的一系列处理对数据进行加工,数据处理好了,再从我们的应用程序把数据给写出来到目标设备

刚刚我们提到了一个东西叫数据源,数据源,顾名思义就是数据的源头。一般的,可以作为数据源的,可以是我们的本地文件、数据库,再或者服务器。也就是说,凡是可以和我们的应用程序产生数据交互的都可以称之为数据源。

注意我们这里说的是交互,也就是说,不只是为应用程序(Program)提供数据的叫数据源,最后接受承载来自应用程序的也称为数据源。数据源分为:源设备(Source),对应输入流;目标设备(Dest),对应输出流

这里其实不必纠结,反过来思考一下,为什么引入数据源这个概念?其实,我们对于设备而言,是不是既可以做源设备提供数据,也可以作为目标设备接收数据呢?也就是说,我们的数据在实际情况下,往往不只需要一个应用程序的一次加工,那么应用程序输出的数据,也就可以作为下一次加工的输入数据了。

接下来,我们再说一下上面提到的这个输入流和输出流。我们总是说IO流,其实说的就是输入流和输出流。那么什么是流?我们用图来理解一下。
IO流
我们说,由于文件的形式、格式有很多种,大小也各不相同,我们往往无法一次性的将所有的数据都灌入到程序中,同样对于输出也是一样。那么,我们就可以在数据源与应用程序之间,建立一个管道,让数据像流一样,通过管道源源不断的流入或流出。这,就是流的概念。当然了,从数据源流入到应用程序的,就叫做输入流(InputStream);从应用程序流出到数据源的,就叫做输出流(OutputStream)。那么,我们数据流需要一个管道作为媒介,这个管道我们把它称为缓冲区,缓冲区的大小我们和以理解成管道的容积。

那么对于任何的数据源和应用程序,我们都可以建立这种流关系,将数据进行读入与写出。工作都做完了,最后,我们还不要忘了把管子给拔下来,也就是关闭流,调用close()方法。

现在概念我们都理解了,接下来就谈一谈对于程序来说,整个过程怎样编写。

File类

想要学习IO,需要了解一个与IO流密不可分的类——File。File就是文件,IO流的本质也就是对文件的处理。

涉及到File对象的创建,路径的使用等等。还有一些File的常用方法,需要掌握。

File体系比较庞大,这里不做详细说明。

有关File类的具体内容请看我的这篇文章

四大常用IO抽象类

在Java中,IO流有四大常用抽象类。也就是说,一般的情况下,我们可以通过继承这四个类完成基本的流操作。而其他更高级的类与方法,也都是在这四个类的基础上编写的,又一次很好地体现了Java的继承思想。

那么下面我们就来介绍这四个类:

InputSream / OutputStream

InputStream是字节输入流的所有类的父类。
OutputStream是字节输出流的所有类的父类。
所有数据的读入和写出都需要他们的子类来实现。

数据单位为字节Byte(8 bit)。

Reader/Writer

Reader是字符输入流的所有类的父类。
Writer是字符输出流的所有类的父类。
所有数据的读入和写出都需要他们的子类来实现。

数据单位为字符char(2 Byte 16 bit)。

下面我们简单说一下字节流和字符流的区别:

也就是什么时候该使用字节流,什么时候使用字符流?
对于字节流,它可以支持声音,视频,图片,文本等所有类型,而字符流只支持文本文件
但是,我们说字节流虽然可以处理所有的文件类型,可是我们需要注意,当用字节流处理文本文件时,由于编码方式的原因,他无法解析中文。所以,我们一般用字符流处理文本文件。

提到了字符和字节,咱们需要简单了解一下编码:

我们知道,计算机的底层都是通过二进制实现的,计算机他只懂得0和1,也只能显示0和1,那如果一直这样的话,我们编写程序和读程序的时候难不成要成天01010101的吗?这不好写,也根本看不懂。
因此,我们就需要把它转化成我们所能理解的方式,这就引入了一种方法——编码。由我们人为地来规定一种规则,什么样的01组合代表什么文字。
在程序编写时,我们用某种编码方式对程序进行编写,在查看程序时,再用该编码对应的解码方式进行解码。(我们的编译器的本质其实就是这个作用)

常见的编码方式有:

 ASCII码:单字节编码,不支持中文。

 GBK:国标码。国产的,采用单双字节变长编码,英文使用单字节完全兼容ASCII字符编码,中文部分采用双字节编码。
 
 Unicode:ISO制定,是一套字符集,支持中文。有三种编码方式:
 	UFT-8:一种变长的编码方案,使用 1~6 个字节来存储;
	UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;
	UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。
 
    UTF-8编码:双字节编码(一个字符),支持中文。(tip:utf-8中,一个汉字占3个字节)
    说到utf-8多说几句,在编程中往往需要设置编码方式。对于utf-8而言,很多时候写UTF-8对,utf-8对,utf8也对。

注:由于Unicode编码强制两字节,对于单字节文字造成空间的浪费,所以在Unicode的基础上,改进并诞生了UTF-8。utf-8 区分每个字符的开始是根据字符的高位字节来区分的,比如用一个字节表示的字符,第一个字节高位以“0”开头;用两个字节表示的字符,第一个字节的高位为以“110”开头,后面一个字节以“10开头”;用三个字节表示的字符,第一个字节 以“1110”开头,后面俩字节以“10”开头;用四个字节表示的字符,第一个字节以“11110”开头,后面的三个字节以“10”开头。

如果编码方式和解码方式不一致的话,就会导致乱码,就是我们常常会看到的那样,文件打开是一堆看不懂的文字,时不时还能蹦出来个笑脸。

这部分我们简单了解一下就好,无需深究。

那么我们突发奇想一下,既然字节比字符单位更小,那么能不能用字节流读取字符呢?

答案是可以的,但是,我们说由于编码方式不同,字节流在读取字符流的时候,会按照ASCII码的方式解码,将字符输出为对应的ASCII码。

言归正传,Java所采用的编码方式是Unicode编码,所以,是双字节编码。两个字节,一个字符。我们继续对四个抽象类的派生类及运用进行进一步的解析:

InputStream

三个常用的类方法为:ByteArrayInputStream、StringBufferInputStream、FileInputStream。它们分别对应了:字节数组、字符串缓冲区和文件的数据读入操作。

PipedInputStream用于建立多线程的数据通道,将多个数据通道合为一个通道,为线程共用的管道中读取数据。

ObjectInputStream 和FilterInputStream 是装饰流,用于流的修饰,前者是用于对象反序列化,后者为过滤器。
//序列化操作可以将一个对象写出,或者读取一个对象到程序中,也就是执行了序列化和反序列化操作。具体后面进行说明。

OutputStream

两个常用类方法为:ByteArrayOutputStream、FileOutputStream 分别对应项Byte 数组、和文件中写出数据。

PipedOutputStream 是向与其它线程共用的管道中写入数据。

ObjectOutputStream 和所有FilterOutputStream 同样用于流的修饰。

Reader

Reader有四个常用子类FileReader、 BufferedReader、InputStreamReader、CharArrayReader。

FileReader实现字符串的读入。

BufferedReader采用缓冲区的方式对数据高效读入。

InputStreamReader用于从字节到字符的转换。

CharArrayReader用于数组数据的读入。

Writer

Writer有六个常用子类:BufferedWriter、FileWriter、PrintWriter、PipedWriter、PrintSWrite、CharArrayWriter。

BufferedWriter重写了父类flush()方法,运用了缓冲区。

FileWriter用于写入字符串到文件。

PipedWriter主要用于线程间通讯,也可以用来传输字符。

PrintSWrite本质上是PrintStream的字符形式的版本

CharArrayWriter是向数组中写出数据。

由于IO的子类都是一对一对的,而且都是源自于四个基础的抽象类,讲了这么多,我们来简单整理一下:

讲了这么多,下面我们对共性的性质进行下简单的描述:
在这里插入图片描述

细化

我们已经对四大抽象类进行了介绍,对IO流有了一个初步的认识,下面我们继续从宏观上整体细化一下IO的整体架构。

我们做一张图对流进行一下描述:
在这里插入图片描述

节点流

文件:对文件进行处理

数组 :对数组进行处理的节点流,对应是内存中的一个数组。

字符串 :对字符串进行处理的节点流

线程:对管道进行处理的节点流,实现多线程的流共用。

FileInputStream读入

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class firstIO {
//程序的主方法
	public static void main(String [] args) throws IOException{
		int i=0;
		FileInputStream in=null;
		try {
			in=new FileInputStream("C:\\Users\\10854\\Desktop\\1.txt");//从文件外读数据
		} catch (FileNotFoundException e) {
		  e.printStackTrace();
		}
		try {
			int num=0;//字节计数器
			while((i=in.read())!=-1){
				System.out.println((char)i);//将ASCII码值转换成字符
				num++;
			}
			in.close();
			System.out.println("传输字节个数:"+num);
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println("读取文件错误");
		}
		
	}
	
}

FileOutputStream写出

import java.io.FileInputStream;
import java.io.FileOutputStream;

public class outIO {
	public static void main(String []args){
		int i=0;
		FileInputStream in=null;
		FileOutputStream out=null;
		try {
			//实例化FileInputStream,FileOutputStream对象
			in=new FileInputStream("C:\\Users\\10854\\Desktop\\1.txt");
			out=new FileOutputStream("C:\\Users\\10854\\Desktop\\2.txt");
			while((i=in.read())!=-1){
				out.write(i);
				System.out.println((char)i);
			}
			in.close();
			out.close();
			System.out.println("文件已复制");
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println("复制失败");
			System.exit(-1);
		} 
		
		
	}
}

FileRead、FileWriter进行文件复制

import java.io.FileReader;
import java.io.FileWriter;
 
public class TestFieldWriter1 {
public static void main(String []args){
	FileReader fr=null;
	FileWriter fw=null;
	try {
		fr=new FileReader("C:\\Users\\10854\\Desktop\\1.txt");//读取的文件
		fw=new FileWriter("C:\\Users\\10854\\Desktop\\3.txt");//目的文件
		int i=0;
		while((i=fr.read())!=-1){
			fw.write(i);
			System.out.println((char)i);//将得到的ASCII码值转换成字符
		}
		fr.close();
		fw.close();
	} catch (Exception e) {
		e.printStackTrace();
	}
}
}

处理流

缓冲流:增加缓冲功能,避免频繁读写硬盘。(通过Buffer实现)

转换流:实现字节流和字符流之间的转换,需要InputStream或OutputStream作为参数。

数据流: 提供将基础数据类型写入到文件中,或者读取出来。

缓冲区

我们上面整理过,处理流中有一个流叫做缓冲流,缓冲流的类包括:BufferedInputStream 、BufferedOutputStream、 BufferedReader、 BufferedWriter。观察可以发现,这四个类都有一个共性的关键字为Buffer(缓冲器、缓冲区),Buffer就是我们缓冲流的辨别标志。

接下来我们来讲一下缓冲区(Buffer),有一个方法flush,作用是刷新缓冲区。
我们前文提到,数据之间的传输我们可以通过一根管子(流)来建立连接,那么有的时候,我们完成了一个阶段的数据传输,想查看一下数据,可是此时管子里还留存着一部分数据,这部分数据如果不进行处理,就无法在文件中查看到这部分数据,那么我们就需要做一个操作flush(刷新)。将这部分数据处理掉,让这一阶段的所有的数据都到它该去的地方,完成数据传输。

对于BufferedInputStream和BufferedReader体现在数据的读入上,缓冲区对于它的意义就是提高运输量,减少解码次数,提高程序的效率,这里我们无需主动调用flush方法。
有一位博主的一篇文章解释得特别好,这里分享给大家BufferedReader流

然后我们继续说写出流,以BufferedInputStream为例,我们首先来看一下源码:

BufferedInputStream 继承自 FilterInputStream。InputStream和FilterInputStream中没有flush方法,buffer的操作是体现在BufferedInputStream对close方法的重写中的。

public void close() throws IOException {
        byte[] buffer;
        while ( (buffer = buf) != null) {
            if (bufUpdater.compareAndSet(this, buffer, null)) {
                InputStream input = in;
                in = null;
                if (input != null)
                    input.close();
                return;
            }
            // Else retry in case a new buf was CASed in fill()
        }
    }

BufferedOutputStream类重写了父类的flush方法,并在其中调用了flushBuffer()方法以及OutputStream的flush()方法。

BufferedOutputStream继承自FilterOutputStream,FilterOutputStream继承自OutputStream。FilterOutputStream和OutputStream类的flush()什么操作都没做,方法为空。

	public void flush() throws IOException {
    }

对于BufferedOutputStream我们查看一下源码:

	public synchronized void flush() throws IOException {
        flushBuffer();
        out.flush();
    }
	private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }
	public synchronized void write(byte b[], int off, int len) throws IOException {
        if (len >= buf.length) {
            /* If the request length exceeds the size of the output buffer,
               flush the output buffer and then write the data directly.
               In this way buffered streams will cascade harmlessly. */
            flushBuffer();
            out.write(b, off, len);
            return;
        }
        if (len > buf.length - count) {
            flushBuffer();
        }
        System.arraycopy(b, off, buf, count, len);  //写入缓冲区
        count += len;
    }

可以看到重写了本为空的flush,调用了新方法flushBuffer。flushBuffer中调用了write。我们可以看到缓冲区buffer本质为一个byte[]数组,BufferedOutputStream的每一次write其实是将内容写入byte[],当buffer空间满了之后,就会将数据写入。

然后我们看一下close()方法:

	@SuppressWarnings("try")
    public void close() throws IOException {
        synchronized (lock) {
            if (out == null) {
                return;
            }
            try (Writer w = out) {
                flushBuffer();
            } finally {
                out = null;
                cb = null;
            }
        }
    }

可以发现在close方法中调用了flushBuffer,清空了缓冲区。

BufferedReader可以指定缓冲区的大小,默认为8192 char。这点在源码中有所体现。

那么我们来总结一下什么时候才需要主动调用flush()?
示例如下:

public static void main(String[] args) throws IOException {

        File demo = new File("demo");
        if(!demo.exists()){
            demo.mkdir();
        }
        File file = new File(demo, "raf.dat");
        if(!file.exists()){
            file.createNewFile();
        }
        PrintWriter pw = new PrintWriter(file);
        String s = "";
        for(int i = 0;i<2000;i++){
            s="我是要写入到记事本文件的内容"+i;
            pw.write(s);
        }
        pw.close();

    }

在这段代码中,
只写pw.close() ,完整输出“我是要写入到记事本文件的内容1999”。
不写pw.close(),输出“我是要写入到记事本文件的内容1804”(不完整输出,因为1804到1999这部分没有填满缓冲区)。
只写pw.flush();也可以进行完整的输出。(清空了缓冲区,所以完整输出。不过此时流未关闭,实际情况下应注意将流关闭)。
也就是close()时会自动flush。而在暂时不关闭流的情况下,需要执行一个刷新操作,把缓冲区的内容写出,就需要调用flush();
BufferWriter源码原理设定与BufferedOutputStream类似,故这种情况适用于字节流:BufferedOutputStream和Writer。

Serializable序列化

Serializable是对象序列化接口,一个类只有实现了Serializable接口,它的对象才能够被实例化。序列化的目的是为了将对象持久性保存。Serializable其实是空的,它只是一个标识接口没有实质性内容,而是作为一个标识存在作用就是为了提醒JVM,这个类需要JVM进行序列化操作。

serialversionUID

实体类在实现Serializable接口后,需要定义一个long类型的值 serialversionUID,这个UID将会在序列化操作时被系统写入到文件。它的作用是作为一个检测依据,当对文件数据进行反序列化操作时,判断二者的serialversionUID是否一致。如若一致,说明一切正常,可以反序列化成功;如果不一致,说明序列化前后类的属性发生了改变,将无法反序列化成功,抛出异常。

已经了解了什么是序列化,下面我们就对ObjectOutputStream和ObjectInputStream进行讲解。大家注意一下顺序:ObjectOutputStream、ObjectInputStream。
ObjectOutputStream可以将一个对象从程序中写出到文件,执行序列化操作。
ObjectInputStream可以将一个对象从文件读入到程序中,执行反序列化操作。
也就是ObjectOutputStream把对象变成序列存到文件中,而ObjectInputStream从文件中把序列取出来变回对象。

下面我们演示一下:

首先创建一个演示类:

public class Student implements Serializable {
//注:类应该实现序列化接口Serializable 
	private String name;
	private Integer age;

	public Student(String name, Integer age) {
		super();
		this.name = name;
		this.age = age;
	}
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}
}

ObjectOutputStream

实现对象的序列化。

 public class TestObjectOutputStream {
	public static void main(String[] args) throws IOException{
		Student s1=new Student("张三",18);
		Student s2=new Student("李四",19);
		ObjectOutputStream os=new ObjectOutputStream(new FileOutputStream("Students.txt"));
		os.writeObject(s1);
		os.writeObject(s2);
		os.close(); 
	}
 
}

ObjectInputStream

实现对象的反序列化。

public class TestObjectInputStream {
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		ObjectInputStream ois=new ObjectInputStream(new FileInputStream("Students.txt"));
		Student s1=(Student) ois.readObject();
		Student s2=(Student) ois.readObject();
		System.out.println(s1);
		System.out.println(s2);
		ois.close(); 
	}
}

优化

在此基础上,我们可以采用数据结构对对象的存取进行优化。

public class Test{
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		ArrayList<Student> list1=new ArrayList<Student>();
		list1.add(new Student("张三",18));
		list1.add(new Student("李四",19));
		list1.add(new Student("王五",20));
		list1.add(new Student("赵六",21));
		ObjectOutputStream os=new ObjectOutputStream(new FileOutputStream("Students.txt"));
		os.writeObject(list1);
		System.out.println("写入成功!");
		os.close();

		ObjectInputStream osi=new ObjectInputStream(new FileInputStream("Students.txt"));
		ArrayList<Student> list2=(ArrayList<Student>) osi.readObject();
		osi.close();
		System.out.println("读取对象集合");
		for (Student student : list2) {
			System.out.println(student);
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值