【java基础】——IO流(上)

时间:2023-02-26 16:43:27

一、IO概述

1、IO流:即InputOutput的缩写。

2、特点:

  • IO流用来处理设备间的数据传输。
  • Java对数据的操作是通过流的方式。
  • Java用于操作流的对象都在IO包中。
  • 流按操作数据分为两种:字节流和字符流。
  • 流按数据传输分为:输入流和输出流。
  • 注意:流只能操作数据,而不能操作文件。

3、IO流的常用基类

  • 字节流的抽象基流:InputStream和OutputStream
  • 字符流的抽象基流:Reader和Writer
注:此四个类派生出来的子类名称都是以父类名作为子类名的后缀,以前缀为其功能;
如InputStream子类FileInputStream;Reader子类FileReader

二、字符流

1、简述

    1)字符流中的对象融合了默认编码表,即当前系统使用的编码。
    2)字符流只用于处理文字数据,而字节流可以处理媒体数据。
    3)IO流是用于操作数据的,数据的常见体现形式是文件。FileWriter用于操作文件对象:该流对象一初始化,就必须有被操作的文件存在。

2、字符流的读写

a)、写入字符流(FileWriter)
步骤:
  • 创建一个FileWriter对象,并关联被操作的文件即文件存放的目的。
         方式一:FileWriter(String fileName)
         方式二:FileWriter(String fileNme, boolean append):boolean 值为true代表不覆盖已有的文件,在文件末尾处写入数据。
  • 调用write方法,将字符写入到流中。
         可以写入单个字符,字符数组、字符数组的一部分,字符串、字符串的一部分。
  • 调用flush()方法,刷新该流的缓冲,将数据刷新到目的地中。
  • 调用close()方法,关闭流资源。但是关闭前会刷新一次内部的缓冲数据,并将数据刷新到目的地中。
写入字符(FileWriter(String fileName))代码示例如下:
package com.huang.io.iotest;

import java.io.FileWriter;
import java.io.IOException;

public class FileWriterDemo {

public static void main(String[] args) throws IOException {
/*
* 创建一个filewriter对象。该对象一初始化就必须明确操作的文件。该文件会放到指定目录下,
* 如果该文件存在,则会覆盖原文件
*/
FileWriter fw = new FileWriter("D:\\file.txt");
// 调用wirte方法,写入数据
fw.write("huangxiang");
// 刷新流对象中的数据,讲数据刷新到目的地中。
fw.flush();
fw.write("haha");
fw.flush();
/*
* 关闭流资源,但其关闭前会先刷新一次流中的数据,将数据刷新到目的地中。
* 与flush的区别,flush刷新后还能继续使用,close后将关闭流,无法继续使用。
*/
fw.close();

//fw.write("关闭之后不能再写入数据!!!!");
}
}

若我们需要对文件进行复写,而不是覆盖时,我们可以使用FileWriter(String fileNme, boolean append),代码示例如下:
package com.huang.io.iotest;

import java.io.FileWriter;
import java.io.IOException;

