Java笔记12 集合

12.1 集合的优点

前面我们保存多个数据使用的是数组,那么数组有不足的地方:

  1. 长度开始时必须指定,一旦指定,不能更改
  2. 保存的必须为同一类型的元素
  3. 使用数组进行增加/删除元素的代码比较麻烦

集合的优点:

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

12.2 集合的框架体系

Java的集合类有很多,主要分为两组(单列集合、双列集合):

  1. Collection接口有两个重要的子接口 List 和 Set ,它们的实现子类都是单列集合在这里插入图片描述

  2. Map接口的实现子类都是双列集合,存放的是K-V在这里插入图片描述

12.3 Collection接口

12.3.1 Collection接口实现类的特点

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

12.3.2 Collection接口常用方法

以实现子类ArrayList为例:

public class CollectionMethod {
    public static void main(String[] args) {
        List list = new ArrayList();
        //add:添加单个元素
        list.add("jack");
        list.add(10);
        list.add(true);
        //list=[jack, 10, true]

        //remove:删除指定元素
        list.remove(0);//删除第一个元素
        //删除第一个元素后,第二个元素会变成第一个元素
        list.remove(true);//删除指定元素

        //contains:查找元素是否存在
        System.out.println(list.contains(10));//true

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

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

        //clear:清空
        list.clear();//list=[]

        //addAll:添加多个元素
        ArrayList list2 = new ArrayList();
        list2.add("西游记");
        list2.add("红楼梦");
        list.addAll(list2);//list=[西游记, 红楼梦]

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

        //removeAll:删除多个元素
        list.add("三国演义");
        list.removeAll(list2);//list=[三国演义]


        System.out.println("list="+list);
    }
}

12.3.3 Collection接口遍历元素

12.3.3.1 方式1 使用Iterator(迭代器)
12.3.3.1.1 介绍
  1. Iterator 对象为迭代器,主要用于遍历 Collection 集合中的元素
  2. 所有实现 Collection 接口的集合类都有一个iterator()方法,用以返回一个实现了 Iterator 接口的对象,即返回一个迭代器
  3. Iterator 的结构:
  4. Iterator仅用于遍历集合,Iterator本身并不存放对象
12.3.3.1.2 执行原理

调用过next()方法后,iterator指向下一个元素
在这里插入图片描述

12.3.3.1.3 Iterator接口的方法
  1. hasNext():如果迭代具有更多元素,则返回 true
  2. next():返回迭代中的下一个元素
  3. remove():从底层集合中删除此迭代器返回的最后一个元素(可选操作)

在调用iterator.next()方法之前必须要调用iterator.hasNext()进行检测。若不调用,且下一条记录无效,直接调用iterator.next()会抛出NoSuchElementException异常

12.3.3.1.4 示例
public class IteratorDemo {
    public static void main(String[] args) {
        Collection col = new ArrayList();

        col.add(new Book("三国演义", "罗贯中", 10.1));
        col.add(new Book("小李飞刀", "古龙", 5.1));
        col.add(new Book("红楼梦", "曹雪芹", 34.6));

        //遍历col集合
        //1.得到col对应的迭代器
        Iterator iterator = col.iterator();
        //2.使用while循环遍历
        while (iterator.hasNext()) {//判断是否还有数据
            //返回下一个元素,类型是Object
            Object obj = iterator.next();
            //obj编译类型是Object,运行类型是真正存放的对象,此处为Book
            System.out.println("obj="+obj);
        }//itit快捷键自动生成此while循环
        //3.当退出while循环时,iterator执行最后的元素
        //iterator.next();//NoSuchElementException
        //4.如果希望再次遍历,需要重置我们的迭代器
        iterator = col.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println("obj="+next);
        }
    }
}
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;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                '}';
    }
}
12.3.3.2 方式2 for循环增强

增强for就是简化版的iterator,本质一样,只能用于遍历集合或数组

语法:

for(元素类型 元素名 : 集合名或数组名) {
    访问元素
}

示例:

//以上边集合col为例
for (Object obj : col) {
    System.out.println("obj="+obj);
}

12.4 List接口

12.4.1 介绍

  1. List接口是Collection接口的子接口
  2. List集合类中的元素有序(添加顺序取出顺序一致)、且可重复
  3. List集合中的每个元素都有其对应的顺序索引,即支持索引
  4. List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
  5. List接口的实现类有:ArrayList、LinkedList、Vector等

12.4.2 常用方法

public class ListMethod {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("张三丰");
        list.add("贾宝玉");

        //void add(int index, Object ele):在 index 位置插入 ele 元素
        list.add(1,"唐僧");//[张三丰, 唐僧, 贾宝玉]

        //boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
        List list1 = new ArrayList();
        list1.add("tom");
        list1.add("jack");
        list1.add("唐僧");
        list.addAll(list1);//[张三丰, 唐僧, 贾宝玉, tom, jack, 唐僧]

        //Object get(int index):获取指定 index 位置的元素
        System.out.println(list.get(1));//唐僧

        //int indexOf(Object obj):返回 obj 在集合中首次出现的位置
        System.out.println(list.indexOf("唐僧"));//1

        //int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
        System.out.println(list.lastIndexOf("唐僧"));//5

        //Object remove(int index):移除指定 index 位置的元素,并返回此元素
        list.remove(0);//[唐僧, 贾宝玉, tom, jack, 唐僧]

        //Object set(int index, Object ele):设置指定 index 位置的元素为 ele, 相当于是替换.
        list.set(4,"孙悟空");//[唐僧, 贾宝玉, tom, jack, 孙悟空]

        //List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合
        //返回的范围是[fromIndex, toIndex)
        List subList = list.subList(0,3);
        System.out.println(subList);//[唐僧, 贾宝玉, tom]

        System.out.println(list);
    }
}

12.4.3 List的三种遍历方式

  1. 使用Iterator
  2. 使用增强for
  3. 使用普通for

