前言:这是一个基本涵盖Java初中级大部分核心知识点的面试题集,包含了Java基础、容器、多线程、Spring、SpringBoot、MyBatis、Linux、MySQL、Redis、MongoDB、网络协议、JVM等方向。所有题目都是我亲自整理的。
因为无法生成自动跳转的目录,同时我也按照分类汇总整理成了PDF版,排版上相比更加整齐并且带有书签 阅读起来也比较方便全部。前前后后花费半个月的时间,共计24W字。
收藏的时候,顺便点个赞呗~
收藏的时候,顺便点个赞呗~
收藏的时候,顺便点个赞呗~
由于篇幅限制
操作字符串的类有:String
、StringBuffer
、StringBuilder
。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象。
而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,
所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
1、数据可变和不可变
String
底层使用一个不可变的字符数组private final char value[];
所以它内容不可变。StringBuffer
和StringBuilder
都继承了AbstractStringBuilder
底层使用的是可变字符数组:char[] value;
2、线程安全
StringBuilder
是线程不安全的,效率较高;而StringBuffer
是线程安全的,效率较低。通过他们的append()
方法来看,StringBuffer
是有同步锁,而StringBuilder
没有:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
3、 相同点
StringBuilder
与StringBuffer
有公共父类AbstractStringBuilder
。
最后,操作可变字符串速度:StringBuilder > StringBuffer > String
,这个答案就显得不足为奇了。
不一样,因为内存的分配方式不一样。String str="i"的方式,Java 虚拟机会将其分配到常量池中;而 String str=new String("i") 则会被分到堆内存中。
代码示例:
String x = "叶痕秋";
String y = "叶痕秋";
String z = new String("叶痕秋");
System.out.println(x == y); // true
System.out.println(x == z); // false
String x = "叶痕秋" 的方式,Java 虚拟机会将其分配到常量池中,而常量池中没有重复的元素,比如当执行“叶痕秋”时,java虚拟机会先在常量池中检索是否已经有“叶痕秋”,如果有那么就将“叶痕秋”的地址赋给变量,如果没有就创建一个,然后在赋给变量;
而 String z = new String(“叶痕秋”) 则会被分到堆内存中,即使内容一样还是会创建新的对象。
两个或一个都有可能,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少遍,都是缓冲区中的那一个。NewString每写一遍,就创建一个新的对象,它使用常量”xyz”对象的内容来创建出一个新String对象。如果以前就用过’xyz’,那么这里就不会创建”xyz”了,直接从缓冲区拿,这时创建了一个StringObject;但如果以前没有用过"xyz",那么此时就会创建一个对象并放入缓冲区,这种情况它创建两个对象。至于String类是否继承,答案是否定的,因为String默认final修饰,是不可继承的。
对于如下代码:
String s1 = "a";
String s2 = s1 + "b";
String s3 = "a" + "b";
System.out.println(s2 == "ab");
System.out.println(s3 == "ab");
第一条语句打印的结果为false,第二条语句打印的结果为true,这说明javac编译可以对字符串常量直接相加的表达式进行优化,不必要等到运行期再去进行加法运算处理,而是在编译时去掉其中的加号,直接将其编译成一个这些常量相连的结果。
题目中的第一行代码被编译器在编译时优化后,相当于直接定义了一个”abcd”的字符串,所以,上面的代码应该只创建了一个String对象。写如下两行代码,
String s ="a" + "b" +"c" + "d";
System.out.println(s== "abcd");
最终打印的结果应该为true。
首先,List与Set具有相似性,它们都是单列元素的集合,所以,它们有一个共同的父接口,叫Collection。
1、Set里面不允许有重复的元素
即不能有两个相等(注意,不是仅仅是相同)的对象,即假设Set集合中有了一个A对象,现在我要向Set集合再存入一个B对象,但B对象与A对象equals相等,则B对象存储不进去,**所以,Set集合的add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true,当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。**Set取元素时,不能细说要取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。
2、List表示有先后顺序的集合
注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用add(Obje)方法时,每次加入的对象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用add(intindex,Obj e)方法,就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List中,每调用一次add方法,这个对象就被插入进集合中一次,其实,并不是把这个对象本身存储进了集合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个索引指向了这个对象。List除了可以用Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以调用get(index i)来明确说明取第几个。
3、Map与List和Set不同
它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。取则可以根据key获得相应的value,即get(Object key)返回值为key所对应的value。另外,也可以获得所有的key的结合,还可以获得所有的value的结合,还可以获得key和value组合成的Map.Entry对象的集合。
总结
List以特定次序来持有元素,可有重复元素。Set无法拥有重复元素,内部排序。Map保存key-value值,value可多值。
Set里的元素是不能重复的,元素重复与否是使用equals()方法进行判断的。
==和equal区别也是考烂了的题,这里再重复说一下:
==操作符专门用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,要比较两个基本类型的数据或两个引用变量是否相等,只能用==操作符。
equals方法是用于比较两个独立对象的内容是否相同,就好比去比较两个人的长相是否相同,它比较的两个对象是独立的。
比如:两条new语句创建了两个对象,然后用a/b这两个变量分别指向了其中一个对象,这是两个不同的对象,它们的首地址是不同的,即a和b中存储的数值是不相同的,所以,表达式a==b将返回false,而这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。
两个类都实现了List接口(List接口继承了Collection
接口),他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,并且其中的数据是允许重复的,这是与HashSet
之类的集合的最大不同处,HashSet
之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。
ArrayList与Vector的区别主要包括两个方面:.
同步性:
Vector
是线程安全的,也就是说是它的方法之间是线程同步的,而ArrayList
是线程序不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用ArrayList
,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用Vector
,因为不需要我们自己再去考虑和编写线程安全的代码。
数据增长:
ArrayList
与Vector
都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加ArrayList
与Vector
的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。Vector
默认增长为原来两倍,而ArrayList
的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的1.5倍)。ArrayList
与Vector
都可以设置初始的空间大小,Vector
还可以设置增长的空间大小,而ArrayList
没有提供设置增长空间的方法。
总结:即Vector增长原来的一倍,ArrayList
增加原来的0.5倍。
ArrayList
和Vector
都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,
Vector
由于使用了synchronized
方法(线程安全),通常性能上较ArrayList
差。而LinkedList
使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,索引就变慢了,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。
LinkedList
也是线程不安全的,LinkedList
提供了一些方法,使得LinkedList
可以被当作堆栈和队列来使用。
HashMap
是Hashtable
的轻量级实现(非线程安全的实现),他们都完成了Map接口,
主要区别在于HashMap
允许空(null)键值(key),由于非线程安全,在只有一个线程访问的情况下,效率要高于Hashtable
。
HashMap
允许将null作为一个entry的key或者value,而Hashtable
不允许。
HashMap
把Hashtable
的contains
方法去掉了,改成containsvalue
和containsKey
。因为contains方法容易让人引起误解。
Hashtable
继承自Dictionary
类,而HashMap
是Java1.2引进的Map interface的一个实现。
最大的不同是,Hashtable
的方法是Synchronize
的,而HashMap
不是,在多个线程访问Hashtable
时,不需要自己为它的方法实现同步,而HashMap
就必须为之提供同步。
就HashMap
与HashTable
主要从三方面来说。
Hashtable
是基于陈旧的Dictionary
类的,HashMap
是Java 1.2引进的Map接口的一个实现Hashtable
是线程安全的,也就是说是同步的,而HashMap
是线程序不安全的,不是同步的HashMap
可以让你将空值作为一个表的条目的key或value同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合ConcurrentHashMap
,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。
不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全上。
同步HashMap
, Hashtable
, HashSet
, Vector
, ArrayList
相比他们并发的实现(ConcurrentHashMap
, CopyOnWriteArrayList
, CopyOnWriteHashSet
)会慢得多。造成如此慢的主要原因是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过使用先进的和成熟的技术像锁剥离。
比如ConcurrentHashMap
会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。
同样的,CopyOnWriteArrayList
允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。
如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。
关于集合的体系是每个人都应该烂熟于心的,尤其是对我们经常使用的List,Map的原理更该如此.这里我们看这张图即可:
poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
PriorityQueue 是一个优先级队列,保证最高或者最低优先级的的元素总是在队列头部,但是 LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。
WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有任何引用时,key/value 将会被回收。
最明显的区别是 ArrrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双向循环链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。
Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
ArrayList 是Java集合框架类的一员,可以称它为一个动态数组. array 是静态的,所以一个数据一旦创建就无法更改他的大小
在 java 7 中,ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)。这就是 Java 7 中 ArrayList 和 HashMap 类的代码片段
private static final int DEFAULT_CAPACITY = 10;
//from HashMap.java JDK 7
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
相同点
都是用于比较两个对象“顺序”的接口
都可以使用Collections.sort()方法来对对象集合进行排序
不同点
Comparable位于java.lang包下,而Comparator则位于java.util包下
Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序
总结
使用Comparable接口来实现对象之间的比较时,可以使这个类型(设为A)实现Comparable接口,并可以使用Collections.sort()方法来对A类型的List进行排序,之后可以通过a1.comparaTo(a2)来比较两个对象;
当使用Comparator接口来实现对象之间的比较时,只需要创建一个实现Comparator接口的比较器(设为AComparator),并将其传给Collections.sort()方法即可对A类型的List进行排序,之后也可以通过调用比较器AComparator.compare(a1, a2)来比较两个对象。
可以说一个是自己完成比较,一个是外部程序实现比较的差别而已。
用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。
比如:你想对整数采用绝对值大小来排序,Integer 是不符合要求的,你不需要去修改 Integer 类(实际上你也不能这么做)去改变它的排序行为,这时候只要(也只有)使用一个实现了 Comparator 接口的对象来实现控制它的排序就行了。
两种方式,各有各的特点:使用Comparable方式比较时,我们将比较的规则写入了比较的类型中,其特点是高内聚。但如果哪天这个规则需要修改,那么我们必须修改这个类型的源代码。如果使用Comparator方式比较,那么我们不需要修改比较的类,其特点是易维护,但需要自定义一个比较器,后续比较规则的修改,仅仅是改这个比较器中的代码即可。
你可以使用有序集合,如 TreeSet 或 TreeMap,你也可以使用有顺序的的集合,如 list,然后通过 Collections.sort() 来排序。
你可以使用 Arrays.toString() 和 Arrays.deepToString() 方法来打印数组。由于数组没有实现 toString() 方法,所以如果将数组传递给 System.out.println() 方法,将无法打印出数组的内容,但是 Arrays.toString() 可以打印每个元素。
双向循环列表,具体实现自行查阅源码.
TreeMap是一个通过红黑树实现有序的key-value集合。
TreeMap继承AbstractMap,也即实现了Map,它是一个Map集合
TreeMap实现了NavigableMap接口,它支持一系列的导航方法,
TreeMap实现了Cloneable接口,它可以被克隆
TreeMap本质是Red-Black Tree,它包含几个重要的成员变量:root、size、comparator。其中root是红黑树的根节点。它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color。Entry节点根据根据Key排序,包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。
错误写法示例一:
public static void remove(ArrayList list) {
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
if (s.equals("bb")) {
list.remove(s);
}
}
}
错误写法示例二:
public static void remove(ArrayList list) {
for (String s : list) {
if (s.equals("bb")) {
list.remove(s);
}
}
}
要分析产生上述错误现象的原因唯有翻一翻jdk的ArrayList源码,先看下ArrayList中的remove方法(注意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的remove方法)是怎么实现的:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
按一般执行路径会走到else路径下最终调用faseRemove方法:
private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // Let gc do its work }
可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动。针对错误写法一,在遍历第二个元素字符串bb时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也是字符串bb)至当前位置,导致下一次循环遍历时后一个字符串bb并没有遍历到,所以无法删除。 针对这种情况可以倒序删除的方式来避免:
public static void remove(ArrayList list) { for (int i = list.size() - 1; i >= 0; i--) { String s = list.get(i); if (s.equals("bb")) { list.remove(s); } } }
因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。
而错误二产生的原因却是foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中):
public Iterator iterator() { return new Itr(); }
这里返回的是AbstractList类内部的迭代器实现private class Itr implements Iterator,看这个类的next方法:
public E next() { checkForComodification(); try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } }
第一行checkForComodification方法:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法把修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或foreach的隐式)不要使用ArrayList的remove,改为用Iterator的remove即可。
public static void remove(ArrayList list) { Iterator it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("bb")) { it.remove(); } } }
HashMap是基于哈希表实现的map,哈希表(也叫关联数组)一种通用的数据结构,是Java开发者常用的类,常用来存储和获取数据,功能强大使用起来也很方便,是居家旅行...不对,是Java开发需要掌握的基本技能,也是面试必考的知识点,所以,了解HashMap是很有必要的。
原理
简单讲解下HashMap的原理:HashMap基于Hash算法,我们通过put(key,value)存储,get(key)来获取。当传入key时,HashMap会根据key.hashCode()计算出hash值,根据hash值将value保存在bucket里。当计算出的hash值相同时怎么办呢,我们称之为Hash冲突,HashMap的做法是用链表和红黑树存储相同hash值的value。当Hash冲突的个数比较少时,使用链表,否则使用红黑树。
内部存储结构
HashMap类实现了Map< K, V>接口,主要包含以下几个方法:
HashMap使用了一个内部类Node< K, V>来存储数据
“ 我阅读的是Java 8的源码,在Java 8之前存储数据的内部类是Entry< K, V>,代码大体都是一样的
”
Node代码:
static class Node implements Map.Entry { final int hash; final K key; V value; Node next; ...}
可以看见Node类中除了键值对(key-value)以外,还有额外的两个数据:
因此可以知道,HashMap的结构大致如下:
我们可以将每个横向看成一个个的桶,每个桶中存放着具有相同Hash值的Node,通过一个list来存放每个桶。
内部变量
// 默认容量大小static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 最大容量static final int MAXIMUM_CAPACITY = 1 << 30;// 装载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 转换为二叉树的阀值static final int TREEIFY_THRESHOLD = 8;// 转换为二叉树的最低阀值static final int UNTREEIFY_THRESHOLD = 6;// 二叉树最小容量static final int MIN_TREEIFY_CAPACITY = 64;// 哈希表transient Node[] table;// 键值对的数量transient int size;// 记录HashMap结构改变次数,与HashMap的快速失败相关transient int modCount;// 扩容的阈值int threshold;// 装载因子final float loadFactor;
常用方法
put操作
put函数大致的思路为:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // resize()是调整table数组大小的,如果table数组为空或长度为0,重新调整大小 if ((p = tab[i = (n - 1) & hash]) == null) // i = (n - 1) & hash | 这里计算出来的i值就是存放数组的位置,如果当前位置为空,则直接放入其中 tab[i] = newNode(hash, key, value, null); else { // hash冲突 Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 如果hash相同,并且key值也相同,则找到存放位置 e = p; else if (p instanceof TreeNode) // 如果当前p是二叉树,则放入二叉树中 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { // 存放到链表中 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 遍历链表并将值放到链表最后 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 如果链表中的值大于TREEIFY_THRESHOLD - 1,则将链表转换成二叉树 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // 表示对于当前key早已经存在 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) // 如果onlyIfAbsent为false或则oldValue为空,替换原来的值 e.value = value; afterNodeAccess(e); return oldValue; // 返回原来的值 } } ++modCount; // HashMap结构修改次数,主要用于判断迭代器中fail-fast if (++size > threshold) // 如果++size后的值比阀值大,则重新调整大小 resize(); afterNodeInsertion(evict); return null;}
代码也比较容易看懂,值得注意的就是
else if (p instanceof TreeNode) // 如果当前p是二叉树,则放入二叉树中 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
与
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 如果链表中的值大于TREEIFY_THRESHOLD - 1,则将链表转换成二叉树
这是Java 8相对于以前版本一个比较大的改变。
在Java 8以前,每次产生hash冲突,就将记录追加到链表后面,然后通过遍历链表来查找。如果某个链表中记录过大,每次遍历的数据就越多,效率也就很低,复杂度为O(n);
在Java 8中,加入了一个常量TREEIFY_THRESHOLD=8,如果某个链表中的记录大于这个常量的话,HashMap会动态的使用一个专门的treemap实现来替换掉它。这样复杂度是O(logn),比链表的O(n)会好很多。
对于前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。
get操作
在理解了put之后,get就很简单了。大致思路如下:
public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) // 如果hash相同并且key值一样则返回当前node return first; if ((e = first.next) != null) { if (first instanceof TreeNode) // 如果当前node为二叉树,则在二叉树中查找 return ((TreeNode)first).getTreeNode(hash, key); do { // 遍历链表 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}
如果在初始化HashMap中没有指定初始容量,那么默认容量为16,但是如果后来HashMap中存放的数量超过了16,那么便会有大量的hash冲突;在HashMap中有自动扩容机制,如果当前存放的数量大于某个界限,HashMap便会调用resize()方法,扩大HashMap的容量。
当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。
HashMap的capacity必须满足是2的N次方,如果在构造函数内指定的容量n不满足,HashMap会通过下面的算法将其转换为大于n的最小的2的N次方数。
// 减1→移位→按位或运算→加1返回static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}
HashMap是非线程安全的,如果在多线程环境下,可以使用HashTable,HashTable中所有CRUD操作都是线程同步的,同样的,线程同步的代价就是效率变低了。
再Java 5以后,有了一个线程安全的HashMap——ConcurrentHashMap,ConcurrentHashMap相对于HashTable来说,ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,并发性的提升是显而易见。
快速失败(fast-fail)
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
在HashMap的forEach方法中有以下代码:
@Overridepublic void forEach(BiConsumer super K, ? super V> action) { Node[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node e = tab[i]; e != null; e = e.next) action.accept(e.key, e.value); } if (modCount != mc) throw new ConcurrentModificationException(); }}
在上面我们说到,modCount是记录每次HashMap结构修改。 forEach方法会在在进入for循环之前,将modCount赋值给mc,如果在for循环之后,HashMap的结构变化了,那么导致的结果就是modCount != mc,则抛出ConcurrentModificationException()异常。
1、什么时候会使用HashMap?他有什么特点? 是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。
2、你知道HashMap的工作原理吗? 通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
3、你知道get和put的原理吗?equals()和hashCode()的都有什么作用? 通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点
4、你知道hash的实现吗?为什么要这样实现? 在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。
5、如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办? 如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。
“ 前段时间因为找工作的缘故背了一些关于HashMap的面试题,死记硬背,也不是很懂,最近看了源码,很多知识才变的清晰,而且看源码挺有趣的。再接再厉。
”
每种编程语言中都有集合。集合框架的部分优点如下:
**1、**使用核心集合类降低开发成本,而非实现我们自己的集合类。
**2、**随着使用经过严格测试的集合框架类,代码质量会得到提高。
**3、**通过使用JDK附带的集合类,可以降低代码维护成本。
**4、**复用性和可操作性。
Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。
泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。
Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。
Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。
List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。
Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。
一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。
克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。
尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此,Map继承Collection毫无意义,反之亦然。
如果Map继承Collection接口,那么元素去哪儿?Map包含key-value对,它提供抽取key或value列表集合的方法,但是它不适合“一组对象”规范。
Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。
下面列出了他们的区别: Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。 Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。
语义不明,已知的是,Iterator的协议不能确保迭代的次序。然而要注意,ListIterator没有提供一个add操作,它要确保迭代的顺序。
它可以在当前Iterator的顶层实现,但是它用得很少,如果将它加到接口中,每个继承都要去实现它,这没有意义。
1、我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
2、Iterator只可以向前遍历,而LIstIterator可以双向遍历。
3、ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
List strList = new ArrayList<>();//使用for-each循环for(String obj : strList){ System.out.println(obj);}//using iteratorIterator it = strList.iterator();while(it.hasNext()){ String obj = it.next(); System.out.println(obj);}
使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出ConcurrentModificationException。
每次我们尝试获取下一个元素的时候,Iterator fail-fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException。Collection中所有Iterator的实现都是按fail-fast来设计的(ConcurrentHashMap和CopyOnWriteArrayList这类并发集合类除外)。
Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。
在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList。
Iterator接口定义了遍历集合的方法,但它的实现则是集合实现类的责任。每个能够返回用于遍历的Iterator的集合类都有它自己的Iterator实现内部类。
这就允许集合类去选择迭代器是fail-fast还是fail-safe的。比如,ArrayList迭代器是fail-fast的,而CopyOnWriteArrayList迭代器是fail-safe的。
UnsupportedOperationException是用于表明操作不支持的异常。在JDK类中已被大量运用,在集合框架java.util.Collections.UnmodifiableCollection将会在所有add和remove操作中抛出这个异常。
HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值。下面的图片解释了详细内容。
其它关于HashMap比较重要的问题是容量、负荷系数和阀值调整。HashMap默认的初始容量是32,负荷系数是0.75。阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值大的时候,HashMap会对map的内容进行重新哈希,且使用更大的容量。容量总是2的幂,所以如果你知道你需要存储大量的key-value对,比如缓存从数据库里面拉取的数据,使用正确的容量和负荷系数对HashMap进行初始化是个不错的做法。
HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则:
(1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。
(2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。
我们可以使用任何类作为Map的key,然而在使用它们之前,需要考虑以下几点:
(1)如果类重写了equals()方法,它也应该重写hashCode()方法。
(2)类的所有实例需要遵循与equals()和hashCode()相关的规则。请参考之前提到的这些规则。
(3)如果一个类没有使用equals(),你不应该在hashCode()中使用它。
(4)用户自定义key类的最佳实践是使之为不可变的,这样,hashCode()值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashCode()和equals()在未来不会改变,这样就会解决与可变相关的问题了。
比如,我有一个类MyKey,在HashMap中使用它。
//传递给MyKey的name参数被用于equals()和hashCode()中 MyKey key = new MyKey('Pankaj'); //assume hashCode=1234 myHashMap.put(key, 'Value'); // 以下的代码会改变key的hashCode()和equals()值 key.setName('Amit'); //assume new hashCode=7890 //下面会返回null,因为HashMap会尝试查找存储同样索引的key,而key已被改变了,匹配失败,返回null myHashMap.get(new MyKey('Pankaj')); 那就是为何String和Integer被作为HashMap的key大量使用。
Map接口提供三个集合视图:
**1、Set keyset():**返回map中包含的所有key的一个Set视图。集合是受map支持的,map的变化会在集合中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。
**2、Collection values():**返回一个map中包含的所有value的一个Collection视图。这个collection受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个collection时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。
**3、Set
(1)HashMap允许key和value为null,而HashTable不允许。
(2)HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境,HashTable适合多线程环境。
(3)在Java1.4中引入了LinkedHashMap,HashMap的一个子类,假如你想要遍历顺序,你很容易从HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的。
(4)HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key的Enumeration进行遍历,它不支持fail-fast。
(5)HashTable被认为是个遗留的类,如果你寻求在迭代的时候修改Map,你应该使用CocurrentHashMap。
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
ArrayList和Vector在很多时候都很类似。
**1、**两者都是基于索引的,内部由一个数组支持。
**2、**两者维护插入的顺序,我们可以根据插入顺序来获取元素。
**3、**ArrayList和Vector的迭代器实现都是fail-fast的。
**4、**ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。
以下是ArrayList和Vector的不同点。
**1、**Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。
**2、**ArrayList比Vector快,它因为有同步,不会过载。
**3、**ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。
Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array是指定大小的,而ArrayList大小是固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。尽管ArrayList明显是更好的选择,但也有些时候Array比较好用。
**1、**如果列表的大小已经指定,大部分情况下是存储和遍历它们。
**2、**对于遍历基本数据类型,尽管Collections使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢。
**3、**如果你要使用多维数组,使用[][]比List>更容易。
ArrayList和LinkedList两者都实现了List接口,但是它们之间有些不同。
**1、**ArrayList是由Array所支持的基于一个索引的数据结构,所以它提供对元素的随机访问,复杂度为O(1),但LinkedList存储一系列的节点数据,每个节点都与前一个和下一个节点相连接。所以,尽管有使用索引获取元素的方法,内部实现是从起始点开始遍历,遍历到索引的节点然后返回元素,时间复杂度为O(n),比ArrayList要慢。
**2、**与ArrayList相比,在LinkedList中插入、添加和删除一个元素会更快,因为在一个元素被插入到中间的时候,不会涉及改变数组的大小,或更新索引。
**3、**LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点存储了前后节点的引用。
ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。
java.util.EnumSet是使用枚举类型的集合实现。当集合创建时,枚举集合中的所有元素必须来自单个指定的枚举类型,可以是显示的或隐示的。EnumSet是不同步的,不允许值为null的元素。它也提供了一些有用的方法,比如copyOf(Collection c)、of(E first,E…rest)和complementOf(EnumSet s)。
Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,可以在多线程环境下使用。Java1.5并发API包括一些集合类,允许迭代时修改,因为它们都工作在集合的克隆上,所以它们在多线程环境中是安全的。
Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为fail-fast的,会抛出ConcurrentModificationException。一部分类为:CopyOnWriteArrayList、 ConcurrentHashMap、CopyOnWriteArraySet。
Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。
BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。
Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
栈和队列两者都被用来预存储数据。 java.util.Queue是一个接口,它的实现类在Java并发包中。队列允许先进先出(FIFO)检索元素,但并非总是这样。Deque接口允许从两端检索元素。
栈与队列很相似,但它允许对元素进行后进先出(LIFO)进行检索。
Stack是一个扩展自Vector的类,而Queue是一个接口。
Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。
如果我们想使用Array或Collection的排序方法时,需要在自定义类里实现Java提供Comparable接口。
Comparable接口有compareTo(T OBJ)方法,它被排序方法所使用。我们应该重写这个方法,如果“this”对象比传递的对象参数更小、相等或更大时,它返回一个负整数、0或正整数。
但是,在大多数实际情况下,我们想根据不同参数进行排序。
比如,作为一个CEO,我想对雇员基于薪资进行排序,一个HR想基于年龄对他们进行排序。这就是我们需要使用Comparator接口的情景,因为Comparable.compareTo(Object o)方法实现只能基于一个字段进行排序,我们不能根据对象排序的需要选择字段。
Comparator接口的compare(Object o1, Object o2)方法的实现需要传递两个对象参数,若第一个参数比第二个小,返回负整数;若第一个等于第二个,返回0;若第一个比第二个大,返回正整数。
Comparable和Comparator接口被用来对对象集合或者数组进行排序。
Comparable接口被用来提供对象的自然排序,我们可以使用它来提供基于单个逻辑的排序。
Comparator接口被用来提供不同的排序算法,我们可以选择需要使用的Comparator来对给定的对象集合进行排序。
如果我们需要对一个对象数组进行排序,我们可以使用Arrays.sort()方法。如果我们需要排序一个对象列表,我们可以使用Collection.sort()方法。两个类都有用于自然排序(使用Comparable)或基于标准的排序(使用Comparator)的重载方法sort()。Collections内部使用数组排序方法,所有它们两者都有相同的性能,只是Collections需要花时间将列表转换为数组。
在作为参数传递之前,我们可以使用Collections.unmodifiableCollection(Collection c)方法创建一个只读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。
我们可以使用Collections.synchronizedCollection(Collection c)根据指定集合来获取一个synchronized(线程安全的)集合。
Java集合框架提供常用的算法实现,比如排序和搜索。Collections类包含这些方法实现。大部分算法是操作List的,但一部分对所有类型的集合都是可用的。部分算法有排序、搜索、混编、最大最小值。
大写的O描述的是,就数据结构中的一系列元素而言,一个算法的性能。Collection类就是实际的数据结构,我们通常基于时间、内存和性能,使用大写的O来选择集合实现。
比如: 例子1:ArrayList的get(index i)是一个常量时间操作,它不依赖list中元素的数量。所以它的性能是O(1)。
例子2:一个对于数组或列表的线性搜索的性能是O(n),因为我们需要遍历所有的元素来查找需要的元素。
1、根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用TreeMap。如果我们不想重复,我们应该使用Set。
2、一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。
3、基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。
4、总是使用类型安全的泛型,避免在运行时出现ClassCastException。
5、使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。
6、尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的实现。它将会提供代码重用性,它有着更好的稳定性和可维护性。
TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。