Java:IO流——基础篇
着重介绍 File 类和 IO流中的字节流(原始流)
目录
前言
IO(Input/Output)流的概念源于早期计算机系统对数据传输的抽象
在Java诞生前的1990年代
C/C++等语言处理文件操作需要直接调用操作系统API
代码繁琐且不可移植
Java的创造者们从Unix"一切皆文件"的哲学中获得灵感
设计了统一的IO流模型
使开发者能用一致的方式处理各种数据源
一、File 类
在介绍具体的IO操作前
我们先来学习一下File类
至于为什么,我们可以这么理解:
首先,File类并不是直接处理文件内容
而是专门用来操作文件和目录的路径信息
就像一个 导航 一样
所有的IO流操作都需要一个明确的目标
从哪个文件读取数据?
要向哪个文件写入数据?
这就需要File类来指定
1、概述
java.io.File 类
是文件和目录路径名的抽象表示
主要用于文件和目录的创建、查找和删除等操作
①构造方法
先来看一下源码:
package java.io;
public class File implements Serializable, Comparable<File>{
//通过将给定路径名字符串来创建新的 File实例
public File(String pathname) {
if (pathname == null) {
throw new NullPointerException();
}
this.path = fs.normalize(pathname);
this.prefixLength = fs.prefixLength(this.path);
}
//从【父级路径名和子路径名字符串】创建新的 File实例
public File(String parent, String child) {
//省略...
}
//用【父级对象和子路径】创建新的 File实例
public File(File parent, String child) {
//省略...
}
//省略...
}
②实例对象
简单尝试一下:
import java.io.File;
public class Test{
public static void main(String[] args){
String pathName = "e:/vscode-java/day28/newFile.txt";
String parent = "e:/vscode-java/day28";
String child = "newFile.txt";
//路径和文件名创建文件对象
File f1 = new File(pathName);
//父级路径和子文件名创建文件对象
File f2 = new File(parent, child);
//父级文件对象和子路径创建文件对象
File parentFile = new File(parent);
File f3 = new File(parentFile, child);
}
}
一个 File 对象代表硬盘中实际存在的一个文件或者目录
无论该路径下是否存在文件或者目录,都不影响 File 对象的创建
2、使用
①查看名称、路径、长度
//File绝对路径名字符串
public String getAbsolutePath();
//File文件构造路径
public String getPath();
//File文件或目录的名称
public String getName();
//File文件或目录的长度
public long length();
实操一下看看情况:
先来看文件:
此时假设文件 newFile.txt 中的内容为“hello world”
package day28.file
import java.io.File;
public class Test {
public static void main(String[] args) {
String pathName = "e:/vscode-java/day28/newFile.txt";
String parent = "e:/vscode-java/day28";
String child = "newFile.txt";
//【针对文件】
//路径和文件名创建文件对象
File f1 = new File(pathName);
//父级路径和子文件名创建文件对象
File f2 = new File(parent, child);
//父级文件对象和子路径创建文件对象
File parentFile = new File(parent);
File f3 = new File(parentFile, child);
//返回文件的名称
System.out.println("fi.name:" + f1.getName());
//返回文件的构造路径
System.out.println("fi.path:" + f1.getPath());
//返回文件的绝对路径名字符串
System.out.println("fi.absolutePath:" + f1.getAbsolutePath());
//返回文件的字节数
System.out.println("f1.length:" + f1.length());
System.out.println("-----------------");
//对比 getPath 和 getAbsolutePath
File f4 = new File("e:/vscode-java/day28/newFile.txt");
File f5 = new File("newFile.txt");
//返回文件构造路径
System.out.println("f4.path:" + f4.getPath());
System.out.println("f5.path:" + f5.getPath());
//jvm会根据当前的工作目录来确定文件的绝对路径
System.out.println("f4.absolutePath:" + f4.getAbsolutePath());
System.out.println("f5.absolutePath:" + f5.getAbsolutePath());
}
}

