Unit 11.1 集合详解

集合的使用

主要内容

  • 集合总体结构介绍
  • List 实现类
  • Set的实现类
  • Map实现类
  • Iterator
  • Collections

学习目标

知识点要求
集合总体结构介绍掌握
List实现类掌握
Set的实现类掌握
Map实现类掌握
Iterator掌握
Collections掌握

一. 集合介绍

1. 介绍

​ 集合又称容器。是 Java 中对数据结构(数据存储方式的具体实现。

​ 我们可以利用集合存放数据,也可以对集合进行新增、删除、修改、查看等操作。

集合中数据都是在内存中,当程序关闭或重启后集合中数据会丢失。所以集合是一种临时存储数据的容器。

2. JDK中集合结构图(常见面试题)

​ 集合作为一个容器,可以存储多个元素,但是由于数据结构的不同,java提供了多种集合类。将集合类中共性的功能,不断向上抽取,最终形成了集合体系结构。(虚线框是接口,实线是实现类

在这里插入图片描述

2.1 List接口和Set接口

List和Set的父接口

​ 1. List 接口:存储有序, 可重复数据。

​ 1. Vector:List 的实现类,底层为可变长度数组实现。所有方法都是同步操作(线程安全),每次扩容成本增长。新数组长度为原数组长度的2倍

​ 2. ArrayList:List 的实现类,底层为可变长度数组实现。所有方法都是非同步操作(非线程安全的),以1.5倍的方式在扩容。常用于: 查询较多的情况

​ 3. LinkedList:List 的实现类,双向非循环链表的实现。常用于: 删 增 较多的情况

Set接口:存储无序,不可重复数据。

​ 1. HashSet:Set 实现类,底层是 HashMap 散列表(数组+链表+(红黑树 jdk1.8及之后))。所有添加到 HashSet 中的元素实际存储到了 HashMap 的 key 中

​ 2. LinkedHashSet:HashSet 子类. 使用 LinkedHashMap 来存储它的元素,存储的值插入到LinkedHashMap 的可以 key 中, 底层实现(数组+链表+(红黑树 jdk1.8 及之后) + 链表), 可以记录插入的顺序

​ 3. TreeSet:Set 实现类,底层是 TreeMap(红黑树实现), 存入到 TreeSet 中的元素, 实际存储到了 TreeMap 中, 根据存储元素的大小可以进行排序

2.2 Map接口

​ Map:独立的接口。每个元素都包含 Key(名称)和 Value(要存储的值)两个值。

​ 1. HashMap:Map 实现类, 对散列表 (数组+链表+(红黑树Java8及之后))的具体实现,非同步操作(非线程安全的)。存储时以 Entry 类型存储(key, value)

​ 2. LinkedHashMap: HashMap 的子类,是基于 HashMap 和链表来实现的。在 hashMap 存储结构之上再添加链表, 链表只是为了保证顺序

​ 3. TreeMap:Map 实现类, 使用的不是散列表, 而是对红黑树的具体实现。根据 key 值的大小, 放入红黑树中, 可实现排序的功能(key 值大小的排序)

​ 4. HashTable:Map 实现类, 和 HashMap 数据结构一样,采用散列表(数组+链表+(红黑树 jdk1.8及之后))的方法实现, 对外提供的 public 函数几乎都是同步的(线程安全)。

常用的集合: ArrayList, HashMap, HashSet

二. Collection接口

**1. **介绍

​ List 和 Set 接口的父接口, 还有其他的实现类或子接口。

2. 继承关系

在这里插入图片描述

3. 包含的API

​ 泛型中<? extends E> 代表:只要是E类型或E类型的子类都可以。

在这里插入图片描述

三. List接口

**1. **介绍

​ Collection 接口的子接口。Collection 中包含的内容 List 接口中可以继承。

​ List 专门存储有序,可重复数据的接口。

2. 包含的API

在这里插入图片描述

四. ArrayList

**1. ** 介绍

​ 实现了 List 接口, 底层实现可变长度数组。存储有序、可重复数据, 有下标。

**2. ** 实例化

​ 常用向上转型进行实例化。绝大多数集合都支持泛型,如果不写泛型认为泛型是<Object>,使用集合时建议一定要指定泛型。

​ List<泛型类型> 对象 = new ArrayList<>( );

**3. ** 内存结构图

在这里插入图片描述

4. 常用API

public class TestArrayList {
  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    //按照下标添加
    list.add("aa");
    //添加到指定的位置
    list.add(1, "bb");
    //查看元素个数
    int size = list.size();
    //根据指定的下标修改
    list.set(0, "aaa");
    //判断
    int bb = list.indexOf("bb"); //元素存在, 返回元素的下标. 不存在, 返回-1
    boolean b = list.contains("bb"); //元素存在, 返回true. 不存在, 返回false
    //删除
    list.remove(0); //根据下标删除
    list.remove("bb"); //根据指定元素删除
    //更新
    list.set(1, 65); 
      
    //查询
    for (String s : list) {
      System.out.println(s);
    }
    for (int i = 0; i < size; i++) {
      System.out.println(list.get(i));
    }
  }
}

remove()方法强调

在这里插入图片描述

五. 泛型为集合类型

1. 介绍

​ 在集合中泛型都是任意引用类型。既然是任意引用类型,也可以是集合类型。(集合 List 的泛型仍是一个集合 List ,如下)

2. 实例化语法

//例如:集合中可以再保存集合
List<List<Integer>> list = new ArrayList<>();

3. 内存结构图

在这里插入图片描述

4. 代码示例

public class Test {
  public static void main(String[] args) {
    List<List<Integer>> list = new ArrayList<>();
    List<Integer> list1 = new ArrayList<>();
    list1.add(1);
    list1.add(2);
    list1.add(3);
    List<Integer> list2 = new ArrayList<>();
    list2.add(11);
    list2.add(22);
    list2.add(33);
    list.add(list1);
    list.add(list2);
    //因此遍历出集合中元素需要两层for循环
    for (List<Integer> integerList : list) {
      for (Integer i : integerList) {
        System.out.println(i);
      }
    }
  }
}

六. LinkedList

1. 介绍

​ LinkedList 是 Java 中对双向非循环链表的实现。实现了 List 接口。

​ 具有 ArrayList 所有常用方法,额外还添加了头尾操作方法(实现了 Deque 接口),这些方法在 List 接口中是不存在的,所以如果希望使用这些头尾操作方法,实例化时不用向上转型。

2. 实例化语法

LinkedList<泛型>  对象 = new LinkedList<>();

3. 常用API

​ ArrayList 里面常用 API 在 LinkedList 中都可以使用。

​ 下面的演示为 LinkedList 比 ArrayList 多的常用方法。

public class TestLinkedList {
  public static void main(String[] args) {
    LinkedList<String> linkedList = new LinkedList<>();
    linkedList.addFirst("aa");//头插
    linkedList.addLast("bb");//尾插

    //获取头结点 没有头结点 java.util.NoSuchElementException
    String first = linkedList.getFirst();
    System.out.println(first);
    //获取尾结点 没有尾结点 java.util.NoSuchElementException
    String last = linkedList.getLast();
    System.out.println(last);

    //获取头结点 没有头结点 返回 null
    String s = linkedList.peekFirst();//不会报异常了,直接返回null
    System.out.println(s);
    //获取尾结点 没有尾结点 放回 null
    String s1 = linkedList.peekLast();
    System.out.println(s1);

    //删除头结点 没有头结点 java.util.NoSuchElementException
    String s2 = linkedList.removeFirst();
    System.out.println(s2); //被删除结点中的值
    //删除尾结点 没有尾结点 java.util.NoSuchElementException
    String s3 = linkedList.removeLast();
    System.out.println(s3);//被删除结点中的值
    
    //删除头结点 没有头结点 返回 null
    String s4 = linkedList.pollFirst();//不会报异常了,直接返回null
    System.out.println(s4);
    //删除尾结点 没有尾结点 放回 null
    String s5 = linkedList.pollLast();
    System.out.println(s5);

  }
}

**问题1:**将 ArrayList 替换成 LinkedList 之后,变化的是什么?

底层的结构变了

ArrayList:数组 LinkedList:双向非循环链表

问题2:到底是使用 ArrayList 还是 LinkedList

根据使用场合而定

大量的根据索引查询的操作,大量的遍历操作(按照索引0–n-1逐个查询一般),建议使用 ArrayList

如果存在较多的添加、删除操作,建议使用 LinkedList

问题3:LinkedList 增加了哪些方法

增加了对添加、删除、获取首尾元素的方法

addFirst( )、addLast( )、removeFirst( )、removeLast( )、getFirst( )、getLast( )

七、Java中栈和队列的实现类

Vector过时了,被ArrayList替代了,Stack也就过时了

public class Stack<E> extends Vector<E>  

Deque和Queue的实现类,用的非常少了解即可
1.ArrayDeque 顺序栈 数组
2.LinkedList 链栈 链表

 public interface Queue<E> extends Collection<E>
 public interface Deque<E> extends Queue<E>

1. 早期的栈结构实现类 Stack

public class Test1 {
    public static void main(String[] args) {
        Stack<String> stack =new Stack<String>();
        // 入栈方法
        stack.push("马云");
        stack.push("马化腾");
        stack.push("马明哲");
        stack.push("马老师");
        // 跳栈 弹栈 取出栈顶元素
        String pop = stack.pop();
        System.out.println(pop);
        System.out.println(stack);
    }
}

2.Queue单端队列

public class Test2 {
    public static void main(String[] args) {
        Queue<String> q=new LinkedList<String>();
        // 入队方法
        q.offer("张三丰");
        q.offer("张翠山");
        q.offer("张无忌");

        System.out.println(q);

        // 出队方法 取出队首
        String poll = q.poll();
        System.out.println(poll);
        System.out.println(q);

    }
}

3.Deque双端队列

public class TestLinkedList2 {
    public static void main(String[] args) { 
        //摞盘子
        Deque<String> deque1 = new LinkedList<String>();
        deque1.offerFirst("盘子1");//队首存值
        deque1.offerLast("盘子2");//队尾存值
        deque1.pollFirst();//队首取值
        deque1.pollLast();//队尾取值
        
        System.out.println(deque1.size());
  
    }
}

八. Set接口

**1. **介绍

​ Set 继承了 Collection 接口。继承的都是 Collection 中的方法, 没有提供额外方法。

​ Set 经常称为实现无序, 不重复数据集合, 指的就是 HashSet 实现类

2. 包含API

在这里插入图片描述

九. HashSet

1. 介绍

完全基于 HashMap (数组 + 链表 + (红黑树)) 实现的。

​ 存储无序, 无下标, 元素不重复数据

​ 无序:存入和取出的顺序没有关系,一般就不一致,也就是存入的顺序没有被维持

​ 不重复:存入的重复值直接被覆盖

2. 代码示例

public class TestHashSet {
  public static void main(String[] args) {
    Set<Integer> set = new HashSet<>();
    //添加元素
    set.add(1);
    set.add(2);
    //元素个数
    int size = set.size();
    //是否包含指定的元素
    boolean contains = set.contains(1);
    ArrayList<Integer> list = new ArrayList<>();
    list.add(3);
    list.add(4);
    list.add(4);
    //将其它集合中的元素添加到set集合中
    set.addAll(list);
    //删除指定的元素
    set.remove(2);
    //查询元素
    for (Integer integer : set) {
      System.out.println(integer);
    }
  }
}

十. TreeSet

**1. **介绍

​ 底层是基于 TreeMap 红黑树。

public class TestTreeSet {
  public static void main(String[] args) {
    Set<Integer> treeSet = new TreeSet<>();
    treeSet.add(2);
    treeSet.add(1);
    treeSet.add(3);
    //查询元素
    for (Integer integer : treeSet) {
      System.out.println(integer);
    }
  }
}

2.使用Set集合分别存储学生对象

public class TestSet2 {
    public static void main(String[] args) {
        //创建一个集合set对象
        //Set<Student> set = new TreeSet<Student>();
        //Set<Student> set = new HashSet<Student>();
        Set<Student> set = new LinkedHashSet<Student>();
        //添加多个学生
        Student stu2 = new Student(2, "lisi", 23, 98);
        Student stu3 = new Student(3, "wangwu", 22, 87);
        Student stu1 = new Student(1, "zhangsan", 23, 90);
        Student stu4 = new Student(1, "zhangsan", 23, 90);
        set.add(stu1);
        set.add(stu2);
        set.add(stu3);
        set.add(stu4);        
        //输出学生
        System.out.println(set.size());
        System.out.println(set);
    }
}

问题1:HashSet、LinkedHashSet :为什么 String 有重复,会保持唯一;为什么 Student 有重复,不会保持唯一。

解答1:HashSet、LinkedHashSet 需要 Student 实现 hashCode( ) 和 equals( )

问题2:TreeSet 为什么 String 可以添加,而 Student 就不让添加到 TreeSet 中呢? 而是抛出异常:

java.lang.ClassCastException: com.bjsxt.entity.Student cannot be cast to java.lang.Comparable

思考:String 是系统类,Student 是自定义类,应该是 String 已经做了某些事情,但是 Student 没有做

解答2:TreeSet 需要 Student 实现 Comparable 接口并指定比较的规则

在这里更加清晰认识到接口是一种规范,接口里面可以没有什么东西,但是它定义了一种默认的功能,想要实现这个功能就要通过实现这个接口,再自定义相关的功能,如此代码才能认识这个功能。

3.重写Student的equals()和hashCode()方法

public class Student implements Comparable<Student>{
    private int sno;
    private String name;
    private int age;
    private double score; 
    @Override
    public int compareTo(Student o) {
        //return this.sno - o.sno;
        //return o.sno - this.sno;
        return -(this.sno - o.sno);
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        if (sno != student.sno) return false;
        if (age != student.age) return false;
        if (Double.compare(student.score, score) != 0) return false;
        return name != null ? name.equals(student.name) : 
student.name == null;
    }
    @Override
    public int hashCode() {
        int result;
        long temp;
        result = sno;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + age;
        temp = Double.doubleToLongBits(score);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }
}

4.比较器Comparator的作用和使用

import java.util.Objects;

public class Student implements Comparable<Student> {

     private  int  age;
     private  String name;
     private  String sex;

    public Student(int age, String name, String sex) {
        this.age = age;
        this.name = name;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        int a= this.age-o.age;
        return a;
    }
}

十一. Map接口

**1. **接口

Map 是独立的接口,和 Collection 没有关系。

​ Map 中每个元素都是 Entry 类型,每个元素都包含 Key(键)和 Value (值)

相当于 key 是钥匙——value 是物品,有了锁才可以拿到物品,锁必须是唯一的,但箱子里的物品可以不唯一。

2. 继承关系

在这里插入图片描述

3. 包含的API

在这里插入图片描述

十二. HashMap

**1. **介绍

​ HashMap 是对散列表的具体实现。

底层是 Hash 表

​ 里面都包含 Key-Value 值。

在这里插入图片描述

2. 代码演示

public class TestHashMap {
  public static void main(String[] args) {
    Map<String, Integer> map = new HashMap<>();

    //向集合中添加元素  key  value 进行添加  添加方法 put
    //如果key相同了,后者的value会把前者相同key的value覆盖掉
    map.put("aa", 11);
    map.put("bb", 22);
    map.put("cc", 33);
    map.put("cc", 33);
    map.put(null, 44);//Tree中不允许key出现空对象的情况

    //获取存储键值个数
    System.out.println(map.size());

    //是否包含指定的key
    System.out.println(map.containsKey("cc"));
    System.out.println(map.containsKey("dd"));

    //根据key获取value
    System.out.println(map.get("cc"));

    //根据key  删除键值对  返回被删除的值
    Integer cc = map.remove("cc");
    System.out.println(cc);

    //遍历
    /*
         * 键遍历
         * 值遍历
         * 键值遍历
         * */
    //键遍历  获取所有的键
    Set<String> strings = map.keySet();
    for (String key : strings) {
      System.out.println("key-> " + key + " value-> " + map.get(key));
    }

    //值遍历
    Collection<Integer> values = map.values();
    for (Integer value : values) {
      System.out.println(value);
    }

    //键值遍历
    Set<Map.Entry<String, Integer>> entries = map.entrySet();
    for (Map.Entry<String, Integer> entry : entries) {
      System.out.println(entry.getKey() + "- " + entry.getValue());
    }
  }
}

十三. TreeMap

1. 简介

​ 红黑树的具体实现。

2. 代码示例

​ 总体和 HashMap 使用非常类型

public class TestTreeMap {
  public static void main(String[] args) {
    Map<Integer, String> treeMap = new TreeMap<>();
    treeMap.put(2, "bb");
    treeMap.put(1, "aa");
    treeMap.put(3, "cc");
    for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
      System.out.println(entry.getKey() + ":" + entry.getValue());
    }
  }
}

