当前位置: 首页 > news >正文

文件操作详解-IO

 

目录

1.认识文件

2.文件的类型

3.java对文件的操作

针对文件系统操作

针对文件内容操作

字节流

字符流

字节流的使用 

字符流的使用

4.文件IO小程序练习

示例1

示例2


1.认识文件

狭义的文件指的是硬盘上的文件和目录

广义的文件泛指计算机中的很多的软硬件资源,操作系统中,把很多的硬件设备和软件资源抽象成了文件,按照文件的方式来统一管理

这里我们只讨论狭义的文件,就是硬盘上的数据

代码中存储数据是靠变量,变量是存储在内存中的,现在的文件是存储在硬盘上的

树形结构组织和目录

文件很多的情况下,在对文件进行管理的时候,采用了我们学过的数据结构--树形结构.

文件夹(folder)和目录(directory)中保存的就是我们所说的关于文件的元信息,通过一个个文件夹将文件组织起来,方便使用

文件路径

绝对路径

每个文件在硬盘上都有一个具体的"路径",从树型结构的角度来看,树的每个节点都可以从一条根开始,一直到达的节点的路径所描述,这种描述方式就是文件的绝对路径(absolute path)

表示一个文件的具体位置路径,就可以使用 /(斜杠)来分割不同的目录级别

我们查看.png文件的路径

这里的路径是用\来分割的路径

(/)斜杠和(\)反斜杠有什么区别呢

 建议使用斜杠来分割目录级别.String path = "d:\epic",此时\c就会被当成转义字符了,就不是\本身了,得写成"d:\\epic",如果写正斜杠,就不会出现问题,并且各种系统都支持.

 

可以用D:/Epic Games来描述位置,能识别出来,但是我们查看路径是反斜杠

 CDE这样的盘符是通过"硬盘分区"来的,每个盘符可以是一个单独的硬盘,也可以是若干个盘符对应一个硬盘

文件的相对路径:以当前所在的目录为基准,以.或者..开头(.有时候省略),找到指定的路径

当前所在的目录称为工作目录,每个程序运行的时候,都有一个工作目录,(在控制台里通过命令操作的时候是很明显的)

我们在命令行下直接输入某个程序的名字,本质上是操作系统去PATH环境变量里查找的.calc本身就在PATH下,可有直接运行,我们自己装的程序,需要把路径加到PATH中后就可以运行了

 

现在我们都是使用图形界面,工作目录就不太直观了

 这就是默认的工作路径

 切换成D盘为工作路径

 相对路径就是由工作路径为基准

 

我们假设当前的工作目录为D:/tmp,那么定位到aaa这个目录,就可以表示为./aaa(./就表示当前的目录),定位到bbb就是./bbb

如果工作目录不同,定位到同一个文件,相对路径写法是不同的

同样定位到aaa

如果工作目录是D:/,相对路径写作./tmp/aaa

如果工作目录是D:/tmp,相对路径写作./aaa

如果工作目录是D:/tmp/bbb,相对路径写作../aaa(..表示当前目录的上级目录)

Linux没有盘符的概念,统一使用cd切换.windows需要先定位到盘符,再cd在当前盘符下切换

我们使用的IDEA的工作路径就是当前项目所在的目录,如果代码中写了一些相对路径的代码,工作路径就是以项目路径为基准的

2.文件的类型

不同的文件整体可以归类为两类:

文本文件

存的是文本,字符串,字符串是由字符构成的,每个字符都是通过一个数字来表示的,这个文本文件里存储的数据,一定是合法字符,都是在指定字符编码的码表之内的数据

二进制文件

没有任何限制,可以存储任何数据

给一个文件,如何区分是文本还是二进制文件呢?

可以使用记事本打开,如果是乱码,就是二进制文件,没乱就是文本文件,因为记事本是默认文本打开的

实际写代码中,这两类文件的处理方式略有差别

3.java对文件的操作

针对文件系统操作

Java提供了一个File类

属性

修饰符及类型属性说明
static StringpathSeparator依赖于系统的路径分隔符,String 类型的表示
static charpathSeparator依赖于系统的路径分隔符,char 类型的表示

pathSeparator是File中的一个静态变量,就是相当于/或者\,用来分割的,方便多平台使用

构造方法

签名说明
File(File parent,String child)根据父目录+孩子文件路径创建一个新的 File 实例
File(String pathname)根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者 相对路径
File(String parent, String child)根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用 路径表示

这里传入绝对路径或者相对路径都可以