12.4.4 List练习

  1. /**
     * 添加10个以上元素,在2号位插入一个元素"java",获得第五个元素
     * 删除第六个元素,修改第七个元素,使用迭代器遍历集合
     * 要求使用List的实现类ArrayList完成
     */
    public class ListExer {
        public static void main(String[] args) {
            List list = new ArrayList();
            //添加10个以上元素
            addEle(list);
            //在2号位插入一个元素"java"
            list.add(2,"java");
            //获得第五个元素
            System.out.println(list.get(5));
            //删除第六个元素
            list.remove(6);
            //修改第七个元素
            list.set(7,100);
            //使用迭代器遍历集合
            Iterator iterator = list.iterator();
            while (iterator.hasNext()) {
                Object next = iterator.next();
                System.out.println(next);
            }
        }
        public static void addEle(List list) {
            for (int i = 1; i < 11; i++) {
                list.add(i);
            }
        }
    }
    
  2. /**
     * 使用List集合的额实现类添加三本图书,并遍历,打印如下效果
     * 名称: xx		价格: xx		作者: xx
     * 名称: xx		价格: xx		作者: xx
     * 名称: xx		价格: xx		作者: xx
     * 要求:
     * 1.按照价格从低到高排序(冒泡)
     * 2.要求使用ArrayList、LinkedList和Vector三种集合实现
     */
    public class ListExer2 {
        public static void main(String[] args) {
            //List list = new LinkedList();
            //List list = new ArrayList();
            List list = new Vector();
    
    
            list.add(new Book_("红楼梦", "曹雪芹", 100));
            list.add(new Book_("西游记", "吴承恩", 10));
            list.add(new Book_("水浒传", "施耐庵", 19));
            list.add(new Book_("三国", "罗贯中", 80));
            Iterator iterator = list.iterator();
            System.out.println("===========排序前============");
            while (iterator.hasNext()) {
                Object next =  iterator.next();
                System.out.println(next);
            }
            //排序
            sortList(list);
            System.out.println("===========排序后============");
            iterator = list.iterator();
            while (iterator.hasNext()) {
                Object next = iterator.next();
                System.out.println(next);
            }
    
        }
        public static void sortList(List list) {
            int listSize = list.size();
            for (int i = 0; i < listSize - 1; i++) {
                for (int j = 0; j < listSize - 1 - i; j++) {
                    Book_ b1 = (Book_) list.get(j);
                    Book_ b2 = (Book_) list.get(j+1);
                    if(b1.getPrice() > b2.getPrice()) {
                        list.set(j,b2);
                        list.set(j+1,b1);
                    }
                }
            }
        }
    }
    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 double getPrice() {
            return price;
        }
    
        @Override
        public String toString() {
            return "名称: " + name + "\t\t价格: " + price + "\t\t作者: " + author;
        }
    }
    

12.4.5 ArrayList

12.4.5.1 ArrayList注意事项
  1. ArrayList可以添加null,并且可以添加多个
  2. ArrayList是由数组来实现数据存储的
  3. ArrayList基本等同于Vector,除了ArrayList是线程不安全的(没有synchronized修饰),执行效率高。在多线程情况下,不建议使用ArrayList
12.4.5.2 ArrayList的底层操作机制源码分析
  1. ArrayList中维护了一个Object类型的数组 elementData
    1. transient Object[] elementData;
    2. transient表示瞬间,短暂的,表示该属性不会被序列化
  2. 当创建ArrayList对象时
    1. 如果使用无参构造器,则初始elementData容量为0。第一次添加,将elementData的容量扩容为10。若需要再次扩容,则扩容为当前容量的1.5倍
    2. 如果使用的是指定大小的构造器,则初始elementData容量为指定的大小,若需要扩容,则直接扩容为当前容量的1.5倍

对于代码:

ArrayList list = new ArrayList();
//ArrayList list = new ArrayList(8);
//使用 for 给 list 集合添加 1-10 数据
for (int i = 1; i <= 10; i++) {
list.add(i);
}
//使用 for 给 list 集合添加 11-15 数据
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(100);
list.add(200);
list.add(null);

源码分析:

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

12.4.6 Vector

12.4.6.1 介绍
  1. Vector类的定义:

    public class Vector<E>
        extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
  2. Vector底层也是一个对象数组:protected Object[] elementData;

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

    public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
    
        return elementData(index);
    }
    
  4. 开发中需要线程同步安全时,考虑使用Vector

  5. 使用无参构造器创建Vector对象,默认容量是10,需要扩容时,扩容为当前容量的两倍

12.4.6.2 源码分析

对于代码:

Vector vector = new Vector(8);
for (int i = 0; i < 10; i++) {
	vector.add(i);
}
vector.add(100)

源码分析:

  1. //new Vector() 底层
    public Vector() {
    	this(10);
    }
    //补充:如果是 Vector vector = new Vector(8);
    //用的方法:
    public Vector(int initialCapacity) {
    	this(initialCapacity, 0);
    }
    
  2. //vector.add(i)
    //下面这个方法就添加数据到 vector 集合
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
    
  3. //确定是否需要扩容 条件 : minCapacity - elementData.length>0
    private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - elementData.length > 0
            grow(minCapacity);
    }
    
  4. //如果 需要的数组大小 不够用,就扩容 , 扩容的算法
    //newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
    //就是扩容两倍. 
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
        	capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
        	newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
        	newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
12.4.6.3 Vector和ArrayList比较
底层结构版本线程安全(同步)效率扩容参数
ArrayList可变数组Object[]jdk1.2不安全,效率高如果无参构造:默认为空数组,第一次扩容为10,第二次开始1.5倍扩容;如果有参构造,1.5倍扩容
Vector可变数组Object[]jdk1.0安全,效率不高如果无参构造:默认容量为10,满后,按2倍扩容;如果有参构造,满后2倍扩容

12.4.7 LinkedList

12.4.7.1 介绍
  1. LinkedList底层实现了双向链表和双端队列特点
  2. 可以添加任意元素(可以重复),包括null
  3. 线程不安全,没有实现同步
12.4.7.2 LinkedList底层操作逻辑
  1. LinkedList底层维护了一个双向链表
  2. LinkedList中维护了两个属性firstlast,分别指向首节点和尾节点
  3. 每个结点(Node对象)里维护了prevnextitem三个属性,其中通过prev指向前一个,通过next指向后一个对象,最终实现双向链表
  4. LinkedList的元素的添加和删除不是通过数组完成的,相对来说效率较高
12.4.7.3 LinkedList增删改查
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println("linkedList=" + linkedList);
//删除一个结点
linkedList.remove(); // 这里默认删除的是第一个结点
//linkedList.remove(2);
System.out.println("linkedList=" + linkedList);

//修改某个结点对象
linkedList.set(1, 999);
System.out.println("linkedList=" + linkedList);

//得到某个结点对象
//get(1) 是得到双向链表的第二个对象
Object o = linkedList.get(1);
System.out.println(o);//999

//因为 LinkedList 是 实现了 List 接口, 遍历方式
System.out.println("===LinkeList 遍历迭代器====");
Iterator iterator = linkedList.iterator();
while (iterator.hasNext()) {
    Object next = iterator.next();
    System.out.println("next=" + next);
}
System.out.println("===LinkeList 遍历增强 for====");
for (Object o1 : linkedList) {
	System.out.println("o1=" + o1);
}
System.out.println("===LinkeList 遍历普通 for====");
	for (int i = 0; i < linkedList.size(); i++) {
	System.out.println(linkedList.get(i));
}