/*
* 对已有的文件进行续写
*/
public class FileWriterDemo3 {

public static void main(String[] args) {
FileWriter fw = null;
try {
// true表示在文件末尾进行续写。不进行覆盖。
fw = new FileWriter("D:\\file.txt", true);
fw.write("shkui");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

}
注意:
    a、其实java自身不能写入数据,而是调用系统内部方式完成数据的书写,使用系统资源后,一定要关闭资源。
    b、FileWriter 创建文件,当指定文件不存在,就会先创建文件,如果文件存在,则会清空文件中的内容。
          并在已有文件的末尾处进行数据续写。(windows系统中的文件内换行用\r\n两个转义字符表示,在linux系统中只用\n表示换行)
    c、在创建对象时,如果指定的位置不存在,则会发生IOException异常,所以在整个步骤中,需要对IO异常进行try处理。
    d、其实字符流默认封装了缓冲区(用的都是数组),且底层是用的是字节流的缓冲区。因为读取流每次读取一个字符是两个字节,
         所以要当数组中存储两个字节才能写入,因为用到了缓冲区,所以要进行刷新操作。

3、IO异常处理方式

原理:为让流对象作用域在整个方法中,要让引用指向null,且资源必须关闭(关闭也会发生异常)。
IO中的异常处理方式的代码示例:

package com.huang.io.iotest;

import java.io.FileWriter;
import java.io.IOException;
/**
*@author huangxiang
*@version 1.0
*/
/*
* IO异常的处理方式
*/
public class FileWriterDemo2 {

public static void main(String[] args) {
/*
* 这里要在外面建立引用,如果在try内进行FileWriter fw = new FileWriter();
* 在finally代码块中fw将不能使用。
*/
FileWriter fw = null;//在外面建立引用
try {
//fw = new FileWriter("K:\\file.txt");//这里会报FileNotFoundException错误,不存在K盘。
fw = new FileWriter("D:\\file.txt");//在内部建立对象。
fw.write("abcde");
} catch (IOException e) {
System.out.println(e.toString());
} finally {
try {//单独处理关闭失败异常
//健壮性判断。。如果fw为空,就没有关闭的可能。
//当fw为空时,会报空指针异常错误。
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

b)、读取字符流(FileReader)
步骤:
  • 创建一个文件读取流对象和指定文件关联。要保证该文件已经存在,若不存在,将会发生异常FileNotFoundException。
  • 调用读取流对象的read()方法。
          第一种方式:通过字符流,int read():读取单个字符,返回int类型的值,如果读到流的末尾,返回-1。
          第二种方式:通过字符数组,int read(char[] buf):将字符读入数组,返回读取的字符数,如果已达到流的末尾,则返回-1。
                     重载形式: int read(char[] cbuf, int off, int len): 将字符读入数组的某一部分
  • 读取后要调用close方法将流资源关闭。
FileReader代码实例(两种读取方式):
package com.huang.io.iotest;

import java.io.FileReader;
import java.io.IOException;

public class FileReaderDemo {

//private static int ch;

public static void main(String[] args) throws IOException {
//创建一个文件读取流对象,和指定的文件相关联
//要保证该文件是存在的,如果不存在,会发生异常
FileReader fr = new FileReader("D:\\file.txt");
System.out.println("*****************第一种读取方式******************");
//read(),一次读取一个
/*int ch = fr.read();
System.out.println("ch="+(char)ch);*/

//全部读取
/*
int ch = 0;
while((ch = fr.read()) != -1){
System.out.println("ch="+(char)ch);
}
*/
System.out.println("******************第二种读取方式*********************");
//通过字符数组进行读取
//定义一个字符数组,用于存储读取到的字符,该read(char[])返回的是读到的字符个数
char[] buf = new char[1024];

int num = 0;
while ((num = fr.read(buf))!= -1) {
System.out.println(new String(buf,0,num));
}
fr.close();
}
}

3、字符流缓冲区

概述:其实就是带有缓冲区的字符流(内部已经封装了数组),缓冲区的出现是为了提高流的操作效率而出现的,所以在创建缓冲区之前,必须要有一个流对象。
对应类
  • BufferedReader
  • BufferedWriter
缓冲技术原理:此对象中封装了数组,将数据存入,再一次性取出。
特有方法
  • readLine():一次读取一行文本,返回该行内容的字符串,如果已到达流末尾,则返回 null 。  
readLine方法原理:无论是读取一个字符,还是读取一行,获取多个字符,其实最终都是在硬盘上一个一个读取,所以最终使用的read方法一次读一个的方法。readLine方法其实是把读取到的一个字符临时存储起来,当其读取到回车符时,就将临时存储的数据返回,视为一行。
  • newLine():写入一个换行符,具有跨平台性。
BufferedReader使用的代码演示:
package com.huang.io.buffertest;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReaderDemo {

public static void main(String[] args) throws IOException {
// 创建一个读取流对象和文件相关联
FileReader fr = new FileReader("D:\\file.txt");
// 为了提高效率,创建一个缓冲区,将字符读取流对象作为参数传递给缓冲区对象的构造方法。
BufferedReader br = new BufferedReader(fr);
//读一行的方法readLine,方便对文件的读取,当读取完毕后,返回空。
// String ch = br.readLine();
// System.out.println(ch);
String ch = null;
//字符串读取不为null时。。
while ((ch = br.readLine()) != null) {
// br.flush();
System.out.println(ch);
}
br.close();
}

}

BufferedWriter使用的代码演示
package com.huang.io.buffertest;
/*
* 缓冲区的出现是为了提高流的操作效率而出现的。
* 所以在创建缓冲区之前得先有流对象。
*
* 该缓冲区提供了一个跨平台的换行符:newLine();
*/
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedWriterDemo {

public static void main(String[] args) throws IOException {
//创建一个字符写入流对象
FileWriter fw = new FileWriter("D:\\file.txt",true);
//为了提高字符写入效率,加入了缓冲技术
//只要将需要被提高效率的流对象作为参数传递给缓冲区的构造函数即可。
BufferedWriter bw = new BufferedWriter(fw);
//跨平台的换行符
bw.newLine();
bw.write("huangxiang");
//*只要用到缓冲区就必须刷新!!!
bw.flush();
fw.close();
//其实关闭缓冲区,就是在关闭缓冲区中的流对象,可不写。
//bw.close();

}

}

缓冲区小练习,通过缓冲区复制一个.java文件,并结合前面所学的IO异常处理方式进行异常处理。
package com.huang.io.demo;

/*
* 通过标准io异常处理方式处理异常。
*/
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/*
* 通过缓冲区复制一个.java文件
*/
public class CopyTextDemo {

public static void main(String[] args) {
// TODO Auto-generated method stub
BufferedReader bufr = null;
BufferedWriter bufw = null;
try {
bufr = new BufferedReader(new FileReader("D:\\ball.java"));
bufw = new BufferedWriter(new FileWriter("E:\\copy.txt"));

String line = null;

while ((line = bufr.readLine()) != null) {
bufw.write(line);
bufw.newLine();
bufw.flush();
}
} catch (IOException e) {
throw new RuntimeException("读写失败");
} finally {
try {
if (bufr != null)
bufr.close();

} catch (IOException e) {
throw new RuntimeException("读取关闭失败");

}
try {
if (bufw != null)
bufw.close();

} catch (IOException e) {
throw new RuntimeException("写入关闭失败");

}
}
}

}

 四、装饰设计模式 

1、什么是装饰设计模式?

当想要对已有的对象进行功能增强时,那么可以自定义类,将已有对象传入,基于已有的功能,并提供加强功能,那么自定义的该类称为装饰类。装饰类通常会通过构造方法接收被装饰的类,并基于被装饰的对象的功能,提供更强的功能。

2、装饰和继承的区别:

  • 装饰模式比继承要灵活。避免了继承体系的臃肿,而且降低了类与类之间的关系。
  • 装饰类因为增强已有对象,具备的功能和已有的是相同的,只不过提供了更强的功能,所以装饰类和被装饰的类通常都是属于一个体系。
  • 从继承结构转为组合结构。  
  • 在设计模式那篇博客中有详细介绍。
装饰设计模式代码体现:
package com.huang.design.demo;
/*
* 装饰设计模式:
* 所谓装饰设计模式,就是对已有对象的功能进行增强。
*/
class Person{
void eat(){
System.out.println("吃饭");
}
}
/*
* 为了不随便改动原来的代码,还要增强其功能。
* 我们可以定义一个新类,对原有对象进行装饰
*/
class NewPerson{
private Person p;
NewPerson(Person p){
this.p = p;
}
public void eat(){
System.out.println("饭前来杯酒");
p.eat();
System.out.println("饭后来根烟");
}
}
public class DecorationDesignDemo {
/**
* @author huangxiang
* @param args
*/
public static void main(String[] args) {
Person p1 = new Person();
System.out.println("月底吃饭的方式(装饰前)");
p1.eat();
NewPerson p2 = new NewPerson(p1);
System.out.println("月初吃饭的方式(装时候)");
p2.eat();
}

}

五、字节流

1、字节流读写文件

①概述
  • 字节流和字符流的基本操作是相同的,但字节流还可以操作其他媒体文件。
  • 媒体文件(所有文件)数据中都是以字节(而进制形式)存储的,所以字节流对象可直接对媒体文件的数据写入到文件中,不用再进行刷流动作。
  • 读取字节流:InputStream       写输出流OutputStream
注意:为何不用进行刷流动作?
因为字节流操作的是字节,即数据的最小单位,不需要像字符流一样要进行转换为字节。所以可直接将字节数据写入到指定文件中。

② InputStream  常用方法
  • 字节流对象的read()方法。
  • InputStream特有方法: int available();//返回文件中的字节个数
注:可以利用此方法来指定读取方式中传入数组的长度,从而省去循环判断。但是如果文件较大,而虚拟机启动分配的默认内存一般为64M。
当文件过大时,此数组长度所占内存空间就会溢出。所以,此方法慎用,当文件不大时,可以使用。

③OutputStream  常用方法
  • 向输出流中写入一个字节: void write(int b)//会将read读取的int型数据强转为byte型,写入最低8位,保持数据的真确信。
  • 写入字节数组: void write(byte[] b),void write(byte[] b,int off,int len)
字节流代码演示:
package com.huang.io.demo;

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

/*
* FileInputStream是InputStream的子类,是操作文件的字节输出流,专门用于提取文件中的数据。
* 通过循环语句来实现数据的持续读取
*/
public class FileInputStreamDemo {

public static void main(String[] args) throws IOException {
//创建一个文件字节输入流
FileInputStream fis = new FileInputStream("D:\\file.txt");
//定义一个int类型的变量b,记住每次读取的一个字符
int b = 0;
while(true){
b = fis.read();
if(b==-1)
break;
System.out.println((char)b);
}
fis.close();
}

}
字节流练习:复制一个MP3文件
package com.huang.put.demo;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.OutputStream;

/*
* 需求:复制一个MP3
* MP3是字节流文件,需一个字节一个字节复制。
* 思路:
* 1.创建一个字节输出流,并关联源文件
* 2.创建一个字节输入流,并关联其目的地
* 3.循环读取
* 4.将读取到的数据写入目的地文件
*/
public class CopyFileDemo {

public static void main(String[] args) throws IOException {
// 创建一个输出流,并关联需要复制的文件
InputStream is = new FileInputStream("F:\\Music\\Beyond - 情人.mp3");
// 创建一个输入流,并关联目的地
OutputStream os = new FileOutputStream("D:\\情人.mp3");
/*
* 循环读取,写入
*/
// 获取拷贝前的系统时间
long begintime = System.currentTimeMillis();
// 定义一个变量,记住每次读取的字节
int len = 0;
while ((len = is.read()) != -1) {
os.write(len);
}
// 获取拷贝后的系统时间
long endtime = System.currentTimeMillis();
long spelltime = endtime - begintime;
System.out.println("拷贝前的时间" + begintime + "拷贝结束的时间" + endtime + ",总共花了"
+ spelltime + "时间");
is.close();
os.close();
}

}

2、字节流的缓冲区

①字节流缓冲区的出现同样是为了提高读写效率。
②字节流缓冲区:定义一个字节数组作为和缓冲区,读写文件时,一次性读取多个字节数据,并保存到字节数组中,然后将字节数组中的数据一次性写入文件。
③读写特点:
  • read():会将字节byte型值提升为int型值
  • write():会将int型强转为byte型,即保留二进制数的最后八位。
注意:字节流的读一个字节的read方法为什么返回值类型不是byte,而是int。

 因为有可能会读到连续8个二进制1的情况,8个二进制1对应的十进制是-1.那么就会数据还没有读完,就结束的情况。因为我们判断读取结束是通过结尾标记-1来确定的。

所以,为了避免这种情况将读到的字节进行int类型的提升。并在保留原字节数据的情况前面了补了24个0,变成了int类型的数值。而在写入数据时,只写该int类型数据的最低8位。

④原理:在该对象中封装指定长度字节数组,定义指针和计数器记录判断读取数组的长度及数组中元素是否取完。如:read()方法是一次性将指定长度的字节数据存入,然后一次返回缓冲区中的一个字节数据,只要缓冲区中还有数据,就继续返回,当缓冲区中数据为零时,继续读取指定长度的字节数据存入字节数组中。
⑤字段摘要:
  • protected  byte[] buf 存储数据的内部缓冲区数组。
  • protected  int count  此值始终处于 0 到 buf.length 的范围内。 
  • protected  int pos  缓冲区中的当前位置,是将从 buf 数组中读取的下一个字符的索引。。
字节流缓冲区举例:依然是复制MP3,不过加入了缓冲区
package com.huang.put.demo;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/*
* 需求:复制一个MP3
* 思路:前面通过一个字节一个字节循环读取的方式,可以复制成功
* 但所需要花费的时间过长,效率太低,先需要提高效率,加入字节流的缓冲区。
* 步骤:
* 1.创建字节输入流,并于源文件相关联
* 2.创建字节输入流,并关联目的地
* 3.使用缓冲区读写文件
* 4.循环读写
*/
public class FastCopyFileDemo {

public static void main(String[] args) throws IOException {
// 创建一个输出流,并关联需要复制的文件
InputStream is = new FileInputStream("F:\\Music\\Beyond - 情人.mp3");
// 创建一个输入流,并关联目的地
OutputStream os = new FileOutputStream("D:\\情人.mp3");
//加入字节流缓冲区
byte[] buff = new byte[1024];
/*
* 循环读取,写入
*/
// 获取拷贝前的系统时间
long begintime = System.currentTimeMillis();
// 定义一个变量,记住每次读取的字节
int len = 0;
while ((len = is.read(buff)) != -1) {
os.write(buff, 0, len);;
}
// 获取拷贝后的系统时间
long endtime = System.currentTimeMillis();
long spelltime = endtime - begintime;
System.out.println("拷贝前的时间" + begintime + "拷贝结束的时间" + endtime + ",总共花了"
+ spelltime + "时间");
is.close();
os.close();
/*
* 加入缓冲区前运行结果:
* 拷贝前的时间1432147828169拷贝结束的时间1432147902739,总共花了74570时间
* 加入缓冲区后运行结果:
* 拷贝前的时间1432147756740拷贝结束的时间1432147756878,总共花了138时间
* 效率大大提高。
*/
}

}

六、标准输入输出流

1、概述

    System.in:in为 InputStream 类型,是标准输入流,用于读取键盘输入的数据。
    System.out:out为 PrintStream 类型,对应的是标准的输出设备,控制台。

2、弊端

使用输入流进行键盘录入时,只能一个字节一个字节进行录入。为了提高效率,希望能像字符流录入整行数据也就是readLine方法。这就需要用到转换流,将字节流转换成字符流。
代码演示如下:
package com.huang.io.demo;

import java.io.IOException;
import java.io.InputStream;

/*
* 读取键盘录入
* System.out:对应的是标准输出设备,控制台
* System.in:对应的是标准输入设备,键盘
*
* 需求:当录入一行数据之后,就将该数据进行打印,
* 如果录入的数据是over,那么停止打印
*/
public class ReadIn {

public static void main(String[] args) throws IOException {
InputStream in = System.in;
StringBuilder sb = new StringBuilder();

while(true){
//判断空格\r\n
int ch = in.read();
if (ch=='\r')
continue;
if(ch=='\n'){
String s = sb.toString();
if("over".equals(s))
break;
System.out.println(s.toUpperCase());
sb.delete(0, sb.length());
}
else {
sb.append((char)ch);
}
}
}

}

七、转换流

1、概述

①转换流也是一种包装流,可以实现字符和字节相互之间的转换,可以在其构造函数中指定编码表,默认使用的是系统的编码。
②为了提高读写效率,可以通过 BufferedReader 和 BufferedWriter 对转换流进行包装。   
③为什么要出现转换流呢?
当字节流中的数据都是字符时,转成字符流效率更高。

2、InputStreamReader

原因:该对象用的是InputStream的read方法,将数据以字节流转化为内存中的字符流,终点是内存。

3、OutputStreamWriter

原因:通过源码不难发现其用的是OutputStream的write方法,从内存中读取刚才读进来的字符流,
将内存中的数据以字节流的形式输出。其实输入与输出是相反地一个过程。因为文件最终是以二进制形式存储的。
看源码就知道,FileWriter的write会调用OutputStreamWriter的write方法,而outputStreamWriter的write方法会调用outputStream的write方法。

代码演示如下所示:
package com.huang.io.demo;
/*
* 读取转化流
*
* 字节转字符
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class TransStreamDemo {

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

////获取键盘录入对象
//InputStream in = System.in;
////将字节流对象转换为字符流对象InputStreamReader
//InputStreamReader isr = new InputStreamReader(in);
//为了提高效率,将字符串进行缓冲区技术高效操作。 BufferedReader
//BufferedReader bufr = new BufferedReader(isr);
//******键盘录入的最常见写法:将上面三句话合为一句
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while((line = bufr.readLine())!= null){
if("over".equals(line))
break;
System.out.println(line.toUpperCase());
}
bufr.close();
}

}

八、小知识点

1、改变标准输入输出流( System 类的方法)
static void setIn(InputStream in) :重新分配“标准”输入流。
 static void setOut(PrintStream out) : 重新分配“标准”输出流。
 
2、异常的日志信息
通过异常体系的 void printStackTrace(PrintStream s) 将异常信息写入文件中。

3、系统信息
通过 Properties 类的 void list(PrintStream out) 将属性列表输出到指定的输出流。

(!!!!)4、流操作的基本规律(在代码中以注释的形式分析)
package com.huang.io.demo;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

/*
* 流操作的基本规律:
* 1.明确源和目的地
* 源:输入流 InputStream ,Reader
* 目的地:输出流OutPutStream,Writer
* 2.操作数据是否为纯文本 是:字符流 否:字节流
* 3.当体系明确后,要明确使用哪个的具体对象
* 通过设备来进行区分 源设备:内存,硬盘。键盘 目的设备:内存,硬盘。控制台。
*/
/*
* 需求:将一个文本中的数据复制到另外一个文本中。 1.源:InputStream ,Reader
*/
public class TransStreamDemo2 {

public static void main(String[] args) throws IOException {
BufferedReader bufr = new BufferedReader(new InputStreamReader(
System.in));

BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(
System.out));

String line = null;
while ((line = bufr.readLine()) != null) {
if ("over".equals(line))
break;
System.out.println(line.toUpperCase());
}
bufr.close();
bufw.close();
}

}