parent表示当前文件所在目录,child表示自身的文件名

例如D:/tmp

parent:D:/

child:tmp

第二个比较常用File(String pathname),直接传入路径创建文件

方法

修饰符及返回 值类型方法签名说明
StringgetParent()返回 File 对象的父目录文件路径
StringgetName()返回 FIle 对象的纯文件名称
StringgetPath()返回 File 对象的文件路径
StringgetAbsolutePath()返回 File 对象的绝对路径
StringgetCanonicalPath()返回 File 对象的修饰过的绝对路径
booleanexists()判断 File 对象描述的文件是否真实存在
booleanisDirectory()判断 File 对象代表的文件是否是一个目录
booleanisFile()判断 File 对象代表的文件是否是一个普通文件
booleancreateNewFile()根据 File 对象,自动创建一个空文件。成功创建后返 回 true
booleandelete()根据 File 对象,删除该文件。成功删除后返回 true
voiddeleteOnExit()根据 File 对象,标注文件将被删除,删除动作会到 JVM 运行结束时才会进行
String[]list()返回 File 对象代表的目录下的所有文件名
File[]listFiles()返回 File 对象代表的目录下的所有文件,以 File 对象 表示
booleanmkdir()创建 File 对象代表的目录
booleanmkdirs()创建 File 对象代表的目录,如果必要,会创建中间目 录
booleanrenameTo(File dest)进行文件改名,也可以视为我们平时的剪切、粘贴操 作
booleancanRead()判断用户是否对文件有可读权限
booleancanWrite()判断用户是否对文件有可写权限

我们来看几组方法的使用:

File file = new File("d:/test.txt");

构造的这个文件对象的路径不一定要真实存在,不存在,file会有相关方法创建出来(createNewFile)

 

 public static void main(String[] args) throws IOException {
        File file = new File("d:/test.txt");
        System.out.println(file.getName());
        System.out.println(file.getParent());
        System.out.println(file.getPath());
        System.out.println(file.getAbsolutePath());
        System.out.println(file.getCanonicalPath());
    }

 

这里看不出什么差异

将d:/换成./,就不同了.因为./是相对路径,这里的工作路径就是我们IDEA中项目的路径

看结果

我们可以看出来绝对路径是在相对路径的基础上拼接了工作路径

getCanonicalPath()得到的是化简后的绝对路径,./省略了

public class IO2 {
    public static void main(String[] args) {
        File file = new File("d:/test.txt");
        //文件是否存在
        System.out.println(file.exists());
        //是否是文件
        System.out.println(file.isFile());
        //判断 File 对象代表的文件是否是一个目录
        System.out.println(file.isDirectory());
    }
}

我们在D盘下创建test文件后运行程序

将d:/换成./,就不同了.因为该项目路径下没有这个文件,既不是文件也没有目录

 但是我们可以创建文件,创建文件后执行程序

 

 在项目目录下也能找到我们创建的文件

 我们使用方法删除test文件

public class IO3 {
    public static void main(String[] args) {
        File file = new File("./test.txt");
        file.delete();
    }
}

删除成功 

deleteOnExit()这个方法是程序退出后删除

适用于临时文件,程序退出后就删除了

public class IO4 {
    public static void main(String[] args) {
        File file = new File("./test");
        //创建目录
        file.mkdir();
        //创建多级目录
        //file.mkdirs();
    }
}

 

 file.mkdirs()创建多级目录,mkdir只能创建一级目录

public class IO5 {
    public static void main(String[] args) {
        File file = new File("./test");
        File dest = new File("./testAA");
        file.renameTo(dest);
    }
}

 

 renameTo更改了文件名

针对文件内容操作

针对文件内容,我们使用"流对象"进行操作的,这个比喻并非是Java独有的,操作系统api就是这样设定的,进一步的各种编程语言,操作文件也继承了这个概念

Java标准库的流对象从类型上分为两类,每个类又有几个不同的功能的类

字节流

字节流是操作二进制文件的

InputStream 

FileInputStream

OutputStream  

FileOutputStream 

字符流

字符流是操作文本文件的

Reader   FileReader   

Writer   FileWriter 

这些类的使用方式非常固定

核心有四个操作

1.打开文件(构造对象)

2.关闭文件(close)

3.读文件(read)针对InputStream /Reader 

4.写文件(writer)针对OutputStream  /Writer 

字节流的使用 

我们来看这个代码,使用字节流读取文本文件:

