【开发随记】JAVA超大文件的读取和写入实现(附demo)

230616更新,鉴于有些小伙伴反应代码运行报没找到invokeCleaner这个方法,在这里补充说明一下。本人环境是java11,而java9之后才有invokeCleaner这个方法。如果使用java9以前版本小伙伴,可以参考这篇文章 https://zhuanlan.zhihu.com/p/37325025
释放资源的时候调用Cleaner类的clean方法释放就行


230111更新,之前读取文件的时候默认以\r\n作为换行符进行判断和处理,实际上很多文件的换行符是\n。因此带来了些许问题,进行了修复,代码更新。主要思路是用一个私有变量lineBreakLength记录换行符的位数\n为1位,\r\n为2位。


因近期工作需要对超大单文本文件进行读取和写入,经过简单研究分别写了读取类和写入类供项目使用。现在简单整理出来分享。

核心思想就是通过MappedByteBuffer进行读取和写入。

读取类 MappedBiggerFileReader.java

package com.io;

import org.apache.commons.lang3.StringUtils;
import sun.misc.Unsafe;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @Description 大文件读取类
 * @Author wqin
 * @Date 2022/12/19
 */
public class MappedBiggerFileReader {
    /**
     * 内存映射缓冲区数组
     */
    private MappedByteBuffer[] mappedBufArray;
    /**
     * 采取readLines方法时用到的缓存数组,为了保证每次读取都是某行开头作为开头,某行结尾作为结尾
     */
    private byte[] tempArray;
    /**
     * 内存映射缓冲区下标
     */
    private int count = 0;
    /**
     * 内存映射缓冲区个数
     */
    private int number;
    /**
     * 文件输入流
     */
    private FileInputStream fileIn;
    /**
     * 指定读取的编码,默认为GBK
     */
    private String charsetName;
    /**
     * 文件总长度
     */
    private long fileLength;
    /**
     * 缓存数组最大大小,默认大小65536
     */
    private int arraySize;
    /**
     * 缓存数组
     */
    private byte[] array;
    /**
     * 换行符长度,默认为\r\n,即2
     * 若为\n,则改为1
     */
    private int lineBreakLength = 2;

    public MappedBiggerFileReader(String fileName) throws IOException {
        this(fileName, "GBK", 65536);
    }

    public MappedBiggerFileReader(String fileName, int arraySize) throws IOException {
        this(fileName, "GBK", arraySize);
    }

    public MappedBiggerFileReader(String fileName, String charsetName) throws IOException {
        this(fileName, charsetName, 65536);
    }

    /**
     * 核心构造方法,读取整个文件到缓存数组mappedBufArray里
     * @param fileName
     * @param charsetName
     * @param arraySize
     * @throws IOException
     */
    public MappedBiggerFileReader(String fileName, String charsetName, int arraySize) throws IOException {
        this.fileIn = new FileInputStream(fileName);
        this.charsetName = charsetName;
        FileChannel fileChannel = fileIn.getChannel();
        this.fileLength = fileChannel.size();
        this.number = (int) Math.ceil((double) fileLength / (double) Integer.MAX_VALUE);
        // 内存文件映射数组
        this.mappedBufArray = new MappedByteBuffer[number];
        long preLength = 0;
        // 映射区域的大小
        long regionSize = Integer.MAX_VALUE;
        // 将文件的连续区域映射到内存文件映射数组中
        for (int i = 0; i < number; i++) {
            if (fileLength - preLength < (long) Integer.MAX_VALUE) {
                // 最后一片区域的大小
                regionSize = fileLength - preLength;
            }
            mappedBufArray[i] = fileChannel.map(FileChannel.MapMode.READ_ONLY, preLength, regionSize);
            // 下一片区域的开始
            preLength += regionSize;
        }
        this.arraySize = arraySize;
    }

