【Java】集合

【Java】集合(Collection)

1 概述

1.1 集合与数组的对比

集合是能够容纳其他对象的对象,容纳的对象称为集合的元素,例如数组就是一种最基本的集合对象。集合内的元素与元素之间具有一定的数据结构,并提供了一些有用的算法,从而为程序组织和操纵批量数据提供强有力的支持。但是,使用 Array 存储对象方面具有一些弊端,而 Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中。

注意:集合、数组都是对多个数据进行存储操作的结构,简称 Java 容器。此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储。

数组在内存存储方面的特点:

  • 数组初始化以后,其长度就确定了。

  • 数组声明的类型,就决定了进行元素初始化的类型。我们就只能操作指定类型的数据了。

    ​ 比如: String[] arr; int[] arr1; Object[] arr2。

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

  • 数组初始化以后,其长度就不可修改,不便于扩展。
  • 数组中提供的方法非常有限,对于添加、删除、插入等数据操作非常不便,且效率不高。同时无法直接获取存储元素的个数。
  • 数组存储数据是有序的、可重复的。对于无序、不可重复的需求,不能满足。

Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。

1.2 集合框架概述

Java 的集合工具类都定义在 java.util 包中,该程序包及其子程序包为 Java 程序提供了一系列有用的工具。Java API 将集合分为两种,一种称为集合 (Collection) 类,用接口 Collection 描述其操作,其中存放的基本单位是单个对象,以列表 (List) 和集合 (Set) 为代表;另一种称为映射 (Map) ,用接口 Map 描述其操作,其中存放的基本单位是对象对 (object pairs) ,其中一个对象称为键 (key) 对象,另一个对象称为值 (value) 对象。

  • Collection 接口:单列数据,定义了存取一组对象的方法的集合。

    • List:元素有序、可重复的集合。 → ” 动态 “ 数组

      • ArrayList, LinkedList, Vector
    • Set:元素无序、不可重复的集合。 → 数学中的 ” 集合 “

      • HashSet, LinkedHashSet, TreeSet
  • Map 接口:双列数据,保存具有映射关系 “key - value 对” 的集合。 → 数学中的函数:y = f (x)

    • HashMap, LinkedHashMap, Treemap, Hashtable, Properties

Java Collection API 的核心接口和常用集合类如下图所示:

【Java】集合_第1张图片

2 Collection 接口与 Iterator 迭代器接口

2.1 Collection 接口

Collection 接口是 List 接口和 Set 接口的父接口,通常情况下不直接使用。Collection 接口中定义了一些通用方法,通过它们可以实现对集合的添加、删除等基本操作。List 接口和 Set 接口实现了 Collection 接口,所以这些方法对 List 和 Set 集合是通用的。如下所示为 Collection 接口定义的常用方法。

  • add(Object e):将元素 e 添加到当前集合中。
  • addAll(Collection coll1):将 coll1 集合中的元素添加到当前的集合中。
  • remove(Object obj):从当前集合中移除 obj 元素,返回值为 boolean 型。
  • removeAll(Collection coll1):从当前集合中移除 coll1 中的所有元素。(差集)
  • retainAll(Collection coll1):仅保留当前集合中包含在 coll1 中的对象,其他的全部移除。(交集)
  • contains(Object obj):判断当前集合中是否包含 obj 。
  • containsAll(Collection coll1):判断形参 coll1 中的所有元素是否都存在于当前集合中。
  • isEmpty():判断当前集合是否为空。
  • size():获取集合中存放元素的个数。
  • clear():清空集合所有元素。
  • iterator():返回 Iterator 接口的实例,用于遍历集合元素。放在 IteratorTest.java 中测试。
  • equals(Object obj):判断当前集合和指定集合是否为同一个集合,返回值为 boolean 型。
  • hashCode():返回当前对象的哈希值。
  • toArray:获得一个包含所有对象的 Object 型数组。 集合 --> 数组
  • 扩展:数组 --> 集合: 调用 Arrays 类的静态方法 asList() 。

2.2 Iterator 迭代器接口

Java Collection API 为集合对象提供了 Iterator (迭代器) 接口,用来实现遍历集合中的元素

GOF (Gang of Four) 给迭代器模式的定义为:提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于 “ 公交车上的售票员 ” 。

Collection 接口继承了 java.lang.Iterable 接口,该接口有一个 iterator() 方法,那么所有实现了Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象。

