JavaSE面试篇章——一文干破Java集合

文章目录

  • Java集合——一文干破集合
    • 一、集合的理解和好处
      • 1.1 数组
      • 1.2 集合
    • 二、集合的框架体系
    • 三、Collection接口和常用方法
      • 3.1 Collection接口实现类的特点
      • 3.2 Collection接口遍历元素方式1-使用Iterator(迭代器)
        • 3.2.1 基本介绍
        • 3.2.2 迭代器的执行原理
        • 3.2.3 Iterator接口的方法
      • 3.3 Collection接口遍历对象方式2-for循环增强(即for-each循环)
        • 3.3.1 基本介绍
        • 3.3.2 案例演示
        • 3.3.3 Debug查看源码 - 看底层是否是迭代器
      • 3.4 练习题
    • 四、List接口和常用方法
      • 4.1 List接口基本介绍
      • 4.2 List接口的常用方法
      • 4.3 List接口课堂练习1
      • 4.4 List【ArrayList、LinkedList、Vector】的三种遍历方式
      • 4.5 List实现类的课堂练习2
      • 4.6 ArrayList底层结构和源码分析⭐
        • 4.6.1 ArrayList的注意事项
        • 4.6.2 ArrayList的底层操作机制源码分析(重点、难点)❤️
          • 4.6.2.1 **无参构造器**
          • 4.6.2.2 有参构造器
      • 4.7 Vector底层结构和源码剖析⭐
        • 4.7.1 Vector的基本介绍
        • 4.7.2 Vector的底层操作机制源码分析(重点、难点)❤️
        • 4.7.3 Vector和ArrayList的比较
      • 4.8 LinkedList底层结构和源码剖析⭐
        • 4.8.1 LinkedList的全面说明
        • 4.8.2 LinkedList的底层操作机制
        • 4.8.3 LinkedList的底层操作机制源码分析(重点、难点)❤️
        • 4.8.4 LinkedList和ArrayList比较
    • 五、Set接口和常用方法
      • 5.1 Set接口基本介绍
      • 5.2 Set接口的常用方法
      • 5.3 Set接口的遍历方式
      • 5.4 HashSet底层结构和源码剖析⭐
        • 5.4.1 HashSet的全面说明
        • 5.4.2 HashSet案例说明 - 引出问题
        • 5.4.3 HashSet的底层操作机制源码分析(重点、难点)❤️❤️❤️
          • 5.4.3.1 模拟一个简单的数组+链表结构
          • 5.4.3.2 HashSet源码分析(add添加方法)⭐
          • 5.4.3.3 HashSet源码分析(扩容和转成红黑树机制)⭐
        • 5.4.4 HashSet课堂练习
          • 5.4.4.1 题一
          • 5.5.4.2 题二
      • 5.5 LinkedHashSet底层结构和源码剖析⭐
        • 5.5.1 LinkedHashSet的全面说明
        • 5.5.2 LinkedHashSet底层机制说明
        • 5.5.3 LinkedHashSet练习题
      • 5.6 Set接口实现类-TreeSet
    • 六、Map接口和常用方法
      • 6.1 Map接口实现类的特点
      • 6.2 Map接口常用方法
      • 6.3 Map接口遍历方法
      • 6.4 Map接口课堂练习
      • 6.5 Map接口实现类-HashMap
        • 6.5.1 HashMap小结
        • 6.5.2 HashMap底层机制及源码剖析
      • 6.6 Map接口实现类-Hashtable
        • 6.6.1 Hashtable的基本介绍
        • 6.6.2 Hashtable和HashMap对比
      • 6.7 Map接口实现类-Properties
        • 6.7.1 基本介绍
        • 6.7.2 基本使用
      • 6.8 Map接口实现类-TreeMap
    • 七、总结-开发中如何选择集合实现类(记住)
    • 八、Collections工具类
      • 8.1 Collections工具类介绍
      • 8.2 Collections各种方法演示

Java集合——一文干破集合

一、集合的理解和好处

1.1 数组

  1. 长度开始时必须指定,而且一旦指定,不能更改
  2. 保存的必须为同一类型的元素
  3. 使用数组进行增加/删除元素的示意代码 - 麻烦
//写出Person数组扩容示意代码
Person[] pers = new Person[1]; //大小是1
pers[0] = new Person();//增加新的Person对象 (需要扩容)
Person[] pers2 = new Person[pers.length + 1]; //新创建数组
for() {} //拷贝pers数组的元素到pers2
pers2[pers2.length - 1] = new Person(); //添加新对象

1.2 集合

  1. 可以动态保存任意多个对象,使用比较方便
  2. 提供了一系列方便的操作对象的方法:add、remove、set、get
  3. 使用集合添加、删除新元素的示意代码 - 简洁

二、集合的框架体系

  • Java的集合类很多,主要分为两大类【重要,背】

img

img

