文件操作 和 IO 流

目录

一. 了解硬盘

二. 认识文件

2.1 文件含义

2.1.1 狭义文件

2.1.2 广义文件

2.2 树型结构组织 和 目录 

2.3 文件路径

2.4 文本文件与二进制文件

三. 文件系统操作

3.1 属性

3.2 构造方法

3.3 文件操作方法

四. 文件内容操作

4.1 流(Stream) 

4.2 输入 和 输出 方向

4.3 文件打开与关闭 

4.3.1 文件描述符表

4.3.2 进程核心 --> PCB

4.3.3 close 要点

4.4 读文件

4.4.1 read()

4.4.2 read(byte[] b)

4.4.3 read(byte[] b, int off, int len)

4.5 写文件

4.6 字符流 

4.7 格式化操作


一. 了解硬盘

硬盘 和 内存 的对比:

  1. 硬盘存储空间大 内存小
  2. 硬盘速度慢 内存快  (硬盘之所以慢,和它硬件物理结构直接相关)
  3. 硬盘成本低 内存高
  4. 硬盘能持久化存储,内存断电后数据丢失 

主流的两种硬盘:机械硬盘 (服务器领域)  和 固态硬盘 (现在的电脑)

    机械硬盘(HDD) 通过一个或多个磁性盘片旋转来存储数据,并使用机械臂上的磁头读写数据磁头改变磁性的方向来写入数据,感应磁性的变化来读取数据)。数据以磁性形式记录在盘片的磁性层上。

    由于涉及到物理移动,机械硬盘读写速度受限于磁头的移动速度和盘片的旋转速度,速度相对较慢,一般顺序读写速度在100MB/s~200MB/s之间。

    机械硬盘 在进行顺序读写时(不需要移动磁头,盘片转动即可),效率相对较高;在进行随机读写时(磁头移动),效率会比较低。


    固态硬盘(SSD) 使用 闪存芯片 来存储数据(更接近于“内存”,通过大规模的集成电路,实现的存储功能),数据以电子信号的形式存储在闪存单元中。

    与传统的机械硬盘相比,固态硬盘没有机械运动部件,因此具有更快的读写速度、更低的功耗和更高的抗震性。

    固态硬盘 一般顺序读写速度可达500MB/s甚至更高,随机读写速度更是远超 机械硬盘,是机械硬盘的数十甚至上百倍。

    虽然固态硬盘的随机读写能力有所改善,但仍然非常逊色于内存。

二. 认识文件

2.1 文件含义
2.1.1 狭义文件

    “狭义文件” 通常指的是存储在计算机存储介质上的数据集合,具有明确的文件名,并且通常是用户可以直接访问和处理的数据。

    在一个电脑上,有很多文件,都是需要靠操作系统进行组织管理的。操作系统专门有一个模块 --> “文件系统”,一般是通过 “文件资源管理器” 这个程序 观察到 “文件系统” 管理的文件。

    文件除了有数据内容之外,还有一部分信息,例如文件名、文件类型、文件大小等,并不作为文件的数据而存在,我们把这部分信息可以视为文件的元信息。

狭义文件主要包含以下几类:

  • 普通文件:这是最常见的文件类型,如文本文件、图片文件、音频文件和视频文件等
  • 程序文件:包含可执行代码的文件,如 .exe文件、.dll文件等
  • 文档文件:用户创建的文档,如Word文档、Excel电子表格等。

狭义文件的特性通常包括:

  • 有具体的文件名和扩展名
  • 占用存储空间,保存在硬盘上,并且可以被复制、移动、删除和修改
  • 可以通过 “文件系统” 进行管理
2.1.2 广义文件

    计算机上的很多 硬件设备 、软件资源,在操作系统中都会视为 “文件”。比如:标准输入(键盘 System.in)标准输出(控制台 System.out)、打印机 、网卡 ( 操作网络的代码 和 操作文件的代码 都是非常类似的)

广义文件的概念比狭义文件更为广泛,它不仅包括狭义文件,还包括以下几类: 

  • 目录(文件夹):在文件系统中,目录是一种特殊的文件,它用于组织和管理其他文件和子目录。
  • 设备文件:在类Unix操作系统中,设备(如打印机、硬盘、串口等)被视为文件,可以通过文件系统的接口进行访问。
  • 管道文件:在类Unix系统中,管道文件用于进程间通信,它允许一个进程的输出直接作为另一个进程的输入。
  • 套接字文件:用于网络通信的文件,允许不同计算机上的进程进行数据交换
  • 特殊文件:如系统文件、链接文件等,它们在操作系统中具有特殊用途。

