javaEE:文件IO

文件IO

什么是文件?

在计算机中,文件可以分成两个类别:

  • 文本文件
  • 二进制文件
文本文件

文本文件保存的内容都是文本,即字符串.但该字符串并不是直接能被读取的,而是需要字符编码(字符集)来进行解析,常见的码表有:UTF-8,GBK等等.如果把UTF-8字符码文本使用GBk字符码打开就会出现类似于 锟斤拷,烫烫烫等乱码

常见的文件文件有.txt.java.log

二进制文件

二进制文件保存的是二进制数据.无法通过读取字符串的方式进行读取.强制使用文本读取会发现打开的都是乱码

常见的二进制文件有.png.exe.mp4 甚至平常用来编写文件的.word文件也是二进制文件

什么是IO?

I:是 Input(输入)

O:是 Output(输出)

输入输出是一个相对的过程

  • 对于人来说:
    人通过键盘给电脑传入信息,这个是输出
    电脑通过显示器反馈给人信息,这个是输入

  • 对于电脑来说:
    人通过键盘给电脑传入信息,这个是输入
    电脑通过显示器反馈给人信息,这个是输出

    文件系统操作

java中有一个 File 类.其中封装了操作系统的文件系统API.通过 File 我们可以进行创建,删除,获取路径等操作.但并不能读写文件内的内容,读写是文件内容操作

File 常见 API 介绍
  1. 构造方法
    方法签名|作用
    -|-
    File(String pathname)|根据字符串路径来创建一个File对象
    File(String parent, String child)|根据父亲路径来创建对象
    File(File parent, String chind)|根据父亲File对象的路径来创建对象
  2. 判断方法
    方法签名|作用
    -|-
    exists()|判断当前文件是否存在
    isFile()|判断当前文件是不是普通文件
    isDirectory()|判断当前文件是不是一个文件夹
  3. 获取属性方法
    方法签名|作用
    -|-
    getName()|获取文件或文件夹的名字
    getParent()|获取文件当前位置
    getPath()|获取当前文件路径
    getAbsolutePatch()|获取当前文件绝对路径
    getCanonicalPatch()|获取当前文件整理后的绝对路径
    String[] list()|返回一个字符串数组.包含该文件夹下所有文件和文件夹名
    File[] listFiles()|返回一个File对象组.包含该文件夹下所有文件和文件夹的File对象
    length()|获取文件大小(字节).不能获取文件夹的大小
    lastModified()|获取最近修改的时间戳
  4. 创建删除方法
    方法签名|作用
    createNewFile()|当文件不存在时创建新的空文件,成功返回true
    mkdir()|创建单级文件夹,父目录不存在会创建失败
    mkdirs()|创建多级文件夹,父目录不存在会连同父目录一起创建
    delete()|删除文件或文件夹.若文件夹不为空会删除失败
    deleteOnExit()|当前对象运行结束后才进行delete()操作
  5. 重命名复制方法
    方法签名|作用
    -|-
    renameTo(File dest)|将当前文件或文件夹进行重命名操作,也可用于移动操作
代码示例:

大部分代码都同字面意思一样好理解.

此处示例getPath(),getAbsolutePatch(),getCanonicalPatch()三个方法和重命名复制方法

  1. 获取路径的方法
  • 当File 是绝对路径时,这三个方法的打印内容无差别
java 复制代码
    public static void main(String[] args) throws IOException {
        File file = new File("E:/code/file");
        //获取当前文件路径
        System.out.println(file.getPath());
        //获取当前文件绝对路径
        System.out.println(file.getAbsolutePath());
        //获取当前文件整理后的绝对路径
        System.out.println(file.getCanonicalPath());
    }

输出内容:

E:\code\file

E:\code\file

E:\code\file

  • 当 File 是相对路径时:
    getPatch()输出的还是相对路径
    getAbsoluePath()输出的是绝对路径
    getCanonicalPath()输出的是去掉相对路径中的"."的路径
