跳到主要内容

第17章-3-集合

概述

  • 集合是Java中重要的内容,是Java数据结构的实现

  • 针对应用中数据处理常见的高频操作进行封装,操作简单

  • 可分为两个系列:Collection系列和Map系列

  • 这两个系列的很多类,都有着相应的存储结构,常见存储结构有

    集合-常用存储结构

Collection系列

提问:数组使用过程中,有没有什么痛点?

:容量固定,不可扩充容量。一般的业务中,都有连续存储相同系列数据的需要,但数量是动态的,或是不确定的

概述

  • Collection是Java中提供的一种容器,可以用来存放多个数据

  • Collection中数据的数据类型只能是引用数据类型,数据个数可动态扩容

  • 应用场景:

    • 现在想要一个存放班级学员的容器,人来来去去的,人数是不确定的,这个人员需要一个动态的容器存储,如下图

      集合-应用场景

    • 一个订单类,不同的人下的订单,里面的产品数量也不相同,这个订单的产品需要一个动态的容器存储

    • ...

  • 与数组对比

    • 数组是固定大小,Collection集合则是动态大小,自动扩容
    • 数组支持基本数据类型和引用数据类型,Collection集合则只支持引用数据类型
  • 在实际开发过程中,更多的使用Collection集合类,并结合Lambda一起使用

  • Collection集合中的所有类和接口都派生于Collection接口

  • 主要分为可重复集合不可重复集合两类

Collection关系图谱

集合-类图

Collection集合关系图谱

常用Collection类和接口

  • Collection接口,集合类的父接口,定义了一系列通用方法
    • List接口,可重复集合的父接口,扩展了一系列方法,可通过索引精确访问元素,数据可重复
      • Vector类,早期旧版集合类,效率较低,但线程安全,现使用ArrayList可完全替代
      • ArrayList类,使用数组存储元素的集合类,能自动扩容;是最常用的集合类
      • LinkedList类,使用链表存储元素的集合类,能自动扩容
    • Set接口,不可重复集合父接口
      • SortedSet接口,提供一个有序的不可重复的接口
        • TreeSet类,SortedSet的具体实现类,底层数据结构是红黑树(一个自平衡的二叉树)
      • HashSet类,基于HashMap实现的,通过哈希算法决定元素存储位置,存取速度快,但是无序的
        • LinkedHashSet类,继承于HashSet,具备HashSet特点,另外通过一个链表维护数据的顺序,可以是有序的,面对大量数据的遍历性能要好

List

概述

  • 是实际开发中常用的Collection集合类
  • 常用的子类有ArrayList、LinkedList
  • ArrayList特点
    • 使用数组存储,有序,可重复
    • 只支持引用数据类型
    • 初始容量(initialCapacity)为10
    • 超过长度后,后续每次按原有容量的50%进行扩容;由于扩容是通过重新申请一个新的数组,然后将旧数组内容拷贝到新数组完成,所以建议如果能预估其容量,尽量一步到位参考最大容量设置初始容量值
    • 线程不安全
    • 提供了很多便利的操作方法
  • LinkedList特点
    • 使用双向链表存储,有序,可重复
    • 只支持引用数组类型
    • 没有初始容量的需要
    • 没有扩容的机制,只要内存足够,就能存储更多的元素
    • 线程不安全
    • 提供了很多便利的操作方法
  • 建议:如果是查询多,插入、删除少,使用ArrayList;如果是插入、删除多,查询少,使用LinkedList

List常用方法

  • size方法,集合长度
  • add方法,往集合中添加一个元素
  • get方法,获取指定下标的元素,下标从0开始
  • remove方法,删除指定下标的数据,也可以删除某个指定元素
  • set方法,修改指定位置的元素
  • contains方法,是否包含某个元素
  • isEmpty方法,集合是否为空,即不包含一个元素
  • ...

注意:后面示例主要使用ArrayList,如果需要,都可以使用LinkedList进行替换

ArrayList

源码解析

​ 注意:模拟的是JDK1.7的代码,其他版本的略有不同

  • 具有两个私有属性,分别是数组和大小

    /**
    * 模拟ArrayList集合
    */
    public class ArrayList<E> {
    /**
    * 定义一个数组,用于保存集合中的元素
    */
    private Object[] elementData;

    /**
    * 定义一个变量,用于保存实际添加元素的个数
    */
    private int size;
    }
  • 具备两个构造方法

    /**
    * 模拟ArrayList集合
    */
    public class ArrayList<E> {
    ...省略私有属性...

    /**
    * 无参构造方法(默认设置elementData数组的空间长度为10)
    */
    public ArrayList() {
    this(10);
    }

    /**
    * 有参构造方法(指定设置elementData数组的空间长度)
    * @param cap 设置数组的空间长度
    */
    public ArrayList(int cap) {
    // 1.判断cap不合法的情况
    if (cap < 0) {
    throw new IllegalArgumentException("参数不合法,cap:" + cap);
    }
    // 2.执行此处,证明cap合法,则对elementData做实例化操作
    this.elementData = new Object[cap];
    }
    }
  • 具备添加元素和扩容方法

    /**
    * 模拟ArrayList集合
    */
    public class ArrayList<E> {
    ...省略私有属性和构造方法...

    /**
    * 添加元素操作
    * @param element 需要添加的元素
    */
    public void add(E element) {
    // 1.判断elementData数组是否需要扩容
    ensureCapacityInternal();
    // 2.把element添加进入elementData数组中
    elementData[size] = element;
    // 3.添加元素之后,size执行递增操作
    size++;
    }

    /**
    * 判断数组是否需要扩容
    */
    private void ensureCapacityInternal() {
    // 1.当数组的空间长度和实际存放元素个数相同时,则执行扩容操作
    if (elementData.length == size) {
    // 2.创建一个比elementData数组长度更大的新数组
    Object[] newArr = new Object[size + (size >> 1)];
    // 3.把elementData数组中的元素依次的拷贝进入newArr数组中
    System.arraycopy(elementData, 0, newArr, 0, size);
    // 4.让elementData指向扩容后的新数组
    this.elementData = newArr;
    }
    }
    }
  • 具备获取元素方法;先检查索引值index是否合法,此处index的合法索引取值范围在[0, size - 1]之间,然后执行获取元素的操作

    /**
    * 模拟ArrayList集合
    */
    public class ArrayList<E> {
    ...省略私有属性和构造方法...

    /**
    * 根据索引获取元素
    * @param index 索引值
    * @return 返回index所对应的元素值
    */
    public E get(int index) {
    // 1.判断index索引是否合法,合法的索引取值范围在[0, size-1]之间
    if (index < 0 || index >= size) {
    throw new ArrayIndexOutOfBoundsException("数组索引越界异常");
    }
    // 2.执行到此处,证明index取值肯定合法,数组的元素值
    return (E) elementData[index];
    }

    /**
    * 获取集合中实际添加元素的个数
    * @return
    */
    public int size() {
    return this.size;
    }
    }
  • 具备修改元素方法,先检查索引值index是否合法,此处index的合法索引取值范围在[0, size-1]之间,然后执行修改操作

    /**
    * 模拟ArrayList集合
    */
    public class ArrayList<E> {
    ...省略私有属性和构造方法...
    /**
    * 根据索引修改元素
    * @param index 修改的索引位置
    * @param element 修改后的元素值
    * @return 返回被修改的元素值
    */
    public E set(int index, E element) {
    // 1.判断index索引是否合法,合法的索引取值范围在[0, size-1]之间
    if (index < 0 || index >= size) {
    throw new ArrayIndexOutOfBoundsException("数组索引越界异常");
    }
    // 2.获取被替换的元素
    Object oldValue = elementData[index];
    // 3.修改索引为index的元素
    elementData[index] = element;
    // 4.返回替换元素的值
    return (E) oldValue;
    }
    }
  • 具备插入元素方法;先检查传入的index索引是否合法(此处index的合法取值范围在[0, size]之间),然后判断数组是否需要扩容,接着将插入索引及其之后的元素往后挪动一位,最后再插入元素

    /**
    * 模拟ArrayList集合
    */
    public class ArrayList<E> {
    ...省略私有属性和构造方法...
    /**
    * 根据索引插入元素
    * @param index 插入的索引位置
    * @param element 插入的元素值
    */
    public void add(int index, E element) {
    // 1. 判断index索引是否合法,合法的索引取值范围在[0, size]之间
    if(index < 0 || index > this.size)
    throw new IndexOutOfBoundsException("插入索引越界");
    // 2.检查数组是否需要扩容
    ensureCapacityInternal();
    // 3.将插入索引及其之后的元素完后挪动一位
    System.arraycopy(elementData, index,elementData, index + 1, size - index);
    // 4.在指定索引位置插入元素
    elementData[index] = element;
    // 5.实际添加元素个数+1
    size++;
    }
    }
  • 具备根据索引删除元素方法;首先先判断索引是否合法,然后将删除索引之后的元素依次往前挪动一位,接着把数组中最后一个元素设置为默认值(null)

    /**
    * 模拟ArrayList集合
    */
    public class ArrayList<E> {
    ...省略私有属性和构造方法...

    /**
    * 根据索引删除元素
    * @param index 删除的索引位置
    * @return 返回被删除的元素值
    */
    public E remove(int index) {
    // 1.判断index索引是否合法,合法的索引取值范围在[0, size-1]之间
    if (index < 0 || index >= size) {
    throw new ArrayIndexOutOfBoundsException("数组索引越界异常");
    }
    // 2.获取删除索引对应的元素
    Object oldValue = elementData[index];
    // 3.删除索引为index的元素
    fastRemove(index);
    // 4.返回被删除的元素
    return (E) oldValue;
    }
    /**
    * 删除索引为index的元素
    * @param index 删除的索引位置
    */
    private void fastRemove(int index) {
    // 1.将删除索引之后的元素依次往后挪动一位
    System.arraycopy(elementData, index + 1, elementData, index, size - index - 1);
    // 2.把最后一个元素设置为默认值
    elementData[size - 1] = null;
    // 3.删除元素后,则实际添加元素个数-1
    size--;
    }
    }
  • 具备删除指定元素方法;首先找到删除元素在数组中所在的索引位置,如果没有找到则证明移除失败,如果找到则进行根据索引来移除元素的操作

    /**
    * 模拟ArrayList集合
    */
    public class ArrayList<E> {
    ...省略私有属性和构造方法...

    /**
    * 删除指定的某个元素
    * @param element 要删除的元素
    * @return 返回true,代表删除成功;返回false,代表删除失败
    */
    public boolean remove(E element) {
    // 因为集合中可以存放null,所以判断之前需判断元素是否为null
    if(element == null) {
    for (int index = 0; index < size; index++)
    // 如果遍历出来的元素为null,则执行删除操作
    if (elementData[index] == null) {
    fastRemove(index);
    return true;
    }
    }else{
    for (int index = 0; index < size; index++)
    // 如果遍历出来元素和删除元素相等,则执行删除操作
    if (element.equals(elementData[index])) {
    fastRemove(index);
    return true;
    }
    }
    // 执行到此处,则意味着删除元素失败,那么返回false
    return false;
    }

    /**
    * 删除索引为index的元素
    * @param index 删除的索引位置
    */
    private void fastRemove(int index) {
    // 1.将删除索引之后的元素依次往后挪动一位
    System.arraycopy(elementData, index + 1, elementData, index, size - index - 1);
    // 2.把最后一个元素设置为默认值
    elementData[size - 1] = null;
    // 3.删除元素后,则实际添加元素个数-1
    size--;
    }
    }
