Como criar um HashMap com duas chaves (par de chaves, valor)?


118

Eu tenho uma matriz 2D de inteiros. Eu quero que eles sejam colocados em um HashMap. Mas eu quero acessar os elementos do HashMap com base no Array Index. Algo como:

Para A [2] [5], map.get(2,5)que retorna um valor associado a essa chave. Mas como faço para criar um hashMap com um par de chaves? Ou, em geral, várias chaves: de Map<((key1, key2,..,keyN), Value)forma que eu possa acessar o elemento usando get (key1, key2, ... keyN).

EDITAR: 3 anos após postar a pergunta, quero acrescentar um pouco mais a ela

Me deparei com outra maneira de NxN matrix.

Índices de matriz ie jpodem ser representados como um único keyda seguinte maneira:

int key = i * N + j;
//map.put(key, a[i][j]); // queue.add(key); 

E os índices podem ser recuperados da keyseguinte maneira:

int i = key / N;
int j = key % N;

Uma solução simples é mapear uma chave em outro hashmap.
Mihai8

1
Por favor, não responda à pergunta na pergunta. Sua edição é interessante, fique à vontade para publicá-la como uma resposta.
Ole VV

@Crocode uau! a matemática por trás da resposta no Edit é cintilante. Basta saber se ele funciona em geral para quaisquer dois inteiros i e j.
likejudo

@Crocode i e j serão repetidos se forem múltiplos de N?
judô

Respostas:


190

Existem várias opções:

2 dimensões

Mapa de mapas

Map<Integer, Map<Integer, V>> map = //...
//...

map.get(2).get(5);

Objeto de chave de invólucro

public class Key {

    private final int x;
    private final int y;

    public Key(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Key)) return false;
        Key key = (Key) o;
        return x == key.x && y == key.y;
    }

    @Override
    public int hashCode() {
        int result = x;
        result = 31 * result + y;
        return result;
    }

}

Implementar equals()e hashCode()é crucial aqui. Então você simplesmente usa:

Map<Key, V> map = //...

e:

map.get(new Key(2, 5));

Table de goiaba

Table<Integer, Integer, V> table = HashBasedTable.create();
//...

table.get(2, 5);

Tableusa mapa de mapas embaixo.

N dimensões

Observe que a Keyclasse especial é a única abordagem que pode ser dimensionada para n dimensões. Você também pode considerar:

Map<List<Integer>, V> map = //...

mas isso é terrível do ponto de vista do desempenho, bem como legibilidade e correção (nenhuma maneira fácil de impor o tamanho da lista).

Talvez dê uma olhada no Scala onde você tem tuplas e caseclasses (substituindo a Keyclasse inteira por uma linha).


3
Olá, outros têm x'ou os dois valores ao fazer hashCode. Por que você usa 31? Eu pensei que tinha algo a ver com inteiros de 32 bits, mas quando penso sobre isso não faz sentido, porque x = 1 ey = 0 ainda mapeia para o mesmo hashcode de x = 0 ey = 31
pete

1
@pete Joshua Bloch, em Effective Java ch 3. s 9., recomenda, "1. Armazene algum valor constante diferente de zero, digamos, 17, em uma variável interna chamada resultado ...", que funciona melhor em termos de colisão se for um primo. Consulte também: stackoverflow.com/questions/3613102/…
fncomp

2
Em vez do objeto de chave Wrapper, por que não usar Map.Entry<K, V>como chave?
Roland

1
Sobre o quê Map<Pair<Key1, Key2>, Value>?
Joaquin Iurchuk

1
observe que hashCode()também pode ser implementado com uma única linha comoObjects.hash(x,y)
xdavidliu

23

Ao criar seu próprio objeto de par de chaves, você deve enfrentar algumas coisas.

Primeiro, você deve estar ciente de como implementar hashCode()e equals(). Você precisará fazer isso.

Em segundo lugar, ao implementar hashCode(), certifique-se de entender como funciona. O exemplo de usuário fornecido

public int hashCode() {
    return this.x ^ this.y;
}

é na verdade uma das piores implementações que você pode fazer. A razão é simples: você tem muitos hashes iguais! E o hashCode()deve retornar valores int que tendem a ser raros, únicos na melhor das hipóteses. Use algo assim:

public int hashCode() {
  return (X << 16) + Y;
}

Isso é rápido e retorna hashes exclusivos para chaves entre -2 ^ 16 e 2 ^ 16-1 (-65536 a 65535). Isso se encaixa em quase todos os casos. Muito raramente você está fora desses limites.

Terceiro, ao implementar equals() também saiba para que serve e esteja ciente de como você cria suas chaves, uma vez que são objetos. Freqüentemente, você faz coisas desnecessárias se as declarações causam, você sempre terá o mesmo resultado.

