【JavaEE】文件操作和IO

目录

1、认识文件

 1.1、路径

1.1.1 、绝对路径 

1.1.2、相对路径 

1.2、文本文件 vs 二进制文件 

 2、文件系统操作

3、文件内容操作

3.1、字节流用来输入的抽象类InputStream的方法使用

3.1.1、FileInputStream类的构造方法

 3.1.2、字节流读操作

 3.1.3、字节流写操作

3.2、 字符流的读写操作

4、文件操作的案例


1、认识文件

我们平时谈到的"文件",指的都是硬盘上的文件。我们在进行数据保存的时候,往往不是保存成一个整体,而是独立成一个个单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的一份一份真是的文件。

硬盘(外存)和内存相比

  • 速度:内存比硬盘快很多
  • 空间:内存空间比硬盘小
  • 成本:内存比硬盘贵
  • 持久化:内存掉电后数据会丢失,外存掉电后数据还在。

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

 我们之前博客中的代码,绝大部分是围绕内存展开的(JavaSE和数据结构),定义一个变量其实就是在内存上申请空间。MySQL主要就是操作硬盘。我们这个博客中的文件IO也是操作硬盘。


 1.1、路径

🎃路径文件系统上一个文件/目录(文件夹)具体位置

✨文件系统是以树形结构来组织文件和目录——这个树形结构为(N叉树) 。

🎉文件路径:就是从数根结点出发,沿着树杈,一路往下走,到达目标文件,此时这中间经过的内容就是文件路径。

📙 实际表示路径,是通过一个字符串表示,每个目录之间使用\\或者/来分割

  1.  上边的图片中我们看到每个目录之间是使用\表示的,反斜杠(\)只是在Windows中适用,我们在写代码的时候需要写成\\,用转义字符将\转换。所以我们在写代码的时候,还是比较建议使用、。
  2. 上述看见两个图中一个路径存在此电脑,一个不存在此电脑。存在此电脑的是因为我们是按照层级打开目录,所以电脑上会显示。另外一个不存在此电脑的是因为,我们想要找(或者是表示)一个文件的时候,默认是从盘符开始的。表示路径的时候,可以把"此电脑"省略,直接从盘符开始表示。

1.1.1 、绝对路径 

从盘符开始,一层一层往下找,这个过程,得到的路径,就是绝对路径。

1.1.2、相对路径 

从给定的某个目录出发,一层一层往下诏,这个过程得到的路径,就是相对路径。

 . 在相对路径中,表示当前目录;

.. 在相对路径中,表示的是当前目录的上级目录。

❗❗❗注意:

  • 相对路径,一定要明确工作目录(基准目录)是什么。 
  •  文件系统上任何一个文件,对应的路径是唯一的,不会存在两个路径相同,但是文件不同的情况。在Linux上可能存在一个文件,有两个不同的路径能找到它;但是在Windows上不存在,Windows上可以认为,路径和文件是一一对应的,路径就相当于一个文件的"身份标识"。

1.2、文本文件 vs 二进制文件 

1️⃣文本文件:存储的是文本(文本文件的内容都是由ASCII表字符构成)。文本文件里存储的数据,就是遵守ASCII或者其他字符集编码(例如:utf8),所得到的文件,本质上存的是字符(不仅仅是char)。

2️⃣二进制文件:存储的是二进制数据,(存储不受任何字符集的限制)

 ✨判定一个文件是文本文件还是二进制文件的方法。

  • 直接使用记事本打开某个文件,如果打开之后的内容你能够看懂,这个文件就是文本文件;如果你看不懂,内容乱糟糟的,这个文件就是二进制文件。(因为记事本是默认按照文本的形式来解析显示的,解析成功就是文本文件,解释失败就是二进制文件)
  • 文本文件:文件后缀为.txt,.java,.c
  • 二进制文件:文件后缀为 .class ,.exe,.jpg,.mp3

 2、文件系统操作

Java标准库给我们提供了File这个类,Flie对象是对硬盘上的一个文件的"抽象"表示。文件是存储在硬盘上的,直接通过代码操作硬盘,不太方便,就在内存中创建一个对象,操作这个内存中的对象,就可以间接的影响到硬盘的文件情况了。

1️⃣构造File对象