十四. Iterator

1. 简介

​ 中文名称:迭代器。是一个接口,每个集合中实现类都对 Iterator 提供了内部类的实现。

​ 通过 Iterator 可以实现遍历集合的效果。

​ 存在意义:

​ 隐藏集合实现细节,无论是哪种集合都是通过 Iterator 进行操作,而不是直接操作集合。通过一套 API 实现所有集合的遍历。

​ 可以在遍历时删除集合中的值。

Iterator 源码:

在这里插入图片描述

2. 实例化

​ 每个实现类都提供了.iterator( );返回值就是迭代器对象。

public class TestIterator {
  public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    //获取集合的迭代器
    Iterator<Integer> iterator = list.iterator();
    //.hasNext() 判断是否有下一个元素
    while (iterator.hasNext()) {
      //获取下一个元素
      Integer next = iterator.next();
      System.out.println(next);
    }
  }
}

3. ConcurrentModificationException

​ 在循环遍历集合时,向集合中插入值或删除集合中值时会出现这个异常。

public class Test {
  public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    for (Integer integer : list) {
      list.remove(integer);
    }
  }
}

​ 异常截图:

在这里插入图片描述

为什么使用for(int i =0;i<list.size();i++){}时不出现这个异常

​ 因为这种循环其实是多次执行 get,调用 get( ) 方法时,集合其他元素删除或新增是没有要求的。而增强for循环是把集合看做一个整体,在遍历集合时,不允许对整个集合进行操作。

