Pular para o conteúdo
Quiz
← Início

Programação orientada a objetos

100 questões

Certas
0
Erradas
0
Não respondidas
100
Dificuldade
Número de questões

100 questões neste filtro

Resumo teórico

Olhe ao redor do seu quarto. Um abajur tem um interruptor e uma luz. Um bichinho de estimação tem nome e sabe comer e brincar. Cada coisa guarda algumas informações sobre si mesma e sabe fazer algumas ações. A programação orientada a objetos, ou POO, constrói programas do mesmo jeito: o programa é uma equipe de pequenos que guardam as próprias informações e pedem uns aos outros que façam coisas.

Quase todo app que você usa é escrito assim: jogos, bancos, lojas, aplicativos de celular. Programas grandes são difíceis de mudar sem quebrar algo. A POO dá aos programadores ferramentas para decidir onde cada pedaço de conhecimento mora, para que uma mudança em um lugar não quebre outros dez. Esse é o objetivo real de toda ideia deste resumo.

Você vai aprender o que são classes e objetos, como os objetos escondem o que têm dentro (encapsulamento), como uma classe pode se apoiar em outra (herança), como a mesma chamada pode fazer coisas diferentes (polimorfismo), para que servem interfaces e classes abstratas, por que muitas vezes preferimos montar com peças em vez de herdar, como reconhecer código bagunçado, o que acontece quando algo dá errado (exceções), como escrever código que funciona para muitos tipos (genéricos), como a memória é limpa, como ler e gravar dados e como outras linguagens fazem POO sem classes. A maioria dos exemplos usa Java, a linguagem do quiz. No fim você estará pronto para respondê-lo.

O panorama: objetos conversando com objetos

Pense numa escola. A professora não abre a mochila do aluno para olhar dentro. Ela pede: 'por favor, me mostre a lição'. O aluno decide como encontrá-la. Na POO, os objetos funcionam assim. Um objeto envia um pedido, chamado de chamada de método, e o outro objeto decide como responder.

As cinco ideias do resumo
IdeiaEm uma fraseImagem do dia a dia
EncapsulamentoUm objeto mantém seus dados privados e oferece só ações seguras.Uma máquina de salgadinhos: você aperta botões, não alcança o interior.
AbstraçãoMostramos o que um objeto sabe fazer e escondemos como ele faz.Você dirige com volante e pedais, sem saber como é o motor.
HerançaUma classe nova parte de uma antiga e acrescenta ou muda coisas.Um filhote é um cachorro, e um cachorro é um animal.
PolimorfismoUm pedido, muitas respostas, conforme o objeto.'Faça um som' faz o cachorro latir e o gato miar.
ComposiçãoUm objeto é construído com outros objetos.Uma bicicleta tem rodas, corrente e campainha.
Assista ao vídeo: Curso de POO Java, Gustavo Guanabara (em português)↑ Voltar ao sumário

Classes, objetos e encapsulamento

Um cortador de biscoito não é um biscoito. É a forma usada para fazer muitos biscoitos. Uma classe é o cortador: ela diz quais dados (chamados de campos) toda coisa desse tipo tem e quais ações (chamadas de métodos) ela sabe fazer. Um objeto é um biscoito feito com ela, também chamado de instância. Duas contas criadas da mesma classe Account têm os mesmos métodos, mas cada uma tem o seu próprio saldo.

class Account {
    private int balance;          // campo: dado de cada objeto
    Account(int start) {          // construtor: roda quando o objeto nasce
        this.balance = start;
    }
    int balance() { return balance; }
}
Account a = new Account(10);
Account b = new Account(99);      // independente de a
Uma classe e dois objetos

Construtores, this e static

  • Um construtor prepara um objeto novo. Tem o mesmo nome da classe e nenhum tipo de retorno.
  • Se você não escreve nenhum construtor, o compilador acrescenta um sem argumentos. Assim que você escreve qualquer construtor, esse de graça desaparece.
  • this.x significa 'o campo x deste objeto'. Ajuda quando um parâmetro tem o mesmo nome do campo.
  • this(...) chama outro construtor da mesma classe. Precisa ser a primeira linha do construtor.
  • Um campo static pertence à classe, não a um objeto. Há uma cópia só para todos, como o número do sinal da escola. Um campo normal (de instância) existe uma vez por objeto.

Ordem de construção