构造的过程中,可以使用绝对路径或者相对路径进行初始化,这个路径指向的文件,可以是真实存在的,也可以是不存在的。

public class IODemo1 {
    public static void main(String[] args) {
        //初始化这个对象的时候,这个文件可以真实存在也可以不存在。
        File file = new File("d:/dog.jpg");
    }
}

2️⃣File类提供的一些方法

 写一些代码来了解这些方法的使用

🎉观察get系列方法的特点和差异

public class IODemo1 {
    public static void main(String[] args) throws IOException {
        //初始化这个对象的时候,这个文件可以真实存在也可以不存在。
        File file = new File("d:/dog.jpg");
        //获取File对象的父目录文件路径
        System.out.println(file.getParent());
        //获取File对象的文件名
        System.out.println(file.getName());
        //获取File对象的全路径
        System.out.println(file.getPath());
        //获取File对象的绝对路径
        System.out.println(file.getAbsoluteFile());
        //获取File对象的修饰过的绝对路径
        System.out.println(file.getCanonicalFile());
    }
}

 🎉普通文件的创建、删除。

public class IODemo2 {
    public static void main(String[] args) throws IOException {
        //初始化的时候,这个文件目录的写法,是相对路径的一种写发,./通常可以省略
        File file = new File("hello_world.txt");
        //判断这个文件是否真实存在
        System.out.println(file.exists());   //false
        //判断File对象代表的文件是否是一个目录
        System.out.println(file.isDirectory());  //false
        //判断File对象代表的文件是否是一个普通文件
        System.out.println(file.isFile());   //false

        //创建文件
        file.createNewFile();
        System.out.println(file.exists());  //true
        System.out.println(file.isDirectory());   //false
        System.out.println(file.isFile());  //true
    }
}

❗❗❗注意:IEDA工作目录就是项目所在目录,写相对路径时,就是以system_code这一级为基准,来展开的。

🎉创建目录 

public class IODemo3 {
    public static void main(String[] args) {
        File file = new File("text-dir/aaa/bbb");
        //只能创建一级目录
        file.mkdir();
        //可以创建多级目录
        file.mkdirs();

    }
}

🎉列出一个目录下包含那些内容

public class IODemo5 {
    public static void main(String[] args) {
        File file = new File("text-dir");
        String[] results1 =  file.list();
        //数组的打印,需要使用数组的工具类调用toString方法进行打印,才能打印出数组当中的内容,要不然打印的是数组的hash值
        System.out.println(Arrays.toString(results1));
        File[] results2 = file.listFiles();
        System.out.println(Arrays.toString(results2));

    }
}

🎉针对文件或目录重命名

public class IODemo6 {
    public static void main(String[] args) {
        File src = new File("./text-dir");
        File dest = new File("./test222");
        src.renameTo(dest);
    }
}


3、文件内容操作

  • 针对文本文件,提供了一组类,统称为"字符流"(典型代表,Reader,Writer),字符流读写的基本单位是字符(根据字符集来确定一个字符占几个字节),字符流每次读写最少是一个字符。
  • 针对二进制文件,提供了一组类,统称为"字节流"(典型代表,InputStream,OutputStream),字节流读写的基本单位是字节(8个bit位),字节流每次读写最少是一个字节。

每种流对象,又分为两种

  • 一种是用来输入的:Reader,InputStream
  • 一种是用来输出的:Writer,OutputStream

 

3.1、字节流用来输入的抽象类InputStream的方法使用

InputStream只是一个抽象类,要使用这个类,需要实例化实现了这个抽象类的子类。InputStream类使用来进行输入的,它的实现类很多,基本上可以认为不同的输入设备都可以对应一个InputStream类,不仅仅是读写硬盘文件,还可以是读写网络编程,或者是读写网卡。本章博客只是针对硬盘文件的输入,所以这里我们只需要实例化FileInputStream类即可。

3.1.1、FileInputStream类的构造方法