Iterator 仅用于遍历集合,lterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。

lterator 接口包含三个方法:

public interface Iterator {
	Object next();
	boolean hasNext(); 
	void remove();
}

next() 和 hasNext():

通过反复调用 next() 方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next() 方法将抛出一个 NoSuchElementException 。因此,需要在调用 next() 之前调用 hasNext() 方法。如果迭代器对象还有多个供访问的元素,这个方法就返回 true 。如果想要查看集合中的所有元素,就请求一个迭代器,并在 hasNext() 返回 true 时反复地调用 next() 方法。

Collection coll = new ArrayList();
Iterator iterator = coll.iterator();

// hasNext():判断是否还有下一个元素
while(iterator.hasNext()) {
	// next():指针下移,将下移后的集合位置上的元素返回
	System.out.println(iterator.next());
}

错误方式一:

public static void main(String[] args) {
	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();
    while (iterator.next() != null) {
        System.out.println(iterator.next());
    }
}

运行结果:

456
Tom

java.util.NoSuchElementException

错误方式二:

public static void main(String[] args) {	
	Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new Person("Jerry", 20));
    coll.add(new String("Tom"));
    coll.add(false);

	while (coll.iterator().hasNext()) {
    System.out.println(coll.iterator().next());
	}
}

运行结果:

123
123
123
123
...		// 123 死循环

集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

remove():

Iterator 接口的 remove() 方法,可以在遍历的时候,删除上次调用 next() 方法时返回的元素。此方法不同于集合直接调用 remove(),是遍历过程中通过迭代器对象的 remove() 方法。

public static void main(String[] args) {
	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());
	}
}

运行结果:

123
456
Person{name='Jerry', age=20}
false

注意:

如果还未调用 next() 或在上一次调用 next() 方法后已经调用了 remove() 方法,在调用 remove() 方法都会报 IllegalStateException。

如果想删除两个相邻的元素,不能直接地这样调用:

iterator.remove();
iterator.remove();	// Error!

必须先调用 next() 越过将要删除的元素。

iterator.remove();
iterator.next();
iterator.remove();	// OK!

使用 “for each” 循环遍历集合元素

Java 5.0 提供了 “for each” 循环迭代访问 Collection 和数组,也称 “增强 for 循环”。“for each” 遍历操作不许获取 Colllection 或数组的长度,无需使用索引访问元素。但 “for each” 遍历集合的底层是调用 Iterator 完成的。

public static void main(String[] args) {
    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);
    }
}

运行结果:

123
456
Person{name='Jerry', age=20}
Tom
false

遍历数组:

public static void main(String[] args) {
	String[] arr = new String[]{"AA", "AA", "AA"};
    // 方式一:普通 for 赋值
    for (int i = 0; i < arr.length; i++) {
        System.out.println(arr[i]);
    }

    // 方式二:增强 for 循环
    for(String s : arr) {
    	System.out.println(s);
    }
}

3 Collection 的子接口:List 接口和 Set 接口

3.1 List 接口

List 接口属于列表类型,且列表的主要特征是以线性方式存储对象,因此 List 容器中的元素有序且可重复,容器中的每个元素都有其对应的顺序索引。List 容器中的每个元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素,所以通常使用 List 替代数组。List 接口继承了 Collection 接口,除继承了 Collection 中声明的方法外,List 接口还增加了一些按位置存取元素、查找、建立 List 视图等新的操作。

在 java.util 包中,提供了实现 List 接口的 ArrayList 类(向量表)、LinkedList 类(双向链表)和 Vector 类(向量)三个工具类。

List:存储有序的、可重复的数据。 → “动态” 数组,替换原有的数组。

  • ArrayList:作为 List 接口的主要实现类,ArrayList 集合数据存储的结构是数组结构。元素增删慢,查找快。由于日常开发中使用最多的功能为查询数据、遍历数据,所以 ArrayList 是最常用的集合,但线程不安全。底层使用 Object[] elementData 存储。
  • LinkedList:数据存储的结构是链结构,内部没有声明数组,而是定义了 Node 作为 LinkedList 中保存数据的基本结构。方便元素添加,删除,但是查询较慢。底层使用双向链表存储。
  • Vector:作为 List 接口的古老实现类。线程安全,效率低。底层使用 Object[] elementData 存储。

List 存储元素的要求:

向 List 中添加的元素,添加的对象所在的类要重写 equals() 方法。