    /**
     * 获取文件的第一行,返回其字符串,可以多次获取,不影响当前读取位置
     * @return
     * @throws IOException
     */
    public String getFirstLine() throws IOException {
        if (number < 1){
            return "";
        }
        MappedByteBuffer mappedByteBuffer = fileIn.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, arraySize);
        int limit = mappedByteBuffer.limit();
        int size = Math.min(limit, arraySize);
        byte[] tBytes = new byte[size];
        mappedByteBuffer.get(tBytes,0,size);
        int firstLineEndIndex = firstLineEndIndex(tBytes);
        byte[] tempArr = new byte[0];
        if (firstLineEndIndex > 0){
            tempArr = new byte[firstLineEndIndex];
            System.arraycopy(tBytes, 0, tempArr, 0,firstLineEndIndex);
        }
        return new String(tempArr, charsetName);
    }

    /**
     * 对外接口:读取第一行到缓存数组array里(只能读取一次,会将读取位置向后移动,必须在调用其他read方法前调用才有效)
     * 注:现在\r\n和\n都视为换行符
     * @return
     * @throws IOException
     */
    public int readFirstLine() throws Exception {
        // 该方法需保证该文件是第一次被调用readLines或readFirstLine
        if (count >= number || count > 0 || tempArray != null) {
            return -1;
        }
        int position = mappedBufArray[count].position();
        if (position != 0){
            return -1;
        }
        int limit = mappedBufArray[count].limit();
        int res = -1;
        if (limit - position > arraySize) {
            array = new byte[arraySize];
            mappedBufArray[count].get(array);
            // 读取完之后解析一下然后截取到第一个行结尾换行符
            int firstLineEndIndex = firstLineEndIndex(array);
            if (firstLineEndIndex == 0) {
                // 这种情况代表整个文件只有一行,且第一行为空串
                array = new byte[0];
                mappedBufArray[count].position(0);
                res = 0;
            }else if (firstLineEndIndex > 0){
                // 正常情况,截取出第1个换行符前的内容
                byte[] tArr = new byte[firstLineEndIndex];
                System.arraycopy(array, 0, tArr, 0, firstLineEndIndex);
                array = new byte[firstLineEndIndex];
                System.arraycopy(tArr, 0, array, 0, firstLineEndIndex);
                mappedBufArray[count].position(firstLineEndIndex+lineBreakLength);
                res = firstLineEndIndex;
            }else {
                throw new Exception("单行超过长度了");
            }
        } else {
            // 整个文件的内容没有arraySize大则把整个文件的内容先放进array里再进行判断和处理
            array = new byte[limit - position];
            mappedBufArray[count].get(array);
            int firstLineEndIndex = firstLineEndIndex(array);
            if (firstLineEndIndex == -1){
                // 如果没有换行符代表整个文件只有一行
                res = limit - position;
                if (count < number) {
                    count++;// 转换到下一个内存文件映射
                }
            }else if(firstLineEndIndex == 0){
                // 这种情况代表整个文件只有一行,且第一行为空串
                array = new byte[0];
                mappedBufArray[count].position(0);
                res = 0;
                if (count < number) {
                    count++;// 转换到下一个内存文件映射
                }
            }else {
                // 正常情况,截取出第1个换行符前的内容
                byte[] tArr = new byte[limit - position];
                System.arraycopy(array, 0, tArr, 0, firstLineEndIndex);
                array = new byte[firstLineEndIndex];
                System.arraycopy(tArr, 0, array, 0, firstLineEndIndex);
                mappedBufArray[count].position(firstLineEndIndex+lineBreakLength);
                res = firstLineEndIndex;
            }
        }
        return res;
    }

    /**
     * 对外接口:按若干行读到缓存数组array里(只能读取小于预设的arraySize的句子,大于会抛异常)
     * 注:现在\r\n和\n都视为换行符
     * @return
     * @throws IOException
     */
    public int readLines() throws Exception {
        if (count >= number) {
            return -1;
        }
        int limit = mappedBufArray[count].limit();
        int position = mappedBufArray[count].position();
        int tempArrayLength = 0;
        if (tempArray != null){
            tempArrayLength = tempArray.length;
        }
        if (limit - position + tempArrayLength > arraySize) {
            array = new byte[arraySize];
            if (tempArrayLength != 0){
                System.arraycopy(tempArray,0,array,0, tempArrayLength);
                mappedBufArray[count].get(array, tempArrayLength, arraySize - tempArrayLength);
                tempArray = null;
            }else {
                mappedBufArray[count].get(array);
            }
            // 读取完之后解析一下然后截取到最后一个换行符
            int lastIndexOfNextLine = lastLineEndIndex(array);
            if (lastIndexOfNextLine >= 0){
                byte[] tempArr = new byte[arraySize];
                System.arraycopy(array, 0, tempArr, 0, arraySize);
                array = new byte[lastIndexOfNextLine];
                System.arraycopy(tempArr, 0, array, 0,lastIndexOfNextLine);
                int length = lastIndexOfNextLine+lineBreakLength;
                tempArray = new byte[arraySize - length];
                System.arraycopy(tempArr, length, tempArray, 0, arraySize - length);
                return length;
            }else {
                throw new Exception("单行超过长度了");
            }
        } else {// 本内存文件映射最后一次读取数据
            array = new byte[limit - position + tempArrayLength];
            if (tempArrayLength != 0){
                System.arraycopy(tempArray,0,array,0, tempArrayLength);
                mappedBufArray[count].get(array, tempArrayLength, limit - position);
                tempArray = null;
            }else {
                mappedBufArray[count].get(array);
            }
            // 如果正好以换行符结尾,则正常读取,否则把当前array作为tempArray继续下次读取
            if (endWitEndSymbol(array) || !mappedBufArray[count].hasRemaining()){
                if (count < number) {
                    count++;// 转换到下一个内存文件映射
                }
                return limit - position + tempArrayLength;
            }else {
                if (count < number) {
                    count++;// 转换到下一个内存文件映射
                }
                tempArray = new byte[limit - position + tempArrayLength];
                System.arraycopy(array,0,tempArray,0,limit - position + tempArrayLength);
                return readLines();
            }
        }
    }


    /**
     * 对外接口:按字节读取内容至缓存数组array里
     * @return
     * @throws IOException
     */
    public int read() throws IOException {
        if (count >= number) {
            return -1;
        }
        int limit = mappedBufArray[count].limit();
        int position = mappedBufArray[count].position();
        if (limit - position > arraySize) {
            array = new byte[arraySize];
            mappedBufArray[count].get(array);
            return arraySize;
        } else {// 本内存文件映射最后一次读取数据
            array = new byte[limit - position];
            mappedBufArray[count].get(array);
            if (count < number) {
                count++;// 转换到下一个内存文件映射
            }
            return limit - position;
        }
    }

    /**
     * 对外接口:释放资源,读取完文件后请调用该方法。否则会影响删除之类的操作
     * 注:sun.misc.Unsafe该类为sun包下的,根据各人jdk不同可能不需要用反射来释放
     * @throws IOException
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    public void close() throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        if (fileIn != null){
            fileIn.close();
        }
        array = null;
        tempArray = null;
        if (mappedBufArray != null){
            Unsafe unsafe = createUnsafe();
            Method invokeCleaner = Unsafe.class.getDeclaredMethod("invokeCleaner", ByteBuffer.class);
            for (MappedByteBuffer mappedByteBuffer : mappedBufArray) {
                // 在关闭资源时执行以下代码释放内存
                invokeCleaner.setAccessible(true);
                invokeCleaner.invoke(unsafe, mappedByteBuffer);
            }
        }
    }

    private static Unsafe createUnsafe() {
        try {
            Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            Field field = unsafeClass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 对外接口,获取当前字符数组缓存(即正读取到的内容)
     * @return
     */
    public byte[] getArray() {
        return array;
    }

    /**
     * 对外接口,获取当前读到的内容,返回的结果是以每一行的字符串列表(这里将\r\n和\n都视为换行符)
     * @return
     * @throws UnsupportedEncodingException
     */
    public List<String> getLineStringList() throws UnsupportedEncodingException {
        String data = new String(array, charsetName);
        if(StringUtils.isNotEmpty(data)){
            String dataT= data.replaceAll("\r\n","\n");
            return Arrays.asList(dataT.split("\n"));
        }
        return new ArrayList<>();
    }

    /**
     * 对外接口,获取当前文件长度
     * @return
     */
    public long getFileLength() {
        return fileLength;
    }

    /**
     * 判断当前字符数组是否以\n为结尾
     * @param array
     * @return
     */
    private boolean endWitEndSymbol(byte[] array){
        if (array == null || array.length == 0){
            return false;
        }
        return array[array.length - 1] == '\n';
    }

    /**
     * 获取当前字符数组最后一个换行符的下标,同时记录换行符占位数lineBreakLength("\r\n"=2,"\n"=1)
     * @param array
     * @return
     */
    private int lastLineEndIndex(byte[] array){
        if (array == null || array.length == 0){
            return -1;
        }
        for (int i = array.length-1; i > 0; i--){
            if (array[i] == '\n'){
                if (array[i-1] == '\r'){
                    lineBreakLength = 2;
                    return i-1;
                }else {
                    lineBreakLength = 1;
                    return i;
                }
            }
        }
        // 第一个字符为换行符的情况
        if (array[0] == '\n'){
            lineBreakLength = 1;
            return 0;
        }
        return -1;
    }

    /**
     * 获取当前字符数组第一个换行符的下标,同时记录换行符占位数lineBreakLength("\r\n"=2,"\n"=1)
     * @param array
     * @return
     */
    private int firstLineEndIndex(byte[] array){
        if (array == null || array.length == 0){
            return -1;
        }
        // 第一个字符即为换行符的情况
        if (array[0] == '\n'){
            lineBreakLength = 1;
            return 0;
        }
        // 其他情况
        for (int i = 0; i < array.length-1; i++){
            if (array[i+1] == '\n'){
                if (array[i] == '\r'){
                    lineBreakLength = 2;
                    return i;
                }else {
                    lineBreakLength = 1;
                    return i+1;
                }
            }
        }
        return -1;
    }
}