Quando o new roda, o Java executa primeiro o construtor da superclasse (a classe de cima, veja herança). Depois executa os inicializadores de campos e os blocos de inicialização { }, na ordem em que estão escritos no arquivo. Só no final roda o resto do corpo do construtor. Um campo int que copia outro campo copia o valor naquele momento e depois fica desligado dele.

Níveis de acesso

Níveis de acesso do Java, do mais fechado ao mais aberto. Use o mais fechado que funcionar.
ModificadorQuem pode usar o membro
privateSomente a própria classe
sem modificador (package-private)Classes do mesmo pacote (pasta de classes)
protectedO pacote e as subclasses
publicTodo mundo

Passar objetos e compará-los

O Java passa tudo por valor. Para um objeto, o valor copiado é a referência, como um papel com o endereço de uma casa. Se um método usa o endereço para pintar a porta, o dono vê a cor nova. Mas se o método escreve um endereço novo na sua própria cópia do papel, o papel do dono não muda. Resumindo: mudar o objeto por meio de um parâmetro é visível para quem chamou, reatribuir o parâmetro não é.

== entre duas variáveis de objeto pergunta 'é exatamente o mesmo objeto?' (identidade). equals serve para perguntar 'têm o mesmo conteúdo?', mas a versão herdada de Object também compara identidade. A classe precisa sobrescrever equals (e hashCode) para comparar campos. Um record do Java escreve os dois para você.

Encapsulamento

Encapsulamento significa que os dados de um objeto só podem ser alcançados pelos métodos da própria classe. Assim a classe protege seus invariantes, regras que devem ser sempre verdadeiras, como 'o saldo nunca é negativo'. Ela também pode mudar como guarda as coisas sem quebrar quem a usa.

↑ Voltar ao sumário

A vida de um objeto: memória e coleta de lixo

Imagine sua mesa e um depósito grande. Bilhetes pequenos ficam na mesa e vão para o lixo quando você sai. Coisas maiores vão para o depósito. No Java, a mesa é a pilha (stack): variáveis locais e parâmetros vivem na caixa de cada chamada de método e somem quando ela retorna. O depósito é o heap: todo objeto criado com new mora lá. Uma variável como Point p guarda só uma referência (um endereço) para o objeto no heap.

  PILHA (mesa)              HEAP (depósito)
 ┌─────────────┐
 │ p ──────────┼───────►  [ Point x=1 y=2 ]
 └─────────────┘
A variável p está na pilha, o objeto está no heap

Quem joga fora os objetos que ninguém usa? No C++ com gerência manual, você escreve o delete. Se esquecer, há um vazamento (leak). Se apagar cedo demais, há um ponteiro pendurado (dangling pointer). O Java entrega esse trabalho ao coletor de lixo (garbage collector, GC).

O que é lixo?

Um objeto é lixo quando o programa não tem como alcançá-lo. O GC parte das raízes (variáveis locais nas pilhas e campos estáticos), segue cada referência e marca o que alcança. O que não foi marcado pode ser liberado. Isso é rastreamento (tracing), ou marcar e varrer (mark and sweep). A alcançabilidade é transitiva: um objeto apontado só por objetos inalcançáveis também é inalcançável.

raiz a ──► [1] ──► [2] ──► [3]

depois de  a.next = null :
raiz a ──► [1]        [2] ──► [3]   (2 e 3 são lixo)
Atribuir null corta uma referência. O 3 ainda é apontado, mas só por lixo
  • Atribuir null remove só aquela referência. Se outro caminho chega ao objeto, ele continua vivo.
  • A contagem de referências libera um objeto quando o contador chega a zero, mas não enxerga ciclos (A aponta para B e B aponta para A). O rastreamento, usado pela JVM, lida com ciclos: se ninguém alcançável aponta para o par, os dois são liberados.
  • Ser elegível não significa ser coletado agora. A máquina virtual decide quando.

Gerações e vazamentos

A maioria dos objetos morre jovem (um resultado temporário, uma variável de laço) e os que sobrevivem tendem a viver muito. Essa é a hipótese geracional. Por isso o heap é dividido: objetos novos nascem numa área jovem pequena (eden), limpa com frequência; os sobreviventes são promovidos para a geração antiga, limpa raramente.

↑ Voltar ao sumário

Herança: uma classe que se apoia em outra

