集合

集合

集合的理解和好处
数组不足:

  • 长度开始时必须指定,,而且一旦指定不能被更改
  • 保存的必须为同一类型元素
  • 使用数组进行元素增加代码较为麻烦

数组扩容示例

Person[] pers = new Person(1);
per[0] = new Person();

增加新的person对象

Person[] pers2 = new Person[pers.length+1]; // 新创建数组
for(){} // for循环拷贝新的数组元素到pers2
pers2[pers2.length-1] = new Person(); // 添加新的对象

集合好处:

  • 可以动态保存任意多个数组,使用方便
  • 提供了一系列方便操作对象的方法:add,remove,set,get等
  • 使用集合添加,删除新的元素简洁明了

集合框架体系(背诵记忆)

框架:

单列集合

双列集合

解读:
1.集合主要分为两组(单列集合,双列集合)
2.Collection 接口有两个重要子接口:List 和 Set ,他们的实现子类都是单列集合
3.Map 接口的实现子类是双列(K键,V值)集合,存放键值对

Collection接口

Collection接口实现特点

  • Collection实现子类可以存放多个元素,每个元素可以是Object
  • 有些Collection的实现类可以存放重复元素,有些不可以
  • 有些Collection的实现类,有些是有序的(List),有些不是有序的(Set)
  • Collection接口没有直接的实现子类,是通过他的子接口Set和List来实现的

Collection常用方法(实现子类ArrayList演示)

  • add :添加单个元素
  • remove :删除指定元素
  • contains :查找元素是否存在
  • size :获取元素个数
  • isEmpty :判断是否为空
  • clear :清空
  • addAll :添加多个元素
  • containsAll :查找多个元素是否都存在
  • removeAll :删除多个元素
    ps:

    删除有这两种输入形式,第一种指定删除内容,返回值为boolean;第二种为删除指定元素下标,返回值为对象

方法详解:

package javaSEStudy.collection_;

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

// Collection 接口方法
public class CollectionMethod {
    // 忽略所有警告信息
    @SuppressWarnings("all")
    public static void main(String[] args) {
        List list = new ArrayList();

        // add 添加单个元素
        list.add("Hello");
        list.add(10);  // 相当于list.add(Integer(10))
        list.add(true);
        list.add("World");
        list.add("Java");
        System.out.println("添加: "+list);

        // remove 删除指定元素
        list.remove("Hello");// 删除Hello
        list.remove(0); // 删除第1个元素
        System.out.println("删除: "+list);

        // contains 查找元素是否存在
        System.out.println(list.contains("Java"));

        // size 获取元素个数
        System.out.println(list.size());

        // isEmpty 判断是否为空
        System.out.println(list.isEmpty());

        // clear 清空,集合里面所有元素都没了
        list.clear();
        System.out.println("清空之后的集合: "+list);

        // addAll 添加多个元素
        ArrayList list1 = new ArrayList();
        list1.add("红楼梦");
        list1.add("三国演义");
        list1.add("西游记");
        list1.add("水浒传");
        list.addAll(list1);
        System.out.println("添加了一个新集合的集合: "+list);

        // containsAll 查找多个元素是否都存在
        System.out.println(list.containsAll(list1));

        // 删除多个元素
        list.add("聊斋");
        list.removeAll(list1);
        System.out.println("将list1集合内容删除后的集合: "+list);

    }
}

Collection接口遍历元素

使用Iterator(迭代器)

  • Iterator 对象称为迭代器,主要用于遍历Collection集合中的元素
  • 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器
  • Iterator结构图
  • Iterator仅用于遍历集合,Iterator本身并不存放对象

Iterator执行原理

package javaSEStudy.collection_;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

// Iterator 迭代器
public class CollectionIterator {
    @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("红楼梦","曹雪芹",62.3));

//        System.out.println(col);
        // 希望遍历col集合
        /**
         * 1.先得到col对应的迭代器
         * 2.使用while循环遍历
         * 3.当退出while循环后,Iterator指向最后的元素
         * 4.如果希望再此遍历,需要重置迭代器 iterator = col.iterator();
         *  快捷键输出while (iterator.hasNext()) {}  -----> itit
         *  Ctrl+j  输出当前可以快捷生成的所有模版
         */
        Iterator iterator = col.iterator();
        while (iterator.hasNext()) {
            // 判断是否还有数据
            // 返回下一个元素类型是Object
            Object obj = iterator.next();
            System.out.println(obj);
        }
        // 重置迭代器
        iterator = col.iterator();
        System.out.println("======第二次迭代======");
        while (iterator.hasNext()) {
            // 判断是否还有数据
            // 返回下一个元素类型是Object
            Object obj = iterator.next();
            System.out.println(obj);
        }
    }
}

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;
    }

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

Iterator接口方法

使用for循环增强

增强for循环可以替代iterator迭代器,特点:增强for循环就是简化版的itreator,本质一样,只能用于遍历集合或数组

// 基本语法
for(元素类型 元素名:集合名或数组名){
    访问元素
}

让我们来举个栗子

package javaSEStudy.collection_;

import java.util.ArrayList;
import java.util.Collection;

// 增强for循环详解
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("红楼梦", "曹雪芹", 62.3));

        /**
         * 详解
         * 1.使用增强for,可以在Collection上
         * 2.增强for,底层仍然是Iterator
         * 3.增强for可以理解为简化版 迭代器遍历
         * 4.快捷键 I
         */
        // 使用增强for循环
        for (Object book : col) {
            System.out.println(book);
        }

        // 也可以直接在数组上使用
        int[] num = {15, 15, 589, 43, 59, 31, 48, 942, 65};
        for (int i : num) {
            System.out.println(i);
        }

    }
}

课堂练习

package javaSEStudy.collection_.class_Test;

import java.util.ArrayList;
import java.util.Iterator;

// 创建3个对象,分别使用功能增强for和Iterator遍历输出
public class Test01 {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        ArrayList list = new ArrayList();

        list.add(new Dog("Jack",1));
        list.add(new Dog("Tom",2));
        list.add(new Dog("Victoria",5));

        // 使用迭代器Iterator
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object D = iterator.next();
            System.out.println(D);
        }

        // 使用增强for循环
        for (Object D : list) {
            System.out.println(D);
        }
    }
}

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;
    }

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

List接口

基本介绍

List接口是Collection接口的子接口

  • List集合类中元素有序(即添加顺序和取出顺序一致),且可重复
  • List集合中的每个元素都有其对应的顺序索引,即支持索引
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可根据序号存取容器中的元素
  • JDK API中List接口的实现类有

常用方法

list集合里添加了一些根据索引来操作集合元素的办法

  • void add(int index,Object ele) :在index位置插入ele元素
  • boolean addAll(int index,Collection eles) :从index位置开始将eles中所有元素添加进来
  • Object get(int index) :获取指定index位置的元素
  • int indexOf(Object obj) :返回obj在当前集合中末次出现的位置
  • Object remove(int index) :移除指定index位置的元素,并返回此元素
  • Object set(int index,Object ele) :设置指定index位置的元素为ele,相当于替换
  • List subList(int formIndex,int toIndex) :返回从formIndex到toIndex位置的子集合
package javaSEStudy.collection_.list;

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

// List接口的方法
public class ListMethod {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        List list1 = new ArrayList();
        list1.add("Tom");
        list1.add("Jack");
        list1.add("Marry");
        list1.add("蔡徐坤");
        list1.add("小黑");

        /**
         * void add(int index,Object ele)  :在index位置插入ele元素
         */
        list1.add(1,"白展堂");
        System.out.println("被插入数据后的集合: "+ list1);

        /**
         * boolean addAll(int index,Collection eles) :从index位置开始将eles中所有元素添加进来
         */
        List list2 = new ArrayList();
        list2.add("小黑");
        list2.add("小白");
        list2.add("小黄");
        list2.add("小红");
        list1.addAll(3,list2);
        System.out.println("加入一个集合后的集合: "+ list1);

        /**
         * Object get(int index) :获取指定index位置的元素
         */
        System.out.println("集合中第6个元素: "+list1.get(5));

        /**
         * int indexOf(Object obj)  :返回obj在当前集合中首次出现的位置
         * int lastIndexOf(Object obj)  :返回obj在当前集合中末次出现的位置
         */
        System.out.println("小黑在集合中首次出现的位置: "+list1.indexOf("小黑"));
        System.out.println("小黑在集合中最后一次出现的位置: "+list1.lastIndexOf("小黑"));

        /**
         * Object remove(int index) :移除指定index位置的元素,并返回此元素
         */
        System.out.println("集合被删除的元素: "+list1.remove(0));
        System.out.println("删除元素后的集合: "+list1);

        /**
         * Object set(int index,Object ele) :设置指定index位置的元素为ele,相当于替换
         */
        list1.set(0,"Victoria");
        System.out.println("将第一个元素替换后的集合: "+list1);

        /**
         * List subList(int formIndex,int toIndex) :返回从formIndex到toIndex位置的子集合
         * [0,3) 左闭右开
         */
        List list = list1.subList(0, 3);
        System.out.println("该位置的子集合: "+list);
    }
}

课堂练习

package javaSEStudy.collection_.list.test;

import java.util.ArrayList;
import java.util.Iterator;

// 对集合进行操作
public class Test01 {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
//        list.add("Jack");
//        list.add("Marry");
//        list.add("Tom");
//        list.add("Java");
//        list.add("Python");
//        list.add("小红");
//        list.add("小黑");
//        list.add("小黄");
//        list.add("小白");
//        list.add("小蓝");
        for (int i = 0; i < 10; i++) {
            list.add("小青"+i);
        }
        System.out.println("初始集合: "+list);

