1、集合和数组-比较
1、数组声明了它容纳的元素的类型,而集合不声明。2、数组是静态的,一个数组实例具有固定的大小,一旦创建了就无法改变容量了。而集合是可以动态扩展容量,可以根据需要动态改变大小,集合提供更多的成员方法,能满足更多的需求。3、数组的存放的类型只能是一种(基本类型/引用类型),集合存放的类型可以不是一种(不加泛型时添加的类型是Object)[基本数据类型需使用其对应的包装类]。4、数组无法判断其中实际存有多少元素,length只告诉了array的容量。5、数组是java语言中内置的数据类型,是线性排列的,执行效率或者类型检查都是最快的。
2、确保一个集合不能被修改?
1、很容易想到用final关键字进行修饰,final修饰词-知识回顾: 1-1、final关键字可以修饰类,方法,成员变量, 1-2、final修饰的类不能被继承,final修饰的方法不能被重写,final修饰的成员变量必须初始化值。 1-3、如果这个成员变量是基本数据类型,表示这个变量的值是不可改变的,如果说这个成员变量是引用类型,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变的。2、前提知识点: 2-1、集合(map,set,list…)都是引用类型,所以我们如果用final修饰的话,集合里面的内容还是可以修改的。3、采用Collections包下的unmodifiableMap方法, 3-1、通过这个方法返回的map,是不可以修改的。他会报 java.lang.UnsupportedOperationException错。 3-2、Collections包也提供了对list和set集合的方法。 3-2-1、ollections.unmodifiableList(List) 3-2-2、ollections.unmodifiableSet(Set)4、代码示例 List<String> list = new ArrayList<>(); list. add("x"); Collection<String> clist = Collections. unmodifiableCollection(list); clist. add("y"); // 运行时此行报错 System. out. println(list. size());
3、迭代器 Iterator
1、迭代器 Iterator -概述 1-1、Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。 1-2、Iterator-特点 1-2-1、只能单向遍历。 1-2-2、可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。 1-3、代码示例 List<String> list = new ArrayList<>(); Iterator<String> it = list. iterator(); while(it. hasNext()){ String obj = it. next(); System. out. println(obj); } ————————————————2、如何边遍历边移除 Collection 中的元素? 2-1、使用 Iterator.remove() 方法 Iterator<Integer> it = list.iterator(); while(it.hasNext()){ *// do something* it.remove(); }3、Iterator 和 ListIterator 有什么区别? 3-1、Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。 3-2、Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。 3-3、ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
3.1、如何边遍历边移除 Collection 中的元素?
2、如何边遍历边移除 Collection 中的元素? 2-1、使用 Iterator.remove() 方法 Iterator<Integer> it = list.iterator(); while(it.hasNext()){ *// do something* it.remove(); }
3.2、Iterator 和 ListIterator 有什么区别?
3、Iterator 和 ListIterator 有什么区别? 3-1、Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。 3-2、Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。 3-2-1、ListIterator遍历可以是逆向的,因为有previous()和hasPrevious()方法 3-3、ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 3-3-1、ListIterator可以定位当前的索引位置,因为有nextIndex()和previousIndex()方法。 3-3-2、ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历。 3-3-3、ListIterator有add()方法,可以向List添加对象,而Iterator却不能。
3.3、comparable 和 comparator的区别?
1、comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序 1-1、源码示例 public interface Comparable<T> { }2、comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 2-1、源码示例 @FunctionalInterface public interface Comparator<T> { }3、需要对一个集合使用自定义排序时,我们就要 3-1、重写compareTo方法或compare方法 3-2、使用两个参数版的Collections.sort().
3.4、
4、如何实现数组和 List 之间的转换?
1、数组转 List:使用 Arrays.asList(array) 进行转换。2、List 转数组:使用 List 自带的 toArray() 方法。【返回值只能是 Object[] 类,强转其他类型可能有问题】
5、为什么 ArrayList 的 elementData 加上 transient 修饰?
private transient Object[] elementData;1、原因分析: 1-1、transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现: private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ int expectedModCount = modCount; s.defaultWriteObject(); s.writeInt(elementData.length); for (int i=0; i<size; i++) s.writeObject(elementData[i]); if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } 1-2、每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。
6、HashSet 的实现原理?
1、HashSet 的实现原理? 1-1、HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT。 1-2、相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。2、HashSet如何检查重复?HashSet是如何保证数据不可重复的? 2-1、向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。HashSet 中的add ()方法会使用HashMap 的put()方法。 2-1-1、HashMap的put方法的返回值为null或者value。如果put成功的话返回null,如果put失败的话,说明put的key已经存在了,就会返回已经存在该key的value值。 private static final Object PRESENT = new Object(); public boolean add(E e) { //PRESENT是一个至始至终都相同的虚值 return map.put(e, PRESENT)==null; } 2-1-2、底层是调用了了HashMap的remove方法,我们知道hashmap的remove方法会返回该key对应的value值。 public boolean remove(Object o) { return map.remove(o)==PRESENT; }3、Object PRESENT的作用: 3-1、HashMap底层的value保存的都是null的话,那么remove成功或者是失败都会返回null,这时候就会出现与add同样无法区分的问题,无法区分remove是成功还是失败了! 3-1-1、源码示例 private static final Object PRESENT = new Object(); public boolean add(E e) { //PRESENT是一个至始至终都相同的虚值 return map.put(e, PRESENT)==null; } public boolean remove(Object o) { return map.remove(o)==PRESENT; }
6.1、HashSet如何检查并保证数据不可重复?
2、HashSet如何检查重复?HashSet是如何保证数据不可重复的? 2-1、向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。HashSet 中的add ()方法会使用HashMap 的put()方法。 2-1-1、HashMap的put方法的返回值为null或者value。如果put成功的话返回null,如果put失败的话,说明put的key已经存在了,就会返回已经存在该key的value值。 private static final Object PRESENT = new Object(); public boolean add(E e) { //PRESENT是一个至始至终都相同的虚值 return map.put(e, PRESENT)==null; } 2-1-2、底层是调用了了HashMap的remove方法,我们知道hashmap的remove方法会返回该key对应的value值。 public boolean remove(Object o) { return map.remove(o)==PRESENT; }
6.2、HashSet中 Object PRESENT的作用
3、Object PRESENT的作用: 3-1、HashMap底层的value保存的都是null的话,那么remove成功或者是失败都会返回null,这时候就会出现与add同样无法区分的问题,无法区分remove是成功还是失败了! 3-1-1、源码示例 private static final Object PRESENT = new Object(); public boolean add(E e) { //PRESENT是一个至始至终都相同的虚值 return map.put(e, PRESENT)==null; } public boolean remove(Object o) { return map.remove(o)==PRESENT; }
7、HashMap实现原理?
1、HashMap实现原理-概述 1-1、基于哈希表/散列表的Map接口的非同步[线程不安全]实现。此实现提供所有可选的映射操作,并允许使用null值和null键。HashMap储存的是键值对,HashMap很快。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 1-2、数组[内部实现数组是Node[]数组]+链表/红黑树2、链表转红黑树的优点 2-1、从原来的O(n)到O(logn)3、JDK1.8之前采用的是拉链法。 3-1、拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 3-2、
7.1、Map-jdk1.7与jdk1.8比较
1、JDK1.8主要解决或优化了一下问题: 1-1、resize 扩容优化 1-1-1、当Map中元素总数超过Entry数组的75%,触发扩容操作 1-1-2、jdk1.7中是先扩容后插入新值的,jdk1.8中是先插值再扩容 1-2、引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考 1-3、解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。2、Map-jdk1.7与jdk1.8比较: 2-1、存放数据的规则 2-1-1、jdk1.7、无冲突时,存放数组;冲突时,存放链表 2-1-2、jdk1.8、无冲突时,存放数组;冲突 & 链表长度 < 7:存放单链表;冲突 & 链表长度 >= 7:树化并存放红黑树。【红黑树转换是链表节点大于7且数组长度大于64,小于64进行扩容】 2-2、插入数据方式 2-2-1、头插法(先讲原位置的数据移到后1位,再插入数据到该位置)。 2-2-2、尾插法(直接插入到链表尾部/红黑树)。
7.2、HashMap的put方法的具体流程?
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}//实现Map.put和相关方法/** 1.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; 2.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向6,如果table[i]不为空,转向3; 3.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向4,这里的相同指的是hashCode以及equals; 4.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5; 5.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; 6.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 步骤①:tab为空则创建 // table未初始化或者长度为0,进行扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 步骤②:计算index,并对null做处理 // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已经存在元素 else { Node<K,V> e; K k; // 步骤③:节点key存在,直接覆盖value // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 将第一个元素赋值给e,用e来记录 e = p; // 步骤④:判断该链为红黑树 // hash值不相等,即key不相等;为红黑树结点 // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null else if (p instanceof TreeNode) // 放入树中 e = ((TreeNode<K,V>)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); //判断链表的长度是否达到转化红黑树的临界值,临界值为8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //链表结构转树形结构 treeifyBin(tab, hash); // 跳出循环 break; } // 判断链表中结点的key值与插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循环 break; // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 p = e; } } //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值 if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent为false或者旧值为null if (!onlyIfAbsent || oldValue == null) //用新值替换旧值 e.value = value; // 访问后回调 afterNodeAccess(e); // 返回旧值 return oldValue; } } // 结构性修改 ++modCount; // 步骤⑥:超过最大容量就扩容 // 实际大小大于阈值则扩容 if (++size > threshold) resize(); // 插入后回调 afterNodeInsertion(evict); return null;}
7.2.1、HashMap的put方法-执行流程-示意图
7.3、HashMap是怎么解决哈希冲突的?
1、什么是哈希冲突/碰撞? 1-1、当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。2、哈希冲突/碰撞-解决方式 2-1、链地址法/拉链法的方式可以解决哈希冲突 2-1-1、本质:数组+链表的形式来解决。put执行首先判断table[i]位置,如果为空就直接插入,不为空判断和当前值是否相等,相等就覆盖,如果不相等的话,判断是否是红黑树节点,如果不是,就从table[i]位置开始遍历链表,相等覆盖,不相等插入。 2-1-2、将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下[只是单纯的用hashCode取余来获取对应的bucket]。 2-2、相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动); 2-2-1、源码: static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 2-3、在jdk1.8中,如果链表长度大于8且节点数组长度大于64的时候,就把链表下所有的节点转为红黑树。3、HashMap是使用了哪些方法来有效解决哈希冲突的: 3-1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据; 3-2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均; 3-3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
7.4、HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
1、原因: 1-1、hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;2、解决办法 32-1、HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均; 2-2、在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储; 2-2-1、这样一来是比取余操作更加有效率, 2-2-2、二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length, 2-2-3、三来解决了"哈希值与数组大小范围不匹配"的问题;
7.5、HashMap 的长度为什么是2的幂次方?
1、原因 1-1、为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。 1-2、源码:.putVal()方法中 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //...... } 1-3、采用二进制位操作 &,相对于%能够提高运算效率 1-3-1、取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作 1-3-2、 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;
7.6、为什么说HashMap是线程不安全的?
1、在接近临界点时,若此时两个或者多个线程进行put操作,都会进行resize(扩容)和reHash(在接近临界点时,若此时两个或者多个线程进行put操作,都会进行resize(扩容)和reHash(为key重新计算所在位置),而reHash在并发的情况下可能会形成链表环。为key重新计算所在位置),而reHash在并发的情况下可能会形成链表环。2、在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,3、是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
7.7、HashMap 链表和红黑树互转条件
1、HashMap 链表转红黑树条件 1-1、一个是链表长度大于等于7:是为了作为缓冲,可以有效防止链表和树频繁转换。 static final int TREEIFY_THRESHOLD = 8; if (binCount >= TREEIFY_THRESHOLD - 1){ treeifyBin(tab, hash); } 1-2、一个是数组长度到64,数组长度小于64则进行扩容,否则转红黑树 static final int MIN_TREEIFY_CAPACITY = 64; final void treeifyBin(Node<K,V>[] tab, int hash) { if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { //....... } }2、HashMap 红黑树转链表条件 2-1、当链表中节点数量小于等于UNTREEIFY_THRESHOLD[6]时,红黑树会转成链表。 2-2、源码示例: static final int UNTREEIFY_THRESHOLD = 6; if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); }
8、HashMap|Hashtable|ConcurrentHashMap原理与区别
8.1、ConcurrentHashMap 和 Hashtable 的区别
1、底层数据结构: 1-1、JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现, 1-2、JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。 1-3、Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;2、实现线程安全的方式(重要): 2-1、在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 2-2、到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本; 2-3、Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。3、继承关系不同 3-1、Hashtable继承自Dictionary 类,实现了Map接口 -- public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {} 3-2、ConcurrentHashMap继承AbstractMap,实现了CocurrentMap接口 -- public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {}
8.2、HashMap和ConcurrentHashMap的原理与区别
1、继承关系不同 1-1、HashMap继承自AbstractMap类,实现了Map接口 -- public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {} 1-2、ConcurrentHashMap继承AbstractMap,实现了CocurrentMap接口 -- public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {}2、底层数据结构-细节上-不同 2-1、jdk1.7: 2-1-1、ConcurrentHashMap由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。 -- Segment 数组,存放数据时首先需要定位到具体的 Segment 中。 -- final Segment<K,V>[] segments; -- Segment 是 ConcurrentHashMap 的一个内部类: Segment 继承于 ReentrantLock,包含一个Entry<K,V>结构,其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。 static final class Segment<K,V> extends ReentrantLock implements Serializable { //HashMap 中的 HashEntry 作用一样,真正存放数据的桶 transient volatile HashEntry<K,V>[] table; //...... } 2-2、jdk1.8: 2-2-1、放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点。 -- Node<K,V> transient volatile Node<K,V>[] table; -- CAS static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); } -- Synchronized Node<K,V> f synchronized (f) { //...... } 2-2-2、因为红黑树在旋转过程中,会改变root节点,所以ConcurrentHashMap加了个treeBin对象,其hash值为-2,所以才可以根据数组下标的hash>=0判断其为链表。 -- 源码示例 //treeBin对象:在垃圾箱的顶部使用的树状物 static final class TreeBin<K,V> extends Node<K,V> { //.putVal()方法中 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; // 代表链表长度 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } }
8.3、HashMap和Hashtable的区别
1、HashMap、Hashtable的区别 1-1、继承的父类不同: 1-1-1、HashMap 继承自AbstractMap 类;而HashTable 继承自Dictionary 类 1-2、key和value是否允许null值 1-2-1、HashMap 允许Key/Value 都为null,Hashtable的Key/Value 都不允许为null。 1-3、线程安全性不同 1-3-1、Hashtable 中的方法是Synchronized的,而HashMap中的方法在缺省情况下是非Synchronized的。应该使用 Collections.synchronizedMap 方法"来包装"该映射。 -- Map m = Collections.synchronizedMap(new HashMap(...)); 1-4、是否提供contains方法 1-4-1、HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。 1-4-2、Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。 1-5、遍历方式的内部实现上不同 1-5-1、Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。 -- 代码示例 Enumeration enumkey = hash.keys() ; while(enumkey.hasMoreElements()){ String str=(String)enumkey.nextElement(); System.out.println("--------"+str); System.out.println("========="+hash.get( str)); if("1".equals(hash.get( str))){ hash.remove( str); } } 1-6、内部实现使用的数组初始化和扩容方式不同 1-6-1、HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。 1-6-2、Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。 1-7、计算index下标方法不同 1-7-1、Hashtable计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length 1-7-2、HashMap计算index方法:index = hash & (tab.length – 1)
9、Collections.sort和Arrays.sort的实现原理
1、Collections.sort是对list进行排序,Arrays.sort是对数组进行排序。2、Collections.sort-底层实现 2-1、Collections.sort方法调用了list.sort方法 2-1-1、源码示例 public static <T> void sort(List<T> list, Comparator<? super T> c) { list.sort(c); } @SuppressWarnings({"unchecked", "rawtypes"}) default void sort(Comparator<? super E> c) { Object[] a = this.toArray(); Arrays.sort(a, (Comparator) c); ListIterator<E> i = this.listIterator(); for (Object e : a) { i.next(); i.set((E) e); } }3、Arrays.sort底层实现:JDK1.7 3-1、legacyMergeSort(a),归并排序, 3-2、ComparableTimSort.sort():即Timsort排序。 3-2-1、结合了合并排序(merge.sort)和插入排序(insertion sort)而得出的排序方法。
10、哪些集合类是线程安全的?哪些不安全?
1、线程安全类 Vector:比Arraylist多了个同步化机制。Vector是扩展1倍 Hashtable:比Hashmap多了个线程安全。 ConcurrentHashMap:是一种高效但是线程安全的集合。 Stack:栈,也是线程安全的,继承于Vector。2、非线程安全类 Hashmap Arraylist:在底层数组不够用时在原来的基础上扩展0.5倍,即 扩容至原来的1.5倍 LinkedList:底层数据结构是双向链表,具体看源码 HashSet TreeSet TreeMap
11、快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
1、快速失败 1-1、在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。2、安全失败 2-1、采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。 2-1-1、在java.util.concurrent 并发包的集合,如 ConcurrentHashMap, CopyOnWriteArrayList等,默认为都是安全失败的。
12、ArrayList 的扩容机制
1、在底层数组不够用时在原来的基础上扩展0.5倍,即 扩容至原来的1.5倍。2、ArrayList 的扩容机制本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。3、ArrayList 的默认大小是 10 个元素 private static final int DEFAULT_CAPACITY = 10;4、源码 public boolean add(E e) { //扩容 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { //如果传入的是个空数组则最小容量取默认容量与minCapacity之间的最大值 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++; // 如果最小需要空间比elementData的内存空间要大,则需要扩容 // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // 获取elementData数组的内存空间长度 int oldCapacity = elementData.length; // 扩容至原来的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); //校验容量是否够 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //若预设值大于默认的最大值,检查是否溢出 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 调用Arrays.copyOf方法将elementData数组指向新的内存空间 //并将elementData的数据复制到新的内存空间 elementData = Arrays.copyOf(elementData, newCapacity); }
13、ArrayList与LinkedList的区别
1、ArrayList与LinkedList的继承关系 1-1、ArrayList的继承关系 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {} 1-2、LinkedList的继承关系 public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {}2、ArrayList与LinkedList的区别 2-1、ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 2-2、ArrayList随机访问集合元素上有较好的性能(RandomAccess接口),LinkedList插入、删除数据效率高。 2-3、存储:LinkedList比ArrayList更占内存,因为LinkedList每一个节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向下一个元素。 2-4、查找:ArrayList随机访问元素的时间复杂度为o(1);LinkedList查找某个元素的时间复杂度是o(n)。 2-5、都具有:有序,允许存null值,允许重复值可以; list存储的类型是object,null属于object类型。