5 coisas que você não sabia sobre ... a API Java Collections, Parte 2

Mutáveis nos quais prestar atenção

É possível usar Java™ Collections em qualquer lugar, mas não as subestime. As Coleções abrigam mistérios e podem trazer problemas se você não tratá-las adequadamente. Neste capítulo sobre 5 coisas que você não sabia, Ted Neward explora o lado complexo e mutável da API Java Collections, com dicas que o ajudarão a se beneficiar mais com o Iterable, HashMap e SortedSet, sem introduzir erros no seu código.

Ted Neward, Principal, Neward & Associates

Ted Neward photoTed Neward é o diretor da Neward & Associates, onde ele dá consultoria, orientação, ensina e faz apresentações sobre Java, .NET, Serviços XML e outras plataformas. Ele reside perto de Seattle, Washington.



21/Mai/2010

Sobre esta série

Então você acha que conhece programação em Java? O fato é que a maior parte dos desenvolvedores tem simplesmente um conhecimento superficial da plataforma Java e sabe apenas o suficiente para fazer seu trabalho. Nesta série, Ted Neward aprofunda a discussão sobre a funcionalidade essencial da plataforma Java para revelar dados pouco conhecidos que podem ajudar a solucionar até mesmo os mais complexos desafios de programação.

As classes Collections no java.util foram projetadas para ajudar, especificamente substituindo arrays e, desse modo, melhorar o desempenho Java. Como você aprendeu no artigo anterior, elas também são maleáveis, favoráveis à customização e podem ser estendidas de todas as formas a serviço de um código correto e limpo.

As Coleções são também poderosas e mutáveis: use-as com cuidado e abuse delas, por sua própria conta e risco.

1. Listas não são a mesma coisa que arrays

Os desenvolvedores de Java frequentemente cometem o erro de pressupor que ArrayList é simplesmente a substituição do array Java. As Coleções são suportadas por arrays, o que leva ao bom desempenho na consulta aleatória de itens em uma coleção. E, assim como arrays, coleções usam ordinais inteiros para obter itens particulares. Além disso, uma coleção não é uma substituição direta para um array.

O truque para diferenciar coleções de arrays é saber a diferença entre ordem e posição. Por exemplo, List é uma interface que preserva a ordem na qual os itens são colocados em uma coleção, como mostra a Listagem 1:

Listagem 1. Chaves mutáveis
import java.util.*;
  public class OrderAndPosition {
  public static <T> void dumpArray(T[] array)     {
  System.out.println("=============");
  for (int i=0; i<array.length; i++)
  System.out.println("Position " + i + ": " + array[i]);
  }
  public static <T> void dumpList(List<T> list)     {
  System.out.println("=============");
  for (int i=0; i<list.size(); i++)
  System.out.println("Ordinal " + i + ": " + list.get(i));
  }
  public static void main(String[] args)
  {
  List<String>
  argList = new ArrayList<String>(Arrays.asList(args));
  dumpArray(args);
  args[1] = null;
  dumpArray(args);
  dumpList(argList);
  argList.remove(1);
  dumpList(argList);
  }
}

Quando um terceiro elemento é removido da List acima, os outros itens "atrás" dele passam para frente a fim de preencher os slots vazios. Claramente, o comportamento dessa coleção difere do comportamento de um array. De fato, a remoção de um item de um array não é a mesma coisa que remover o item de uma List— "remover" um item de um array significa substituir seu slot de índice por uma nova referência ou por um valor nulo.


2. Iterator, você me surpreende!

Não há dúvida de que os desenvolvedores Java adoram as Java Collections Iterator, mas qual foi a última vez que você realmente olhou para uma interface Iterator? Na maioria das vezes, simplesmente colocamos o Iterator dentro de um loop for() ou de um loop aprimorado for() e seguimos adiante, por assim dizer.

Mas, para aqueles que continuam se aprofundando, o Iterator tem duas surpresas.

Primeiro, o Iterator oferece suporte à capacidade de remover, com segurança, um objeto de uma coleção de origem chamando remove() no próprio Iterator. O ponto aqui é evitar um ConcurrentModifiedException, que indica precisamente o que seu nome implica: que uma coleção foi modificada enquanto um Iterator estava aberto nela. Algumas coleções permitirão que você escape removendo ou adicionando elementos a uma Collection enquanto realiza a iteração através dela, mas chamar remove() no Iterator é uma prática mais segura.

