一篇文章轻松拿捏Java基础之集合那些事儿~

笔记整理来源 B站UP主韩顺平https://www.bilibili.com/video/BV1YA411T76k/?spm_id_from=333.999.0.0&vd_source=763902af33f2b5f3d2e194f3923d02ca

集合

集合框架体系图

  1. 单列集合(单个元素为个体)

image-20220617105811273

  1. 双列集合(两个元素为个体,键值对[Key-Value]形式)

    image-20220617104023962

Collection接口 特点 方法

  • Iterator对象称为迭代器,主要用于遍历Collection集合中的元素。

  • 所有实现了Collection接口的集合类都有一个iterator()方法(即都可以使用迭代器遍历),用以返回一个实现了Iterator接口的对象,即可返回一个迭代器

  • Iterator仅用于遍历集合,Iterator本身并不存放对象

  • 增强for循环(底层实现就是迭代器),可以理解成简化版的迭代器,不仅仅适用于集合,数组也行


Collection接口的子接口:List实现类:ArrayList、LinkedList、Vector

List接口方法、特点

  1. List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
  2. List集合中每个元素都有其对应的顺序索引,即支持索引
1.1 ArrayList注意事项
  1. ArrayLiat可以加入null,并且多个
  2. ArrayList是由数组来实现数据存储的
  3. ArrayList基本等同于Vector(线程安全),除了ArrayList是线程不安全(执行效率高)看源码,在多线程情况下,不建议使用ArrayList
1.2 ArrayList底层结构和源码分析
  1. ArrayList中维护了一个Object类型的数组elementData

    transient Object[] elementData;// transient 表示瞬间,短暂的,表示该属性不会被序列化
    
  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始时elementData容量为0,第一次添加,则扩容elementData为10,如需再次扩容,则扩容elementData为1.5倍

    int newCapacity = oldCapacity + (oldCapacity >> 1);// 增加为原容量的1.5倍
    
  3. 如果使用的是指定大小的构造器,则初始时elementData容量为指定大小,如需扩容,则直接扩容elementData为1.5倍

  4. 注:具体函数调用

2.1 Vector注意事项
  1. Vector底层也是一个对象数组,protected Object[] elementData

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

    public synchronized boolean add(E e) {
            modCount++;
            ensureCapacityHelper(elementCount + 1);
            elementData[elementCount++] = e;
            return true;
        }
    
  3. 在开发中,需要线程同步安全时,考虑使用Vector

2.2 Vector底层结构与源码分析

image-20220619215923689

debug看底层源码

3.1 LinkedList底层结构
  1. LinkedList底层实现了双向链表双端队列(Deque)特点

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

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

LinkedList的底层操作机制
  1. LinkedList底层维护了一个双向链表

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

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

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

3.2 LinkedList源码图解

debug底层源码!!!

4.1 List集合选择
4.2 ArrayList和LinkedList比较
底层结构增删的效率改查的效率
ArrayList可变数组较低,数组扩容较高
LinkedList双向链表较高,通过链表追加较低
4.3 如何选择ArrayList和LinkedList
  1. 如果改查的操作比较多,选择ArrayList

  2. 如果增删的操作比较多,选择LinkedList

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

  4. 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另一个模块使用LinkedList


Collection接口的子接口:Set实现类:HashSet、TreeSet

Set接口方法、特点

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

  2. 注意:取出的顺序虽然不是添加的顺序,但是是固定的

  3. 不允许重复元素,所以最多包含一个null

HashSet全面说明
  1. HashSet实现了Set接口
  2. HashSet实际上是HashMap,看源码

image-20220621150513442

  1. 可以存放null值,但只能有一个null

  2. HashSet不保证元素是有序的,取决于hash后,在确定索引的结果

  3. 不能有重复元素/对象

HashSet底层机制说明

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

分析HashSet的添加元素是如何实现的(hash()+equals())
  1. HashSet底层是HashMap

  2. 添加一个元素时,先得到hash值(底层使用hashcode())—>>>索引值

  3. 找到存储结构表table,看这个索引位置是否已经存放有元素

  4. 如果没有,直接加入

  5. 如果有,调用 equals ()比较,如果相同,就放弃添加,如果不相同,则添加到最后

  6. JDK1.8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),

    并且table的大小>=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍采用数组扩容机制