ArrayList 的源码分析:

JDK 7 情况下:

    ArrayList list = new ArrayList();    // 底层创建了长度是 10 的 Object[] 数组 elementData
    list.add(123);    // elementData[0] = new Integer(123);
    ...
    list.add(11);    // 如果此次添加导致底层 elementData 数组容量不够,则扩容。
	// 默认情况下,扩容为原来容量的 1.5 倍,同时需要将原有数组中的数据复制到新的数组中。

	// 结论:建议开发中使用带参的构造器:ArrayList list = new Arraylist(int capacity);

JDK 8 中 ArrayList 的变化:

    ArrayList list = new ArrayList();    // 底层 Object[] elementData 初始化为 {},并没有创建长度是 10 的数组。

    list.add(123);    // 第一次调用 add() 时,底层才创建了长度为 10 的数组,并将数据 123 添加到 elementData,
    ...
    // 后续的添加和扩容操作与 JDK 7 相同。

小结:JDK 7 中的 ArrayList 的对象的创建类似于单例的饿汉式,而 JDK 8 中的 ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

LinkedList 的源码分析:
    LinkedList list = new LinkedList();    // 内部声明了 Node 类型的 first 和 last 属性,默认值为 null。
    list.add(123);    // 将 123 封装到 Node 中,创建了 Node 对象。
    
    // 其中,Node 定义为:体现了 LinkedList 的双向链表的说法
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
        }
    }
Vector 的源码分析:

JDK 7 和 JDK 8 中通过 Vector() 构造器创建对象时,底层都创建了长度为 10 的数组。

在扩容方面,默认扩容为原来的数组长度的 2 倍。

List 接口中常用方法:
  • void add(int index, Object ele):在 index 位置插入 ele 元素。其他元素的索引位置相对后移一位。
  • boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加到指定集合中。
  • Object remove(int index):移除指定 index 位置的元素,并返回此元素。
  • Object set(int index, Object ele):将集合中指定 index 位置的元素修改为 ele。
  • Object get(int index):获取指定 index 位置的元素。
  • int indexOf(Object obj):获取 Obj 在集合中出现的索引位置。当存在多个时,返回第一个索引位置;当不存在时,返回 -1。
  • int lastIndexOf(Object o):获取 Obj 在集合中出现的索引位置。当存在多个时,返回最后一个索引位置;当不存在时,返回 -1。
  • List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的左闭右开的子集合(重新生成一个新的 List 集合)。

注意:

1. list.addAll(list1); 和 list.add(list1);

    ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 12));
        list.add(456);
        
    List list1 = Arrays.asList(1, 2, 3);
    list.addAll(list1);    // 将 list1 的每一个元素添加到 list 中
    System.out.println(list.size());    // 结果 list 的大小为 9
    ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 12));
        list.add(456);
        
    list.add(list1);    // 将 list1 作为整体一个元素添加到 list 中,结果 list 的大小为 7
    System.out.println(list.size());    // 结果 list 的大小为 7        

2. remove(int index) 和 remove(Object obj)

删除索引为 2 的元素:

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        updataList(list);
        System.out.println(list);
    }

    private void updataList(List list) {
        list.remove(2);    // index: 2
    }

运行结果:

[1, 2]

删除数据为 2 的元素:

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        updataList(list);
        System.out.println(list);
    }

    private void updataList(List list) {
        list.remove(new Integer(2));    // element: 2
    }

运行结果:

[1, 3]

总结:

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

3.2 Set 接口

Set 集合是最简单的一种集合,存放于集中的对象不按特定方式排序,只是简单地把对象加入集合中,类似于向口袋里放东西。Set 集合包括 Set 接口以及 Set 接口的所有实现类。因为 Set 接口实现了 Collection 接口,所以 Set 接口拥有 Collection 接口提供的所有方法,它自身没有提供额外的方法。

JDK 中提供了实现 Set 接口的 HashSet 类和 TreeSet 类。

Set 接口:存储的数据是无序的、不可重复的,类似数学中的 “集合”。

  • HashSet:作为 Set 接口的主要实现类;线程不安全;可以存储 null 值。
    • LinkedHashSet:作为 HashSet 的子类,遍历其内部数据时,可以按照添加的顺序遍历。对于频繁的遍历操作,LinkedHashSet 效率高于 HashSet。
  • TreeSet:可以按照添加对象的指定属性,进行排序。
