声明:题目大部分来源于Java后端公众号,有些个人整理,但答案皆为个人整理,仅供参考。
GitHub:https://github.com/JDawnF
目录
Java中的集合
List 和 Set 区别
1.Set:集合中的对象不按特定方式排序(针对内存地址来说,即非线性),并且没有重复对象。它的有些实现类能对集合中的对象按特定方式排序。
2.List:集合中的对象线性方式储存,可以有重复对象,允许按照对象在集合中的索引位置检索对象。有序可重复。
Set和hashCode以及equals方法的联系
List 和 Map 区别
1.Map:通过键值对进行取值,key-value一一对应的,其中key不可以重复,而value可以重复
区别:
Arraylist 与 LinkedList 区别
1.Arraylist(线程不安全):
2.LinkedList(线程不安全):
区别:
ArrayList 与 Vector 区别
1.Vector(线程安全):
区别:
HashMap 的工作原理及代码实现,什么时候用到红黑树
1.HashMap(线程不安全,基于jdk1.7):
注意:
2.Hashtable(线程安全):
HashMap 和 Hashtable 的区别:
HashSet 和 HashMap 区别:
1.HashSet(线程不安全):
区别:
ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数
1.ConcurrentHashMap(线程安全):
总结
HashMap 和 ConcurrentHashMap 的区别
多线程情况下HashMap死循环的问题
介绍一下LinkedHashMap
HashMap出现Hash DOS攻击的问题
手写简单的HashMap
看过那些Java集合类的源码
什么是快速失败的故障安全迭代器?
Iterator和ListIterator的区别
什么是CopyOnWriteArrayList,它与ArrayList有何不同?
迭代器和枚举之间的区别
总结:
Java中的集合主要分为value,key-value(Collection,Map)两种,存储值分为List和Set,存储为key-value得失Map。
Collection接口中主要有这些方法:
boolean add(Object o) :向集合中加入一个对象的引用
void clear():删除集合中所有的对象,即不再持有这些对象的引用
boolean isEmpty() :判断集合是否为空
boolean contains(Object o) : 判断集合中是否持有特定对象的引用
Iterartor iterator() :返回一个Iterator对象,可以用来遍历集合中的元素
boolean remove(Object o) :从集合中删除一个对象的引用
int size() :返回集合中元素的数目
Object[] toArray() : 返回一个数组,该数组中包括集合中的所有元素
boolean equals(Object o):判断值是否相等
int hashCode(): 返回当前集合的hash值,可以作为判断地址是否想相等
Collection接口继承 Iterable
List和Set都是继承Collection接口。
可以允许重复的对象,可以插入多个null元素。
是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
因为set接口中是不允许存在重复的对象或者值的,所以需要对存入set中的对象或者值进行判断,而hashCode和equals就是用来对这些对象和值进行判断的。
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
实现了 RandomAccess 接口,所以支持随机访问
private static final int DEFAULT_CAPACITY = 10;
数组的默认大小为 10。
插入数据的时候,会先进行扩容校验,添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1)
,也就是旧容量的 1.5 倍。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
add(index,e)
在指定位置添加的话: public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//复制,向后移动
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容最终调用的代码,也是一个数组复制的过程。由此可见 ArrayList
的主要消耗是数组扩容以及在指定位置添加数据,在日常使用时最好是指定大小,尽量减少扩容。更要减少在指定位置插入数据的操作。
删除元素
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。
由于 ArrayList 是基于动态数组实现的,所以并不是所有的空间都被使用。因此使用了 transient
修饰,可以防止被自动序列化。
transient Object[] elementData;
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
//只序列化了被使用的数据
for (int i=0; i 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i
当对象中自定义了 writeObject 和 readObject 方法时,JVM 会调用这两个自定义方法来实现序列化与反序列化。序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
index
离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间。node()会以O(n/2)的性能去获取一个结点;如果索引值大于链表大小的一半,那么将从尾结点开始遍历。 public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
这样的效率是非常低的,特别是当 index 越接近 size 的中间值时。
LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
ArrayList是可改变大小的数组,而LinkedList是双向链接串列
在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的
ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
这样的话,开销比较大,所以 Vector
是一个同步容器并不是一个并发容器。Hashtable的synchronized是对整张hash表进行锁定即让线程独享整张hash表,在安全同时造成了浪费。当一个线程使用put方法添加元素的时候,其他线程不但不能进行put方法添加,也不能进行get方法获取元素,因为得不到锁。
Hashtable
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
HashMap
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
//以下是Hashtable的方法
public synchronized boolean contains(Object value)
public synchronized boolean containsKey(Object key)
public boolean containsValue(Object value)
//以下是HashMap中的方法,注意,没有contains方法
public boolean containsKey(Object key)
public boolean containsValue(Object value)
HashMap的存储规则:优先使用数组存储, 如果出现Hash冲突, 将在数组的该位置拉伸出链表进行存储(在链表的尾部进行添加), 如果链表的长度大于设定值后, 将链表转为红黑树.
HashTable的存储规则:优先使用数组存储, 存储元素时, 先取出下标上的元素(可能为null), 然后添加到数组元素Entry对象的next属性中(在链表的头部进行添加).出现Hash冲突时, 新元素next属性会指向冲突的元素. 如果没有Hash冲突, 则新元素的next属性就是null。
Entry e = (Entry) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
参照:https://blog.csdn.net/wangxing233/article/details/79452946
private transient HashMap map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
两个变量:
map
:用于存放最终数据的。PRESENT
:是所有写入 map 的 value
值。构造函数:利用了 HashMap
初始化了 map
public HashSet() {
map = new HashMap<>();
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
add方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
Hashtable将存放的对象当做了 HashMap
的健,value
都是相同的 PRESENT
。由于 HashMap
的 key
是不能重复的,所以每当有重复的值写入到 HashSet
时,value
会被覆盖,但 key
不会受到影响,这样就保证了 HashSet
中只能存放不重复的元素。
HashSet
的原理比较简单,几乎全部借助于 HashMap
来实现的。所以 HashMap
会出现的问题 HashSet
依然不能避免。
HashMap实现了Map接口,而Hashtable实现Set接口;
总量*负载因子
发生扩容时会出现环形链表从而导致死循环。resize()
方法里的 rehash()
时,容易出现环形链表。这样当获取一个不存在的 key
时,计算出的 index
正好是环形链表的下标时就会出现死循环。https://www.cnblogs.com/dongguacai/p/5599100.html
https://blog.csdn.net/linsongbin1/article/details/54708694
https://blog.csdn.net/striveb/article/details/84660709
快速失败的Java迭代器可能会引发ConcurrentModifcationException在底层集合迭代过程中被修改。故障安全作为发生在实例中的一个副本迭代是不会抛出任何异常的。快速失败的故障安全范例定义了当遭遇故障时系统是如何反应的。例如,用于失败的快速迭代器ArrayList和用于故障安全的迭代器ConcurrentHashMap。
●ListIterator有add()方法,可以向List中添加对象,而Iterator不能。
●ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
●ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
●都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改
https://blog.csdn.net/striveb/article/details/86744846
如果面试官问这个问题,那么他的意图一定是让你区分Iterator不同于Enumeration的两个方面:
●Iterator允许移除从底层集合的元素。
●Iterator的方法名是标准化的。
https://blog.csdn.net/helongzhong/article/details/52869981
1. 如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
2. 如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
3. 在除需要排序时使用TreeSet,TreeMap外,都应使用HashSet,HashMap,因为他们 的效率更高。
4. 要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
5. 容器类仅能持有对象引用(指向对象的指针),而不是将对象信息copy一份至数列某位置。一旦将对象置入容器内,便损失了该对象的型别信息。
6. 尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。