源码分析:

//分析代码:
//LinkedList linkedList = new LinkedList();
//linkedList.add(1);

//1. LinkedList linkedList = new LinkedList();
//new LinkedList()构造方法对应的源码:
public LinkedList() {}
//这时 linkeList 的属性 first = null last = null

//2. linkedList.add(1);
//add(1) 执行 添加
public boolean add(E e) {
    linkLast(e);
    return true;
}
//linkLast(e)将新的结点,加入到双向链表的最后
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++;
}
//分析代码:
//linkedList.remove(); // 这里默认删除的是第一个结点

//remove() 执行了 removeFirst()
public E remove() {
	return removeFirst();
}

//removeFirst() 通过执行unlinkFirst(f)完成删除
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
    	throw new NoSuchElementException();
    return unlinkFirst(f);
}

//unlinkFirst(f)将 f 指向的双向链表的第一个结点拿掉
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
    	last = null;
    else
    	next.prev = null;
    size--;
    modCount++;
    return element;
}

12.4.8 ArrayList和LinkedList比较

底层结构增删效率改查效率
ArrayList可变数组较低,需要数组扩容较高,可以通过索引直接定位
LinkedList双向链表较高,通过链表追加较低,需要从头一个一个遍历

如何选择:

  1. 改查操作比较多,选ArrayList
  2. 增删操作比较多,选LinkedList
  3. 一般在程序中,80%~90%都是查询,因此大部分情况下选择ArrayList
  4. 在项目中,根据业务灵活选择,比如,一个模块使用ArrayList,因一个模块使用LinkedList
  5. ArrayList和LinkList都是线程不安全的,尽量在单线程操作时选用

12.5 Set接口

12.5.1 介绍

  1. 无序(添加和取出的顺序不一致),没有索引
    1. 取出的顺序的顺序虽然不是添加的顺序,但是取出的顺序是固定的
  2. 不允许重复元素,最多包含一个null
  3. Set接口的实现类有:HashSet、LinkedHashSet、TreeSet等

12.5.2 常用方法

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

12.5.3 遍历方式

  1. 迭代器
  2. 增强for
  3. 不能使用索引的方式获取

12.5.4 示例

public class SetMethod {
    public static void main(String[] args) {
        Set set = new HashSet();
        set.add("john");
        set.add("lucy");
        set.add("john");//再次添加 john
        set.add("jack");
        set.add("hsp");
        set.add("mary");
        set.add(null);
        set.add(null);//再次添加 null
        System.out.println(set);//[null, hsp, mary, john, lucy, jack]

        set.remove("mary");

        System.out.println("====使用Iterator====");
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println(next);
        }

        System.out.println("====使用增强for====");
        for (Object o : set) {
            System.out.println(o);
        }
    }
}

12.5.5 HashSet

12.5.5.1 介绍
  1. HashSet实现了Set接口

  2. HashSet实际上是HashMap

    public HashSet() {
        map = new HashMap<>();
    }
    
  3. 可以存放null,但只能有一个

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

  5. 不能有重复的元素/对象

12.5.5.2 案例
public class HashSet01 {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        //说明
        //1. 在执行 add 方法后,会返回一个 boolean 值
        //2. 如果添加成功,返回 true, 否则返回 false
        //3. 可以通过 remove 指定删除哪个对象
        System.out.println(set.add("john"));//T
        System.out.println(set.add("lucy"));//T
        System.out.println(set.add("john"));//F
        System.out.println(set.add("jack"));//T
        System.out.println(set.add("Rose"));//T
        set.remove("john");
        System.out.println("set=" + set);//3 个对象
        
        set = new HashSet();
        System.out.println("set=" + set);//0
        //4 Hashset 不能添加相同的元素/数据?
        set.add("lucy");//添加成功
        set.add("lucy");//加入不了
        set.add(new Dog("tom"));//OK
        set.add(new Dog("tom"));//Ok
        System.out.println("set=" + set);
        
        //再加深一下. 非常经典的面试题
        //看源码,做分析, 先给小伙伴留一个坑,以后讲完源码,你就了然
        //去看他的源码,即 add 到底发生了什么?=> 底层机制. 
        set.add(new String("hsp"));//ok
        set.add(new String("hsp"));//加入不了. 
        System.out.println("set=" + set);
    }
}
class Dog { //定义了 Dog 类
    private String name;
    public Dog(String name) {
    	this.name = name;
    }
    @Override
    public String toString() {
        return "Dog{" +
        "name='" + name + '\'' +
        '}';
    }
}
12.5.5.3 HashSet底层机制
12.5.5.3.1 HashSet存储结构

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

HashSet的存储结构是哈希表

  • jdk1.8版本之前:

    • 哈希表=数组+链表
  • jdk1.8版本之后:

    • 哈希表=数组+链表
    • 哈希表=数组+红黑树(查询速度极快)

模拟简单的数组+链表结构,实现如图所示效果:

在这里插入图片描述

public class HashSetStructure {
    public static void main(String[] args) {
        //模拟一个HashSet的底层

        //1.创建一个数组,数组的类型是Node[]
        //2.有些人直接把Node[]数组称为表
        Node[] table = new Node[16];

        //3.创建结点
        Node john = new Node("john", null);
        table[2] = john;//把john结点放在table表索引为3的位置
        Node jack = new Node("jack", null);
        john.next = jack;//将jack结点 挂载 到john后边
        Node rose = new Node("rose", null);
        jack.next = rose;//将rose结点挂载到jack后边

        Node lucy = new Node("lucy", null);
        table[3] = lucy;    //把lucy结点放在table表索引为3的位置

        System.out.println("table="+table);
    }
}
class Node {
    Object item;
    Node next;

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }
}
12.5.5.3.2 创建HashSet及添加元素

创建HashSet底层其实是创建了一个HashMap

HashSet添加元素的底层实现:

  1. HashSet底层是HashMap
  2. 添加一个元素,先得到 hash值 会转成》索引值
  3. 找到存储数据表table,看这个索引位置是否已经存放的有元素
  4. 如果没有,直接加入
  5. 如果有,调用equals()依次进行比较,如果相同,则放弃添加;如果不同,则添加到最后
  6. 在JDK8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认为8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认为64),就会进行树化(成为红黑树)

源码解读:

对于代码:

HashSet hashSet = new HashSet();
hashSet.add("java");//到此位置,第 1 次 add 分析完毕.
hashSet.add("php");//到此位置,第 2 次 add 分析完毕
hashSet.add("java");
System.out.println("set=" + hashSet);

