【Java EE初阶十一】文件操作(IO)

1. 认识文件

所谓的文件是一个广义的概念,可以代表很多东西;在操作系统里面,会把很多的硬件设备和软件设备都抽象成"文件",统一进行管理;但是大部分情况下,我们读到的文件,都是指硬盘的文件,文件就相当于是针对"硬盘"数据的一种抽象;

1.1 简单了解硬盘

1.1.1 机械硬盘HDD

1.1.2 固态硬盘SSD

固态硬盘里面就都是集成程度很高的芯片,且固态硬盘就要比机械硬盘的效率高很多;

我们在进行服务器开发的过程中,涉及到的硬盘有的是机械硬盘有的是固态硬盘,特别是一些用来存储大规模数据的机器,任然是机械硬盘为主,但是固态硬盘的读写速速要比内存慢很多;

内存和硬盘的区别:

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

1.1.3 文件的操作方式

文件的操作方式是通过操作硬盘来实现的,一台计算机上有很多的文件,这些文件是通过"文件系统"(有由操作系统所提供的模块)来进行组织的,操作系统使用"目录"这样的结构来组织文件;

即可以使用目录的层次结构(路径)来描述文件所在的位置

F:\B站下载\【水灰】我们再一起去迪士尼吧

如上所示的一个字符串,体现了当前文件在哪个目录中。

  1. 绝对路径:就是以C,D盘开头的,这种路径成为"绝对路径"
  2. 相对路径:需要先指定一个目录,从基准目录出发,看看沿啥样的路径能够找到指定文件,此时所涉及的路径就是"相对路径",

1.2 文件的类型

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

1、文本(文件中保存的数据,都是字符串,保存的内容都是合法的字符串)

2、二进制(文件中保存的数据,仅仅是二进制数据,不要求保存的内容是合法的字符)

合法的字符->字符集/字符编码(主流的就是utf8,一个规定的表格,里面规定了什么样的字符对应什么样的编码),如果我们的文件时utf8编码的,此时文件中的每一个数据都是合法的utf8编码的字符,就可以认为这个文件是文本文件了;如果存在一些不是utf8合法字符的情况,就是二进制文件了;

本身计算机存储的数据都是二进制的;

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

直接使用记事本来打开文件,如果打开之后是乱码,文件是二进制文件;否则是文本文件,记事本就是尝试按照字符的方式来展示内容,这个过程就会自动查码表,如下图所示:

很多文件都是二进制的文件,docx,png......等文件都属于二进制文件;区分文本文件和二进制文件是十分重要的;我们在写代码的时候,文本文件和二进制文件,代码编写的方式是不同的;

Java针对文件的操作,分成两类:

1、针对文件系统的操作-> File

即创建文件,删除文件,判定文件是否存在,判定文件类型,重命名

2、针对文件内容的操作->流对象

即读文件,写文件;

2. File 概述

2.1 file的属性

2.2 file类的构造方法

一个file对象,就表示一个硬盘上的文件,在构造对象的时候,就需要把这个文件的路径指定进来(使用绝对、相对路径都可以)

File提供的核心方法:文件名=前缀+扩展名->使用路径构造file对象,一定要把前缀和扩展名都写上;一个文件系统上都会对文件有权限的限制(约定了这个文件,那些用户可以读,那些用户可以写),我们是创建file对象的时候,就会使用到java提供的import java.io.File;该包里面的io分别表示:

I:input

0:output

2.3 file的相关方法

一般来说,在使用文件的内容访问时候,io操作都需要抛出import java.io.IOException;下图是关于file类方法的调用:

q:关于下图输出的数值是文件的内存地址吗?

a:首先在jvm上层,java代码中是没有任何方法获取到"内存地址"的,想要获取内存地址,只能靠native方法,进入jvm内部,通过c++代码获取到;其次我们获取到的字符串是哈希值,是通过调用hashcode的方法获取到的

3. 流对象

基于文件数据传输的特点,将文件中传输的数据称之为"文件流"

在标准库中,提供的读写文件的流对象,不是一两个类,而是有很多类,但是实际上可以吧这么多类归结于两个类中:

1、字节流:(对应着二进制文件)

每次读写的最小单位是字节(8bit)

提供了两个父类:inputstream,outputstream

提供了两个父类:reader,writer