注:调用 equals ()比较,其中equals ()是由程序员重写(控制),按照程序员的规则!!!,不能简单理解成比较内容。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 定义了辅助变量
        // table 就是 HashMap 的一个数组,类型为Node[]
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // if 语句表示如果当前table 是null,或者 大小=0
        // 就第一次扩容,到16个空间
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 1、根据key,得到的hash值去计算该key应该存放到table表的哪个索引位置
        // 并且将该位置的对象,赋给变量 p
        // 2、判断 p,是否为null
        // 2.1、如果 p为null,表示该位置还没有存放过数据,就创建一个Node(key value)
        // 就放在该位置 tab[i] = newNode(hash, key, value, null);
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 一个开发技巧提示:在需要局部变量(辅助变量)时,再创建比较好!!!
            Node<K,V> e; K k;
            // 如果当前索引位置对应的链表的第一个元素hash值和准备添加的key的hash值一样
            // 并且满足下面两个条件之一:
            // 1、准备加入的key和p指向的Node结点的key是同一个对象
            // 2、准备加入的key调用equals(k)方法比较后相同后
            // 就不能加入
            // 注:key调用的equals()方法可以被程序员重写,改变比较的规则!!!
            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对应的索引位置是一个链表且该链表第一个位置元素不满足第一个if条件
            // 就需要for循环遍历比较链表剩下的位置元素【比较方式与第一个if条件比较方式相同】
            // 在比较中可能出现如下两种情况:
            // 1、如果在链表中遍历到最后没有发现满足比较条件相同的元素就创建一个结点放入要加入的元素加到该链表最后
            // 注意:在把元素加入到该链表后,立即判断该链表长度是否已经 >=TREEIFY_THRESHOLD(8),
            //      如果满足,就调用treeifyBin()对当前链表进行树化(红黑树)
            // 注意:在转成红黑树前,要进行判断,判断条件如下:
             if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
                        resize();
            // 如果条件成立,就对table表扩容
            // 如果条件不成立,才进行树化(红黑树)
            // 2、如果在链表中发现满足比较条件相同的元素,直接跳出循环,不添加该元素
            else {
                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;
        // size 就是我们每加入一个结点Node(hash, key, value, null),size++
        // 不要简单理解为只有table位置(链表第一个元素)被占入后>threshold后才调用resize()方法
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
分析HashSet的扩容和转成红黑树机制
  1. HashSet底层是HashMap,第一次添加时,table扩容到DEFAULT_INITIAL_CAPACITY(默认初始容量:16),
    临界值(threshold)是DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR(加载因子:0.75)=12

  2. 如果table数组使用到了临界值12,就会扩容到16乘2=32,新的临界值就是12乘2=24,依次类推

  3. 在JDK1.8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),

    并且table>=MIN_TREEIFY_CAPACITY(最小树形容量:64),就会进行树化(红黑树),否则仍然采用数组扩容机制

HashSet最佳实践
实践一
定义一个Employee类,该类包括:private成员属性name,age要求:
1.创建3个Employee放入HashSet中
2.当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
public class HashSetExercise1 {

    public static void main(String[] args) {
        
        Set<Employee> employees = new HashSet<>();
        employees.add(new Employee("小黄", 20));
        employees.add(new Employee("小冀", 21));
        employees.add(new Employee("小黄", 20));
        System.out.println(employees);
        
    }
}

class Employee {
    private String name;
    private int age;

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

    @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 && Objects.equals(name, employee.name);
    }

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

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

}

问题要点:如果name 和 age 值相同,就返回相同的hashCode

解决方法:

  1. 在idea中输入快捷键 fn+alt+insert

  2. 点击image-20220622211021855

  3. 点击下一步image-20220622211146911

  4. image-20220622212931974

  5. image-20220622213340334

  6. 生成如下代码:

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Employee employee = (Employee) o;
        // 当加入相同name和age值的对象时,根据其下面重写的hashCode()方法,会得到相同的hashCode
        // 此时会在table位置为该hash值的链表上循环遍历比较要加入的对象与先前加入的对象是否一样
        // 具体比较方法:equals()
        // 此时重写了equals()【按照我们的规则重写:当name和age的值相同时,认为是相同员工】
        return age == employee.age && name.equals(employee.name);
    }

    @Override
    public int hashCode() {
        // 此时hashCode()就取决于我们选择的名字与年龄了
        return Objects.hash(name, age);
    }
