[JavaEE]文件操作和IO


专栏简介: 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 String pathSeparator 依赖于系统的路径分隔符 , String 类型的表示
static char pathSeparator 依赖于系统的路径分隔符 , char 类型的表示

2.2 构造方法

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

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

或相对路径

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


2.3 方法

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

修饰符及返回类型 方法签名 说明
String getParent() 返回 File 对象的父目录文件路径
String getName() 返回 FIle 对象的纯文件名称
String getPath() 返回 File 对象的文件路径(可能是绝对也可能是相对)
String getAbsolutePath() 返回 File 对象的绝对路径
String getCanonicalPath() 返回 File 对象修饰的绝对路径
boolean isDirectory() 判断 File 对象代码的文件是否是一个目录
boolean exists() 判断 File 对象描述的文件是否真实存在
boolean isFile() 判断File对象代表的文件是否是一个普通文件
boolean createNewFile() 根据 File 对象自动创建一个空文件 , 成功创建后返回true
boolean delete() 根据 File 对象 , 删除该文件 , 成功删除后返回 true
void deleteOnExit()

根据 File 对象 , 标准文件将被删除 , 删除操作会到JVM运行结束时才执行.

String[] list() 返回 File 对象代表的目录下的所有文件名
File[] listFiles() 返回 File 对象代表的目录下的所有文件 , 以 File 对象表示
boolean mkdir() 创建 File 对象代表的目录
boolean mkdirs() 创建 File 对象代表的目录 , 如果必要会创建中间目录
boolean renameTo(File dest) 进行文件改名
boolean canRead() 判断用户是否对文件有可读权限
boolean canWrite() 判断用户是否对文件有可写权限

示例一:测试 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 概述

方法:

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

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

数据量;-1代表读完了

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

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概述

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

write(byte[] b,int off,

int len)

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

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;
    }
}

 


猜你喜欢

转载自blog.csdn.net/liu_xuixui/article/details/128676310