广义文件的特点包括:

  • 不仅限于用户数据,还包括系统数据和设备接口
  • 可以是数据集合,也可以是抽象的访问点或接口
  • 在操作系统中,广义文件通常具有统一的访问接口,即使它们的实际功能和用途可能大相径庭

2.2 树型结构组织 和 目录 

    随着文件越来越多,对文件的系统管理也被提上了日程,如何进行文件的组织呢,一种合乎自然的想法出现了,就是按照层级结构进行组织也就是我们数据结构中学习过的树形结构。这样,一种专门用来存放管理信息的特殊文件诞生了,也就是我们平时所谓文件夹(folder)或者目录(directory)的概念。

2.3 文件路径

 基于上述结构,就可以找到某个文件在硬盘上的具体位置。


(这一串目录,就描述了文件所在的位置)

 
(路径中,约定使用分隔符,分割目录和目录)

—— \ ( 反斜杠) ——         —— / ( 斜杠 )  ——

    绝大部分的系统,都是使用 /(斜杠) 作为目录的分隔符,只有 Windows 是既能使用 / 也能使用 \ 。( 之所以 Windows 使用 \ (反斜杠)是因为 Windows 的前身 DOS 系统,就是使用 \ ,是一个历史遗留问题)
 

< 继承自 Unix 使用  /  ( 斜杠 ) >

  • Linux => 源于 Unix 演化出来的
  • MacOs => Unix的一个分支 演化出来的
  • Android / IOS 都是类似的
  • ....................

Windows 源于 DOS 使用  \ ,为了能够兼容其他系统的用户,Windows 能够兼容 /


在编程的时候,字符串中,如果是 \ ,必须通过 \\ 转义字符表示,这样就太麻烦了。但是这件事情在最近几年也得到了改善:主流编程语言都支持语法特性 raw string (原始字符串),不必写转义字符的。

表示路径的两种风格: 

  • 相对路径:谈到相对路径,必然有一个“参考系”,就是有一个 “基准路径” 或 “工作路径”
    如果基准目录不同,对应的相对路径也是不同的

    比如 约定 D:\JAVA\javaee\file&io\src 为基准路径,file 目录的相对路径就是 .\file
            约定 D:\JAVA\javaee\file&io 为基准路径,file 目录的相对路径就是 .\src.\file

    相对路径的前提,一定要明确“工作路径”!
  •        
  • 绝对路径:从盘符开始,一直到文件名结束  D:\JAVA\javaee\file&io\src\file ;如果是 Linux,从 / (根目录)

(..表示当前目录的上一级目录,即父节点。命令行输入 cd.. 会从当前目录向上移动一级)
(. 表示当前目录,即当前节点。命令行输入cd. 不会改变位置) 

    后续写代码,常用的路径形式,都是相对路径,使同一套代码兼容不同的机器;绝对路径,只适合于自己的机器,一旦把代码换到别人的机器上,如果代码依赖绝对路径,就可能在别人的电脑上跑不起来(你这里的绝对路径只在自己的电脑上存在,别人的机器上不存在)

    一个程序能够正确运行,不仅仅取决于代码,也取决于代码的运行环境(操作系统版本,第三方库,配置文件,数据文件,其他目录的文件,硬件设备.....)。因此,以后测试代码的时候,要多找一些机器,对代码做更充分的测试(尤其是使用刚新装的系统来测试)

2.4 文本文件与二进制文件
  •     文本文件 是一种包含普通字符数据的文件,这些字符通常是可读的,并且按照一定的编码格式(如ASCll、UTF-8等)存储。虽然叫做文本文件,但是文件里存储的所有内容还是二进制,只不过这些二进制“有据可查”。可以用普通的文本编辑器(如记事本、Notepad++等)打开和编辑文本文件。
  •     二进制文件 是一种包含以二进制形式(0和1)存储的数据的文件。这些数据可能代表文字、图片、程序指令等(.exe,  .dll,  .mp3,  .class......),文件内容在字符集对应的表格中不可查。二进制文件通常需要特定的软件或程序来正确解读和显示,用普通的文本编辑器打开 会出现看不懂的乱码。

    虽然.docx文件包含了大量的文本内容,但是这些文本内容并不是以纯文本形式存储的,实际上是二进制文件。word 是一个 “富文本编辑器”。如果将.docx文件扩展名改为 .zip ,就可以看到里面包含的XML文件和其他资源(如图片、字体等)

    针对文本文件来说,Java 已经进行了很多内置处理了。虽然文本文件底层仍然是 二进制存储,Java 把文件读取出来的时候,自动查询码表,把二进制内容转成一个个字符;而二进制文件,则没有上述转换过程。