2、字符流:(对应着文本文件)

每次读写的最小单位是字符(一个字符对应很多个字节,主要是取决于当前的读取对象的字符集,gbk中一个中文字符对应两个字节;utf8中一个中文字符对应3个字节)

字符流本质上是对字节流进行了又一层封装,字符流,就能够自动的帮我们把文件中的几个相邻的字节,转换成一个字符(帮我们完成了一个自动差字符集表)

3、关于输入和输出的识别:

论将数据保存到硬盘中,这个过程是输入还是输出?

1、站在硬盘的角度,输入

2、站在cpu的角度,输出;(我们要带入到cpu的视角)

3.1 reader 概述

Reader是一个抽象类,不能new实例,只能new一个子类,java标准库提供了一个现成的类filereader类。其创建方法如下图所示:

创建reader对象的过程,就是"代开文件"的过程,下面是关于reader的三种方法:

1、五参数read:一次只读取一个字符;

2、一个参数read:一次读取若干个字符,回答参数指定的cbuf数组给填充满

3、三个参数read:一次读取若干个字符,回答参数指定的cbuf数组中的从off这个位置开始,到len这么长的范围内尽量填满;代码细节如下所示:

在java标准库内部,对于字符编码是进行了很多处理工作的,如果只使用char,此时使用的字符集,固定的就是unicode;如果是使用string,此时就会自动的把每一个unicode转换为utf8;

char【】 c ->包含的每一个字符都是unicode,一旦使用这个字符数组构造成string,string s = new string(c),就会在内部把每一个字符都转换成utf8

把多个unicode连续放到一起,是很难区分从哪里到哪里地一个完整的字符的,utf8是可以做到区分的;utf8可以认为是针对连续多个字符进行传输时候的一种改进方案

对于reader.read( )这个方法里面,应该是往这个read里面传入的是一个空的字符数组(不是null,而是没有实际意义数据的数组),然后由read方法内部,对这个数组内容进行填充,此时的cbuf这个参数,称为"输出型参数"

如果文件为空,就直接返回-1了;

当一个文件读取完了之后,我们要记得进行关闭close,使用colse方法,最主要的就是为了释放文件描述符;

pcb这里会包含很多的属性:pid,内存指针,文件描述符表(本质上是由数组构成的顺序表),一个进程每一次打开一个文件,就需要早这个表里分配一个元素,但是这个数组的长度是有限的,如果我们的代码中在运行的过程中,一直在进行打开文件而不能及时的关闭,就会是这个表里面的元素越来越多,最终就昂这个数组沾满,后续在尝试打开文件就会出现报错;

这样的错误就是文件资源泄露,类似于内存泄漏,在java中我们打开的文件还是需要我们进行手动释放的,此时的代码如下所示:

java 复制代码
package io;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

// Reader
public class Demo7 {
    public static void main(String[] args) throws IOException {
         Reader reader = new FileReader("d:/test.txt");
            // 2. 一次 read 多个字符
            while (true) {
                char[] cbuf = new char[3];
                // n 表示当前读到的字符的个数.
                int n = reader.read(cbuf);
                if (n == -1) {
                    // 读取完毕
                    break;
                }
                System.out.println("n = " + n);
                for (int i = 0; i < n; i++) {
                    System.out.println(cbuf[i]);
                }
            }
            // 3. 一个文件使用完了, 要记得, close !!!
            reader.close();              
    }
}

但是代码如上所示,我们的代码依旧存在文件资源泄露的风险,因为如果我们的代码在前期出现逻辑异常而进行抛出异常处理的话,就执行不到close方法,所以对上述代码进行改进处理,如下图所示:

3.2 writer 概述

writer对象主要是写入文件,默认情况下就会把原有的文件内容清空掉,如果不想清空,就需要在构造方法中加个参数,如下图所示;

关于write方法的种类如下图所示:

System.in ====》inputstream;

关于write部分代码如下所示:

java 复制代码
package io;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;

