你知道吗,可能存在一个文件同时存在又不存在?您是否知道,您可以删除文件并仍然使用它?发现软件开发中的这些和其他文件边缘情况。
在我之前关于软件开发中的边缘情况的文章中,我写了有关文本陷阱的文章,并给了您一些建议,以及如何避免它们。在这篇博文中,我想重点讨论文件和文件 I/O 操作。
java.io.File API 提供以下 3 种方法:
#exists()
#isDirectory()
#isFile()
人们可能会认为,如果它由存在的给定路径指向,则对象要么是文件,要么是目录——就像 Stack Overflow 上的这个问题一样。然而,这并不总是正确的。
File#isFile() javadocs 中没有明确提及,但 file **there确实意味着**常规文件。因此,特殊的 Unix 文件(如设备、套接字和管道)可能存在,但它们不是该定义中的文件。
看下面的代码片段:
import java.io.File val file = File("/dev/null") println("exists: ${file.exists()}") println("isFile: ${file.isFile()}") println("isDirectory: ${file.isDirectory()}")
正如您在现场演示中看到的,可能存在既不是文件也不是目录的文件。
符号链接也是特殊文件,但在(旧)java.io API 中几乎所有地方都以透明方式处理它们。唯一的例外是 #getCanonicalPath()/#getCanonicalFile() 方法系列。这里的透明意味着所有操作都转发到目标,就像直接在目标上执行一样。这种透明度通常很有用,例如您可以只读取或写入某个文件。您不关心可选的链接路径分辨率。然而,这也可能会导致一些奇怪的情况。例如,可能有一个文件同时存在和不存在。
让我们考虑一个悬空符号链接。它的目标不存在,因此上一节中的所有方法都将返回 false。尽管如此,源文件路径仍然被占用,例如您无法在该路径上创建新文件。这是演示这种情况的代码:
import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths val path = Paths.get("foo") Files.createSymbolicLink(path, path) println("exists : ${path.toFile().exists()}") println("isFile : ${path.toFile().isFile()}") println("isDirectory : ${path.toFile().isDirectory()}") println("createNewFile: ${path.toFile().createNewFile()}")
还有现场演示。
在 java.io API 中,要创建一个可能不存在的目录并确保它随后存在,可以使用 File#mkdir() (如果要创建不存在的父目录,则可以使用 File#mkdirs() )好)然后是 File#isDirectory()。按上述顺序使用这些方法非常重要。让我们看看如果顺序颠倒会发生什么。需要两个(或更多)线程执行相同的操作来演示这种情况。在这里,我们将使用蓝色和红色线程。
(红色) isDirectory()? — 不,需要创建
(蓝色) isDirectory()? — 不,需要创建
(红色)mkdir()? - 成功
(蓝色)mkdir()? - 失败
正如您所看到的,蓝色线程无法创建目录。但它确实是被创造出来的,所以结果应该是积极的。如果 isDirectory() 在最后调用,结果总是正确的。
给定 UNIX 进程同时打开的文件数限制为 RLIMIT_NOFILE 的值。在 Android 上,这通常是 1024,但实际上(不包括框架使用的文件描述符)您可以使用更少(在 Android 8.0.0 上使用空 Activity 进行测试期间,大约有 970 个文件描述符可供使用)。如果您尝试打开更多会发生什么?好吧,文件不会被打开。根据上下文,您可能会遇到具有明确原因的异常(打开文件过多),一点点神秘的消息(例如此文件无法作为文件描述符打开;它可能是压缩的),或者当您通常期望 true 时只是将 false 作为返回值。请参阅演示这些问题的代码:
package pl.droidsonroids.edgetest import android.content.res.AssetFileDescriptor import android.support.test.InstrumentationRegistry import org.junit.Assert import org.junit.Test class TooManyOpenFilesTest { //asset named "test" required @Test fun tooManyOpenFilesDemo() { val context = InstrumentationRegistry.getContext() val assets = context.assets val descriptors = mutableListOf() try { for (i in 0..1024) { descriptors.add(assets.openFd("test")) } } catch (e: Exception) { e.printStackTrace() //java.io.FileNotFoundException: This file can not be opened as a file descriptor; it is probably compressed } try { context.openFileOutput("test", 0) } catch (e: Exception) { e.printStackTrace() //java.io.FileNotFoundException: /data/user/0/pl.droidsonroids.edgetest/files/test (Too many open files) } val sharedPreferences = context.getSharedPreferences("test", 0) Assert.assertTrue(sharedPreferences.edit().putBoolean("test", true).commit()) } }
请注意,如果您使用#apply(),该值将不会被持久保存——因此您不会遇到任何异常。但是,在持有该 SharedPreferences 实例的应用程序进程被终止之前,它将可以访问。这是因为共享首选项也保存在内存中。
人们可能认为僵尸、食尸鬼和其他类似的生物只存在于奇幻和恐怖小说中。但是……它们在计算机科学中是真实存在的!这些常见术语指的是僵尸进程。其实亡灵文件也可以轻松制作。
在类Unix操作系统中,文件删除通常是通过取消链接来实现的。未链接的文件名将从文件系统中删除(假设它是最后一个硬链接),但任何已打开的文件描述符仍然有效且可用。您仍然可以读取和写入此类文件。这是片段:
import java.io.BufferedReader import java.io.File import java.io.FileReader val file = File("test") file.writeText("this is file content") BufferedReader(FileReader(file)).use { println("deleted?: ${file.delete()}") println("content?: ${it.readLine()}") }
还有现场演示。
首先,请记住,在创建不存在的目录时,我们不能忘记正确的方法调用顺序。此外,请记住,同时打开的文件数量是有限的,并且不仅计算您明确打开的文件。最后但并非最不重要的一点是,在最后一次使用之前删除文件的技巧可以为您提供更多的灵活性。
最初于 2017 年 9 月 27 日发布于 www.thedroidsonroids.com。
免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。
Copyright© 2022 湘ICP备2022001581号-3