解读:

对于创建HashSet:HashSet hashSet = new HashSet();

及第一次add:hashSet.add("java");

  1. //HashSet hashSet = new HashSet();
    //执行的是构造器,底层是创建了HashMap
    public HashSet() {
        map = new HashMap<>();
    }
    

    执行后:

    在这里插入图片描述

  2. //hashSet.add("java");
    //执行add()
    public boolean add(E e) {	//e = "java"
        return map.put(e, PRESENT)==null;
        //PRESENT是HashSet的一个对象,没什么意义,占位的目的
        //PRESENT是静态的,共享
        //private static final Object PRESENT = new Object();
        //put()之后返回null代表成功了 
    }
    
  3. //执行 put()
    public V put(K key, V value) {	//key = "java" value = PRESENT
        return putVal(hash(key), key, value, false, true);
    }
    
  4. //hash(key)方法,如果key为null则返回0
    //不为空则通过(h = key.hashCode()) ^ (h >>> 16)
    //算法得到key对应的hash值(该hash值不完全等于hashCode)
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
  5. //putVal()方法
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;//定义了辅助变量
        //table 就是HashMap的一个属性,类型是 Node[]
        //if语句表示如果当前table是null,或者大小 = 0
        //就是第一次扩容,到16个空间
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        	//调用resize()方法后,table容量变为16
        //1.根据key得到的hash值,去计算该key应该存放到table的哪个索引位置
        //此处key = "java",计算出的i的值为3
        //并把这个位置的对象赋给辅助变量p
        //2.判断p是否为空
        //(1)如果p为空,表示没有存放元素,就创建一个Node(key,value)
        //当前key = "java" value = PRESENT
        //(2)放在该位置tab[i] = newNode(hash, key, value, null);
        //即将key为"java"的newNode存放在tab[3]
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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);
                        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;
        //判断当前大小是否大于临界值threshold
        if (++size > threshold)
            resize();//resize()相当于扩容
        afterNodeInsertion(evict);
        //afterNodeInsertion(evict)是空方法,作用是:
        //留给HashMap的子类(如LinkedHashMap)去实现该方法
        //完成相关操作
        return null;//返回空代表成功了
    }
    
  6. final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //oldTab也就是table为空时,oldCap为0
        int oldThr = threshold;
        //threshold是HashMap的一个属性,相当于数组扩容临界值 
        //定义该属性的代码为  int threshold;
        //当table为空时,threshold为int类型默认值0
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //oldCap和oldThr均为0时
            newCap = DEFAULT_INITIAL_CAPACITY;
            //将new赋值为1 << 4 即16,即扩容为16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            //newThr相当于临界值
            //计算方法是加载因子DEFAULT_LOAD_FACTOR*DEFAULT_INITIAL_CAPACITY
            //DEFAULT_LOAD_FACTOR = 0.75
            //DEFAULT_INITIAL_CAPACITY = 16
            //即0.75 * 16 = 12 时,就要准备扩容
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;//将threshold更新为new
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //创建一个容量为newCap的Node[]数组newTab
        table = newTab;//将newTab赋给table
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    
  7. 插入后在这里插入图片描述

第二次add:hashSet.add("php");

  1. public boolean add(E e) {	//e = "php"
        return map.put(e, PRESENT)==null;
    }
    
  2. public V put(K key, V value) {	//key = "php" value = PRESENT
        return putVal(hash(key), key, value, false, true);
    }
    
  3. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //同上,根据key得到的hash值,去计算该key应该存放到table的哪个索引位置
        //此处key = "php",计算得i = 9
        //tab[9]为空,创建key为"php"的newNode放在该位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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);
                        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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    
  4. 插入后在这里插入图片描述

对于插入相同的元素:hashSet.add("java");

  1. public boolean add(E e) {//e = "java"
        return map.put(e, PRESENT)==null;
    }
    
  2. public V put(K key, V value) {//key = "java" value = PRESENT
        return putVal(hash(key), key, value, false, true);
    }
    
  3. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据key = "java",计算得i = 3,tab[3]不为空
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //在需要局部变量(辅助变量)的时候再创建
            Node<K,V> e; K k;//辅助变量
            //如果 当前索引位置对应的链表的第一个元素p 和 准备添加的key 的hash值一样
            //并且满足下面两个条件之一:
            //(1)准备加入的key 和 p指向的Node结点的key 是同一个对象
            //(2)两者通过equals方法判断结果相同
            //equals方法可以被程序员通过重写来自定义比较的内容
            if (p.hash == hash &&	
                ((k = p.key) == key || (key != null && key.equals(k))))	//此时key已经与链表第一个元素比较过了
                e = p;
            //判断p是不是一棵红黑树
            //如果是红黑树,调用putTreeVal()方法来添加
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //如果table对应的索引位置已经是一个链表了
            //就使用for循环依次比较
            else {
                //有几种情况:
                //(1)依次跟该链表的的每个元素比较后,都不相同,则加入到该链表的最后
                //(2)依次跟该链表的的每个元素比较的过程中,如果有相同的情况,就直接break
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//判断是否比到链表最后一个元素了
                        //没有跟第一个元素比较吗?
                        //在前边已经比较过
                        p.next = newNode(hash, key, value, null);//加入到链表最后
                        //添加到链表后,立即判断链表结点个数是否达到了临界值8
                        //true则调用treeifyBin()方法考虑是否将当前链表转换为红黑树
                        //treeifyBin()方法会判断当前table数组是否为空或大小小于64
                        //(1)true则调用resize()对table数组进行扩容
                        //(2)false才进行树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //key与链表的元素进行比较
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    
  4. final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //treeifyBin()方法会判断当前table数组是否为空或大小小于64
        //(1)true则调用resize()对table数组进行扩容
        //(2)false才进行树化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
    
12.5.5.3.3 HashSet扩容或转成红黑树机制
  1. HashSet底层是HashMap,第一次添加时,table数组扩容到默认初始化容量值DEFAULT_INITIAL_CAPACITY(16),临界值threshold= 加载因子loadFactor(默认为DEFAULT_LOAD_FACTOR = 0.75) * 默认初始化容量值DEFAULT_INITIAL_CAPACITY(16) = 12
    1. HashMap维护一个int类型的变量size,表示当前结点的总数,每加入一个结点Node(k,v,hash,next),不管该节点是加在table表某索引的第一个位置,还是加在table表的某一条链表上,size的值都会加1
  2. 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,以此类推
  3. 在JDK8中,如果一条链表的元素个数达到了树形化阈值TREEIFY_THRESHOLD(8),并且table的大小 >= 最小树形化阈值MIN_TREEIFY_CAPACITY(64),就会进行树化(红黑树),否则仍然采用数组扩容机制

debug以下代码,查看hashSet的值

public class HashSetIncrement {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();

        //HashSet底层是HashMap,第一次添加时,
        //table数组扩容到默认初始化容量值`DEFAULT_INITIAL_CAPACITY`(16),
        //临界值`threshold`= 加载因子loadFactor(默认为`DEFAULT_LOAD_FACTOR = 0.75`) * 默认初始化容量值`DEFAULT_INITIAL_CAPACITY`(16) = 12
        //如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,以此类推
//        for (int i = 0; i < 100; i++) {
//            hashSet.add(i);//1,2,3,4,5,...,100
//        }

        //在JDK8中,如果一条链表的元素个数达到了树形化阈值`TREEIFY_THRESHOLD`(8),
        //并且table的大小 >= 最小树形化阈值`MIN_TREEIFY_CAPACITY`(64),
        //就会进行树化(红黑树),否则仍然采用数组扩容机制
        for (int i = 0; i < 13; i++) {
            hashSet.add(new A(i));
            //当i = 10时,table数组大小为64 = MIN_TREEIFY_CAPACITY,
            //且索引为36存放的链表结点数为10 > TREEIFY_THRESHOLD,
            //所以添加n = 10的A类对象后,进行了树化
        }

        System.out.println("hashSet=" + hashSet);
    }
}
class A {
    private int n;

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