Set 实现类之一:HashSet

HashSet 类是用哈希 (Hash) 表实现了 Set 接口,是 Set 的典型实现,大多数时候使用 Set 集合时都使用这个实现类。一个 HashSet 对象中的元素存储在一个哈希表中,而且用 HashSet 类实现 Set 集合能够快速定位集合中的元素。 用 HashSet 类实现的 Set 集合中的对象必须是唯一的,所以在添加对象时,要重写 equals() 方法进行验证,从而保证插入集合中的对象标识的唯一性。用 HashSet 类实现的 Set 集合按照哈希码排序,并根据对象的哈希码来确定对象的存储位置,所以在添加对象时,要重写 hashCode() 方法,从而保证插入集合中的对象能够合理地分在集合中,以便于快速定位集合中的对象。

Set 接口存储的数据是无序的、不可重复的,以 HashSet 为例说明:

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

HashSet 具有以下特点:

  • 不能保证元素的排列顺序。
  • HashSet 不是线程安全的。
  • 集合元素可以是 null。

HashSet 集合判断两个元素相等的标准:

两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。

向 HashSet 中添加元素的过程:

我们向 HashSet 中添加一个元素 a,首先调用元素 a 所在类的 hashCode() 方法,计算元素 a 的哈希值,
此哈希值接着通过某种算法(散列函数)计算出在 HashSet 底层数组的存放位置(即为:索引位置),并判断数组此位置上是否已经有元素:

  • 如果此位置没有其他元素,则元素 a 添加成功。(情况 1)

  • 如果此位置上有其他元素 b(或以链表形式存在多个元素),则比较元素 a 与元素 b 的 hash 值:

    • 如果 hash 值不同,则添加元素成功。(情况 2)

    • 如果 hash 值相同,进而需要调用元素 a 所在类的 equals() 方法:

      • equals() 返回 false,则元素 a 添加成功。(情况 3)
      • equals() 返回 true,则元素 a 添加失败。

补充:

关于添加成功的情况 2 和情况 3 而言:元素 a 与已经存在指定索引位置上数据以链表的方式存储。(HashSet 底层:数组 + 链表的结构)

  • JDK 7 :元素 a 放到数组中,指向原来的元素。
  • JDK 8 :原来的元素在数组中,指向元素 a。
Set 实现类之二:LinkedHashSet

LinkedHashSet 作为 HashSet 的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据(双向链表),这使得元素看起来是以插入顺序保存的。LinkedHashSet 插入性能略低于 HashSet,但如果需要频繁的遍历操作,LinkedHashSet 效率高于 HashSet。

HashSet 和 LinkedHashSet 存储元素的要求:

向 HashSet 和 LinkedHashSet 中添加的元素,其所在的类一定要重写 hashCode() 和 equals()。重写的 hashCode() 和 equals() 尽可能保持一致性:相等的对象必须具有相等的哈希值。

Set 实现类之三:TreeSet

TreeSet 类不仅实现了 Set 接口,还实现了 java.util.SortedSet 接口,从而保证在遍历集合时按照递增的顺序获得对象。遍历对象时可能是按照自然顺序递增排列(自然排序),所以,存入由 TreeSet 类实现的 Set 集合的对象时必须实现 Comparable 接口;也可能是按照指定比较器 Comparator 递增排列(定制排序),即可以通过比较器对由 TreeSet 类实现的Set集合中的对象进行排序。默认情况下,TreeSet 采用自然排序。

  • 自然排序中,比较两个对象是否相同的标准为:compareTo() 返回 0,不再是 equals() 。
  • 定制排序中,比较两个对象是否相同的标准为:compare() 返回 0,不再是 equals() 。
 	// 自然排序
	@Test
    public void test1() {
        TreeSet set = new TreeSet();

        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", 66));

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
	// 定制排序
	@Test
	public void test2() {
   		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("Jack", 33));
    	set.add(new User("Mary", 33));
    	set.add(new User("Jack", 66));

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

	}

TreeSet 存储元素的要求:

向 TreeSet 中添加的数据,要求是同一个类的对象。