4. 遍历集合时删除元素内容

public class Test {
  public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    //获取集合的迭代器
    Iterator<Integer> iterator = list.iterator();
    //.hasNext() 判断是否有下一个元素
    while (iterator.hasNext()) {
      //获取下一个元素
      Integer next = iterator.next();
      //删除当前元素
      iterator.remove();
    }
  }
}

总结:

  1. Iterator 专门为遍历集合而生,集合并没有提供专门的遍历的方法

​ Iterator 实际上迭代器设计模式的实现

  1. 哪些集合可以使用 Iterator 遍历

    层次1:Collection、List、Set可以、Map不可以

    层次2:提供 iterator( ) 方法的就可以将元素交给 Iterator;

​ 层次3:实现 Iterable 接口的集合类都可以使用迭代器遍历

  1. for-each 循环和 Iterator 的联系

​ for-each 循环(遍历集合)时,底层使用的是 Iterator

  1. for-each 循环和 Iterator 的区别

​ for-each 还能遍历数组,Iterator 只能遍历集合

​ 使用 for-each 遍历集合时不能删除元素,会抛出异常 ConcurrentModificationException 使用 Iterator 遍历合时能删除元素

5.ListIterator

ListIterator 和 Iterator 的关系

  1. ​ public interface ListIterator<E> extends Iterator<E>
  2. ​ 都可以遍历 List