Um filhote é um cachorro, e um cachorro é um animal. Tudo que vale para animais vale para cachorros, e os cachorros acrescentam seus truques. A herança faz isso no código: class Manager extends Employee torna Manager uma subclasse de Employee (a superclasse). Ela recebe os membros da superclasse e pode acrescentar ou mudar comportamento. Como cumpre toda promessa da superclasse, um Manager pode ser usado onde se espera um Employee. O contrário é falso.

        Object
           │
        Animal
        ╱     ╲
      Dog     Cat
       │
     Puppy
Uma árvore de classes. Toda classe Java tem uma superclasse direta; a raiz é Object
  • O Java permite uma superclasse só (herança simples). Isso evita receber duas versões de um método por dois caminhos. Para cumprir muitos contratos, uma classe usa implements com várias interfaces.
  • Object é a raiz e dá a toda classe toString, equals e hashCode.
  • Um objeto da subclasse contém os campos de toda a cadeia. Um campo private da superclasse existe nele, mas só os métodos da superclasse podem tocá-lo. Isso mantém os invariantes da superclasse a salvo.
  • final em uma variável proíbe reatribuição, em um método proíbe sobrescrever, em uma classe proíbe herdar (como String).

Construtores em cadeia

Todo construtor começa chamando outro: this(...) ou super(...). Se você não escreve nenhum, o compilador insere super(). Se a superclasse só tem construtores com parâmetros, essa chamada escondida não funciona e o código não compila: a subclasse precisa chamar super(args) explicitamente. Assim a construção sobe até Object, e os corpos rodam da classe mais geral para a mais específica.

new C()  onde C extends B extends A

corpo de A roda primeiro  ->  corpo de B  ->  corpo de C
Os corpos rodam do topo da árvore para baixo

Regras de sobrescrita

Para sobrescrever (override) um método, a subclasse repete a assinatura com um corpo novo. Ela pode oferecer mais que a superclasse, nunca menos: o acesso pode ficar igual ou ser ampliado (protected para public), o tipo de retorno pode ser um subtipo, e as exceções verificadas não podem ser mais amplas. Assim, qualquer código escrito para a superclasse continua funcionando.

Conversão para cima e para baixo

Um upcast (Animal a = new Cat()) é implícito e sempre seguro. Um downcast ((Dog) a) precisa ser escrito e é verificado na execução contra a classe real do objeto: se a guarda um Cat, lança ClassCastException. Teste antes com instanceof, que no Java moderno também declara a nova variável: if (a instanceof Dog d).

↑ Voltar ao sumário

Polimorfismo e despacho dinâmico

Diga 'faça um som' para uma sala com um cachorro, um gato e uma vaca. Cada um responde do seu jeito, e você não precisou saber qual era o animal. Isso é polimorfismo, 'muitas formas'. No código, escrevemos contra um tipo geral (uma superclasse ou uma interface), por exemplo rule.apply(total), e cada objeto executa a sua versão. Tipos novos entram sem mudar quem chama.

Dois tipos para uma variável

Em Shape s = new Circle(); a variável tem um tipo declarado (estático), Shape, e o objeto tem um tipo real (dinâmico), Circle. O compilador usa o tipo declarado para conferir que a chamada é permitida. A máquina virtual usa o tipo real para escolher o código. Escolher na execução é o despacho dinâmico (dynamic dispatch, também ligação tardia). Normalmente usa uma tabela de métodos por classe, chamada vtable.

s.area()
  tipo declarado Shape -> compilador: area() existe? sim
  tipo real Circle     -> execução: usa o area() de Circle

a busca começa na classe real e sobe:
Puppy -> Dog -> Animal -> Object
Dois passos para uma chamada

Se a subclasse não sobrescreve o método, a busca sobe e acha a versão da superclasse mais próxima. Objetos da própria superclasse mantêm o original. Dentro de uma sobrescrita, super.draw() chama a versão da superclasse direta da classe onde está escrito, sem despacho dinâmico. Isso permite estender em vez de substituir.

Sobrecarga não é sobrescrita

PontoSobrecarga (overloading)Sobrescrita (overriding)
O que éMesmo nome, parâmetros diferentes, mesma classeMesma assinatura, corpo novo em uma subclasse
EscolhidaNa compilação, pelos tipos declarados dos argumentosNa execução, pela classe real do receptor
Exemploprint(Object) e print(String)Dog.sound() substitui Animal.sound()