定义和使用
  • 方式1,普通方式,支持任意Object类型的对象

    实例代码

    package com.bjpowernode.demo;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * Collection集合类示例
    */
    public class CollectionDemo {
    public static void main(String[] args) {
    //定义
    //支持任意Object类型的对象
    ArrayList datas = new ArrayList();

    //使用-设置
    //添加整数,会将整数字面量装箱成Integer包装类,然后添加到集合
    datas.add(1);
    //添加小数,会将整数字面量装箱成Double包装类,然后添加到集合
    datas.add(0.1);
    //添加字符串
    datas.add("abc");
    //添加自定义类型对象
    datas.add(new Student());

    //使用-获取
    Object obj = datas.get(0); //正确
    Double doubleReferenc = datas.get(1); //错误,不支持从Object到Double类型的自动类型转换
    double doubleVariable = datas.get(1); //错误,不支持从Object到double基本数据类型的转换
    String stringReferenct = datas.get(2); //错误,不支持从Object到String引用数据类型的转换
    Student student = datas.get(3); //错误,不支持从Object到Student引用数据类型的转换
    }
    }
  • 方式2,泛型方式常用方式,只支持指定类型的对象

    实例代码

    package com.bjpowernode.demo;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * Collection集合类示例
    */
    public class CollectionDemo {
    public static void main(String[] args) {
    //定义
    //定义只支持Integer类型的集合
    List<Integer> integerDatas = new ArrayList <>();
    //定义只支持String类型的集合
    List<String> stringDatas = new ArrayList <>();
    //定义只支持自定义类型的集合
    List<Student> students = new ArrayList <>();

    //使用-设置
    integerDatas.add(1); //正确
    integerDatas.add(new Integer(2)); //正确
    integerDatas.add(3.3); //错误,无法将double自动转型成Integer

    stringDatas.add("abc"); //正确
    stringDatas.add(new String("abc")); //正确
    stringDatas.add(123); //错误,无法将int自动转型成String

    students.add(new Student()); //正确
    students.add(new Bird()); //错误,无法将Bird类型自动转型成Student

    //使用-获取
    Integer integerReference = integerDatas.get(0); //正确
    int intVariable = integerDatas.get(0); //正确
    Long longReference = integerDatas.get(0); //错误,无法将包装类Integer转换成Long
    long longVariable = integerDatas.get(0); //正确,会里德拆箱,并兼容

    String stringReference = stringDatas.get(0); //正确

    Student student = students.get(0); //正确
    Object object = students.get(0); //正确,会进行自动类型转换
    Bird bird = students.get(0); //错误,无法将Student类型转换成Bird类型
    }
    }
