文件操作和IO

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6


 

专栏简介: JavaEE从入门到进阶

题目来源: leetcode,牛客,剑指offer.

创作目标: 记录学习JavaEE学习历程

希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.

学历代表过去,能力代表现在,学习能力代表未来! 


目录

1.认识文件

1.1 计算机中的文件有狭义与广义之说:

1.2 树形结构组织和目录

1.3 文件路径(Path)

1.4 文本文件和二进制文件 

2.Java中操作文件--File类

2.1 属性

2.2 构造方法

2.3 方法

3.文件内容的读写--数据流

3.1 InputStream 概述

3.2 FileIntputStream 概述

3.3 OutputStream概述

3.4 利用 Scanner 进行字符读取.

4.小程序练习


1.认识文件

1.1 计算机中的文件有狭义与广义之说:

  • 狭义上的文件指的是 , 针对硬盘这种持久化存储的I/O设备 , 保存数据时会分割成一个个独立的单位 , 这些独立的单位就被抽象成文件的概念.
  • 广义上的文件指的是 , 操作系统中会把很多的硬件设备和软件资源抽象成文件 , 按照文件的方式来统一管理 , 例如: 网卡这个硬件设备在网络编程中 , 通常会被当做文件来操作.

文件中除了有数据内容之外 , 还有一部分信息如: 文件名 文件类型 文件大小等不作为文件的数据而存在 , 我们把这些信息称为描述文件的元信息.


1.2 树形结构组织和目录

随着文件数量的越来越多 , 如何高效的管理文件提上日程 , 为了将文件按照层级结构进行组织 , 树形存储结构应用而生 , 这样 , 一种专门用来存放管理信息的文件诞生了 , 也就是我们常说的文件夹(folder)和目录(directory).

 文件夹和目录中保存的就是我们之前提到的元信息.


1.3 文件路径(Path)

如何在文件中定位我们要找的唯一文件 , 就是当前要解决的问题 , 从树形角度的结构来看 , 树的每个节点都是从一条根开始一直到达节点的路径 , 这种描述方式被称为文件的绝对路径(absolute path). 

1).绝对路径风格:

以 c:  d: 盘符开头的路径

2).相对路径:

以当前所在的工作目录为基准 , 找到指定的路径.

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

例如: 定位到 d:/tmp/111

如果工作目录是 d:/  相对路径写作 ./tmp/111

如果工作目录是 d:/tmp  相对路径写作 ./111

如果工作目录是 d:/tmp/222/111  相对路径写作 ../111(..表示当前目录的上级目录)

如果工作目录是 d:/tmp/222/bbb/111  相对路径写作 ../../111


1.4 文本文件和二进制文件 

Windows 系统中对文件类型的区分非常详细 , word,exe,图片,视频,音频,源代码,动态库....这些不同的文件整体可用归为 文本文件 和 二进制文件.

  • 文本文件: 存的是字符串 , 都是由字符构成 , 每个字符都是通过一个数字来表示 , 这些字符一定是合法的 , 都是在我们指定字符编码的码表之内的数据.
  • 二进制文件: 没有任何限制 , 可以存储你想要的任何数据.

如果判断文本文件和二进制文件?

有一个简单粗暴地方式就是用记事本打开 , 因为记事本是文本文件 , 如果打开以后乱码了就是二进制文件 , 如果没乱码就是文本文件.


2.Java中操作文件--File类

Java 对文件的操作可以分为:

  • 1. 针对文件系统操作.(文件的创建 , 删除 , 重命名)
  • 2. 针对文件内容操作.(文件的读和写)

Java 标准库中 , 提供了一个 File类 , 专门用来进行文件系统操作 , 注意! 有File对象并不代表有实际的文件/目录.

2.1 属性

pathSeparator 是 File 中的一个静态变量 , 表示 / 或 \ 根据具体的系统表示.

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

2.2 构造方法

签名说明
File(File parent , String child)根据父目录+还在文件路径 , 创建一个新的 File 实例
File(String pathname)

根据文件路径创建一个新的 File 实例 , 路径可以是绝对路径

或相对路径

 第二种是最常见的构造方法.


2.3 方法

File 方法较为简单 , 通过方法名即可知道方法的作用.

修饰符及返回类型方法签名说明
StringgetParent()返回 File 对象的父目录文件路径
StringgetName()返回 FIle 对象的纯文件名称
StringgetPath()返回 File 对象的文件路径(可能是绝对也可能是相对)
StringgetAbsolutePath()返回 File 对象的绝对路径
StringgetCanonicalPath()返回 File 对象修饰的绝对路径
booleanisDirectory()判断 File 对象代码的文件是否是一个目录
booleanexists()判断 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()判断用户是否对文件有可写权限

