1、遍历一个 List集合有哪些不同的方式?举例实现。
// 普通for循环
for (int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
System.out.println("==============");
// 增强for循环
for (Integer l:list){
System.out.println(l);
}
System.out.println("==============");
// Lambda表达式
list.forEach(s-> System.out.println(s));
System.out.println("==============");
// 迭代器遍历
Iterator<Integer> it = list.iterator();
while (it.hasNext()){
System.out.println(it.next());
}
System.out.println("===============");
// 列表迭代器
ListIterator<Integer> ite = list.listIterator();
while (ite.hasNext()){
System.out.println(ite.next());
}
2、单列集合和双列集合都可以使用迭代器吗?
单列集合和双列集合都可以使用迭代器,只不过双列集合要使用Map.Entry()方法间接实现迭代来访问键值对。
// 单列集合实现迭代
Iterator<String> iterator1 = list.iterator();
while (iterator1.hasNext()){
String s = iterator1.next();
System.out.println(s);
}
// 双列集合实现迭代
Iterator<Map.Entry<String, Integer>> iterator2 = map.entrySet().iterator();
while (iterator2.hasNext()) {
Map.Entry<String, Integer> entry = iterator2.next();
System.out.println(entry.getKey() + ": " + entry.getValue());
}
3、Iterator 和 ListIterator 有什么区别?
区别 | Iterator | ListIterator |
使用范围不同 | 适用于所有实现Iterator接口的集合类的通用迭代器 | 只适用于List集合的迭代器 |
遍历方向不同 | 只支持从前往后遍历 | 支持从前往后遍历和从后往前遍历 |
常用方法不同 | 只支持使用remove()方法删除元素 | 支持set()方法修改元素;add()方法增加元素;remove()方法删除元素 |
获取索引方法不同 | 没有获取索引的方法 | 支持previousIndex()方法获取上一个元素索引;nextIndex()方法获取下一个元素索引 |
// Iterator
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if ("b".equals(s)){
iterator.remove();
}
}
// ListIterator
ListIterator<String> listIterator = list.listIterator();
// 从前往后遍历
while (listIterator.hasNext()){
String s1 = listIterator.next();
// 增加元素
if ("c".equals(s1)){
listIterator.add("d");
int d1 = listIterator.previousIndex();
System.out.println(d1);
}
// 修改元素
if ("a".equals(s1)){
listIterator.set("h");
int d2 = listIterator.nextIndex();
System.out.println(d2);
}
}
System.out.println(list);
// 从后往前遍历
while (listIterator.hasPrevious()){
String s2 = listIterator.previous();
System.out.println(s2);
}
4、什么是TreeMap,如何决定使用 HashMap 还是 TreeMap?
TreeMap是一个有序的键值对集合,通过红黑树实现。如果输出的结果要有限那么可以用TreeMap,因为HashMap中排列顺序是不固定的。
5、comparable 和 comparator的区别?
区别 | Comparable | Comparator |
接口所在包不同 | java.lang.Comparable | java.util.Comparator |
比较逻辑不同 | Comparable是内部比较器 | Comparator是外部比较器 |
排序方式不同 | 重写了compareTo(T o) | 重写了compare(T o1,T o2) |
6、说一下 HashSet 的实现原理?
HashSet内部是基于HashMap实现的,所有的元素都存储在HashMap的Key中,value值则统一使用一个PRESENT静态常量。当向集合中add()添加一个实际上是调用HashMap的put()方法实现。HashSet利用HashMap的Key值不可重复性来保证集合中不会有重复元素。
7、HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现是什么?
不同:
底层数据结构:JDK1.7中,HashMap使用数组+链表的方式来处理哈希冲突,JDK1.8,引入了红黑树来优化处理哈希冲突,即在集合元素超过一定阈值后将链表转化为红黑树,以提高查找性能。
并发安全性:在JDK1.7中HashMap不是线程安全的,多线程操作需要额外的同步措施;而在JDK1.8后引入了分段锁(Segment),通过将存储空间分成多个段,每个段都类似于一个小的HashMap,不同段的数据可以并行处理,从而提高并发性能。
HashMap的底层实现是基于哈希表的,它使用数组+链表(红黑树)的结构来存储键值对。具体实现流程如下:
计算哈希值:根据键对象的hashCode方法来计算出哈希值。
确定存储位置:通过哈希值和数组长度取模的方式来确定存储位置,即确定元素在数组中的索引位置。
处理哈希冲突:如果不同的键对象计算出的哈希值相同(发生哈希冲突),则在改位置形成一个链表(或红黑树),将键对象插入到链表(或红黑树)的末尾。
数组扩容:当元素数量到达数组容量的某个阈值后,会触发扩容操作。通常是将数组容量翻倍,将原有的元素重新散列到新数组。
存储键值对:将键值对存储到确定位置上,如果有相同的键的元素,则覆盖原有的值。
8、HashMap的扩容操作是怎么实现的?
当集合元素的数量超过HashMap的阈值(容量*负载因子),引发HashMap的扩容。扩容为原有容量的倍数,然后计算元素在新数组的位置,确定位置后将元素存储到新数组中。
9、HashMap 和 ConcurrentHashMap、 Hashtable 的区别
HashMap | ConcurrentHashMap | HashTable |
线程不安全,适用于单线程环境,Key和Value可以为null | 线程安全,锁的是每个链表的头结点,降低了锁冲突的概率;充分利用CAS机制;优化了扩容方式;Key和Value不可以为空;适用于多线程环境 | 线程安全,锁的是整个HashTable对象,效率低,Key和Value不可以为空 |
10、ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?
ConcurrentHashMap是线程安全的,它是Java并发包中的提供的一种高效的并发Map实现,底层采用了分段锁,不同的段(Segment)可以被不同的线程同时访问,从而提高了并发性能。
ConcurrentHashMap采用了数组+链表+红黑树的数据结构来实现哈希表。数组的每个元素都是一个段,每个段都是一个独立的哈希表,包含了若干个键值对。每个段都有自己的锁,不同的段可以被不同的线程同时访问,提高了并发性能。
ConcurrentHashMap的put操作和get操作都是非常高效的,因为它们都可以并发进行,不需要对整个哈希表加锁。在进行put操作时,先根据Key的哈希值找到对应的段,然后对该段加锁,再在该段进行插入操作。在进行get操作时,也是先根据Key的哈希值找到对应的段,然后对该段加锁,在进行查找操作。这样就可以实现高并发的插入和查找操作。
ConcurrentHashMap的扩容操作也是高效的,当某个段的元素个数超过了阈值,就会触发扩容。在进行扩容时,只需要对该段进行加锁,不需要对整个哈希表加锁。同时扩容时只需要将旧的元素加入新的段即可,不需要向HashMap一样对所有元素进行哈希值的计算,因此扩容效率更高。
总之,ConcurrentHashMap的底层采用了分段锁,不同的段可以被不同的线程同时访问,从而提供并发性。同时采用数组+链表+红黑树的数据结构来实现哈希表,具有高效的插入、查找和扩容等操作。
java8中的ConcurrentHashMap引入了新的实现方式,称为基于CAS的实现,相比与java7中的基于分段锁的实现,具有了更好的性能。此外,Java 8 中的 ConcurrentHashMap 还引入了新的方法,如 forEach、reduce、search 等,使得对 ConcurrentHashMap 的操作更加方便和高效。但是,Java 7 中的 ConcurrentHashMap 仍然可以使用,并且在某些场景下,其性能表现可能更好。
总的来说,JDK1.8 分段锁+CAS。JDK1.7分段锁。
11、为什么HashMap中String、Integer这样的包装类适合作为K?如果使用Object作为HashMap的Key,应该怎么办呢?
String、Integer等包装类的特性能够保证Hash值的不可更改下和计算准确性,能够有效的减少哈希碰撞的概率。
都是final类型,即不可变性,保证了Key值的不可更改性,即获取的哈希值不会出现不同情况。
内部已重写了hashCode和equals方法,遵守了HashMap内部的规范(不清楚的可以看看putValue的过程),不容易出现哈希值计算错误的情况。
Object作为HashMap的Key,应该怎么办呢?
重写hashCode()和equals()方法
重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;
12、HashMap是怎么解决哈希冲突的?
什么是哈希冲突?
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
开放定址法
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
链地址法
链地址法将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
再哈希法
当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
建立公共溢出区
将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。