ListIterator 和 Iterator 的区别

  1. ​ 使用范围不同
  2. ​ Iterator 可以应用于更多的集合,Set、List 和这些集合的子类型。
  3. ​ ListIterator 只能用于 List 及其子类型。
public class TestListIterator {
    public static void main(String[] args) {
        //创建一个集合对象
        List<Integer> list = new ArrayList<Integer>();
        //向集合中添加分数
        list.add(78);
        list.add(80);
        list.add(89);        
        ListIterator<Integer> lit = list.listIterator();        
        while(lit.hasNext()){
            lit.next();
        }        
        while(lit.hasPrevious()){
            int elem = lit.previous();
            System.out.println(elem +"  "+lit.nextIndex()+"  "+lit.previousIndex());
        }
    }
}

十五. Collections

1. 介绍

​ Collections 是一个工具类型,一个专门操作集合的工具类

​ Collection 是集合接口

2. 代码示例

public class TestCollections {
    public static void main(String[] args) {
        //添加元素
        List<Integer> list = new ArrayList();
        Collections.addAll(list, 10, 50, 30, 90, 85, 100);//6
        System.out.println(list);
        //排序
        Collections.sort(list);//默认按照内部比较器
        System.out.println(list);
        //查找元素(元素必须有序)
        int index = Collections.binarySearch(list, 500);//不存在返回负数
        System.out.println(index);
        //获取最大值和最小值
        int max = Collections.max(list);
        int min = Collections.min(list);
        System.out.println(max + "   " + min);
        //填充集合
        Collections.fill(list, null);
        System.out.println(list);
        //复制集合
        List list2 = new ArrayList();
        Collections.addAll(list2, 10, 20, 30, 50);
        System.out.println(list2);
        Collections.copy(list, list2);//dest.size >= src.size  目标列表的长度至少必须等于源列表。
        System.out.println(list);
        //同步集合
        //StringBuffer 线程安全效率低 StringBuilder 线程不安全,效率高
        //Vector 线程安全  效率低  ArrayList 线程不安全,效率高
        //难道是要性能不要安全吗,肯定不是。
        //在没有线程安全要求的情况下可以使用ArrayList
        //如果遇到了线程安全的情况怎么办
        //方法1:程序员手动的将不安全的变成安全的   
        //方法2:提供最新的线程安全并且性能高的集合类
        List list3 = new ArrayList();
        Collections.addAll(list3, 10, 90, 30, 40, 50, 23);
        System.out.println(list3);
        //将list3转换成线程安全的集合类
        list3 = Collections.synchronizedList(list3);
        //下面再操作,就线程安全了
    }
}

