【Java】IO流基础——核心细节、以及打造实现缓冲区功能

目录

 

什么是IO流?

数据长度单位转换

File类:操作文件和属性

创建File

获取系统属性——文件分隔符、路径分隔符

File类的常见方法

FilenameFilter接口:文件名过滤器

使用FilenameFilter接口

FileFilter接口:文件过滤器

递归删除目录:遍历指定文件夹下的所有子目录和文件并删除

IO流

基类

输出流:自动创建目的问题

结论

输入流:读取文件数据到数组问题

输入流:读取时的指针问题

缓冲区:大小设置

输入流:读取上一次未读取完的字节

复制文件

缓冲流:缓冲区刷新问题

编码:编码与解码问题

字节流操作中文

GBK

ASCII

Unicode

UTF-8

总结

转换流与字符流便捷类的区别

Line: 当年难倒N多人的行问题

BufferedReader类的String readLine()方法

System.in和System.out使用问题

阻塞概念

自己实现BufferedReader,BufferedWriter

装饰设计模式

IO流规则总结

一、明确源、目的地

二、明确操作是否包含语言的文本

三、明确操作的具体设备

四、是否需要额外功能?


什么是IO流?

I(Input)输入:读取资源数据,O(Output)输出:输出数据。
什么是流(Stream)?不同系统的内核给我们提供了不同的资源(如文件)操作,流(Stream):即(系统资源)
Java语言本身不会操作系统资源,而是使用系统给我们提供的资源进行IO操作

数据长度单位转换

1byte  = 8bit (位)

1K = 1024byte

1M = 1024K

1G = 1024M

1T = 1024G

File类:操作文件和属性

要操作文件里的数据,首先得了解怎么操作文件

创建File

File file = new File("c:\\1.txt");
		
File file1 = new File("c:\\", "1.txt");
		
File dir = new File("c:\\");
		
File file2 = new File(dir, "1.txt");

在Java中,一个斜杠【\】代表着转意,\\两个斜杠就是把后一个斜杠\转意成字符。
如果这个程序在Unix系统运行,就会出现文件分隔符的问题,Java是跨平台语言,提供了相应的方法获取系统属性

获取系统属性——文件分隔符、路径分隔符

java.lang.System类

//获取指定键指示的系统属性
public static String getProperty(String key)

示列:

String file_separator = System.getProperty("file.separator");
System.out.println(file_separator);
String path_separator = System.getProperty("path.separator");
System.out.println(path_separator);

windows输出:

\
;

Key是固定的

当然File类给我们提供了字段


不同的地方在于一个返回的是char,一个是String

System.out.println(File.separator);
System.out.println(File.separatorChar);
System.out.println(File.pathSeparator);
System.out.println(File.pathSeparatorChar);

windows输出:

\
\
;
;

File类的常见方法

  • boolean exits() 是否存在
  • boolean mkdirs() 创建多级文件夹(如果封装的抽象路径没有,则创建包括:父目录,子目录。new File("C:\\a\\b\\c\\d\\e"))
  • boolean mkdir() 创建文件夹(只能创建指定目录下的一级目录)
  • boolean createNewFile() 创建文件(文件不一定有后缀名,创建的目录也可以带点 .)
  • boolean delete() 删除文件或目录(当删除的是一个目录时,目录里必须为null,否则删除失败)
  • boolean IsDirectory() 是否是文件夹
  • boolean IsFile() 是否是文件
  • boolean IsHidden() 是否隐藏
  • boolean CanRead() 是可读
  • boolean CanWrite() 是否可写
  • long lastModified() 最后一次修改时间(单位毫秒值);
  • long length() 文件长度大小 (以字节为单位)
  • boolean renameTo(File dest) 重命名
  • String getParent() 获得父目录
  • String getAbsolutePath() 获得绝对路径
  • long getTotalSpace() 返回此抽象路径名指定的分区总大小
  • long getFreeSpace() 返回此抽象路径名指定的分区未分配的字节数
  • static File[] listRoots() 系统有效盘符都拿到
  • String[] list() 返回指定目录下的 目录和文件的名称(包含隐藏文件)
  • File[] listFiles() 返回指定目录下的 目录和文件的名称(包含隐藏文件)(可操作目录或文件,也意味着需要更多内存)

