JavaEE初阶-文件IO2


前言

在这里对Java标准库中对文件内容的操作进行总结,总体上分为两部分,字节流和字符流,就是以字节为单位读取文件和以字符为单位读取文件内容。


一、字节流

1.1 读文件

字节流在Java中的类就是InputStream,他是一个抽象类,我们在这里操作的文件所以就需要通过它的子类FileInputStream向上转型的方式,因为InputStream不能初始化。后面如果我们要进行网络IO,就也是使用相对应的子类来进行实现。
代码示例1:

package io;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Demo6 {

    public static void main(String[] args) throws IOException {
//         读文件的两种不同参数的read方法的使用
//        InputStream inputStream = new FileInputStream("F:/test.txt");
//        while (true) {
//            int b = inputStream.read();
//            if (b == -1) {
//                break;
//            }
//            System.out.printf("0x%x ", b);
//        }
//        System.out.println();

//        while (true) {
//            byte[] arrB = new byte[1024];
//            int n = inputStream.read(arrB);
//            System.out.println("n = " + n);
//            if(n==-1) {
//                //读毕 n就会返回-1
//                // 在这里首先第一次会把数组填满 后面第二次循环文件内没字节了就返回-1
//                break;
//            }
//            for (int i = 0; i < n; i++) {
//                System.out.printf("0x%x ", arrB[i]);
//
//            }
//            System.out.println();
//        }
//
//    }      
    }


}

这段代码是使用字节流来读取文件内容的一个简单过程,因为我们读取硬盘中的文件内容给内存中的变量去接受,显然数据是往cpu的方向流动的,因此是一个输入的过程,我们使用InputStream抽象类以及FileInputStream类进行向上转型来构造这样的一个字节流对象。我们想要读取哪个文件中的信息就可以将文件的路径或者File类对象用以初始化字节流对象,如下。

InputStream inputStream = new FileInputStream("F:/test.txt");

FileInputStream这样的字节流对象提供了一些读取文件数据的方法如下图。
在这里插入图片描述
这三种方法有些许的不同,首先read方法,它每次读取一个字节,然后返回值就是就是这个字节的相应的ASCII值,如果说读到文件末尾那么此时就会返回-1。那么既然每次都是读一个字节,返回的就是一个字节的范围,那么返回值类型直接设为byte即可,那么为什么会设为int类型的返回呢?
有以下几点原因:
(1)确保每次返回的数都是正数,因为从原则上说字节这样的概念本身是无符号的但是byte类型本身是有符号的,如果说你拿byte来返回无符号数,那么范围是0~255,此时就无法表示-1。只有你使用int类型,才能够返回值是正数,还能使用-1来表示文件结尾。
(2)这时我们又会想到,那么我们直接用short不就行了,这样也就可以包含-1以及0到255的整数。但是这里就涉及到计算机发展的问题了,因为计算机发展到现在存储空间不再是核心矛盾了,存储设备的成本是越来越便宜了,此时随着cpu越来越牛,它单次处理数据的长度也越来越长。对于32位cpu,一次就能够处理四个字节的数据,此时要是使用short还要将其转成int再按int进行处理,显然64位cpu也是类似的,此时的short就更没意义了,我们学过的c语言中的整形提升也是类似的道理。因此在我们使用short的场景换成int,在我们使用float的场景我们换成double。
这里补充一下,为什么说字节数据从原则上是无符号的,因为字节数据不是用来进行算数运算的,例如一张图片就是由很多字节数据构成的,如果对其字节进行加一或者减一操作,那么这张图像很可能直接崩掉
第二个read方法和第一个read方法不同的点在于它的参数是一个字节数组,这个数组是一个空数组,读文件数据的时候就会把读到的数据全放到这个数组当中,去把这个字节数组给填满,能填多少填多少。返回值也是类似,你读到多少个字节就返回多少,读到文件尾就返回-1。
第三个read方法其实和第二个read方法就很类似了,也是建立一个空数组来作为参数,然后指定一个区间,读到的文件数据只放到这个空数组的对应区间当中。返回值就和第二个read方法一样了,读到多少字节就返回多少,读到文件尾就返回-1。
这里提一嘴,read()和read(byte[] b)这两种方法谁的效率比较高?
事实上是第二个方法的效率高,我们都知道第一个方法是一次读取一个字节,第二个方法是一次读一个数组的字节,对于固定的文件内容,肯定是第二个方法读取的次数比较少,文件的IO在我们的代码中是一个比较低效的操作,每次读取都要进行一次IO,显然IO次数少的方法效率就更高。
此时我们把视角转回上面的代码示例1,我们分别使用方法1和方法2来读取文件数据,然后在外面套一个死循环,当read方法返回-1代表读到文件尾此时跳出循环,逻辑还是比较简单的。
代码示例2:

