文件操作和IO

x33g5p2x  于2022-03-28 转载在 其他  
字(17.0k)|赞(0)|评价(0)|浏览(568)

一、文件的概念

在Java中,一般谈到文件,都是指一个存储在磁盘上的文件(狭隘的文件),如果抛开Java,站在系统的角度来看,操作系统在管理很多软件资源和硬件设备的时候,都是把这些东西抽象成一个一个的文件。这是系统中典型的“一切皆文件”的思想,可以把 显示器 键盘 打印机 网卡 抽象成文件。

狭义的文件可以分为两大类:
1.普通文件
2.目录文件(文件夹)

二、File类

1.File类介绍

在Java中为了我们方便去操作文件,标准库给出了一个File类。因此操作文件就可以使用File类来进行操作。

Java 中通过 java.io.File 类来对一个文件(包括目录)进行抽象的描述。注意,有 File 对象,并不代表真实存在该文件

2.File类中方法的介绍

我们先来看看 File 类中的常见属性、构造方法和方法:

属性

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

构造方法

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

方法

修饰符及返回值类型方法签名说明
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(Filedest)进行文件改名,也可以视为我们平时的剪切、粘贴操作
booleancanRead()判断用户是否对文件有可读权限
booleancanWrite()判断用户是否对文件有可写权限

3.相对路径和绝对路径的介绍

有一个构造方法为File(String pathname),这里是给File类传一个具体的路径,通过这个路径来指定唯一的一个文件(可能是普通文件,也可以是目录文件)。

pathname是系统中用来描述文件位置的一种方式。
C:\java-language\java-language\20220307\src\JavaFile 就是一个路径

在一台主机上,一个文件对应这唯一的路径,通过这个路径就可以确定这个文件的位置

路径中的\就是一个pathSeparator,\就是在分割不同的层级的目录。
在Windows上既可以使用 \ (反斜杠)作为目录的分隔符,也可以使用 / (斜杆)来分割。
在Linux或者Mac使用 / 来作为路径之间的分隔符。

在实际开发中,表示一个路径,通常有两个方式:
1.绝对路径。C:\java-language\java-language\20220307\src\JavaFile就是一个绝对路径。绝对路径是以一个盘符开头的路径
2.相对路径。一般是以 . 或 … 开头的。一个 . 就表示当前的基准目录,两个 . 就表示当前基准目录退回的上一级等待目录。有一个基准目录(工作目录),以工作目录为基准,去找到对应的路径。

此时如果以C:\java-language\java-language\20220307\src为基准目录,此时要表示JavaFile的文件,相对路径可以写作:.\JavaFile, . 就表示当前的基准目录。

如果基准目录不一样,那么相对路径的写法也会不同
1.例如此时的基准目录为C:\java-language\java-language\20220307,则要表示JavaFile的文件,相对路径可以写作:.\src\JavaFile
2.例如此时的基准目录为C:\java-language\java-language\20220307\src\Thread0312(Thread0312跟JavaFile是同一级别的文件),则要表示JavaFile的文件,相对路径可以写作:..\JavaFile

4.方法的使用

4.1 构造方法中要注意的路径细节

构造方法中有一个File(String pathname),传的可以是一个绝对路径,也可以是一个相对路径。例如有这个代码:File file = new File("c:\test.txt");,虽然系统支持 \ 的写法,但是由于在Java中,\ + 其它字符会被当成转义字符。因此使用 \ 的时候要写成File file = new File("c:\\test.txt");

代码:

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

运行结果:

这样打印的结果不太明显,可以换为相对路径观察:

public class TestFile {
    public static void main(String[] args) throws IOException {
        File file = new File("./test.txt");
        System.out.println(file.getParent());
        System.out.println(file.getName());
        System.out.println(file.getPath());
        System.out.println(file.getAbsolutePath());//相对路径
        System.out.println(file.getCanonicalPath());//简洁版的相对路径
    }
}

打印结果:

我们可以看到打印的结果中将 / 都变为了 \ ,这是由于我们是在Windows系统下默认是使用 \ 来进行分割的,虽然Windows都支持,但是在代码内部一般用 \ 来表示。

如果我们将传入的相对路径更长一些:

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

打印结果:

如果是在IDEA中运行程序,此时的工作目录,就是当前项目所在的目录。在代码中涉及的相对路径,其实就是以这个当前项目所在的目录为基准。
工作目录:

4.2 其它方法的使用

我们可以先在该文件目录底下创建一个helloword.txt。
代码1:

public class TestFile1 {
    public static void main(String[] args) {
        File file = new File("helloword.txt");
        System.out.println(file.exists());
        System.out.println(file.isFile());//是否是文件
        System.out.println(file.isDirectory());
    }
}

运行结果:

代码2:

public class TestFile1 {
    public static void main(String[] args) {
        File file = new File("c:/");
        System.out.println(file.exists());
        System.out.println(file.isFile());//是否是文件
        System.out.println(file.isDirectory());
    }
}

运行结果:

接下来我们可以利用代码来创建helloworld.txt:
代码3:

public class TestFile1 {
    public static void main(String[] args) throws IOException {
        File file = new File("helloword.txt");
        file.createNewFile();
        System.out.println(file.exists());
        System.out.println(file.isFile());
        System.out.println(file.isDirectory());
    }
}

当我们调用createNewFile方法的时候会抛出一个异常让我们处理,也就是说创建文件可能会失败,那么创建文件为什么会抛异常呢?
两个典型的理由:
1.没有权限。文件系统中的权限,典型的就是读和写,会针对不同的用户,给予不同的权限
2.磁盘的空间不足。

演示delete方法:deleteOnExit方法是等JVM运行结束才删除文件。
代码4:

public class TestFile2 {
    public static void main(String[] args) throws IOException, InterruptedException {
        File file = new File("helloworld.txt");
        file.createNewFile();
        Thread.sleep(3000);
        System.out.println(file.delete());
    }
}
//打印结果:true

可能在IDEA左侧栏中没有显示出helloworld.txt,但在文件目录的路径底下可以看到helloworld.txt先创建了,过了3s又自动删除了。

关于deleteOnExit方法:
该方法主要是用于一些“临时文件”,例如当我们使用Word或者Excel,打开文件的时候,系统会同时生成一个临时的隐藏文件。把Word或者Excel关了的时候,该临时文件就会自动删除。

如果在文件中写了很多内容,此时突然断电没有保存,就是相当于在内存中去写,如果断电,内存中的数据就会丢失。这样造成的损失是很大的。

当在写文件的时候,临时文件就一直存在,除非是正常关闭它临时文件才销毁。但断电后重新有电时,我们可以通过临时文件来恢复数据,就能够知道上次是异常结束,会提示用户是否要恢复数据

使用list和listFiles方法:
代码5:

public class TestFile3 {
    public static void main(String[] args) throws IOException {
        File file = new File("./helloworld.txt");
        file.createNewFile();
        String[] files = file.list();
        System.out.println(Arrays.toString(files));
    }
}
//打印结果为:null

注:如果不用Arrays.toString方法去打印数组里的值,则打印的结果是String的类型和引用的哈希值。

打印结果为null,那么说明这个helloworld.txt文件不是一个目录文件。因此它没有子目录

那么假设传入的是文件目录,代码:

public class TestFile3 {
    public static void main(String[] args) throws IOException {
        File file = new File(".");
        file.createNewFile();
        String[] files = file.list();
        System.out.println(Arrays.toString(files));
    }
}

运行结果:

可见目录中有:

因此是没有问题的。

一道经典面试题
给你一个list方法,遍历一个目录中所有的文件(包含子目录中的文件)
能够打印当前文件目录的所有子文件。

public class TestFile4 {
    public static List<String> result = new ArrayList<>();
    public static void getAllFiles(String basePath) {
        File file = new File(basePath);
        if(file.isFile()) {
            result.add(basePath);
            return;
        }else if(file.isDirectory()) {
            String[] files = file.list();
            for (String s: files) {
                getAllFiles(basePath+"/"+s);
            }
        }
    }
    public static void main(String[] args) {
        getAllFiles(".");
        for (String s:result) {
            System.out.println(s);
        }
    }
}

mkdir和mkdirs的使用:
mkdir方法是用来创建一个文件的。不能用mkdirs来创建目录。而mkdirs是用来创建目录的。
用mkdir来创建目录不行的。只能创建文件。

public class TestFile10 {
    public static void main(String[] args) {
        File file = new File("test/aaa/bbb");
        System.out.println(file.exists());
        file.mkdir();
        System.out.println(file.exists());
        System.out.println(file.isDirectory());
    }
}
//运行结果:
false
false
false

将mkdir换成mkdirs即可:

注:如果在IDEA下传入File构造方法的参数例如只传test,那就说明在当前文件目录下进行操作

renameTo方法的使用:该方法不仅能够用来改文件的名字,还可以移动文件(把一个文件从一个目录移动到另一个目录)
代码:

public class FileDemo8 {
    public static void main(String[] args) {
        File file = new File("./test.txt");
        File file2 = new File("./helloworld.txt");
        file.renameTo(file2);
    }
}

那么此时在IDEA文件目录的底下就能够看到test的名字改为了helloworld的名字

用renameTo来移动文件:

public class FileDemo8 {
    public static void main(String[] args) {
        File file = new File("./test.txt");
        File file2 = new File("./out/test.txt");
        file.renameTo(file2);
    }
}

可以看到原本在基准目录底下的test.txt被转移到了out文件底下。

所谓的文件移动(剪切粘贴),对于操作系统来说,其实是个非常高效的动作。每个文件,都有个属性,这个属性就是该文件的路径,移动操作就只是修改了一下文件的属性而已。如果要是把文件跨硬盘来操作仍然会比较低效(例如U盘上的数据移到硬盘上)。

所谓的文件复制(复制粘贴),对于操作系统来说,很可能是个非常低效的操作。它需要把文件的内容都读出来,然后再拷贝一份写到磁盘中。这个过程就需要消耗比较大的开销了(文件可能很大)。

上面的File类中提供的这些操作,都是一些操作文件的基础动作(操作文件的属性)。它们统称为“文件系统操作”

要操作文件的内容就要使用到InputStream和OutputStream

三、InputStream和OutputStream的使用

1.InputStream和OutputStream的介绍

在Java标准库中,读写文件相关的类有很多:
InputStream和FileInputStream为文件读取操作,按照字节为单位进行读文件。
OnputStream和FileOnputStream为文件写入操作,按照字节为单位进行写文件。

上面的几个类统称为字节流。流的意思就是:我们可以选择性的选择文件中的内容进行修改。例如用水龙头来接100ml的水,可以选择一次性接100ml,也可以选择分两次一次接50,另一次接入50等等。那么在文件中可以选择以字节为单位地对文件进行操作

Java中除了字节流外,还有字符流,是以字符为单位进行读写了。字节是8个bit位,字符是16个比特位。

2.InputStream和OutputStream中方法的使用

2.1 InputStream中的方法

InputStream概述:
方法:

修饰符及返回值类型方法签名说明
intread()读取一个字节的数据,返回 -1 代表已经完全读完了
intread(byte[] b)最多读取 b.length 字节的数据到 b 中,返回实际读到的数量;-1 代表以及读完了
intread(byte[] b,int off, int len)最多读取 len - off 字节的数据到 b 中,放在从 off 开始,返回实际读到的数量;-1 代表以及读完了
voidclose()close()

说明:
InputStream 只是一个抽象类,要使用还需要具体的实现类。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使用 FileInputStream

FileInputStream 概述:
构造方法

签名说明
FileInputStream(File file)利用 File 构造文件输入流
FileInputStream(String name)利用文件路径构造文件输入流

代码示例:
将文件完全读完的两种方式。相比较而言,后一种的 IO 次数更少,性能更好。
代码1:

import java.io.*;
// 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "Hello" 的内容
public class Main {
    public static void main(String[] args) throws IOException {
        try (InputStream is = new FileInputStream("hello.txt")) {
            while (true) {
                int b = is.read();
                if (b == -1) {
                    // 代表文件已经全部读完
                    break;
               }
                
                System.out.printf("%c", b);
           }
       }
   }
}

代码2:

import java.io.*;
// 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "Hello" 的内容
public class Main {
    public static void main(String[] args) throws IOException {
        try (InputStream is = new FileInputStream("hello.txt")) {
            byte[] buf = new byte[1024];
            int len;
            
            while (true) {
                len = is.read(buf);
                if (len == -1) {
                    // 代表文件已经全部读完
                    break;
               }
                
                for (int i = 0; i < len; i++) {
               System.out.printf("%c", buf[i]);
               }
           }
       }
   }
}

