quarta-feira, 25 de janeiro de 2012

O método hashCode()

Vimos o critério de igualdade entre objetos varia de acordo com as necessidades de projeto quando estamos falando de programação. Agora, vamos abordar o método hashCode() para verificar o que ele tem haver com a comparação de objetos, para posteriormente, podermos 'fechar' a relação entre hashCode() e equals() analizando o contrato estabelecido para os dois em java no próximo post. Por hora, tenham em conta que existe um contrato de funcionalidade que amarra esses dois métodos no que diz respeito aos seus retornos. Eles precisam ser consistentes entre si, mesmo que a regra que os defina possa ser qualquer uma.
O código de espalhamento (hashCode), em java, é um número que nos permitirá dividir os objetos de uma classe em 'grupos' segundo um critério que precisa ser claro e consistente. Se pegarmos todos os objetos de uma classe no momento da execução e olharmos para eles sem um critério que os separem entre si, estaremos apenas olhando para um grupo específico de objetos daquela classe existentes naquele momento: o todo. Para termos grupos menores, precisamos estabelecer um critério que coloque cada objeto num subgrupo. Inicialmente, podemos perguntar porque estamos preocupados com isso, mas a resposta vem rapidamente quando começamos a trabalhar na área: em vários momentos, o desempenho da aplicação vai depender fortemente disso. De forma resumida (pois iremos ver isso com mais calma depois), precisamos agrupar objetos em grupos menores que o todo quando estamos trabalhando com coleções grandes desses objetos.

Como exemplo, vamos pensar numa versão de hashCode() para a nossa classe Algo:

@Override
public int hashCode() {
    // Se o nome, apesar de não nulo, for vazio, o 'grupo' será zero.
    if ("".equals(nome)) {
        return 0;
    }
    /*
     * ATENÇÂO: Todo caractere é um número inteiro. A tabela de caracteres
     * (charset) que estiver em uso no momento é que vai mapear cada caractere
     * com o seu valor. No entanto, durante a execução, essa tabela não vai
     * mudar. Então, posso confiar que para um determinado caractere, o
     * inteiro correspondente será sempre o mesmo.
     * 
     * Retorno o inteiro correspondente ao meu primeiro caractere, qualquer
     * que seja ele.
     */
    return Character.getNumericValue(nome.toCharArray()[0]);
}

Obs.: Todo caractere internamente é representado por um inteiro. Teste o seguinte: System.out.println('a' + 'b');. No meu caso aqui, com o charset que estou usando, a saída no console foi 195. Observe que 'somei' dois caracteres e não duas strings de um caractere cada. Sabemos que estamos falando de caracteres pelas aspas simples. Se executasse a instrução com strings, a saída seria 'ab'. Isso acontece porque em java o operador +, somente no caso das strings, é sobrecarregado nativamente na linguagem, e assim, a 'soma de strings' se converte em concatenação.

O método hashCode() devolve um inteiro, cujo sentido tentaremos compreender. Também é definido em Object, e portanto, lá ele tem uma implementação válida tanto do ponto de vista sintático (devolve um inteiro), quanto do ponto de vista semântico (respeita o contrato de hashCode() e equals()). Novamente, se ele não for sobrescrito incorretamente por uma superclasse que não Object, estará validamente disponível em qualquer classe java.

O inteiro devolvido pelo hashCode() indica o 'grupo' daquele objeto. Assim, objetos diferentes podem ter o mesmo hashCode. No nosso exemplo, eu tenho uma propriedade 'nome' do tipo String para basear a forma como vou separar meus objetos em grupos. O hashCode() sempre fará uso de alguma propriedade do seu objeto. No nosso caso, usaremos a propriedade 'nome' do Algo. É importante dizer neste momento que o que você venha a fazer com essa String para obter o seu hashCode pode ser praticamente qualquer coisa, desde que você devolva um inteiro e respeite o contrato de hashCode() e equals(). No nosso caso agora, escolhi devolver o inteiro que corresponde ao primeiro caractere do nome do meu Algo. Isso vai separar os meus Algos 'alfabeticamente', informando que todos os Algos que têm nome começando com 'a' vão ser do mesmo 'grupo'. Não é exatamente alfabético porque o hashCode é numérico e a definição da minha classe permite um nome começando com outros caracteres como numeros ou caracteres especiais etc. Mas isso não quebra o contrato, uma vez que esses objetos serão agrupados entre si.

