JAVA集合常见问题

作者:wuog
链接:https://www.acwing.com/blog/content/490/#comment_802
来源:AcWing

集合

1,Vector和ArrayList的区别?

  1)Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要 影响性能,因此,ArrayList的性能比Vector好。
  2)当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加大 约1.5的大小,这样ArrayList就有利于节约内存空间。
  3)Vector可以设置capacityIncrement(容量增长的参数),而ArrayList不可以。
  4)List<> data=Collections.synchronizedList(new ArrayList<>()); 可以解决ArrayList的线程安全问题,或者使用ThreadLocal。

    使用方法如下:

      假如你创建的代码如下:List<Map<String,Object>> data=new ArrayList<Map<String,Object>>();

      那么为了解决这个线程安全问题你可以这么使用Collections.synchronizedList(),如:

      List<Map<String,Object>> data=Collections.synchronizedList(new ArrayList<Map<String,Object>>());

2,ArrayrList和LinkedList的区别?

  1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
  2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
  3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

3,ArrayList的动态扩容?

1,在JKD1.6中,如果通过无参构造的话,初始数组容量为10。每次通过copeOf的方式扩容后容量为原来的1.5倍加1。
2,在JDK1.7中,如果通过无参构造的话,初始数组容量为0,当真正对数组进行添加时,才真正分配容量,每次按照大约1.5倍(位运算)的比率通过copeOf的方式扩容。
3,在JKD1.8中,arraylist这个类中,扩容 调用的是grow()方法,通过grow()方法中调用的Arrays.copyof()方法进行对原数组的复制,在通过调用System.arraycopy()方法进行复制,达到扩容的目的。

4,HashMap和TreeMap的区别?

1、实现
  TreeMap:实现了SortMap接口,基于红黑树。

        HashMap:基于哈希散列表实现,内部是一个数组,每个数组内是一个链表,链表可以扩展为红黑树 

2、存储
  TreeMap:默认按键的升序排序
  HashMap:随机存
3、遍历
  TreeMap:Iterator遍历是排序的
  HashMap:Iterator遍历是随机的
4、性能损耗
  TreeMap:插入、删除速度慢,需要维护树的平衡
  HashMap:基本无损耗
5、键值对
  TreeMap:键、值都不能为null
  HashMap:键、值均可为null
6、安全
  TreeMap:非并发安全Map
  HashMap:非并发安全Map
7、效率
  TreeMap:低
  HashMap:高

5,HashMap和Hashtable有什么区别?

1、HashMap是非线程安全的,HashTable是线程安全的。

2、HashMap的键和值都允许有null值存在,而HashTable则不行。

3、因为线程安全的问题,HashMap效率比HashTable的要高。

4、Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。

一般现在不建议用HashTable,因为: 

1,是HashTable是遗留类,内部实现很多没优化和冗余。
2,HashTable内部是全部加了Syn锁,重量级锁,效率会很低。
3,即使在多线程环境下,现在也有同步的ConcurrentHashMap替代,没有必要因为是多线程而用HashTable。

6,HashMap和LinkedHashMap有什么区别?

LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录是先插入的,也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会 比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关,如果需要输出的顺序和输入的相同,那么 用LinkedHashMap 可以实现,它还可以按读取顺序来排列。

7,HashMap的实现?

hashmap底层:链表数组,初始容量16扩容阀值数组长度乘以0.75,到这个阀值,扩容两倍如果当前map大小大于64,并且同一hash链表长度大 于8,同一哈希值冲突部分自动转换为红黑树如果小于64就不能转红黑树,直接扩容。根据key的哈希值计算出存放数组的索引位置。同一哈希值用链表形式向下存储,不同的key有可能哈希值一样。
链表是单向链表。若链表元素个数小于等于6时,树结构还原成链表HashMap在多线程的环境下的put操作容易引起死循环(死链),HahsMap里面 的Entry链表会产生环形的数据结构,链表成了一个环,会一直在循环,是不安全的,但是HashTable效率太低了,所以多线程下一般使用CurrentHashMap。
注意:构造函数不会初始化数组,在put的时候进行初始化