    @Override
    public int hashCode() {
        return 100;//将A类对象的hashCode都置为100,确保加在table表同一索引位置
    }
}
12.5.5.3.4 HashSet练习
  1. /**
     * 定义一个Employee类,该类包含:private成员属性name、age 要求:
     * 1. 创建三个Employee对象放入HashSet中
     * 2. 当name和age相同时,认为是相同员工,不能添加到HashSet集合中
     */
    public class HashSetExercise {
        public static void main(String[] args) {
            HashSet hashSet = new HashSet();
            System.out.println(hashSet.add(new Employee("jack", 18)));//true
            System.out.println(hashSet.add(new Employee("tom", 20)));//true
            System.out.println(hashSet.add(new Employee("tom", 22)));//true
            System.out.println(hashSet.add(new Employee("jack", 18)));//false
        }
    }
    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 String toString() {
            return "Employee{" +
                    "name='" + name + '\'' +
                    ", 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);
        }
    }
    
  2. /**
     * 定义一个Employee类,该类包含:private成员属性name、sal、birthday(MyDate类)
     * 其中birthday为MyDate类的型(属性包括:year、month、day),要求:
     * 1. 创建三个Employee对象放入HashSet中
     * 2. 当name和birthday相同时,认为是相同员工,不能添加到HashSet集合中
     */
    public class HashSetExercise {
        public static void main(String[] args) {
            HashSet hashSet = new HashSet();
            System.out.println(hashSet.add(new Employee("tom", 2000, new MyDate(2000, 9, 1))));
            System.out.println(hashSet.add(new Employee("jack", 3000, new MyDate(2000, 9, 1))));
            System.out.println(hashSet.add(new Employee("tom", 4000, new MyDate(2000, 7, 1))));
            System.out.println(hashSet.add(new Employee("jack", 5000, new MyDate(2000, 9, 1))));
        }
    }
    class Employee {
        private String name;
        private double sal;
        private MyDate birthday;
    
        public Employee(String name, double sal, MyDate birthday) {
            this.name = name;
            this.sal = sal;
            this.birthday = birthday;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Employee employee = (Employee) o;
            return Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(name, 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;
        }
    
        @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);
        }
    }
    

12.5.6 LinkedHashSet

12.5.6.1 介绍
  1. LinkedHashSet是HashSet的子类
  2. LinkedHashSet底层是一个LinkedHashMap,底层维护了一个 数组+双向链表
  3. LinkedHashSet根据元素的hashCode来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来像是以插入顺序保存的
  4. LinkedHashSet不允许添加重复元素
12.5.6.2 LinkedHashSet底层原理

在这里插入图片描述

对于代码:

Set set = new LinkedHashSet();
set.add(new String("AA"));
set.add(456);
set.add(456);//false
set.add(new Customer("刘",1001));
set.add(123);
set.add("tom");

System.out.println(set);
  1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致

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

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

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

  5. 数组是 HashMap$Node[]。存放的元素/数据是LinkedHashMap$Entry类型

    //源码可见,Entry继承HashMap的静态内部类Node
    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);
        }
    }
    
12.5.6.3 练习
/**
 * Car1类(属性:name、price),如果name和price一样
 * 则认为是相同元素,就不能添加
 */
public class LinkedHashSetExercise {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();

        linkedHashSet.add(new Car1("奥拓",1000));
        linkedHashSet.add(new Car1("奥迪",300000));
        linkedHashSet.add(new Car1("法拉利",10000000));
        linkedHashSet.add(new Car1("奥迪",300000));
        linkedHashSet.add(new Car1("保时捷",70000000));
        linkedHashSet.add(new Car1("奥迪",300000));

    }

}
class Car1 {
    private String name;
    private double price;

    public Car1(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;
        Car1 car1 = (Car1) o;
        return Double.compare(car1.price, price) == 0 && Objects.equals(name, car1.name);
    }

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

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

12.5.7 TreeSet

12.5.7.1 介绍
  1. TreeSet 实现了 Set 接口
  2. TreeSet 最大的特点是可以排序
  3. 当我们使用无参构造器创建TreeSet时,仍然是无序的
  4. 若希望添加的元素按照某种规则排序,使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类),并指定排序规则
  5. TreeSet 是线程不安全的(没有synchronized)
  6. TreeSet 底层用的是 TreeMap