十六. 综合实战案例 - 简易电话本

​ 要求:

​ 1. 所有控制台输入、输出代码只能出现在 SystemService 的实现类中。

​ 2. 每个人必须输入两个电话号码,并且不可以重复。

​ 3. 查看电话本、录入联系人、删除联系人、修改联系人手机号码等所有功能都是联系人的相关功能。

​ 包含的功能:

​ 1. 查看电话本功能

​ a) 先查看所有的联系人

​ b) 选择具体的联系人, 再查看其电话号码

​ 2. 录入联系人功能

​ 3. 删除联系人功能

源码分析

一、哈希表

1. 引入Hash表

在这里插入图片描述

2. 哈希表的结构和特点

在这里插入图片描述

3. 哈希表是如何添加数据的

在这里插入图片描述

在这里插入图片描述

4. 哈希表更多

4.1 如何查询数据

在这里插入图片描述

4.2 HashCode和equals到底有什么作用

在这里插入图片描述

4.3 各种类型数据的哈希码值该如何获取 HashCode()

不管怎么计算,都是为了减少碰撞

在这里插入图片描述
在这里插入图片描述

5. 装填因此/加载因子

在这里插入图片描述

二、HashMap底层源码分析(JDK1.7及以前)

1. 结构简介

在这里插入图片描述

