Featured image of post java高级之IO流

java高级之IO流

本文阅读量

IO

字符集

ASCII字符集

标准ASCII使用1个字节存储一个字符,首位是0,总共可表示128个字符,对欧美来说完全够用

GBK

汉字内码扩展规范

包含了2w多个汉字等字符,GBK中一个中文字符编码由2个字节的形式存储

GBK兼容了ASCII字符集

GBK规定:汉字的第一个字节的第一位必须是1,因此以0开头的就是对ASCII字符集的兼容

Unicode字符集(统一码,万国码)

unicode是国际组织制定的,可以容纳世界上所有的文字、符号的字符集

UTF-32

4个字节表示一个字符

UTF-8

是Unicode字符集的一种编码方案,采取可变长编码方案,共分4个长度区:1个字节、2个字节、3个字节、4个字节

英文字符、数字等只占1个字节(兼容标准ASCII编码),汉字字符占用3个字节

0xxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 0xxxxxxx

那怎么区分是1字节长度还是2字节长度3字节长度4字节长度呢?

1字节长度: 0xxxxxxx,以0开头

2字节长度:110xxxxx 10xxxxxx 以110开头,第二个以10开头

3字节长度:1110xxxx 10xxxxxx 10xxxxxx 以1110开头,第二个以10开头,第三个以10开头

4字节长度:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 以11110开头,第二个以10开头,第三个以10开头,第三个以10开头

编码与解码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
String str = "我爱学习java!";
// 1、编码
byte[] bytes = str.getBytes(); // 默认用平台编码UTF-8
System.out.println(Arrays.toString(bytes));

byte[] bytes2 = str.getBytes("GBK"); // 指定GBK编码
System.out.println(Arrays.toString(bytes2));

// 2、解码
String rs = new String(bytes); // 默认用平台编码UTF-8解码
System.out.println(rs);

String rs2 = new String(bytes,"GBK"); // 指定用GBK编码解码
System.out.println(rs2);

概述

IO流用于读写数据的(可以读写文件、网络中的数据…)

I指Input,称为输入流,负责把数据读到内存中去

O指Output,称为输出流,负责写数据出去

分类

按流的方向

输入流与输出流

按流中数据的最小单位

字节流:适合操作所有类型的文件,如:音频、视频、图片、文本文件的复制、转移等

字符流:只适合操作纯文本文件,如:读写txt、java文件等

因此,IO流总统可以分为4大流

字节输入流,字节输出流,字符输入流,字符输出流

字节输入流

抽象类:InputStream

实现类:FileInputStream(文件字节输入流)

FileInputStream:以内存为基准,可以把磁盘文件中的数据以字节的形式读入到内存中去

每次读取一个字节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 1、创建文件字节输入流管道与目标文件接通
// InputStream is = new FileInputStream(new File("cn.snailsir.study/src/aa.txt")); // 完整写法
InputStream is = new FileInputStream("cn.snailsir.study/src/aa.txt"); // 简洁写法,实际就是对完整方法的封装

// 2、每次读取一个字节返回
// public int read(): 每次读取一个字节返回,如果没有字节可读,返回-1
int b1 = is.read();
System.out.println(b1);


int b2 = is.read();
System.out.println((char)b2);


// 3、使用循环读取
int b;
while((b=is.read()) != -1){
  System.out.println((char)b);
}

扩展:

  1. 性能差,每次一个一个字节读取,性能很差
  2. 无法避免读取汉字输出乱码问题,会截断汉字的字节

每次读取多个字节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1、创建文件字节输入流管道与目标文件接通 
InputStream is = new FileInputStream("cn.snailsir.study/src/aa.txt"); // 简洁写法,实际就是对完整方法的封装


// 2、每次读取一个字节数组的字节,会返回读取的字节个数,没有字节可读返回-1
// public int read(byte[] buffer)
byte[] buffer = new byte[3];
int len1 = is.read(buffer);
System.out.println("内容"+new String(buffer));
System.out.println("个数"+len1);


int len2 = is.read(buffer);
// 读取多少就倒出多少
System.out.println("内容"+new String(buffer,0,len2));// 如果是new String(buffer)写法,而只能取出两个字节,那么返回的内容也是3个字节,只不过最后一个字节内容是上一个桶内容剩下的
System.out.println("个数"+len2);