不管创建的File对象的抽象路径是否存在,对象的方法都是可用的

FilenameFilter接口:文件名过滤器

需求:查找的目录或文件,只能有指定包含的名字或后缀名!

普通情况下查找

File file = new File("F:\\");
String[] names = file.list();
for (String name : names) {
    if (name.endsWith(".java")) { //只要有此后缀名的文件
		System.out.println(name);
	}
}

输出
StringDemo.java

使用FilenameFilter接口

此接口只有一个重写的方法

boolean	accept(File dir, String name)

dir : 指定的目录下
name : 目录名或文件名

FilenameBySuffix.java(后缀名过滤器)

public class FilenameBySuffix implements FilenameFilter {
	private String suffix;
	
	FilenameBySuffix(String suffix) {
		this.suffix = suffix;
	}

	@Override
	public boolean accept(File dir, String name) {
		return name.endsWith(suffix);
	}

}
File file = new File("F:\\");
String[] names = file.list(new FilenameBySuffix(".java"));
for (String name : names) {
	System.out.println(name);
}

输出:
StringDemo.java

FilenameByContent.java(包含内容过滤器)

public class FilenameByContent implements FilenameFilter {
	private String content;
	
	FilenameByContent(String content) {
		this.content = content;
	}

	@Override
	public boolean accept(File dir, String name) {
		return name.contains(content);
	}

}
File file = new File("F:\\");
String[] names = file.list(new FilenameByContent("Demo"));
for (String name : names) {
	System.out.println(name);
}

输出:
StringDemo.class
StringDemo.java

FileFilter接口:文件过滤器

boolean	accept(File pathname)

pathname : 得到的是抽象文件对象File。这个比FilenameFilter接口更为强大,同样能实现文件名过滤。

MyFileFilter.java(文件过滤器)

逻辑为判断是否是文件而不是目录都列出

public class MyFileFilter implements FileFilter {

	@Override
	public boolean accept(File pathname) {
		return pathname.isFile();
	}

}
File file = new File("C:\\");
File[] paths = file.listFiles(new MyFileFilter());
for (File path : paths) {
	System.out.println(path);
}

输出了C盘的文件,包括隐藏文件

C:\bootmgr
C:\BOOTNXT
C:\hiberfil.sys
C:\swapfile.sys

只要更改一下文件过滤器逻辑,同样能实现文件名过滤

@Override
public boolean accept(File pathname) {
	return pathname.getName().endsWith(".java");
}

递归删除目录:遍历指定文件夹下的所有子目录和文件并删除

递归:无非就是方法里调用方法本身,使用递归一定要判断条件,否则会出现栈内存溢出异常。

Exception in thread "main" java.lang.StackOverflowError
public static void show() {
	show();
}

递归删除目录

因为删除一个目录,里面必须没有内容,如果有内容必须从里向外删。

File file = new File("F:\\test");
deleteDir(file);

public static void deleteDir(File dir) {
	System.out.println(dir); //输出进入的目录
	
	// 获取指定目录下的目录和文件夹
	File[] files = dir.listFiles();
	
	for (File file : files) {
		if (file.isDirectory()) { //判断是目录则继续遍历此目录
			deleteDir(file); //会遍历到最里层的目录,方法压栈进栈
		} else { //判断是文件则删除
			System.out.println(file + ": " + file.delete());
		}
	}
		
	// 遍历完一个指定目录,这个目录里已经没有内容
	// 此方法准备出栈,删除这个没人内容的目录
	System.out.println(dir + ": " + dir.delete());
}

输出

F:\test

    F:\test\a 
    F:\test\a\a.txt: true
    F:\test\a: true

    F:\test\b
    F:\test\b\b.txt: true
    F:\test\b: true

    F:\test\c
    F:\test\c\c.txt: true
    F:\test\c: true

F:\test: true

当然我们指定的目录可能不存在,所以判断不存在则退出

