I/O流
基本概念
什么是 I/O?
通过 I/O 可以完成硬盘文件的读和写。
概念图:输入、输出是相对于内存而言的,以内存为参照物。
1、I/O 流的分类方式:
-
按照流的方向进行分类:
以内存为参照物,往内存中去称为输入(Input)或者读(Read)。
-
按照读取数据方式不同进行分类:
有的流是按照字节的方式读取数据,一次读取 1 个字节(byte)= 8 bit。这种流是万能的,什么类型文件都可以读取(文本文件、音频文件、图片文件等等)。
-
按照字符的方式读取数据,一次读取一个字符,这种流是为了方便读取普通文本文件(能用记事本编辑的文件,比如java文件、txt 文件等)而存在的,不能读取其他类型的文件,甚至 word 文件也无法读取( word 文件不是普通文本)。
示例:a中国bc张三fe
a
等字符在 windows 占用一个字节(但是在Java中占用两个字节),中
等汉字在 windows 占用两个字节。
按字节读:
第一次:一个字节,读到a
;
第二次:一个字节,读到中
字符的一半;
……
按字符读:
第一次:一个字符,读到a
;
第二次:一个字符,读到中
;
……
2、I/O 四大流:(都是抽象类)
java.io.InputStream
:字节输入流
java.io.OutputStream
:字节输出流
java.io.Reader
:字符输入流
java.io.Writer
:字符输出流
所有的流都实现了java.io.Closeable
接口,都有close()
方法,都是可关闭的,流相当于内存和硬盘之间的管道用完后要及时关闭,否则会占用很多资源。
所有的输出流都实现了java.io.Flushable
接口,都是可刷新的,都有flush()
方法表示将通道当中剩余未输出的数据强行输出完(清空管道),输出流在最终输出之后一定要记得刷新一下清空管道。(如果没有flush()
可能会导致丢失数据)
注意:在 java 中只要类名以stream
结尾的都是字节流;以 Reader/Writer 结尾的都是字符流。
文件专属:
1、java.io.FileInputStream
(掌握)
public static void main(String[] args){
//创建文件字节流输入对象
FileInputStream files = null;
try{
/*
方式一:
files = new FileInputStream("E:/java课堂笔记/file");
* */
//方式二:文件绝对路径是“E:\java课堂笔记\file”,"\\"其中一个是转义字符表示“\”
files=new FileInputStream("E:\\java课堂笔记\\file"); //存储内容:abcdef
/*
读数据
int readeData=files.read(); //返回值:读取到的字节本身,读到文件末尾没有数据就返回-1
System.out.println(readeData);//结果:97 (字符a的ASCII码)*/
/*while(true){//循环读出
int readeData=files.read();
if(readeData == -1){
break;
}
System.out.println(readeData);
}*/
//改造循环
int readeData;
while((readeData=files.read()) != -1){
System.out.println(readeData);
}
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//确保一定关闭流
if(files != null){
//如果files为空则不用关闭流
try {
files.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意:如果是空格则返回 32 。
缺点:一次读取一个字节,硬盘和内存的数据交互太频繁效率较低,开发时使用较少。
改进:使用int read(byte[] b)
一次最多读取b.length
个字节,减少硬盘和内存的交互,提高程序的执行效率,往byte[]
数组当中读。
相对路径是从当前所在的位置作为起点。
IDEA默认的当前路径是工程Project的根。
例如:上图中的根就是ideaProjects
,和train
并列的文件路径为
files = new FileInputStream("file");
//在train文件夹中的文件
files = new FileInputStream("train/file");
读取数据:
public static void main(String[] args){
//创建文件字节流输入对象
FileInputStream files = null;
try{
/*
方式一:
files = new FileInputStream("E:/java课堂笔记/file");
* */
//方式二:文件绝对路径是“E:\java课堂笔记\file”,"\\"其中一个是转义字符表示“\”
files=new FileInputStream("E:\\java课堂笔记\\file"); //存储内容:abcdef
//准备一个4个长度的byte数组,一次最多读取4个字节
byte[] bytes = new byte[4];
//返回值:读取到的字节数量
int readCount = files.read(bytes);
System.out.println(readCount);//第一次读到4个字节
readCount = files.read(bytes);//第二次只能读到2个字节
System.out.println(readCount);//2
readCount = files.read(bytes);//第三次一个字节都没,返回-1
System.out.println(readCount);//-1
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//确保一定关闭流
if(files != null){
//如果files为空则不用关闭流
try {
files.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
将读出的字节数组转换成字符串输出:
//准备一个4个长度的byte数组,一次最多读取4个字节
byte[] bytes = new byte[4];
//返回值:读取到的字节数量
int readCount = files.read(bytes);
System.out.println(readCount);//第一次读到4个字节
System.out.println(new String(bytes));//返回:abcd
readCount = files.read(bytes);//第二次只能读到2个字节
System.out.println(readCount);//2
System.out.println(new String(bytes));//返回:efcd
readCount = files.read(bytes);//第三次一个字节都没,返回-1
System.out.println(readCount);//-1
由以上代码看出第二次读出的数据是efcd
,与我们的期望不符合,我们应该读取多少个字节就转换多少个。
System.out.println(new String(bytes,0,readCount));
//表示从数组bytes的第0个下标开始,读取长度为readCount的字节数组
读取的最终版本:
public static void main(String[] args){
//创建文件字节流输入对象
FileInputStream files = null;
try{
/*
方式一:
files = new FileInputStream("E:/java课堂笔记/file");
* */
//方式二:文件绝对路径是“E:\java课堂笔记\file”,"\\"其中一个是转义字符表示“\”
files=new FileInputStream("E:\\java课堂笔记\\file"); //存储内容:abcdef
//准备一个4个长度的byte数组,一次最多读取4个字节
byte[] bytes = new byte[4];
/* while(true){
//返回值:读取到的字节数量
int readCount = files.read(bytes);
if(readCount = -1){
break;
}
System.out.println(new String(bytes,0,readCount));
}*/
//改进
int readCount = 0;
while((readCount = files.read(bytes)) != -1){
System.out.println(new String(bytes,0,readCount));
}
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//确保一定关闭流
if(files != null){
//如果files为空则不用关闭流
try {
files.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FileInputStream
类的常用方法:
1、int available()
返回流当中剩余的没有读到的字节数量。
应用:获取文件的总字节数量,然后通过read()
方法一次性读完,但是因为数组不能太大所以不适合太大的文件。
public static void main(String[] args){
//创建文件字节流输入对象
FileInputStream files = null;
try{
//方式二:文件绝对路径是“E:\java课堂笔记\file”,"\\"其中一个是转义字符表示“\”
files=new FileInputStream("E:\\java课堂笔记\\file"); //存储内容:abcdef
System.out.println("总字节数量:"+ files.available());
//准备一个4个长度的byte数组,一次最多读取4个字节
byte[] bytes = new byte[files.available()];
//知道了总字节数不需要循环了,一次性读取完
int readCount = files.read(bytes);
System.out.println(new String(bytes));
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//确保一定关闭流
if(files != null){
//如果files为空则不用关闭流
try {
files.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2、long skip(long n)
跳过几个字节不读。
public static void main(String[] args){
//创建文件字节流输入对象
FileInputStream files = null;
try{
//方式二:文件绝对路径是“E:\java课堂笔记\file”,"\\"其中一个是转义字符表示“\”
files=new FileInputStream("E:\\java课堂笔记\\file"); //存储内容:abcdef
System.out.println("总字节数量:"+ files.available());
//skip跳过几个字节不读取
files.skip(3);//表示从 d 开始读
System.out.println(files.read());//返回:100 (即d的ASCII值)
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//确保一定关闭流
if(files != null){
//如果files为空则不用关闭流
try {
files.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2、java.io.FileOutputStream
(掌握):将数据从内存中写到硬盘中。
1)第一种:每次都是先将文件清空,然后重新写入数据。
public static void main(String[] args){
//创建文件字节流输入对象
FileOutputStream fos = null;
try{
//方式二:文件绝对路径是“E:\java课堂笔记\file”,"\\"其中一个是转义字符表示“\”
fos = new FileOutputStream("E:\\java课堂笔记\\myfile"); //将数据写入“E:\java课堂笔记\myfile”,文件不存在时会自动创建
//开始写
byte[] bytes = {97,98,99,100,101};//写入abcde
//将bytes数组全部写出
fos.write(bytes);
//将bytes数组的一部分写出
fos.write(bytes,0,2);//再写出ab,即abcdeab
//写完之后,最后一定要刷新
fos.flush();
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//确保一定关闭流
if(fos != null){
//如果files为空则不用关闭流
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2)FileOutputStream(String name,boolean append)
写入的内容写在文件已有的数据后面
public static void main(String[] args){
//创建文件字节流输入对象
FileOutputStream fos = null;
try{
//方式二:文件绝对路径是“E:\java课堂笔记\file”,"\\"其中一个是转义字符表示“\”
//此处要处理异常
fos = new FileOutputStream("E:\\java课堂笔记\\myfile",true); //将数据写入“E:\java课堂笔记\myfile”,文件不存在时会自动创建
//开始写
byte[] bytes = {97,98,99,100,101};//写入abcde
//将bytes数组全部写出
fos.write(bytes);
//将bytes数组的一部分写出
fos.write(bytes,0,2);//再写出ab,即abcdeab
//写出字符串
String s = "我是谁";
//将字符串转换成byte数组
byte[] b = s.getBytes();
//写出
fos.write(b);
//写完之后,最后一定要刷新
fos.flush();//此处要处理异常
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//确保一定关闭流
if(fos != null){
//如果files为空则不用关闭流
try {
fos.close();//此处要处理异常
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
每次执行后都是在原有数据的基础上追加数据。
3)文件的拷贝(FileInputStream、FileOutputStream)
拷贝过程:文件一边输入内存,一边从内存中输出。(一边读一边写)使用以上的字节流拷贝文件的时候,文件类型随意,什么文件(文件夹不能,只是单个文件)都能拷贝。
public static void main(String[] args){
FileInputStream fis = null;
FileOutputStream fos = null;
try{
//创建一个输入流对象
fis = new FileInputStream("D://javase");
//创建一个输出流对象
fos = new FileOutputStream("E://javase");
//核心:一边读一边写
byte[] bytes = new byte[1024 * 1024];//一次最多读取1MB
int readCount = 0;
while((readCount = fis.read(bytes)) != -1){
fos.write(bytes,0,readCount);
}
//输出流最后要刷新
fos.flush();
}catch(FileNotFoundException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}finally{
//这里输入输出流的关闭的异常要分开处理;如果一起处理有一个出现异常,可能会影响到另一个流的关闭
if(fos != null){
try{
fos.close();
}catch(IOException e){
e.printStackTrace();
}
}
if(fis != null){
try{
fis.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
3、java.io.FileReader
文件字符输入流(读,内存读取硬盘信息),只能读普通文本(能用记事本编辑的文件,比如text文件等)。
public static void main(String[] args){
FileReader reader = null;
try{
reader = new FileReader("E:\\java课堂笔记\\myfile");
/*
显示读出的信息
//准备一个char数组
char[] chars = new char[4];
//往char数组中读
reader.read(chars);//按字符方式读取,第一次a,第二次b,…(一个汉字也是一个字符)
//遍历
for(char c : chars){
System.out.println(c);
}
*/
//一次读取4个字符
char[] chars = new char[4];
int readCount = 0;
while((readCount = reader.read(chars)) != -1){
System.out.print(new String(chars,0,readCount));
}
}catch(FileNotFoundException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}finally{
if(reader != null){
try{
reader.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
4、java.io.FileWriter
文件字符输出流(写,内存写入硬盘)只能输出普通文本。
public static void main(String[] args){
FileWriter out = null;
try{
//创建文件字符输出流对象,没有该文件时会自动创建
out = new FileWriter("file");
/* //写入时紧接着文件内容的后面写入,不会清空文件内容重新写入
out = new FileWriter("file",true);
*/
//开始写
char[] chars = {'躬','行'};
//会将文件内容清空然后重新写入
out.write(chars);
//接着写入数组的一部分,从下标为0开始长度为1
out.write(chars,0,1);
//写入一个字符串
out.write("我是一名程序员");
//刷新
out.flush();
}catch(IOException e){
e.printStackTrace();
}finally{
if(out != null){
try{
out.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
复制普通文本文件的过程和复制字节流文件相同。
缓冲流专属:
1、java.io.BufferedReader
带有缓冲区的字符输入流。使用这个流的时候不需要自定义 char 数组,或者说不需要自定义 byte 数组,自带缓冲。
public static void main(String[] args) throws Exception{//此处将异常统一处理,不需要try-catch
FileReader reader = new FileReader("copy.java");
//当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做:节点流。
//外部负责包装的这个流叫做:包装流(也叫处理流)
//FileReader就是一个节点流,BufferedReader就是包装流(处理流)
BufferedReader br = new BufferedReader(reader);
/*
一行一行的读:
String firstLine = br.readLine();
System.out.println(firstLine);
String secondLine = br.readLine();
System.out.println(secondLine);
*/
//当readLine()返回值为null时读操作结束
String s = null;
while((s = br.readLine()) != null){//br.readLine()方法读取一个文本行,但不会换行,文本会输出在同一行
System.out.println(s);//输出一行文本后换行
}
//关闭流,对于包装流来说,只需要关闭包装流就行了,因为包装流的close()方法中调用了节点流的close()方法
br.close();
}
观察源码发现:关闭流时,对于包装流来说,只需要关闭包装流就行了,因为包装流的close()方法中调用了节点流的close()方法。
转换流:(将字节流转换成字符流)
1、java.io.InputStreamReader
public static void main(String[] args) throws Exception{//此处将异常统一处理,不需要try-catch
//字节流
FileInputStream in = new FileInputStream("copy.java");
//通过转换流转换(InputStreamReader将字节流转换成字符流)
InputStreamReader reader = new InputStreamReader(in);//in是节点流,reader是包装流
//这个构造方法只能传一个字符流,不能传字节流
BufferedReader br = new BufferedReader(reader);//reader是节点流,br是包装流
//合并写法
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("copy.java")));
//输出
String line = null;
while((line = br.readLine()) != null){
System.out.println(line);
}
}
2、java.io.OutputStreamWriter
带有缓冲区的字符输出流。
public static void main(String[] args) throws Exception{//让写入内容追加在原内容之后
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("file",true)));
//开始写
out.write("hello world");
out.write("\n"); //换行符
out.write("world");
//刷新
out.flush();
//关闭最外层
out.close();
}
数据流专属:
1、java.io.DataOutputStream
这个流可以将数据连同数据类型一并写入文件,这个文件不是普通文本文档(用记事本打不开。)
public static viod main(String[] args) throws Exception{
//创建数据专属的字节流
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));
//写数据
byte b = 100;
long l = 400L;
double d = 3.14;
char c = 'a';
//写,把数据及数据类型一并写入文件中
dos.writeByte(b);
dos.writeLong(l);
dos.writeDouble(d);
dos.writeChar(c);
//刷新
dos.flush();
//关闭最外层
dos.close();
}
OutputStream
是抽象类不能实例化,可以实例化它的子类FileOutputStream
。
使用DataOutputStream
写入的数据无法用记事本打开(打开是乱码),我们必须使用DataInputStream
还要按照写入时的顺序读出来。
2、java.io.DataInputStream
数据字节输入流,读DataOutputStream
写的文件,读的时候需要提前知道写入的顺序,读的顺序和写的数据顺序一致才能正常读取出数据。
public static void main(String[] args) throws Exception{
DataInputStream dis = new DataInputStream(new FileInputStream("data"));
//读取数据
byte b = dis.readByte();
long l = dis.readLong();
double d = dis.readDouble();
char c = dis.readChar();
System.out.println(b);
System.out.println(l);
System.out.println(d);
System.out.println(c);
dis.close();
}
标准输出流:
java.io.PrintStream
(掌握)
标准的字节输出流,默认输出到控制台。
public static void main(String[] args){
PrintStream ps = System.out;
ps.println("hello world");
//合并写法
System.out.println("hello world");
//标准输出流不需要手动关闭
//标准输出流不再指向控制台,指向“log”文件
PrintStream printStream = new PrintStream(new FileOutputStream("log"));
//修改输出方向为“log”文件
System.setOut(printStream);
//输出
System.out.println("hello world");
}
setOut()
的参数为PrintStream
类型的,而PrintStream
的参数是OutputStream
抽象类不能实例化,可以用其子类FileOutputStream
代替。
记录日志文件的方法:
public class Logger{
public static void log(String msg){
try{
//指向一个日志文件
PrintStream out = new PrintStream(new FileOutputStream("log.txt",true));
//改变输出方向
System.setOut(printStream);
//日期当前时间
Date nowTime = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(nowTime);
System.out.println(strTime+":"+msg);
}catch(FileNotFoundException e){
e.printStackTrace();
}
}
}
//测试日志工具类
public class LogTest{
public static void main(String[] args){
Logger.log("调用了。。");
}
}
结果:在“log.text”文件中打印了2020-07-18 ……:调用了。。