IO、存储、硬盘、文件系统相关常识,字节流字符流介绍及使用

发布于:2024-04-19 ⋅ 阅读:(30) ⋅ 点赞:(0)

1. 什么是IO

IO就是单词Input和Output的缩写.Input是输入,Output是输出.

我们怎么确定是一个操作是输入还是输出呢?这其实是一套人为规定的规则.

网卡->硬盘->内存->CPU

如果数据符合上述的传输方向,就是输入;反之,就是输出.比如,我们在控制台上输入一个数,这个数从硬盘存储到内存中,就是输入;从内存中读取数据到硬盘中,就是输出.这套规则还是比较符合我们的常识与直觉的.

2. 文件

2.1 文件的概念

我们提到的"文件"是一个十分狭义的概念,指的就是保存在硬盘上的文件.

文件夹也是一种文件,称为"目录文件",也保存在硬盘上.在一个硬盘中,可能有许多目录文件,其中又嵌套着目录文件.把这个数据结构抽象出来,得到的就是一个N叉树型结构.

2.2 文件的路径

既然文件这么多,我们需要一种属性来标识出这个文件存放在哪里,这就是文件的路径.文件的路径获取方式:从根节点出发,一层一层往下走,最终达到目标文件之后,中间的这些目录集合在一起,就构成了路径.

2.2.1 绝对路径

以盘符为开头的就是绝对路径.

路径形式:"C:/program/qq/Bin/qq.exe".注:推荐使用'/',在Window系统下,'/'和'\'都支持;但是在其他系统下可能只支持'/'.

2.2.2 相对路径

比如我们现在仍然需要找qq.exe

如果我们当前的基准目录是

C:/program/qq/Bin,通过./qq.exe可以找到这个文件.

C:/program/qq,通过./Bin/qq.exe可以找到这个文件.

如果我们现在基准目录是

C:/program/qq/Bin/plugins,通过../qq.exe可以找到这个文件.

总结: 

当前基准文件(用.代替)+后续路径 = 该文件的绝对路径.

使用..来返沪上一层目录.

2.3 文件的分类方式

文件的分类方式又很多种,这里主要讨论一种和编写代码密切相关的:文本文件和二进制文件.

2.3.1 文本文件

文本文件在硬盘上存储的数据是文本数据,这些文件的数据中都是字符串(本质上是能够构成合法字符的二进制数据),每一个部分都是合法的字符.(通过GBK,UTF8等码表让数字和字符一一对应).这是给人看的文件.

2.3.2 二进制文件

二进制文件与文本文件相对,其中保存的就是纯二进制数据,这些二进制数据通过码表对应过来可能是一堆乱码.这是给机器看的文件.

区分清楚一个文件到底是文本文件还是二进制文件是十分重要的.因为对于两种文件的编程处理方式是不同的.

那么到底怎么区分呢?最简单的方式就是使用记事本打开这个文件,读得懂就是文本文件,出现一堆乱码就是二进制文件.

对于日常常见的一些文件:

docx,pptx,mp3,mp4,pdf 都属于二进制文件;md,html,java,c 都属于文本文件.

3. 使用Java操作文件

3.1 针对文件系统进行操作

3.1.1 获取文件的路径

针对文件系统的操作包括:创建文件,删除文件,创建目录,重命名文件......

Java标准库提供了一个File类表示一个文件,进一步通过File提供的方法,就可以进行文件系统的操作了.

假如我们在"C:\code\test\io",这个路径下创建了一个文本文件:test.txt

public static void main(String[] args) throws IOException {
        File f = new File("./test.txt");
        System.out.println(f.getParent());//获取上一级目录
        System.out.println(f.getName());//获取文件名称
        System.out.println(f.getPath());//获取当前工作路径
        System.out.println(f.getAbsolutePath());//获取拼接当前工作路径和相对路径
        System.out.println(f.getCanonicalPath());//获取简化后的拼接路径
    }

结果如下:

3.1.2 文件的创建