java 复制代码
    public static void main(String[] args) throws IOException {
        File file = new File("./code/file");
        //获取当前文件路径
        System.out.println(file.getPath());
        //获取当前文件绝对路径
        System.out.println(file.getAbsolutePath());
        //获取当前文件整理后的绝对路径
        System.out.println(file.getCanonicalPath());
    }

输出内容:

.\code\file

E:\code\class-118-java\practice\J20251217.\code\file

E:\code\class-118-java\practice\J20251217\code\file

  1. 重命名和移动方法
  • 重命名
java 复制代码
public class Main {
    public static void main(String[] args) throws IOException {
        //创建父目录对象
        File parentFile = new File("E:/code/file");
        //创建旧文件对象
        File oldFile = new File("E:/code/file/old.txt");
        //创建新文件对象
        File newFile = new File("E:/code/file/new.txt");
        //创建父目录
        parentFile.mkdirs();
        //创建旧文件
        oldFile.createNewFile();
        //列出重命名前父目录下的所有文件
        System.out.print("重命名前的目录文件");
        System.out.println(Arrays.toString(new File("E:/code/file/").list()));
        //重命名old.txt为new.txt
        oldFile.renameTo(newFile);
        //列出重命名后父目录下的所有文件
        System.out.print("重命名后的目录文件");
        System.out.println(Arrays.toString(new File("E:/code/file/").list()));
        //运行完毕后删除文件
        newFile.deleteOnExit();
    }

运行结果:

重命名前的目录文件[old.txt]

重命名后的目录文件[new.txt]

  • 移动文件
java 复制代码
public class Main {
    public static void main(String[] args) throws IOException {
        //创建旧文件对象
        File oldFile = new File("E:/code/file/1.txt");
        //创建新文件对象
        File newFile = new File("E:/code/file/1/1.txt");
        //创建文件目录
        new File(newFile.getParent()).mkdirs();
        //创建旧文件
        oldFile.createNewFile();
        //移动前的文件
        System.out.println("移动前:");
        System.out.print("旧的的文件是否存在:");
        System.out.println(oldFile.exists());
        System.out.print("新的文件是否存在");
        System.out.println(newFile.exists());
        //移动1.txt到1/1.txt
        oldFile.renameTo(newFile);
        //移动后的文件
        System.out.println("移动后:");
        System.out.print("旧的的文件是否存在:");
        System.out.println(oldFile.exists());
        System.out.print("新的文件是否存在");
        System.out.println(newFile.exists());
        //运行完毕后删除文件
        newFile.deleteOnExit();
    }

运行结果:

移动前:

旧的的文件是否存在:true

新的文件是否存在false

移动后:

旧的的文件是否存在:false

新的文件是否存在true

文件内容操作

针对文件里的数据进行读写操作(只适用于普通文件,不适用目录文件)

目录文件的读写是由操作系统自己负责的

在文件内容操作中,读写是通过 来实现的

流有两大类别

  1. 字节流 :针对文件读写的基本操作是字节
    用于操作二进制文件
  2. 字符流 :针对文件读写的基本单位是字符
    用于操作文本文件
    一个字符是由多个字节构成的
什么是流?

流就像水流一样.一个文件就是一个水桶.读取文件时,就像是把硬盘中的水通过管道流到内存中的水桶里

假设要读取的水桶有1000ml水

那么我们可以读取10次100ml的水

也可以读取100次100ml的水

流读取的特点就是不管你怎么读取,最后读取的总和一定是相同的

字节流的使用

java中提供了两个字节流的核心类