上面两个代码需要注意的细节
1.如果使用流对象,一定要记得关闭资源。而Java中提供了try with resource语法,在try语块结束后流对象会自动关闭。注意:只有实现了Closeable接口的流对象才可以在try with resource语法中使用。
2.FileNotFoundException是继承自IOException的。我们可以直接抛出的是IOException异常即可。
3.第二个代码中的执行过程:
代码中的read会尝试把buffer给填满,buffer是1024个字节。
如果假设该文件的长度是2049,读取的过程:
第一次循环,读取出1024个字节,放到buffer数组中,read返回一个1024.
第二次循环,读取出1024个字节,放到buffer数组中,read返回一个1024.
第三次循环,读取出一个字节,放到buffer数组中,read返回1.
第四次循环,此时已经读到文件末尾了(EOF),read返回-1.

对于读操作需要注意的是:
中文的编码方式和英文不太一样的。如果是英文的话,直接就是ascii码,就比较简单。如果是英文的话,就需要UFT-8或者GBK,就会更复杂。

因此代码中的 %c 其实相当于是按照ascii的方式进行打印的,这个时候,如果读取的这个字节是UTF-8或者GBK的一部分,此时很可能这个结果不是合法的ascii值。于是这里就出现了异常

这里我们把文件内容中填充中文看看,注意,写中文的时候使用 UTF-8 编码。hello.txt 中填写 “你好中国”
注意:这里我利用了这几个中文的 UTF-8 编码后长度刚好是 3 个字节和长度不超过 1024 字节的现状,但这种方式并不是通用的。

import java.io.*;
// 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "你好中国" 的内容
public class Main {
    public static void main(String[] args) throws IOException {
        try (InputStream is = new FileInputStream("hello.txt")) {
            byte[] buf = new byte[1024];
            int len;
            while (true) {
                len = is.read(buf);
                if (len == -1) {
                    // 代表文件已经全部读完
                    break;
               }
                // 每次使用 3 字节进行 utf-8 解码,得到中文字符
                // 利用 String 中的构造方法完成
                // 这个方法了解下即可,不是通用的解决办法
                for (int i = 0; i < len; i += 3) {
                    String s = new String(buf, i, 3, "UTF-8");
                    System.out.printf("%s", s);
               }
           }
       }
   }
}

2.2 利用 Scanner 进行字符读取

构造方法说明
Scanner(InputStream is, String charset)使用 charset 字符集进行 is 的扫描读取

一个汉字是由3个字节来构成的。借助标准库中内置的处理字符集的方式,有Scanner。
因为Scanner也是一个需要关闭的资源,因此也要用try with resource的语法来让其自动关闭即可。使用Scanner需要传入的是文件的路径和字符集编码