if (!dir.exists()) {
	return;
}

IO流

基类

  • 字节流的抽象基类

       InputStream(读)、OutputStream(写)。
       可操作计算机二进制文件,如.mp3, .mp4, .txt, .jpg。等等

  • 字符流的抽象基类

       Reader(读)、Writer(写)。
       针对字符的,如中文、日文,编码的乱码现象就是在此解决

输出流:自动创建目的问题

我先用最简单的输出流示范

 我的F盘符下没有文件

我创建一个字节输出流对象

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

    FileOutputStream out = new FileOutputStream("F:\\test.txt");

}

 此时自动创建了一个test.txt

我用windows的记事本输入abc并保存,然后再运行main函数

此时会覆盖掉这个test.txt并清空内容

同时输出流默认写出信息时,会覆盖掉上一次的内容

FileOutputStream out = new FileOutputStream("F:\\test.txt");
out.write("123".getBytes());
out.close();

结论

输出流所关联的目的地,如果不存在则自动创建,如果存在则默认覆盖(先前文件有内容也清空)。

输出流每次写出的内容都会覆盖掉上一次的内容。

要想如果文件存在时不清空内容,则使用带布尔值的构造,会在后面续写:

FileOutputStream out = new FileOutputStream("F:\\test.txt", true);

换行符

因为不同的系统的换行符是不一样的,所以我们在输出要换行时应该获取系统的换行符。

//System的两个静态方法都能获取系统的换行符
System.lineSeparator();

System.getProperty("line.separator")

输入流:读取文件数据到数组问题

FileOutputStream out = new FileOutputStream("F:\\test.txt");
out.write("abc".getBytes()); //写入abc
out.close();

FileInputStream in = new FileInputStream("F:\\test.txt");
byte[] buffer = new byte[2]; //创建缓冲区大小为2字节

//每次读取2字节
//读取到的数据存入缓冲区,并返回读取的字节数
//如果读取的内容小于缓冲区则全部读取,读取到末尾无数据则返回 -1
while ((in.read(buffer)) != -1) {
	System.out.println(new String(buffer));  //构造String使用平台默认编码表进行解码
}
in.close();

输出:

ab
cb

问题看图:

改代码:

FileOutputStream out = new FileOutputStream("F:\\test.txt");
out.write("abc".getBytes()); //写入abc
out.close();

FileInputStream in = new FileInputStream("F:\\test.txt");
byte[] buffer = new byte[2]; //创建缓冲区大小为2字节
int length; //存储每次读取到的字节数

//每次读取2字节
//读取到的数据存入缓冲区,并返回读取的字节数
//如果读取的内容小于缓冲区则全部读取,读取到末尾无数据则返回 -1
while ((length = in.read(buffer)) != -1) {
	System.out.println(new String(buffer, 0, length)); //构造String使用平台默认编码表进行解码
}
in.close();

输出:

ab
c

输入流:读取时的指针问题

每次读一波数据,指针都会移动到已读到数据的最后一位,当指针已到达数据末尾返回-1则没有数据。这个输入流对象此后都不能读取到数据,以及调用输入流的available()方法永远返回0。available()方法得到的是当前未读到的数据字节数。

调用mark后并且或调用reset方法即可重置指针位置,针对设备的支持,否则抛出异常。

缓冲区:大小设置

设置的缓冲区大小,符合系统的缓冲区值范围。

以512字节倍数增长、1024、2048、4096、8192、...,不能太大,根据系统缓存大小定义

直接设置与之文件对应的缓冲区大小出现的问题:

FileInputStream in = new FileInputStream("F:\\test.txt");
byte[] buffer = new byte[in.available()];
in.read(buffer);

这样会觉得连遍历都不用遍历了,直接读取。

如果这个文件的大小是10m、100m、1g的,这个程序就异常崩溃了,内存分配溢出了。

输入流:读取上一次未读取完的字节

long skip(long n)方法:从输入流中跳过并丢弃 n 个字节的数据。

比如下载的文件未下载完,下一次下载时就可以跳过已下载的字节数,并下载未完成的字节数。

