2022年Java应届生面试之集合框架

1、干嘛用的:用于存储数据的容器

集合框架是为表示和操作集合而规定的一种统一的标准的体系结构
任何集合框架都包含三大内容:
对外的接口接口的实现、和对集合运算的算法

1.1 接口:

表示集合的抽象数据类型,接口允许我们操作集合时不必关注具体实现,从而达到“多态”,在面向对象编程语言
接口通常用来形成规范。

1.2实现:

集合接口的具体实现,是重用性很高的数据结构

1.3算法:

在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法, 例如查找、排序等。
这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现,事实上,算法是可复用的函数。它减少了程序设计的辛劳。

集合框架通过提供有用的数据结构和算法使你能集中注意力在我们的程序的重要部分,
而不是为了让程序正常运转而将注意力于底层设计上。
特点:

*对象封装数据,对象多个也需要存储。集合用于存储对象
*对象的个数确定可以使用数组,对象的个数不确定可以使用集合,因为集合是可变长度。

2、集合和数组的区别:

2.1、数组是固定长度的, 集合可变长度的。
2.2、数组可以存储基本数据类型,也可以存储引用数据类型。
集合只能存储引用数据类型。
2。3、数组存储的元素必须是同一个数据类型。
集合存储的对象可以是不同的数据类型

3、数据结构:就是容器中存储数据的方式。

对于集合容器,有很多种,因为每一种容器的自身特点不同,其实原理在于每个容器内部数据结构不同

4、集合框架的好处:
4.1、容量自增长;
4.2、提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
4.3、允许不同API之间的互操作,API之间可以来回传递集合;
4.4、可以方便地扩展或改写集合,提供代码复用性和可操作性。
4.5、通过使用JDK自带的集合类,可以降低代码维护和学习新API成本
5、Iterator接口

5.1作用:用于遍历集合元素的接口。

5.2 定义了三个方法:
修饰与类型 方法与描述
boolean hasNext()如果仍有元素可以迭代,则返回true。
E next()返回迭代的下一个元素
void remove()从迭代器指向的collection中移除迭代器返回最后一个元素(可选)

每一个集合都有自己的数据结构(也就是容器中存储数据的方式),都有特点的取出自己内部元素的方式。
为了便于操作所有容器,取出元素
将容器内部的取出方式按照一个统一的规则向外提供。这个规则就是iterator接口。使得对容器的遍历操作与其具体的底层实现相隔离,从而达到解耦的效果。

					使用迭代器遍历集合元素
public static void main(String[] args){
	List<String> list = new ArrayList<>();
	list.add("abc1");
	list.add("abc2");
	list.add("abc");
	
	while循环方式遍历
	Iterator it = list.iterator();
	while (it.hasNext()){
	system.out.println(it.next());
	}
	for循环方式遍历
	for (Iterator it = list.iterator(); it.hasNext(); ){
	System.out.println(it.next());
	}
	}
5.3 使用Iterator迭代器进行删除集合元素,则不会出现并发修改异常。为啥?

在执行remove操作时,同样先执行chexkForComodification(),然后会执行ArrayList的remove()方法。
该方法会将modCount值加+1,这里我们将expectedModCount = modCount,使之保持统一。
使用Iterator方式,如果有并发,需要对Iterator对象加锁。

5.4ListIterator接口

ListIterator是一个功能更加强大的迭代器,它继承于Iterator接口,只能用于各种List类型的访问。
可以通过调用listIterator()方法产生一个指向List开始处的ListIteror,还可以调用listIterator(n)方法创建一个一开始就指向列表
索引为n的元素处的ListIterator