A funcionalidade provida pelo hashCode() tem sido referida neste post como 'agrupar' apenas na tentativa de facilitar a compreensão da funcionalidade esperada desse método. No entanto, o propósito dele é o de espalhar o grande grupo de todos os objetos de uma classe em grupos menores. Além disso, encontraremos no uso do hashCode(), a necessidade de produzir um espalhamento sempre o mais eficiente possível para podermos tirar vantagem das coleções que fazem o uso de hashing, isto é, espalhamento.

A implementação acima do hashCode() é ineficiente para uso em uma aplicação real. Estou esperando que ela seja eficiente no intuito de esclarecer o funcionamento esperado do método. No entanto, apesar de ineficiente, ela é válida. Quando chegarmos ao contrato, tentaremos diferenciar o que é válido do que é eficiente. Mas porque ela é ineficiente? Quando temos coleções gigantes de objetos, da ordem de centenas de milhares, torna-se muito caro, em termos de processamento, encontrar um objeto específico dentro de um conjunto como esse por meios normais, isto é, iterando a coleção inteira. Com um hashCode que separa centenas de milhares de objetos em 26 grupos, imaginando nomes que começam apenas com letras (nosso exemplo), a iteração no subgrupo continua cara, pois o subgrupo continua com muitos milhares de objetos. Quando pensamos em muitos usuários procurando objetos em coleções grandes dentro de um servidor o problema se torna bastante real.

Neste momento encontramos um limite na explicação do hashCode(). Completaremos a explicação em duas partes: No próximo post, quando veremos o contrato que une hashCode() e equals() e quando abordarmos as coleções e mapas do tipo hash. Antes das coleções, no entanto, passaremos pelas interfaces Comparable e Comparator, que junto com hashCode() e equals() formam o quarteto necessário para a correta utilização dos vários tipos de coleções e mapas da linguagem java.

sábado, 14 de janeiro de 2012

Métodos hashCode() e equals() - Parte 1

Quem são os seus objetos?

Uma das features básicas existentes no java que costuma confundir as pessoas no início, é o par de métodos utilizados para 'identificar' objetos: hashCode() e equals().
Para falar desses dois, primeiro precisamos falar das referências, pois existe um critério de igualdade que precisaremos abordar. Na programação, o critério de igualdade não é rigidamente definido. Em outras palavras, a maneira de verificar se um objeto de uma classe A é igual a outro, muitas vezes é diferente da maneira como determinamos se um objeto da classe B é igual a outro. No geral, para primitivos a igualdade é mais clara. É razoável imaginarmos que 10 == 10 retorne true. No entanto, pode ser surpreendente para o iniciante que:

String a = "TESTE";
String b = new String("TESTE");

System.out.println("TESTE" == a); // imprime true.
System.out.println("TESTE" == b); // imprime false.
System.out.println(a == b); // imprime false.
No entanto, todas as chamadas abaixo imprimem true:
System.out.println("TESTE".equals(a));
System.out.println("TESTE".equals(b));
        
System.out.println(a.equals(b));
System.out.println(a.equals("TESTE"));
        
System.out.println(b.equals(a));
System.out.println(b.equals("TESTE"));
Obs.: “TESTE” é um literal de String. Isso quer dizer que esse cara é um objeto do tipo String como qualquer outro mas a sua instanciação é feita pelo compilador, pois é como se ele fosse um objeto que existe no fonte, o que obviamente não é verdade. Se você não entende muito bem isso, não se preocupe. Falarei disso futuramente. Por hora, basta compreender que, como objeto, é tão válida a chamada "TESTE".equals(a) quanto a.equals("TESTE").

Sem entrar no modelo de memória java para strings, que tem as suas peculiaridades (a exploração desse tema fica para um outro post), no início pode parecer intimidador que objetos possam ter um comportamento que parece inconsistente. No entanto, os resultados acima estão corretos e espera-se da JVM que apresente esse comportamento e não outro.

Operador ==

Ao compararmos os objetos String acima, utilizamos duas maneiras: Através do operador == e através do método equals(). O operador == compara o número da referência dos objetos (por isso falei de referências). Sendo uma comparação de números, temos aqui precisão aritmética. Mas, o mais importante a guardarmos é que, ao fazer uma comparação com o operador == estamos perguntando à JVM: essas duas variáveis apontam para o mesmo objeto na memória?

Método equals()

O método equals() por sua vez, pode implementar qualquer critério de igualdade inclusive o mesmo que o operador ==. Através dele, não ficamos mais presos a uma comparação que nos diga se estamos lidando com o mesmo objeto na memória. Isso é importante para muitas situações práticas. Consideremos uma classe hipotética:

/**
 * Essa classe descreve um Algo que tem 
 * um nome não nulo que não pode mudar, 
 * e que tem obrigatoriamente que ser 
 * fornecido quando o objeto for construído.
 */
public class Algo extends Object {
    
    private final String nome;
    
    /**
     * Se o nome fornecido for nulo, não posso ser construído. 
     * 
     * @throws IllegalArgumentException 
     */
    public Algo(String nome){
        
        if(nome == null){ 
            throw new IllegalArgumentException(
                    "Algos têm que ter um nome não nulo.");
        }
        this.nome = nome;
    }
    
    public String getNome(){
        
        return this.nome;
        
    }
}

Ocorre que, no meu sistema hipotético eu guardo os Algos no banco de dados para serem usados depois. Assim, quando o usuário preenche o formulário com o nome do Algo, eu tenho que ser capaz de verificar se já existe um Algo igual na base de dados. Um Algo será criado a partir do formulário e outro Algo virá do banco de dados. Nesse caso, estamos claramente falando de objetos diferentes, em endereços diferentes na memória. O operador == vai retornar false se eu comparar esses dois objetos. Nesse momento eu preciso de um critério diferente para determinar a igualdade. Nesse caso, o que parece mais razoável é que devo considerar iguais, Algos que tenha o mesmo nome, ao invés de verificar se estamos falando do mesmo Algo na memória. Abaixo, uma versão do Algo com um equals() gerado pelo eclipse. Reparem que uma simples comparação de uma única String em um objeto pode ser mais sutil do que se imagina no início:

/**
 * Essa classe descreve um Algo que tem 
 * um nome não nulo que não pode mudar, 
 * e que tem obrigatoriamente que ser 
 * fornecido quando o objeto for construído.
 * 
 *  Objetos dessa classe serão considerados 
 *  iguais se tiverem o mesmo nome.
 */
public class Algo extends Object {
    
    private final String nome;
    
    /**
     * Se o nome fornecido for nulo, não posso ser construído. 
     * 
     * @throws IllegalArgumentException 
     */
    public Algo(String nome){
        
        if(nome == null){ 
            throw new IllegalArgumentException(
                    "Algos têm que ter um nome não nulo.");
        }
        this.nome = nome;
    }
    
    public String getNome(){
        
        return this.nome;
        
    }

    @Override
    public boolean equals(Object obj) {
        
        // Se for o mesmo objeto, evidentemente terá o mesmo nome.
        if (this == obj){
            return true;
        }
        
        // Se o outro for nulo, evidentemente é diferente de mim.
        if (obj == null){ 
            return false;
        }
        
        // ATENÇÂO: Neste caso, só posso ser 
        // comparado com outros Algos. Isso nem sempre 
        // será verdade. Ex.: Situações onde é razoável 
        // comparar objetos de subclasses.
        if (getClass() != obj.getClass()) {
            return false;
        }
        
        // Aqui já sei que o outro não é nulo nem de 
        // outra classe. Posso fazer o cast sem problemas.
        Algo other = (Algo) obj; 
        
        // Aqui, finalmente verificarei se o outro tem o 
        // mesmo nome que eu. Observe que não
        // verifico se o nome dele está no mesmo lugar 
        // da memória que o meu (==), mas se 
        // o nome dele é formado pelos mesmos 
        // caracteres que o meu.
        // ATENÇÂO: Teste de nulidade sobre os nomes 
        // removido, porque sei que o meu construtor
        // só permite nomes não nulos.
        if (!this.nome.equals(other.nome)){
            return false;
        }
        return true;
    } 

}


A partir dessa implementação, podemos fazer comparações entre Algos vindos de lugares diferentes pois poderemos perguntar:

if (algo1 != null) {
    boolean b = algo1.equals(algo2);
}


Obs.: Alguem poderia perguntar: não posso pegar a String vinda do usuário e fazer algo como stringDoUsuario.equals(algo.getNome())? Sintaticamente pode, mas o nome disso é gambiarra. Algo não é String, nem caso específico de String, nem tem nenhum relacionamento semântico que faça essa comparação ter sentido. Portanto, evite ao máximo recorrer a esse tipo de solução 'fácil'. Essas coisas costumam voltar como assombrações no futuro ;).

Agora cabe explicar de onde vem esse tal de equals(). Esse método é declarado na classe Object, que é a superclasse implícita de qualquer classe java. Se você não colocar 'extends Object' na declaração da sua classe o compilador vai colocar para você, a menos que a sua classe estenda outra. Se você colocar 'extends Object' na sua classe que a princípio não estendia nenhuma, vai compilar sem problemas. Teste. A moral da história é que todos dos objetos em java têm, inicialmente, implementações válidas de equals() e hashCode() (além das de alguns outros), que são as que vêm de Object. Você só pode herdar uma implementação inválida de um desses métodos se uma superclasse que não Object sobrescrevê-los de forma inválida. A declaração de equals() em Object é a seguinte:

public boolean equals(Object obj) {
    return (this == obj);
}


Reparem que em Object, equals() faz uma comparação plana das referências dos objetos envolvidos. Então, a princípio, no mínimo podemos verificar se duas variáveis apontam para o mesmo objeto em qualquer classe java.
Utilizei o método equals() em strings acima. String sobrescreve equals() para comparar se os dois objetos, além de serem strings, são formados pela mesma cadeia de caracteres. O importante a perceber aqui é que comparar a cadeia de caracteres de duas strings para saber se são iguais faz sentido para se determinar se duas strings são iguais. Para tipos de objetos diferentes, podemos vir a comparar números, combinados ou não com strings, ou mesmo outros objetos que sejam atributos daquele que estamos comparando, sabendo que esses atributos terão seus próprios critérios de igualdade. Em suma, o critério de igualdade pode ser qualquer, e fundamentalmente só pode ser determinado pelas necessidades de projeto da sua classe.
Assim, uma implementação adequada de equals(), além de lidar corretamente com os atributos envolvidos, verificando nulidade, classe do objeto em questão etc., deve, em primeiro lugar, definir um critério de igualdade que faça sentido do ponto de vista semântico, para distinguir objetos considerados iguais dentro das necessidades do projeto, daqueles que são diferentes entre si.

quinta-feira, 5 de janeiro de 2012

Programação leve através de fundamentos sólidos