Em segundo lugar, o Iterator suporta um parente derivado (comprovadamente mais poderoso). ListIterator, disponível apenas em Lists, suporta a adição e a remoção de uma List durante a iteração, bem como rolagem bidirecional pelas Lists.

A rolagem bidirecional pode ser eficiente, em especial para cenários como o onipresente "conjunto móvel de resultados", mostrando 10 dos vários resultados recuperados de um banco de dados ou de outra coleção. Ela também pode ser usada para "retroceder" em uma coleção ou lista, em vez de tentar fazer tudo avançando em frente. Chamar um ListIterator é muito mais fácil que usar parâmetros de inteiros de contagem regressiva para que List.get() retroceda em uma List.


3. Nem todo Iterable é proveniente de coleções

Desenvolvedores de Ruby e Groovy orgulham-se de poder executar a iteração através de um arquivo de texto e imprimir seu conteúdo no console com uma única linha de código. Eles dizem que, na maioria das vezes, fazer a mesma coisa em programação Java utiliza dezenas de linhas de código: abra um FileReader, depois um BufferedReader e, em seguida, crie um loop while() para chamar getLine() até que se torne null. Além disso, é claro que tudo isso deve ser feito em um bloco try/catch/finally que identificará exceções e fechará o identificador de arquivos quando tiver terminado.

Ele pode parecer um argumento bobo e pedante, mas tem algum mérito.

O que eles (e muitos desenvolvedores de Java) não sabem é que nem todos os Iterables têm de ser provenientes de coleções. Em vez disso, um Iterable pode criar um Iterator que sabe como produzir o próximo elemento a partir do nada, em vez de manipulá-lo cegamente a partir de uma Collection pré-existente:

Listagem 2. Iterando um arquivo
// FileUtils.java import java.io.*;
 import java.util.*;
 public class FileUtils {
 public static Iterable<String>
 readlines(String filename)
 throws IOException     {
 final FileReader fr = new FileReader(filename);
 final BufferedReader br = new BufferedReader(fr);
 return new Iterable<String>() {
 public <code>Iterator</code><String> iterator() {
 return new <code>Iterator</code><String>() {
 public boolean hasNext() {
 return line != null; 
 }     				
 public String next() {
 String retval = line; 
 line = getLine();     	
 return retval;     	
 }     			
 public void remove() {  
 throw new UnsupportedOperationException();  
 }     		
 String getLine() {    
 String line = null;     	
 try {     			
 line = br.readLine();     	
 }     				
 catch (IOException ioEx) {    
 line = null;     			
 }     		
 return line;     
 }     			
 String line = getLine();    
 };     
 }	    
 };  
 }
 } 
 //DumpApp.java import java.util.*; 
 public class DumpApp {  
 public static void main(String[] args)  
 throws Exception     {    
 for (String line : FileUtils.readlines(args[0]))     
 System.out.println(line);  
 }
 }

Essa abordagem tem a vantagem de não abrigar todo o conteúdo de um arquivo na memória, mas com a limitação de que, conforme afirmado, não close() o identificador de arquivos subjacente. É possível corrigir isso fazendo o fechamento sempre que readLine() retornar nulo, mas isso não resolverá os casos em que Iterator não é executado totalmente.


4. Cuidado com o mutável hashCode()

Map é uma coleção maravilhosa, trazendo a engenhosidade das coleções de pares de chave/valor frequentemente encontradas em outras linguagens, como Perl. Além disso, o JDK fornece uma excelente implementação Map na forma de HashMap, que usa hashtables internamente para suportar rápidas consultas de chave para valores correspondentes. No entanto, aí está um problema delicado: As chaves que suportam códigos hash que dependem do conteúdo de campos mutáveis são vulneráveis a um erro que enlouquecerá até mesmo o desenvolvedor de Java mais paciente.

Pressupondo que o objeto Person na Listagem 3 tenha um típico hashCode() (que usa os campos firstName, lastName e age— nenhum definitivo — para calcular o hashCode()), a chamada get() para Map irá falhar e retornar null:

Listagem 3. O mutável hashCode() me enlouquece
// Person.java import java.util.*;
  public class Person   
  implements Iterable<Person> {   
  public Person(String fn, String ln, int a, Person... kids)     {    
  this.firstName = fn;
  this.lastName = ln; 
  this.age = a;    
  for (Person kid : kids)             children.add(kid); 
  }     
  // ...        
  public void setFirstName(String value) { 
  this.firstName = value; 
  }    
  public void setLastName(String value) { 
  this.lastName = value;
  }   
  public void setAge(int value) {
  this.age = value; 
  }      
  public int hashCode() {
  return firstName.hashCode() & lastName.hashCode() & age;     
  }   
  // ...    
  private String firstName; 
  private String lastName;  
  private int age;  
  private List<Person> 
  children = new ArrayList<Person>(); 
  } 
  //
  MissingHash.java import java.util.*; 
  public class MissingHash {  
  public static void main(String[] args)     {    
  Person p1 = new Person("Ted", "Neward", 39);  
  Person p2 = new Person("Charlotte", "Neward", 38);   
  System.out.println(p1.hashCode());      
  Map<Person, Person> map = new HashMap<Person, Person>(); 
  map.put(p1, p2);          
  p1.setLastName("Finkelstein");      
  System.out.println(p1.hashCode());        
  System.out.println(map.get(p1));   
  }
  }

Essa abordagem é claramente um suplício, mas a solução é simples: Nunca use um tipo de objeto mutável, como uma chave, em um HashMap.


5. equals() vs Comparable

Ao lidar com os Javadocs, os desenvolvedores Java frequentemente encontram o tipo SortedSet (e sua única implementação no JDK, o TreeSet). Uma vez que SortedSet é a única Collection no pacote java.util que oferece qualquer comportamento de classificação, os desenvolvedores frequentemente começam a usá-la sem questionar muito seus detalhes. A Listagem 4 demonstra:

Listagem 4. SortedSet, estou tão feliz por encontrá-lo!
 import java.util.*; 
 public class UsingSortedSet { 
 public static void main(String[] args)     {     
 List<Person> persons = Arrays.asList(      
 new Person("Ted", "Neward", 39),       
 new Person("Ron", "Reynolds", 39),   
 new Person("Charlotte", "Neward", 38),      
 new Person("Matthew", "McCullough", 18) 
 );     
 SortedSet ss = new TreeSet(new Comparator<Person>() {    
 public int compare(Person lhs, Person rhs) {      
 return lhs.getLastName().compareTo(rhs.getLastName());   
 }   
 });     
 ss.addAll(perons);     
 System.out.println(ss);   
 } 
 }

Depois de trabalhar um pouco com esse código, você descobrirá um dos principais recursos de Set: que ele não permite duplicações. Na verdade, esse recurso é descrito no Javadoc Set. Um Set é uma "coleção que não contém elementos duplicados. Mais formalmente, sets não contêm pares de elementos e1 e e2 como e1.equals(e2) e têm, no máximo, um elemento nulo".

No entanto, esse realmente não parece ser o caso — embora nenhum dos objetos Person na Listagem 4 seja equivalente (de acordo com a implementação equals() em Person), apenas três objetos estão presentes em TreeSet no momento da impressão.

Diferentemente da proclamada natureza do set, TreeSet, que requer que os objetos implementem Comparable diretamente ou tenham um Comparator passado no momento da construção, não usa equals() para comparar os objetos; em vez disso, usa os métodos compare ou compareTo de Comparator/Comparable.

Assim, objetos armazenados em um Set terão dois meios possíveis de determinar equivalência: o esperado método equals() e o método Comparable/Comparator, dependendo do contexto de quem está solicitando.

O pior é que não basta simplesmente declarar que os dois devem ser idênticos, porque a comparação para fins de classificação não é o mesmo que a comparação para fins de equivalência: Pode ser perfeitamente aceitável considerar duas Persons equivalentes ao classificá-las pelo sobrenome, mas não equivalentes em termos de conteúdo.

Verifique sempre se a diferença entre equals() e Comparable.compareTo() que retorna 0 está clara ao implementar Set. Por extensão, a diferença também deve estar clara em sua documentação.


Conclusão

A biblioteca Java Collections é distribuída com "petiscos" que podem tornar sua vida muito mais fácil e mais produtiva, você só precisa conhecê-los. Descobrir esses "petiscos" frequentemente envolve alguma complexidade, no entanto, essa descoberta possibilita que você encontre seu caminho até o HashMap, desde que nunca use um tipo de objeto mutável, como sua chave.

Até agora, escavamos a superfície das Coleções, mas ainda não encontramos a mina de ouro: Coleções Concorrentes, introduzidas no Java 5. As próximas cinco dicas desta série se concentrarão em java.util.concurrent.


Download

DescriçãoNomeTamanho
Source code for the places applicationj-5things3-src.zip15KB

Recursos

Aprender

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=490722
ArticleTitle=5 coisas que você não sabia sobre ... a API Java Collections, Parte 2
publish-date=05212010