5.4.1特点:
1、允许我们向前、向后两个方向遍历List;
2、在遍历是修改List的元素;
3、遍历是获取迭代器当前游标所在的位置
修饰与类型 方法与描述
void add(E e) 将指定的元素插入到列表 (可选操作)。
boolean hasNext() 如果此列表迭代器在前进方向还有更多的元素时,返回 true。
boolean hasPrevious() 如果此列表迭代器在相反方向还有更多的元素时,返回 true。
E next() 返回列表中的下一个元素和光标的位置向后推进。
int nextIndex() 返回调用 next()后返回的元素索引。
E previous() 返回列表中的上一个元素和光标的位置向前移动。
int previousIndex() 返回调用previous() 后返回的元素索引 。
void remove() 删除列表中调用next()或previous()的返回最后一个元素。
void set(E e) 用指定元素替换列表中调用next()或previous()的返回最后一个元素。
6Collection接口

所有集合类都位于java.util包下,Java的集合类主要由两个接口派生而出:CollectionMapCollectionMap是Java集合框架的根接口
这两个接口又包括了一些子接口或实现类。

   1、Collection一次存一个元素,是单列集合;
2、Map一次存一对元素,是双列集合。
Map存储的一对元素:键--值,键(key)与值(value)间有对应(映射)关系。
							单列集合继承关系图

2022年Java应届生面试之集合框架_第1张图片

			Collection集合主要由List和set两大接口

6.1、List:有序(元素存入集合的顺序和取出的顺序一致),元素都索引,元素可以重复。 可以多个NULL
6.2、Set:无序(存入和取出顺序有可能不一致),元素不可以重复,必须保证元素的唯一性。只能一个NULL

7.List集合

List是元素有序并且可以重复的集合。
list的主要实现:ArrayListLinkListVector
2022年Java应届生面试之集合框架_第2张图片

			ArrayList、LinkedList、Vector 的区别
ArrayList LinkedList Vector
底层实现 数组 双向链表 数组
同步性效率 不同步,非线程安全,效率高,支持随时访问 不同步,非线程安全,效率高 同步、线程安全、效率低
特点 查询快,增删慢 查询慢,增删快 查询快,增删慢
默认容量 10 / /

7.1 总结:
ArrayList和Vector基于数组实现,对应随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针

1、LinkedList不会出现扩容的问题,所有比较适合随机增删,但是其基于链表实现,
	所有定位时需要线性扫描,效率较低
2、当操作是在一列数据的后面添加数据时而不是在前面或中间,
	并且需要随机地访问其中的元素时,ArrayList会提供更好的性能
3、当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中元素时,
	就应该使用LinkedList。

2022年Java应届生面试之集合框架_第3张图片

foreach循环遍历容器本质上是使用迭代器进行遍历的,会对修改次数modCount进行检查,不允许集合进行更改操作

/**
  * Description: 使用迭代器遍历
  */
public static void remove3(List<String> list) {
    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
        String s = it.next();
        if (s.equals("b")) {
            it.remove();
        }
    }
}
public static void main(String[] args) {
    List<String> arrayList = new ArrayList<String>();
    arrayList.add("a");
    arrayList.add("b");
    arrayList.add("b");
    arrayList.add("c");
    arrayList.add("d");
    arrayList.add("e");
    // remove(arrayList);
    // remove2(arrayList);
    remove3(arrayList);
    System.out.println(arrayList);
}
7.2总结:
如果想要正确的循环遍历删除(增加)元素,需要使用迭代器遍历删除(增加)方法
8.Set集合

Set集合元素无序(存入和取出的顺序不一定一致),并且没有重复的对象
Set的主要实现类:HashSet 、 TreeSet

				Set的常用方法

2022年Java应届生面试之集合框架_第4张图片

HashSet、TreeSet、LinkedHashSet的区别
HashSet TreeSet LinkedHashSet
底层实现 HashMap 红黑树 LindedHashMap
重复性 不允许 不允许 不允许
有无序 无序 有序 有序支持自然排序(默认)和定制两种排序方式以元素插入的顺序来维护集合的链表
时间复杂度 add()remover()contains()方法的时间复杂度是O(1) add(),remove(), contains()方法的时间复杂度是O(1) LinkedHashSet在迭代访问Set中的全部元素时,contains()方法的时间复杂度是O(login) 性能比HashSet好,插入性能稍微逊色于HashSet,时间复杂度是 O(1)。
同步性 不同步、线程不安全 不同步、线程不安全 不同步、线程不安全
null值 允许null值 不允许、会抛出空指针异常 允许
8.1 如何检测HashSet重复---->被问了n次了

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