12.5.7.2 示例
public class TreeSetDemo {
    public static void main(String[] args) {
        //TreeSet treeSet = new TreeSet();
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //下面 调用 String 的 compareTo 方法进行字符串大小比较
                //如果认为字符串相等为相同元素
                //return ((String) o2).compareTo((String) o1);
                //如果要求加入的元素,按照长度大小排序
                //此时认为长度相等为相同元素
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        //添加数据.
        treeSet.add("jack");
        treeSet.add("tom");//长度为3
        treeSet.add("sp");
        treeSet.add("a");
        treeSet.add("abc");//长度为3
        //已经有tom,abc和tom比较长度相等,abc加入不了

        System.out.println(treeSet);//[a, sp, tom, jack]

    }
}
12.5.7.3 源码解读
  1. 构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性 this.comparator

    public TreeMap(Comparator<? super K> comparator) {
    	this.comparator = comparator;
    }
    
  2. 在 调用 treeSet.add(“tom”), 在底层会执行到 TreeMap 的 put() 方法

    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check
    
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {//cpr就是我们自定义的匿名内部类(对象)
            do {
                parent = t;
                //动态绑定到我们的匿名内部类(对象)的compare
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else	//如果相等,即compare返回0,这个数据就加入不了
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
    

12.6 Map接口

12.6.1 介绍

  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在这里插入图片描述
Map map = new HashMap();
map.put("no1", "韩顺平");//k-v
map.put("no2", "张无忌");//k-v
map.put("no1", "张三丰");//当有相同的 k, 就等价于替换
map.put("no3", "张三丰");//k-v
map.put(null, null);//k-v
map.put(null, "abc");//等价替换
map.put("no4", null);//k-v
map.put("no5", null);//k-v
map.put(1, "赵敏");//k-v
map.put(new Object(), "金毛狮王");//k-v
// 通过 get 方法,传入 key ,会返回对应的 value
System.out.println(map.get("no2"));//张无忌

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

//1.k-v 最后是通过 HashMap$Node node = newNode(hash, key, value, null);存在table表中的HashMap$Node中
//2.k-v 为了方便遍历,还会创建 EntrySet 集合,该集合存放的是一个一个的Entry
//  一个Entry对象包含有k、v,即EntrySet<Entry<K, V>>
//  源码:transient Set<Map.Entry<K,V>> entrySet;
//3.Entry中,定义的类型是Map$Entry,但实际上他存放的还是HashMap$Node
//  这是因为HashMap$Node实现了Map.Entry接口
//  源码:static class Node<K,V> implements Map.Entry<K,V>{...}
//  其实就是用到了向上转型,即Map$Entry entry = node;
//  编译类型是Map$Entry,运行类型是HashMap$Node
//4.当把HashMap$Node对象放到entrySet中,就方便我们遍历
//  因为Entry提供了getKey()和getValue()方法

12.6.2 常用方法

  1. put:添加
  2. remove:根据 key 删除映射关系
  3. get:根据 key 获取 value
  4. size:获取元素个数
  5. isEmpty:判断元素个数是否为0
  6. clear:清空
  7. containsKey:查找 key 是否存在
public class MapMethod {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("邓超", new Book("", 100));//OK
        map.put("邓超", "孙俪");//替换-> 一会分析源码
        map.put("王宝强", "马蓉");//OK
        map.put("宋喆", "马蓉");//OK
        map.put("刘令博", null);//OK
        map.put(null, "刘亦菲");//OK
        map.put("鹿晗", "关晓彤");//OK
        System.out.println("map=" + map);
        // remove:根据键删除映射关系
        map.remove(null);
        System.out.println("map=" + map);
        // get:根据键获取值
        Object val = map.get("鹿晗");
        System.out.println("val=" + val);
        // size:获取元素个数
        System.out.println("k-v=" + map.size());
        // isEmpty:判断个数是否为 0
        System.out.println(map.isEmpty());//F
        // containsKey:查找键是否存在
        System.out.println("结果=" + map.containsKey("宋喆"));//T
        System.out.println("map=" + map);
        // 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;
    }
}

12.6.3 六大遍历方式

常用方法:

  1. containsKey:查找 key 是否存在
  2. keySet:获取所有的 key
  3. values:获取所有的 value
  4. entrySet:获取所有的k-v
public class MraverseMap {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("宋喆", "马蓉");
        map.put("刘令博", null);
        map.put(null, "刘亦菲");
        map.put("鹿晗", "关晓彤");

        //第一组: 先取出 所有的 Key , 通过 Key 取出对应的 Value
        System.out.println("======第一组======");
        Set keySet = map.keySet();

        System.out.println("====增强 for====");
        for (Object key : keySet) {
            System.out.println(key + "-" +map.get(key));
        }

        System.out.println("====迭代器====");
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println(next + "-" + map.get(next));
        }

        //第二组: 把所有的 values 取出
        System.out.println("======第二组======");
        Collection values = map.values();
        //这里可以使用所有的 Collections使用的遍历方法
        System.out.println("====增强 for====");
        for (Object value : values) {
            System.out.println(value);
        }
        System.out.println("====迭代器====");
        Iterator iterator1 = values.iterator();
        while (iterator1.hasNext()) {
            Object next = iterator1.next();
            System.out.println(next);
        }

        //第三组: 通过 EntrySet 来获取 k-v
        System.out.println("======第三组======");
        Set entrySet = map.entrySet();

        System.out.println("====增强 for====");
        for (Object entry : entrySet) {
            //将entry转成Map.Entry
            Map.Entry mEntry = (Map.Entry) entry;
            System.out.println(mEntry.getKey() + "-" + mEntry.getValue());
        }
        System.out.println("====迭代器====");
        Iterator iterator2 = entrySet.iterator();
        while (iterator2.hasNext()) {
            Map.Entry next = (java.util.Map.Entry) iterator2.next();
            System.out.println(next.getKey() + "-" + next.getValue());
        }
    }
}

12.6.4 练习

/**
 * 使用HashMap添加3个员工对象,要求:
 * key:员工id
 * value:员工对象
 * 并遍历显示工资>18000的员工(遍历方式最少两种)
 * 员工类:姓名、工资、员工id
 */
public class MapExercise {
    public static void main(String[] args) {
        Employee tom = new Employee("tom", 20000, 10001);
        Employee mary = new Employee("mary", 25000, 10002);
        Employee jack = new Employee("jack", 16000, 10003);

        Map map = new HashMap();
        map.put(tom.getId(),tom);
        map.put(mary.getId(),mary);
        map.put(jack.getId(),jack);

        System.out.println("====keySet增强for====");
        Set keySet = map.keySet();
        for (Object key : keySet) {
            Employee employee = (Employee) map.get(key);
            if (employee.getSal() > 18000) {
                System.out.println(employee);
            }
        }

        System.out.println("====entrySet迭代器====");
        Set entrySet = map.entrySet();
        Iterator iterator = entrySet.iterator();
        while (iterator.hasNext()) {
            Map.Entry next = (Map.Entry) iterator.next();
            Employee employee = (Employee) next.getValue();
            if (employee.getSal() > 18000) {
                System.out.println(employee);
            }
        }
    }
}
class Employee {
    private String name;
    private double sal;
    private int id;

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

    public String getName() {
        return name;
    }

    public double getSal() {
        return sal;
    }

    public int getId() {
        return id;
    }

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

12.6.5 HashMap

12.6.5.1 HashMap小结
  1. Map 接口的常用实现类:HashMap、HashTable 和 Properties