        // 插入
        list.add(1,"蔡徐坤");
        // 获取第5个元素
        System.out.println("第五个元素: "+list.get(4));
        // 删除第六个元素
        list.remove(5);
        // 修改第七个元素
        list.set(6,"叼毛");

        // Iterator迭代器遍历
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object list1 =  iterator.next();
            System.out.print(list1+" ");
        }
    }
}

List的遍历方式

ps:ArrayList,LinkedList,Vector的遍历方式也和List一样
方式一:使用Iterator
方式二:使用增强for循环
方式三:使用普通循环

package javaSEStudy.collection_.list;

import java.util.*;

// List接口的遍历方式
public class ListFor {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        /**
         * 下面这三种运行类型都不报错,证明使用遍历方法都是一样的
         */
        List list = new ArrayList();
//        List list = new Vector();
//        List list = new LinkedList();

        list.add("Jack");
        list.add("Marry");
        list.add("Tom");
        list.add("Java");
        list.add("Python");

        // Iterator
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            System.out.println(obj);
        }

        System.out.println("=========================");

        // 增强for
        for (Object list1 : list) {
            System.out.println(list1);
        }

        System.out.println("=========================");

        // 普通for
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

课堂练习

package javaSEStudy.collection_.list.test;

import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

// 实现类的课堂练习
public class Test02 {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        /**
         * 使用ArrayList
         */
        ArrayList list1 = new ArrayList();
        list1.add(new Book("红楼梦", "曹雪芹", 100.1));
        list1.add(new Book("西游记", "吴承恩", 10.1));
        list1.add(new Book("水浒传", "施耐庵", 9));
        list1.add(new Book("三国演义", "罗贯中", 80));
        list1.add(new Book("西游记", "吴承恩", 10.1));

        bubbleSort(list1);
        System.out.println(list1);

        /**
         * LinkedList
         */
        LinkedList list2 = new LinkedList();
        list2.add(new Book("红楼梦", "曹雪芹", 100.1));
        list2.add(new Book("西游记", "吴承恩", 10.1));
        list2.add(new Book("水浒传", "施耐庵", 9));
        list2.add(new Book("三国演义", "罗贯中", 80));
        list2.add(new Book("西游记", "吴承恩", 10.1));

        bubbleSort(list2);
        System.out.println(list2);

        /**
         * Vector
         */
        Vector list3 = new Vector();
        list3.add(new Book("红楼梦", "曹雪芹", 100.1));
        list3.add(new Book("西游记", "吴承恩", 10.1));
        list3.add(new Book("水浒传", "施耐庵", 9));
        list3.add(new Book("三国演义", "罗贯中", 80));
        list3.add(new Book("西游记", "吴承恩", 10.1));

        bubbleSort(list3);
        System.out.println(list3);
    }

    // 冒泡排序
    public static void bubbleSort(List<Book> list) {
        for (int i = 0; i < list.size() - 1; i++) {
            for (int j = 0; j < list.size() - i - 1; j++) {
                if (list.get(j).getPrice() > list.get(j + 1).getPrice()) {
                    Book temp = list.get(j);
                    list.set(j, list.get(j + 1));
                    list.set(j + 1, temp);
                }
            }
        }
    }
}

@Data
@AllArgsConstructor
class Book {
    private String name;
    private String author;
    private double price;
}

ps:因为使用可lambok,就没有更改重写格式,输出就不是如图效果,但是原理是一样的

ArrayList

注意事项

  • permits all elements,includding null ,ArrayList 可以加入多个null
  • ArrayList 是由数组来实现数据存储的
  • Arrays基本等同于Vector,除了ArrayList是线程不安全(执行效率高),在多线程情况下不建议使用ArrayList

底层源码分析

  • ArrayList中维护了一个Object类型的数组elementData
    transient Object[] elementData; // transient 表示短暂的,瞬间, 表示该属性不会被序列化
  • 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次添加时,则扩容为10,如需再次扩容,则扩容elementData为1.5倍,也就是elementData*1.5,单数舍去小数部分
  • 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍
package javaSEStudy.collection_;

import java.util.ArrayList;

// ArrayList注意事项及源码分析
@SuppressWarnings({"all"})
public class ArrayListDetail {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();

        /**
         * ArrayList 可以存放多个null
         * 他的所有方法都没有加 ynchronized ()
         */
//        list.add(null);
//        list.add(null);
//        list.add("玛丽");
//        System.out.println(list);

        /**
         * 使用有参和无参构造器创建对象
         * 默认初始化是无参构造
         */
//        ArrayList list = new ArrayList(8);
        // 使用for循环添加1~10个数据
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        // 使用for循环添加11~15个数据
        for (int i = 11; i <= 15; i++) {
            list.add(i);
        }
        list.add(100);
        list.add(200);
        list.add(null);


    }
}

无参构造器源码

首先执行方法,创建一个空的elementData数组

然后执行添加方法,但是会先确认ensureCapacityInternal() 确认是否扩容,在执行赋值

再确认minCapacity值,最开始minCapacity=0,DEFAULT_CAPACITY=10,取大值赋值

注意,此时不会先对其赋值,先去判断大小够不够

这里是执行扩容机制的源码

然后最后开始逐步返回值,将其扩容

有参构造器源码

首先执行的是有参构造

中间程序都和无参构造一样
而最后扩容机制

  • 第一次扩容就按照elementData*1.5来扩容
  • 整个执行流程都差不多

Vevtor

介绍

  • Vector类的定义说明
  • Vector的底层也是一个对象数组 protected Object[] elementData;
  • Vector 是线程同步的,即线程安全,Vector类的操作方法带有synchronized
  • 在开发中,需要线程同步安全时,考虑使用Vector

Vector和ArrayList的比较

底层结构 版本 线程安全,同步效率 扩容倍数
ArrayList 可变数组 JDK1.2 不安全,效率高 如果为有参构造,则是1.5倍;如果是无参构造,第一次为10,第二次为1.5倍
Vector 可变数组 JDk1.0 安全,效率不高 如果是无参构造,默认为10,满后就按照2倍扩容;如果指定大小,则每次直接按2倍扩容
package javaSEStudy.collection_;

import java.util.Vector;

// Vector详解
@SuppressWarnings({"all"})
public class VectorDetail {
    public static void main(String[] args) {
        /**
         * JDK17版本
         * new Vector() 底层就是10
         * 初始化方法(无参构造):
         *     public Vector() {
         *         this(10);
         *     }
         * 初始化方法(有参构造):
         *     public Vector(int initialCapacity) {
         *         this(initialCapacity, 0);
         *     }
         */
//        Vector v = new Vector(8);
        Vector v = new Vector();
        for (int i = 0; i < 10; i++) {
            v.add(i);
        }
        v.add(100);
        /**
         * 扩容机制
         * 1.首先自动装箱
         *     public static Integer valueOf(int i) {
         *         if (i >= IntegerCache.low && i <= IntegerCache.high)
         *             return IntegerCache.cache[i + (-IntegerCache.low)];
         *         return new Integer(i);
         *     }
         * 2.添加数据到Vector集合
         *    public synchronized boolean add(E e) {
         *         modCount++;
         *         add(e, elementData, elementCount);
         *         return true;
         *     }
         * 3.确定是否需要扩容
         *     private void add(E e, Object[] elementData, int s) {
         *         if (s == elementData.length)
         *             elementData = grow();
         *         elementData[s] = e;
         *         elementCount = s + 1;
         *     }
         * 4.执行扩容
         * 第一步
         * private Object[] grow(int minCapacity) {
         *         int oldCapacity = elementData.length;
         *         int newCapacity = ArraysSupport.newLength(oldCapacity,
         *                 minCapacity - oldCapacity, /* minimum growth
         * capacityIncrement > 0 ? capacityIncrement : oldCapacity /* preferred growth );
         * return elementData = Arrays.copyOf(elementData, newCapacity);
         *}
         * 第二步
         *   int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
         *   if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
         *         return prefLength;
         *      } else {
         *          // put code cold in a separate method
         *          return hugeLength(oldLength, minGrowth);
         *       }
         *   }
         * 第三步
         *     public static int max(int a, int b) {
         *         return (a >= b) ? a : b;
         *     }
         * 将扩容后的集合返回
         *     public static <T> T[] copyOf(T[] original, int newLength) {
         *         return (T[]) copyOf(original, newLength, original.getClass());
         *     }
         */
    }
}

LinkedList

说明

  • LinkedList底层实现了双向链表双端队列特点
  • 可以添加任意元素(元素可以重复),包括null
  • 线程不安全,没有实现同步

底层操作机制

  • LinkedList底层维护了一个双向链表
  • LinkedList中维护了两个属性first和last分别指向首节点和尾节点
  • 每个节点(Node对象),里面又维护了prev,next,item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终实现双向链表
  • 所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高
  • 模拟一个简单的双向链表

我们对双向链表简单理解

package javaSEStudy.collection_.LinkedLIst;

