IO详解(文件,流对象,一些练习)
文件概念
文件的路径
路径有俩种表示风格
文件类型
如何区分文本文件还是二进制文件?
java对文件的操作
File类中的一些方法
流对象
流对象的简单概念
java标准库的流对象
1.字节流,(操作二进制数据的)
2.字符流 (操作文本数据的)
流对象最核心的四个操作
注意,intput 和 output的方向
读文件(字节流)
read无参数版本
read一个参数版本
理解read的行为和返回值
写文件
关于close
如何确保close被执行到
字符流
读操作
写操作
关于Scanner
练习
用户输入一个查询的词,看看当前目录下(以及子目录里)是否有匹配的结果,如果有匹配结果,就询问用户是否删除
把一个文件拷贝成另一个文件(把第一个文件按照字节读取,把结果写入到另一个文件中)
文件
文件概念
狭义的文件:指的是硬盘上的 文件 和 目录(文件夹)
广义的文件:泛指计算机中的很多 软硬件资源
操作系统中,把很多的硬件设备和软件资源抽象成了文件,按照文件的方式来统一管理.
例如:在网络编程中的网卡,操作系统是把网卡当作一个文件来进行操作的
文件的路径
之前学习的代码中,存储数据,主要靠变量,变量是在内存中的
现在学习的文件,则是在硬盘上
每个文件,在硬盘上都有一个具体的路径
路径有俩种表示风格
1.绝对路径:以c:d:盘符开头的路径
2.相对路径:以当前所在目录为基准,以.或..开头(.有时可以省略),找到指定路径
当前所在目录:称为工作目录,每个程序运行的时候,都有一个工作目录(在控制台里通过命令操作较为明显)
假定,当前的工作目录是,d:/tmp
tmp目录下有111这个目录,定位到111这个目录,就可以表示成
./111(./就表示的是当前的目录 d:/tmp)
../表示当前目录的上级目录,如果工作目录是d:/tmp/222 想要定位到111这个目录,相对路径写作.
../111(..表示当前目录d:/tmp/222 的上级目录 d:/tmp)
文件类型
如何区分文本文件还是二进制文件?
直接使用 记事本 打开,如果乱码了,说明是二进制文件,如果没乱,就是文本文件
java对文件的操作
1.针对文件系统操作(文件的创建,删除,重命名)
2.针对文件内容操作(文件读和写)
java标准库提供了File这个类
在new File 对象的时候,构造方法参数中,可以指定一个路径.此时File对象代表这个路径对应的文件
File类中的一些方法
方法
代码
public static void main(String[] args) throws IOException {File file = new File("./test.txt");System.out.println(file.getName());System.out.println(file.getParent());System.out.println(file.getAbsoluteFile());System.out.println(file.getPath());System.out.println(file.getCanonicalFile());}
运行结果
方法
代码
public static void main(String[] args) throws IOException {File file = new File("D:/test.txt");file.createNewFile();System.out.println(file.exists());System.out.println(file.isFile());System.out.println(file.isDirectory());}
运行结果
方法
代码
public static void main(String[] args) throws IOException {File file = new File("D:/test.txt");System.out.println(file.exists());file.createNewFile();System.out.println(file.exists());file.delete();System.out.println(file.exists());}
运行结果
方法
代码
public static void main(String[] args) {File file = new File("./test");file.mkdir();}
public static void main(String[] args) {File file = new File("./test/aaa/bbb");file.mkdirs();}
运行结果
流对象
流对象的简单概念
针对文件内容,使用"流对象" 进行操作,从文件中读100个字节,我们可以一次读100个字节,一次读完
也可以一次读20个字节,5次读完,我们可以随心所欲的读
java标准库的流对象
从类型上分为俩大类
1.字节流,(操作二进制数据的)
InputStream FileInputStream
OutputStream FileOutputStream
2.字符流 (操作文本数据的)
Reader FileReader
Writer FileWriter
流对象最核心的四个操作
这些类的使用方式是固定的,核心就是四个操作
1.打开文件(构造对象)
2.读写文件(read) ==>针对的是 InputStream/Reader
3.写文件(writer) ==> 针对OutputStream/Writer
4.关闭文件(close)
注意,intput 和 output的方向
我们是以CPU为中心,来看待这个方向的
数据朝着CPU的方向流向,就是输入,所以把从硬盘中读取数据到内存 这个过程称为读 input
数据远离CPU的方向流向,就是输入,所以把从内存中写数据到硬盘 这个过程称为写 output
读文件(字节流)
read无参数版本
代码
public static void main(String[] args) throws IOException {
//创建 InputStream 对象的时候,使用绝对路径或者相对路径,都可以,也可以使用File对象InputStream inputStream = new FileInputStream("D:/test.txt");
//进行读操作while (true){int b = inputStream.read();if(b == -1){break;}System.out.println(""+(byte)b);}inputStream.close();
}
运行结果
D盘中test的文件内容
read一个参数版本
public static void main(String[] args) throws IOException {//创建 InputStream 对象的时候,使用绝对路径或者相对路径,都可以,也可以使用File对象InputStream inputStream = new FileInputStream("D:/test.txt");while(true){byte[] buffer = new byte[1024];int len = inputStream.read(buffer);System.out.println("len "+len);if(len == -1){break;}for (int i = 0; i < len; i++) {System.out.println(""+buffer[i]);}}inputStream.close();}
运行结果
理解read的行为和返回值
上面这里给的数组长度是1024,read就会尽可能读取1024个字节,填到数组里.但实际上,文件剩余长度是有限的,如果剩余长度超过1024,此时1024个字节就会填满,返回值是1024,如果当前剩余的长度不足1024,此时有多少就填多少,read方法就会返回当前实际读取的长度
第二个版本的代码有什么好处?
buffer存在的意义,是为了提高IO效率,单次IO操作,是要访问硬盘IO设备,单次操作是比较消耗时间的
如果频繁进行这样的IO操作,耗时比较大
单次IO时间是一定的,如果能缩短IO次数,此时就可以提高程序的整体效率了
第一个版本的代码,是一次读取一个字节,循环次数比较高,read次数很高,读取IO次数也很高
第二个版本的代码,是一次读取1024个字节,循环次数降低了很多,read次数变少了
写文件
代码
public static void main(String[] args) throws IOException {OutputStream outputStream = new FileOutputStream("D:/test.txt")outputStream.write(97);outputStream.write(98);outputStream.write(99);outputStream.write(100);outputStream.close();}
运行结果
对于OutputStream 来说,默认情况下,打开一个文件,会先清空文件原有的内容(这样,之前的"hello"就被清空了)
如果不想清空,流对象还提供了一个"追加写"对象,通过这个就可以实现不清空文件,把新内容追加到后面.
关于close
对于上述代码中的outputStream.close();这里的close操作,含义是关闭文件
一个线程对于一个PCB,一个进程对应1个或多个PCB
PCB有一个重要的属性,文件描述符表(相当于一个数组),记录了该进程打开了哪些文件.(即使一个进程有多个线程多个PCB也没关系,这些PCB共用一个文件描述符表)
如果没有close,对应的表项,没有及时释放.虽然 java有GC,GC操作会在回收这个outputStream对象的时候完成释放操作,但是这个GC不一定及时...
所以,如果不能手动释放,意味着文件描述符表可能很快就被占满了(文件描述符表这个数组,不能自动扩容,有上限)
如果占满了后,再次打开文件,就会打开失败
close 一般来说是要执行的,但是如果一个程序,有一些文件对象自始至终都要使用,也可以不用关闭
随着进程结束,PCB销毁了,文件描述符表也就销毁了,对应的资源操作系统就自动回收了,因此,如果一个文件close之后,程序就立即结束了,此时也可以省略close
有时我们可能会发现,写文件的内容没有真正在文件中出现,很大可能是因为缓存区,
写操作其实是,先写到缓冲区里(缓冲区有很多种形态,自己写的代码里可以有缓冲区,标准库里也可以有缓冲区,操作系统内核里也可以有缓冲区)
写操作执行完了,内容可能在缓冲区,还没有真正进入硬盘,close操作 就会触发缓冲区的刷新(刷新操作就是把缓冲区的内容写到硬盘里)
除了close之外,还可以通过flush方法刷新缓冲区(此时文件不会立即关闭)
如何确保close被执行到
刚刚的代码可以改成这样
public static void main(String[] args) throws IOException {try(OutputStream outputStream = new FileOutputStream("D:/test.txt")) {outputStream.write(97);outputStream.write(98);outputStream.write(99);outputStream.write(100);}}
这是更推荐的写法,这个写法虽然没有显示的写 close,实际上会执行的,只要try语句块执行完毕,就可以自动执行到close
这个语法,在java中被称为try with resources ,当然不是随便拿一个对象放在try()里就能自动释放,必须要这个对象实现了Closeable接口
实现了这个Closeable接口的类才可以放到try()中被自动关闭,这个接口提供的方法就是close方法
字符流
读操作
代码
public static void main(String[] args) throws IOException {try(Reader reader = new FileReader("D:/test.txt")){while(true){int ch = reader.read();if(ch == -1){break;}System.out.println((char)ch+"");}}}
运行结果
写操作
public static void main(String[] args) throws IOException {try(Writer writer = new FileWriter("D:/test.txt")){writer.write("hello world");}}
运行结果
关于Scanner
Scanner是搭配流对象来使用的
代码
public static void main(String[] args) throws IOException {try(InputStream inputStream = new FileInputStream("D:test.txt")) {//此时读取的内容就是从 文件 中读取了Scanner scanner = new Scanner(inputStream);scanner.next();}}
练习
用户输入一个查询的词,看看当前目录下(以及子目录里)是否有匹配的结果,如果有匹配结果,就询问用户是否删除
代码
import java.io.File;
import java.util.Scanner;public class IODemo10 {static Scanner scanner = new Scanner(System.in);public static void main(String[] args) {//让用户搜索一个指定搜索的目录System.out.println("请输入要搜索的路径: ");String basePath = scanner.next();//针对用户输入进行简单判定File root = new File(basePath);if(!root.isDirectory()){//路径不存在,或者只是一个普通的文件,此时无法进行搜索System.out.println("输入的目录有误!");return;}//再让用户输入一个要删除的文件名System.out.println("请输入要删除的文件");//此处要使用next,而不能使用nextLineString nameToDelete = scanner.next();//针对指定的路径进行扫描,递归操作//先从根目录出发//判定一下,当前的这个目录里,是否包含我们所需要删除的目录.如果是则删除,否则跳过下一个//如果当前目录里包含一些目录,再针对子目录进行递归scanDir(root,nameToDelete);}private static void scanDir(File root, String nameToDelete) {//1.先列出 root 下的文件和目录File[] files = root.listFiles();if(files == null){//当前root 目录下没有东西,是一个空目录//结束继续递归return;}//2.遍历当前列出的结果for(File x : files){if(x.isDirectory()){//如果是目录,就进一步递归scanDir(x,nameToDelete);}else{//如果是普通文件,则判定是否要删除if(x.getName().contains(nameToDelete)){System.out.println("确实是否要删除: "+x.getAbsolutePath()+"嘛");String choice = scanner.next();if(choice.equals("y")||choice.equals("Y")){x.delete();System.out.println("删除成功");}else{System.out.println("删除取消");}}}}}
}
运行结果
把一个文件拷贝成另一个文件(把第一个文件按照字节读取,把结果写入到另一个文件中)
代码
import java.io.*;
import java.util.Scanner;public class IODemo11 {public static void main(String[] args) throws IOException {//输入俩个路径//源 和 目标(从哪里,拷贝到哪里)Scanner scanner = new Scanner(System.in);System.out.println("请输入要拷贝哪个文件: ");String srcPath = scanner.next();System.out.println("请输入要拷贝到哪个地方: ");String destPath = scanner.next();File srcFile = new File(srcPath);if(!srcFile.isFile()){//如果源路径不是一个文件(是一个目录,或者不存在)//此时不做任何操作System.out.println("输入的源路径有误");return;}File destFile = new File(destPath);if(destFile.isFile()){//如果目标路径已经存在,认为不能拷贝System.out.println("当前输入的目标路径有误");return;}//进行拷贝操作try(InputStream inputStream = new FileInputStream(srcFile);OutputStream outputStream = new FileOutputStream(destFile)){//进行读文件操作while(true){int b = inputStream.read();if(b == -1){break;}//进行写操作outputStream.write(b);}}}
}