示例一:测试 get系列的特点和差异

 public static void main(String[] args) throws IOException {
        File file = new File("d:/text.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.getCanonicalFile());
    }

 示例二:普通文件的判断

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

示例三:观察 deleteOnExit 的现象

public static void main(String[] args) throws IOException {
        File file = new File("some-file.txt");//要求该文件不存在才能看到相同的现象
        System.out.println(file.exists());
        System.out.println(file.createNewFile());
        System.out.println(file.exists());
        file.deleteOnExit();
        System.out.println(file.exists());

    }

当程序运行结束后文件才会被删除 , 类似于平常编写word文档时 , 系统会自动打开一个临时版本的word文档保存临时数据 , 当word文档被突然关闭 , 临时word文档也会随之关闭 , 当下次打开时会询问你是否要恢复临时文件中编辑的内容.

示例四: 观察目录的创建

public static void main(String[] args) throws IOException {
        File file = new File("some-dir");//要求该文件不存在才能看到相同的现象
        System.out.println(file.isDirectory());
        System.out.println(file.isFile());
        System.out.println(file.mkdir());
        System.out.println(file.isDirectory());
        System.out.println(file.isFile());
    }


3.文件内容的读写--数据流

针对文件内容 , 使用"流对象" 进行操作 , 将文件内容中的数据类比于水流. 这是因为读/写文件和接/灌水类似 , 水可以任意ml的接/灌 , 数据也可以任意byte的读/写.

Java 标准库的流对象 , 从类型上分成两个大类:

  • 1.字节流: InputStream OutputStream 以字符为单位读取.
  • 2.字符流: Reader Writer 以字符为单位读取.

这些类的使用非常固定 , 核心就是四个操作.

  • 1.打开文件.(构造对象)
  • 2.关闭文件.(close)
  • 3.读文件.(read)=>针对InputStream/Reader
  • 4.写文件.(writer)=>针对OutputStream/Writer

Tips: InputStream/OutputStream/Reader/Writer 都是抽象类 , 不能直接 new 还需要具体的实现类. 关于具体的实现类有很多 , 我们现在只关心从文件中读写 , 所以使用 FileInputStream/FileOutputStream/FileReader/FileWriter.


3.1 InputStream 概述

方法:

修饰符及返回值类型方法签名说明
intread()读取一个字节的数据 , 返回-1代表已经读完了
intread(byte[] b)

最多读取b.length 字节的数据到 b中 , 返回实际读到的

数据量;-1代表读完了

intread(byte[] b,int off,int len)最多读取 len-off 字节的数据到 b中 , 从 off开始 , 返回实际读到的数量;-1代表读完了.
voidclose关闭字节流

3.2 FileIntputStream 概述

构造方法:

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

示例一:read 读取文件第一版

读取完毕后返回 -1.

public static void main(String[] args) throws IOException {
        //创建InputStream对象时,使用绝对路径和相对路径都可以,也可以使用File对象
        InputStream inputStream = new FileInputStream("./text.txt");
        //进行读操作
        while (true){
            int b = inputStream.read();
            if(b == -1){
                //读完完毕
                break;
            }
            System.out.println((byte)b);
        }
        inputStream.close();
    }

文件中内容为 "hello"

读取结果:


示例二: read 读取文件第二版

read 读取文件的第二版需要提前准备好一个字节数组 , 然后将字节数组作为参数传给 read 方法 , 让 read 内部对这个数组进行填写.(此处参数相当于输出形参数)

 public static void main(String[] args) throws IOException {
        //创建InputStream对象时,使用绝对路径和相对路径都可以,也可以使用File对象
        InputStream inputStream = new FileInputStream("./text.txt");
        while (true){
            byte[] buffer = new byte[1024];
            int len = inputStream.read(buffer);
            System.out.println("len:" +len);
            if(len == -1){
                break;
            }
            //此时读到的结果就放到了 buffer中
            for (int i = 0; i < len; i++) {
                System.out.printf("%x\n",(byte)buffer[i]);
            }
        }
        inputStream.close();
    }

文案内容为 "你好"

 测试结果:

Tips: 由于硬盘的大小是有限的 , 我们不可能等到所有数据都读取到硬盘中再去处理 , 通常是一边读取一边处理 , 如果不这么做读满硬盘后 , 下一轮数据会覆盖上一轮数据.


两种读取方式的比较:

第二次读取版本需要一个字节数组作为缓冲区 , 这样做的目的是提高IO操作的效率 , 单次IO操作需要访问硬盘IO设备 , 如果像第一次读取版本 , 频繁的访问IO设备 , 会耗时更多.因此如果能缩短IO的次数就能提高程序整体的效率.

  • 第一个版本的代码是一次读取一个字节 , 循环的次数较高 , read的次数也很高.
  • 第二个版本的代码是一次读取1024个字节 , 循环次数降低了很多 , read的次数也变少了.

3.3 OutputStream概述

修饰符及返回值类型方法签名说明
voidwrite(int b)写入要给字节的数据
voidwrite(byte[] b)将 b 个字符数组中的数据全部写入 os 中
void

write(byte[] b,int off,

int len)

将 b 这个字符数组中从off 开始的数据写入 os 中,一共写len个
voidclose()关闭字节流
voidflush()刷新缓冲区

OutputStream 同样也是一个抽象类 , 要使用还需具体的实现类 , 我们此时只关心文件的读写 , 所以使用FileOutputStream.

代码测试:

public static void main(String[] args) throws IOException {
       OutputStream outputStream = new FileOutputStream("./text.txt");
       outputStream.write(97);
       outputStream.write(98);
       outputStream.write(99);
       outputStream.write(100);
       outputStream.close();
    }

对于 OutputStream 来说 , 默认情况下 , 打开一个文件 , 会先清空文件的内容再进行写操作.


如何区分 InputSream和 OutputSream?

input 和 output的方向是以CPU为中心来判断的.

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


close()操作的作用

close 操作的作用不仅仅是关闭文件释放资源还有刷新缓冲区.

在内核中使用PCB这样的数据结构来表示进程 , 一个线程对应一个PCB , 一个进程可对应一个也可对应多个PCB. PCB中有一个重要的属性 , 文件描述符表  , 相当于一个数组记录了该进程打开了哪些文件(如果一个进程里有多个线程多个PCB , 那么这些PCB共用一个文件描述符表) , 

 每次打开文件操作 , 就会在文件描述符表中申请一个位置 , 把这个信息放进去. 每次关闭文件也会把这个文件描述符表对应的表项给释放掉.如果没有及时释放文件 , Java中虽然有垃圾回收机制 , 但这个垃圾回收机制并不一定及时 , 那么就意味着文件描述符表可能会被占满 , 占满之后再打开文件就会打开失败.

使用 try-with-resource 简化close 写法.

这个写法虽然没有显示的写 close , 实际上只要 try 语句执行完毕 , 就可以自动执行到 close.

try ( OutputStream outputStream = new FileOutputStream("./text.txt");){
            outputStream.write(97);
            outputStream.write(98);
            outputStream.write(99);
            outputStream.write(100);
        } 

Tips: 不是随便一个对象放入 try() 中就可以释放 , 得实现Closeable接口的类. 


3.4 利用 Scanner 进行字符读取.

上述例子中我们可以看到对字符类型直接使用 InputStream 进行读取是非常困难的 , 所以使用 Scanner类可以更加简洁的完成该操作.

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

文件内容:

public static void main(String[] args) throws IOException {
        try(InputStream inputStream = new FileInputStream("./text.txt")) {
            Scanner scanner = new Scanner(inputStream,"UTF-8");
            System.out.println(scanner.next());
        }catch (IOException e){
            e.printStackTrace();
        }
    }


4.小程序练习

示例一:

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

类似于如下操作:

public class ThreadDemo9 {
    private static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) throws IOException {
        //让用户输入一个指定搜索目录
        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) {
        System.out.println("[sanDir]" + root.getAbsolutePath());
        // 1.列出当前路径下包含的内容
        File[] files = root.listFiles();//相当于看了一下目录中有啥?
        if (files == null) {
            // 空目录
            return;
        }
        // 2.遍历当前列出结果
        for (File file : files) {
            if (file.isDirectory()) {
                // 如果是目录进一步递归
                scanDir(file, nameToDelete);
            } else {
                if (file.getName().contains(nameToDelete)) {
                    System.out.println("确认是否要删除" + file.getAbsolutePath() + " 嘛?");
                    String choice = scanner.next();
                    if (choice.equals("y") || choice.equals("Y")) {
                        file.delete();
                        System.out.println("删除成功!");
                    } else {
                        System.out.println("删除失败!");
                    }
                }
            }
        }
    }
}