复制文件

只要是二进制文件都能复制,爱谁谁。音视频、图片、文本

明确数据源和目的地

FileInputStream in = new FileInputStream("F:\\test.txt"); //数据源
FileOutputStream out = new FileOutputStream("F:\\test2.txt"); //目的地

byte[] buffer = new byte[1024];
int length;

while ((length = in.read(buffer)) != -1) {
	out.write(buffer, 0, length);
}
in.close();
out.close();

缓冲流:缓冲区刷新问题

BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter、InputStreamReader和OutputStreamWriter(写出或读取时的是字符串会进行解码存入自身缓冲区),顾名思义,它们里面都维护着一个缓冲区,也就是一个数组。

读取和输出的顺序,比如从硬盘读取,在缓冲输入流 读取数据时,会先将数据读取到它本身封装的数组,此时我们一般会在外面写一个数组,再读取此输入流缓冲区的数据,

在缓冲输出流 输出数据时,会先将数据存储到它维护的数组里,等到一定字节数量时会自动一并输出,这时它本身会调用flush()方法。如最后存储的数据字节未满足缓冲区数量,就有可能导致数据未输出完,应该手动调用flush()方法。

使用缓冲流复制文件的例子

FileInputStream in = new FileInputStream("F:\\test.txt"); //数据源
FileOutputStream out = new FileOutputStream("F:\\test2.txt"); //目的地

BufferedInputStream bis = new BufferedInputStream(in); //转换流
BufferedOutputStream bos = new BufferedOutputStream(out); //转换流
//BufferedInputStream bis = new BufferedInputStream(in, 8192); //构造指定缓冲区大小
//BufferedOutputStream bos = new BufferedOutputStream(out, 8192); //构造指定缓冲区大小

byte[] buffer = new byte[1024];
int length;

while ((length = bis.read(buffer)) != -1) {
	bos.write(buffer, 0, length);
}
bis.close();
bos.flush(); //刷新缓冲区写入
bos.close();

编码:编码与解码问题

字节流操作中文

FileOutputStream out = new FileOutputStream("F:\\test.txt"); //目的地
out.write("你A好".getBytes()); //写入中文和英文:平台默认编码
out.close();

FileInputStream in = new FileInputStream("F:\\test.txt"); //数据源
byte[] buffer = new byte[1024];
int length = in.read(buffer); //这里就直接读取了
in.close();

System.out.println(new String(buffer, 0, length)); //使用平台默认编码解码
System.out.println("平台默认编码 : " + System.getProperty("file.encoding"));

for (int index = 0; index < length; index++) {
	byte b = buffer[index];
	int binary = b | 0xFFFFFF00;
	System.out.println(b + ":二进制 = " + Integer.toBinaryString(binary).substring(24, 32));
}

输出:

你A好
平台默认编码 : GBK
-60:二进制 = 11000100
-29:二进制 = 11100011
65:二进制 = 01000001
-70:二进制 = 10111010
-61:二进制 = 11000011

我的系统是windows简体中文,默认编码是GBK。。

GBK

中国的中文编码升级版(上一个是GB2312,一个中文字符用2个字节表示,每个字节最高位为1,以后7位为有效位)。而GBK升级版扩展了更多的中文字符。一个字符也用2个字节表示,第一个字节的最高为1,而第二个字节的最高位不一定为1,这样也就兼容GB2312,扩容了更多中文字符(所以它们的编码都为负数)。兼容ASCII。

ASCII

美国标准信息交换码,一个字符用1个字节表示,二进制最高位为0,以后7位表示一个字符,可表示127个字符,包括英文字母、阿拉伯数字、英文符号等。

Unicode

国际标准码,所有文字都用2个字节表示,Java语言使用的就是Unicode。此字符集并不兼容ASCII,本来能用1个字节表示的字符用了2个字节表示,比如英文字母或阿拉伯数字都用2个字节表示,这就导致内存的浪费,之后就出现了UTF-8。

UTF-8

