Soluções — Substituição e Falha: o Princípio de Liskov e a Taxonomia de Exceções
Aula 9 — Programação Orientada a Objetos
Dica(Resposta) Teste 1 — Late Binding e VTable
- ✗ Falso — Métodos
staticnão participam de Late Binding — eles são resolvidos estaticamente pelo compilador, associados à classe declarada da referência, não à classe real do objeto no Heap. Só métodos de instância não-privados e não-finalpassam pelo mecanismo de VTable. Confundir “todo método sofre despacho polimórfico” é o erro estrutural que este item testa. - ✔ Verdadeiro — Com Early Binding, a decisão de qual código executar é tomada em tempo de compilação, olhando o tipo estático. Isso significa que o compilador precisaria conhecer, no momento da compilação do código cliente, todas as classes concretas que algum dia ocupariam aquela referência — uma subclasse nova exigiria recompilar (ou, na prática, seria impossível de despachar corretamente sem essa recompilação). É exatamente o oposto do desacoplamento temporal que o Late Binding garante.
- ✗ Falso — É exatamente o inverso do mecanismo de Late Binding: a VTable consultada é sempre a da classe concreta do objeto no Heap (a “realidade do Heap”), nunca a do tipo declarado da referência (a “lente do compilador”). Essa inversão de responsabilidade é o próprio motivo de o polimorfismo funcionar.
- ✗ Falso — Late Binding pressupõe uma referência de um tipo comum (uma superclasse ou interface) através da qual o código cliente enxerga o método. Sem herança ou interface compartilhada, não existe uma “referência comum” cujo tipo declare o método — o compilador simplesmente não aceitaria uma variável desse tipo comum chamando o método em questão. Duas classes soltas com o mesmo nome de método não formam polimorfismo, só uma coincidência de nomenclatura (a diferença entre “mesmo nome” e “mesmo contrato de tipo”).
Dica(Resposta) Teste 2 — Sobrescrita como Contrato (
@Override)
- ✔ Verdadeiro —
@Overrideé uma checagem puramente sintática — ela confirma que a assinatura corresponde a um método existente no supertipo, nada mais. O compilador não tem como avaliar a semântica de negócio de um método (o que “validar” ou “excluir” significam no domínio), então uma mudança de significado radical compila normalmente. É essa lacuna — sintaxe correta, semântica quebrada — que o LSP existe para cobrir. - ✔ Verdadeiro — É exatamente o propósito de
@Override: se o método anotado deixar de corresponder a nenhum método do supertipo (porque foi renomeado, teve a assinatura alterada, ou removido), o compilador rejeita a compilação da subclasse. Sem a anotação, esse erro passaria despercebido — o método “sobrescrito” viraria silenciosamente um método novo e independente. - ✔ Verdadeiro — Java permite covariância de retorno desde o Java 5: o método sobrescrito pode devolver um subtipo do tipo de retorno declarado no pai, porque isso só fortalece a pós-condição (o cliente que esperava o tipo mais genérico ainda recebe algo compatível) — coerente com a Regra 2 do LSP vista nesta aula (pós-condições podem ser fortalecidas, nunca enfraquecidas).
- ✗ Falso —
@Overridesó valida assinatura (nome, parâmetros, tipo de retorno compatível). Alterações de comportamento — desde que a assinatura seja mantida — compilam sem problema algum; é justamente por isso que violações semânticas do LSP não são pegas pelo compilador, precisando de disciplina de design (e não de uma anotação) para serem evitadas.
Dica(Resposta) Teste 3 — LSP: a Definição Formal
- ✔ Verdadeiro — É a aplicação direta da fórmula: se \(\phi\) vale para todo \(x\) do tipo \(T\) (nunca retorna null), mas existe um \(y\) do subtipo \(S\) para o qual \(\phi(y)\) é falso (retorna null), a implicação universal quantificada falha — e é exatamente essa falha que caracteriza uma violação de LSP (nesse caso, uma violação de pós-condição).
- ✗ Falso — É a distinção central da aula entre sintaxe e semântica — compilar corretamente garante apenas que as assinaturas são compatíveis (subtyping estrutural), não que o comportamento observável (as propriedades \(\phi\)) se mantenha.
PagamentoCartaoBloqueadocompila perfeitamente contraPagavele ainda assim viola o LSP ao lançar uma exceção inesperada. - ✔ Verdadeiro — Esse padrão de teste (às vezes chamado de “contract test” ou “teste de substituição”) é exatamente a intuição do cliente cego posta em prática: o código de teste não sabe (nem precisa saber) qual implementação concreta está recebendo, e espera que todas passem pelos mesmos testes — se uma implementação falha nos testes que as demais passam, ela provavelmente viola o LSP em relação às outras.
- ✗ Falso — O LSP é uma relação assimétrica por definição — ela diz respeito à substituição de \(T\) por \(S\) (o subtipo assume o lugar do supertipo), não o contrário. \(T\) não é sequer, em geral, um subtipo de \(S\), então a pergunta “\(T\) satisfaz o LSP em relação a \(S\)” nem está bem formada na direção inversa — inverter a relação de subtipagem livremente é o erro estrutural deste item.
Dica(Resposta) Teste 4 — Pré-condições no LSP
- ✔ Verdadeiro — Enfraquecer (ampliar) uma pré-condição é permitido pela Regra 1 do LSP — a subclasse fica mais tolerante, nunca menos, então todo cliente que já enviava valores dentro de 0–1000 continua funcionando exatamente como antes, e ainda passa a aceitar mais casos. Não há surpresa possível para o cliente existente.
- ✔ Verdadeiro — É a formulação direta da Regra 1: pré-condições podem ser mantidas ou enfraquecidas, nunca fortalecidas. Aceitar mais entradas do que o pai aceitava nunca surpreende um cliente que já respeitava o contrato original — o risco só aparece quando a subclasse restringe (fortalece) o domínio de entradas válidas.
- ✗ Falso — A ausência de documentação explícita não significa ausência de pré-condição implícita — o comportamento observado pelos clientes existentes (todo valor que já funcionava) define, na prática, o contrato real. Adicionar uma restrição nova quebra qualquer cliente que já passava um valor agora rejeitado, independentemente de essa pré-condição estar ou não escrita num Javadoc — é o mesmo problema de “invariante invisível” já visto na Aula 8, agora aplicado a pré-condições.
- ✔ Verdadeiro — Do ponto de vista do cliente, “meu valor de R$50 antes era aceito e agora é rejeitado” tem o mesmo efeito observável de uma funcionalidade removida — não importa que a assinatura do método não tenha mudado, o conjunto de operações que funcionavam através do tipo base encolheu. É esse efeito, não a mudança sintática, que caracteriza a violação.
Dica(Resposta) Teste 5 — Pós-condições e Invariantes no LSP
- ✔ Verdadeiro — Retornar um objeto não-nulo tecnicamente cumpre “nunca retornar null”, mas a pós-condição real que o cliente espera geralmente é mais rica do que isso — um “ID de cobrança válido”, por exemplo, implica um ID que pode ser usado para consultar o pagamento depois. Um objeto vazio ou um ID sem sentido evita a
NullPointerExceptionsó na superfície, mas ainda entrega “menos do que o prometido” seja qual for a operação seguinte do cliente — a mesma lição do exemploreturn "";do bloco de pós-condições. - ✔ Verdadeiro — É a formulação direta da Regra 2: pós-condições podem ser mantidas ou fortalecidas, nunca enfraquecidas. Entregar mais do que o prometido nunca surpreende negativamente um cliente que já esperava o mínimo garantido pelo pai — o risco só existe na direção contrária (entregar menos).
- ✗ Falso — O acesso
protecteddá ao filho a capacidade técnica de alterar o atributo diretamente, mas a Regra 3 (preservação de invariantes) não depende de “sobrescrever um método” — é uma obrigação sobre o estado do objeto em qualquer ponto observável, independente de qual código (herdado ou próprio) o alterou. Alterarsaldodiretamente para um valor negativo, sem passar por nenhum método sobrescrito, ainda quebra a invariantesaldo >= 0da mesma forma destrutiva. - ✗ Falso — Confunde invariante com imutabilidade. Uma invariante como
saldo >= 0permite — e espera — que o valor mude ao longo da vida do objeto (depósitos, saques); o que ela proíbe é que o valor saia de uma região válida em qualquer momento observável. Uma classe pode ter atributos totalmente mutáveis e ainda assim preservar rigorosamente suas invariantes, desde que toda transição respeite a regra.
Dica(Resposta) Teste 6 — O Paradoxo Círculo-Elipse
- ✔ Verdadeiro — Sem mutabilidade pós-construção, nunca existiria a chamada
elipse.setAxes(10, 20)que força a escolha impossível — o objeto nasceria já com seus eixos definitivos, e umCirculo(coma == bdesde a criação) seria umElipseperfeitamente substituível para qualquer operação de leitura. É a mutação de estado depois da construção que introduz o risco. - ✔ Verdadeiro — Removendo a mutação, não existe mais nenhuma operação que force um
Circuloa escolher entre violar sua própria natureza matemática ou a expectativa do cliente — todo objeto já nasce com seu formato definitivo e correto. É exatamente a saída “objetos imutáveis” apontada na aula como uma das duas correções honestas. - ✗ Falso — O paradoxo é uma consequência específica da combinação mutabilidade + pré-condição estrutural adicional do subtipo (
a == b), não uma condenação geral de qualquer hierarquia que modele formas geométricas. Com objetos imutáveis, como o item anterior já estabelece, a mesma hierarquia deixa de violar o LSP — generalizar “evitar sempre” é o exagero que este item testa. - ✔ Verdadeiro — É a segunda saída honesta discutida na aula: se o comportamento mutável de \(S\) não condiz com \(T\), a correção pode ser abandonar a relação de herança problemática e usar uma abstração mais genérica (uma interface comportamental, como calcular área ou perímetro) que não inclua o método causador do conflito — sem
setAxesna interface comum, não há mais nenhuma chamada que force a escolha impossível.
Dica(Resposta) Teste 7 — Contratos de Exceção e o LSP
- ✗ Falso — Pelo contrário: o compilador Java impede essa situação diretamente — se o método do pai não declara uma exceção checked, a subclasse não pode declarar (nem lançar sem capturar internamente) uma checked nova que não seja subtipo de alguma já declarada, exatamente para proteger o código cliente de precisar mudar. É por isso que a violação real do LSP com exceções costuma vir de exceções unchecked novas (
RuntimeExceptione subtipos), que o compilador não bloqueia. - ✗ Falso — É exatamente o padrão apontado na aula como violação quase invariável do LSP — se uma subclasse precisa “pular” um método herdado lançando uma exceção que o cliente do tipo base não esperava, isso é sinal de que ela não deveria ser um subtipo daquele pai (talvez nem seja uma especialização legítima, já que não consegue honrar toda a interface). “Especialização legítima” não perdoa esse padrão — ao contrário, o padrão costuma ser evidência de que a especialização não é, de fato, legítima.
- ✔ Verdadeiro — Exceções unchecked não são bloqueadas pelo compilador mesmo quando não declaradas — é por isso que
PagamentoCartaoBloqueadolançandoSecurityExceptioncompila normalmente e ainda assim viola o LSP: o comportamento observável surpreende um cliente que nunca previu aquele erro em condições normais. A proteção do compilador cobre só exceções checked; a proteção contra exceções unchecked inesperadas depende inteiramente da disciplina de design. - ✔ Verdadeiro — Um cliente que já captura (ou declara)
PagamentoExceptioncontinua funcionando sem qualquer mudança, porqueSaldoInsuficienteExceptioné capturada pelo mesmocatch (PagamentoException e)— é a mesma lógica de covariância já vista para tipos de retorno, aplicada a exceções: especializar dentro da hierarquia já esperada nunca surpreende o cliente.
Dica(Resposta) Teste 8 — Taxonomia do Erro e Captura Polimórfica
- ✔ Verdadeiro — É a mesma vantagem de desacoplamento temporal do polimorfismo comum: um
catch (PagamentoException e)escrito hoje automaticamente também captura qualquer subtipo dePagamentoExceptioncriado meses depois (uma novaPixRecusadoException, por exemplo), sem que o código de tratamento precise ser tocado — a captura é por tipo, não por identificador de mensagem. - ✔ Verdadeiro — Em Java, blocos
catchsão avaliados em ordem, e o primeiro cujo tipo corresponde captura a exceção. Se o bloco mais genérico (PagamentoException) vier antes do mais específico (CartaoExpiradoException), o segundo bloco se torna código inalcançável — nenhuma exceção chegaria até ele, já que qualquerCartaoExpiradoExceptionjá teria sido capturada pelocatchgenérico anterior. O compilador Java detecta essa situação e recusa a compilação (“exception CartaoExpiradoException has already been caught”), exatamente porque a hierarquia de exceções permite múltiplos níveis de generalidade compilarem contra o mesmo objeto lançado, e a ordem determina qual nível efetivamente reage. - ✗ Falso — É exatamente o cuidado simétrico apontado na aula: capturar
Exception(ouThrowable) indiscriminadamente esconde erros de lógica genuínos — como umNullPointerExceptionpor um bug de inicialização — que deveriam interromper a execução para correção, não ser silenciados junto com falhas de negócio esperadas. “Nenhum erro escapa” soa seguro, mas na prática troca bugs visíveis por comportamento incorreto silencioso. - ✗ Falso — As duas ideias são complementares, não concorrentes: a “transição de estado prevista” é o papel conceitual do erro na máquina de estados do objeto (Aula 2); a hierarquia de exceções é o mecanismo técnico usado para representar e reagir a cada tipo de transição de falha. Uma máquina de estados bem desenhada usa justamente uma taxonomia de exceções para diferenciar os motivos pelos quais a transição para “Falha” ocorreu.
Dica(Resposta) Teste 9 — Checked vs. Unchecked e Wrapping
- ✔ Verdadeiro — É o problema central que motiva o wrapping: se
SQLExceptionvaza até o controlador, qualquer código que capture ou trate esse erro no nível de negócio precisa “saber” que a persistência usa SQL — trocar o banco de dados por um NoSQL, mais tarde, quebraria esse código de tratamento. Traduzir para uma exceção de domínio isola exatamente essa dependência. - ✗ Falso — É o oposto do que a aula recomenda: descartar a exceção original apaga o stack trace técnico (onde exatamente a
IOExceptionocorreu, qual linha, qual causa de mais baixo nível) — a depuração técnica fica impossível a partir daquele ponto. Preservar a causa original (o segundo argumento do construtor, como emFalhaComunicacaoException("...", e)) é o que mantém a rastreabilidade sem vazar o detalhe técnico para a interface pública. - ✔ Verdadeiro — É a aplicação direta do critério Unchecked/DNA: um valor inválido na entrada é uma violação de contrato de quem chamou o método, não uma contingência do ambiente externo — a resposta correta é falhar imediatamente (Fail-Fast), não esperar que o erro se manifeste de forma confusa mais adiante no fluxo.
- ✗ Falso — É o mesmo erro discutido na Pausa Ativa 3: capturar um erro de DNA (violação de contrato) e seguir em frente com um valor padrão não corrige o código que chamou o método com um argumento inválido — só esconde o sintoma, trocando uma falha visível e rastreável por um comportamento silenciosamente incorreto que vai se manifestar, sem explicação aparente, em algum ponto mais distante do sistema.