Java编程思想(一)第1~13、16章
Java编程思想(二)第14章-类型信息
Java编程思想(三)第15章-泛型
Java编程思想(四)第17章-容器深入研究
Java编程思想(五)第18章-Java IO系统
Java编程思想(六)第19章-枚举类型
Java编程思想(七)第20章-注解
Java编程思想(八)第21章-并发
Set
的元素必须定义equals()
方法以确保对象的唯一性。hashCode()
只有这个类被置于HashSet
或者LinkedHashSet
中时才是必需的。但是对于良好的编程风格而言,你应该在覆盖equals()方法时,总是同时覆盖hashCode()方法。SortedSet
(TreeSet
是其唯一实现),那么它必须实现Comparable
接口。SortedSet
的意思是“按对象的比较函数对元素排序”,而不是指“元素插入的次序”。插入顺序用LinkedHashSet
来保存。LinkiedList
和PriorityQueue
,它们仅有排序行为的差异,性能上没有差异。PriorityQueue
的排列顺序也是通过实现Comparable
而进行控制的。映射表(也称为关联数组Associative Array)。
HashMap
使用了特殊的值,称作散列码(hash code),来取代对键的缓慢搜索。散列码是“相对唯一”的、用以代表对象的int
值,它是通过将该对象的某些信息进行转换而生成的。
hashCode()是根类Object中的方法,因此所有对象都能产生散列码。
对Map中使用的键的要求与对Set中的元素的要求一样:
equals()
方法;hashCode()
方法;TreeMap
,那么它必须实现Comparable
。 HashMap使用equals()
判断当前的键是否与表中存在的键相同。
默认的Object.equals()只是比较对象的地址。如果要使用自己的类作为HashMap的键,必须同时重写hashCode()
和equals()
。
正确的equals()
方法必须满足下列5个条件:
x
,x.equals(null)
一定返回false
。 使用散列的目的在于:想要使用一个对象来查找另一个对象。
Map的实现类使用散列是为了提高查询速度。
散列的价值在于速度:散列使得查询得以快速进行。由于瓶颈位于查询速度,因此解决方案之一就是保持键的排序状态,然后使用
Collections.binarySearch()
进行查询。散列则更进一步,它将键保存在某处,以便能够很快找到。存储一组元素最快的数据结构是数组,所以用它来表示键的信息(请小心留意,我是说键的信息,而不是键本身)。但是因为数组不能调整容量,因此就有一个问题:我们希望在Map中保存数量不确定的值,但是如果键的数量被数组的容量限制了,该怎么办?
答案就是:数组并不保存键本身。而是通过键对象生成一个数字,将其作为数组的下标。这个数字就是散列码,由定义在Object中的、且可能由你的类覆盖的
hashCode()
方法(在计算机科学的术语中称为散列函数)生成。为解决数组容量固定的问题,不同的键可以产生相同的下标。也就是说,可能会有冲突,即散列码不必是独一无二的。因此,数组多大就不重要了,任何键总能在数组中找到它的位置。
综上,散列就是将一个对象生成一个数字保存下来(作为数组的下标),然后在查找这个对象时直接找到这个数字就可以了,所以散列的目的是为了提高查找速度,而手段是将一个对象生成的数字与其关联并保存下来(通过数组,称为散列表)。这个生成的数字就是散列码。而生成这个散列码的方法称为散列函数(hashCode()
)。
因此,HashMap
中查询一个key
的过程就是:
list
,然后对list
中的值使用equals()
方法进行线性查询。 因此,不是查询整个list
,而是快速地跳到数组的某个位置,只对很少的元素进行比较。这便是HashMap
会如此快速的原因。
事实证明,质数实际上并不是散列桶的理想容量。近来,(通过广泛的测试)Java的散列函数都使用 2的整数次方。对现代处理器来说,除法与求余数是最慢的操作。使用2的整数次方长度的散列表,可用 掩码代替除法。因为get()是使用最多的操作,求余数的%操作是其开销最大的部分,而使用2的整数次方可以消除此开销(也可能对hashCode()有些影响)。
package net.mrliuli.containers;
import java.util.*;
public class SimpleHashMap extends AbstractMap {
// Choose a prime number for the hash table size, to achieve a uniform distribution:
static final int SIZE = 997;
// You can't have a physical array of generics, but you can upcast to one:
@SuppressWarnings("unchecked")
LinkedList>[] buckets = new LinkedList[SIZE];
@Override
public V put(K key, V value){
int index = Math.abs(key.hashCode()) % SIZE;
if(buckets[index] == null){
buckets[index] = new LinkedList>();
}
LinkedList> bucket = buckets[index];
MapEntry pair = new MapEntry(key, value);
boolean found = false;
V oldValue = null;
ListIterator> it = bucket.listIterator();
while(it.hasNext()){
MapEntry iPair = it.next();
if(iPair.equals(key)){
oldValue = iPair.getValue();
it.set(pair); // Replace old with new
found = true;
break;
}
}
if(!found){
buckets[index].add(pair);
}
return oldValue;
}
@Override
public V get(Object key){
int index = Math.abs(key.hashCode()) % SIZE;
if(buckets[index] == null) return null;
for(MapEntry iPair : buckets[index]){
if(iPair.getKey().equals(key)){
return iPair.getValue();
}
}
return null;
}
@Override
public Set> entrySet(){
Set> set = new HashSet>();
for(LinkedList> bucket : buckets){
if(bucket == null) continue;
for(MapEntry mpair : bucket){
set.add(mpair);
}
}
return set;
}
public static void main(String[] args){
SimpleHashMap m = new SimpleHashMap();
for(String s : "to be or not to be is a question".split(" ")){
m.put(s, s);
System.out.println(m);
}
System.out.println(m);
System.out.println(m.get("be"));
System.out.println(m.entrySet());
}
}
《Effective Java™ Programming Language Guide (Addison-Wesley, 2001)》为怎样写出一个像样的hashCode()给出了一个基本的指导:
int
变量result
赋予一个非零值常量,如17
f
(即每个可以做equals()
操作的域)计算出一个int
散列码c
:域类型 | 计算 |
---|---|
boolean | c=(f?0:1) |
byte、char、short或int | c=(int)f |
long | c=(int)(f^(f>>>32)) |
float | c=Float.floatToIntBits(f); |
double | long l = Double.doubleToLongBits(f); |
Object,其equals()调用这个域的equals() | c=f.hashCode() |
数组 | 对每个元素应用上述规则 |
3. 合并计算散列码:result = 37 * result + c;
4. 返回result。
5. 检查hashCode()
最后生成的结果,确保相同的对象有相同的散列码。
已证明
0.0
是包含在Math.random()
的输出中的,按照数学术语,即其范围是[0,1)
。
HashMap中的一些术语:
HashMap
和HashSet
都具有允许你指定初始容量的构造器。0
,而半满表的负载因子是0.5
,依此类推。负载轻的表产生冲突的可能性小,因此对于插入和查找都是最理想的(但是会减慢使用迭代器进行遍历的过程)。HashMap
和HashSet
都具有允许你指定负载因子的构造器,表示当负载情况达到该负载的水平时,容器将自动增加其容量(桶位数),实现方式是使容量大致加倍,并重新将现有对象分布到新的桶位集中(这被称为再散列)。
HashMap
使用的默认负载因子是0.75
(只有当表达到四分之三满时,才进行再散列),这个因子在时间和空间代价之间达到了平衡。更高的负载因子可以降低表所需的空间,但会增加查找代价,这很重要,因为查找是我们在大多数时间里所做的操作(包括get()
和put()
)。
Collections类有办法能够自动同步整个容器。其语法与“不可修改的”方法相似:
package net.mrliuli.containers;
import java.util.*;
public class Synchronization {
public static void main(String[] args){
Collection<String> c = Collections.synchronizedCollection(new ArrayList<String>());
List<String> list = Collections.synchronizedList(new ArrayList<String>());
Set<String> s = Collections.synchronizedSet(new HashSet<String>());
Set<String> ss = Collections.synchronizedSortedSet(new TreeSet<String>());
Map<String, String> m = Collections.synchronizedMap(new HashMap<String, String>());
Map<String, String> sm = Collections.synchronizedSortedMap(new TreeMap<String, String>());
}
}
Java容器有一种保护机制能够防止多个进行同时修改同一个容器的内容。Java容器类类库采用快速报错(fail-fast)机制。它会探查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其他进程修改了容器,就会立刻抛出ConcurrentModificationException
异常。这就是“快速报错”的意思——即,不是使用复杂的算法在事后来检查问题。
package net.mrliuli.containers;
import java.util.*;
public class FailFast {
public static void main(String[] args){
Collection c = new ArrayList<>();
Iterator it = c.iterator();
c.add("An Object");
try{
String s = it.next();
}catch(ConcurrentModificationException e){
System.out.println(e);
}
}
}