Wussten Sie, dass es eine Datei geben kann, die gleichzeitig existiert und nicht existiert? Wussten Sie, dass Sie eine Datei löschen und weiterhin verwenden können? Entdecken Sie diese und andere Dateirandfälle in der Softwareentwicklung.
In meinem vorherigen Artikel über Grenzfälle in der Softwareentwicklung habe ich über Textfallen geschrieben und Ihnen einige Vorschläge gegeben, wie Sie diese vermeiden können. In diesem Blogbeitrag möchte ich mich auf Dateien und Datei-E/A-Vorgänge konzentrieren.
Die java.io.File API bietet unter anderem diese 3 Methoden:
#exists()
#isDirectory()
#isFile()
Man könnte denken, dass ein Objekt entweder eine Datei oder ein Verzeichnis ist, wenn auf es durch einen bestimmten vorhandenen Pfad verwiesen wird – wie in dieser Frage zu Stack Overflow. Dies ist jedoch nicht immer der Fall.
Es wird in File#isFile() Javadocs nicht explizit erwähnt, aber Datei **dort bedeutet wirklich **normale Datei. Daher können spezielle Unix-Dateien wie Geräte, Sockets und Pipes existieren, sie sind jedoch keine Dateien in dieser Definition.
Sehen Sie sich den folgenden Ausschnitt an:
import java.io.File val file = File("/dev/null") println("exists: ${file.exists()}") println("isFile: ${file.isFile()}") println("isDirectory: ${file.isDirectory()}")
Wie Sie in der Live-Demo sehen können, kann eine Datei existieren, die weder eine Datei noch ein Verzeichnis ist.
Symbolische Links sind ebenfalls spezielle Dateien, werden jedoch fast überall in der (alten) java.io-API transparent behandelt. Die einzige Ausnahme ist die Methodenfamilie #getCanonicalPath()/#getCanonicalFile(). Transparenz bedeutet hier, dass alle Vorgänge an das Ziel weitergeleitet werden, so wie sie direkt auf diesem ausgeführt werden. Eine solche Transparenz ist in der Regel nützlich, z.B. Sie können einfach aus einer Datei lesen oder in eine Datei schreiben. Die optionale Linkpfadauflösung ist Ihnen egal. Allerdings kann es auch zu seltsamen Fällen kommen. Beispielsweise kann es eine Datei geben, die gleichzeitig existiert und nicht existiert.
Betrachten wir einen baumelnden symbolischen Link. Sein Ziel existiert nicht, daher geben alle Methoden aus dem vorherigen Abschnitt „false“ zurück. Dennoch ist der Quelldateipfad noch belegt, z.B. Sie können in diesem Pfad keine neue Datei erstellen. Hier ist der Code, der diesen Fall demonstriert:
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()}")
Und eine Live-Demo.
Um in der java.io-API ein möglicherweise nicht vorhandenes Verzeichnis zu erstellen und sicherzustellen, dass es anschließend existiert, können Sie File#mkdir() (oder File#mkdirs(), wenn Sie nicht vorhandene übergeordnete Verzeichnisse erstellen möchten) verwenden na ja) und dann File#isDirectory(). Es ist wichtig, diese Methoden in der angegebenen Reihenfolge anzuwenden. Mal sehen, was passieren kann, wenn die Reihenfolge umgekehrt wird. Um diesen Fall zu veranschaulichen, sind zwei (oder mehr) Threads erforderlich, die dieselben Vorgänge ausführen. Hier verwenden wir blaue und rote Fäden.
(red) isDirectory()? – Nein, es muss erstellt werden
(blau) isDirectory()? – Nein, es muss erstellt werden
(red) mkdir()? - Erfolg
(blau) mkdir()? - scheitern
Wie Sie sehen, konnte ein blauer Thread kein Verzeichnis erstellen. Es wurde jedoch tatsächlich erstellt, sodass das Ergebnis positiv sein sollte. Hätte isDirectory() am Ende aufgerufen, wäre das Ergebnis immer korrekt gewesen.
Die Anzahl der gleichzeitig von einem bestimmten UNIX-Prozess geöffneten Dateien ist auf den Wert von RLIMIT_NOFILE begrenzt. Unter Android ist dies normalerweise 1024, aber effektiv (mit Ausnahme der vom Framework verwendeten Dateideskriptoren) können Sie noch weniger verwenden (bei Tests mit leerer Aktivität unter Android 8.0.0 standen etwa 970 Dateideskriptoren zur Verwendung zur Verfügung). Was passiert, wenn Sie versuchen, mehr zu öffnen? Nun, die Datei wird nicht geöffnet. Abhängig vom Kontext kann es zu einer Ausnahme mit einem expliziten Grund (Zu viele geöffnete Dateien), einer etwas rätselhaften Meldung (z. B. Diese Datei kann nicht als Dateideskriptor geöffnet werden; es ist wahrscheinlich komprimiert) oder einfach nur „false“ als Rückgabewert, wenn Sie normalerweise „true“ erwarten. Sehen Sie sich den Code an, der diese Probleme demonstriert:
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()) } }
Beachten Sie, dass der Wert bei Verwendung von #apply() einfach nicht dauerhaft gespeichert wird – Sie erhalten also keine Ausnahme. Es bleibt jedoch zugänglich, bis der App-Prozess, der diese SharedPreferences-Instanz enthält, beendet wird. Denn auch geteilte Präferenzen werden im Speicher gespeichert.
Man könnte meinen, dass Zombies, Ghule und andere ähnliche Kreaturen nur in Fantasy- und Horrorromanen existieren. Aber... in der Informatik gibt es sie wirklich! Solche gebräuchlichen Begriffe beziehen sich auf die Zombie-Prozesse. Tatsächlich können auch Untotendateien problemlos erstellt werden.
In Unix-ähnlichen Betriebssystemen wird das Löschen von Dateien normalerweise durch Aufheben der Verknüpfung implementiert. Der nicht verknüpfte Dateiname wird aus dem Dateisystem entfernt (vorausgesetzt, es handelt sich um den letzten Hardlink), alle bereits geöffneten Dateideskriptoren bleiben jedoch gültig und verwendbar. Sie können weiterhin aus einer solchen Datei lesen und in sie schreiben. Hier ist der Ausschnitt:
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()}") }
Und eine Live-Demo.
Denken Sie zunächst daran, dass wir beim Erstellen nicht vorhandener Verzeichnisse die richtige Reihenfolge beim Methodenaufruf nicht vergessen dürfen. Beachten Sie außerdem, dass die Anzahl der gleichzeitig geöffneten Dateien begrenzt ist und nicht nur explizit von Ihnen geöffnete Dateien gezählt werden. Und zu guter Letzt kann Ihnen ein Trick mit dem Löschen von Dateien vor der letzten Verwendung etwas mehr Flexibilität verschaffen.
Ursprünglich veröffentlicht auf www.thedroidsonroids.com am 27. September 2017.
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3