Java orientado a objetos

Valores e referências, contratos, composição, igualdade, coleções e genéricos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

O herói tem posição e vida. Os métodos que alteram estes valores têm de preservar as regras do jogo: a vida fica entre zero e cem e o movimento só chega a células permitidas. Escolher essas operações vem antes de desenhar uma hierarquia de classes.

Ler e correr um programa

Os exemplos completos usam uma classe pública Main, guardada em Main.java. javac Main.java compila o código para bytecode; java Main executa-o na máquina virtual Java, JVM. Precisas de um JDK para compilar, não apenas de um ambiente que execute Java. No projeto Gradle, as tarefas fazem estes passos por ti.

O ponto de entrada é public static void main(String[] args). void indica que o método não devolve um valor; args recebe os argumentos da linha de comandos. As chavetas delimitam classes e métodos, e ; termina instruções como uma atribuição ou chamada.

Numa declaração int somar(int a, int b), o primeiro int é o tipo do resultado. a e b são parâmetros, e return a + b; devolve o resultado ao chamador. Uma chamada somar(2, 3) fornece os argumentos. O retorno não escreve no ecrã; System.out.println é uma operação separada.

new Posicao(3, 4) cria um objeto e chama o construtor. Um construtor tem o nome da classe e não declara tipo de retorno. Em this.x = x, this.x é o campo do objeto e x é o parâmetro recebido. Um import permite escrever nomes curtos de tipos de outros pacotes; não instala uma biblioteca.

Tipos, literais e controlo

Java é estaticamente tipado: cada variável tem um tipo conhecido na compilação. Os tipos primitivos são byte, short, int, long, float, double, char e boolean. int guarda inteiros de 32 bits; double representa números de vírgula flutuante, com aproximação. char é uma unidade UTF-16, não necessariamente um carácter completo. String é um objeto imutável, não um primitivo.

Literais como 3, 3L, 2.5, 'A', "cima" e true têm tipos próprios. Em int metade = 5 / 2, a divisão entre inteiros produz 2; 5 / 2.0 produz 2.5. Um cast não recupera a fração já perdida.

if, switch, for e while controlam a execução como em outras linguagens. Uma condição Java tem de ser booleana; um inteiro não serve como condição. && e || fazem curto-circuito, por isso p != null && p.equals(origem) não chama equals quando p é null.

Uma variável local tem de receber um valor antes de ser lida; o compilador não inventa um valor inicial. Os arrays têm tamanho fixo e índices de zero a length - 1. As coleções permitem representar grupos que crescem e diminuem.

Promoção e overflow inteiro

Um byte representa inteiros de -128 a 127. Em b + 1, Java promove os operandos para int, por isso o resultado dessa expressão é um int. Atribuir esse resultado a um byte exige uma conversão explícita quando não se trata de uma constante representável conhecida pelo compilador.

byte b = 127;
int soma = b + 1;       // 128, sem alterar b
b++;                   // -128, depois da conversão de volta para byte
b += 1;                // -127
System.out.println(soma + " " + b);

A saída é 128 -127. ++ e a atribuição composta incluem a conversão de volta ao tipo da variável. A soma não escolhe automaticamente um tipo maior para a guardar; ao reduzir a byte, conserva os oito bits menos significativos. Já b = b + 1 não compila neste exemplo: a soma é int e a atribuição simples não faz essa redução implicitamente.

O mesmo cuidado vale para int: operações inteiras podem fazer overflow sem lançar uma exceção. Em (long) x + deslocamento, a conversão vem antes da soma; em (long) (x + deslocamento), a soma ainda pode ter transbordado em int. Escolhe o tipo e a política de erro pelo intervalo do contrato, como no movimento de MVC.

Valores e referências

Uma variável primitiva guarda o valor. Uma variável de tipo de referência guarda uma referência para um objeto, ou null. Atribuir uma referência não copia o objeto.

Duas referências, um objeto
import java.util.ArrayList;
import java.util.List;

public class Main {
    static void alterar(List<String> nomes) {
        nomes.add("Rita");
        nomes = new ArrayList<String>();
        nomes.add("Luis");
    }

    public static void main(String[] args) {
        final List<String> equipa = new ArrayList<String>();
        equipa.add("Ana");
        List<String> outra = equipa;
        alterar(outra);
        System.out.println(equipa);
        System.out.println(equipa == outra);
    }

}
Resultado

