Java编程思想——第17章 容器深入研究(two)

时间:2021-05-08 03:58:59

六、队列

  排队,先进先出。除并发应用外Queue只有两个实现:LinkedList,PriorityQueue。他们的差异在于排序而非性能。

  一些常用方法:

  继承自Collection的方法:

  add 在尾部增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常

  remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常

  element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常

  自带的方法:

  这些更适用于缓冲和并发访问,最主要是不报异常啊

  offer 在尾部添加一个元素并返回true 如果队列已满,则返回false

  poll 移除并返问队列头部的元素 如果队列为空,则返回null

  peek 返回队列头部的元素 如果队列为空,则返回null

  put 添加一个元素 如果队列满,则阻塞

  take 移除并返回队列头部的元素 如果队列为空,则阻塞

七、理解Map

  标准的Java类库包含以几种基本实现:HashMap,TreeMap,LinkedHashMap,WeakHashMap,ConcurrentHashMap,IdentityHahMap.

1.性能

  普通的Map中get()方法呈线性搜索,执行速度相当慢,而hashMap使用了特殊的:散列码来取代对键缓慢的搜索。

散列码:“相对唯一”的,用以代表对象的int值。hashCode()是根类Object中的方法,所以所有Java对象都有散列码,HashMap就是使用对象的hashCode()进行快速查询的。

HashMap *:Map基于散列表的实现。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量和负载因子以调节容器的性能。最常用的Map。

LinkedHashMap:使用链表维护内部顺序,所以迭代访问快。get访问要慢一点点。

TreeMap:基于红黑树实现的。键会由Comparable或Comparator进行排序,是唯一带有subMap()方法的Map;

      TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(2, "two");
treeMap.put(1, "one");
treeMap.put(3, "three");
treeMap.put(4, "fore");
//fromKey-- 返回映射中键的低端点。
//fromInclusive-- true如果低端点要包含在返回的视图。
//toKey-- 返回映射中键的高端点。
//toInclusive-- 这为true如果高端点要包含在返回的视图。
NavigableMap<Integer, String> navigableMap = treeMap.subMap(1, true, 3, true);
System.out.println("values: " + navigableMap);
结果: values: {1=one, 2=two, 3=three}

WeakHashMap:弱键(weak key)映射,允许释放映射所指向的对象;如果映射之外没有引用指向某个"键",则此”键“可以被垃圾回收

ConcurrentHashMap:一种线程安全的Map.详见 Java编程思想——第21章 并发 读书笔记系列

IdentityHashMap:使用== 代替 equals()对键进行比较的散列映射。

  对Map的键要求于Set中的元素要求一样,任何键都要由一个equals()方法;如果是散列Map,键要实现hashCode()方法;如果是TreeMap,必须实现Comparable。

2.SortedMap

 TreeMap是现在的唯一实现,确保键处于排序状态,以下是由SortedMap提供的方法:

    //返回当前Map使用的Comparator
public Comparator<? super K> comparator() {
return comparator;
}
//返回Map的第一个Key
public K firstKey() {
return key(getFirstEntry());
}
//返回Map的最后一个Key
public K lastKey() {
return key(getLastEntry());
}
//生成Map子集 由fromKey(包含) 到 toKey(不包含)的键值组成
public SortedMap<K,V> subMap(K fromKey, K toKey) {
return subMap(fromKey, true, toKey, false);
}
//生成Map子集 由键小于toKey的键值组成
public SortedMap<K,V> headMap(K toKey) {
return headMap(toKey, false);
}
//生成Map子集 由键大于或等于fromKey的键值组成
public SortedMap<K,V> tailMap(K fromKey) {
return tailMap(fromKey, true);
}

3.LinkedHashMap

  为了提高速度LindedHashMap散列化所有的元素,但是遍历键值对时又以元素的插入顺序返回键值对。

  *可以在构造函数中设定LinkedHashMap,使之采用基于访问的最近最少使用(LRU)算法。

// 初始化大小,加权因子,true开启LRU算法 false插入顺序
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

八、散列与散列码

HashMap使用equals()判断当前的键是否与表中存在的键相同。

正确的equals()方法需满足一下条件:

1)自反性。x.equals(x) 是true;

2)对称性。x.equalse(y) 返回true y.equals(x)也得是true;

3)传递性。x.equals(y) 返回true ,y.equals(z) 返回true , x.equals(z)返回true;

4)一致性。如果对象中用于等价比较的信息没有变,那么无论多少次 x.equals(y)返回值不会变

5)x.equals(null) 返回 false ;注意:(null).equals(x)报空指针。

