george-andrikopoulos/domain-java

Disciplina de engenharia para trabalhar em Java. Aborda: um caminho especulado é desotimizado quando a entrada muda; uma visão não é uma cópia; um tipo wrapper não é gratuito aqui; feche o que você abre; projete para herança ou proíba-a; igualdade é um único contrato; exceções nomeiam o que falhou; identidade não é igualdade para caixas; nenhuma referência a internos escapa; nulo não é um valor; publique com segurança ou não publique; sele as alternativas; Serializable é um segundo construtor.

O que é domain-java?

Esta skill define um conjunto de regras de engenharia de software para Java, focadas em desempenho, correção e boas práticas no ecossistema JVM. Ela aborda tópicos como otimização especulativa do JIT (Just-In-Time), gerenciamento de recursos com try-with-resources, design de herança segura, contratos de igualdade e hashCode, publicação segura de objetos, tratamento de nulos e sealed classes. A skill é útil para desenvolvedores Java que desejam escrever código robusto e eficiente, evitando armadilhas comuns como vazamentos de memória, erros de concorrência e degradação de desempenho. As regras são baseadas em princípios como os JVM Anatomy Quarks de Aleksey Shipilev e as melhores práticas da linguagem, com exemplos práticos e verificações de falha para cada regra. A skill é aplicável a qualquer plataforma Java, incluindo aplicações de servidor, microsserviços e sistemas de alto desempenho, e fornece justificativas técnicas e alternativas para cada decisão de design.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/george-andrikopoulos/relearn/tree/HEAD/claude-pack/skills/domain-java

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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. set escreve através para o array; add e remove lançam UnsupportedOperationException. 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çar ConcurrentModificationException em 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] busca List.copyOf em 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 AutoCloseable e a disciplina sobe um nível; a coisa a recusar é um campo que é aberto e nunca fechado por ninguém em particular.
  • Um ExecutorService não é fechado apenas por shutdown(). shutdown() então awaitTermination() então shutdownNow() é a sequência, e pular deixa threads não-daemon segurando a JVM.
  • Um Stream sobre 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:

  • final por 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() ou readObject(), 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 final em 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:

  • compareTo deve concordar com equals onde quer que o tipo alcance um TreeMap ou um conjunto ordenado, que usam ordenação e nunca chamam equals. Um comparador dizendo que dois objetos são equivalentes enquanto equals diz que eles diferem produz um conjunto que silenciosamente mantém um deles.
  • Herança e igualdade não compõem. Um equals escrito com instanceof aceita uma subclasse e é então assimétrico; um escrito com getClass() é 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] prefere final.

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. IllegalArgumentException porque o argumento não é válido; IllegalStateException porque 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 ser RuntimeException.
  • 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.
  • equals em 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, use equals.
  • Boxed primitivos como Boolean, Byte e Character para 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 com new 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) ou this.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.

  • Optional para 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 diz Optional. 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.requireNonNull para 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.
  • @Nullable e @NonNull anotaçõ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 Optional interno 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 (final em todos os campos, nenhum this escapando no construtor) é seguro para publicar por qualquer meio. String, Integer, record.
  • Imutabilidade efetiva: Um objeto cujos campos não são final mas nunca são escritos após a publicação requer um happens-before entre a gravação final e a primeira leitura da outra thread. Uma inicialização preguiçosa segura usa volatile ou um AtomicReference.
  • final no campo: Um campo final é garantido pelo JMM como visível para outros threads quando o construtor termina. Um campo não final não tem tal garantia.
  • Volatile e atômicos: Um campo volatile fornece visibilidade para suas próprias gravações e leituras. As classes Atomic* fornecem operações atômicas com visibilidade.
  • Sincronização: Um bloco synchronized fornece visibilidade para todos os campos lidos ou escritos dentro do bloco. Um monitor compartilhado entre threads fornece o happens-before.
  • Construção segura: não deixe this escapar. Um construtor que passa this para 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: this escapa 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, Dog e Bird devem ser subclasses válidas de Animal, sele a interface. O compilador então verifica que nenhuma outra classe estende e que o código de correspondência de padrão sobre Animal está 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: switch pode ser exaustivo sobre uma interface ou classe sealed sem um caso padrão. Uma cláusula default não é necessária; o compilador emite um erro se um caso é omitido.
  • Uso: public abstract sealed class Animal permits Cat, Dog, Bird { } ou public 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.

  • transient para 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 um private static final long serialVersionUID para 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.
  • readObject para validação: Forneça um método private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException que primeiro desserializa normalmente e depois valida o estado do objeto. Lance InvalidObjectException se a validação falhar.
  • readResolve para substituição: Forneça um método private Object readResolve() throws ObjectStreamException que retorna o objeto a ser usado onde este apareceu. Isto é útil para singleton e enum.
  • Evite Serializable a 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.

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

george-andrikopoulos/domain-low-latency

Engineering discipline for working in low-latency. Covers: attack the design in a second pass; a measurement matches the regime it reports; a queue without a bound has no overload behaviour; a structure keeps the regime it was proved under; allocated is not resident; answer the requirement at its layer; no allocation on the hot path; no coordinated omission; no false sharing on a hot line; no retry loop on a contended path; no stall inside a publication window; no syscall on a bounded path;...

george-andrikopoulos/domain-rust

Engineering discipline for working in rust. Covers: design types first; borrow in signatures; module visibility is deliberate; must use on consequential returns; newtype liberally; parse wide then range check; seal closed trait sets; typestate builder for required fields; typestate for protocols; verify the abstraction compiled away; async all the way; errors name what failed; justify every clone; no let underscore on a local; parse dont validate; private fields only; xplat fixtures; no...

george-andrikopoulos/global

Engineering discipline that applies to every project and language. Covers: guarantee needs a reader; verdict survives the channel; check the claim you inherit; doc currency; measure the claim not a subset; no silent spend; prefer by construction; repair the lying artefact; signal needs a consequence; verify through production path; detector excludes own definitions; an ambiguous write target is a question; anchor outside the checked partys reach; delegate a fan out; diff against the known...

Habilidades Relacionadas