Executa o código para ver a saída e os gráficos.

A saída é [Ana, Rita] e true, pois Java passa argumentos por valor, incluindo o valor de uma referência: o parâmetro e a variável do chamador começam a apontar para o mesmo objeto e add altera esse objeto, enquanto atribuir uma lista nova ao parâmetro muda apenas a referência local.

Segue as setas: acrescentar Rita muda a lista partilhada, não as referências. No último painel, só a seta de nomes muda de destino. A lista com Luis é nova; equipa e outra continuam a ver [Ana, Rita].

final impede atribuir outra referência à variável equipa. Continua a ser possível alterar a lista. Para obter imutabilidade temos de controlar também o estado do objeto e as referências mutáveis que expomos.

Classes e objetos

Uma classe define estado e operações. Um objeto é uma instância concreta com o seu estado. O construtor estabelece o estado inicial; os métodos devem manter os invariantes, condições que todo o objeto válido satisfaz.

Uma vida entre zero e cem é um invariante. Um setter público que aceita qualquer inteiro quebra-o. Uma operação sofrerDano(int dano) pode validar um dano não negativo e impedir que a vida desça abaixo de zero. Encapsular não é só escrever private e acrescentar getters e setters; é escolher operações que preservam o contrato.

A visibilidade controla quem pode chamar ou consultar: private fica na classe; sem modificador fica no pacote; protected permite acesso no pacote e, com regras próprias, por subclasses; public expõe uma API. Um método static pertence à classe e não dispõe de um this. Um método de instância atua sobre o objeto a que this se refere.

Um valor imutável

Uma posição identifica uma célula. Escolhemos inteiros e um movimento que devolve uma posição nova, sem alterar a origem. Esta classe não decide se a célula está dentro da arena; essa regra pertence à arena.

Mover sem alterar a origem
import java.util.HashSet;
import java.util.Set;

final class Posicao {
    private final int x;
    private final int y;

    Posicao(int x, int y) {
        this.x = x;
        this.y = y;
    }

    Posicao somar(int dx, int dy) {
        return new Posicao(x + dx, y + dy);
    }

    @Override
    public boolean equals(Object outro) {
        if (this == outro) return true;
        if (!(outro instanceof Posicao)) return false;
        Posicao p = (Posicao) outro;
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return 31 * x + y;
    }

    @Override
    public String toString() {
        return "(" + x + "," + y + ")";
    }

}

public class Main {
    public static void main(String[] args) {
        Posicao origem = new Posicao(3, 4);
        Posicao destino = origem.somar(1, -2);
        Set<Posicao> visitadas = new HashSet<Posicao>();
        visitadas.add(destino);
        visitadas.add(new Posicao(4, 2));
        System.out.println(origem + " -> " + destino);
        System.out.println(destino == new Posicao(4, 2));
        System.out.println(destino.equals(new Posicao(4, 2)));
        System.out.println(visitadas.size());
    }
}
Resultado

Executa o código para ver a saída e os gráficos.

A saída é (3,4) -> (4,2), false, true e 1. A origem ficou intacta. O Set reconheceu as duas posições de coordenadas iguais como o mesmo valor.

Igualdade e hash

Para objetos, == compara identidade, ou seja, se são o mesmo objeto. equals compara a igualdade definida pelo tipo. Por defeito, Object.equals também usa identidade; em Posicao escolhemos igualdade por coordenadas.

equals deve ser reflexivo, simétrico, transitivo e consistente enquanto o estado relevante não muda. Para um objeto não nulo, equals(null) deve ser falso. Se dois objetos são iguais, têm de ter o mesmo hashCode. O inverso não é exigido, porque dois valores diferentes podem colidir.

Por isso redefinimos os dois métodos: um HashSet usa o hash para escolher onde procurar e depois a igualdade para distinguir valores, pelo que, se o hash dependesse da identidade, posições iguais poderiam ir para grupos diferentes.

Evita alterar campos usados em equals e hashCode enquanto o objeto é uma chave de HashMap ou um elemento de HashSet. Pode deixar de ser encontrado no grupo em que foi colocado. Um tipo imutável evita esta mudança.

A assinatura de equals

public boolean equals(Object outro) redefine o método de Object. Um método boolean equals(Posicao outra) é uma sobrecarga, mesmo que compare corretamente as coordenadas. Uma chamada com argumento declarado Posicao pode escolhê-la, enquanto uma chamada através de Object continua a usar a operação herdada.