应用实例
  • 实例1,使用for循环遍历;汇总整型数据

    实例代码

    package com.bjpowernode.demo;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * ArrayList示例
    */
    public class ArrayListDemo {
    public static void main(String[] args) {
    //定义ArrayList对象
    List <Integer> datas = new ArrayList <>();

    //添加数据
    datas.add(11);
    datas.add(22);
    datas.add(33);
    datas.add(99);

    //遍历汇总
    int sum = 0;
    for (int index = 0; index < datas.size(); index++) {
    sum += datas.get(index);
    }

    //输出
    System.out.println("汇总结果:" + sum);
    }
    }

    输出结果,运行查看结果

  • 实例2,使用foreach循环遍历;汇总国外订单中的产品总数

    实例代码

    package com.bjpowernode.demo;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Scanner;

    /**
    * 订单类,汇总海外订单的产品总数
    */
    public class Order {
    private Integer no; //订单编号
    private Integer productCount; //订单产品数量
    private String remark; //订单备注
    private Boolean isForeign; //是否为国外订单

    /**
    * 入口方法
    *
    * @param args
    */
    public static void main(String[] args) {
    //定义标准输入对象
    Scanner scanner = new Scanner(System.in);
    //定义接收数据的集合
    List <Order> orders = new ArrayList <>();

    //接收用户输入,直到输出的产品编号小于0为止
    //定义订单引用,接收用户输入
    Order order;
    while (true) {
    System.out.println("------------请输入产品信息------------");
    order = new Order();
    System.out.print("订单编号:");
    order.setNo(scanner.nextInt());
    //如果订单编号小于0,退出循环;否则,将输入产品添加到集合
    if (order.getNo() != null && order.getNo().intValue() < 0) {
    break;
    } else {
    orders.add(order);
    }
    System.out.print("产品数量:");
    order.setProductCount(scanner.nextInt());
    System.out.print("备注:");
    order.setRemark(scanner.next());
    System.out.print("是否国外订单?");
    order.setForeign(scanner.nextBoolean());
    }

    //遍历汇总,其实也可以在输入的时候汇总
    int foreignProductSum = 0;
    for (Order item : orders) {
    if (item.getForeign() == true) {
    foreignProductSum += item.getProductCount();
    }
    }

    System.out.println("------------国外订单汇总信息------------");
    //输出
    System.out.println("国外订单产品总数:" + foreignProductSum);
    }

    public Integer getNo() {
    return no;
    }

    public void setNo(Integer no) {
    this.no = no;
    }

    public Integer getProductCount() {
    return productCount;
    }

    public void setProductCount(Integer productCount) {
    this.productCount = productCount;
    }

    public String getRemark() {
    return remark;
    }

    public void setRemark(String remark) {
    this.remark = remark;
    }

    public Boolean getForeign() {
    return isForeign;
    }

    public void setForeign(Boolean foreign) {
    isForeign = foreign;
    }
    }

    输出结果,运行查看结果

  • 实例3,常用方法的使用

    学生类

    package com.bjpowernode.listdemo;

    /**
    * 学生类
    */
    public class Student {
    private Integer id;
    private String name;

    public Student(Integer id, String name) {
    this.id = id;
    this.name = name;
    }

    public Integer getId() {
    return id;
    }

    public void setId(Integer id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

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

    ArrayList常用方法使用

    package com.bjpowernode.listdemo;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * ArrayList常用方法使用
    */
    public class ArrayListDemo {
    public static void main(String[] args) {
    //定义ArrayList
    List<Student> students = new ArrayList<>();

    //初始化数据,【add方法】
    Student student = new Student(1,"张三");
    students.add(student);
    student = new Student(2,"李四");
    students.add(student);
    student = new Student(3,"王五");
    students.add(student);

    //输出列表内容
    System.out.println("----------初始内容----------");
    System.out.println(students);

    //【size方法】
    System.out.println("----------size----------");
    System.out.println(students.size());

    //【get方法】
    System.out.println("----------get----------");
    //获取第1个元素并输出
    Student temp = students.get(0);
    System.out.println(temp.toString());
    //获取第3个元素并输出
    temp = students.get(2);
    System.out.println(temp.toString());

    //【remove方法】
    System.out.println("----------remove----------");
    //根据序号删除
    System.out.println("----根据序号删除----");
    System.out.println("【删除前数据】");
    System.out.println(students);
    students.remove(0);
    System.out.println("【删除后数据】");
    System.out.println(students);
    //根据对象删除
    System.out.println("----根据对象删除(错误方式)----");
    System.out.println("【删除前数据】");
    System.out.println(students);
    //能不能删除?
    Student zs = new Student(2,"李四");
    students.remove(zs);
    System.out.println("【删除后数据】");
    System.out.println(students);
    System.out.println("----根据对象删除(正确方式)----");
    System.out.println("【删除前数据】");
    System.out.println(students);
    //能不能删除?
    Student ls = students.get(0);
    students.remove(ls);
    System.out.println("【删除后数据】");
    System.out.println(students);

    //【set方法】
    System.out.println("----------set----------");
    System.out.println("【修改前数据】");
    System.out.println(students);
    temp = new Student(9,"王小波");
    students.set(0,temp);
    System.out.println("【修改后数据】");
    System.out.println(students);

    //【contains方法】
    System.out.println("----------contains----------");
    System.out.println("----根据对象包含(正确方式)----");
    boolean result = students.contains(temp);
    System.out.println("是否包含:"+result);
    System.out.println("----根据对象包含(错误方式)----");
    temp = new Student(9,"王小波");
    result = students.contains(temp);
    System.out.println("是否包含:"+result);

    //【isEmpty方法】
    System.out.println("----------isEmpty方法----------");
    System.out.println("是否为空:"+students.isEmpty());
    students.clear(); //清除所有数据
    System.out.println("是否为空:"+students.isEmpty());
    }
    }

【练习】

  1. 练习应用实例内容,完成代码编写
  2. 【扩展】重构程序,将第16章-包装类中的练习产品和仓库进行重构,要求如下
    1. 产品列表使用List类型
    2. 添加方法:
      1. 新增产品,形参为Product类对象,返回新增是否成功;注意,如果产品编号已经存在,则不能新增
      2. 修改产品,形参为Product类对象,返回修改是否成功;
      3. 删除产品,形参为产品编号,返回删除是否成功
    3. 测试上述方法

LinkedList【扩展】

链式存储结构
  • 单链表

    • 单链表采用的是链式存储结构,使用一组地址任意的存储单元来存放数据元素

    • 在单链表中,存储的每一条数据都是以节点来表示的,每个节点的构成为:元素(存储数据的存储单元) + 指针(存储下一个节点的地址值)

    • 单链表的节点结构如下图所示

      集合-Collection-LinkedList-链式存储结构-1

    • 另外,单链表中的开始节点,我们又称之为首节点;单链表中的终端节点,我们又称之为尾节点。如下图所示

      集合-Collection-LinkedList-链式存储结构-2

    • 根据序号获取节点操作

      ​ 在线性表中,每个节点都有一个唯一的序号,该序号是从0开始递增的。通过序号获取单链表的节点时,我们需要从单链表的首节点开始,从前往后循环遍历,直到遇到查询序号所对应的节点时为止 ​ 以下图为例,我们需要获得序号为2的节点,那么就需要依次遍历获得“节点11”和“节点22”,然后才能获得序号为2的节点,也就是“节点33”

      ​ 在链表中通过序号获得节点的操作效率是非常低的

      集合-Collection-LinkedList-链式存储结构-3

    • 根据序号删除节点操作

      ​ 根据序号删除节点的操作,我们首先应该根据序号获得需要删除的节点,然后让“删除节点的前一个节点”指向“删除节点的后一个节点”,这样就实现了节点的删除操作 ​ 以下图为例,我们需要删除序号为2的节点,那么就让“节点22”指向“节点44”即可,这样就删除了序号为2的节点,也就是删除了“节点33”

      ​ 通过序号来插入节点,时间主要浪费在找正确的删除位置上。但是,单论删除的操作,也就是无需考虑定位到删除节点的位置

      集合-Collection-LinkedList-链式存储结构-4

    • 根据序号插入节点操作

      ​ 根据序号插入节点的操作,我们首先应该根据序号找到插入的节点位置,然后让“插入位置的上一个节点”指向“新插入的节点”,然后再让“新插入的节点”指向“插入位置的节点”,这样就实现了节点的插入操作。 ​ 以下图为例,我们需要在序号为2的位置插入元素值“00”,首先先把字符串“00”封装为一个节点对象,然后就让“节点22”指向“新节点00”,最后再让“节点00”指向“节点33”,这样就插入了一个新节点。

      ​ 通过序号来插入节点,时间主要浪费在找正确的插入位置上。但是,单论插入的操作,也就是无需考虑定位到插入节点的位置

      集合-Collection-LinkedList-链式存储结构-5

  • 双链表

    • 双链表也叫双向链表,它依旧采用的是链式存储结构。在双链表中,每个节点中都有两个指针,分别指向直接前驱节点(保存前一个节点的地址值)和直接后继节点(保存后一个节点的地址值),如下图所示

      集合-Collection-LinkedList-链式存储结构-6

    • 所以,从双链表中的任意一个节点开始,都可以很方便地访问它的直接前驱节点和直接后继节点,如下图所示

      集合-Collection-LinkedList-链式存储结构-7

  • 单链表和双链表的区别

    • 单链表:对于一个节点,有储存数据的data和指向下一个节点的next。也就是说,单链表的遍历操作都得通过前节点—>后节点
    • 双链表:对于一个节点,有储存数据的data和指向下一个节点的next,还有一个指向前一个节点的pre。也就是说,双链表不但可以通过前节点—>后节点,还可以通过后节点—>前节点
  • 环形链表

    • 环形链表依旧采用的是链式存储结构,它的特点就是设置首节点和尾节点相互指向,从而实现让整个链表形成一个环。在我们实际开发中,常见的环形链表有

      • 环形单链表,在单链表中,尾节点的指针指向了首节点,从而整个链表形成一个环,如下图所示

        集合-Collection-LinkedList-链式存储结构-9

      • 环形双链表,在双链表中,尾节点的指针指向了首节点,首节点的指针指向了尾节点,从而整个链表形成一个环,如下图所示

        集合-Collection-LinkedList-链式存储结构-10

源码解析
  • LinkedList集合底层采用双向链表实现的存储,也就意味着LinkedList集合采用的是链式存储结构。在双链表中,每个节点中都有两个指针,分别指向前一个节点(保存前一个节点的地址值)和后一个节点(保存后一个节点的地址值)

  • LinkedList集合的特点为:查询效率低,增删效率高,线程不安全。我们打开LinkedList源码,可以看到里面包含了双向链表的相关代码

    集合-Collection-LinkedList-LinkedList源码解析-1

  • 源码中Node类中包含了item、next和prev三个成员变量,其中item保存了节点中存储的数据,next和prev分别指向了后一个节点和前一个节点,则意味着Node类就是双向链表的节点类

  • 将接下来,我们再继续看LinkeList集合包含了哪些属性,源码如下

    源码中first属性保存的就是双链表的首节点,last属性保存的就是双链表的尾结点,而size属性保存了双链表实际存放元素的个数。

    因此,对LinkedList集合的操作,其实就是对双链表做的操作,下面我们来模拟LinkedList集合的底层实现

    集合-Collection-LinkedList-LinkedList源码解析-2

  • 因为LinkedList类属于Deque接口的实现类,也就意味着LinkedList集合对比List接口新增了一些方法,这些新增的方法都是实现于Deque接口的方法,新增的部分方法如下

    集合-Collection-LinkedList-LinkedList源码解析-3

  • 节点类的定义,双链表节点类很简单,item用于存储节点中的数据,prev用于存储前一个节点的地址值,next用于存放前后一个节点的地址

    /**
    * 模拟LinkedList集合
    */
    public class LinkedList<E> {
    /**
    * 节点类
    * @param <E> 指的就是节点中存储“元素”的类型
    */
    private static class Node<E> {
    /**
    * 定义一个变量,用于保存前一个节点的地址值
    */
    private Node<E> prev;

    /**
    * 定义一个变量,用于保存节点中的数据
    */
    private E item;

    /**
    * 定义一个变量,用于保存后一个节点的地址值
    */
    private Node<E> next;

    /**
    * 有参构造方法(为item、prev和next做初始化)
    * @param prev
    * @param item
    * @param next
    */
    public Node(Node<E> prev, E item, Node<E> next) {
    this.item = item;
    this.prev = prev;
    this.next = next;
    }
    }
    }
  • 私有属性,共3个

    /**
    * 模拟LinkedList集合
    */
    public class LinkedList<E> {
    /**
    * 存储双链表首节点
    */
    private Node<E> firstNode;
    /**
    * 存储双链表尾节点
    */
    private Node<E> lastNode;
    /**
    * 存储实际添加元素的个数
    */
    private int size;

    /**
    * 获得实际添加元素的个数
    * @return
    */
    public int size() {
    return size;
    }
    ...省略双链表节点类...
    }
  • 添加元素方法,在链表的尾部追加一个节点,但是要注意判断当前链表是否为空链表

    /**
    * 模拟LinkedList集合
    */
    public class LinkedList<E> {
    ...省略私有属性和构造方法...
    /**
    * 添加元素
    * @param element
    */
    public void add(E element) {
    // 1.把元素内容包装为一个节点对象
    Node<E> node = new Node<>(null, element, null);
    // 2.如果双链表不存在尾结点,则证明双链表还不存在
    if (lastNode == null) {
    // 把node节点设置为首节点
    firstNode = node;
    }
    // 3.如果双链表中存在尾节点,那么把尾节点和node链接起来
    else {
    // 把尾节点和node链接起来
    lastNode.next = node;
    node.prev = lastNode;
    }
    // 4.更新双链表的尾节点
    lastNode = node;
    // 5.设置实际添加元素个数+1
    size++;
    }
    ...省略双链表节点类..
    }
  • 获取元素方法,获取双链表中指定序号(此处序号类似于数组的索引)的元素,首先需要判断序号是否合法,然后通过循环来找到对应序号的节点,从而拿到节点中存放的内容

    /**
    * 模拟LinkedList集合
    */
    public class LinkedList<E> {
    ...省略私有属性和构造方法...
    /**
    * 根据序号获得元素
    * @param index 序号值
    * @return 返回序号对应的元素
    */
    public E get(int index) {
    // 1.检查index的取值是否合法
    if (index < 0 || index >= size)
    throw new IndexOutOfBoundsException("链表序号越界异常");
    // 2.获取元素节点的内容
    return node(index).item;
    }

    /**
    * 根据序号获得节点对象
    * @param index 序号
    * @return 返回index对应的节点对象
    */
    private Node<E> node(int index) {
    // 1.如果索引在前半区,则“从前往后”开始查找
    if (index < (size >> 1)) {
    // 1.1准备开始从首节点开始查找
    Node<E> currentNode = firstNode;
    // 1.2从前往后遍历节点,一直到index所在位置
    for (int i = 0; i < index; i++)
    // 获得当前节点的下一个节点
    currentNode = currentNode.next;
    // 1.3返回找到的节点
    return currentNode;
    }
    // 2.如果索引在后半区,则“从后往前”开始查找
    else {
    // 2.1准备开始从尾节点开始查找
    Node<E> currentNode = lastNode;
    // 2.2从后往前遍历节点,一直到index所在位置
    for (int i = size - 1; i > index; i--)
    // 获得当前节点的下一个节点
    currentNode = currentNode.prev;
    // 2.3返回找到的节点
    return currentNode;
    }
    }
    ...省略双链表节点类..
    }
  • 修改元素方法,根据序号修改元素,首先判断序号取值是否合法,然后通过循环来找到对应序号位置的节点,最后再修改节点中存放的内容

    /**
    * 模拟LinkedList集合
    */
    public class LinkedList<E> {
    ...省略私有属性和构造方法...
    /**
    * 根据序号修改元素
    * @param index 序号
    * @param element 元素
    * @return 返回被修改的元素值
    */
    public E set(int index, E element) {
    // 1.检查index的取值是否合法
    if (index < 0 || index >= size)
    throw new IndexOutOfBoundsException("链表序号越界异常");
    // 2.根据索引获取元素节点
    Node<E> node = node(index);
    // 3.获取节点以前存放的内容
    E oldVal = node.item;
    // 4.修改节点中存放的内容
    node.item = element;
    // 5.返回节点修改之前的内容
    return oldVal;
    }

    /**
    * 根据序号获得节点对象
    * @param index 序号
    * @return 返回index对应的节点对象
    */
    private Node<E> node(int index) {
    // 1.如果索引在前半区,则“从前往后”开始查找
    if (index < (size >> 1)) {
    // 1.1准备开始从首节点开始查找
    Node<E> currentNode = firstNode;
    // 1.2从前往后遍历节点,一直到index所在位置
    for (int i = 0; i < index; i++)
    // 获得当前节点的下一个节点
    currentNode = currentNode.next;
    // 1.3返回找到的节点
    return currentNode;
    }
    // 2.如果索引在后半区,则“从后往前”开始查找
    else {
    // 2.1准备开始从尾节点开始查找
    Node<E> currentNode = lastNode;
    // 2.2从后往前遍历节点,一直到index所在位置
    for (int i = size - 1; i > index; i--)
    // 获得当前节点的下一个节点
    currentNode = currentNode.prev;
    // 2.3返回找到的节点
    return currentNode;
    }
    }
    ...省略双链表节点类..
    }
  • 插入元素方法,在链表中指定序号位置插入元素,首先判断序号的取值是否合法,然后通过循环来找到对应索引位置的节点,最后执行插入操作,而插入操作核心就是修改节点之间的指向

    /**
    * 模拟LinkedList集合
    */
    public class LinkedList<E> {
    ...省略私有属性和构造方法...
    /**
    * 根据序号插入元素
    * @param index 序号
    * @param element 插入的元素
    */
    public void add(int index, E element) {
    // 1.检查index取值是否合法
    if (index < 0 || index > size)
    throw new IndexOutOfBoundsException("链表序号越界异常");
    // 2.节点插入操作
    // 2.1如果index和size相等,那么意味着添加到双链表末尾
    if (index == size) {
    // 2.2调用add()方法,将元素添加到集合的末尾
    add(element);
    }
    // 2.2如果index和size不相等,则进行插入操作
    else {
    // 2.3根据索引获取链表中的节点
    Node<E> node = node(index);
    // 2.4进行插入操作
    linkBefore(element, node);
    // 2.5链表中节点递增
    size++;
    }
    }

    /**
    * 插入节点操作
    * @param element 插入的元素
    * @param targetNode 目标节点(插入位置节点)
    */
    public void linkBefore(E element, Node<E> targetNode) {
    // 1.获取目标节点的上一个节点
    Node<E> preNode = targetNode.prev;
    // 2.把元素内容包装为一个节点对象
    Node<E> newNode = new Node<>(preNode, element, targetNode);
    // 3.把targetNode节点的prev指向newNode
    targetNode.prev = newNode;
    // 4.把preNode的next指向newNode
    // 4.1如果preNode存在,正常处理
    if (preNode != null)
    preNode.next = newNode;
    // 4.2如果preNode不存在,则证明newNode为首节点
    else
    firstNode = newNode;
    }
    ...省略双链表节点类...
    }
  • 删除元素方法

    • 根据序号来删除集合元素,首先先判断序号取值是否合法,然后通过循环来找到对应索引位置的节点,最后删除该节点,而删除操作核心就是修改节点之间的指向

      /**
      * 模拟LinkedList集合
      */
      public class LinkedList<E> {
      ...省略私有属性和构造方法...
      /**
      * 根据序号删除元素
      * @param index 序号
      * @return 返回被删除的元素
      */
      public E remove(int index) {
      // 1.检查index的取值是否合法
      if (index < 0 || index >= size)
      throw new IndexOutOfBoundsException("链表序号越界异常");
      // 2.找到需要删除的节点对象
      Node<E> node = node(index);
      // 3.获取被删除节点中存放的内容
      E value = node.item;
      // 4.执行移除节点操作
      unlink(node);
      // 5.返回被删除节点的内容
      return value;
      }

      /**
      * 执行删除节点的操作
      * @param node 需要删除的节点
      */
      public void unlink(Node<E> node) {
      // 1.获取被移除节点的上一个节点和下一个节点
      Node<E> prev = node.prev;
      Node<E> next = node.next;
      // 2.判断prev是否为空
      if(prev == null)
      // 2.1prev为null,则证明删除node后,next就为链表的首节点
      firstNode = next;
      else
      // 2.2prev不为null,则把prev.next设置为next
      prev.next = next;
      // 3.判断next是否为空
      if(next == null)
      // 3.1next为null,则证明删除node后,prev就为链表的尾节点
      lastNode = prev;
      else
      // 3.2next不为null,则把next.prev设置为prev
      next.prev = prev;
      // 4.释放node对象的引用关系
      node.next = null;
      node.prev = null;
      node.item = null;
      // 5.链表中节点递减
      size--;
      }
      ...省略双链表节点类...
      }
    • 删除链表中指定某个元素,首先找到该元素在链表中所在的位置,如果没有找到则证明移除失败,如果找到则删除该节点

      /**
      * 模拟LinkedList集合
      */
      public class LinkedList<E> {
      ...省略私有属性和构造方法...
      /**
      * 删除指定某个元素
      * @param element 需要删除的元素
      * @return 删除成功,则返回true;删除失败,则返回false
      */
      public boolean remove(E element) {
      // 情况一:处理要删除的元素就是null的情况
      if(element == null) {
      // 通过for循环,从前往后遍历得到双链表中的所有节点
      for(Node<E> node = firstNode; node != null; node = node.next) {
      // 如果遍历出来的元素为null,则就删除该元素
      if(node.item == null) {
      // 执行删除节点操作,然后再返回true
      unlink(node);
      return true;
      }
      }
      }
      // 情况二:处理要删除的元素不是null的情况
      else {
      // 通过for循环,从前往后遍历得到双链表中的所有节点
      for(Node<E> node = firstNode; node != null; node = node.next) {
      // 如果遍历出来的元素和删除元素相等,则执行删除操作
      if(element.equals(node.item)) {
      // 执行删除节点操作,然后再返回true
      unlink(node);
      return true;
      }
      }
      }
      // 执行到此处,则意味着删除失败,那么返回false
      return false;
      }

      /**
      * 执行删除节点的操作
      * @param node 需要删除的节点
      */
      public void unlink(Node<E> node) {
      // 1.获取被移除节点的上一个节点和下一个节点
      Node<E> prev = node.prev;
      Node<E> next = node.next;
      // 2.判断prev是否为空
      if(prev == null)
      // 2.1prev为null,则证明删除node后,next就为链表的首节点
      firstNode = next;
      else
      // 2.2prev不为null,则把prev.next设置为next
      prev.next = next;
      // 3.判断next是否为空
      if(next == null)
      // 3.1next为null,则证明删除node后,prev就为链表的尾节点
      lastNode = prev;
      else
      // 3.2next不为null,则把next.prev设置为prev
      next.prev = prev;
      // 4.释放node对象的引用关系
      node.next = null;
      node.prev = null;
      node.item = null;
      // 5.链表中节点个数递减
      size--;
      }
      ...省略双链表节点类...
      }

【练习】

  1. 阅读LinkedList源码

Vector【扩展】

概述
  • Vector集合和ArrayList集合的用法几乎一模一样,底层都是采用了数组来存储集合中的元素。只不过Vector类的方法都加了同步检查,因此“线程安全,效率低”

  • 比如:add(E e)方法就增加了synchronized同步标记

    集合-Collection-LinkedList-LinkedList源码解析-4

应用实例
  • 应用实例1,如何使用

    public class Test {
    public static void main(String[] args) {
    // 实例化一个Vector
    Vector<String> vector = new Vector<String>();
    // 添加元素,addElement()方法和add()方法类似
    vector.addElement("java");
    vector.addElement("HTML");
    vector.addElement("JavaScript");
    vector.addElement("CSS");
    // 遍历集合, elements()方法类似于iterator()方法
    Enumeration<String> elements = vector.elements();
    while(elements.hasMoreElements()) {
    System.out.println(elements.nextElement());
    }
    }
    }
使用建议
  • 需要保证线程安全时,建议选用Vector
  • 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)
  • 不存在线程安全问题时,增加或删除元素较多用LinkedList

Set

概述
  • 是Collection系列中的类,主要特点是值不能重复
  • 常用的子类有HashSet、TreeSet
  • HashSet特点
    • 无序,不可重复
    • 可以有一个null值
    • 泛型数据如果有业务上的比较(如比较两个对象是否相同),必须覆盖equals和hashCode方法
  • TreeSet特点
    • 有序,不可重复;底层使用红黑树实现
    • 是SortedSet接口的子类,是一个排序的Set
    • 一般泛型数据类型必须实现Comparable接口,并按业务覆盖其中的compareTo方法,进行自然排序,(使用Comparator接口也可以,俗称定制排序)

Set常用方法

  • size方法,Set长度
  • add方法,往Set中添加一个元素
  • remove方法,删除指定元素
  • contains方法,是否包含某个元素
  • isEmpty方法,集合是否为空,即不包含一个元素
  • ...

HashSet

源码解析
  • HashSet类属于Set接口的实现类,在HashSet类中没有新增任何方法,并且HashSet依旧采用哈希表来实现,因此添加元素对应的类就必须重写hashCode()和equals()方法

  • 在HashSet集合中,底层实际上采用HashMap来存储元素的,因此HashSet集合本质就是一个简化版的HashMap。我们打开HashSet集合的源码,发现里面有一行核心代码

    集合-Collection-Set-HashSet-HashSet源码解析-1

  • 我们发现里面有个map属性,这就是HashSet的核心秘密。我们再看add()方法,发现增加一个元素说白了就是在map中增加一个键值对,键对象就是要添加的元素,值对象是名为PRESENT的Object对象。说白了,就是“往set中加入元素,本质就是把这个元素作为key加入到了内部的HashMap集合中”

    ​ 由于HashMap集合中的key不能重复的,并且HashMap存储的键值对都是无序的,因此HashSet存储的元素也不能重复,并且HashSet存储的元素还属于无序

    集合-Collection-Set-HashSet-HashSet源码解析-2

定义和使用
  • 方式1,常用方法

    package com.bjpowernode.demo;

    import java.util.HashSet;
    import java.util.Set;

    /**
    * HashSet常用方法
    */
    public class HashSetDemo {
    /**
    * 入口方法
    *
    * @param args
    */
    public static void main(String[] args) {
    //定义Set
    Set <Integer> datas = new HashSet <Integer>();

    System.out.println("-------------add方法-------------");
    //add方法
    datas.add(1);
    datas.add(2);
    datas.add(3);
    System.out.println("添加后的数据:" + datas);

    //remove方法
    System.out.println("-------------remove方法-------------");
    datas.remove(2);
    System.out.println("删除后的数据:" + datas);

    //size方法
    System.out.println("-------------size方法-------------");
    System.out.println("当前Set的size:" + datas.size());

    //contains方法
    System.out.println("-------------contains方法-------------");
    System.out.println("contains(1):" + datas.contains(1));
    System.out.println("contains(99):" + datas.contains(99));

    //isEmpty方法
    System.out.println("-------------isEmpty方法-------------");
    System.out.println("isEmpty():"+datas.isEmpty());
    }
    }
  • 方式2,包装类

    package com.bjpowernode.demo;

    import java.util.HashSet;
    import java.util.Set;

    /**
    * HashSet包装类示例
    */
    public class HashSetDemo {
    /**
    * 入口方法
    * @param args
    */
    public static void main(String[] args) {
    //定义Set
    Set <Integer> datas = new HashSet <>();

    //添加元素
    datas.add(1);
    datas.add(2);
    datas.add(3);
    datas.add(1); //重复添加
    datas.add(1); //重复添加

    //输出Set,【注意】,将不会有重复数据
    System.out.println(datas);
    }
    }
  • 方式3,自定义类型

    工作人员类

    package com.bjpowernode.demo;

    /**
    * 工作人员类
    */
    public class Employee {
    //姓名
    private String name;
    //性别
    private char sex;
    //年龄
    private int age;

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

    public Employee() {
    }


    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public char getSex() {
    return sex;
    }

    public void setSex(char sex) {
    this.sex = sex;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

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

    测试类

    package com.bjpowernode.demo;

    import java.util.HashSet;
    import java.util.Set;

    /**
    * HashSet自定义类型示例
    */
    public class HashSetDemo {
    /**
    * 入口方法
    *
    * @param args
    */
    public static void main(String[] args) {
    //定义Set
    Set <Employee> employees = new HashSet <>();

    //定义员工
    Employee e1 = new Employee("张三", '男', 18);
    Employee e2 = new Employee("李四", '女', 28);
    Employee e3 = new Employee("王五", '女', 38);
    Employee e4 = new Employee("张三", '男', 18);

    //添加到Set
    employees.add(e1);
    employees.add(e2);
    employees.add(e3);
    employees.add(e4);
    employees.add(e1); //重复添加
    employees.add(e1); //重复添加

    //输出Set,【注意】,将不会有重复数据
    System.out.println(employees);
    }
    }

    输出结果,Set中共4个对象

    集合-Set-HashSet定义与使用-自然

  • 方式4,普通类,如果两个对象在业务上相同,不能重复,怎么办?如上面的e1和e4,业务规则是姓名和年龄相同,则认为是同一个对象,不能重复

    • 注意:此时需要覆盖equals方法和hashCode方法,(可通过右键generate...菜单中的equals() and hashCode()子菜单自动实现)
      • equals方法:根据业务逻辑比较,如姓名和年龄相同就表示是同一个对象
      • hashCode方法:两个相同的对象,需要返回的hashCode值也一样,故需要使用equals中比较的业务字段进行hash算法

    工作人员类

    package com.bjpowernode.demo;

    import java.util.Objects;

    /**
    * 工作人员类
    */
    public class Employee {
    //姓名
    private String name;
    //性别
    private char sex;
    //年龄
    private int age;

    /**
    * 覆盖Object方法,实现自己业务需要的工作人员比较逻辑
    * @param o 要比较的对象
    * @return 比较结果,true为相同,false为不相同
    */
    @Override
    public 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 && name.equals(employee.name);
    }

    /**
    * hashCode方法,要保证按业务相同的两个方法,返回的hashCode也要一致;
    * 使用Objects的hash方法来实现
    * @return
    */
    @Override
    public int hashCode() {
    return Objects.hash(name, age);
    }

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

    public Employee() {
    }


    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public char getSex() {
    return sex;
    }

    public void setSex(char sex) {
    this.sex = sex;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

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

测试类

  package com.bjpowernode.demo;

import java.util.HashSet;
import java.util.Set;

/**
* HashSet自定义类型示例,【按业务规则去重】
*/
public class HashSetDemo {
/**
* 入口方法
*
* @param args
*/
public static void main(String[] args) {
//定义Set
Set <Employee> employees = new HashSet <>();

//定义员工
Employee e1 = new Employee("张三", '男', 18);
Employee e2 = new Employee("李四", '女', 28);
Employee e3 = new Employee("王五", '女', 38);
Employee e4 = new Employee("张三", '男', 18);

//添加到Set
employees.add(e1);
employees.add(e2);
employees.add(e3);
employees.add(e4); //【重复添加】因为姓名和年龄相同
employees.add(e1); //重复添加
employees.add(e1); //重复添加

//输出Set,【注意】,将不会有重复数据
System.out.println(employees);
}
}

输出结果,Set中只有3个对象

集合-Set-HashSet定义与使用-业务

TreeSet

源码解析
  • TreeSet类属于Set接口的实现类,在TreeSet类中没有新增任何方法,并且底层采用“红黑树”来实现,因此使用TreeSet集合就必须配合“比较器”来实现

  • 在TreeSet集合中,底层实际上采用TreeMap来存储元素的,因此TreeSet集合本质就是一个简化版的TreeMap。我们打开TreeSet集合的源码,发现里面有一行核心代码

    集合-Collection-Set-TreeSet-TreeSet源码解析-1

  • NavigableMap是一个接口,而TreeMap类就属于NavigableMap接口的实现类,因此给m赋值的就可以为TreeMap集合,此处使用了“多态”来实现,分析构造方法源码如下

    集合-Collection-Set-TreeSet-TreeSet源码解析-2

  • 接下来,我们再分析add()方法的源码,发现增加一个元素就是在TreeMap集合中增加一个键值对,键对象就是添加元素,值对象是名为PRESENT的Object对象。说白了,就是“往TreeSet集合中添加元素,本质就是把这个元素作为key加入到了内部的TreeMap集合中”

    因为TreeMap集合存储的key不能重复,并且TreeMap还能根据key实现排序操作,因此TreeSet集合存储的元素也不能重复,并且存储的元素还可以实现排序(升序|降序)操作

    集合-Collection-Set-TreeSet-TreeSet源码解析-3

定义与使用
  • 方式1,对象必须实现Comparable接口,并覆盖其中的compareTo方法

    工作人员类,注意compareTo方法

    package com.bjpowernode.demo;

    /**
    * 工作人员类,实现Comparable接口,并覆盖compareTo方法
    */
    public class Employee implements Comparable <Employee> {
    //姓名
    private String name;

    //性别
    private char sex;
    //年龄
    private int age;

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

    public Employee() {
    }

    /**
    * 覆盖compareTo方法,实现两个工作人员比较
    * 比较逻辑,使用年龄比较,当前对象年龄大于比较对象年龄,返回1,小于返回-1,相对返回0
    * 注意,是否重复,是根据返回是否为0判断
    *
    * @param o
    * @return
    */
    @Override
    public int compareTo(Employee o) {
    if (this.getAge() > o.getAge()) {
    return 1;
    } else if (this.getAge() < o.getAge()) {
    return -1;
    } else {
    return 0;
    }
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public char getSex() {
    return sex;
    }

    public void setSex(char sex) {
    this.sex = sex;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

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

测试类,注意定义员工中年龄的顺序

  package com.bjpowernode.demo;

import java.util.Set;
import java.util.TreeSet;

/**
* TreeSet示例
*/
public class TreeSetDemo {
public static void main(String[] args) {
//定义Set
Set <Employee> employees = new TreeSet <>();

//定义员工
Employee e1 = new Employee("张三", '男', 18);
Employee e2 = new Employee("李四", '女', 98);
Employee e3 = new Employee("王五", '女', 38);
Employee e4 = new Employee("张三", '男', 18);

//添加到Set
employees.add(e1);
employees.add(e2);
employees.add(e3);
employees.add(e4); //【重复添加】因为姓名和年龄相同
employees.add(e1); //重复添加
employees.add(e1); //重复添加

//输出Set,【注意】,将不会有重复数据
System.out.println(employees);
}
}

输出结果,注意有按年龄排序

集合-Set-TreeSet定义与使用

应用简介

【练习】

  1. 练习应用实例内容,完成代码编写
  2. 【扩展】重构程序,将第16章-包装类中的练习产品和仓库进行重构,要求如下
    1. 产品列表使用HashSet类型
    2. 添加方法:
      1. 新增产品,形参为Product类对象,返回新增是否成功;注意,如果产品编号已经存在,则不能新增
      2. 修改产品,形参为Product类对象,返回修改是否成功;
      3. 删除产品,形参为产品编号,返回删除是否成功
    3. 测试上述方法

迭代器

概述
  • 在Java中,提供了很多集合类,它们在存储元素时,采用的存储方式不同
  • 当需要取出这些集合中的元素,可通过一种通用的获取方式来完成,这样使得对容器的遍历操作与其具体的底层实现相隔离,达到解耦的效果
  • Collection集合中元素的通用获取方式:在取元素之前先要判断集合中有没有元素,如果有元素则把元素取出,然后继续再判断下一个元素,如果还有就再取出,直到把集合中的所有元素全部取出为止。这种取出方式专业术语称为迭代或迭代器
  • 迭代器一般多使用Iterator接口和ListIterator接口
Iterator
  • 对于Iterator接口而言,因为本身是一个接口,所以需要通过Collection接口提供的 Iterator<E> iterator();方法,这样就能获得一个Iterator接口的实现类对象

  • 接口主要方法如下

    集合-Collection-迭代器-Iterator-1

  • 应用实例

    • 应用实例1,使用Iterator遍历

      public class Test01 {
      public static void main(String[] args) {
      // 创建ArrayList集合
      ArrayList<String> list = new ArrayList<>();
      list.add("aaa");
      list.add("bbb");
      list.add("ccc");
      // 获取集合的迭代器对象
      Iterator<String> iterator = list.iterator();
      // 迭代器while循环的形式的使用
      while(iterator.hasNext()) {
      System.out.println(iterator.next());
      }
      // 迭代器for循环的形式的使用
      for(Iterator<String> iter = list.iterator(); iter.hasNext();) {
      System.out.println(iter.next());
      }
      }
      }
    • 应用实例2,使用迭代器时,通过集合的方法对元素进行操作的错误案例

      ​ 使用Iterator迭代器遍历集合时,如果集合中已经没有元素了,还继续使用迭代器的next()方法,将会抛出java.util.NoSuchElementException没有集合元素的异常。

      ​ 如果在迭代过程中,使用了集合的方法对元素进行操作(例如使用集合的方法删除元素)。导致迭代器并不知道集合中的变化,容易引发数据的不确定性,从而会抛出并发修改异常(java.util.ConcurrentModificationException)

      public class Test01 {
      public static void main(String[] args) {
      // 创建ArrayList集合
      ArrayList<Integer> list = new ArrayList<Integer>();
      list.add(11);
      list.add(22);
      list.add(33);
      // 获取集合的迭代器对象
      Iterator<Integer> iterator = list.iterator();
      // 使用迭代器来遍历集合中的元素
      while (iterator.hasNext()) {
      Integer value = iterator.next();
      System.out.println(value);
      // 当获取的value小于30时,通过集合的方法删除元素
      if (value > 30) {
      // 通过集合的方法删除元素,会抛出并发修改异常
      list.remove(value); // 错误演示
      }
      }
      }
      }
    • 应用实例3,通过迭代器的remove()方法删除集合元素案例

      ​ 使用Iterator接口提供的方法只能实现删除元素(remove)操作,如果还想在迭代过程中实现集合的添加元素或修改元素的操作,那么就应该使用ListIterator来实现。

      ​ 另外,通过for-each遍历集合的过程中,不能通过集合的方法对元素进行操作,否则会抛出并发修改异常,因为for-each循环底层用的就是Iterator来实现的

      public class Test01 {
      public static void main(String[] args) {
      // 创建ArrayList集合
      ArrayList<Integer> list = new ArrayList<Integer>();
      list.add(11);
      list.add(22);
      list.add(33);
      // 获取集合的迭代器对象
      Iterator<Integer> iterator = list.iterator();
      // 使用迭代器来遍历集合中的元素
      while(iterator.hasNext()) {
      Integer value = iterator.next();
      System.out.println(value);
      // 当获取的value小于30时,通过集合的方法删除元素
      if(value > 30) {
      // 调用迭代器的方法来删除遍历出来的运算
      iterator.remove(); // 正确做法
      }
      }
      }
      }
  • Iterator和foreach的区别

    • 使用for-each不但能遍历集合,还能遍历数组,但是使用Iterator只能遍历集合。
    • 使用for-each遍历集合时不能删除元素,但是使用Iterator遍历集合时可以删除元素
  • 注意:欲知抛出java.util.ConcurrentModificationException异常的原因,深度解读可查阅这篇博文https://www.cnblogs.com/dolphin0520/p/3933551.html

ListIterator

  • ListIterator接口继承于Iterator接口,因此ListIterator提供的功能比Iterator更加的强大

  • 使用ListIterator迭代器,不但能“从前往后”遍历集合,还可以“从后往前”遍历集合,并且遍历集合的过程中还能删除、修改和插入元素,常用方法如下

    集合-Collection-迭代器-Iterator-1

  • 在List接口中,提供了获得ListIterator迭代器的方法,获取方式如下

    集合-Collection-迭代器-ListIterator-2

  • 使用ListIterator迭代器遍历集合,想要实现“从前往后”遍历List集合,则游标位置需要设置为“-1”,也就是建议使用ListIterator<E> listIterator();来获得迭代器;想要实现“从后往前”遍历List集合,则游标位置需要设置为“实际添加元素的个数”,也就是必须使用ListIterator<E> listIterator(int index);获得迭代器

  • 注意:使用Iterator迭代器,只能删除遍历出来的元素;而使用ListIterator迭代器,不但能删除遍历出来的元素,还能修改遍历出来的元素和在遍历出来元素之前或之后插入元素

  • 应用实例

    • 应用实例1,从前往后”遍历ArrayList集合

      public class Test01 {
      public static void main(String[] args) {
      // 创建ArrayList集合
      ArrayList<String> list = new ArrayList<>();
      // 添加元素
      list.add("aaa");
      list.add("bbb");
      list.add("ccc");
      // 获得迭代器,用于“从前往后”遍历集合
      ListIterator<String> iterator = list.listIterator();
      // 如果存在下一个元素,则执行循环
      while(iterator.hasNext()) {
      // 获得集合中的下一个元素并输出
      System.out.println(iterator.next());
      }
      }
      }
    • 应用实例2,“从前往后”遍历ArrayList集合

      public class Test01 {
      public static void main(String[] args) {
      // 创建ArrayList集合
      ArrayList<String> list = new ArrayList<>();
      // 添加元素
      list.add("aaa");
      list.add("bbb");
      list.add("ccc");
      // 获得迭代器,用于“从后往前”遍历集合
      ListIterator<String> iterator = list.listIterator(list.size());
      // 如果存在前一个元素,则执行循环
      while (iterator.hasPrevious()) {
      // 获得集合中的前一个元素并输出
      System.out.println(iterator.previous());
      }
      }
      }
    • 应用实例3,迭代过程中对ArrayList元素操作

      public class Test01 {
      public static void main(String[] args) {
      // 创建ArrayList集合
      LinkedList<Integer> list = new LinkedList<Integer>();
      // 添加元素
      list.add(10);
      list.add(20);
      list.add(30);
      // 获得迭代器,用于“从前往后”遍历集合
      ListIterator<Integer> iterator = list.listIterator();
      // 如果还存在下一个元素,则执行循环
      while(iterator.hasNext()) {
      // 获得遍历出来的元素
      Integer value = iterator.next();
      // 遍历集合的过程中,执行删除、修改和插入操作
      if(value <= 10) {
      iterator.remove(); // 删除当前迭代的这个元素
      } else if (value <= 20) {
      iterator.set(22); // 修改当前迭代的这个元素
      } else {
      iterator.add(40); // 在遍历出来元素后面添加元素
      }
      }
      System.out.println(list); // 输出:[22, 30, 40]
      }
      }
  • Iterator和ListIterator的区别

    • Iterator应用于Set集合和List集合,但是ListIterator只能应用于List集合
    • Iterator只能从前往后遍历集合,而ListIterator能从前往后和从后往前遍历集合
    • Iterator只有remove()方法,而ListIterator有remove()、add()和set()方法

【练习】

  1. 练习应用实例内容,完成代码编写

Collections工具类

  • 实际工作中,会经常对Collection系列和Map系列进行操作

  • Java中提供了Collections工具类,里面包含了很多针对集合的静态方法,主要是针对Collection系列的数据进行操作

  • 常用方法

    • reverse,反转数据
    • shuffle,随机数据
    • sort,对数据进行排序,对于自定义类型,需要用到Comparator接口匿名类定义比较逻辑
    • max,取数据中的最大值,对于自定义类型,需要用到Comparator接口匿名类定义比较逻辑
    • min,取数据中的最小值,对于自定义类型,需要用到Comparator接口匿名类定义比较逻辑
    • ...
  • 应用实例

    • 应用实例1,常用方法

      用户类

      package com.bjpowernode.demo;

      /**
      * 用户类
      */
      public class User implements Comparable<User> {
      //普通属性
      private Integer id;
      private String name;
      private String sex;
      private Integer age;
      private Boolean enabled;

      /**
      * 无参构造方法
      */
      public User(){
      }

      /**
      * 有参构造
      * @param id
      * @param name
      * @param sex
      * @param enabled
      */
      public User(Integer id, String name, String sex, Integer age, boolean enabled){
      this.id=id;
      this.name=name;
      this.sex= sex;
      this.age = age;
      this.enabled =enabled;
      }

      public Integer getAge() {
      return age;
      }

      public void setAge(Integer age) {
      this.age = age;
      }

      public Integer getId() {
      return id;
      }

      public void setId(Integer id) {
      this.id = id;
      }

      public String getName() {
      return name;
      }

      public void setName(String name) {
      this.name = name;
      }

      public String getSex() {
      return sex;
      }

      public void setSex(String sex) {
      this.sex = sex;
      }

      public Boolean isEnabled() {
      return enabled;
      }

      public void setEnabled(Boolean enabled) {
      this.enabled = enabled;
      }

      @Override
      public String toString() {
      return "[ID:"+this.id+",姓名:"+this.name+",性别:"+this.sex+",年龄:"+this.age+",是否有效:"+this.isEnabled()+"]";
      }


      /**
      * 【自然排序】覆盖Comparable接口方法,实现按id排序;一般写好了就固定了,灵活较低
      * @param o
      * @return
      */
      @Override
      public int compareTo(User o) {
      if(this.getId()>o.getId()){
      return 1;
      }else if(this.getId()<o.getId()){
      return -1;
      }else{
      return 0;
      }
      }
      }

      Collections示例类

      package com.bjpowernode.demo;

      import java.util.ArrayList;
      import java.util.Collections;
      import java.util.Comparator;
      import java.util.List;

      /**
      * Collections工具类,针对ArrayList的使用
      */
      public class CollectionsDemo {
      public static void main(String[] args) {
      //初始化数据
      List<User> users = new ArrayList <>();
      users.add(new User(1, "张三", "male", 55, true));
      users.add(new User(2, "李四", "male", 44, false));
      users.add(new User(3, "王五", "female", 20, true));
      users.add(new User(4, "赵六", "male", 57, false));
      users.add(new User(5, "田七", "female", 18, false));

      //输出原始数据
      System.out.println("-----------------------------原始数据-----------------------------");
      for (User user : users) {
      System.out.println(user.toString());
      }

      //反转
      Collections.reverse(users);

      //输出反转后的数据
      System.out.println("-----------------------------反转后的数据-----------------------------");
      for (User user : users) {
      System.out.println(user.toString());
      }

      //随机
      Collections.shuffle(users);
      //输出随机后的数据
      System.out.println("-----------------------------随机后的数据-----------------------------");
      for (User user : users) {
      System.out.println(user.toString());
      }

      //自然排序
      System.out.println("-----------------------------【自然排序】排序后的数据-----------------------------");
      //自然排序,也称内排序,类写好了就固定了;一般写好了就固定了,灵活较低
      Collections.sort(users);
      //输出排序后的数据
      for (User user : users) {
      System.out.println(user.toString());
      }

      //定制排序
      System.out.println("-----------------------------【定制排序】排序后的数据-----------------------------");
      //定制排序,也称外排序,使用匿名类;相对较灵活
      Collections.sort(users, new Comparator<User>() {
      @Override
      public int compare(User o1, User o2) {
      //按年龄排序
      if (o1.getAge() > o2.getAge()) {
      return 1;
      } else {
      return -1;
      }
      }
      });
      //输出排序后的数据
      for (User user : users) {
      System.out.println(user.toString());
      }

      //最大值&最小值
      System.out.println("-----------------------------【最大值&最小值】数据-----------------------------");
      User maxUser = Collections.max(users, new Comparator <User>() {
      @Override
      public int compare(User o1, User o2) {
      //按年龄比较
      if (o1.getAge() > o2.getAge()) {
      return 1;
      } else {
      return -1;
      }
      }
      });
      System.out.println("最大年龄用户:"+maxUser.toString());
      User minUser = Collections.min(users, new Comparator <User>() {
      @Override
      public int compare(User o1, User o2) {
      //按年龄比较
      if (o1.getAge() > o2.getAge()) {
      return 1;
      } else {
      return -1;
      }
      }
      });
      System.out.println("最小年龄用户:"+minUser.toString());
      }
      }

​ 输出结果,运行查看

【练习】

  1. 练习应用实例内容,完成代码编写
  2. 【扩展】重构程序,针对“第16章-包装类”中的练习产品和仓库进行应用,要求如下
    1. 列出仓库产品数量排行榜,降序
    2. 列出仓库库存金额排行榜,降序
    3. 找出单价最昂贵的产品
    4. 找出单价最便宜的产品

实战和作业

  1. 编写程序,创建一个字符串的ArrayList,要求如下

    1. 添加如下String类型的数据:“张三”,“李四”,“王五”,“张三”,”赵六”

    2. 判断这个集合是否为空,并打印判断的结果

    3. 打印这个集合的大小

    4. 判断这个集合是否包含数据"王五"

    5. 将”张三”这条数据删掉

    6. 输出集合中的每个元素

  2. 【扩展】编写程序,进行产品的分类汇总,要求如下

    1. 定义一个产品信息列表,存放多个产品信息,单个产品信息包括产品编号、产品名称、产品类别(包括如手机、电脑、食品、医药、鞋包、百货等)、产品价格、产品数量

    2. 分别汇总不同产品类别的总价值(产品价格*产品数量),并按总价值降序输出

    3. 如,有如下产品数据

      001 小米13 手机 2000 5 002 小米电视 电视 5000 2 003 iPhone13 手机 8000 3 004 格力空调 空调 4500 10 005 海尔电视 电视 3000 3

      则,分类汇总结果为

      空调 45000 手机 34000 电视 19000

Map系列

概述

  • Map是Java集合框架中重要的系列接口与类

  • 区别于Collection系列,Map系列中,单个元素是以键:值对形式存储,也称为key:value,即成双成对出现

  • 应用场景

    • 如果经常需要根据学号快速的查询学生信息,可以使用Map,效率更高效,模式如下图

      Map-应用场景

    • 像生活中还公司中工号与人的对应;身份证号与人的对应等

    • ...

Map的特点

  • key、value都只能是引用数据类型
  • key不允许重复,value可以
  • key和value一一对应

Map关系图谱

Map-类图

Map关系图谱

常用Map类和接口

  • Map接口,Map系列集合的父接口,定义了一系列通用的方法
    • ConcurrentMap接口,是一个线程安全的Map接口
      • ConcurrentHashMap类,在ConcurrentMap的基础上,使用了HashMap的特点,性能更好
    • HashTable类,是一个较早的类,key不允许null值,是线程安全的;与HashMap类似,HashMap系列的类基本能实现HashTable类功能
    • HashMap类,无序,key允许有一个null,是非线程安全的,性能更好;是最常用的Map集合类
    • TreeMap类,有序,相比HashMap,性能稍差

HashMap

概述

  • 是实际开发中常用的Map集合类
  • 在访问效率方面,区别于基于数组和链表的类,HashMap查询、插入、删除的效率都很高
  • 特点
    • 无序key不可重复,key可以有一个null
    • 只支持引用数据类型
    • 底层使用数组,并结合链表、红黑树结构
      • 数组,key通过哈希算法计算后,存储到指定的数组位置
        • 默认初始容量(initialCapacity)为16,可以在创建对象的时候通过构造方法设置
        • 自动扩容,如果使用默认初始容量,会随着元素的增加扩容,载荷因子(loadFactor)默认为0.75;即当数组中使用超过当前容量*0.75个元素时,会自动扩容,扩容为原来容量的2倍
      • 链表,不同的key通过哈希算法计算后,如果存储到了相同的位置(哈希冲突),则该位置的存储方式会转换成链表
      • 红黑树,当上述链表的元素个数大于8时,会转成效率更高的红黑树结构;红黑树是一个平衡二叉树,查询、插入、删除效率都很好
    • 线程不安全
    • 提供了很多便利的操作方法

源码解析【扩展】

  • 哈希表也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如:Redis)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出现在各类的面试题中,重要性可见一斑

  • 数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构。而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组

  • 其中存储的核心代码如下,分别是数组和链表,下面是JDK1.7的源码截图

    集合-Map-HashMap-HashMap源码解析-1

  • 其中的,Node[] table 就是HashMap的核心数组结构,我们也称之为“位桶数组”。我们再继续看Node是什么,源码如下

    集合-Map-HashMap-HashMap源码解析-2

  • 在Node类中,key存储的是“键对象”,value存储的是“值对象”,hash存储的是“键对象的哈希值”,而next存储的就是“指向下一个节点的地址值”。显然Node类就是一个单链表节点类,我们使用图形表示该单链表如下所示

    集合-Map-HashMap-HashMap源码解析-3

  • 然后,我们画出Node[]数组的结构(这也是HashMap在JDK1.7的存储结构);并由图可知,在JDK1.7版本中,HashMap底层使用的哈希表就是“数组+链表”来实现,也就是数组存储的每个元素都是一个单链表

    集合-Map-HashMap-HashMap源码解析-4

存储原理分析

  • 存储原理图如下

    集合-Map-HashMap-存储原理分析-1

  • 散列函数

    • 散列函数(也就是hashCode()方法),通过散列函数获得key对象的哈希码,实际上就是建立起key值与int值映射关系的函数。这就好比每个人都有一个身份证号一样,无论是男是女,出生在何处,都可以通过身份证号来分辨,这就是把人的信息映射成一串数字的典型做法。散列函数和此类似,不过是把任意的Java对象,映射成一个int数值(散列均匀,尽量不重复),供哈希表使用
    • 另外,通过hashCode()方法获得的结果是一个很大的整数,我们的哈希表不可能提供那么大的存储空间,所以我们还需要对获得的哈希码值做处理。现实开发中,最常见的做法就是“取余法”,把获得的“哈希值 % 数组长度”,这样得到的结果就在[0, 数组长度-1]之间,也就是获得键值对在table数组的存储位置
  • 散列碰撞

    • 通过hashCode()方法,获得了一个散列均匀的哈希码值,但是不同的key对象获得哈希码值可能相同;另外,通过对不同的“哈希值 % 数组长度”得到的索引值也可能相同,那就意味着在哈希表的某个索引位置需要存储多条数据,这就是所谓的散列碰撞问题。

    • 目前比较通用的解决散列碰撞的方法,可以使用“数组+链表”组合的方式。当出现散列碰撞时,在该位置的数据就通过单链表的方式链接起来,这样一来数组中的每个元素维护的就是一个单链表啦

  • JDK1.7与JDK1.8的区别

    • 在JDK1.7中,底层table数组在构造方法中初始化,在JDK1.8中,底层table数组在第一次调用put()方法时初始化。

    • 在JDK1.7中,处理哈希冲突采用的是“头插法”;在JDK1.8中,处理哈希冲突采用的是“尾插法”。

    • 在JDK1.7中,HashMap底层采用“数组+链表”来实现;在JDK1.8中,HashMap底层采用“数组+链表+红黑树”来实现。

      说明:当单链表的个数大于等于8并且table数组的空间长度大于等于64,则单链表就会变为红黑树;删除红黑树的节点时,如果节点个数小于等于6,则红黑树就变为单链表

      集合-Map-HashMap-存储原理分析-2

  • 底层table数组的长度

    • 通过HashMap集合的无参构造方法来创建集合,则底层table数组的空间长度默认为16

      集合-Map-HashMap-长度-1

    • 通过HashMap集合的有参构造方法创建集合,则底层table数组肯定为2的整数次幂,也就是空间长度为大于实参的最小2的整数次幂,如下代码所示

      public class Test02 {
      public static void main(String[] args) {
      // 调用构造方法的实参为15,则底层的table数组空间长度为:16
      HashMap<String, Integer> hashMap1 = new HashMap<>(15);
      // 调用构造方法的实参为1000,则底层的table数组空间长度为:1024
      HashMap<String, Integer> hashMap2 = new HashMap<>(1000);
      }
      }
    • 为什么HashMap集合底层table数组的空间长度必须为2的整数次幂呢???把table数组空间长度设置为2的整数次幂,就是为了加快散列计算以及减少散列冲突。

    • 为什么可以加快散列计算?因为& 运算属于位运算,是直接基于二进制做的操作,因此&运算比%运算效率更高。当table数组的空间长度(length)为2的整数次幂,则执行“hash & (length - 1)”和“hash % length”的运算的结果相同,但是&运算的效率高于%运算,所以建议哈希表的容量设置为2的整数次幂,代码验证如下

      public class Test01 {
      public static void main(String[] args) {
      // 获得散列码值,值为:2998638
      int hashCode = "ande".hashCode();
      // 计算得到2^14的结果,结果为:16384
      int length = (int)Math.pow(2, 14);
      // &运算结果为:366
      System.out.println(hashCode & (length - 1));
      // %运算结果为:366
      System.out.println(hashCode % length);
      }
      }
    • 为什么可以减少冲突?假设现在table数组空间长度length可能是偶数也可能是奇数。

      ​ 当length为偶数时,则length-1为奇数,而奇数的二进制最低位肯定为1,这样hash &(length-1) 结果的二进制最低位可能为0,也可能为1(这取决于hash的值)。也就是运算的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性。

      ​ 当length为奇数时,则length-1为偶数,而偶数的二进制最低位肯定为0,这样hash & (length-1)结果的二进制最低位肯定为0。也就是运算的结果肯定为偶数,这样得到的结果对应的就是table数组索引为偶数的位置,那么流浪费了近一半的空间。

      ​ 因此,table数组空间长度为2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列

  • 设置HashMap初始化容量

    • 当哈希表中的元素越来越多的时候,散列碰撞的几率也就越来越高(因为数组的长度是固定的),从而导致单链表过长,降低了哈希表的性能,此时我们就需要对哈希表进行扩容操作。
    • 那么HashMap什么时候进行扩容呢?当执行put()操作的时候,如果HashMap中存储元素的个数超过“数组长度* loadFactor”的结果(loadFactor指的是负载因子,loadFactor的默认值一般为0.75),那么就就需要执行数组扩容操作。
    • 所谓的扩容操作,就是把数组的空间大小扩大一倍,然后遍历哈希表中元素,把这些元素重新均匀分散到扩容后的哈希表中。例如,默认情况下,数组大小为16,那么当Hashtable中元素个数超过16*0.75=12的时候,就需要执行扩容操作,把数组的大小扩展为2*16=32,然后重新计算每个元素在数组中的位置,这是一个非常消耗性能的操作。
    • 为了避免扩容带来的性能损坏,建议使用哈希表之前,先预测哈希表需要存储元素的个数,提前为哈希表中的数组设置合适的存储空间大小,避免去执行扩容的操作,进一步提升哈希表的性能。例如:我们需要存储1000个元素,按照哈希表的容量设置为2的整数次幂的思想,我们设置哈希表的容量为1024更合适。但是0.75*1024 < 1024,需要执行消耗性能的扩容操作,因此我们设置哈希表的容量为2048更加合适,这样既考虑了&的问题,也避免了扩容的问题。
    • 思考:当我们创建一个HashMap对象,设置哈希表的容量为15,请问HashMap对象创建成功后,哈希表的实际容量为多少呢??

常用接口

  • size方法,Map集合长度
  • put方法,往Map集合中添加一个元素,如果key已经存在,则只会更新value
  • get方法,根据key获取指定元素的value
  • remove方法,删除key的元素
  • containsKey方法,是否包含某个key的元素
  • containsValue方法,是否包含某个value的元素
  • isEmpty方法,Map集合是否为空,即不包含一个元素
  • keySet方法,获取所有key列表,返回值类型为Set类型
  • values方法,获取所有value列表,返回值类型为Collection类型
  • entrySet方法,返回Map集合中的数据Set列表,多用于遍历每一对key:value,返回类型为Set<Map.Entry<kye,value>>
  • ...

定义和使用

  • 方式1,泛型方式常用方式,只支持指定类型的对象

    实例代码

    //定义,只支持指定类型的数据
    Map<Integer,Student> map=new HashMap <>();

    //使用
    //添加
    Student s1 = new Student();
    s1.setId(1001);
    s1.setName("张三");
    map.put(s1.getId(),s1);
    Student s2 = new Student();
    s2.setId(1002);
    s2.setName("李四");
    map.put(s2.getId(),s2);

    //根据key获取其中某一个元素
    map.get(1002);

    //根据key删除其中某一个元素
    map.remove(1001);//删除

    //...其他方法

应用实例

  • 应用实例1,正常使用,使用字符串和包装类;人员工资操作

    实例代码

    package com.bjpowernode.demo;

    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;

    /**
    * HashMap示例
    */
    public class HashMapDemo {
    public static void main(String[] args) {
    //定义人员工资map
    Map <String,Double> map = new HashMap <>();

    //使用,添加元素
    map.put("张三",12000.8);
    map.put("李四",7500.98);
    map.put("王小五",3400.0);
    map.put("赵大六",6800.88);

    //使用,根据key查找元素
    Double salary = map.get("李四");
    System.out.println("李四的工资:"+salary);

    //使用,根据key删除
    map.remove("李四");

    //使用,重新根据key查找元素
    salary = map.get("李四");
    System.out.println("李四的工资:"+salary);

    //使用,是否包含某个key
    System.out.println("HashMap中是否有王小五?"+map.containsKey("王小五"));

    //使用,是否包含某个value
    System.out.println("HashMap中是否有工资为6800.88的人?"+map.containsValue(6800.88));

    //使用,获取所有key列表
    Set<String> names = map.keySet();
    System.out.println("员工列表:"+names);

    //使用,获取所有value列表
    Collection <Double> salaries = map.values();
    System.out.println("工资列表:"+salaries);

    //使用,遍历Map集合
    System.out.println("----------员工工资列表----------");
    Set<Map.Entry<String,Double>> datas = map.entrySet();
    for(Map.Entry<String,Double> item :datas){
    System.out.println(item.getKey()+":"+item.getValue());
    }
    }
    }

    输出结果,运行查看结果

  • 应用实例2,正常使用,使用自定义类型

    学生类

    package com.bjpowernode.demo;

    import java.util.Objects;

    /**
    * 学生类
    */
    public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private Float weight;

    public Student() {
    }

    public Student(Integer id, String name, Integer age, Float weight) {
    this.id = id;
    this.name = name;
    this.age = age;
    this.weight = weight;
    }

    public Integer getId() {
    return id;
    }

    public void setId(Integer id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public Integer getAge() {
    return age;
    }

    public void setAge(Integer age) {
    this.age = age;
    }

    public Float getWeight() {
    return weight;
    }

    public void setWeight(Float weight) {
    this.weight = weight;
    }

    /**
    * 覆盖toString方法
    *
    * @return
    */
    @Override
    public String toString() {
    return "[" + this.id + "," + this.getName() + "," + this.getAge() + "," + this.getWeight() + "]";
    }

    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return id.equals(student.id);
    }

    @Override
    public int hashCode() {
    return Objects.hash(id);
    }
    }

    HashMap实例

    package com.bjpowernode.demo;

    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;

    /**
    * 正常使用,使用自定义类型
    */
    public class StudentHashMapDemo {
    public static void main(String[] args) {
    //定义人员工map
    Map<Integer, Student> map = new HashMap<>();

    //使用,添加元素
    Student student = new Student(1, "张三", 33, 75.3f);
    map.put(student.getId(), student);
    student = new Student(2, "李四", 18, 60.3f);
    map.put(student.getId(), student);
    student = new Student(3, "王小五", 27, 55.73f);
    map.put(student.getId(), student);
    student = new Student(4, "赵大六", 47, 78f);
    map.put(student.getId(), student);

    System.out.println("--------------原始数据--------------");
    System.out.println(map);

    //使用,根据key查找元素
    System.out.println("--------------根据key查找元素(key为2)--------------");
    student = map.get(2);
    System.out.println("ID为2的学生信息:" + student);

    //使用,根据key删除
    System.out.println("--------------根据key删除元素(key为2)--------------");
    map.remove(2);
    System.out.println("删除后的数据为:");
    System.out.println(map);

    //使用,重新根据key查找元素
    System.out.println("--------------重新根据key查找元素(key为2)--------------");
    student = map.get(2);
    if (student != null) {
    System.out.println("ID为2的学生信息:" + student);
    } else {
    System.out.println("ID为2的学生信息不存在!");
    }

    //使用,是否包含某个key
    System.out.println("--------------是否包含某个key的元素(key为2)--------------");
    System.out.println("HashMap中是否有key为2的元素?" + map.containsKey(2));

    //使用,是否包含某个value;必须覆盖泛型类型Student中的equals、hashCode方法,明确业务上地相等
    System.out.println("--------------是否包含某个value的元素(value为新的学生对象)--------------");
    Student value = new Student(1, "张三", 33, 75.3f);
    System.out.println("HashMap中是否有value上述对象的元素?" + map.containsValue(value));

    //使用,获取所有key列表
    System.out.println("--------------获取所有key--------------");
    Set<Integer> ids = map.keySet();
    System.out.println("学生ID列表:" + ids);

    //使用,获取所有value列表
    System.out.println("--------------获取所有value--------------");
    Collection<Student> students = map.values();
    for (Student item : students) {
    System.out.println("学生信息:" + item);
    }

    //使用,遍历Map集合
    System.out.println("--------------遍历map--------------");
    Set <Map.Entry <Integer, Student>> datas = map.entrySet();
    for (Map.Entry <Integer, Student> item : datas) {
    System.out.println(item.getKey() + ":" + item.getValue());
    }
    }
    }

    输出结果,运行查看结果

LinkedHashMap

概述

  • LinkedHashMap类继承于HashMap类,在LinkedHashMap类中没有新增任何方法,它的底层采用“哈希表+链表”结构,能够保证元素存与取的顺序完全一致。
  • LinkedHashMap集合和HashMap集合用法几乎一模一样。当然,LinkedHashMap集合中的key不能重复,我们需要通过重写hashCode()与equals()方法来保证可以的唯一。另外,key的取值可以为null,并且如果key发生了重复,则做覆盖操作

应用实例

  • 应用实例1,LinkedHashMap使用

    public class Test02 {
    public static void main(String[] args) {
    // 创建一个LinkedHashMap对象
    LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
    // 添加键值对
    map.put("xixi", 11);
    map.put("haha", 22);
    map.put("hehe", 33);
    map.put(null, 44);
    // 获得key的迭代器
    Iterator<String> iterator = map.keySet().iterator();
    while (iterator.hasNext()) {
    // 获取key值
    String key = iterator.next();
    // 输出key和value
    System.out.println("key:" + key + "value:" + map.get(key));
    }
    }
    }

HashTable

概述

  • Hashtable集合和HashMap集合用法几乎一模一样,它的底层依旧采用“哈希表”来存储键值对,具备查询速度快,在很多情况下它们可以互用。

  • 只不过Hashtable类继承于Dictionary类并实现了Map接口,HashMap类只是Map接口的实现类。另外Hashtable的方法添加了Synchronized关键字确保线程同步检查,效率较低。在Hashtable类中,put(K key, V value)方法就增加了synchronized同步标记

    集合-Map-HashTable-1

  • 相比较于HashMap类,Hashtable类中还包含了许多传统的方法,显然这些方法都不属于集合框架,在Hashtable类中常见的传统方法如下所示

    集合-Map-HashTable-2

  • 通过Hashtable的keys()和elements()方法,返回的结果是Enumeration接口,此处Enumeration接口功能类似于Iterator接口,都可以实现迭代器的效果,常见的方法如下

    集合-Map-HashTable-3

应用实例

  • 应用实例1,HashTable使用

    public class Test {
    public static void main(String[] args) {
    // 创建Hashtable对象
    Hashtable<String, String> ht = new Hashtable<String, String>();
    // 添加键值对
    ht.put("cn", "China");
    ht.put("jp", "Japan");
    ht.put("us", "America");
    // 遍历所有的key,keys()方法类似于keySet()方法
    Enumeration<String> keys = ht.keys();
    while(keys.hasMoreElements()) {
    String key = keys.nextElement();
    System.out.println(key + "-->" + ht.get(key));
    }
    // 遍历所有的value,elements()方法类似于values()方法
    Enumeration<String> elements = ht.elements();
    while(elements.hasMoreElements()) {
    String element = elements.nextElement();
    System.out.println("value: " + element);
    }
    }
    }

Properties

概述

  • Java中有个比较重要的类Properties(Java.util.Properties),主要用于读取Java的配置文件,各种语言都有自己所支持的配置文件,配置文件中很多变量是经常改变的,这样做也是为了方便用户,让用户能够脱离程序本身去修改相关的变量设置。

  • 在Java语言中,配置文件常为.properties文件,格式为文本文件,文件的内容的格式是“键=值”的格式,文本注释信息可以用"#"来注释。

  • Properties类继承自Hashtable类,属于线程安全的类,其特点如下:

    • 属于Hashtable的子类,Map集合中的方法都可以用。
    • 该Properties集合中没有泛型,键值都是字符串。
    • 它是一个可以持久化的属性集,具有和IO流技术相结合的方法。
  • 在Properties集合中,除了能使用Map接口提供的方法,并且在Properties集合还新增了一些方法,常用的方法如下

    集合-Map-Properties-1

应用实例

  • 应用实例1,Properties使用

    public class Test02 {
    public static void main(String[] args) {
    // 创建一个Properties对象
    Properties properties = new Properties();
    // 添加元素
    properties.setProperty("张三", "重庆");
    properties.setProperty("李四", "成都");
    properties.setProperty("王五", "上海");
    properties.setProperty("赵六", "北京");
    properties.setProperty("王五", "西安"); // 此处会覆盖上海的王五
    // 获得所有的key的Set集合
    Set<String> set = properties.stringPropertyNames();
    // 通过迭代器来遍历Properties集合
    Iterator<String> iterator = set.iterator();
    while (iterator.hasNext()) {
    String key = iterator.next();
    String value = properties.getProperty(key);
    System.out.println("key:" + key + ",value:" + value);
    }
    }
    }

实战和作业

  1. 编写程序,创建一个HashMap,要求如下:
    1. 持续从控制台接收多个人员信息输入,每个人员信息包括姓名(name)和年龄(age),存储到这个HashMap,其中键、值分别为姓名、年龄;直到输入的姓名为end为止
    2. 判断这个Map集合是否为空,并输出判断的结果
    3. 输出这个Map集合的大小
    4. 判断这个Map集合是否包含姓名为“王五”的元素,并输出断结果
    5. 将map里姓名为”王五”的数据删掉
    6. 获取map的所有键,并输出
    7. 获取map的所有值,并输出
    8. 遍历map的所有健值对,并输出

2、编写程序,使用HashMap,完成本章Collection系列中“实战和作业”第2题产品分类汇总作业