文件操作主要有两大类:

  • 文件系统操作(创建文件,删除文件,创建目录,重命名文件,判定文件是否存在......)
  • 文件内容操作(读文件、写文件)

    针对文件系统操作,在 C 的标准库中,并没有提供,如果确实想操作文件,就需要使用系统提供的 API。

    Java中提供了 File类,进行文件系统操作。这个对象 会使用 “路径” 进行初始化,从而表示一个具体的文件 ( 这个文件可以存在,也可以不存在 ) ,并基于这个对象进行后续操作

三. 文件系统操作

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

 路径分隔符:Windows 系统  ==>     Linux / Mac 系统 ==>  /

3.2 构造方法
签名说明
File(File parent, String child)根据父目录+孩子文件路径,创建一个新的 File 实例
File(String pathname)根据文件路径创建一个新的File 实例,路径可以是绝对路径或者相对路径
File(String parent, String child)根据父目录+孩子文件路径,创建一个新的File 实例,父目录用路径表示

    如果 File 中写的是 “相对路径” 的时候,就需要明确基准目录是什么。代码中,相对路径的基准目录取决于运行程序的方式

1) 直接在IDEA中运行
此时,基准路径就是该项目所在的目录路径

注:

  • 在实例化 File 对象时,路径名一定要使用 “双反斜杠” 或者 “斜杠”,不能使用反斜杠。但在IDEA中,当复制路径到双引号中时,IDEA 自动将反斜杠转换为了双反斜杠。


2) 在命令行中,通过java命令来运行
此时,基准路径就是java命令所处的目录


3) 某个程序 可能是被其他进程调用的
进程1通过创建子进程的方式,运行进程2(虽然在java中很少见,但是可以做到)
进程2的基准路径,就和进程1相同


4) 代码执行过程中,还可以通过一些 api 修改基准路径,改成我们指定的某个路径

3.3 文件操作方法
返回值类型方法签名说明
StringgetParent()返回 File 对象的父目录文件路径
StringgetName()返回 FIle 对象的纯文件名称
StringgetPath()返回 File 对象的文件路径
StringgetAbsolutePath()返回 File 对象的绝对路径
StringgetCanonicalPath()返回 File 对象的修饰过的绝对路径
booleanexists()判断File对象描述的文件是否真实
存在
booleanisDirectory()判断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()判断用户是否对文件有可写权限

1)获取文件的路径或名称 

public class Demo1 {
    public static void main(String[] args) throws IOException {
        //D:\JAVA\javaee\file&io\text.txt
        String path = "D:\\JAVA\\javaee\\file&io\\text.txt";
        File file1 = new File(path);
        System.out.println(file1.getParent());
        System.out.println(file1.getName());
        System.out.println(file1.getPath());
        System.out.println(file1.getAbsolutePath());
        System.out.println(file1.getCanonicalPath());


        System.out.println("------------------");
        // 通过idea运行程序,基准路径就是 idea 打开的这个项目所在的路径
        File file2 = new File("../text.txt");
        System.out.println(file2.getParent());
        System.out.println(file2.getName());
        System.out.println(file2.getPath());
        // 获取的是绝对路径
        System.out.println(file2.getAbsolutePath());
        // 获取的是简化后的绝对路径
        System.out.println(file2.getCanonicalPath());

        System.out.println("------------------");
        // 指定的这个路径 文件不存在,但程序不会报错
        File file3 = new File("../test.txt");
        System.out.println(file3.getParent());
        System.out.println(file3.getName());
        System.out.println(file3.getPath());
        // 获取的是绝对路径
        System.out.println(file3.getAbsolutePath());
        // 获取的是简化后的绝对路径
        System.out.println(file3.getCanonicalPath());
    }
}

 

 2)文件判断

public class Demo2 {
    public static void main(String[] args) throws IOException {
        File file = new File("./text.txt");
        boolean ok = file.createNewFile();// 创建文件 很可能会抛出异常
        System.out.println(ok);
        System.out.println(file.exists());
        System.out.println(file.isFile());
        System.out.println(file.isDirectory());
    }
}

createNewFile() 创建文件 可能抛出异常的常见原因:

      1)磁盘空间不足。这样的情况生活中不太会常见,但在日后工作中非常普遍。尤其是服务器,存储很多数据,会记录很多日志,每天都会生产很多新的内容,一般都需要定时清理硬盘。像这类工作就是 “运维工程师”。

       2)没有写权限。当前 Java 没有足够的权限在指定的目录下创建文件。对于文件的权限,典型的就是两个:读和写

       3)文件已存在