// 3、使用循环读取
byte[] buffer = new byte[3];
int len;
while((len = is.read(buffer)) != -1){
  String rs = new String(buffer,0,len);
  System.out.print(rs);
} 

扩展:

性能比较好

缺点:无法避免读取汉字输出乱码的问题

一次读取完全部字节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 1、创建文件字节输入流管道与目标文件接通 
InputStream is = new FileInputStream("cn.snailsir.study/src/aa.txt"); // 简洁写法,实际就是对完整方法的封装

// 2、定义一个字节数组与源文件一模一样大小
File f = new File("cn.snailsir.study/src/aa.txt");
long size = f.length();
System.out.println("文件大小是:"+size);
byte[] buffer = new byte[(int)size];

int len = is.read(buffer);
System.out.println("读取的字节"+len);
System.out.println(new String(buffer));

// 以上方法jdk9之前都要这样写

// 以下方法jdk9开始使用
byte[] buffer = is.readAllBytes();
Syste.out.println(new String(buffer));

缺点:不能读取大文件内容,会内存泄漏

字节输出流

抽象类:OutputStream

实现类:FileOutputStream(文件字节输出流)

作用:以内存为基准,把内存中的数据以字节的形式写出到文件中去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 // 1、创建一个文件字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream("cn.snailsir.study/src/aa.txt"); // 覆盖管道
OutputStream os = new FileOutputStream("cn.snailsir.study/src/aa.txt",true); // 追加管道

// 2、开始写字节数据出去
// public void write(int a) 每次写一个字节
os.write('a');
os.write(97);
os.write('中'); // 乱码 因为一个中文占3个字节,而这里只写入一个字节
os.wtite("\r\n".getBytes()); // 换行

// 3、每次写一个字节数组的数据
// public void write(byte[] buffer)
byte[] bytes = "abc我爱中国".getBytes();
os.write(bytes);

// 4、写每一个字节数组的一部分
// public void write(byte[] buffer,int pos,int len)
// params1: 字节数组
// params2: 写出去的第一个字节的索引
// params3: 总共写出去多少个字节
os.write(bytes,3,12);// 我爱中国

// io流管道属于系统资源,会占用内存和响应的IO资源(总线)
// 用完之后必须关闭管道,以释放占用的系统资源
// os.flush(); // 刷新缓存中的数据到磁盘文件中去(因为内存写入的快,硬盘写入的慢,没有引入缓存,硬盘会拖累内存的效率)
// 字节输出流:没有提供缓存区
os.close(); // 关闭包含刷新了

字符输入流

抽象类:Reader

实现类:FileReader

以内存为基准,可以把文件中的数据以字符的形式读入到内存中去

每次读取一个字符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 1、创建文件字符输入流管道与源文件接通
// public FileReader(File file) 
// public FileReader(String filePath)
Reader fr = new FileReader("cn.snailsir.study/src/aa.txt");

// 2、每次读取一个字符返回,没有字符可读返回-1
// public int read(): 每次读取一个字节返回,如果没有字节可读,返回-1
int c1 = fr.read();
System.out.println(c1);


int c2 = fr.read();
System.out.println((char)c2);


// 3、使用循环读取
int b;
while((b=fr.read()) != -1){
  System.out.println((char)b);
}

解决了中文乱码问题

性能较差(工作中不用)

每次读取多个字符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
try(
// 1、创建文件字符输入流管道与目标文件接通 
  Reader fr = new FileReader("cn.snailsir.study/src/aa.txt");
){
  // 2、定义一个字符数组用于读取多个字符
  char[] buffer = new char[3];
  int len; // 记住每次读取多少个字符
  while((len = fr.read(buffer)) != -1){
    String rs = new String(buffer,0,len);
    System.out.print(rs);
  } 
}catch(Exception e){
  e.printStackTrace();
}

拓展

避免了中文乱码

性能可以

字符输出流

抽象类:Writer

实现类:FileWriter

