2021-11-01 十三、集合

1. 集合框架概述

1.1 集合框架与数组的对比及概述

集合、数组都是对多个数据进行存储操作的结构,简称Java容器。

说明;此时的存储,主要是指能存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)

回顾一下数组在内存存储方面的特点:

  1. 一旦初始化以后,它的长度就确定了。
  2. 数组一旦定义好,它的数据类型也就确定了。我们就只能操作指定类型的数据了。比如:String[] arr; / int[] str;

数组在存储数据方面的弊端:

  1. 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
  2. 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用。
  3. 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。

Java集合类可以用于存储数量不等的多个对象(动态地把多个对象的引用放入容器中),还可以保存具有映射关系的关联数组

集合的使用场景:开发过程中返回数据给前端
在这里插入图片描述

1.2 集合框架涉及到的API

Java 集合可分为CollectionMap两种体系
Collection接口:单列数据,定义了存取一组对象的方法的集合

  1. List元素有序、可重复的集合
  2. Set元素无序、不可重复的集合

Map接口:双列数据,保存具有映射关系key-value的集合

Collection接口继承树
在这里插入图片描述

Map接口继承树
在这里插入图片描述

2. Collection接口

Collection 接口是ListSetQueue 接口的父接口,该接口里定义的方法既可用于操作Set 集合,也可用于操作List 和Queue 集合。
JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现
在Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成Object 类型处理;从JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。

常用方法

功能方法名
添加add(Objec tobj)
添加全部addAll(Collection coll)
获取有效元素的个数int size()
清空集合void clear()
是否是空集合boolean isEmpty()
是否包含某个元素(是通过元素的equals方法来判断是否是同一个对象)boolean contains(Object obj)
是否包含某个元素,调用元素的equals方法,拿两个集合的元素挨个比较。boolean containsAll(Collection c)
通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素boolean remove(Object obj)
取当前集合的差集boolean removeAll(Collection coll)
取两个集合的交集(把交集的结果存在当前集合中,不影响c)boolean retainAll(Collection c)
集合是否相等boolean equals(Object obj)
转成对象数组Object[] toArray()
获取集合对象的哈希值hashCode()
遍历(返回迭代器对象,用于集合遍历)iterator()
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;

public class CollectionTest {

	// add/size/addAll/clear/isEmpty
    @Test
    public void test1(){        // 没有指定类型,所以是Object
        Collection coll = new ArrayList();

        //add(Object e):将元素e添加到集合coll中
        coll.add("AA");
        coll.add("BB");
        coll.add(123);  //自动装箱
        coll.add(new Date());

        //size():获取添加的元素的个数
        System.out.println(coll.size());    //4

        //addAll(Collection coll1):将coll1集合中的元素添加到当前的集合中
        Collection coll1 = new ArrayList();
        coll1.add(456);
        coll1.add("CC");
        coll.addAll(coll1);

        System.out.println(coll.size());    //6
        System.out.println(coll);

        //clear():清空集合元素
        coll.clear();

        //isEmpty():判断当前集合是否为空
        System.out.println(coll.isEmpty()); //true
    }
}

import java.util.Objects;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
/**
重写equals方法和hashcode方法 
*/ 
class Person {

    private String name;
    private int age;

    public Person() {
        super();
    }

    public Person(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 "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }


    @Override
    public boolean equals(Object o) {
        System.out.println("Person equals()....");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {

        return Objects.hash(name, age);
    }
}

public class CollectionTest {
	// contains
    @Test
    public void test2(){        
    	Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);

        Person p = new Person("Jerry",21);
        coll.add(p);
        coll.add(new Person("Jerry",20));

        coll.add(new String("Tom"));
        coll.add(false);

        //1.contains(Object obj):判断当前集合中是否包含obj
        //我们在判断时会调用obj对象所在类的equals()。
        boolean contains = coll.contains(123);
        System.out.println(contains);
        System.out.println(coll.contains(new String("Tam")));
        System.out.println(coll.contains(p));//true
        System.out.println("----person equals----");
        System.out.println(coll.contains(new Person("Jerry",20)));//从头开始比对,匹配上第一个就返回true,false -->true

        System.out.println("----person equals end----");
        //2.containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在于当前集合中。
        Collection coll1 = Arrays.asList(123,4567);
        System.out.println(coll.containsAll(coll1));
    }

    // remove/removeAll
    @Test
    public void test3(){        
    	//3.remove(Object obj):从当前集合中移除obj元素。
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new Person("Jerry", 20));
        coll.add(new String("Tom"));
        coll.add(false);

        coll.remove(1234);
        System.out.println(coll);

        coll.remove(new Person("Jerry", 20));
        System.out.println(coll);

        //4. removeAll(Collection coll1):差集:从当前集合中移除coll1中所有的元素。
        Collection coll1 = Arrays.asList(123, 456);
        coll.removeAll(coll1);
        System.out.println(coll);
    }

	// retainAll/equals
    @Test
    public void test4(){
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new Person("Jerry",20));
        coll.add(new String("Tom"));
        coll.add(false);

        //5.retainAll(Collection coll1):交集:获取当前集合和coll1集合的交集,并返回给当前集合
//        Collection coll1 = Arrays.asList(123,456,789);
//        coll.retainAll(coll1);
//        System.out.println(coll);

        //6.equals(Object obj):要想返回true,需要当前集合和形参集合的元素都相同。
        Collection coll1 = new ArrayList();
        coll1.add(456);
        coll1.add(123);
        coll1.add(new Person("Jerry",20));
        coll1.add(new String("Tom"));
        coll1.add(false);