public class IO6 {
    public static void main(String[] args) throws IOException {
        //绝对路径,相对路径,File对象都可以
        //FileNotFoundException是IOException的子类,如果想要打开一个文件去读
        //但是必须保证这个文件是存在的,否则会出异常
        InputStream inputStream = new FileInputStream("d:test.txt");
        //进行操作
        while(true){

            int b = inputStream.read();
            if(b == -1){
                //读取完毕
                break;
            }
            System.out.println(""+(byte)b);
        }
        inputStream.close();
    }
}

 

 InputStream /OutputStream  /Reader/Writer 这几个类都是抽象类,不能直接实例化

 read有三个版本,无参版本一次读一个字节.一个参数版本:把读到的内容填充到参数的这个字节数组中(此处的参数是输出型参数,返回值是实际读取的字节数)

三个参数版本:和一个参数版本类似,只不过往数组的一部分区间里尽可能填充

返回值为int, read读的是一个字节,返回一个byte就可以,但是实际上返回的是int,除了要表示byte的0~255(-128~127)这样的情况,还要表示一个特殊情况,-1表示读取文件结束了(读到文件末尾了),但是byte就没有多余空间表示这个非法值了,所以需要用int作为返回值,拿到的int可以强转byte

运行程序,结果

 可以看到读的每个字节都是数字,这些数字就是美国信息交换标准代码(ASCII),对照后就是hello .

如果将test中内容改为中文

 这些数字就表示"你好"这两个字的编码方式,如果按十六进制打印,

这里使用的是utf8的,对照utf8码表

 

 这里的txt文件是文本文件,使用字节流也能读,但是不方便,更希望使用字符流读,就更方便了

上述使用的是read的第一个版本,无参的.接下来使用一个参数的版本

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.printf("%x\n",buffer[i]);
            }
        }

 read的第二个版本需要提前准备一个数组,传参操作就相当于把刚才准备好的数组交给read来填充,此处参数相当于"输出型参数",Java中习惯使用输入的信息作为参数,输出的信息作为返回值,但是也有少数情况,使用参数来返回内容.

要注意理解read的行为和返回值.read会尽可能的把传进来的数组给填满,上面给到的数组是1024,read就会尽可能的读取1024个字节,如果文件的剩余长度超过1024,此时1024个字节都会被填满,返回值就是1024,如果当前的文件不足1024,那么读取多少就会返回多少.read方法就是返回当前实际读取的长度.

如果超过1024了,就会循环下一轮继续读,我们把test文件中多放点内容然后运行程序

 

 整个文件是13k,因此需要多次循环read,由于数组长度是1024,所以前面每次read读到的长度都是1024,最后一轮只剩122字节了.数据不足1024了,实际能读到多少字节,就读多少字节,不同的文件最后一次剩余的也不同,读完122就是文件末尾了,再下一轮循环,没有内容可读,返回-1,循环结束.

文件是在磁盘上的,有的文件内容可能会很多,甚至超出内存的上限,有时想把整个文件读取到内存在处理不一定行,一般都是一边读一边处理,处理好了一部分,再处理下一个部分..

再来看一个问题

byte[] buffer

我们为啥起名叫buffer

buffer叫做"缓冲区",存在的意义就是提高IO的效率,单次IO操作,是要访问硬盘/IO设备,单次操作是比较消耗时间的,如果频繁的IO操作,耗时肯定就多了.

单次IO时间是一定的,如果能缩短IO的次数,此时就可以提高程序整体的效率了,第一个版本的代码是一次读一个字节,循环次数比较多,read次数也多,第二个版本是一次读1024个字节,循环次数降低很多,read次数也变少了..缓冲区就是缓和了冲突,减小访问的次数.

使用了InputStream来读文件 ,还可以使用OutputStream 写文件.

写方法有三个版本,第一个版本是一次写一个字节,范围是0~255,和read是相同的.

第二个版本是准备一个数组,然后一次写到数组中去

第三个版本是从数组的off下标开始写到len结束

public class IO7 {
    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();
    }
}

执行程序后打开文件

我们之前写进去的内容已经被清空了,然后写入了abcd

对于OutputStream默认情况下,打开一个文件,会先清空文件原有的内容,这样的话,之前我们放进去的内容就没有了

如果不希望清空,流对象还提供了一个"追加写"对象,通过这个对象可以不清空文件,把新内容追加到后面

还要注意:input和output的方向,input不应该是输入的意思吗,那应该对应的是写文件啊,为什么是读文件呢..

这里input和output是基于CPU的方向