最多用3个字节表示一个字符,兼容ASCII,此字符不像Unicode这样的限制,它可以以一个字节显示的字符就用一个字节,两个字节就两个,三个就三个。例如:

10xx_xxxx

110x_xxxx 10xx_xxxx

1110_xxxx 10xx_xxxx 10xx_xxxx

x 为有效表示位

以上一个例子:指定字符编码字符并输出,指定字符编码解码读取的字符

FileOutputStream out = new FileOutputStream("F:\\test.txt"); //目的地
out.write("你A好".getBytes("UTF-8")); //写入中文和英文:指定字符编码
out.close();

FileInputStream in = new FileInputStream("F:\\test.txt"); //数据源
byte[] buffer = new byte[1024];
int length = in.read(buffer); //这里就直接读取了
in.close();

System.out.println(new String(buffer, 0, length, "UTF-8")); //使用指定编码解码
System.out.println("平台默认编码 : " + System.getProperty("file.encoding"));
		
for (int index = 0; index < length; index++) {
	byte b = buffer[index];
	int binary = b | 0xFFFFFF00;
	System.out.println(b + ":二进制 = " + Integer.toBinaryString(binary).substring(24, 32));
}

输出:

你A好
平台默认编码 : GBK
-28:二进制 = 11100100
-67:二进制 = 10111101
-96:二进制 = 10100000
65:二进制 = 01000001
-27:二进制 = 11100101
-91:二进制 = 10100101
-67:二进制 = 10111101

读取时解码流程

当然以UTF-8演示,读取【你】时读取到第一个字节打头1110,则后续再读取两个自己。在读取到【A】打头0,则直接去以兼容下来的ASCII编码查询,...。

总结

操作除(ASCII编码表)英文、英文字符、阿拉伯数字,如中文,需要使用封装好的转换流(字符流)

转换流与字符流便捷类的区别

FileWriter fw = new FileWriter("F:\\test.txt");
//等效于
FileOutputStream out = new FileOutputStream("F:\\test.txt");
OutputStreamWriter osw = new OutputStreamWriter(out);

区别在于便捷类只能构造文件,不能构造流,而且还不能指定字符编码,使用的是默认编码。而转换流能构造传入各种流,和指定字符编码。

Line: 当年难倒N多人的行问题

首先是问题:你觉得换行是啥?系统是通过什么表示换行的?

比如你在一个文本中输入字符,等你输入到有些数量时,自动地在下一行写入了,这是换行吗?

不,这不是换行。换行是等你按下回车时。

不同的系统平台表示的换行符都可能有不同的表示,通过以下方法获取系统属性换行符:

System.getProperty("line.separator")

BufferedWriter类的newLine()方法就是调用此方法。

BufferedReader类的String readLine()方法

每次读取一行,读取到\n或\r即可认为换行,返回是字符串,没有则null。

你在使用系统控制台输入数据时(读取键盘),你使用的Scanner类而不是高效BufferedReader的区别:

Scanner sin = new Scanner(System.in);
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

读取键盘录入专业:Scanner = 流 + 正则。方法都是按照某种规则在读取数据。

而使用BufferedReader更为直接。

System.in和System.out使用问题

System.in = 键盘录入。System.out = 输入到显示器。这两个流对象在程序启动时跟随虚拟机启动而启动。

当程序结束时会自动关闭。但是如果你在程序运行过程中关闭了这两某个流对象,在程序后面的运行中还想再次拿到这两个流,就再也拿不到了。

阻塞概念

比如输入流的read()方法

就好比使用键盘录入时System.in,在启动程序时调用此read()方法将会在外面等录入的信息,也就是如果你没有录入我就在那等着,有数据我就读取,当然玩键盘录入时要做判断结束标记,才能停止方法的阻塞,让方法出栈。

自己实现BufferedReader,BufferedWriter

缓冲的实现原理:临时存储数据的方式。就是减少程序(在内存)与设备(硬盘)之间的数据交换的频率,数据就是存储到了内存中,无非就是维护了一个数组。

在上面有一张实现的原理图,先看看:

我们先看看Java实现的缓冲流对象是怎么实现的:

MyBufferedReader.java

