Collections Performance incorreta e Pq o >>> resolve o problema de não existir inteiro unsigned no java?

Boa noite!

Antes de mais nada peço desculpas pela postagem gigante.

Sou iniciante em java e estou lendo o curso do Caelum que é muito bom. Na parte de Collections fiquei um pouco confuso e fui procurar outras explicações na internet e acabei me deparando com mais dúvidas. Se os mais experientes ajudarem eu ficarei grato.

Fiz um pequeno programa para comparar performance de várias implementaçoes da Collections e para minha surpresa, os resultados não são como esperados. ArrayList por exemplo é mais rápido do que LinkedList em todas operações que eu tentei e pelo que eu entendei isso não deveria acontecer.

package br.com.collections.teste.performance;

public class Temporizador {

	private long start;
	private long end;
	
	public void Start() {
		
		start = System.currentTimeMillis();
		
	}
	
	public long End() {
		
		end = System.currentTimeMillis();
		
		return  end -= start;
		
	}
	
}

package br.com.collections.teste.performance;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class MyArrayList {
	
	Collection<Integer> al = new ArrayList<Integer>();
	
	public void Insere(int inc) {
	
		for (int loop = 0 ; loop < inc ; loop++) {
		
			al.add(loop);
		
		}
	}
	
	
	public void Consulta(int inc) {
		
		if (inc > al.size()) {
			
			return;
		}
		
		for (int loop = 0 ; loop < inc ; loop++) {
		
			al.contains(loop);
		
		}
	
	}
	
	public void Remove(int inc) {
		
		al.remove(inc);
		
	}
	
	public void IteratorFindOneEntry(int inc) {
		
		/*if (inc > al.size()) {
			
			return;
		}*/
		
		Iterator<Integer> it = al.iterator();
		
		while (it.hasNext()) {
		
			if (it.next() == inc) {
				
				return;
				
			}
			
			
		}
		
	}
	
}

package br.com.collections.teste.performance;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;


public class MyLinkedList {

	Collection<Integer> ll = new LinkedList<Integer>();
	
	public void Insere(int inc) {
		
		for (int loop = 0; loop < inc ; loop++) {
			
			ll.add(loop);
			
		}
		
	}
	
	public void Consulta(int inc) {
		
		if (inc > ll.size()) {
			
			return;
		}
		
		for(int loop = 0; loop < inc; loop++) {
			
			ll.contains(loop);
			
		}
		
	}
	
	public void Remove(int inc) {
		
		ll.remove(inc);
		
	}
	
	public void IteratorFindOneEntry(int inc) {
		
		/*if (inc > ll.size()) {
		
			return;
		
		}*/
		
		Iterator<Integer> it = ll.iterator();
		
		while (it.hasNext()) {
			
			if (it.next() == inc) {
				
				return;
				
			}
			
		}
		 
	}
	
}

package br.com.collections.teste.performance;

import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Vector;

public class MyVector {

	Collection<Integer> vtr = new Vector<Integer>();
	
	public void Insere(int inc) {
		
		for (int loop = 0; loop < inc ; loop++) {
			
			vtr.add(loop);
			
		}
		
	}
	
	public void Consulta(int inc) {
		
		if (inc > vtr.size()) {
			
			return;
		}
		
		for(int loop = 0; loop < inc; loop++) {
			
		vtr.contains(loop);
			
		}
		
	}
	
	public void Remove(int inc) {
		
		vtr.remove(inc);
		
	}
	
	public void IteratorFindOneEntry(int inc) {
		
		/*if (inc > ll.size()) {
		
			return;
		
		}*/
		
		Enumeration e = ((Vector<Integer>) vtr).elements();
		
		while(e.hasMoreElements()) {
			
			if (e.nextElement() == (Integer) inc) {
				
				return;
			}
			
		}
		
		 
	}
	
}

package br.com.collections.teste.performance;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;


public class MyLinkedList {

	Collection<Integer> ll = new LinkedList<Integer>();
	
	public void Insere(int inc) {
		
		for (int loop = 0; loop < inc ; loop++) {
			
			ll.add(loop);
			
		}
		
	}
	
