Java:集合

集合概述

产生背景:
一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array(数组)存储对象方面具有一些弊端,而Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中。

  • 数组在内存存储方面的特点:
    1. 数组初始化以后,长度就确定了。
    2. 数组声明的类型,就决定了进行元素初始化时的类型
  • 数组在存储数据方面的弊端:(反向说明了集合的优点)
    4. 数组初始化以后,长度就不可变了,不便于扩展
    5. 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储元素的个数
    6. 数组存储的数据是有序的、可以重复的。----> 存储数据的特点单一

集合特点:
Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。

集合的使用场景:
在这里插入图片描述

Java集合体系:
Java 集合可分为 Collection 和 Map 两种体系。

Collection接口:单列数据,定义了存取一组对象的方法的集合。

  • List接口:元素有序、可重复的集合。(相当于“动态”的数组)
  • Set接口:元素无序、不可重复的集合。
    在这里插入图片描述

Map接口:双列数据,保存具有映射关系“key-value对”的集合。
在这里插入图片描述

Collection接口

  • Collection 接口是ListSetQueue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
  • 特点:
    • JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。
    • 在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。
  • 要求:
    • 向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals(),因为Collection集合中判断两个元素是否相等会用到。

Collection 接口的方法

  1. 添加
    • add(Object obj)
    • addAll(Collection coll)
  2. 获取有效元素的个数
    • int size()
  3. 清空集合
    • void clear()
  4. 判断是否是空集合
    • boolean isEmpty()
  5. 判断是否包含某元素
    • boolean contains(Object obj):是通过元素的equals方法来判断是否包含某个对象。
    • boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
  6. 删除
    • boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
    • boolean removeAll(Collection coll):当前集合与coll做求差集操作。
  7. 取两个集合的交集
    • boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
  8. 判断集合是否相等,(判断集合元素是否相同)
    • boolean equals(Object obj)
  9. 转成对象数组
    • T[] toArray()
  10. 获取集合对象的哈希值
    • hashCode()
  11. 遍历
    • 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 集合里添加了一些根据索引来操作集合元素的方法。

  1. void add(int index, Object ele): 在index位置插入ele元素
  2. boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
  3. Object get(int index): 获取指定index位置的元素
  4. int indexOf(Object obj):返回obj在集合中首次出现的位置
  5. int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
  6. Object remove(int index): 移除指定index位置的元素,并返回此元素
  7. Object set(int index, Object ele): 设置指定index位置的元素为ele
  8. 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情况下

  1. 调用构造器

    public ArrayList() {
    	this(10);//默认创建初始长度为10的数组。
    }
    public ArrayList(int initialCapacity) {
    	super();
    	if (initialCapacity < 0)
    		throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    		this.elementData = new Object[initialCapacity];
    }
    
  2. 添加操作

    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;
    }
    
  3. 结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)。尽量避免使用扩容机制影响效率。

JDK8情况下

  1. 调用构造器

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    //不会立刻创建数组。等到要赋值时再创建长度为10的数组。避免资源浪费。
    
  2. 添加操作

    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类,效率较高

  • 新增方法:

    1. void addFirst(Object obj)
    2. void addLast(Object obj)
    3. Object getFirst()
    4. Object getLast()
    5. Object removeFirst()
    6. 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;
        } 
    }
    

    在这里插入图片描述

  • 特点:

    • 只记录了首末元素,元素并没有下标,所以涉及查询、删除操作时,需要沿着链表寻找指定元素(移动指针)。
    • 不用考虑扩容。

源码分析

  1. 用于存储的结构(属性):双向链表的实现

    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;
    	}
    }
    
  2. 添加操作

    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类完全可以被替代!

  • 新增方法:

    1. void addElement(Object obj)
    2. void insertElementAt(Object obj,int index)
    3. void setElementAt(Object obj,int index)
    4. void removeElement(Object obj)
    5. void removeAllElements()
  • 源码分析:(与ArrayList非常相似)

    • 底层用数组存储:Object[] elementData
    • 通过Vector()构造器创建对象时,底层都创建了长度为10的数组,在扩容方面,默认扩容为原来的数组长度的2倍

面试题

1、请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?

  1. 三者的”同“
    1. 三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
  2. ArrayList和LinkedList的”异同“
    1. 二者都线程不安全,相对线程安全的Vector,执行效率高。
    2. 此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
      • ArrayList、Vector底层用Object[] 数组存储;LinkedList用内部类Node存储(双向链表)。
    3. 对于随机访问get和set,ArrayList觉对优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
  3. ArrayList和Vector的区别
    1. Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。
    2. Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。
    3. Vector还有一个子类Stack。
      1. 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 集合中的元素

使用:

  1. Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
  2. Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
  3. 集合对象每次调用iterator()方法都得到一个全新的迭代器对象默认游标都在集合的第一个元素之前