2. 内部成员变量含义

3. put() 方法

4. addEntry() 方法

void addEntry(int hash, K key, V value, int bucketIndxe){
    //如果达到了阈值,容量扩充为原来容量的2倍 16--32
    if((size >= threshold) && (null != table[bucketIndex])){
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    //添加结点
    createEntry(hash, key, value, bucketIndex);
}

5. get() 方法

public V get(Object key){
    //根据key找到Entry(Entry中有key和value)
    Entry<K, V> entry = getEntry(key);
    //如果entry == null,返回null,否则返回value
    return null == entry ? null : entry.getValue();
}

三、HashMap底层源码分析(JDK1.8及以后)

在这里插入图片描述

1. 基本属性

public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable{
    //序列化和反序列化时使用相同的id
    private static final long serialVersionUID = 362498820763181265L;
    
    //初始化容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
    //默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    //树形阈值
    static final int TREEIFY_THRESHOLD = 8;
    
    //取消阈值
    static final int UNTREEIFY_THRESHOLD = 6;
    
    //最小树形容量
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    //节点
    transient Node<K,V>[] table;
    
    //存储键值对的个数
    transient int size;
    
    //散列表被修改的次数
    transient int modCount;
    
    //扩容临界值
    int threshold;
    
    //负载因子
    final float loadFactor;
    
    
}

2. 构造方法

//无参构造器,加载因子默认为0.75
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

//指定容量大小的构造器,但调用了双参数的构造器,加载因子0.75
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);
    //HashMap的最大容量只能是MAXIMUM_CAPACITY,哪怕传入的数值大于最大值,也按照最大容量赋值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //加载因子必须大于0
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
    this.loadFactor = loadFactor;
    
    this.threshold = tableSizeFor(initialCapacity);
    }

//将传入的子Map中的全部元素逐个添加到HashMap中
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

3. Node结点

1.7及以前是 Entry 节点,1.8及以后是 Node 节点,其实相差不大,因为都是 实现了 MapEntry(Map接口中的Entry接口)接口,即:实现了getKey( ) getValue( ) equals(Object o) hashCode( )等方法

static class Node<K,V> implements Map.Entry<K,V> {
    //hash值
    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;
     }
	
    //返回与此项对应的键
    public final K getKey()     { return key; }
    //返回与此项对应的值
    public final V getValue()    { return value; }
    public final String toString() { return key + "=" + value; }

    //hash值
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    //判断2个Entry是否相等,必须key和value都相等,才返回true
    public final boolean equals(Object o) {
        if (o == this)
            return true;
         
         return o instanceof Map.Entry<?, ?> e
           && Objects.equals(key, e.getKey())
           && Objects.equals(value, e.getValue());
    }
}

4. 添加键值对