        System.out.println(coll.equals(coll1));
    }

	// hashCode/toArray/asList
    @Test
    public void test5(){
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new Person("Jerry",20));
        coll.add(new String("Tom"));
        coll.add(false);

        //7.hashCode():返回当前对象的哈希值
        System.out.println(coll.hashCode());

        //8.集合 --->数组:toArray()
        Object[] arr = coll.toArray();
        for(int i = 0;i < arr.length;i++){
            System.out.println(arr[i]);
        }

        //拓展:数组 --->集合:调用Arrays类的静态方法asList()
        List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
        System.out.println(list);

        List arr1 = Arrays.asList(123, 456);
        System.out.println(arr1);//[123, 456]

        List arr2 = Arrays.asList(new int[]{123, 456});
        System.out.println(arr2.size());//1

        List arr3 = Arrays.asList(new Integer[]{123, 456});
        System.out.println(arr3.size());//2
    }
}

2.1 Iterator迭代器接口

Iterator对象称为迭代器(设计模式的一种:迭代器模式),主要用于遍历Collection 集合中的元素
GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
Iterator 仅用于遍历集合,Iterator本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
在这里插入图片描述

2.1.1 使用Iterator遍历Collection

主要使用hasNext()next()方法

import org.junit.Test;

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

public class IteratorTest {

    @Test
    public void test(){
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new Person("Jerry",20));
        coll.add(new String("Tom"));
        coll.add(false);

        Iterator iterator = coll.iterator();

        //方式一:
//        System.out.println(iterator.next());
//        System.out.println(iterator.next());
//        System.out.println(iterator.next());
//        System.out.println(iterator.next());
//        System.out.println(iterator.next());
//        //报异常:NoSuchElementException
//        //因为:在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。
//        System.out.println(iterator.next());

        //方式二:不推荐
//        for(int i = 0;i < coll.size();i++){
//            System.out.println(iterator.next());
//        }

        //方式三:推荐
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

执行原理
在这里插入图片描述
注意事项

  1. 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,所以不要在while循环条件判断的时候直接用collection.iterator()来创建迭代器对象,这样会一直打印第一个元素

2.1.2 Iterator迭代器remove()的使用

内部定义了remove(),可以在遍历的时候,删除集合中的元素。此方法不同于集合直接调用remove()

import org.junit.Test;

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

public class IteratorTest {

    //测试Iterator中的remove()方法
    @Test
    public void test3(){
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new Person("Jerry",20));
        coll.add(new String("Tom"));
        coll.add(false);

        // 删除集合中”Tom”
        // 删除指定元素之后后面的元素会前移
        Iterator iterator = coll.iterator();
        while(iterator.hasNext()){
            Object obj = iterator.next();
            if("Tom".equals(obj)){
                iterator.remove();               
            }
        }

        //遍历集合
        iterator = coll.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }

    }
}

2.2 新特性foreach循环遍历集合或数组

Java 5.0 提供了foreach循环迭代访问Collection和数组。遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
遍历集合的底层调用Iterator完成操作,所以本质上是Iterator的语法糖。
在这里插入图片描述

import org.junit.Test;

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

public class ForTest {

    @Test
    public void test(){
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new Person("Jerry",20));
        coll.add(new String("Tom"));
        coll.add(false);

        //for(集合元素的类型 局部变量 : 集合对象),内部仍然调用了迭代器。
        for(Object obj : coll){
            System.out.println(obj);
        }
    }

    @Test
    public void test2(){
        int[] arr = new int[]{1,2,3,4,5,6};
        //for(数组元素的类型 局部变量 : 数组对象)
        for(int i : arr){
            System.out.println(i);
        }
    }

    //练习题
    @Test
    public void test3(){
        String[] arr = new String[]{"SS","KK","RR"};

//        //方式一:普通for赋值
//        for(int i = 0;i < arr.length;i++){
//            arr[i] = "HH";
//        }

        //方式二:增强for循环
        for(String s : arr){
            s = "HH";
        }
		// 注意方式1和方式2操作的区别,方式1使用的是本身的数组元素,所以会改变数组元素的值,
		// 方式2只是把数组中的数取出来赋值给局部变量s,只会修改s的指向,不会影响到原始数组
        for(int i = 0;i < arr.length;i++){
            System.out.println(arr[i]);
        }
    }
}

2.3 Collection子接口之一:List接口

鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组

  1. List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
  2. List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素

JDK API中List接口的实现类常用的有:ArrayListLinkedListVector

List接口常用实现类的对比

|----Collection接口:单列集合,用来存储一个一个的对象
      |----List接口:存储有序的、可重复的数据。(本质上就是“动态”数组,超过长度就调用Arrays.copyOf(elementData, newCapacity)替换原有的数组)
           |----ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储
           |----LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
           |----Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储

面试题:比较ArrayList、LinkedList、Vector三者的异同?
同:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
不同:见上对比

2.3.1 ArrayList的源码分析

ArrayList是List 接口的典型实现类、主要实现类(平时开发中常用)
本质上,ArrayList是对象引用的一个”变长”数组

JDK1.7
ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组
list.add(123);//elementData[0] = new Integer(123);

list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。

JDK1.8中,直接上源码
主要区别在创建ArrayList对象和第一次添加的时候,其他无异

	private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	// 空参构造器并没有创建长度为10的数组,而是给了一个{}
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

	// 第一次调用add()才会创建长度为10的数组,然后再添加数据
    public boolean add(E e) {
       ensureCapacityInternal(size + 1);  // Increments modCount!!
       elementData[size++] = e;
       return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
       ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    private static final int DEFAULT_CAPACITY = 10;
    // 在这进行长度的校验,如果是{}那就返回10
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    // 用来判断当前所需要的最小容量是否大于数组的长度,大于的话就扩容
   private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

JDK7中的ArrayList的对象的创建类似于单例的饿汉式,而JDK8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

2.3.2 LinkedList的源码分析

对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高
LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。
在这里插入图片描述

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     */
    transient Node<E> last;
	
	// 内部结点类
    private static class Node<E> {
    	E item;
    	Node<E> next;
    	Node<E> prev;

LinkedList的新增操作主要是调整地址的指向

  1. 将传入的元素封装到一个Node里面作为一个新的结点
  2. 将原先链表中的最后一个结点指向这个新的结点

下面贴上新增add()的源码

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    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++;
    }

2.3.3 Vector的源码分析

Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用
这里偷个懒直接说结论,感兴趣的同学可以参照上面来看源码,JDK7和JDK8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组,默认扩容为原来的数组长度的2倍。

2.3.4 List接口中的常用方法

List除了从Collection集合继承的方法外,还添加了一些根据索引来操作集合元素的方法。

和索引相关的方法作用
void add(int index, Object ele)在index位置插入ele元素
boolean addAll(int index, Collection eles)从index位置开始将eles中的所有元素添加进来
Object get(int index)获取指定index位置的元素
int indexOf(Object obj)返回obj在集合中首次出现的位置
int lastIndexOf(Object obj)返回obj在当前集合中末次出现的位置
Object remove(int index)移除指定index位置的元素,并返回此元素
Object set(int index, Object ele)设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex)返回从fromIndex到toIndex位置,[fromIndex,toIndex)