以内存为基准,把内存中的数据以字符的形式写出到文件中去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
try(
	// 1、创建一个文件字符输出流管道与目标文件接通
	// Write fw = new FileWrite("cn.snailsir.study/src/aa.txt"); // 覆盖管道
	Write fw = new FileWrite("cn.snailsir.study/src/aa.txt",true); // 追加管道
){
    // 2、开始写字符数据出去
  	// 2.1 写一个字符出去: public void write(int a)
    fw.write('a');
    fw.write(97);
    fw.write('中');
    fw.wtite("\r\n".getBytes()); // 换行
  
  	// 2.2 写一个字符串出去
  	fw.write("我爱你中国");
  
  	// 2.3 写字符串一部分出去 public void write(String s,int pos,int len)
  	fw.write("我爱你中国",0,3);// 我爱你
  
    // 2.4 写一个字符数组出去:public void write(char[] chars)
  	char[] chars = "我爱你中国".toCharArray();
  	fw.write(chars);
 
    
    // 2.5 写每一个字符数组的一部分 public void write(char[] chars,int pos,int len)
    fw.write(chars,6,2);
  
  	//fw.flush();// 刷新内存缓存数据到硬盘上
  	
}catch(Exception e){
  e.printStackTrace();
}

资源释放方式

try-catch- finally

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
OutputStream os = null;
try{
  ...
  ...
}catch(IOException e){
  e.printStackTrace();
}finally{
  // 释放资源
  try{
    if(os != null) os.close(); // != null判断是因为排除try第一句就出现异常,这个时候os还是null
  }catch(Exception e){
    e.printStackTrace();
  }
}

Finally代码区特点:无论try中的程序是正常执行了,还是出现异常,最后都一定会执行finally区,除非JVM终止

Finally代码区作用:一般用于在程序执行完成后进行资源的释放操作

try-with-resource

jdk7开始提供了更简单的资源释放方案

1
2
3
4
5
try(定义资源1定义资源2;...){
  可能出现异常的代码;
} catch(异常类名 变量名){
  异常的处理代码;
}

该资源(try内部)使用完毕后,会自动调用其close()方法,完成对资源的释放

1
2
3
4
5
6
7
8
try(
  // 资源:一般指的是最终实现了`AutoCloseable`接口
	OutputStream os = new FileOutputStrem("aa.txt");
){
  
}catch(Exception e){
  e.printStackTrace();
}

IO缓冲流

字节缓冲流

对原始流进行包装,以提高原始流读写数据的性能

字节缓冲输入流

BufferedInputStream

原理

字节缓冲输入流自带了8KB缓冲池

字节缓冲输出流

BufferedOutputStream

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
try(
	// 1、创建字节输入流管道与源文件接通
	InputStream is = new FileInputStream("cn.snailsir.study/src/aa.txt");
  // 使用高级缓存字节输入流包装低级的字节输入流
  InputStream bis = new BufferedInputStream(is);
  
  // 2、创建字节输出流管道与目标文件接通
	OutputStream os = new FileOutputStream("cn.snailsir.study/src/aa-bak.txt");
  // 使用高级缓存字节输出流包装低级的字节输出流
  OutputStream bos = new BufferedOutputStream(os);
){
    // 3、准备一个字节数组
  byte[] buffer = new byte[1024];
  // 4、转移数据
  int len;
  while((len = bis.read(buffer)) != -1){
    bos.writ(buffer,0,len);
  } 
  	
}catch(Exception e){
  e.printStackTrace();
}

原理

字节缓冲输出流自带了8KB缓冲池

字符缓冲输入流

BufferedReader

原理

字符缓冲输出流自带了8K(8192个字符)缓冲池

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
try(
	// 1、创建字符输入流管道与源文件接通
	Reader fr = new FileReader("cn.snailsir.study/src/aa.txt");
  // 使用高级缓存字节输入流包装低级的字节输入流
  BufferedReader br = new BufferedInputStream(fr);
){
  // 2、准备一个字符数组用于读取多少个字符
  char[] buffer = new char[1024];
  // 3、转移数据
  int len;
  while((len = br.read(buffer)) != -1){
    String rs = new String(buffer,0,len);
    System.out.print(rs);
  } 
 	// 4、缓存字符输入流多了一个按照行读取内容的功能
  String line;
  while((line = br.readLine()) != NULL){
    System.out.println(line);
  }
}catch(Exception e){
  e.printStackTrace();
}

注意:

缓存字符输入流多了一个按照行读取内容的功能readLine

字符缓冲输出流

BufferedWriter

原理

