黑马程序员技术交流社区

标题: 【上海校区】Java编程思想(四) [打印本页]

作者: 不二晨    时间: 2018-9-7 10:01
标题: 【上海校区】Java编程思想(四)
1 Set和存储顺序2 队列3 Map

  映射表(也称为关联数组Associative Array)。

3.1 性能

  HashMap使用了特殊的值,称作散列码(hash code),来取代对键的缓慢搜索。散列码是“相对唯一”的、用以代表对象的int值,它是通过将该对象的某些信息进行转换而生成的。
hashCode()是根类Object中的方法,因此所有对象都能产生散列码。

  对Map中使用的键的要求与对Set中的元素的要求一样:

4 散列与散列码

  HashMap使用equals()判断当前的键是否与表中存在的键相同。
  默认的Object.equals()只是比较对象的地址。如果要使用自己的类作为HashMap的键,必须同时重写hashCode()和equals()
  正确的equals()方法必须满足下列5个条件:

4.1 散列概念

  使用散列的目的在于:想要使用一个对象来查找另一个对象。
  Map的实现类使用散列是为了提高查询速度

散列的价值在于速度:散列使得查询得以快速进行。由于瓶颈位于查询速度,因此解决方案之一就是保持键的排序状态,然后使用Collections.binarySearch()进行查询。
散列则更进一步,它将键保存在某处,以便能够很快找到。存储一组元素最快的数据结构是数组,所以用它来表示键的信息(请小心留意,我是说键的信息,而不是键本身)。但是因为数组不能调整容量,因此就有一个问题:我们希望在Map中保存数量不确定的值,但是如果键的数量被数组的容量限制了,该怎么办?
答案就是:数组并不保存键本身。而是通过键对象生成一个数字,将其作为数组的下标。这个数字就是散列码,由定义在Object中的、且可能由你的类覆盖的hashCode()方法(在计算机科学的术语中称为散列函数)生成。
为解决数组容量固定的问题,不同的键可以产生相同的下标。也就是说,可能会有冲突,即散列码不必是独一无二的。因此,数组多大就不重要了,任何键总能在数组中找到它的位置。
4.2 理解散列

  综上,散列就是将一个对象生成一个数字保存下来(作为数组的下标),然后在查找这个对象时直接找到这个数字就可以了,所以散列的目的是为了提高查找速度,而手段是将一个对象生成的数字与其关联并保存下来(通过数组,称为散列表)。这个生成的数字就是散列码。而生成这个散列码的方法称为散列函数(hashCode())。

4.3 HashMap查询过程(快速原因)

  因此,HashMap中查询一个key的过程就是:

  因此,不是查询整个list,而是快速地跳到数组的某个位置,只对很少的元素进行比较。这便是HashMap会如此快速的原因

4.4 简单散列Map的实现package net.mrliuli.containers;import java.util.*;public class SimpleHashMap<K, V> extends AbstractMap<K, V> {    // 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<MapEntry<K,V>>[] 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<MapEntry<K,V>>();        }        LinkedList<MapEntry<K,V>> bucket = buckets[index];        MapEntry<K,V> pair = new MapEntry<K,V>(key, value);        boolean found = false;        V oldValue = null;        ListIterator<MapEntry<K,V>> it = bucket.listIterator();        while(it.hasNext()){            MapEntry<K,V> 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<K,V> iPair : buckets[index]){            if(iPair.getKey().equals(key)){                return iPair.getValue();            }        }        return null;    }    @Override    public Set<Map.Entry<K,V>> entrySet(){        Set<Map.Entry<K,V>> set = new HashSet<Map.Entry<K, V>>();        for(LinkedList<MapEntry<K,V>> bucket : buckets){            if(bucket == null) continue;            for(MapEntry<K,V> mpair : bucket){                set.add(mpair);            }        }        return set;    }    public static void main(String[] args){        SimpleHashMap<String, String> m = new SimpleHashMap<String, String>();        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());    }}4.5 覆盖hashCode()  设计`hashCode()`时要考虑的因素:
《Effective Java™ Programming Language Guide (Addison-Wesley, 2001)》为怎样写出一个像样的hashCode()给出了一个基本的指导:
域类型
计算

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()最后生成的结果,确保相同的对象有相同的散列码。

5 选择不同接口的实现5.1 微基准测试的危险(Microbenchmarking dangers)
已证明0.0是包含在Math.random()的输出中的,按照数学术语,即其范围是[0,1)。
5.2 HashMap的性能因子

  HashMap中的一些术语:

HashMap使用的默认负载因子是0.75(只有当表达到四分之三满时,才进行再散列),这个因子在时间和空间代价之间达到了平衡。更高的负载因子可以降低表所需的空间,但会增加查找代价,这很重要,因为查找是我们在大多数时间里所做的操作(包括get()和put())。
6 Collection或Map的同步控制

  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>());    }}6.1 快速报错(fail-fast)

  Java容器有一种保护机制能够防止多个进行同时修改同一个容器的内容。Java容器类类库采用快速报错(fail-fast)机制。它会探查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其他进程修改了容器,就会立刻抛出ConcurrentModificationException异常。这就是“快速报错”的意思——即,不是使用复杂的算法在事后来检查问题。

package net.mrliuli.containers;import java.util.*;public class FailFast {    public static void main(String[] args){        Collection<String> c = new ArrayList<>();        Iterator<String> it = c.iterator();        c.add("An Object");        try{            String s = it.next();        }catch(ConcurrentModificationException e){            System.out.println(e);        }    }}

作者: 不二晨    时间: 2018-9-13 16:09

很不错,受教了




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2