【java面试02-Java集合】

1. 说说List,Set,Map三者的区别?

List (对付顺序的好帮手): 存储的元素是有序的、有下标、可重复的。
Set (注重独一无二的性质): 存储的元素是无序的、无下标、不可重复的。
Map (用 Key 来搜索的专家): 使用键值对(kye-value)存储,类似于数学上的函数y=f(x),“x”代表 key,"y"代表 value,Key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。

arraylist初始容量为0,加入一个元素之后默认容量为10,之后按原来容量的1.5倍扩容。
treeset直接帮你把元素排好序。
hashmap刚创建时,默认容量为0,当添加了一个元素时,默认初始容量为16,当存放元素个数超过(16*0.75)12时,开始扩容,扩容为之前的2倍。jdk1.8后,当链表长度大于8且数组长度大于64,将单链表调整为红黑树。jdk1.8后,当红黑树长度小于6,调整为链表。jdk1.8前,链表是头插入,jdk1.8后是尾插入。

2. Arraylist 与 LinkedList 区别?

在这里插入图片描述

  1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  2. 底层数据结构: Arraylist 底层使用的是 Object 数组; LinkedList 底层使用的是双向链表数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别。)
  3. 插入和删除是否受元素位置的影响:一、 ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。二、LinkedList 采用链表存储,所以对于 add(E e) 方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置 i 插入和删除元素的话( (add(int index, E element) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插入。
  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) 方法)。
  5. 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间 (因为要存放直接后继和直接前驱以及数据)。

(补充)

ArrayList:ArrayList 类是一个可以动态修改的数组。

import java.util.ArrayList; // 引入 ArrayList 类
ArrayList<E> objectName =new ArrayList<>();  // 初始化

LinkedList:链表是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。

// 引入 LinkedList 类
import java.util.LinkedList; 
LinkedList<E> list = new LinkedList<E>();   // 普通创建方法
// 或者
LinkedList<E> list = new LinkedList(Collection<? extends E> c); // 使用集合创建链表

HashSet:HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
HashSet 允许有 null 值。
HashSet 是无序的,即不会记录插入的顺序。
HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。

import java.util.HashSet; // 引入 HashSet 类
HashSet<E> sites = new HashSet<E>();

HashMap:HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。HashMap 是无序的,即不会记录插入的顺序。

import java.util.HashMap; // 引入 HashMap 类
// 创建一个 HashMap 对象 Sites, 整型(Integer)的 key 和字符串(String)类型的 value:
HashMap<Integer, String> Sites = new HashMap<Integer, String>();

(补充)RandomAccess 接口

众所周知,在List集合中,我们经常会用到ArrayList以及LinkedList集合,但是通过查看源码,就会发现ArrayList实现RandomAccess接口,但是RandomAccess接口里面是空的!Linked并没有实现RandomAccess接口。
原来RandomAccess接口是一个标志接口(Marker),然而实现这个接口有什么作用呢?
解答:只要List集合实现这个接口,就能支持快速随机访问。

RandomAccess用来当标记,是一种标记接口。用处是当要实现某些算法时,会判断当前类是否实现了RandomAccess接口,会选择不同的算法。
在这里插入图片描述
接口RandomAccess中内容是空的,只是作为标记用。比如List下的ArrayList和LinkedList。其中ArrayList实现了RandomAccess,而LinkedList没有。为什么呢?我觉得还是和底层数据结构有关! ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。 ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList实现 RandomAccess 接口才具有快速随机访问功能的!
所以说RandomAccess只是一个空的,用来标记的接口。

3. ArrayList 与 Vector 区别呢?为什么要用Arraylist取代 Vector呢?

在这里插入图片描述

在这里插入图片描述

ArrayList 是 List 的主要实现类,底层使用 Object[ ] 存储,适用于频繁的查找工作,线程不安全 ;
Vector 是 List 的古老实现类,底层使用 Object[ ] 存储,线程安全的。vector是线程(Thread)同步(Synchronized)的,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
如果集合中的元素的数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而arraylist增长率为目前数组长度的50%.如果在集合中使用数据量比较大的数据,用vector有一定的优势。
在这里插入图片描述