3)文件删除



执行 delete 操作后,返回值表示文件是否删除成功



deleteOnExit() : 进程结束之后才删除,存在的意义就是可以用来构造 “临时文件”

     比如 使用 word 创建一个文档,打开显示隐藏文件,在word 文档的同级目录下,就有一个 隐藏文件,名字带有一些奇怪符号。一旦把现在编辑的文件关闭,这个隐藏文件就自动消失了。这个隐藏文件中保存了当前正在修改,但还没有真正保存的内容。如果程序异常关闭(断电...),临时文件不会消失,就可以通过这个文件还原之前编辑的内容。

4)目录操作

public class Demo5 {
    public static void main(String[] args) {
        File file = new File("./src");
        System.out.println(file.isDirectory());
        String[] list = file.list();
        // 不能直接打印string数组,会是一个hashCode值
        System.out.println(Arrays.toString(list));
    }
}

file.list() / file.listFiles() : 直接使用 list / listFile 只能看到 当前目录中的内容。如果想获取某个目录下的所有文件和目录,就需要递归完成(树形结构)。 一般使用 listFiles方法,可以获得 File 对象集合。

listFiles 返回值:

获取某个目录下的所有目录和文件(递归):

public class Demo6 {
    private static void scan(File current){
        /// 1.先判定是否是目录
        if(!current.isDirectory()){
            return;
        }
        //2. 列出当前目录中包含的所有文件
        File[] files = current.listFiles();
        if(files == null || files.length == 0){
            // 不存在的路径/空目录
            return;
        }

        //3. 打印当前目录
        System.out.println(current.getAbsolutePath());
        //4. 遍历所有内容 依次判定
        for(File file:files){
            if(file.isFile()){
                //如果是普通文件 直接打印文件路径
                System.out.println(file.getAbsolutePath());
            }else{
                // 如果是目录 递归调用
                scan(file);
            }
        }
    }
    public static void main(String[] args) {
        File f = new File("./");
        scan(f);
    }
}

注意:

创建目录:

  • mkdir() : 创建一个新的目录。如果指定的路径中包含的上级目录不存在,目录会创建失败
  • mkdirs() : 用于创建一个或多个新的目录。如果指定的路径中包含的上级目录不存在,mkdirs会递归地创建所有必要的上级目录,以确保整个路径都被创建。

(单词缩写方式 --> 将单词中的元音字母缩写掉: make --> mk    send --> snd   receive --> rcv   count --> cnt)

5)重命名renameTo

public class Demo9 {
    public static void main(String[] args) {
        File srcFile = new File("./abc");
        // 将abc文件重命名为abc1234
        File destFile = new File("./abc1234");
        boolean ok = srcFile.renameTo(destFile);
        System.out.println(ok);
    }
}

     重命名,也可以用来移动文件。移动文件就是修改文件所在的路径,因此文件路径的修改也可以视为是一种 “重命名”,类似于剪切粘贴操作。

public class Demo10 {
    public static void main(String[] args) {
        // 将 abc 目录 下的 def 目录 移动到与 abc 同一级下
        // 借助重命名操作 可以实现移动的效果
        File srcFile = new File("./abc/def");
        File destFile = new File("./def");
        boolean ok = srcFile.renameTo(destFile);
        System.out.println(ok);
    }
}

    在实例化File对象时,指定的路径可以是一个不存在的文件。因为 File对象 不仅仅能针对已存在的文件操作,也能针对不存在的文件操作,比如 判定文件是否存在,创建新的文件,创建目录.....

四. 文件内容操作

4.1 流(Stream) 

    文件内容操作,主要是读文件和写文件,都是操作系统提供了API,Java 也进行了封装(“文件流” / “IO流”)。

形象的比喻

—— 水流的特点 ——

水龙头接100ml 水

  • 一口气接100ml,1次接完
  • 一次接50ml,分2次接完
  • 一次接10ml,分10次接完
  • 一次接1ml,分100次接完
  • ...................

—— IO流的特点 ——

从文件中读取 100 字节的数据:

  • 一口气读完100字节,1次读完
  • 一次读50字节,分2次
  • 一次读10字节,分10次
  • 一次读1字节,分100次
  • .....................

