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


Não posso descobrir nem instanciar tipos genéricos! Porque?

Por Paulo Silveira em 28/04/08

São incontáveis os posts no GUJ com uma pergunta semelhante: “Posso extrair o nome de um tipo genérico?“, “Não consigo extrair tipo do genérico!“, “Utilizando generics para instanciar objetos“, entre outros. Curioso que esse tipo de pergunta tem aumentado muito nos últimos tempos, identificando um possível crescimento no uso do Java 5 em diante. Já era a hora!

Eu já havia postado sobre isso quando falei de reificação de tipos parametrizados, mas de uma maneira mais geral.

A questão básica é a seguinte: Se eu tenho um tipo genérico, que recebe um tipo parametrizado T como argumento, eu posso instanciar T de alguma forma?

Quem sabe tentar assim:

class Dao<T> {
  public T cria() {
    return new T();
  }  
}

Essa forma não funciona. Você não tem garantias sobre os construtores que o tipo T possui. E então se tentarmos assim:

class Dao<T> {
  public T cria() {
    return T.class.newInstance();
  }  
}

Aqui a sintaxe até poderia ser possível, mas infelizmente o java não sabe quem é T nem mesmo em tempo de execução. Nem mesmo com manipulação de bytecode ou qualquer outro recurso. Isso porque o compilador “apaga” essa informação depois de utilizada: é a tal da mal falada erasure.

Qual seria a vantagem da erasure?

Antes de mais nada: erasure não serve para poder rodar código do Java 5 em VMs Java 1.4 ou menor! Não é esse o objetivo.

Quando a JSR14 do generics foi proposta, eles queriam mais que compatibilidade para trás em relação a compilação, eles queriam também a possibilidade de migrar o código antigo para poder usar código novo: uma ArrayList precisa poder ser passada como argumento para alguém que receba List<QualquerClasse> como argumento!

Para ilustrar a situação, imagine que eu tenho uma aplicação grande X que usasse a classe ArrayList em muitos lugares (como a grande maioria das aplicações). Essa aplicação X usa a biblioteca B, que recebe List como argumento em muitos de seus métodos. Se um dia a bibioteca B passasse a usar List genérica, gostaríamos que a aplicação X atualizasse B sem maiores problemas: sem precisar recompilar nada, nem trocar nada no código fonte. O Neal Gafter escreveu muito sobre isso pouco antes da release do Java 5, dada as inúmeras críticas que eles estavam recebendo.

Se o Java tivesse optado por outra maneira de implementar generics, teria ou quebrado compatibilidade com o uso não genérico de classes que viraram genéricas, ou precisaria criar classes paralelas as atuais, praticamente copiadas e coladas, só que estas genéricas, tornando as antigas deprecated ou legadas.

Como o .NET resolveu o mesmo problema?

O .NET seguiu essa segunda forma, e criou uma hierarquia quase que paralela de coleções dentro do name space System.Collections.Generic.

A IList é a interface que define as operações em uma lista não genérica, e a ArrayList é sua implementação mais comumente usada. Quando entrou generics no .NET eles criaram uma outra interface para a lista, com mesmo nome, só que genérica: a IList<T>.

A classe List já é a implementação da interface genérica, e é ela quem você vai usar em vez de ArrayList. Ela possui uma definição bem estranha:

public class List : IList, ICollection,
    IEnumerable, IList, ICollection, IEnumerable

Ela implementa tanto a lista genérica como a não genérica. O .NET tem um recurso que nós não temos, que faz com que apesar dessa lista também implementar a interface não genérica, você só consegue invocar os métodos não genéricos (que trabalham com Object) se estiver se referenciando a ela explicitamente como uma lista não genérica, como o código abaixo:

IList x = new List<String>();
x.Add(2);

Apesar desse código compilar, no .NET temos essa informação dos tipos genéricos em tempo de execução, o que fará gerar uma exceção:

System.ArgumentException: The value "2" is not of type "System.String" e cannot be used in this generic collection

No Java teríamos apenas um unchecked warning na linha da declaração da referência, e uma possível ClassCastException mais a frente no código.

Aqui a vantagem é você poder passar uma List genérica para um código .NET antigo, que recebe como argumento uma IList não genérica. Além disso, no .NET você pode sim descobrir quem é T em tempo de execução:

class ClasseGenerica<T> {
  void metodo() {  Console.WriteLine(typeof(T))}
}

