文章目录
集合概述
产生背景:
一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array(数组)存储对象方面具有一些弊端,而Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中。
- 数组在内存存储方面的特点:
1. 数组初始化以后,长度就确定了。
2. 数组声明的类型,就决定了进行元素初始化时的类型- 数组在存储数据方面的弊端:(反向说明了集合的优点)
4. 数组初始化以后,长度就不可变了,不便于扩展
5. 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储元素的个数
6. 数组存储的数据是有序的、可以重复的。----> 存储数据的特点单一
集合特点:
Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。
集合的使用场景:
Java集合体系:
Java 集合可分为 Collection 和 Map 两种体系。
Collection接口:单列数据,定义了存取一组对象的方法的集合。
- List接口:元素有序、可重复的集合。(相当于“动态”的数组)
- Set接口:元素无序、不可重复的集合。
Map接口:双列数据,保存具有映射关系“key-value对”的集合。
Collection接口
- Collection 接口是List、Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
- 特点:
- JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。
- 在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。
- 要求:
- 向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals(),因为Collection集合中判断两个元素是否相等会用到。
Collection 接口的方法
- 添加
add(Object obj)
addAll(Collection coll)
- 获取有效元素的个数
int size()
- 清空集合
void clear()
- 判断是否是空集合
boolean isEmpty()
- 判断是否包含某元素
boolean contains(Object obj)
:是通过元素的equals方法来判断是否包含某个对象。boolean containsAll(Collection c)
:也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
- 删除
boolean remove(Object obj)
:通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素boolean removeAll(Collection coll)
:当前集合与coll做求差集操作。
- 取两个集合的交集
boolean retainAll(Collection c)
:把交集的结果存在当前集合中,不影响c
- 判断集合是否相等,(判断集合元素是否相同)
boolean
equals(Object obj)
- 转成对象数组
T[] toArray()
- 获取集合对象的哈希值
hashCode()
- 遍历
iterator()
:返回迭代器对象,用于集合遍历
List接口
鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
特点:
- List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
JDK API中List接口的实现类常用的有:
- ArrayList(JDK1.2):常用的,线程不安全,效率较高,底层用数组存储
- LinkedList(JDK1.2):双向链表结构,底层用Node节点(是内部类)存储,适用于频繁增删操作
- Vector(JDK1.0):古老的,与ArrayList结构相似,Vector是线程安全的。
数组与List集合之间的转换:
/* 数组与集合之间的转换 */
Collection coll = new ArrayList();
coll.add(1);
coll.add(2);
//集合 ——> 数组 :toArray()
Object[] arr = coll.toArray();
for (Object o : arr) {
System.out.println(o);
}
//数组 ——> 集合:调用Arrays类的静态方法asList(T ... args)
List<String> list = Arrays.asList("AA","BB");
System.out.println(list);
//注意点:
List<String> list1 = Arrays.asList(new Integer[]{1,2});
List<String> list2 = Arrays.asList(new int[]{1,2});
System.out.println(list1.size());//1
System.out.println(list2.size());//2
//看asList(T ... args)源码如下:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
注意点:Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合匿名实现类的实例。
List接口方法
List除了从Collection集合继承的方法外,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位置的元素,并返回此元素
- Object set(int index, Object ele): 设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex)
:返回从fromIndex到toIndex位置的子集合
ArrayList类
简单介绍
-
ArrayList 是 List 接口的典型实现类、开发中主要使用。
-
本质上,ArrayList是对象引用的一个”变长”数组
-
ArrayList的JDK1.8之前与之后的实现区别?
- JDK1.7:调用ArrayList()像饿汉式,直接创建一个初始容量为10的数组
- JDK1.8:调用ArrayList()像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组
-
ArrayList的扩容机制?
- JDK7/8都一样:默认将原有数组扩容1.5倍;若不够,扩容为原有数组长度+添加元素的长度;封顶是Integer.MAX_VALUE。
源码分析:(JDK7 和 JDK8有所不同的实现)
底层都用数组存储:Object[] elementData
JDK7情况下
-
调用构造器
public ArrayList() { this(10);//默认创建初始长度为10的数组。 } public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); this.elementData = new Object[initialCapacity]; }
-
添加操作
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! //size:数组元素的个数 elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { modCount++; //protected transient int modCount = 0; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) {//扩容机制 // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);//扩容机制:默认原有数组扩容1.5倍。 if (newCapacity - minCapacity < 0) newCapacity = minCapacity;//还不够的情况下,直接用长度:原有数组长度+添加元素的长度。 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);//如果超过指定最大值,直接长度为Integer.MAX_VALUE // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity);//扩容成功后,将原有数组元素复制到新数组 } private static int hugeCapacity(int minCapacity) {//相当于取Integer.MAX_VALUE if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
-
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)。尽量避免使用扩容机制影响效率。
JDK8情况下
-
调用构造器
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //不会立刻创建数组。等到要赋值时再创建长度为10的数组。避免资源浪费。
-
添加操作
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) {//处理elementData为空的情况 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity);//DEFAULT_CAPACITY 为 10 } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) {//扩容,和之前(JDK7)一样。 // overflow-conscious code int oldCapacity = elementData.length;//只不过默认初始化后第一次赋值elementData.length是0,但结果都一样。 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); }
LinkedList类
简单介绍
-
对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高
-
新增方法:
- void addFirst(Object obj)
- void addLast(Object obj)
- Object getFirst()
- Object getLast()
- Object removeFirst()
- Object removeLast()
-
存储结构:双向链表的存储结构,内部没有声明数组,而是定义了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; } }
-
特点:
- 只记录了首末元素,元素并没有下标,所以涉及查询、删除操作时,需要沿着链表寻找指定元素(移动指针)。
- 不用考虑扩容。
源码分析:
-
用于存储的结构(属性):双向链表的实现
transient int size = 0;//记录链表长度 transient Node<E> first;//记录链表的首节点 transient Node<E> last;//记录链表的末节点 //Node是其内部类 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; } }
-
添加操作
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++; }
Vector类
简单介绍
-
Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全(方法声明为
synchronized
)的。 -
在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。
-
在 Collections 类中有方法:
synchronizedList(List<T> list)
,用于把List集合转为线程安全的。所以Vector类完全可以被替代! -
新增方法:
- void addElement(Object obj)
- void insertElementAt(Object obj,int index)
- void setElementAt(Object obj,int index)
- void removeElement(Object obj)
- void removeAllElements()
-
源码分析:(与ArrayList非常相似)
- 底层用数组存储:
Object[] elementData
- 通过Vector()构造器创建对象时,底层都创建了长度为10的数组,在扩容方面,默认扩容为原来的数组长度的2倍。
- 底层用数组存储:
面试题
1、请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?
- 三者的”同“
- 三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
- ArrayList和LinkedList的”异同“
- 二者都线程不安全,相对线程安全的Vector,执行效率高。
- 此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
- ArrayList、Vector底层用Object[] 数组存储;LinkedList用内部类Node存储(双向链表)。
- 对于随机访问get和set,ArrayList觉对优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
- ArrayList和Vector的区别
- Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。
- Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。
- Vector还有一个子类Stack。
- Stack:栈。特点:先进后出。
2、看下面代码
@Test
public void testListRemove() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list);
System.out.println(list);//[1,2]
}
private static void updateList(List list) {
list.remove(2);
/*
考的是remove()的使用,remove()有重载方法,remove(int i)和remove(Object o),此时2当作int型传入。如果传入new Integer(2)就是另一种结果了!
*/
}
Iterator接口
迭代器:GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。 类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”。
Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
使用:
- Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
- Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
- 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
Iterator接口的方法
说明:
-
在调用
next()
方法之前必须要调用hasNext()
进行检测。若不调用,且下一条记录无效,直接调用next()
会抛出NoSuchElementException
异常- 执行原理图解:
- 执行原理图解:
-
remove()方法:现在是default的。
-
示例
Iterator iter = coll.iterator();//回到起点 while(iter.hasNext()){ Object obj = iter.next(); if(obj.equals("Tom")){ iter.remove(); } }
-
注意:
- Iterator可以删除集合的元素,但是是遍历过程中须要通过迭代器对象的
remove()
,不是集合对象的remove()
(因为遍历时会生成新的集合对象)。 - 如果还未调用
next()
或在上一次调用next()
之后已经调用了remove()
,再调用remove都会报IllegalStateException
。
- Iterator可以删除集合的元素,但是是遍历过程中须要通过迭代器对象的
-
遍历集合
三种方式:Iterator、foreach循环、for循环 (List是有序的,所以可以遍历!)
使用Iterator接口
示例:
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
Iterator iterator = coll.getIterator();
//方式一(不推荐)
for (int i = 0; i < coll.size(); i++) {
System.out.println(iterator.next());
}
//方式二(推荐)
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
使用 foreach 循环
-
Java 5.0 提供了 foreach 循环迭代访问 Collection和数组。
-
遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
-
遍历集合的底层还是通过调用Iterator完成操作的。
-
foreach还可以用来遍历数组。
-
语法格式:
-
使用注意点:
foreach循环遍历时会声明新的变量。String[] arr = new String[]{"tom", "mike"}; System.out.println(Arrays.toString(arr)); for (int i = 0; i < arr.length; i++) { arr[i] = "new"; } System.out.println(Arrays.toString(arr)); System.out.println("-------------------------------"); String[] arr2 = new String[]{"tom", "mike"}; System.out.println(Arrays.toString(arr2)); for (String str : arr2) {//注意foreach循环遍历会声明新的变量str来接收参数。 str = "new"; } System.out.println(Arrays.toString(arr2)); /**结果 [tom, mike] [new, new] ------------------------------- [tom, mike] [tom, mike] */
Set接口
简单介绍
- Set接口是Collection的子接口,set接口没有提供额外的方法
- 特点:
- 存储无序的、不可重复的数据
- Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
- Set 判断两个对象是否相同不是使用 == 运算符,而是根据
equals()
方法
- 存储无序的、不可重复的数据
- Set接口的常用实现类:
- HashSet:作为Set接口的主要实现类,线程不安全的,可以存储null,底层 数组+链表 的结构。
- LinkedHashSet:作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序遍历,双向链表结构
- TreeSet:可以按照被添加对象的指定属性进行排序。
- HashSet:作为Set接口的主要实现类,线程不安全的,可以存储null,底层 数组+链表 的结构。
HashSet类
HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
HashSet 具有以下特点:
- 不能保证元素的排列顺序 ——> 无序性(注意无序性不是随机性)
- 存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值。
- 不能存在相同的元素,相等时添加失败。 ——> 不可重复性
- HashSet 集合判断两个元素相等的标准:
- 两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
- HashSet 集合判断两个元素相等的标准:
- HashSet 不是线程安全的
- 集合元素可以是 null
使用须知:
- 对于存放在Set容器中的对象,对应的类一定要重写==equals()和hashCode(Object obj)==方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。(为什么呢?和底层存储逻辑有关)
重写 equals() 方法的基本原则
- 当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,它们仅仅是两个对象(它们可能equals上相等,哈希值却不相等)。因此,违反了“相等的对象必须具有相等的散列码”。
- 结论:重写equals方法的时候一般都需要同时重写hashCode方法。**通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。**来满足需要:两个对象equals()相等,哈希值也是相等。
hashCode()的重写
以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals和hashCode。问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?
- 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
- 并且31只占用5bits,相乘造成数据溢出的概率较小。
- 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
- 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)
底层逻辑:
HashSet本质上是HashMap, 底层上可以看到,关于HashSet的操作,实际上是操纵HashMap对象来完成。所以,这里就不讲源码了,看HashMap的源码去吧。下面的LinkdedHashSet、TreeSet同理。
private transient HashMap<E,Object> map;//有HashMap型属性
public HashSet() {
map = new HashMap<>();
}
//然后在方法里,实际是调用map的方法。如:
public boolean add(E e) {
return map.put(e, PRESENT)==null;//PRESENT是一个空对象。
//结论:HashSet的值作为HashMap的key值存储。//进一步观察Set接口的其他实现类,会发现都是作为对应的Map实现类的key值存储。
}
LinkedHashSet类
LinkedHashSet 是 HashSet 的子类。
底层:本质上是LinkedHashMap的key集合。
特点:
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置(依然具有无序性)
- 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。(可以按插入顺序遍历)
- LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能(频繁的遍历操作时优于HashSet)。
- LinkedHashSet 不允许集合元素重复。(不可重复性)
TreeSet类
简单介绍
- TreeSet 是 SortedSet 接口(SortedSet接口继承于Set接口)的实现类,TreeSet 可以确保集合元素处于排序状态
- TreeSet有不可重复性: 不会存在两个相同的元素。如相等,则添加失败。
- TreeSet底层使用红黑树结构存储数据
- 底层:本质上是TreeMap的key集合。
新增的方法如下: (了解)
- Comparator comparator()
- Object first()
- Object last()
- Object lower(Object e)
- Object higher(Object e)
- SortedSet subSet(fromElement, toElement)
- SortedSet headSet(toElement)
- SortedSet tailSet(fromElement)
TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
自然排序
- TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
- 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
- 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
- Comparable 的典型实现:
BigDecimal、BigInteger
以及所有的数值型对应的包装类:按它们对应的数值大小进行比较Character
:按字符的 unicode值来进行比较Boolean
:true 对应的包装类实例大于 false 对应的包装类实例String
:按字符串中字符的 unicode 值进行比较Date、Time
:后边的时间、日期比前面的时间、日期大
- 向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
- 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
- 对于 TreeSet 集合而言,它判断两个对象是否相等的标准是:两个对象通过 compareTo(Object obj) 方法比较返回值为 0
- 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。否则,让人难以理解。
定制排序
- TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。
- 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
- 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
- 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
- 使用定制排序判断两个元素相等的标准是:通过Comparator的compare(T o1,T o2)比较两个元素返回了0。
题目
面试题
其中Person类中重写了hashCode()和equal()方法
HashSet set = new HashSet();
Person p1 = new Person(1001,"AA");
Person p2 = new Person(1002,"BB");
set.add(p1);
set.add(p2);
p1.name = "CC";
set.remove(p1);
System.out.println(set);//[{1002,"BB"},{1001,"CC"}]。为什么是这俩呢?答:① Set是按照哈希值决定元素存储位置的,重写hashCode()一般会将属性加入算法中,Set判断两个元素是是否相等依据是:equals()和hashCode()的结果。set在存储p1时根据哈希值指定位置,而删除p1时改变了属性,所以哈希值与存储时不一致,所以remove()找不到原来的位置,删除不了。 ② 存储的是对象的地址值,所以p1改变属性值会影响set的元素。
set.add(new Person(1001,"CC"));
System.out.println(set);//[{1002,"BB"},{1001,"CC"},{1001,"CC"}]
set.add(new Person(1001,"AA"));
System.out.println(set);//[{1002,"BB"},{1001,"CC"},{1001,"CC"},{1001,"AA"}]。为什么还能加进去呢?答:因为此时存的位置虽然与前面p1的位置重复了,但是与p1equals()时返回false的,因为此时它们属性不一致!所以它们以链表形式存储。
练习
在List内去除重复数字值,要求尽量简单
public static List duplicateList(List list) {
HashSet set = new HashSet();
set.addAll(list);
return new ArrayList(set);
}
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Integer(1));
list.add(new Integer(2));
list.add(new Integer(2));
list.add(new Integer(4));
list.add(new Integer(4));
List list2 = duplicateList(list);
for (Object integer : list2) {
System.out.println(integer);
}
}
Map接口
简单介绍
Map与Collection并列存在。用于保存具有映射关系的数据:key-value(双列数据)。
特点:(key-value结构)
-
Map 中的 key 和 value 都可以是任何引用类型的数据
-
key (存储特点:无序、不允许重复),
- 可用Set存放。(Map里定义了方法:
Set keySet()
)。即同一个 Map 对象所对应的类,key对象须重写hashCode() 和 equals() 方法(TreeMap例外)。Map的实现类定义了内部类:KeySet。然后方法
Set keySet()
返回的是一个KeySet对象(一个内部类),而不是HashSet或其他。
- 可用Set存放。(Map里定义了方法:
-
value(存储特点:无序可重复),
- 可用Collection存放。(Map里定义了方法:
Collection values()
)。即value对象须重写equals()。Map的实现类定义了内部类:Values。然后方法
Collection values()
返回的是一个Values对象(一个内部类)。
- 可用Collection存放。(Map里定义了方法:
-
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
-
每个key-value放入一个Entry对象中。Entry(存储特点:无序不重复),
-
可用Set来存放(按照Entry中key的哈希值决定存储位置)。(Map里定义了方法:
Set entrySet()
)Map的实现类定义了内部类:EntrySet。然后方法
Set entrySet()
返回的是一个Values对象。Map接口本质上就是用数组存储一个个的Entry对象:底层存储为单列数据,却能保存双列数据
-
-
图示
Map接口的常用实现类
HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类
Map接口 since: JDK1.2
-
HashMap(JDK1.2):作为Map接口的主要实现类,线程不安全的,效率高,可以存储null值(key/value)
-
LinkedHashMap(JDK1.2):保证在遍历map元素时,可以按照添加的顺序实现遍历。
原因:在原有的HashMap的底层结构基础上,添加了一对指针,指向前一个和后一个元素。
对于频繁的遍历操作,此类执行效率高于HashMap
-
-
TreeMap(JDK1.4):保证按照添加的 key-value对 进行排序,实现排序遍历。(考虑key的自然排序或者定制排序)
底层使用红黑树。 -
Hashtable(JDK1.0):古老的实现类。线程安全的,效率低,不能存储null值(key/value)
- Properties:常用来处理配置文件。key和value都是String类型。
涉及的面试题
- HashMap的底层实现原理?
- HashMap和Hashtable的异同?
- CurrentHashMap与Hashtable的异同?
- CurrentHashMap用于多线程操作HashMap的结构。
Map常用方法
-
添加、删除、修改操作:
Object put(Object key,Object value)
:将指定key-value添加到(或修改)当前map对象中void putAll(Map m)
:将m中的所有key-value对存放到当前map中Object remove(Object key)
:移除指定key的key-value对,并返回valuevoid clear()
:清空当前map中的所有数据
-
元素查询的操作
Object get(Object key)
:获取指定key对应的valueboolean containsKey(Object key)
:是否包含指定的keyboolean containsValue(Object value)
:是否包含指定的valueint size()
:返回map中key-value对的个数boolean isEmpty()
:判断当前map是否为空boolean equals(Object obj)
:判断当前map和参数对象obj是否相等
-
元视图操作的方法:
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());
}
HashMap
HashMap是 Map 接口使用频率最高的实现类。
特点:(key-value)
- 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
- 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
- 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
- 一个key-value构成一个entry
- 所有的entry构成的集合是Set:无序的、不可重复的
- HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hash值也相等。
- HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
HashMap源码中的重要常量
- DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
- MAXIMUM_CAPACITY : HashMap的最大支持容量,2^30
- DEFAULT_LOAD_FACTOR:HashMap的默认加载因子
- **TREEIFY_THRESHOLD:**Bucket中链表长度大于该默认值,转化为红黑树
- UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表
- MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
- table:存储元素的数组,总是2的n次幂
- entrySet:存储具体元素的集
- size:HashMap中存储的键值对的数量
- modCount:HashMap扩容和结构改变的次数。
- threshold:扩容的临界值,= 容量*填充因子
- loadFactor:填充因子
HashMap的底层原理:JDK 1.8之前。以JDK7为例说明
-
HashMap的内部存储结构其实是数组和链表的结合。
- 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。 (数组)
- 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。 (链表)
-
添加元素的过程:(体现数组+链表的存储结构)
- 向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元素。(链表)
JDK8的添加过程,与上一致,只有一点区别:链表的指向不同
- 在最后一步,如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。原有的entry元素指向entry1。
(巧记:七上八下)
- 向HashMap中添加
-
HashMap的扩容
- 当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
-
那么HashMap什么时候进行扩容呢?
- 当HashMap中的元素个数超过
数组大小(数组总大小length,不是数组中个数size)*loadFactor
时 , 就 会 进 行 数 组 扩 容 , loadFactor 的默认 值 (DEFAULT_LOAD_FACTOR
)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY
)为16,那么当HashMap中元素个数超过16*0.75=12
(这个值就是代码中的threshold值,也叫做临界值)且新元素存放的位置上已经有元素的时候,就把数组的大小扩展为2*16=32
,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
- 当HashMap中的元素个数超过
-
源码展示:
-
实例化:
//实例化时,默认底层会创建一个长度为16的数组。 public HashMap() { //DEFAULT_INITIAL_CAPACITY为16,DEFAULT_LOAD_FACTOR为0.75f //确定了底层数组初始长度和加载因子 this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } //此方法用于构造数组 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //MAXIMUM_CAPACITY为1 << 30,保证上限 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); //以下才是重点 // Find a power of 2 >= initialCapacity //1.确定底层初始容量:保证了底层创建的数组长度一定为2的倍数,且至少为16。 //当调用构造器HashMap(5),你以为底层创建了长度为5的数组,不,如下逻辑,会变为16! int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor;//加载因子默认值:0.75f //2.确定threshold临界值:容量*0.75。(上限是MAXIMUM_CAPACITY + 1) threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //3.实例化table:用于存储的数组 table = new Entry[capacity]; useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); init();//方法里是空的 }
-
添加数据
//此方法用于添加数据 public V put(K key, V value) { //1.可以放null值 if (key == null) return putForNullKey(value); //2.通过自己定义的算法取得 key的哈希值 int hash = hash(key); //3.通过自己定义的算法取得该元素在数组中的存放位置 int i = indexFor(hash, table.length); //4.若该位置有元素,遍历该位置上的元素,让它们的key值进行比较,相等则覆盖,并返回旧元素结束方法。 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //满足相等的必要条件:哈希值相等和 equals()返回true。 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //5.若该位置没有元素或者遍历时equals都为false,将元素添加至该位置 addEntry(hash, key, value, i); return null; } void addEntry(int hash, K key, V value, int bucketIndex) { //6.如果数组元素个数大于临界值且要放的位置上也有元素时,执行扩容 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//扩容。扩容为原来的2倍。(说明底层的数组长度一定为2的倍数) //扩容后添加元素的数据得重新计算。那么,重新计算后元素的位置会不会改变呢?改变后的位置上也有元素该怎么办?算法会设计到通常下改变的情况极少。 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } //7.添加元素 createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e);//将新的元素放在数组该位置。即新的元素为链表头 size++; } void resize(int newCapacity) {//扩容细节:根据新确定的容量创建新数组、重新确定数组的临界值、复制旧元素至新数组 Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; boolean oldAltHashing = useAltHashing; useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean rehash = oldAltHashing ^ useAltHashing; transfer(newTable, rehash); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } final int hash(Object k) { int h = 0; if (useAltHashing) { if (k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h = hashSeed; } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } static int indexFor(int h, int length) { return h & (length-1); }
-
存储单元:Entry
//内部类 static class Entry<K,V> implements Map.Entry<K,V> { //实现了父接口Map的Entry final K key; V value; Entry<K,V> next; int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n;//新的元素指向旧的元素 key = k; hash = h; } ...
-
HashMap的底层原理:JDK 1.8
-
HashMap的内部存储结构其实是数组+链表+红黑树的结合。
- 当实例化一个HashMap时,会初始化
initialCapacity
和loadFactor
,在put第一对映射关系时,系统会创建一个长度为initialCapacity
的Node
数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。(数组) - 每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。(链表)
- 也可能是一个个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。(树)
- 当实例化一个HashMap时,会初始化
-
那么HashMap什么时候进行扩容和树形化呢?
- 当HashMap中的元素个数超过数组大小
(数组总大小length,不是数组中个数size)*loadFactor
时 , 就会进行数组扩容 ,loadFactor
的默认 值 (DEFAULT_LOAD_FACTOR
)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY
)为16,那么当HashMap中元素个数超过16*0.75=12
(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2*16=32
,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。 - 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
- 当HashMap中的元素个数超过数组大小
-
关于映射关系的key是否可以修改?answer:不要修改
- 映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。
-
总结:JDK1.8相较于之前的变化:
- HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
- 当首次调用map.put()时,再创建长度为16的数组
- 数组为Node类型,在jdk7中称为Entry类型
- 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
- 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。
-
问题
-
为什么加载因子默认要设为0.75呢?
目的:尽可能地让链表结构少出现。所以要让加载因子小一点,但是太小如0.3时数组的利用率太低,太大如0.9会导致链表结构出现得多些,开发人员经过某种统计确定用0.75。
数组一定会装满吗?因为是根据hash值确定存储位置的,所以不确定。
参考答案:
- 负载因子的大小决定了HashMap的数据密度。
- 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
-
-
源码展示
-
实例化
//实例化时默认并没有立即创建数组,会在首次添加数据时创建一个长度为16的数组。 public HashMap() { //DEFAULT_LOAD_FACTOR为0.75f //确定了加载因子 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //底层数组并不会根据构造器指定的容量而立即创建数组。而是等添加数据时再创建。 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) { //1.算出key的哈希值 return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab:旧数组 //p:新元素在数组存放的位置上的元素 //n:旧数组的长度 //i:新元素在数组存放的位置索引 Node<K,V>[] tab; Node<K,V> p; int n, i; //2.当数组table为空时(即初始情况):扩容数组:此时会创建默认容量、默认临界值的数组。 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //3.根据哈希值算出元素应存放的位置并判断该位置上是否有元素 if ((p = tab[i = (n - 1) & hash]) == null) //4.如果该位置上没有元素,则将新元素放进去。 tab[i] = newNode(hash, key, value, null); //5.如果该位置上有元素,则 else { Node<K,V> e; K k; //情况一:先将新元素与该位置上的元素比较。比较的是key(hash值与equals())。若相等,则覆盖 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) //TREEIFY_THRESHOLD为8 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; //6.判断当前数组元素个数是否达到临界值,是则扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } //此方法用于扩容 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //1.确定旧数组容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; //2.确定旧数组的临界值 int oldThr = threshold; //3.会根据不同情况确定新数组的容量、临界值 int newCap, newThr = 0; //情况一:当前数组容量大于默认值时,即不是初始化情况时。 if (oldCap > 0) { //旧数组容量大于指定最大值时,不会扩容数组,且临界值改为Integer.MAX_VALUE,方法结束。 if (oldCap >= MAXIMUM_CAPACITY) {//static final int MAXIMUM_CAPACITY = 1 << 30; threshold = Integer.MAX_VALUE; return oldTab; } //当前数组容量小于指定最大值且大于默认值时,新临界值为旧临界值的2倍。 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 //扩容:扩容为旧数组的临界值大小。与JDK7不同,JDK8不能保证底层数组长度一直是2的倍数:因为当使用构造器时指定容量的话,可以让底层创建奇数的长度的数组。 newCap = oldThr; //情况二:初始化情况 else { // zero initial threshold signifies using defaults //初始情况时,用默认值初始化数组容量、数组的临界值。 newCap = DEFAULT_INITIAL_CAPACITY;//16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//16*0.75 } //情况三:当新临界值溢出时(因为是进行位运算,所以溢出会为0),新临界值为“新数组容量*加载因子(0.75)“ if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //4.更新临界值的操作 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //创建新数组 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //5.更新底层数组的操作 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; } final void treeifyBin(Node<K,V>[] tab, int hash) {//将数组上指定位置的元素(链表)转为红黑树 int n, index; Node<K,V> e; //并不是马上转为树,而是有要求:当底层数组长度大于64是时候才可以转成树。否则虽然链表长度大于8,依然不转为树,而是扩容。 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//MIN_TREEIFY_CAPACITY为64 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); } } TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
存储单元:
-
Node:与Entry差别不大。
//内部类 static class Node<K,V> implements Map.Entry<K,V> {//实现了父接口Map的Entry,本质没有改变 final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next;//差别在这,旧的元素指向新的元素 } ...
-
TreeNode:红黑树的节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } ...
-
-
面试题
-
谈谈你对HashMap中put/get方法的认识?如果了解再谈谈HashMap的扩容机制?默认大小是多少?什么是负载因子(或填充比)?什么是吞吐临界值(或阈值、threshold)?
答:
-
负载因子值的大小,对HashMap有什么影响?
答:
- 负载因子的大小决定了HashMap的数据密度。
- 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
LinkedHashMap
简单介绍
- LinkedHashMap 是 HashMap 的子类
- 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
- LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致(插入=添加)
源码展示
-
实例化
//用父类的逻辑 public LinkedHashMap() { super(); accessOrder = false; }
-
没有重写put()方法,那怎么添加数据呢?它重写了 newNode()方法,此方法在put()方法被调用。
说明了:此类存储元素的逻辑用的是父类的,但元素的结构是自己定义的。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; }
-
那么元素的结构做了什么改变呢?
-
HashMap中存储单元:Node
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
-
LinkedHashMap中的存储单元:Entry
static class Entry<K,V> extends HashMap.Node<K,V> {//继承了HashMap.Node,本质没变 Entry<K,V> before, after;//添加了前后指针,记录了上一个元素、和下一个元素,所以可以按照插入顺序遍历 Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
-
TreeMap
-
实现了SortedMap接口,而SortedMap接口继承于Map接口。
-
TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
-
TreeSet底层使用红黑树结构存储数据
- 特点:有序,查询速度比List快
再具体就不说了,可以参看http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html,对红黑树的讲解写得不错。
- 特点:有序,查询速度比List快
-
TreeMap 是根据Key进行排序的:TreeSet底层就是作为TreeMap的key存储的,逻辑写TreeSet那里,这里不写第二遍了。
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象(
TreeMap(Comparator<? super K> comparator)
),该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
-
TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
Hashtable
- Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
- Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
- Hashtable底层存储结构是 数组+链表
- 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
- 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
- Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
Properties
-
Properties 类是 Hashtable 的子类,该对象用于处理属性文件
-
由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
-
存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法(注意文件的编码,避免出乱码)
Properties pros = new Properties(); pros.load(new FileInputStream("jdbc.properties"));//加载属性文件:后缀为.properties String user = pros.getProperty("user"); System.out.println(user);
properties文件的格式要求
#注释
# 不许有空格。
xuxiansheng=18;
Collections工具类
简单介绍
-
Collections 是一个操作 Set、List 和 Map 等集合的工具类。since: JDK1.2。
-
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
Collections 常用静态方法
-
查找、替换
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 对象的所有旧值
-
排序操作:
reverse(List)
:反转 List 中元素的顺序shuffle(List)
:对 List 集合元素进行随机排序sort(List)
:根据元素的自然顺序对指定 List 集合元素按升序排序sort(List,Comparator)
:根据指定的 Comparator 产生的顺序对 List 集合元素进行排序swap(List,int, int)
:将指定 list 集合中的 i 处元素和 j 处元素进行交换
-
同步控制
- Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
- Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
-
创建只读集合:
一系列方法创建的集合特点:只能读
<T> Collection<T> unmodifiableCollection(Collection<? extends T> c)
<T> List<T> java.util.Collections.unmodifiableList(List<? extends T> list)
<K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m)
- …
List<String> list = Collections.unmodifiableList(Arrays.asList("a", "b", "c")); Set<String> set = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));//创建只读集合 List<Integer> list1 = Arrays.asList(1,2,3,4,5);//此时得到的集合List也是一个只读集合。 list1.add(6);//抛异常
面试题
- Collection和Collections的区别?
补充
Enumeration
-
Enumeration 接口是 Iterator 迭代器的 “古老版本”
Enumeration stringEnum = new StringTokenizer("a-b*c-d-e-g", "-"); while(stringEnum.hasMoreElements()){ Object obj = stringEnum.nextElement(); System.out.println(obj); }