这里注意:
在了解 getPath() 和 getAbsolutePath() 区别前
首先要了解一个java运行的机制
虽然我们编译出了 Test.class 文件
我们下意识认为这就是它的全名了
但是java中会认为它的全名是包含包名的
此时也就是 “ day28.file.Test.class ”
所以运行的时候
要去包名的根目录输入运行命令 “ java day28.file.Test ”
此时的工作目录是:e:/vscode-java
那么这个时候就可以回去看这句话了:
jvm会根据当前的工作目录来确定文件的绝对路径
我们发现,这样生成的绝对路径,好像并不是正确的绝对路径
期望路径: e:/vscode-java/day28/newFile.txt
实际路径: e:/vscode-java/newFile.txt
恭喜你,发现了一个常见的 getAbsolutePath() 使用陷阱!
解决办法也很简单:
1、使用相对路径指定完整目录结构
// 正确方式:指定相对路径包含目录 File correctFile = new File("day28/newFile.txt"); System.out.println("正确路径: " + correctFile.getAbsolutePath()); // 输出: e:\vscode-java\day28\newFile.txt2、使用完整绝对路径(推荐)
// 最可靠方式:使用完整绝对路径 File absoluteFile = new File("e:/vscode-java/day28/newFile.txt"); System.out.println("绝对路径: " + absoluteFile.getAbsolutePath()); // 输出: e:\vscode-java\day28\newFile.txt3、动态构建路径
// 根据当前工作目录动态构建正确路径 String currentDir = System.getProperty("user.dir"); File correctFile = new File(currentDir, "day28/newFile.txt"); System.out.println("动态构建路径: " + correctFile.getAbsolutePath());
目录大差不差
大家自己尝试即可
需要注意的就是:
如果File对象是目录时
调用length()方法
那么返回值为0
原因:
public long length()
作用:返回由此抽象路径名表示的文件的长度(字节为单位)
返回值:文件大小(字节),如果文件不存在或者发生IO错误则返回 0L
如果是目录则没有指定返回值,也返回0
②判断、创建和删除操作
//判断文件或目录是否存在
public boolean exists();
//判断是否是文件
public boolean isFile();
//判断是否是目录
public boolean isDirectory();
//当且仅当具有该名称的文件尚不存在时,创建一个新的空文件
public boolean createNewFile();
//创建目录
public boolean mkdir();
//创建多级目录
public boolean mkdirs();
//文件或目录的删除
public boolean delete();
实操看一下效果:
package day28.file;
import java.io.File;
import java.io.IOException;
public class Test {
public static void main(String[] args) {
String parent = "e:/vscode-java/day28";
File parentFile = new File(parent);
File f4 = new File("e:/vscode-java/day28/newFile.txt");
File f5 = new File("newFile.txt");
//判断文件是否存在
System.out.println("f4.exists:" + f4.exists());
System.out.println("f5.exists:" + f5.exists());
System.out.println("-----------------");
//判断是文件还是目录
System.out.println("f4.isFile:" + f4.isFile());
System.out.println("f5.isFile:" + f5.isFile());
System.out.println("f4.isDirectory:" + parentFile.isDirectory());
System.out.println("f5.isDirectory:" + f5.isDirectory());
System.out.println("-----------------");
File f6 = new File("Test.txt");
//创建文件,注意异常处理
try {
System.out.println("f6.createNewFile:" + f6.createNewFile());
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("-----------------");
//删除文件
System.out.println("f6.delete:" + f6.delete());
System.out.println("-----------------");
File f7 = new File("newDir");
//创建目录
System.out.println("f7.mkdir:" + f7.mkdir());
//创建多级目录,会自动创建不存在的父目录
File f8 = new File("newDir/newDir");
System.out.println("f8.mkdirs:" + f8.mkdirs());
System.out.println("-----------------");
//删除目录,只能删除空目录
//虽然指向完整目录,但是删除的时候只能删最下面一层的空目录
System.out.println("f8.delete:" + f8.delete());
System.out.println("f7.delete:" + f7.delete());
}
}

这里要注意的点就是:
1、创建文件的时候由于源码抛出了异常,所以记得异常处理
2、删除操作时,虽然一次性指向了整个目录,但是只会删除最底端的一个空目录
目录必须为空才可以删除
③目录遍历操作
package java.io;
public class File implements Serializable, Comparable<File>{
//省略...
//目录文件调用该方法,获取目录中所有子文件名,返回String数组
//其他文件调用该方法,返回null
public String[] list();
//目录文件调用该方法,获取目录中所有子文件,返回File数组
//其他文件调用该方法,返回null
public File[] listFiles();
//目录文件调用该方法,获取目录中符合筛选条件的子文件,返回File数组
//其他文件调用该方法,返回null
public File[] listFiles(FileFilter filter);
//省略...
}
案例:
准备目录 e:/vscode-java/test,放入各类文件,并对其遍历:
import java.io.File;
import java.io.IOException;
public class Test{
public static void main(String[] args){
File f9 = new File("test");
File[] files = f9.listFiles();
for (File file : files) {
System.out.println(file.getName());
}
File f10 = new File("test");
f10.mkdir();
//放入各类文件
File f11 = new File("test/13-类加载、反射");
File f12 = new File("test/01-java基础入门.pdf");
File f13 = new File("test/01-java基础入门思路.mp4");
File f14 = new File("test/11-File、IO流.pdf");
try {
//创建目录
f11.mkdir();
//创建文件
f12.createNewFile();
f13.createNewFile();
f14.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}

二、IO流
1、流的概念
老生常谈的话题,计算机中的概念基本都是抽象的
流就像日常生活中的水
只不过它是以二进制的形式在程序与设备之间流动传输
这里的设备可以是文件、网络、内存等
流具有方向性,分为输入和输出
这里是以Java程序为参照点的
以文件为例:
数据从程序“流向”文件——输出流
数据从文件“流向”程序——输入流
2、流的分类
①按数据流向
- 输入流(InputStream):数据从其他设备上读取到程序中的流
- 输出流(OutputStream):数据从程序中写出到其他设备上的流
②按数据类型
- 字节流:以字节为单位(byte),读写数据的流,父类 InputStream 和 OutputStream
- 字符流:以字符为单位(char),读写数据的流,父类 Reader 和 Writer

几乎所有的流,都是派生自这四个抽象的父类
- InputStream:字节输入流类型
- OutputStream:字节输出流类型
- Reader:字符输入流类型
- Writer:字符输出流类型
③按功能
- 节点流(原始流)
- 增强流(包装流)
节点流是最基本的 IO流,直接与数据源或目标进行交互,缺乏一些高级功能
增强流在节点流基础上提供了额外的功能和操作
本篇我们主要介绍节点流,下一篇介绍增强流
3、字节流
一切文件数据(文本、图片、视频等)都是以二进制数字的形式进行存储、传输
所以字节流可以传输任意文件数据
使用流操作数据基本步骤:
1、声明流
2、创建流
3、使用流
4、关闭流
InputStream 和 OutputStream 有很多子类
我们先介绍最简单的:
⑴FileInputStream——文件输入流
用于从文件中读取字节数据的
源码(构造方法和 read()方法):
package java.io;
public class FileInputStream extends InputStream{
//省略...
//构造方法
//通过File对象来创建一个 FileInputStream
public FileInputStream(File file) throws FileNotFoundException;
//通过文件路径名(字符串)实例化FileInputStream对象
public FileInputStream(String name) throws FileNotFoundException;
//read()方法
//逐个字节读取,返回值为读取的单个字节
public int read() throws IOException;
//小数组读取,将结果存入数组,返回值为读取的字节个数
public int read(byte b[]) throws IOException;
//小数组读取,存入数组指定位置,返回值为读取的字节个数
public int read(byte b[], int off, int len) throws IOException;
//省略...
}
注意,它们都会抛出异常,我们需要异常处理
下面示例为了可读性,我们选择继续抛出异常
①int read()
调用者:FileInputStream对象
参数:无
返回值:读取的字节值(0-255),如果到达文件末尾返回 -1
作用:读取单个字节
注意事项:每次调用只能读取一个字节,效率较低
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException, FileNotFoundException {
String path = "newFile.txt";
File file = new File(path);
//判断文件是否存在,不存在就新建一个
if(file.exists()){
System.out.println("文件存在");
}else{
System.out.println("文件不存在");
file.createNewFile();
}
//通过文件对象创建流对象
InputStream is = new FileInputStream(file);
//读取文件内容
int r;
while((r = is.read()) != -1){
System.out.print((char)r);
}
is.close();
}
}
注意:read()的调用需要放在循环里,否则只会读取一个字节
②int read(byte[] b)
调用者:FileInputStream对象
参数:byte[] b ——用于存储读取数据的字节数组
返回值:实际读取的字节个数,如果到达文件末尾返回 -1
作用:批量读取多个字节到数组中
public static void main(String[] args) throws IOException {
//1.创建流对象【IO流对象 跟 文件进行关联】
InputStream is = new FileInputStream("D:\\test\\a.txt");
System.out.println("is: " + is);
//2.读取文件内容
// int read(byte[] arr);
// 读多个字节,放入arr数组,返回成功读取字节数目,
// 如果到文件末尾,则返回-1
byte[] arr = new byte[10];
int len = is.read(arr);
System.out.println("成功读取字节数目:" + len);
//遍历数组有效内容
for(int i = 0; i < len; i++)
System.out.println(arr[i]);
System.out.println("------------");
//再次读取
len = is.read(arr);
System.out.println("第二次读取: " + len); // -1
//3.关闭流对象,释放资源
is.close();
}
//输出结果:
is: java.io.FileInputStream@7852e922
成功读取字节数目:3
97
98
99
------------
第二次读取: -1
③int read(byte[] b, int off, int len)
调用者:FileInputStream对象
参数:byte[] b ——用于存储读取数据的字节数组
int off ——指定在字节数组中开始存储数据的位置
int len ——要读取的最大字节数
返回值:实际读取的字节个数,如果到达文件末尾返回 -1
作用:批量读取多个字节到数组中
public static void main(String[] args) throws Exception {
//1.创建流对象
InputStream is = new FileInputStream("D:\\test\\a.txt");
System.out.println("is: " + is);
//2.读取
// 读取5个字节往arr中 往后偏移3个位置 放入
// 如果读取成功,则返回实际读取长度
// 如果返回-1,则表示读取到文件末尾
byte[] arr = new byte[10];
int len = is.read(arr,3,5); //arr[ , , , a, b, c, ...]
System.out.println("成功读取: " + len);
//遍历数组所有内容
for(int i = 0; i < arr.length; i++)
System.out.print(arr[i] + " ");
//3.关闭资源
is.close();
}
//输出结果:
is: java.io.FileInputStream@7852e922
成功读取: 3
0 0 0 97 98 99 0 0 0 0
⑵FileOutputStream——文件输出流
用于写入字节数据到文件中
源码(构造方法 和 write()方法):
package java.io;
public class FileOutputStream extends OutputStream{
//创建文件输出流以写入由指定的 File对象表示的文件。
public FileOutputStream(File file) throws FileNotFoundException;
//创建文件输出流以指定的名称写入文件
public FileOutputStream(String name) throws FileNotFoundException;
//追加模式,如果第二个参数不写true默认为false,即写入的数据会覆盖原数据
//写上true之后,写入的数据会跟在原数据之后
public FileOutputStream(File file, boolean append) throws FileNotFoundException;
public void write(int b) throws IOException;
public void write(byte b[]) throws IOException;
public void write(byte b[], int off, int len) throws IOException;
//省略...
}
注意:
- 创建一个输出流对象时,传入的文件路径可以不存在,不会抛出异常,系统会自动创建这个文件,但是目录必须存在,系统不会自动创建
- 如果有这个文件,系统默认会清空这个文件的数据
示例1:
提前创建好目录 src/dir,用文件输出流写入字节到 src/dir/a.tx
public class Test_Write {
public static void main(String[] args) throws Exception {
//1.关联流对象和文件
// 实例化输出流时,目标文件a.txt不存在不会抛异常,系统会自动创建
// 但src/dir目录必须存在,系统不会自动创建目录
OutputStream os = new FileOutputStream("src/dir/a.txt");
System.out.println("os: " + os);
//2.写数据
os.write(97); //a
os.write(98); //b
os.write(99); //c
//3.关闭资源
os.close();
}
public static void main02(String[] args) throws Exception {
//1.关联流对象和文件
OutputStream os = new FileOutputStream("src/dir/a.txt");
System.out.println("os: " + os);
//2.写数据
String str = "abcd";
byte[] arr = str.getBytes();
//将arr所有元素全部写入文件
//写入 会 覆盖 文件原有内容
os.write(arr);
//3.关闭资源
os.close();
}
public static void main03(String[] args) throws Exception {
//1.关联流对象和文件
OutputStream os = new FileOutputStream("src/dir/a.txt");
System.out.println("os: " + os);
//2.写数据 '1''2''3''4''5'
byte[] arr = {49,50,51,52,53,54,55};
//os.write(arr,0,arr.length);
//从arr[2]开始,获取arr数组的3个字节,即[51,52,53],然后写入a.txt
//写入 会 覆盖 文件原有内容
os.write(arr,2,3);
// 写出一个换行, 换行符号转成数组写出
os.write("\r\n".getBytes());
//3.关闭资源
os.close();
}
}
示例2:
将A.txt的内容拷贝到B.txt
public class Test_Copy {
public static void main(String[] args) throws Exception {
//1.关联文件和流对象
InputStream is = new FileInputStream("src/dir/A.txt");
OutputStream os = new FileOutputStream("src/dir/B.txt");
//2.拷贝
//2.1 逐个字节拷贝
// int r;
// while((r = is.read()) != -1) {
// os.write(r);
// }
//2.2 小数组拷贝,使用最多
byte[] arr = new byte[8];
int len;
while((len = is.read(arr)) != -1) {
//注意事项:读取多少个字节 就写出多少个字节
os.write(arr,0,len);
}
System.out.println("拷贝完成");
//3.关闭资源
//注意:先关闭后打开的,后关闭先打开的
os.close();
is.close();
}
}
⑶ByteArrayInputStream——内存输入流
使用文件流,我们可以操作文件中的数据
使用内存流,我们可以操作内存中字节数组中的数据
用于读取内存中的字节数组中的数据
源码:
package java.io;
public class ByteArrayInputStream extends InputStream {
protected byte buf[];
protected int pos;
protected int count;
//关键构造器
public ByteArrayInputStream(byte buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
//省略...
}
示例:
1.从键盘录入1行字符串,将其转换为byte[]
2.由byte[]构建一个内存输入流对象
3.从内存输入流中用小数组方式读取数据,并写入到 src\dir\b.txt 文件中
4.关闭流、释放资源
public class Test_ByteArrayInput {
public static void main(String[] args) throws Exception {
//1.实例化sc对象 并录入一行字符串
Scanner sc = new Scanner(System.in);
System.out.println("input line:");
String line = sc.nextLine();
//2.将字符串转换成字节数组
byte[] bytes = line.getBytes();
//由 字节数组 构建 内存输入流对象
InputStream is = new ByteArrayInputStream(bytes);
//创建文件输出流对象
OutputStream os = new FileOutputStream("src/dir/b.txt");
//3.小数组方式读取内存流数据
byte[] arr = new byte[5];
int len;
while((len = is.read(arr)) != -1) {
//4.写入b.txt文件中
os.write(arr, 0, len);
}
System.out.println("文件操作完成!");
//5.关闭流、释放资源
os.close();
// 注意:内存流不需要关闭
}
}
⑷ByteArrayOutputStream——内存输出流
用于把数据写入到内存中的字节数组中
源码:
package java.io;
public class ByteArrayOutputStream extends OutputStream {
//存储数据的数组
protected byte buf[];
//存入字节数组的元素(字节)个数
protected int count;
//无参构造器创建的字节数组输出流,数组大小为32个字节
public ByteArrayOutputStream() {
this(32);
}
//关键方法:获取内存输出流中存储的数据,返回字节数组
public synchronized byte toByteArray()[] {
return Arrays.copyOf(buf, count);
}
//省略...
}
示例:
读取 src/dir/a.txt 文件中的所有内容,写入到字节数组输出流 中,然后从字节输出流中获取所有数据,最后转换成String字符串输出
public class Test_ByteArrayOutput {
public static void main(String[] args) throws IOException {
//1.关联流对象和文件
// 创建内存输出流对象[new byte[32]]
InputStream is = new FileInputStream("src/dir/a.txt");
ByteArrayOutputStream os = new ByteArrayOutputStream();
//2.读取文件内容 然后写入到 内存输出流中
byte[] arr = new byte[8];
int len;
while((len = is.read(arr)) != -1) {
//写入 内存输出流
os.write(arr, 0, len);
}
System.out.println("拷贝完成!");
//3.关键方法:获取内存输出流中的数据
byte[] byteArray = os.toByteArray();
//4.将byte[] --> String 并输出
System.out.println(new String(byteArray));
//5.注意:内存流不需要close()释放资源
}
}
再次强调,内存流使用完不需要 close() 释放资源
内存流的底层数据源是内存中的字节数组
会自动被垃圾回收器回收
4、字符流
本质上字符流底层借助字节流实现的
相比于字节流,字符流提供了更高效方便的字符处理方式
可以直接读写字符,无需进行字节与字符的转换
这使得字符流更适合处理文本数据
Reader 和 Writer 同样有很多子类
我们依旧从最简单的开始:
⑴文件字符流
FileReader——用于读取字符文件
FileWriter——用于写出字符到文件
源码:
package java.io;
public class FileReader extends InputStreamReader {
public FileReader(String fileName) throws FileNotFoundException;
public FileReader(File file) throws FileNotFoundException;
//省略...
}
public class FileWriter extends OutputStreamWriter {
public FileWriter(String fileName) throws IOException;
public FileWriter(String fileName, boolean append) throws IOException;
public FileWriter(File file) throws IOException;
public FileWriter(File file, boolean append) throws IOException;
//省略...
}
示例:
使用文件字符流拷贝a.txt的文件内容到b.txt的文件末尾
public class Test025_FileReaderWriter {
public static void main(String[] args) throws IOException {
// 1.实例化流对象
File file1 = new File("src/dir/a.txt");
File file2 = new File("src/dir/b.txt");
Reader reader = new FileReader(file1);
// 设置文件追加
Writer writer = new FileWriter(file2,true);
// 2.使用流进行文件拷贝
int len = -1;
char[] buf = new char[8];
while ((len = reader.read(buf)) != -1) {
writer.write(buf, 0, len);
}
//刷新流
writer.flush();
// 3.关闭流
writer.close();
reader.close();
}
}
⑵操作字节文件?
示例:
使用文件字符流拷贝图片
仿照前面的案例即可,略微修改文件名即可
会发现拷贝的图片打不开
所以字符流只能操作文本文件
不能操作图片、视频等非文本文件
基础节点流差不多就介绍这些,我们下篇开始介绍增强流

更多推荐








所有评论(0)