集合分类
- Collection接口:单列数据,定义了存取一组对象的方法的集合
- List:元素有序、可重复的集合
- Set:元素无序、不可重复的集合
-
Map接口:双列数据,保存具有映射关系“key-value对”的集合 List接口 数组具有局限性,通常用lList代替接口 List类中元素有序、可重复,每个元素都有对应的索引 List接口的实现类常用的有:ArrayList、LinkedList和Vector。 |—ArrayList:List接口的主要实现类,使用transient Object[ ] elementData存储,频繁添加、查找推荐使用 |—LinkedList:链式存储,使用双向链表实现,频繁的插入、删除效率比ArrayList高 静态内部类: private static class Node { E item; Node next; Node prev; Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
} 删除:将待删除元素的前置节点给它后面节点的前置节点,将它后置节点给它的前面元素的后置节点。插入类似。 |—Vector:比List接口出现早(1.0),效率低。 ArrayList源码分析 JDK7: ArrayList arrayList=new ArrayList();创建了一个长度为10的数组; arraylist.add()扩容:容量不够时,默认扩容为原来容量的1.5倍,并将原有数组中。 不推荐使用空参构造器。 JDK8: ArrayList arrayList=new ArrayList();
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
只有当调用add方法后才会创建长度为10的数组,并将数据add进去。类似于懒汉式,创建时间推迟,节省内存。 add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
LinkedList底层源码分析
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
add方法
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
Set接口、 |----------HashSet:线程不安全;可以存储null值
- LinkHashSet:HashSet子类,底层使用map
|----------TreeSet:数据同类型,红黑树存储 Set特点 无序:存储的数据的存放顺序不是按照索引顺序添加,根据数据的hash值计算确定。 不可重复: Set底层使用数组,jdk7中初始化为16,jdk8中在调用add才会初始化 为了保证数据不重复,便需要进行数据比较,如果一个一个比较,当数据很多时,效率就会慢。 于是就有了hashcode,添加数据是首先会根据添加数据的hashcode值用算法求得该数据的索引,如果索引位置没有数据直接存入数组。当添加的数据位置上已经有元素时,会比较hashcode值。如果hashcode相同则用equals判断是否相同,如果相同那么两个数据就是相同的。 若hashcode不同则使用链表将数据添加进去,这时同一个索引就存了多个数据。jdk7,8的插入方式不同。 jdk7头插法:当前插入的数据放在数组中当头结点,之前的数据放在它后面。 jdk8尾插法:添加的数据都往后放作为尾结点。 这种添加方式在数据较多时优势明显,它首先会通过hash值计算存储位置,如果位置上有元素,就进入链表比较再添加,时间复杂度大大降低。 因此向set中添加数据一定要重写equals()与hashcode() 因为底层使用map, Set的扩容放在下面一起讨论 TreeSet 两种排序方式 1.自然排序
public void testSet(){
TreeSet treeSet1=new TreeSet();
treeSet1.add("lis");
treeSet1.add("ons");
treeSet1.add("las");
System.out.println(treeSet1);
System.out.println("----------------");
TreeSet treeSet2=new TreeSet();
treeSet2.add(new User("lisi", 18));
treeSet2.add(new User("suli", 20));
treeSet2.add(new User("liufang", 19));
treeSet2.add(new User("mumian", 17));
treeSet2.add(new User("mumian", 18));
System.out.println(treeSet2);
}
结果:
[las, lis, ons]
----------------
[User{usrname='suli', age=20}, User{usrname='mumian', age=18},
User{usrname='mumian', age=17}, User{usrname='liufang', age=19},
User{usrname='lisi', age=18}]
定制排序
public void testCo(){
Comparator comparator= (o1, o2) -> {
if(o1 instanceof User &&o2 instanceof User){
User user1= (User) o1;
User user2= (User) o2;
return user1.getUsrname().compareTo(user2.getUsrname());
}else
throw new RuntimeException("输入数据类型不匹配");
};
TreeSet treeSet2=new TreeSet(comparator);
treeSet2.add(new User("lisi", 18));
treeSet2.add(new User("suli", 20));
treeSet2.add(new User("liufang", 19));
treeSet2.add(new User("mumian", 17));
treeSet2.add(new User("mumian", 18));
System.out.println(treeSet2);
}
结果:
[User{usrname='lisi', age=18}, User{usrname='liufang', age=19},
User{usrname='mumian', age=17}, User{usrname='suli', age=20}]
Map接口(1.2)
- HashMap(1.2):线程不安全,效率高,key-value可以存null
- LinkedHashMap(1.4):频繁遍历操作适合
- TreeMap:排序,底层使用红黑树
- Hashtable(1.0):线程安全,线程不安全,不能存null
- propeties:处理配置文件
HashMap底层结构: 1.7及之前:数组+链表 1.8之后:数组+链表+红黑树 key-value键值对 key:不可重复,无序,用set存储 value:可重复,无序可重复Collection存储。 一个key-value封装成一个Entry对象,不可重复,无序用set存储。 HashMap底层实现原理 jdk1.7: HashMap hashMap=new HashMap(); 实例化后创建长度为16的一维数组Entry[] table map.put(key,value) 调用key所在类的hashcode()计算key的hash值,经过算法处理得到其在Entry数组中的位置。
如果位置上没有数据直接插入;
如果位置上有数据比较哈希值,如果哈希值不同直接添加
如果位置上哈希值相同,调用equals()方法比较:
如果不同false添加成功
如果返回true 就替换掉key对应的value
扩容时默认为原来的两倍,扩容条件:长度大于等于临界值且当前存放位置不为空。 jdk1.8: HashMap hashMap=new HashMap();不进行初始化容量,懒加载 存放的数组是Node[ ],而不是Entry[ ]本质没有区别 当数组中某个链表形式存储的数据>8且数组长度>64时,使用红黑树存储,否则就扩容
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
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;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
扩容:
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<K,V>[] tab; Node<K,V> p; int n, i;
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 {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
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);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR *
DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
LinkedHashMap
底层使用Entry存储
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;//有这个就能实现遍历,便于频繁遍历操作
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
**加载因子**
① 如果空间利用率高,那么经过的哈希算法计算存储位置的时候,会发现很多存储位置已经有数据了(哈希冲突);
② 如果为了避免发生哈希冲突,增大数组容量,就会导致空间利用率不高。
而加载因子就是表示Hash表中元素的填满程度。
加载因子 = 填入表中的元素个数 / 散列表的长度
加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;
加载因子越小,填满的元素越少,冲突发生的机会减小,但空间浪费了更多了,而且还会提高扩容rehash操作的次数。
冲突的机会越大,说明需要查找的数据还需要通过另一个途径查找,这样查找的成本就越高。因此,必须在“冲突的机会”与“空间利用率”之间,寻找一种平衡与折衷。
**负载因子值的大小,对HashMap有什么影响**
1.负载因子的大小决定了HashMap的数据密度。
2.负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,
造成查询或插入时的比较次数增多,性能会下降。
3.负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的
几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性
能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建
议初始化预设大一点的空间。
4. 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此
时平均检索长度接近于常数。
|