Java orientado a objetos
Valores e referências, contratos, composição, igualdade, coleções e genéricos.
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.
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);
}
}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.
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());
}
}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.
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());
}
}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:
| Interface | O que representa | Exemplo |
|---|---|---|
List<E> | Sequência com posições e duplicados | Comandos pela ordem em que chegaram. |
Set<E> | Elementos sem duplicados lógicos | Células já visitadas. |
Map<K,V> | Uma associação por chave | Comando associado a cada tecla. |
Queue<E> | Elementos a retirar segundo uma política | Açõ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 para 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:
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);
}
}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 comoNumber. Não podes acrescentar umIntegerarbitrário, porque a lista pode ser deDouble.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.
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);
}
}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
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
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
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.
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?
Pista
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.
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.
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
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
extendsna origem esuperno destino. - Justifiquei a rejeição de
List<Double>como destino dosInteger.
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.
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.
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.
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.