O Java faz despacho simples (single dispatch): na execução só conta a classe do objeto que recebe a chamada, nunca as classes dos argumentos. Se você passa uma String guardada em uma variável Object, o compilador escolhe print(Object) e fica assim. Quando o comportamento precisa depender dos dois objetos, usa-se o despacho duplo (o padrão Visitor).

Por que importa: acabar com as cadeias de if

Se a mesma decisão por tipo (if role == DEVELOPER ... else if role == DBA ...) se repete em vários métodos, cada novo cargo exige editar todos eles, e esquecer um é fácil. Com polimorfismo, cada cargo é uma classe que implementa uma interface comum e traz as próprias regras. O código antigo não muda: aberto para extensão, fechado para modificação (princípio aberto-fechado).

Exemplo de regras em uma lista, aplicadas em ordem, cada uma partindo do resultado da anterior: um preço de 10000 centavos, depois 10% de desconto, depois 500 a menos, depois 10% de desconto de novo.

Preço em centavos depois de cada regra de desconto
início
10,000 centavos
10% off
9,000 centavos
menos 500
8,500 centavos
10% off
7,650 centavos
↑ Voltar ao sumário

Abstração: interfaces e classes abstratas

Uma tomada na parede não se importa se você liga um abajur ou uma torradeira. Ela só define o formato do plugue. Uma interface é o plugue: lista as assinaturas de métodos que se podem pedir a um objeto, e as classes que a implementam (implements) fornecem o código. O código escrito contra a interface funciona com qualquer implementação, presente ou futura.

Uma classe abstrata é uma receita com um passo faltando. Pode ter campos, construtores e métodos prontos, mas deixa um ou mais métodos sem corpo, marcados abstract. Como falta uma parte, não se pode criar um objeto direto dela. A primeira subclasse concreta precisa dar corpo a todo método abstrato herdado.

CaracterísticaInterfaceClasse abstrata
Campos de instância e construtoresNãoSim
Quantas uma classe pode usarVárias (implements A, B)Uma (usa o único extends)
Melhor paraUm papel que tipos sem parentesco podem exercerCompartilhar estado e código entre parentes próximos

Métodos default e o diamante

Antigamente, acrescentar um método a uma interface quebrava toda classe que a implementava. Um método default traz um corpo que as classes herdam, e por isso forEach pôde entrar nas coleções do Java 8 sem quebrar código antigo. Mas uma classe pode então receber dois defaults com a mesma assinatura. O Java resolve com três regras:

  1. Um método vindo de uma classe sempre vence um vindo de uma interface (regra 'a classe vence'). Por isso um default nunca muda uma classe que já tinha esse método, e não pode redefinir toString nem equals.
  2. Uma interface mais específica vence a que ela estende.
  3. Se ainda houver empate, a classe não compila até sobrescrever o método ela mesma.

Template Method

A classe abstrata pode fixar a ordem dos passos em um método final que as subclasses não conseguem mudar, e deixar alguns passos abstratos ou sobrescrevíveis. A subclasse preenche as lacunas, e a ordem geral continua garantida pela superclasse. Esse é o padrão Template Method.

abstract class Game {
    final void play() {          // a ordem é fixa
        start(); turn(); end();
    }
    void start() { }             // passo opcional
    abstract void turn();        // cada jogo preenche este
    void end() { }
}
A receita é fixa, um passo está aberto

Dois princípios de projeto

  • Inversão de dependência: regras de negócio e detalhes devem depender de abstrações. Se Checkout faz new SaoPauloTax() lá dentro, está colado nessa classe. Receba uma interface Tax pelo construtor (injeção por construtor) e ele pode usar qualquer imposto.
  • Segregação de interfaces: prefira interfaces pequenas, uma por papel. Uma interface larga Document (ler, escrever, apagar) obriga uma classe somente leitura a lançar UnsupportedOperationException, o que quebra quem confia no contrato. Divida-a, e deixe os métodos pedirem o tipo mais estreito de que precisam (Readable para um preview).
↑ Voltar ao sumário

Composição contra herança

Há duas formas de reaproveitar uma classe. A herança diz 'é um': um cachorro é um animal. A composição diz 'tem um': um carro tem um motor. Um carro não pode ser usado onde se espera um motor, então Car extends Engine está errado. A pergunta decisiva é: um pode ser usado no lugar do outro? Se sim, a herança pode ser considerada. Se não, guarde o outro objeto em um campo e delegue o trabalho a ele.