4.ArrayList 的扩容机制

5.HashMap 和 Hashtable 的区别

  1. 线程是否安全: HashMap是非线程安全的,HashTable是线程安全的,因为 HashTable 内部的方法基本都经过 synchronized修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap吧!);
  2. 效率: 因为线程安全的问题, HashMap 要比 HashTable 效率高一点。另外, HashTable基本被淘汰,不要在代码中使用它;
  3. 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为
    键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException 。
  4. 初始容量大小和每次扩充容量大小的不同 : 1 创建时如果不指定容量初始值, Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化大 小为 16。之后每次扩充,容量变为原来的 2 倍。2 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小
    ( HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总
    是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了􏰂大的变化,当链表⻓度
    大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的⻓度小于 64,那么 会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时 间。Hashtable 没有这样的机制。

6.HashMap 和 HashSet区别

如果你看过 HashSet 源码的话就应该知道: HashSet 底层就是基于 HashMap 实现的。( HashSet 的源码非常非常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
在这里插入图片描述

7.HashSet如何检查重复

当你把对象加入 HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较􏰂,如果没有相符的 hashcode , HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让加入操作成功。
hashCode() 与 equals() 的相关规定:

  1. 如果两个对象相等,则 hashcode 一定也是相同的
  2. 两个对象相等,对两个equals() 方法返回 true
  3. 两个对象有相同的 hashcode 值,它们也不一定是相等的
  4. 综上, equals() 方法被覆盖过,则 hashCode() 方法也必须被覆盖
  5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode() ,则该class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

(补充)==与 equals 的区别

对于基本类型来说,== 比􏰂的是值是否相等;
对于引用类型来说,== 比􏰂的是两个引用是否指向同一个对象地址(两者在内存中存放的地址 (堆内存地址)是否指向同一个地方);
对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String),则比􏰂的是地址里的内容。

8.HashMap的底层实现

在这里插入图片描述

  1. JDK1.8 之前
    JDK1.8 之前 HashMap 底层是数组和链表结合在一起使用也就是链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
    所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一 些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
  2. JDK1.8 之后
    相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了􏰎大的变化,当链表⻓度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的⻓度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
    在这里插入图片描述

9. HashMap 的⻓度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量􏰂少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比􏰂均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的⻓度取模运算, 得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash ”。(n代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 方)。” 并且采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的⻓度为什么是2的幂次方。

10. HashMap 多线程操作导致死循环问题

主要原因在于并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。

11. ConcurrentHashMap 和 Hashtable 的区别

Hashtable和ConcurrentHashMap存储的内容为键-值对(key-value),且它们都是线程安全的容器。
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。 Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
在这里插入图片描述
实现线程安全的方式**(重要)**:
1、在 JDK1.7 的时候, ConcurrentHashMap (分段锁) 对整个桶数组进行了分割分段( Segment ),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经 摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
2、 Hashtable (同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
在这里插入图片描述

(补充)Synchronized容器和Concurrent容器有什么区别?

在Java语言中,多线程安全的容器主要分为两种:Synchronized和Concurrent,虽然它们都是线程安全的,但是它们在性能方面差距比较大。
Synchronized容器(同步容器)主要通过synchronized关键字来实现线程安全,在使用的时候会对所有的数据加锁。需要注意的是,由于同步容器将所有对容器状态的访问都串行化了,这样虽然保证了线程的安全性,但是这种方法的代价就是严重降低了并发性,当多个线程竞争容器时,吞吐量会严重降低。于是引入了Concurrent容器(并发容器),Concurrent容器采用了更加智能的方案,该方案不是对整个数据加锁,而是采取了更加细粒度的锁机制,因此,在大并发量的情况下,拥有更高的效率。

12.比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

HashSet 特点:

  1. HashSet中不能有相同的元素,可以有一个Null元素,存入的元素是无序的。
  2. HashSet如何保证唯一性?
    1).HashSet底层数据结构是哈希表,哈希表就是存储唯一系列的表,而哈希值是由对象的hashCode()方法生成。
    2).确保唯一性的两个方法:hashCode()和equals()方法。
  3. 添加、删除操作时间复杂度都是O(1)。
  4. 非线程安全。

