JavaIO流超详解
转载请标明文章出处
前言
做java开发的时候,难免要与外部设备做一些数据交互,这时候,就需要我们用到IO流,本篇文章将先介绍一下javaIO流的四个基类,再从设计角度的模式介绍IO流的体系,最后再介绍一下常用流的使用。希望对大家有帮助!!!
一、IO流是什么?
I/O 即输入Input/ 输出Output的缩写,其实就是计算机调度把各个存储中(包括内存和外部存储)的数据写入写出的过程,在Java中把不同的输入/输出源抽象表述为"流"。流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
下面贴出JavaIO流的体系结构图(红色棋子代表过时):
二、IO流的四大基类
1、四大基类的概述
InputStream,OutputStream,Reader,Writer四大基类都实现Closeable和AutoCloseable两个接口。除此之外,OutputStream,Writer还实现了Flushable接口;Reader实现了Readable接口,Writer实现了Appendable接口。
Closeable接口和AutoCloseable接口:
Closeable接口定义于 java.io包中,于JDK5添加;AutoCloseable接口 定义于java.lang包中, 于JDK7添加,Closeable扩展了AutoCloseable,即Closeable接口继承了AutoCloseable接口,两个接口都只定义了一个close()方法,需要实现类去重写,从JDK7开始,Closeable和AutoCloseable具有相同的功能:使用 try-with-resources 语法,达到自动调用close 方法进行资源释放的目的。
Flushable接口:
实现了Flushable接口的类的对象,可以强制将缓存的输出写入到与对象关联的流中。写入流的所有I/O类都实现了Flushable接口。其中,IO输出流Writer和OutputStream的子类中除了BufferedOutputStream、BufferedWriter真正实现flush方法的,其它都是直接调用父类空的flush方法
Readable接口(了解):Readable接口就是为了Scanner类专门创建的一个接口,使得Scanner的入口参数不必限于某个类。
Appendable接口(了解):能够被追加 char 序列和值的对象。如果某个类的实例打算接收来自 Formatter的格式化输出,那么该类必须实现 Appendable 接口。
2、InputStream
InputStream抽象类的方法介绍:
public abstract class InputStream implements Closeable {
//最大跳过的字节数和缓存数
private static final int MAX_SKIP_BUFFER_SIZE = 2048;
/*从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。
如果因为已经到达流末尾而没有可用的字节,则返回值 -1。
子类必须提供此方法的一个实现。*/
public abstract int read() throws IOException;
/*从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
以整数形式返回实际读取的字节数。*/
public int read(byte b[]) throws IOException {}
/*底层是 public int read(byte b[]),
建议子类提供此方法更为有效的实现。*/
public int read(byte b[], int off, int len) throws IOException {}
/*此类的 skip 方法创建一个 byte 数组,然后重复将字节读入其中,
直到读够 n 个字节或已到达流末尾为止。
建议子类提供此方法更为有效的实现。*/
public long skip(long n) throws IOException {}
/*返回此输入流下一个方法调用可以不受阻塞
地从此输入流读取(或跳过)的估计字节数,
类 InputStream 的 available 方法总是返回 0。
此方法应该由子类重写。*/
public int available() throws IOException {}
/*关闭此输入流并释放与该流关联的所有系统资源。
InputStream 的 close 方法不执行任何操作。*/
public void close() throws IOException {}
/*在此输入流中标记当前的位置,通过reset方法
将此流重新定位到最后一次对此输入流调用 mark 方法时的位置。
readlimit 参数告知此输入流在标记位置失效之前允许读取的字节数。
BufferedInputStream类调用mark(int readlimit)方法后读取多少字节标记才失效,
是取readlimit和BufferedInputStream类的缓冲区大小两者中的最大值,
而并非完全由readlimit确定。*/
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {}
/*测试此输入流是否支持 mark 和 reset 方法。
是否支持 mark 和 reset 是特定输入流实例的不变属性。
InputStream 的 markSupported 方法返回 false。*/
public boolean markSupported() {}
}
3、OutputStream
OutputStream抽象类的方法介绍:
package java.io;
public abstract class OutputStream implements Closeable, Flushable {
/*将指定的字节写入此输出流。OutputStream 的子类必须提供此方法的实现。 */
public abstract void write(int b) throws IOException;
/*OutputStream 的子类必须提供此方法的实现。底层是void write(byte b[], int off, int len) */
public void write(byte b[]) throws IOException {}
/*将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。
建议子类重写此方法并提供更有效的实现。*/
public void write(byte b[], int off, int len) throws IOException {}
/*刷新此输出流并强制写出所有缓冲的输出字节。
flush 的常规协定是:如果此输出流的实现已经缓冲了以前写入的任何字节,
则调用此方法指示应将这些字节立即写入它们预期的目标。
OutputStream 的 flush 方法不执行任何操作。 */
public void flush() throws IOException { }
/*关闭此输出流并释放与此流有关的所有系统资源。
OutputStream 的 close 方法不执行任何操作。 */
public void close() throws IOException { }
}
4、Reader
/*试图将字符读入指定的字符缓冲区。缓冲区可照原样用作字符的存储库:
所做的唯一改变是 put 操作的结果。不对缓冲区执行翻转或重绕操作。 */
public int read(java.nio.CharBuffer target) throws IOException {}
/*读取单个字符。用于支持高效的单字符输入的子类应重写此方法。 */
public int read() throws IOException {}
/*将字符读入数组。底层是:
abstract public int read(char cbuf[], int off, int len)*/
public int read(char cbuf[]) throws IOException {}
/*将字符读入数组的某一部分。*/
abstract public int read(char cbuf[], int off, int len) throws IOException;
/*跳过字符*/
public long skip(long n) throws IOException {}
/*判断是否准备读取此流。如果保证下一个 read() 不阻塞输入,则返回 True,
否则返回 false。注意,返回 false 并不保证阻塞下一次读取。 */
public boolean ready() throws IOException {}
/*判断此流是否支持 mark() 操作。默认实现始终返回 false。子类应重写此方法。 */
public boolean markSupported() {}
/*标记流中的当前位置。对 reset() 的后续调用将尝试将该流重新定位到此点。*/
public void mark(int readAheadLimit) throws IOException {}
/*重置该流。如果已标记该流,则尝试在该标记处重新定位该流。*/
public void reset() throws IOException {}
/*关闭该流并释放与之关联的所有资源。*/
abstract public void close() throws IOException;
5、Writer
/*写入单个字符。用于支持高效单字符输出的子类应重写此方法。 */
public void write(int c) throws IOException {}
/*写入字符数组。底层是:
abstract public void write(char cbuf[], int off, int len) */
public void write(char cbuf[]) throws IOException {}
/*写入字符数组的某一部分。 */
abstract public void write(char cbuf[], int off, int len) throws IOException;
/*写入字符串。底层是:
public void write(String str, int off, int len) */
public void write(String str) throws IOException {}
/*写入字符串的某一部分。 */
public void write(String str, int off, int len) throws IOException {}
/*将指定字符序列添加到此 writer。底层是:
public void write(String str) */
public Writer append(CharSequence csq) throws IOException {}
/*将指定字符序列的子序列添加到此 writer.Appendable,底层是:
public void write(String str)*/
public Writer append(CharSequence csq, int start, int end) throws IOException {}
/*将指定字符添加到此 writer。底层是:
public void write(int c) */
public Writer append(char c) throws IOException {}
/*刷新该流的缓冲。如果该流已保存缓冲区中各种 write() 方法的所有字符,则立即将它们写入预期目标。
然后,如果该目标是另一个字符或字节流,则将其刷新。
因此,一次 flush() 调用将刷新 Writer 和 OutputStream 链中的所有缓冲区。
如果此流的预期目标是由底层操作系统提供的一个抽象(如一个文件),则刷新该流只能保证将以前写入到流的字节传递给操作系统进行写入,
但不保证能将这些字节实际写入到物理设备(如磁盘驱动器) */
abstract public void flush() throws IOException;
/*关闭此流,但要先刷新它*/
abstract public void close() throws IOException;
三、JavaIO流体系分析
在介绍JavaI/O体系的之前,我们先来学习一下设计模式中的装饰模式,这个模式在Java语言中应用最著名的就是Java中的I/O体系。
1、一个例子走进装饰模式
举一个手机升级换代的例子:
先看类图:
抽象手机类,定义了手机的初始功能和手机的使用方法:
abstract public class Phone {
//手机的现有参数
public abstract void initPerformance();
//用户使用手机
public abstract void use(String name);
}
下面的vivo手机类实现上方的手机类,并初始化参数:
public class vivoPhone extends Phone {
@Override
public void initPerformance() {
System.out.println("手机的现有像素");
System.out.println("手机的现有cpu");
System.out.println("手机的现有内存");
System.out.println("手机的现有运行内存");
}
@Override
public void use(String name) {
initPerformance();
System.out.println(name+" 使用手机");
}
}
定义一个用户类,查看手机参数并使用手机:
public class User {
public static void main(String[] args) {
Phone use = new vivoPhone();
use.use("乐无声");
/*手机的现有像素
手机的现有cpu
手机的现有内存
手机的现有运行内存
乐无声 使用手机*/
}
}
如果我们手机的参数很差,游戏没玩多久手机就顶不住了,影响心态,那怎么办?最好的办法肯定是买个新的咯!升级换代,直接飞起。
修改类图:
因此我们再定义一个升级换代的类:
public class updataVivo extends vivoPhone{
public void updatePicture(){
System.out.println("增加手机的像素");
}
public void updateROM(){
System.out.println("增加手机的内存");
}
public void initPerformance(){
System.out.println("手机的现有像素");
System.out.println("手机的现有cpu");
System.out.println("手机的现有内存");
System.out.println("手机的现有运行内存");
}
@Override
public void use(String name){
updatePicture();
updateROM();
initPerformance();
System.out.println(name+" 使用手机");
}
}
上面的升级换代类,我们提高了像素,升级了运行内存。
接下来我们再次来体验一下它。
public class User {
public static void main(String[] args) {
Phone use = new updataVivo();
use.use("乐无声");
/*增加手机的像素
增加手机的内存
手机的现有像素
手机的现有cpu
手机的现有内存
手机的现有运行内存
乐无声 使用手机*/
}
}
可以预知,手机的性能提升了。
通过继承,我们实现了我们的需求,但仔细想想,手机能提升的硬件还有很多,如果后面,用户又想提高运行内存呢,想换更好的cpu等等呢,再者,难道我们只有vivo手机这个类吗?如果再增加其他的手机,我们是不是要让其他的手机类都继承多个相同的升级换代类?结果可以想到:那就是类爆炸,不仅更加复杂,维护的成本也不可想象。那有没有什么好的办法呢?有的。
我们定义一批专门负责升级换代的类,然后根据实际情况来决定是否需要进行升级换代。
修改类图:
即增加一个抽象类和两个实现类,其中update的作用是封装Phone类,装饰类(update)的作用也就是一个特殊的代理类,真实的执行者还是被代理的角色vivoPhone
代码如下:
public abstract class update extends Phone {
private Phone phone;
update(Phone phone){
this.phone = phone;
}
public void initPerformance() {
this.phone.initPerformance();
}
public void use(String name){
this.phone.use(name);
}
}
public class updatePicture extends update{
updatePicture(Phone phone) {
super(phone);
}
public void picture(){
System.out.println("增加手机的像素");
}
public void use(String name){
picture();
super.use(name);
}
}
public class updateRAM extends update {
updateRAM(Phone phone) {
super(phone);
}
public void RAM(){
System.out.println("增加手机的运行内存");
}
public void use(String name){
RAM();
super.use(name);
}
}
import java.io.*;
public class User {
public static void main(String[] args) throws IOException {
Phone use = new vivoPhone();
use = new updatePicture(use);
use = new updateRAM(use);
use.use("乐无声");
/*这样也可以:
updateRAM ph = new updateRAM(new updatePicture(new vivoPhone()));
ph.use("乐无声");*/
/*增加手机的运行内存
增加手机的像素
手机的现有像素
手机的现有cpu
手机的现有内存
手机的现有运行内存
乐无声 使用手机*/
}
}
从上面我们可以看出:装饰类还是把动作的执行委托给需要装饰的对象。update抽象类的目的很简单,就是要让子类来封装Phone的子类,怎么封装? 重写use()方法!
2、装饰模式的介绍
装饰模式(Decorator Pattern)是一种比较常见的模式,其定义如下:Attach additionalresponsibilities to an object dynamically keeping the same interface.Decoratorsprovide a flexible alternative to subclassing for extending functionality.( 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。)
装饰模式的通用类图如图所示:
说明:
●Component抽象构件
Component是一个接口或者是抽象类,就是定义我们最核心的对象,也就是最原始的对象,如上面的成绩单
注意 在装饰模式中,必然有一个最基本、最核心、最原始的接口或抽象类充当Component抽象构件
●ConcreteComponent 具体构件
ConcreteComponent是最核心、最原始、最基本的接口或抽象类的实现,你要装饰的就是它
●Decorator装饰角色
一般是一个抽象类,做什么用呢?实现接口或者抽象方法,它里面可不一定有抽象的方法呀,在它的属性里必然有一个private变量指向Component抽象构件
● 具体装饰角色
ConcreteDecoratorA和ConcreteDecoratorB是两个具体的装饰类,你要把你最核心的、最原始的、最基本的东西装饰成其他东西
注意 原始方法和装饰方法的执行顺序在具体的装饰类是固定的,可以通过方法重载实现多种执行顺序。
3、半透明的装饰模式
然而,纯粹的装饰模式很难找到。装饰模式的用意是在不改变接口的前提下,增强所考虑的类的性能。在增强性能的时候,往往需要建立新的公开的方法。例如上面的例子中,如果我们想要换不同的cpu,那就得用不同的方法。
这就导致了大多数的装饰模式的实现都是“半透明”的,而不是完全透明的。换言之,允许装饰模式改变接口,增加新的方法。这意味着客户端可以声明ConcreteDecorator类型的变量,从而可以调用ConcreteDecorator类中才有的方法.
半透明的装饰模式是介于装饰模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰模式实际上是半透明的装饰模式,这样的装饰模式也称做半装饰、半适配器模式。
装饰模式有透明和半透明两种,这两种的区别就在于装饰角色的接口与抽象构件角色的接口是否完全一致。
透明的装饰模式也就是理想的装饰模式,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。相反,如果装饰角色的接口与抽象构件角色接口不一致,也就是说装饰角色的接口比抽象构件角色的接口宽的话,装饰角色实际上已经成了一个适配器角色,这种装饰模式也是可以接受的,称为“半透明”的装饰模式
4、JavaI/O流的装饰模式
初步了解了一下装饰模式后,我们再来看Java的I/O体系感觉就不会像以前那么复杂了。类似:DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(“e:/a.txt”)));我们也知道为什么要这么调用了。
接下来我们以InputStream类为例,刨析一下装饰模式,其他三个基类类似:
根据上图可以看出:
●抽象构件(Component)角色:由InputStream扮演。这是一个抽象类,为各种子类型提供统一的接口。
●具体构件(ConcreteComponent)角色:由ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream等类扮演。它们实现了抽象构件角色所规定的接口。
●抽象装饰(Decorator)角色:由FilterInputStream扮演。它实现了InputStream所规定的接口。
●具体装饰(ConcreteDecorator)角色:由几个类扮演,分别是BufferedInputStream、DataInputStream以及两个不常用到的类LineNumberInputStream、PushbackInputStream
下面来看一个例子:
import java.io.*;
public class Demo02 {
public static void main(String[] args) {
InputStream in = null;
BufferedInputStream bis = null;
DataInputStream dis = null;
byte[] bytes = new byte[3];
try {
in = new FileInputStream("e:/a.txt");
bis = new BufferedInputStream(in);
dis = new DataInputStream(bis);
System.out.println(dis.read(bytes));
System.out.println(new String(bytes));
/*运行结果:
3
abc
*/
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
通过debug我们可以看出看出一些运行的细节:
字符的读取调用的是FileInputStream的read方法:
而调用此方法的则是BufferedInputStream的下列语句:
最终获取到数据的则是DataInputStream的read方法:
上面的例子中,FileInputStream是核心,其余两个则是对他的装饰类。
四、常用流的使用
1、访问操作文件(FileInputStream/FileReader ,FileOutputStream/FileWriter)
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo04 {
public static void main(String[] args) {
int b=0;
FileInputStream in = null;
FileOutputStream out = null;
try {
in =new FileInputStream("e:/a.txt");
out=new FileOutputStream("e:/b.txt");
}catch(FileNotFoundException e){
System.out.println("file is not found");
System.exit(-1);
}
try {
while ((b=in.read())!=-1) {
out.write(b);
}
in.close();
out.close();
}catch(IOException e) {
System.out.println("IO异常,读取失败");
System.exit(-1);
}
System.out.println("文件复制完成");
}
}
2、缓存流的使用(BufferedInputStream/BufferedOutputStream,BufferedReader/BufferedWriter)
import java.io.*;
public class Demo05 {
public static void main(String[] args) throws IOException {
BufferedInputStream bis=null;
BufferedOutputStream bos=null;
try {
FileInputStream fis = new FileInputStream("e:/a.txt");
FileOutputStream fos = new FileOutputStream("e:/c.txt");
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
byte[] b = new byte[1024];
int off=0;
/*底层调用的FileInputStream的读取方法,将读取的字节存入缓冲区中,
即BufferedInputStream就是装饰类*/
while ((off=bis.read(b))>0) {
bos.write(b,0,off);
}
bis.close();
bos.close();
}catch (IOException e) {
e.printStackTrace();
}finally {
bis.close();
bos.close();
}
}
}
3、转换流的使用(InputStreamReader/OutputStreamWriter)
InputStreamReader/OutputStreamWriter中读取或写入的时候,将根据默认或者指定的字符集,解码和编码字节。
public class Demo03 {
public static void main(String[] args) throws IOException {
/*根据默认字符集读取从控制台输入的字符串,遇到over停止读取,并缓冲各个字符串。
而后将字符串其经由缓冲区写入到指定的文件*/
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new FileWriter("e:/a.txt"));
String line =null;
//readline()读取一行,遇到回车或者换行返回读取结果
while ((line=br.readLine())!=null) {
if ("over".contentEquals(line)) {
break;
}
bw.write(line);
bw.newLine();
bw.flush();
}
bw.close();
br.close();
}
}
4、字节数组流的使用(ByteArrayInputStream/ByteArrayOutputStream)
ByteArrayInputStream/ByteArrayOutputStream就是自己跟自己玩,即数据存放在内存中
import java.io.*;
public class Demo06 {
public static void main(String[] args) {
//ByteArrayOutputStream的本质就是在对象中申明的一个字节数组
ByteArrayOutputStream baos=new ByteArrayOutputStream();
DataOutputStream dos=new DataOutputStream(baos);
try {
//在字节数组流中写入一个随机数
dos.writeDouble(Math.random());
dos.writeBoolean(true);
//toByteArray()方法返回字节数组输出流中的字节数组副本,作为字节数组输入流读取的缓冲区
ByteArrayInputStream bias=new ByteArrayInputStream(baos.toByteArray());
//输出可以读取的字节的个数
System.out.println(bias.available());
DataInputStream dis=new DataInputStream(bias);
//将字节以double类型返回
System.out.println(dis.readDouble());
System.out.println(dis.readBoolean());
dos.close();
dis.close();
}catch (IOException e) {
e.printStackTrace();
}
/*运行结果
9
0.09273062959933076
true*/
}
}
5、对象流的使用(FileInputStream/ObjectOutputStream)
import java.io.*;
public class Demo07 {
public static void main(String[] args) throws Exception {
try {
Person P=new Person("乐无声",22);
FileOutputStream fos=new FileOutputStream("e:/d.txt");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(P);
oos.flush();
oos.close();
}catch(FileNotFoundException e) {
e.printStackTrace();
}catch(IOException e) {
e.printStackTrace();
}
FileInputStream fis=new FileInputStream("e:/d.txt");
ObjectInputStream ois=new ObjectInputStream(fis);
Person P2=(Person)ois.readObject();
System.out.println(P2.name+"的年龄为"+P2.age);
}
}
class Person implements Serializable {
String name=null;
int age=0;
Person(String _name,int _age){
name=_name;
age=_age;
}
}
总结
本文介绍了I/O流的四大基类的相应方法,对于一些方法,子类有必要重写更好的实现;再从设计模式的角度介绍了I/O流的体系,让大家对这个体系有一个更加直观的认识,最后,再介绍了一些常用流的使用案例,希望对大家有所帮助!!!
本文地址:https://blog.csdn.net/zengsao/article/details/113852929
上一篇: JAVA 插入排序
下一篇: UML用例模型和类图练习