class Car {
    private final Engine engine;      // tem-um
    Car(Engine engine) { this.engine = engine; }
    void start() { engine.ignite(); } // delega
}
Composição e delegação

Agregação e composição na UML

As duas são ligações todo-parte. Na agregação (losango vazio) a parte tem vida própria e pode pertencer a vários todos: um time e seus atletas. Na composição (losango preenchido) a parte pertence a um só todo, que a cria e a destrói: um prédio e seus apartamentos, um pedido e suas linhas. Qual escolher depende dos requisitos do sistema.

Por que 'prefira composição à herança'

  • Acoplamento mais forte. Uma subclasse depende de como a superclasse foi escrita, não só do que ela promete. Com composição, você usa apenas a interface pública e pode até trocar o objeto durante a execução.
  • Classe base frágil. Um CountingBag que sobrescreve add e addAll pode contar os itens duas vezes se o addAll da superclasse chama o próprio add. Esse detalhe interno não está no contrato e pode mudar. Se o contador, em vez disso, guarda uma sacola e delega, as chamadas internas nunca voltam para ele.
  • Herdou demais. Stack extends ArrayList herda também add(0, x) e remove, e então um cliente pode quebrar a regra último a entrar, primeiro a sair. Com composição a classe expõe só push e pop. O java.util.Stack real (que estende Vector) tem esse defeito.
  • Comportamento fixado no nascimento. A herança decide o comportamento quando o objeto é criado. Com composição, a parte que varia mora em outro objeto atrás de uma interface, e pôr um objeto diferente no campo muda o comportamento durante a execução. Isso também evita uma explosão de subclasses quando há vários eixos (frete e imposto).
Assista ao vídeo: Nothing is Something, Sandi Metz

O princípio de substituição de Liskov

Um programa escrito para o tipo base deve continuar correto com qualquer subtipo. Na matemática todo quadrado é um retângulo. Mas se Rectangle tem setWidth e setHeight, um Square mutável que muda os dois lados quebra quem define 5 por 4 e espera área 20. Logo, o 'é um' da herança trata de comportamento, não de como as coisas são classificadas no mundo real.

Não peça mais, não entregue menos
Parte do contratoUm subtipo podeExemplo que quebra
Pré-condição (o que pede)Ser igual ou mais fraca: aceitar pelo menos tantoA base aceita qualquer valor positivo, a subclasse recusa valores acima de 1000
Pós-condição (o que garante)Ser igual ou mais forte: prometer pelo menos tantoA base garante resultado não negativo, a subclasse pode devolver negativo
↑ Voltar ao sumário

Acoplamento, coesão e maus cheiros de código

Uma boa caixa de ferramentas guarda martelos com martelos e parafusos com parafusos. Isso é coesão: as partes de uma classe trabalham para um só objetivo. Uma classe coesa tem um único motivo para mudar (o princípio da responsabilidade única), é pequena e fácil de nomear e testar. Um sinal de baixa coesão é uma classe cujos métodos usam conjuntos diferentes de campos.

Acoplamento é o quanto uma classe depende de outras, como fios entre caixas. Não dá para eliminá-lo, já que os objetos precisam trabalhar juntos, mas dá para escolhê-lo: dependa de poucas coisas, e de coisas estáveis como interfaces. O objetivo clássico é alta coesão e baixo acoplamento.

  • Acoplamento aferente: quantas classes dependem de mim. Se é alto, carrego muita responsabilidade e mudar minha interface pública é arriscado.
  • Acoplamento eferente: de quantas classes eu dependo. Se é alto, tenho muitos motivos externos para mudar.
  • Uma classe usada por 20 outras e que não usa nenhuma é a mais arriscada de ter a interface alterada. Aponte as dependências para módulos estáveis.

Maus cheiros (code smells)

Um mau cheiro de código é um padrão que sugere código difícil de entender ou mudar. O programa funciona e os testes passam, mas a próxima mudança será lenta. Refatorar é mudar a estrutura sem mudar o comportamento, e a prova é que os mesmos testes passam antes e depois.