写入类 MappedBiggerFileWriter.java

package com.io;

import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Description 大文件写入类
 * @Author wqin
 * @Date 2022/12/19
 */
public class MappedBiggerFileWriter {

    /**
     * 对外接口:以指定编码字符串对应的编码,向目标文件写入内容(字符串),如果没有则创建
     * @param to 目标文件全路径
     * @param content 待写入内容
     * @param charsetString 编码字符串
     * @throws IOException UnsupportedEncodingException异常
     */
    public static void write(String to, String content, String charsetString) throws IOException {
        byte[] bs;
        if (charsetString != null && charsetString.length() > 0){
            bs = content.getBytes(charsetString);
        }else {
            bs = content.getBytes();
        }
        write(to, bs);
    }

    /**
     * 对外接口:以指定编码,向目标文件写入内容(字符串),如果没有则创建
     * @param to 目标文件全路径
     * @param content 待写入内容
     * @param charset 编码
     * @throws IOException IO异常
     */
    public static void write(String to, String content, Charset charset) throws IOException {
        byte[] bs;
        if (charset != null){
            bs = content.getBytes(charset);
        }else {
            bs = content.getBytes();
        }
        write(to, bs);
    }

    /**
     * 对外接口:以默认编码向目标文件写入内容(字符串),如果没有则创建
     * @param to 目标文件全路径
     * @param content 待写入内容
     * @throws IOException IO异常
     */
    public static void write(String to, String content) throws IOException {
        write(to, content, "");
    }