hashmap在多线程下形成死循环(在扩容的时候会发生死链):(待补充

因为在hashmap扩容的过程中,会发生链表上的元素的位置发生改变,当hashmap在多线程的情况下,put元素,可能会发生扩 容,当扩容的时候,如果一个链表上有A和B两个元素,线程1将A和B的位置改变还没执行完的时候,这时候线程2也将A和B的位置改变,当线程2还没执行完的时候线程1执行完 了,将A和B的位置彻底改变了,这时候线程2就会出现B.next=A; A.next=B的情况,形成死链。

8,为什么HashMap链表长度超过8会转成树结构?

1,纯链表的平均查找长度为(n+1)/2,红黑树平均查找长度则为log2n。长度为8的时候,红黑树平均查找长度为3,链 表平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,速度已经很快,并且转化为树结构和生成树的时间不会很短,所以没必要转成红黑树。

2,需要注意的是,HashMap的链表长度超过8时不一定会直接转为树结构。只有当前map大小大于64,此时插入数据导致某一链表的长度达到8时,该哈希值冲突部分才会自动转换为红黑树。如果map的大小于64就不会转红黑树,直接对HashMap扩容。

3,选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一 个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

9,HashSet和TreeSet的区别?

1、TreeSet是二叉树实现的(内部基于TreeMap,数据结构和TreeMap一致),Treeset中的数据是自动排好序的,不允许放入null值
2、HashSet是哈希表 实现的(内部基于HashMap,数据结构和HashMap一致),HashSet中的数据是无序的,可以放入null,但只能放入一个null。
3、HashSet要求放入的对象必须实 现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的对象,hashcode一 样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例。
4、TreeSet为基本操作(add、remove和contains)的时间复杂度是log(n)。另外,TreeSet是非同步的。它的iterator方法返回的迭代器是fail-fast的。

10,哈希?

哈希,又称为散列。是一个任意长度的输入,通过散列算法,得到一个固定长度的输出的值,也叫作压缩映射,容易产生哈希碰撞,即是可能出现同一个哈希值。比如:直接取余法就是一种hash算法。
解决哈希冲突,通常是:

1,开放寻址
2,再散列
3,链地址法(解决hashmap的hash冲突的时候采用这种方式) 注意:MD4,MD5,sha之类的所谓的加密算法,不是加密算法而是哈希算法,而且是不可逆 的,网上那些可以逆的网站,是因为他们把常见的一些加密出来的密码统计起来了,然后做成了一张彩虹表。

10,位运算?

Java实际保存int型时,正数 第31位 =0。 负数:第31位=1。 int类型四个字节,一个字节8位,一共32位。 常用位运算有:

位与 & (1&1=1 1&0=0 0&0=0)
位或 | (1|1=1 1|0=1 0|0=0)
位非 ~ ( ~1=0 ~0=1)
位异或 ^ (1^1=0 1^0=1 0^0=0)
<<有符号左移 

>>有符号的右移 

>>>无符号右移
例如:8 << 2 = 32

           8>>2 = 2

取模的操作

a % (Math.pow(2,n)) 等价于 a&( Math.pow(2,n)-1)

a%(x) 等价于 a&(x-1)

快速取模算法

 

快速幂算法

 

11,ConcurrentHashMap(弱一致的)?

1.7及以前: 一个ConcurrentHashMap里包含一个Segment数组,每个Segment里包含一个HashEntry数组,我们称之为table,每个HashEntry是一个链表结构 的元素,每个key和value后会计算出一个hash值,hash值如果相同的key,value会封装成一个对象,然后放入到HashEntity的相同链表中,不同则放在其他链表中,其实 总结就是,Segment数组就是一个加锁的数组,每个线程对应一个Segment,每个Segment中包含一个hashmap。

注意:ConcurrentHashMap采用了二次hash的方 式,第一次hash将key映射到对应的segment,而第二次hash则是映射到segment的不同桶(bucket)中。 保证线程安全性的原因:
ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术,对segment进行加锁,segment本身也是一个 锁(继承了ReentrantLock),就保证了线程安全,初始化是根据并发数的大小和数组的大小来确定segment的大小,同时为了快速定位,通过算法保证segment的大小为2的 指数,初始化的时候只初始化segment的第一个元素。

concurrencyLevel并发度: 默认16。并发度可以理解为程序运行时能够同时更新ConccurentHashMap且不产生锁 竞争的大线程数,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过 大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。

扩容: concurrencyLevel(并发度)一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩 容,ConcurrentHashMap不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样的好处是扩容过程不需要对整个ConcurrentHashMap做rehash,而只 需要对Segment里面的元素做一次rehash就可以了。table中的元素位置变化,是根据扩容多大,比如扩大n,则元素下标不变化的就位置把持不变,变化的就在原来下标的基础 上+n即可,可以快速定位和减少重排次数。 这边需要特别注意一下两个变量,分别是segmentShift和segmentMask,这两个变量在后面将会起到很大的作用,假设构造函 数确定了Segment的数量是2的n次方,那么segmentShift就等于32减去n,而segmentMask就等于2的n次方减一。


get:
1,定位segment:先通过获取key的hashCode方法获取到哈希值,然后再通过WangJenkins哈希算法再进行散列,通过偏移量获取到一个高位哈希串再取模,然后寻 找到具体所在的segment位置。
2,定位table:先通过获取key的hashCode方法获取到哈希值,然后再通过WangJenkins哈希算法再进行散列,再和table的长度进 行取模,然后寻找到具体所在的table位置。
3,再在table中寻找对应的链表,去循环链表中的元素。

12, 在高并发下的情况下如何保证取得的元素是新的?

答:用于存储键值对数据的HashEntry,在设计上它的成员变量value等都是volatile类型的,这样就保证别的线程对value值的修改,get方法可以马上看到。 put: 1、首先定位segment,当这个segment在map初始化后,还为null,由ensureSegment方法负责填充这个segment。 2、对Segment加锁。 3、定位所在的table元素,hash(key)得到hash值,并扫描table下的链表,如果有相同的hash值,再判断key是否相同,如果相同就覆盖,不同就将这个值挂在链 表尾部。

size:首次会进行两次不加锁的统计,如果一致就返回,不一致就加锁之后再统计。因为可能会存在把segment所有的都加锁,所以尽量避免使用size方法。
弱一致性: get方法和containsKey方法没有加锁,他们都是通过对链表遍历判断是否存在key相同的节点以及获得该节点的value。但由于遍历过程中其他线程可 能对链表结构做了调整,因此get和containsKey返回的可能是过时的数据,这一点是ConcurrentHashMap在弱一致性上的体现。

1.8以后:与1.7相比的重大变化:

1、 取消了segment数组,直接用table保存数据,锁的粒度更小,减少并发冲突的概率。
2、 存储数据时采用了链表+红黑树的形式,纯链表 的形式时间复杂度为O(n),红黑树则为O(log2n),性能提升很大。什么时候链表转红黑树?当key值相等的元素形成的链表中元素个数超过8个并且容量大于64的时候,如果 容量小于64就先扩容。

  主要数据结构和关键变量:
    Node类存放实际的key和value值。

sizeCtl:负数:表示进行初始化或者扩容,-1表示正在初始化,-N表示有N-1个线程正在进行扩容
正数:0 表示还没有被初始化,>0的数,初始化或者是下一次进行扩容的阈值
TreeNode用在红黑树,表示树的节点,
TreeBin是实际放在table数组中的,代表了这个红黑树的根,将TreeNode进行了封装。

初始化过程:

在put的时候,会调用initTable方法,
if ((sc = sizeCtl) < 0) Thread.yield(); 会判断当sizeCtl小于0的时候,表示有其他线程正在初始化,当前线程就会进行yield,让出cpu的执行权。否则: else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) ,会用一个CAS操作设置sizeCtl的值,并初始化Node数组,然后: sc = n - (n >>> 2); sc =0.75n。将sizeCtl设置为0.75n的阈值。

