java集合面试题

记录自己看到的一些知识点。

1. Java 中常用的容器有哪些?
List set map
2. ArrayList 和 LinkedList 的区别?
ArrayList底层是数组,LinkedList底层是链表。

3. ArrayList 实现 RandomAccess 接口有何作用?为何 LinkedList 却没实现这个接口?
只要List集合实现这个接口,就能支持快速随机访问。
通过查看源代码,发现实现RandomAccess接口的List集合采用一般的for循环遍历,而未实现这接口则采用迭代器。ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快。
具体请看这篇博客 很详细 https://blog.csdn.net/u011679955/article/details/89297346

4. ArrayList 的扩容机制?

 private void grow(int minCapacity) {
          // 获取到ArrayList中elementData数组的内存空间长度
          int oldCapacity = elementData.length;
         // 扩容至原来的1.5倍
         int newCapacity = oldCapacity + (oldCapacity >> 1);
         // 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组,
          // 不够就将数组长度设置为需要的长度
         if (newCapacity - minCapacity < 0)
             newCapacity = minCapacity;
         //若预设值大于默认的最大值检查是否溢出
         if (newCapacity - MAX_ARRAY_SIZE > 0)
             newCapacity = hugeCapacity(minCapacity);
         // 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
         // 并将elementData的数据复制到新的内存空间
         elementData = Arrays.copyOf(elementData, newCapacity);
     }

从此方法中我们可以清晰的看出其实ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。
具体 请看 https://www.cnblogs.com/dengrongzhang/p/9371551.html

5. Array 和 ArrayList 有何区别?什么时候更适合用 Array?

下面列出了Array和ArrayList的不同点:
Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
Array大小是固定的,ArrayList的大小是动态变化的。
ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

6. HashMap 的实现原理/底层数据结构?JDK1.7 和 JDK1.8
entry数组和链表
1.7数据结构是这样的,他的数据结构则是采用的位桶和链表相结合的形式完成了,即拉链法。
在这里插入图片描述
1.8的数据结构是这样的,采用的是位桶数组,其底层采用红黑树。相对于JDK1.7,1.8的HashMap处理hash冲突时,会首先存放在链表中去,但是一旦链表中的数据较多(即>8个)之后,就会转用红黑树来进行存储,优化存储速度。O(lgn)。如果是链表。一定是O(n)。
在这里插入图片描述
具体看这篇 https://blog.csdn.net/qq_30683329/article/details/80454518

7. HashMap 的 put 方法的执行过程?

1、先计算出key对应的hash值
2、对超出数组范围的hash值进行处理
3、根据正确的hash值(下标值)找到所在的链表的头结点
4、如果头结点==null,直接将新结点赋值给数组的该位置
5、否则,遍历链表,找到key相等的节点,并进行value值的替换,返回旧的value值
6、如果没有找到,采用尾插法(1.8/头插法(1.7)创建新结点并插入到链表中
7、将存储元素数量+1
8、校验是否需要扩容

8. HashMap 的 get 方法的执行过程?

1、先计算出key对应的hash值
2、对超出数组范围的hash值进行处理
3、根据正确的hash值(下标值)找到所在的链表的头结点
4、遍历链表,如果key值相等,返回对应的value值,否则返回null

9. HashMap 的 resize 方法的执行过程?

1、resize发生在table初始化, 或者table中的节点数超过threshold值的时候, threshold的值一般为负载因
子乘以容量大小.
2、每次扩容都会新建一个table, 新建的table的大小为原大小的2.
3、扩容时,会将原table中的节点re-hash到新的table中, 但节点在新旧table中的位置存在一定联系: 
要么下标相同, 要么相差一个原table的大小.

10. HashMap 的 size 为什么必须是 2 的整数次方?

hashMap源码获取元素的位置:

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

解释:
h:为插入元素的hashcode
length:为map的容量大小
&:与操作 比如 1101 & 1011=1001
如果length为2的次幂 则length-1 转化为二进制必定是11111……的形式,在于h的二进制与操作效率会非常的快,
而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在于h与操作,
最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费

原文链接:https://blog.csdn.net/a2615381/article/details/78151061

  1. HashMap 多线程死循环问题?
    由于在jdk1.7中使用的是头插法,扩容后链表指向顺序改变,而多线程中一个线程扩容改变了指针指向,单其他线程不知道,就造成了死循环。

  2. HashMap 的 get 方法能否判断某个元素是否在 map 中?
    个人感觉是不行的,因为hashmap中是允许存在null的,当key存在,但是value为null时。返回的就是null。总结了两个判断key是否存在方法。

方法一
HashMap map = new HashMap();
		map.put("1", "value1");
	map.put("2", "value2");
 
		Iterator keys = map.keySet().iterator();
		while(keys.hasNext()){
			String key = (String)keys.next();
			if("2".equals(key)){
				System.out.println("存在key");
			}
		}
方法二

boolean flag=map.containsKey("opt")

13. HashMap 与 HashTable 的区别是什么?
HashTable
底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
初始size为11,扩容:newsize = olesize2+1
计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
底层数组+链表实现,可以存储null键和null值,线程不安全
初始size为16,扩容:newsize = oldsize
2,size一定为2的n次幂
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
计算index方法:index = hash & (tab.length – 1)

ConcurrentHashMap
底层采用分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

具体请看这篇博客 https://blog.csdn.net/xuhuaabc/article/details/91475761

17. HashSet 的实现原理?
HashSet 的内部采用了HashMap作为数据存储,HashSet其实就是在操作HashMap的key

因为HashMap是无序的,因此HashSet也不能保证元素的顺序
因为HashSet中没有对应同步的操作,因此是线程不安全的
支持null元素(因为hashMap也支持null键和null值)

  1. HashSet 怎么保证元素不重复的?
    hashSet是维护了一个hashmap,hashmap的key是不重复的,hashSet的值就是放在map的key上,而value则全是PRESENT,只是要给占位符。那么hashMap又是怎么保证key不重复的呢。

最关键是下面一句

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

调用了对象的hashCode和equals方法进行的判断,所以又得出一个结论:若要将对象存放到HashSet中并保证对象不重复,应根据实际情况将对象的hashCode方法和equals方法进行重写。

具体请看 https://blog.csdn.net/weixin_30817749/article/details/97074603

19. LinkedHashMap 的实现原理?
1.在LinkedHashMap中,是通过双联表的结构来维护节点的顺序的。上文中的程序,实际上在内存中的情况如下图所示,每个节点都进行了双向的连接,维持插入的顺序(默认)。head指向第一个插入的节点,tail指向最后一个节点。
2.LinkedHashMap是HashMap的亲儿子,直接继承HashMap类。LinkedHashMap中的节点元素为Entry<K,V>,直接继承HashMap.Node<K,V>。

  1. Iterator 怎么使用?有什么特点?
public class IteratorDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add("item" + i);
        }
        //获取迭代器
        Iterator<String> listIterator = list.iterator();
        //判断是否还有元素
        while (listIterator.hasNext()) {
            System.err.println(listIterator.next());
            //对剩下的元素执行指定操作
            listIterator.forEachRemaining((String consumer) -> {
                System.err.println(consumer.concat("-test"));
            });
        }

    }
}