总结:常用方法
增:add(Object obj)
删:remove(int index) / remove(Object obj)
改:set(int index, Object ele)
查:get(int index)
插:add(int index, Object ele)
长度:size()
遍历:① Iterator迭代器方式 ② 增强for循环 ③ 普通的循环

import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

public class ListTest {
	// add/addAll/size/get
	@Test
    public void test(){
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom",12));
        list.add(456);

        System.out.println(list);

        //void add(int index, Object ele):在index位置插入ele元素
        list.add(1,"BB");
        System.out.println(list);

        //boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
        List list1 = Arrays.asList(1, 2, 3);
        list.addAll(list1);
//        list.add(list1);
        System.out.println(list.size());//9

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

    }
	// indexOf/ lastIndexOf/ remove/ set/ subList
    @Test
    public void tets2(){
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom",12));
        list.add(456);
        //int indexOf(Object obj):返回obj在集合中首次出现的位置。如果不存在,返回-1.
        int index = list.indexOf(4567);
        System.out.println(index);

        //int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置。如果不存在,返回-1.
        System.out.println(list.lastIndexOf(456));

        //Object remove(int index):移除指定index位置的元素,并返回此元素
        Object obj = list.remove(0);
        System.out.println(obj);
        System.out.println(list);

        //Object set(int index, Object ele):设置指定index位置的元素为ele
        list.set(1,"CC");
        System.out.println(list);

        //List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的左闭右开区间的子集合
        List subList = list.subList(2, 4);
        System.out.println(subList);
        System.out.println(list);
    }

	// 迭代
    @Test
    public void test3(){
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");

        //方式一:Iterator迭代器方式
        Iterator iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }

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

        //方式二:增强for循环
        for(Object obj : list){
            System.out.println(obj);
        }

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

        //方式三:普通for循环
        for(int i = 0;i < list.size();i++){
            System.out.println(list.get(i));
        }
    }
}

面试题:请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?

  1. ArrayList和LinkedList都是线程不安全的,Vector线程安全,所以Vector的性能相对较差。
  2. ArrayList是使用动态数组的方式来存放数据,而LinkedList是使用链表的方式,通过地址的指向来存放下一个数据。
    因此,在随机访问的get/set中,ArrayList的性能高于LinkedList,因为LinkedList需要指针的位移,而ArrayList可以直接返回数组对应位置的值;对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。

面试题:ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?

  1. ArrayList的底层是动态的数组,默认扩容是1.5倍原长度;
  2. Vector默认扩容是2倍原长度,除此之外,ArrayList和Vector在底层几乎相同,唯一的区别是Vector的所有公开方法是同步方法(synchronized),属于强同步类,因此性能会弱于ArrayList、在平时开发中习惯使用ArrayList,因为线程安全可以自己定义。

面试题:区分List中remove(int index)和remove(Object obj)

  1. remove(int index):通过查找下标的方式来删除数据,返回删除值
  2. remove(Object obj):通过比较元素的方式来删除数据,返回布尔值
    源码如下
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                	// fastRemove和下面用索引删除的过程一样
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
    public static void main(String[] args) {

        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        updateList(list);
        System.out.println(list);
    }
    private static void updateList(List list) {
    	// 删除下标2的元素并返回
        System.out.println(list.remove(2));
        // 删除对象为2的元素
//        list.remove(new Integer(2));
    }

2.4 Collection子接口之二:Set接口

Set接口没有提供额外的方法

  1. 不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
  2. 判断两个对象是否相同不是使用==运算符,而是根据equals()方法

Set接口实现类对比

 |----Collection接口:单列集合,用来存储一个一个的对象
      |----Set接口:存储无序的、不可重复的数据   (类似于高中讲的“集合”)
           |----HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
                |----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
                                   对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
                |----TreeSet:可以按照添加对象的指定属性,进行排序。

2.4.1 Set的无序性与不可重复性的理解

  1. 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的
  2. 不可重复性:保证添加的元素按照equals()判断时,不能返回true.即:相同的元素只能添加一个。

import org.junit.Test;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

class User {
    private String name;
    private int age;

    public User() {
    }

    public User(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 "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        User user = (User) o;

        if (age != user.age) {
            return false;
        }
        return name != null ? name.equals(user.name) : user.name == null;
    }

    @Override
    public int hashCode() { 
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}

public class SetTest {