4.1 put() 方法
//添加键值对
public V put(K key, V value) {
    /*
    参数一:调用hash()方法
    参数二:键
    参数三:值
    */
    return putVal(hash(key), key, value, false, true);
}
4.2 hash() 方法
static final int hash(Object key) {
    int h;
    //hashCode和h移位 右移16位进行按位异或运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
4.3 putVal() 方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    //声明tab和p用于操作原数组和节点
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果原数组为空或者原数组长度为0,那么通过resize()方法进行创建初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        //获取到创建后数组的长度n
        n = (tab = resize()).length;
    
    //通过key的hash值和数组长度-1 计算出存储元素节点的数组中位置
    //并且,如果该位置为空时,则直接创建元素节点赋值给该位置,后继元素节点为null
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        //否则,说明该位置存在元素
        Node<K,V> e; K k;
        //判断table[i]的元素的key是否与添加的key相同,若相同则直接用新value覆盖旧value
    	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 {
            //遍历table[i],并判断添加的key是否已经存在,和之前判断一样,hash和equals
            //遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据
        	for (int binCount = 0; ; ++binCount) {
                //如果遍历的下一个节点为空,那么直接插入
                //该插入方式是尾插法,与1.7不同
                //将p的next赋值给e进行以下判断
            	if ((e = p.next) == null) {//后面没有了
                    //直接创建新节点连接在上一个节点的后继上
                	p.next = newNode(hash, key, value, null);
                    //如果插入节点后,链表的节点数大于等于7时,则进行红黑树的转换
                    //注意不仅仅是链表大于8,并且会在treeifyBin方法中判断数组是否为空或数组长度是否小于64
                    //如果小于64则进行扩容,并且不是直接转换为红黑树
                	if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    	treeifyBin(tab, hash);
                    //完成后直接退出循环
                	break;
             	}
                //不退出循环时,则判断两个元素的key是否相同
                //若相同,则直接退出循环,进行下面替换的操作
             	if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                 	break;
                //否则,让p只想下一个元素节点
             	p = e;
         	}
    	}
        //接着上面的第二个break,如果e不为空,直接用新的value覆盖旧的value并且返回旧的value
    	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;
}
4.4 resize() 方法
//该函数有两种使用情况:初始化哈希值或前数组容量过小,需要扩容
final Node<K,V>[] resize() {
    //获取原数组
    Node<K,V>[] oldTab = table;
    //获取原数组的容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //获取原扩容阈值
    int oldThr = threshold;
    //新的容量和阈值目前都为0
    int newCap, newThr = 0;
    if (oldCap > 0) {
        //如果原数组容量大于等于最大容量,那么不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
         //若没有超过最大容量,那么扩容为原来的2倍
         }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            //扩容为原来的2倍
            newThr = oldThr << 1; // double threshold
    //经过上面的if,那么这步为初始化容量(使用有参构造器的初始化)
	}else if (oldThr > 0) // initial capacity was placed in threshold
         newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        //否则,使用的无参构造器
        //那么,容量为16,阈值为12(16*0.75)
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //计算新的resize上限
    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
    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)
                    //如果无链表,确定元素存放的位置,扩容前元素的位置为(oldCap-1)&e.hash,所以这
                    //里的新位置只有两种可能:1.位置不变 2.变为原来位置+oldCap,下面会详细介绍
                    newTab[e.hash & (newCap - 1)] = e;
                //判断是否是树节点,如果是则执行树操作
                else if (e instanceof TreeNode)
                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    //否则说明该元素上存在链表,那么进行元素的移动
                    //根据变化的最高位的不同,也就是0或1,将链表拆分开
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        //最高位为0时,则将节点加入loTail.next
                        if ((e.hash & oldCap) == 0) {
                        	if (loTail == null)
                              	loHead = e;
                             else
                                loTail.next = e;
                             loTail = e;
                        //最高位为1则将节点加入hiTail.next
                        }else {
                             if (hiTail == null)
                                hiHead = e;
                             else
                                hiTail.next = e;
                             hiTail = e;
                        }
                     } while ((e = next) != null);
                    //通过loHead hiHead来保存链表的头结点,然后将两个头结点放到newTab[j] 与newTab[j+oldCap]上面去
                    if (loTail != null) {
                       loTail.next = null;
                       newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                       hiTail.next = null;
                       newTab[j + oldCap] = hiHead;
                    }
                }
             }
          }
     }
     return newTab;
}

5. 问题

问题一:

存储在 Node 中的 Hash 值,是否就是 key 的 hashCode( )?

static final int hash(Object key){
    int h;
    //hashCode和右移16进行按位异或运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >> 16);
}

答案:不是,存储的是对 Key 先做 hashCode( ) 计算,然后再无符号右移16,再按位异或。

问题二:

如何知道一个节点到底存储在Hash表(散列表)的哪个位置?

答案:根据 key 计算相关的 hash 值(并不是简单的 hashCode( )),(数组长度-1)& hash 进行计算得出具体的下标,如果下标只有这一个节点,直接返回,非一个节点,继续在链表或红黑树中查找。

问题三:

什么时候需要把链表转化为红黑树?

答案:链表的节点数大于8(从0开始,所以判断条件是 >=7),数组的长度必须大于等于64,那么就会进行数组的扩容。

问题四:

什么时候扩容?

答案:

​ 情况一:HashMap 的 Size 达到 Hash 中数组长度*loadFactor(扩容因子)时扩容。即比 threshold 大,进行扩容。每次都扩容为原数组的一倍。

​ 情况二:Hash 表中某个链表长度达到8,且 Hash 表中数组的长度小于64.

问题五:

Hash表中数组最大长度是多少?