public static void main(String[] args) throws IOException {
        File f = new File("./test.txt");
        f.createNewFile();
        System.out.println(f.exists());//是否存在
        System.out.println(f.isFile());//是否是文件
        System.out.println(f.isDirectory());//是否是目录
    }

结果如下:

文件的创建并不一定总是会成功的,当权限不足或者给出的路径非法时,文件的创建就会失败.

创建成功后,左侧列表就会出现一个文本文件.

3.1.3 文件的删除

public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 文件删除
        File f = new File("./test.txt");
        // f.delete();
        f.deleteOnExit();
        // 把程序阻塞住
        scanner.next();
        System.out.println(f.exists());
    }

当我们这个程序运行完之后,左侧的文本文件就消失了.结果如下图所示:

3.1.4 目录的创建

我们也可以通过File类中的一些方法来创建目录.

public static void main(String[] args) {
        File dir = new File("./testDir/aaa/bbb/ccc/ddd");
        // mkdir 只能创建出一级目录
        // dir.mkdir();
        // 可以创建多级目录
        dir.mkdirs();
        System.out.println(dir.isDirectory());
    }

同样会在左侧的列表中出现一个我们创建的目录,结果如下:

3.1.5 更改文件名称

public static void main(String[] args) {
        File f1 = new File("./test2.txt");
        File f2 = new File("./testDir/test2.txt");
        f1.renameTo(f2);
    }

这里的意思相当于将文件f1重新命名为f2.

3.2 Java对于文件内容进行操作

3.2.1 Stream流

对于文件内容的操作有:读文件,写文件,打开文件和关闭文件.

在Java中通过"流"(stream流)这样的一组类,进行上述的文件内容操作.

这个"流"分成两组:字节流和字符流.

字节流以字节为单位读取数据.主要针对二进制文件读写数据.主要涉及的两个流对象是InputStream和OutputStream.

字符流以字符为读取单位.主要针对文本文件读写数据.主要涉及的两个流对象是Reader和Writer.

注意: 字符!=字节,一个字符往往对应了多个字节.

3.2.2 核心操作

1. 通过构造方法,打开文件.

2. 通过read方法读文件内容.

3. 通过write方法写文件内容.

4. 通过close关闭文件.

3.2.3 字节流的使用

3.2.3.1 读文件

首先,我们需要创建一个流对象inputStream,但是我们不能直接new出一个InputStream的实例.

因为InputStream是一个抽象类,需要一个类来继承它,这个类我们也有现成的,就是FileInputStream

那么我们new一个FileInputStream的实例,再使用InputStream向上转型接收即可.

然后,我们需要创建一个大小为1024,byte类型的数组来存放读取到的字节.

随后,我们就要开始读取了,调用inputStream对象的read方法.

3.2.3.2 两个read方法API的比较

1.read(),一个字节一个字节读,频次高

2.read(byte[]),一次读许多个字节,频次低

这两个方法哪一个执行的效率高?答:第二个,因为它访问IO设备的次数少,这个过程是十分耗费时间的.在使用Java做算法题时,有的题目就会卡输入输出,输入输出超时的原因就是访问IO设备的次数过多,我们解决的方法是减少IO访问次数,欲知后事如何,请等待下篇博客.

我们这里直接调用参数只有一个byte类型数组的read方法即可.这个方法标识的返回值是从-1到整型的最大值,但实际读取到字节的范围是0-255.返回值设置为int的原因是,当返回-1时,就可以标识着读取结束.

接着,我们可以打印出来看一下,使用for循环+printf使用16进制格式化输出.

最后,需要调用inputStream对象的close关闭流.

3.2.3.3 close为什么重要

我们在大多数时候没关闭文件是很难感知出来的,但是一旦遇到问题就是大事情.

当我们打开文件时,会在操作系统内核PCB中给"文件描述符表"添加一个元素,这个元素就是表示当前打开的文件相关信息.

close方法的执行相当于就释放了文件描述符表上对应的元素.如果一直打开文件而不关闭,文件描述符表就可能被占满,被占满后,尝试再次打开文件,就会使打开文件失败,可能会使得程序卡死.

