仅使用 Java.IO 在文件中读取/写入字节

Posted

技术标签:

【中文标题】仅使用 Java.IO 在文件中读取/写入字节【英文标题】:Read/Write Bytes to and From a File Using Only Java.IO 【发布时间】:2022-01-01 14:11:10 【问题描述】:

我们如何在 Java 中将字节数组写入文件(并从该文件中读取)?

是的,我们都知道已经有很多这样的问题,但是由于有很多方法可以完成这项任务,因此它们变得非常混乱和主观。

所以让我们缩小问题的范围:

域:

安卓/Java

我们想要什么:

快(尽可能) 无错误(以严格细致的方式)

我们没有做什么:

第三方库 任何需要高于 23 (Marshmallow) 的 android API 的库

(所以,排除了Apache Commons、Google Guava、Java.nio,剩下的是good ol' Java.io)

我们需要什么:

字节数组在经过先写后读过程后总是完全相同(内容和大小) Write 方法只需要两个参数:File 文件和 byte[] 数据 Read 方法返回一个字节[],并且只需要一个参数:文件文件

在我的特定情况下,这些方法是私有的(不是库)并且不负责以下内容,(但如果您想创建一个适用于更广泛受众的更通用的解决方案,去吧):

线程安全(文件不会同时被多个进程访问) 文件为空 文件指向不存在的位置 文件位置缺少权限 字节数组太大 字节数组为空 处理任何“索引”、“长度”或“附加”参数/功能

所以......我们正在寻找未来人们可以认为可以安全使用的最终防弹代码,因为您的答案有很多赞成票,并且没有 cmets 说,“如果...可能会崩溃。”

这是我目前所拥有的:

将字节写入文件:

private void writeBytesToFile(final File file, final byte[] data) 
        try 
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(data);
            fos.close();
         catch (Exception e) 
            Log.i("XXX", "BUG: " + e);
        
    

从文件中读取字节:

private byte[] readBytesFromFile(final File file) 
        RandomAccessFile raf;
        byte[] bytesToReturn = new byte[(int) file.length()];
        try 
            raf = new RandomAccessFile(file, "r");
            raf.readFully(bytesToReturn);
         catch (Exception e) 
            Log.i("XXX", "BUG: " + e);
        
        return bytesToReturn;

根据我的阅读,可能的例外是:

FileNotFoundException:我是否正确,只要提供的文件路径是使用 Android 自己的内部工具派生的和/或如果应用程序经过正确测试,这种情况就不应该发生?

IOException :我真的不知道是什么导致了这种情况......但我假设如果它确实没有办法解决它。

考虑到这一点……这些方法可以改进或替换吗?如果可以,用什么方法?

【问题讨论】:

一个改进是使用 try-with-resources 而不是手动调用close()。您也未能在您的读取方法中关闭 RandomAccessFile IOException可能是存储空间不足引起的。 或者权限问题。 【参考方案1】:

看起来这些将成为必须在 Android API 23 或更高版本上运行的核心实用程序/库方法。

关于库方法,我发现最好不要假设应用程序将如何使用这些方法。在某些情况下,应用程序可能希望接收已检查的IOExceptions(因为文件中的数据必须存在才能使应用程序工作),在其他情况下,应用程序甚至可能不在乎数据是否不可用(因为文件中的数据是只有也可以从主要来源获得的缓存)。

对于 I/O 操作,永远无法保证操作一定会成功(例如,用户将手机掉在厕所里)。库应该反映这一点,并让应用程序可以选择如何处理错误。

要优化 I/O 性能,请始终假设“快乐路径”并捕获错误以找出问题所在。这与普通编程相反,但在处理存储 I/O 时必不可少。例如,在读取文件之前仅检查文件是否存在会使您的应用程序变慢两倍 - 所有这些类型的 I/O 操作加起来很快就会减慢您的应用程序的速度。假设文件存在,如果出现错误,则仅检查文件是否存在。

因此,考虑到这些想法,主要功能可能如下所示:

public static void writeFile(File f, byte[] data) throws FileNotFoundException, IOException 
    try (FileOutputStream out = new FileOutputStream(f)) 
        out.write(data);
    


public static int readFile(File f, byte[] data) throws FileNotFoundException, IOException 
    try (FileInputStream in = new FileInputStream(f)) 
        return in.read(data); 
    

关于实现的说明:

这些方法还可能引发运行时异常,例如 NullPointerExceptions - 这些方法永远不会“没有错误”。 我认为在上述方法中不需要/不需要缓冲,因为只完成了一次本机调用 (另见here)。 应用程序现在还可以选择只读取文件的开头。

为了让应用程序更容易读取文件,可以添加一个额外的方法。但请注意,由库来检测任何错误并将其报告给应用程序,因为应用程序本身无法再检测到这些错误。

public static byte[] readFile(File f) throws FileNotFoundException, IOException 
    int fsize = verifyFileSize(f);
    byte[] data = new byte[fsize];
    int read = readFile(f, data);
    verifyAllDataRead(f, data, read);
    return data;


private static int verifyFileSize(File f) throws IOException 
    long fsize = f.length();
    if (fsize > Integer.MAX_VALUE) 
        throw new IOException("File size (" + fsize + " bytes) for " + f.getName() + " too large.");
    
    return (int) fsize;


public static void verifyAllDataRead(File f, byte[] data, int read) throws IOException 
    if (read != data.length) 
        throw new IOException("Expected to read " + data.length 
                + " bytes from file " + f.getName() + " but got only " + read + " bytes from file.");
    

这个实现增加了另一个隐藏的失败点:OutOfMemory 在创建新数据数组的点。

为了进一步适应应用程序,可以添加其他方法来帮助处理不同的场景。例如,假设应用程序真的不想处理已检查的异常:

public static void writeFileData(File f, byte[] data) 
    try 
        writeFile(f, data);
     catch (Exception e) 
        fileExceptionToRuntime(e);
    


public static byte[] readFileData(File f) 
    try 
        return readFile(f);
     catch (Exception e) 
        fileExceptionToRuntime(e);
    
    return null;


public static int readFileData(File f, byte[] data) 
    try 
        return readFile(f, data);
     catch (Exception e) 
        fileExceptionToRuntime(e);
    
    return -1;


private static void fileExceptionToRuntime(Exception e) 
    if (e instanceof RuntimeException)  // e.g. NullPointerException
        throw (RuntimeException)e;
    
    RuntimeException re = new RuntimeException(e.toString());
    re.setStackTrace(e.getStackTrace());
    throw re;

fileExceptionToRuntime 方法是一个最小的实现,但它在这里展示了这个想法。

该库还可以帮助应用程序在发生错误时进行故障排除。例如,canReadFile(File f) 方法可以检查文件是否存在、是否可读且不是太大。应用程序可以在文件读取失败后调用此类函数并检查无法读取文件的常见原因。写入文件也可以这样做。

【讨论】:

感谢有用且内容丰富的答案。我把它放在一个项目中,看看我是否能更好地理解它。从我所拥有的更改 readBytes 方法签名的原因是什么? (您的将 byte[] 作为参数之一并返回 int)。另外,您的最后一段代码是否打算成为库或应用程序的一部分? 也不会出现“return (int) f.length();”这一行由于 f.length 大于 Integer.MAX_VALUE 而崩溃? @NerdyBunz 关于最后一个问题:不,“向下转换”不会给出错误,在这种情况下,当fsize 值太大时会引发 IOException。另外,我应该在那里重用fsize(因为f.length() 会导致I/O 操作)。 关于第一个问题:所有这些都是为了成为图书馆的一部分。我的byte[] readFile(File f) 与您的byte[] readBytesFromFile(final File file) 相似。我的byte[] readFileData(File f) 方法是如何进一步自定义这些功能的示例。我很难弄清楚哪些方法要公开(public)和隐藏(private),我认为这是一个只有你可以回答的问题:你希望应用程序使用哪些方法而不限制应用程序?【参考方案2】:

虽然您不能使用第三方库,但您仍然可以阅读他们的代码并从他们的经验中学习。例如,在 Google Guava 中,您通常将文件读入如下字节:

FileInputStream reader = new FileInputStream("test.txt");
byte[] result = ByteStreams.toByteArray(reader);

这个的核心实现是toByteArrayInternal。在调用它之前,您应该检查:

传递了一个非空文件(NullPointerException) 文件存在 (FileNotFoundException)

之后,它被简化为处理 InputStream 以及 IOExceptions 的来源。读取流时,应用程序无法控制的许多事情可能会出错(坏扇区和其他硬件问题、驱动程序故障、操作系统访问权限)并通过 IOException 表现出来。

我在这里复制实现:

private static final int BUFFER_SIZE = 8192;

/** Max array length on JVM. */
private static final int MAX_ARRAY_LEN = Integer.MAX_VALUE - 8;

private static byte[] toByteArrayInternal(InputStream in, Queue<byte[]> bufs, int totalLen)
      throws IOException 
    // Starting with an 8k buffer, double the size of each successive buffer. Buffers are retained
    // in a deque so that there's no copying between buffers while reading and so all of the bytes
    // in each new allocated buffer are available for reading from the stream.
    for (int bufSize = BUFFER_SIZE;
        totalLen < MAX_ARRAY_LEN;
        bufSize = IntMath.saturatedMultiply(bufSize, 2)) 
      byte[] buf = new byte[Math.min(bufSize, MAX_ARRAY_LEN - totalLen)];
      bufs.add(buf);
      int off = 0;
      while (off < buf.length) 
        // always OK to fill buf; its size plus the rest of bufs is never more than MAX_ARRAY_LEN
        int r = in.read(buf, off, buf.length - off);
        if (r == -1) 
          return combineBuffers(bufs, totalLen);
        
        off += r;
        totalLen += r;
      
    

    // read MAX_ARRAY_LEN bytes without seeing end of stream
    if (in.read() == -1) 
      // oh, there's the end of the stream
      return combineBuffers(bufs, MAX_ARRAY_LEN);
     else 
      throw new OutOfMemoryError("input is too large to fit in a byte array");
    
  

如您所见,大部分逻辑都与分块读取文件有关。这是为了在开始阅读之前处理您不知道 InputStream 大小的情况。在你的情况下,你只需要读取文件,你应该能够事先知道长度,这样就可以避免这种复杂性。

另一个检查是 OutOfMemoryException。在标准 Java 中,这个限制太大了,但在 Android 中,这个值会小得多。在尝试读取文件之前,您应该检查有enough memory available。

【讨论】:

以上是关于仅使用 Java.IO 在文件中读取/写入字节的主要内容,如果未能解决你的问题,请参考以下文章

Java IO学习--字节和字符数组

Java IO流 - 字节流的使用详细介绍

java 按行读取txt文件的数字

java IO流

什么是 java.io.EOFException,消息:无法读取服务器的响应。预期读取 4 个字节,读取 0 个字节

[Java] Java IO Files