什么是数据流
顾名思义,I 表示input,O 表示output,也就是输入输出流,主要是在程序与文件之间,用于传输数据的通道。既然要传输数据,那么我们需要理解文件和程序之间哪种方向的传输是输入流,哪种传输作为输出流?我们可以举一个例子,如下图所示:
IO 流是 Java IO 中的核心概念。流是在概念上表示无穷无尽的数据流。IO 流连接到数据源或数据的目的地,连接到数据源的叫输入流,连接到数据目的地的叫输出流。Java 程序不能直接从数据源读取和向数据源写入,只能借助 IO 流从输入流中读取数据,向输出流中写入数据。
Java IO 中的流可以是基于字节的(读取和写入字节)也可以基于字符的(读取和写入字符),所以分为字节流和字符流,两类流根据流的方向都可以再细分出输入流和输出流。
注意:这里的输入、输出是站在CPU的角度。
一、字节流
字节流主要操作字节数据或二进制对象。字节流有两个核心抽象类:InputStream 和 OutputStream。所有的字节流类都继承自这两个抽象类。
1.1 输入字节流:InputStream
InputStream 只是一个抽象类,要使用还需要具体的实现类。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使用 FileInputStream。
常用的方法:
参考实例代码:
package CharStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class Demo3 {
public static void main(String[] args) {
//1、创建File对象
try(InputStream inputStream = new FileInputStream("D:/text.txt")){
byte[] buffer = new byte[1024];
int n = inputStream.read(buffer); //2、读取文件内容
System.out.println("n:"+n);
for (int i = 0; i < n; i++) {
System.out.printf("%x\n",buffer[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
利用 Scanner 进行字符读取
我们看到了对字符类型直接使用 InputStream 进行读取是非常麻烦且困难的,所以,我们使用一种我们之前比较熟悉的类来完成该工作,就是 Scanner 类。
package CharStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class Demo5 {
public static void main(String[] args) {
try(InputStream inputStream = new FileInputStream("d:/text.txt")){
//此时scanner就是从文件中读取了!!
Scanner scanner = new Scanner(inputStream);
String s = scanner.next();
System.out.println(s);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
1.2 输出字节流:OutputStream
构造方法:
OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中,所以使用 FileOutputStream。
常用方法:
参考实例代码:
package CharStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class Demo4 {
public static void main(String[] args) {
//1. 打开文件,如果文件存在,会清除文件的内容,如果文件不存在,就会创建新文件
try(OutputStream outputStream = new FileOutputStream("d:/text.txt",true)){
String s= "你好世界";
//2. 使用wirte方法写入内容,字符串调用getBytes()方法可以得到字符串对应的字节数组
outputStream.write(s.getBytes());
//和writer类似,OutPutStream打开一个文件,就会默认清空文件的原有内容,如果不想清空,在构造方法中第二个参数传入true
//3. 关闭文件
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
上述,我们其实已经完成输出工作,但总是有所不方便,我们接来下将 OutputStream 处理下,使用PrintWriter 类来完成输出,因为PrintWriter 类中提供了我们熟悉的 print/println/printf 方法。
参考实例代码如下:
package CharStream;
import java.io.*;
public class Demo6 {
public static void main(String[] args) {
try(OutputStream outputStream = new FileOutputStream("d:/text.txt")){
//这里就相当于把字节流转成字符流了
PrintWriter writer = new PrintWriter(outputStream);
writer.println("hello");
writer.flush(); //
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
二、 字符流
上面的字节流是以字节为单位读写文件数据,但是对unicode字符来说,中文字符占了两个字节,处理不当会出现“乱码”现象。
字符流操作的是字符,字符流有两个核心类:Reader 类和 Writer 。所有的字符流类都继承自这两个抽象类。
2.1 输入字符流: Reader
字符流输入流作用:读取文件内容。
参考实例代码:
package CharStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
public class Demo1 {
/**
* 字符流
* read
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
// Reader reader = new FileReader("d:/text.txt");
/**
* 1.一次read一个字符
*
* while(true){
* int c = reader.read(); //无参数read,一次只返回一个字符
* if (c== -1){
* //读取完毕
* break;
* }
* char ch = (char) c;
* System.out.println(ch);
* }
*/
try{
//2.一次read多个字符
while(true){
char[] cubf = new char[1024];
int n = reader.read(cubf);
* n表示当前读取到的字符的个数
* 一个参数read:一次读取若干个字符,会把参数指定的cbuf数组给填充满
* 应该往这个read里传入的是一个空的字符数组,然后由read方法内部对这个数组内容进行填充,次数"cbhf"这个参数,称为”输出型参数·“
if (n== -1){
//完毕读取
break;
}
System.out.println("n = "+n);
for (int i = 0; i < n; i++) {
System.out.println(cubf[i]);
}
}
}finally {
reader.close();
//3. 一个文件使用完了,要记得close (使用close方法,最主要的方法是为了释放文件描述符)
}
*/
//try with resources语法的目的 :()定义的变量,会在try代码快结束的时候(无论是正常结束还是抛异常),自动调用其中的close方法
try(Reader reader = new FileReader("D:/text.txt") ){
while(true){
char[] cubf = new char[3];
int n = reader.read(cubf);
if (n == -1){
break;
}
System.out.println("n:"+n);
for (int i = 0; i < n; i++) {
System.out.println(cubf[i]);
}
}
}
// reader.read(); //三个参数read:一次读取若干个字符,会把参数执行的cbuf数组中的off这个位置开始,到len这么长的范围内填满
}
}
2.2 输出字符流:Writer
参考代码实例:
package CharStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
/**
* @author Zhang
* @date 2024/5/1620:23
* @Description:
*/
public class Demo2 {
/**
*字符流
* write
* @param args
*/
public static void main(String[] args) {
try(Writer writer = new FileWriter("d:/text.txt",true);) {
writer.write("我在学习IO"); //写入文件,默认会把原来的内容清空,解决办法加true
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
三、 字节流、字符流怎么选择?
字节流和字符流都有 read()、write()、flush()、close() 这样的方法,这决定了它们的操作方式近似。
字节流的数据是字节(二进制对象)。主要核心类是 InputStream 类和 OutputStream 类。字符流的数据是字符,主要核心类是 Reader 类和 Writer 类。所有的文件在硬盘或传输时都是以字节方式保存的,例如图片,影音文件等都是按字节方式存储的。字符流无法读写这些文件。
所以,除了纯文本数据文件使用字符流以外,其他文件类型都应该使用字节流方式。字节流到字符流的转换可以使用 InputStreamReader 和 OutputStreamWriter。使用 InputStreamReader 可以将输入字节流转化为输入字符流,使用OutputStreamWriter可以将输出字节流转化为输出字符流。
相关练习:扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件:
package CharStream;
import java.io.File;
import java.util.Scanner;
/**
* @author Zhang
* @date 2024/5/1715:28
* @Description:
*/
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 1. 先让用户输入一个要扫描的目录
System.out.println("请输入要扫描的路径");
String path = scanner.next();
//判断路径是否存在
File rootPath = new File(path);
if (!rootPath.isDirectory()){
System.out.println("您输入的扫描的路径有误");
return;
}
//2.再让用户输入一个要查询的关键词
System.out.println("请输入要删除文件的关键词");
String word = scanner.next();
//3. 进行递归扫描,这个方法进行递归
scanDir(rootPath,word);
}
private static void scanDir(File rootPath, String word) {
//1. 先列出rootPath 中的所有文件和目录
File[] files = rootPath.listFiles();
if(files == null){
///当前目录为空,就可以直接返回了
return;
}
//2. 遍历这里的每个元素,针对不同各类型做出不同的处理
for (File f: files){
System.out.println("当前扫描的文件:"+f.getAbsolutePath()); //加个日志,方便观察当前递归的执行过程
if (f.isFile()){
//普通文件,检查是否要被删除,并执行删除操作
checkDelete(f,word);
}else {
//目录,运用递归再去判定子目录里包含的内容
scanDir(f,word);
}
}
}
private static void checkDelete(File f, String word) {
if (!f.getName().contains(word)){
//不必删除,直接方法结束
return;
}
//需要删除
System.out.println("当前文件为:"+f.getAbsolutePath() +",确认是否要删除");
Scanner scanner = new Scanner(System.in);
String choice = scanner.next();
if (choice.equals("Y") || choice.equals("y")){
//真正执行删除操作
f.delete();
System.out.println("删除完毕!");
}else{
//如果出入其他值,不一定是n,都会取消删除操作
System.out.println("取消操作!");
}
}
}
总结
文件IO流框架: