Caelum | Ensino e Inovação - Cursos de Java, Scrum, Ruby on Rails


Ordenando coleções com Comparable e Comparator

Por Nico Steppat em 22/06/10

Uma tarefa comum no dia-a-dia dos desenvolvedores é ordenar uma lista ou array. Para não inventar a roda, a Collections API do Java (também conhecida pelo nome do seu pacote, o java.util) vem pronta para ajudar nessa tarefa. Falamos dessa API extensivamente na apostila do curso FJ-11, e vou aqui passar para o problema específico de ordenação.

Imagine que você gostaria de ordenar uma lista de Contas bancárias. Cada conta possui um número (int) e um titular (String):

Conta conta1 = new Conta(5452, "Phillip Lahm");
Conta conta2 = new Conta(1234, "Lucas Podolski");
Conta conta3 = new Conta(3145, "Arne Friedrich");

List<Conta> lista = new ArrayList<Conta>();
lista.add(conta1);
lista.add(conta2);
lista.add(conta3);

O método para ordenar uma lista se encontra na classe java.util.Collections (repare o “s” no final). Ela possui métodos estáticos que ajudam a manipular coleções, entre eles o método sort. Assim podemos tentar ordenar a lista de contas:

Collections.sort(lista);

Mas infelizmente a linha acima nem compila. Antes de invocar o método sort é preciso definir o critério de ordenação: uma forma de informar, dado duas contas, qual vem “antes” e qual vem “depois. Considerando que queremos ordenar pelo número da conta, a classe Conta implementar a interface java.lang.Comparable que define o que será nossa “ordem natural”. A interface possui apenas um método compareTo:

public interface Comparable<T> {
    int compareTo(T outro);
}

As contas então devem ser comparáveis. Vamos definir a ordem natural baseada no número da conta:

public Conta implements Comparable<Conta> {

    private int numero;
    private String titular;
    // outros metodos e atributos

    public int compareTo(Conta outraConta) {
        if (this.numero < outraConta.numero) {
            return -1;
        }
        if (this.numero > outraConta.numero) {
            return 1;
        }
        return 0;
    }
}

Se o número da conta atual é menor do que da outraConta retormamos -1 (ou qualquer int negativo, indicando que this deve vir “antes” de outraConta), se for maior retornamos 1 (ou qualquer int positivo) e se for igual então devolvemos 0.

Agora podemos invocar Collections.sort(lista).

Mas se surgir a necessidade de ordenar pelo titular da conta? Não queremos alterar o método compareTo na classe Conta, já que isso mudaria a ordem natural. Queremos definir um outro critério de ordenação. Para tal, existe uma outra interface: a Comparator:

public interface Comparator<T> {
    int compare(T o1, T o2);
}

Vamos então implementar a interface para definir a ordem pelo titular (String) da conta. Comparar duas Strings é difícil, mas, como você pode imaginar, esse problema já foi resolvido na API do Java. A classe String já sabe compara dois strings, sabemos isso pois ela implementa a interface Comparable (por esse mesmo motivo podemos invocar Collections.sort para uma List de String). Podemos então delegar essa tarefa ao método compareTo das Strings:

public class TitularComparator implements Comparator<Conta> {
    public int compare(Conta conta, Conta outraConta) {
        return conta.getTitular().
                compareTo(outraConta.getTitular());
    }
}

Como escolher para que esse critério de comparação seja utilizado em vez da ordem natural? O método sort é sobrecarregado e pode receber um objeto do tipo Comparator:

TitularComparator comparator = new TitularComparator();
Collections.sort(lista, comparator);

Falta mencionar que o método compareTo da interface Comparable deve ser consistente com o método equals. Quando uma conta é igual a outra (a classe Conta sobreescreve o método equals para definir igualdade), o método compareTo deve devolver zero também. Devemos também pensar se receberemos null como Contas para ordenar, e tomar as devidas precauções nos comparadores.

Através de Comparable e Comparator também são controladas a ordenação da coleção TreeSet e as chaves do mapa TreeMap.

Esse tópico também faz parte da prova de certificação de programador, a SCJP.

  • Share/Bookmark

Guardando senhas criptografadas em Java

Por Ricardo Nakamura em 17/06/10

Eu e o Thiago Ferreira estavámos mais uma vez na Caelum passando pela situação de gravar as senhas do usuário no banco de dados. Ainda hoje alguns grandes sites cometem o grave erro de guardar as senhas dos usuários em texto puro, fazendo com que um possível roubo de dados acarrete num problema ainda maior.

O processo clássico é guardar um hash (chamado também de digest nesse caso) da senha do usuário, usando algum algoritmo de hash unidirecional. Isso pode ser feito utilizando uma chamada de função na query do banco de dados (como MD5()no MySQL), ou, o mais utilizado para não ter de trafegar a senha entre o servidor web e o banco de dados: com o MessageDigest do javax.security. Através dessa classe você pode facilmente gerar o hash de uma senha:

MessageDigest algorithm = MessageDigest.getInstance("MD5");
byte messageDigest[] = algorithm.digest("senha".getBytes("UTF-8"));

Agora temos um array de bytes que podemos guardar no banco de dados. Quando o usuário logar, basta digerirmos novamente a senha colocada no formulário web, e comparar o hash resultante com o que há no banco. Você poderia guardar esse array de byte como uma String fazendo new String(bytes, encoding), porém muito mais usual é guardar os hexadecimais desses bytes dentro de uma String:

StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
	hexString.append(String.format("%02X", 0xFF & b));
}
String senha = hexString.toString();

Isso gerará e8d95a51f3af4a3b134bf6bb68a213a.

Apesar de muito usado, o MD5 já é considerado um algoritmo de hash quebrado, pois hoje em dia podemos rapidamente, através de força bruta, descobrir uma String que gere o mesmo hash, já que neste algoritmo ocorrem mais colisões do que o que foi inicialmente imaginado por seus criadores. Muitos passaram a usar o SHA-1, porém este já da sinais de que, num futuro próximo, será quebrado com força bruta. O próprio governo americano já evita utilizar a família SHA-1, priorizando o uso do SHA-2. Logo, o procedimento completo mais adequado atualmente seria usar algo como:

			MessageDigest algorithm = MessageDigest.getInstance("SHA-256");
			byte messageDigest[] = algorithm.digest(original.getBytes("UTF-8"));

			StringBuilder hexString = new StringBuilder();
			for (byte b : messageDigest) {
				hexString.append(String.format("%02X", 0xFF & b));
			}
			String senha = hexString.toString();

Para tornar seu sistema ainda mais seguro em relação as senhas guardadas, pode-se ainda adicionar salts, mais iterações de hash e outras técnicas que tornaria ainda mais difícil um possível ataque de força bruta contra uma base de dados roubada, se protegendo também contra o provável uso de senhas fracas por parte de seus usuários.

  • Share/Bookmark

Revisitando a concatenação de Strings: StringBuilder e StringBuffer

Por Paulo Silveira em 13/06/10

Uma discussão muito antiga que frequentemente aparece no Java é o uso errado da concatenação de Strings, que pode acarretar numa grave perda de performance e trashing de memória. Mas por que?

O problema é muito simples de enxergar. Imagine um laço em que você concatena uma String com todos os números de 0 a 30 mil:

                String numeros = "";
                for (int i = 0; i<30000; i++) {
                        numeros += i;
                }
                System.out.println(numeros.length());

Em um computador bom, isso vai levar vários segundos. Agora vamos verificar o mesmo código usando um StringBuider:

                StringBuilder numeros = new StringBuilder();
                for (int i = 0; i<30000; i++) {
                        numeros.append(i);
                }
                System.out.println(numeros.toString().length());

Rodando esse segundo código, o tempo gasto é irrisório, mal sendo percepitível. Alguns chegam até a dizer que não devemos utilizar o operador +, nem mesmo em operações simples como essas:

String hql = "select u from";
hql += " User as u";
System.out.println(hql);

Porém este é o caso que o uso do operador + é mais que bem vindo. No fundo, este operador não existe para a JVM, é apenas um syntactic sugar na linguagem e único operator overload do Java. O próprio compilador trata este operador, como podemos verificar no bytecode desse código através do javap -c, resultando no trecho de mneumônicos como este. Logo, o que está acontecendo na verdade é um código como:

System.out.println(new StringBuilder())
     .append("select u from").append(" User as u:").toString());

Em outras palavras, o operador + sempre usa o StringBuilder, o que torna desnecessário evitar o uso do operador + neste caso. Se ele já usa o StringBuilder, por que o código que vimos primeiramente roda tão mais lento com o operador em relação ao StringBuilder puro? Voltando ao primeiro código, ele gera este bytecode, que podemos facilmente perceber que há uma instanciação de um novo StringBuilder a cada iteração do laço, laço o qual está definido entre as instruções 5 e 34 (o goto). Em Java teríamos:

                String numeros = "";
                for (int i = 0; i<30000; i++) {
                        numeros = new StringBuilder()
                             .append(numeros).append(i).toString();
                }
                System.out.println(numeros.length());

Repare que, com um novo StringBuilder sendo instanciado a cada iteração, a String numeros está sendo copiada inteiramente (append(numeros)) toda vez para esse novo objeto, gastando bastante tempo (no final, é um tempo quadrático em relação ao tamanho da String). O nosso segundo código já apresentado é bem mais eficiente: ele cria um StringBuilder uma única vez, fora do laço, e depois invoca o append apenas para as novas partes da String, sem ter de copiar o que já foi previamente processado (resultando em tempo linear).

Por último temos o StringBuffer: é a versão antiga e thread safe do StringBuilder, que era usado antigamente para realizar as operações do +. Como ele usa sincronização, custa um pouco mais caro para executar seus métodos, e foi preterido por essa ser uma situação thread safe.

  • Share/Bookmark

Últimos aprendizados e inovações na Caelum

Por Anderson Leite em 27/05/10

Inovação é parte fundamental da missão da Caelum. E, como uma empresa de treinamentos, Ensinar e Aprender são nosso dia a dia. Muitos nos perguntam o que devem estudar no seu tempo livre, onde devem investir. A Caelum tem seguido várias linhas e iniciativas que eu gostaria de apresentar nesse post.

De tempos em tempos, temos na Caelum um tech day interno onde cada um apresenta o que tem estudado e aplicado nos projetos e nas aulas. O último encontro aconteceu nesse mês de maio e trouxe muitas novidades com testes, métodos ágeis, web, linguagens dinâmicas, estruturas de dados, cloud e mais.

Guilherme Silveira falou sobre SOA versus REST, recapitulando REST e fazendo comparações com o objetivo de ajudar nas escolhas de quando usar ou não as tecnologias. Mostrou um pouco de seus últimos experimentos com hypermedia e code on demand.

O Lucas Cavalcanti e o Caires Vinicius apresentaram a palestra Shoulda stay or shoulda go?, mostrando pontos positivos e negativos da utilização do Shoulda em um projeto Ruby on Rails que passaram enquanto estavam em um projeto de consultoria da Caelum. Ainda em Rails, David Paniz e Pedro Matiello apresentaram Aerotrem: Colocando sua app Rails no ar. Eles mostraram como manter, de maneira fácil, várias VMs ruby na mesma máquina. Foram além e fizeram o HAProxy balanceando dois servidores Web, além de mostrar como fazer o deploy sua aplicação no cloud da Amazon via o Heroku.

O Paulo Silveira apresentou Tudo que você sempre precisou saber sobre Hash e um pouco mais. Paulo mostrou como funciona um Hash, mostrou algumas implementações em Java e como funções de hash ruins podem impactar sua tabela além de um truque que o Yahoo! usou para melhorar seu filtro de Spam.

Sérgio Lopes e Alberto Souza apresentaram técnicas para deixar mais rápido o carregamento das páginas Web, seguindo as diretrizes do YSlow do Yahoo!. Mostraram algumas das métricas usadas e truques para atingi-las com ferramentas Java e usando recursos do Google AppEngine. Ainda com relação à Web client-side, eu – Anderson Leite -, Pedro Mariano e Caires Vinicius falamos sobre as novidades do HTML 5. Alguns browsers já possuem implementações do draft atual e muitas mudanças estão sendo consideradas para essa nova versão. Entre ela as tags audio e video, a nova API Geolocation, novas tags semânticas, novos input types, cache e web storage, e a tag canvas para desenho 2d. Veja um demo de canvas e a nova JS API:

A Cecilia Fernandes apresentou Do Scrum ao Lean, com os próximos passos a serem tomados para uma equipe ir do Scrum ao Lean, vantagens, desvantagens e um exercício prático para enxergar a diferença entre a produção empurrada e puxada, alem da variação sem especialização do conhecimento.

Além de todas essas palestras no nosso Tech Day, muitos outros temas têm feito parte do dia a dia de estudo e inovação da Caelum. Estamos apostando fortemente no Android para o mercado Java Mobile, nas novidades do Java EE 6 para simplificar o Java corporativo (como JPA2 e JSF2) e em novas iniciativas em cloud computing e NoSQL.

E você? O que tem estudado ultimamente? Quais são suas apostas?

  • Share/Bookmark

Logar é preciso, debugar não é preciso?

Por Paulo Silveira em

Muitas vezes você percebe que está debugando o mesmo trecho de código incessantemente: horas são gastas tentando procurar o erro, checando variáveis, adicionando e removendo break points e até adicionando complexos alarmes condicionais. Ferramentas como o Netbeans e o Eclipse facilitam muito esse trabalho, mesmo quando ele é feito remotamente, o que é ainda mais complexo.

O tempo gasto com debug é relevante, e devemos tentar minimizá-lo. Não é a toa que Brian Kernighan, considerado um dos co-autores do C, é frequentemente citado em relação a este assunto, dizendo que:

Como escolha pessoal, nós tendemos a não usar debuggers a não ser para tirar uma stracktrace ou o valor de uma ou outra variável. A razão é que é fácil de se perder em tantos detalhes de estruturas de dados e controle de fluxo complicados. Nós achamos que ir passo a passo no programa é menos produtivo que adicionar logs e asserções em lugares estratégicos. Ficar clicando em linhas de código toma mais tempo que olhar o log. Toma menos tempo decidir onde colocar as invocações ao log que fazer o passo a passo até a seção crítica do código, mesmo assumindo que saibamos onde ela está. Mais importante, debug por logging fica no programa, já o processo de debug é transiente.“.

Como já estamos bastante acostumados a usar logging, vamos aproveitar pra ver que nem sempre é tão trivial: às vezes, temos de tomar cuidado ao colocar inúmeras invocações à API de log sem muito critério. Para ilustrar o caso, vamos imaginar uma situação bastante comum: estamos querendo descobrir por que os emails que deveriam chegar pro cliente não estão sendo enviados. Considere o seguinte código que usa o Apache Commons Mail:

class EnviadorEmail {
  public void enviaEmailDeCadastro(String to) {
    try {
      SimpleEmail email = new SimpleEmail();
      email.setHostName("mail.myserver.com");
      email.addTo(to, "nome do cliente");
      email.send();
    } catch(EmailException e) {
    }
  }
}

O erro aqui é a péssima prática de engolir exceptions, que costuma aparecer em diversos códigos por aí. Ao debugar rapidamente, iremos perceber que devemos corrigir esse codigo. Como? Uma sugestão seria colocar um logging aí.

Então vamos nos aventurar a usar um sistema de logging para o Java. Existem vários, sendo o mais clássico o Log4J, mas ainda há a própria API de logging do Java (desde 1.4) e temos tambem wrappers, como o commons logging ou o SL4J – simple logging, este último adotado pelo Hibernate. Vamos utilizar o Log4J dada a sua grande adoção. Baixe o Log4J e coloque o JAR no build path do seu projeto.

Agora, podemos criar um logger especifico para a nossa classe e, dentro do catch, mostrar uma mensagem:

class EnviadorEmail {
  private static final Logger LOG =
      Logger.getLogger(EnviadorEmail.class);
  public void enviaEmailDeCadastro(String to) {
    try {
      SimpleEmail email = new SimpleEmail();
      email.setHostName("mail.myserver.com");
      email.addTo(to, "nome do cliente");
     email.send();
     log.info("E-mail enviado para  " + to);
    } catch (EmailException e) {
      log.error("problemas enviando email", e);
    }
  }
}