	public void Consulta(int inc) {
		
		if (inc > ll.size()) {
			
			return;
		}
		
		for(int loop = 0; loop < inc; loop++) {
			
			ll.contains(loop);
			
		}
		
	}
	
	public void Remove(int inc) {
		
		ll.remove(inc);
		
	}
	
	public void IteratorFindOneEntry(int inc) {
		
		/*if (inc > ll.size()) {
		
			return;
		
		}*/
		
		Iterator<Integer> it = ll.iterator();
		
		while (it.hasNext()) {
			
			if (it.next() == inc) {
				
				return;
				
			}
			
		}
		 
	}
	
}

package br.com.collections.teste.performance;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;

public class MyHashSet {
		
	Collection<Integer> hs = new HashSet<Integer>();
	
/*		@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((hs == null) ? 0 : hs.hashCode());
		return result;
	}


	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		MyHashSet other = (MyHashSet) obj;
		if (hs == null) {
			if (other.hs != null)
				return false;
		} else if (!hs.equals(other.hs))
			return false;
		return true;
	}*/
		
		public void Insere(int inc) {
		
			for (int loop = 0 ; loop < inc ; loop++) {
			
				hs.add(loop);
			
			}
		}
		
		
		public void Consulta(int inc) {
			
			if (inc > hs.size()) {
				
				return;
			}
			
			for (int loop = 0 ; loop < inc ; loop++) {
			
				/*if (hs.contains(loop)) {
					
					System.out.println("Encontrou: ");
					return;
				}*/
				
				hs.contains(loop);
			
			}
		
		}
		
		public void Remove(int inc) {
			
			hs.remove(inc);
			
		}
		
		public void IteratorFindOneEntry(int inc) {
			
			/*if (inc > al.size()) {
				
				return;
			}*/
			
			Iterator<Integer> it = hs.iterator();
			
			while (it.hasNext()) {
			
				int tmp = it.next();
				if (tmp == inc) {
					//System.out.println("Encontrou: " + tmp);
					return;
					
				}
				
				
			}
			
		}
		
}

package br.com.collections.teste.performance;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

public class MyHashMap {

	HashMap<Integer, Integer> hm = new HashMap<Integer, Integer>();
	
	public void Insere(int inc) {
	
		for (int loop = 0 ; loop < inc ; loop++) {
		
			hm.put(loop, loop);
		
		}
	}
	
	
	public void Consulta(int inc) {
		
		if (inc > hm.size()) {
			
			return;
		}
		
		for (int loop = 0 ; loop < inc ; loop++) {
		
			/*if (hs.contains(loop)) {
				
				System.out.println("Encontrou: ");
				return;
			}*/
			
			hm.containsKey(loop);
		
		}
	
	}
	
	public void Remove(int inc) {
		
		hm.remove(inc);
		
	}
	
	public void IteratorFindOneEntry(int inc) {
		
		/*if (inc > al.size()) {
			
			return;
		}*/
		
		Set<Integer> KeySet = hm.keySet();
		
		for(Integer i : KeySet) {
			
			if (i == inc) {
				
				return;
			}
			
			
		}
		
	}
	
}
  1. A performance do LinkedList deveria ser melhora em algumas operações do que ArrayList? Ainda, o Vector muitos lugares dizem ser ultrapassado e lento por ser sincronizado, mas para inserir os valores ele foi o mais rápido de todos. O que eu estou fazendo de errado?

  2. Eu fiquei muito confuso com o hashcode() e li bastante a resposito, a conclusão que eu cheguei é que o algoritmo tem como objetivo evitar as colisões (por exemplo em HashSet) na indexação e não melhorar a performance, certo? Caso negativo, pode me explicar com exemplos (pq eu sou bem anta mesmo).

  3. Na pratica é impossível evitar colisões e a qualidade e tamanho do hascode influenciam, pelo que eu entendi o Set começa com 16 posições (0 até 15) e usa o hashcode() da Key e o tamanho da tablea index para achar como distribuir. Se ele achar um valor com a mesmo hashcode e mesma key, ele substitui o valor. Então a minha dúvida é, quão comum é uma colisão na pratica e dados serem trocados por um valor errado? A questão de sobescrever o valor (V -> Value) não deveria ser validado? Eu imaginava que ambos valores fossem importantes. Talvez eu não entenda muito bem a utilização dessa forma de armazenamento com os exemplos que vi na internet. :thinking:

ref. https://www.javatpoint.com/working-of-hashmap-in-java

  1. Tentando entender o hashcode() achei essa explicação de hashmap

E vi essa parte

// This function ensures that hashCodes that differ only by

// constant multiples at each bit position have a bounded

// number of collisions (approximately 8 at default load factor).

h ^= (h >>> 20 ) ^ (h >>> 12 );

return h ^ (h >>> 7 ) ^ (h >>> 4 );

Eu não sabia o que o >>> fazia, então achei nesse link abaixo uma explicação que é para evitar overflow já que o java não aceita unsigned inteeiro.

Eu entendi que o número que você passar na frente do >>> ele vai adicionar em zeros a direita e remover o mesmo montante de bits do final. O que eu não entendi é qual o problema do inteiro unsigned e pq isso resolve a conta corretamente. Alguem pode me explicar por favor?

Agradecido.

2 curtidas

LinkedList é mais rápido que o ArrayList nas operações add e remove e é mais lento na operação get(int index).

ArrayList é mais rápido que o LinkedList na operação get(int index) e é mais lento nas operações add e remove.

A principal diferença está na alocação de memória, o LinkedList é a implementação clássica de uma lista encadeada, então ele só deixa reservado espaço para uma nova referência a ser adicionada.

Já o ArrayList utiliza um array internamente pra pré-alocar o espaço para as referências a serem adicionadas e quando esse array enche, ele cria um novo array com o dobro do tamanho e transfere os elementos do array antigo para o novo.

É basicamente isso mesmo.

Isso pode acontecer quando se implementa hashcodes malfeitos que geram o mesmo hash para objetos com propriedades diferentes.
Eu costumo utilizar uma API própria que criei para o hashcode e equals, acho ela mais legível do que usar os códigos gerados pelas IDEs e acho mais leve que APIs de terceiros.
Ela está disponível no GitHub:

Tem exemplos de uso nos seguintes links:

O >>> não adiciona zeros à direita.

As operações <<, >> e >>> são para deslocamentos de bits.
Se você imprimir os valores numéricos em binário, aplicar as operações e exibir o resultado em binário, vai ficar fácil de entender.

 << desloca bits para a esquerda, preenchendo à direita com 0

>>> desloca bits para a direita, preenchendo à esquerda com 0

 >> desloca bits para a direita, preenchendo à esquerda com o valor do bit mais significativo
    ou seja, ele preserva o sinal
    se aplicar em números positivos, vai preencher à esquerda com 0
    se aplicar em números negativos, vai preencher à esquerda com 1

Exemplos:

21 << 1 vai deslocar um bit para a esquerda no número 21:

  binário    decimal
 00010101         21

Deslocando um bit para a esquerda fica:

  binário    decimal
 00101010         42

21 >> 1 vai deslocar um bit para a direita no número 21 preenchendo à esquerda com o valor do bit mais significativo:

  binário    decimal
 00010101         21

Deslocando um bit para a direita fica:

  binário    decimal
 00001010         10

Agora com -21 >> 1 veja o que acontece (em números negativos o bit mais significativo sempre é 1):

  binário    decimal
 11101011        -21

Deslocando um bit para a direita fica:

  binário    decimal
 11110101        -11

21 >>> 1 vai deslocar um bit para a direita no número 21 preenchendo à esquerda com 0:

  binário    decimal
 00010101         21

Deslocando um bit para a direita fica:

  binário    decimal
 00001010         10

Veja agora -21 >>> 1:

  binário    decimal
 11101011        -21

Deslocando um bit para a direita fica:

  binário    decimal
 01110101        117

Particularmente gosto bastante de utilizar máscaras de bits para reduzir consumo de memória.
Se eu tenho uma classe com 8 atributos do tipo boolean, eu substituo eles por 1 atributo do tipo byte e uso o valor de cada bit desse byte para representar o atributo que seria um boolean.