Por isso um teste a.equals(b) com ambas as variáveis declaradas Posicao pode esconder uma assinatura errada. Verifica também a operação do contrato Object, por exemplo a.equals((Object) b), e o uso numa coleção. Colocar @Override no método deteta este erro na compilação. Depois mantém o contrato de igualdade e o hashCode correspondente; corrigir a assinatura não torna válida uma comparação que esquece uma coordenada.

Herança, interfaces e composição

Uma interface descreve operações que um cliente pode pedir. Uma classe concreta implementa-as. O polimorfismo dinâmico escolhe a implementação em tempo de execução conforme o objeto, mesmo quando a variável tem o tipo da interface.

Uma interface com uma única operação abstrata é uma interface funcional. Uma lambda fornece a implementação dessa operação, como () -> System.out.println("fim") para Runnable.run(). Os parâmetros ficam antes de -> e o corpo depois. Uma expressão new Runnable() { ... } cria uma instância de uma classe anónima que implementa a interface; o bloco contém os seus métodos.

Uma classe abstrata pode combinar operações abstratas, implementação e estado partilhados. Uma classe Java só estende uma classe, mas pode implementar várias interfaces. Uma interface não obriga os implementadores a herdarem o mesmo estado.

A herança deve permitir substituição: qualquer objeto derivado tem de cumprir as promessas da base. Ter campos semelhantes não chega. A composição guarda um colaborador e delega-lhe uma tarefa. Permite variar essa tarefa sem criar subclasses para todas as combinações.

O mesmo inimigo, dois movimentos
interface Movimento {
    int proximoX(int atual);
}

class ParaDireita implements Movimento {
    public int proximoX(int atual) { return atual + 1; }
}

class Parado implements Movimento {
    public int proximoX(int atual) { return atual; }
}

class Inimigo {
    private int x;
    private final Movimento movimento;

    Inimigo(int x, Movimento movimento) {
        this.x = x;
        this.movimento = movimento;
    }

    void passo() { x = movimento.proximoX(x); }
    int getX() { return x; }

}

public class Main {
    public static void main(String[] args) {
        Inimigo a = new Inimigo(2, new ParaDireita());
        Inimigo b = new Inimigo(2, new Parado());
        a.passo();
        b.passo();
        System.out.println(a.getX() + " " + b.getX());
    }
}
Resultado

Executa o código para ver a saída e os gráficos.

Obtemos 3 2. Inimigo contém um Movimento; não precisa de saber que classe o implementa. A posição e o algoritmo ficam separados. Esta colaboração será o padrão Strategy.

Overriding redefine numa subclasse um método herdado com assinatura compatível; a implementação executa-se segundo o objeto em execução. Overloading oferece métodos com o mesmo nome e parâmetros diferentes; a sobrecarga escolhe-se na compilação pelo tipo declarado dos argumentos.

Por isso visitar(a) com a declarado Animal chama visitar(Animal) mesmo que o objeto seja Gato, e dentro dele falar() já usa Gato. Vê o exercício resolvido de despacho na prática. @Override permite ao compilador detetar uma assinatura escrita por engano.

Tipos e coleções

No jogo, os comandos pendentes precisam de conservar uma ordem; as células visitadas precisam de ignorar visitas repetidas. Guardar ambos numa lista obrigaria o segundo cliente a procurar duplicados. Escolhe a coleção pelo contrato:

InterfaceO que representaExemplo
List<E>Sequência com posições e duplicadosComandos pela ordem em que chegaram.
Set<E>Elementos sem duplicados lógicosCélulas já visitadas.
Map<K,V>Uma associação por chaveComando associado a cada tecla.
Queue<E>Elementos a retirar segundo uma políticaAções pendentes numa fila.

ArrayList dá acesso por índice em tempo constante e acrescenta no fim em tempo constante amortizado. Inserir ou remover no meio desloca elementos, com custo linear no número de elementos. HashSet e HashMap têm operações básicas com custo esperado constante quando os hashes se distribuem bem; não prometem ordem de iteração.

Um Map substitui o valor quando inseres uma chave igual à existente. Usa containsKey quando tens de distinguir ausência de um valor null. Um for-each usa um iterador; remover diretamente da coleção durante a iteração pode provocar ConcurrentModificationException. Se o contrato o permitir, usa Iterator.remove() ou constrói uma coleção do resultado.

Ordem e unicidade num TreeSet