public class FileDemo11 {
    public static void main(String[] args) {
        // 尝试从文件中读取出中文. 借助标准库中内置的处理字符集的方式.
        // Scanner 不光能从控制台读取标准输入, 也可以从文件中读取数据.
        try (InputStream inputStream = new FileInputStream("./test.txt")) {
            // Scanner 里面也有个 close 方法, 这个 close 其实也就是用来关闭 Scanner 内包含的 InputStream
            try (Scanner scanner = new Scanner(inputStream, "UTF-8")) {
                while (scanner.hasNext()) {
                    String s = scanner.next();
                    System.out.print(s);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注:在之前使用Scanner没有close的习惯,也没有做要求,是什么原因呢?
当时Scanner内部持有的InputStream是System.in(标准输入),标准输入这个文件,一般是不关闭的,这个文件是每个进程创建出来之后,操作系统默认打开的文件;当进程关闭,该文件也会自动关闭

2.3 OutputStream中的方法

方法:

修饰符及返回值类型方法签名说明
voidwrite(int b)写入要给字节的数据
voidwrite(byte[] b)将 b 这个字符数组中的数据全部写入 os 中
intwrite(byte[]b, int off, int len)将 b 这个字符数组中从 off 开始的数据写入 os 中,一共写 len 个
voidclose()关闭字节流
voidflush()重要:我们知道 I/O 的速度是很慢的,所以,大多的 OutputStream 为了减少设备操作的次数,在写数据的时候都会将数据先暂时写入内存的一个指定区域里,直到该区域满了或者其他指定条件时才真正将数据写入设备中,这个区域一般称为缓冲区(其实是一段OutputStream自带的内存空间)。但造成一个结果,就是我们写的数据,很可能会遗留一部分在缓冲区中。需要在最后或者合适的位置,调用 flush(刷新)操作,将数据刷到设备中。

说明:
OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中,所以使用 FileOutputStream

write方法:
注:一旦按照 OutputStream 的方式打开文件, 就会把文件的原来的内容给清空掉
1.一次写入一个字节

public class TestFile11 {
    public static void main(String[] args) {
        // 一旦按照 OutputStream 的方式打开文件, 就会把文件的原来的内容给清空掉
        try(OutputStream outputStream = new FileOutputStream("./helloworld.txt")) {
            outputStream.write('a');
            outputStream.write('b');
            outputStream.write('c');
            outputStream.write('d');
        }catch(IOException e) {
            e.printStackTrace();
        }
    }
}

2.我们以字节数组的方式写入数据:

public class TestFile11 {
    public static void main(String[] args) {
        // 一旦按照 OutputStream 的方式打开文件, 就会把文件的原来的内容给清空掉
        try(OutputStream outputStream = new FileOutputStream("./helloworld.txt")) {
            byte[] buffer = new byte[] {
                    (byte)'a',(byte)'b',(byte)'c'
            };
            outputStream.write(buffer);
        }catch(IOException e) {
            e.printStackTrace();
        }
    }
}

3.可以将字符串转为字节数组写入,这样写入的速度更快:

public class TestFile11 {
    public static void main(String[] args) {
        // 一旦按照 OutputStream 的方式打开文件, 就会把文件的原来的内容给清空掉
        try(OutputStream outputStream = new FileOutputStream("./helloworld.txt")) {
            String s = "helloworld";
            outputStream.write(s.getBytes(s));
        }catch(IOException e) {
            e.printStackTrace();
        }
    }
}

4.使用 PrintWriter 类来包装一下 OutputStream 然后可以更方便的进行写数据。这样写入的是字母和中文都没有问题。

public class FileDemo13 {
    public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("./test.txt")) {
            // 使用 PrintWriter 类来包装一下
            try (PrintWriter writer = new PrintWriter(outputStream)) {
                writer.println("你好世界");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.4 综合使用InputStream和OutputStream的案例

案例1:
指定一个目录,扫码这个目录,找到文件名中包含了指定字符的文件。并提示用户是否要删除这个文件,根据用户的输入决定是否删除。(由于要扫码目录,因此需要使用递归的方式来把目录中的子文件都获取到)

代码:

public class FileDemo14 {
    public static void main(String[] args) throws IOException {
        // 1. 让用户指定一个待扫描的根目录 和 要查询的关键词
        System.out.println("请输入要扫描的根目录(绝对路径): ");
        Scanner scanner = new Scanner(System.in);
        String root = scanner.next();
        File rootDir = new File(root);
        if (!rootDir.isDirectory()) {
            System.out.println("您输入的路径错误! 程序直接退出!");
            return;
        }
        System.out.println("请输入要查找的文件名中包含的关键词: ");
        String token = scanner.next();

        // 2. 递归的遍历目录
        //    result 表示递归遍历的结果. 就包含着所有带有 token 关键词的文件名.
        List<File> result = new ArrayList<>();
        scanDir(rootDir, token, result);
        // 3. 遍历 result, 问用户是否要删除该文件. 根据用户的输入决定是否删除
        for (File f : result) {
            System.out.println(f.getCanonicalPath() + " 是否要删除? (Y/n)");
            String input = scanner.next(); if (input.equals("Y")) {
                f.delete();
            }
        }
    }

    // 递归的来遍历目录, 找出里面所有符合条件的文件.
    private static void scanDir(File rootDir, String token, List<File> result) throws IOException {
        // list 返回的是一个文件名(String), 使用 listFiles 直接得到的是 File 对象, 用起来更方便一些.
        File[] files = rootDir.listFiles();
        if (files == null || files.length == 0) {
            // 当前的目录是一个空的目录
            return;
        }
        for (File f : files) {
            if (f.isDirectory()) {
                // 如果当前的文件是一个目录, 就递归的进行查找
                scanDir(f, token, result);
            } else {
                // 如果当前文件是一个普通的文件, 就判定这个文件是否包含了待查找的关键词
                if (f.getName().contains(token)) {
                    result.add(f.getCanonicalFile());
                }
            }
        }
    }
}

案例2:
复制一个文件,启动程序后,让用户输入一个文件的路径(绝对路径),要求这个文件是一个普通文件,而不是目录。然后再指定一个要复制过去的目标路径。

代码:

public class FileDemo15 {
    // 进行文件复制
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要复制的文件: (绝对路径)");
        String srcPath = scanner.next();
        File srcFile = new File(srcPath);
        if (!srcFile.isFile()) {
            System.out.println("文件路径错误! 程序直接退出");
            return;
        }
        System.out.println("请输入要复制到的目标路径: (绝对路径)");
        String destPath = scanner.next();
        // 要求这个 destFile 必须不能存在.
        File destFile = new File(destPath);
        if (destFile.exists()) {
            System.out.println("目标文件的路径已经存在! 程序直接退出!");
            return;
        }
        if (!destFile.getParentFile().exists()) {
            // 父级目录不存在, 也提示一个报错, 也可以不存在就创建出来. 使用 mkdirs 就能创建.
            System.out.println("目标文件的父目录不存在! 程序直接退出!");
            return;
        }
        // 具体进行复制操作.
        // 复制操作就是打开待复制的文件, 读取出每个字节, 然后再把这些字节给写入到目标的文件中.
        try (InputStream inputStream = new FileInputStream(srcFile);
             OutputStream outputStream = new FileOutputStream(destFile)) {
            // 从 inputStream 中按照字节来读, 然后把结果写入到 outputStream 中
            while (true) {
                byte[] buffer = new byte[1024];
                int len = inputStream.read(buffer);
                if (len == -1) {
                    break;
                }
                outputStream.write(buffer, 0, len);
            }
            // 如果这里不加 flush, 触发 close 操作, 也会自动刷新缓冲区.
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("复制完成!");
    }
}

案例三:
扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
注意:我们现在的方案性能较差,所以尽量不要在太复杂的目录下或者大文件下实验

public class FileDemo16 {
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        // 1. 让用户输入一个路径. 待搜索的路径
        System.out.println("请输入要扫描的根目录: ");
        String rootDir = scanner.next();
        File rootFile = new File(rootDir);
        if (!rootFile.isDirectory()) {
            System.out.println("该目录不存在或者不是文件! 直接退出. ");
            return;
        }
        // 2. 再让用户输入一个查询词, 表示要搜索的结果中要包含这个词.
        System.out.println("请输入要查询的词: ");
        String query = scanner.next();
        // 3. 遍历目录以及文件, 进行匹配
        List<File> results = new ArrayList<>();
        scanDirWithContent(rootFile, query, results);
        // 4. 把结果打印出来
        for (File f : results) {
            System.out.println(f.getCanonicalPath());
        }
    }

    private static void scanDirWithContent(File rootFile, String query, List<File> results) {
        File[] files = rootFile.listFiles();
        if (files == null || files.length == 0) {
            // 针对空的目录, 直接返回
            return;
        }
        for (File f : files) {
            if (f.isDirectory()) {
                scanDirWithContent(f, query, results);
            } else {
                if (f.getName().contains(query)) {
                    // 看看文件名称中是否包含
                    results.add(f);
                } else if (isContentContains(f, query)) {
                    // 看看文件内容中是否包含
                    results.add(f);
                }
            }
        }
    }

    private static boolean isContentContains(File f, String query) {
        // 打开 f 这个文件, 依次取出每一行结果, 去和 query 来进行一个 indexOf
        StringBuilder stringBuilder = new StringBuilder();
        try (InputStream inputStream = new FileInputStream(f)) {
            Scanner scanner = new Scanner(inputStream, "UTF-8");
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                stringBuilder.append(line + "\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 只要结果不等于 -1, 就说明查到了.
        return stringBuilder.indexOf(query) != -1;
    }
}

2.5 资源泄漏问题

刚才提到如果使用流对象没有关闭资源的话就会造成非常严重的后果。系统中很多资源都是有限的。如:内存、文件描述符。

操作系统分为:

每个进程的PCB都相当于一个文件描述符表(类似于数组这样的顺序表)。
每次这个进程打开一个文件,就会具体的产生一个文件描述符(是一个整数),再把这个文件描述符放到文件描述符表中

准确地来说,这个文件描述符表类似于一个数组,数组的元素是 struct File,数组的下标,就是文件描述符。

每次打开文件,就要在文件描述符表中申请一项。
每次关闭文件,也就可以把这个表项释放掉,然后供后面继续使用。
由于这个文件描述符表的长度是存在上限的,此时如果一直持续不断地打开文件,又不关闭的话,很快就会把这个文件描述符表给耗尽,此时再打开文件就会失败。

相关文章