Em resumo: o sistema de generics do .NET é realmente seguro, não temos como burlá-lo através de unchecked casts, como ocorre em Java. O Java novamente sacrificou alguns recursos interessantes em favor a compatibilidade de versões e interoperabilidade entre classes genéricas e as não genéricas já existentes. Cada um com sua vantagem. Como citei no outro artigo, existem algumas idéias de dar suporte a tipos genéricos reificados no Java, ao mesmo tempo que outros ficariam ainda com a erasure, sendo que você pode escolher qual o que te agrada para aquela classe genérica em particular. Talvez ter as duas opções adicione ainda mais complexidade a tipagem genérica do Java, mas eu particularmente gosto da idéia.

Mais uma vez um post que era para ser sucinto ficou longo. Agradeço ao Rafael Steil e Rodrigo Kumpera pela colaboração, e ao Lucas Cavalcanti e Guilherme Moreira pedindo para que fosse elaborado um post mais completo sobre esse assunto tão pertinente.

  • Share/Bookmark

Evento Falando em Java 2008

Por Sérgio Lopes em 24/04/08

Dia 18 de maio, a Caelum promoverá o evento Falando em Java 2008. Será um dia repleto de palestras sobre diversos temas atuais da área, com instrutores da Caelum como palestrantes. (Veja como foi o evento do ano passado)

E, neste ano, como palestrante convidado, teremos um dos líderes da JPA 2.0 e líder da implementação JPA do Hibernate, Emmanuel Bernard, vindo direto dos EUA especialmente para o evento. Ele trará as novidades da JPA 2.0 e o Hibernate Search.

Teremos também Arquitetura de Projetos Java, Domain Driven Design (DDD), JRuby on Rails, Scrum e jQuery. E ainda uma palestra sobre os projetos open-source da Caelum, incluindo novos lançamentos! Além do Emmanuel Bernard, os palestrantes serão: Paulo Silveira, Guilherme Silveira, Fábio Kung, Alexandre Magno, Guilherme Moreira, Thadeu Russo e Sérgio Lopes – todos instrutores da Caelum.

E, claro, é uma grande oportunidade para a comunidade Java se reunir, fazer contatos, trocar experiências. Ofereceremos um brunch bem reforçado e dois coffee breaks nos intervalos. Toda a equipe da Caelum estará lá presente; aproveite para nos conhecer se ainda não nos conhece!

O Falando em Java 2008 será Domingo dia 18 de maio no Colégio Arquidiciocesano (localizado na frente no metrô Santa Cruz). Aproveite para fazer sua inscrição agora, com preços bastante acessíveis – e com descontos para alunos e ex-alunos!

Acesse agora mesmo o site oficial do Falando em Java ou veja como foi a edição do ano passado.

  • Share/Bookmark

Entendendo o serialVersionUID

Por Paulo Silveira em 01/04/08

O serialVersionUID é uma dúvida constante entre muitos desenvolvedores. Afinal, quando e para que exatamente usá-lo? Devo gerar um número aleatório bem grande, ou um número qualquer? Essas perguntas são comuns, e ao desenvolvedor experiente é necessário conhecer a fundo esse detalhe do processo de serialização do Java.

Quando um objeto é serializado no Java, essa sequência de bytes, além de conter seus atributos de instância não transientes, carrega consigo um número que indentifica a “versão” da classe que foi usada durante o processo. Esse é o chamado serialVersionUID, ou seja, o indentificador de versão de serialização de uma classe. Esse número é utilizado para saber se o objeto que estamos recuperando é de uma versão “compatível” com a versão da classe que foi utilizada quando serializamos o objeto: em outras palavras, os arquivos .class não precisam ser necessariamente os mesmos para que o processo de serialização ocorra com sucesso.

Por exemplo, considere a seguinte classe Usuario:

package br.com.caelum;

public class Usuario implements Serializable {
  private String login;
}

Essa classe possui o serialVersionUID igual a 2806421523585360625L. Esse número não é aleatório! Ele é um hash (SHA) calculado em cima dos nomes dos seus atributos, e assinaturas dos métodos em uma ordem bem definida pela especificação do processo de serialização. E como eu sei esse número? O JDK vem com a ferramenta serialver, que implementa esse mesmo hash:

serialver br.com.caelum.Usuario