强调:默认的Object.equals()只比较对象的地址,因此如果使用自己的类作为HashMap的Key,必须同时重载hashCode()和equals()。

hashCode()并不需要总是能够返回唯一的标识码,但是equals()方法必须严格的判断两个对象是否相等,作为键必须唯一否则系统报错。

    @Override
public boolean equals(Object o) {
return o instanceof T && (i.equals(((T) o).i)));
}

instanceof检查了此对象是否为null ,是null则返回false。

为速度而散列

  以线性查询的是最慢的查询方式,存储一组元素最快的数据结构是数组,所以使用它来标识键的信息(注意,这里说的是键信息不是键本身)。由于数组不能调整容量,所以数组不保存键本身,而是通过键对象生成一个散列码,将其作为数组的下标,这个散列码就是由Object中的、或自己的类覆盖的hashCode()生成的。

  数组固定的问题解决了,但是键可以产生相同的下标,也就是说可能会有冲突。数组多大不重要,任何键总能在数组中找到它的位置。于是,查询一个值的过程首先就是计算散列码,然后使用散列码查找数组。如果能够保证没有冲突(如果被查询的值的数量是固定的,就有可能)。

  通常,冲突由外部链接处理;数组并不直接保存值,而是保存值的list。然后对list中的值使用equals()方法进行线性查询。这部分查找会比较慢,但是如果散列函数好的话,数组每个位置就有较少的值。

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

  我们把散列表的数组称为bucket(桶),为了散布均匀且速度快,桶的容积通常使用质数或者2的整数次方,用LinkedList填充桶。

put()操作,计算key的hashCode(),找到桶中的位置,看LinkedList内容,有值用equals()与值的key相比,相等就替换,不等或者没有值就在尾部加上新值。

覆盖hashCode()

  桶下标值是无法控制的,这个值依赖于具体的HashMap对象的容量,而容量的改变与容器的充满程度和负载因子有关。hashCode()生成的结果,经过畜栏里后成为桶位的下标。

Joshua Blochw指出为写出一份像样的hashCode给出了知道:

1)给 int 变量 result 赋予某个非0常量,

2)为对象内每个有意义的域f(既每个可以做equals()操作的域)计算一个int 散列码 c:

域类型:                                                                 计算:

boolean                                                                   c=(f?0:1)

byte、char、short、int                                           c=(int)f

float                                                                         c=(int)(f^(f>>>32))

double                                                                     long I =Double.doubleToLongBits(f); c=(int)(I^(I>>>32))

Object,其equals()调用这个域的equals()       c=f.hashCode()

数组                     对每个元素应用上述规则   

3)合并计算得到散列码

result = 37*result+c   

九、选择接口的不同实现

容器之间的区别通常归结于由什么数据结构实现的。

  比如:ArrayList和LinkedList都实现List接口,所以基本操作都是一样的。然而ArrayList底层是数组实现的,而LinkedList是双向链表实现的,其中每个对象包含数据的同时还包含直想链表中前一个和后一个元素的引用。因此更适合用于插入、删除多的操作。而随机访问就应该选择ArrayLIst,根据不同操作的性能选择实现

  再比如:TreeSet、HashSet、LinkedHashSet都实现Set接口。每种都有不同行为:HashSet查询速度最快;LinkedHashSet保持元素插入的次序;TreeSet基于TreeMap,生成一个处于排序状态的Set。所以根据不同行为选择的实现

对List的选择

  对于数组支撑的List和ArrayList,无论列表的大小如何,访问速度都是一样的快。而对于LinkedList,访问时间对于较大的列表将明显增加。所以操作随机访问类型的操作,数组结构要比链表结构更合适。

  当使用迭代器插入新元素时,对于ArrayList当列表变大时,开销变大。但对于LinkedList,并不会随着列表尺寸变化而明显变化。因为,ArrayList插入时,必须为数组扩展空间,并将引用向前移动。而LinkedList则只需要链接新的元素,而不必修改列表中剩余的元素。

  LinkedList对List的端点会进行特殊处理——这使得LinkedList在作用于Queue时,效率提高。LInkedList中的插入和移除代价相当低廉,并且不会随着列表尺寸发生变化,但是对于ArrayList插入操作的代价高昂,并且代价将随列表尺寸增加而增加。

  对于随机访问get() 和 set() 操作,ArrayList明显速度快于LinkedList,因为LInkedLIst不是针对随机访问设计的。

  最佳的做法时选择ArrayList,只有经常插入和删除而影响性能时才会选择LinkedList.