扩容操作:

transfer()方法进行实际的扩容操作,table大小也是翻倍的形式,有一个并发扩容的机制。同时检测到某个table链表元素小于6个了的红黑树,就会自动把 红黑树又转为链表结构。 同时,在put的时候,判断如果有线程正在进行扩容,当前线程会帮助扩容,tab=helpTransfer(tab,f);,这个其实 是put和hashmap大不同之处,可以并发扩容,帮助移动数组中的Node的位置。

链表转为红黑树:

if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } 其中,if (binCount >= TREEIFY_THRESHOLD)表示当链表长度大于8的时候,通过treeifyBin(tab, i);转为红黑树。

put操作:

1,根据key进行两次hash算法得到hash值。
2,判断Node数组是否为空,如果为空进行初始化。 3,根据hash值得出所在的数组的位置,并判断当前数组里有没有链表存在,没有就通过CAS操作将元素加入到当前位置中。
4,else if ((fh = f.hash) == MOVED),判断是否有线程正在进行扩容,当前线程会帮助扩容,tab = helpTransfer(tab, f);,这个其实 是put和hashmap大不同之处。
5,如果当前数组位置已经存在元素了,就先用synchronized加锁,然后再判断当前位置是链表,还是红黑树,再对比hash值 和equls,hash值相同的,如果key相同就覆盖,key不相同就挂在当前链表后面,hash值不同,就挂在新节点上。

get操作:

1,首先判断当前node数组的位置的元素是否就是当前key,并且就是一个元素,没有链表,如果是就直接返回。 2,如果不是,再判断是否是红黑树,是就去红黑树中查找。
3,如果不是,就去链表中查找。
4,如果当前table为空,还没初始化,就直接返回null。