原文链接:https://blog.csdn.net/u010647035/article/details/79826457

Java中的Iterator功能比较简单,并且只能单向移动:

(1) 方法iterator()将返回一个Iterator。首次调用next()方法时,它将返回第一个元素

(2) next() 返回下一个元素

(3) hasNext() 检查集合中是否还有元素

(4) remove() 将迭代器新返回的元素删除

(5) forEachRemaining() 对每个剩余的元素执行指定的操作

21. Iterator 和 ListIterator 有什么区别?
1… ListIterator有add()方法,可以向List中添加对象,而Iterator不能

2… ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。

3… ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。

4… 都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。

22. Iterator 和 Enumeration 接口的区别?
1 Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。
2 Iterator除了能读取集合的数据之外,也能数据进行删除操作;而Enumeration只能读取集合的数据,而不能对数据进行修改。
3 Iterator支持fail-fast机制,而Enumeration不支持fail-fast机制。
23. fail-fast 与 fail-safe 有什么区别?

在我们详细讨论这两种机制的区别之前,首先得先了解并发修改。

1.什么是并发(同步)修改?
当一个或多个线程正在遍历一个集合Collection,此时另一个线程修改了这个集合的内容(添加,删除或者修改)。这就是并发修改。

2.什么是 fail-fast 机制?
fail-fast机制在遍历一个集合时,当集合结构被修改,会抛出Concurrent Modification Exception。
fail-fast会在以下两种情况下抛Concurrent Modification Exception
(1)单线程环境
集合被创建后,在遍历它的过程中修改了结构。
注意 remove()方法会让expectModcount和modcount 相等,所以是不会抛出这个异常。
(2)多线程环境
当一个线程在遍历这个集合,而另一个线程对这个集合的结构进行了修改。

注意:迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。

  1. fail-fast机制是如何检测的?
    迭代器在遍历过程中是直接访问内部数据的,因此内部的数据在遍历的过程中无法被修改。为了保证不被修改,迭代器内部维护了一个标记 “mode” ,当集合结构改变(添加删除或者修改),标记"mode"会被修改,而迭代器每次的hasNext()和next()方法都会检查该"mode"是否被改变,当检测到被修改时,抛出Concurrent Modification Exception。

  2. fail-safe机制
    fail-safe任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException
    fail-safe机制有两个问题
    (1)需要复制集合,产生大量的无效对象,开销大
    (2)无法保证读取的数据是目前原始数据结构中的数据。

一:快速失败(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包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

转载于 https://blog.csdn.net/lmlcode/article/details/83270816
24. Collection 和 Collections 有什么区别?
1、java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。

List,Set,Queue接口都继承Collection。
直接实现该接口的类只有AbstractCollection类,该类也只是一个抽象类,提供了对集合类操作的一些基本实现。List和Set的具体实现类基本上都直接或间接的继承了该类。

2、java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
例如经常用到的排序
这里是根据包装类的某一个字段进行排序。

 Collections.sort(valuelist, new Comparator<DSsensorValueInfo>() {
            @Override
            public int compare(DSsensorValueInfo o1, DSsensorValueInfo o2) {
                return  o2.getTime().compareTo(o1.getTime());
            }
        });

原文链接:https://blog.csdn.net/xiangyuenacha/article/details/84237663

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值