1、说说 List, Set, Queue, Map 四者的区别?
⚫
List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
⚫
Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。
⚫
Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序
的、可重复的。
⚫
Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数
y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、
可重复的,每个键最多映射到一个值
2、如何选用集合?
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map 接口下
的集合,需要排序时选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选
用 ConcurrentHashMap。
当我们只需要存放元素值时,就选择实现 Collection 接口的集合,需要保证元素唯一时
选择实现 Set 接口的集合比如 TreeSet 或 HashSet,不需要就选择实现 List 接口的比
如 ArrayList 或 LinkedList,然后再根据实现这些接口的集合的特点来选用。
3、为什么要使用集合?
当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就
是数组,但是,使用数组存储对象具有一定的弊端, 因为我们在实际开发中,存储的数据的
类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该
数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 但是集合
提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保
存具有映射关系的数据。
4、 Arraylist 和 Vector 的区别?
⚫
ArrayList 是 List 的主要实现类,底层使用 Object[]存储,适用于频繁的查找工作,线程
不安全 ;
⚫
Vector 是 List 的古老实现类,底层使用 Object[] 存储,线程安全的。
5、Arraylist 与 LinkedList 区别?
⚫
是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
⚫
底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链
表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链
表的区别,下面有介绍到!)
⚫
插入和删除是否受元素位置的影响:
a.
ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比
如:执行 add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末
尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话
(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候
集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
b.
LinkedList 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影
响(add(E e)、addFirst(E e)、addLast(E e)、removeFirst() 、 removeLast()),
近似 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E
element),remove(Object o)) 时间复杂度近似为 O(n) ,因为需要先移动到指定
位置再插入。
⚫
是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快
速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index)方法)。
⚫
内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空
间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空
间(因为要存放直接后继和直接前驱以及数据)。
6、Comparable 和 Comparator 的区别?
⚫
comparable 接口实际上是出自 java.lang 包 它有一个 compareTo(Object obj)方法用
来排序
⚫
comparator 接口实际上是出自 java.util 包它有一个 compare(Object obj1, Object
obj2)方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写 compareTo()方法或
compare()方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名
和歌手名分别采用一种排序方法的话,我们可以重写 compareTo()方法和使用自制的
Comparator 方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们
只能使用两个参数版的 Collections.sort().
7、无序性和不可重复性的含义是什么
⚫
什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组
索引的顺序添加 ,而是根据数据的哈希值决定的。
⚫
什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要
同时重写 equals()方法和 HashCode()方法。
8、比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
⚫
HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可
以存储 null 值;
⚫
LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历;
⚫
TreeSet 底层使用红黑树,元素是有序的,排序的方式有自然排序和定制排序。
9、试比较 Queue 与 Deque 的区别
Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出
(FIFO) 规则。
Queue 扩展了 Collection 的接口,根据 因为容量问题而导致操作失败后处理方式的不
同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。
Deque 是双端队列,在队列的两端均可以插入或删除元素。
Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失
败后处理方式的不同分为两类:
事实上,Deque 还提供有 push() 和 pop() 等其他方法,可用于模拟栈。
10、请谈一下对 PriorityQueue 的认识?
PriorityQueue 是在 JDK1.5 中被引入的, 其与 Queue 的区别在于元素出队顺序是与优先
级相关的,即总是优先级最高的元素先出队。
这里列举其相关的一些要点:
⚫
PriorityQueue 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据
⚫
PriorityQueue 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和
删除堆顶元素。
⚫
PriorityQueue 是非线程安全的,且不支持存储 NULL 和 non-comparable 的对象。
⚫
PriorityQueue 默认是小顶堆,但可以接收一个 Comparator 作为构造参数,从而来自定
义元素优先级的先后。
11、HashMap 和 Hashtable 的区别?
⚫
线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因
为 Hashtable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话
就使用 ConcurrentHashMap 吧!);
⚫
效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,
Hashtable 基本被淘汰,不要在代码中使用它;
⚫
对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null
作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否
则会抛出 NullPointerException。
⚫
初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,
Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默
认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量
初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂
次方大小(HashMap 中的 tableSizeFor()方法保证,下面给出了源代码)。也就是
说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
⚫
底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长
度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于
64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减
少搜索时间。Hashtable 没有这样的机制。
12、HashSet 如何检查重复?
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的
位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,
HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用
equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会
让加入操作成功。
在 openjdk8 中,HashSet 的 add()方法只是简单的调用了 HashMap 的 put()方法,并
且判断了一下返回值以确保是否有重复元素
。
也就是说,在 openjdk8 中,实际上无论
HashSet 中是否已经存在了某元素,HashSet 都会直接插入,只是会在 add()方法的返回值
处告诉我们插入前是否存在相同元素。
13、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 的幂次
方。
14、ConcurrentHashMap 和 Hashtable 的区别?
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
⚫
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,
JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。
Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形
式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
⚫
实现线程安全的方式(重要): ① 在 JDK1.7 的时候,ConcurrentHashMap(分段
锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线
程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的
时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来
实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁
做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还
能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线
程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put
添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率
越低。
15、ConcurrentHashMap 线程安全的具体实现方式是怎样的?
JDK1.7
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其
中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。
HashEntry 用于存储键值对数据。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构
和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每
个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元
素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。
JDK1.8
ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并
发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度
超过一定阈值(
8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为
O(log(N)))
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会
产生并发,效率又提升 N 倍。
16、TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如
何比较元素?
TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素
的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键
值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。
Collections 工具类的 sort 方法有两种重载的形式:
第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比
较。
第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是
Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定
义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java
中对函数式编程的支持)。
17、Collection 和 Collections 有什么区别?
java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进
行基本操作的通用接口方法。Collection 接口在 Java 类库中有很多具体的实现。Collection
接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有 List 与
Set。
Collections 则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合
中元素进行排序、搜索以及线程安全等各种操作。
18、Array 和 ArrayList 有何区别?
⚫
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
⚫
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有
ArrayList 有。
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据
类型的时候,这种方式相对比较慢。
19、HashMap 和 ConcurrentHashMap 的区别
ConcurrentHashMap 对整个桶数组进行了分割分段(Segment),然后在每一个分段上都
用 lock 锁进行保护,相对于 HashTable 的 synchronized 锁的粒度更精细了一些,并发性能
更好,而 HashMap 没有锁机制,不是线程安全的。(JDK1.8 之后 ConcurrentHashMap 启
用了一种全新的方式实现,利用 CAS 算法。)
HashMap 的键值对允许有 null,但是 ConCurrentHashMap 都不允许。
20、如果使用 Object 作为 HashMap 的 Key,应该怎么办呢?
重写 hashCode()和 equals()方法
⚫
重写 hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算
中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的 Hash 碰
撞。
⚫
重写 equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非 null 的引
用值 x,x.equals(null)必须返回 false 的这几个特性,目的是为了保证 key 在哈希表中的
唯一性。
21、为什么 HashMap 中 String、Integer 这样的包装类适合作为 K?
String、Integer 等包装类的特性能够保证 Hash 值的不可更改性和计算准确性,能够有
效的减少 Hash 碰撞的几率。
⚫
都是 final 类型,即不可变性,保证 key 的不可更改性,不会存在获取 hash 值不同的情
况
⚫
内部已重写了 equals()、hashCode()等方法,遵守了 HashMap 内部的规范(不清楚可
以去上面看看 putValue 的过程),不容易出现 Hash 值计算错误的情况;
22、什么是哈希冲突?
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰
撞(哈希碰撞)。
23、你知道 fail-fast 和 fail-safe 吗?
fail-fast 是 Java 中的一种 快速失败 机制,java.u til 包下所有的集合都是快速失败的,快
速失败会抛出 ConcurrentModificationException 异常,fa ail-fast 你可以把它理解为一种
快速检测机制它只能用来检测错误,不会对错误进行恢复,fail-fast 不一定只在多线程 环境
下存在,ArrayList 也会抛出这个异常,主要原因是由于 modCount 不等于 ex
pectedModCount。
fail-safe 是 Java 中的一种安全失败机制,它表示的是在遍历时不是直接在原集合上进行
访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。 由于迭代时是对原集合的拷贝
进行遍历,所以在 遍历过程中对原集合所作的修改并不能被迭代器检测到 所以不会触
发 ConcurrentModificationException。java.util.conc urrent 包下的容器都是安全失败的,
可以在多线程条件下使用,并发修改。
24、Arrays.asList 获得的 List 应该注意什么?
Arrays.asList 是 Array 中的一个静态方法,它能够实现把数组转换成为 List 序列,需要注意
下面几点:
⚫
Arrays.asList 转换完成后的 List 不能再进行结构化的的修改,什么是结构化的修改?就是
不能再进行任何 List 元素的增加或者减少的操作。
public static void main(String[] args) {
Integer[] integer = new Integer[] { 1, 2, 3, 4 };
List integetList = Arrays.asList(integer);
integetList.add(5);
}
// 结果抛出异常
// Exception in thread "main" java.lang.UnsupportedOperationException
我们看源码就很容易发现问题:
// 这是 java.util.Arrays 内部类,
//而不是 java.util.ArrayList
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
继承 AbstractList 中对 add、remove、set 方法是直接抛异常的,也就是说如果继承的子
类没有去重写这些方法,那么子类的实例去调用这些方法是会直接抛异常的。
25、final、finally 和 finalize()的区别
finally 是一个关键字,它经常和 try 块一起使用,用于异常处理。使用 try...finally 的代码块种,
finally 部分的代码一定会被执行,所以我们经常在 finallv 方法中用于资源的关闭操作。
JDK1.7 中,推荐使用 try-with-resources 优雅的关闭资源,它直接使用 try0 进行资源的关闭即可,
就不用写 finally 关键字了。
finalize 是 Object 对象中的一个方法,用于对象的回收方法,这个方法我们一般不推荐使用,
finalize 是和垃圾回收关联在一起的,在 Java9 中,将 finalize 标记为了 deprecated,如果没有特别
原因,不要实现 finalize 方法,也不要指望他来进行垃圾回收。
26、内部类有哪些分类,分别解释一下
在 Java 中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类
的一个属性,与其他属性定义方式一致。内部类的分类一般主要有四种:
⚫
成员内部类
⚫
局部内部类
⚫
匿名内部类
⚫
静态内部类
静态内部类就是定义在类内部的静态类,静态内部类可以访问外部类所有的静态变量,而不可访
问外部类的非静态变量;
成员内部类就是定义在类内部,成员位置上的非静态类,就是成员内部类。成员内部类可以访问
外部类所有的变量和方法,包括静态和非静态,私有和公有。
定义在方法中的内部类,就是局部内部类。定义在实例方法中的局部类可以访问外部类的所有变
量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。
匿名内部类就是没有名字的内部类,除了没有名字,匿名内部类还有以下特点:
⚫
匿名内部类必须继承一个抽象类或者实现一个接口
⚫
匿名内部类不能定义任何静态成员和静态方法。
⚫
当所在的方法的形参需要被匿名内部类使用时,必须声明为 final.
⚫
匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
27、项目为 UTF-8 环境,charc=中,是否合法
可以,因为 Unicode 编码采用 2 个字节的编码,UTF-8 是 Unicode 的一种实现,它使用可变长
度的字符集进行编码,charc=中是两个字节,所以能够存储。合法。
28、动态代理是基于什么原理
代理一般分为静态代理和动态代理,它们都是代理模莫式的一种应用,静态代理指的是在程序运
行前已经编译好,程序知道由谁来执行代理方法。
而动态代理只有在程序运行期间才能确定,相比于静态代理,动态代理的优势在于可以很方便的
对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。可以说动态代理是基于反射实现
的。通过反射我们可以直接操作类或者对象,比如获取类的定义,获取声明的属性和方法,调用方
法,在运行时可以修改类的定义。
动态代理是一种在运行时构建代理、动态处理方法调用的机制。动态代理的实现方式有很多,
Java 提供的代理被称为 JDK 动态代理,JDK 动态代理是基于类的继承。
29、Exception 和 Error 有什么区别
Exception 泛指的是异常,Exception 主要分为两种异常,一种是编译期出现的异常,称为
checkedException,一种是程序运行期间出现的异常常,称为 uncheckedException,常见的
checkedException 有 IOException,uncheckedExc eption 统称为 RuntimeException ,常见的
RuntimeException 主要有 NullPointerException、IllegalArgumentException 、
ArrayIndex0utofB oundException 等,Exception 可以被捕获。
Error 是指程序运行过程中出现的错误,通常情况下会造成程序的崩溃,Error 通常是不可恢复
的,Error 不能被捕获。
30、反射的基本原理,反射创建类实例的三和方式是什么
反射机制就是使 Java 程序在运行时具有自省(introsp ect) 的能力,通过反射我们可以直接操作
类 和对象,比如获取某个类的定义,获取类的属性和方法 构造方法等。
创建类实例的三种方式是
⚫
对象实例.getClass();
⚫
通过 ClassforName()创建
⚫
对象实例.newInstance)方法创建