package com.bin.demo;

import java.io.IOException;
import java.io.Reader;

public class MyBufferedReader extends Reader {
	
	//持有一个字符输入流对象
	private Reader in;
	//缓冲区
	private char[] buffer;
	//指针
	private int pointer;
	//字符计数
	private int count;
	
	public MyBufferedReader(Reader in) {
		this(in, 8192);
	}
	
	public MyBufferedReader(Reader in, int size) {
		this.in = in;
		buffer = new char[size];
	}
	
	@Override
	public int read() throws IOException {
		int result = -1;
		
		//如果当前的字符数量为0,则读取下一批数据到缓冲区,并重置指针
		if (count == 0) {
			count = in.read(buffer);
			pointer = 0;
		}
		
		//读取设备文件数据已达末尾则为-1,则返回-1
		if (count != -1) {
			//获得一个字符
			result = buffer[pointer];
			//移动指针到下一个字符
			pointer++;
			//减少一个字符数
			count--;
		}
		
		return result;
	}
	
	@Override
	public int read(char[] cbuf, int off, int len) throws IOException { //重写的方法,读入指定数组的某一部分
		int count = 0;
		int c;
		
		for (int index = off, i = 0; (c = read()) != -1 && i < len; index++, i++) {
			cbuf[index] = (char) c;
			count++;
		}
		
		return count == 0 && c == -1 ? -1 : count;
	}
	
	public String readLine() throws IOException {
		String result = null;
		
		//使用StringBuffer防止String对象创建过多
		StringBuffer sb = new StringBuffer();
		
		int len;
		while ((len = read()) != -1) {
			char c = (char) len;
			
			//windows的换行符为\r\n组合,如果是运行在Unix是/n。所以这个类写完是不能在unix中运行的,这时候可以获取系统的换行符来计算。
			if (c == '\r') {
				continue;
			} else if (c == '\n') {
				break;
			}
			
			sb.append(c);
		}
		
		//有数据时才进行转换,防止不返回null
		if (sb.length() > 0) {
			result = sb.toString();
		}
		
		return result;
	}
	
    @Override
	public void close() throws IOException {
		in.close(); //关闭的是持有流
	}

}

主要实现方法为:read(); 和 readLine();

Reader抽象类的方法

BufferedWriter各位自己实现,原理一样

装饰设计模式

大家也都看到我写的类是继承自Reader抽象类,就是java起初设计了IO的缓冲流为装饰设计模式。

为什么?想想看,如果继承自InputStreamReader的那些类有多个,那我就得继承这些子类实现缓冲流,这样会造成继承体系过多庞大,变得耦合和复杂。

设计模式六大原则的:合成复用原则说到,尽量使用合成/聚合的方式,而不是使用继承。
解决:可以对对象提供额外的功能(职责),比继承这种方法更为灵活。
装饰类与被装饰类都属于同一体系。
同时装饰类持有被装饰类的引用。

这样便捷类也能转换为拥有缓冲的功能

BufferedReader buf = new BufferedReader(new FileReader("F:\\test.txt"));

IO流规则总结

一、明确源、目的地

其实就是在明确使用IO体系。InputStream、OutputStream。Reader、Writer

需求操作的是源:意味着读 :InputStream、Reader。

需求操作的目的:意味着写:OutputStream、Writer

二、明确操作是否包含语言的文本

除英文、阿拉伯数字、英文字符外,如:(中文、日文、韩文。。。)

是并且是源:Reader

是并且是目的:Writer

三、明确操作的具体设备

源设备

  • 硬盘:File开发的流
  • 键盘:System.in
  • 内存:数组
  • 网络:Socker流

目的设备

  • 硬盘:File开发的流
  • 显示器:System.out
  • 内存:数组
  • 网络:Socker流

第三步明确就已经确定流对象了。

四、是否需要额外功能?

需要高效吗?缓冲流,Buffered开发。

需要编码转换吗?转换流。

发布了27 篇原创文章 · 获赞 33 · 访问量 9498

猜你喜欢

转载自blog.csdn.net/qq_42470947/article/details/104567325