通常的:
当标准类库中的类被作用HashMap的键。它用的很好,因为它具备了键所需的全部性质。
当你自己创建用作HashMap的键的类,有可能会忘记在其中放置必须的方法,而这时通常会犯的一个错误。
例如:考虑一个天气系统,将Groundhog对象与Prediction对象联系起来。
class Groundhog { protected int number; public Groundhog(int n) { number = n; } public String toString() { return "Groundhog # " + number; } } class Prediction { private static Random rand = new Random(47); private boolean shadow = rand.nextDouble() > 0.5; @Override public String toString() { if (shadow) { return "Six more weks of Winter"; } else { return "Early Spring"; } } } public class Main { public static <T extends Groundhog> void detectSpring(Class<T> type) throws Exception { Constructor<T> ghog = type.getConstructor(int.class); Map<Groundhog, Prediction> map = new HashMap<Groundhog, Prediction>(); for (int i = 0; i < 10; i++) { map.put(ghog.newInstance(i), new Prediction()); System.out.println("map : " + map); } Groundhog gh = ghog.newInstance(3); System.out.println("Looking up prediction for " + gh); if (map.containsKey(gh)) { System.out.println(map.get(gh)); } else { System.out.println("Key not found : " + gh); } } public static void main(String[] args) throws Exception { map : {Groundhog # 0=Six more weks of Winter} map : {Groundhog # 0=Six more weks of Winter, Groundhog # 1=Six more weks of Winter} map : {Groundhog # 0=Six more weks of Winter, Groundhog # 2=Early Spring, Groundhog # 1=Six more weks of Winter} map : {Groundhog # 0=Six more weks of Winter, Groundhog # 3=Early Spring, Groundhog # 2=Early Spring, Groundhog # 1=Six more weks of Winter} map : {Groundhog # 0=Six more weks of Winter, Groundhog # 3=Early Spring, Groundhog # 4=Six more weks of Winter, Groundhog # 2=Early Spring, Groundhog # 1=Six more weks of Winter} map : {Groundhog # 0=Six more weks of Winter, Groundhog # 3=Early Spring, Groundhog # 4=Six more weks of Winter, Groundhog # 2=Early Spring, Groundhog # 1=Six more weks of Winter, Groundhog # 5=Early Spring} map : {Groundhog # 0=Six more weks of Winter, Groundhog # 3=Early Spring, Groundhog # 4=Six more weks of Winter, Groundhog # 2=Early Spring, Groundhog # 6=Early Spring, Groundhog # 1=Six more weks of Winter, Groundhog # 5=Early Spring} map : {Groundhog # 0=Six more weks of Winter, Groundhog # 3=Early Spring, Groundhog # 4=Six more weks of Winter, Groundhog # 2=Early Spring, Groundhog # 7=Early Spring, Groundhog # 6=Early Spring, Groundhog # 1=Six more weks of Winter, Groundhog # 5=Early Spring} map : {Groundhog # 0=Six more weks of Winter, Groundhog # 3=Early Spring, Groundhog # 4=Six more weks of Winter, Groundhog # 2=Early Spring, Groundhog # 7=Early Spring, Groundhog # 6=Early Spring, Groundhog # 1=Six more weks of Winter, Groundhog # 8=Six more weks of Winter, Groundhog # 5=Early Spring} map : {Groundhog # 0=Six more weks of Winter, Groundhog # 3=Early Spring, Groundhog # 4=Six more weks of Winter, Groundhog # 2=Early Spring, Groundhog # 9=Six more weks of Winter, Groundhog # 7=Early Spring, Groundhog # 6=Early Spring, Groundhog # 1=Six more weks of Winter, Groundhog # 8=Six more weks of Winter, Groundhog # 5=Early Spring} Looking up prediction for Groundhog # 3 Key not found : Groundhog # 3 detectSpring(Groundhog.class); } }首先会使用Groundhog和与之相关联的Prediction填充HashMap,然后打印此HashMap,以便观察它是否被填入了一些内容。然后使用标识数组3作为Groundhog的主键,查找与之对应的内容。
但是,它无法找到数字3这个键。问题出在Groundhog自动继承基类Object,所以这里使用Object的hashCode()生成散列码,而它默认的使用对象的地址计算散列码。因此,由Groundhog(3)生成的第一个实例的散列码与由Groundhog的第二个实例的散列码是不同的,而我们正是使用的后者进行查找的。
注意的:
可能你会认为,只需要编写恰当的hashCode()方法的覆盖版本即可。但是它仍然无法正常运行,除非你同时覆盖equals()方法,它也是Object的一部分。HashMap使用equals()判断判断当前的键是否与表中的键相同。
正确的equals()方法必须满足下列五个条件:
1,自反性。对任意x,x.equals(x)一定返回true。
2,对称性。对任意x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。
3,传递性。对任意x、y、z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true。
4,一致性。对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用多少次x.equals(y),返回的 结果应该保持一致,要么一直是true,要么一直是false。
5,对任何不适null的x,x.equals(null)一定返回false。
再次强调,默认的Object.equals()只是比较对象的地址,所以一个Groundhog(3)并不等于另一个Groundhog(3)。因此,要使用自己的类作为HashMap的键,必须同时重载hashCode()和equals()。
public class Groundhog2 extends Groundhog { public Groundhog2(int n) { super(n); } public int hashCode() { reutrn number;} public boolean equals(Object o) { return o instanceof Groundhog2 && (number == ((Groundhog2)o).number); } }另外需要注意的:
Groundhog2.hashCode()返回Groundhog的标识数字作为散列码。在此例中,程序员负责确保不同的Groundhog具有不同的编号。hashCode()并不需要总是返回唯一的标识码,但是equals()方法必须严格的判断两个对象是否相同。尽管看起来equals()方法只是检查其参数是否是Groundhog2的实例,但是instanceof悄悄的检查了此对象是否为null,因为如果instanceof左边的参数为null,它会返回false。
理解hashCode()
前面的例子只是正确解决问题的第一步。它只说明,如果不为你的键覆盖hashCode()和equals(),那么使用散列的数据结构(HashSet,HashMap,LinkedHashSet或LinkedHashMap)就无法正确处理你的键。然而,要很好的解决此问题,你必须了解这些数据结构的内部结构。
首先,使用散列的目的在于:想要使用一个对象来查找另一个对象。不过使用TreeMap或者你自己实现的Map也可以达到此目的。与散列实现相反,下面的示例用一对ArrayList实现了一个Map。
class MapEntry<K,V> implements Map.Entry<K, V> { private K key; private V value; public MapEntry(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } public V setValue(V value) { V result = this.value; this.value = value; return result; } @Override public int hashCode() { return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); } @Override public boolean equals(Object o) { if (!(o instanceof MapEntry)) { return false; } MapEntry me = (MapEntry) o; return (key == null ? me.getKey() == null : key.equals(me.getKey())) && (value == null ? me.getValue() == null : value.equals(me.getValue())); } @Override public String toString() { return key + " = " + value; } } class SlowMap<K,V> extends AbstractMap<K, V> { private List<K> keys = new ArrayList<K>(); private List<V> values = new ArrayList<V>(); public V put(K key, V value) { V oldValue = get(key); if (!keys.contains(key)) { keys.add(key); values.add(value); } else { values.set(keys.indexOf(key), value); } return oldValue; } public V get(Object key) { if (!keys.contains(key)) { return null; } return values.get(keys.indexOf(key)); } @Override public Set<java.util.Map.Entry<K, V>> entrySet() { Set<Map.Entry<K, V>> set = new HashSet<Map.Entry<K,V>>(); Iterator<K> ki = keys.iterator(); Iterator<V> vi = values.iterator(); while(ki.hasNext()) { set.add(new MapEntry<K, V>(ki.next(), vi.next())); } return set; } } public class Main { public static void main(String[] args) throws Exception { {ANGOLA=Luanda, CAPE VERDE=Praia, EGYPT=Cairo, BURUNDI=Bujumbura, BENIN=Porto-Novo, ALGERIA=Algiers, CAMEROON=Yaounde, CONGO=Brazzaville, CENTRAL AFRICAN REPUBLIC=Bangui, EQUATORIAL GUINEA=Malabo, COMOROS=Moroni, DJIBOUTI=Dijibouti, BURKINA FASO=Ouagadougou, CHAD=N'djamena, BOTSWANA=Gaberone} null [ANGOLA = Luanda, CAPE VERDE = Praia, EGYPT = Cairo, BURUNDI = Bujumbura, BENIN = Porto-Novo, ALGERIA = Algiers, CAMEROON = Yaounde, CONGO = Brazzaville, CENTRAL AFRICAN REPUBLIC = Bangui, EQUATORIAL GUINEA = Malabo, COMOROS = Moroni, DJIBOUTI = Dijibouti, BURKINA FASO = Ouagadougou, CHAD = N'djamena, BOTSWANA = Gaberone] SlowMap<String, String> m = new SlowMap<String, String>(); m.putAll(Countries.capitals(15)); System.out.println(m); System.out.println(m.get("BULGARIA")); System.out.println(m.entrySet()); } }Map.entrySet()方法必须产生一个Map.Entry对象集。但是,Map.Entry是一个接口,用来描述依赖于实现的结构,因此如果你想要创建自己的Map类型,就必须同时定义Map.Entry的实现。
容器深入研究 --- 散列与散列码(一),布布扣,bubuko.com