Se você criar chaves como esta: map.put(new Key(x,y),V);você nunca comparará as referências de suas chaves. Porque toda vez que você quiser acessar o mapa, você fará algo parecido map.get(new Key(x,y));. Portanto, você equals()não precisa de uma declaração como if (this == obj). Isso nunca vai acontecer .

Em vez de if (getClass() != obj.getClass())em seu equals()melhor uso if (!(obj instanceof this)). Será válido até mesmo para subclasses.

Então, a única coisa que você precisa comparar é realmente X e Y. Portanto, a melhor equals()implementação neste caso seria:

public boolean equals (final Object O) {
  if (!(O instanceof Key)) return false;
  if (((Key) O).X != X) return false;
  if (((Key) O).Y != Y) return false;
  return true;
}

Então, no final, sua aula principal é assim:

public class Key {

  public final int X;
  public final int Y;

  public Key(final int X, final int Y) {
    this.X = X;
    this.Y = Y;
  }

  public boolean equals (final Object O) {
    if (!(O instanceof Key)) return false;
    if (((Key) O).X != X) return false;
    if (((Key) O).Y != Y) return false;
    return true;
  }

  public int hashCode() {
    return (X << 16) + Y;
  }

}

Você pode fornecer seus índices de dimensão Xe Yum nível de acesso público, pois são finais e não contêm informações confidenciais. Não tenho 100% de certeza se o privatenível de acesso funciona corretamente em qualquer caso ao transmitir o Objectpara umKey .

Se você quer saber sobre as finais, eu declaro qualquer coisa como final cujo valor é definido na instanciação e nunca muda - e, portanto, é uma constante de objeto.


7

Você não pode ter um mapa hash com várias chaves, mas pode ter um objeto que recebe vários parâmetros como chave.

Crie um objeto chamado Índice que assume um valor x e y.

public class Index {

    private int x;
    private int y;

    public Index(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public int hashCode() {
        return this.x ^ this.y;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Index other = (Index) obj;
        if (x != other.x)
            return false;
        if (y != other.y)
            return false;
        return true;
    }
}

Então tenha o seu HashMap<Index, Value>para obter o seu resultado. :)


4
Você precisa substituir hashCodee equals.
Tom Hawtin - tackline

4
a implementação de hashCode não diferenciou entre (2,1) e (1,2)
user1947415

1
Isso é uma colisão. O Hashcode não precisa garantir um valor diferente para cada objeto diferente. @ user1947415
Ajak6

6

Implementado em coleções comuns MultiKeyMap


@Wilson Consertei o link agora, esperando a revisão por pares
computingfreak

@computingfreak parece que uma visão favorável veio. Viva! NB esta é a melhor resposta IMHO. A menos que você queira passar horas competindo com os engenheiros especialistas do Apache por alguma funcionalidade muito útil (como sempre), mas, em última análise, mundana.
roedor de microfone de

4

Duas possibilidades. Use uma chave combinada:

class MyKey {
    int firstIndex;
    int secondIndex;
    // important: override hashCode() and equals()
}

Ou um mapa do mapa:

Map<Integer, Map<Integer, Integer>> myMap;

2
Use um mapa de mapas apenas se você não se importar com o desempenho ou uso de memória aqui (ou seja, o mapa é pequeno), ou se houver muitas chaves com o mesmo primeiro índice - uma vez que esta solução significa pagar a sobrecarga de um objeto HashMap para cada primeiro índice único.
BeeOnRope

1
Para melhorar esta resposta, aqui estão informações sobre substituição hashCodee equalsmétodos.
Pshemo


1

Crie uma classe de valor que representará sua chave composta, como:

class Index2D {
  int first, second;

  // overrides equals and hashCode properly here
}

tomando cuidado para substituir equals()e hashCode()corretamente. Se isso parecer muito trabalhoso, você pode considerar alguns contêineres genéricos prontos, comoPair fornecidos pelo apache commons, entre outros.

Também há muitas perguntas semelhantes aqui, com outras ideias, como usar a Tabela de Guava , embora permita que as chaves tenham tipos diferentes, o que pode ser um exagero (no uso de memória e complexidade) no seu caso, pois entendo que suas chaves são inteiras.


1

Se forem dois inteiros, você pode tentar um truque rápido e sujo: Map<String, ?>usar a tecla comoi+"#"+j .

Se a chave i+"#"+jfor a mesma que j+"#"+itente min(i,j)+"#"+max(i,j).


2
Realmente má ideia. Em primeiro lugar, é simplesmente ruim. Em segundo lugar, esta técnica será copiada para outros tipos onde chaves diferentes podem ser mapeadas na mesma Stringcom consequências hilárias.
Tom Hawtin - tackline