字符缓冲输出流自带了8K(8192个字符)缓冲池

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
try(
	// 1、创建一个文件字符输出流管道与目标文件接通
	// Write fw = new FileWrite("cn.snailsir.study/src/aa.txt"); // 覆盖管道
	Write fwo = new FileWrite("cn.snailsir.study/src/aa.txt",true); // 追加管道
  // 使用高级缓存字符输出流包装低级的字符输出流
  BufferedWriter fw = new BUfferedWriter(fwo); // 追加管道
){
    // 2、开始写字符数据出去
  	// 2.1 写一个字符出去: public void write(int a)
    fw.write('a');
    fw.write(97);
    fw.write('中');
    fw.wtite("\r\n".getBytes()); // 换行
  	fw.newLine(); // 换行新增
  
  	// 2.2 写一个字符串出去
  	fw.write("我爱你中国");
  
  	// 2.3 写字符串一部分出去 public void write(String s,int pos,int len)
  	fw.write("我爱你中国",0,3);// 我爱你
  
    // 2.4 写一个字符数组出去:public void write(char[] chars)
  	char[] chars = "我爱你中国".toCharArray();
  	fw.write(chars)
    
    // 2.5 写每一个字符数组的一部分 public void write(char[] chars,int pos,int len)
    fw.write(chars,6,2);
  
  	//fw.flush();// 刷新内存缓存数据到硬盘上
  	
}catch(Exception e){
  e.printStackTrace();
}

注意:

缓存字符输出流多了一个换行的功能newLine

原始流、缓冲流的性能分析

1
2
3
4
5
6
// 使用低级的字节流按照一个个字节的形式复制文件:慢的无人让人忍受,直接淘汰,禁止使用
// 使用低级的字节流按照字节数组的形式复制文件:还可以,但是相对较慢
// 使用高级的缓冲字节流按照一个个字节的形式复制文件:还是特别慢,不推荐使用
// 使用高级的缓冲字节流按照字节数组的形式复制文件:极快,推荐使用

调节字节数组大小默认8k),可以提高效率但并不是越大越好

IO转换流

为了解决不同编码读取时会乱码的问题

字符输入转换流

InputStreamReader:解决不同编码时,字符流读取文件内容乱码问题

原理

先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
try(
	// 1、得到GBK文件的原始字节输入流
  InputStream is = new FileInputStream("aa.txt");
  // 2、通过字符输入转换流把原始字节流按照指定编码转换成字符输入流
  // public InputStreamReader(InputStream is,String charset) 把原始的字节输入流,按照指定字符集编码转成字符输入流(重点)
  Reader isr = new InputStreamReader(is,"GBK");
  // 3、把字符输入流包装成高级的缓冲字符输入流
  BufferedReader br = new BufferedReader(isr);
){
  // 4、按照行数读取
  String line;
  while((line = br.readLine()) != null){
    System.out.println(line)
  }
}catch(Exception e){
  e.printStackTrace();
}

字符输出转换流

控制写出去的字符使用什么字符集编码

String data = "我爱你中国"; byte[] bytes = data.getBytes("GBK");在进行写入操作。这种方式也可以

原理

获取字节输出流,再按照指定的字符集编码将其转换成字符输出流,以后写出去的字符就会用该字符集编码了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
try(
	// 1、创建一个文件字节输出流通向目标文件
  OutputStream is = new FileOutputStream("out.txt");
  // 指定写出去的编码时GBK
  // public OutputStreamWriter(OutputStream os,String charset) 把原始的字节输出流,按照指定编码转成字符输出流
  Reader isr = osw OutputStreamWriter(is,"GBK");
  // 2、把字符输出流包装成高级的缓冲流
  BufferedWrite bw = new BufferedWriter(osw);
){
  // 4、写入数据
  bw.write("xxxx");
  bw.newLine();
  bw.write("yyyy");
  bw.newLine();
  bw.write("zzzz");
}catch(Exception e){
  e.printStackTrace();
}

IO打印流

PrintStream字节输出流

PrintWriter 字符输出流

作用