Iterator接口的方法
在这里插入图片描述
在这里插入图片描述
说明:

  1. 在调用next()方法之前必须要调用hasNext()进行检测。若不调用,且下一条记录无效,直接调用next()会抛出NoSuchElementException异常

    • 执行原理图解:
      在这里插入图片描述
  2. remove()方法:现在是default的。

    1. 示例

      Iterator iter = coll.iterator();//回到起点
      while(iter.hasNext()){
          Object obj = iter.next();
          if(obj.equals("Tom")){
          	iter.remove();
          } 
      }
      
    2. 注意

      1. Iterator可以删除集合的元素,但是是遍历过程中须要通过迭代器对象的remove(),不是集合对象的remove()(因为遍历时会生成新的集合对象)。
      2. 如果还未调用next()或在上一次调用 next()之后已经调用了 remove(),再调用remove都会报IllegalStateException

遍历集合

三种方式: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类

HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。

HashSet 具有以下特点:

  1. 不能保证元素的排列顺序 ——> 无序性(注意无序性不是随机性)
    • 存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值。
  2. 不能存在相同的元素,相等时添加失败。 ——> 不可重复性
    • HashSet 集合判断两个元素相等的标准
      • 两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
  3. HashSet 不是线程安全的
  4. 集合元素可以是 null

使用须知:

  1. 对于存放在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集合。
特点:

  1. LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置(依然具有无序性
  2. 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。(可以按插入顺序遍历)
  3. LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能(频繁的遍历操作时优于HashSet)。
  4. LinkedHashSet 不允许集合元素重复。(不可重复性
    在这里插入图片描述

TreeSet类

简单介绍

  • TreeSet 是 SortedSet 接口(SortedSet接口继承于Set接口)的实现类,TreeSet 可以确保集合元素处于排序状态
  • TreeSet有不可重复性: 不会存在两个相同的元素。如相等,则添加失败。
  • TreeSet底层使用红黑树结构存储数据
  • 底层:本质上是TreeMap的key集合。

新增的方法如下: (了解)

  1. Comparator comparator()
  2. Object first()
  3. Object last()
  4. Object lower(Object e)
  5. Object higher(Object e)
  6. SortedSet subSet(fromElement, toElement)
  7. SortedSet headSet(toElement)
  8. SortedSet tailSet(fromElement)

TreeSet 两种排序方法:自然排序定制排序。默认情况下,TreeSet 采用自然排序。
自然排序

  1. TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
  2. 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
    1. 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
    2. Comparable 的典型实现:
      1. BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
      2. Character:按字符的 unicode值来进行比较
      3. Boolean:true 对应的包装类实例大于 false 对应的包装类实例
      4. String:按字符串中字符的 unicode 值进行比较
      5. Date、Time:后边的时间、日期比前面的时间、日期大
    3. 向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
    4. 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
    5. 对于 TreeSet 集合而言,它判断两个对象是否相等的标准是:两个对象通过 compareTo(Object obj) 方法比较返回值为 0
    6. 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。否则,让人难以理解。

定制排序

  1. TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。
  2. 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
  3. 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
  4. 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
  5. 使用定制排序判断两个元素相等的标准是:通过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结构)

  1. Map 中的 key 和 value 都可以是任何引用类型的数据

  2. key (存储特点:无序、不允许重复)

    1. 可用Set存放。(Map里定义了方法:Set keySet())。即同一个 Map 对象所对应的类,key对象须重写hashCode()equals() 方法(TreeMap例外)。

      Map的实现类定义了内部类:KeySet。然后方法Set keySet()返回的是一个KeySet对象(一个内部类),而不是HashSet或其他。

  3. value(存储特点:无序可重复)

    1. 可用Collection存放。(Map里定义了方法:Collection values())。即value对象须重写equals()

      Map的实现类定义了内部类:Values。然后方法Collection values()返回的是一个Values对象(一个内部类)。

  4. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value

  5. 每个key-value放入一个Entry对象中。Entry(存储特点:无序不重复)

    1. 可用Set来存放(按照Entry中key的哈希值决定存储位置)。(Map里定义了方法:Set entrySet()

      Map的实现类定义了内部类:EntrySet。然后方法Set entrySet()返回的是一个Values对象。

      Map接口本质上就是用数组存储一个个的Entry对象:底层存储为单列数据,却能保存双列数据

  6. 图示
    在这里插入图片描述

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类型。

涉及的面试题

  1. HashMap的底层实现原理?
  2. HashMap和Hashtable的异同?
  3. CurrentHashMap与Hashtable的异同?
    1. CurrentHashMap用于多线程操作HashMap的结构。

Map常用方法

  1. 添加、删除、修改操作:

    1. Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
    2. void putAll(Map m):将m中的所有key-value对存放到当前map中
    3. Object remove(Object key):移除指定key的key-value对,并返回value
    4. void clear():清空当前map中的所有数据
  2. 元素查询的操作

    1. Object get(Object key):获取指定key对应的value
    2. boolean containsKey(Object key):是否包含指定的key
    3. boolean containsValue(Object value):是否包含指定的value
    4. int size():返回map中key-value对的个数
    5. boolean isEmpty():判断当前map是否为空
    6. boolean equals(Object obj):判断当前map和参数对象obj是否相等
  3. 元视图操作的方法:

    1. Set keySet():返回所有key构成的Set集合
    2. Collection values():返回所有value构成的Collection集合
    3. 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)

  1. 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
  2. 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
  3. 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
  4. 一个key-value构成一个entry
  5. 所有的entry构成的集合是Set:无序的、不可重复的
  6. HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hash值也相等。
  7. HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。

HashMap源码中的重要常量

  1. DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
  2. MAXIMUM_CAPACITY HashMap的最大支持容量,2^30
  3. DEFAULT_LOAD_FACTOR:HashMap的默认加载因子
  4. **TREEIFY_THRESHOLD:**Bucket中链表长度大于该默认值,转化为红黑树
  5. UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表
  6. MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
  7. table:存储元素的数组,总是2的n次幂
  8. entrySet:存储具体元素的集
  9. size:HashMap中存储的键值对的数量
  10. modCount:HashMap扩容和结构改变的次数。
  11. threshold:扩容的临界值,= 容量*填充因子
  12. loadFactor:填充因子

HashMap的底层原理:JDK 1.8之前。以JDK7为例说明

  1. HashMap的内部存储结构其实是数组和链表的结合
    在这里插入图片描述

    1. 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。 (数组)
    2. 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。 (链表)
  2. 添加元素的过程:(体现数组+链表的存储结构)

    1. 向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。(数组)
    2. 如果位置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。

    (巧记:七上八下)

  3. HashMap的扩容

    1. 当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
  4. 那么HashMap什么时候进行扩容呢?

    1. 当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的性能。
  5. 源码展示

    1. 实例化:

      //实例化时,默认底层会创建一个长度为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();//方法里是空的
      }
      
    2. 添加数据

      //此方法用于添加数据
      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);
      }
      
    3. 存储单元: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

  1. HashMap的内部存储结构其实是数组+链表+红黑树的结合
    在这里插入图片描述

    1. 当实例化一个HashMap时,会初始化initialCapacityloadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacityNode数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。(数组)
    2. 每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。(链表)
    3. 也可能是一个个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。(树)
  2. 那么HashMap什么时候进行扩容和树形化呢?

    1. 当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的性能。
    2. 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
  3. 关于映射关系的key是否可以修改?answer:不要修改

    1. 映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改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对使用红黑树进行存储。
  5. 问题

    1. 为什么加载因子默认要设为0.75呢?

      目的:尽可能地让链表结构少出现。所以要让加载因子小一点,但是太小如0.3时数组的利用率太低,太大如0.9会导致链表结构出现得多些,开发人员经过某种统计确定用0.75。

      数组一定会装满吗?因为是根据hash值确定存储位置的,所以不确定。

      参考答案:

      1. 负载因子的大小决定了HashMap的数据密度。
      2. 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
      3. 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
      4. 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
  6. 源码展示

    1. 实例化

      //实例化时默认并没有立即创建数组,会在首次添加数据时创建一个长度为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);
      }
      
    2. 添加数据

      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);
      }
      
      
    3. 存储单元:

      1. 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;//差别在这,旧的元素指向新的元素
                }
            	...
        
      2. 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);
                }
        		...
        