实践二
定义一个Employee2类,该类包括:private成员属性name,salary,birthday(自定义MyDate类型),其中birthday为自定义MyDate类型(属性包括:year,month,day),要求:
1.创建3个Employee2放入HashSet中
2.当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
public class HashSetExercise2 {
    public static void main(String[] args) {
        
        Set<Employee2> employee2s = new HashSet<>();
        employee2s.add(new Employee2("小黄", 10000, new MyDate(2002, 4, 13)));
        employee2s.add(new Employee2("小冀", 10000, new MyDate(2001, 4, 13)));
        employee2s.add(new Employee2("小黄", 20000, new MyDate(2002, 4, 13)));
        System.out.println(employee2s);

    }
}

class Employee2 {

    private String name;

    private int salary;

    private MyDate birthday;

    public Employee2(String name, int salary, MyDate birthday) {
        this.name = name;
        this.salary = salary;
        this.birthday = birthday;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Employee2 employee2 = (Employee2) o;
        return Objects.equals(name, employee2.name) && Objects.equals(birthday, employee2.birthday);
    }

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

}

class MyDate {

    int year;
    int month;
    int day;

    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        MyDate myDate = (MyDate) o;
        return year == myDate.year && month == myDate.month && day == myDate.day;
    }

    @Override
    public int hashCode() {
        return Objects.hash(year, month, day);
    }

}

大体上和实践1相似,但是需要注意birthday引用类型变量(MyDate)

解决这个问题之前需要看一下hashCode()和equals()源码

public static int hashCode(Object a[]) {
        if (a == null)
            return 0;

        int result = 1;

    // 看下面图片可知在获取hashCode时会将我们选择的标准(name 和 birthday)
    // 转换为一个Object类型数组,循环遍历获取数组各个位置【element.hashCode()】hashCode,
    // 进行累加得到最终对象的hashCode【result】
    // 那么问题来了,birthday对象的hashCode怎么按照我们的标准使其因生日相同而获取相同的hashCode呢?
    // 答案是:在MyDate类中重写hashCode()方法!!!
        for (Object element : a)
            result = 31 * result + (element == null ? 0 : element.hashCode());

        return result;
    }

public static boolean equals(Object a, Object b) {
    // 在Mydate类中重写equals()方法理由与重写hashCode()方法异曲同工!!!
        return (a == b) || (a != null && a.equals(b));
    }

image-20220622222531511

LinkedHashSet全面说明
  1. LinkedHashSet是HashSet的子类

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

  3. LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,

    这使元素看起来是以插入顺序保存的

  4. LinkedHashSet不允许添重复元素

LinkHashSet底层机制示意图

image-20220622234243390

LinkedHashSet源码解读
  1. LinkedHashSet 加入元素顺序 和 取出元素顺序一致

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

  3. LinkedHashSet 底层结构 (数组table+双向链表)

  4. 添加第一次时,直接将数组table扩容到16,存放的结点类型是LinkedHashMap$Entry(即)

  5. 数组是 HashMap N o d e [ ] 存 放 的 元 素 是 L i n k e d H a s h M a p Node[] 存放的元素是 LinkedHashMap Node[]LinkedHashMapEntry类型

image-20220623142008498

LinkedHashSet最佳实践
Car类(属性:name,price),如果name和price一样,则认为是相同元素,就不能添加。
public class LinkedHashSetTest {
    public static void main(String[] args) {

        Set<Car> carLinkedHashSet = new LinkedHashSet<>();
        carLinkedHashSet.add(new Car("奥拓", 1000));
        carLinkedHashSet.add(new Car("奥迪", 300000));
        carLinkedHashSet.add(new Car("法拉利", 10000000));
        carLinkedHashSet.add(new Car("奥迪", 300000));
        carLinkedHashSet.add(new Car("保时捷", 7000000));
        carLinkedHashSet.add(new Car("奥迪", 300000));
        System.out.println(carLinkedHashSet);

    }
}