TreeSet 类通过实现 SortedSet 接口所增加的方法:

  • comparator():获得对该集合采用的比较器,返回值为 Comparator 类型。如果未采用任何比较器,则返回 null 。
  • first():返回在集合中排序位于第一的对象。
  • last():返回在集合中排序位于最后的对象。
  • headSet(E toElement):截取在集合中排序位于对象 toElement (不包含)之前的所有对象,重新生成一个 Set 集合并返回。
  • subSet(E fromElement,E toElement):截取在集合中排序位于对象 fromElement (包含)和对象 toElement (不包含)之间的所有对象,重新生成一个 Set 集合并返回。
  • tailSet(E fromElement):截取在集合中排序位于对象 fromElement (包含)之后的所有对象,重新生成一个 Set 集合并返回。

4 Map 接口

4.1 Map 接口概述

Map 集合为映射类型,与 Collection 并列存在,用于保存具有映射关系映射关系的数据:key-value(类似与数学中的函数:y = f(x) )。映射与集和列表有明显的区别,映射中的每个对象都是成对存在的。映射中存储的每个对象都有一个相应的键(key)对象,在检索对象时必须通过相应的键对象来获取值(value)对象。Map 中的 key 和 value 都可以是任何引用类型的数据。其中, Map 中的 key 用 Set 来存放,不允许重复。

4.2 Map 实现类的结构

Map:双列数据,存储 key-value 对的数据。 → 类似与数学中的函数:y = f(x)

  • HashMap:作为 Map 的主要实现类。线程不安全,效率高。可以存储 null 的 key 和 value。

    • LinkedHashMap:保证在遍历 Map 元素时,可以按照添加的顺序实现遍历。

      原因:在原有的 HashMap 底层结构基础上,添加了一对指针,指向前一个和后一个元素。

      对于频繁的遍历操作,此类执行的效率高于 HashMap。

  • TreeMap:保证按照添加的 key-value 对进行排序,实现排序遍历。此时考虑 key 的自然排序或定制排序。(底层使用红黑树)

  • Hashtable:作为 Map 的古老实现类。线程安全,效率低。不能存储 null 的 key 和 value。

    • Properties:常用类处理配置文件。key 和 value 都是 String 类型。

Map 结构的理解:

  • Map 中的 key:无序的、不可重复的,使用 Set 存储所有的 key。 → key 所在的类需要重写 equals() 和 hashCode() (以 HashMap 为例)
  • Map 中的 value:无序的、可重复的,使用 Collection 存储所有的 value。 → value 所在的类需要重写 equals()
  • Map 中的 entry:无序的、不可重复的,使用 Set 存储所有的 entry。(一个键值对:key-value 构成了一个 entry 对象。)

4.3 Map 接口:常用方法

添加、删除、修改操作:

  • Object put(Object key,Object value):将指定 key-value 添加到(或修改)当前 map 对象中。
  • void putAll(Map m):将 m 中的所有 key-value 对存放到当前 map 中。
  • Object remove(Object key):如果存在指定的 key,移除指定 key 的 key-value 对,并返回 value,否则返回 null。
  • void clear():清空当前 map 中的所有的映射关系。

查询操作:

  • Object get(Object key):如果存在指定的 key,获取指定 key 对应的 value ,否则返回 null。
  • boolean containsKey(Object key):是否包含指定的 key。
  • boolean containsValue(Object value):是否包含指定的 value。
  • int size():返回 map 中 key-value 对的个数。
  • boolean isEmpty():判断当前 map 中是否包含 key-value 对。为空返回 true。
  • boolean equals(Object obj):判断当前 map 和指定对象 obj 是否为同一个对象。

元视图操作方法:

  • Set keySet():将该 map 中的所有 key,以 Set 集合的形式返回。
  • Collection values():将该 map 中的所有 value,以 Collection 集合的形式返回。
  • Set entrySet():将该 map 中的所有 key-value 对,以 Set 集合的形式返回。

总结:常用方法

  • 添加:put(Object key, Object value)
  • 删除:remove(Object key)
  • 修改:put(Object key, Object value)
  • 查询:get(Object key)
  • 长度:size()
  • 遍历:keySet() / values() / entrySet()

注意:

Map 允许值对象为 null,并且没有个数限制。所以,当 get() 方法的返回值为 null 时,可能有两种情况:一种是在集合中没有该键对象,另一种是该键对象没有映射任何值对象,即值对象为 null。因此,在 Map 集合中不应该利用 get() 方法来判断是否存在某个键,而应该利用 containsKey() 方法来判断。

Map 常用方法测试:

添加、修改操作:

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

	// 修改,当 key 相同时,替换 value
	map.put("AA", 87);

	System.out.println(map);

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

	map.putAll(map1);

	System.out.println(map);
}