内存更接近CPU,硬盘离CPU更远,CPU是一台计算机最核心的部分,相当于人的大脑

以CPU为中心,朝着CPU的方向流向,就是输入.所以把数据从硬盘到内存这个过程称为读,input

数据远离CPU方向流向,就是输出,所以把数据从CPU到硬盘这个过程称为写,output.

outputStream.close();

这里的close操作,含义是关闭文件

之前所说的进程,在内核里,使用PCB这样的数据结构来表示进程,一个线程对应一个PCB,一个进程可以对应一个或多个PCB

PCB中有一个重要的属性,文件描述符表(相当于一个数组)记录了该进程打开了哪些文件

即使一个进程有多个线程,多个PCB,这些PCB还是共用一个文件描述符表

 文件描述符表中的每个元素,都是内核里的一个file_struct对象,这个对象就表示一个打开了的文件

每次打开文件操作,就会在文件描述符表中申请一个位置,把这个信息记录进去,每次关闭文件,就会把这个文件描述符表对应的表项给释放掉

outputStream.close();起到的作用就是释放对应的表项,如果这个代码没写或者没执行到

那么文件描述符表中的表项就没有及时释放,GC操作会在回收这个outputStream对象的时候去完成这个释放操作,虽然GC会完成操作,但是不一定及时释放,因此如果不手动释放,当使用文件很多的情况下,文件描述符表会很快被占满,这个数组是不能扩容,存在上限的,如果被占满了,再次打开文件,就会打开失败!!文件描述符表最大长度对于不同的系统不太一样,但是基本都是几百至几千左右

close一般来说是要执行的,但是如果一个程序,这里的文件自始至终都要用,不关闭问题也不大,因为程序一结束,整个进程都会结束,对应的资源都会被操作系统回收了,也就是说,如果close之后程序就结束了,那么不手动释放资源也没事!

当然我们一般写代码中还是要close的,那么如何保证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!!

这个语法叫做:try with resources

那么这个语法能针对锁执行完了就释放吗?显然不是的,这个语法不是随便一个对象放到try()中就可以自动释放的!需要满足一定的要求

 可以看到这个outputstream实现了closeable类,所以只有实现了这个接口的类才能自动释放,这个接口提供的方法就是close方法

以上是关于字节流的使用

字符流的使用

字符流的使用和字节流是相似的,我们演示一下方法

 第一个版本是一次返回一个char类型字符,第二个版本是返回一个字符数组,第三个版本是相当于对第二个字符数组封装了,最后一个版本是返回一个从off到len 的字符数组