“流” 并非是Java原创的概念,而是操作系统本身的概念。操作系统本身提供的文件读写 API 就是流式,Java标准库的叫法 也是继承自操作系统原生的 API。C/C++ 等其它语言中,文件操作也是称为 “流”。

Java中实现  IO流 的类有很多,由 一系列的类 构成整个 IO流 的体系。 

主要分成两个大类: 

  • 字节流(二进制):读写数据的基本单位 --> 字节(每次读取至少读1个字节,即8个bit)
    InputStream 和 OutputStream 是两个比较关键的类
  • 字符流(文本):读写数据的基本单位 --> 字符(每个字符的大小 取决于编码的字符集)
    字符流内部做的工作会多一些,会自动查询码表,把二进制数据转换成对应的字符
    Reader 和  Writer 是两个比较关键的类

    InputStream 和 OutputStream 及 Reader 和  Writer 都是抽象类,Java中提供了很多的类,实现了这四个抽象类。
    由于类太多了,就使得咱们对于 IO流的理解会非常费劲。虽然类的种类多,但是用法都是差不多的。
    但凡类名 以 Reader / Writer 结尾的,都是字符流;以 InputStream 和 OutputStream 结尾的,都是字节流。 


4.2 输入 和 输出 方向

把内存中的数据,读到硬盘上,视为  “输入” 还是 “输出” 呢 ?

  • 如果站在内存视角,就是输出
  • 如果站在硬盘视角,就是输入

后面谈到 “输入”和“输出”,都是以 cpu 视角来谈的。数据远离 cpu 就是 “输出”;靠近 cpu 就是 “输入” 。内存 更接近 cpu,硬盘 更远离 cpu。

把数据从内存往硬盘上去写,就是 “输出”  过程 ;把硬盘上的数据往向内存上读,就是 “输入”  过程。从输入设备(键盘)读取数据到内存,就是 “输入”;向输出设备(显示器)显示内容,就是 “输出”。


4.3 文件打开与关闭 

FileInputStream 构造方法 

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

(此处隐含了一个操作 --> “打开文件”)

    针对文件进行读写,务必需要先打开文件(操作系统的基本要求)。既然打开文件了,对应的就要关闭文件。尤其是针对服务器,需要长时间持续运行。

    打开文件,其实是在该进程文件描述符表 中,创建了一个新的表项。

4.3.1 文件描述符表

    文件描述符表,描述了该进程需要操作哪些文件,可以认为是一个数组,数组的每个元素就是一个 struct file 对象(Linux 内核),每个结构体就描述了对应操作文件的信息(包括 文件路径,大小,上次修改时间....)。其中,数组的下标,就称为 “文件描述符”
    每次打开一个文件,就相当于在数组上 占用了一个位置。而在系统内核中,文件描述符表数组,是 固定长度 不可扩容 的,资源是有限的不同系统,文件描述符表的长度是不一样的,是可以配置的。
    每次打开一个文件,都会消耗指定进程文件描述符表上面的一个项,占用一个空间,消耗资源,除非主动调用 close 关闭文件,才会释放空间。
    如果代码里文件一直打开,不关闭,就会使这里的资源越来越少。当文件描述符表被占用满了,后续再打开文件就会打开失败,引入严重的bug。
    上述这个问题 称为“文件资源泄露”。

“文件资源泄漏” “内存泄漏”(比如 C语言 动态内存管理 malloc 申请内存后,需要 free 释放内存) 本质上是一个问题,最终都无法使用资源。

    “文件资源泄露” 及 “内存泄漏” 是非常不友好的问题:1)程序逻辑都能正常执行,不易发现问题所在;2)泄露 不是一瞬间泄露完毕,而是一个持续的过程。整个问题 直到所有的资源泄露完毕,这一刻才会集中爆发出来(不定时炸弹)。
    程序抛出 “异常” 导致程序崩溃,这种问题其实是友好:1)出现异常,能够第一时间发现;2)异常信息里的线索,有助于很快解决问题。 


4.3.2 进程核心 --> PCB

    PCB 即进程控制块,是操作系统核心中一种数据结构,用于描述进程的基本信息和运行状态。它是操作系统管理进程的最重要的数据结构,每一个进程在内核中都有一个对应的PCB。

PCB的主要内容:

  • 进程标识信息:进程ID、用户ID......
  • 进程状态信息:进程状态(如新建、就绪、运行、阻塞、终止等)、进程优先级....
  • 程序计数器:指示下一条要执行的指令地址
  • 寄存器信息:通用寄存器(保存函数参数和返回值)、用户栈指针(指向用户空间的栈)....
  • 内存管理信息
  • 记账信息:进程使用CPU时间、进程使用时钟数、进程被调度次数.....