Se o serialVersionUID utilizado durante a serialização não bater exatamente com o serialVersionUID da classe que está sendo usada para recuperar essa informação, uma exception é lançada: java.io.InvalidClassException.

Por exemplo, se adicionarmos um novo atributo na nossa classe Usuario:

public class Usuario implements Serializable {
  private String login;
  private String senha;
}

Agora teremos o serialVersionUID valendo 416295346730660862L. Caso você serialize um Usuario com a primeira classe aqui definida, e tentar recuperar essa informação usando essa nova versão de classe, receberemos a conhecida java.io.InvalidClassException. Esse é o comportamente que em muitos casos queremos, mas algumas vezes fazemos pequenas modificações na classe as quais percebemos que não impactarão no processo de serialização, e precisamos manter compatibilidade com a versão antiga daquela classe. Para isso, basta definirmos explicitamente qual é o nosso serialVersionUID, e no caso de querer manter compatibilidade com a classe Usuario anterior, vamos utilizar o valor de serialVersionUID que seria gerado pela JVM: 2806421523585360625L. O código ficaria:

public class Usuario implements Serializable {
  private static final long serialVersionUID = 2806421523585360625L;
  private String login;
  private String senha;
}

Às vezes recebemos um warning do Eclipse, e ele pede para que seja definido o serialVersionUID da classe em questão. Isso ocorre porque você implementa Serializable ou uma de suas mães a implementa. O Eclipse então te abre três opções: utilizar o @SurpressWarnings para você assumir o risco, usar um valor default, ou usar o valor gerado. O gerador de UIDs do Eclipse é exatamente o mesmo gerador utilizado pelo Java SE para criar os UIDs padrão! Reforçando, esse número não é um número aleatório!

serialVersionUID

Quando alguém esquece de manter o mesmo serialVersionUID para duas versões compatíveis de uma classe, podemos ter problemas em usar diferentes versões do software que são teoricamente compatíveis. Isso muitas vezes acontece em servidores de aplicação, e se seu cliente esta desatualizado em relação a versão dos jars necessários pelo servidor, podemos ter alguns InvalidClassExceptions que poderiam ser facilmente evitados se o serialVersionUID tivesse sido corretamente aplicado. Claro que algumas outras vezes as versões realmente não são compatíveis e a exception procede.

Esse grave problema pode acontecer mesmo usando classes do Java SE entre diferentes versões, como é o caso da classe java.text.AttributedCharacterIterator.Attribute (utilizada pela java.awt.Font). Do Java 1.3 para o Java 1.4 essa classe foi levemente alterada, e o serialVersionUID gerado pelo algoritmo da JVM desta classe mudou de -1514471214376796190L para -9142742483513960612L. Quando alguém serializava uma java.awt.Font em uma versão não podia desserializa-la em outra, sendo que as versões tecnicamente são compatíveis: a não definição explícita do serialVersionUID gerou um bug no Java SE. Como isto foi resolvido? Definiram o serialVersionUID como -1514471214376796190L, que é o valor que seria gerado pela JVM na versão anterior da classe.

Como então devemos proceder para escolher um serialVersionUID apropriado? É muito simples: se essa classe está nascendo neste momento, você pode se dar ao luxo de utilizar um serialVersionUID, como por exemplo:

public class Usuario implements Serializable {
  private static final serialVersionUID = 1L;
  private String login;
}

Porém se você está definindo o serialVersionUID de uma classe já em produção, e sabe que a mudança que está fazendo é compatível com a versão anterior, você deve utilizar o serialVersionUID que seria gerado pela JVM na primeira versão, como foi o caso aqui quando adicionamos o atributo senha na classe Usuario, e também foi o caso da correção do bug da classe java.text.AttributedCharacterIterator.Attribute. Quando você fizer uma alteração onde percebe que o cliente precisará de atualização das classes envolvidas, basta definir um serialVersionUID diferente dos anteriormente utilizados.

Para completar, implementar uma interface que não define métodos (Serializable) e ser forçado a escrever um atributo sem um contrato mais burocrático é um tanto estranho em uma linguagem como o Java. Sem dúvida, se esse mecanismo todo tivesse sido inventado já com a existência de anotações, Serializable seria uma anotação e version um atributo dela, talvez obrigatório, criando algo como @Serializable(version=12345L). Boas serializações e invocações remotas!

  • 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