运行结果:

{AA=123, BB=56, 45=123}
{AA=87, BB=56, 45=123}
{AA=87, BB=56, CC=123, DD=123, 45=123}

删除操作:

@Test
public void test(){
	Map map = new HashMap();
	map.put("AA", 123);
	map.put(45, 123);
	map.put("BB", 56);
	
	// remove(Object key)
    Object value = map.remove("AA");    // 返回移除元素的 value
    System.out.println(value);
    Object value1 = map.remove("CC");   // 返回 null
    System.out.println(value1);
    System.out.println(map);

    // clear()
    map.clear();    // 与 map = null 操作不同,map 还在,只是里面没有数据了
    System.out.println(map.size());
    System.out.println(map);
}

运行结果:

123
null
{BB=56, 45=123}
0
{}

查询操作:

@Test
public void test() {
        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));
        System.out.println(map.get(455));    // null

        //boolean containsKey(Object key)
        boolean isExist = map.containsKey("BB");
        System.out.println(isExist);

        // boolean containsValue(Object value)
        isExist = map.containsValue(123);
        System.out.println(isExist);

        // boolean isEmpty()
        map.clear();
        System.out.println(map.isEmpty());

        // boolean equals(Object obj)
        Map map1 = new HashMap();
        map1.put("AA", 123);
        map1.put(45, 123);
        map1.put("BB", 56);

        map.put("AA", 123);
        map.put(45, 123);
        map.put("BB", 56);

        System.out.println(map.equals(map1));
    }

运行结果:

123
null
true
true
true
true

元视图操作:

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

    // 遍历所有的 key 集:keySet()
    Set set = map.keySet();    // map 不能直接调用 iterator()
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
    System.out.println();
    
    // 遍历所有的 value 集:values()
    Collection values = map.values();
    for (Object obj : values) {
        System.out.println(obj);
    }
    System.out.println();

    // 遍历所有的 Key-value 集
    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());
    }
}       

运行结果:

AA
BB
45

123
56
1234

AA---->123
BB---->56
45---->1234

4.4 Map 实现类之一:HashMap

HashMap 类实现了 Map 接口,是通过哈希码对其内部的映射关系进行快速查找,即基于哈希表的 Map 接口的实现,是 Map 接口使用频率最高的实现类。其允许使用 null 键和 null 值,与 HashSet 一样,不保证映射的顺序。

  • 所有的 key 构成的集合是 Set:无序的、不可重复的。所以,key 所在的类要重写:equals() 和 hashCode() 。
  • 所有的 value 构成的集合是 Collection:无序的、可以重复的。所以,value 所在的类要重写: equals()。
  • 所有的 entry 构成的集合是 Set:无序的、不可重复的。
  • HashMap 判断两个 key 相等的标准相等的标准是:两个 key 通过 equals() 方法返回 true,
    并且 hashCode 值也相等。
  • HashMap 判断两个 value 相等的标准相等的标准是:两个 value 通过 equals() 方法返回 true。

HashMap 的底层存储结构:

  • JDK 7 之前:数组 + 链表(即链地址法)
  • JDK 8 之后:数组 + 链表 + 红黑树
4.4.1 HashMap 的底层实现原理:

HashMap 源码中的重要常量:

  • DEFAULT_INITIAL_CAPACITY : HashMap 的默认容量。(16)
  • MAXIMUM_CAPACITY : HashMap 的最大支持容量。(2 ^ 30)
  • DEFAULT_LOAD_FACTOR:HashMap 的默认填充因子。(0.75)
  • TREEIFY_THRESHOLD:Bucket 中链表长度大于该默认值,转化为红黑树。(8)
  • UNTREEIFY_THRESHOLD:Bucket 中红黑树存储的 Node 小于该默认值,转化为链表。(6)
  • MIN_TREEIFY_CAPACITY:桶中的 Node 被树化时最小的 hash 表容量。(64)
  • table:存储元素的数组,总是 2 的 n 次幂。
  • entrySet:存储具体元素的集。
  • size:HashMap 中存储的键值对的数量。
  • modCount:HashMap 扩容和结构改变的次数。
  • threshold:扩容的临界值,= 容量 * 填充因子(16 * 0.75 = 12)
  • loadFactor:填充因子。

JDK 8 之前:

HashMap map = new HashMap():