    @Test
    public void test(){
        Set set = new HashSet();
        set.add(123);
        set.add(456);
        set.add("fgd");
        set.add("book");
        set.add(new User("Tom",12));
        set.add(new User("Tom",12));
        set.add(129);

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

输出结果,可以看到和添加的顺序不一致

User equals()....
129
fgd
book
456
123
User{name='Tom', age=12}

2.4.2 HashSet的使用

HashSetSet 接口的典型实现,大多数时候使用Set 集合时都使用这个实现类。
HashSetHash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。

HashSet具有以下特点:

  • 不能保证元素的排列顺序
  • HashSet不是线程安全的
  • 集合元素可以是null
    底层也是数组初始容量为16,当如果使用率超过0.75,(16*0.75=12)就会扩大容量为原来的2倍。(16扩容为32,依次为64,128…等)
  • HashSet 集合判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的equals()方法返回值为true。
  • 对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

HashSet中元素的添加过程
首先我们可以通过源码看到,HashSet底层:数组+链表的结构。

  1. 我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,
  2. 此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),
  3. 判断数组此位置上是否已经有元素:
    3.1 如果此位置上没有其他元素,则元素a添加成功。 —>情况1
    3.2 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
    • 如果hash值不相同,则元素a添加成功。—>情况2
    • 如果hash值相同,进而需要调用元素a所在类的equals()方法:
      ① equals()返回true,元素a添加失败;
      ② equals()返回false,则元素a添加成功。—>情况3

对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
并且需要注意:(七上八下)
JDK7:元素a放到数组中,指向原来的元素。(头插法)
JDK8:原来的元素在数组中,指向元素a。(尾插法)
在这里插入图片描述

2.4.3 关于hashCode()和equals()的重写

重写hashCode() 方法的基本原则

  1. 在程序运行时,同一个对象多次调用hashCode()方法应该返回相同的值。
  2. 当两个对象的equals()方法比较返回true时,这两个对象的hashCode()方法的返回值也应相等。
  3. 对象中用作equals() 方法比较的属性(Field),都应该用来计算hashCode值。

重写equals() 方法的基本原则

  1. 当一个类有自己特有的“逻辑相等”概念,需要改写equals()
  2. 修改equals()方法的时候总是要改写hashCode()。比如:根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,它们是两个不同对象。因此,违反了“相等的对象必须具有相等的散列码”。

结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

知识拓展:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?

  1. 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
    并且31只占用5bits,相乘造成数据溢出的概率较小。
  2. 31可以由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
  3. 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

可以看一下Objects.hash()方法,底层提供的工具类也是31

面试题:两个对象的hashCode()相同,则equals()也一定为true么?
首先给出答案:肯定是不一定。同时反过来equals()为true,hashCode()也不一定相同。
因为类的hashCode()方法和equals()方法都可以重写,返回的值完全在于自己定义
hashCode()返回该对象的哈希码值;equals()返回两个对象是否相等。
关于hashCode()和equals()方法是有一些常规协定

  1. 两个对象用equals()比较返回true,那么两个对象的hashCode()方法必须返回相同的结果。
  2. 两个对象用equals()比较返回false,不要求hashCode()方法也一定返回不同的值,但是最好返回不同值,以提高哈希表性能。
  3. 重写equals()方法,必须重写hashCode()方法,以保证equals方法相等时两个对象hashcode返回相同的值。

2.4.4 LinkedHashSet的使用

LinkedHashSet根据元素的hashCode值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
LinkedHashSet插入性能略低于HashSet,但在迭代访问Set 里的全部元素时有很好的性能。
LinkedHashSet不允许集合元素重复。
在这里插入图片描述
在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。
优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet

示例


import org.junit.Test;

import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

class User {
    private String name;
    private int age;

    public User() {
    }

    public User(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 "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }

    @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}


public class SetTest {

