【文件IO】文件系统的操作 流对象 字节流(Reader/Writer)和字符流 (InputStream/OutputStream)的用法

目录

1.文件系统的操作 (File类)

2.文件内容的读写 (Stream流对象)

[2.1 字节流](#2.1 字节流)

[2.2 字符流](#2.2 字符流)

[2.3 如何判断输入输出?](#2.3 如何判断输入输出?)

[2.4 reader读操作 (字符流)](#2.4 reader读操作 (字符流))

[2.5 文件描述符表](#2.5 文件描述符表)

[2.6 Writer写操作 (字符流)](#2.6 Writer写操作 (字符流))

[2.7 InputStream (字节流)](#2.7 InputStream (字节流))

[2.8 OutputStream (字节流)](#2.8 OutputStream (字节流))

[2.9 字节流转字符流](#2.9 字节流转字符流)

3.练习题

[3.1 扫描指定目录](#3.1 扫描指定目录)

[3.2 进行文件的复制](#3.2 进行文件的复制)

[3.3 扫描指定目录](#3.3 扫描指定目录)


什么是文件?

文件是一个广义的概念。操作系统里会把很多的硬件设备和软件资源都抽象成"文件",统一进行管理。大部分情况下,谈到的文件都是指硬盘的文件。文件就相当于是针对"硬盘"数据的一种抽象。

硬盘大致分为:

  • 机械硬盘(HDD) 适合于顺序读写,不适合随机读写。
  • 固态硬盘(SSD) 现在的电脑基本都是固态硬盘。

IO

  • I : input输入
  • O: output输出

内存和硬盘

  1. 内存速度快,硬盘速度慢。
  2. 内存空间小,硬盘空间大。
  3. 内存贵,硬盘便宜。
  4. 内存的数据断电就会丢失,硬盘的数据断电还在。

**目录:**其实是一种树形结构,描述文件所在的位置。

**文件系统:**用来管理一台计算机上的很多文件

**路径:**分为绝对路径和相对路径。用来定位某一计算机资源。

  • 绝对路径:指的是一个文件从根目录开始的实际存在于硬盘中的路径。
  • 相对路径:指的是相对于当前目录的路径。

文本类型

从编码的角度来看,文件主要是两大类:

  1. 文本 (文件中保存的数据,都是字符串,保存的内容要求都是合法的字符)
  2. 二进制 (文件中保存的数据,是二进制数据,不要求保存的内容是合法的字符)

常见的字符集有UTF-8,GBK。 查看码表 -> 点击查看


如何判断一个文件是文本还是二进制?

一种方法是直接用记事本打开这个文件,如果打开之后出现乱码,则文件就是二进制;反之就是文本。(能用记事本打开的原因是因为记事本就是会尝试按照字符的方式来展示内容,这个过程就会自动查码表)。在我们电脑上,很多文件都是二进制的,比如docx,pptx,... 都属于二进制。同时区分文本和二进制也是很重要的。


Java中对于文件的操作 ,有两类:

(1)文件系统的操作 (File类)

  • 创建文件,删除文件,判断文件是否存在,判定文件类型等。

(2)文件内容的操作 (流对象 Stream)

  • 读文件/写文件(InputStream / OutputStream,Reader / Writer)

1.文件系统的操作 (File类)

一个目录中,用来分割目录的路径分隔符,"/"。由于操作系统不同,有的可能是"/",有的可能是"\"。于是Java提供了pathSeparator()这个方法,后续使用都可以调用这个方法,可以避免在不同的操作系统上运行时出错。


构造方法:

最常用是第二个,在构造对象的时候。传入路径。相对路径或绝对路径。


常用的方法:

举个例子:我们在D盘下创建一个文件:text.txt。

java 复制代码
public class test {
    public static void main(String[] args) throws IOException {

        File file = new File("d:/test.txt");
        boolean ret = file.createNewFile();                         //创建一个空文件
        System.out.println("父目录文件路径: " + file.getParent());     //得到父目录文件路径
        System.out.println("文件名: " + file.getName());             //得到文件名
        System.out.println("文件路径: " + file.getPath());           //得到文件路径
        System.out.println("绝对路径: " + file.getAbsolutePath());   //得到绝对路径

        System.out.println("==================");

        System.out.println("文件是否存在: " + file.exists());              //判断文件是否存在
        System.out.println("文件是否是一个目录: " + file.isDirectory());    //判断该文件是否是一个目录
        System.out.println("文件是否是一个普通文件: " + file.isFile());      //判断该文件是否是一个普通文件
        
    }

}

注意: getName方法得到的文件名是加扩展名的,文件=前缀+扩展名。扩展名类似 .txt,.java这样的。 并且在构造对象时,文件名要完整。有扩展名的要加上。


2.删除文件操作

有两种,一种是直接删除 delete() 方法,另一种是等JVM运行结束后再删除 deleteOnExit() 方法。比如有多线程执行时,等执行完后再删除。后者的使用举例:比如我们在写word还没保存时,系统就会生成一种临时文件机制。


3.查看文件名,创建,删除目录, 文件重命名

使用list方法可返回当前目录下的所有文件名。前提是该对象是目录,而不是一个类似于.txt记事本这样的文件,这种会返回null。并且在打印时不能直接打印对象,这样会打印出哈希值。要用Arrays.toString() 方法打印。

创建目录有两种,一种是只能创建一级目录 mkdir() 方法,另一种是可以创建多级目录 mkdirs() 方法。

文件重命名使用 renameTo() 方法。

java 复制代码
public class test {
    public static void main(String[] args) throws IOException {

        File srcFile = new File("d:/aaa");
        File destFile = new File("d:/bbb");
        //File srcFile = new File("d:/test.txt");
        //File destFile = new File("d:/test2.txt");
        srcFile.renameTo(destFile);  //注意是谁调用谁
        
    }
}

2.文件内容的读写 (Stream流对象)

在Java标准库中,提供的读写文件的流对象,不是一两个类,而是有很多类,虽然这里有很多的类,实际上这些类都可以归结到两个大的类别中。

2.1 字节流
  • 对应着"二进制文件",每次读写的最小单位,都是"字节"。
  • 读 :Reader
  • 写: Writer
2.2 字符流
  • 对应着文本文件,每次读写的最小单位是"字符",(一个字符对应着一个或多个字节,比如在GBK中,一个中文字符对应两个字节;UTF-8中,一个中文字符对应三个字节)。
  • 字符流本质上是针对字节流进行了一层封装。
  • 读:InputStream
  • 写:OutputStream

2.3 如何判断输入输出?

假设我们把一个数据保存到硬盘中,如果站在硬盘的角度就是输入,如果站在CPU的角度就是输出。

而我们通常所说的输入输出就是站在CPU的角度去看的。

  • 读操作就可以认为是将硬盘上的数据放到CPU中使用。(进入cpu)
  • 写操作就可以认为是通过CPU往硬盘上存储数据。(远离cpu)

2.4 reader读操作 (字符流)

有下面这三种方法:

  1. read(); 无参数:一次只读取一个参数。返回值为整数。
  2. read(char[] cbuf); 一次读取若干个字符,会把参数指定的cbuf数组给填充满。
  3. read(char[] cbuf,int off,int len); 一次读取若干个字符,会把这个数组从off到len范围内填充。就是把数据存到这个字符数组中。

1.read();

java 复制代码
public class test {
    public static void main(String[] args) throws IOException {

        Reader reader = new FileReader("d:/test.txt");

        while(true) {
            int n = reader.read();  //无参
            if(n==-1) break;
            char ch = (char)n;
            System.out.println(ch);
        }
        
    }
}

至于为什么要返回整数而不应该是char型?源码:

翻译为:读取的字符,取值范围为0到65535 (0x00-0xffff)的整数(两个字节的范围),如果已经到达流的结尾,则为-1。即就是每次读取一个两个字节的数据,当读取到-1则表示读取结束。

使用整形一方面是表示-1这样的特殊情况。

还有就是到0到65535是两个字节的范围,我们知道在unicode编码中是一个中文字符对应两个字节,而在UTF-8中,一个中文字符对应三个字节。那要是使用UTF-8不会范围不够吗?

其实在Java标准库内部,对于字符集编码进行了很多的处理工作,如果只使用char,此时使用的字符集固定就是unicode;如果是使用String,就会自动的把每个字符的unicode转换成UTF-8。


  1. read(char[] cbuf); 往read里传入一个空的数组,数组用来存储数据。这里的返回值表示实际读取到的字符的个数,与无参的不同,读到末尾,还是返回-1。
java 复制代码
public class test {
   
    public static void main(String[] args) throws IOException {
       
        //一次readr读多个字符 
        Reader reader = new FileReader("d:/test.txt");
        while(true) {
            char[] cbuf = new char[1024];   //创建一个空的字符数组
            int n = reader.read(cbuf);      //传入空字符数组
            if(n==-1) {                     //-1表示读取结束
                break;
            }
            System.out.println("字符个数为:"+n);  //此处这个带参的 n 表示读取的字符个数
            for(int i=0;i<n;i++) {
                System.out.println(cbuf[i]);     //打印独读到的数据
            }
        }

        //一个文件使用完后,一定要记得close !!!
        reader.close();
    
    }
    
}

这里还有一个重要的细节:如果使用Java的Reader方法读取文件后,一定要使用close()方法。主要目的是为了释放空间,更深的来说是释放文件描述符。

2.5 文件描述符表

是PCB中的一个属性,其本质上是一个顺序表(数组),一个进程每次打开一个文件,操作系统会为其在这个表里分配一个文件描述符(元素),而这个数组的长度是存在上限的。如果我们编写的代码一直去打开文件,而不去关闭的话,就会使这个表里的元素越来越多,直到把这个数组给占满,后续再尝试打开文件就会出错。会造成文件泄露,类似内存泄漏。

但内存泄漏,Java不用担心,Java中有GC(Garbage Collection)垃圾收集机制。但是文件还是要手动释放关闭的。所以用完文件后一定要记得 手动 close


不过上面代码的代码还是存在一些问题,有可能close方法执行不到,万一代码还没执行到close时抛出个啥异常,就会导致不能close。

解决办法 :使用try...finally... 将close方法写到fianlly里,这样无论抛出异常与否,fianlly语句都会执行。

java 复制代码
public class test{

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

        Reader reader = new FileReader("d:/test.txt");

        try{
            while(true) {
                char[] cbuf = new char[1024];   //创建一个空的字符数组
                int n = reader.read(cbuf);      //传入空字符数组
                if(n==-1) {                     //-1表示读取结束
                    break;
                }
                System.out.println("字符个数为:"+n);  //此处这个带参的 n 表示读取的字符个数
                for(int i=0;i<n;i++) {
                    System.out.println(cbuf[i]);     //打印独读到的数据
                }
            }
        }finally {
            //一个文件使用完后,一定要记得close !!!
            reader.close();
        }

    }

}

上面这样写也可以,但是代码不够优雅,改进一下。

java 复制代码
public static void main(String[] args) throws IOException {
        try(Reader reader = new FileReader("d:/test.txt")) {
            while(true) {
                char[] arr = new char[1024];
                int n = reader.read(arr);
                if(n==-1) break;
                System.out.println(n);   //n为实际读到的字符个数
                for(int i=0;i<n;i++){
                    System.out.println(arr[i]);
                }
            }
        }
    }

这次我们在try后跟了括号,一种新奇的写法,为啥能跟小括号呢?

这是Java7所提供的try-with-resources机制,会将实现了AutoCloseable接口(或者Closeable接口)的资源定义在try块无论是正常结束或是异常结束,这个资源都会被自动关闭。try小括号里面的部分称为try-with-resources块。虽然我们既没有fianlly,也没有调动close方法。但是只要你使用了这样的语法,编译器在背后就会自动帮我们去生成finally块,并且在里面也会调用关闭资源的close方法。

try的括号中所有实现Closeable的类声明都可以写在里面,流操作都可以这样写。这个语法的目的是()里定义的变量,会在try代码块结束的时候(无论是正常结束还是异常结束),自动调用其中的方法。前提是()里的对象必须要实现Closeable接口。所以我们以后在操作流对象时基本都这样写了。


2.6 Writer写操作 (字符流)

同Reader一样,Writer也有多种重载方法。

  • write(int c); 一次写一个字符
  • write(String str); 一次写一个字符串
  • write(char[ ] cbuf); 一次写多个字符 字符数组
  • 后两个值的是从数组/字符串的第offset个字开始写。
  • 上面这些方法最常用的还是write(String str),一次写一个字符串。
java 复制代码
public static void main(String[] args) throws IOException {
        try(Writer writer = new FileWriter("d:/test.txt")) {
            writer.write("你好!");
        }
    }

这样写默认情况是每次会覆盖掉之前文件中的内容。如果不想覆盖,而是想写到原来文件内容的末尾,就要在创建对象参数列表里加参数true。

java 复制代码
public static void main(String[] args) throws IOException {
        try(Writer writer = new FileWriter("d:/test.txt",true)) {
            writer.write("你好!");
        }
    }

2.7 InputStream (字节流)

用法跟Reader差不多,都是读操作,不过这里是字节流,以字节为单位。只能传入字节byte数组。这里的返回值为实际读到的字节数。

java 复制代码
public static void main(String[] args) {
        try(InputStream inputStream = new FileInputStream("d:/test.txt")) {
            byte[] bytes = new byte[1024];    //创建空的byte数组
            int n = inputStream.read(bytes);  //传入byte数组
            System.out.println(n);
            for(int i=0;i<n;i++) {
                System.out.printf("%x\n",bytes[i]); //以十六制形式打印
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
2.8 OutputStream (字节流)

用法也是跟Writer差不多,都是写操作。不过这里是字节流,以字节为单位。只能传入字节byte数组。

虽然只能传入byte数组,但是我们可以通过字符串转byte数组。使用getBytes()方法。

java 复制代码
public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("d:/test.txt")) {
            String str = "你好!";
            outputStream.write(str.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
}

同样,默认情况下,这种写操作是会覆盖掉之前文件里的内容,要想不覆盖掉,创建对象的参数中加true。

java 复制代码
public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("d:/test.txt",true)) {
            String str = "你好!";
            outputStream.write(str.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
}
2.9 字节流转字符流

读操作使用Scanner。将InputStream实例对象传入Scanner中。就可以使用Scanner提供的一些方法 (例如.next()等) 读取数据了。

java 复制代码
public static void main(String[] args) {
        try (InputStream inputStream = new FileInputStream("d:/test.txt")) {
            //将实例对象传入Scanner
            Scanner sc = new Scanner(inputStream);
            //使用Scanner提供的一些方法读取文件中的数据
            String s = sc.next();
            System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        }
}

写操作使用PrintWriter。也是和Scanner一样,将OutputStream实例对象传入PrintWriter中。

java 复制代码
public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("d:/test.txt")) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.println("Hello");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

不过上面这样写,我们会发现最终并没有写成功。原因是存在"缓冲区"。我们这里的写文件实际上是写入硬盘。

PritWriter这样的类,在进行写入的时候不一定是直接写入硬盘,而是先把数据写入到一个由内存构成的"缓冲区"(buffer)。引入缓冲区目的还是为了提高效率。因为把数据写入内存是非常快的,而写入硬盘却是非常慢的。为了提高效率,就会减少写硬盘的操作。这样就会使数据还未写入硬盘,进程就结束了。

解决办法: 手动添加一行代码:printWriter.flush(); 作用刷新缓冲区

java 复制代码
public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("d:/test.txt")) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.println("Hello");

            printWriter.flush();   //加这段代码很重要 不能忽略

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

3.练习题

3.1 扫描指定目录

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

java 复制代码
public class test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个要扫描的路径:");
        String path = sc.next();
        File file = new File(path);
        //判断路径是否合法
        if(!file.isDirectory()) {
            System.out.println("您输入的路径有误!");
            return;
        }
        //让用户输入一个目标文件名的关键词
        System.out.println("请输入文件的关键词:");
        String key = sc.next();
        //让这个方法进行递归扫描
        scanDir(file,key);
    }

    private static void scanDir(File rootPath, String key) {
        //当前文件的所有子文件
        File[] files = rootPath.listFiles();
        // 判断文件(文件夹)是否为空
        if(files==null) {
            return;
        }else{
            //判断是否是普通文件
            for(File f:files) {
                //输出当前扫描文件的进度
                System.out.println(f.getAbsolutePath());
                if(f.isFile()) {
                    //是普通文件 判断并删除
                    isDel(f,key);
                }else{
                    //不是普通文件(文件夹) 继续递归遍历
                    scanDir(f,key);
                }
            }

        }
    }
    //对普通文件进行删除操作
    private static void isDel(File file,String key) {
        if(file.getName().contains(key)) {
            System.out.println("是否要删除?Y/N");
            Scanner sc = new Scanner(System.in);
            String choice = sc.next();
            if(choice.equals("Y") || choice.equals("y")) {
                boolean bool = file.delete();
                if(bool) {
                    System.out.println("删除成功!");
                }
            }else{
                return;
            }
        }
    }

}
3.2 进行文件的复制

文件内容的复制。

首先会检查源文件是否是一个普通文件,再检查目的路径是否是一个目录。目的路径目录下有重名文件时是否覆盖功能。无重名文件时创建原空文件,并读写数据。

java 复制代码
public class test {
    public static void main(String[] args) throws IOException {
        //先让用户输入要复制的源文件地址(仅输入目录)
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入要复制源文件的路径:");
        String sources = sc.next();
        File file = new File(sources);
        //判断文件是否是一个目录(即文件夹)
        if(!file.isFile()) {
            System.out.println("不是一个有效的普通文件!");
            return;
        }
        //让用户输入复制文件的目的地(不包含文件本身,只包含目录)
        System.out.println("请输入要复制文件的目的路径:");
        String destDir = sc.next();
        File file1 = new File(destDir);
        if(!file1.isDirectory()) {
            System.out.println("目的路径有误!");
            return;
        }

        //得到源文件的文件名
        String yuan = file.getName();
        //创建完整的目的文件
        File file2 = new File(destDir+"/"+yuan);
        if(!file.getName().equals(file2.getName())) {
            System.out.println("请检查目的路径输入是否正确:"+file2.getAbsolutePath());
            return;
        }
        //判断目的路径是否有和源文件同名的文件
        if(file2.exists()) {
            // 是否覆盖
            System.out.println(file2.getAbsolutePath());
            System.out.println("该路径下已有重名文件,是否覆盖? Y/N");
            String choice = sc.next();
            if(choice.equals("Y") || choice.equals("y")) {
                //不用创建普通文件 进行覆盖
                copyFile(file,file2);
            }
            return;
        }
        file2.createNewFile();
        //进入复制操作
        copyFile(file,file2);
    }

    private static void copyFile(File sourDir, File destDir) throws IOException {
        //先将目标文件数据读出来,再写入新的文件
        try(OutputStream os = new FileOutputStream(destDir)) {
            try(InputStream is = new FileInputStream(sourDir)) {
                //字节流读 先创建字节数组 再将源文件的数据读到一个临时的数组中 再把这个临时数组中的内容写入新复制的文件中
                while(true) {
                    byte[] bytes = new byte[1024];
                    int n = is.read(bytes);
                    if(n==-1) {
                        break;
                    }
                    //将数据写入新复制的文件中
                    os.write(bytes,0,n);

                }
            }
            os.flush();
        }
        System.out.println("文件复制完成!");

    }
}
3.3 扫描指定目录

扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)。

和3.1差不多,再判断普通文件名是否包含关键词的基础上加了一个普通文件内容是否包含关键词。

java 复制代码
public class test03 {
    public static void main(String[] args) throws IOException {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个要扫描的路径:");
        String path = sc.next();
        File file = new File(path);
        //判断路径是否合法
        if(!file.isDirectory()) {
            System.out.println("您输入的路径有误!");
            return;
        }
        //让用户输入一个目标文件名的关键词
        System.out.println("请输入文件的关键词:");
        String key = sc.next();
        //让这个方法进行递归扫描
        scanDir(file,key);
    }

    private static void scanDir(File rootPath, String key) throws IOException {
        //当前文件的所有子文件
        File[] files = rootPath.listFiles();
        // 判断文件(文件夹)是否为空
        if(files==null) {
            return;
        }else{
            //判断是否是普通文件
            for(File f:files) {
                //输出当前扫描文件的进度
                //System.out.println(f.getAbsolutePath());
                if(f.isFile()) {
                    //是普通文件 进行判断
                    isContains(f,key);
                }else{
                    //不是普通文件(文件夹) 继续递归遍历
                    scanDir(f,key);
                }
            }

        }
    }

    private static void isContains(File f, String key) throws IOException {
        String str = null;
        //字节流转字符流 Scanner
        try (InputStream is = new FileInputStream(f)) {
            //读内容 字节流
            //将InputStream对象传入Scanner中
            Scanner sc = new Scanner(is);
            str = sc.next();
        }

        if(f.getName().contains(key) || str.contains(key)) {
            System.out.println(f.getAbsolutePath());
        }

    }
    
}
相关推荐
禁默11 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
Cachel wood18 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
Code哈哈笑21 分钟前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
gb421528723 分钟前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶24 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
zfoo-framework32 分钟前
【jenkins插件】
java
风_流沙37 分钟前
java 对ElasticSearch数据库操作封装工具类(对你是否适用嘞)
java·数据库·elasticsearch
ProtonBase1 小时前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
乐之者v1 小时前
leetCode43.字符串相乘
java·数据结构·算法