关于集合问题,是面试中最常被问到的问题,自己也面临面试,将集合问题稍微总结一下。
1.数组
数组作为我们常用的也是最容易想到的存储对象的容器,有以下特点:
- 长度是固定的,一旦创建了,长度便不可变
- 只能通过下标查询数组中的元素
- 数组中的元素类型必须一致
- 在对数组中的元素进行删除或者插入时需要对数组的长度进行重新的创建,十分繁琐
以上数组的特点,其实也可以说是数组的缺点,但在处理一些同类型数据量小的时候用数组梳理也不是不可。
2.集合(Collection)
集合同样作为存储的容器,相比数组,适用的范围更广,在处理很多问题上有很好的性能。
2.1 集合的特点
- 集合是用来存放对象的数据结构,长度可变
- 可以存放不同类型的对象
- 提供了大批操作集合中对象的方法(参见JDK API手册),更具普适性
2.2 常用集合及其关系
常用的集合有List、Set、以及Map,他们之间有如下关系:
图中Collection作为父类接口,提供了一系列操作集合对象的方法,而List和Set继承了Collection接口,进而获得了Collection接口中的方法。结合JDK API手册我们可以更加直观的看到。
而Map集合是通过键值对存储,HashMap和TreeMap实现了Map集合。如API手册所示
2.3 List、Set、Map区别
- List:存储的元素是有顺序的、可重复
- Set:存储的元素是无序的、不可重复的
- Map:使用键值对(key-value)存储,其中key是无序的,不可重复的,value是无序的,可重复的
2.4 List 、Set、Map底层数据结构
想要更好的了解各个集合的结构特点,我们需要从底层源码看起(虽然真的看不懂)
2.4.1 List
- ArrayList:底层是一个Object[]数组,所以具有数组的特点查询快、增删慢
- LinkedList:底层是双向链表,查询需要遍历,所以相对较慢,但是增删快,我们可以把链表想象成一根锁链,需要去除一截的时候,只要找到那一截,然后解开这一截与他相邻两截的扣,就可以实现元素的增加与删除。
2.4.2 Set
- HashSet:是基于HashMap来实现的,底层维护的是HashMap
- TreeSet:红黑树(自平衡的排序二叉树)
2.4.3 Map
- HashMap:在JDK1.8之前HashMap由数组+链表组成,主要以数组为主,链表的存在主要是为了解决哈希碰撞存在(常见的方法有开放寻址法和“拉链法”)。而在JDK1.8之后,方法有所改变,当链表长度大于8时,会转换为红黑树,而当链表长度小于6后,红黑树又会转变为链表。HashMap底层是Entry[]数组,当我们存放数据时,会根据hash算法来计算数据的存放位置:hash(key)%n,其中n时数组的长度,也就是集合的容量,初始容量为16,加载因子是0.75,也就是存的数据达到容量的75%时开始扩容,按照2的次幂进行扩容。
- TreeMap:红黑树(自平衡的排序二叉树)
2.5 集合该怎么选用?
面对常用的几种集合,我们在处理不同的问题时,要根据问题本身的要求,以及集合的特点来选择合适的集合。
1、当我们只需要存在元素值时,可以选择实现Collection接口的集合,其中如果需要保证元素唯一就选择Set集合,不需要就选择List集合。
2、如果我们需要根据键值对获取元素值时就选用Map集合,其中如果需要排序选择TreeMap,不需要排序就选择HashMap,另外提一个ConcurrentHashMap可以保证线程的安全。(在leetcode上刷了一些算法题发现HashMap用到的很多)
2.6 集合的迭代
如何获取集合中全部的元素,大家最容易想到的是for循环,但对于集合来说,他们有着自己特有的迭代器iterator。
2.6.1 List集合
ArrayList<Integer> list = new ArrayList<>();
list.add(100);
list.add(200);
list.add(300);
list.add(400);
list.add(500);
list.add(null);
/**List集合迭代4种方法*/
System.out.println("方法一:普通for循环");
for (int i = 0 ; i < list.size(); i++){
System.out.println(list.get(i));
}
System.out.println("方法二:高效for循环");
for (Integer ans : list){
System.out.println(ans);
}
System.out.println("方法三:iterator迭代器");
Iterator<Integer> it1 = list.iterator();//获取迭代器对象
while (it1.hasNext()){//判断集合中是否有下一个元素
System.out.println(it1.next());
}
System.out.println("方法四:List集合独特的迭代器listIterator");
ListIterator<Integer> it2 = list.listIterator();
while (it2.hasNext()){
System.out.println(it2.next());
}
2.6.2 Set集合
HashSet<String> set = new HashSet<>();
set.add("aaa");
set.add("bbb");
set.add("ccc");
set.add("ddd");
set.add(null);
/**Set集合的迭代*/
Iterator<String> it = set.iterator();//获取迭代器对象
while (it.hasNext()){
System.out.println(it.next());
}
2.6.3 Map集合
Map<Integer,String> map = new HashMap<>();
map.put(1,"小明");
map.put(2,"小红");
map.put(3,"小王");
map.put(4,"老李");
/**Map集合的迭代
* Map没有自己的迭代器,所以要先将Map转换为Set*/
System.out.println("方法一:将key值放入map");
Set<Integer> ks = map.keySet();//得到map集合中所有的key值放入Set中
Iterator<Integer> it = ks.iterator();//获取迭代器(这里的迭代器是key值所组成的集合的迭代器)
while (it.hasNext()){
Integer key = it.next();//这里获取到的元素是每一个key值
String value = map.get(key);//这里的value根据key值获取
System.out.println(key+","+value);
}
System.out.println("方法二:将key-value作为一个整体(Entry<k,v>)放入Set");
Set<Map.Entry<Integer, String>> eS = map.entrySet();
Iterator<Map.Entry<Integer, String>> it1 = eS.iterator();//获取迭代器
while (it1.hasNext()){
Map.Entry<Integer, String> entry = it1.next();//遍历到的每一个元素是一个entry数组
Integer key = entry.getKey();//获取entry的key值
String value = entry.getValue();//获取entry的value值
System.out.println(key+","+value);
}
大家可能会问,既然可以用for循环进行遍历集合获取元素,那为什么还要有迭代器这个东西,Iterator之所以讯在,是因为它在遍历集合时,可以确保如果当前的集合元素被更改时,会抛出ConcruuentModificationException异常。
我们常用的集合其实都不是线程安全的,当然最简单的办法就是用线程安全的集合来代替,具体可以查看API手册,java.util.concurrent包中提供许多并发容器让我们使用,从而避免线程不安全问题。
3 Collection子接口下各集合的区别
3.1 List(ArrayList、LinkedList)
- 从线程安全性角度来看:不同步,即不保证线程安全。
- 从底层数据结构来看:ArrayList底层为Object数组;LinkedList底层为双向链表(JDK1.6之前为双向循环链表,JDK1.7之后为双向链表)。
- 从访问方式来看:ArrayList支持随机访问,而LinkedList不支持。
3.2 Set(HashSet、TreeSet)
- 从线程安全性角度来看:不同步,即不安全。
- 从底层数据结构来看:HashSet底层为HashMap,Tree底层为红黑树
3.3 Map(HashMap、TreeMap)
TreeMap和HashMap都继承自AbstractMap,但是TreeMap还实现了NavigableMap和SortedMap接口,所以相比HashMap来说 ,TreeMap多了对集合中元素根据键排序的能力以及对集合内元素的搜索能力。
4 其他问题(自己理解不够的问题转载的一些大佬的博文)
- 双向链表和双向循环链表的区别?看图轻松理解数据结构与算法系列(双向链表) - 掘金 (juejin.cn)
- HashMap的长度为什么是2的幂次方?
- 为什么HashMap的长度一定是2的次幂?_ju_362204801的博客-CSDN博客_为什么hashmap的长度是2的
- 红黑树的问题?(非科班同学的噩梦)