Estou começando hoje com este blog. Há algum tempo já pensava em fazer alguma coisa nesse sentido, mas, como todos que trabalham com tecnologia sabem, o tempo costuma ser escasso. Porém, se a gente fica adiando, a coisa acaba não saindo nunca. Portanto, vamos lá.

Como trabalho com java, os posts vão acabar sendo mais relacionados a isso, mas vou fazendo um esforço para abordar as tecnologias de forma mais abrangente e sem sectarismo.

Eu tenho algumas propostas com relação ao blog em si: Quando estava começando a estudar programação, tive dificuldade para achar materiais mais básicos que não fossem 'receitas de bolo', independente de serem configurações de ide, servidores ou o que seja. Essas contribuições dos colegas são importantes porque nos jogam diretamente na prática, pois a gente vai executando os passos até as coisas funcionarem. O problema é que nesse momento, estamos fazendo as coisas sem saber porque. Isso muitas vezes levanta dúvidas na cabeça dos iniciantes, pois, geralmente, os passos se referem a várias ferramentas e tecnologias diferentes para se realizar uma única coisa. Se você está querendo levantar um servidor, por exemplo, pode acabar tendo que criar um XML (ou pelo menos alterar um). Se você está querendo fazer uma conexão com um banco de dados, pode ter que setar a sua configuração via JDBC num trecho de código através da API java. Para quem está querendo começar, pode ser bem irritante, e não é incomum surgirem perguntas como:

  1. Porque tenho que por essas informações num XML, que é feio que dói de olhar, se poderia colocar isso num arquivo mais simples? (um properties talvez...)
  2. Esse trechinho de código tem uns nomes meio indecifráveis (no momento) e cada linha parece ter um formato contraditório com relação às outras. O que é Class.forName() ?
  3. Porque eu não posso criar uma conexão através de um 'new Connection(“”, ””, “”)'?

No meu começo, não achei respostas simples para perguntas desse tipo. Talvez a minha pesquisa tenha falhado, ou talvez as documentações oficiais tenham realmente um formato meio críptico, uma maneira meio formal demais de descrever as ferramentas. No caso dos fórums, mesmo nos tópicos básicos, as respostas são de pessoas que já atuam, para pessoas que já atuam. Seria mais fácil se tivesse achado coisas como:

  1. Claro que um properties é mais simples. Mas se você precisar hierarquizar o conteúdo dessa configuração, vai enroscar.
  2. Class.forName() é um método da api de reflection. Todas as suas classes são carregadas pela VM em algum momento. O que esse cara faz, é carregar o driver de conexão, que em java é uma classe, para a que ele esteja disponível quando você pedir uma conexão através do DriverManager.getConnection();
  3. Até pode. Mais vai dar bastante trabalho. O caso é que Connection é uma interface e você pode implementar sem problemas. Mas como você vai precisar conhecer mais uma pancada de coisas, por enquanto, é melhor usar a implementação que vem no jar do conector que você baixou. O que está acontecendo é que o DriverManager vai catar lá na VM o driver que você carregou com o Class.forName(), e ele sabe como te devolver a conexão prontinha.

Sei que esses exemplos são de pouco interesse mas o propósito era apenas dar um ponto de vista. Não estou dizendo com isso que falta boa vontade para tentar esclarecer dúvidas dos outros, mas apenas que acredito que posso contribuir para fechar um buraco nesse esforço.

Para além disso, no final essa coisa evoluiu e a minha conclusão hoje é que retornar aos detalhes das coisas mais básicas, e que a gente usa sem nem pensar mais, acaba sendo muito útil. Isso porque são os fundamentos, pelo menos para mim, que esclarecem as coisas complexas que virão pela frente.

Assim, estou na proposta que colocar aqui um conteúdo que ataque essa lacuna que eu senti (como outros talvez), mas também na ambição de colocar coisas que sejam úteis para os desenvolvedores não tão iniciantes (como eu). A meta é um dia surpreender os avançados.