  • InputStream 输入字节流类
  • OutputStream 输出字节流类
InputStream 输入字节流的使用

我们并不能直接 new InputStream 来创建该实例类.因为InputStream本质上是一个抽象方法.但java中已经写好了有关文件内容操作的类:FileInputStream

构造方法 作用
new InputStream(String name) 通过文件路径构造对象
new InputStream(File file) 通过File对象的路径来构造对象
API 作用
int read() 一次读取一个字节
int read(byte[] b) 一次读取多个字节
int read(byte[] b, int off. int len) 从off开始读,读取len个元素
void close() 释放资源
int read()|一次读取一个字节
  • 为什么这个方法一次只读取一个字节却使用int作为返回类型而不是byte?
  1. 读取内容与实际不符:java中byte的范围是-128~127.但如果在读取的字节流中恰好有一段11111111的二进制,通过byte强转为有符号的byte会变成-1
  2. 冲突:若使用byte,由原因1可知会在读取时遇到-1,而-1也正好是文件结束的标志,这会导致一但读取到11111111的二进制就会直接结束读取
    而当我们使用int来表示时,对于读取到的0~255范围的值,java会在前面加上24个0来变成一个int类型.如果读取到真正的文件结尾(即-1时),那就返回-1.这样除了读到文件结尾的-1时,才会返回真正的-1
  • 说回这个方法:
    这个方法并不常用.原因是一次读取一个字节的再面对读取1mb的文件时IO开销太大了(需要操作1048576次).所以更多的是使用int (read(byte[]) b)这个方法
  • 代码示例:
java 复制代码
public class Main {
    public static void main(String[] args) {
        //提前在该目录下创建对应的文件,内容填写为abc
        try(InputStream inputStream = new FileInputStream("E:/code/file/1.txt");) {
            while(true) {
                int data = inputStream.read();
                if(data == -1) {
                    break;
                }
                System.out.print((char)data);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

输出内容:

abc

int read(byte[] b)|一次读取多个字节

通过传入一个字节数组,可以达到一次读取多个字节的效果.能够大大减少硬盘的IO操作

  • 代码示例:
java 复制代码
public class Main {
    public static void main(String[] args) {
        //提前在该目录下创建对应的文件,内容填写为abc
        try(InputStream inputStream = new FileInputStream("E:/code/file/1.txt")) {
            while(true) {
                //data的长度可以自定义
                byte[] data = new byte[1024];
                int  n = inputStream.read(data);
                if(n == -1) {
                    break;
                }

                for (int i = 0; i < n; i++) {
                    //不能直接打印内容,得转换为char
                    //System.out.print(data[i]);
                    //正确的打印方法
                    System.out.print((char)data[i]);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

输出内容:

abc

int read(byte[] b, int off. int len)|从off开始读,读取len个元素

我们先看下段的代码

  • 代码示例:
java 复制代码
    public static void main5(String[] args) {
        //提前在该目录下创建对应的文件,内容填写为abc
        try(InputStream inputStream = new FileInputStream("E:/code/file/1.txt")) {
            while(true) {
                //data的长度可以自定义
                byte[] data = new byte[1024];
                int  n = inputStream.read(data,0,1);
                if(n == -1) {
                    break;
                }

                for (int i = 0; i < n; i++) {
                    System.out.print((char)data[i]);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

输出内容:

abc

对于这段代码,如果每次循环只读取从0位置开始的第一个元素,那么这个理应死循环输出a.但实际输出内容却是abc

造成以上的原因是文件指针

InputStraem中有一个叫文件指针的东西.它会记住上次读到了哪里.对于上段代码

  1. 第一次进入循环时文件指针在0处,读取文件指针的后一位是a.然后文件指针走到下一个未被读取的字节
  2. 第二次进入循环时文件指针在1处.读取文件指针的后一位是b,然后文件指针走到下一个未被读取的字节
  3. 第二次进入循环时文件指针在2处.读取文件指针的后一位是c,然后文件指针走到下一个未被读取的字节
  4. 第二次进入循环时文件指针在3处.读取文件指针的后一位是结束标志(-1),退出循环
    所以这个方法看起来虽然和substring很像,但实际运行的并非一个逻辑
  • 正确的代码示例:
java 复制代码
    public static void main(String[] args) {
        //提前在该目录下创建对应的文件,内容填写为abc
        try(InputStream inputStream = new FileInputStream("E:/code/file/1.txt")) {
            byte[] data = new byte[1024];
            //去掉while循环
            int n = inputStream.read(data,0,1);
            for (int i = 0; i < n; i++) {
                System.out.println((char)data[i]);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

输出内容:

a

void close()|释放资源

通过前面三个方法的代码示例,可能已经忘掉了还需要close().也可能会认为这个colse()方法是可以省略的.毕竟JVM有内存回收机制

但事实是对于IO操作,JVM的内存回收机制是不会回收IO操作的实例的.原因是IO操作是对硬盘打交道,而不是对内存打交道,显然内存和硬盘不是一个东西

因此,我们还是得手动close().close()方法一般放在finally{}代码块中

  • 代码示例:
java 复制代码
public class Main {
    public static void main(String[] args) {
        //提前在该目录下创建对应的文件,内容填写为abc
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream("E:/code/file/1.txt");
            while (true) {
                int data = inputStream.read();
                if(data == -1) {
                    break;
                }
                System.out.print((char)data);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

那为什么前面三个方法没有使用close()方法?

这段代码对比上述代码我们会发现一处很明显的不一样.创建实例化操作被放在了try{}代码块内容,而前面三个方法的代码都是放在try()参数内部.区别也就在这,如果将实例化操作放在try()参数中,在执行完实例的逻辑后会自动调用close(),这也就是为什么前面的代码没有写colse()的原因.虽然看起来没有释放资源,但其实已经自动释放资源了

OutputStream 输出字节流的使用

和InputStream类似.我们并不能直接 new OutputStream 来创建该实例类.因为OutputStream本质上是一个抽象方法.但java中已经写好了有关文件内容操作的类FileOutputStream:

构造方法 作用
new FileOutputStream(String name) 通过字符串路径构造对象.若路径存在会覆盖原本内容
new FileOutputStream(String name, boolean append) 作用同上,但若append内容为true,则在文件末尾进行追加而不覆盖
new FileOutputStream(File file) 通过File对象获取路径
API 作用
int write() 一次写入一个字节
int write(byte[] b) 一次写入多个字节
int write(byte[] b, int off. int len) 从off开始读,写入len个元素
void close() 释放资源
代码示例:
java 复制代码
public class Main {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("E:/code/file/1.txt");
            InputStream inputStream = new FileInputStream("E:/code/file/1.txt")){
            //写入文件内容
            byte[] bytes = {97,98,99};
            outputStream.write(bytes);
            //写入后读取
            while(true) {
                int data = inputStream.read();
                if(data == -1) {
                    break;
                }
                System.out.print((char)data);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

输出内容:

abc

对于上段代码,多次运行后仍然是abc

若把OutputStream的构造参数中添加一个append参数,就会发现每运行一次都会多一段abc的内容

OutputStream和InputStream的细节区分
  1. 当文件不存在时:
    FileInputStream会直接抛出找不到文件的异常
    FileOutputStream会自动创建改文件,前提是父级文件夹存在
  2. 当文件中存在数据时:
    FileInputStream不会修改文件
    FileOutputSteram默认会直接覆盖原文件,需要手动添加append参数才不会默认覆盖原文件
  3. flush()机制:
    java中为了提高效率写入的数据都会先暂存起来,存到一定程度才会真正写入硬盘.这会导致有些情况如果不调用flush()或不调用close()(close()会自动执行flash()操作),既是程序显示执行完毕,但可能并未写入硬盘
字符流的使用

字符流的本质还是字节流.但字符流会整理读取到的字节,通过编码表将这些字节数据转化为字符数据(也就是我们熟悉的中文)

在java中同样提供了两个字符流的核心类

  • Reader 读取字符流
  • Writer 写入字符流
Reader 读取字符流的使用

同样的,Reader和InputStream一样.也是一个抽象类.java中主要使用FileReader类来进行实例化

构造方法 作用
FileReader(String FileName) 传入一个文件路径
FileReader(File file) 传入一个File对象来获取路径
FileReader(String FileName, Charset charSet) 传入一个文件路径并指定其对应编码表(如StandardCharsets.UTF_8)

若给定的路径无对应文件会直接抛异常

API 作用
int read() 一次读取一个字符
int read(char[] cbuf) 一次读取多个字符
void close() 释放资源
int read()|一次读取一个字符
java 复制代码
public class Main {
    public static void main(String[] args) {//// 提前在该目录下创建对应的文件 , 内容填写为 你好世界
        try(Reader reader = new FileReader("E:/code/file/1.txt")) {
            while(true) {
                int data = reader.read();
                if(data == -1) {
                    break;
                }
                System.out.print((char)data);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

输出内容:

你好世界

int read(char[] cbuf)|一次读取多个字符
java 复制代码
public class Main {
    public static void main(String[] args) {//// 提前在该目录下创建对应的文件 , 内容填写为 你好世界
        try(Reader reader = new FileReader("E:/code/file/1.txt")) {
            while(true) {
                char[] data = new char[1024];
                int n = reader.read(data);
                if(n == -1) {
                    break;
                }
                for (int i = 0; i < n; i++) {
                    System.out.println(data[i]);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

输出内容:

Writer 写入字符流的使用
构造方法 作用
FileWriter(String FileName) 自动覆盖原文件
FileWriter(String FileName, boolean append) 若append参数设置为true,则更改为追加模式,不会清空原文件
FileWriter(String fileName, Charset charset) 指定编码集,但仍然会清空原文件(可再添加一个true解决)
API 作用
void write(intc) 写入一个字符,虽然传入参数是int,但会根据对应编码转换为对应字节
void write(char[] cbuf) 写入多个字符
void write(String str) 写入一个字符串
void write(String str, int off, int len) 写入字符串中的off索引后的len个字符
void close() 释放资源
void write(intc)|写入一个字符
java 复制代码
public class Main {
    public static void main(String[] args) {
        try(Reader reader = new FileReader("E:/code/file/1.txt");
            Writer writer = new FileWriter("E:/code/file/1.txt")) {
            writer.write('你');
            writer.write('好');
            writer.write('世');
            writer.write('界');
            //使用flush()方法,否则无输出
            writer.flush();
            char[] data = new char[1024];
            int n = reader.read(data);
            for (int i = 0; i < n; i++) {
                System.out.print(data[i]);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

输出内容:

你好世界

void write(char[] cbuf)|写入多个字符
java 复制代码
    public static void main(String[] args) {
        try(Reader reader = new FileReader("E:/code/file/1.txt");
            Writer writer = new FileWriter("E:/code/file/1.txt")) {
            char[] c = {'你','好','世','界'};
            writer.write(c);
            //使用flush()方法,否则无输出
            writer.flush();
            char[] data = new char[1024];
            int n = reader.read(data);
            for (int i = 0; i < n; i++) {
                System.out.print(data[i]);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

输出内容:

你好世界

void write(String str, int off, int len)|写入字符串中的off索引后的len个字符
java 复制代码
public class Main {
    public static void main(String[] args) {
        try(Reader reader = new FileReader("E:/code/file/1.txt");
            Writer writer = new FileWriter("E:/code/file/1.txt")) {
            String str = "你好世界";
            writer.write(str);
            //使用flush()方法,否则无输出
            writer.flush();
            char[] data = new char[1024];
            int n = reader.read(data);
            for (int i = 0; i < n; i++) {
                System.out.print(data[i]);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

输出内容:

你好世界

相关推荐
小Y._2 小时前
ConcurrentHashMap高效并发机制深度解析
java·并发·juc·concurrenthashmap
tang_jian_dong2 小时前
springboot + vue3 集成tianai.captcha验证码
java·spring boot·spring
Traving Yu2 小时前
JVM 底层与调优
java·jvm
三棱球2 小时前
Java 基础教程 Day2:从数据类型到面向对象核心概念
java·开发语言
indexsunny2 小时前
互联网大厂Java面试实录:微服务+Spring Boot在电商场景中的应用
java·spring boot·redis·微服务·eureka·kafka·spring security
wuminyu2 小时前
专家视角看Java线程生命周期与上下文切换的本质
java·linux·c语言·jvm·c++
程序猿乐锅2 小时前
Java第十三篇:Stream流
java·笔记
林三的日常2 小时前
SpringBoot + Druid SQL Parser 解析表名、字段名(纯Java,最佳方案)
java·spring boot·sql
deviant-ART2 小时前
java stream 的 findFirst 和 findAny 踩坑点
java·开发语言·后端