1、如果两个对象相等,则hashcode一定也相同
2、两个对象相等,equals方法返回true
3、两个对象有相同的hashcode值,他们也不一定相等的
4、总结:equals()方法被覆盖时,则hashCode方法也必须被覆盖

hashCode()默认行为是对上的对象产生独特值,如果没有重写hashcode,则class的两个对象无论如果也不会相等
(即使两个对象有相同的数据)。
hashSet是一个通用功能的Set,而LinkedHashSet提供元素插入顺序的保证,TreeSet则是一个SortedSet实现,由Comparable或者Comparable指定的元素顺序存储元素

SortedSet(java.util.SortedSet)是Set接口的子类,
Sortedset接口的行为类似于普通Set,但它包含的元素是在内部排序的, 
这意味着,当迭代SortedSet的元素时,这些元素将按排序的顺序进行迭代

科普:红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组

9.Map接口

Map是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。Map没有继承collection接口。
从Map集合中检索时,只要给出键对象,就会返回对应的值对象。
Map的常用实现类:HashMap 、 TreeMap 、 HashTable 、LinkedHashMap、ConcurrentHashMap
双列集合继承关系图
2022年Java应届生面试之集合框架_第5张图片

9.1Map常用方法
2022年Java应届生面试之集合框架_第6张图片

说说关于Map的循环
1for(Integer key : map.keySet()){}
2、map.forEach((key ,value) -> {});
3for(Map.Entry<Integer, String> entry : map.entrySet()){}
9.2HashMap、HashTable、TreeMap的区别
HashMap HashTable TreeMap
底层实现 哈希表(数组和链表) 哈希表(数组和链表) 红黑树
同步性 线程不同步 同步 线程不同步
null值 允许key和value是null,但是只允许一个key为null,且这个元素存放在哈希表 不允许 value允许为null。当未实现 当实现了,若未对 null 情况进行判断,则可能抛NullPointerException异常。如果针对null情况实现了,可以存入,但是却不能正常使用get()访问,只能通过遍历去访问
hash 使用hash(Object key)扰动函数对key的hashCode进行扰动后作为hash值 直接使用key的hashCode()返回值作为hash值
容量 容量为 2^4 且容量一定是 2^n 默认容量是11,不一定是 2^n
扩容 两倍,且哈希桶的下标使用&运算代替了取模 2倍+1,取哈希桶下标是直接用模运算

TreeMap: 取出来的是排序后的键值对。如果要按自然顺序或者自定义顺序遍历键,则TreeMap更适合

LinkedHashMap:是HashMap一个子类,如果要输出的顺序和输入的顺序相同,那么用LinkedHashMap可以实现,还可以读取顺序来排列

HashMap:里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值。
具有很快的访问速度,在Map中插入和定位元素,HashMap是很好的选择

9.3	HashMap在JDK1.7和JDK1.8中有哪些不同

不同JDK 1.7JDK 1.8存储结构数组 + 链表数组 + 链表 + 红黑树插入数据方式头插法(先讲原位置的数据移到后1位,再插入数据到该位置)
尾插法(直接插入到链表尾部/红黑树)

10 集合工具类Collections
Collections:集合工具类,方便对集合的操作。这个类不需要创建对象,内部提供的都是静态方法

11、	Collection 和 Collections的区别

Collections是个java.util下的类,是针对集合类的一个工具类,提供一系列静态方法
实现对集合的查找排序替换线程安全化
将非同步的集合转换成同步的,等操作

Collection是个java.util下的接口,它是各种集合结构的父接口,
继承于它的接口主要由List和Set
提供了关于集合的一些操作,如插入、删除等操作
12Collections用法:

void sort(List list):根据元素的自然顺序对指定 List 集合的元素按升序进行排序。