5 curtidas

Só pra constar que usar System.currentTimeMillis não é uma maneira confiável de medir corretamente o tempo de execução de um programa (sugiro que leia tudo que está neste link para mais detalhes).

2 curtidas

Muito obrigado @staroski e @hugokotsubo pelo tempo em ajudar e pela qualidade das respostas.

Foi exatamente isso que eu li em praticamente todos os lugares, mas fazendo o teste com esse código que postei (e mesmo trocando o System.currentTimeMillis() por System.nanoTime()) meu resultado é bem diferente. Tentei usar também o System.gc() apesar de não ter garantias pelo que entendi, mas ele fez apenas em alguns momentos o primeiro e segundo lugar ficarem intercalando de posição.

Rodando no meu computador para inserir (add()) o ArrayList() e Vector() são bem mais rápidos. O LinkedList() para minha surpresa perde em tudo para o ArrayList(). Para encontrar um dado o HashSet() e HashMap() bem nitidamente mais rápidos.

Uma coisa que notei, é que a performance entre as implementações muda com o tamanho do for que eu faço para preencheer. Infelizmente não consegui testar com muito mais pois o LinkedList() falha com um erro na heap.

Apenas a título de curiosidade esse é um dos meus resultados com System.nanoTime().

ArrayList:
Tempo utilizado para preencher o ArrayList() foi de: 305856533
Tempo utilizado para buscar as entradas no ArrayList() foi de: 2822524875
Tempo utilizado para encontrar uma entrada no ArrayList() foi de: 18336358
Tempo utilizado para remover uma entrada no ArrayList() foi de: 16582734

LinkedList:
Tempo utilizado para preencher o LinkedList() foi de: 1033384108
Tempo utilizado para  buscar as entradas na LinkedList() foi de: 9772664310
Tempo utilizado para encontrar uma entrada no LinkedList() foi de: 57661621
Tempo utilizado para remover uma entrada no LinkedList() foi de: 38737411

HashSet:
Tempo utilizado para preencher o HashSet() foi de: 630291022
Tempo utilizado para  buscar as entradas no HashSet() foi de: 4090184
Tempo utilizado para encontrar uma entrada no HashSet() foi de: 50932074
Tempo utilizado para remover uma entrada no HashSet() foi de: 35768

HashMap:
Tempo utilizado para preencher o HashMap() foi de: 647697063
Tempo utilizado para  buscar as entradas no HashMap() foi de: 2935945
Tempo utilizado para encontrar uma entrada no HashMap() foi de: 94056631
Tempo utilizado para remover uma entrada no HashMap() foi de: 18527

Vector:
Tempo utilizado para preencher o Vector() foi de: 302781226
Tempo utilizado para  buscar as entradas no Vector() foi de: 2899651013
Tempo utilizado para encontrar uma entrada no Vector() foi de: 73437154
Tempo utilizado para remover uma entrada no Vector() foi de: 7548765

Eu não imaginava que para fazer um benchmark simples desse para aprendizado (conforme sugerido na apostila da Caelum") fosse tão difícil em java. :scream:

Muito interessante, obrigado. Li os 3 blog posts e tentei entender o código. Apesar da sua implementação ser mais fácil, pelo que eu entendi da do seu blogpost a do Eclipse não é ruim, apenas maior / mais difícil (o que com certeza faz diferença).

Pergunta muito boba, você saberia me dizer por favor um exemplo de uso que HashSet por exemplo seria o ideal e os outros não? Talvez algum site que tenha esse tipo de informação? Existe?

Os que eu encontro são todos esses exemplos com números ou frutas para exemplo de aprendizado.

Essa sua explicação e exemplos foi sensacional! Muito boa!

Que encrenca, e eu achando que era fácil. Li tudo e estou vendo a JMH, mas talvez tenha até que mudar o código. :thinking:

Agradecido.

HashSet é uma implementação de Set.

Quando sua coleção não pode ter elementos repetidos, use implementações de Set.

Quando sua coleção pode ter elementos repetidos, use implementações de List.