LinkedHashSet 特点:

  1. LinkedHashSet中不能有相同元素,可以有一个Null元素,元素严格按照放入的顺序排列。
  2. LinkedHashSet如何保证有序和唯一性?
    1).底层数据结构由哈希表和链表组成。
    2).链表保证了元素的有序即存储和取出一致,哈希表保证了元素的唯一性。
  3. 添加、删除操作时间复杂度都是O(1)。
  4. 非线程安全。

TreeSet 特点:

  1. TreeSet是中不能有相同元素,不可以有Null元素,根据元素的自然顺序进行排序。
  2. TreeSet如何保证元素的排序和唯一性?
    底层的数据结构是红黑树(一种自平衡二叉查找树)
  3. 添加、删除操作时间复杂度都是O(log(n))
  4. 非线程安全

总体而言:
如果你需要一个访问快速的Set,你应该使用HashSet;
当你需要一个排序的Set,你应该使用TreeSet;
当你需要记录下插入时的顺序时,你应该使用LinedHashSet。

13. 集合框架底层数据结构总结

Collection 接口下面的集合:
1.List
Arraylist : Object[] 数组
Vector : Object[] 数组
LinkedList : 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
2.Set
HashSet (无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet : LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样, 不过还是有一点点区别的
TreeSet (有序,唯一): 红黑树(自平衡的排序二叉树)

Map 接口下面的集合:
3.Map
HashMap : JDK1.8 之前 HashMap 由数组+链表组成的,数组是HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了􏰎大的变化,当链表⻓度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的⻓度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap : LinkedHashMap 继承自 HashMap ,所以它的底层仍然是基于拉链式散 列结构即由数组和链表或红黑树组成。另外, LinkedHashMap 在上面结构的基础上,增加 了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的 操作,实现了访问顺序相关逻辑。
Hashtable : 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
TreeMap : 红黑树(自平衡的排序二叉树)

14.如何选用集合?

主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map 接口下的集合,需要排序时选择 TreeMap ,不需要排序时就选择 HashMap ,需要保证线程安全就选用ConcurrentHashMap 。
当我们只需要存放元素值时,就选择实现 Collection 接口的集合,需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet ,不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList ,然后再根据实现这些接口的集合的特点来选用。

15. 几种遍历

collection

//		 遍历【重点】
//		 方法1:增强for
		for (Object object : collection) {
			System.out.println(object);
		}
//		方法2:迭代器
//		Iterator有三个方法.hasNext(),   .next()获取下一个元素并自动后移一位,   .remove();
		Iterator it= collection.iterator();
		while(it.hasNext()) {
			String s = (String)it.next();
			System.out.println(s);
			//collection.remove(s); 这个会报错的
			it.remove();
		}

List

//		遍历【重点】
//		方法1:for循环
		for(int i=0;i<list.size();i++) {
			System.out.println(list.get(i));
		}
//		方法2:增强for
		for (Object object : list) {
			System.out.println(object);
		}
//		方法3:迭代器
		Iterator it = list.iterator();
		while(it.hasNext()) {
			System.out.println(it.next());
		}
//		方法4:列表迭代器(可以从前往后 也可以从后往前)
		ListIterator lit = list.listIterator();
		while(lit.hasNext()) {
			System.out.println(lit.next());
		}
		while(lit.hasPrevious()) {
			System.out.println(lit.previous());
		}

Vector

//		遍历(枚举器)
		Enumeration en = vector.elements();
		while(en.hasMoreElements()) {
			String o = (String)en.nextElement();
			System.out.println(o);
		}

16. TreeSet和TreeMap

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值