public class Test1 {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        List prices = new ArrayList();
        for (int i = 0; i < 5; i++) {
            System.out.println("请输入第 " + (i + 1) + " 个商品的价格:");
            int p = input.nextInt();
            prices.add(Integer.valueOf(p)); // 将录入的价格保存到List集合中
        }
        Collections.sort(prices); // 调用sort()方法对集合进行排序
        System.out.println("价格从低到高的排列为:");
        for (int i = 0; i < prices.size(); i++) {
            System.out.print(prices.get(i) + "\t");
        }
    }
}
					数组工具类Arrays

用于操作数组对象的工具类,里面都是静态方法。
数组——>集合:asList方法,将数组转换成list集合

String[] arr ={"abc","kk","qq"};
List<String> list = Arrays.asList(arr);
将数组转换成集合,有什么好处?

用asList方法,将数组变为集合;

1、可以通过list集合的方法来操作数组中的元素:isEmpty()、contains、indexOf、set;
2、主要(局限性),数组是固定长度,不可以使用集合对象新增或删除等,会改变数组长度的功能方法。比如add、remove、clear。(会报不支持操作异常UnsupportedOperationException);数组元素的修改,会影响到转化过来的集合。
3、如果数组中存储的引用数据类型,直接作为集合的元素可以直接用集合的方法操作
4、如果数组中存储的是基本数据类型,asList会将数组实体作为集合元素存在;
集合 ——> 数组:

用的是Collection接口中的toArray()方法;
如果给toArray传递的指定类型的数据长度小于了集合的size,那么toArray方法,会自定义创建该类型的数据,长度为集合size。

如果传递的指定的类型数组的长度大于了集合的size,那么toArray方法,就不会创新数组,直接使用该数组即可。

并将集合元素存储到数组中,其他为存储元素的位置默认值为null。
所以,在传递指定类型数组时,最好的方式就是指定的长度和size相等的数组。