    /**
     * 对外接口:向目标文件写入内容(字节数组),如果没有则创建
     * @param to 目标文件全路径
     * @param bs 待写入内容字节数组
     * @throws IOException 异常
     */
    public static void write(String to, byte[] bs) throws IOException {
        File f = new File(to);
        // 文件/目录不存在则将文件/目录创建
        if (!f.exists()){
            if (!f.getParentFile().exists()){
                f.getParentFile().mkdirs();
            }
            f.createNewFile();
        }
        // 通过RandomAccessFile拿到channel,然后利用MappedByteBuffer写文件
        RandomAccessFile acf = null;
        FileChannel fc;
        try {
            acf = new RandomAccessFile(to, "rw");
            fc = acf.getChannel();
            long offset = f.length();
            MappedByteBuffer mbuf = fc.map(FileChannel.MapMode.READ_WRITE, offset, bs.length);
            mbuf.put(bs);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (acf != null){
                acf.close();
            }
        }
    }

}

Demo代码 MappedBiggerFileDemo.java

package com.io;

/**
 * @Description: 大文件读写类demo
 * @Author : wqin
 * @DateTime : 2022/12/19 15:25
 */
public class MappedBiggerFileDemo {
    public static void main(String[] args) throws Exception {
        // 大文件读取用法
        String readFilePath = "待读取的文件的全路径+文件名";
        MappedBiggerFileReader reader = new MappedBiggerFileReader(readFilePath);
        // 读取第一行,返回值为实际读取到的位数,-1为读取有误
        if (reader.readFirstLine() != -1 ){
            System.out.println("该文件第一行内容为:\n" + new String(reader.getArray()));
        }
        // reader.readLines()是以若干行为单位,读取剩下的所有内容
        // 用reader.read()读取是以字节为单位,效率更高但不方便比较和处理,如果要处理的就是字节流用该方法更合适
        while (reader.readLines() != -1){
            System.out.println("按若干行读取文件,本次读取到的内容为:");
            System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓下面为本次读取到的内容↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓");
            System.out.println(new String(reader.getArray()));
            System.out.println("↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑上面为本次读取到的内容↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑");
        }
        // 读取完文件后要释放资源
        reader.close();

        // 大文件写入用法,因用法更简单一些所以直接写成了工具类的形式。释放资源等在方法里已经做过了直接调用即可。
        String writeFilePath = "待写入的文件的全路径+文件名";
        MappedBiggerFileWriter.write(writeFilePath, "待写入的内容...");

    }
}

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值