对Set的选择

  HashSet的性能总比TreeSet好,特别是在添加和删除元素时,而这两个操作更为重要。TreeSet唯一好吃就是可以维持元素的排序;因为排序 所以TreeSet的迭代通常比HashSet快。

  对于插入LinkedHashSet要比HashSet代价高;因为要额外维护链表所带来的额外开销。

对Map的选择

  除了IdentityHashMap外,所有的Map实现的插入操作都会随着Map尺寸的变大而明显变慢,但是查找操作代价要小得多。

  TreeMap通常比HashMap慢,TreeMap是一种创建有序列表的方式。树的行为是:保证有序,并且不必进行特殊排序。一旦填充TreeMap,就可以通过keySet()方法获取键的Set试图,然后调用toArray()形成键的数组。

  当使用Map时HahsMap应该是首选,除非需要Map始终保持有序时使用TreeMap。

  LinkedHashMap在插入时比HashMap慢一点,因为在维护散列数据结构得同时还要维护链表,也因此迭代速度更快。

  IdentityHashMap具有完全不同的性能因为使用== 而不是 equals()来比较元素。

HashMap的性能因子

  可以通过手动调整HashMap提高性能,这里有些术语必须了解:

  容量:表中的桶位。

  初始容量:表在创建时所拥有的桶位数。HashMap和HashSet都可以通过构造函数指定初始化容量。

  尺寸:表中当前存储的项数。

  负载因子:尺寸/容量。空表的负载因子是0,半满表的负载因子是0.5,负载轻的表冲突可能性小,因此插入和查找更快,迭代则慢一些。HashMap和HashSet都具有指定负载因子的构造器,当负载达到该负载因子水平时,容器会自动增加容量,实现方式是使容量大致加倍,并重新将现有对象分布到新的桶位集中(再散列)。

  HashMap使用默认的负载因子是0.75,更高的负载因子会增加查找代价。

十、实用方法

  Collections类(注意不是Collection)内部有很多卓越的的静态方法:

public static <T extends Object & Comparable<? super T>> T max/min(Collection<? extends T> coll) 返回Collection中最大或最小的元素-采用Collection内置的自然比较法,
                               (Collection<? extends T> coll, Comparator<? super T> comp) - 采用Comparator进行比较。
public static int indexOfSubList/lastIndexOfSubList(List<?> source, List<?> target) 返回target在source中第一次/最后一次出现的位置,找不到返回-1
public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal) 使用newVal替换oldVal
public static void reverse(List<?> list) 逆转所有元素次序
public static <T> Comparator<T> reverseOrder() 返回一个排序规则 逆转自然顺序 例:TreeSet tr=new TreeSet(Collections.reverseOrder());
public static <T> Comparator<T> reverseOrder(Comparator<T> cmp) 逆转参数的顺序 例:TreeSet tr=new TreeSet(Collections.reverseOrder(new StrLenComparator()));
public static void rotate(List<?> list, int distance) 所有元素向后移动distance个位置,将末尾元素移到前面。
public static void shuffle(List<?> list) 随机改变指定列表顺序 参数列表:(List<?> list, Random rnd)时可使用自己的随机机制
public static <T> void sort(List<T> list) 使用List中的自然排序 参数列表:(List<T> list, Comparator<? super T> c) 时,利用参数中排序规则排序
public static <T> void copy(List<? super T> dest, List<? extends T> src) 将src中的元素复制到dest
public static void swap(List<?> list, int i, int j) 替换list中位置i和位置j的元素
public static <T> void fill(List<? super T> list, T obj) 用元素x替换list中的元素
public static boolean disjoint(Collection<?> c1, Collection<?> c2) 两个集合没有任何相同元素时 返回true
public static int frequency(Collection<?> c, Object o) 返回集合中等于o的元素个数
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) 在有排序的list中查找key元素的位置
public static <T> List<T> nCopies(int n, T o) 返回大小为n的List,且List不可改变,o为List中元素
emptyList()/emptyMap()/emptySet()返回不可变的空集合
singleton(T t)/singleList(T t)/singleMap(K key,V value) 产生不可变的集合,只包含参数中的元素

设定Collection或Map为不可修改

  对“不可修改的”方法调用并不会产生编译时检查,但是完成转换后,任何会改变容器内容的操作都会引起UnsupportedOperationException异常。

  不可修改:Collections.unmodifiableXXX(XXX); 比如:Collections.unmodifiableList(new ArrayList(data));

同步

  Collections.synchronizedXXX(XXX) 比如:Collections.synchronizedList(new ArrayList(data)) ; 一旦发现两个线程同时操作就会抛出ConcurrentMdificationException