示例二

进行普通文件的复制

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);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Tips:OutputStream 在写文件时如果文件不存在 , 就会自动创建 , 但 InputStream 不行会抛出异常. 

示例三:

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

public class ThreadDemo11 {
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要扫描的目录: ");
        String dir = scanner.next();
        File rootDir = new File(dir);
        if(!rootDir.isDirectory()){
            System.out.println("输入目录错误 退出!");
            return;
        }
        System.out.println("请输入要找出的文件名中的字符");
        String token = scanner.next();
        List<File> ret = new ArrayList<>();
        //因为文件是树形结构 , 所以我们使用深度优先遍历
        scanDirWithContent(rootDir,token,ret);
        System.out.println("共找到"+ret.size()+"个符合条件的文件,它们分别是: ");
        for (File file:ret){
            System.out.println(file.getCanonicalFile());
        }
    }

    private static void scanDirWithContent(File rootDir, String token, List<File> ret) throws IOException {
        File[] files = rootDir.listFiles();
        if (files.length == 0||files == null){
            return;
        }
        for (File file:files){
            if(file.isDirectory()){
                scanDirWithContent(file,token,ret);
            }else{
                if(isContentContain(file,token)){
                    ret.add(file.getAbsoluteFile());
                }
            }
        }
    }

    private static boolean isContentContain(File file, String token) throws IOException {
        StringBuilder sb = new StringBuilder();
        try (InputStream is = new FileInputStream(file);
             Scanner scanner = new Scanner(is)){
             while (scanner.hasNextLine()){
                 sb.append(scanner.nextLine());
//                 sb.append("/r/n");
             }
        }
        return sb.indexOf(token) !=-1;
    }
}

 


阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6