`将集合转变成数组后有什么好处,

限定了对集合中的元素进行增删操作,只要获取这些元素即可。
用基本数据类型的数组转换ArrayList,ArrayList的size有问题
.对于转换过来的集合,它的 add/remove/clear 方法会抛出: UnsupportedOperationException
Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。

public static void main(String[] args) {
    int[] arr1 = { 1, 2, 3, 4, 5 };
    List<int[]> intList = Arrays.asList(arr1);
    // intList size: 1
    System.out.println(String.format("intList size: %s", intList.size()));

    Integer[] arr2 = { 1, 2, 3, 4, 5 };
    List<Integer> integerList = Arrays.asList(arr2);
    // integerList size: 5
    System.out.println(String.format("integerList size:%s", integerList.size()));
}
问题:asList 方法接受的参数是一个泛型的变长参数,
我们都知道基本数据都是无法泛型化的,也就是说基本数据类型是无法作为asList()方法参数的
,要想作为泛型参数就必须使用其所对应的包装类型。但实例为什么没有出错?

因为实例是将int类型的数组当做其他参数,而在java中数组是一个对象,它是可以泛型化。所有例子不会产生错误。既然例子
是将整个int 类型的数组当做泛型参数,那么经过asList转换就只有一个int 的列表了.

结论:在使用asList()方法时尽量不要讲基本数据类型数组转List
HashMap 为什么线程不安全

1、在JDK1.7中,当并发执行扩容操作时会造成环形链 和数据丢失的情况。(链表的头插法,造成环形链)
2、JDK1.8中,在并发执行put操作时 会发生数据覆盖的情况。 (元素插入时是使用尾插法)
HashMap在put的时候,插入的元素超过了容量 (由负载因子决定) 就会触发扩容操作,就是rehash。
这个会将原数组的内容 重新 hash 到新的扩容数组中。
在多线程环境下,存在同时其他元素也在进行put操作,如果hash值相同,可能同时在同一个数组下用链表表示。
造成闭环,导致在get时出现死循环,所有HashMap是线程不安全

		HashMap在jdk7和8中的区别
1、JDK1.7用的是头插法,而JDK1.8之后使用的是尾插法。

JDK1.7用的是单链表进行的纵向延伸,采用头插法就是能够提高插入的效率。但是也容易出现逆序 且环形链表 死循环问题。
JDK1.8中之后是因为加入了红黑树使用的尾插法,能避免出现逆序 且环形链表死循环问题。

2、扩容后数据存储位置的计算方式也不一样,

JDK1.7的时候是直接用hash值 和 需要扩容的二进制数进行 & (这就是为什么扩容的时候为什么一定 必须是 2的多少次幂的原因所在,
因为如果只有 2的n次幂的情况时最后一位二进制数才一定是1.这样能最大程度减少hash碰撞)
JDK1.8当往HashMap放入元素时,如果元素的个数大于threshold(阀值)时,会进行扩容,使用2倍容量的数据替代原有数组

为什么新的数组要用2倍的容量?

由于数组的容量是以2的幂次方扩容的,那么在扩容时,新的位置要么在原来位置,要么在长度+原位置的位置

为什么?

原因是数组的长度变为原来的2倍,表现在二进制上就是多了一个高位参与数组下标计算
也就是说,在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还0,
是0的话索引没有变,是1的话索引变成“原索引+oldCap” (Java 是根据 (e.hash & oldCap) == 0 来判断的:)
这样可以省去重新计算hash值的时间,而且新增的1bit是0还是1,可以认为是随机的,因此resize的过程会均匀的把
之前的冲突的节点分数到新的bucket(桶下标)。 当node的hash值 & 旧数组的长度 == 0时,这个数据是不需要换桶位置的

HashMap 为啥将链表改成红黑树?

提高检索时间,在链表长度大于8的时候和数组容量大于64,将后面的数据存在红黑树中,以提高检索速度。复杂度变为O(logn)
为何要用红黑树,为一上来不树化?
链表只有一个两个元素的时候,最多计较也是两三次。
链表断的情况下,性能是比红黑树好的。

红黑树何时会退化成链表

1、在扩容时如果拆分时,树元素个数 <= 6 则会退化链表
2、remove树节点时,若root root.left root.right root.left.left. 中有一个为null,也会退化为链表

HashSetHashMap 之间有什么关系?它们的key用什么策略来放置?
如果有冲突用什么方法解决?

HashSet底层是是基于HashMap实现的,HashSet只用了HashMap的key,没有用value。

如果两个不同对象映射到散列表(数组)的元素下标相同,
这种现象称为hash冲突。
1、开发寻址法;
2、在散列法:建立多个hash函数,若是当发生hash冲突时,使用下一个hash函数,直到找和存放元素的位置
3、拉链法:(链地址法):就是在冲突的位置上建立一个链表,然后将冲突的元素插入链表的尾部
4、建立公共溢出区:将哈希表分为基本表和溢出表,将与基本表发送冲突的元素放入溢出表中
HashMap的put()

使用Hash算法计算key的索引,如果该索引出没有存在元素,就直接插入。
如果索引出有元素,一般有两种情况:

1、在链表形式就直接遍历到尾端插入
           2、在红黑树就按照红黑树结构插入
ConcurrentHashMap原理 ,JDK1.7  VS  JDK1.8

1.7:

数据结构:ReentrantLock + Segment + HashEntry,一个Segment中包含一个HashEntry数组,每一个HashEntry又是一个
链表结构
元素查询:二次hash,(key)第一次Hash定位到Segment,第二次Hash定位到元素所在链表的头部
锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他操作的Segment不受影响,并发度为Segment个数
可以通过构造函数指定,数组扩容不会影响其他的Segment
get方法无需加锁,voltile保证
segment.put(){
Lock();加锁
将封装进来的Entry对象,放到Segment中的内部数组中
}

1.8:

数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
查找、替换、赋值操作都是使用CAS
锁 : 锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时:阻塞所有读写操作,并发扩容
读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量相互可见
数组用volatile修饰,保证扩容时被读线程感知

未完待续

你可能感兴趣的:(java,面试,数据结构)