总结而言,close释放了文件打开需要的资源,保证文件的打开能够正常进行,十分重要.

接下来,我们编写代码来实现读文件操作.

比如,我现在在"C:/code/test/io/test.txt"这个文件中写了一个"helloworld",我们来写一下代码看看效果:

public static void main(String[] args) throws IOException {
        InputStream inputStream = new FileInputStream("C:/code/test/io/test.txt");
        try {
            while (true) {
                byte[] bytes = new byte[1024];
                int n = inputStream.read(bytes);
                if(n==-1) {
                    return;
                }
                for (int i = 0; i < n; i++) {
                    System.out.printf("0x%x ",bytes[i]);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            inputStream.close();
        }
    }

结果如下:

我们来找一下,helloworld里面的字符对应的十六进制数表示形式.

发现与我们的结果相同,最后两个十六进制数其实对应的不是字母,而是换行/回车符,标志字符串在文件中的结尾位置.

虽然这个效果我们达到了,但是这个代码还可以写得更优雅.我们可以把流对象的创建放在try后面的括号里面,这样我们就不需要手动地进行close操作.代码如下:

try(InputStream inputStream = new FileInputStream("C:/code/test/io/test.txt")) {
     while (true) {
     byte[] bytes = new byte[1024];
     int n = inputStream.read(bytes);
         if(n==-1) {
             return;
         }
         for (int i = 0; i < n; i++) {
             System.out.printf("0x%x ",bytes[i]);
         }
     }
}        

但是并不是任何对象的创建都可以这样操作,要实例化对象的这个类必须是实现了Closeable接口才可以这样进行操作.我们接下来的代码将使用这种方式编写.

3.2.3.4 写文件

假如我们要对上述路径的文件写入一个abcd,我们可以先创建流对象outputStream(同样是向上转型创建),再调用这个流对象的write方法,给它传入一个int型的参数(对应字母的assic码).

代码如下:

public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("C:/code/test/io/test.txt")) {
            outputStream.write(97);
            outputStream.write(98);
            outputStream.write(99);
            outputStream.write(100);
        }catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

结果如下:

注意:

1. 如果我们在进行写文件时,文件还没有被创建,系统就会自动地为我们创建一个新文件.但是,读操作并不会自动地创建新的文件,如果文件找不到就会抛出FileNotFoundException.

2. 对于写操作,如果这个文件中原先有数据,这些数据会在被打开时清空(被打开操作清空的).但是,我们可以在创建流对象时,多传入一个参数true来实现"追加写".如下图所示:

3.2.4 字符流的使用

3.2.4.1 读文件

这里我们的操作和之前的读操作差不多,不过多赘述.主要的差别在于我们这里直接一个个字符读取,再转换成int型(对于assic码),最后再转回字符输出即可.代码和结果如下:

public static void main(String[] args) throws IOException {
        try (Reader reader = new FileReader("C:/code/test/io/test.txt")) {
            while (true) {
                int c = reader.read();
                if(c==-1) {
                    break;
                }
                char cc = (char)c;
                System.out.print(cc);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

3.2.4.2 写文件

这里我们的操作和之前的写操作差不多,直接上代码和结果.代码和结果如下:

public static void main(String[] args) throws IOException {
        try (Writer writer = new FileWriter("C:/code/test/io/test.txt",true)) {
            writer.write(97);
            writer.write(98);
            writer.write(99);
            writer.write(100);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

3.3 代码案例

3.3.1 案例1

要求:扫描指定目录,找到名称中包含指定字符的所有普通文件,并询问用户是否要删除该文件.代码如下:

public class Demo {
    private static void searchFile(File file,String searchWord) {
        Scanner scanner = new Scanner(System.in);
        File[] files = file.listFiles();
        if(files==null) {
            return;
        }
        for(File f :files) {
            if(f.isFile()) {
                String fileName = f.getName();
                if(fileName.contains(searchWord)) {
                    System.out.println("找到了目标文件:"+f.getAbsolutePath());
                    System.out.println("是否要删除该文件,删除1/不删除0");
                    int flag = scanner.nextInt();
                    if(flag==1) {
                        f.delete();
                        return;
                    }
                }
            }else if(f.isDirectory()) {
                searchFile(f,searchWord);
            }
        }
    }
    public static void main(String[] args) {
        //1.输入目录路径信息和搜索关键词
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要搜索的目录路径:");
        String rootPath = scanner.next();
        System.out.println("请输入要搜索的关键词:");
        String searchWord = scanner.next();
        File file = new File(rootPath);
        //2.路径合法性判断
        if(!file.isDirectory()) {
            System.out.println("输入的路径非法!");
            return;
        }
        //3.开始查找
        searchFile(file,searchWord);
    }
}

这里我们主要分析一下searchFile方法,这个方法内部:

先把当前file中的文件列表放入一个File数组中,如果这个数组为空直接返回,再去遍历访问这个数组中的每一个文件.

如果这个文件是普通文件,判断名称中是否包含关键词.包含就输出,再判断是否要删除.

如果这个文件是目录文件,递归调用searchFile方法,这是一种DFS搜索方式.

3.3.2 案例2

要求:复制一个文件,输入一个路径表示要被复制的文件,输入另一个路径表示要复制到的目标目录.

 public static void main(String[] args) {
        //1.输入两个路径
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入被复制文件的路径:");
        String srcFilePath = scanner.next();
        File srcFile = new File(srcFilePath);
        System.out.println("输入需要复制到的文件的路径:");
        String desFilePath = scanner.next();
        File desFile = new File(desFilePath);
        //2.路径合法性判断
        if(!srcFile.isFile()) {
            System.out.println("输入的路径非法:");
            return;
        }
        if(!destFile.getParentFile().isDirectory()) {
            System.out.println("输入的路径非法:");
            return;
        }
        //3.读文件,写文件
        try (InputStream inputStream = new FileInputStream(srcFilePath)
             ;OutputStream outputStream = new FileOutputStream(desFilePath) ) {
            while (true) {
                byte[] bytes = new byte[1024];
                int n = inputStream.read(bytes);
                if(n==-1) {
                    break;
                }
                outputStream.write(bytes,0,n);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

对于这个案例我们要注意,我们不是直接去判断des文件是否存在,因为大概率不存在.我们只需要判断des的parentPath是不是目录即可.

3.3.3 案例3

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

private static void searchFile(File file,String searchWord) {
        File[] files = file.listFiles();
        if (files == null) {
            return;
        }
        for (File f : files) {
            if (f.isFile()) {
                String fileName = f.getName();
                if (fileName.contains(searchWord)) {
                    System.out.println("找到了目标文件:" + f.getAbsolutePath());
                    return;
                }else {
                    matchWord(f,searchWord);
                }
            } else if (f.isDirectory()) {
                searchFile(f, searchWord);
            }
        }
    }

    private static void matchWord(File f, String searchWord) {
        try (Reader reader = new FileReader(f)) {
            StringBuilder stringBuilder = new StringBuilder();
            while (true) {
                int c = reader.read();
                if(c==-1) {
                    break;
                }
                stringBuilder.append((char)c);
            }
            if(stringBuilder.indexOf(searchWord)>0) {
                System.out.println("找到了目标文件:" + f.getAbsolutePath());
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要搜索的目录路径:");
        String rootPath = scanner.next();
        System.out.println("请输入要搜索的关键词:");
        String searchWord = scanner.next();
        File file = new File(rootPath);
        //2.路径合法性判断
        if(!file.isDirectory()) {
            System.out.println("输入的路径非法!");
            return;
        }
        //3.开始查找
        searchFile(file,searchWord);
    }

在这个案例中,我们仍然递归查找,只不过是在案例一的基础之上多加了读和匹配操作.这里使用字符流进行读取,利用StringBuilder进行字符串的拼接,最后调用StringBuilder的API判断是否包含关键词.