JAVA文件相关API

不管现实多么惨不忍睹,都要持之以恒地相信,这只是黎明前短暂的黑暗而已。不要惶恐眼前的难关迈不过去,不要担心此刻的付出没有回报,别再花时间等待天降好运。真诚做人,努力做事!你想要的,岁月都会给你。JAVA文件相关API,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

在java中,最难用的api非文件相关的api莫属。首先,在java文件相关api中划分了很多层,每一层又有很多功能和概念相同的api,不仔细区分和理解很容易混淆;其次,由于划分层次太细,导致api调用起来相当麻烦。
在文件读写时,可以划分为两大类:

  1. 字符相关的API一般都是以Writer关键词结尾的,这类文件就是平时我们能够读懂的文本文件,文本文件对人类友好,但涉及到内容的编码和解码,所以性能不如二进制好。
  2. 二进制相关的API一般都是以Stream结尾的,这类文件如图片、音乐、视频等字节文件,这类文件需要指定的编解码器才能处理,直接打开文件内容不能读懂,但二进制文件对机器友好,性能更高。

在操作系统底层,对于文件这类涉及到外部设备读写的操作,会涉及到内核态和用户态切换,这两种状态间切换会损耗很大的系统性能。所以系统底层就出现了零拷贝技术,它的核心思想就是将用户缓冲区和内核缓冲区建立映射关系,用户操作应用内存就是操作系统内存,避免了用户态和内核态之间的切换。
我们的应用大多部署在linux中,对于linux底层,实现零拷贝有两种方案,一种是mmap技术,另外一种是sendfile。对于mmap技术,适用于文件内容随时更改,建立内存映射后,操作内存数据就是操作文件数据;对于sendfile技术,常用于整个文件拷贝或发送场景。

接下来分别介绍java文件读写的几个常用api。
在介绍API时,先在类中定义相关内容如下:

public class TestFile {

    /**
     * 指定循环次数,这里是针对上面内容的循环次数,保证数据写入到文件中的数据是1G大小
     */
    public static int COUNT = 0;

    /**
     * 模拟一个64字节字符串写入文件
     */
    public static final String STR_CONTENT = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890|\n";
//    public static final byte[] STR_BYTES = STR_CONTENT.getBytes(StandardCharsets.UTF_8);

    /**
     * 内存映射文件大小
     */
    public static final int FILE_SIZE = 1073741824;

    /**
     * 定义内容大小的几个常量
     */
    public static final int BYTES_64 = 64;
    public static final int BYTES_1KB = 1024;
    public static final int BYTES_8KB = 8 * 1024;
    public static final int BYTES_64KB = 64 * 1024;
    public static final int BYTES_512KB = 512 * 1024;
    public static final int BYTES_4MB = 4 * 1024 * 1024;
    public static final int BYTES_64MB = 64 * 1024 * 1024;
    public static final int BYTES_256MB = 256 * 1024 * 1024;
    public static final int BYTES_1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) {
        // 每次写数据长度:要测试不同数据长度修改相关常量参数
        int length = BYTES_64;
        // 测试写文件类型:要测试不同的api修改参数类型即可
        int type = 0;
    
        // 拼接内容字符串
        StringBuilder builder = new StringBuilder();
        // 循环次数(每个字符串长度是64byte)
        int loop = length / 64;
        for(int i = 0; i < loop; ++i) {
            builder.append(STR_CONTENT);
        }
    
        // 计算写文件循环次数
        COUNT = FILE_SIZE / length;
    
        // 获取写入文件字符串和字节数组
        final String content = builder.toString();
        final byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
    
        // 写文件并获取执行耗时
        final long start = System.currentTimeMillis();
    
        switch (type) {
            case 0:
                writeFile0(content);
                break;
            case 1:
                writeFile1(content);
                break;
            case 2:
                writeFile2(content);
                break;
            case 3:
                writeFile3(bytes);
                break;
            case 4:
                writeFile4(bytes);
                break;
            case 5:
                writeFile5(bytes);
                break;
            case 6:
                writeFile6(bytes);
                break;
        }
    
        System.out.println("write file use time : " + (System.currentTimeMillis() - start));
    }
}

