domínio: regras java
Um caminho otimizado com base nas entradas de ontem é desotimizado quando as de hoje chegam [R:a-speculated-path-deoptimises-when-the-input-changes]
Escrito a partir de Aleksey Shipilev, "JVM Anatomy Quarks", sobre poluição de perfil, inlining e desotimização; mecanismo uncommon-trap do HotSpot.
Um JIT não compila seu código. Ele compila uma aposta sobre seu código, e a aposta é liquidada no momento em que a aposta deixa de ser verdadeira.
O HotSpot compila com base em perfil: este ponto de chamada viu apenas um tipo de receptor, então faça inline e proteja; este branch nunca foi tomado, então não o emita; este campo nunca foi nulo, então pule a verificação; esta classe nunca foi carregada, então assuma que nenhuma subclasse existe. Cada suposição recebe uma proteção, e o caminho compilado é rápido precisamente porque as proteções são baratas e os corpos estão ausentes. Quando uma proteção falha, o quadro é desotimizado: uma armadilha incomum (uncommon trap), quadros do interpretador reconstruídos a partir dos compilados, execução continuando interpretada, e uma espera para ser recompilado com o novo perfil.
A temporização é o que torna esta uma regra de latência, não de taxa de transferência. As suposições são quebradas quando a entrada muda -- o tipo de ordem incomum, a primeira exceção do dia, o formato de mensagem de um novo local, a segunda implementação de uma interface carregada na sexta hora. Portanto, a pior latência que o caminho produzirá é entregue no evento menos comum, que geralmente é o evento que mais importava. Testes de carga com tráfego representativo da média não reproduzem nada disso.
Também é silencioso por padrão. Nada é registrado, nenhuma métrica se move, e o único traço é um outlier de latência indistinguível de uma pausa de coleta ou atraso de escalonamento. -XX:+PrintCompilation e -XX:+TraceDeoptimization dizem o que aconteceu; JFR carrega os eventos com menos sobrecarga e é o que vale a pena manter ligado.
Portanto, aqueça o caminho com os casos incomuns também, além dos comuns -- que é onde isto encontra [R:a-measurement-matches-the-regime-it-reports]: um aquecimento construído apenas a partir de tráfego típico produz um perfil que o tráfego típico confirma e o tráfego atípico destrói. Mantenha pontos de chamada quentes deliberadamente monomórficos; uma chamada de interface megamórfica no caminho quente é uma barreira permanente de inlining em vez de uma armadilha única. Carregue as classes que o caminho precisará antes que o caminho esteja ativo. E trate uma lambda ou um ponto de chamada dinâmico adicionado a um caminho quente como um novo perfil que precisa ser conquistado novamente.
Verificação de modo de falha: o que o JIT assumiu sobre este caminho, e o que acontece na primeira vez que uma dessas suposições é falsa? Se a resposta honesta é que ninguém sabe o que foi assumido, essa é a constatação -- as suposições são legíveis, e lê-las é o trabalho.
Uma visão de coleção é uma janela para os dados de outra pessoa [R:a-view-is-not-a-copy]
Saiba se você está segurando dados ou uma janela para dados. O tipo estático é List<T> de qualquer maneira e nunca lhe dirá.
A biblioteca retorna visões com muito mais frequência do que a maioria do código assume, e cada uma tem sua própria maneira de surpreender:
Arrays.asList(a)é um wrapper de tamanho fixo sobre o array.setescreve através para o array;adderemovelançamUnsupportedOperationException.List.of(...)é imutável e independente, e geralmente é o que foi pretendido.list.subList(from, to)é uma janela ativa. Escritas passam, e qualquer mudança estrutural no pai faz a sublista lançarConcurrentModificationExceptionem seu próximo uso -- incluindo o idioma comum de pegar uma sublista e depois limpar o pai.map.keySet(),values(),entrySet()são visões.keySet().remove(k)remove a entrada do mapa. Isso é ocasionalmente o que você quer e sempre vale a pena ser deliberado sobre.Collections.unmodifiableList(l)envolve sem copiar: o wrapper recusa escritas e o original ainda funciona, então defende contra o chamador e não contra você. Não é uma cópia defensiva, e é por isso que[R:no-reference-to-internals-escapes]buscaList.copyOfem vez disso.Stream.toList()é não modificável;collect(Collectors.toList())não oferece tal garantia e pode ou não ser. Não confie em nenhum deles como mutável.
Portanto, indique qual você deseja no ponto em que o cria. List.copyOf quando o receptor deve ser independente, a visão quando o compartilhamento é a intenção e o tempo de vida é curto. Uma visão armazenada em um campo é a forma que merece um olhar mais atento: ela sobrevive à expressão que a criou, e a coleção de suporte pode mudar sob ela a qualquer momento depois.
A mesma distinção governa a iteração. Modificar uma coleção enquanto a itera lança ConcurrentModificationException mesmo em uma única thread, porque o iterador também é uma visão -- use removeIf, ou itere uma cópia, ou colete as remoções e aplique-as depois.
Verificação de modo de falha, para qualquer coleção recebida ou retornada: se alguém escrever nisto, o que mais muda? Se você não pode responder sem abrir o Javadoc do método de fábrica, copie-o.
Um tipo wrapper custa um objeto até que a análise de escape o remova [R:a-wrapper-type-is-not-free-here]
Dê a conceitos de domínio seus próprios tipos aqui também -- um OrderId que não pode ser passado onde um AccountId pertence vale tanto em Java quanto em qualquer outro lugar. Mas não carregue o argumento de custo junto com o argumento de design, porque apenas um deles viaja.
Em Rust, um newtype é uma construção em tempo de compilação: struct OrderId(u64) é um u64 em tempo de execução e o compilador prova isso. Em Java, record OrderId(long value) é um objeto -- um cabeçalho, um campo, uma referência a ser rastreada e uma linha de cache separada do que quer que o tenha apontado. O JIT pode removê-lo: análise de escape mais substituição escalar aplanará um wrapper que comprovadamente não escapa de sua unidade de compilação, o que cobre uma grande quantidade de código comum. O que isso não cobre é o caso que você se importa -- um wrapper armazenado em um campo, colocado em uma coleção, passado através de um método que o JIT se recusou a incluir em linha, ou em um array, onde OrderId[] é um array de ponteiros e long[] é um array de números.
Portanto, a disciplina se divide por onde o valor vive:
- Em um limite de API e em qualquer lugar onde a correção é a preocupação, use o wrapper. O bug de argumento trocado que ele previne vale uma alocação que o JIT geralmente removerá de qualquer maneira.
- Dentro de um loop quente, em um array grande ou em uma estrutura por mensagem, meça antes de assumir que o wrapper desapareceu. Isso é
[R:verify-the-abstraction-compiled-away]chegando em uma linguagem onde a decisão do otimizador é dinâmica:-XX:+PrintInlining, um perfil de alocação JFR ou um dump de análise de escape diz o que realmente aconteceu, e a resposta pode diferir entre execuções do mesmo binário. - Onde ele não desaparece e o orçamento é real, mantenha o primitivo e coloque a segurança em algum lugar que não custe nada -- um parâmetro nomeado, um builder, uma fábrica estática cuja assinatura não pode ser chamada incorretamente.
A versão desta regra que está por vir vale a pena ser conhecida em vez de esperada: as classes de valor do Project Valhalla são projetadas para fazer exatamente este wrapper aplanar por especificação em vez de por critério do otimizador. Até que o código execute em uma JVM onde isso seja verdade, o custo é dinâmico e deve ser medido em vez de assumido.
Verificação de modo de falha, antes de envolver um primitivo em um caminho com orçamento: este objeto ainda existe depois que o JIT terminou, e o que me disse? "É apenas um wrapper" é a resposta do Rust, e é a que não se transfere.
Um recurso é fechado pelo construto que o abriu, em todos os caminhos [R:close-what-you-open]
Abra um recurso em um cabeçalho try-with-resources e em nenhum outro lugar. O construto fecha em cada saída, em ordem reversa, e -- a parte que um finally escrito à mão não pode fazer -- ele mantém a exceção original quando o fechamento também falha.
try (var conn = pool.take(); var stmt = conn.prepare(sql)) {
return stmt.execute();
}
A falha que um bloco finally introduz vale a pena ser declarada claramente porque parece correta. Se o corpo lança e close() também lança, a exceção do finally substitui a do corpo: o programa relata uma falha ao fechar uma conexão e não diz nada sobre a consulta que falhou primeiro. Try-with-resources, em vez disso, suprime a falha de fechamento e a anexa à original, então ambas sobrevivem e a causa é a que você queria. Um vazamento custa a você um handle; uma causa perdida custa a investigação.
Três casos onde o construto não se aplica e a disciplina ainda sim:
- Um recurso cujo tempo de vida é um campo, não um bloco -- um pool, um cliente, um executor. O objeto proprietário torna-se
AutoCloseablee a disciplina sobe um nível; a coisa a recusar é um campo que é aberto e nunca fechado por ninguém em particular. - Um
ExecutorServicenão é fechado apenas porshutdown().shutdown()entãoawaitTermination()entãoshutdownNow()é a sequência, e pular deixa threads não-daemon segurando a JVM. - Um
Streamsobre um arquivo (Files.lines,Files.walk) mantém um handle e deve ser fechado. É o único stream que vaza, e parece exatamente com os que não vazam.
Não recorra a um finalizador ou Cleaner como mecanismo principal. Eles executam em um tempo não especificado ou nunca, e existem para capturar o caso onde a disciplina já falhou.
Verificação de modo de falha, para cada recurso: qual construto fecha este se a próxima linha lançar? Se a resposta é uma chamada close() mais adiante no método, nada fecha.
Uma classe é final, ou sua herança é projetada e documentada [R:design-for-inheritance-or-forbid-it]
Torne a classe final a menos que você tenha projetado para uma subclasse, e se tiver, diga exatamente o que uma subclasse pode sobrescrever e o que não deve.
Herança não é o mecanismo de reuso que parece. Uma subclasse depende de quais métodos da superclasse chamam outros -- addAll chamando add é o exemplo canônico -- e isso é um detalhe de implementação que a superclasse tem o direito de mudar em uma versão de patch. Portanto, uma subclasse funcional quebra quando o pai é refatorado, sem que nenhum dos autores faça nada errado. Composição não tem esta propriedade: um wrapper depende do contrato público e de nada mais.
O caso do construtor é pior que frágil, está quebrado desde o início. Um construtor que chama um método sobrescrevível executa a sobrescrita da subclasse antes que os próprios campos da subclasse sejam atribuídos, então a sobrescrita vê null e 0 em campos que declarou final e inicializou. Nada avisa. Este é o mesmo risco de publicação que [R:no-reference-to-internals-escapes] nomeia como this escapando, alcançado por uma rota que parece despacho de método comum -- e [R:publish-safely-or-not-at-all] é por que importa mesmo quando há apenas uma thread.
Portanto:
finalpor padrão em classes e em qualquer método que uma subclasse não tenha negócio em substituir. Selamento ([R:seal-the-alternatives]) é a forma mais forte onde o conjunto de subtipos é fechado e conhecido.- Nunca chame um método sobrescrevível de um construtor, de um inicializador, ou de
clone()oureadObject(), que são construtores com outros nomes. - Se uma classe é projetada para herança, documente o autouso: quais métodos chamam quais, quais são seguros de sobrescrever, o que uma subclasse deve chamar. Essa documentação faz parte do contrato, e o custo dela é a razão honesta pela qual a maioria das classes deveria ser
finalem vez disso. - Prefira composição. Um wrapper que mantém a instância e delega é mais longo de escrever, imune aos internos do pai, e é a coisa a buscar quando a motivação foi reuso em vez de substituibilidade.
Verificação de modo de falha, para cada classe não final: quais dos meus métodos chamam uns aos outros, e uma subclasse sobrescrevendo um deles ainda estaria correta depois que eu os reordenar? Se isso não pode ser respondido, a classe foi extensível por acidente.
equals e hashCode são uma única decisão, e uma chave mutável quebra ambos [R:equality-is-one-contract]
Decida a igualdade uma vez, implemente ambas as metades na mesma alteração, e derive-as de campos que não podem mudar enquanto o objeto estiver acessível a partir de uma coleção.
O contrato é mecânico: objetos iguais devem ter códigos hash iguais. Quebre-o e um HashMap olha no bucket errado, então get retorna nulo para uma chave que contém e remove não encontra uma entrada para remover -- um vazamento cujo tamanho cresce com o tráfego. Nada detecta isso. A coleção está se comportando exatamente como especificado; a especificação foi entregue a um objeto que mentiu.
A metade da chave mutável é a que sobrevive à revisão, porque a classe está correta quando é escrita e se torna errada quando alguém adiciona um setter. Um objeto cujo hashCode depende de um campo que mais tarde é mutado mudou de bucket sem que o mapa fosse informado, e agora está inalcançável através da própria chave sob a qual está arquivado. Portanto, um tipo usado como chave é imutável, ou não é usado como chave. Um record fornece ambas as metades geradas a partir dos componentes e campos finais por construção, e é por isso que é a forma padrão para um tipo de valor e por que escrever estes dois métodos manualmente deve parecer uma decisão em vez de código repetitivo.
Mais dois que seguem do mesmo contrato e valem a pena ser declarados porque cada um tem sua própria maneira de dar errado:
compareTodeve concordar comequalsonde quer que o tipo alcance umTreeMapou um conjunto ordenado, que usam ordenação e nunca chamamequals. Um comparador dizendo que dois objetos são equivalentes enquantoequalsdiz que eles diferem produz um conjunto que silenciosamente mantém um deles.- Herança e igualdade não compõem. Um
equalsescrito cominstanceofaceita uma subclasse e é então assimétrico; um escrito comgetClass()é simétrico e rejeita toda subclasse. Não há terceira opção, que é uma das razões pelas quais[R:design-for-inheritance-or-forbid-it]preferefinal.
Verificação de modo de falha, antes de um tipo ser usado como chave ou colocado em um conjunto: de quais campos depende sua igualdade e seu código hash, e esses campos podem mudar enquanto o tipo estiver em uma coleção? Se a resposta não é "não", o tipo não é imutável e a coleção está quebrada.
Exceções nomeiam o que falhou, não o que estava sendo tentado [R:exceptions-name-what-failed]
Mensagens de erro devem responder "o que quebrou?", não "o que estávamos fazendo?" A pilha já diz onde você estava; a mensagem deve dizer por que.
throw new IOException("falha ao escrever no arquivo") diz o que estava sendo tentado. O stack trace já mostra o método de escrita que falhou, e o nome da exceção diz a categoria. A parte que apenas a mensagem pode carregar é qual recurso falhou -- o caminho do arquivo, o nome do host, o ID da sessão.
throw new IOException(Path + ": " + reason) em vez disso. O contexto que torna a mensagem útil é o que não está na pilha.
Três práticas que tornam isso específico:
- O nome da exceção deve refletir a causa, não o método que a lança.
IllegalArgumentExceptionporque o argumento não é válido;IllegalStateExceptionporque o objeto não está no estado certo para a operação. A distinção importa para manipuladores de catch upstream que decidem o que tentar novamente em vez de abortar. - Falhas recuperáveis usam exceções verificadas; falhas de programação usam exceções não verificadas.
[R:exception-types-are-a-contract]faz desta distinção um caso para si, mas a forma segue o uso: um consumidor de fila vazio não é um erro de programação e não deve serRuntimeException. - Suprimir exceções em um catch vazio é o erro. Catch e registre, ou catch e lance um novo com a causa anexada. O bloco catch que não faz nenhum dos dois é como o código mente para si mesmo.
Verificação de modo de falha, ao escrever um lance: a próxima pessoa que olha para este log sabe qual recurso falhou sem abrir o código? Se não, adicione o identificador.
Identidade não é igualdade para caixas criadas pela linguagem [R:identity-is-not-equality-for-boxes]
== no tipo Integer compara identidade de referência -- dois objetos diferentes -- não o valor que eles mantêm. equals compara o valor. A confusão entre eles é o bug que tem seu próprio nome e não é sutil.
O Java cacheia Integer de -128 a 127, então Integer.valueOf(100) == Integer.valueOf(100) é verdadeiro na maioria das JVMs e Integer.valueOf(200) == Integer.valueOf(200) é falso. A correção não é aprender o intervalo de cache; a correção é nunca usar == com um valor de caixa.
==para caixas é sempre o bug da caixa, e o verificador de estilo deve sinalizá-lo como erro.equalsem um campo que é sempre a mesma referência desperdiça a comparação de identidade, que é a comparação mais barata -- veja[R:a-wrapper-type-is-not-free-here]para o custo, que se aplica ao método também. Se você sabe que seus operandos são a mesma instância,==é mais rápido e semanticamente equivalente. Mas você sabe isso apenas quando a fonte é local e compartilhada; caso contrário, useequals.- Boxed primitivos como
Boolean,ByteeCharacterpara o intervalo ASCII são todos cacheados, então testá-los com==funciona na prática. O que ainda o torna o bug da caixa.
Verificação de modo de falha, para cada == em tipos de caixa: o operando esquerdo e direito são variáveis primitivas, ou achei que eram? A correção é escrever equals.
Nenhuma referência a internos escapa a menos que você tenha decidido que sim [R:no-reference-to-internals-escapes]
O chamador obtém o que pede, não o que você armazena. Devolver uma referência a um campo mutável é dar acesso de gravação a quem recebe, e a mutação acontece dentro do seu estado interno sem que você saiba.
this.items não é o valor a ser retornado; new ArrayList<>(this.items) ou Collections.unmodifiableList(this.items) ou List.copyOf(this.items) é, e a escolha entre eles depende de quão caro é copiar e se o chamador precisa de mutabilidade.
Três padrões tornam isso concreto:
- Devolver um array mutável de um getter.
getItems()retornando um array interno permite que o chamador mude seu estado. Devolva o array.clone(). - Devolver uma coleção mutável de um getter. Mesmo problema. Devolva uma visão não modificável com
Collections.unmodifiableList(items)ou uma cópia comnew ArrayList<>(items).List.copyOfé imutável e independente, se a imutabilidade é aceitável. - Armazenar uma referência recebida em um campo. O chamador pode mais tarde mutar o objeto que você pensou que era seu. Copie na atribuição:
this.items = List.copyOf(items)outhis.items = new ArrayList<>(items).
A exceção é quando o objeto mantido internamente é imutável por design (String, Integer, record). Então compartilhar a referência é seguro porque não há estado a ser mutado.
Verificação de modo de falha, para cada campo que sai de um getter: esta referência pode ser usada para alterar o estado interno do objeto proprietário? Se sim, copie.
nulo não é um valor [R:null-is-not-a-value]
Nulo quebra todas as três: chamadas de método em nulo lançam NullPointerException; passar nulo para uma coleção que não o aceita é um bug lento e silencioso; retornar nulo de um método que prometeu um valor força o chamador a verificar nulo onde nenhum caso nulo deveria existir.
A correção não é verificar nulo em todos os lugares. A correção é não produzir nulo em primeiro lugar.
Optionalpara retornos onde a ausência é um caso a ser tratado.Optional.empty()em vez de null. O chamador sabe que deve verificar porque o tipo de retorno dizOptional. Ainda é um objeto e[R:a-wrapper-type-is-not-free-here] se aplica onde o orçamento é mensurável e a ausência é o caso quente.Objects.requireNonNullpara parâmetros e retornos que não devem ser nulos. Lance o NPE cedo e claramente, onde a fonte do nulo é mais fácil de localizar.@Nullablee@NonNullanotações para documentar a intenção. O compilador e o IDE podem então verificar e prevenir a cadeia de nulo.- Se o código de terceiros retorna nulo, envolva-o. Um wrapper no limite traduz nulo externo em
Optionalinterno ou lança um NPE no ponto de contato, para que o nulo não se propague.
Verificação de modo de falha, para cada método: se este método retorna nulo, o chamador sabe disso? Se a assinatura não diz isso de forma verificável, mude-a.
Publique com segurança ou não publique [R:publish-safely-or-not-at-all]
Um objeto que não é seguro para thread é publicado sem sincronização e outro thread o vê em um estado parcialmente construído ou vê valores de campo desatualizados.
O modelo de memória Java não garante que um thread veja as gravações de outro thread a menos que haja uma relação de acontece-antes (happens-before) entre elas. Publicar sem sincronização é uma corrida de dados e é um bug de boas-vindas.
- Imutabilidade segura para publicação: Um objeto imutável (
finalem todos os campos, nenhumthisescapando no construtor) é seguro para publicar por qualquer meio.String,Integer,record. - Imutabilidade efetiva: Um objeto cujos campos não são
finalmas nunca são escritos após a publicação requer umhappens-beforeentre a gravação final e a primeira leitura da outra thread. Uma inicialização preguiçosa segura usavolatileou umAtomicReference. finalno campo: Um campofinalé garantido pelo JMM como visível para outros threads quando o construtor termina. Um campo nãofinalnão tem tal garantia.- Volatile e atômicos: Um campo
volatilefornece visibilidade para suas próprias gravações e leituras. As classesAtomic*fornecem operações atômicas com visibilidade. - Sincronização: Um bloco
synchronizedfornece visibilidade para todos os campos lidos ou escritos dentro do bloco. Um monitor compartilhado entre threads fornece ohappens-before. - Construção segura: não deixe
thisescapar. Um construtor que passathispara outro thread (por exemplo, em um campo estático, um ouvinte, um executor de thread) publica o objeto antes que ele seja totalmente construído.[R:design-for-inheritance-or-forbid-it]adverte sobre chamar métodos sobrescrevíveis de um construtor pela mesma razão:thisescapa antes que as atribuições de campo da subclasse estejam visíveis.
Verificação de modo de falha, para cada objeto que cruza um limite de thread: existe um happens-before entre a gravação que torna este objeto visível e a leitura que o vê? Se não, corrija a publicação.
Sele as alternativas [R:seal-the-alternatives]
Sealed classes (sealed, permits) permitem ao autor da classe definir o conjunto completo de subtipos permitidos. O compilador conhece todas as subclasses, então correspondência de padrão pode ser exaustiva e a API pode prometer que nenhum outro subtipo existe.
[R:design-for-inheritance-or-forbid-it] recomenda final por padrão. Selamento é a alternativa para hierarquias onde o conjunto de variantes é fechado e conhecido em tempo de projeto. Isto é comum em ADTs (tipos de dados algébricos) modelados como classes.
- Domínio fechado: Se você sabe que apenas
Cat,DogeBirddevem ser subclasses válidas deAnimal, sele a interface. O compilador então verifica que nenhuma outra classe estende e que o código de correspondência de padrão sobreAnimalestá completo. - Imutabilidade e segurança: Uma sealed class torna a lista de subtipos conhecida, o que significa que o código do cliente não precisa se preparar para um desconhecido. O espaço de estado é finito e fechado.
- Correspondência de padrão:
switchpode ser exaustivo sobre uma interface ou classe sealed sem um caso padrão. Uma cláusuladefaultnão é necessária; o compilador emite um erro se um caso é omitido. - Uso:
public abstract sealed class Animal permits Cat, Dog, Bird { }oupublic sealed interface Animal permits Cat, Dog, Bird { }.
Verificação de modo de falha, para cada hierarquia: o conjunto de subtipos é conhecido e fechado, ou mais podem ser adicionados? Se o primeiro, sele-o; se o último, não.
Serializable é um segundo construtor [R:serializable-is-a-second-constructor]
Serialização no Java não usa construtores. Ela cria um objeto sem executar nenhum construtor, usando ObjectInputStream.readObject e o mecanismo mágico de readObject e readResolve.
Isto significa que campos não transitórios são definidos diretamente a partir do stream, ignorando qualquer validação ou invariante que tenha sido estabelecido no construtor. Serializable efetivamente abre um buraco no encapsulamento do objeto.
transientpara todos os campos que devem ser definidos pelo construtor. Um campo seguro para serialização é um que pode ser corretamente desserializado sem o construtor executar. Se não, torne-o transitório.serialVersionUID: Declare umprivate static final long serialVersionUIDpara cada classe serializável. A JVM usa isso para verificar a compatibilidade da classe com o stream. Se omitido, a JVM calcula um a partir dos detalhes da classe, e a serialização quebra com uma alteração não relacionada de campo.readObjectpara validação: Forneça um métodoprivate void readObject(ObjectInputStream in) throws IOException, ClassNotFoundExceptionque primeiro desserializa normalmente e depois valida o estado do objeto. LanceInvalidObjectExceptionse a validação falhar.readResolvepara substituição: Forneça um métodoprivate Object readResolve() throws ObjectStreamExceptionque retorna o objeto a ser usado onde este apareceu. Isto é útil para singleton e enum.- Evite
Serializablea menos que necessário. O custo é alto: seu formato de serialização é difícil de mudar, seu estado interno é exposto, e o barramento de serialização eventualmente exige que você faça manutenção compatível com versões anteriores.[R:serialization-is-a-public-api]faz desta uma decisão de API.
Verificação de modo de falha, para cada classe Serializable: se este objeto é desserializado, seus invariantes se mantêm? Em particular, quais campos não transient podem ser definidos para valores inválidos através de um stream manipulado?
Translated by an automated service. The English original is authoritative.