PCB在操作系统中的工作流程:

  1. 创建进程:当一个新的进程被创建时,操作系统会为其分配一个PCB,并初始化相关信息。
  2. 进程切换:在进程切换时,操作系统会保存当前进程的PCB信息,并加载下一个将要运行进程的PCB信息。
  3. 进程调度:调度器根据PCB中的信息(如进程优先级、调度策略...)来决定下一个要运行的进程。
  4. 进程结束:当进程结束时,操作系统会释放PCB所占用的资源,并从系统中去除该PCB。

    打开任务管理器,就可以看到进程管理列表。使用 PCB 表示一个进程信息,再使用数据结构(链表等)将多个 PCB 串到一起,表示整体。


PCB 与 文件描述符表的关系:    

  •     PCB中通常包含一个指向文件描述符表的指针。这意味着文件描述符表是PCB的一部分,专门用于管理进程的文件和I/O资源。
     
  •     当进程被创建时,操作系统会为它分配一个PCB,并在PCB中初始化文件描述符表;当进程结束时,操作系统会根据PCB中的信息,关闭所有打开的文件描述符,并释放相关资源。
     
  •     当进程执行系统调用(如open、read、close)时,操作系统会根据PCB中的文件描述符表来定位具体的文件或I/O资源,并执行相应的操作。

     (虽然要求使用完文件后要close,但此代码不写close也行。因为 close 之后,紧接着进程就结束了,进程结束 意味着整个 PCB 被销毁了,PCB 上的文件描述符表 也被释放了。)

4.3.3 close 要点

    有的时候,如果程序中间出现 return、抛出异常等操作,close 代码可能会执行不到。为了确保close执行到,应将 close操作 放到 try-catch-finally 中的 finally 中。

