12. O pacote java.lang

Já usamos, por diversas vezes, as classes String e System. Vimos o sistema de pacotes do Java e nunca precisamos dar um import nessas classes. Isso ocorre porque elas estão dentro do pacote java.lang, que é automaticamente importado para você. É o único pacote com esta característica.

Vamos ver um pouco de suas principais classes.

Um pouco sobre a classe System

A classe System possui uma série de atributos e métodos estáticos. Já usamos o atributo System.out, para imprimir.

Olhando a documentação, você vai perceber que o atributo out é do tipo PrintStream do pacote java.io. Veremos sobre essa classe mais adiante. Já podemos perceber que poderíamos quebrar o System.out.println em duas linhas:

O System conta também com um método que simplesmente desliga a virtual machine, retornando um código de erro para o sistema operacional, é o exit.

Veremos também um pouco mais sobre a classe System nos próximos capítulos e no apêndice de Threads. Consulte a documentação do Java e veja outros métodos úteis da System.

java.lang.Object

Todo método que precisamos receber algum parâmetro temos que declarar o tipo do mesmo. Por exemplo, no nosso método saca precisamos passar como parâmetro um valor do tipo double. Se tentarmos passar qualquer coisa diferente disso teremos um erro de compilação.

Agora vamos observar o seguinte método do próprio Java:

Neste caso, o método println está recebendo uma String e poderíamos pensar que o tipo de parâmetro que ele recebe é String. Mas ao mesmo tempo podemos passar para esse método coisas completamente diferentes como int, Conta, Funcionario, SeguroDeVida, etc. Como esse método consegue receber tantos parâmetros de tipos diferentes?

Uma possibilidade seria o uso da sobrecarga, declarando um println para cada tipo de objeto diferente. Mas claramente não é isso que acontece já que conseguimos criar uma classe qualquer e invocar o método println passando essa nova classe como parâmetro e ele funcionaria!

Para entender o que está acontecendo, vamos considerar um método que recebe uma Conta:

Esse método pode ser invocado passando como parâmetro qualquer tipo de conta que temos no nosso sistema: ContaCorrente e ContaPoupanca pois ambas são filhas de Conta. Se quiséssemos que o nosso método conseguisse receber qualquer tipo de objeto teríamos que ter uma classe que fosse mãe de todos esses objetos. É para isso que existe a classe Object!

Sempre quando declaramos uma classe, essa classe é obrigada a herdar de outra. Isto é, para toda classe que declararmos, existe uma superclasse. Porém, criamos diversas classes sem herdar de ninguém:

Quando o Java não encontra a palavra chave extends, ele considera que você está herdando da classe Object, que também se encontra dentro do pacote java.lang. Você até mesmo pode escrever essa herança, que é o mesmo:

Todas as classes, sem exceção, herdam de Object, seja direta ou indiretamente, pois ela é a mãe, vó, bisavó, etc de qualquer classe.

Podemos também afirmar que qualquer objeto em Java é um Object, podendo ser referenciado como tal. Então, qualquer objeto possui todos os métodos declarados na classe Object e veremos alguns deles logo após o casting.

Métodos do java.lang.Object: equals e toString

toString

A habilidade de poder se referir a qualquer objeto como Object nos traz muitas vantagens. Podemos criar um método que recebe um Object como argumento, isto é, qualquer objeto! Por exemplo, o método println poderia ser implementado da seguinte maneira:

Dessa forma, qualquer objeto que passarmos como parâmetro poderá ser impresso no console desde que ele possua o método toString. Para garantir que todos os objetos do Java possuam esse método, ele foi implementado na classe

Object

Por padrão, o método toString do Object retorna o nome da classe @ um número de identidade:

Mas e se quisermos imprimir algo diferente? Na nossa tela de detalhes de conta, temos uma caixa de seleção onde nossas contas estão sendo apresentadas com o valor do padrão do toString. Sempre que queremos modificar o comportamento de um método em relação a implementação herdada da superclasse, podemos sobrescrevê-lo na classe filha:

Agora podemos usar esse método assim:

E o melhor, se for apenas para jogar na tela, você nem precisa chamar o toString! Ele já é chamado para você:

Gera o mesmo resultado!

Você ainda pode concatenar Strings em Java com o operador +. Se o Java encontra um objeto no meio da concatenação, ele também chama o toString dele.

equals

Até agora estamos ignorando o fato que podemos mais de uma conta de mesmo número e agência no nosso sistema. Atualmente, quando inserimos uma nova conta, o sistema verifica se a conta inserida é igual a alguma outra conta já cadastrada. Mas qual critério de igualdade é utilizado por padrão para fazer essa verificação?

Assim como no caso do toString, todos objetos do Java possuem um outro método chamado equals que é utilizado para comparar objetos daquele tipo. Por padrão, esse método apenas compara as referências dos objetos. Como toda vez que inserimos uma nova conta no sistema estamos fazendo new em algum tipo de conta, as referências nunca vão ser iguais, mesmo os dados (número e agência) sendo iguais.

Mas, e se fosse preciso comparar os atributos? Quais atributos ele deveria comparar? O Java por si só não faz isso, mas podemos sobrescrever o equals da classe Object para criarmos esse critério de comparação.

O equals recebe um Object como argumento e deve verificar se ele mesmo é igual ao Object recebido para retornar um boolean. Se você não reescrever esse método, o comportamento herdado é fazer um == com o objeto recebido como argumento.

Casting de referências

No momento que recebemos uma referência para um Object, como vamos acessar os métodos e atributos desse objeto que imaginamos ser uma Conta? Se estamos referenciando-o como Object, não podemos acessá-lo como sendo Conta. O código acima não compila!

Poderíamos então atribuir essa referência de Object para Conta para depois acessar os atributos necessários? Tentemos:

Nós temos certeza de que esse Object se refere a uma Conta, já que a nossa lista só imprime contas. Mas o compilador Java não tem garantias sobre isso! Essa linha acima não compila, pois nem todo Object é uma Conta.

Para realizar essa atribuição, para isso devemos “avisar” o compilador Java que realmente queremos fazer isso, sabendo do risco que corremos. Fazemos o casting de referências, parecido com de tipos primitivos:

O código passa a compilar, mas será que roda? Esse código roda sem nenhum problema, pois em tempo de execução a JVM verificará se essa referência realmente é para um objeto de tipo Conta, e está! Se não estivesse, uma exceção do tipo ClassCastException seria lançada.

Com isso, nosso método equals ficaria assim:

Você poderia criar um método com outro nome em vez de reescrever equals que recebe Object, mas ele é importante pois muitas bibliotecas o chamam através do polimorfismo, como veremos no capítulo do java.util.

O método hashCode() anda de mãos dadas com o método equals() e é de fundamental entendimento no caso de você utilizar suas classes com estruturas de dados que usam tabelas de espalhamento. Também falaremos dele no capítulo de java.util.

Regras para a reescrita do método equals

Pelo contrato definido pela classe Object devemos retornar false também no caso do objeto passado não ser de tipo compatível com a sua classe. Então antes de fazer o casting devemos verificar isso, e para tal usamos a palavra chave instanceof, ou teríamos uma exception sendo lançada.

Além disso, podemos resumir nosso equals de tal forma a não usar um if:

  • 13. Um pouco de arrays