  2. HashMap 是 Map 接口使用频率最高的实现类

  3. HashMap 是以 key-value 对的方式存储数据的(HashMap$Node类型)

  4. key 不能重复,但是值可以重复,允许使用 null 键和 null值

  5. 如果添加相同的 key,则会覆盖原来的 key-value ,等同于修改(key 不会替换,value会替换)看源码

    //putVal()方法中,当两者比较key相同为true后,
    //若原先的元素非空,进行value值的替换
    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    
  6. 和 HashSet 一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(JDK8的HashMap底层是 数组+链表+红黑树)

  7. HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized

12.6.5.2 HashMap底层机制及源码剖析

存储结构:

在这里插入图片描述

扩容机制(和HashSet相同):

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

12.6.6 Hashtable

12.6.6.1 介绍
  1. Hashtable 存放的元素是键值对:key-value
  2. Hashtable 的 key 和 value 都不能为 null,否则会抛出NullPointerException
  3. Hashtable 是线程安全的(synchronized),HashMap是线程不安全的
Hashtable table = new Hashtable();
table.put("john", 100);//ok
table.put("null", 100);//异常
table.put("john", null);//异常
table.put("lucy", 100);//ok
table.put("lic", 100);//ok
table.put("lic", 88);//替换
12.6.6.2 底层结构
  1. 底层是一个数组Hashtable$Entry[],初始化大小为11

  2. 临界值threshold = 当前容量 * 0.75,即初始为8

  3. 扩容:先判断要添加的元素是否为空

    1. 为空则抛异常

    2. 不为空则调用方法addEntry(hash, key, value, index);添加k-v

      1. 当元素个数未到达临界值,直接添加

      2. 当元素个数到达临界值,调用rehash()进行扩容

        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();
        
        1. 扩容算法为int newCapacity = (oldCapacity << 1) + 1;
        2. 即当前容量的两倍加1
        3. 扩容后将 临界值 置为 新容量 * 0.75
12.6.6.3 Hashtable和HashMap比较
版本线程安全(同步)效率允许null键null值
HashMap1.2不安全允许
Hashtable1.0安全较低不允许

12.6.7 Properties

12.6.7.1 介绍
  1. Properties 类继承自 Hashtable 类并实现了 Map 接口,也是使用一种键值对的形式来保存数据
  2. 它的特点和 Hashtable 类似
  3. Properties 还可以用于从 xxx.properties 文件中,加载到 Properties 类对象,并进行读取和修改
12.6.7.2 基本使用
public class PropertiesDemo {
    public static void main(String[] args) {
        Properties properties = new Properties();
        //增加
//        properties.put(null, "abc");//抛出空指针异常
//        properties.put("abc", null);//抛出空指针异常
        properties.put("john", 100);//ok
        properties.put("lucy", 100);//ok
        properties.put("lic", 100);//ok
        properties.put("lic", 88);//替换

        //通过key获取value
        System.out.println(properties.get("lic"));

        //删除
        properties.remove("lucy");

        //修改
        properties.put("john", "约翰");

        System.out.println(properties);
    }
}

12.6.8 TreeMap

12.6.8.1 介绍

可以参考TreeMap原理实现及常用方法