    @Test
    public void test2(){
        Set set = new LinkedHashSet();
        set.add(456);
        set.add(123);
        set.add(123);
        set.add("AA");
        set.add("CC");
        set.add(new User("Tom",12));
        set.add(new User("Tom",12));
        set.add(129);

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

2.4.5 TreeSet的使用

TreeSetSortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。注意,TreeSet用的是compareTo来判断两个元素是否相等
TreeSet底层使用红黑树结构存储数据

新增的方法如下:(了解)

  • Comparator comparator()
  • Object first()
  • Object last()
  • Object lower(Object e)
  • Object higher(Object e)
  • SortedSet subSet(fromElement, toElement)
  • SortedSet headSet(toElement)
  • SortedSet tailSet(fromElement)

TreeSet两种排序方法:自然排序定制排序。默认情况下,TreeSet采用自然排序。
特点:有序,查询速度比List快
下面讲解自然排序

自然排序
TreeSet会调用集合元素compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。
如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable 接口
实现Comparable 的类必须实现compareTo(Object obj)方法,两个对象即通过compareTo(Object obj)方法的返回值来比较大小。

Comparable 的典型实现:

  1. BigDecimal、BigInteger以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
  2. Character:按字符的unicode值来进行比较
  3. Boolean:true 对应的包装类实例大于false 对应的包装类实例
  4. String:按字符串中字符的unicode 值进行比较
  5. Date、Time:后边的时间、日期比前面的时间、日期大

向TreeSet中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
因为只有相同类的两个实例才会比较大小,所以向TreeSet中添加的应该是同一个类的对象

对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较返回值
当需要把一个对象放入TreeSet中,重写该对象对应的equals()方法时,应保证该方法与compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals()方法比较返回true,则通过compareTo(Object obj)方法比较应返回0。否则,让人难以理解。

这里就不对红黑树进行讲解了,具体等到数据结构那部分会更新的

示例


import org.junit.Test;

import java.util.Iterator;
import java.util.TreeSet;
class User implements Comparable {
    private String name;
    private int age;

    public User() {
    }

    public User(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 "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        User user = (User) o;

        if (age != user.age) {
            return false;
        }
        return name != null ? name.equals(user.name) : user.name == null;
    }

    @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

    //按照姓名从大到小排列,年龄从小到大排列
    @Override
    public int compareTo(Object o) {
        if (o instanceof User) {
            User user = (User) o;
//            return this.name.compareTo(user.name);  //按照姓名从小到大排列
//            return -this.name.compareTo(user.name);  //按照姓名从大到小排列
            int compare = -this.name.compareTo(user.name);  //按照姓名从大到小排列
            if (compare != 0) {   
                return compare;
            } else {
                //年龄从小到大排列
                return Integer.compare(this.age, user.age);
            }
        } else {
            throw new RuntimeException("输入的类型不匹配");
        }
    }
}

public class TreeSetTest {

    @Test
    public void test() {
        TreeSet set = new TreeSet();

        //失败:不能添加不同类的对象
//        set.add(123);
//        set.add(456);
//        set.add("AA");
//        set.add(new User("Tom",12));

        //举例一:
//        set.add(34);
//        set.add(-34);
//        set.add(43);
//        set.add(11);
//        set.add(8);

        //举例二:
        set.add(new User("Tom",12));
        set.add(new User("Jerry",32));
        set.add(new User("Jim",2));
        set.add(new User("Mike",65));
        set.add(new User("Jack",33));
        set.add(new User("Jack",56));

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


下面讲解定制排序

定制排序:
TreeSet的自然排序要求元素所属的类实现Comparable接口。
如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。
定制排序通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。利用int compare(T o1,T o2)方法,比较o1和o2的大小:

  • 如果方法返回正整数,则表示o1大于o2;
    如果返回0,表示相等;
    返回负整数,表示o1小于o2。

要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0

示例

import org.junit.Test;

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

public class TreeSetTest {

    @Test
    public void tets2(){
        Comparator com = new Comparator() {
            //按照年龄从小到大排列
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof User && o2 instanceof User){
                    User u1 = (User)o1;
                    User u2 = (User)o2;
                    return Integer.compare(u1.getAge(),u2.getAge());
                }else{
                    throw new RuntimeException("输入的数据类型不匹配");
                }
            }
        };

        TreeSet set = new TreeSet(com);
        set.add(new User("Tom",12));
        set.add(new User("Jerry",32));
        set.add(new User("Jim",2));
        set.add(new User("Mike",65));
        set.add(new User("Mary",33));
        set.add(new User("Jack",33));
        set.add(new User("Jack",56));


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

TreeSet练习题
在这里插入图片描述

import org.junit.Test;

import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
/**
 * MyDate类包含:
 * private成员变量year,month,day;并为每一个属性定义getter,  setter 方法;
 */
class MyDate implements Comparable {
    private int year;
    private int month;
    private int day;

    public MyDate() {
    }

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

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }

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

    @Override
    public int compareTo(Object o) {
        if (o instanceof MyDate) {
            MyDate m = (MyDate) o;

            //比较年
            int minusYear = this.getYear() - m.getYear();
            if (minusYear != 0) {
                return minusYear;
            }
            //比较月
            int minusMonth = this.getMonth() - m.getMonth();
            if (minusMonth != 0) {
                return minusMonth;
            }
            //比较日
            return this.getDay() - m.getDay();
        }

        throw new RuntimeException("传入的数据类型不一致!");

    }
}

/**
 * 定义一个Employee类。
 * 该类包含:private成员变量name,age,birthday,
 * 其中birthday 为MyDate 类的对象;
 * 并为每一个属性定义getter, setter 方法;
 * 并重写toString 方法输出name, age, birthday
 */
class Employee implements Comparable {
    private String name;
    private int age;
    private MyDate birthday;

    public Employee() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public MyDate getBirthday() {
        return birthday;
    }

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

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

    //按name排序
    @Override
    public int compareTo(Object o) {
        if (o instanceof Employee) {
            Employee e = (Employee) o;
            return this.name.compareTo(e.name);
        }
//        return 0;
        throw new RuntimeException("传入的数据类型不一致");
    }
}
public class EmployeeTest {
 //问题一:使用自然排序
    @Test
    public void test(){
        TreeSet set = new TreeSet();

        Employee e1 = new Employee("wangxianzhi",41,new MyDate(334,5,4));
        Employee e2 = new Employee("simaqian",43,new MyDate(-145,7,12));
        Employee e3 = new Employee("yanzhenqin",44,new MyDate(709,5,9));
        Employee e4 = new Employee("zhangqian",51,new MyDate(-179,8,12));
        Employee e5 = new Employee("quyuan",21,new MyDate(-340,12,4));

        set.add(e1);
        set.add(e2);
        set.add(e3);
        set.add(e4);
        set.add(e5);

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

    //问题二:生日日期的先后排序
    @Test
    public void test2(){
        TreeSet set = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof Employee && o2 instanceof Employee){
                    Employee e1 = (Employee)o1;
                    Employee e2 = (Employee)o2;

                    MyDate b1 = e1.getBirthday();
                    MyDate b2 = e2.getBirthday();

                    //方式一:
//                    //比较年
//                    int minusYear = b1.getYear() - b2.getYear();
//                    if(minusYear != 0){
//                        return minusYear;
//                    }
//
//                    //比较月
//                    int minusMonth = b1.getMonth() - b2.getMonth();
//                    if(minusMonth != 0){
//                        return minusMonth;
//                    }
//
//                    //比较日
//                    return b1.getDay() - b2.getDay();

                    //方式二:
                    return b1.compareTo(b2);
                }
//                return 0;
                throw new RuntimeException("传入的数据类型不一致!");
            }
        });

        Employee e1 = new Employee("wangxianzhi",41,new MyDate(334,5,4));
        Employee e2 = new Employee("simaqian",43,new MyDate(-145,7,12));
        Employee e3 = new Employee("yanzhenqin",44,new MyDate(709,5,9));
        Employee e4 = new Employee("zhangqian",51,new MyDate(-179,8,12));
        Employee e5 = new Employee("quyuan",21,new MyDate(-340,12,4));

        set.add(e1);
        set.add(e2);
        set.add(e3);
        set.add(e4);
        set.add(e5);

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

Set的练习题

import org.junit.Test;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

public class CollectionTest {

    @Test
    public void test3(){
        HashSet set = new HashSet();
        Person2 p1 = new Person2(1001,"AA");
        Person2 p2 = new Person2(1002,"BB");

        set.add(p1); // hashcode=33111
        set.add(p2); // hashcode=33174
        System.out.println(set); // [Person{id=1002, name='BB'}, Person{id=1001, name='AA'}]
        p1.name = "CC"; // hashcode=33175
        set.remove(p1); // 因为p1存放的时候hashcode=33111,修改了值之后通过新的hashcode=33175去查找会找不到,所以删除失败
        System.out.println(set); // [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
        set.add(new Person2(1001,"CC")); // 新增的时候hashcode=33175会发现位置上没数据,直接插入
        System.out.println(set);
        set.add(new Person2(1001,"AA")); // 新增的时候虽然hashcode=33111上面有值,然是通过equals比较的时候不相同,所以也插入了
        System.out.println(set); // [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]
    }
}

3. Map接口

回顾一下,Map接口继承树
在这里插入图片描述

3.1 Map接口及其多个实现类的对比

|----Map:双列数据,存储key-value对的数据   (类似于高中的函数:y = f(x))
     |----HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
          |----LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
                        原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
                        对于频繁的遍历操作,此类执行效率高于HashMap。
     |----TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
                  底层使用红黑树
     |----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
          |----Properties:常用来处理配置文件。key和value都是String类型

3.2 Map中存储的key-value的特点

在这里插入图片描述

MapCollection并列存在。用于保存具有映射关系的数据:key-value(底层存放用的是Entry<K,V>

  1. Map中的keyvalue都可以是任何引用类型的数据
  2. Map 中的key 可以理解为相当于是Set来存放(底层是通过比较元素hash值和equals的方式来存放的),不允许重复,即同一个Map 对象所对应的类,须重写hashCode()equals()方法
    常用String类作为Map的“键”
  3. key和value之间存在单向一对一关系,即通过指定的key总能找到唯一的、确定的value
  4. Map接口的常用实现类:HashMapTreeMapLinkedHashMapProperties。其中,HashMap是Map接口使用频率最高的实现类

3.3 Map实现类之一:HashMap

基本介绍

  1. HashMap是Map接口使用频率最高的实现类。
  2. 允许使用null键和null,与HashSet一样,不保证映射的顺序
  3. 所有的key构成的集合可以看成是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
  4. 所有的value构成的集合可以看成是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
  5. 一个key-value构成一个entry,所有的entry构成的集合可以看成是Set:无序的、不可重复的(JDK1.7和JDK 1.8的存放结构不一样,读者注意自己的JDK版本,这里不用纠结
  6. HashMap判断两个key相等的标准是:两个key通过equals()方法返回true,hashCode值也相等。
  7. HashMap判断两个value相等的标准是:两个value 通过equals()方法返回true。

3.3.1 HashMap的底层实现原理(重要)

前置小知识:HashMap源码中的重要常量

DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
MAXIMUM_CAPACITY:HashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
table:存放元素的数组,大小总为2的幂次方
entrySet:存储数据的集(用于迭代)
size:HashMap中存储的键值对数量
modCount:HashMap扩容和结构改变的次数
loadFactor:填充因子

JDK8中添加的的常量
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64(这个值至少是TREEIFY_THRESHOLD的四倍,以避免在调整大小和树形化之间产生冲突)
UNTREEIFY_THRESHOLD:桶中红黑树长度小于该默认值,转化为链表:6

一、JDK 7及以前版本HashMap是数组+链表结构(即为链地址法)

在这里插入图片描述

  1. HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为DEFAULT_INITIAL_CAPACITY的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
    :如果JDK版本为1.8的话是Node,这个下面再说
  2. 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素。因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。(头插法,如上图所示)

添加元素的过程:

  1. 首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
  2. 如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
  3. 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),那么就比较key1和已经存在的一个或多个数据的哈希值:
    3.1 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
    3.2 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续调用key1所在类的equals(key2)方法进行比较:
    • 如果equals()返回false:此时key1-value1添加成功。----情况3
    • 如果equals()返回true:使用value1替换value2。

补充:

  1. 关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。
  2. 在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

HashMap的扩容:当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize()方法。
问题那么HashMap什么时候进行扩容呢?

  1. 当HashMap中的元素个数超过threshold(扩容的临界值 = 数组容量*loadFactor)时,就 会 进 行 数 组 扩 容, loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。
  2. 也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为216=32,即扩大一倍。
  3. 然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

二、JDK 8版本发布以后HashMap是数组+链表+红黑树实现。
在这里插入图片描述

  1. HashMap的内部存储结构其实是数组+链表+红黑树的结合。当实例化一个HashMap时,会初始化loadFactor,在put第一对映射关系时,系统会创建一个长度为DEFAULT_INITIAL_CAPACITY(默认值为16)的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素
  2. 每个bucket中存储一个元素(Node对象),但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链,或者是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点leftright,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。(尾插法

添加元素的过程
思路和JDK7一样

问题HashMap什么时候进行扩容和树形化?
扩容的思路和JDK1.7相同
但是,当HashMap中的其中一个链的对象个数如果达到了TREEIFY_THRESHOLD(8)个,此时如果capacity没有达到MIN_TREEIFY_CAPACITY(64),那么HashMap会先扩容解决如果已经达到了64,那么这个链会变成红黑树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于UNTREEIFY_THRESHOLD(6)个,也会把红黑树再转为链表。

问题关于映射关系的key是否可以修改?
不能,映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。
这点和之前Set的练习题一样,会导致重复添加

总结
jdk8 相较于jdk7在底层实现方面的不同:

  1. new HashMap():底层没有创建一个长度为16的数组
  2. jdk 8底层的数组是:Node[],而非Entry[]
  3. 首次调用put()方法时,底层创建长度为16的数组
  4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
    4.1形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
    4.2当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。

3.3.2 LinkedHashMap的底层实现原理(了解)

LinkedHashMap是HashMap的子类
在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
与LinkedHashSet类似,LinkedHashMap可以维护Map 的迭代顺序:迭代顺序与Key-Value 对的插入顺序一致

HashMap中的内部类:Node
在这里插入图片描述
LinkedHashMap中的内部类:Entry
在这里插入图片描述

3.4 Map中的常用方法

添加、删除、修改操作:

方法功能
Object put(Object key,Object value)将指定key-value添加(或修改)到当前map对象中
void putAll(Map m)将m中的所有key-value对存放到当前map中
Object remove(Object key)移除指定key的key-value对,并返回被移除的value
void clear()清空当前map中的所有数据
import org.junit.Test;

import java.util.*;

public class MapTest {

    @Test
    public void test3(){
        Map map = new HashMap();
        //添加
        map.put("AA",123);
        map.put(45,123);
        map.put("BB",56);
        //修改
        map.put("AA",87);

        System.out.println(map);

        Map map1 = new HashMap();
        map1.put("CC",123);
        map1.put("DD",456);
        map.putAll(map1);

        System.out.println(map);

        //remove(Object key)
        Object value = map.remove("CC");
        System.out.println(value); // 123
        System.out.println(map);

        //clear(),只是置空,不等于map = null
        map.clear();
        System.out.println(map.size());
        System.out.println(map);
    }
}

元素查询的操作:

方法功能
Object get(Object key)获取指定key对应的value
boolean containsKey(Object key)是否包含指定的key
boolean containsValue(Object value)是否包含指定的value
int size()返回map中key-value对的个数
boolean isEmpty()判断当前map是否为空
boolean equals(Object obj)判断当前map和参数对象obj是否相等
import org.junit.Test;

import java.util.*;

public class MapTest {

	@Test
    public void test4(){
        Map map = new HashMap();
        map.put("AA",123);
        map.put(45,123);
        map.put("BB",56);
        // Object get(Object key)
        System.out.println(map.get(45));
        //containsKey(Object key)
        boolean isExist = map.containsKey("BB");
        System.out.println(isExist);

        isExist = map.containsValue(123);
        System.out.println(isExist);

        map.clear();

        System.out.println(map.isEmpty());
    }
}

元视图操作的方法:

方法功能
Set keySet()返回所有key构成的Set集合
Collection values()返回所有value构成的Collection集合
Set entrySet()返回所有key-value对构成的Set集合
import org.junit.Test;

import java.util.*;

public class MapTest {

    @Test
    public void test5(){
        Map map = new HashMap();
        map.put("AA",123);
        map.put(45,1234);
        map.put("BB",56);

        //遍历所有的key集:keySet()
        Set set = map.keySet();
        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
        System.out.println("*****************");

        //遍历所有的values集:values()
        Collection values = map.values();
        for(Object obj : values){
            System.out.println(obj);
        }
        System.out.println("***************");
        //遍历所有的key-values
        //方式一:
        Set entrySet = map.entrySet();
        Iterator iterator1 = entrySet.iterator();
        while (iterator1.hasNext()){
            Object obj = iterator1.next();
            //entrySet集合中的元素都是entry
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "---->" + entry.getValue());

        }
        System.out.println("/");

        //方式二:
        Set keySet = map.keySet();
        Iterator iterator2 = keySet.iterator();
        while(iterator2.hasNext()){
            Object key = iterator2.next();
            Object value = map.get(key);
            System.out.println(key + "=====" + value);
        }
        System.out.println("/");
        
        // 方式三:
        for (Object obj  : map.entrySet()) {
            final Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "---->" + entry.getValue());
        }
    }
}

总结常用方法:

  1. 添加:put(Object key,Object value)
  2. 删除:remove(Object key)
  3. 修改:put(Object key,Object value)
  4. 查询:get(Object key)
  5. 长度:size()
  6. 遍历:keySet() / values() / entrySet()

3.5 TreeMap两种添加方式的使用

TreeMap存储Key-Value 对时,需要根据key-value对进行排序。TreeMap可以保证所有的Key-Value 对处于有序状态
TreeSet底层使用红黑树结构存储数据
TreeMap的Key的排序:

  1. 自然排序:TreeMap的所有的Key 必须实现Comparable接口,而且所有的Key应该是同一个类的对象,否则将会抛出ClasssCastException
  2. 定制排序:创建TreeMap时,传入一个Comparator 对象,该对象负责对TreeMap中的所有key 进行排序。此时不需要Map 的Key实现Comparable 接口

TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。


import org.junit.Test;

import java.util.*;
class User implements Comparable{
    private String name;
    private int age;

    public User() {
    }

    public User(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 "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }

    @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

    //按照姓名从大到小排列,年龄从小到大排列
    @Override
    public int compareTo(Object o) {
        if(o instanceof User){
            User user = (User)o;
//            return -this.name.compareTo(user.name);
            int compare = -this.name.compareTo(user.name);
            if(compare != 0){
                return compare;
            }else{
                return Integer.compare(this.age,user.age);
            }
        }else{
            throw new RuntimeException("输入的类型不匹配");
        }

    }
}

public class TreeMapTest {

    //自然排序
    @Test
    public void test(){
        TreeMap map = new TreeMap();
        User u1 = new User("Tom",23);
        User u2 = new User("Jerry",32);
        User u3 = new User("Jack",20);
        User u4 = new User("Rose",18);

        map.put(u1,98);
        map.put(u2,89);
        map.put(u3,76);
        map.put(u4,100);

        Set entrySet = map.entrySet();
        Iterator iterator1 = entrySet.iterator();
        while (iterator1.hasNext()){
            Object obj = iterator1.next();
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "---->" + entry.getValue());

        }
    }

    //定制排序
    @Test
    public void test2(){
        TreeMap map = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof User && o2 instanceof User){
                    User u1 = (User)o1;
                    User u2 = (User)o2;
                    return Integer.compare(u1.getAge(),u2.getAge());
                }
                throw new RuntimeException("输入的类型不匹配!");
            }
        });
        User u1 = new User("Tom",23);
        User u2 = new User("Jerry",32);
        User u3 = new User("Jack",20);
        User u4 = new User("Rose",18);