一、OutputStreamWriter 写文本文件:可以指定文件内容的编码方式,并且通过FileOutputStream构造方法的第二个参数指定文件内容是追加写入。

public static void writeFile0(String content) {
    try (FileOutputStream os = new FileOutputStream("store/testfile0.txt", true);
         OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8")) {
        for(int i = 0; i < COUNT; i++) {
            osw.write(content);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

二、BufferedWriter:它实际上是在OutputStreamWriter基础上在包装了一层,增加了缓冲区,每次缓冲区满了之后将数据刷出到磁盘,其实在OutputStreamWriter和BufferedWriter里面有一个方法flush(),这个方法可以手动将数据刷出到磁盘

public static void writeFile1(String content) {
    try (FileOutputStream fos = new FileOutputStream("store/testfile1.txt", true);
         OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
         BufferedWriter bw = new BufferedWriter(osw)) {
        for(int i = 0; i < COUNT; i++) {
            bw.append(content);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

三、FileWriter、PrintWriter:这两个api也可以实现字符文件写入,api方法都是相通的,在执行文件写入时,性能不如BufferedWriter

public static void writeFile2(String content) {
    try (FileWriter fw = new FileWriter("store/testfile2.txt", true)) {
        for(int i = 0; i < COUNT; i++) {
            fw.append(content);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

四、FileOutputStream:这个api直接写二进制字节数组到文件中

public static void writeFile3(byte[] bytes) {
    try (FileOutputStream os = new FileOutputStream("store/testfile3.txt", true)) {
        for(int i = 0; i < COUNT; i++) {
            os.write(bytes);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

五、BufferedOutputStream:它是对OutputStream的封装,增加了缓冲区加快文件写入速度

public static void writeFile4(byte[] bytes) {
    try (FileOutputStream os = new FileOutputStream("store/testfile4.txt", true);
         BufferedOutputStream bos = new BufferedOutputStream(os)) {
        for(int i = 0; i < COUNT; i++) {
            bos.write(bytes);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

下面分别介绍使用FileChannel方法和FileChannel中的map方法写入数据到文件:

FileChannel获取有以下几种方式:

# 通过FileOutputStream获取FileChannel,这种方式获取到的FileChannel只能写入数据
FileOutputStream fos = new FileOutputStream("store/testfile0.txt", true);
FileChannel channel = fos.getChannel();

# 通过FileInputStream获取FileChannel,这种方式获取到的FileChannel只能读取数据
FileInputStream fis = new FileInputStream("store/testfile0.txt");
FileChannel channel = fis.getChannel();

# 通过RandomAccessFile获取FileChannel,这种方式获取到的FileChannel既可以读也可以写数据
RandomAccessFile raf = new RandomAccessFile("store/testfile0.txt", "rw");
FileChannel channel = raf.getChannel();

# 通过FileChannel的open方法获取,这里可以指定打开方式,默认是只读方式打开
FileChannel channel = FileChannel.open(Paths.get("store/testfile0.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ);

六、FileChannel:通过FileOutputStream的getChannel方法获取到FileChannel管道,通过管道写数据到文件中

public static void writeFile5(byte[] bytes) {
    try (FileOutputStream fos = new FileOutputStream("store/testfile5.txt", true);
         FileChannel channel = fos.getChannel()) {
        for(int i = 0; i < COUNT; i++) {
            ByteBuffer wrap = ByteBuffer.wrap(bytes);
            channel.write(wrap);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

七、mmap内存映射写文件:通过使用FileChannel的map方法将应用缓冲区与内核缓冲区建立映射关系,这时可以直接将数据写入到内核态,减少了数据拷贝

public static void writeFile6(byte[] bytes) {
    try (RandomAccessFile raf = new RandomAccessFile("store/testfile6.txt", "rw");
         FileChannel fc = raf.getChannel()) {
        MappedByteBuffer mapper = fc.map(FileChannel.MapMode.READ_WRITE, 0, FILE_SIZE);
        for(int i = 0; i < COUNT; i++) {
            mapper.put(bytes);
        }
        mapper.force();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

下面的统计数据是在本地分别调用上面几个api写入1G文件不同内容大小的耗时(单位:毫秒):

使用API类型 64byte耗时 1kb耗时 8kb耗时 64kb耗时 512kb耗时 4mb耗时 64mb耗时 256mb耗时
OutputStreamWriter 2786 2527 2352 2400 2530 2572 3633 3457
BufferedWriter 2469 2122 2032 2017 2083 2091 2327 2144
FileWriter 2781 2599 2365 2422 2488 2695 3517 3527
FileOutputStream 56625 4456 1511 1033 1038 1002 1281 1716
BufferedOutputStream 2296 1674 1508 971 939 1006 1370 1648
FileChannel 55131 4521 1528 965 973 1025 1277 1317
MappedByteBuffer 2843 1953 1929 1941 1962 1902 2055 1996

对比上面api的调用耗时发现,FileOutputStream和FileChannel执行耗时非常高,其他几个API基本上相差不大,这是因为FileOutputStream每次调用write方法时就会将用户态数据刷出到内核态,然后持久化到磁盘文件中,其他api都会存在一个buffer缓冲区,当缓冲区满了时才会执行一次数据刷出,这个缓冲区默认大小是8kb,通过缓冲区技术可以减少用户态和内核态之间的切换,提高了系统的性能。
通过FileChannel写数据时,每次写入数据比较小时性能表现很差,当把每次写入数据量增大到几kb或几十kb时,会发现性能提升非常明显。
通过缓冲区技术可以减少状态转换,但是还是会涉及到将数据从用户内存写入到内核内存区域的过程,MappedByteBuffer写数据则用到了零拷贝技术,通过mmap技术实现内存映射,这时程序写内存直接写到内核缓冲区,操作系统会在合适时机将数据刷出到磁盘。
其实在rocketmq底层就有两种数据持久化方式,一种是使用mmap方式写,还有一种是使用DirectByteBuffer+FileChannel方式,并且使用异步方式刷盘,这样就会使写入文件性能非常高,但是该方式存在数据丢失风险,所以采用主从同步+异步持久化方案来保证高可用。
总结:通过以上api执行耗时对比,当写入小数据量时,通过缓冲区方案或内存映射方式写文件性能差异并不明显,但是当每次写入数据量比较大时,通过FileChannel写文件性能提升明显。如果在写入文件时又涉及到读取文件内容,这时选择MappedByteBuffer会是更加高明的方案。

对于文本文件读取常用的api如下:

一、BufferedReader:带有缓冲区的读取文件方法,与写缓冲区类似,都是为了加快文件读取速度。

public static void readFile0() {
    InputStreamReader isr = null;
    BufferedReader br = null;
    try {
        File file = new File("store/testfile0.txt");
        if(file.exists()) {
            isr = new InputStreamReader(new FileInputStream(file), "utf-8");
            br = new BufferedReader(isr);
            String line = br.readLine();
            while (line != null) {
                line = br.readLine();
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if(br != null) br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if(isr != null) isr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

二、Scanner:通过遍历方式读取文件,避免一次将文件都加载进内存导致内存溢出。

public static void readFile1() {
    Scanner scanner = null;
    try {
        scanner = new Scanner(new File("store/testfile0.txt"));
        String line = null;
        while (scanner.hasNextLine()) {
            line = scanner.nextLine();
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if(scanner != null) scanner.close();
    }
}

三、RandomAccessFile:通过RandomAccessFile也可以实现逐行读取数据。

public static void readFile2() {
    RandomAccessFile raf = null;
    try {
        raf = new RandomAccessFile("store/testfile0.txt", "rw");
        String line = raf.readLine();
        while (line != null) {
            line = raf.readLine();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if(raf != null) raf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

对于文本文件的读取还有其他相关的工具类做了非常好的封装,大家可以多了解一些,这里的方法都是基于jdk里面的相关api做的总结。

而二进制文件的读取涉及到解码,这里就不做过多的阐述,二进制文件可以通过RandomAccessFile相关的api或mmap内存映射来读取。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/181863.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!