Um TreeSet mantém ordem natural, definida por Comparable.compareTo, ou a ordem de um Comparator fornecido na construção. Para as operações do conjunto, dois elementos são equivalentes quando a comparação dá zero. As operações básicas têm custo O(log⁡n)O(\log n) para nn elementos.

Se as posições (2,1) e (2,9) se comparam só por x, a segunda inserção é recusada: a comparação dá zero, embora equals por ambas as coordenadas dê falso. Para guardar posições distintas em ordem, compara primeiro x e, em empate, y. Assim, zero coincide com igualdade por coordenadas.

int comparar(Posicao a, Posicao b) {
    int porX = Integer.compare(a.getX(), b.getX());
    return porX != 0 ? porX : Integer.compare(a.getY(), b.getY());
}

Este excerto pressupõe getters de inteiros nas posições. Integer.compare evita o overflow que a.getX() - b.getX() poderia sofrer. Um TreeSet com comparação incoerente com equals tem comportamento definido, mas não cumpre o contrato geral de Set. Não alteres os campos que determinam a ordem enquanto o objeto pertence ao conjunto, pelo mesmo motivo que protege as chaves de hash.

Proteger uma coleção

Um campo private deixa de proteger o estado se um getter devolve a lista interna e os clientes a alteram diretamente. Escolhe o que o cliente pode fazer e se deve acompanhar mudanças futuras.

Uma cópia defensiva cria outra coleção. Uma vista não modificável impede alterações através dessa vista, mas continua ligada à coleção original. Compara os dois casos:

Uma vista acompanha mudanças, uma cópia guarda a sequência
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> nomes = new ArrayList<String>(Arrays.asList("Ana"));
        List<String> vista = Collections.unmodifiableList(nomes);
        List<String> copia = new ArrayList<String>(nomes);
        nomes.add("Rita");
        System.out.println(vista);
        System.out.println(copia);
    }
}
Resultado

Executa o código para ver a saída e os gráficos.

A vista mostra [Ana, Rita]; a cópia mostra [Ana]. Chamar vista.add lançaria UnsupportedOperationException, mas alterar nomes continua permitido. Para devolver um snapshot não modificável, podes envolver uma cópia com Collections.unmodifiableList.

A cópia é superficial: copia as referências dos elementos, não cada objeto. Aqui as strings são imutáveis. Numa lista de heróis mutáveis, a cópia não impediria alterar a vida de um herói partilhado. Também pode ser preciso copiar a coleção recebida no construtor, para o chamador não alterar o estado por uma referência que conserva.

Definir classes e métodos genéricos

Antes dos wildcards, aprende a declarar o teu próprio tipo genérico. Caixa<T> guarda um valor de um tipo escolhido pelo cliente:

class Caixa<T> {
    private T valor;
    Caixa(T valor) { this.valor = valor; }
    T get() { return valor; }
    void set(T valor) { this.valor = valor; }
}

Caixa<Integer> c = new Caixa<Integer>(3); guarda inteiros; c.get() devolve Integer sem cast. O compilador verifica os usos de T e aplica apagamento de tipos ao bytecode. As instâncias de Caixa<Integer> e Caixa<String> partilham a mesma classe em execução; não podemos usar instanceof Caixa<Integer>. Isto não significa que toda a informação genérica desapareça: declarações podem conservar metadados consultáveis por reflexão.

Um método também pode introduzir o seu próprio parâmetro:

static <E> E primeiro(java.util.List<E> l) { return l.get(0); }

<E> antes do retorno declara o parâmetro do método. O compilador infere E a partir do argumento: primeiro(Arrays.asList("a", "b")) infere E = String.

Pacotes organizam tipos relacionados e controlam visibilidade: classes do mesmo pacote veem membros sem modificador.

super(...) chama o construtor da base e super.metodo() a sua implementação; usa-o quando a subclasse precisa de reaproveitar construção ou comportamento da base em vez de o duplicar.

Genéricos e wildcards

List<String> deixa o compilador verificar que só acrescentas strings. Não precisa de casts ao ler. Usa tipos wrapper, como Integer, em vez de int como parâmetro de tipo; Java faz boxing e unboxing, mas unboxing de null lança NullPointerException.

Mesmo que Integer seja subtipo de Number, List<Integer> não é subtipo de List<Number>. Se fosse, um cliente poderia acrescentar um Double à lista de inteiros. Os genéricos são invariantes.