package io;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Demo6 {

    public static void main(String[] args) throws IOException {
        // 但是每当建立一个线程或进程 建立的PCB中的文件描述符表这个顺序表它的长度是有限的
        // 每当打开一个文件 它的长度就要加一、
        // 所以打开文件后要及时关闭
        // 所以这里联想到处理unlock()的方法 即使用try finally语句
//        InputStream inputStream = new FileInputStream("F:/test.txt");
//        try {
//            while (true) {
//                byte[] arrB = new byte[1024];
//                int n = inputStream.read(arrB);
//                System.out.println("n = " + n);
//                if (n == -1) {
//                    //读毕 n就会返回-1
//                    // 在这里首先第一次会把数组填满 后面第二次循环文件内没字节了就返回-1
//                    break;
//                }
//                for (int i = 0; i < n; i++) {
//                    System.out.printf("0x%x ", arrB[i]);
//                }
//                System.out.println();
//            }
//        } finally {
//            inputStream.close();
//        }

        }

    }


}

上述代码相对于代码示例1加入了try-finally这样的代码,因为如同代码中注释所描述的每次你打开一个文件,就会在PCB当中的文件描述符表当中添加一个元素,这个元素当中就是文件的相关信息,文件描述符表类似于一个顺序表,它总会有上限当这样不关闭文件的操作多了,会占满文件描述符表,此时若是再想打开文件就不行了。因此每次我们使用FileInputStream构造流对象打开文件读取文件内容等一系列操作之后就要关闭文件,使用try-finally就可以保证每次使用完文件之后能够关闭文件的流对象,此时文件描述符表中的对应元素就会被释放。
代码示例3:

package io;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Demo6 {

    public static void main(String[] args) throws IOException {
        // 使用try finally确实可以 但是作为一个程序员要追求去写优雅的代码
        // 上述代码修改如下
        // 直接将流对象的创建放到try右边的括号中 这样java会自动帮你调用close()
        // 注意不是什么对象都可以这样 必须要实现Closeable接口的类才可以
        try (InputStream inputStream = new FileInputStream("F:/test.txt");){
            while (true) {
                byte[] arrB = new byte[1024];
                int n = inputStream.read(arrB);
                System.out.println("n = " + n);
                if (n == -1) {
                    //读毕 n就会返回-1
                    // 在这里首先第一次会把数组填满 后面第二次循环文件内没字节了就返回-1
                    break;
                }
                for (int i = 0; i < n; i++) {
                    System.out.printf("0x%x ", arrB[i]);
                }
                System.out.println();
            }
        }

    }


}

代码示例2我们解决了关闭文件流对象释放文件描述符表中元素的问题,但是使用try-catch好像不够优雅,于是我们将InputStream流对象构造的这条语句放入try,这样java会在文件操作完成之后自动给我们的流对象调用close方法,但是要注意的一点就是这种编写方式的前提是放入try后的括号的对象对应的类必须实现了Closeable接口。
另外在我们使用FileInputStream类对象时可以配合Scanner对象来进行使用,代码示例如下:

package io;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class  Demo10 {

    public static void main(String[] args) {

        try (InputStream inputStream = new FileInputStream("F:/test.txt")) {

            Scanner sc = new Scanner(inputStream);
            // 本来这里要在控制台输入 但是现在直接将文件中的数据读走了
            sc.next();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }

}

以前我们使Scanner对象都是在终端输入数据,这样写代码就是将我们从终端输入的数据换成了文件中的数据。另外能这么写的原因看下图Scanner类的构造函数的参数就不难理解了,本来就是InputStream类型的,另外我们在之前写入Scanner对象的System.in也是InputStream类型的。
在这里插入图片描述

1.2 写文件

写文件的流程和读文件的流程类似的,需要使用OutputStream抽象类以及其子类FileOutputStream。写文件也要通过FileOutputStream对象的write方法来实现,FileOutputStream对象的write也是要分为三个版本,如下图。
在这里插入图片描述
和前面的read参数很类似,第一个版本就是一次在文件中写入一个字节,写入的字节由b指定。第二个版本就是一次写入文件一整个数组,第三个版本也是往文件中写入一个数组,只是只写入数组在区间内的部分。
代码示例如下:

package io;

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

public class Demo7 {

    public static void main(String[] args) {

        try (OutputStream outputStream = new FileOutputStream("F:/test.txt",true)) {
            // 注意这里打开文件会发现文件内原本的内容被清空 里面全是新添加的内容
            // 这里的清空操作是打开文件时清空的,即构造对象时清空的
            // 要是想在文件内追加即append内容 只需在构造对象时将第二个参数设为true
            outputStream.write(97);
            outputStream.write(98);
            outputStream.write(98);
            outputStream.write(99);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

这里写入文件的代码需要注意一点就是构造FileOutputStream对象时会直接将对应路径上的文件内容给清空,如果想要实现写入文件是一种append的效果,就要在构造流对象时将第二个参数设为true。示例代码中的注释也说明了。

二、字符流

使用字符流和字节流操作文件内容基本的流程是类似的,但是字符流读取和写入文件内容的基本单位是字符,字节流是字节。

2.1 读文件

使用字符流读文件过程和使用字节流读文件的流程是相似的,只有很少的差别。
代码示例如下:

package io;


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

public class Demo8 {
    public static void main(String[] args) {

        try (Reader reader = new FileReader("./testDir/test2.txt")) {
            // 读入字符的时候要思考一个问题
            // 文件中的字符编码集用的是utf8 一个中文字是三个字节
            // java中的一个中文字是两个字节
            // 为什么java中的char还能接收并且打印中文字
            // 原因:因为在java中的char类型在接收字符时会自动将utf8转换为unicode编码集
            // 实际上java中很多类型在接收数据时都会进行字符集的转换
            while (true) {
                int n = reader.read();
                if(n==-1) {
                    break;
                }

                System.out.printf("%c ",n);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }

}

如以上的示例代码,与字节流的FileinputStream不同这里读文件内容使用的是FileReader类,过程还是类似的,外面套个循环,每次读取一个字符,直到读取操作返回值为-1代表文件读完此时跳出循环。
但是这里会有一个疑问,在windows下文件中的字符是采用utf8的编码集,在这个编码集当中单个汉字的字节数是三个,为什么在java中还能使用char类型接收汉字并且打印,char类型在java中只有两个字节。这个问题的答案就是说java中读取到文件中内容时会自动转换编码集,对于char类型java中使用的时unicode编码集,读到的文件中汉字会自动转为char类型,也就会经过utf8到unicode这个过程,在unicode当中汉字占两个字节。
在java中不同的类型实际上用的编码集都是不同的,比如说String类型内部是使用utf8,char类型使用的是unicode。使用String类型变量保存你好就需要6个字节,当使用字符串s.charAt()这种方法将字符赋给char类型变量时,就会将编码集从utf8转为unicode,此时一个字就从三个字节转为了两个字节。

2.2 写文件

流程都是类似的,写文件的操作主要是通过Writer类中的write方法来实现的。
代码示例如下:

package io;

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class Demo9 {

    public static void main(String[] args) {
        try (Writer writer = new FileWriter("F:/test.txt")) {
            // 字符流写文件和字节流一样 都是要加上一个true这个参数才能实现append这样的效果
            writer.write("你好世界");


        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

和字节流的write一样,要想在写文件时实现append的效果就需要在构建FileWriter对象时将第二个参数设为true,否则写文件时都会先清空路径上的文件内容。

三、文件IO三道例题

(1)扫描指定目录,并找到名称中包含指定字符的所有普通文件(包含目录)。

package io;

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

public class Demo11 {

    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);

        System.out.println("输入要查找文件的目录:");
        String dirPath = sc.nextLine();
        System.out.println("输入要查找文件的关键词:");
        String keyWord = sc.nextLine();

        // 根据输入的路径构造file对象
        File dir = new File(dirPath);

        // 如果输入的路径不是目录则直接返回并报错
        if (!dir.isDirectory()) {
            System.out.println("输入路径非法!");
            return;
        }

        // 关键词和目录都是正确的那么就开始查找文件
        searchFile(dir, keyWord);

    }

    private static void searchFile(File dir, String keyWord) {
        // 将目录下的所有文件输出成数组
        File[] files = dir.listFiles();

        // 如果数组为空直接返回 这也是递归结束的条件
        if (files == null) {
            return;
        }

        // 遍历数组
        for (File file : files) {
            // 如果文件是文件 那么判断是否包含关键词
            if (file.isFile()) {
                // 包含则匹配成功
                if (file.getName().contains(keyWord)) {

                    System.out.println("匹配成功:" + file.getAbsoluteFile());
                }
                // 如果是目录则进行递归 在下一级目录进行查找
            } else if (file.isDirectory()) {

                searchFile(file, keyWord);

            }


        }


    }
}

(2)复制文件,输入一个路径,表示要被复制的文件,输入另一个路径,表示要复制到的目标路径。

package io;

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

public class Demo12 {

    public static void main(String[] args) throws IOException {

        Scanner sc = new Scanner(System.in);
        System.out.println("输入要复制的文件路径:");
        String srcPath = sc.nextLine();
        System.out.println("输入要复制到的文件路径");
        String destPath = sc.nextLine();

        File fileSrc = new File(srcPath);
        File fileDest = new File(destPath);

        if (!fileSrc.isFile()) {
            System.out.println("输入的要复制的文件路径不合法!");
            return;
        }
        if (!fileDest.getParentFile().isDirectory()) {
            System.out.println("输入的复制到的文件路径不合法!");
            return;
        }


        byte[] bytes = new byte[1024];
        // OutPutStream会自动创建文件
        try (InputStream inputStream = new FileInputStream(fileSrc);
             OutputStream outputStream = new FileOutputStream(fileDest)) {
            while (true) {
                int n = inputStream.read(bytes);
                if (n == -1) {
                    break;
                }
                outputStream.write(bytes, 0, n);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}

(3)输入一个路径再输入一个查询词,搜索这个路径中文件内容包含这个查询次的文件。

package io;

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

public class Demo13 {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入目标目录:");
        String dirPath = sc.nextLine();
        System.out.println("请输入要匹配的关键词:");
        String keyWord = sc.nextLine();

        File dirFile = new File(dirPath);

        if (!dirFile.isDirectory()) {
            System.out.println("输入路径不合法!!");
            return;
        }

        search(dirFile, keyWord);

    }

    private static void search(File dirFile, String keyWord) {
        File[] files = dirFile.listFiles();

        if (files == null) {

            return;
        }

        for (File f :
                files) {
            if (f.isFile()) {

                match(f, keyWord);
            } else if (f.isDirectory()) {

                search(f, keyWord);
            }
        }


    }

    private static void match(File f, String keyWord) {
        StringBuilder stringBuilder = new StringBuilder();
        try (Reader reader = new FileReader(f)) {
            while (true) {
                int b = reader.read();
                if (b == -1) {
                    break;
                }

                stringBuilder.append((char) b);

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        if (stringBuilder.indexOf(keyWord) >= 0) {

            System.out.println("匹配成功:" + f.getAbsolutePath());
        }

    }


}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值