public class Demo1 {
    public static void main(String[] args) {
        InputStream is = null;
        try{
            is = new FileInputStream("./src/test.txt");
        } catch( IOException e){
            e.printStackTrace();
        }finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

(这种写法 虽然能够确保严谨,但是比较麻烦)

还有一种简单可靠的办法:

public class Demo2 {
    public static void main(String[] args) {
        try(InputStream is = new FileInputStream("./src/test.txt")){
            
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

(这样的写法,不必写 finally,也不必写 close 了。)

     这种写法是 Java 引入的特殊语法:try with resources   在当前的 try括号 里面,加上要管理的资源(可以是多个资源)。这里的资源会在 try 代码块执行结束后,无论是 抛出异常 或 return 了,都会自动执行关闭 。

    但是 并非任何的对象都可以填写到 try括号中,必须实现 Closable 接口的对象,才能放到 try括号 里面。

4.4 读文件

InputStream 将数据从 硬盘 读到 内存中的方法:

返回值方法签名说明
intread()每次读取一个字节的数据,返回值是这个字节的内容(byte);返回 -1 代表已经完全读完了
intread(byte[] b)最多读取 b.length 字节的数据到 b 中,返回实际读到的数量;返回 -1 代表已经读完了
intread(byte[] b, int off, int len)最多读取 (len - off) 字节的数据到 b中,放在从 off 开始,返回实际读到的数量;返回 -1 代表已经读完了
4.4.1 read()
public class Demo2 {
    public static void main(String[] args) {
        try(InputStream is = new FileInputStream("./src/test.txt")){
            while(true){
                int b = is.read();
                if(b == -1){
                    // 读取完毕了
                    break;
                }
                //表示字节,更习惯使用十六进制显示打印
                System.out.printf("0x%x\n", b);
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

 十进制 方便生活使用计算,十六进制 更适合计算机,可以认为是二进制的简化表示形式:

  • 1 个16进制数字 <=> 4 个二进制 bit位
  • 2 个16进制数字 <=> 1 个字节(byte)

—————— 运行结果 ——————

文件存“hello”
   

文件存 “你好”


 为什么  read  的返回值 是 int 类型?

    在计算机中,byte 类型通常是一个8位的无符号整数,这意味着它的取值范围是从 0~255(十进制),或者从 0x00 ~ 0xFF(十六进制)。由于 byte 是无符号的,它不能直接表示负数,因此标准的 byte 类型不能表示 -1。

    在Java中,byte字节一般是默认为有符号的,范围是 -128 ~ 127。然而当从文件中读取字节的时候,Java 会将byte 视为无符号值,范围是 0~ 255.

    实际上,有符号 / 无符号 只是打印或者计算的时候是 正 / 负,显示或计算规则是不同的。但对于计算机来说,都是二进制存储,不关心有符号还是无符号,内存表示的数据是一样的。比如:

  • 1111 1111 有符号  --> -1
  • 1111 1111 无符号 --> 128

    为了能表示 -1,要使用比 byte 数据范围更大的类型,比如 short、int 这样更长的数据表示。但是 short 几乎是整个职业生涯中 大概率不会用到的类型~~ short 其实是一个历史遗留问题,由于三四十年前的计算机,存储空间非常有限(1MB 的内存就是 “大内存”),因此程序员要非常重视空间的问题。但现在的存储空间十分充裕了,CPU 和 内存 交互数据的 “宽度” (即一次可以传输的数据位数)变的更宽了(目前普遍是64位(bit)的数据宽度),读写 short 可能会比读写 int 效率更低。现在仍有一些特殊场景,涉及到的计算机硬件水平非常有限,short 仍有存在的价值(比如 嵌入式开发)

    因此,Java 标准库选择 int 作为 read 的返回值。

4.4.2 read(byte[] b)

    一次读一个字节的做法效率是比较低的,这就意味着要 频繁读取多次硬盘,进行 I/O 操作耗时会比较大,希望能够减少 I/O 的次数,此时就可以使用 read(byte[] b) 方法。 

public class Demo3 {
    public static void main(String[] args) {
        try(InputStream is = new FileInputStream("./src/test.txt")){
            // 如果文件没有读完, n != -1 ,重复执行
            while(true){
                // 把硬盘中读到的数据 填充到buffer数组中
                byte[] buffer = new byte[1024];
                // n 返回值表示 read 操作 实际读取到多少个字节 
                int n = is.read(buffer);
                if(n == -1){
                    // 读取完毕了
                    break;
                }
                for(int i = 0; i < n; i++){
                    System.out.printf("0x%x\n", buffer[i]);
                }
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

    这个操作就会把硬盘中读到的对应的数据,填充到 buffer 内存的字节数组中,并且在一次IO中 尽可能填满(如果文件长度足够,就会将 buffer 数组填满;如果文件长度不够1024,能填多少是多少),比执行多次 IO 更加高效。虽然一次读的内容多了,但仍然比一次读一个字节,分很多次读效率高。

    此处是把 buffer 形参当成了 “输出型参数”,平时写代码 方法的参数一般是 “输入型参数”,使用返回值 表示输出结果。

  • 输入型参数
    输入型参数是指传递给函数的参数,这些参数的值在函数内部被读取,但不会被函数修改。换句话说,输入型参数用于向函数提供数据。
  • 输出型参数
    输出型参数在函数或方法调用时,传递的是变量的引用 (在某些语言中是地址 C/C++),而不是变量的值。这意味着函数可以修改这个参数所引用的变量,而修改的结果可以在函数外部被访问

    Java中 通常不区分输入和输出参数,因为参数通过引用传递的对象,可以被视为有输出效果的参数,因为它们可以在函数外部被观察到修改后的状态。输出型参数在 Java 中非常少见,但在 C++ 中很常见。

4.4.3 read(byte[] b, int off, int len)

    这个版本 类似于刚才版本,也是把数据往字节数组 b 里填充,但不是使用整个数组,而是使用数组中 [off, off + len) 范围的区间。off ==> offset 表示 “偏移量”

    写代码时经常见到 buffer / buf ,表示的是 “缓冲区” 的意思。缓冲区 就是一个内存空间,通过这个内存空间,暂时存储某个数据。 

4.5 写文件

OutputStream 将数据写到文件中的方法:

返回值方法签名说明
voidwrite(int b)写入要给字节的数据
voidwrite(byte[] b)将 b 这个字符数组中的数据全部写入OutputStream中
intwrite(byte[] b, int off, int len)将b这个字符数组中从 off 开始的数据写入OutputStream中,一共写 len 个
voidflush()我们知道 I/O的速度是很慢的,所以,大多的OutputStream为了减少设备操作的次数,在写数据的时候都会将数据先暂时写入内存的一个指定区域里,直到该区域满了或者其他指定条件时才真正将数据写入设备中,这个区域一般称为缓冲区。但造成一个结果,就是我们写的数据,很可能会遗留一部分在缓冲区中。需要在最后或者合适的位置,调用flush(刷新)操作,将数据刷到设备中。

1)一次写入一个字节

public class Demo4 {
    public static void main(String[] args) {
        try(OutputStream os = new FileOutputStream("D:\\JAVA\\javaee\\file&io\\text.txt")){
            // 一个字节一个字节写入
            os.write(97);// a
            os.write(98);// b
            os.write(99);// c
            // 根据 utf8 编码 一个字符是3个字节
            // 你好
            os.write(0xe4);
            os.write(0xbd);
            os.write(0xa0);
            os.write(0xe5);
            os.write(0xa5);
            os.write(0xbd);
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

 2)一次写入一个字节数组

public class Demo5 {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("D:\\JAVA\\javaee\\file&io\\text.txt")){
            // 一次写入一个字节数组 注意显示的强制类型转换
            byte[] buffer = new byte[]{(byte)0xe4,(byte)0xbd,(byte)0xa0,(byte)0xe5,(byte)0xa5,(byte)0xbd};
            outputStream.write(buffer);
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

    OutputStream 默认是写入之前,会清空原来的内容。只要使用 OutputStream 打开文件,内容就被清空了。还有一个操作,“追加写” 保持原内容不变,在末尾继续写入新内容。


4.6 字符流 

    InputStream / OutputStream 读写数据就是按照字节来操作的。如果要读写字符的话(中文),此时,就需要靠程序员手动区分出哪几个字节是一个字符,再确保把这几个字节作为整体来写入。为了更方便处理字符,于是引入字符流。

public class Demo6 {
    public static void main(String[] args) {
        try(Reader reader = new FileReader("./text.txt")){
            while(true){
                // read 无参方法一次读取一个字符
                int c = reader.read();
                if(c == -1){
                    // 读到文件末尾 返回-1
                    return;
                }
                // 强制类型转换
                char ch = (char)c;
                System.out.print(ch);
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

    最初按照字节来读,能够看到这里是 utf-8 编码情况,每个汉字应该是3个字节;但使用字符流读取数据时,返回值 char 是两个字节,并非 3 个字节。这是由于 在使用字符流读取数据的过程中,Java标准库内部会自动针对数据的编码进行转码。当使用 char 表示这里的汉字时,不再使用 utf-8,而是使用 unicode 编码(在unicode中,一个汉字 2 个字节

public class Demo7 {
    public static void main(String[] args) {
       try(Reader reader = new FileReader("./text.txt")){
           while(true){
               // 一次读若干字符 放到字符数组中 以输出型参数的方式表示
               char[] buffer = new char[1024];
               // 返回值表示实际读到的字符个数
               int n = reader.read(buffer);
               if(n == -1){
                   return;
               }
               String s = new String(buffer,0,n);
               System.out.print(s);
           }
       }catch(IOException e){
           e.printStackTrace();
       }
    }
}

public class Demo8 {
    public static void main(String[] args) {
        try(Writer writer = new FileWriter("./text.txt",true)){
            // 可以直接写入一个 String 到文件中
            writer.write("hello world");
            // 可以不手动刷新缓冲区 文件关闭后会自动刷新
            // writer.flush();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

写入文件自动刷新缓冲区情况:

  • 当缓冲区满时
  • 当关闭流时
4.7 格式化操作

利用Scanner进行字符读取
    我们经常使用 Scanner scanner = new Scanner(System.in) 对用户输入进行操作, System.in 其实就是一个 InputStream 类

构造方法说明
Scanner(InputStream is, String charset)使用 charset 字符集进行is 的扫描读取
public class Demo9 {
    public static void main(String[] args) {
        try(Scanner scanner = new Scanner(new FileInputStream("./text.txt"),"UTF-8")){
            // 类似于 Scanner(System.in)
            while(scanner.hasNextLine()){
                String line = scanner.nextLine();
                System.out.println(line);
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

利用 PrinterWriter 将对象的格式化表示打印到文本输出流
     上述我们其实已经完成输出工作,但总是有所不方便,我们接来下将 OutputStream 处理下,使用 PrintWriter类 来完成输出,因为 PrintWriter类 中提供了我们熟悉的 print / println / printf 方法。

构造方法说明
PrintWriter(Writer/OutputStream out, boolean autoFlush)创建一个新的 PrintWriter对象,可以指定是否自动刷新缓冲区。如果autoFlush 参数 为 true,则每当写入换行符时,缓冲区都会被刷新。
PrintWriter(String fileName, String charsetName)使用指定的文件名和字符集作为输出目的地。
public class Demo10 {
    public static void main(String[] args) {
        try(OutputStream os = new FileOutputStream("./src/test.txt");
            PrintWriter pw = new PrintWriter(os)){
            // 类似于 System.out.println
            pw.println("hello world");
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值