HashMap 的内部存储结构其实是数组和链表的结合。在实例化以后,底层创建了长度为 Capacity 的一维数组 Entry[] table,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为 “ 桶 ” (bucket),每个 bucket 都有自己的索引,系统可以根据索引快速的查找 bucket 中的元素。每个 bucket 中存储一个元素,即一个 Entry 对象,但每一个 Entry 对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个 Entry 链。而且新添加的元素作为链表的 head。

添加元素的过程( map.put(key1, value1) ):

首先,调用 key1 所在类的 hashCode() 计算 key1 的哈希值,此哈希值经过某种算法(散列函数)计算以后, 得到在 Entry 数组中的存放位置。

  • 如果此位置上的数据为空,此时的 key1-value1 添加成功。(情况 1)

  • 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表的形式存在)),比较 key1 和已经存在的一个或多个数据的哈希值:

    • 如果 key1 的哈希值与已经存在的数据的哈希值都不相同,此时 key1-value1 添加成功。

      (情况 2)

    • 如果 key1 的哈希值与已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用 key1 所在类的 equals(key2) 方法,比较:

      • 如果 equals() 返回 false:此时 key1-value1 添加成功。(情况 3)
      • 如果 equals() 返回 true:使用 value1 替换 value2。

补充:

  1. 关于添加成功的情况 2 和情况 3 而言:此时 key1-value1 和原来的数据以链表的方式存储。

  2. 在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。

    默认的扩容方式:扩容为原来的 2 倍,并将原有的数据复制过来。

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

JDK 8 相较于 JDK 8 之前在底层实现方面的不同:

  1. HashMap map = new HashMap(); 默认情况下,先不创建长度为16的数组。

  2. 当首次调用 map.put() 时,底层再去创建一个长度为 16 的数组。

  3. JDK 8 底层的数组是:Node 类型;而 JDK 7 底层的数组是:Entry 类型。

  4. JDK 7 底层结构:数组 + 链表。 JDK 8 中底层结构:数组 + 链表 + 红黑树。 当数组的某一个索引位置上的元素以链表的形式存在的数据个数大于 8 ,且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。(方便查找)

  5. JDK 7 :新添加的元素指向已经存在的元素;JDK 8:已经存在的元素指向新添加的元素。

4.5 Map 实现类之二:LinkedHashMap

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

HashMap 中的内部类:Node

static class Node implements Map.Entry {
	final int hash;
	final K key;
	V value;
	Node next;
}

LinkedHashMap 中的内部类:Entry

static class Entry extends HashMap.Node {
	Entry before, after;
	Entry(int hash, K key, V value, Node next) {
		super(hash, key, value, next);
	}
}

4.6 Map 实现类之三:TreeMap

TreeMap 类不仅实现了 Map 接口,还实现了 Map 接口的子接口 java.util.SortedMap。由 TreeMap 类实现的 Map 集合,不允许键对象为 null,因为集合中的映射关系是根据键对象按照一定顺序排列的。向 TreeMap 中添加 kay-value,要求 key 必须是由同一个类创建的对象。因为要按照 key 进行排序。

TreeMap 的 Key 的排序:自然排序、定制排序。

自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException。

定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口。

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

// 自然排序
@Test
public void test1() {
    TreeMap map = new TreeMap();
    User u1 = new User("Tom", 23);
    User u2 = new User("Jerry", 32);
    User u3 = new User("Jake", 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());

    }
}

运行结果:

User{name='Tom', age=23}---->98
User{name='Rose', age=18}---->100
User{name='Jerry', age=32}---->89
User{name='Jake', age=20}---->76
// 定制排序:按年龄排序
@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("Jake", 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());

    }
}

运行结果:

User{name='Rose', age=18}---->100
User{name='Jake', age=20}---->76
User{name='Tom', age=23}---->98
User{name='Jerry', age=32}---->89

5 Collections 工具类

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

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

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

查找、替换操作:(均为static方法)

  • 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 对象的所有旧值。

同步控制:(均为static方法)

Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。

  • synchronizedCollection(Collection c)
  • synchronizedList(List list)
  • synchronizedMap(Map m)
  • synchronizedSet(Set s)
public static void main(String[] args) {
    
	// 返回的 list1 即为线程安全的 List
	List list1 = Collections.synchronizedList(list);

}

你可能感兴趣的:(Java,学习笔记,java,容器,开发语言,集合)