package com.zanedu.collection_;import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;public class Collection_ {@SuppressWarnings({"all"})public static void main(String[] args) {
//        Collection
//        Map/*解读1. 集合主要是两组(单列集合、双列集合)2. Collection 接口有两个重要的子接口 List Set,他们的实现子类都是单列集合(单个单个的元素)3. Map 接口的实现子类 是双列集合(存放的就是K-V)即key和value*///单列集合,放单列数据就是单列集合ArrayList arrayList = new ArrayList();arrayList.add("jack");arrayList.add("tom");//双列集合,放双列数据就是双列集合HashMap hashMap = new HashMap();hashMap.put("NO1", "北京");hashMap.put("NO2", "上海");}
}

三、Collection接口和常用方法

3.1 Collection接口实现类的特点

public interface Collection<E> extends Iterable<E>

  1. collection实现子类可以存放多个元素,并且每个元素都可以是Object
  2. 有些Collection的实现类,有些可以存放重复的元素,有些不可以
  3. 有些Collection的是实现类,有些是有序的(List),有些不是有序的(Set)
  4. Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现的
  • Collection接口常用方法,以实现子类ArrayList来演示

img

package com.hspedu.collection_;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class CollectionMethod_ {@SuppressWarnings({"all"})public static void main(String[] args) {List list = new ArrayList();
//        add:添加单个元素list.add("jack");list.add(10);//list.add(new Integer(10)); //因此他是对象了list.add(true);System.out.println(list);
//        remove:删除指定元素
//        list.remove(0);//删除第一个元素 //返回booleanlist.remove(true);//指定删除某个元素System.out.println(list);
//        contains:查找元素是否存在System.out.println(list.contains("jack"));//true
//        size:获取元素个数System.out.println(list.size());//2
//        isEmpty:判断是否为空System.out.println(list.isEmpty());//false
//        clear:清空
//        list.clear();
//        System.out.println(list);
//        addAll:添加多个元素ArrayList list2 = new ArrayList();list2.add("红楼梦");list2.add("三国演义");list.addAll(list2);System.out.println(list);
//        containsAll:查找多个元素是否都存在System.out.println(list.containsAll(list2));//true
//        removeAll:删除多个元素list.add("聊斋");list.removeAll(list2);System.out.println(list);
//        说明:以ArrayList实现类来演示.}
}

img

3.2 Collection接口遍历元素方式1-使用Iterator(迭代器)

3.2.1 基本介绍

img

  1. Iterator对象称为迭代器,主要用于遍历 Collection 集合中的元素
  2. 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器
  3. Iterator仅用于遍历集合,Iterator本身并不存放对象
3.2.2 迭代器的执行原理
Iterator iterator = coll.iterator(); //得到一个集合的迭代器// hasNext(); 判断是否还有下一个元素
while (iterator.hasNext()) {// next()作用:1. 下移 	2.将下移以后集合位置上的元素返回System.out.println(iterator.next());
}

img

  • 解读:每next一次,就会向下移动一次
  • 查看Iterator接口的源码 - 发现如果还有元素就返回true

img

3.2.3 Iterator接口的方法

img

img

  • 提示:在调用iterator.next()方法之前必须要调用iterator.hasNext()来进行检测,看看是否还有元素。若不调用,且之后没有元素,再调用iterator.next()会抛出NoSuchElementException异常
public class CollectionIterator {@SuppressWarnings({"all"})public static void main(String[] args) {//以ArrayList类来演示Collection col = new ArrayList();col.add(new Book("三国演义", "罗贯中", 10.1));col.add(new Book("小李飞刀", "古龙", 5.1));col.add(new Book("红楼梦", "曹雪芹", 34.1));//        System.out.println(col);//希望能够遍历col集合//1. 先得到 col 对于的迭代器Iterator iterator = col.iterator();//2. 使用while循环遍历即可while (iterator.hasNext()) { //判断是否还有数据//返回下一个元素,类型是Object,什么都可以返回Object obj = iterator.next();System.out.println(obj);}//快捷键,快速生成 while循环 itit//显示所有的快捷键的快捷键 ctrl + j//3. 当退出while循环后,这时iterator迭代器指向了最后的元素
//        iterator.next();//抛出异常 NoSuchElementException//4. 如果希望再次遍历,需要重置我们的迭代器,即将箭头指向最前面的元素iterator = col.iterator();System.out.println("===第二次遍历===");while (iterator.hasNext()) {Object next = iterator.next();System.out.println(next);}}
}
//Book类 - 很简单可以跳
class Book {private String name;private String author;private double price;public Book(String name, String author, double price) {this.name = name;this.author = author;this.price = price;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAuthor() {return author;}public void setAuthor(String author) {this.author = author;}public double getPrice() {return price;}public void setPrice(double price) {this.price = price;}@Overridepublic String toString() {return "Book{" +"name='" + name + '\'' +", author='" + author + '\'' +", price=" + price +'}';}
}

img

3.3 Collection接口遍历对象方式2-for循环增强(即for-each循环)

3.3.1 基本介绍

增强for循环,可以代替iterator迭代器

特点:增强for循环就是简化版的iterator,但底层还是一样的,都是迭代器,只能用于遍历集合或数组

  • 基本语法:
for (元素类型 元素名 : 集合名或数组名) {访问元素
}
3.3.2 案例演示
public class CollectionFor {@SuppressWarnings({"all"})public static void main(String[] args) {Collection col = new ArrayList();col.add(new Book("三国演义", "罗贯中", 10.1));col.add(new Book("小李飞刀", "古龙", 5.1));col.add(new Book("红楼梦", "曹雪芹", 34.1));//解读//1. 使用增强for循环遍历集合,在Collection集合上使用//2. 增强for ,底层仍然是迭代器//3. 增强for可以理解为就是简化版本的迭代器遍历//4. 快捷方式 I
//        for (Object o :) {
//
//        }for (Object obj : col) {System.out.println(obj);}//增强for,也可以直接在数组使用int[] nums = {1, 8, 10, 90};for (int i : nums) {System.out.println(i);}}
}

img

3.3.3 Debug查看源码 - 看底层是否是迭代器
  • 在增强for那里打断点,Debug
  • 发现直接就进入了iterator迭代器

img

  • 一步步走下去,发现都是迭代器的next和hasNext,因此得出结论:增强for的底层就是迭代器

img

img

3.4 练习题

编程题:

  1. 创建 3 个Dog{name, age}对象,放入到 ArrayList中,赋给 List 引用
  2. 用迭代器和增强for循环两种方式来遍历
  3. 重写Dog的toString方法,输出name和age
public class CollectionExercise {@SuppressWarnings({"all"})public static void main(String[] args) {List list = new ArrayList();list.add(new Dog("tom", 2));list.add(new Dog("smith", 1));list.add(new Dog("jack", 3));//iterator 迭代器遍历Iterator iterator = list.iterator();while (iterator.hasNext()) {Object obj = iterator.next();System.out.println(obj);}//for循环增强for (Object o : list) {System.out.println(o);}}
}
/*** 创建3个Dog {name, age} 对象,放入到ArrayList中,赋给List引用* 用迭代器和增强for循环两种方式来遍历* 重写Dog的 toString方法,输出name和age*/
class Dog {private String name;private int age;public Dog(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Dog{" +"name='" + name + '\'' +", age=" + age +'}';}
}

四、List接口和常用方法

4.1 List接口基本介绍

  • List接口是Collection接口的子接口
  1. List集合类中元素有序(即添加顺序和取出顺序一致),且可重复
  2. List集合中的每个元素都有其对应的顺序索引,即支持索引**(索引从0开始)**
  3. List容量中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
  4. JDK API中List接口的是实现类有:

img

  • 常用的有:ArrayList、LinkedList、Vector
public class List_ {public static void main(String[] args) {//1. List集合类中元素有序(即添加顺序和取出顺序一致),且可重复List list = new ArrayList();list.add("jack");list.add("tom");list.add("mary");list.add("zan");list.add("tom");System.out.println(list);//2. List集合中的每个元素都有其对应的顺序索引,即支持索引//索引从0开始System.out.println(list.get(3));//zan}
}

img

4.2 List接口的常用方法

public class ListMethod {@SuppressWarnings({"all"})public static void main(String[] args) {List list = new ArrayList();list.add("张三丰");list.add("贾宝玉");//        void add(int index, Object ele):在index位置插入ele元素//在 index = 1 的位置插入一个对象list.add(1, "zan");System.out.println(list);//        boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来List list2 = new ArrayList();list2.add("jack");list2.add("tom");list.addAll(1, list2);System.out.println(list);//        Object get(int index):获取指定index位置的元素//        int indexOf(Object obj):返回obj在集合中首次出现的位置System.out.println(list.indexOf("tom"));//2//        int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置list.add("zan");System.out.println(list.lastIndexOf("zan"));//5//        Object remove(int index):移除指定index位置的元素,并返回此元素list.remove(0);System.out.println(list);//        Object set(int index, Object ele):设置指定index位置的元素为ele , 相当于是替换.list2.set(1, "玛丽");
//        list2.set(10, "玛丽");//越界会抛出异常IndexOutOfBoundsExceptionSystem.out.println(list2);//        List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合// 注意返回的子集合 fromIndex <= subList < toIndex  == 左闭右开List reslist = list.subList(0, 2);System.out.println(reslist);}
}

4.3 List接口课堂练习1

需求:添加10个以上的元素(比如String “hello”),在2号位插入一个元素"zan",获得第五个元素,删除第六个元素,修改第七个元素,再使用迭代器遍历集合,要求:使用List的实现类ArrayList完成

/*** 添加10个以上的元素(String "hello"),在2号位插入一个元素"zan";* 获得第5个元素,删除第6个元素,修改第7个元素,在使用迭代器遍历集合* 要求:使用List的实现类ArrayList完成*/
public class ListExercise {@SuppressWarnings({"all"})public static void main(String[] args) {List list = new ArrayList();for (int i = 1; i < 12; i++) {list.add("hello" + i);}System.out.println(list);list.add(2, "zan");System.out.println(list);System.out.println("第五个元素=" + list.get(4));//删除第6个元素list.remove(5);System.out.println(list);//修改第7个元素list.set(6, "三国演义");System.out.println(list);//使用迭代器遍历Iterator iterator = list.iterator();while (iterator.hasNext()) {Object obj = iterator.next();System.out.println(obj);}}
}

4.4 List【ArrayList、LinkedList、Vector】的三种遍历方式

方式一:使用iterator迭代器

Iterator iter = col.iterator();
while (iter.hasNext()) {Object o = iter.next();
}

方式二:使用增强for循环

for (Object o : col) {
}

方式三:使用普通for循环

for (int i = 0; i < list.size(); i++) {Object obj = list.get(i);System.out.println(obj);
}
public class ListFor {@SuppressWarnings({"all"})public static void main(String[] args) {//List 接口的实现子类 Vector LinkedList ArrayList都可以
//        List list = new ArrayList();
//        List list = new Vector();List list = new LinkedList();list.add("jack");list.add("tom");list.add("鱼香肉丝");list.add("北京烤鸭");//遍历//1. 迭代器Iterator iterator = list.iterator();while (iterator.hasNext()) {Object next = iterator.next();System.out.println(next);}//2.增强for循环System.out.println("==增强for==");for (Object o : list) {System.out.println(o);}//3. 普通for循环System.out.println("==普通for==");for (int i = 0; i < list.size(); i++) {System.out.println(list.get(i));}}
}

4.5 List实现类的课堂练习2

使用List的实现类添加三本图书,并遍历

需求:

  1. 按价格排序,从低到高(使用冒泡法)
  2. 要求使用ArrayList、Linked List、Vector三种集合实现
/*** 使用List的实现类添加三本图书,并遍历* 要求:按价格排序,从低到高(使用冒泡)* 要求使用ArrayList、LinkedList、Vector三种集合实现*/
@SuppressWarnings({"all"})
public class ListExercise02 {public static void main(String[] args) {
//        List list = new ArrayList();
//        List list = new Vector();List list = new LinkedList();list.add(new Book("红楼梦", "曹雪芹", 100)); // 向上转型 Book -> Objectlist.add(new Book("三国演", "罗贯中", 10));list.add(new Book("西游记", "吴承恩", 120));//如何对集合进行排序//遍历for (Object o : list) {System.out.println(o);}//冒泡排序sort(list);System.out.println("===排序后===");for (Object o : list) {System.out.println(o);}}//静态方法public static void sort(List list) {for (int i = 0; i < list.size(); i++) {for (int j = 0; j < list.size() - i - 1; j++) {// 取出对象Book// 这里只有向下转型,将其编译类型转成Book,才能调用Book类里面的方法(调用子类的方法)Book book1 = (Book)list.get(j);Book book2 = (Book)list.get(j + 1);if (book1.getPrice() > book2.getPrice()) {list.set(j, book2);list.set(j + 1, book1);}}}}
}class Book {private String name;private String author;private double price;public Book(String name, String author, double price) {this.name = name;this.author = author;this.price = price;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAuthor() {return author;}public void setAuthor(String author) {this.author = author;}public double getPrice() {return price;}public void setPrice(double price) {this.price = price;}@Overridepublic String toString() {return "名称:" + name + "\t\t价格:" + price + "\t\t作者:" + author;}
}

4.6 ArrayList底层结构和源码分析⭐

4.6.1 ArrayList的注意事项

1、permits all elements,including null,ArrayList可以加入null,并且可以加入多个(可以放入空元素)

2、ArrayList是由数组来实现数据存储

3、ArrayList基本等同于Vector,除了ArrayList是线程不安全(但是执行效率高),因此在多线程情况下,不建议使用ArrayList

public class ArrayListDetail {@SuppressWarnings({"all"})public static void main(String[] args) {//ArrayList 是线程不安全的,可以看源码,没有synchronized关键字修饰/*public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;}*/ArrayList arrayList = new ArrayList();arrayList.add(null);arrayList.add("jack");arrayList.add(null);arrayList.add(null);System.out.println(arrayList);}
}

img

4.6.2 ArrayList的底层操作机制源码分析(重点、难点)❤️

结论:

  1. ArrayList中维护了一个Object类型的数组elementDate

    transient Object[] elementData; // transient 表示瞬间,短暂的,该关键字修饰该属性不会被序列化

  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次添加后,扩容elementData为10,如需要再次扩容,则扩容elementData为原先的1.5倍

  3. 如果使用的是指定大小的构造器(即有参构造器),则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍

总结:

无参构造器:初始为0,第一次添加为10,后面扩容1.5倍(0 -> 10 -> 15 -> 22)

有参构造器:初始为指定大小,后面扩容1.5倍(8 -> 12 -> 18 -> 27)

public class ArrayListSource {@SuppressWarnings({"all"})public static void main(String[] args) {//注意:Idea再默认情况下,Debug显示的数据是简化后的,如果希望看到完整的数据需要做设置,//使用无参构造器创建ArrayList对象
//        ArrayList list = new ArrayList();ArrayList list = new ArrayList(8);//使用for循环给list集合添加1-10数据for (int i = 1; i <= 10; i++) {list.add(i);}//使用for循环给list集合添加 11-15数据for (int i = 11; i <= 15; i++) {list.add(i);}list.add(100);list.add(200);list.add(null);}
}

查看源码 - Debug

4.6.2.1 无参构造器
  • 首先就是创建了一个空的elementData数组 = {}

img

img

  • 然后执行list.add()

    1、先确定是否要扩容(ensureCapacityInternal方法)

    2、然后再执行赋值操作

img

  • calculateCapacity()该方法确定minCapacity,也就是最小容量

    1、第一次扩容为10

img

  • ensureExplicitCapacity()方法判断是否要扩容

    1、modCount++:记录集合被修改的次数(防止多线程操作出现的异常)

    2、如果elementData的大小/容量不够,就调用grow()去扩容

img

  • 进行扩容(grow方法)

    1、使用了扩容机制来确定要扩容到多大

    2、第一次newCapacity = 10

    3、第二次及其以后,按照1.5倍扩容(因为oldCapacity >> 1 代表除以2,即1 + 0.5 = 1.5倍,>>向右移动一位)

    4、扩容使用的是Arrays.copyof()

img

img

4.6.2.2 有参构造器
  • 与无参构造器的区别在于刚开始创建数组的时候
  • 有参构造器是创建了一个指定大小elementData数组
  • elementData = new Object[initialCapacity]

img

img

  • 有参构造器的扩容机制:第一次扩容,就按照elementData的1.5倍扩容,整个的执行流程还是跟前面无参构造器的一样

4.7 Vector底层结构和源码剖析⭐

4.7.1 Vector的基本介绍
  • Vector类的定义说明

img

Vector底层也是一个对象数组:protected Object[] elementData;

Vector是线程同步的,即线程安全因为Vector类的操作方法带有synchronized

在开发中,需要线程同步安全时,考虑使用Vector

4.7.2 Vector的底层操作机制源码分析(重点、难点)❤️

结论:

  • 无参构造器刚开始开辟10个空间的数组,有参构造器就开辟特定大小的数组
  • 如果需要的数组大小不够用,就扩容2倍

总结:

无参构造器:初始为10个空间,后面扩容2倍(10 -> 20 -> 30)

有参构造器:初始为指定大小,后面扩容2倍(8 -> 16 -> 32)

@SuppressWarnings({"all"})
public class Vector_ {public static void main(String[] args) {//无参构造器//有参数的构造Vector vector = new Vector(8);for (int i = 0; i < 10; i++) {vector.add(i);}vector.add(100);System.out.println("vector=" + vector);//解读源码//1. new Vector() 底层/*public Vector() {this(10);}补充:如果是  Vector vector = new Vector(8);走的方法:public Vector(int initialCapacity) {this(initialCapacity, 0);}2. vector.add(i)2.1  //下面这个方法就添加数据到vector集合public synchronized boolean add(E e) {modCount++;ensureCapacityHelper(elementCount + 1);elementData[elementCount++] = e;return true;}2.2  //确定是否需要扩容 条件 : minCapacity - elementData.length>0private void ensureCapacityHelper(int minCapacity) {// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);}2.3 //如果 需要的数组大小 不够用,就扩容 , 扩容的算法//newCapacity = oldCapacity + ((capacityIncrement > 0) ?//                             capacityIncrement : oldCapacity);//但是由于capacityIncrement = 0,所以就是扩容两倍private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity);}*/}
}
  • 源码分析 - Debug

img

img

  • 添加数据到vector集合

img

  • 确定是否需要扩容条件 : minCapacity - elementData.length>0

img

  • 如果需要的数组大小不够用,就扩容 , 扩容的算法 - 为扩容2倍

img

4.7.3 Vector和ArrayList的比较

img

4.8 LinkedList底层结构和源码剖析⭐

4.8.1 LinkedList的全面说明

1、LinkedList底层实现了双向链表和双端队列特点

2、可以添加任意元素(元素可以重复),包括null

3、线程不安全,没有实现同步

4.8.2 LinkedList的底层操作机制

1、LinkedList底层维护了一个双向链表

2、LinkedList中维护了两个属性first和last,分别指向首节点和尾节点

3、每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终实现了双向链表

img

所以LinkedList的元素的添加和删除,不是通过数组来完成的,相对来说效率较高

img
  • 模拟一个简单的双向链表
package com.zanedu.list_;public class LinkedList01 {public static void main(String[] args) {//模拟一个简单的双向链表Node jack = new Node("jack");Node tom = new Node("tom");Node zan = new Node("zan");//连接三个节点,形成双向链表//jack -> tom -> zanjack.next = tom;tom.next = zan;//zan -> tom -> jackzan.pre = tom;tom.pre = jack;Node first = jack; //让first引用指向jack,就是双向链表的头节点Node last = zan;//让last引用指向zan,就是双向链表的尾节点//演示从头到尾进行遍历System.out.println("===从头到尾进行遍历===");while (true) {if (first == null) {break;}//输出first的信息System.out.println(first);first = first.next;}//演示从尾到头进行遍历System.out.println("===从尾到头进行遍历===");while (true) {if (last == null) {break;}System.out.println(last);last = last.pre;}//演示链表的添加对象/数据,是多么的方便//在 tom 和 zan 之间插入一个对象 smith//1. 先创建一个Node节点,名字为smithNode smith = new Node("smith");//下面就把smith加入到了双向链表smith.next = zan;smith.pre = tom;zan.pre = smith;tom.next = smith;//重置first,让first再次指向jackfirst = jack;System.out.println("===从头到尾进行遍历===");while (true) {if (first == null) {break;}//输出first的信息System.out.println(first);first = first.next;}last = zan;//让last指向最后一个节点//演示从尾到头进行遍历System.out.println("===从尾到头进行遍历===");while (true) {if (last == null) {break;}System.out.println(last);last = last.pre;}}
}
//定义一个Node类,Node对象表示双向链表的一个节点
class Node {public Object item;//真正存放数据的地方public Node next;//指向后一个节点public Node pre;//指向前一个节点public Node(Object item) {this.item = item;}@Overridepublic String toString() {return "Node name=" + item;}
}

img

4.8.3 LinkedList的底层操作机制源码分析(重点、难点)❤️
@SuppressWarnings({"all"})
public class LinkedListCRUD {public static void main(String[] args) {LinkedList linkedList = new LinkedList();linkedList.add(1);linkedList.add(2);linkedList.add(3);System.out.println(linkedList);/*演示一个删除节点的*/linkedList.remove();//默认删除第一个节点//linkedList.remove(2);System.out.println(linkedList);//修改某个节点对象linkedList.set(1, 999);System.out.println(linkedList);//得到某个节点对象//get(1)是得到双向链表的第二个对象System.out.println(linkedList.get(1));//因为LinkedList是实现了List接口,所以遍历方式可以迭代器,也可以增强forSystem.out.println("===LinkedList遍历迭代器===");Iterator iterator = linkedList.iterator();while (iterator.hasNext()) {Object next = iterator.next();System.out.println(next);}System.out.println("===LinkedList的在增强for===");for (Object o : linkedList) {System.out.println(o);}System.out.println("===普通for===");for (int i = 0; i < linkedList.size(); i++) {System.out.println(linkedList.get(i));}/*linkedList.add源码解读1. LinkedList linkedList = new LinkedList();public LinkedList() {}2. 这时 LinkedList 的属性 first = null,last = null3. 执行public boolean add(E e) {linkLast(e);return true;}4. 将新的节点,加入到双向链表的最后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;elsel.next = newNode;size++;modCount++;}*//*linkedList.remove()源码解读1. 执行removeFirstpublic E remove() {return removeFirst();}2. 执行public E removeFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return unlinkFirst(f);}3. 执行unlinkFirst,将 f 指向的双向链表的第一个节点拿掉(删除)private E unlinkFirst(Node<E> f) {// assert f == first && f != null;final E element = f.item;final Node<E> next = f.next;f.item = null;f.next = null; // help GCfirst = next;if (next == null)last = null;elsenext.prev = null;size--;modCount++;return element;}*/}
}
  • linkedList.add源码解读

    (1)LinkedList linkedList = new LinkedList();

img

  • 这时LinkedList的属性 first = null,last = null

img

(2)执行

img

(3)将新的节点,加入到双向链表的最后

img

  • 第一次add结束后

img

  • linkedList.remove()源码解读

    (1)执行removeFirst

img

(2)执行

img

(3)执行unlinkFirst,将 f 指向的双向链表的第一个节点拿掉(删除)

img

4.8.4 LinkedList和ArrayList比较

img

  • 如何选择ArrayList和LinkedList

1、如果我们改查的操作多,选择ArrayList

2、如果我们增删的操作多,选择LinkedList

3、一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList

4、在一个项目中,根据业务灵活选择,可能会这样,即一个模块使用的是ArrayList,另外一个模块是LinkedList,也就是说,要根据业务来进行选择

5、注意:两个都是线程不安全的

五、Set接口和常用方法

5.1 Set接口基本介绍

1、无序(添加和取出的顺序不一致),没有索引

2、不允许出现重复元素,所以最多包含一个null

3、JDK API中Set接口的实现类有:

img

5.2 Set接口的常用方法

  • 和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样
@SuppressWarnings({"all"})
public class SetMethod {public static void main(String[] args) {//解读/*1. 以Set接口的实现类 HashSet 来讲解Set接口的方法2. Set 接口的实现类的对象(Set接口对象),不能存放重复的元素,可以添加一个null3. Set接口对象存放数据是无序的(即添加的顺序和取出的顺序不一致)4. 注意:取出的顺序是固定的,虽然不是添加的顺序(有序),但是是固定的*/Set set = new HashSet();set.add("john");set.add("lucy");set.add("john");//重复set.add("jack");set.add("zan");set.add(null);set.add(null);//再次添加nullSystem.out.println(set);set.remove(null);System.out.println(set);}
}

img

5.3 Set接口的遍历方式

同Collection的遍历方式一样,因为Set接口是Collection接口的子接口

1、可以使用迭代器

2、增强for循环

3、不能使用索引的方式来获取(即不能使用普通循环)

@SuppressWarnings({"all"})
public class SetMethod {public static void main(String[] args) {Set set = new HashSet();set.add("john");set.add("lucy");set.add("jack");set.add("zan");set.add(null);//遍历//方式一:迭代器System.out.println("===使用迭代器===");Iterator iterator = set.iterator();while (iterator.hasNext()) {Object o = iterator.next();System.out.println(o);}//方式二:增强forSystem.out.println("===使用增强for===");for (Object o : set) {System.out.println(o);}//Set接口对象,不能通过索引来获取}
}

5.4 HashSet底层结构和源码剖析⭐

5.4.1 HashSet的全面说明

1、HashSet实现了Set接口

2、HashSet实际上是HashMap(看源码)

img

3、可以存放null值,但是只能有一个null

4、HashSet不保证元素是有序的,取决于hash值,再确定索引的结果(即不能保证存放元素的顺序和取出顺序一致

5、不能有重复元素/对象

@SuppressWarnings("all")
public class HashSet_ {public static void main(String[] args) {//解读//1. 构造器走的源码/*public HashSet() {map = new HashMap<>();}2. HashSet 可以存放null,但是只能有一个null,即不能重复*/Set set = new HashSet();set.add(null);set.add(null);System.out.println(set);}
}

img

5.4.2 HashSet案例说明 - 引出问题
@SuppressWarnings("all")
public class HashSet01 {public static void main(String[] args) {//说明//1. 在执行add方法后,会返回一个boolean//2. 如果添加成功,返回true,否则返回false//3. 可以通过remove指定删除哪个对象HashSet set = new HashSet();System.out.println(set.add("john"));//trueSystem.out.println(set.add("lucy"));//trueSystem.out.println(set.add("john"));//falseSystem.out.println(set.add("jack"));//trueSystem.out.println(set.add("Rose"));//trueset.remove("john");System.out.println(set);set = new HashSet();System.out.println(set);//4. HashSet不能添加相同的元素/数组?set.add("lucy");//添加成功set.add("lucy");//加入不了//都能加进去两只狗,只是名字相同而已set.add(new Dog("tom"));//OKset.add(new Dog("tom"));//OKSystem.out.println(set);//再加深一下,非常经典的面试题//看源码,做分析//去看它的源码,即add到底发生了什么,看下面的底层机制分析set.add(new String("zan"));//okset.add(new String("zan"));//添加不进去System.out.println(set);}
}
class Dog { //定义了一个Dog类private String name;public Dog(String name) {this.name = name;}@Overridepublic String toString() {return "Dog{" +"name='" + name + '\'' +'}';}
}
5.4.3 HashSet的底层操作机制源码分析(重点、难点)❤️❤️❤️

分析HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)

由于放在一个数组里面效率很低,所以Java底层这样子来设计这种结构

5.4.3.1 模拟一个简单的数组+链表结构
public class HashSetStructure {public static void main(String[] args) {// 模拟一个 HashSet的底层(HashMap 的底层结构)// 1. 创建一个数组,数组的类型是 Node[]// 2. 有些人直接把 Node[] 数组称为 表Node[] table = new Node[16];// 3. 把john结点放在2的位置Node john = new Node("john", null);table[2] = john;System.out.println(table);// 4. 把jack结点放在john后面Node jack = new Node("jack", null);john.next = jack; // 将jack结点挂载到john// 5. 把rose结点放在jack后面Node rose = new Node("rose", null);jack.next = rose;// 6. 把lucy结点放在3的位置Node lucy = new Node("lucy", null);table[3] = lucy;System.out.println(table);}
}class Node { // 代表结点(存储数据,可以指向下一个结点,从而形成链表)public Object item; // 代表数据public Node next; // 指向下一个结点public Node(Object item, Node next) {this.item = item;this.next = next;}
}
  • 以上就是简单的数组+链表结构

  • 接下来分析HashSet的添加元素底层是如何实现的( hash() + equals() )

5.4.3.2 HashSet源码分析(add添加方法)⭐

结论:

  • HashSet底层是HashMap

  • 添加一个元素时,会先得到hash值,然后会转成索引值

  • 找到存储数据表table,看这个索引位置是否已经存放过元素,即是否含有元素

  • 如果没有,则直接加入

  • 如果有,则调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后(equals方法可以进行重写,由程序员决定)

  • 在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table表的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(即转换成红黑树)

总结:

添加元素,会获取Hash值,转换成对应的索引值,看放在table数据表里面的哪个位置

加入的位置没有元素:直接加入即可

加入的位置有元素:需要调用equals()方法比较**(该链表的每一个元素都需要比较)**,如果元素相同,则不能添加,若不相同,就添加到最后(equals方法可以程序员进行重写,自行控制)

注意:当一条链表的元素达到了8,并且table表的大小达到了64,那么这条链表会进行树化,转化成红黑树

@SuppressWarnings("all")
public class HashSetSource {public static void main(String[] args) {HashSet hashSet = new HashSet();hashSet.add("java");//到此为止,第一次add分析完毕hashSet.add("php");//到此为止,第二次add分析完毕hashSet.add("java");System.out.println(hashSet);/*对HashSet的源码解读1. 执行构造器 HashSet()public HashSet() {map = new HashMap<>();}2. 执行 add() 方法public boolean add(E e) {return map.put(e, PRESENT)==null;}// PRESENT 是 HashSet里面的东西,private static final Object PRESENT = new Object();3. 执行 map.put() 方法public V put(K key, V value) { // key = "java"  value = PRESENT共享return putVal(hash(key), key, value, false, true);}3.1 优先执行 hash() 方法,按照对应的算法(为了更好的获取不同的Hash值)得到一个Hash值,注意不是HashCode,不等价于HashCode值static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}4. 执行 putVal() 方法final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// 定义了辅助变量Node<K,V>[] tab; Node<K,V> p; int n, i;// table 就是 HashMap的一个数组,类型是 Node[]// if 语句表示当前table是null,或者大小为0// 说明就是第一次扩容,扩容到16个空间(具体扩容看resize()方法)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// (1) 去判断传入的key放哪,根据得到的Hash值去计算该key应该存放到table表的哪个索引位置,并且把整个位置的对象,赋给辅助变量p// (2) 判断 p 是否为null// (2.1) 如果 p 为null,表示该位置还没有存放过元素,那么就创建一个Node(key="java", value=PRESENT)//       并且将传入的key就放在该位置 tab[i] = newNode(hash, key, value, null);if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);// (2.2) 如果 p 不为null,表示该位置已经有元素了,那么需要去判断该值是否已经添加过了//       如果添加过了,那么就不进行添加,如果没有添加,那么就添加到最后面else {// 辅助变量 - 开发技巧提示:在需要局部变量(辅助变量)的时候,再进行创建Node<K,V> e; K k;// (2.2.1) 如果当前索引位置对应的链表的第一个元素和准备添加的key的Hash值一样(比较数组上的元素)//         并且满足 下面两个条件之一://         ① 准备加入的 key 和 p 指向的 Node 结点的 key 是同一个对象//         ② p 指向的 Node 结点的 key 的equals() 和 准备加入的 key 比较后相同(这个得看equals方法)//         就不能够加入!!!if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// (2.2.2) 再判断 p 是不是一棵红黑树//         如果是的话,就调用 putTreeVal() 方法来进行添加else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// (2.2.3) 这个是数组上面是一个链表的情况,要去判断链表上的每一个元素(比较数组里面的所有链表元素)// ① 依次和该链表的每个元素比较后,都不相同,说明添加的元素是没有重复,因此需要将该元素添加到链表的最后面//    注意:在把元素添加到链表后,立即判断该链表是否达到8个结点,如果达到,就调用 treeifyBin(),对当前//    这个链表进行树化(转化成红黑树)//    注意:在转成红黑树时,还进行一个判断://    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//            resize();//    上面条件成立,先table扩容//    上面条件不成立,才进行树化//    判断table数组的大小是否已经达到64了,如果达到了,进行树化,如果没有,则只是数组的扩容// ② 依次和该链表的每个元素比较过程中,如果有相同的,那么就不进行添加该元素,进行breakelse {// 无限循环for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;// threshold 是临界值(根据因子计算出来的临界值),如果达到了,那么就需要扩容了if (++size > threshold)resize();// afterNodeInsertion() 该方法是对HashMap的子类来实现重写了,相当于是写一些自己的逻辑在里面,留下了一个扩展的位置afterNodeInsertion(evict);return null;}*/}
}
  • 源码解读

    (1)先执行 HashSet()构造器

img

(2)执行add(),这里的PRESENT => private static final Object PRESENT = new Object();

img

(3)再执行 put() - 该方法会执行 hash(key)方法,得到 key 对应的hash值,不是hashCode,是经过算法处理的

img

(4)执行 hash(key) - 为了求出hash值 - 算法如下(注意:如果重写了hashCode方法,都返回同一个值,那么所有的元素都会在一条链表上,都在一个数组item中)

img

(5)执行 putVal()

img

定义了辅助变量 - 开发技巧提示:在需要局部变量(辅助变量)的时候,再进行创建

img

table就是HashMap的一个属性,类型是 Node[]

if 语句表示如果当前table是null或者大小为0,那么就是第一次扩容,扩容16个空间(具体扩容看resize()方法)

img

(1)去判断传入的key放哪,根据得到的Hash值去计算该key应该存放到table表的哪个索引位置,并且把整个位置的对象,赋给辅助变量p

(2)判断 p 是否为null

(2.1)如果 p 为null,表示该位置还没有存放过元素,那么就创建一个Node(key=“java”, value=PRESENT),并且将传入的key就放在该位置 tab[i] = newNode(hash, key, value, null);

img

  • 进入上面的else里面

(2.2)如果 p 不为null,表示该位置已经有元素了,那么需要去判断该值是否已经添加过了,如果添加过了,那么就不进行添加,如果没有添加,那么就添加到最后面

(2.2.1)如果当前索引位置对应的链表的第一个元素和准备添加的key的Hash值一样(比较数组上的元素)并且满足 下面两个条件之一:

① 准备加入的 key 和 p 指向的 Node 结点的 key 是同一个对象

② p 指向的 Node 结点的 key 的equals() 和 准备加入的 key 比较后相同(这个得看equals方法)

就不能够加入!!!

img

  • 上面的if条件不成立

(2.2.2)再判断 p 是不是一棵红黑树,如果是的话,就调用 putTreeVal() 方法来进行添加

img

  • 上面的else if条件不成立

(2.2.3)这个是数组上面是一个链表的情况,要去判断链表上的每一个元素(比较数组里面的所有链表元素)

① 依次和该链表的每个元素比较后,都不相同,说明添加的元素是没有重复,因此需要将该元素添加到链表的最后面

注意:在把元素添加到链表后,立即判断该链表是否达到8个结点,如果达到,就调用 treeifyBin(),对当前这个链表进行树化(转化成红黑树)

注意:在转成红黑树时,还进行一个判断:

if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();

上面条件成立,先table扩容

上面条件不成立,才进行树化

判断table数组的大小是否已经达到64了,如果达到了,进行树化,如果没有,则只是数组的扩容

② 依次和该链表的每个元素比较过程中,如果有相同的,那么就不进行添加该元素,进行break

img

  • treeifyBin()方法中的前置判断(树化的前置判断)

img

  • 最后的size

size就是我们每加入一个节点Node(key, value, hash, next),size就会++

不管节点夹在table的第一个位置,还是加在链表的某一个位置,都会++

img

5.4.3.3 HashSet源码分析(扩容和转成红黑树机制)⭐

结论:

1、HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75(为了保险提前定好的加载因子) = 12

2、如果table数组使用到了临界值 12,就会扩容到 16*2 = 32,新的临界值就是 32*0.75=24,以此类推

3、在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table表的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制

总结:

HashSet底层是HashMap,初始为0,第一次添加后,table数组扩容到16,临界值为0.75*16=12

当达到临界值的时候,按照两倍进行扩容

树化的情况:一条链表的元素已经达到8,并且table表的大小达到了64,那么链表就会进行树化,不然还是table数组两倍扩容

  • 底层分析
@SuppressWarnings("all")
public class HashSetIncrement {public static void main(String[] args) {/*HashSet底层是HashMap,第一次添加时, table 数组扩容到 16临界值(threshold) 是 16*加载因子(loadFactor)是0.75 = 12如果 table数组使用到了临界值 12,就会扩容到 16*2=32,而新的临界值就是32*0.75=24,依次类推*/HashSet hashSet = new HashSet();
//        for (int i = 0; i < 100; i++) {
//            hashSet.add(i);
//        }/*在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),满足了这个条件,都会去判断table的大小并且table的大小 >= MIN_TERRIFY_CAPACITY(默认是64),就会进行树化(红黑树)否则仍然采用数组扩容机制*///这里为了将一条链表的元素个数达到8,因此这里重写了hashCode方法,只是为了将hash值一样
//        for (int i = 0; i <= 12; i++) {
//            hashSet.add(new A(i));//
//        }/*当我们向hashSet增加一个元素时,到底层会封装成 Node -> 加入table表,就算是增加了一个 size++//不管节点加在table的第一个位置,还是加在链表的某一个位置,都会++*/for (int i = 1; i <= 7; i++) { //在table表的某一条链表上添加了7个对象hashSet.add(new A(i));}for (int i = 1; i <= 7; i++) { //在table表的另一条链表上添加了7个对象hashSet.add(new B(i));}}
}
class B {private int n;public B(int n) {this.n = n;}@Overridepublic int hashCode() {return 200;}
}class A {private int n;public A(int n) {this.n = n;}@Overridepublic int hashCode() {return 100;}
}

扩容底层分析:

img

(1)刚开始添加,table表扩容到16

img

(2)由于临界值是12,因此当table表添加到12的时候,再进行添加,table表会进行扩容(table表有12个)

img

(3)再次添加后 - 进行了扩容(2倍扩容)

img

红黑树底层分析:

img

(1)同一个索引上有8条数据

img

(2)再在这条链表上添加元素,会去查看table的大小是否>=64,若不大于,则会扩容

img

(3)若还在这条链表上添加数组,则仍然会去判断table表是否>=64,若没有64,则会扩容

img

(4)再进行添加,又会去判断,这时table表已经有64个数据了,因此不会再扩容,会将其转换成红黑树

img

  • 我们看到红黑树里就会有left、right等等属性

  • 情况分析(重写HashCode方法,使得hash值都一致):

(1)由于下面在定义类的时候,重写了hashCode的方法,因此计算的hash值会一致

img

(2)我们可以看到,在结束上一个循环的时候,size已经为7,现在我们在另一个表添加对象,看看是否会变化?

img

(3)我们发现再次添加的时候,它的size变成了8

img

(4)因此:不管节点加在table的第一个位置,还是加在链表的某一个位置,size都会++

5.4.4 HashSet课堂练习
5.4.4.1 题一

定义一个Employee类,该类包含:private成员属性name,age

需求:

1、创建3个Employee对象放入HashSet中

2、当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中

@SuppressWarnings("all")
public class HashSetExercise {public static void main(String[] args) {/**定义一个Employee类,该类包含:private成员属性name,age 要求:创建3个Employee 对象放入 HashSet中当 name和age的值相同时,认为是相同员工, 不能添加到HashSet集合中*/HashSet hashSet = new HashSet();hashSet.add(new Employee("jack", 20));//okhashSet.add(new Employee("zan", 18));//okhashSet.add(new Employee("jack", 20));//no//未重写的时候,加入了3个//因为hash值不同System.out.println(hashSet);}
}
//创建Employee
class Employee {private String name;private int age;public Employee(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Employee{" +"name='" + name + '\'' +", age=" + age +'}';}//如果name和age的值相同,则返回相同的hash值//alt+hashCode@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Employee employee = (Employee) o;return age == employee.age && Objects.equals(name, employee.name);}@Overridepublic int hashCode() { // 这是为了放在同一个链表上(不重写可能会导致在不同的数组item上,导致equals没用,因为没有数据)return Objects.hash(name, age);}
}
5.5.4.2 题二

定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中birthday为MyData类型(属性包括year、month、day)

需求:

1、创建3个Employee放入HashSet中

2、当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中

@SuppressWarnings({"all"})
public class HashSetExercise02 {public static void main(String[] args) {HashSet hashSet = new HashSet();hashSet.add(new Employee1("jack", 20, new MyData(10, 10, 10)));hashSet.add(new Employee1("zan", 20, new MyData(12, 10, 10)));hashSet.add(new Employee1("jack", 20, new MyData(10, 10, 10)));System.out.println(hashSet);}
}
class MyData {private int year;private int month;private int day;public MyData(int year, int month, int day) {this.year = year;this.month = month;this.day = day;}public int getYear() {return year;}public void setYear(int year) {this.year = year;}public int getMonth() {return month;}public void setMonth(int month) {this.month = month;}public int getDay() {return day;}public void setDay(int day) {this.day = day;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;MyData myData = (MyData) o;return year == myData.year && month == myData.month && day == myData.day;}@Overridepublic int hashCode() {return Objects.hash(year, month, day);}
}
class Employee1 {private String name;private double sal;private MyData birthday;public Employee1(String name, double sal, MyData birthday) {this.name = name;this.sal = sal;this.birthday = birthday;}public String getName() {return name;}public void setName(String name) {this.name = name;}public double getSal() {return sal;}public void setSal(double sal) {this.sal = sal;}public MyData getBirthday() {return birthday;}public void setBirthday(MyData birthday) {this.birthday = birthday;}@Overridepublic String toString() {return "Employee1{" +"name='" + name + '\'' +", sal=" + sal +", birthday=" + birthday +'}';}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Employee1 employee1 = (Employee1) o;return Objects.equals(name, employee1.name) && Objects.equals(birthday, employee1.birthday);}@Overridepublic int hashCode() {return Objects.hash(name, birthday);}
}

5.5 LinkedHashSet底层结构和源码剖析⭐

5.5.1 LinkedHashSet的全面说明

1、LinkedHashSet是HashSet的子类

img

img

2、LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表

3、LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的(有序)

4、LinkedHashSet不允许添加重复元素

5.5.2 LinkedHashSet底层机制说明

说明:

1、在LinkedHashSet中维护了一个hash表和双向链表(LinkedHashSet有head和tail)

2、每一个节点有before和after属性,这样可以形成双向链表

3、在添加一个元素时,先求hash值,在求索引,确定该元素在table表的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加**【原则和HashSet一样】**)

4、这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致

img

@SuppressWarnings("all")
public class LinkedHashSetSource {public static void main(String[] args) {//分析一下LinkedHashSet的底层机制Set set = new LinkedHashSet();set.add(new String("AA"));set.add(456);set.add(456);set.add(new Customer("刘", 1001));set.add(128);set.add("zan");System.out.println(set);//解读//1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致//2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)//3. LinkedHashSet 底层结构(数组table + 双向链表)//4. 添加第一次时,直接将数组table扩容到 16,并且存放的节点的类型是 LinkedHashMap$Entry//5. 数组是 HashMap$Node[] ,但是存放的元素/数据是 LinkedHashMap$Entry类型/*//继承关系是在内部类完成的static class Entry<K,V> extends HashMap.Node<K,V> {Entry<K,V> before, after;Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}}*/}
}
class Customer {private String name;private int n;public Customer(String name, int n) {this.name = name;this.n = n;}
}
  • 底层源码说明

(1)LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)

img

(2)添加第一次时,直接将数组table扩容到 16(跟HashSet一样的扩容机制,按照0.75的临界值,进行2倍扩容),并且存放的节点的类型是 LinkedHashMap$Entry

(3)table表数组是 HashMap N o d e [ ] ∗ ∗ ∗ ∗ ,但是存放的元素 / 数据是 ∗ ∗ ∗ ∗ L i n k e d H a s h M a p Node[]** **,但是存放的元素/数据是** **LinkedHashMap Node[],但是存放的元素/数据是LinkedHashMapEntry类型(向上转型)

img

(4)这就是由于继承关系或者实现关系

注意:这个继承关系是在内部类完成的

img

img

(5)添加第二个元素后,就形成双向链表了

还有那个head和tail,指向头和尾

img

5.5.3 LinkedHashSet练习题

Car类(属性:name、price),如果name和price一样,则认为是相同元素,就不能添加

@SuppressWarnings("all")
public class LinkedHashSetExercise {public static void main(String[] args) {LinkedHashSet linkedHashSet = new LinkedHashSet();linkedHashSet.add(new Car("奥拓", 1000));//OKlinkedHashSet.add(new Car("奥迪", 300000));//OKlinkedHashSet.add(new Car("法拉利", 10000000));//OKlinkedHashSet.add(new Car("奥迪", 300000));//加入不了linkedHashSet.add(new Car("保时捷", 70000000));//OKlinkedHashSet.add(new Car("奥迪", 300000));//加入不了System.out.println(linkedHashSet);}
}
class Car {private String name;private double price;public Car(String name, double price) {this.name = name;this.price = price;}public String getName() {return name;}public void setName(String name) {this.name = name;}public double getPrice() {return price;}public void setPrice(double price) {this.price = price;}@Overridepublic String toString() {return "Car{" +"name='" + name + '\'' +", price=" + price +'}';}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Car car = (Car) o;return Double.compare(car.price, price) == 0 && Objects.equals(name, car.name);}@Overridepublic int hashCode() {return Objects.hash(name, price);}
}

5.6 Set接口实现类-TreeSet

  • 要注意的是TreeSet的add机制,若使用匿名内部类,则当返回的是0就无法添加成功
@SuppressWarnings({"all"})
public class TreeSet_ {public static void main(String[] args) {/*解读1. 当我们使用无参构造器,创建 TreeSet时,是有序的,会按照首字母来排序2. 需求:添加的元素,按照字符串大小来排序3. 使用TreeSet提供的构造器,可以传入一个比较器(匿名内部类)并指定排序规则4. 看源码*/
//        TreeSet treeSet = new TreeSet();TreeSet treeSet = new TreeSet(new Comparator() {@Overridepublic int compare(Object o1, Object o2) {//下面调用String的 compareTo方法进行字符串大小比较//如果要求加入的元素,按照长度大小排序
//                return ((String)o2).compareTo((String)o1);return ((String)o2).length() - ((String)o1).length();}});//添加数据treeSet.add("jack");treeSet.add("tom");//3个大小treeSet.add("za");treeSet.add("a");treeSet.add("tom");treeSet.add("abc");//加入不了System.out.println(treeSet);/*解读1. 构造器会把传入的比较器对象,给到底层,即赋给了 TreeSet的底层的 TreeMap的属性 this.comparator = comparatorpublic TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;}2. 第一次添加:把k-v 封装到 Entry对象,放入rootEntry<K,V> t = root;if (t == null) {compare(key, key); // type (and possibly null) check,检测是否为空,为空就抛异常root = new Entry<>(key, value, null);size = 1;modCount++;return null;}3. 在调用 treeSet.add("tom") ,在底层会执行到if (cpr != null) { //cpr就是我们的匿名内部类(对象)do {parent = t;cmp = cpr.compare(key, t.key); //动态绑定到我们的匿名内部类的compare方法if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;else //如果相等,即返回0,这个key就没有加入return t.setValue(value);} while (t != null);}*/}
}

六、Map接口和常用方法

6.1 Map接口实现类的特点

  • 注意:这里是JDK8的Map接口特点
  1. Map与Collection并列存在,用于保存具有映射关系的数据 Key-Value
  2. Map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
  3. Map中的key不允许重复,原因和HashSet一样
  4. Map中的value可以重复
  5. Map的key可以为吧null,value也可以是null,注意key为null,只能有一个,但是value为null,可以有多个
  6. 常用String类作为Map的key
  7. key 和 value 之间存在单向一对一关系,即通过指定的key总能找到对应的value
package com.zanedu.map_;import java.util.HashMap;
import java.util.Map;
@SuppressWarnings({"all"})
public class Map_ {public static void main(String[] args) {//Map接口实现类的特点,使用实现类HashMap//1. Map与Collection并列存在,用于保存具有映射关系的数据:Key-Value(双列元素)//2. Map中的key 和 value 可以是任何引用类型的数据,会封装到HashMap$Node对象中//3. Map中的key不允许重复,原因为HashSet一样//4. Map中的value可以重复//5. Map 的key可以为null,value也可以为null,但是注意key为null,只能有1个,但value可以有多个//6. 常用String类作为Map的 key//7. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 valueMap map = new HashMap();map.put("no1", "大帅哥");//k-vmap.put("no2", "张无忌");//k-vmap.put("no1", "张三丰");//当有相同的key时,就等价于替换map.put("no3", "张三丰");//k-vmap.put(null, null);//k-vmap.put(null, "abc");//等价替换map.put("no4", null);//k-vmap.put("no5", null);//k-vmap.put(1, "赵敏");map.put(new Object(), "金毛狮王");//通过get方法,传入key,会返回对应的valueSystem.out.println(map.get("no2"));System.out.println(map);}
}
  1. Map存放数据的key-value示意图,一对k-v是放在HashMap$Node中的,因为Node实现了Entry接口,有些书上说一对k-v就是一个Entry

img

真正的Key和Value是存在HashMap$Node中的,而Set和Collection集合只是指向了它,只是建立了一个引用

  • Node实现了Entry接口

img

  • 深入讲解
package com.zanedu.map_;import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@SuppressWarnings({"all"})
public class MapSource_ {public static void main(String[] args) {Map map = new HashMap();map.put("no1", "大帅哥");//k-vmap.put("no2", "张无忌");//k-vmap.put(new Car(), new Person());//解读//1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null)//2. k-v 为了方便程序员的遍历,还会创建 EntrySet集合,该集合存放的元素类型是 Entry//   而一个 Entry对象就有 k,v EntrySet<Entry<K,V>> 即:public Set<Map.Entry<K,V>> entrySet()//3. 在entrySet中,定义的类型是 Map.Entry,但是实际上存放的还是 HashMap$Node//为什么能够放进去,这是因为 HashMap$Node implements Map.Entry//即底层代码为:static class Node<K,V> implements Map.Entry<K,V>//4. 这样当把 HashMap$Node 对象存放到 entrySet后,就方便我们的遍历了,因为Map.Entry提供了重要方法//   K getKey();  V getValue();Set set = map.entrySet();System.out.println(set.getClass()); // HashMap$EntrySetfor (Object obj : set) {
//            System.out.println(obj.getClass()); //HashMap$Node//为了从HashMap$Node中取出k-v//1. 先做一个向下转型Map.Entry entry = (Map.Entry)obj;System.out.println(entry.getKey() + "-" + entry.getValue());}Set set1 = map.keySet();System.out.println(set1.getClass());Collection values = map.values();System.out.println(values.getClass());}
}
class Car {}
class Person {}
  • k-v 为了方便程序员的遍历,还会创建 EntrySet集合,该集合存放的元素类型是 Entry**。**
  • 而一个 Entry对象就有 k,v。 EntrySet<Entry<K,V>> 即:public Set<Map.Entry<K,V>> entrySet()

img

  • entrySet的运行类型

img

  • 在entrySet中,定义的类型是 Map.Entry,但是实际上存放的还是 HashMap$Node
  • 为什么能够放进去,这是因为 HashMap$Node implements Map.Entry
  • **即底层代码为:**static class Node<K,V> implements Map.Entry<K,V>

img

  • 这样当把 HashMap$Node 对象存放到 entrySet后**,就方便我们的遍历了,因为Map.Entry提供了重要方法**
  • K getKey(); V getValue();

img

  • 向下转型就是因为****HashMap$Node implements Entry

img

  • entrySet是指向table表的

img

img

Map真正的结构:table表(数组+链表+红黑树),将每一个Node封装成一个entry,然后再放入entrySet集合

img

6.2 Map接口常用方法

package com.zanedu.map_;import java.util.HashMap;
import java.util.Map;@SuppressWarnings({"all"})
public class MapMethod {public static void main(String[] args) {//演示map接口常用方法Map map = new HashMap();map.put("邓超", new Book("", 100));//OKmap.put("邓超", "孙俪");//替换-> 一会分析源码map.put("王宝强", "马蓉");//OKmap.put("宋喆", "马蓉");//OKmap.put("刘令博", null);//OKmap.put(null, "刘亦菲");//OKmap.put("鹿晗", "关晓彤");//OKmap.put("zan", "zan的老婆");System.out.println(map);//remove:根据键key删除映射map.remove(null);System.out.println(map);//get:根据键获取值System.out.println(map.get("鹿晗"));//size:获取元素个数System.out.println(map.size());//isEmpty:判断个数是否为0System.out.println(map.isEmpty());//clear:清除
//        map.clear();System.out.println(map);//containsKey:查找键是否存在System.out.println(map.containsKey("zan"));}
}
class Book {private String name;private int num;public Book(String name, int num) {this.name = name;this.num = num;}
}

6.3 Map接口遍历方法

  • Map遍历的示意图(比List和Set复杂点,但是基本原理一样)

img

  1. containsKey:查找键是否存在
  2. keySet:获取所有的键,key,get(key)
  3. entrySet:获取所有关系 k-v,getKey(),getValue()
  4. values:获取所有的值
package com.zanedu.map_;import java.util.*;@SuppressWarnings({"all"})
public class MapFor {public static void main(String[] args) {Map map = new HashMap();map.put("邓超", "孙俪");map.put("王宝强", "马蓉");map.put("宋喆", "马蓉");map.put("刘令博", null);map.put(null, "刘亦菲");map.put("鹿晗", "关晓彤");//第一组:先取出所有的Key,再通过Key取出对应的ValueSet keyset = map.keySet();//(1)增强forSystem.out.println("===第一种方式===");for (Object key : keyset) {System.out.println(key + "-" + map.get(key));}//(2)迭代器System.out.println("===第二种方式===");Iterator iterator = keyset.iterator();while (iterator.hasNext()) {Object key = iterator.next();System.out.println(key + "-" + map.get(key));}//第二组:把所有的 Values取出Collection values = map.values();//这里可以使用所有Collection使用的遍历方法//(1)增强forSystem.out.println("===取出所有的value===");for (Object value : values) {System.out.println(value);}//(2)迭代器while (iterator.hasNext()) {Object value = iterator.next();System.out.println(value);}//第三种:通过EntrySet来获取k-vSet entrySet = map.entrySet();//EntrySet<Entry<K,V>>//(1)增强forSystem.out.println("===使用EntrySet的增强for");for (Object entry : entrySet) {//将entry转成 Map.EntryMap.Entry m = (Map.Entry)entry;System.out.println(m.getKey() + "-" + m.getValue());}//(2)迭代器Iterator iterator1 = entrySet.iterator();while (iterator1.hasNext()) {Object entry = iterator1.next();
//            System.out.println(entry.getClass());//HashMap$Node ->实现 Map.Entry//向下转型 -> Map.EntryMap.Entry m2 = (Map.Entry)entry;System.out.println(m2.getKey() + "-" + m2.getValue());}}
}

6.4 Map接口课堂练习

使用HashMap添加3个员工对象

需求:键:员工id,值:员工对象

遍历显示工资 > 18000的员工

员工类:姓名、工资、员工id

package com.zanedu.map_;import java.util.*;@SuppressWarnings({"all"})
public class MapExercise {public static void main(String[] args) {HashMap hashMap = new HashMap();hashMap.put(1, new Employee("jack", 20000, 1));hashMap.put(2, new Employee("tom", 30000, 2));hashMap.put(3, new Employee("smith", 10000, 3));//遍历 - 两种方式//1. 使用keySet -> 增强forSet keySet = hashMap.keySet();for (Object key : keySet) {//先获取valueEmployee employee = (Employee) hashMap.get(key);if (employee.getSal() > 18000) {System.out.println(employee);}}//2. 使用 EntrySet -> 迭代器Set entrySet = hashMap.entrySet();Iterator iterator = entrySet.iterator();while (iterator.hasNext()) {Map.Entry entry = (Map.Entry) iterator.next();//通过entry取得key和valueEmployee employee = (Employee) entry.getValue();if (employee.getSal() > 18000) {System.out.println(employee);}}}
}
class Employee {private String name;private double sal;private int id;public Employee(String name, double sal, int id) {this.name = name;this.sal = sal;this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public double getSal() {return sal;}public void setSal(double sal) {this.sal = sal;}public int getId() {return id;}public void setId(int id) {this.id = id;}@Overridepublic String toString() {return "Employee{" +"name='" + name + '\'' +", sal=" + sal +", id='" + id + '\'' +'}';}
}

6.5 Map接口实现类-HashMap

6.5.1 HashMap小结
  1. Map接口的常用实现类:HashMap、Hashtable、Properties
  2. HashMap是Map接口使用频率最高的实现类
  3. HashMap是以 key-value对的方式来存储数据(HashMap$Node类型)
  4. key不能重复,但是值可以重复,允许使用null键和null值
  5. 如果添加相同的key,则会覆盖原来的key-val,等同于修改(key不会替换,value会替换)

img

  1. 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(JDK8的HashMap底层:数组+链表+红黑树)
  2. HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized
6.5.2 HashMap底层机制及源码剖析
  • 示意图

img

  1. (k, v)是一个Node实现了Map.Entry<K, V>
  2. JDK7.0的HashMap底层实现是【数组+链表】,JDK8.0底层【数组+链表+红黑树】
  • 结论:扩容机制(和HashSet相同)
  1. HashMap底层维护了Node类型的数组table,默认为null
  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75
  3. 当添加key-val时,通过key的hash值得到在table表上的索引,然后判断该索引处是否有元素,如果们没有元素则直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等,如果相等,则直接替换val;如果不相等则需要判断是树结构还是链表结构,作出相应处理。如果添加时发现容量不够,则需要扩容
  4. 第一次添加,则需要扩容table容量为16,临界值(threshold)为12(16*0.75)
  5. 以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24,依次类推
  6. 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table表的大小 >= MIN_TERRIFY_CAPACITY(默认64),就会进行树化(转成红黑树)
package com.zanedu.map_;import java.util.HashMap;
@SuppressWarnings({"all"})
public class HashMapSource1 {public static void main(String[] args) {HashMap map = new HashMap();map.put("java", 10);//okmap.put("php", 10);//okmap.put("java", 20);//替换 valueSystem.out.println("map=" + map);/*解读1. 执行构造器 new HashMap()初始化加载因子 loadfactor = 0.75HashMap$Node[] table = null2. 执行put,会调用hash方法,计算key的hash值  h = key.hashCode()) ^ (h >>> 16)public V put(K key, V value) { //K = "java", value = 10return putVal(hash(key), key, value, false, true);}3. 执行 putValfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//辅助变量//如果底层的table数组为null,或者length = 0,就扩容到16if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//取出hash值对应的table表的索引位置的Node节点,如果为null,就直接把加入的k-v创建成一个Node,加入到该位置即可if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//辅助变量//如果table表的索引位置的key的hash值相同和新的key的hash值相同,//并且满足(table表中现有的节点的key和准备添加的key是同一个对象 || equals返回true)//就认为不能加入新的k-vif (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode) //如果当前的table已有的Node是红黑树,就按照红黑树的方式处理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);//加入后,判断当前链表的个数,是否已经到达8个,到8个后,就调用treeifyBin方法,进行红黑树的树化if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash && //如果在循环比较过程中,发现有相同的,就break,就只是替换下value((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value; //替换key对应的值afterNodeAccess(e);return oldValue;}}++modCount; //每增加一个Node,就size++if (++size > threshold) //[12-24-48],如果size > 临界值,就扩容resize();afterNodeInsertion(evict);return null;}5. 关于树化(转成红黑树)//如果table表为null,或者大小 < 64,就暂时不树化,而是进行扩容//否则才会真正的树化 -> 剪枝,即将树转换成链表final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();*/}
}
  • (1) 执行构造器 new HashMap(),初始化加载因子 loadfactor = 0.75,HashMap$Node[] table = null

img

  • (2)执行put,会调用hash方法,计算key的hash值 h = key.hashCode()) ^ (h >>> 16)

img

  • (3)执行 putVal

img

  • 补充:关于树化
  • 如果table表为null,或者大小 < 64,就暂时不树化,而是进行扩容,否则才会真正的树化 -> 剪枝 = 即将树转换成链表

img

  • 模拟HashMap触发扩容、树化的情况
package com.zanedu.map_;import java.util.HashMap;public class HashMapSource2 {public static void main(String[] args) {HashMap hashMap = new HashMap();for (int i = 1; i <= 12; i++) {hashMap.put(new A(i), "hello");}hashMap.put("aaa", "bbb");System.out.println(hashMap);//12个 k-v//验证table的扩容机制//0 -> 16(12) -> 32(24) -> 64(48) -> 128(96)}
}class A {private int num;public A(int num) {this.num = num;}//所有A对象的hashCode都是100
//    @Override
//    public int hashCode() {
//        return 100;
//    }@Overridepublic String toString() {return "A{" +"num=" + num +'}';}
}
  • 由于树化需要一条链表的数据为8,因此将其的hashCode重写,保证hash值一样,能在同一条链表上
  • 看一下扩容情况:
  • 已经在一条链表上加了8条数据,在加入数据看是扩容,还是树化?

img

  • 发现其table表数据没有64个,因此扩容

img

  • 继续添加,继续扩容

img

  • 当扩容到64个时,我们发现再次添加数据,会进行树化
  • 发现其多了prev、left、right等等变量

img

6.6 Map接口实现类-Hashtable

6.6.1 Hashtable的基本介绍
  1. 存放的元素是键值对:即 K-V
  2. Hashtable的键和值都不能为null,否则会抛出NullPointerException
  3. Hashtable的使用方法基本和HashMap一样
  4. Hashtable是线程安全的(synchronized),而HashMap是线程不安全的
package com.zanedu.map_;import java.util.Hashtable;
@SuppressWarnings("all")
public class HashTableExercise {public static void main(String[] args) {Hashtable table = new Hashtable();//oktable.put("john", 100); //ok//table.put(null, 100); //异常 NullPointerException//table.put("john", null);//异常 NullPointerExceptiontable.put("lucy", 100);//oktable.put("lic", 100);//oktable.put("lic", 88);//替换table.put("hello1", 1);table.put("hello2", 1);table.put("hello3", 1);table.put("hello4", 1);table.put("hello5", 1);table.put("hello6", 1);System.out.println(table);/*简单说明以下Hashtable的底层1. 底层有一个数组 Hashtable$Entry[] 初始化大小为112. threshold 临界值是 8 = 11 * 0.753. 扩容:按照自己的扩容机制来进行即可4. 执行方法  addEntry(hash, key, value, index); 添加一个K-V,封装到Entry5. 当 if (count >= threshold) 满足时,就进行扩容6. 按照 int newCapacity = (oldCapacity << 1) + 1;的大小扩容*/}
}
  • 看一下底层的扩容机制
  • 发现:初始化大小为11,并且之后扩容都是 *2+1

img

  • 扩容就是开辟一个以newCapcacity大小的新Entry数组

img

6.6.2 Hashtable和HashMap对比

img

6.7 Map接口实现类-Properties

6.7.1 基本介绍
  1. Properties类继承了Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据
  2. 他的使用特点和Hashtable类似
  3. Properties还可以用于从 xxx.properties文件中,加载数据到Properties类对象并进行读取和修改
6.7.2 基本使用
package com.zanedu.map_;import java.util.Properties;
@SuppressWarnings({"all"})
public class Properties_ {public static void main(String[] args) {//解读/*1. Properties 继承 Hashtable2. 可以通过 k-v 存放数据,当然 key 和 value不能为null*///增加Properties properties = new Properties();properties.put("john", 100);
//        properties.put(null, 100);//抛出空指针异常 NullPointerException
//        properties.put("john", null);//抛出空指针异常 NullPointerExceptionproperties.put("lucy", 100);properties.put("lic", 100);properties.put("lic", 80);//如果有相同的 key,value会被替换System.out.println(properties);//通过k 获取对应的valueSystem.out.println(properties.get("lic"));//80System.out.println(properties.getProperty("lic"));//80//删除properties.remove("lic");System.out.println(properties);//修改properties.put("john", "雨涵");System.out.println(properties);}
}

6.8 Map接口实现类-TreeMap

package com.zanedu.map_;import java.util.Comparator;
import java.util.TreeMap;
@SuppressWarnings({"all"})
public class TreeMap_ {public static void main(String[] args) {//使用默认的构造器创建 TreeMap,会默认排序,按照首字母排序//要求:/*按照传入的 k(String) 的大小进行排序按照传入的 k(String) 的长度来排序*/
//        TreeMap treeMap = new TreeMap();TreeMap treeMap = new TreeMap(new Comparator() {@Overridepublic int compare(Object o1, Object o2) {
//                return ((String)o2).compareTo((String)o1);return ((String)o1).length() - ((String)o2).length();}});treeMap.put("jack", "杰克");treeMap.put("tom", "汤姆");treeMap.put("kristina", "克瑞斯提诺");treeMap.put("smith", "斯密斯");treeMap.put("zan", "老张");//加入不了,因为是长度添加,相等为0了,直接返回,但是value值会被替换System.out.println(treeMap);/*解读源码1. 构造器,把传入的实现类Comparator接口的匿名内部类(对象),传给了TreeMap的comparatorpublic TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;}2. 调用put方法2.1第一次添加:把k-v 封装到 Entry对象,放入rootEntry<K,V> t = root;if (t == null) {compare(key, key); // type (and possibly null) check,检测是否为空,为空就抛异常root = new Entry<>(key, value, null);size = 1;modCount++;return null;}2.2 以后添加Comparator<? super K> cpr = comparator;if (cpr != null) {do { //遍历所有的key,给当前key找到适当的位置parent = t;cmp = cpr.compare(key, t.key);//动态绑定到我们的匿名内部类的compareif (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;else //如果遍历过程中,发现准备添加Key和当前已有的Key相等,就不添加,直接返回return t.setValue(value);} while (t != null);}*/}
}

七、总结-开发中如何选择集合实现类(记住)

  • 在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择

1)先判断存储的类型(一组对象[单列] 或 一组键值对[双列])

2)一组对象[单列]:Collection接口

允许重复:List

增删多:LinkedList(底层维护了一个双向链表)

改查多:ArrayList(底层维护Object类型的可变数组)

不允许重复:Set

无序:HashSet(底层是HashMap,维护了一个哈希表,即数组+链表+红黑树)

排序:TreeSet

插入和取出顺序一致:LinkedHashSet(维护数组+双向链表)

3)一组键值对[双列]:Map

键无序:HashMap(底层是哈希表,jdk7:数组+链表,jdk8:数组+链表+红黑树)

键排序:TreeMap

键插入和取出的顺序一致:LinkedHashMap

读取文件:Properties

八、Collections工具类

8.1 Collections工具类介绍

  1. Collections是一个操作 Set、List和Map等集合的工具类
  2. Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作

8.2 Collections各种方法演示

  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 处元素进行交换
  6. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  7. Object max(Collection, Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
  8. Object min(Collection)
  9. Object min(Collection, Object)
  10. int frequency(Collection, Object):返回指定集合中指定元素的出现次数
  11. void copy(List dest, List src):将src中的内容复制到dest中
  12. boolean replaceAll(List list, Object oldVal, Object newVal):使用新值来替换List对象的所有旧值
package com.zanedu.collections_;import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@SuppressWarnings({"all"})
public class Collections_ {public static void main(String[] args) {//创建ArrayList集合,用于测试List list = new ArrayList();list.add("tom");list.add("smith");list.add("king");list.add("milan");//        reverse(List):反转 List 中元素的顺序Collections.reverse(list);System.out.println(list);//        shuffle(List):对 List 集合元素进行随机排序
//        for (int i = 0; i < 5; i++) {
//            Collections.shuffle(list);
//            System.out.println(list);
//        }//        sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序Collections.sort(list);System.out.println("自然排序后=" + list);//        sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序//按照字符串的长度大小来排序Collections.sort(list, new Comparator() {@Overridepublic int compare(Object o1, Object o2) {//可以加入校验代码
//                if (o1 instanceof String) {
//
//                }return ((String)o1).length() - ((String)o2).length();}});System.out.println("按照字符串长度大小排序=" + list);//        swap(List, int,int):将指定 list 集合中的 i 处元素和 j 处元素进行交换Collections.swap(list, 0, 1);System.out.println("交换后=" + list);//Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素System.out.println(Collections.max(list));//Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素Object max = Collections.max(list, new Comparator() {@Overridepublic int compare(Object o1, Object o2) {return ((String)o1).length() - ((String)o2).length();}});System.out.println(max);//Object min(Collection)//Object min(Collection,Comparator)//参照上面min即可//int frequency(Collection,Object):返回指定集合中指定元素的出现次数System.out.println(Collections.frequency(list, "milan"));//void copy(List dest,List src):将src中的内容复制到dest中List dest = new ArrayList();//为了完成一个完整的拷贝,需要先给dest赋值,大小和list.size()一样for (int i = 0; i < list.size(); i++) {dest.add("");}//拷贝Collections.copy(dest, list);System.out.println(dest);//boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值//如果list中,有tom就替换成 汤姆Collections.replaceAll(list, "tom", "汤姆");System.out.println(list);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/391674.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java基础 之 equals和==的区别

文章目录 浅谈“”特点比较基本类型比较引用类型 浅谈“equals”背景和使用重写equals自定义类为什么需要重写equals方法 总结附录代码及文章推荐 前言&#xff1a; 1、8大基本数据类型&#xff0c;它们的值直接代表了某种数据&#xff0c;不是对象的实例&#xff0c;不能使用n…

关于企微群聊天工具功能的开发---PHP+JS+CSS+layui (手把手教学)

文章目录 前言准备工作PHP代码示例前端代码示例 主要是js踩的小坑&笔记最终达成的效果总结 前言 公司要求开发企微群聊天工具。首先一个客户一个群&#xff0c;其余群成员都是公司销售、设计师、工长、售后等人员。要求开发一个群聊天工具&#xff0c;工长点击进来以后就可…

ReentrantLock源码分析

文章目录 一、AQS1、state属性2、等待队列3、条件变量 二、ReentrantLock1、非公平锁实现原理1.1 获取锁1.2 释放锁1.3 可重入原理1.4 可打断原理不可打断可打断 1.5 公平锁实现原理1.6 条件变量原理awaitsignal 一、AQS AQS全称是 AbstractQueuedSynchronizer&#xff0c;是阻…

Python面试宝典第27题:全排列

题目 给定一个不含重复数字的数组nums&#xff0c;返回其所有可能的全排列 。备注&#xff1a;可以按任意顺序返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] 示例 2&#xff1a; 输…

FPGA开发——数码管的使用(二)

一、概述 在上一篇文章中我们针对单个数码管的静态显示和动态显示进行了一个设计和实现&#xff0c;这篇文章中我们针对多个数码管同时显示进行一个设计。这里和上一篇文章唯一不同的是就是数码管位选进行了一个改变&#xff0c;原来是单个数码管的显示&#xff0c;所以位选就直…

详细说明Java中Map和Set接口的使用方法

Map与Set的基本概念与场景 Map和set是一种专门用来进行搜索的容器或者数据结构&#xff0c;其搜索的效率与其具体的实例化子类有关。以前常见的搜索方式有&#xff1a; 1. 直接遍历&#xff0c;时间复杂度为O(N)&#xff0c;元素如果比较多效率会非常慢。 2. 二分查找&#x…

WordPress网站被入侵,劫持收录事件分析

7.15&#xff0c;网站被入侵&#xff0c;但是直到7月17日&#xff0c;我才发现被入侵。 16日&#xff0c;17日正常更新文章&#xff0c;17日查询网站收录数据时&#xff0c;在站长资源平台【流量与关键词】查询上&#xff0c;我发现了比较奇怪的关键词。 乱码关键词排名 起初…

JavaDS —— AVL树

前言 本文章将介绍 AVL 树的概念&#xff0c;重点介绍AVL 树的插入代码是如何实现的&#xff0c;如果大家对 AVL 树的删除&#xff08;还是和二叉搜索树一样使用的是替换删除法&#xff0c;然后需要判断是否进行旋转调整&#xff09;感兴趣的话&#xff0c;可以自行去翻阅其他…

关于Unity转微信小程序的流程记录

1.准备工作 1.unity微信小程序转换工具&#xff0c;minigame插件&#xff0c;导入后工具栏出现“微信小游戏" 2.微信开发者工具稳定版 3.MP微信公众平台申请微信小游戏&#xff0c;获得游戏appid 4.unity转webgl开发平台&#xff0c;Player Setting->Other Setting…

市场主流 AI 视频生成技术的迭代路径

AI视频生成技术的迭代路径经历了从GANVAE、Transformer、Diffusion Model到Sora采用的DiT架构&#xff08;TransformerDiffusion&#xff09;等多个阶段&#xff0c;每个阶段的技术升级都在视频处理质量上带来了飞跃性的提升。这些技术进步不仅推动了AI视频生成领域的快速发展&…

评估生成分子/对接分子的物理合理性工具 PoseBusters 评测

最近在一些分子生成或者对接模型中&#xff0c;出现了新的评估方法 PoseBusters&#xff0c;用于评估生成的分子或者对接的分子是否符合化学有效性和物理合理性。以往的分子生成&#xff0c;经常以生成分子的有效性、新颖性、化学空间分布&#xff0c;与口袋的结合力等方面进行…

微软蓝屏事件揭示的网络安全深层问题与未来应对策略

目录 微软蓝屏事件揭示的网络安全深层问题与未来应对策略 一、事件背景 二、事件影响 2.1、跨行业连锁反应 2.2、经济损失和社会混乱 三、揭示的网络安全问题 3.2、软件更新管理与风险评估 3.2、系统复杂性与依赖关系 3.3、网络安全意识与培训 四、未来的网络安全方向…

网络云相册实现--nodejs后端+vue3前端

目录 主页面 功能简介 系统简介 api 数据库表结构 代码目录 运行命令 主要代码 server apis.js encry.js mysql.js upload.js client3 index.js 完整代码 主页面 功能简介 多用户系统&#xff0c;用户可以在系统中注册、登录及管理自己的账号、相册及照片。 每…

众人帮蚂蚁帮任务平台修复版源码,含搭建教程。

全修复运营版本的任务平台&#xff0c;支持垂直领域细分&#xff0c;定向导流&#xff0c;带有排行榜功能&#xff0c;任务发布上传审核&#xff0c;用户信用等级&#xff0c;充值接口等等均完美可用。支付对接Z支付免签接口&#xff0c;环境配置及安装教程都已经打包。 搭建环…

C语言调试宏全面总结(六大板块)

C语言调试宏进阶篇&#xff1a;实用指南与案例解析C语言调试宏高级技巧与最佳实践C语言调试宏的深度探索与性能考量C语言调试宏在嵌入式系统中的应用与挑战C语言调试宏在多线程环境中的应用与策略C语言调试宏在并发编程中的高级应用 C语言调试宏进阶篇&#xff1a;实用指南与案…

【Linux】网络基础_4

文章目录 十、网络基础5. socket编程网络翻译服务 未完待续 十、网络基础 5. socket编程 网络翻译服务 基于UDP&#xff0c;我们实现一个简单的翻译。 我们导入之前写的代码&#xff1a; InetAddr.hpp&#xff1a; #pragma once#include <iostream> #include <sys…

开源智能低代码自动化助手:Obsei

**Obsei&#xff1a;**低代码&#xff0c;高效率&#xff0c;Obsei让AI自动化触手可及。- 精选真开源&#xff0c;释放新价值。 概览 Obsei是一款开源的低代码人工智能自动化工具&#xff0c;它为企业提供了一套灵活的解决方案&#xff0c;以应对日益增长的数据处理需求。该工…

uvm_config_db 和 uvm_resource_db :

uvm_config_db class my_driver extends uvm_driver;int my_param;function new(string name, uvm_component parent);super.new(name, parent);endfunctionvirtual task run_phase(uvm_phase phase);// 在组件内部获取配置值if (!uvm_config_db#(int)::get(this, ""…

[Git][远程操作]详细讲解

1.理解分布式版本控制系统 形象理解&#xff1a;每个⼈的电脑上都是⼀个完整的版本库 这样⼯作的时候&#xff0c;就不需要联⽹了&#xff0c; 因为版本库就在⾃⼰的电脑上 既然每个⼈电脑上都有⼀个完整的版本库&#xff0c;那多个⼈如何协作呢&#xff1f; 例如&#xff1a;…

ajax图书管理项目

bootstrap弹框 不离开当前页面&#xff0c;显示单独内容&#xff0c;让用户操作 功能&#xff1a;不离开当前页面&#xff0c;显示单独内容&#xff0c;供用户操作步骤&#xff1a; 1.引入bootstrap.css和bootstrap.js …