size方法:

估计的大概数量,不是精确数量,因为没有加锁,所以是弱一致性的。

putIfAbsent():

如果没有对应好的key就放入map,有这个值则返回key原来对应的值。

13,ConcurrentSkipListMap和ConcurrentSkipListSet?

是TreeMap和TreeSet有序的容器的并发版本,内部加入了跳表。

1,为什么选择跳表?

目前经常使用的平衡数据结构有:B树,红黑树,AVL树,SplayTree,Treep等,想象一下,给你一张草稿纸,一只笔,一个编辑器,你能立即实现一颗红黑树,或者AVL树出来吗?很难吧,这需要时间,要考虑很多细节,要参考一堆算法与数据结构之类的树,还要参考网上的代码,相当麻烦。
用跳表吧,跳表是一种随机化的数据结构,目前开源软件Redis和LevelDB(全文搜索引擎)都有用到它,它的效率和红黑树以及AVL树不相上下,但跳表的原理相当简单,时间复杂 度非常趋近于红黑树。

2,ConcurrentHashMap中为什么不使用跳表?

因为ConcurrentHashMap,因为用了分段锁,本身的空间利用率很低,如果再使用跳表用空间换取时间,就会使得ConcurrentHashMap的空间利用率更 低,所以不使用跳表。

3,跳表(可以提高普通链表查询的速度,是扩展空间负责度)?

SkipList,以空间换时间,在原链表的基础上形成多层索引,当某个节点在插入时,随 机决定这个节点是否成为上层索引,所以跳表又称为概率数据结构,这样有了索引之后,在查找的时候提高了效率。
跳表的高度: n个元素的跳表,每个元素插入的时候都要做一次实验,用来决定元素占据的层数K,跳表的高度等于这n次实验中产生的大k
跳表具有如下性质:

(1) 由很多层结构组成。
(2) 每一层都是一个有序的链表。
(3) 底层(Level 1)的链表包含所有元素。
(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。

14,ConcurrentLinkedQueue?

是LinkedList的并发版本,无界非阻塞队列,底层是个链表,遵循先进先出FIFO原则。

其中的方法:

add,offer:将元素插入到尾部
peek:拿头部的数据,但是不移除
poll:拿头部的数据,但是移除

15,写时复制容器?

CopeOnWriteArrayList,CopeOnWriteArraySet 写时复制的容器,通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器 进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对容器进行并发的读,而不需要加 锁,因为当前容器不会添加任何元素。所以写时复制容器也是一种读写分离的思想,读和写不同的容器。如果读的时候有多个线程正在向容器添加数据,读还是会读到旧的数 据,因为写的时候不会锁住旧的,只能保证终一致性。 适用读多写少的并发场景,常见应用:白名单/黑名单,商品类目的访问和更新场景。
注意:存在内存占用问题。

和读写锁的区别?

写时复制容器是读写可以同时进行的,而读写锁是一个线程写的时候,其他线程不能读,是互斥的。

16,Java集合框架的基础接口有哪些?

Collection:为集合层级的根接口,一个集合代表一组对象。这些对象即为它的元素,Java平台不提供这个接口不论什么直接的实现。
Set:是一个不能包括反复元素的集合,这个接口对数学集合抽象进行建模。被用来代表集合,就如一副牌。
List:是一个有序集合。能够包括反复元素,你能够通过它的索引来訪问不论什么元素。List更像长度动态变换的数组。
Map:是一个将key映射到value的对象.一个Map不能包括反复的key:每一个key多仅仅能映射一个value。

17.Iterator?

Iterator接口提供遍历不论什么Collection的接口,我们能够从一个Collection中使用迭代器方法来获取迭代器实例。迭代器代替了Java集合框架 中的Enumeration。迭代器同意调用者在迭代过程中移除元素。

1,原理:

用到了Iterator设计模式,又叫做游标(Cursor)模式。迭代器模式:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。
从定义可见,迭代器模式是为容器而生。很明显,对容器对象的访问必然涉及到遍历算法。你可以一股脑的将遍历方法塞到容器对象中去;或者根本不 去提供什么遍历算法,让使用容器的人自己去实现。这两种情况好像都能够解决问题。 然而在前一种情况,容器承受了过多的功能,它不仅要负责自己“容器”内的元素维护(添加、删除等等),而且还要提供遍历自身的接口;而且由于遍历状态保 存的问题,不能对同一个容器对象同时进行多个遍历。第二种方式倒是省事,却又将容器的内部细节 暴露无遗。所以,这时候在内部定义一个迭代器去控制元素的遍历。
注意:对于自身不是线程安全的容器,modCount也只是用来保证快速失败,因此不要在多线程的环境下使用iterator进行并行遍历操作。如果需要并行遍历可以使 用Spliterator进行并行遍历。

2,Enumeration和Iterator接口的区别:

Enumeration的速度是Iterator的两倍,也使用更少的内存,Enumeration是非常基础的,也满足了基础的须要。但是,与Enumeration相比:

1,Iterator更加安全,由于当一个集合正在被遍历的时候。它会阻止其他线程去改动集合(就是快速失败)。
2,迭代器可以从集合中移除元素,而Enumeration不能做到。
3,为何没有像Iterator.add()这种方法:

因为Iterator的协议不能确保迭代的次序,所以ListIterator没有提供一个add操作,它要确保迭代的顺序。

4,为何迭代器在不需要移动游标的情况下,直接获取下一个元素:

它可以在当前Iterator的顶层实现,但是它用得很少,如果将它加到接口中,每个继承都要去实现它,这没有意义。

5,Iterater和ListIterator之间有什么区别:

(1)我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
(2)Iterator只可以向前遍历,而LIstIterator可以双向遍历。
(3)ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。


6,为何Iterator接口没有详细的实现?

Iterator接口定义了遍历集合的方法。但它的实现则是集合实现类的责任。每一个能够返回用于遍历的Iterator的集合类都有它自己的Iterator实现内部 类。 这就同意集合类去选择迭代器是fail-fast还是fail-safe的。比方,ArrayList迭代器是fail-fast的。而CopyOnWriteArrayList迭代器是failsafe的。

7,快速失败(fail-fast)和安全失败(fail-safe)的区别

一:快速失败(fail—fast)

在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为 了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
场 景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

二:安全失败(fail—safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍 历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。 场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使 用,并发修改。

18,EnumSet是什么?

java.util.EnumSet是使用枚举类型的集合实现。当集合创建时,枚举集合中的全部元素必须来自单个指定的枚举类型,能够是显示的或隐示的。EnumSet是不同步的,不同意值为null的元素。它也提供了一些实用的方法,比方copyOf(Collection c)、of(E first,E…rest)和complementOf(EnumSet s)。

19,哪些集合类是线程安全的?

Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,能够在多线程环境下使用。

20,Comparable和Comparator接口的区别:

Comparable:

Comparable可以认为是一个内比较器,实现了Comparable接口的类有一个特点,就是这些类是可以和自己比较的,至于具体和另一个 实现了Comparable接口的类如何比较,则依赖compareTo方法的实现,compareTo方法也被称为自然比较方法。如果开发者add进入一个Collection的对象想 要Collections的sort方法帮你自动进行排序的话,那么这个对象必须实现Comparable接口。compareTo方法的返回值是int,有三种情况:

1、比较者大 于被比较者(也就是compareTo方法里面的对象),那么返回正整数
2、比较者等于被比较者,那么返回0
3、比较者小于被比较者,那么返回负整数

Comparator:

Comparator可以认为是是一个外比较器,有两种情况可以使用实现Comparator接口的方式:

1、一个对象不支持自己和自己比较(没有实现Comparable接口),但是又想对两个对象进行比较
2、一个对象实现了Comparable接口,但是开发者认为compareTo方法中的比较方式并不是自己想要的那种比较方式

Comparator接口里面有一 个compare方法,方法有两个参数To1和To2,是泛型的表示方式,分别表示待比较的两个对象,方法返回值和Comparable接口一样是int,有三种情况:

1、o1大于o2,返回正整数
2、o1等于o2,返回0
3、o1小于o3,返回负整数 两种比较器Comparable和Comparator,后者相比前者有如下优点:

1、如果实现类没有实现Comparable接口,又想对两个类进行比较(或者实现类实现 了Comparable接口,但是对compareTo方法内的比较算法不满意),那么可以实现Comparator接口,自定义一个比较器,写比较算法。
2、实现Comparable接口的方式比实现Comparator接口的耦合性要强一些,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较 的,不需要对实现类有任何修 改。从这个角度说,其实有些不太好,尤其在我们将实现类的.class文件打成一个.jar文件提供给开发者使用的时候。实际上实 现Comparator接口的方式后面会写到就是一种典型的策略模式。 当然,这不是鼓励用Comparator,意思是开发者还是要在具体场景下选择合适的那种比较器 而已。

小结:

重点学习set.map,list.Collection的具体一些细节上的区别,和一些方法对运用!而且还有高并发的处理,该分享用于java考试与具体学习!

posted @ 2019-08-20 19:22  lllunaticer  阅读(425)  评论(0编辑  收藏  举报