public class Demo12 {
    public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("d:/test.txt")) {
            // 这就相当于把字符流转成字节流了.
            PrintWriter writer = new PrintWriter(outputStream);
            writer.println("hello");
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码分析:

缓冲区:printwriter这样的类,在进行写入的时候,不一定直接写入硬盘,而是先把数据写在一个内存构成的"缓冲区"中(buffer)

我们引入缓冲区,目的是为了提高效率;把数据写入内存是非常快的;把数据写到硬盘,是非常慢的;

这样会导致一个新的问题:当我们写入缓冲区后,如果还没来得及把缓冲区里的数据写进硬盘,进程就结束了,此时数据就会丢失;没有正真的写入硬盘;(进程结束,该内存就会释放)->为了确保数据会被写入硬盘,就应该在合适的时机,使用flush方法进行手动刷新缓冲区;

该fiush操作,可以理解为"刷新缓冲区",将我们存放在内存缓冲区中的数据冲刷到硬盘中;

4. 代码实例

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

4.1 案例分析

文件系统操作:

1、list列出目录内容

2、判定文件的类型

3、删除文件

所谓的扫描指定文件,就是找到目录中的所有文件,以及子目录中的所有文件,只要遇到子目录都能往里面找->采用递归的方式,把所有的子目录都给扫描一遍;

4.2 代码实现

java 复制代码
package io;

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

public class Demo13 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 1. 先让用户输入一个要扫描的目录
        System.out.println("请输入要扫描的路径: ");
        String path = scanner.next();
        File rootPath = new File(path);
        if (!rootPath.isDirectory()) {
            System.out.println("您输入的扫描的路径有误!! ");
            return;
        }
        // 2. 再让用户输入一个要查询的关键词.
        System.out.println("请输入要删除文件的关键词: ");
        String word = scanner.next();
        // 3. 可以进行递归的扫描了.
        //    通过这个方法进行递归.
        scanDir(rootPath, word);
    }

    private static void scanDir(File rootPath, String word) {
        // 1. 先列出 rootPath 中所有的文件和目录.
        File[] files = rootPath.listFiles();
        if (files == null) {
            // 当前目录为 null, 就可以直接返回了.
            return;
        }
        // 2. 遍历这里的每个元素, 针对不同类型做出不同的处理.
        for (File f : files) {
            // 加个日志, 方便观察当前递归的执行过程.
            System.out.println("当前扫描的文件: " + f.getAbsolutePath());
            if (f.isFile()) {
                // 普通文件. 检查文件是否要删除. 并执行删除动作.
                checkDelete(f, word);
            } else {
                // 目录. 递归的再去判定子目录里包含的内容
                scanDir(f, word);
            }
        }
    }

    private static void checkDelete(File f, String word) {
        if (!f.getName().contains(word)) {
            // 不必删除, 直接方法结束
            return;
        }
        // 需要删除
        System.out.println("当前文件为: " + f.getAbsolutePath() + ", 请确认是否要删除(Y/n): ");
        Scanner scanner = new Scanner(System.in);
        String choice = scanner.next();
        if (choice.equals("Y") || choice.equals("y")) {
            // 真正执行删除操作
            f.delete();
            System.out.println("删除完毕!");
        } else {
            // 如果输入其他值, 不一定非得是 n, 都会取消删除操作.
            System.out.println("取消删除!");
        }
    }
}

ps:本篇的内容到这里就结束了,如果感兴趣的话就请一键三连哦!!!

相关推荐
m0_5873830012 分钟前
智慧场馆解决方案实战指南:从系统架构到落地部署全解析
java·spring boot·架构·系统架构
ZC跨境爬虫17 分钟前
LeetCode 13. 罗马数字转整数(多解法详解 + Java Python 实现)
java·python·leetcode
省长22 分钟前
Sa-Token v1.46.0 发布 🚀,来看看有没有令你心动的功能!
java·后端·开源
LayZhangStrive1 小时前
融360 一面
java·面试·后端开发
城管不管1 小时前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
Java小白笔记1 小时前
Windows系统免软件命令激活
java·网络·人工智能·windows·ai·ai编程
爱敲键盘的猴子1 小时前
深入理解 Java HashMap
java·hashmap
霸道流氓气质1 小时前
Java开发者AI编程常用MCP、Skills、Rules全景汇总
java·开发语言·ai编程
天涯醉青楼2 小时前
Spring Boot 接口返回 Bean 时字段名“变形”的深度剖析与解决方案
java
步行cgn2 小时前
MyBatis 高级映射与延迟加载完全指南
java·tomcat·mybatis