public static void main(String[] args) {
        try(Reader reader = new FileReader("d:/test.txt")){
            while(true){
                int ch = reader.read();
                if(ch ==-1){
                    break;
                }
                System.out.println(""+(char) ch);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

结果:

 我们把test内容换成中文"你好"后运行程序

 也是一次打印一个字符

写操作也是类似的

public class IO9 {
    public static void main(String[] args) {
        try(Writer writer = new FileWriter("d:/test.txt")){
            writer.write("hello");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

 有时候我们发现写的内容没在真的文件中存在,很大可能是缓冲区的问题

writer.write("hello");

像这样的写操作是先写到缓冲区,缓冲区是有很多的形态(代码中,操作系统内核,标准库),写操作执行完了,内容可能在缓冲区里,还没真正进入硬盘,close操作就会触发缓冲区的冲刷(flush),刷新操作其实就是把缓冲区的操作写到硬盘中..除了close之外,还能通过flush方法,也能起到刷新缓冲区的效果


Scanner也是搭配流对象使用的

Scanner scanner = new Scanner(System.in);
System.in

其实就是一个输入流对象,字节流,看看源码

 这里Scanner scanner = new Scanner(System.in);的System.in是指向标准输入的流对象,就从键盘读取,如果指向的是一个文件流对象,就从文件读

public class IO10 {
    public static void main(String[] args) {
        //Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = new FileInputStream("d:test.txt")){
            Scanner scanner = new Scanner(inputStream);
            //这里读取的内容就是从文件进行读取了
            scanner.next();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Scanner的close本质上是要关闭内部包含的这个流对象,此时,内部的inputStream对象已经被try()关闭了,里面的Scanner不关闭也没事,但是如果 内部的inputStream对象没有关闭,那么就要关闭内部包含的这个流对象


4.文件IO小程序练习

接下来我们写几个小程序,练习文件的基本操作 + 文件内容读写操作

 示例1

扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要 删除该文件

也就是给定一个目录,目录里包含很多的文件和子目录,用户输入一个要查询的词,如果目录下(包含子目录)有匹配的结果(文件名)就进行删除

import java.io.File;
import java.util.Scanner;

public class IO11 {
    private static Scanner scanner = new Scanner(System.in);
    public static void main(String[] args) {
        //输入目录
        //Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要搜索的路径");
        String basePath = scanner.next();
        //针对输入进行判定
        File root  = new File(basePath);
        if(!root.isDirectory()){
            //如果路径不存在,或者只有一个普通文件,此时无法进行搜索
            System.out.println("输入的路径有误");
            return;
        }
        //再让用户输入一个要删除的文件名
        System.out.println("请输入要删除的文件名:");
        //此处用next,不用nextLine
        String nameToDelete = scanner.next();
        /*
        针对指定的路径进行扫描.递归操作
        先从根目录出发(root)
        先制定一下,当前这个目录里看看是否有要删除的文件,如果是就删除,如果不是就跳过下一个
        如果这里包含了一些目录,在针对子目录进行递归
         */
        scanDir(root,nameToDelete);
    }

    private static void scanDir(File root, String nameToDelete) {
        //列出当前目录包含的内容
        File[] f = root.listFiles();//相当于打开文件资源管理器,打开了一个目录一样
        if(f == null){
            //空目录
            return;
        }
        //遍历当前的列出结果
        for (File files: f) {
            if(files.isDirectory()){
                //是目录就进一步递归
                scanDir(files,nameToDelete);
            }else {
                //不是目录,判定是否删除
                if(files.getName().contains(nameToDelete)){
                    System.out.println("是否要删除: "+files.getName()+"?");
                    String choice = scanner.next();
                    if(choice.equals("y")||choice.equals("Y")){
                        files.delete();
                        System.out.println("删除成功");
                    }else {
                        System.out.println("删除取消");
                    }
                }
            }
        }
    }
}

执行程序后 

在函数中加上扫描路径的打印 

 

示例2

进行普通文件的复制

把一个文件拷贝成另一个文件

import java.io.*;
import java.util.Scanner;

public class IO12 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        //输入两个路径
        System.out.println("输入要拷贝的文件:");
        String srcPath = sc.next();
        System.out.println("输入要拷贝的路径:");
        String destPath = sc.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){
                    return;
                }
                outputStream.write(b);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

 try with resources语法支持包含多个流对象,多个流对象之间用分号隔开就行

我们将d:/tmp/aaa的文件111拷贝到d:/tmp/bbb中去

源路径

 目标路径

 执行程序后

 拷贝成功

 

文件章节到这里就结束了

祝各位大佬新年快乐!新年胜旧年,兔年"兔飞猛进"~~

相关文章:

  • 23种设计模式(十四)——中介者模式【接口隔离】
  • 量子机器学习相关的最近研究动态(复数篇论文的一些简单整理)
  • 第十三届蓝桥杯省赛JavaA组 D 题、Java C 组 G 题、Python C 组 G题——GCD(AC)
  • 字节青训前端笔记 | 响应式系统与 React
  • Allegro如何输出第三方网表操作指导
  • 89. 注意力机制以及代码实现Nadaraya-Waston 核回归
  • 云原生技能树-docker caontainer 操作
  • 使用 Burpsuite 测试的常用操作(一)
  • 应届生身份为什么重要?
  • PHP MySQL Delete
  • JVM类的结构与字节码
  • 【Java IO流】字符流详解
  • 线段树入门
  • 【C语言从0到1之文件操作】(原理 画图 举例 不信教不会你 不要放收藏夹落灰 学起来好嘛)
  • java的数据类型:引用数据类型(String、数组、枚举)
  • linux系统管理
  • 2023牛客寒假算法基础集训营2(11/12)
  • linux搭建webapp实战
  • Golang语法快速上手3
  • 【JavaEE初阶】第五节.多线程 ( 基础篇 ) 线程安全问题(上篇)
  • https://app.hackthebox.com/machines/Inject
  • Spring —— Spring简单的读取和存储对象 Ⅱ
  • 渗透测试之冰蝎实战
  • Mybatis、TKMybatis对比
  • Microsoft Office 2019(2022年10月批量许可版)图文教程
  • 《谷粒商城基础篇》分布式基础环境搭建
  • 哈希表题目:砖墙
  • Vue 3.0 选项 生命周期钩子
  • 【车载嵌入式开发】AutoSar架构入门介绍篇
  • 【计算机视觉 | 目标检测】DETR风格的目标检测框架解读