Você sabia que pode haver um arquivo que existe e não existe ao mesmo tempo? Você sabe que pode excluir um arquivo e ainda usá-lo? Descubra esses e outros casos extremos de arquivos no desenvolvimento de software.
Em meu artigo anterior sobre casos extremos no desenvolvimento de software, eu estava escrevendo sobre armadilhas de texto e dei algumas sugestões sobre como evitá-las. Nesta postagem do blog, gostaria de me concentrar em arquivos e operações de E/S de arquivos.
A API java.io.File fornece, entre outros, estes 3 métodos:
#existe()
#isDirectory()
#isFile()
Pode-se pensar que, se for apontado por um determinado caminho existente, um objeto é um arquivo ou um diretório - como nesta pergunta no Stack Overflow. No entanto, isso nem sempre é verdade.
Não é mencionado explicitamente nos javadocs File#isFile(), mas arquivo **lá realmente significa **arquivo normal. Assim, arquivos Unix especiais como dispositivos, soquetes e pipes podem existir, mas não são arquivos nessa definição.
Veja o seguinte trecho:
import java.io.File val file = File("/dev/null") println("exists: ${file.exists()}") println("isFile: ${file.isFile()}") println("isDirectory: ${file.isDirectory()}")
Como você pode ver na demonstração ao vivo, pode existir um arquivo que não é um arquivo nem um diretório.
Links simbólicos também são arquivos especiais, mas são tratados de forma transparente em quase todos os lugares da (antiga) API java.io. A única exceção é a família de métodos #getCanonicalPath()/#getCanonicalFile(). Transparência aqui significa que todas as operações são encaminhadas ao alvo, da mesma forma que são realizadas diretamente nele. Essa transparência é geralmente útil, por ex. você pode simplesmente ler ou gravar em algum arquivo. Você não se importa com a resolução opcional do caminho do link. No entanto, também pode levar a alguns casos estranhos. Por exemplo, pode haver um arquivo que existe e não existe ao mesmo tempo.
Vamos considerar um link simbólico pendente. Seu alvo não existe, então todos os métodos da seção anterior retornarão falso. No entanto, o caminho do arquivo de origem ainda está ocupado, por ex. você não pode criar um novo arquivo nesse caminho. Aqui está o código que demonstra este caso:
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()}")
E uma demonstração ao vivo.
Na API java.io, para criar um diretório possivelmente inexistente e garantir que ele exista posteriormente, você pode usar File#mkdir() (ou File#mkdirs() se quiser criar diretórios pai inexistentes como bem) e então File#isDirectory(). É importante usar esses métodos na ordem mencionada. Vamos ver o que pode acontecer se a ordem for invertida. Dois (ou mais) threads executando as mesmas operações são necessários para demonstrar este caso. Aqui, usaremos linhas azuis e vermelhas.
(vermelho) isDirectory()? - não, preciso criar
(azul) isDirectory()? - não, preciso criar
(vermelho) mkdir()? - sucesso
(azul) mkdir()? - falhar
Como você pode ver, um thread azul falhou ao criar um diretório. Porém, ele foi de fato criado, portanto o resultado deverá ser positivo. Se isDirectory() tivesse chamado no final, o resultado sempre teria sido correto.
O número de arquivos abertos ao mesmo tempo por um determinado processo UNIX é limitado ao valor de RLIMIT_NOFILE. No Android, geralmente é 1024, mas efetivamente (excluindo os descritores de arquivos usados pela estrutura) você pode usar ainda menos (durante os testes com Activity vazia no Android 8.0.0, havia aproximadamente 970 descritores de arquivos disponíveis para uso). O que acontece se você tentar abrir mais? Bem, o arquivo não será aberto. Dependendo do contexto, você pode encontrar uma exceção com um motivo explícito (Muitos arquivos abertos), uma pequena mensagem enigmática (por exemplo, Este arquivo não pode ser aberto como um descritor de arquivo; provavelmente está compactado) ou apenas falso como valor de retorno quando você normalmente espera verdadeiro. Veja o código que demonstra esses problemas:
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()) } }
Observe que, se você usar #apply(), o valor simplesmente não será salvo de forma persistente - então você não receberá nenhuma exceção. No entanto, ele estará acessível até que o processo do aplicativo que contém a instância SharedPreferences seja eliminado. Isso porque as preferências compartilhadas também são salvas na memória.
Pode-se pensar que zumbis, ghouls e outras criaturas semelhantes existem apenas na fantasia e na ficção de terror. Mas… eles são reais na ciência da computação! Esses termos comuns referem-se aos processos zumbis. Na verdade, arquivos mortos-vivos também podem ser criados facilmente.
Em sistemas operacionais do tipo Unix, a exclusão de arquivos geralmente é implementada por desvinculação. O nome do arquivo desvinculado é removido do sistema de arquivos (assumindo que seja o último hardlink), mas quaisquer descritores de arquivo já abertos permanecem válidos e utilizáveis. Você ainda pode ler e gravar nesse arquivo. Aqui está o trecho:
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()}") }
E uma demonstração ao vivo.
Em primeiro lugar, lembre-se de que não podemos esquecer a ordem adequada de chamada de método ao criar diretórios inexistentes. Além disso, lembre-se de que o número de arquivos abertos ao mesmo tempo é limitado e não são contabilizados apenas os arquivos abertos explicitamente por você. E por último, mas não menos importante, um truque para excluir arquivos antes do último uso pode lhe dar um pouco mais de flexibilidade.
Publicado originalmente em www.thedroidsonroids.com em 27 de setembro de 2017.
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3