答案:最大长度为 1<<30,即2的30次方

计算操作时,发现 Hash 表中数组长度为2的倍数效率最高,需要一直保持长度为2的倍数。数组长度最大值取为2的31次方减一。所以里面最大的2的倍数为2的30次方。

问题六:

1.Hash表中使用的是单向链表还是双向链表?

答案:单向链表。

2.数组扩容时,链表使用的是尾加还是头加?

答案:尾加。

JDK1.8 及以后用的是尾插法,JDK1.7 及以前使用的头插法

问题七:

链表转化为红黑树时,数组中是所有的链表都转化为红黑树,还是什么情况?

答案:只有数组里某个下标的节点个数 >8 ,并且数组长度 >64,该下标中的链表转换为红黑树。

问题八:

为什么Java8中长度超过8以后将链表变为红黑树?

答案:红黑树的查询效率高于链表。

问题九:

为什么选择8作为转换值?

答案:元素个数为8的红黑树中,高度为4,最多查找4次就能找到需要的值,长度为8的链表,最多找7次。

例如长度为4就转换,红黑树高度为3,最多找3次,链表最多找3次。

例如长度为7就转换,红黑树高度为3,最多找3次,链表最多6次,多找3次和转换的性能消耗相比不值得。

从源码上可以看出,在理想状态下,受随机分布的 hashCode 影响。链表中的节点遵循泊松分布,而且根据统计,链表中节点数是8的概率已经接近千分之一,而且此时链表的性能已经很差了,所以在这种罕见和极端的情况下,才会把链表转变为红黑树。

6. 总结HashMap底层原理(常见面试题)

从 Java8 开始 HashMap 底层由数组+链表变成数组+链表+红黑树。

使用 HashMap 时,当使用无参构造器实例化时,设置扩容因子默认为 0.75.

当向 HashMap 添加内容时,会对 Key 做 Hash 计算,把得到的 Hash 值和数组长度-1按位与,计算出存储的位置。

如果数组中没有内容,直接存入数组中(Node 节点对象),该下标中有 Node 对象了,把内容添加到对应的链表或红黑树中。

如果添加后链表长度大于等于8,会判断数组长度是否大于64,如果小于64,对数组扩容,扩容长度为原长度的2倍。扩容后把原 Hash 表内容重新放入新的 Hash 表中。如果 Hash 长度大于等于64会把链表转化为红黑树。

最终判断 HashMap 中元素个数是否已经达到扩容阈值(threshold),如果达到扩容值,需要扩容,扩容一倍。反之,如果删除元素后,红黑树的元素个数小于6,由红黑树转换为链表。

四、TreeMap底层原理

1. 介绍

在这里插入图片描述

2. 基本属性

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    @SuppressWarnings("serial") // Conditionally serializable
    //外部比较器,是自然排序,还是定制排序,使用final修饰,表明一旦赋值就不可更改
    private final Comparator<? super K> comparator;
	//红黑树的根节点
    private transient Entry<K,V> root;
	//TreeMap中存放的键值对的数量
    private transient int size = 0;
	//修改的次数
    private transient int modCount = 0;

   

3. 节点

在这里插入图片描述

static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;//键
        V value;//值
        Entry<K,V> left;//左孩子节点
        Entry<K,V> right;//右孩子节点
        Entry<K,V> parent;//父节点
        boolean color = BLACK;//节点的颜色,在红黑树中,只有两种颜色,红色和黑色
    	//省略有参构造 无参构造 equals() 和 hashCode() getter 和 setter
}

4. 构造器

//构造方法,comparator比较器
public TreeMap() {
    comparator = null;
}
//构造方法,提供比较器,用指定比较器排序
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

5. 添加键值

5.1 put()方法
private V put(K key, V value, boolean replaceOld) {
    Entry<K,V> t = root;
    if (t == null) {
        addEntryToEmptyMap(key, value);
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else {
                V oldValue = t.value;
                if (replaceOld || oldValue == null) {
                    t.value = value;
                 }
                 return oldValue;
            }
         } while (t != null);
    } else {
        Objects.requireNonNull(key);
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else {
                V oldValue = t.value;
                if (replaceOld || oldValue == null) {
                    t.value = value;
                }
                return oldValue;
            }
         } while (t != null);
    }
    addEntry(key, value, parent, cmp < 0);
    return null;
}
红黑树原理

在这里插入图片描述

5.2 Comparator()默认比较器
5.3 fixAfterInsertion( )方法

6. 总结

五、TreeSet 和 HashSet

TreeSet 和 HashSet底层是TreeMap和HashMap

把Set的值当作Map的Key,Map中Value存储new Object( )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值