  1. TreeMap存储K-V键值对,通过红黑树(R-B tree)实现;
  2. TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要TreeMap自己去实现;
  3. TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;
  4. TreeMap因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过Key值的自然顺序进行排序;
12.6.8.2 示例
public class TreeMapDemo {
    public static void main(String[] args) {
        //使用默认的构造器,创建 TreeMap, 是无序的(也没有排序)
        //TreeMap treeMap = new TreeMap();
        
        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //按照 K(String) 的大小来排序
                //return ((String) o1).compareTo((String) o2);
                //按照 K(String) 的长度大小排序
                return ((String) o1).length() - ((String) o2).length();
            }
        });

        treeMap.put("jack", "杰克");
        treeMap.put("tom", "汤姆");
        treeMap.put("kristina", "克瑞斯提诺");
        treeMap.put("smith", "斯密斯");
        treeMap.put("mary", "玛丽");
        //若按照字符串长度排序,key(mary)加入不了,value会替换为"玛丽"

        System.out.println(treeMap);
    }
}
12.6.8.3 源码分析
  1. 构造器. 把传入的实现了 Comparator 接口的匿名内部类(对象),传给给 TreeMap 的 comparator

    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    
  2. 调用 put 方法

    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {//第一次添加 t为空
            compare(key, key); // type (and possibly null) check
            //两个key相同,且没有接收结果,只是为了检查是不是空值
    
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        //第二次及之后添加,将传入的比较器赋给cpr
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {//遍历所有的key,给当前key找到适当位置
                parent = t;
                cmp = cpr.compare(key, t.key);//动态绑定到我们的匿名内部类的compare
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else//如果遍历过程中,发现准备添加的key和当前已有的key相等,就不添加,而是将value替换
                    return t.setValue(value);//替换value
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
    

12.7 如何选择集合实现类

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

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

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

12.8 Collections工具类

12.8.1 介绍

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

12.8.2 排序

  1. reverse(List):反转 List 中元素的顺序
  2. shuffle(List):对 List 集合元素进行随机排序
  3. sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
    1. 字符串集合按首字母大小排序
  4. sort(List, Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
  5. swap(List, int i, int j):进指定 List 集合中的 i 处元素和 j 处元素进行交换

12.8.3 查找替换

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

12.8.4 示例

public class CollectionsDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("tom");
        list.add("smith");
        list.add("king");
        list.add("milan");
        list.add("tom");
        System.out.println("原集合: "+list);//[tom, smith, king, milan, tom]

        // reverse(List):反转 List 中元素的顺序
        Collections.reverse(list);
        System.out.println("reverse: "+list);//[tom, milan, king, smith, tom]

        // shuffle(List):对 List 集合元素进行随机排序
        for (int i = 0; i < 5; i++) {
            Collections.shuffle(list);
            System.out.println("第" + i + "次shuffle: " + list);
        }
        /**
         * [milan, tom, king, tom, smith]
         * [tom, king, tom, smith, milan]
         * [tom, tom, milan, smith, king]
         * [tom, milan, tom, smith, king]
         * [king, smith, tom, milan, tom]
         */

        // sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
        Collections.sort(list);
        System.out.println("sort: " + list);//[king, milan, smith, tom, tom]

        // 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);//[tom, tom, king, milan, smith]

        // swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
        Collections.swap(list, 0, 4);
        System.out.println("swap后: " + list);//[smith, tom, king, milan, tom]

        //Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
        System.out.println("max = " + Collections.max(list));//tom

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

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

        //void copy(List dest,List src):将 src 中的内容复制到 dest
        ArrayList dest = new ArrayList();
        //此时dest大小小于list,直接copy将抛索引越界异常
        //为了完成一个完整拷贝,我们需要先给 dest 赋值,大小和 list.size()一样
        for(int i = 0; i < list.size(); i++) {
            dest.add("");
        }
        Collections.copy(dest, list);
        System.out.println("dest = " + dest);//[smith, tom, king, milan, tom]

        //boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
        Collections.replaceAll(list, "tom", "汤姆");
        System.out.println("将tom替换为汤姆: " + list);//[smith, 汤姆, king, milan, 汤姆]
    }
}

12.9 练习

  1. /**
     * 按要求实现:
     * (1)封装一个新闻类,包含标题和内容属性,提供get,set方法,重写toString方法,打印对象
     * 时只打印标题
     * (2)只提供一个带参数的构造器,实例化对象时,只初始化标题:并且实例化两个对象:
     * 新闻一:新冠确诊病例超干万,数百万印度教信徒赴恒河“圣浴”引民众担忧
     * 新闻二:男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生
     * (3)将新闻对象添加到ArrayList集合中,并且进行倒序遍历;
     * (4)在遍历集合过程中,对新闻标题进行处理,超过15字的只保留前15个,然后在后边加"..."
     * (5)在控制台打印遍历出经过处理的新闻标题
     */
    public class Exer12_1 {
        public static void main(String[] args) {
            //实例化两个对象
            News news1 = new News("新冠确诊病例超干万,数百万印度教信徒赴恒河“圣浴”引民众担忧");
            News news2 = new News("男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生");
    
            //将新闻对象添加到ArrayList集合中,并且进行倒序遍历
            ArrayList list = new ArrayList();
            list.add(news1);
            list.add(news2);
    
            int size = list.size();
            //在遍历集合过程中,对新闻标题进行处理,超过15字的只保留前15个,然后在后边加"..."
            for (int i = size -1; i >= 0; i--) {
                System.out.println(handlingTitle((News) list.get(i)));
            }
        }
        public static String handlingTitle(News news) {
            String title = news.getTitle();
            if (title == null || title.length() == 0) {
                return "";
            }
            if (title.length() < 16) {
                return title;
            } else {
                return title.substring(0, 15) + "...";
            }
        }
    }
    class News {
        private String title;
        private String content;
    
        public News(String title) {
            this.title = title;
        }
    
        @Override
        public String toString() {
            return "NEWS{" +
                    "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;
        }
    }
    
  2. /**
     * 按要求完成下列任务
     * 1)使用HashMap类实例化一个Map类型的对象m,键(String)和值(int)分别用于存储员
     * 工的姓名和工资,存入数据如下: jack-650元; tom-1200元: smith-2900元;
     * 2)将jack的工资更改为2600元
     * 3)为所有员工工资加薪100元;
     * 4)遍历集合中所有的员工
     * 5)遍历集合中所有的工资
     */
    public class Exer12_2 {
        public static void main(String[] args) {
            //1)使用HashMap类实例化一个Map类型的对象m,键(String)和值(int)分别用于存储员
            //工的姓名和工资,存入数据如下: jack-650元; tom-1200元: smith-2900元;
            Map m = new HashMap();
    
            m.put("jack", 650);
            m.put("tom", 1200);
            m.put("smith", 2900);
    
            //2)将jack的工资更改为2600元
            m.put("jack", 2600);
    
            //3)为所有员工工资加薪100元;
            Set keySet = m.keySet();
            for (Object key : keySet) {
                m.put(key, (int)(m.get(key)) + 100);
            }
    
            //遍历集合中所有的员工
            Set entrySet = m.entrySet();
            for (Object o : entrySet) {
                Map.Entry entry = (Map.Entry) o;
                System.out.println(entry.getKey() + "-" +entry.getValue());
            }
    
            //5)遍历集合中所有的工资
            Collection values = m.values();
            Iterator iterator = values.iterator();
            while (iterator.hasNext()) {
                Object next = iterator.next();
                System.out.println(next);
            }
        }
    }
    
  3. 试分析HashSet和TreeSet分别如何实现去重的

    1. HashSet 的去重机制:hashCode() + equals(),底层先通过存入对象,进行运算得到一个 hash 值,通过 hash 值得到对应的索引,如果发现 table 索引所在的位置没有数据,就直接存放;如果有数据,就进行 equals 比较[遍历比较],如果比较后不相同,就加入,否则就不加入。
    2. TreeSet 的去重机制:如果你传入了一个 Comparator 匿名对象,就使用实现的 compare 去重,如果方法返回0,就认为是相同的元素/数据,就不添加;如果你没有传入一个 Comparator 匿名对象,则以你添加的对象实现的Compareable接口的compareTo去重。
  4. 下面代码运行会不会抛出异常,并从源码层面说明原因(考察 读源码+接口编程+动态绑定)

    public class Exer12_3 {
        public static void main(String[] args) {
            TreeSet treeSet = new TreeSet();
            //add方法,因为TreeSet构造器没有传入Comparator接口的匿名内部类
            //所以在底层compare(key, key);时,
            //(Comparable<? super K>)k1会将key转换为Comparable类型
            //即要将Person对象转成Comparable类型
            //又因person没有实现Comparable接口,所以抛类型转换异常
            treeSet.add(new Person());//Person cannot be cast to java.lang.Comparable
        }
    }
    class Person {}
    
  5. /**
     * 下面代码输出什么?
     * 已知:Person类按照id和name重写了hashCode和equals方法
     */
    public class Exer12_3 {
        public static void main(String[] args) {
            HashSet set = new HashSet();
            Person p1 = new Person(1001, "AA");
            Person p2 = new Person(1002, "BB");
            set.add(p1);
            set.add(p2);
            p1.name = "CC";//修改后,p1的hash值改变
            set.remove(p1);//按照修改后的hash值找不到set中的p1,删除失败
            System.out.println(set);//2个 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
            //Person(1001, "CC")的hash值对应的位置还未存放元素,可以添加成功
            set.add(new Person(1001, "CC"));
            System.out.println(set);//3个 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]
            //Person(1001, "AA")的hash对应的位置已有元素Person(1001, "CC")
            //比较内容不相同,将Person(1001, "AA")添加到Person(1001, "CC")后边
            set.add(new Person(1001, "AA"));
            System.out.println(set);//4个 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]
        }
    }
    class Person {
        private int id;
        public String name;
    
        public Person(int id, String name) {
            this.id = id;
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person person = (Person) o;
            return id == person.id && Objects.equals(name, person.name);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(id, name);
        }
    }
    
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值