三、Java集合面试题
1、集合概述:
用于存储数据的容器。
2、集合框架:
集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。
任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。
3、集合和数组的区别:4、List、Set两者的区别
List、Set都继承Collection,都是单链集合
5、HashMap、HashTable、HashSet6、HashMap的存储原理
定义:基于哈希表(关联数组)实现的Map。
原理:基于Hash算法,通过put存储,get取值。传入key值时,根据key.hashCode()计算出hash值,再根据hash值将value保存到bucket中。
优先使用数组存储。当计算的hash值相同时,也就是hash冲突。此时转换为链表存储hash值相同的value。当链表长度大于8时,为提高寻址速度,改为红黑树存储。
7、迭代器lterator
作用:提供遍历任何Collection的接口
特点:单向遍历,更加安全。若在遍历时集合元素被更改,就会抛出异
8、ArrayList、LinkedList、Vector常。
9、集合框架底层数据结构Collection
List
Arraylist: Object数组
LinkedList: 双向循环链表
Vector: Object数组
Set
HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
Map
HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
TreeMap: 红黑树(自平衡的排序二叉树
10、怎么确保一个集合不能被修改?
12、迭代器 Iterator 是什么?
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
因为所有Collection接口继承了Iterator迭代器
13、Iterator 和 ListIterator 有什么区别?
Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
14、List和Set的区别
List: 有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,在逐个遍历,还可以使用get(int index)获取指定下标的元素
Set: 无序,不可重复,最够允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元素,在逐一遍历各个元素
15、ArrayList和LinkedList的区别
16、HashMap和hasptable有什么区别
从功能特性的角度来说HashTable是线程安全的,而HashMap不是。
HashMap的性能要比HashTable更好,因为,HashTable采用了全局同步锁来保证安全性,对性能影响较大
从内部实现的角度来说HashTable使用数组加链表、HashMap采用了数组+链表+红黑树。
HashMap初始容量是16、HashTable初始容量是11
HashMap可以使用null作为key,HashMap会把null转化为0进行存储,而Hashtable不允许。
最后,他们两个的key的散列算法不同,HashTable直接是使用key的hashcode对数组长度做取模。而HashMap对key的hashcode做了二次散列,从而避免key的分布不均匀问题影响到查询性能。
17、说一下HashMap的Put方法
先说HashMap的Put方法的大体流程:
根据Key通过哈希算法与与运算得出数组下标
如果数组下标位置元素为空,则将key和value封装为Entry对象 (JDK1.7中是Entry对象, JDK1.8中是Node对象)并放入该位置
如果数组下标位置元素不为空,则要分情况讨论
如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中
如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node
如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value
如果此位置上的Node对象是链表节点,则将key和value封装为一 个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在
遍历链表的过程中会判断是否存在当前key,如果存在则更新vatue,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个
数,如果大于等于8,那么则会将该链表转成红黑树
将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束PUT方法
18、HashMap的扩容机制原理
1.7版本
1.先生成新数组
2.遍历原来数组中的每个位置上的链表上的每个元素
3.取每个元素 的key,并基于新数组长度,计算出每个元素在新数组中的下标
4.将元素添加到新数组中去
5.所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
先生成新数组
遍历原来数组中的每个位置上的链表或红黑树
如果是链表,则直接将链表中的每个元索重新计算下标,并添加到新数组中去
如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元索元素在新数组中的下标位置。
统计每个下标位置的元素个数
如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
如果该位置下的元素个数没有超过8,那么则生成一个链表, 并将链表的头节点添加到新数组的对应位置
5.所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
19、说一下 ArrayList 的优缺点
ArrayList的优点如下:
ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
ArrayList 在顺序添加一个元素的时候非常方便。
ArrayList 的缺点如下:
删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
插入元素的时候,也需要做一次元素复制操作,缺点同上。
ArrayList 比较适合顺序添加、随机访问的场景。
ArrayList的扩容机制
默认情况下,新的容量会是原容量的1.5倍。 新容量=旧容量右移一位(相当于除于2)在加上旧容量
ArrayList 的底层是用动态数组来实现的。我们初始化一个ArrayList 集合还没有添加元素时,其实它是个空数组,只有当我们添加第一个元素时,内部会调用扩容方法并返回最小容量10,也就是说ArrayList 初始化容量为10。 当前数组长度小于最小容量的长度时(前期容量是10,当添加第11个元素时就就扩容),便开始可以扩容了,ArrayList 扩容的真正计算是在一个grow()里面,新数组大小是旧数组的1.5倍,如果扩容后的新数组大小还是小于最小容量,那新数组的大小就是最小容量的大小,后面会调用一个Arrays.copyof方法,这个方法是真正实现扩容的步骤。
20、如何实现数组和 List 之间的转换?
数组转 List:使用 Arrays. asList(array) 进行转换
List 转数组:使用 List 自带的 toArray() 方法。
21、ArrayList 和 Vector 的区别是什么?
22、说一下 HashSet 的实现原理?
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
23、 HashSet如何检查重复?HashSet是如何保证数据不可重复的?
向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。
24、说一下HashMap的实现原理?
25、HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
HashMap JDK1.8之前
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
HashMap JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
26、说说什么是红黑树
27、能否使用任何类作为 Map 的 key?
28、HashMap 的长度为什么是2的幂次方
29、HashMap 与 HashTable 有什么区别?
30、Array 和 ArrayList 有何区别?
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
31、comparable 和 comparator的区别?
comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().
32、Collection和Collections有什么区别?
java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
四、Java线程面试题
1、为什么要使用多线程?
(1)充分发挥多核CPU的优势
(2)防止程序阻塞
(3)便于建模。
2、单线程和多线程的区别。
(1)单核CPU中,每个时间片段,只能有一个线程运行。
(2)单线程程序执行效率更快,多线程用户响应时间更少。
3、进程和线程
进程:程序(有指令和数据的文件)的一次执行过程。
线程:轻量级进程。进程的组成单元。同类多个线程共享同一块内存空间和一组资源。
4、并行和并发
并发:一个CPU在多个线程之间快速切换,达到同时执行多个任务的目的。
并行:多个CPU可以同时执行一个进程中的多个线程。
5、线程的创建方式
方法一:继承Thread类,作为线程对象存在(继承Thread对象)
方法二:实现runnable接口,作为线程任务存在
方法三:匿名内部类创建线程对象
方法四:创建带返回值的线程
方法五:定时器Timer
方法六:线程池创建线程
方法七:利用java8新特性stream 实现并发
让线程等待的方法
Thread.sleep(200); //线程休息2ms
Object.wait(); //让线程进入等待,直到调用Object的notify或者notifyAll时,线程停止休眠
6、线程的六种状态()
(1)初始状态:线程构建,但还没有调用start()方法。
(2)运行状态:操作系统中的“就绪”和“运行”。
(3)阻塞状态:线程阻塞于锁。
(4)等待状态:当前线程需要等待其他线程做出特定动作(通知/中断)。
(5)超时等待状态:在特定的时间自行返回。
(6)终止状态:当前线程已执行完毕。
7、如何停止一个正在运行的线程?
(1)使用推出标致(当run()方法完成后线程终止,正常退出)。
(2)使用stop方法强行终止,(不推荐,过期作废)。
(3)使用interrupt方法中断线程
8、start()和run()方法的区别?
Start():调用后才能出现多线程的特性,不同线程的run()方法里面的代码交替执行。
run():如果调用后,代码还是同步执行的,必须等待一个线程的run()方法里的代码全部执行完毕,另一个run()才能开始执行。
9、为什么调用start()方法的时候会执行run()方法,而不能直接调用run()方法?
JVM执行start方法,会令其一条线程执行继承的thread的run方法,这才起到多线程的效果。
如果直接调用thread的run方法,其方法还是在运行的主线程中,没有起到多线程的效果。
10、什么是线程安全?
多线程访问同一代码,而不会产生不确定的结果。
11、Java中堆和栈的区别?
栈:在函数中定义的基本类型的变量和对象的引用变量都在函数的栈内存中。(用来存放执行程序)
堆:用于存放由new创建的对象和数组。
12、如何确保线程安全?
(1)对非安全的代码进行“加锁”。
(2)使用线程安全的类。
(3)多线程高并发情况下,线程共享的变量改为“方法级的局部变量”。
13、线程的安全级别?
线程安全:在多 线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
(1)不可变:不可变的对象一定是线程安全的,永远也不需要额外的同步。
(2)无条件的线程安全:由类的规格说明所规定的的约束,在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要额外的同步。
(3)有条件的线程安全:有条件的线程安全类对于单独的操作可以是线程安全,但是某些操作序列可能需要额外的同步。
(4)非线程安全(线程兼容):线程兼容类不是线程安全的,但是可以通过正确使用同步,在并发环境中安全地使用。
(5)线程对立:是那些不管是否采用了“同步措施”,都不能在多线程环境中并发使用的代码。
14、线程类的构造方法和静态块是被哪个线程调用的?
是被new这个线程类所在的线程所调用的,而run里面的代码才是被线程自身调用。
15、线程池的作用
(1)降低资源消耗。(重复利用已创建的线程降低线程创建和销毁所造成的消耗)
(2)提高响应速度。(任务到达时,任务可以不需要等到线程创建就能立即执行)
(3)提高线程的可管理性。(线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同一的分配,调优和监控)
16、Java中的死锁问题
死锁:死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
必要条件:
(1)互斥条件:进程对所分配到的资源进行排他性使用。
(2)请求和保持条件:进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程占有。
(3)不剥夺条件:进程已获得的资源,在未使用完之前,不能被剥夺。
(4)环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
如何解决死锁问题
程序出现死锁,是因为在多线程环境里面两个或两个以上的线程同时满足互斥条件、请求保持条件、不可抢占条件、循环等待条件。出现死锁以后,可以通过jstack命令去导出线程的dump日志,然后从dump日志里面定位到具体死锁的程序代码。通过修改程序代码去破坏这四个条件里面的任意一个,就可以解决死锁问题。当然,因为互斥条件因为是锁本身的特性,所以不能被破坏。
17、如何避免死锁和检测
(1)预防死锁:破坏互斥关系、破坏请求和保持条件、破坏不剥夺条件、破坏循环等待条件。
(2)设置加锁顺序。
18、synchronized底层原理?
同步代码块:
synchronized(锁对象){ 需要同步执行的代码 }
原理:一旦线程进入了该代码块,就持有锁,JVM会有监视器监视进入锁的线程,其他线程想进入代码,监视器就会拒绝访问;一旦持有锁的线程执行完代码,锁就会被释放,其他线程才可进入。
注意:任何对象都可以作为锁,必须是
19、生产者消费者模型的作用是什么?
(1)平衡生产者的生产能力和消费者的消费能力,提升整个系统的运行效率。
(2)解耦。成员变量。
20、生产者消费者模式
生产者是生产数据的线程,消费者是消费数据的线程。
如果生产者处理速度远快过消费数据的速度,那么生产者必须等待消费者处理完,才能继续生产数据。
反之,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。
21、创建线程池的几种方法
方式一:通过构造方法实现
方式二:通过****Executor 框架的工具类Executors来实现 我们可以创建三种类型的
22、自定义线程池的配置
ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
参数说明:优化配置:
核心线程数大于或等于CPU内核的数量(如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1,如果是IO密集型任务,参考值可以设置为2*NCPU)获得CPU的内核数:Runtime.getRuntime().availableProcessors()
最大线程数和核心线程数配置一样,性能比较高,因为避免临时线程的创建和销毁
如果临时线程比较多,可以将存活时间配置稍微长点,减少临时线程的创建和销毁
4、阻塞队列LinkedBlockingQueue的性能比较高
23、什么是乐观锁和悲观锁悲观锁
想法悲观,认为当前的资源存在竞争,所以每次获得资源时都会上锁,阻塞住其它线程。
数据库中的行锁、表锁、读锁、写锁都属于悲观锁,Java的synchronized和ReentrantLock也属于悲观锁。
悲观锁会降低系统性能和吞吐量,提高数据的安全性,适用于多写少读的场景。
乐观锁
想法乐观,认为当前的资源不存在竞争,所以每次获得资源时都不上锁
乐观锁执行效率高,有利于提高系统吞吐量,适用于多读少写的场景。