Seis cheiros do catálogo deste repositório
CheiroComo apareceCura usual
Long Method (método longo)40 linhas em blocos separados por comentáriosExtract Method: cada bloco vira um método com nome
God Class (classe deus)Uma classe valida, calcula, envia e-mails e gera relatórios; difícil de testarExtract Class: uma por responsabilidade, a antiga coordena
Feature Envy (inveja de recursos)Um método usa só getters de outra classeMove Method para a classe dona dos dados (tell, don't ask)
Shotgun Surgery (cirurgia de espingarda)Uma mudança exige edições em muitos lugares (switches de formas de pagamento)Reunir: uma interface com uma classe por forma
Primitive Obsession (obsessão por primitivos)Strings e ints para e-mail, CPF, dinheiro; argumentos trocados compilamTipos pequenos (objetos de valor) que validam no construtor
Cadeia condicional por código de tipoA mesma cadeia de if por tipo em vários métodosTrocar por polimorfismo

Na medição deste repositório, ao acrescentar um quarto tipo de entrega a um checkout:

Arquivos existentes editados para acrescentar um tipo novo
cadeias de if
4 arquivos
polimorfismo
1 arquivos

A troca não é de graça: com polimorfismo, um tipo novo é uma classe nova, mas uma operação nova sobre todos os tipos edita a interface e todas as classes. Escolha pelo que muda mais vezes.

Complexidade ciclomática

Ela conta os caminhos independentes de um método, então indica quantos testes você precisa. No quiz, comece em 1 e some 1 para cada `for`, `if`, ternário `?:`, `&&` e `||`. Um método com um if e um for tem complexidade 3. Valores altos sugerem extrair métodos ou usar polimorfismo.

↑ Voltar ao sumário

Exceções: quando algo dá errado

Numa corrida de revezamento, se uma corredora deixa cair o bastão, ela grita e a equipe interrompe o plano normal até alguém saber o que fazer. Uma exceção é esse grito. Lançá-la interrompe o fluxo normal, e a máquina virtual desfaz a pilha de chamadas método por método, procurando um catch compatível. Se chega ao topo sem achar, a thread termina e o erro é impresso. Isso permite que um método de baixo nível detecte o problema e um nível mais alto, que sabe o que fazer, trate dele, sem códigos de retorno passados de mão em mão.

main()  ── tem catch (IOException)
  └ load()
      └ read()   <- lança IOException aqui

busca: read -> load -> main (achou!)
A exceção sobe até um catch combinar

Verificadas e não verificadas

Tudo que se lança descende de Throwable. As exceções verificadas (checked, por exemplo IOException) são falhas que podem acontecer mesmo num programa correto, como um arquivo que não existe. O compilador obriga a tratá-las ou declará-las com throws. As não verificadas (unchecked: RuntimeException e suas filhas, como NullPointerException) normalmente mostram um bug, e Error é uma falha grave da máquina. O compilador não obriga a capturá-las.

try, catch, finally

  • Quando um comando do try lança, o resto do try é pulado. O controle vai para o primeiro catch compatível, depois para o finally, e então o programa continua depois da estrutura toda. Não há volta à linha que falhou.
  • Os blocos catch são testados em ordem e vence o primeiro cujo tipo é igual à exceção ou é uma superclasse dela. Por isso o mais específico vai primeiro. Pôr uma superclasse antes da sua subclasse não compila, porque o segundo bloco seria código morto.
  • O finally roda por qualquer caminho que o controle use para sair do try: fim normal, return, exceção capturada ou não. Só não roda se a máquina for parada (por exemplo System.exit).
static int value() {
    int x = 1;
    try { x = 2; return x; }   // o resultado 2 é guardado antes
    finally { x = 3; }         // tarde demais para mudá-lo
}                              // devolve 2
O valor de retorno é separado antes de o finally rodar

Try-with-resources

Para arquivos e conexões, escreva try (Res a = ...; Res b = ...). Os objetos que implementam AutoCloseable são fechados para você quando o bloco termina, com ou sem exceção. Eles são fechados na ordem inversa da abertura, como uma pilha, porque um recurso mais novo pode usar um mais antigo.

Traduza, mas guarde a causa

Cada camada deve falar a sua língua: quem chama place(order) não precisa saber SQL, então você captura SQLException e lança OrderNotSavedException. A nova exceção precisa carregar a original como causa: new OrderNotSavedException(msg, e). Isso é o encadeamento de exceções; o rastro impresso mostra as duas, ligadas por Caused by. Sem isso, o diagnóstico se perde.

↑ Voltar ao sumário

Genéricos e coleções

Caixas com etiqueta são melhores que caixas misteriosas. Uma List<String> é uma lista com a etiqueta 'só strings'. O compilador rejeita add(42) e sabe que get(0) devolve uma String. Sem a etiqueta, tudo é Object, e cada leitura precisa de uma conversão (cast) que pode falhar na execução. Os genéricos deixam você escrever uma classe ou método uma vez e usá-lo com muitos tipos, ainda conferido.

As principais coleções

Escolha a interface pelo contrato, depois a implementação pelo custo e pela ordem
InterfaceContratoImplementações comuns
ListOrdenada por posição, aceita duplicatasArrayList, LinkedList
SetSem duplicatasHashSet (sem ordem, rápido), LinkedHashSet (ordem de inserção), TreeSet (ordenado, O(log n))
Queue / DequeFila de espera, uma ou as duas pontasArrayDeque
MapChaves únicas, cada uma com um valorHashMap, TreeMap

Em um Map, put com uma chave nova acrescenta uma entrada. Com uma chave que já existe, troca o valor e devolve o antigo, então o tamanho conta chaves distintas. O TreeSet ordena pela ordem natural (Comparable) ou por um Comparator.

Regras dos genéricos do Java

  • Só tipos por referência. List<int> não compila. Use a classe invólucro Integer (e Double, Boolean). O compilador converte nos dois sentidos (autoboxing e unboxing), ao custo de um objeto por elemento.
  • Invariância. Integer é subtipo de Number, mas List<Integer> não é uma List<Number>. Se fosse, você poderia pôr um 3.14 numa lista de inteiros.
  • Limites. Um T sem limite só tem os métodos de Object. T extends Comparable<T> libera o compareTo, então max(a, b) funciona para qualquer tipo que se compara com o seu próprio.
  • Curingas (wildcards). ? extends T para o que você só lê (um produtor). ? super T para o que você só escreve (um consumidor). Regra para lembrar, PECS: producer extends, consumer super. Copiar de List<Integer> para List<Number> usa copy(List<? super T> dst, List<? extends T> src).
  • Apagamento de tipos (type erasure). Depois da verificação, o compilador troca T pelo seu limite (ou Object) e insere conversões. O código compilado não sabe o que era T. Por isso List<String> e List<Integer> têm a mesma classe na execução, e você não pode escrever new T(), new T[10] nem x instanceof List<String>.

Iteradores fail-fast

As coleções de java.util contam suas modificações. Se você remove de uma lista dentro de um laço for-each, o iterador percebe e lança ConcurrentModificationException. O nome engana: não precisa de uma segunda thread. Para remover durante o laço, use o remove() do próprio iterador ou removeIf.

↑ Voltar ao sumário

Fluxos, entrada e saída, serialização

Um fluxo (stream) é como uma mangueira de jardim: os dados correm em uma direção e você os pega aos poucos. Um fluxo de bytes (InputStream, OutputStream) move números crus de 8 bits, sem significado. Um fluxo de caracteres (Reader, Writer) acrescenta a codificação, a regra que transforma bytes em letras. Usar o tipo errado com texto corrompe as letras acentuadas.

Byte e caractere são iguais só para ASCII simples. O UTF-8 tem tamanho variável: os primeiros 128 caracteres usam 1 byte, uma letra acentuada como ç usa 2, o símbolo do euro 3, um emoji 4. Para contar os bytes de um texto, some o tamanho de cada caractere (a palavra ação tem 4 caracteres mas 6 bytes). Leia texto com um fluxo de caracteres e diga a codificação, ou você corta letras acentuadas ao meio.

Bytes de um caractere em UTF-8
a
1 bytes
ç
2 bytes
€
3 bytes
emoji
4 bytes

Buffers

Ir ao sistema operacional para cada caractere é como ir à biblioteca para cada palavra. Um fluxo com buffer (BufferedReader) traz uma página inteira de uma vez e entrega as letras da memória. A escrita é parecida, com um aviso: os dados só chegam ao destino quando o buffer enche, ou em flush() ou close().

Serialização

Um objeto na memória é uma teia de referências que só faz sentido dentro do programa em execução. Para gravá-lo em um arquivo ou enviá-lo pela rede, você o serializa: transforma seu estado em uma fila de bytes (binário do Java, com ObjectOutputStream) ou em texto (JSON, XML). Uma classe Java aceita isso implementando a interface marcadora Serializable.

  • transient deixa um campo de fora: use para senhas, caches, conexões. Quando o objeto é lido de volta, ele é reconstruído sem chamar o construtor da classe serializável, e os campos transient voltam como null, 0 ou false.
  • O fluxo também grava o nome da classe e um número de versão. Na leitura, compara com a classe carregada. Se você mudou a classe e não declarou serialVersionUID, o número calculado muda e ler arquivos antigos lança InvalidClassException. Declarar private static final long serialVersionUID permite que a classe evolua de forma compatível.
↑ Voltar ao sumário

POO entre linguagens: traits, interfaces, protocolos

Nem toda linguagem tem classes e herança como o Java. Mas todas precisam de jeitos de agrupar dados e comportamento e de deixar coisas diferentes responderem ao mesmo pedido. Veja como algumas fazem.

A mesma necessidade, ferramentas diferentes
LinguagemComo faz polimorfismoReuso
Java, C#Interfaces nominais: a classe precisa declarar implementsHerança e composição
TypeScriptInterfaces estruturais: ter os membros bastaClasses e composição
GoInterfaces estruturais, sem classes nem herançaComposição
RustTraits em structs e enums, estáticas (T: Display) ou dinâmicas (dyn Trait)Sem herança de implementação
ElixirProtocolos (defprotocol) implementados por tipo de dadoFunções e módulos, sem classes

Nominal contra estrutural

Em um sistema nominal (Java), um valor pertence a um tipo porque a sua classe diz isso pelo nome. Em um sistema estrutural (TypeScript, Go), ter os membros necessários basta. Um Book com price serve como Priced no TypeScript sem dizer implements, e um objeto simples { price: 5 } também serve, o que é útil para dublês de teste. A mesma classe sem implements é recusada no Java.

interface Priced { price: number }
class Book { constructor(public price: number) {} }
const items: Priced[] = [new Book(10), { price: 5 }];   // os dois são aceitos
Tipagem estrutural no TypeScript

A tipagem nominal registra a intenção e evita coincidências (dois métodos run com significados diferentes). A estrutural dá flexibilidade e dispensa adaptadores para tipos que não são seus. No Go, uma interface pequena pode ser definida do lado de quem consome, e um tipo que já existe, até de outra biblioteca, a satisfaz sem mudar.

O problema da expressão

Os objetos agrupam o código por variante (uma classe por tipo). Acrescentar um tipo é fácil, acrescentar uma operação exige editar todas as classes. Funções com casamento de padrões agrupam o código por operação. Acrescentar uma operação é fácil, acrescentar um tipo exige editar todas as funções. Este repositório mediu: acrescentar 'leve e pague' a um carrinho foi um arquivo novo com objetos, e quatro edições dentro de um arquivo com funções. A mudança oposta tem o custo oposto.

↑ Voltar ao sumário

Juntando tudo

Todas as ferramentas da POO respondem a uma pergunta: onde mora cada pedaço de conhecimento, e como uma mudança se espalha? Use esta tabela quando encontrar uma questão do quiz.

Se o problema é...Pense em...Porque...
Gente de fora quebra minhas regrasEncapsulamento, cópia defensivaAs regras moram junto dos dados
O mesmo if por tipo em todo lugarPolimorfismoCada tipo carrega as próprias regras
Difícil trocar uma dependênciaInterface e injeção por construtorDependa de abstrações
A subclasse quebra quando o pai mudaComposiçãoHerança é o acoplamento mais forte
Ler arquivos binários antigos falhaserialVersionUIDO formato está acoplado à classe
A memória cresce sem parar com um GCAlcançabilidade, coleções estáticasO GC libera só o inalcançável
Qual método roda?Tipo declarado nas sobrecargas, tipo real nas sobrescritasSão dois momentos diferentes

Erros comuns

  • Achar que private basta enquanto um getter devolve a lista interna.
  • Confundir sobrecarga (compilação) com sobrescrita (execução), e esquecer que métodos e campos estáticos não são sobrescritos.
  • Usar herança só para reaproveitar código, ou porque 'um quadrado é um retângulo'.
  • Chamar um método sobrescrevível dentro de um construtor.
  • Capturar a exceção da superclasse antes da subclasse, ou lançar uma exceção nova sem a causa.
  • Achar que List<Integer> é uma List<Number>, ou que o tipo T ainda existe na execução.
  • Confiar no coletor de lixo para fechar arquivos.
↑ Voltar ao sumário