class Car {
    private String name;
    private double price;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Car car = (Car) o;
        return Double.compare(car.price, price) == 0 && Objects.equals(name, car.name);
    }

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

    @Override
    public String toString() {
        return "\nCar{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}
TreeSet源码解读
  1. 当我们使用无参构造器时,创建TreeSet时,按照什么规则排序呢???【我们已知TreeSet是有序的】

源码解读:

 Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            // 重点看这里!!!
            // 因为使用的是无参构造器,所以没有传入比较器,所以cpr==null,即会执行else语句
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
           // 下面这条语句会将Key向上转型为Comparable<? super K>【前提Key对象所在的类实现了Comparable接口】
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                // 然后调用compareTo()方法比较排序
                // 这种方式与传入比较器实现compare()方法进行比较排序有着异曲同工之妙
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
  1. 使用TreeSet提供的一个有参构造器,可以传入一个比较器Comparator【接口】的实现对象(匿名内部类),

    在其compare(T o1, T o2)方法中制定排序规则!!!

 public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

 int compare(T o1, T o2);

源码解读:

  • 构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性comparator
  public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
  • 在调用 treeSet.add(“xxx”) 方法时,在底层会执行到

    Comparator<? super K> cpr = comparator;
    // 该cpr就是我们传入的匿名内部类(比较器对象)
    if (cpr != null) {
        do {
            parent = t;
            // 动态绑定到我们到传入的匿名内部类(比较器对象)的compare()方法
            // 实现我们想要的排序结果
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                // 如果按照我们的排序规则相等【即cmp==0】,该数据Key就加入不了!!!
                return t.setValue(value);
        } while (t != null);
    }
    

Map接口的实现类:HashMap、Hashtable、TreeMap、Properties

Map接口[很实用] 特点 方法 遍历方式

Map接口特点(JDK8)
  1. Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value

  2. Map中Key和Value可以是任何引用类型的数据,会封装到HashMap$Node对象中

  3. Map中的Key不允许重复,原因和HashSet一样,当有相同的Key时,Value会被覆盖

           if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    // 当有相同的Key时,Value会被覆盖
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
  1. Map中Value可以重复

  2. Map中Key可以为null,Value也可以为null,注意Key为null,只能有一个,Value为null,可以多个

  3. 常用String类作为Map的Key

  4. Key和Value之间存在单向一对一关系,即通过指定的Key总能找到对应的Value

  5. Map存放数据的Key-Value示意图,一对K-V是放在一个HashMap$Node中的,又因为Node实现了Entry接口,

    有些书上也说一对K-V就是一个Entry

image-20220623165456133

HashMap源码解读
  1. K-V 最后是放在 HashMap$Node node = newNode(hash, key, value, null)

  2. K-V 为了方便程序员的遍历,还会创建 EntrySet 集合,该集合存放的元素类型是 Entry

    而一个Entry对象就存放的K,V ,EntrySet<Entry<K,V>>,即:transient Set<Map.Entry<K,V>> entrySet

    image-20220623205750331

    image-20220623205814178

    也就是说EntrySet是Set的孙子辈!!!

    具体entrySet的运行类型可由以下代码获取:

     Map<String, String> map = new HashMap<>();
     Set<Map.Entry<String, String>> entries = map.entrySet();
     System.out.println(entries.getClass());
    

    可以看到其具体运行类型:image-20220623210434841

  3. 在entrySet中,定义的类型是Map.Entry,但是实际上其存放的还是 HashMap$Node

    :HashMap$Node实现了Map.Entry接口

    image-20220623212255907

  4. 当把 HashMap$Node 对象存放到 entrySet 就方便我们的遍历,

    因为Map.Entry提供了两个重要的方法:K getKey() 和 V getValue()

    image-20220623212949847

    为了从 HashMap$Node 取出K-V

     for (Iterator<Map.Entry<String, String>> iterator = entries.iterator(); iterator.hasNext(); ) {
                Map.Entry<String, String> next = iterator.next();
                System.out.println(next.getClass());
                System.out.println(next.getKey() + "---" + next.getValue());
            }
    

  5. 注意:EntrySet<Entry<K,V>> entrySet是引用(地址),没有真正存放数据

    使用存入对象数据类型验证(字符串不显示地址)

    image-20220623215708187

  6. 除此之外还有单独的 KeySet 和 Values 集合

    源代码:

    public class MapDemo {
        public static void main(String[] args) {
    
            Map<Object, Object> map = new HashMap<>();
            map.put("No.1", "小黄");
            map.put("No.2", "小明");
            map.put("No.3", "小明");
            map.put("No.1", "小冀");
            map.put(new Person(), new Car());
            System.out.println(map);
    
            Set<Map.Entry<Object, Object>> entries = map.entrySet();
            System.out.println(entries.getClass());
    
            for (Iterator<Map.Entry<Object, Object>> iterator = entries.iterator(); iterator.hasNext(); ) {
                Map.Entry<Object, Object> next = iterator.next();
                System.out.println(next.getClass());
                System.out.println(next.getKey() + "---" + next.getValue());
            }
            
            // 除此之外还有单独的 KeySet 和 Values 集合
            Set<Object> strings = map.keySet();
            System.out.println(strings.getClass()); // class java.util.HashMap$KeyS
            Collection<Object> values = map.values();
            System.out.println(values.getClass());// class java.util.HashMap$Values
    
        }
    }
    
    class Person {
    
    }
    
    class Car {
    
    }
    
Map接口六大遍历方法
public class MapDemo1 {
    public static void main(String[] args) {

        Map<String, String> map = new HashMap<>();
        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("宋喆", "马蓉");
        map.put("刘令博", "null");
        map.put("null", "刘亦菲");
        map.put("鹿晗", "关晓彤");

        System.out.println(map);

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

        // 第一组:先取出所有的Key,再通过Key取出对应的Value
        Set<String> keySet = map.keySet();
        // 1、增强for
        for (String s : keySet) {
            System.out.println(s + "-" + map.get(s));
        }

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

        // 2、迭代器
        for (Iterator<String> iterator = keySet.iterator(); iterator.hasNext(); ) {
            String next = iterator.next();
            System.out.println(next + "-" + map.get(next));
        }

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


        // 第二组:把所有的values取出
        Collection<String> values = map.values();
        // 这里可以使用所有的Collection使用的遍历方法
        // 1、迭代器
        for (Iterator<String> iterator = values.iterator(); iterator.hasNext(); ) {
            String next = iterator.next();
            System.out.println(next);
        }

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

        // 2、增强for
        for (String value : values) {
            System.out.println(value);
        }

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

        // 第三组:通过EntrySet 取出K-V
        Set<Map.Entry<String, String>> entries = map.entrySet();
        // 1、迭代器
        for (Iterator<Map.Entry<String, String>> iterator = entries.iterator(); iterator.hasNext(); ) {
            Map.Entry<String, String> next = iterator.next();
            System.out.println(next.getKey() + "-" + next.getValue());
        }

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

        // 2、增强for
        for (Map.Entry<String, String> entry : entries) {
            System.out.println(entry.getKey() + "-" + entry.getValue());
        }

    }
}
Map最佳实践
使用HashMap添加3个对象,要求:
键:员工id
值:员工对象
并遍历显示员工工资>18000的员工(遍历方式最少两种)
员工类:姓名,工资,员工id
public class HashMapTest {
    public static void main(String[] args) {

        Map<Integer, Employee> employeeMap = new HashMap<>();
        Employee employee1 = new Employee("小黄", 25000, 1);
        Employee employee2 = new Employee("小冀", 20000, 2);
        Employee employee3 = new Employee("张三", 10000, 3);
        employeeMap.put(employee1.getId(), employee1);
        employeeMap.put(employee2.getId(), employee2);
        employeeMap.put(employee3.getId(), employee3);

        System.out.println(employeeMap);

        System.out.println("***************************************遍历方式一**********************************************");

        Set<Map.Entry<Integer, Employee>> entries = employeeMap.entrySet();
        for (Iterator<Map.Entry<Integer, Employee>> iterator = entries.iterator(); iterator.hasNext(); ) {
            Map.Entry<Integer, Employee> next = iterator.next();
            if (next.getValue().getSalary() > 18000) {
                System.out.println(next.getKey() + "--->>>\t" + next.getValue());
            }
        }

        System.out.println("***************************************遍历方式二**********************************************");
        Set<Integer> keySet = employeeMap.keySet();
        for (Integer integer : keySet) {
            if (employeeMap.get(integer).getSalary() > 18000) {
                System.out.println(integer + "--->>>\t" + employeeMap.get(integer));
            }
        }

        System.out.println("***************************************遍历方式三**********************************************");
        Collection<Employee> values = employeeMap.values();
        for (Iterator<Employee> iterator = values.iterator(); iterator.hasNext(); ) {
            Employee next = iterator.next();
            if (next.getSalary() > 18000) {
                System.out.println(next.getId() + "--->>>\t" + next);
            }
        }


    }
}

class Employee {
    private String name;
    private double salary;
    private int id;

    public Employee(String name, double salary, int id) {
        this.name = name;
        this.salary = salary;
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public double getSalary() {
        return salary;
    }

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

}
HashMap小结
  1. Map接口的常见实现类:HashMap、HashTable、Properties
  2. HashMap是Map接口使用频率最高的实现类
  3. HashMap是以 K-V 【键值对】的方式来存储数据(HashMap$Node类型)
  4. Key不能重复,但是Value可以重复,允许使用null键和null值
  5. 如果添加相同的Key,则会覆盖原来的Key-Value,等同于修改,【Key不会替换,Value会替换】
  6. 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的
  7. HashMap没有实现同步,所以是线程不安全的,方法没有做同步互斥的操作,没有synchronized关键字
HashMap扩容树化触发
设计程序,验证HashMap扩容机制【table扩容and链表树化】
public class HashMapSource {
    public static void main(String[] args) {

        Map<A, String> map = new HashMap<>();

        // 注意:i的值
        for (int i = 0; i < 48; i++) {
            map.put(new A(i), "小黄");
        }

        map.put(new A(100), "小黄");

        System.out.println(map);

    }
}

class A {
    private int num;

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


    @Override
    public int hashCode() {
        // 小技巧:重点:为了实现我们能够快速看到table扩容and链表树化效果
        // 设计重写其hashCode()方法,是的我们无论向HashMap中增加不同的对象
        // 都会获取相同的hashCode,从而获取相同的hash值,从而使不同对象人为地
        // 加入到一条链表中!!!
        return 100;
    }

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

预计效果:

  1. 当向HashMap中加入第九个键值对时,此时table会扩容到原来的2倍【16*2=32】

  2. 当再加一个时,table会再次扩容到原来的2倍【32*2=64】

  3. 当再加一个时,此时就会满足链表树化条件【table.size()>=64&&单个链表长度>8】

  4. 再加入时,当满足加入到HashMap中键值对的个数 > 当前table.size*0.75【48】 ,就会对table扩容到原来的2倍【128】

  5. 以此类推

  6. 可以看到符合我们的预期:image-20220624165235586

Hashtable基本介绍
  1. 存放的元素是键值对:即Key-Value
  2. Hashtable的键和值都不能为null,否则会抛出NullPointerExceptio
  3. Hashtable使用方法基本上和HashMap一样
  4. Hashtable是线程安全的,HashMap是线程不安全的
Hashtable底层源码解读
  1. 底层是数组 Hashtable$Entry[] 初始大小为11

  2. 临界值 threshold 8 = 11*0.75

  3. 执行 private void addEntry(int hash, K key, V value, int index) 方法,进行添加K-V 封装到Entry

  4. 扩容原理:

    当大于临界值时,扩容

    int newCapacity = (oldCapacity << 1) + 1;【原容量*2+1】

HashMap和Hashtable对比
版本线程安全(同步)效率允许null Key null Value
HashMap1.2不安全可以
Hashtable1.0安全较低不可以
Properties基本介绍
  1. Properties类继承自Hashtable并实现了Map接口,也是使用一种键值对的形式来保存数据

  2. 它的使用特点和Hashtable类似

  3. Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改

  4. 说明:工作后,xxx.properties文件通常作为配置文件,这个知识点在IO流举例

    Java 读写Properties配置文件

    CURD方法
      // 增
          properties.put("小黄", 686);
      // 删
          properties.remove("john");
      // 改 注:放入相同Key,不同Value,即可达到修改作用!!!
          properties.put("小黄", 100);
      // 查
          properties.get("小黄");
    
TreeMap源码解读

具体源码debug【看TreeSet源码解读】

集合选型规则

总结-开发中如何选择集合实现类【记住】

image-20220624221218996

注:遍历一个集合时如何避免ConcurrentModificationException!!!

ConcurrentModificationException异常

image-20220711160121239

image-20220711160451087

Collections工具类的使用

image-20220625135629817

image-20220625135653085

集合实操

实践1

试分析HashSet和TreeSet分别如何怎么去重的
  1. HashSet去重机制:hashCode()+equals() ,底层先通过存入对象,进行运算得到一个hash值,通过hash值得到对应的索引,如果发现table索引所在的位置,没有数据,就直接存放,如果有数据,就进行equals()比较【遍历比较】,如果比较后,不相同,就加入,否则就不加入

  2. TreeSet去重机制:

    • 如果你传入了一个比较器Comparator匿名对象,就使用实现的compare()去重,如果方法返回0,就认为是相同的元素/数据,就不添加;

    • 如果你没有传入了一个比较器Comparator匿名对象,即使用无参构造器,则以你添加的对象实现的Compareable接口的compareTo()方法去重

实践2

下面代码运行会不会抛出异常,并从源码层面说明原因【考察:读源码+接口编程+Java动态绑定机制】
public class Test3 {
    public static void main(String[] args) {

        TreeSet<Person> treeSet = new TreeSet<>();
        treeSet.add(new Person());
        System.out.println(treeSet);

    }
}

class Person {
}

答案是:会抛出 ClassCastException 异常!!!

原因:

  • 首先使用的是TreeSet无参构造器

  • Person没有实现Compareable接口并重写compareTo()方法

  • 源码:

      Comparator<? super K> cpr = comparator;
            if (cpr != null) {
                do {
                    parent = t;
                    cmp = cpr.compare(key, t.key);
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    else
                        return t.setValue(value);
                } while (t != null);
            }
            else {
                // 重点看这里!!!
                // 因为使用的是无参构造器,所以没有传入比较器,所以cpr==null,即会执行else语句
                if (key == null)
                    throw new NullPointerException();
                @SuppressWarnings("unchecked")
                // 当执行下面这条语句时,就会抛出ClassCastException异常!!!
                // 原因:要加入的对象的类没有实现Compareable接口
                // 所以在强转为Comparable<? super K>类型时,抛出异常
                    Comparable<? super K> k = (Comparable<? super K>) key;
                do {
                    parent = t;
                    cmp = k.compareTo(t.key);
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    else
                        return t.setValue(value);
                } while (t != null);
            }
    

实践3

已知:Person2类按照id和name重写了hashCode()和equals(),问下面代码输出什么???
提示:这道题很有意思,稍不注意就掉进陷阱
看不出来就debug源码!!!
关键:set.add()方法和set.remove()方法底层机制!!!
public class Test6 {
    public static void main(String[] args) {

        HashSet<Person2> set = new HashSet<>();
        Person2 p1 = new Person2(1001, "AA");
        Person2 p2 = new Person2(1002, "BB");
        set.add(p1);
        set.add(p2);
        p1.name = "CC";
        set.remove(p1);
        // [Person2{id=1002, name='BB'}, Person2{id=1001, name='CC'}]
        System.out.println(set);
        set.add(new Person2(1001,"CC"));
        // [Person2{id=1002, name='BB'}, Person2{id=1001, name='CC'}, Person2{id=1001, name='CC'}]
        System.out.println(set);
        set.add(new Person2(1001,"AA"));
        // [Person2{id=1002, name='BB'}, Person2{id=1001, name='CC'}, Person2{id=1001, name='CC'}, Person2{id=1001, name='AA'}]
        System.out.println(set);

    }
}

class Person2 {

    private int id;
    String name;

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

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

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

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

}

输出结果:

image-20220625185034846

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

eliauk._

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值