Para copiar sem perder essa verificação, declara cada lista pelo papel que desempenha na operação. Um wildcard indica como a coleção participa:

  • List<? extends Number> produz números que podes ler como Number. Não podes acrescentar um Integer arbitrário, porque a lista pode ser de Double.
  • List<? super Integer> aceita inteiros. Ao ler, só sabes que o resultado é Object, porque a lista pode conter outros objetos.
  • List<?> aceita uma lista de qualquer tipo, mas não permite inserir um valor não nulo sem conhecer esse tipo.

Não interpretes extends como imutabilidade: ainda podes, por exemplo, remover elementos. A restrição diz respeito ao tipo dos valores que acrescentas.

Copiar de quem produz para quem recebe
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    static <T> void copiar(List<? extends T> origem,
    List<? super T> destino) {
        for (T valor : origem) destino.add(valor);
    }

    public static void main(String[] args) {
        List<Integer> origem = Arrays.asList(2, 3);
        List<Number> destino = new ArrayList<Number>();
        destino.add(1.5);
        copiar(origem, destino);
        System.out.println(destino);
    }

}
Resultado

Executa o código para ver a saída e os gráficos.

A saída é [1.5, 2, 3]. A origem produz valores de um tipo compatível com T; o destino aceita esse tipo. O método não precisa de converter a lista inteira nem de perder a verificação na compilação. A biblioteca segue a mesma regra em Collections.copy(List<? super T> dest, List<? extends T> src): o destino recebe e a origem fornece.

Exceções e contratos

Uma exceção interrompe o fluxo normal até encontrar um catch compatível. throw lança uma exceção; throws declara as exceções que um método pode propagar. Exceções verificadas, como IOException, têm de ser tratadas ou declaradas. RuntimeException e subclasses, como IllegalArgumentException, não exigem essa declaração.

Usa uma exceção com significado para o erro. Não captures Exception só para continuar com um estado inválido. Um recurso que precisa de ser fechado deve ficar num try-with-resources, como veremos em I/O. A recolha de lixo liberta memória sem referências alcançáveis, mas não substitui o fecho de ficheiros nem a libertação de outros recursos.

Exercícios

Prever uma alteração através de um parâmetro

Uma lista L começa com um elemento. Um método recebe L, acrescenta outro elemento e depois atribui uma lista vazia ao parâmetro. Quantos elementos tem L depois da chamada?

Pista
Desenha duas referências para o objeto inicial.
Mais uma pista

Acrescentar altera o objeto; a atribuição muda só a referência local.

Solução

L tem dois elementos. Percorre: à entrada, parâmetro e L apontam para a mesma lista com um elemento; add altera esse objeto para dois; a reatribuição troca só a referência local para uma lista nova. O chamador continua a ver dois elementos.

Erros frequentes

Java não passa a variável do chamador por referência.
Voltar à explicação
Igualdade sem identidade

Duas posições diferentes em memória têm as mesmas coordenadas. O tipo redefine equals corretamente, mas usa um hash dependente da identidade. Explica o problema num HashSet e a correção.

Pista
O HashSet começa a procurar através do hash.
Mais uma pista

Se os objetos iguais forem colocados em grupos diferentes, a igualdade pode não ser consultada entre eles.

Solução

As posições são iguais pelo contrato, mas os hashes podem diferir, violando o contrato de hashCode. O conjunto pode aceitar duplicados lógicos ou falhar uma procura. Calcula o hash a partir das coordenadas e mantém esse estado estável enquanto está no conjunto.

Confere a tua resposta:

  • Distingue identidade de igualdade por coordenadas.
  • Afirma que objetos iguais têm de ter o mesmo hash.
  • Deriva o hash de estado compatível com equals.

Erros frequentes

Não é necessário que todos os objetos diferentes tenham hashes diferentes.

Voltar à explicação
Receber números de tipos diferentes

Um método só lê números de uma lista e chama doubleValue em cada um. Deve aceitar List<Integer> e List<Double>. Qual é o parâmetro adequado?

A tua resposta

Pista
A lista fornece valores ao método.
Mais uma pista

Precisamos de ler como Number, sem acrescentar um tipo arbitrário.

Solução