可以实现更方便、更高效的打印数据出去,能实现打印啥出去就是啥出去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
try(
  // public PrintStream(OutputStream/File/String); 打印流直接通向字节输出流/文件/文件路径
  // public PrintStream(String fileName,Charset charset); 可以指定写出去的字符编码
  // public PrintStream(OutputStream out,boolean autoFlush); 可以指定实现自动刷新
  // public PrintStream(OutputStream out,boolean autoFlush,String encoding); 可以指定实现自动刷新,并可指定字符的编码
	PrintStream ps = new PrintStream("ps.txt");
  // PrintWriter pw = new PrintWriter(new FileWriter("ps.txt"),true); // 追加 与PrintStream使用方法一模一样
){
  // 以下打印的内容会原封不动的存储到ps.txt文件中
  // public void println(Xxx xx) 打印任意类型的数据出去
  // public void write(int/String/char[]/...) 可以支持写字符数据出去
  ps.println(6666); 
  ps.println('a');
  ps.println(true);
  ps.println("我爱你中国");
}catch(Exception e){
  e.printStackTrace();
}

PrintStream与PrintWriter区别

  • 打印数据的功能上是一模一样的:都是使用方便,性能高效
  • PrintStream继承自字节输出流OutputStream,因此支持写字节数据的方法
  • PrintStream继承自字符输出流Writer,因此支持写字符数据出去

应用

输出语句的重定向:打印语句一般打印在控制台,我们可以让其打印到文件中去,这就是输出语句的重定向

1
2
3
4
5
6
7
System.out.println("xxx");
System.out.println("yyy");

// 重定向
PrintStream ps = new PrintStream(new FileOutputStream("out.txt",true));
System.setOut(ps); // 把系统类的打印流改成我的打印流
System.out.println("zzz");

IO数据流

常用于通讯领域

DataOutputStream(数据输出流)

允许把数据和其类型一并写出去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
try(
	DataOutputStream dos = new DataOutputStream(new FileOutputStream("out.txt"));
){
  // 写数据和类型出去
  dos.writeByte(97); 
  dos.writeBoolean(true); 
  dos.writeInt(1000); 
  dos.writeChar('a'); 
  dos.writeUTF("我爱你中国"); 
}catch(Exception e){
  e.printStackTrace();
}

DataInputStream(数据输入流)

用于读取数据输出流写出去的数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
try(
  // 创建高级的特殊数据输入流管道包装低级的字节输入流管道
	DataInputStream dis = new DataInputStream(new FileInputStream("out.txt"));
){
  // 开始读取,顺序要与写入流顺序一致
  byte b = dis.readByte(); 
  System.out.println(b);
  
  boolean b1 = dis.readBoolean();
  System.out.println(b1);
  
  int i = dis.readInt();
  System.out.println(i);
  
  char c = dis.readChar();
  System.out.println(c);

  String s = dis.readUTF();
  System.out.println(s);
}catch(Exception e){
  e.printStackTrace();
}

IO序列化流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import lombok.AllArgsConstructor; // 使用30版本,28版本有问题
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
// 注意:如果学生对象要参与序列化,必须实现序列化接口
public class Student implements Serializable{
  private String name;
  private int age;
  // transient 修饰的成员变量将不参与序列化
  private transient String password;
}

对象序列化(ObjectOutputStream)

把java对象写入到文件中去

1
2
3
4
5
6
7
8
// 准备一个对象
Student s = new Student("snailsir",27,"123456");
// 创建对象字节输出流管道与目标文件管道
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"));
// 开始写对象出去
oos.writeObject(s);
// 关闭资源
oos.close();

对象反序列化(ObjectInputStream)

把文件里的java对象读出来

1
2
3
4
5
6
// 创建对象字节输入流管道
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"));
// 读取对象进来(对象反序列化)
Student s = (Student)ois.readObject();
// 关闭资源
ois.close();

IO框架

框架:解决某类问题,编写的一套类、接口等

好处:在框架的基础上开发,可以得到优秀的软件框架,并能提高开发效率

框架形式:一般是把类、接口等编译成class形式,再压缩成一个.jar结尾的文件发行出去

IO框架:封装了java提供的对文件、数据进行操作的代码,对外提供了更简单的方式来对文件进行操作,对数据进行读写等

案例

复制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 1、创建字节输入流管道与源文件接通
InputStream is = new FileInputStream("aa.txt");

// 2、创建字节输出流管道与目标文件接通
InputStream is = new FileInputStream("aa-bak.txt");

// 3、准备一个字节数组
byte[] buffer = new byte[1024];

// 4、转移数据
int len;
while((len = is.read(buffer)) != -1){
  os.write(buffer,0,len);
}

// 5、释放资源
os.close();
is.close();

任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题

使用 Hugo 构建
主题 StackJimmy 设计