// 双向链表简单理解
@SuppressWarnings({"all"})
public class Doubly_LinkedList {
    public static void main(String[] args) {
        // 实现一个简单的双向链表
        Node jack = new Node("jack");
        Node queen = new Node("queen");
        Node lady = new Node("lady");

        // 连接三个结点,形成双向链表
        // jack --> queen --> lady
        jack.next = queen;
        queen.next = lady;

        // lady --> queen --> jack
        lady.prev = queen;
        queen.prev = jack;

        // 让first引用指向jack,就是双向链表的头结点
        Node first = jack;
        // 让last引用指向 lady,就是双向链表的尾结点
        Node last = lady;

        /**
         * 演示 从头到尾遍历(重要)
         */
        System.out.println("------从头到尾-------");
        while (true){
            if (first == null){
                break;
            }
            System.out.println(first);
            first = first.next;
        }

        /**
         * 演示 从尾到头遍历(重要)
         */
        System.out.println("------从尾到头-------");
        while (true){
            if (last == null){
                break;
            }
            System.out.println(last);
            last = last.prev;
        }

        /**
         * 演示双向链表的添加删除对象
         * 在queen和lady之间添加一个对象
         */
        Node victoria = new Node("victoria");

        queen.next = victoria;
        victoria.next = lady;

        lady.prev = victoria;
        victoria.prev = queen;

        // 注意需要对first重置,否则无法输出
        first = jack;
        System.out.println("------从头到尾(二次遍历)-------");
        while (true){
            if (first == null){
                break;
            }
            System.out.println(first);
            first = first.next;
        }

        last = lady;
        System.out.println("------从尾到头(二次遍历)-------");
        while (true){
            if (last == null){
                break;
            }
            System.out.println(last);
            last = last.prev;
        }
    }
}

// 定义一个Node类,Node 对象 表示双向链表的一个结点
@SuppressWarnings({"all"})
class Node{
    public Object item; // 真正存放数据的地方,所以是Object
    public Node next;   // 指向下一个结点
    public Node prev;   // 指向上一个结点

    public Node(Object name) {
        this.item = name;
    }

    @Override
    public String toString() {
        return "Node name : " + item;
    }
}

LinkedList修改

话不多说,我们直接上代码

package javaSEStudy.collection_.LinkedLIst;

import java.util.Iterator;
import java.util.LinkedList;

// LinkedList的增删改查
@SuppressWarnings({"all"})
public class LinkedListCRUD {
    public static void main(String[] args) {

        LinkedList list = new LinkedList();

        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }
        System.out.println("添加: " + list);
        /**
         * 添加方法底层源码
         * 1.new LinkedLIst 创建了一个空数组
         *     public LinkedList() {
         *     }
         * 此时它的first和last都等于null
         * 2.执行add方法
         *     public boolean add(E e) {
         *         linkLast(e);
         *         return true;
         *     }
         * 3.将新的结点加到双向链表的最后
         *     void linkLast(E e) {
         *         final Node<E> l = last;
         *         final Node<E> newNode = new Node<>(l, e, null);
         *         last = newNode;
         *         if (l == null)
         *             first = newNode;
         *         else
         *             l.next = newNode;
         *         size++;
         *         modCount++;
         *     }
         */

        // 演示一个删除结点
        list.remove();  // 如果没有参数默认删除第一个元素
        // 此时在对新链表删除第三个元素
        list.remove(2);
        System.out.println("删除: " + list);
        /**
         * list.remove();
         * 执行方法:
         *     public E remove() {
         *         return removeFirst();
         *     }
         * 再次执行:
         *    public E removeFirst() {
         *         final Node<E> f = first;
         *         if (f == null)
         *             throw new NoSuchElementException();
         *         return unlinkFirst(f);
         *     }
         * 删除底层源码unlinkFirst
         *     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 GC
         *         first = next;
         *         if (next == null)
         *             last = null;
         *         else
         *             next.prev = null;
         *         size--;
         *         modCount++;
         *         return element;
         *     }
         */

        // 修改某个结点对象
        list.set(0, 999);
        System.out.println("修改: " + list);

        // 得到某个结点对象
        // get(5)是得到双向链表的第六个对象
        System.out.println(list.get(5));

        // 遍历
        // 因为LinkedList实现了List接口,遍历方式可用迭代器,增强for,普通for
        System.out.println("--------遍历----------");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        System.out.println("--------遍历----------");
        for (Object o : list) {
            System.out.println(o);
        }

        System.out.println("--------遍历----------");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

比较

ArrayList和LinkedList比较

底层结构 增删的效率 改查的效率
ArrayList 可变数组 较低;数组扩容 较高
Vector 双向链表 较高,通过链表增加 较低

如何选择?

  • 如果我们改查操作较多,选择ArrayList
  • 如果我们增删的操作较多,选择LInkedList
  • 一般来说,在程序中80%~90%都是查询,因此大部分情况下都会选择ArrayList
  • 在一个项目中,根据业务灵活选择,也可能一个模块使用ArrayList,一个模块使用LinkedList

Set

基本介绍

  • 无序(添加和取出的顺序不一致),没有索引
  • 不允许重复元素,所以最多包含一个null
  • JDK API中的Set接口的实现类有(最主要的是(HashSet TreeSet))

Set接口

常用方法
和List接口一样,Set也是Collection的子接口,因此常用方法和Collection接口一样

遍历方式

同Collection的遍历方式一样,因为Set接口是Collection接口的子接口
1.可以使用迭代器
2.增强for
3.不能使用索引的方式来获取(也没有get方法),因此不能使用普通for

让我们来举个栗子

package javaSEStudy.collection_.set_;

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

// Set方法详解
@SuppressWarnings({"all"})
public class SetMethod {
    public static void main(String[] args) {

        // 以Set的实现类HashSet展示
        /**
         * 1.set接口的实现类的对象(set接口对象),不能存放重复数据
         * 2.可添加一个null
         * 3.set接口对象存放数据是无序的(添加和取出顺序不一致)
         * 4.但是取出顺序虽然不是添加的顺序,但是固定的
         */
        Set set = new HashSet();
        set.add("jack");
        set.add("tom");
        set.add("james");
        set.add("james");
        set.add(null);
        set.add(null);
        // 每次取出的值都是固定的
        for (int i = 0; i < 10; i++) {
            System.out.println(set);
        }

        /**
         * 遍历 迭代器,增强for
         * 即使自己遍历,输出顺序也是固定的
         * Set没有get方法,也不能通过索引获取,所以不能使用普通for
         */
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        System.out.println("-------------------------");
        for (Object o : set) {
            System.out.println(o);
        }
    }
}

HashSet

说明

  • HashSet实现了Set接口
  • HashSet实际上是HashMap
  • 可以存放null,但是也只能有一个null
  • HashSet不保证元素使有序的,取决于Hash后,在确定索引值
  • 不能有重读元素/对象,Set接口特性
package javaSEStudy.collection_.set_;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.HashSet;

// HashSet详解
@SuppressWarnings({"all"})
public class HashSet_ {
    public static void main(String[] args) {

        HashSet hashSet = new HashSet();
        /**
         * 1.HashSet本质上是HashMap
         * 调用HashSet底层源码(构造器)
         *     public HashSet() {
         *         map = new HashMap<>();
         *     }
         *
         * 2.能存放null,只能存放一个
         * 3.不保证存放元素顺序和取出顺序一致
         */

        hashSet.add(1);
        hashSet.add(null);
        hashSet.add(null);

        // 执行add方法后,会返回一个boolean值,添加成功 返回一个true,失败则返回false
        System.out.println(hashSet.add(5));
        System.out.println(hashSet.add(6));
        System.out.println(hashSet.add(5));

        // 可以删除对象,添加的对象加了引号,则删除时也得加,否则不用
        hashSet.remove(6);
        System.out.println(hashSet);

        System.out.println("-----------------------------");

        // 将hashSet重置,此时他为空
        hashSet = new HashSet();

        /**
         * HashSet不能存放相同的元素/对象?
         * 注意对象! 这里是new的,Dog类默认调用的是Object的equals和hashCode,基于内存比较
         * 此时他们内存地址不同,认为这里是两个不同对象,所有都能存进去
         */

        /**
         * 假如在Dog类添加注解@Data @AllArgsConstructor
         * Lombok生成的equals()和hashCode()基于name字段判断相等性。
         * 所以此时字段相同,认为是同一个对象,去重保留一个
         */
        hashSet.add("jack");
        hashSet.add("jack");
        hashSet.add(new Dog("Tom"));  // 可以加入
        hashSet.add(new Dog("Tom"));  // 可以加入
        System.out.println(hashSet);

        // 经典面试题 只加入了一个蔡徐坤
        // 因为存放的是String,将equals方法重写,按内容进行比较
        hashSet.add(new String("蔡徐坤"));  // T
        hashSet.add(new String("蔡徐坤"));  // F
        System.out.println(hashSet);

    }
}

//@Data
//@AllArgsConstructor
class Dog{
    private String name;
    public Dog(String name) {
        this.name = name;
    }

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

TreeSet

底层其实是TreeMap
本质上是调用String类的compare比较器进行排序

package javaSEStudy.collection_Map.collection_.set_.treeSet;

import java.util.Comparator;
import java.util.TreeSet;

// TreeSet详解
@SuppressWarnings({"all"})
public class TreeSet_ {
    public static void main(String[] args) {
//        TreeSet ts = new TreeSet();
        /**
         * 底层源码
         *  1.在没有做任何处理的时候,它是无序的(使用无参构造器)
         *  2.使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类),并指定排序规则
         *      首先构造器把传入的构造器对象赋给了TreeSet 的底层 TreeMap
         *      然后在调用 treeSet.add("world"),在底层会执行到
         *
         *      // 先将比较器取出
         *      Comparator<? super K> cpr = comparator;
         *      if (cpr != null) {
         *             do {
         *                 parent = t;
         *
         *                 // 这里的cpr就是我们的匿名内部类(对象)
         *                 cmp = cpr.compare(key, t.key);
         *                 if (cmp < 0)
         *                     t = t.left;
         *                 else if (cmp > 0)
         *                     t = t.right;
         *                 else {
         *                     // 如果相等则返回0,那么这个数据则加入不了
         *                     V oldValue = t.value;
         *                     if (replaceOld || oldValue == null) {
         *                         t.value = value;
         *                     }
         *                     return oldValue;
         *                 }
         *             } while (t != null);
         *
         */
        TreeSet ts = new TreeSet(new Comparator() {

            @Override
            public int compare(Object o1, Object o2) {
                // 调用String的compareTo方法进行字符串的大小比较
                // 按unicode编码进行排序,也就是首字母顺序
//                return ((String) o1).compareTo((String) o2);

                // 按加入元素的长度大小进行排序(从大到小)
                // 这里相同长度的就无法加入,因为底层将其 认为长度相等就是同一个元素
                return ((String) o2).length() - ((String) o1).length();
            }
        });
        ts.add("hello");
        ts.add("world");
        ts.add("what");
        ts.add("are");
        ts.add("you");
        ts.add("弄啥嘞");

        System.out.println(ts);
    }
}

源码初解

HashMap底层是(数组+链表+红黑树)

存放方式:

  • 先获取元素的哈希值(HashCode方法)
  • 对哈希值进行运算`,得出一个索引值,即为要存放在哈希表的位置号
  • 如果该位置上没有别的元素,则直接存放
  • 如果有元素,先进行equals判断,相等则不存放,不相同则以链表形式添加
    ps:注意,有无对equals方法重写,未重写则比较的是地址
package javaSEStudy.collection_.set_;

// HashSet 模拟底层链表存储
@SuppressWarnings({"all"})
public class HashSetStructure {
    public static void main(String[] args) {
        // 模拟HashSet的底层 (HashMap的底层结构)

        /**
         * 1.创建一个数组,数组,数组类型是 Node[]
         * Node[] 也可称为表
         */
        Node[] table = new Node[16];

        // 创建一个结点
        Node john = new Node("john", null);
        // 将该结点存放在索引为2的位置上
        table[2] = john;
        Node jane = new Node("jane", null);
        // 将jane结点挂载到john后面
        john.next = jane;
        // 在链表后再添加一个数据
        Node rose = new Node("rose", null);
        jane.next = rose;

        Node victoria = new Node("victoria", null);
        // 在索引为3的位置存入结点victoria
        table[3] = victoria;
        System.out.println(table);

    }
}

// 创建一个结点  存储数据,可以指向下一个结点,从而形成链表
class Node{
    // 存放数据
    Object item;
    // 指向下一个结点
    Node next;

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }

}

形成链表结构

再添加元素

底层机制

添加元素

一个开发小技巧:在哪个位置需要局部变量(辅助变量),就在哪个位置创建

  • 1.HashSet底层是HashMap
  • 2.添加一个元素时,先得到hash值-->会转换成索引值
  • 3.找到存储数据表table,看这个索引位置是否已经存放有元素
  • 4.如果没有则直接加入
  • 5.如果有调用equals(该equals方法由程序员自己确定)比较,如果相同,就放弃添加,如果不同则添加到最后
  • 6.在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8)最大值,并且table表的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
package javaSEStudy.collection_.set_;

import java.util.HashSet;

// HashSet底层也就是HashMap详解
@SuppressWarnings({"all"})
public class HashSetSource {
    public static void main(String[] args) {

        HashSet hashSet = new HashSet();
        hashSet.add("php");
        hashSet.add("java");
        hashSet.add("python");
        hashSet.add("java");
        System.out.println(hashSet);

        /**
         * HashSet源码解读
         * 1.执行构造器
         *     public HashSet() {
         *         map = new HashMap<>();
         *     }
         * 2.执行一个add方法,E是泛型,将字符串常量赋值给e
         *     public boolean add(E e) {
         *         // 这里验证返回值是否为空,如果返回一个查询对象,则证明加入失败,为null才加入成功
         *         return map.put(e, PRESENT)==null;
         *         // 在这里 PERSENT是:    private static final Object PRESENT = new Object();
         *         // 用于占位,让HashSet使用HashMap
         *     }
         * 3.执行put方法,该方法会执行hash(key)方法,得到key对应的hash值
         *     public V put(K key, V value) {  // k-->"php",value-->PRESENT(共享)
         *         return putVal(hash(key), key, value, false, true);
         *     }
         *
         *     哈希值是不是HashCode?  不是,因为这里(h = key.hashCode()) ^ (h >>> 16)执行了处理
         *     哈希值计算方法
         *         static final int hash(Object key) {
         *         int h;
         *         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
         *         }
         * 4.核心代码
         *     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[] 用于存放结点
         *         // 如果这个table的大小为0或者null,执行扩容,扩容到16个空间
         *         // resize()方法中计算了一个数组大小临界值(数组扩容大小*0.75),到达这个值(这里是12)就开始准备扩容
         *         if ((tab = table) == null || (n = tab.length) == 0)
         *             n = (tab = resize()).length;
         *
         *         // (1)根据key,得到hash 去计算该key应该存放到table表中的哪个索引位置
         *               并把这个位置的对象赋值给p
         *         // (2)再判断p是否为空
         *               如果p为null,表示还没有存放过数据,就创建一个Node(key,value) 在这里key="php",value=PRESENT
         *               就将该值放在这个位置 tab[i] = newNode(hash, key, value, null);
         *         // 第二次存入数据则直接进入这个判断,判断存入哪个位置
         *         if ((p = tab[i = (n - 1) & hash]) == null)
         *             // 在这里还存入了 hash,用于之后添加数据比较
         *             tab[i] = newNode(hash, key, value, null);
         *         else {
         *             // 开发技巧:在哪儿需要辅助变量,就在哪个位置定义
         *             Node<K,V> e; K k;  // 定义辅助变量
         *             // 如果当前索引位对应链表的第一个元素和准备添加的key的hash值一样
         *             // 并且满足下面两个条件之一:
         *                      准备加入的key 和 p指向的Node结点的 key 是同一个对象
         *                      p指向的Node结点的 key的equals()和准备加入的key 比较后相同
         *                 就不能加入
         *             if (p.hash == hash &&
         *                 ((k = p.key) == key || (key != null && key.equals(k))))
         *                 e = p;
         *
         *             // 再判断p是不是一颗红黑树
         *             // 如果是红黑树,就调用putTreeVal该方法增加
         *             else if (p instanceof TreeNode)
         *                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
         *
         *             // 如果table对应的索引位置是一个链表,就依次比较
         *             else {
         *                 // 循环比较,将拿到的值和链表中的值一个一个比较
         *                          1.如果依次比较后,都不相同,则放在最后
         *                              ps:如果将该元素添加到链表后,立马判断该链表是否 >=8 个结点;
         *                              就调用treeifyBin(tab, hash)进行树化(红黑树);
         *                              在进行树化前,进行判断,判断条件(表为空或长度小于64):
         *                                  if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
         *                                      // 对table执行扩容
         *                                      resize();
         *                              只有当上述条件不成立,才执行树化
         *
         *                          2.如果依次比较后,有相同情况,直接break
         *                 for (int binCount = 0; ; ++binCount) { // 死循环,退出情况只有两种
         *                     if ((e = p.next) == null) {
         *                         p.next = newNode(hash, key, value, null);
         *
         *                         // 没有相同情况
         *                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
         *                             treeifyBin(tab, hash);
         *                         break;
         *                     }
         *
         *                     // 有相同情况
         *                     if (e.hash == hash &&
         *                         ((k = e.key) == key || (key != null && key.equals(k))))
         *                         break;
         *                     p = e;
         *                 }
         *             }
         *             if (e != null) { // existing mapping for key
         *                 V oldValue = e.value;
         *                 if (!onlyIfAbsent || oldValue == null)
         *                     e.value = value;
         *                 afterNodeAccess(e);
         *                 return oldValue;
         *             }
         *         }
         *         // 记录修改次数
         *         ++modCount;
         *         // 如果该数组大于了临界值(12),就执行扩容
         *         if (++size > threshold)
         *             resize();
         *         // 该方法对于HashMap是个空方法,留给子类去使用,例如生成一个有序链表
         *         afterNodeInsertion(evict);
         *         // 返回空代表挂载成功,否则就是返回原来的旧值
         *         return null;
         *     }
         */

    }
}

HashSet扩容和转成红黑树机制

  • HashSet的底层是HashMap,第一次添加时,table表数组扩容到16,临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12
    ps:链表上的元素也被认为是添加了一个元素,所以即使table只有两个结点有值,但是值仍然到达了12,也会执行扩容
  • 如果table数组使用到了临界值12,就会扩容到162 = 32,新的临界值就是320.75 = 24,以此类推
  • 在java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认为8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制
    ps:此处转红黑树机制:必须只有一条链表长度到达了8,且表长度为64才转红黑树,如果没有链表达到8,就无法树化,会一直按照扩容机制扩容
package javaSEStudy.collection_.set_;

import java.util.HashSet;

// 链表转红黑树
@SuppressWarnings({"all"})
public class HashSetIncrement {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        /**
         * 执行数组扩容机制
         * 当链表达到最大长度8,执行数组扩容机制时
         * 如果hash值相同,仍然会存储在该处,但是数组会扩容一次
         * 直到表到达64,进行树化
         */
        for (int i = 0; i < 12; i++) {
            hashSet.add(new A(i));
        }
    }
}

// 重写HashCode方法,让返回的值都为100,形成链表
class A {
    private int n;

    public A(int n) {
        this.n = n;
    }

    @Override
    public int hashCode() {
        return 100;
    }
}

课堂练习


ps:当重写equals方法时,HashCode也得重写,否则比较无意义

package javaSEStudy.collection_.set_.test;

import java.util.HashSet;
import java.util.Objects;

// name和age被认为是相同员工不能存储
@SuppressWarnings({"all"})
public class Test01 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("John", 10));
        hashSet.add(new Employee("Marry", 56));
        hashSet.add(new Employee("John", 10));
        System.out.println(hashSet);
    }
}

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;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return age == employee.age && Objects.equals(name, employee.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

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

package javaSEStudy.collection_.set_.test;

import java.util.HashSet;
import java.util.Objects;

// name和age被认为是相同员工不能存储
@SuppressWarnings({"all"})
public class Test01 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("John", 22,new MyDate(2005,5,4)));
        hashSet.add(new Employee("Marry", 22,new MyDate(2005,5,4)));
        hashSet.add(new Employee("John", 22,new MyDate(2005,5,4)));

        System.out.println(hashSet);
    }
}

class Employee {
    private String name;
    private int age;
    private MyDate birthday;

    public Employee(String name, int age, MyDate birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    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;
    }

    public MyDate getBirthday() {
        return birthday;
    }

    public void setBirthday(MyDate birthday) {
        this.birthday = birthday;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return Objects.equals(getName(), employee.getName()) && Objects.equals(getBirthday(), employee.getBirthday());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getName(), getBirthday());
    }

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

class MyDate {
    private int year;
    private int month;
    private int day;

    public MyDate(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;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        MyDate myDate = (MyDate) o;
        return getYear() == myDate.getYear() && getMonth() == myDate.getMonth() && getDay() == myDate.getDay();
    }

    @Override
    public int hashCode() {
        return Objects.hash(getYear(), getMonth(), getDay());
    }

    @Override
    public String toString() {
        return year +"-"+ month +"-"+ day;
    }
}

LinkedHashSet

说明

  • LinkedHashSet是HashSet的子类
  • LinkedHashSet 底层是 LinkedHashMap,底层维护了一个 数组+双向链表
  • LinkedHashSet 根据元素 HashCode 值来决定元素的存储位置,同时使用链表来维护元素的次序,这使得元素看起来是以插入顺序保存的
  • LinkedHashSet 不允许添加重复元素

底层源码

  • 1.在LinkedHashSet中维护了一个Hash表和双向链表(LinkedHashSet 有head和tail)
  • 2.每一个节点有before和after属性,这样可以形成双向链表
  • 3.在添加一个元素时,先求Hash值,再求索引,确定该元素在table表中的位置,然后将添加的元素加入到双向链表,如果已存在,则不添加(原则上和hashSet一样)
  • 4.这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致(有序地)
  • 5.第一次添加时,直接将数组table扩容到15,存放的节点类型是LinkedHashMap$Entry
  • 6.数组是 HashMap$Node[] 存放的元素是 LinkedHashMap$Entry类型 就说明Node是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);
        }
    }

Map接口


ps:这里讲解的是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
  • 8.Map存放数据的key-value示意图,一对 k-v 是放在一个HashMap$Node中的,有因为Node实现了 Entry接口,有些书上也说一对k-v就是一个Entry

    对8进行详细说明
    左边是一个table表,使用HashMap$Node 存放数据,然后将这些数据集中存放到一个集合当中,这个集合叫做entrySet,在将entrySet这个集合存放到右边的Entry中(方便控制,加强管理),Entry是实现了Set接口的,使用KeySet方法可单独控制输入Key值,使用Value可单独控制传入Value值,注意这里并不是存储,而是指向,实际数据还是存放在table中
    key使用的是Set接口,而value使用的是Collection接口

话不多说,我们直接看代码

package javaSEStudy.collection_.map_;

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

// Map接口实现类特点
@SuppressWarnings({"all"})
public class Map_ {
    public static void main(String[] args) {
        /**
         * 1.保存具有映射关系数据 Key-Value
         *   这里虽然看起来是有序的,但是其实还是无序的
         *   底层是按照哈希算法进行排序存放
         * 2.map中的 key 和 value 可以是任何引用类型的数据,会封装到HashMap$Node对象中
         */
        Map map = new HashMap();
        map.put("NO.1","Jack");  // 第一个为 Key , 第二个为 Value
        map.put("NO.2","Marry");

        /**
         * 3.map中的 key 不允许重复,和HashSet一样
         */
        // 注意,这里当key值相同时,这里是采用替换机制去做
        // 所以key值为NO.2的value被替换为 蔡徐坤
        map.put("NO.2","蔡徐坤");  // 当有相同的key值,等价于替换

        /**
         * 4.map中的value可以重复
         */
        map.put("NO.3","Jack");

        /**
         * 5.Map中的 key 可以为null,value也可以为 null,注意:key为null只能有一个,value为null,可以有多个
         */
        map.put(null,null);
        map.put(null,"基尼太美");
        map.put("NO.4",null);
        map.put("NO.5",null);

        /**
         * 6.常用String类作为Map的key
         */
        map.put(1,"张三丰");
        map.put("String类","无极");
        map.put(new Object(),"天下无贼");  // 也满足 key-value

        /**
         * 7.key 和 value 之间存在单向一对一关系,即通过指定的 key 值总能找到对应的value
         *      通过get方法传入一个key值,会返回对应的value
         */
        System.out.println(map.get(1));
        System.out.println(map.get(null));
        System.out.println(map.get("NO.5"));

        System.out.println(map);

        /**
         * 8.k-v为了方便程序员遍历,还会创建EntrySet集合,该集合存放的元素类型 Entry
         *      而一个Entry对象就有 k , v --> EntrySet<Entry<K,V>>
         *   entrySet 中,定义的类型为 map.Entry,但是实际上还是存放的是 HashMap$Node
         *      这是因为 static class Node<K,V> implement Map.Entry<K,V>
         *
         *   这样做的好处:当把 HashMap$Node 对象存放到 EntrySet 就方便我们的遍历,因为 Map.Entry 提供了重要的方法
         *      getKey()  getValue()
         */
        Set set = map.entrySet();
        // 查看类型
        System.out.println(set.getClass());  // HashMap$EntrySet
        for (Object obj : set) {
            // 查看存储的类型
//            System.out.println(obj.getClass());  // HashMap$Node
            // 为了从HashMap$Node取出k-v,需要向下转型
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey()+":"+entry.getValue());
        }
    }
}

常用方法

  • put :添加
  • remove :根据键删除对应映射关系
  • get :根据键获取值
  • size :获取元素个数
  • isEmpty :判断个数是否为0
  • clear :清除
  • containsKey :查找键是否存在

话不多说,我们直接上代码

package javaSEStudy.collection_.map_;

import org.w3c.dom.ls.LSOutput;

import java.util.HashMap;
import java.util.Map;

// map接口常用方法
@SuppressWarnings({"all"})
public class MapMethod {
    public static void main(String[] args) {
        /**
         * put 添加
         */
        Map map = new HashMap();
        map.put("邓超",new Book("",10)); // 第一次正常存入
        map.put("邓超","孙俪");  // 第二次被替换
        map.put("王宝强","马蓉");  // 正常存入
        map.put("宋喆","马蓉"); // 正常放入,男主不同
        map.put("大碗宽面",null);  // 进去了,key值唯一,正常存入
        map.put(null,"刘亦菲");  // 正常存入
        map.put("蔡徐坤","基尼太美");  // 正常存入

        System.out.println(map);

        /**
         * remove 根据键值删除映射关系
         * 这里有两种方法,一种只填入key值,另一种将key和value一起输入
         */
        map.remove(null);
        map.remove("蔡徐坤","基尼太美");
        System.out.println(map);

        /**
         * get 根据键获取值
         */
        System.out.println(map.get("邓超"));

        /**
         * size 获取元素个数,k-v算一个
         */
        System.out.println(map.size());

        /**
         * isEmpty 判断个数是否为0 输出为Boolean
         */
        System.out.println(map.isEmpty());

        /**
         * containsKey 查找键是否存在
         * containsValue 查找值是否存在
         */
        System.out.println(map.containsKey("王宝强"));
        System.out.println(map.containsValue("马蓉"));

        /**
         * clear 清除k-v
         */
        map.clear();
        System.out.println("map: "+map);

    }
}

class Book {
    private String name;
    private int num;

    public Book(String name, int num) {
        this.name = name;
        this.num = num;
    }
}

Map六大遍历方式

遍历需要用到方法
containsKey :查找键是否存在
keySet :获取所有的键
entrySet :获取所有的关系
values :获取所有的值

package javaSEStudy.collection_.map_;

import java.util.*;

// Map的六大遍历方式
@SuppressWarnings({"all"})
public class MapTraversal {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("邓超",new Book("",10)); // 第一次正常存入
        map.put("邓超","孙俪");  // 第二次被替换
        map.put("王宝强","马蓉");  // 正常存入
        map.put("宋喆","马蓉"); // 正常放入,男主不同
        map.put("大碗宽面",null);  // 进去了,key值唯一,正常存入
        map.put(null,"刘亦菲");  // 正常存入
        map.put("蔡徐坤","基尼太美");  // 正常存入

        /**
         * 第一组:将所有的key值取出,通过key取出对应的values
         */
        Set keySet = map.keySet();
        
        // 1.增强for 最简单
        System.out.println("======第一种:增强for======");
        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.增强for
        System.out.println("------增强for------");
        for (Object value : values) {
            // 无法通过values反向取值,只能显示values
            System.out.println(value);
        }

        // 2.迭代器
        System.out.println("------迭代器------");
        Iterator iterator1 = values.iterator();
        while (iterator1.hasNext()) {
            Object value = iterator1.next();
            System.out.println(value);
        }

        /**
         * 第三组:通过EntrySet 来获取k-v
         */
        Set entrySet = map.entrySet();
        // 1.增强for
        System.out.println("******* 增强for *******");
        for (Object entry : entrySet) {
            // 将entry转成Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + ":" + m.getValue());
        }
        // 2.迭代器
        System.out.println("******* 迭代器 *******");
        Iterator iterator2 = entrySet.iterator();
        while (iterator2.hasNext()) {
            // HashMap$Node -(实现)-> Map.Entry(getKey,getValue)
            // 在这里个取出的m就是 HashMap$Node
            Map.Entry m = (Map.Entry) iterator2.next();
            System.out.println(m.getKey() + ":" + m.getValue());
        }

    }
}

该怎么使用?

  • 1.默认使用的是entrySet()
  • 2.大数据量时必选entrySet()
  • 3.需要删除元素时,使用迭代器

Map课堂练习

package javaSEStudy.collection_.map_.test;

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

// 添加员工对象
@SuppressWarnings({"all"})
public class Test01 {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        hashMap.put(1,new Employees("Jack",19000,1));
        hashMap.put(2,new Employees("Jane",15000,2));
        hashMap.put(3,new Employees("James",21000,3));
        hashMap.put(4,new Employees("Marry",12000,4));
        hashMap.put(5,new Employees("Tom",1000,5));

        // 开始遍历
        // 使用增强for(效率低)
        Set set = hashMap.keySet();
        for (Object key :set) {
            // 通过键获取对应的值,并强制转换为Employees
            Employees obj =(Employees) hashMap.get(key);
            if (obj.getSalary() > 18000){
                System.out.println(obj);
            }
        }
        System.out.println("---------------------------------");
        // 使用 entrySet 迭代器(效率高)
        Set set1 = hashMap.entrySet();
        Iterator iterator = set1.iterator();
        while (iterator.hasNext()) {
            // 获取下一个键值对,并转换为entry
            Map.Entry entry = (Map.Entry) iterator.next();
            // 从entry中获取值,并强制转换为Employees
            // if可以简写 ((Employees) entry.getValue()).getSalary()
            Employees value =(Employees) entry.getValue();
            if (value.getSalary() > 18000){
                System.out.println(value);
            }
        }

    }
}

class Employees {
    private String name;
    private double salary;
    private int Id;

    public Employees(String name, double salary, int Id) {
        this.name = name;
        this.salary = salary;
        this.Id = Id;
    }

    public String getName() {
        return name;
    }

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

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    public int getId() {
        return Id;
    }

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

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

HashMap小结

  • 1.Map接口的常用的实现类:HashMap,Hashtable和Properties
  • 2.HashMap是Map接口使用频率最高的实现类
  • 3.HashMap是以key-value 对的方式来存储数据(HashMap$Ndoe类型)
  • 4.key不能重复,但是值可以重复,允许使用null键和null值
  • 5.如果添加相同的key,则会覆盖原来的key-value,等同于修改(key不会替换,value会)
  • 6.与HashSet一样,不保证映射顺序,因为底层是以hash表存储(JDK8的HashMap 底层: 数组+链表+红黑树)
  • 7.HashMap没有实现同步(方法没有做同步互斥操作),因此线程不安全

HashMap底层源码

扩容机制(和HashSet相同)

  • 1.HashMap底层维护了Node类型的数组table,默认为null
  • 2.当创建对象时,将加载因子(loadfactor)初始化为0.75
  • 3.当添加k-v时,通过key的哈希值得到table的索引,然后判断该索引处是否有元素,如果没有元素则直接添加,如果该索引处有元素,继续判断key值和准备加入的key值是否相等,如果相等则直接替换value,如果不相等需要判断是树结构还是链表结构,做出相应处理,如果添加时发现容量不够,则需要扩容 rasize()
  • 4.第一次添加,则需要扩容table容量为16,临界值(threshould)为12 (16*0.75 )
  • 5.以后扩容,则将扩容的table表的容量为原来的2倍,临界值也为原来的两倍,以此类推
  • 6.在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认为8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认为64),就会进行树化(红黑树)
    ps:此处转红黑树机制:必须只有一条链表长度到达了8,且表长度为64才转红黑树,如果没有链表达到8,就无法树化,会一直按照扩容机制扩容

我们直接上代码展示

package javaSEStudy.collection_.map_;

import java.util.HashMap;

// HashMap源码详解
@SuppressWarnings({"all"})
public class HashMapSource {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        hashMap.put("java", 10);
        hashMap.put("python", 10);
        hashMap.put("java", 20);

        /**
         * 代码解读
         * 1.初始化加载因子
         *     public HashMap() {
         *         this.loadFactor = DEFAULT_LOAD_FACTOR;
         *     }
         *     此时 HashMap$Node[] table = null
         *
         * 2.执行put方法(会计算哈希值)
         *     public V put(K key, V value) {
         *         return putVal(hash(key), key, value, false, true);
         *     }
         * 3.执行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表为0,执行扩容到16
         *         if ((tab = table) == null || (n = tab.length) == 0)
         *             n = (tab = resize()).length;
         *
         *         // 如果计算出的哈希值对应table的索引位置为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表的索引位置的哈希值和新加入的哈希值相同,且(同一个对象 或 equals(内容相同)返回为真)
         *             // 就认为不能加入
         *             if (p.hash == hash &&
         *                 ((k = p.key) == key || (key != null && key.equals(k))))
         *                 e = p;
         *
         *             // 是否是一颗红黑树,是则按照红黑树方式处理
         *             else if (p instanceof TreeNode)
         *                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
         *
         *             // 是否是一个链表
         *             else {
         *                 // 死循环
         *                 for (int binCount = 0; ; ++binCount) {
         *                     // 如果在这个链表中没有相同的值,则放在最后
         *                     if ((e = p.next) == null) {
         *                         p.next = newNode(hash, key, value, null);
         *                         // 判断链表长度是否到达8,从而进入treeifyBin判断是否树化
         *                         if (binCount >= TREEIFY_THRESHOLD - 1)
         *                             treeifyBin(tab, hash);
         *                         break;
         *                     }
         *                     // 如果找到了相同的值,替换value值,再退出
         *                     if (e.hash == hash &&
         *                         ((k = e.key) == key || (key != null && key.equals(k))))
         *                         break;
         *                     p = e;
         *                 }
         *             }
         *             if (e != null) { // existing mapping for key
         *                 V oldValue = e.value;
         *                 if (!onlyIfAbsent || oldValue == null)
         *                    // 在这里做了一个替换操作
         *                     e.value = value; // 将key对应的value替换
         *                 afterNodeAccess(e);
         *                 return oldValue;
         *             }
         *         }
         *         ++modCount;
         *         if (++size > threshold)
         *             resize();
         *         afterNodeInsertion(evict);
         *         return null;
         *     }
         * 4.关于树化(红黑树)
         *     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();
         *     如果table表为null,或者大小还没到64,暂时不执行树化,而是先扩容
         *     否则才会进行真正的树化 --> 剪枝 (红黑树退化为链表)
         */
    }
}

LinkedHashMap

LinkedHashMap 详解

LinkedHashMap 是 Java 集合框架中的一个重要类,它继承自 HashMap,同时通过维护一个双向链表来保持元素的插入顺序或访问顺序。

基本特性

  1. 继承关系:LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
  2. 有序性:与 HashMap 的无序不同,LinkedHashMap 可以保持元素的插入顺序或访问顺序
  3. 性能:提供了与 HashMap 相近的时间复杂度(O(1) 的基本操作)

核心实现原理

LinkedHashMap 通过在 HashMap 的节点基础上添加双向链表指针来实现有序性:

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);
    }
}

两种排序模式

  1. 插入顺序(默认):元素按照插入顺序排列

    • 新插入的元素放在链表尾部
    • 已存在的键重新插入不会改变顺序
  2. 访问顺序:元素按照访问顺序排列(最近访问的放在最后)

    • 通过构造方法设置:new LinkedHashMap(16, 0.75f, true)
    • 访问包括 get、put、replace 等操作
    • 适合实现 LRU 缓存

重要方法

  1. 构造方法:

    // 默认插入顺序
    LinkedHashMap()
    // 指定初始容量和加载因子 
    LinkedHashMap(int initialCapacity, float loadFactor)
    // 指定初始容量、加载因子和排序模式 
    LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
    
  2. 覆盖的钩子方法:

    // 在插入新节点后调用,可用于实现LRU的移除策略 
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest)
    

实现LRU缓存示例

// 最大容量为100的LRU缓存
final int MAX_ENTRIES = 100;
Map<String, String> lruCache = new LinkedHashMap<String, String>(MAX_ENTRIES, 0.75f, true) {
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
};

与HashMap的比较

特性 HashMap LinkedHashMap
顺序 无序 插入顺序或访问顺序
迭代性能 较快 稍慢(需要遍历链表)
内存占用 较少 较多(维护链表指针)
适用场景 通用键值存储 需要保持顺序的场景

使用注意事项

  1. 线程不安全:与 HashMap 一样,LinkedHashMap 不是线程安全的
  2. 迭代性能:迭代 LinkedHashMap 比迭代 HashMap 稍慢
  3. 内存开销:每个元素需要额外存储两个引用(before 和 after)

LinkedHashMap 是一个非常实用的集合类,特别适合需要保持元素顺序的场景,如实现缓存机制、记录操作历史等。

Hashtable

基本介绍

  • 1.存放的元素是键值对:即k-v
  • 2.Hashtable的键和值都不能为null,否则抛出异常:NullPointterException
  • 3.Hashtable的使用方法基本上和HashMap一致
  • 4.Hashtable是线程安全的,HashMap是线程不安全的
package javaSEStudy.collection_Map.map_.hashtable_;

import java.util.Hashtable;

// Hashtable详解
@SuppressWarnings({"all"})
public class Hashtable_ {
    public static void main(String[] args) {
        Hashtable table = new Hashtable();
        table.put("A", 1);
        table.put("B", 2);
        table.put("C", 3);
        table.put("D", 4);
        table.put("D", 88); //执行替换

        // 以下三种写法都会报空指针异常
//        table.put(null, null);
//        table.put("E", null);
//        table.put(null, 5);

        System.out.println(table);

        /**
         * 简单说明底层
         * 1.底层有个数组 Hashtable$Entry[] 初始化大小为11
         * 2.threshold(临界值) 8 --> 11*0.75
         * 3.扩容:按照自己的扩容机制,并非2倍
         */
    }
}

扩容机制

package javaSEStudy.collection_Map.map_.hashtable_;

import java.util.Hashtable;

// Hashtable 底层源码
@SuppressWarnings({"all"})
public class HashtableSource {
    public static void main(String[] args) {
        Hashtable table = new Hashtable();
        table.put("A", 1);
        table.put("B", 2);
        table.put("C", 3);
        table.put("D", 4);
        table.put("D", 88);
        table.put("hello1", 1);
        table.put("hello2", 1);
        table.put("hello3", 1);
        table.put("hello4", 1);
        table.put("hello5", 1);
        System.out.println(table);

        /**
         * Hashtable 自己的扩容机制
         *  真正执行扩容的方法:
         *              addEntry(hash, key, value, index);   添加k-v 封装到 Entry\
         *
         *              rehash(); 这个里面是扩容具体方法
         *
         *     扩容方法详解
         *     protected void rehash() {
         *         int oldCapacity = table.length;
         *         Entry<?,?>[] oldMap = table;
         *
         *         // overflow-conscious code
         *         // 扩容机制: 原来的数组容量 * 2 + 1
         *         int newCapacity = (oldCapacity << 1) + 1;
         *         if (newCapacity - MAX_ARRAY_SIZE > 0) {
         *             if (oldCapacity == MAX_ARRAY_SIZE)
         *                 // Keep running with MAX_ARRAY_SIZE buckets
         *                 return;
         *             newCapacity = MAX_ARRAY_SIZE;
         *         }
         *         Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
         *
         *         modCount++;
         *         threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
         *         table = newMap;
         *
         *         for (int i = oldCapacity ; i-- > 0 ;) {
         *             for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
         *                 Entry<K,V> e = old;
         *                 old = old.next;
         *
         *                 int index = (e.hash & 0x7FFFFFFF) % newCapacity;
         *                 e.next = (Entry<K,V>)newMap[index];
         *                 newMap[index] = e;
         *             }
         *         }
         *     }
         *
         *
         */
    }
}

比较

HashMap 和 Hashtable

版本 线程同步(安全) 效率 允许Null键null值
HashMap 1.2 不安全 可以
Hashtable 1.0 安全 较低 不可以

Properties

基本介绍

  • Properties类继承自Hashtable类并实现接口Map接口,也是实现一种键值对的形式保存数据
  • 使用特点和Hashtable相似
  • Properties还可以用于从xxx.properties 文件中,加载数据到properties类对象,并进行读取和修改
  • 说明:xxx.properties文件通常作为配置文件

基本使用

  • put 增
  • remove 删
  • 这里使用相同的key值,会更改值的特性 改
  • get()(括号里面是k值) 或 getProperty()(括号里面是k值) 查
package javaSEStudy.collection_Map.map_.hashtable_.properties_;

import lombok.extern.jackson.Jacksonized;

import java.util.Properties;

// Properties 详解
@SuppressWarnings({"all"})
public class Properties_ {
    public static void main(String[] args) {

        /**
         * put 增加
         * 因为底层存储结构与HashMap类似,所以不保证无序
         * 但是可通过扩展和包装实现有序
         */
        Properties pro = new Properties();
        pro.put("hello", "world"); // k-v 存储
        pro.put("hello1", 100);
        pro.put("hello1", 520); // 当键相同时,也会发生值的替换
        pro.put("蔡徐坤", "基尼太美");

        // 注意 它和它的超类Hashtable一样,不允许空值,否则会报空指针异常
//        pro.put(null, 520);
//        pro.put("Jack", null);
        System.out.println(pro);

        /**
         * 查:
         * 获取值:通过key,获取对应的值
         */
        System.out.println(pro.get("hello"));
        System.out.println(pro.getProperty("hello"));

        /**
         * remove 删除
         */
        pro.remove("蔡徐坤");
        System.out.println(pro);

        /**
         * put 修改
         */
        pro.put("大碗宽面",66);
        pro.put("大碗宽面",123456);

    }
}

TreeMap

package javaSEStudy.collection_Map.map_.treeMap_;

import java.util.Comparator;
import java.util.TreeMap;

// TreeMap 详解
@SuppressWarnings({"all"})
public class TreeMap_ {
    public static void main(String[] args) {

        // 使用默认构造器
        // 如果什么也不处理,无序输出
//        TreeMap treeMap = new TreeMap();

        /**
         * 使用提供的构造器对字符串大小进行排序
         */
        TreeMap treeMap = new TreeMap(new Comparator() {

            @Override
            public int compare(Object o1, Object o2) {
                // 默认比较大小的方式(字符串首字母)
//                return ((String) o2).compareTo((String) o1);
                // 根据字符串长度
                return ((String) o2).length() - ((String) o1).length();
            }
        });

        treeMap.put("ba", "好难听");
        treeMap.put("cdu", "卡布基诺");
        treeMap.put("wsajid", "萨瓦迪卡");
        treeMap.put("ddio", "汴京");
        treeMap.put("a", "发生啥了");



        System.out.println(treeMap);


        /**
         * 1.调用构造器,把穿入了实现Compare接口的匿名内部类(对象),传给了TreeMap的compare
         *     final int compare(Object k1, Object k2) {
         *         return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
         *             : comparator.compare((K)k1, (K)k2);
         *     }
         * 2.调用put方法
         *      第一次添加 把k-v对象封装到 Entry 对象中,放入 root
         *      第一次添加调用了比较器,但是并没有去接受比较的结果,仅检查是不是空值
         *      private V put(K key, V value, boolean replaceOld) {
         *         Entry<K,V> t = root;
         *         if (t == null) {
         *             addEntryToEmptyMap(key, value);
         *             return null;
         *         }
         *
         *       后续添加
         *       Comparator<? super K> cpr = comparator;
         *         if (cpr != null) {
         *             do {
         *             // 遍历所有的key,给当前key找存放位置
         *                 parent = t;
         *
         *                 // 动态绑定到匿名内部类Compare中
         *                 cmp = cpr.compare(key, t.key);
         *
         *                 // 找应放在哪个位置
         *                 if (cmp < 0)
         *                     t = t.left;
         *                 else if (cmp > 0)
         *                     t = t.right;
         *                 else {
         *                 // 如果遍历过程中发现与key相同的key,直接返回,不添加
         *                     V oldValue = t.value;
         *                     if (replaceOld || oldValue == null) {
         *                         t.value = value;
         *                     }
         *                     return oldValue;
         *                 }
         *             } while (t != null);
         */
    }
}

Collections工具类

Collections工具类介绍

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

方法

排序操作: (均为static方法)

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

替换和查找

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

话不多说,我们直接上代码

package javaSEStudy.collection_Map.collections_;

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

// Collections 工具类详解
@SuppressWarnings({"all"})
public class Collections_ {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        list.add("f");
        list.add("Tom");
        list.add("Tom");
        list.add("Tom");
        list.add("Tom");
        list.add("Jack");
        list.add("Luccy");
        System.out.println("原始数据: " + list);

        /**
         * reverse(list)  翻转元素
         */
        Collections.reverse(list);
        System.out.println("元素进行翻转: " + list);

        /**
         * shuffle(list)  将元素位置随机打乱
         * 使用场景: 抽奖
         */
        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() {
            @Override
            public int compare(Object o1, Object o2) {
                // 可以加入校验
                // 从高到低
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        System.out.println("按照字符串的大小进行升序" + list);

        /**
         * swap(list,int,int)  将指定的list集合中的i处元素和j处元素进行交换位置,i和j的位置得是有效的
         * 倘若位置不是有效的,会报数组下标越界异常 IndexOutOfBoundsException
         */
        Collections.swap(list, 2, 6);
        System.out.println("交换位置: " + list);

        System.out.println("------------------------------------------------------------");
        // 这里sort返回的是列表,不支持链式调用
        Collections.sort(list);
        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() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        System.out.println("字符串长度最大值: " + max);

        /**
         * Object min(Collection) 返回最小值
         */
        System.out.println("自然排序最小值: " + Collections.min(list));

        /**
         * Object min(Collection,Comparator): 根据Comparator 指定的顺序,返回给定集合中的最小元素
         * 例如:返回字符串长度最小的
         */
        Object min = Collections.min(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        System.out.println("字符串长度最小值: " + min);

        /**
         * int frequency(Collection,Object): 返回指定集合中元素的出现次数
         */
        System.out.println("Tom出现的次数: " + Collections.frequency(list, "Tom"));

        /**
         * void copy(List dest,List src): 将src中的内容复制到dest中
         */
        ArrayList dest = new ArrayList();
        // 将后一个的内容拷贝到前一个list中
        // 注意,下面这么写会抛出数组下标越界异常,因为dest长度为0,而list长度明显不是0
//        Collections.copy(dest, list);
        // 因此为了完成拷贝,我们需要先给dest赋值,大小和list.size相同
        for (int i = 0; i < list.size(); i++) {
            // 增加空间的时候直接将值给添加了
//            dest.add(list.get(i));
            dest.add("");
        }
        Collections.copy(dest, list);
        System.out.println("dest: " + dest);

        /**
         * bollean replaceAll(List list,Object oldVal,object newVal): 使用新的值替换list中所有的旧值
         */
        // 如果list中有Tom,就替换为中文的汤姆
        Collections.replaceAll(list, "Tom", "汤姆");
        System.out.println("最终的列表: " + list);

    }
}

总结

如何选择集合类?
如何选择集合,主要取决于业务操作特点,根据集合实现类的特性进行选择

  1. 先判断存储的是一组对象或是一组键值对
  2. 一组对象(单列):Collection接口
      允许重复:List:
        改删多:LinkedList(底层维护了一个双向链表)
        改查多:ArrayList(底层维护 Object 类型的可变数组,大多数时候用这个)
      不允许重复:Set:
        无序:HashSet(底层是HashMap,维护了一个哈希表,即(数组+链表+红黑树))
        排序:TreeSet
        插入和取出顺序一致:LinkedHashSet(底层是LinkedHashMap,维护 数组+双向链表)
  3. 一组键值对(双列):Map
      键无序:HashMap(底层是哈希表,数组+链表+红黑树)
      键排序:TreeMap
      键插入和取出顺序一致:LinkedHashMap(底层是HashMap)
      读取文件:Properties

本章作业

第一问

package javaSEStudy.collection_Map.homework;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;

// 包装新闻类...
@SuppressWarnings({"all"})
public class Test01 {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(new News("新冠确诊病例超过千万,数百万印度教信徒赴恒河\"圣浴\"引民众担忧"));
        list.add(new News("男子突然想起2个月前钓的鱼还在网兜之中,捞起来一看,赶紧放生"));

        // 不是倒序遍历
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()) {
//            News news = (News) iterator.next();
//            String title = news.getTitle();
//
//            if (title.length() > 15) {
//                title = title.substring(0, 15)+"...";
//            }
//            System.out.println(title);
//        }

        // 使用ListIterator 倒序遍历
//        ListIterator<News> iterator = list.listIterator(list.size());
//        while (iterator.hasPrevious())  {
//            News news = iterator.previous();
//            String title = news.getTitle();
//
//            // 如果标题长度 >15,截取前15个字符并加 "..."
//            if (title.length()  > 15) {
//                title = title.substring(0,  15) + "...";
//            }
//            System.out.println(title);
//        }

        int size = list.size();
        for (int i = size - 1; i >= 0; i--) {
            News news = (News)list.get(i);
            System.out.println(ProcessTitle(news.getTitle()));
        }

    }
    // 专门写一个方法对标题进行处理 process 处理
    public static String ProcessTitle(String title){
        if (title == null){
            return "";
        }
        if (title.length() > 15){
            title = title.substring(0, 15)+"...";
        }
        return title;
    }
}

class News{
    private String title;
    private String content;

    public News(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return  "title: " + title;
    }
}

第二问

package javaSEStudy.collection_Map.homework;

import java.util.ArrayList;
import java.util.Iterator;

// 对汽车进行处理
@SuppressWarnings({"all"})
public class Test02 {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        Car car1 = new Car("宝马",400000);
        Car car2 = new Car("宾利", 50000000);
        Car car3 = new Car("法拉利", 50000000);
        Car car4 = new Car("劳斯莱斯", 50000000);
        Car car5 = new Car("布加迪", 50000000);
        Car car6 = new Car("五菱宏光", 5000);
        Car car7 = new Car("雪铁龙", 10000);

        /**
         * 1.添加操作
         */
        list.add(car1);
        list.add(car2);
        list.add(car3);
        list.add(car4);
        list.add(car5);
        list.add("小牛");
        System.out.println("添加后的列表: "+list);

        /**
         * 2.删除
         */
        list.remove(4);
        list.remove(car4);
        list.remove("小牛");
        System.out.println("删除后的列表: "+list);

        /**
         * 3.查找元素是否存在
         */
        System.out.println("是否存在 "+list.contains(car1));
        System.out.println("是否存在 "+list.contains(car5));

        /**
         * 4.获取元素个数
         */
        System.out.println("元素的个数为 "+list.size());

        /**
         * 5.是否为空
         */
        System.out.println("是否为空 "+list.isEmpty());

        /**
         * 6.添加多个元素
         */
        ArrayList list1 = new ArrayList();
        list1.add(car6);
        list1.add(car7);

        list.addAll(list1);
        System.out.println(list);

        /**
         * 7.查找多个元素是否存在
         */
        System.out.println("多个元素是否存在 "+list.containsAll(list1));

        /**
         * 8.删除多个元素
         */
        list.removeAll(list1);
        System.out.println("将多个元素删除后的列表 "+list);

        /**
         * 9.清空
         */
        list.clear();
        System.out.println("清空后的列表 "+list);

        System.out.println("------------------------------------------");

        list.add(car1);
        list.add(car2);
        list.add(car3);
        list.add(car4);
        list.add(car5);

        /**
         * 使用迭代器
         */
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Car car = (Car) iterator.next();
            System.out.println(car);
        }
        System.out.println("========");

        /**
         * 使用增强for
         */
        for (Object car : list) {
            System.out.println(car);
        }
    }
}

class Car{
    private String name;
    private double price;

    public Car(String name, double price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return  "name: " + name +
                ", price: " + price;
    }
}

第三问

package javaSEStudy.collection_Map.homework;

import java.util.*;

// 员工工资问题
@SuppressWarnings({"all"})
public class Test03 {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("Jack", 650);
        map.put("Tom", 1200);
        map.put("Smith", 3000);

        /**
         * 1. 更改员工工资
         */
        map.put("Jack", 2600);
        System.out.println(map);

        /**
         * 2.为所有员工工资加薪100
         */
//        map.replaceAll((key, value) -> value+100);

        // 复杂版
        for (String key : map.keySet()) {
            // 替换,更新
            map.put(key, map.get(key) + 100);
        }


        /**
         * 3.遍历所有的员工
         */
        for (Object key : map.keySet()) {
            System.out.println(key);
        }

        System.out.println("-------------------------");

        /**
         * 4.遍历所有的工资
         */
        Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
        Iterator<Map.Entry<String, Integer>> iterator = entrySet.iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Integer> entry = iterator.next();
            System.out.println(entry.getValue());
        }
    }
}

第四问


1.HashSet去重机制:HashCode + equals(), 底层首先存入一个对象,根据这个对象计算出哈希值,根据哈希值存放到对应table表中的索引位置,如果该索引位置没有值,则直接放入;如果有,进行equals(可重写)遍历比较,如果相同则不添加,不相同则放到末尾.
2.TreeSet去重机制:如果传入了一个Comparator(比较器)匿名对象,就使用实现的compare方法去重,如果方返回值为0,就认为是相同的元素,不添加;如果没有传入一个Comparator匿名对象,就以你添加的对象实现的Comparable接口的compareTo去重.
PS:字符串实现了Comparable接口,这里向上转型为Comparable,再调用compareTo方法去重

第五问

package javaSEStudy.collection_Map.homework;

import java.util.TreeSet;

// TreeSet添加对象
@SuppressWarnings({"all"})
public class Test05 {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();
        treeSet.add(new Person());

        /**
         * 源码分析
         *      add方法,因为TreeSet()构造器没有传入Comparator接口的匿名内部类
         *      底层:  return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
         *      即把 Person 转成 Comparable 类型
         *      所以报错 ClassCastException 类型转换错误
         */

        /**
         * 如果想要成功加入,需要让Person实现Comparable接口
         */
    }
}

class Person {}

第六问

package javaSEStudy.collection_Map.homework;

import java.util.HashSet;
import java.util.Objects;

// 关于重写HashCode与equals
@SuppressWarnings({"all"})
public class Test06 {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        // 下面两个对象的哈希值由id和name共同计算得到
        Person1 p1 = new Person1(1001, "AA");
        Person1 p2 = new Person1(1002, "BB");
        set.add(p1);
        set.add(p2);
        p1.name = "CC";
        set.remove(p1); // p1的哈希值由1001,CC计算得到新值,此时删除操作由当前新值去查找,对应位置可能不同,无法删除
        System.out.println(set); // 还是会输出p1和p2

        /**
         * 虽然哈希值与修改后的AA相同,但是存储位置可能不同,虽然值被更改,但是存储位置未更新
         * 新增的对象被认为是一个新的实例,也能添加
         */
        set.add(new Person1(1001,"CC"));
        System.out.println(set); // 显示3个对象

        /**
         * 这里由于哈希值已被更改,这里是new的对象,HashMap只会比较他的哈希值是否相同
         * 因为new出来的两个对象是两个不同的内存地址,不会调用equals方法比较
         */
        set.add(new Person1(1001,"AA"));
        System.out.println(set); // 显示4个对象

       // 这道题这里发生了内存泄漏
    }
}

class Person1 {
    public int id;
    public String name;

    public Person1(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Person1 person1 = (Person1) o;
        return id == person1.id && Objects.equals(name, person1.name);
    }

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

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

第七问

底层结构 版本 线程安全(同步) 效率 扩容倍数
ArrayList 可变数组 JDK1.2 不安全,效率高 如果是有参构造,则是按照给的大小扩容1.5倍;如果是无参构造,第一次为10,第二次为1.5倍
Vector 可变数组 JDK1.0 安全,效率不高 如果是无参构造,默认为10,满后按照两倍扩容;如果指定大小,则每次直接扩大两倍
posted @ 2025-05-21 19:20  sprint077  阅读(21)  评论(0)    收藏  举报