Usa List<? extends Number>. Integer e Double são subtipos de Number, e ambos fornecem doubleValue. A lista continua a poder ser mutável; o wildcard não dá imutabilidade.

  • List<Number>. A invariância impede passar diretamente List.
  • List<? extends Number>. Cada elemento pode ser lido como Number.
  • List<? super Number>. Ao ler, só podemos assumir Object, e listas de Integer não satisfazem esse limite.

Erros frequentes

Não acrescentes um Double a uma lista cujo tipo concreto pode ser Integer.

Voltar à explicação
Um getter não modificável chega?

Uma equipa guarda diretamente a lista de nomes recebida no construtor. O getter devolve Collections.unmodifiableList(nomes). O chamador conserva a lista original e acrescenta "Rita". A equipa prometia conservar os nomes do momento da construção. Explica a falha e corrige a fronteira de entrada e a de saída. Os nomes são strings.

Pista

Conta quantas referências chegam à lista que a equipa guardou.

Mais uma pista

A proibição do getter só atua sobre alterações feitas através dessa vista.

Solução

O estado da equipa passa a incluir Rita porque o campo e o chamador partilham a lista. No construtor, guarda new ArrayList<String>(nomesRecebidos). No getter, devolve uma vista não modificável dessa lista privada ou uma cópia, conforme o contrato. As strings são imutáveis, por isso partilhá-las não permite alterar os nomes. Se os elementos fossem mutáveis, esta cópia da coleção não bastaria para os tornar independentes.

Confere a tua resposta:

  • Reconhece que a vista acompanha alterações na lista original.
  • Copia a lista recebida no construtor.
  • Não expõe a lista interna para alterações e distingue cópia superficial de cópia dos elementos.

Erros frequentes

final no campo também não impede o chamador de alterar o objeto partilhado.

Voltar à explicação
Copiar entre listas com tipos diferentes

Escreve a assinatura e o corpo de um método genérico copiar(origem, destino) que acrescenta todos os elementos da origem ao fim do destino, sem casts. Ambas as listas são não nulas e diferentes, e o destino permite acrescentar elementos.

O método deve aceitar uma origem List<Integer> com [2, 3] e um destino List<Number> que já contém 1.5. Mostra o conteúdo final e explica por que não deve aceitar List<Double> como destino dessa mesma origem.

Pista
A origem produz valores; o destino tem de aceitar esses valores.
Solução
static <T> void copiar(List<? extends T> origem, List<? super T> destino) {
    for (T valor : origem) destino.add(valor);
}

Com T = Integer, o destino List<Number> aceita os inteiros e fica [1.5, 2, 3]. A origem pode produzir valores lidos como T; o destino pode receber T. Double não é supertipo de Integer, por isso uma lista de Double não pode receber esses objetos. O compilador não converte automaticamente cada Integer para Double. O método declara T; os wildcards descrevem como cada lista o usa.

Confere a tua resposta:

  • Declarei um parâmetro de tipo antes de void.
  • Usei extends na origem e super no destino.
  • Justifiquei a rejeição de List<Double> como destino dos Integer.
Voltar à explicação
Sobrecarga e redefinição na mesma chamada

Considera este excerto. Gato redefine falar() de Animal:

class Animal { String falar() { return "animal"; } }
class Gato extends Animal {
    @Override String falar() { return "gato"; }
}

// Estes dois métodos pertencem à classe Main.
static String visitar(Animal a) { return "geral: " + a.falar(); }
static String visitar(Gato g) { return "gato: " + g.falar(); }

// Estas chamadas pertencem ao método main de Main.
Animal a = new Gato();
System.out.println(visitar(a));

Prevê a linha escrita e explica como Java escolhe cada método.

Pista

As duas escolhas acontecem em momentos diferentes.

Mais uma pista

A variável passada chama-se a e tem tipo Animal. O objeto criado tem classe Gato.

Solução

A linha é geral: gato. A resolução da sobrecarga seleciona visitar(Animal) na compilação, porque a tem tipo declarado Animal. Dentro desse método, a chamada de instância a falar() usa a implementação de Gato, escolhida pelo objeto em tempo de execução. Não houve cast nem mudança de tipo da variável.

Confere a tua resposta:

  • Obtém geral: gato.
  • Escolhe visitar(Animal) pelo tipo declarado da variável.
  • Escolhe Gato.falar pelo objeto em tempo de execução.

Erros frequentes

O tipo concreto do objeto não faz Java reconsiderar automaticamente todas as sobrecargas na execução.

Voltar à explicação
O cálculo cabe em int, o campo é byte

Considera este corpo de main:

byte b = 127;
int soma = b + 1;
b++;
b += 1;
System.out.println(soma + " " + b);

Prevê a saída. Se substituirmos b++ por b = b + 1, o programa compila? E por b = (byte) (b + 1)? Justifica cada caso pelos tipos e pelo intervalo de byte.

Solução

A saída é 128 -127. A soma inicial promove b para int, calcula 128 e não altera b. O incremento calcula o novo valor e converte-o de volta para byte: 127 passa a -128. A atribuição composta seguinte passa a -127.

b = b + 1 não compila porque o resultado é int e a atribuição simples não autoriza essa redução implicitamente. Com o cast explícito, compila e volta a obter -128 nesse passo, terminando também em -127. O cast conserva os oito bits menos significativos; não aumenta a capacidade do byte nem lança uma exceção por este overflow.

Confere a tua resposta:

  • Obtive 128 para soma e -127 para b.
  • Distingui promoção para int de conversão implícita em ++ e +=.
  • Expliquei por que a atribuição simples b = b + 1 não compila e o cast não recupera capacidade.
Voltar à explicação
Um teste de igualdade que só passa com um tipo

O tipo seguinte pretende comparar posições pela coordenada x:

final class Posicao {
    private final int x;
    Posicao(int x) { this.x = x; }
    public boolean equals(Posicao outra) {
        return outra != null && x == outra.x;
    }
    @Override public int hashCode() { return x; }
}

Duas posições a e b são criadas separadamente com x igual a 7. Prevê a.equals(b) e a.equals((Object) b). Diagnostica o erro e escreve a redefinição correta. Que verificação evitaria aceitar a versão original só porque o primeiro resultado é verdadeiro?

Solução

A primeira chamada dá true: a compilação escolhe a sobrecarga equals(Posicao). A segunda dá false: o argumento tem tipo Object, seleciona equals(Object) herdado, que compara identidade; os objetos são distintos. O hash igual não obriga essa igualdade.

A redefinição correta é:

@Override
public boolean equals(Object outra) {
    if (this == outra) return true;
    if (!(outra instanceof Posicao)) return false;
    Posicao p = (Posicao) outra;
    return x == p.x;
}

O tipo é final, a coordenada é imutável e o hash x é compatível com esta igualdade. Testa a operação também com argumento declarado Object, objetos diferentes e null; numa coleção hash, verifica que duas posições de x igual contam uma vez. Colocar @Override na assinatura errada faz o compilador recusá-la antes dos testes.

Confere a tua resposta:

  • Obtive true na primeira chamada e false na segunda.
  • Identifiquei a sobrecarga equals(Posicao), sem redefinir equals(Object).
  • Corrigi a assinatura e indiquei teste através de Object e hash compatível.
Voltar à explicação
Duas posições distintas, uma entrada

Posicao é imutável e redefine corretamente equals e hashCode pelas duas coordenadas inteiras x e y. Um TreeSet<Posicao> recebe um comparator que compara só x. Inserimos, nesta ordem, (2,1), (2,9) e (1,5).

Que resultado dá a segunda chamada a add? Qual é o tamanho e que posição com x igual a 2 fica guardada? Escreve um comparator que preserve todas as posições distintas e as ordene por x e depois y. Por que a.getX() - b.getX() não é uma alternativa segura para todos os inteiros?

Solução

A segunda inserção dá false: comparar (2,1) e (2,9) só por x dá zero. O conjunto guarda a primeira, (2,1), e depois aceita (1,5), ficando com tamanho 2. Não consultou o equals para decidir que (2,9) era distinto.

Comparator<Posicao> ordem = (a, b) -> {
    int porX = Integer.compare(a.getX(), b.getX());
    return porX != 0 ? porX : Integer.compare(a.getY(), b.getY());
};

Com essa ordem, fica (1,5), (2,1), (2,9). A comparação só dá zero quando ambas as coordenadas coincidem, de acordo com equals. Subtrair pode fazer overflow e trocar o sinal: Integer.MIN_VALUE - 1 produz um valor positivo em int, embora o primeiro operando seja menor. Integer.compare determina a ordem sem essa subtração.

Confere a tua resposta:

  • Obtive false na inserção de (2,9) e tamanho 2 depois das três tentativas.
  • Relacionei comparação zero com equivalência no TreeSet.
  • Comparei x e depois y, sem subtração sujeita a overflow, coerente com equals.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.