面试题

  1. 谈谈你对HashMap中put/get方法的认识?如果了解再谈谈HashMap的扩容机制?默认大小是多少?什么是负载因子(或填充比)?什么是吞吐临界值(或阈值、threshold)?

    答:

  2. 负载因子值的大小,对HashMap有什么影响?

    答:

    1. 负载因子的大小决定了HashMap的数据密度。
    2. 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
    3. 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
    4. 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。

LinkedHashMap

简单介绍

  • LinkedHashMap 是 HashMap 的子类
  • 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
  • LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致(插入=添加)

源码展示

  1. 实例化

    //用父类的逻辑
    public LinkedHashMap() {
    	super();
    	accessOrder = false;
    }
    
  2. 没有重写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;
    }
    
  3. 那么元素的结构做了什么改变呢?

    1. HashMap中存储单元:Node

      static class Node<K,V> implements Map.Entry<K,V> {
          final int hash;
          final K key;
          V value;
          Node<K,V> next;
      }
      
    2. 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,对红黑树的讲解写得不错。

  • 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 是一个操作 SetListMap 等集合的工具类。since: JDK1.2。

  • Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

Collections 常用静态方法

  1. 查找、替换

    1. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
    2. Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
    3. Object min(Collection)
    4. Object min(Collection,Comparator)
    5. int frequency(Collection,Object):返回指定集合中指定元素的出现次数
    6. void copy(List dest,List src):将src中的内容复制到dest中
    7. boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List 对象的所有旧值
  2. 排序操作

    1. reverse(List):反转 List 中元素的顺序
    2. shuffle(List):对 List 集合元素进行随机排序
    3. sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
    4. sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
    5. swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
  3. 同步控制

    1. Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
      在这里插入图片描述
  4. 创建只读集合

    一系列方法创建的集合特点:只能读

    • <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);//抛异常
    

面试题

  1. 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); 
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值