Comparable和Comparator接口区别
- Comparable & Comparator 都是用来实现集合中元素的比较、排序的,只是 Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序,所以,如想实现排序,就需要在集合外定义 Comparator 接口的方法或在集合内实现 Comparable 接口的方法
Comparable:一个实现了comparable接口的对象的实例可以被用于和相同类的不同实例做对比。它本身必须实现java.lang.Comparable的接口,这样它就拥有了对比的能力;java.util.Collections.sort(List) 和 java.util.Arrays.sort(Object[]) 方法被用来排列使用内在排序(natural ordering)方式的对象.
Comparator:一个实现了comparator接口的对象能够对比不同的对象。它不能用于同一个类的不同实例的对比,但是可以用于其他的类的实例做对比。它必须实现java.util.Comparator的接口;java.util.Collections.sort(List, Comparator) 和 java.util.Arrays.sort(Object[], Comparator)方法在Comparator如果可供比较的时候会被用到.
两者的接口实现差异:
java.lang.Comparable: int compareTo(Object o1)当前对象与o1对象做对比,返回int值:
positive – 当前对象大于o1
zero – 当前对象等于o1
negative – 当前对象小于o1
java.util.Comparator: int compare(Object o1, Objecto2)
用于o1与o2对象做对比,返回int值:
positive – o1大于o2
zero – o1等于o2
negative – o1小于o2
实例:
内在ID顺序排序public class Employee implements Comparable<Employee> { private int empId; private String name; private int age; /** * Compare a given Employee with this object. * If employee id of this object is * greater than the received object, * then this object is greater than the other. */ public int compareTo(Employee o) { return this.empId - o.empId ; } …. }
按照name排序
public class EmpSortByName implements Comparator<Employee>{
public int compare(Employee o1, Employee o2) {
return o1.getName().compareTo(o2.getName());
}
}
Collections.sort()与Arrays.sort()
Arrays.sort()在其内部实现上针对数组元素的不同类型采用不同的排序方式: ①基本类型数据(int, short, long, float, double等)采用QuickSort, 而对对象类型采取MergeSort. Why? 我们知道QuickSort在统计意义上效率比MergeSort高,为何不都采用 QuickSort ?
Answer: 概括的说,一个是稳定性,一个是移动次数。使用不同类型的排序算法主要是由于 quick sort 是不稳定的,而 merge sort 是 stable 的。这里的 stable 是指比较相等的数据在排序之后仍然按照排序之前的前后顺序排列(保序性)。对于基本数据类型,稳定性没有意义。而对于对象类型,稳定性是比较重要的,因为对象相等的判断可能只是判断关键属性,最好保持相等对象的非关键属性的顺序与排序前一直;另外一个原因是由于合并排序相对而言比较次数比快速排序少,移动(对象引用的移动)次数比快速排序多,而对于对象来说,比较一般比移动耗时。MergeSort:()1)最坏时间复杂度是 O(nlgn);(2)平均时间复杂度是 O(nlgn);(3)空间复杂度是 O(n)。
QuickSort: (1)最坏时间复杂度是 O(n^2);(2)平均时间复杂度是 O(nlgn);(3)空间复杂度最坏是 O(n),均匀就是O(logn)。
Collections.sort()主要是针对集合的排序,比如List, 其内部实现如下:
public static <T extends Comparable<? super T>> void sort(List<T> list) { Object[] a = list.toArray(); Arrays.sort(a); ListIterator<T> i = list.listIterator(); for (int j=0; j<a.length; j++) { i.next(); i.set((T)a[j]); } }
可以看出其内部实际采用的就是Arrays.sort()
HashMap与LinkedHashMap,TreeMap
HashMap实现原理内部对键值对的存储结构使用的是数组+链表+红黑树的形式(JDK1.8增加了红黑树部分,当一个链表长度>8就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能),每一个数组元素都是一个键值对,分别都对应着一个链表结构
- put存储元素HashMap在存储Entry对象的时候,首先根据Key的hashCode()方法得到其hashCode值,然后根据高位运算和取模运算来判定将存储到Entry[]数组的哪一个位置对应的链表上O(1),然后调用key的Equals方法去遍历链表比较key值,如果找到则更新value并返回旧址,否则将新的Entry加入到该链表头部
- 内部结构图:
- 遍历顺序也就是说Entry
对象随机地分配到某个Entry[] table数组的元素表示的链表上, HashMap内部Entry 的遍历顺序是对Entry[] table 数组,从index=0开始,依次遍历table[i]上的链表上的Entry对象;所以对HashMap遍历Entry对象的顺序和Entry对象的存储顺序之间没有任何关系. - 重新hash调节大小HashMap允许null键插入,其内部会将null存到table[0]中进行处理. 内部有负载因子和数组长度、size等参数,当map中元素达到16*0.75=12时,因为链表长度可能很长导致查找效率降低,hashmap会对原来的所有元素重新进行hash,数组容量变为原来的2倍,当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组;重新hash时链表顺序会被反转,可以避免尾部遍历(1.8中不会倒置,也不会重新计算哈希).HashMap; 在HashMap调整大小时会出现条件竞争,也就是多个线程同时来调整大小,出现死循环.如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
- key不可变Map的key值应该使用不可变对象,以防put和get的时候对象发生变化从而导致hashcode变化,取不到正确的对象;HashMap也可以声明为同步的,使用Map m = Collections.synchronizeMap(hashMap);
- Fail-Fast机制:在使用迭代器的过程中有其他线程修改了map结构,那么将抛出ConcurrentModificationException,这就是所谓fail-fast机制。这一机制在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap元素个数的修改都将增加这个值;在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map中元素个数.例如:HashMap在foreach迭代器循环遍历时如果其他线程对该map进行结构上的更改(比如进行add和map.remove元素操作,而不是简单的修改集合元素的内容)会导致ConcurrrentModificationEaception
- 遍历区别①for循环遍历时删除或者增加某个元素只会导致数组越界(rangeCheck函数),或者一直add生成无穷序列以及遍历不完整 ②使用Iterator遍历或者foreach遍历时如果采用iterator.remove()不会报错,因为他会重新设置expectedModCount=modCount,如果采用list.remove()则会出现ConcurrrentModificationException
LinkedHashMap是HashMap的子类,它可以实现对容器内Entry的存储顺序和对Entry的遍历顺序保持一致; LinkedHashMap内部使用了一个Entry类型的双向链表,用这个双向链表记录Entry的存储顺序。当需要对该Map进行遍历的时候,实际上是遍历的是这个双向链表。
内部结构图:
遍历方式:
//构造函数的默认值为false,按照插入顺序排序,先进先出; //true代表按照访问顺序排序,那么调用get方法后,内部会调用recordAccess方法记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除,不断访问可以形成按访问顺序排序的链表;可以重写removeEldestEntry方法返回true值指定插入元素时移除最老的元素。 Map<String, String> linkedHashMap = new LinkedHashMap<String, String>(16,0.75f,true); Iterator<Map.Entry> iterator= linkedHashMap.entrySet().iterator(); while(iterator.hasNext()) { Map.Entry entry = iterator.next(); System.out.println(entry.getKey()+":"+entry.getValue()); } for(Entry<String, Integer> entry : linkedHashMap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
TreeMap实现SortedMap接口,可以保证元素的排列以key值有序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。内部实现采用红黑树(一种平衡排序二叉树),循环遍历时直接中序遍历树输出。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
HashTable,Hashset和ConcurrentHashMap
- HashTable实现Map接口(和HashMap一样)不允许null键的存在,他是一个synchronized的map,因为内部的put,get方法进行了同步所以导致效率降低,线程安全性强于ConcurrentHashMap
- HashSet实现set接口,不允许重复元素的存在,但是允许null存在;每次add之前会先调用hashcode和equals方法去判断该对象是否已经存在,已存在则返回false,不会更改set中任何内容;存取速度没有HashMap快;但是其内部实现就是一个HashMap,只是每一个
中的value都是PRESET - HashMap和 HashTable 的区别:
- 两者都实现Map接口,但是HashTable继承Dictionary类,HashMap则是继承AbstractMap接口;
- HashTable是线程安全的(通过直接对get,put等方法加Synchronized锁来实现),HashMap则是线程不安全的;
- HashMap可以让你将空值作为一个表的条目的key或value,但是HashTable不可以;
- 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值;
- 两个遍历方式的内部实现上不同,Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式
- ConcurrentHashMap是一个Java5以后才提供的同步容器,其内部实现采用二次hash,分区同步的方式,内部默认维护一个包含16个segment对象的数组,每个数组对象又对应一个桶数组(相当于一个HashMap),每个桶数组对象又维护着一个HashEntry链表;对于put的hashcode相同的key会出现同步阻塞,因为它会将当前hashcode对应的segment锁住,但是hashcode不相同的key插入时是不会出现同步阻塞的,这样相对于HashTable(同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器)就可以大大提高效率,提高了并发性,相当于16个线程同时操作。
- ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得大多数时候,读操作不需要加锁就可以正确获得值
- 其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁,而是分段加锁
- 源码中containsValue和size操作实现原理: ConcurrentHashMap的size操作是在多个Segment中进行,但是也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁。具体怎么做的呢???Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历了
- hash函数: 用到了Wang/Jenkins hash算法的变种,主要的目的是为了减少哈希冲突,使元素能够均匀的分布在不同的Segment上,从而提高容器的存取效率。假如哈希的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义
ArrayList,CopyOnWriteArrayList和LinkedList
- ArrayList、LinkedList、Vector的区别:ArrayList 和Vector底层是采用数组方式存储数据,ArrayList可以通过
List list = Collections.synchronizeList(arrayList);
实现同步;Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差,LinkedList使用双向链表实现存储,随机存取比较慢,但是增删非常快,LinkedList中包含了 first 和 last 两个指针(Node)。Node 中包含了上一个节点和下一个节点的引用,这样就构成了双向的链表 - CopyOnWriteArrayList: CopyOnWriteArrayList的核心思想是利用高并发往往是读多写少的特性,对读操作不加锁,对写操作加锁,进行写操作时先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性。这种“写入时复制”策略,对容器的写操作将导致的容器中基本数组的复制,性能开销较大。所以在有写操作的情况下CopyOnWriteArrayList性能不佳,而且如果容器容量较大的话容易造成溢出
- LinkedBlockingQueue: 它是线程安全的阻塞队列,是作为生产者消费者的首选,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来
- ConcurrentLinkedQueue: ConcurrentLinkedQueue是一个无锁的并发线程安全的非阻塞队列;Queue中元素按FIFO原则进行排序.采用CAS操作,来保证元素的一致性