Construir um estado que faz sentido
Uma classe organiza estado e operações. Antes de escolher campos, escreve as regras que tornam uma instância válida. Para um limite de capacidade, são exemplos: nome presente, quantidade entre 1 e 99 e ausência de uma fase pública em que o objeto tenha metade dos dados. Um construtor pode recusar entradas inválidas antes de entregar a instância ao chamador. A regra tem de continuar verdadeira nas operações posteriores: validar apenas a criação não chega se um setter aceitar valores proibidos.
Em class Ticket { Ticket(String id) {} }, a declaração explícita não vem acompanhada de um Ticket() gerado. Uma chamada sem argumentos falha na compilação; não recebe automaticamente null. Quando precisas de uma alternativa, define o seu significado: um construtor sem argumentos que cria um identificador vazio pode compilar e ainda violar o domínio. Outra possibilidade é uma fábrica que só entrega um objeto depois de reunir e validar os dados. Nas perguntas, distingue rejeição na compilação, exceção na construção e objeto criado com um estado inadequado.
Contrato comum e composição
Uma interface permite nomear operações que diferentes implementações têm de fornecer. Em interface Meter { int read(); }, read é público mesmo sem a palavra public. A classe concreta deve fornecer uma implementação pública compatível; um método com acesso de pacote não cumpre essa obrigação. Compilar verifica parte do contrato. A equipa ainda tem de definir unidades, resultados ausentes, erros e efeitos observáveis.
Imagina ReceiptService, que prepara texto e o envia por um Sender recebido no construtor. O contrato devolve uma referência de aceitação ou lança SendFailure. Um adaptador SMTP e um adaptador em memória traduzem os seus detalhes para essa fronteira. ReceiptService guarda o Sender e delega a operação, sem mencionar os fornecedores. Esta composição permite substituir a implementação. Herdar de um cliente SMTP apenas para reutilizar send prende o serviço a esse tipo; guardar Object e fazer cast para SMTP mantém a mesma dependência.
A referência de aceitação não comprova entrega ao destinatário. Se uma implementação devolver sucesso quando apenas guardou uma tentativa falhada, a interface pode estar sintaticamente implementada e o contrato semântico continua quebrado. A pergunta de desenho exige ambos.
Defaults e comportamento herdado
Uma interface pode disponibilizar um método default. Isso fornece comportamento reutilizável, mas não uma regra de prioridade baseada na ordem de implements. Se duas interfaces independentes oferecem source() e a classe não o implementa, o conflito tem de ser resolvido. Uma implementação pública na classe pode definir qual é o significado de source para aquele adaptador. Antes de decidir, pergunta se as duas interfaces usam o mesmo nome para o mesmo conceito; juntar resultados incompatíveis só para satisfazer o compilador pode criar outro erro.
O caso de uma superclasse é diferente. Se Parent já fornece public String mode(), e uma interface oferece um default compatível com a mesma assinatura, a classe Worker que estende Parent herda a implementação concreta da classe. Mudar o tipo da referência para a interface não escolhe outro corpo. Para diagnosticar, identifica primeiro as assinaturas e a origem dos métodos, depois a implementação que a classe efetivamente possui. Não generalizes esta regra a métodos privados, static ou abstratos com regras de herança diferentes.
Estado de instância e estado da classe
Um campo de instância acompanha cada objeto; um campo static acompanha a classe. Num contador com static int total e int own, duas chamadas em a e uma em b produzem total 3, a.own 2 e b.own 1, se todos começarem em zero e a execução for sequencial. Criar b não repõe total: não existe uma cópia desse campo para cada novo objeto.
A escolha deve corresponder à propriedade dos dados. Um limite de pedidos por cliente guardado inadvertidamente em static pode ser substituído pela configuração do cliente seguinte. Mesmo sem threads, duas instâncias deixam de ter políticas independentes. Corrigir essa propriedade exige decidir a quem pertence o limite e como é passado; criar mais instâncias não resolve o campo partilhado.
Estes exemplos isolam a partilha para facilitar a leitura. static não fornece exclusão mútua e total++ não passa a ser uma operação de contagem segura entre threads por estar num campo de classe. A aula de concorrência tratará a coordenação. Não atribuas ao exemplo sequencial uma garantia que ele não analisa.
Records: componentes e construção
Um record descreve um conjunto fixo de componentes. Em record Station(String code, int platforms) {}, o acesso ao segundo valor é platforms(), sem prefixo get e sem argumento. O record pode implementar interfaces, mas é final e não pode estender uma classe escolhida nem acrescentar campos de instância fora dos componentes. Isto favorece representar valores; não elimina a necessidade de estabelecer as regras desses valores.
O construtor compacto permite validar ou normalizar os parâmetros antes de o compilador os atribuir aos campos. Em Route { code = Objects.requireNonNull(code).strip(); }, code designa o parâmetro. Ignorar o resultado de strip deixa o texto por normalizar. Escrever this.code = ... nesse corpo é um erro de compilação: a atribuição ao campo é implícita após conclusão normal. Num record público, um construtor canónico explicitamente declarado também tem de ser público.
Decide separadamente o que fazer com texto vazio. Recusar null e remover espaços pode transformar uma entrada em vazio; se vazio não é válido, é necessária outra verificação. O exemplo de Route testa apenas as duas regras declaradas. O excerto de WindowLimits abaixo usa componentes primitivas e verifica explicitamente o intervalo de ambas.
Igualdade e estabilidade dos valores
Um record não torna imutáveis os objetos a que os componentes se referem. Em Parcel(int[] weights), o chamador pode conservar o array e alterar pesos depois da construção; o accessor gerado também devolve essa referência. Copiar à entrada fecha a primeira via. Copiar à saída fecha a segunda. Para um array de int não há objetos internos a copiar, mas continua a existir uma decisão separada sobre igualdade.
Com equals gerado, dois Parcel construídos com arrays distintos {3, 8} são diferentes: a igualdade do componente array usa identidade. Os mesmos elementos não bastam. Clonar nas duas fronteiras protege o estado, mas reconstruir pelos accessors produz outro array e não preserva a igualdade gerada. Seria preciso definir conjuntamente igualdade e hash, ou escolher uma representação mais adequada.
No caso WindowLimits, só existem dois inteiros e a API ainda pode mudar. Componentes int first e int second retiram o array da representação e permitem igualdade gerada por valores. Noutra situação com quantidade variável de elementos, a decisão seria diferente. A aula de coleções aprofunda a escolha de componentes e de chaves.
Revisão de um valor reconstruído
Uma revisão de record deve acompanhar construção, acesso e reconstrução. Considera um limite guardado como 15 cujo accessor devolve no máximo 10. O campo continua a conter 15. Construir outro record com o resultado do accessor guarda 10; equals gerado lê os campos, pelo que os dois objetos diferem. O compilador aceita o accessor, mas não verifica que a API conserva a expectativa de reconstruir um valor igual a partir dos seus componentes.
Há duas decisões de domínio possíveis: recusar valores acima do limite ou normalizá-los na construção. Em ambos os casos, o accessor deve representar fielmente o componente. Se existe uma vista calculada que aplica um limite apenas para apresentação, um método separado pode dar-lhe um nome próprio, preservando o accessor do componente.
Na revisão guiada de WindowLimits, classifica quatro propostas: array partilhado; cópia só na entrada; cópias nas duas fronteiras com equals gerado; e duas componentes int. Para cada proposta, identifica quem pode alterar o estado e se reconstruir pelos accessors mantém a igualdade. Escolhe a representação que cumpre as duas regras e justifica por que as restantes falham. A avaliação pede selecionar respostas e interpretar excertos, sem escrever ou executar programas.
record WindowLimits(int first, int second) {
WindowLimits {
if (first < 0 || first > 99 || second < 0 || second > 99) {
throw new IllegalArgumentException("limits must be in 0..99");
}
}
}
// Read the values; no execution is required.
// var original = new WindowLimits(12, 40);
// var reconstructed = new WindowLimits(original.first(), original.second());
// original.equals(reconstructed) is true.
// new WindowLimits(12, 100) rejects the second limit.
O record tem acesso de pacote; os exemplos assumem código com acesso ao tipo. 12 e 40 passam a validação e a reconstrução mantém as duas componentes. 100 é rejeitado antes de o chamador receber uma instância. Não há array partilhado nem métodos que alterem os limites.
Armadilhas comuns
Confundir default com prioridade textual; reduzir o acesso de métodos de interface; guardar estado por cliente em static; assumir cópia profunda em records; transformar accessors sem rever a igualdade.
Tópicos relacionados: Referências, identidade e texto · Métodos, contratos e fluxo de controlo · Coleções, generics e chaves · Concorrência, estado e cancelamento
Um tipo útil preserva o seu contrato desde a construção até ao uso. Em records, verifica separadamente estabilidade do estado e igualdade.
Referência: JLS 25: Record classes · Java SE 25; no preview features