一、ArrayList和Vector的区别
共同点:
- 都实现了List接口继承了AbstractList接口
- 底层实现都是数组。所以说都是存储有序的集合
- 允许存储null
不同点:
- ArrayList是线程不安全的,Vector则是线程安全的,即便我们需要线程安全的数组容器也是会选择ArrayList,只是在初始化的时候通过
Collections
集合类的工具类来进行包装一下。因为这种同步实现更科学 - 扩容时ArrayList扩容系数为1.5,Vector则是2
二、HashMap和Hashtable的区别
总的来说HashTable的存在和Vector是一个意思。都是早期的实现,并且盲目的保证了集合的线程安全。却在一些不必要的操作上影响了集合的性能,并且早期函数的命名并不科学简洁严谨。
共同点:
- 实现了Map接口
不同点:
- HashMap非同步,HashTable同步。当我们需要同步的映射容器的时候Java也提供了
ConcurrentHashMap
这个类。 - HashMap允许null键和null值,HashTable不允许null键或者null值。
- HashMap没有保留HashTable的contains函数,而且和HashTable一样添加了
containsValue
和containsKey
函数。并选用containsKey函数来替代contains的功能。 - 两者继承不同。HashTable继承自Dictionary父类而HashMap继承自AbstractMap父类。
三、List和Map的区别
存储结构不同
- List是单列存储,Map是key-value键值存储
- List允许存重复元素,Map则key值不能重复
- List存储有序,Map映射存储无序。
四、Set集合定义存储在其中的元素是不能重复的,元素的重复与否是如何判断的?是用==还是equals()?
首先需要清楚Set集合的常见实现类的底层实现用的都是Map映射的子类。
拿HashSet来举例,它的底层是现金就是HashMap。而我们知道HashSet的存储方式是把存入的元素对应的存到了HashMap的key中,而value则是固定的Object对象的常量。
所以问题就落到了HashMap是如何处理存入相同key的情况的呢?
HashMap源码的632-635行是这样判断的,如果key值散列到的桶的头元素与该key的hash值相等。并且key值相==
或者key值equals的话,那么就认为该key值已经存在在了HashMap中。那么就跳出判断循环。
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
=============
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
HashMap源码652行做了如上处理,即拿出旧值并返回,对应的节点的value替换上新值。
解答:
所以针对上述问题,对于Set来说,当插入重复元素的时候,就代表这HashMap要插入具有重复key值得节点。那么由于插入到Set集合的节点的value均是相同的无意义的Object常量对象。所以底层HashMap相当于只是在频繁的修改无疑的value。
从源码可以看出来==
和equals
两者都用了。
五、Collection和Collections的区别
- Collection是集合类的顶级接口,实现它的有Set接口和=,List接口,Queue接口。
- Collections是集合的工具类。提供了一系列的静态函数来对集合进行搜索,查找,以及同步的一些操作等。
六、说出ArrayList,LinkedList的存储性能和特性
ArrayList底层是数组实现,存储有序。LinkedList底层是双向链表实现的,存储无序。
由于ArrayList底层实现是动态数组,所以对于ArrayList来说访问其元素可以根据角标的方式访问,非常快。而LinkedList要访问元素的时候只能通过遍历来找到要找的元素。所以ArrayList比LinkedList的访问速度要快,性能要好。
同样的由于是数组的原因也就导致它在进行增删操作的时候是不方便的比较缓慢的(移动数组元素,扩容复制数组)。LinkedList只需要修改对应节点next指针即可。消耗很小,所以LinkedList比ArrayList的增删速度要快,性能要好。
注意:
但同时我们需要细想一个删除操作具体体现的不仅仅是删除这一个操作。对于不同的数据结构而言,找到要删除的元素也是一个性能差异很大的点。
比如对于LinkedList的删除操作而言,删除节点本身是很简易的,但是慢就慢在它需要对所有元素去迭代一遍来找这个元素。而对于ArrayList来说删除操作是不易的,因为它要面临着被删除位置后的数组元素前移的操作。而找元素这个操作对数组来说却是速度很快的一个操作。所以整体上来说两者增删操作的性能差异就不是那么的确定了。比较时考虑的点还是比较的多
HashTable,SynchronizedMap,ConcurrentHashMap区别
HashTable
相比于HashMap,HashTable与其最大的区别就是所有函数都实现了同步。并且HashTable由于是最早设计的容器框架,有许多设计上的不足,并且后续并没有改进。最典型的元素插入时的put函数,元素的散列位置是通过%
来计算的,而非像HashMap中(table.length-1)&hash(key)
synchronizedMap
synchronizedMap可以作用于任何Map实现类上,但是相比于HashTable最大的区别则是它并不是盲目的对所有的函数都实现了同步。一定程度上性能要高于HashTable不少。但是两者都是对象锁。所以当并发情况下有一个线程在执行被锁函数那么并发情况下的其它线程必然都是等待状态。synchronizedMap相比于HashTable保证的只是非同步方法在多线程情况下的并行运行。
潜在问题:
Java代码
// shm是SynchronizedMap的一个实例
if(shm.containsKey('key')){
shm.remove(key);
}
这段代码用于从map中删除一个元素之前判断是否存在这个元素。这里的 containsKey和reomve方法都是同步的,但是整段代码却不是。考虑这么一个使用场景:线程A执行了containsKey方法返回 true,准备执行remove操作;这时另一个线程B开始执行,同样执行了containsKey方法返回true,并接着执行了remove操作;然 后线程A接着执行remove操作时发现此时已经没有这个元素了。要保证这段代码按我们的意愿工作,一个办法就是对这段代码进行同步控制,但是这么做付出 的代价太大。
同样还有: 并发情况下的迭代及修改。可能我们在迭代的时候某个线程对该Map正在执行并发修改的操作。此时就会报出并发修改的异常
参考:SynchronizedMap和ConcurrentHashMap 区别
ConcurrentHashMap
ConcurrentHashMap相比于前两者不但汲取了synchronizedMap的优点并没有对一些必要的函数实现同步。并且非同步的函数和HashMap的函数处理逻辑基本一致。而对于需要同步的函数,其内部采用的也是“桶锁”,它不会对整个Map映射上锁。这从一定程度上大大提升了性能,使其可以在多线程并发的情况下函数间并行执行,而不会影响执行结果和效率。
ArrayList和LinkedList增删分析
ArrayList插入分析:
ArrayList的add(E e)
函数默认是尾插。 我们如果采用该方法插入大量元素的话。不会存在元素移位的情况。这种插入方式和LinkedList的插入方式一样高效,复杂度都是O(1)。不存在元素移位的情况,而如果采用add(int index, E element)
函数来对某个索引处的元素前插入大量元素的话,每插入一次元素都会 使数组索引后续元素都进行一次元素移动的操作,复杂度是O(n)。关键区别看你插入大量元素时想插到哪里
LinkedList插入分析:
LinkedList的add(E e)
函数默认插入方式是尾插法。由于它是双向链表并且纪录了头尾两个节点。所以用这个方式插入大量元素的时间复杂度是0(1)。addFirst和addLast同样的道理由于纪录了头尾两节点的原因。
ArrayList删除分析:
而ArrayList提供的remove函数在删除大量元素的时候,就必然会进行数组元素的移位操作。它的耗时操作也就在此处。
LinkedList删除分析:
LinkedList由于是双向链表,所以头删尾删都有性能上的优势,复杂度均是O(1)。只需要考虑查找元素的复杂度即可。而且LinkedList中进行了折半查找,先对元素的索引和总元素数size进行比较来确认从头迭代的找还是尾迭代的找。元素总规模先>>
后才进行的迭代。
扩展:ArrayList和LinkedList增删性能比较
ArrayList的增删未必就是比LinkedList要慢。
如果增删都是在末尾来操作【每次调用的都是remove()和add()】,此时ArrayList就不需要移动数组元素了(增加元素的时候还是有可能触发扩容操作可能会扩容并复制数组)。如果数据量有百万级的时,ArrayList的速度是会比LinkedList要快的。(我测试过)
如果删除操作的位置是在中间。由于LinkedList的消耗主要是在遍历上,ArrayList的消耗主要是在数组元素的移动上(底层调用的是arraycopy()方法,是native方法)。
LinkedList的遍历速度是要慢于ArrayList的数组元素移动的速度的
如果数据量有百万级的时,还是ArrayList要快。(我测试过)
删末尾元素 ArrayList 删掉最后一个元素, LinkedList删掉最后一个元素。指针的变化,相对复杂一些
删中间元素,ArrayList需要做的是移动数组后续元素。而arraycopy是native函数,在JVM运行时有性能优势
而且本身进行的也是赋值操作,简单速度快
LinkedList需要做的是遍历元素,内存中指针的移动相对速度慢于赋值操作一些。
所以LinkedList的遍历速度要慢于ArrayList的移动速度。
所以可以认为百万级别的增删,如果是在容器中间或中间以后的元素操作基本都是ArrayList快
七、Set集合的各实现类
首先有3个,HashSet,TreeSet,LinkedHashSet
HashSet
常用的应该是HashSet,底层实现是HashMap(由于LinkedHashSet继承自HashSet,所以底层实现也提供了一种LinkedHashMap的构造函数),无论存储还是查询都具有很好性能。都可以根据元素的hash值快速的散列到所在桶的位置,然后进行相应的操作。同时由于HashMap在jdk1.8的时候把HashMap底层的实现换成了散列表+红黑树的方式。导致即便碰巧大量元素散列到一个桶上也不会出现链表特别长,遍历性能不好的情况。当链表数量超过8个时会自动将链表哦转化成红黑树(元素数大于64),遍历时性能非常好。由于底层是HashMap实现,所以我们在迭代的时候调用iterator方法返回的其实是HashMap迭代时调用的keySet函数返回的Set集合的Iterator对象来进行迭代。HashMap底层会对所有的Key用一个Set集合来进行存储(不重)。对所有value用一个Collection来进行存储。
TreeSet
TreeSet底层实现是TreeMap,即根据插入元素的自然排序或者自定义比较方式来比较插入元素的大小来决定其插入位置。进而形成一个有序的红黑树。TreeSet和TreeMap一样,插入的元素会有序的存储在一个红黑树上。根据我们制定的规则,或者元素的自然排序顺序。一般Tree系列的数据结构,我们用来保证一种有序性的数据结构才会用,存入到其中的元素必须实现Comparable或者Comparator接口来保证元素的可比较性。我们一般都是重写compareTo或者compare方法来制定自己的比较规则,让后续存到数据结构中的元素满足我们想要的顺序。
LinkedHashSet
LinkedHashSet继承自HashSet,底层实现是LinkedHashMap即散列表和双向链表。HashSet也为LinkedHashSet提供了一个构造LinkedHashMap的构造函数,但需要注意的是,HashSet只为LinkedHashSet提供了一种只有initialCapacity
和loadFactor
的构造函数,也就意味着LinkedHashSet的元素无法像LinkedHashMap那样具有两种迭代顺序,又由于默认LinkedHashMap的迭代顺序是元素的插入顺序,所以它一般的作用就是用来保证元素的存储顺序和我们插入元素的顺序一致。这样我们在迭代获取元素的时候有一个预估值和期望值。
HashSet各方面性能好,不用排序一般用它,也是因为HashMap的优势。TreeSet一般用在比较根据元素进行排序的时候,LinkedHashSet则是用到我们要元素的插入顺序的时候。
八、Map集合及实现类
首先有3个,HashMap,TreeMap,LinkedHashMap。
HashMap
最常用的一般是HashMap,底层实现是散列表+单链表+红黑树。从性能上来讲,无论增删还是查都可以根据传入的key值快速定位到元素所在的桶。并且桶的数据结构也对大量hash碰撞的特殊情况做了优化,单链表元素数超过8采用红黑树存储。允许使用null做键值。迭代的时候有三种方式
1.
for (Entry<String, String> entry : map.entrySet())
2.
for (String keys : map.keySet())
3.
Iterator<String> keys = map.keySet().iterator();
while (keys.hasNext()){
}
- 第一种调用的是
HashIterator
。根据散列表的数组下标来进行迭代。 - 第二种和第三种其实是一样的。
keySet()
返回的KeySet类继承自AbstractSet
,在foreach
迭代的时候会自动调用该类的iterator()
方法,返回KeyIterator
类,而这个类又继承自HashIterator
类。只是实现了next()
方法,但函数内也还是调用了HashIterator
类的nextNode()
方法。
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
TreeMap
TreeMap是TreeSet的底层实现。底层数据结构式红黑树,实现了SortedMap接口,该映射是根据其键的自然顺序进行排序的,或者根据在构建TreeMap时传入的Comparator来进行排序。需要注意,存入的元素必须实现了Comparable接口或者其类型被指定的Comparator所接受。而且比较的对象参数不能为null。在使用这种数据结构的时候我们要注的除了是元素需要具有可比性,同时我们还需要注意元素具体的比较规则,即元素的类重写的compare方法和compareTo方法的比较逻辑。
LinkedHashMap
LinkedHashMap是LinkedHashSet的底层实现,继承自HashMap,底层数据结构是散列表+单链表+红黑树+双向链表。所以和HashMap一样无论增删还是查找元素都具有极高的性能。不同的是迭代元素的方式。HashMap是通过迭代每一个“桶”来进行的迭代,迭代时获取的Entry都是未知的。而LinkedHashMap则默认是通过节点的插入顺序来进行迭代,或者通过节点的最近最少来进行迭代。之所以可以如此,是因为LinkedHashMap扩展了HashMap的Node
节点。LinkedHashMap的Entry节点比其多维护两个指针变量。分别指向该节点的前驱和后继节点,所以比HashMap可以多维护一个双向链表。
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
LinkedHashMap默认是按key的插入顺序来排序,也可以通过在构造时通过指定accessOrder为true来使其按元素的LRU顺序来排序,在其迭代的时候可以看出它的元素排列顺序。
LinkedHashMap不允许存储的元素的key值为null。两种比较方式都不支持。元素在插入到红黑树的时候肯定会与其他树中的节点进行比较,此时会报错。
参考:HashMap,LinkedHashMap,TreeMap的区别
九、Enumeration和Iterator接口的区别
Enumeration最早出来主要是针对当时所有的集合类的如Vector和HashTable的。但是由于原本Vector与Hashtable设计上的问题,以及Enumeration在设计上的一些缺陷到之后来出现了Iterator替代了它。
并且Iterator支持所有集合类。并对Enumeration进行了扩展,1. 添加了一个可选择的删除操作。并且在2. 函数命名上也更加科学,简洁了。同时Iterator也在并发操作的时候针对其实现类在并发情况下可能出现的3.并发修改问题利用fail-fast
机制进行补足。Enumeration由于本身针对的就是并发情况下同步的类Vector和HashTable所以不存在并发上的问题。
十、ListIterator的特点
我们前面说到Enumeration最早吹来是服务于Vector和HashTable来进行遍历的。后来Iterator的出现替代了它,但是对应不同的集合类迭代方式也不尽相同。所以ListIterator继承自Iterator,看类名就知道他是负责List集合的迭代的。
- 继承自Iterator接口,用于遍历List集合的元素。
- 可以实现双向遍历。添加元素。设置元素
十一、Java中HashMap的key值要是为类对象则该类需要满足什么条件?
需要同时重写该类的hashCode()方法和它的equals()方法。
因为HashMap在插入元素的时候在put
函数中就会向调用的putVal
函数传递key的hash值。然后putVal
函数再对key的哈希值进行散列,找到对应的桶。既然这样,那和重写对象到的hashcode有什么关系呢?
那是因为我们的对象做为key来存到hashMap中的话,我们最后肯定是根据key来获取到对应的value。而这个entry又存储在桶中,我们要想找到对应的桶,对于HashMap函数来说只能通过对象的hashcode值来进行散列,散列到对应的桶上。但是我们存储和取出的时候其实用的是两个对象(两次都会new新的对象)。但是两个对象具有相同的属性值。
如果我们不重写对象的hashcode的函数的话,去更改他hash值的生成策略的话是无法散列到对应的桶的(根据对象地址生成的hash值)。也就无法取出我们存进去的值。所以我们需要重写它的hashcode函数让它生成是只需要根据对象的属性进行生成即可,这样下次我们无论穿进去这个类的哪个对象,只要它保证我们指定的生成hashcode参考的属性相等即可。equals函数就更不用说了,Object类的equals默认比较的是两个对象的地址。而对我们开发来说这样的意义并不大。一般来说我们认为只要两个对象的成员变量的值是相等的,那么我们就认为这两个对象是相等的!
显然hashcode相比于equals函数具偶然性。在hashmap元素的比较中也是把hash值得比较前置了。因为hashcode可能在一些情况下出现hash碰撞相等的情况。所以我们重写equals函数前必须先重写hashcode函数,这样既保证了正确性,也在性能上得到了提升。
十二、与Java集合框架相关的有哪些最好的实践
- 根据需要确定集合的类型。如果是单列的集合,我们考虑用Collection下的子接口ArrayList和Set。如果是映射,我们就考虑使用Map
- 确定完我们的集合类型,我们接下来确定使用该集合类型下的哪个子类~我认为可以简单分成几个步骤:
-
是否需要同步
- 找线程安全的集合类使用
-
迭代时是否需要有序(插入顺序有序)
- 找Linked双向列表结构的
-
是否需要排序(自然顺序或者手动排序)
- 找Tree红黑树类型的(JDK1.8)
- 估算存放集合的数据量有多大,无论是List还是Map,它们实现动态增长,都是有性能消耗的。在初始集合的时候给出一个合理的容量会减少动态增长时的消耗
- 使用泛型,避免在运行时出现ClassCastException
- 尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的工具函数。它提供了我们常用的针对List集合的功能函数,具有很高的重用性,以及更好的稳定性和可维护性。