Ao rodar o programa, se o sistema não conseguir enviar um email, a exception sera capturada e logada. Mas não configuramos onde esse log deve aparecer, então o Log4J nos alertará sobre isso. É bastante comum esse alerta aparecer quando usamos, por exemplo, o Hibernate sem configurar o log:

log4j:WARN No appenders could be found for
				logger (org.hibernate.cfg.annotations.Version).
log4j:WARN Please initialize
				the log4j system properly.

Então, além de uma configuração básica de log4j.xml para o seu sistema, você precisa configurar para que o log do Hibernate use um appender, no caso chamado sysout que vai jogar pra saída padrão:

	<category name="org.hibernate">
		<priority value="info" />
		<appender-ref ref="sysout" />
	</category>

Ou, no caso do nosso sistema, o name da nossa categoria deve refletir o nome do nosso pacote. Nosso sistema agora loga corretamente a falha, mas ainda há um problema maior: quem pediu para executar a acao de enviar o email, mesmo que ele nao seja enviado, tem a impressão de que tudo ocorreu normalmente. Isto é:

cadastra(usuario);
EnviadorEmail ee = new EnviadorEmail();
ee.enviaEmailDeCadastro(usuario.getEmail());
System.out.println("sucesso no cadastro");

O codigo acima executará normalmente, mesmo que haja uma falha no envio do e-mail. É isso que gostaríamos? Em alguns casos, talvez seja interessante não falhar mesmo que um e-mail não seja enviado, mas esse tipo de decisão deve ser tomada pela lógica de negócio, e não pela classe que escrevemos. Deveríamos então ter propagado a exception, que era checked. Podemos fazer isso atraves da mesma exception ou “envelopando-a” numa unchecked que você considere, tal como:

    try {
      SimpleEmail email = new SimpleEmail();
      email.setHostName("mail.myserver.com");
      email.addTo(to, "nome do cliente");
      email.send();
      log.info("E-mail enviado para  " + to);
    } catch (EmailException e) {
      log.error("problemas enviando email", e);
      throw new IllegalStateException("falha ao enviar email para " + to, e);
    }
 

Pronto. Agora, além de falhar, vai logar pra gente o motivo da falha. Será que agora temos um logging bom que vai evitar que debuguemos nosso código? Ainda precisamos ajustar diversos detalhes. Um deles é que, nesse mesmo codigo, estamos aplicado o log and throw antipattern, que, no caso de existir alguém, em algum nivel acima, fazendo o try catch, vai provavelmente logar a exception, e ela será logada duas vezes. Devemos logar exceptions apenas quando não a relançamos (wrapped ou não).

Também precisaríamos colocar muito mais logging, acertar os níveis (info, error, warn, debug) e tomar cuidado até mesmo com a performance: apesar das APIs de logging serem incrivelmente rápidas, muitas vezes elas vão acabar invocando métodos seus, como o toString:

log.debug("cadastrando usuario " + usuario);

Aqui, usuario.toString() será invocado ao concatenar a mensagem, mesmo se o log não estiver definido em debug no seu arquivo de configuração, podendo gastar um tempo precioso se for invocado inúmeras vezes. É por esse motivo que as vezes aparece o seguinte idiomismo evitando chamadas desnecessárias:

if (log.isDebugEnable()) {
  log.debug("cadastrando usuario " + usuario);
}

As APIs de log mais modernas, como o SL4J, vão utilizar de varios argumentos ou de varargs, para que não tenha de cair nesse tipo de código, fazendo essas chamadas de toString apenas se aquele nível de log estiver habilitado.

Concluindo: debugar é sim preciso, mas devemos, através de boas práticas e um forte e criterioso logging, diminuir ao máximo a sua necessidade. E siga as boas práticas e faça boas logadas.

  • Share/Bookmark



Caelum | Ensino e Inovação
São Paulo: Rua Vergueiro, 3185, cj. 87, próximo ao Metrô Vila Mariana   |   Tel. (11) 5571-2751
Rio de Janeiro: Rua Senador Dantas, 80, cj. 307/308 - Centro   |   Tel. (21) 2220-4156 ou 2297-0033
Brasília: SCS Qd. 8 Bl. B-50, Sala 521 - Ed. Venâncio 2000   |   Tel. (61) 3039-4222