Parece-me que i#j = j#i, quando i == jisso usando o min/maxtruque não vai fazer.
Matthieu

1
@Matthieu qual é a diferença entre 5#5e 5#5trocados?
enrey

@enrey None. Isso é o que eu estava apontando. Realmente depende do conhecimento que você tem sobre as suas chaves.
Matthieu

@Matthieu aha entendo o que você quer dizer. Eu acho que o que @arutaku quis dizer é quando você quer 5#3ter o mesmo hash 3#5, então você usa min / max para fazer cumprir 3#5esta ordem.
enrey

1

Você também pode usar a implementação da tabela goiaba para isso.

Tabela representa um mapa especial onde duas chaves podem ser especificadas de forma combinada para se referir a um único valor. É semelhante a criar um mapa de mapas.

//create a table
  Table<String, String, String> employeeTable = HashBasedTable.create();

  //initialize the table with employee details
  employeeTable.put("IBM", "101","Mahesh");
  employeeTable.put("IBM", "102","Ramesh");
  employeeTable.put("IBM", "103","Suresh");

  employeeTable.put("Microsoft", "111","Sohan");
  employeeTable.put("Microsoft", "112","Mohan");
  employeeTable.put("Microsoft", "113","Rohan");

  employeeTable.put("TCS", "121","Ram");
  employeeTable.put("TCS", "122","Shyam");
  employeeTable.put("TCS", "123","Sunil");

  //get Map corresponding to IBM
  Map<String,String> ibmEmployees =  employeeTable.row("IBM");

1

Você poderia criar seu objeto-chave da seguinte forma:

public class MapKey {

public  Object key1;
public Object key2;

public Object getKey1() {
    return key1;
}

public void setKey1(Object key1) {
    this.key1 = key1;
}

public Object getKey2() {
    return key2;
}

public void setKey2(Object key2) {
    this.key2 = key2;
}

public boolean equals(Object keyObject){

    if(keyObject==null)
        return false;

    if (keyObject.getClass()!= MapKey.class)
        return false;

    MapKey key = (MapKey)keyObject;

    if(key.key1!=null && this.key1==null)
        return false;

    if(key.key2 !=null && this.key2==null)
        return false;

    if(this.key1==null && key.key1 !=null)
        return false;

    if(this.key2==null && key.key2 !=null)
        return false;

    if(this.key1==null && key.key1==null && this.key2 !=null && key.key2 !=null)
        return this.key2.equals(key.key2);

    if(this.key2==null && key.key2==null && this.key1 !=null && key.key1 !=null)
        return this.key1.equals(key.key1);

    return (this.key1.equals(key.key1) && this.key2.equals(key2));
}

public int hashCode(){
    int key1HashCode=key1.hashCode();
    int key2HashCode=key2.hashCode();
    return key1HashCode >> 3 + key2HashCode << 5;
}

}

A vantagem disso é: sempre garantirá que você esteja cobrindo todos os cenários de iguais.

NOTA : Seu key1 e key2 devem ser imutáveis. Só então você será capaz de construir um objeto de chave estável.


1

podemos criar uma classe para passar mais de uma chave ou valor e o objeto desta classe pode ser usado como um parâmetro no mapa.

import java.io.BufferedReader; 
import java.io.FileReader;
import java.io.IOException;
import java.util.*;

 public class key1 {
    String b;
    String a;
    key1(String a,String b)
    {
        this.a=a;
        this.b=b;
    }
  }

public class read2 {

private static final String FILENAME = "E:/studies/JAVA/ReadFile_Project/nn.txt";

public static void main(String[] args) {

    BufferedReader br = null;
    FileReader fr = null;
    Map<key1,String> map=new HashMap<key1,String>();
    try {

        fr = new FileReader(FILENAME);
        br = new BufferedReader(fr);

        String sCurrentLine;

        br = new BufferedReader(new FileReader(FILENAME));

        while ((sCurrentLine = br.readLine()) != null) {
            String[] s1 = sCurrentLine.split(",");
            key1 k1 = new key1(s1[0],s1[2]);
            map.put(k1,s1[2]);
        }
        for(Map.Entry<key1,String> m:map.entrySet()){  
            key1 key = m.getKey();
            String s3 = m.getValue();
               System.out.println(key.a+","+key.b+" : "+s3);  
              }  
  //            }   
        } catch (IOException e) {

        e.printStackTrace();

    } finally {

        try {

            if (br != null)
                br.close();

            if (fr != null)
                fr.close();

        } catch (IOException ex) {

            ex.printStackTrace();

        }

    }

    }

 }

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.