一 容器概述
在Java内存层面上,对多个对象进行统一管理和操作的的容器有:集合,数组,哈希结构
1)数组在存储数据方面的特点
- 数组一旦初始化,它的长度就是确定的
- 数组有索引,可以很方便的对指定位置上的元素进行查找和替换
- 数组在存储数据的时候是有序的,且使用连续的内存空间
- 数组在定义的时候,就明确了存储的数据的类型
2)数组在存储数据方面的弊端
- 数组一旦初始化,它的长度就不能改变,如果需要扩容,则必须新建一个数组
- 数组在插入,删除操作的效率较差
- 可以表达的多个对象之间的关系较为简单,不能表示丰富的对象关系
- 数组中可使用的方法非常少
1.1 数组和集合之间的转换
1)将集合转为数组:toArray() ```java Collection c1 = new ArrayList(); c1.add(123);//自动装箱 c1.add(new Person(“Tom”,12)); c1.add(new String(“AA”)); c1.add(new Date(234234324L)); c1.add(“BB”);
Object[] arr = c1.toArray(); for(int i = 0;i < arr.length;i++){ System.out.println(arr[i]); }
2)将数组转化为集合:**Arrays.asList(T...t)**
```java
String[] arr1 = {"AA","CC","MM","GG"};
List list = Arrays.asList(arr1);
System.out.println(list);
List list1 = Arrays.asList("AA","CC","MM");
1.2 Java集合的两种体系
1.2.1 Collection
- 单列数据,定义了存储一组对象的方法的集合
1)List:元素有序,可重复的集合
2)Set:元素无序,不可重复的集合
1.2.2 Map
二 Collection接口
2.1 Collection概述
1)Collection接口是List,Set和Queue 接口的父接口。该接口中定义的方法可以用于其各种子接口的实现类中。
2)JDK不提供Collection接口的实现类,而是提供更具体的子接口来实现。
3)在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。
2.1.1 Collection接口定义的方法
add(Object obj) // 添加元素obj到当前集合中
size() // 获取集合中元素的个数
addAll(Collection coll) // 将coll集合中的元素添加到当前集合中。
clear() // 清空当前集合
isEmpty() // 判断当前集合是否为空。
contains(Object obj) // 判断当前集合中是否包含obj元素,具体的:需要调用obj对象所在类的equals(Object o)
containsAll(Collection coll) // 判断当前集合中是否包含coll集合中的所有元素
remove(Object obj) // 删除当前集合中首次出现的obj元素
removeAll(Collection coll) //差集:从当前集合中移除其与coll集合共有的元素
retainAll(Collection coll) // 交集:获取当期集合与coll集合共有
equals(Object obj) // 判断当前集合与obj元素是否相等,要想返回true,要求形参obj也是一个同类型的集合对象,同时集合元素都相同。(如果是List的话,要求顺序也相同)
hashCode() // 返回当前集合对象的哈希值
2.2.2 迭代器接口:Iterator
1)Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
2)提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
3)Collection继承了java.lang.Iterable 接口,该接口有一个 iterator() 方法,所以所有地 Collection 子接口实现类集合都有一个 iterator() 方法,用于返回一个实现了 Iterator接口的对象。
4)Iterator仅用于遍历集合,本身不具有承载对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。
5)集合每一次调用 iterator() 方法都会得到一个全新的迭代器对象,默认游标在集合的第一个元素前。
Iterator接口的方法:
hasNext() // 判断是否存在下一个元素
E next() // 返回集合的一个元素,并将游标移向下一位
remove() // 删除集合的一个元素(使用方式还不清晰)
2.2 List子接口
List概述:
①鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
②List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
③List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
④JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。
2.2.1 List接口新增的方法
void add(int index, Object ele) // 在index位置插入ele元素
boolean addAll(int index, Collection eles) // 从index位置开始将eles中的所有元素添加进来
Object get(int index) // 获取指定index位置的元素
int indexOf(Object obj) // 返回obj在集合中首次出现的位置
int lastIndexOf(Object obj) // 返回obj在当前集合中末次出现的位置
Object remove(int index) // 移除指定index位置的元素,并返回此元素
remove(Object obj) // 移除集合的指定元素,但是如果传入的是int类型,那么会自动调用的是remove(int index)方法
Object set(int index, Object ele) // 设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex) // 返回从fromIndex到toIndex位置的子集合
注意:remove(Object obj),remove(int index) 当传入一个int类型的元素时,调用的是第二个方法,但是如果想要int类型匹配的是Object元素的时候,那么就应该手动将int装箱。
如上述 list.remove(123) 报异常,数组越界,虽然list有123元素,但是默认调用的索引参数的方法。
list.remove(new Integer(123)) 删除的是集合中的元素 123。
增:add(Object obj)
删:remove(Object obj) / remove(int index)
改:set(int index, Object ele)
查:get(int index)
插:add(int index, Object ele)
长度:size()
遍历:iterator() / 增强for循环 / for
2.2.2 ArrayList
// JDK8源码
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
1)ArrayList概述:
①是List接口的典型实现类,主要实现类。
②本质上,ArrayList是对象引用的一个“变长”的动态数组。
③在JDK7时,ArrayList更像饿汉式,直接创建一个初始容量为10的数组;在JDK8,变得像懒汉式,开始只创建一个长度为0的数组,当添加第一个元素时,再创建一个容量为10的数组。
④Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合。
⑤线程不安全,效率高,底层使用Object数组存储。
2.2.3 LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
1)LinkedList概述:
①底层使用双向链表来存储数据,对于频繁的插入,删除操作的时候效率高
②内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:
// prev变量记录前一个元素的位置
// next变量记录下一个元素的位置
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
2)LinkedList类新增的方法:
void addFirst(Object obj)
void addLast(Object obj)
Object getFirst()
Object getLast()
Object removeFirst()
Object removeLast()
2.2.4 Vector
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
1)Vector概述:
①古老的实现类,在jdk1.0的时候就存在了,线程安全,效率低,底层也是使用Object数组来存储的。
②大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
2)Vector类新增的方法:
void addElement(Object obj)
void insertElementAt(Object obj,int index)
void setElementAt(Object obj,int index)
void removeElement(Object obj)
void removeAllElements()
2.3 Set子接口
Set概述:
①Set接口是Collection的子接口,set接口没有提供额外的方法。
②Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
③Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法。
2.3.1 HashSet
1)HashSet概述:
①是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类
②按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
③不能保证元素的排列顺序,不是线程安全的,集合元素可以为null
④对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
2)HashSet判断两个元素相同的标准:两个对象通过hashcode()方法比较哈希值相等,并且使用两个对象的equals()方法比较返回true。
2.3.2 LinkedHashSet
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
LinkedHashSet概述:
①LinkedHashSet 是 HashSet 的子类。
②LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
③LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
④LinkedHashSet 不允许集合元素重复。
2.3.3 TreeSet
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
1)TreeSet概述:
①TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
②TreeSet底层使用红黑树结构存储数据。
③TreeSet和 TreeMap采用红黑树的存储结构。
④特点:有序,查询速度比List快。
2)TreeSet新增的方法:
Comparator comparator()
Object first()
Object last()
Object lower(Object e)
Object higher(Object e)
SortedSet subSet(fromElement, toElement)
SortedSet headSet(toElement)
SortedSet tailSet(fromElement)
3)TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
三 Map接口
Map 与Collection 并列存在。用于保存具有映射关系的数据:key-value。Map 中的 key 和 value 都可以是任何引用类型的数据。Map 中的 key 用Set 来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode() 和equals() 方法。常用String 类作为Map 的“键”。key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value。
Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类。
3.1 常用方法
3.1.1 添加,删除,修改
- Object put(Object key, Object value):将指定key-value添加到(或修改)当前map对象中
- void putAll(Map m):将m中的所有key-value对存放到当前map中
- Object remove(Object key):移除指定key的key-value对,并返回value
- void clear():清空当前map中的所有数据
3.1.2 查询获取
- Object get(Object key):获取指定key对应的value
- boolean containsKey(Object key):是否包含指定的key
- boolean containsValue(Object value):是否包含指定的value
- int size():返回map中key-value对的个数
- boolean isEmpty():判断当前map是否为空
- boolean equals(Object obj):判断当前map和参数对象obj是否相等
3.1.3 元视图操作
- Set keySet():返回所有key构成的Set集合
- Collection values():返回所有value构成的Collection集合
- Set entrySet():返回所有key-value对构成的Set集合
- 例子
Map map = new HashMap();
// map.put(..,..)省略
System.out.println("map的所有key:");
Set keys = map.keySet(); // HashSet
for (Object key : keys) {
System.out.println(key + "->" + map.get(key));
}
System.out.println("map的所有的value:");
Collection values = map.values();
Iterator iter = values.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
System.out.println("map所有的映射关系:");
// 映射关系的类型是Map.Entry类型,它是Map接口的内部接口
Set mappings = map.entrySet();
for (Object mapping : mappings) {
Map.Entry entry = (Map.Entry) mapping;
System.out.println("key是:" + entry.getKey() + ",value是:" + entry.getValue());
}
3.2 HashMap
HashMap最早出现在 JDK1.2 中,底层基于散列算法实现的。HashMap允许 null 作为 键,值。当 null 作为 键时,哈希值为 0。HashMap 不保证键值对的顺序,这就意味着在进行某些操作后,键值对的顺序可能会发生变化。HashMap 同时也是线程不安全类,在并发环境下会有安全问题。
(这里主要研究 JDK1.8 的源码)
HashMap 经过几代的优化,现在已经变得很复杂了。涉及的知识点也很多。主要包括:
- 散列表的实现
- 扰动函数
- 初始化容量
- 负载因子
- 扩容元素拆分
- 链表树化
- 红黑树
- 插入
- 查找
- 删除
- 遍历
- 分段锁
3.2.1 散列表的实现
1、当有一串元素,需要存放在一个数组中,要求在获取每个元素的时候,时间复杂度是:O(1)。也就是说存放在数组的元素,在获取的时候,需要直接通过下标来获取到指定的元素,不能使用遍历获取的方式。
2、那么每个元素存储在数组中的位置,可以使用元素的 HashCode 来作为数组的下标,这样在想要获取特定的元素的时候,可以通过元素的 hashCode 来获取。
3、但是需要注意,hashCode的范围为 [-2147483648, 2147483647],数组的长度不能为这么长,可以让 hashCode 和数组元素做与运算,得到一个存放在数组的下标位置。如果出现有两个相同的下标,则可以使用链表,红黑树的方式在存放相同下标的元素。
HashMap数组的长度为啥需要为 2 的幂次方?
在对哈希值映射到数组的下标时,一般是使用hashCode直接对数组长度进行求余【hash % n】,Java8中的源码优化是使用了与运算,计算速度更快,但是这个前提就是数组的长度 n 需要是 2 的倍数,在这个前提下,【hash % n】才等价于【(n-1) & hash】。所以在源码的 get,put 方法中,都可以看到 tab[ (n-1) & hash ],就是给数组的下标位置存储元素。
3.2.2 扰动函数
Java8中 HashMap 的扰动函数
static final int hash(Object key) {
int h;
// 如果 key 为 null,那么key 的哈希值就是 0
// 将 key 的哈希值右移 16 位,然后和原哈希值进行异或运算,
// 这样可以混合了原哈希值的高位和低位,增大了随机性
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
总结:扰动函数的存在就是为了增加随机性,让数据元素更加均匀的散列,减少碰撞
3.2.3 初始化容量和负载因子
HashMap有一个构造器,可以在创建对象的时候传入初始容量。但是在此构造器中,会通过 tableSizeFor 这个方法计算出一个比该值大的最小的 2 的幂次方。如:传入一个 17,那么比它大的最小的 2 的幂次方就是 32,那么阈值就是 32。即 threshold = 32.
HashMap 还有一个 loadFactor 属性:负载因子,默认值为 0.75. 这个值存在的意义就是当 hashMap 中实际存储的元素数量大于 n*0.75 时,就需要进行扩容了。
3.2.4 扩容元素拆分
一般扩容后的数组长度为原数组的2倍。这时不需要对所有的元素进行重新计算哈希值,只需要将哈希值和扩容新增的长度进行 & 运算,如果为 0 则下标不变,如果不为 0,则需要将位置+新增的数组长度,这个就是其扩容后元素的存储位置。
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-else主要计算扩容后的元素数组的长度和阈值是多少
if (oldCap > 0) {
// 1、旧的数组长度大于0,证明hashMap已经初始化过
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果数组长度已大于 1<<30,那么阈值调整为整数的最大值
threshold = Integer.MAX_VALUE;
// 不进行扩容,返回原数组
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; // 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 12=16*0.75
}
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 { // preserve order
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;
}
3.2.5 插入元素
/**
* 插入一个元素
* @param key 键
* @param value 值
*/
public V put(K key, V value) {
// 首先对key进行hash值的扰动,获取一个新的哈希值
return putVal(hash(key), key, value, false, true);
}
/**
* 这个方法被 put 方法调用,是它和相关方法的实现
*
* @param hash 键的哈希值
* @param key 键
* @param value 值
* @param onlyIfAbsent 如果为true,那么就不改变现有值,即不做更新
* @param evict 如果为 false,则表示处于创建模式
* @return 前一个value值,如果没有,则返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 判断hashMap的数组table是为null,或者长度是否为0,如果是,那么先进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// i = (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;
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) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.2.1 存储结构
JDK7及以前:HashMap是数组+链表结构(链地址法) JDK8及以后:HashMap是数组+链表+红黑树
3.2.1.1 JDK7中的HashMap
1)HashMap在JDK7中的存储结构
HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。
2)添加元素的过程
向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果hash值相同,继续比较二者是否equals。如果返回值为true,则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。
3)HasnMap的扩容
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
4)那么HashMap在什么时候扩容合适
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组元素存在的个数size)loadFactor(负载因子) 时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
3.2.1.2 JDK8中的HashMap
1)HashMap在JDK8中的存储结构
HashMap的内部存储结构其实是数组+链表+红黑树的结合。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
2)HashMap进行扩容和树形化
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
3)关于映射关系的key是否可以修改?
答案:不要修改
映射关系存储到HashMap中会存储key的hash值,这样就不用每次查找都重新计算每个 ENTRY 或 Node(TreeNode)的Hash值了,因此如果已经put到HashMap中的映射关系,再修改key的属性话,而这个属性又参与了hashCode值得计算,那么就会导致匹配不上。
4)总结:JDK1.8相较之前的变化
1)HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
2)当首次调用map.put()时,再创建长度为16的数组
3)数组为Node类型,在jdk7中称为Entry类型
4)形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
5)当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。
3.3 LinkedHashMap
1)概述:LinkedHashMap 是 HashMap 的子类。在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序。与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致。
2)HashMap的内部类:Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
3)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);
}
}
3.4 TreeMap
1)TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
2)TreeSet底层使用红黑树结构存储数据。
3)TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException。
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key Comparable 接口。
4)TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
3.5 Hashtable
1)Hashtable是个古老的 Map 实现类,JDK1.0就存在了。不同于HashMap,Hashtable是线程安全的。
2)Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
3)与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value。
4)与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序。
5)Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
3.6 Properties
1)Properties 类是 Hashtable 的子类,该对象用于处理属性文件。
2)由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型。
3)存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);
四 Collections工具类
4.1 概述
1)Collections 是一个操作 Set、List 和 Map 等集合的工具类
2)Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。
排序操作:(均为static方法)
reverse(List):反转List 中元素的顺序
shuffle(List):对List集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
4.2 Collections常用方法
1)查找、替换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
2)同步控制:Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
五 常见面试题
1、jdk8后,HashMap极限情况下,存储多少条数据会形成红黑二叉树?
答:极限情况下,存储11条数据会形成红黑树
①链表的元素为8个:当HashMap的链表的数据超过8个的时候,会尝试转换为二叉树
②链表的元素为9个:判断容量是否足够,扩容 162=>32
③链表的元素为10个:判断容量是否足够,扩容 322=>64
④链表元素为11个:转换为红黑二叉树
2、为什么HashMap扩容必须是2倍
答:必须是2的n次方,才有可能让数组中的位置元素都有可能倍占用,否则就会浪费内存了
3、集合类在遍历的时候同时删除某个元素就会报异常:
Java ConcurrentModificationException