        map.put(u1,98);
        map.put(u2,89);
        map.put(u3,76);
        map.put(u4,100);

        Set entrySet = map.entrySet();
        Iterator iterator1 = entrySet.iterator();
        while (iterator1.hasNext()){
            Object obj = iterator1.next();
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "---->" + entry.getValue());

        }
    }
}

3.5 Hashtable

Hashtable是个古老的Map 实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的
Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
与HashMap不同,Hashtable不允许使用null 作为key和value
与HashMap一样,Hashtable也不能保证其中Key-Value 对的顺序
Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。

面试题:HashMapHashtable的异同?

  1. HashMapHashtable都实现了Map接口。由于HashMap的非线程安全性,效率上可能高于Hashtable。Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。
  2. HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
  3. HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。
  4. Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。
  5. Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。

3.6 Properties处理属性文件

Properties类是Hashtable的子类,该对象用于处理属性文件
由于属性文件里的key、value都是字符串类型,所以Properties 里的key和value都是字符串类型
存取数据时,建议使用setProperty(String key,Stringvalue)方法和getProperty(String key)方法
现在可以只做了解,但是WEB开发的时候会经常接触

使用流程:

  1. 新建application.properties文件
    在这里插入图片描述
  2. 在里面编写键值对
server.port=8080
age=18
name=zhangsan
  1. 读取配置
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class PropertiesTest {
    private static final String PRE_FIX = "server";

    //Properties:常用来处理配置文件。key和value都是String类型
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            // 1. 加载流对应文件
            Properties properties = new Properties();
            fis = new FileInputStream("application.properties");
            properties.load(fis);

            String name = properties.getProperty("name");
            String password = properties.getProperty("age");

            System.out.println("name = " + name + ",password = " + password);


            
            // 下面简单实现一下Spring读取文件的流程
            // 2. 获取properties中所有的key
            final Map<String, String> propertiesMap = new HashMap<>();
            final Set<String> keys = properties.stringPropertyNames();
            for (String key : keys) {
                // 去掉前缀
                if (key.startsWith(PRE_FIX)) {
                    //server.port ==> port
                    String realKey = key.substring(key.indexOf(PRE_FIX) + PRE_FIX.length() + 1);
                    String value = properties.getProperty(key);
                    propertiesMap.put(realKey, value);
                }
            }

            // 将对应的值,使用反射技术,注入到这个bean中
            final Server server = new Server();
            if (propertiesMap.size() > 0) {
                propertiesMap.forEach((key, value) -> {
                    final Class serverClass = server.getClass();
                    // 字段校验
                    final Field[] declaredFields = serverClass.getDeclaredFields();
                    for (Field declaredField : declaredFields) {
                        // 匹配上就塞入
                        if (declaredField.getName().equals(key)) {
                            // 因为是私有的所以加上
                            try {
                                declaredField.setAccessible(true);
                                // String和Integer
                                if (declaredField.getType().equals(String.class)) {
                                    declaredField.set(server, value);
                                } else {
                                    final Integer port = Integer.parseInt(value);
                                    declaredField.set(server, port);
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
            }
            // 输出对应信息
            System.out.println("Server.port = " + server.port);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class Server {
        private Integer port;

        public Integer getPort() {
            return port;
        }

        public void setPort(Integer port) {
            this.port = port;
        }
    }
}

遇上中文乱码的话IDEA进入Settings-Editor-File Encodings 把格式改成UTF-8

4. Collections工具类

Collections 是一个操作SetListMap 等集合的工具类
Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
顺带一提操作数组的工具类:Arrays

方法作用
reverse(List)反转List 中元素的顺序
shuffle(List)对List集合元素进行随机排序
sort(List)根据元素的自然顺序对指定List 集合元素按升序排序
sort(List,Comparator)根据指定的Comparator 产生的顺序对List 集合元素进行排序
swap(List,int,int)将指定list 集合中的i处元素和j 处元素进行交换
Object max(Collection)根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator)根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)根据元素的自然顺序,返回给定集合中的最小元素
Object min(Collection,Comparator)根据 Comparator 指定的顺序,返回给定集合中的最小元素
int frequency(Collection,Object)返回指定集合中指定元素的出现次数
void copy(List dest,List src)将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal)使用新值替换 List 对象的所有旧值
import org.junit.Test;

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

public class CollectionTest {
    @Test
    public void test(){
        List list = new ArrayList();
        list.add(123);
        list.add(43);
        list.add(765);
        list.add(765);
        list.add(765);
        list.add(-97);
        list.add(0);

        System.out.println(list);

//        Collections.reverse(list);
//        Collections.shuffle(list);
//        Collections.sort(list);
//        Collections.swap(list,1,2);
        int frequency = Collections.frequency(list, 123);

        System.out.println(list);
        System.out.println(frequency);
    }

    @Test
    public void test2(){
        List list = new ArrayList();
        list.add(123);
        list.add(43);
        list.add(765);
        list.add(-97);
        list.add(0);

        //报异常:IndexOutOfBoundsException("Source does not fit in dest"),因为dest的长度为0
//        List dest = new ArrayList();
//        Collections.copy(dest,list);
        //正确的:
        List dest = Arrays.asList(new Object[list.size()]);
        System.out.println(dest.size());//list.size();
        Collections.copy(dest,list);

        System.out.println(dest);

        /**
         * Collections 类中提供了多个 synchronizedXxx() 方法,
         * 该方法可使将指定集合包装成线程同步的集合,从而可以解决
         * 多线程并发访问集合时的线程安全问题
         */
        //返回的list1即为线程安全的List
        List list1 = Collections.synchronizedList(list);
    }
}

面试题:Collection 和 Collections的区别?

  1. Collection是集合类的上级接口,继承于他的接口主要有Set 和List.
  2. Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作.

补充知识:Enumeration
Enumeration 接口是Iterator迭代器的“古老版本”
在这里插入图片描述

        Enumeration stringEnum = new StringTokenizer("a-b*c-d-e-g", "-");
        while (stringEnum.hasMoreElements()) {
            Object obj = stringEnum.nextElement();
            System.out.println(obj);
        }
上一篇总目录下一篇
十二、枚举与注解Java基础总目录十四、泛型
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值