public class IODemo7 {
    public static void main(String[] args) throws IOException {
        //这个过程就相当于文件打开操作
        //让当前的inputStream变量和硬盘上的文件关联起来
        InputStream inputStream = new FileInputStream("D:/test.txt");
        //释放资源
        //释放资源这部操作非常重要,不能省略
        inputStream.close();

    }
}
  • 上述代码中的资源释放的操作非常重要,千万不要忘记!因为Java有垃圾回收机制,可以帮助我们将使用过的资源释放掉,但是在文件操作这里垃圾回收机制并不能帮我们把资源释放掉。需要我们手动释放这里说的文件操作的时候需要释放资源,释放的资源主要是文件描述符表
  • 文件描述符表:记载了当前进程打开了那些文件,每次打开一个文件,就会在这个表里,申请到一个位置,这个表可以当成一个数组,数组下标就是文件描述符,数组元素就是这个文件在内核中的结构体的表示。但是这个表的长度是有限制的,不能无休止的打开文件,但是又不释放。一旦文件表述符表满了,继续打开就会打开失败,这就导致了文件资源泄露

上述的这种写法虽然写到了将文件释放,但是这样写,在执行的时候,中间出现一些问题,比如return或者抛异常,就会导致close执行不到了。所以为了保证close一定被执行到,所以这里我们了解一下比较稳妥的写法。

 1️⃣第一种写法:这种写法虽然可以保证一定执行到close,但是这种写法并不好看。

public class IODemo7 {
    public static void main(String[] args) throws IOException {
        InputStream inputStream = null;
        try{
            inputStream = new FileInputStream("D:/test.txt");
        } finally{
            //释放资源
            inputStream.close();
        }
    }
}

2️⃣第二种写法try with resources:带有资源的try操作,会在try代码块结束时,自动执行close关闭操作。

public class IODemo7 {
    public static void main(String[] args) throws IOException {
        try(InputStream inputStream = new FileInputStream("D:/test.txt")){
            
        }
    }
}

之所以上述的写法能够释放资源是因为InputStream实现了一个特定的接口Closeable

 3.1.2、字节流读操作

InputStream类中提供了3种读操作的方法。

 先来了解一下无参的read方法.

public class IODemo7 {
    public static void main(String[] args) throws IOException {
        try(InputStream inputStream = new FileInputStream("D:/test.txt")){
            //读文件
            //无参数的read,相当于一次读一个字节,但是返回类型是int,当读到末尾返回-1
            while(true){
                int b = inputStream.read();
                if(b==-1){
                    //读到末尾了,结束循环
                    break;
                }
                System.out.println(b);
            }
        }
    }
}

❓❓❓上述代码中read方法读取的是二进制文件中的内容,读取出来的数据应该是字节,应该使用byte来接收,为什么使用int类型的变量来接收??


❗❗❗因为源码当中的注释写到read返回的是一个0~255之间的数字,但是如果文件读取完毕,再次读取就会返回-1,这个时候-1没有在这个范围中,所以就要使用int来接收,但是说到这里有的人会说到short也可以,但是应为内存对齐的原因,short并没有int快。

 ✨再来看一下上述程序的执行结果与test.txt文件中的内容之间的关系。

1️⃣二进制文本中内容为英文

2️⃣如果test文件中将内容编辑为中文出现的结果会是什么(test文件中输入中文“世界你好”)

 可以看到上述控制台上输出的10进制数和我们查到的不一样,这时因为我们查到的是把3个字节放在一起,搞了一个大10进制,和咱们控制台上3个字节分开打印是不一样的,我们通过控制它将汉字以16进制打印出来观察结果。

 //按照16进制打印
System.out.printf("%x\n",b);

 

✨读操作的总结:

  1.  read():无参数版本,返回该字节的内容
  2. read(byte[ ] buffer,int offset):这个版本,读到的内容放到参数buffer数组中,此时返回值是读到的字节数。
  3. read(byte[ ] buffer,int offset ,int length):这个版本也是读到buffer数组中,但是不是从头放,是从offset位置放,最多放length长度,返回值也是实际读的长度。

 3.1.3、字节流写操作

可以看到写操作也有多种方法。这里我们来了解每次写一个的字符的方法。

 代码示例

public class IODemo8 {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("d:/test.txt")){
            outputStream.write(97);
            outputStream.write(101);
            outputStream.write(100);
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

 写操作的执行结果我们要在对应的二进制文件中寻找。

✨总结:

  • read和write还可以一次读写多个字节,使用byte[]来表示。
  • read会尽可能把byte[]填满。如果读到文件内容末尾,返回-1 
  • write会把byte[]所有数据都写入文件

3.2、 字符流的读写操作

字符流文件读写操作和字节流核心逻辑一样,使用Reader,Writer这两个抽象类的子类的构造方法来打开文件(FileReader,FileWriter).read方法来读(一次读一个char或者char[]);write方法来写(一次写一个char或者char[]或者String),close释放资源。

public class IODemo9 {
    public static void main(String[] args) {
        try(Reader reader = new FileReader("d:/test.txt")) {
            while(true){
                int c = reader.read();
                if(c == -1){
                    break;
                }
                char ch = (char)c;
                System.out.println(ch);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这个代码和上面的字节流读取代码逻辑基本相似,就是在打印的时候,将int类型的c强制类型转换为char类型,这样每次读取打印的内容就是字符。


4、文件操作的案例

遍历目录,在里面的文件内容中查找。

在你的电脑上,有很多目录,目录里面有很多文件,每个文件里有很多内容,假设某些文件中,包含"hello"关键词。下面的程序就是找出那些文件,是包含这个关键词的。

我们这里的实现方式是一个简单粗暴的方式:

  1. 先以递归的方式遍历目录。比如给定一个d:/去递归的把这里包含的所有文件都列出来。
  2. 每次找到一个文件,都打开,并读取文件内容(这个时候就得到一串字符)
  3. 再判定要查询的词,是否在上述文件内容中存在,如果存在,即为所求。
package io;

import java.io.*;
import java.util.Scanner;

public class IODemo10 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        //1.先让用户指定一个要搜索的根目录
        System.out.println("请输入要扫描的根目录:");
        //将用户输入的内容构造常File对象
        File rootDir = new File(scanner.next());
        //判断一下输入的路径名表示的文件是否为一个目录
        if(!rootDir.isDirectory()){
            System.out.println("输入有误,你输入的目录不存在!");
            return;
        }
        //2.让用户输入一个要查的词
        System.out.println("请输入要查询的词:");
        String word = scanner.next();

        //3.递归的进行目录或者文件的遍历
        scanDir(rootDir,word);

    }

    private static void scanDir(File rootDir, String word) {
        //列出当前rootDir中的内容,没有内容,直接递归结束
        File[] files = rootDir.listFiles();//返回rootDir对象(d盘)下的所有文件
        if(files == null){
            //当前rootDir是一个控的目录,这里啥都没有。
            //就没有必要在递归了,直接返回就行。
            return;
        }
        //目录中有内容,就遍历目录中的每个元素
        for(File f:files){//通过这个for循环遍历这个文件数组
            //设置一个日志
            System.out.println("当前搜索到:"+f.getAbsoluteFile());//打印文件的绝对路径
            if(f.isFile()){
                //判断是普通文件
                //打开文件,读取内容,比较看是否包含上述关键词
                String content = readFile(f);
                //判断看读到的文件内容中是否包含关键字
                if(content.contains(word)){
                    //文件中有要查中的关键字,则输出这个文件的绝对路径
                    System.out.println(f.getAbsoluteFile()+"包含要查找的关键字");
                }
            }else if(f.isDirectory()){
                //判断是目录
                scanDir(f,word);//此处的递归就是以但钱f这个目录作为起点,搜索t目录里面的内容。
            }else{
                //不是普通文件,也不是目录文件直接跳过,以Linux为例,文件种类有很多,普通文件,目录文件,管道文件...
                continue;
            }

        }
    }
    //读取文件的整个内容,返回出来
    private static String readFile(File f) {
        //使用字符流来读取,由于咱们匹配的是字符串,此处只能按照字符流处理,才是有意义的。
        StringBuilder stringBuilder = new StringBuilder();
        try(Reader reader = new FileReader(f)){
            //一次读一个字符,把读到的结果给拼装到StringBuffer中,同意转成String
            while(true){
                int c = reader.read();
                if(c == -1){
                    break;
                }
                //再进行字符串拼接的时候,需要将刚刚读取到的c数据强转成char类型,c的类型本身是一个int,为了防止超出char的范围
                stringBuilder.append((char)c);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return stringBuilder.toString();
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值