【JavaSE基础】08-集合框架(三)

【JavaSE】之集合框架 Java集合框架详细介绍,包含所有集合类的介绍与使用~ 阅读详情

Set 接口

public interface Set<E> extends Collection<E>

0:接口说明

Set 接口是对数学中的集合的抽象,满足集合的一般性质:

  • 无序性(进出顺序)
  • 唯一性(不包含重复元素)
  • 至多一个null值元素(可以没有)

1:功能概述

Set 接口完全继承,并根据集合的特点重写了 Collection的 15个方法。没有任何特有方法。

  • 添加功能 2 add、addAll
  • 删除功能 4 clear、remove、removeAll、retainAll
  • 获取功能 3 size、Iterator、hashCode
  • 判断功能 4 isEmpty、equals、contains、containsAll
  • 转换功能 2 toArray、toArray

Set VS List 异同点

不同点:

  • Set:无序的集合,不可重复,无索引,无任何特有方法
  • List:有序的集合,可重复,有索引,10个特有方法 【针对索引的 添加功能:add、addAll;针对索引的 删除功能:remove;读写器:get、set;特有遍历:listIterator、listIterator(index);获取功能:indexOf、lastIndexOf(高开销的线性搜索);获取局部视图功能:subList (操作的时候,直接映射到原来的List上面)】

相同点:

  • 两者同时继承了 Collection 的15个基本操作方法,以及作为集合的特性(可变的数组:数据类型可变、元素个数可变)
  • Set 没有 关于索引的操作,以及读写器,源于Set的无序性

2、实现子类

HashSet

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable

0:类的描述:

此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例【参见 构造器的底层源码】) 支持的。不保证集合的 迭代顺序,特别是不保证此顺序恒久不变,此类允许 null 元素。

此类为基本操作提供了稳定的性能,这些基本操作是 add、remove、contains 和 size。 假设 哈希函数将这些元素正确分布在桶中。对此 set 进行迭代所需时间与 HashSet 实例的大小(元素的数量)和 底层 HashMap 实例(桶的数量)的“容量”的和成正比。因此,如果迭代器的性能很重要,则不要将初始容量设置的太高。(或将加载因子设置的太低)

此类 单线程高效,需要同步进行安全访问的时候,粗腰创建 同步的集合Collections.synchroniizedSet(new HashSet(...))

请注意,并发修改异常 ConcurrentModificationException :创建迭代器之后,使用集合的方法进行集合元素的修改,Iterator抛出的异常,是快速失败。

1:功能方法
在这里插入图片描述

根据哈希表的结构,来实际分析 基本操作的 实现原理:

  • add:实现的过程:(1)获取需添加的元素的 hashCode() ,根据元素的 哈希值(和哈希函数) 找到 链表在数组中位置;(2)遍历链表,使用 equals() 进行判断,如果已经存在 直接返回 false;否则添加在链表最后,并返回true。

  • remove:实现过程:(1)获取需删除的元素的 hashCode() ,根据元素的 哈希值(和哈希函数) 找到 链表在数组中位置;(2)遍历链表,使用 equals() 进行判断,如果存在 直接删除,并返回true;否则直接返回 false。

  • contains:实现过程:(1)获取需判断的元素的 hashCode(),根据元素的 哈希值(和哈希函数) 找到 链表在数组中位置;(2)遍历链表,使用equals()进行判断,如果存在,则返回 true,否则返回 false

  • size:实现过程:直接遍历数组中每个链表的,返回 所有链表的元素个数之和。

package com.rupeng.set;

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

public class Demo2
{
	public static void main(String[] args)
	{
		Set set = new HashSet();

		set.add("com");
		set.add("Rupeng");
		set.add("www");

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

		/** 虽然 进出的顺序相同,但不能说明 Set 是有序的。 */
		/** 暂且将有序定义为:不论如何添加元素,在写代码的那一刻,你就知道取出时的顺序了 */
		/** 比如:数组列表、队列、栈等有序集合 */
	}
}

注意一个问题:虽然 Set 是无序的,但也有自身的 存储结构。所以即使是说 向 Set 中添加元素的顺序和遍历取出的顺序相同,并不能说明 Set 的有序性。

说完了 有序的问题,再来看看 Set 集合的唯一性,是如何保证的?实际上,Set 基本操作的实现过程已经说明了,Set 元素唯一性的原因:

  • 先看 hashCode() 值是否相同,不相同 直接添加到集合中(作为新链表的首元素);相同 则继续看 equals() 比较的结果,返回 true ,元素存在 不添加;返回 false ,原始不存在,添加到集合中(作为原有链表的尾元素)

源码分析:

// HashMap 类中 实现添加最核心的语句
if (e.hash == hash && 
		((k = e.key) == key || (key != null && key.equals(k))))
{
	break;
}

默认情况下,hashCode() 和 equals() 都是继承自Object超类的,进行比较的是地址值,但是有些情况下,比较地址值的意义不大,相反,使用类的成员变量的比较更有意义。比如:两个学生是否相等,如果是用地址值比较的话,因为在堆内存中不可能公用一块地址,即使是同一个人实例化两次,也会得到 为 false 的判断结果,这个时候,就需要重写 这两个方法 了。

案例1:创建字符串的 HashSet 集合

package com.rupeng.set;

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

public class Demo2
{
	public static void main(String[] args)
	{
		Set set = new HashSet();

		set.add("com");
		set.add("Rupeng");
		set.add("www");
		set.add("Rupeng");

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

String 类 重写了来自 Object 的 hashCode() 和 equals(),对于相等的比较,是按位比较 字符串中的每个字符是否相等。故此,可以保证元素的唯一性。

案例2:创建自定义学生对象的 HashSet 集合

package com.rupeng.set;

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

public class Demo3
{
	public static void main(String[] args)
	{
		Student stu1 = new Student("Cathy", 18);
		Student stu2 = new Student("Cathy", 18);
		Student stu3 = new Student("Ian", 17);
		Student stu4 = new Student("Ian", 17);

		Set<Student> set = new HashSet<Student>();
		set.add(stu1);
		set.add(stu2);
		set.add(stu3);
		set.add(stu4);

		for (Iterator<Student> it = set.iterator(); it.hasNext();)
		{
			Student st = it.next();
			System.out.println(st);
		}
	}
}

从代码的执行结果,可能会觉得错了。实际上 计算器 又被冤枉了,它只是按照 引用对象的地址值进行比较,确定两个值是不是相等的,而我们想要的是 通过对象的成员变量的比较来确定两个 对像是不是相等,这个时候就需要 在自定义类 Student 中重写 那两个关键的方法(hashCodse() 和 equals())了。【为了更加方便地输出 学生信息,可以重写 toString() 方法】

// 只是简单的重写,程序的健壮性什么的没有考虑
// 更完善的代码,可以直接使用 快捷键生成 这两个方法
// 快捷键:ALT + SHIFT + S + 键盘键入 h或H
@Override
public int hashCode()
{
	return name.hashCode() + age * 2015;
}

@Override
public boolean equals(Object obj)
{
	Student other = (Student) obj;
	if (other == this)
	{
		return true;
	}
	if (other.getName() == name)
	{
		if (other.getAge() == age)
		{
			return true;
		}
	}
	return false;
}

综上,HashSet 如何保证 元素的唯一性

  • a、底层数据结构是 哈希表【元素是链表的 数组】
  • b、哈希表依赖于哈希值存储
  • c、添加功能底层实现依赖两个方法 (hashCodse() 和 equals())
LinkedHashSet

public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, Serializable

a、可预知迭代顺序的 Set接口,底层是哈希表和链表的实现

b、元素有序且唯一,允许 null 元素【哈希表保证元素的唯一性,链表保证元素的有序性】,增删效率高

c、按照链表定义的迭代顺序与其插入顺序相同

d、单线程高效,安全需要时,使用同步Set集合方式定义 Collections.synchronizedSet(new LinkedHashSet(...)); ,同样存在快速失败的 并发修改异常 ConcurrentModificationException。

TreeSet

public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable

public interface NavigableSet<E> extends SortedSet<E>

0:类的描述

根据实际的继承关系,可以知道 NavigableSet 接口实现了一个排序的接口。

TreeSet 是基于 TreeMap 的 NavigableSet 实现,实际上是 TreeMap 的一个实例。使用元素的自然顺序(Comparable)对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。

此实现为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。 (时间复杂度)

TreeSet 具有 唯一性和排序性。不允许 null 元素 增删效率高

因为二叉树对于位置的定位很快,所以无论是添加 删除 效率都很高。
添加时:只需要定位添加的位置
删除时:只需要定位待删除元素的位置

1:数据结构

TreeSet 底层数据结构是 红黑树(Red-Blank Tree),也就是自平衡的二叉查找树(元素的快速查找)

二叉查找树:每个节点有且只有一个父节点,最多存在两个子节点,并且该节点的值大于等于左子树中的所有节点,小于等于右子树的所有节点。

存储方式:

  • 第一个元素作为 Root 元素
  • 之后的元素和Root元素 比较
    • 大于 Root – 在右子树中进行存储
    • 小于 Root – 在左子树中进行存储
    • 等于 Root – 不进行存储

取出方式:
深层 -自上而下 三种主要的遍历方式

  • a、先序遍历 Root - Left - Right
  • b、中序遍历 Left - Root - Right
  • c、后序遍历 Left - Right - Root

注意:不论是存储还是取出的时候,这种定义都是 递归 的

简单元素的存取:
在这里插入图片描述

说明: 从上面的存储过程,证明 TreeSet 实际上在存元素时,就已经给定了一个顺序了,之后 取出的元素 是按照 从小到大的,再结合 二叉查找树的遍历结结果 可以知道,实际上 TreeSet 迭代实现是 中序遍历 。

2:功能方法

自然排序 构造器:public TreeSet()
比较器排序 构造器:public TreeSet(Comparator<? extends E> c)

其余功能方法和Set一致,不再赘述。

案例1:创建字符串的 TreeSet 集合

package com.rupeng.set;

import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

public class TreeSetDemo1
{
	public static void main(String[] args)
	{
		Set<String> set = new TreeSet<String>();
		set.add("Javase");
		set.add("Rupeng");
		set.add("JoianSUN");
		set.add("Shanhai");
		set.add("20150705");

		for (Iterator<String> it = set.iterator(); it.hasNext();)
		{
			String str = it.next();
			System.out.println(str);
		}
	}
}

案例2:创建自定义对象的 TreeSet 集合

package com.rupeng.set;

import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

public class TreeSetDemo2
{
	public static void main(String[] args)
	{
		Set<Student> set = new TreeSet<Student>();
		set.add(new Student("Joian", 18));
		set.add(new Student("Du", 27));
		set.add(new Student("Yzk", 10));

		for (Iterator<Student> it = set.iterator(); it.hasNext();)
		{
			Student stu = it.next();
			System.out.println(stu);
		}
	}
}

在这里插入图片描述

无法将 Student 转换为 Comparable :Student 不是接口 Comparable 的实现类,所以无法装换。

在构造 TreeSet 对象的时候,使用了默认的构造器,集合中元素的排序方式是自然排序,也就是说 添加到集合的元素必须是 Comparable 接口的实现类实例。否则报错。简言之:需要的 Comparable 接口的实现类实例,实际是 未实现接口的 Student 。之所以 String 类可以正常添加,是因为 String 类实现了 Comparable 接口。

如何解决呢?

  • 让自定义对象实现 Comparable 自然排序 接口;
  • 使用比较器 Comparator的构造,让集合有序

为了篇幅的整齐,直接在此处给出具体的解决方案。进行具体的问题解决之前,先去看一下 这两个 接口的相关内容吧,就在这一节的后面。

解决方案1:元素 实现 自然排序接口

// 仅仅给出了 Comparable 接口的方法实现
public class Student implements Comparable<Student>
{
	/** 实现的是 先按照姓名升序,姓名相同,按照年龄升序 */
	@Override
	public int compareTo(Student o)
	{
		int result = getName().compareTo(o.getName());
		if (result == 0)
		{
			return getAge() - o.getAge();
		} else
		{
			return result;
		}
	}
}

解决方案2:TreeSet 实现 比较器接口的构造

// 使用比较器 构造 TreeSet 对象,并对代码进行了重构
/** 实现的是 先按照姓名升序,姓名相同,按照年龄升序 */
Set<Student> set = new TreeSet<Student>(new Comparator<Student>()
{
	@Override
	public int compare(Student o1, Student o2)
	{
		int result = o1.getName().compareTo(o2.getName());
		if (result == 0)
		{
			result = o1.getAge() - o2.getAge();
		}
		return result;
	}
});

实现的是 先按照姓名升序,姓名相同,按照年龄升序,实际完成需求时,需要自行取分析主要条件和次要条件,譬如:给定的需求会是 请按照学生姓名对集合中的元素进行升序排序,那么 主要条件是 姓名,次要条件是 年龄(如果姓名相同的话,如何按照年龄排序,可升序 可降序)

案例:根据学生的姓名长度进行排序

package com.rupeng.set;

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

/**
 * 需求:根据学生姓名长度进行升序排序
 * 
 * 分析:
 * 	 (1)主要条件:学生姓名长度
 *  (2)次要条件:学生姓名、学生年龄
 * 
 * 编码实现:
 *		 因为实际给定了一个 比较,使用比较器 构造 TreeSet,
 *		相对 使Student 实现自然排序,更有意义,自然排序 应该是只和对象成员变量本身有关,
 *		才更有意义,而且一般不要更改已经写好的 类
 */
public class TreeSetDemo3
{
	public static void main(String[] args)
	{
		Set<Student> set = new TreeSet<Student>(new Comparator<Student>()
		{
			@Override
			public int compare(Student o1, Student o2)
			{
				int result = o1.getName().length() - o2.getName().length();
				if (result == 0)
				{
					result = o1.getName().compareTo(o2.getName());
					if (result == 0)
					{
						result = o1.getAge() - o2.getAge();
					}
				}
				return result;
			}
		});

		set.add(new Student("Joian", 18));
		set.add(new Student("Cathy", 18));
		set.add(new Student("Du", 27));
		set.add(new Student("Du", 27));
		set.add(new Student("Yzk", 10));
		set.add(new Student("Yzk", 19));

		Iterator<Student> it = set.iterator();
		while (it.hasNext())
		{
			Student stu = it.next();
			System.out.println(stu);
		}
	}
}

TreeSet 的排序方式 取决于使用的构造方法:public TreeSet() 自然排序,public TreeSet(Comparator<? extends E> c) 比较器排序

public TreeSet(Comparator<? extends E> c)
/**
 * 接口 作为参数传递,实际需要的是 接口的实现类对象:
 * 主要三种方式 给定 实现类对象
 * (1)匿名内部类(最常用)
 * (2)外部类
 * (3)借口实现
 */ 

TreeSet 唯一性和元素排序的原理

  • A :唯一性:底层数据结构红黑树支持,具体实现是 比较的返回值是否为 0 来决定
  • B :排序:
    • a 、自然排序(元素具有比较性)让元素所属类实现 Comparable 自然排序接口
    • b 、比较器排序(TreeSet具有比较性) 给 TreeSet 构造方法提供一个 Comparator 比较器接口实现类对

理解

  • 元素具有比较性:学生按照自身身高在教室中中坐座位
  • 集合具有比较性:学生按照教室中给定的编号坐座位

综合案例1:使用HashSet 重新实现:获取 10个 1~20 范围内的整数,且不可重复

package com.rupeng.set;

import java.util.HashSet;
import java.util.Random;
import java.util.Set;

public class HashSetDemo
{
	public static void main(String[] args)
	{
		Random random = new Random();
		Set set = new HashSet();

		while (set.size() < 10)
		{
			int num = random.nextInt(20) + 1;
			set.add(num);
		}

		System.out.println(set);
	}
}

综合案例2:键盘录入五个学生的成绩信息,按照总分降序排序

package com.rupeng.set;

import java.util.Comparator;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;

/**
 * 需求:键盘录入五个学生的成绩信息,按照总分降序排序 
 * 格式 为: 姓名 语文成绩 数学成绩 英语成绩。譬如:Joian 90 89 100
 */
public class TreeSetDemo
{
	public static void main(String[] args)
	{
		Scanner sc = new Scanner(System.in);
		String[] strs = new String[5];

		for (int i = 0; i < strs.length; i++)
		{
			System.out.println("Please enter the No."+(i + 1)+" student's record:");
			strs[i] = sc.nextLine();
		}
		sc.close();

		Set<Student> set = new TreeSet<Student>(new Comparator<Student>()
		{
			public int compare(Student o1, Student o2)
			{
			/* int result = o2.getTotal() - o1.getTotal();
				if (result == 0)
				{
					result = o1.getName().compareTo(o2.getName());
					if (result == 0)
					{
						result = o1.getChinese() - o2.getChinese();
						if (result == 0)
						{
							result = o1.getMath() - o2.getMath();
							if (result == 0)
							{
								result = o1.getEnglish() - o2.getEnglish();
							}
						}
					}
				}
				return result;*/
				int num1 = o2.getTotal() - o1.getTotal();
				int num2 = (num1 == 0) ? (o1.getName().compareTo(o2.getName()))
						: num1;
				int num3 = (num2 == 0) ? (o1.getChinese() - o2.getChinese())
						: num2;
				int num4 = (num3 == 0) ? (o1.getMath() - o2.getMath()) : num3;
				int num5 = (num4 == 0) ? (o1.getEnglish() - o2.getEnglish())
						: num4;
				return num5;
			};
		});

		for (String str : strs)
		{
			String[] info = str.split(" ");
			Student stu = new Student(info[0], Integer.parseInt(info[1]),
					Integer.parseInt(info[2]), Integer.parseInt(info[3]));
			set.add(stu);
		}

		System.out.println("----------------------------");
		for (Student st : set)
		{
			System.out.println(st.printScore());
		}
	}
}

Comparable 自然排序接口

底层源码:

public interface Comparable<T> 
{
	public int compareTo(T o);
	// 实际修饰符 应该是 public abstract
}

接口强行对实现它的每一个类进行整体排序,称为类的 自然排序。类的方法 compareTo 被称为 自然排序方法。

  • 实现此接口的对象集合和数组,可以通过对应工具类中的 sort 方法(Collections.sort() 和 Arrays.sort())进行整体排序。

  • 实现此接口的对象可以作为 有序映射(SortedMap)的键和有序集合(SortedSet)的元素,无需指定比较器(Comparator) (TreeMap 和 TreeSet 就是有序映射、集合)

对于集合两个对象 e1 和 e2 ,当且仅当 e1.equals(e2) 和 e1.compareTo(e2)==0 返回相同的 boolean 值时,类的自然排序才叫做与 equals 一致。 现阶段 我们需要这种一致,如果不一致的话,会出现很奇怪的想象,所以注意 与equals一致。实际上,所有实现 Comparable 接口的 Java 核心类都具有与 equals 一致的自然排序。 但是 java.util.BigDecimal 是个特例,它的自然排序 将值相同、精度不同的对象视为相等。

注意: null 不是任何类型的实例,即使 e.euaals(null) 返回false,e.compareTo(null) 也会抛出异常 NullPointerException。 这个就是 为什么 TreeSet 元素 和 TreeMap 的键 不允许 null 元素的原因。

功能方法
public abstract int compareTo(T o);

比较当前对象与指定对象的顺序。如果当前对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

实现整体排序,实际的底层 是 常见的排序算法。

注意:ClassCastException 异常,如果 o 无法转换为指定类的对象。

Comparator 比较器接口

底层源码:Java 1.8 之前

public interface Comparator<T> 
{
	// Java 1.8 以前 全部的方法
	int compare(T o1, T o2);
	boolean equals(Object obj);
	// 实际修饰符:public abstract
}

强行对实现接口的 对象集合进行整体排序的比较器。

  • 可以将 Comparator 传递给 sort 方法 (Collections.sort() 和 Arrays.sort()) 从而实现排序顺序的精确控制。

  • 可以将使用 Comparator 来控制某些数据结构的顺序 (有序映射 TreeMap 和 有序集合 TreeSet)

  • 可以为没有实现自然排序的对象 的集合提供排序

对于Comparator c 的两个元素 e1 和 e2 ,当且仅当 e1.equals(e2) 和 c.compare(e1, e2)==0 返回相同的 boolean 值时,Comparator c 提供的排序才叫做与 equals 一致。

功能方法

  • int compare(T o1, T o2);

比较用来排序的两个参数。根据第一个参数小于、等于或大于第二个参数分别返回负整数、零或正整数。

注意:ClassCastException 异常,如果 o1、o2 无法转换为指定类的对象。

  • boolean equals(Object obj);

指示某个其他对象是否“等于”此 Comparator。仅当 指定的对象也是一个 Comparator,并且强行实施与此 Comparator 相同的排序时,此方法才返回 true。

Map 接口

0:接口说明

public interface Map<K,V> java.util 包下。

Map 接口是 键映射到值的对象。 键具有唯一性,值可重复 。 Map 实际对应的是 数学中函数。键为自变量,值为因变量。 Map 中的每一个键值对,实际就是 函数中每一个自变量与所得因变量的对应关系,这种对应关系在 Map 中的体现是 就是键值对 public static interface Map.Entry<K,V> ,是以内部类(实际是接口)形式定义的 。

public static interface Map.Entry<K,V>

  • Map 的成员方法 entrySet 返回的就是 Map 的 内部类(接口)Entry 的对象集合 。

  • 获得映射项引用的唯一方法就是通过 集合视图的 迭代器 实现(新特性 增强 for实质也是迭代器实现的)

  • Map.Entry 仅仅在迭代期间有效,更确切来讲,在迭代器返回项之后,修改底层映射,会造成映射行为的不确定性。注意:setValue方法例外 。

Map.Entry 接口成员方法:

  • boolean equals(Object e) 给定对象 为映射项且和当前对象拥有相同的映射关系,返回 true ,更确切的说,满足如下关系的 e1 和 e2 才具有相同的映射关系 (e1.getKey() == null ? e2.getKey() == null : e1.getKey().equals(e2.getKey())) && (e1.getValue() == null ? e2.getValue() : e1.getValue().equals(e2.getValue())) 也就是说 当两个 映射项 对的键和值对影响等的是时候,才说 这两个对应关系是 相同对应关系,允许 null 键值,这可以确保 equals 方法在不同的 Map.Entry 接口实现间可正确地工作。(定义中之所以使用 equals 方法 而不是 == ,是因为可能存在着需要 自定义的相等关系,而不只是 地址相等,可能会是 对象的成员变量对应相等,即视为 对象相等这样的定义,这很常见)
  • int hashCode() 返回此映射项的哈希码值。 映射项 e 的哈希码值的定义如下:(e.getKey() == null ? 0 : e.getKey().hashCode())^(e.getValue() ? 0 :e.getValue().hashCode()) 这个确保 e1.equals(e2) 因为着 对于任意的两个项 e1 和 e2 而言 e1.hashCode() == e2.hashCode(),这也正是 Object.hashCode 常规协定所要求的。
  • K getKey() 获取当前映射项的键
  • V getValue() 获取当前映射项的值
  • V setValue(V value) 设置 当前映射项的值 (注意各种异常),返回 旧的值

Map 接口提供了三个视图:

  • 键的集合:键具有唯一性 public Set<K> keySet()
  • 值的集合:值可重复 public Collection<V> values()
  • 映射项的集合:根据键值的点,键值对(映射项)具有唯一性 public Set<Map.Entry<K,V>> entrySet()

三种视图的迭代器,支持移除操作(remove、removeAll、clear),但是不支持添加操作。

映射顺序:迭代器在视图上返回元素的顺序。TreeMap 保证这种顺序,HashMap 不保证这种顺序。(对比 HashSet、TreeSet【底层实现就是 对应的HashMap、TreeMap的键集合】)

通过映射实现类应该有两种构造器:

  • 无参构造 public 实现类名()
  • 根据已有映射关系复制建立的 public 实现类名(Map<? extends K,? extends V> m)

注意:equals 和 hashCode 的定义必须是明确的。

1:功能概述

先来看看Map与Collection的区别?

  • Map 集合元素 是成对出现的 Entry 实例,Entry<K,V> 中 K 是唯一性的,V 是可重复的;Map 集合的数据额结构针对键有效,与值无关
  • Collection 集合元素 是单个出现的 Java 任意类(含有自定义类)的实例;Collection 的 子接口中 Set 是唯一的,List 是可重复的;Collection 集合的数据结构针对元素有效
  • a、添加功能

    • V put(K key, V value) 修改集合中的映射项,返回的是与 key 关联的旧的值,如果该映射项不存在,则执行添加 并返回 null(注意:如果集合 允许 null 值,返回的 null 包含两种可能性:(1)第一次添加;(2)修改之前的值 是 null )
    • void putAll(Map<? extends K, ? extends V> m) 从指定映射 m 中,将其所有得映射项 复制到 当前的映射中。相当于:从 m 中 遍历每一个映射项,并对当前的 映射 进行 put 方法的调用(修改/添加)
  • b、删除功能

    • void clear() 清楚当前映射

    • V remove(Object key) 移除 key 键所在的映射项,如果不存在这样的映射项,则返回 null,如果存在的话,执行删除 并返回 与之对应的 值。 满足的映射项的 键 k 满足以下条件:key == null ? k == null : key.equals(k) 。(注意:如果集合 允许 null 值,返回的 null 包含两种可能性:(1)不能存在对应的映射项;(2)删除的映射项的值 是 null )

  • c、判断功能

    • boolean equals(Object o) 给定的 映射 o 和当前映射相等,返回 true,相等的定义是,具有相同的映射关系,也就是 o.entrySet().equals(this.entrySet()) 返回 true 。注意重写 hashCode 方法,需要满足 Java 的常规协定,hashCode() 返回此映射的哈希码值。映射的哈希码定义为此映射 entrySet() 视图中每个项的哈希码之和。这确保 m1.equals(m2) 对于任意两个映射 m1 和 m2 而言,都意味着 m1.hashCode()==m2.hashCode(),正如 Object.hashCode() 常规协定的要求。

    • boolean isEmpty() 如果 当前的映射不包含任何的键值对(映射项)的话,返回true

    • boolean containsKey(Object key) 如果此映射包含指定键的映射关系,则返回 true。更确切地讲,当且仅当此映射包含针对满足 (key==null ? k==null : key.equals(k)) 的键 k 的映射关系时,返回 true。(最多只能有一个这样的映射关系)。

    • boolean containsValue(Object value) 如果此映射将一个或多个键映射到指定值,则返回 true。更确切地讲,当且仅当此映射至少包含一个对满足 (value==null ? v==null : value.equals(v)) 的值 v 的映射关系时,返回 true。对于大多数 Map 接口的实现而言,此操作需要的时间可能与映射大小呈线性关系。 (Map 的数据结构只是针对 键 有效,所以这个操作是需要 遍历映射的 所有映射项)

  • d、获取功能

    • V get(Object key) 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。 更确切地讲,如果此映射包含满足 (key==null ? k==null : key.equals(k)) 的键 k 到值 v 的映射关系,则此方法返回 v;否则返回 null。(最多只能有一个这样的映射关系)。(注意:如果集合 允许null值,返回 null 包含两种含义:(1)不存在该项;(2)该项的值是null)
    • int size() 该映射中键值对(映射项) 的个数。注意 int 值的范围
    • 键的集合:键具有唯一性 public Set<K> keySet()
    • 值的集合:值可重复 public Collection<V> values()
    • 映射项的集合:根据键值的点,键值对(映射项)具有唯一性 public Set<Map.Entry<K,V>> entrySet()

获取映射中的项,必须使用 迭代器

  • 键集合 + get 方法
  • 键值集合 + Entry<K, V> 获取功能

案例:遍历映射实例

  • 方式1:键集合 + get 方法
package com.rupeng.map;

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

/** 遍历 映射 */
public class MapDemo1
{
	public static void main(String[] args)
	{
		Map<Integer, String> map = new HashMap<Integer, String>();
		map.put(1, "Joian");
		map.put(2, "Yzk");
		map.put(3, "Cathy");
		map.put(4, "Apple");

		Set<Integer> set = map.keySet();
		for (Integer in : set)
		{
			System.out.println(in + " " + map.get(in));
		}
	}
}

  • 方式2:键值集合 + Entry<K, V> 获取功能
package com.rupeng.map;

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

/** 遍历 映射 */
public class MapDemo1
{
	public static void main(String[] args)
	{
		Map<Integer, String> map = new HashMap<Integer, String>();
		map.put(1, "Joian");
		map.put(2, "Yzk");
		map.put(3, "Cathy");
		map.put(4, "Apple");

		Set<Entry<Integer, String>> set = map.entrySet();
		for (Iterator<Entry<Integer, String>> it = set.iterator(); it.hasNext();)
		{
			Map.Entry<Integer, String> me = it.next();
			System.out.println(me.getKey() + " " + me.getValue());
		}
	}
}

2:实现子类

HashMap

基于哈希表的Map接口实现。四个特性:

  • Map 中键唯一,值可重复,映射项唯一,这个是所有的 Map实现类的**共性**
  • Map 的数据结构 只对 键有效,与值无关,第一点的映射项唯一就是依靠键的唯一实现
  • HashMap 允许 null 键、null值(但这样的键值对,最多出现一次)
  • HashMap 不保证 映射的顺序,特别是不保证其顺序很久不变。

注意 : 如果出现存入的顺序和取出的顺序一样,并不能说明 HashMap的有序性,只是 HashMap 也有其存储的顺序 而已。

使用四个案例 来说明,HashMap的特性

  • 案例1:HashMap<String, String>
  • 案例2:HashMap<Integer, String>
    👆 自行完成

说明: 你会觉得,结果好像是 按照键 排序的,but you are wrong !只是存储的顺序而已。HashMap 不保证 映射的顺序,特别是不保证其顺序很久不变。插入和取出的顺序 是不一样的。说明了 HashMap 的无序性,你也可以随便测试一下 允许 null 键值。【Integer 和 String 重写了 Object 类的equals 和hashCode 方法】

案例3:HashMap<String, Student>

package com.rupeng.map;

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

/** c: Map<String,Student> */
public class HashMapDemo3
{
	public static void main(String[] args)
	{
		Map<String, Student> map = new HashMap<String, Student>();
		Student stu1 = new Student("Joian", 18);
		Student stu2 = new Student("Cathy", 10);

		map.put("2008", stu1);
		map.put("2011", stu1);
		map.put("2011", stu2);
		/** 键相同 只添加一次,之后的操作是 进行 值的修改 */

		Set<Map.Entry<String, Student>> set = map.entrySet();
		for (Entry<String, Student> entry : set)
		{
			System.out.println(entry.getKey() + " " + entry.getValue());
		}
	}
}

说明: 键相同 只添加一次,之后的操作是 进行 值的修改。 映射项的个数 不会改变 。

案例4:HashMap<Student, String>

package com.rupeng.map;

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

/** d: Map<Student,String> */
public class HashMapDemo4
{
	public static void main(String[] args)
	{
		Map<Student, String> map = new HashMap<Student, String>();
		Student stu1 = new Student("Joian", 18);
		Student stu2 = new Student("Cathy", 10);
		Student stu3 = new Student("Joian", 18);

		map.put(stu1, "高中");
		map.put(stu2, "大学");
		map.put(stu3, "初中");

		Set<Student> set = map.keySet();
		for (Student stu : set)
		{
			System.out.println(stu + " " + map.get(stu));
		}
	}
}

说明: 全部添加了,但是 实际上,stu1、stu3 是同一个人,之所以出现问题,是因为默认的是使用 Object 类的equals 方法,返回 true的条件是两个对象的地址值相同,需要重写 equals ,自定义对象的相等条件。同时 更具 Java 的常规协定,需要同时重写 hashCode,使得满足 e1.equals(e2) == true的对象 e1、e2,必须满足e1.hashCode() == e2.hashCode()。

重写 equals 和 hashCode 如下:

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

	@Override
	public boolean equals(Object obj)
	{
		if (this == obj)
		{
			return true;
		}
		if (obj == null)
		{
			return false;
		}
		if (!(obj instanceof Student))
		{
			return false;
		}
		Student other = (Student) obj;
		if (age != other.age)
		{
			return false;
		}
		if (name == null)
		{
			if (other.name != null)
			{
				return false;
			} else if (!name.equals(other.name))
			{
				return false;
			}
		}
		return true;
	}

LinkedHashMap

基于链表和哈希表的Map接口实现。四个特性:

  • Map 中键唯一,值可重复,映射项唯一,这个是所有的 Map实现类的 共性
  • Map 的数据结构 只对 键有效,与值无关
  • LinkedHashMap 允许 null 键、null值(但这样的键值对,最多出现一次,继承自HashMap 的特性)
  • LinkedHashMap 保证 映射的顺序,底层数据结构 链表实现,键的唯一性依赖底层数据结构 哈希表。保证 插入的顺序和取出的顺序相同
package com.rupeng.map;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/** 验证插入、取出的有序性 */
public class HashMapDemo1
{
	public static void main(String[] args)
	{
		Map<String, String> map = new LinkedHashMap<String, String>();
		map.put("2011", "软件技术 一年级");
		map.put("2012", "软件技术 二年级");
		map.put("2015", "软件工程 四年级");
		map.put("2013", "软件技术 三年级");
		map.put(null, null);
		map.put(null, "我在逗你呢");
		map.put("2014", "软件工程 三 年级");

		Set<Map.Entry<String, String>> set = map.entrySet();
		for (Map.Entry<String, String> entry : set)
		{
			System.out.println(entry.getKey() + " : " + entry.getValue());
		}
	}
}
TreeMap

基于红黑树(Red-Black tree)的 NavigableMap 实现。该映射根据其键的自然顺序 Compare 进行排序【元素具有排序性】,者根据创建映射时提供的 Comparator 进行排序【集合具有排序性】,具体取决于使用的构造方法。

此实现为 containsKey、get、put 和 remove 操作提供受保证的 log(n) 时间开销。

注意,如果要正确实现 Map 接口,则有序映射所保持的顺序(无论是否明确提供了比较器)都必须与 equals 一致。(关于与 equals 一致 的精确定义,请参阅 Comparable 或 Comparator:(1)对于集合或映射两个对象 e1 和 e2 ,当且仅当 e1.equals(e2) 和 e1.compareTo(e2)==0 返回相同的 boolean 值时,类的自然排序才叫做与 equals 一致;(2)对于Comparator c 的两个元素 e1 和 e2 ,当且仅当 e1.equals(e2) 和 c.compare(e1, e2)==0 返回相同的 boolean 值时,Comparator c 提供的排序才叫做与 equals 一致。)。这是因为 Map 接口是按照 equals 操作定义的,但有序映射使用它的 compareTo(或 compare)方法对所有键进行比较,因此从有序映射的观点来看,此方法认为相等的两个键就是相等的。即使排序与 equals 不一致,有序映射的行为仍然是 定义良好的,只不过没有遵守 Map 接口的常规协定。

TreeSet 实现 键的唯一性 和 排序性,进而实现映射项的唯一和排序性。

构造器:

  • TreeMap()
    使用键的自然顺序构造一个新的、空的树映射。
  • TreeMap(Comparator<? super K> comparator)
    构造一个新的、空的树映射,该映射根据给定比较器进行排序。
  • TreeMap(Map<? extends K,? extends V> m)
    构造一个与给定映射具有相同映射关系的新的树映射,该映射根据其键的自然顺序 进行排序。
  • TreeMap(SortedMap<K,? extends V> m)
    构造一个与指定有序映射具有相同映射关系和相同排序顺序的新的树映射。

分别使用 自然排序和比较器排序实现下面的案例(对比 TreeSet 案例需求 自行完成)

案例1:TreeMap<String, String>

package com.rupeng.map;

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

public class TreeMapDemo
{
	public static void main(String[] args)
	{
		Map<String, String> map = new TreeMap<String, String>(
				new Comparator<String>()
				{
					@Override
					public int compare(String o1, String o2)
					{
						return 0;
					}
				});

		map.put("a", "Aa");
		map.put("v", "Vv");
		map.put("d", "Dd");
		map.put("l", "Ll");
		map.put("m", "Mm");
		System.out.println(map);
	}
}

说明: 键作为String类对象,String本身已经实现了 Comparable 接口,不需要提供 带有比较器的 TreeMap 构造了。 可以想一下,如果仍然定义比较器的话,会怎样呢? 答案是: 同时定义,为元素提供自然排序,并为集合提供比较器的话,使用的是 比较器排序 。举个例子:学生虽然可以按照身高排序,但是如果教室给定编号,按照编号坐座位,你还会按照身高坐座位吗?

案例2:HshMap<Student, String>

package com.rupeng.map;

import java.util.Map;
import java.util.TreeMap;

public class TreeMapDemo2
{
	public static void main(String[] args)
	{
		Map<Student, String> map = new TreeMap<>();

		Student stu1 = new Student("Joian", 18);
		Student stu2 = new Student("Cathy", 10);
		Student stu3 = new Student("Joian", 18);

		map.put(stu1, "高中");
		map.put(stu2, "大学");
		map.put(stu3, "初中");

		System.out.println(map);
	}
}

在这里插入图片描述
说明: 这个异常大家已经见惯了,表示说 希望的是 Comparable的实现子类的对象,给定的却不是。所以出现了问题。

解决方法:

  • 元素具有排序性:Student 实现 自然排序接口 Comparable 。构造器 :无参构造
  • 集合具有排序性:TreeMap 给定比较器实例 作为参数构造 TreeMap 实例
/** 改写Student,实现接口Comparable */
public class Student implements Comparable<Student>
{
	@Override
	public int compareTo(Student obj)
	{
		if (this == obj)
		{
			return 0;
		}

		Student other = (Student) obj;
		int result0 = name.compareTo(other.name);
		int result = (result0 == 0) ? age - other.age : result0;
		return result;
	}
}

给定了 “先按照姓名升序,姓名相同,按照年龄升序” 的排序
在这里插入图片描述

	/** 改写TreeMap的构造器,传递 Comparator实现类对象作为参数 */
	Map<Student, String> map = new TreeMap<>(new Comparator<Student>()
	{
		@Override
		public int compare(Student o1, Student o2)
		{
			if (o1 == o2)
			{
				return 0;
			}
			int result0 = o1.getName().compareTo(o2.getName());
			int result = (result0 == 0) ? (o2.getAge() - o1.getAge())
					: result0;
			return result;
		}
	});

给定了 “先按照姓名升序,姓名相同,按照年龄降序” 的排序
在这里插入图片描述

**说明:**根据上面两例的结果可以得出:同时,为元素提供自然排序,并为集合提供比较器的话,使用的是 比较器排序

综合案例

三个 实现子类都是单线程高效的,为了多线程的安全 可以使用 映射的同步方式去创建它们,以TreeMap 为例 Collections.synchronizedSortedMapMap(new TreeMap(...));

案例1:统计字符串中每一种字符出现的次数,并格式化打印,比如:“a(5)b(3)”,表示a出现5次、b出现3次
方式1:传统统计思想

package com.rupeng.map;

/**
 * 需求:统计字符串中每一种字符出现的次数,并格式化打印
 * 		 比如:"a(5)b(3)",表示a出现5次、b出现3次 方式1:传统统计思想
 * 
 * 分析:
 *   (1)定义一个字符串
 *   (2)获取每一个字符
 *   (3)统计每一个字符出现的次数
 *   (4)定义一个字符串生成器对象 
 *   (5)按照格式拼接
 *   (6)调用字符串生成器的toString,转换为字符串并打印
 */
public class Demo1
{
	public static void main(String[] args)
	{
		/** (1)定义一个字符串 */
		String str = "aasasdsadfasdasfgasewsfd";

		/** (2)获取每一中字符 */
		char[] chr0 = str.toCharArray();
		StringBuilder sb0 = new StringBuilder();
		for (int i = 0; i < chr0.length; i++)
		{
			if (!sb0.toString().contains(chr0[i] + ""))
			{
				sb0.append(chr0[i]);
			}
		}
		char[] chr = sb0.toString().toCharArray();

		/** (3)统计每一个字符出现的次数 */
		int[] nums = new int[chr.length];
		for (int i = 0; i < chr.length; i++)
		{
			for (int j = 0; j < chr0.length; j++)
			{
				if (chr[i] == chr0[j])
				{
					nums[i]++;
				}
			}
		}

		/** (4)定义一个字符串生成器对象 */
		StringBuilder sb = new StringBuilder();

		/** (5)按照格式拼接 */
		for (int i = 0; i < chr.length; i++)
		{
			sb.append(chr[i] + "(" + nums[i] + ")");
		}

		/** (6)调用字符串生成器的toString,转换为字符串并打印 */
		String outStr = sb.toString();
		System.out.println(outStr);
	}
}

方式2:定义一个映射Map<Character,Integer>

package com.rupeng.map;

import java.util.TreeMap;
import java.util.Map;
import java.util.Set;

/**
 * 需求:统计字符串中每一种字符出现的次数,并格式化打印
 * 		 比如:"a(5)b(3)",表示a出现5次、b出现3次 方式1:传统统计思想
 * 
 * 分析:
 *   (1)定义一个字符串
 *   (2)定义一个Map<Character,Integer>
 *   (3)获取字符串中的每一个字符
 *   (4)将获取的字符作为键在Map中获取值
 *   		a、返回 null ,说明不存在 添加 put(c,1)
 *   		b、返回 非null,说明已经存在,修改值 put(c,value+1) 
 *   (5)定义一个字符串生成器
 *   (6)按照格式拼接
 *   (7)调用字符串生成器的toString,转换为字符串并打印
 */
public class Demo2
{
	public static void main(String[] args)
	{
		/**   (1)定义一个字符串*/
		String str = "aasasdsadfasdasfgasewsfd";
		/**   (2)定义一个Map<Character,Integer>*/
		Map<Character,Integer> map = new TreeMap<Character,Integer>();
		/**   (3)获取字符串中的每一个字符*/
		char[] chr = str.toCharArray();
		/**   (4)将获取的字符作为键在Map中获取值*/
		for (char c : chr)
		{
			/**	a、返回 null ,说明不存在 添加 put(c,1)*/
			if(map.get(c) == null)
			{
				map.put(c,1);
			}
			/**	b、返回 非null,说明已经存在,修改值 put(c,value+1)*/
			else {
				map.put(c, map.get(c)+1);
			}
		}
		/**   (5)定义一个字符串生成器*/
		StringBuilder sb = new StringBuilder();
		/**   (6)按照格式拼接*/
		Set<Character> keyset = map.keySet();
		for (Character ch : keyset)
		{
			sb.append(ch).append("(").append(map.get(ch)).append(")");
		}
		/**   (7)调用字符串生成器的toString,转换为字符串并打印*/
		String outStr = sb.toString();
		System.out.println(outStr);
	}
}

案例2:集合的嵌套使用
1、HashMap嵌套HashMap
在这里插入图片描述

2、HashMap嵌套ArrayList
在这里插入图片描述

3、ArrayList嵌套HashMap
在这里插入图片描述

4、HashMap自身嵌套三层

面试题

0、两个对象值相同(x.equals(y) == true),但却可有不同的 hash code,这句话对不对?

解答::不对,如果两个对象 x 和 与满足 x.equals(y) == true ,它们的哈希码值(hash code)应当相同。Java对于equals和hashCode 方法是这样规定的:(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定相同;(2)如果两个对象的hashCode相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现,在使用容器时,相同的对象可以在Set集合中,同时增加新元素的效率会大大降低(对于使用哈希存储的系统,如果哈希码频繁的冲突,会造成存取性能急剧下降)。

补充:关于 equals 和 hasnCode 方法,很多 Java 程序员都知道,但很多也就仅仅是知道而已,在 Joshua Bloch 的大作 《Effective Java》中这样介绍 equals 方法:首先equals方法必须满足自反性(x.equals(x) 必须返回 true)、对称性(x.equals(y) 和 y.equals(x) 必须相同返回)、传递性(x.equals(y) 和 x.equals(z) 返回 true,x.equals(z) 必须返回 true)、一致性(x.equals(y) 在 没有改变 x、y信息的时候,必须永远具有相同的返回),而且对于任何非 null 值的引用 x , x.equals(null) 必须返回 false。实现高质量 equals 方法的诀窍包括:

  • (1) 使用 “==” 操作符检查 “参数是否为当前对象的引用”;
  • (1-2)对于任何非 null 值的引用 x , x.equals(null) 必须返回 false
  • (2)使用 instanceOf 操作符检查 “参数是否为正确的类型”;快捷重写中使用的是 getClass()方法
  • (3)对于类中的关键属性,检查参数传入对象的属性是否与之相等;
  • (4)编写完成 equals 方法后,问问自己它是否满足 对称性、传递性、一致性、自反性;
  • (5)重写 equals 总是要重写 hashCode 方法;
  • (6)不要将 equals 方法总的 Object 对象替换成任何的其它类型,在重写的时候,不要忘记 @Override 注解。

快捷生成的 Student 类的 equals 方法

/** (5)重写 equals 时,总是需要重写 hashCode 方法 */
@Override
public int hashCode()
{
	final int prime = 31;
	int result = 1;
	result = prime * result + age;
	result = prime * result + ((name == null) ? 0 : name.hashCode());
	return result;
}

@Override
public boolean equals(Object obj)
{
	/** (1)使用 == 检查参数是否为当前对象的引用 */
	/** 若是,直接返回 true 【自反性:x.equals(x) 为true】 */
	if (this == obj)
		return true;
	/** (1-2)检查参数是否为 null,为null 则必须返回 false */
	if (obj == null)
		return false;
	/** (2)使用 getClass 检查参数是否当前类的对象 */
	/** 若不是,直接返回 false 【也可使用 instanceOf 符号判断】*/
	if (getClass() != obj.getClass())
	/** 等价于:if(!(obj instanceOf Student)) */
		return false;
	/** 若是,进行强制类型转化,调用方法判断对应属性是否相同 */
	Student other = (Student) obj;
	/** (3)检查参数的关键属性,是否和当前对象相同 */
	/** 先进行基本数据类型属性的对比 */
	if (age != other.age)
		return false;
	/** 再进行引用数据类型属性的对比 */
	/** 满足 name == null ? obj.name == null : name.equals(obj.name) 时,为 true */
	if (name == null)
	{
		if (other.name != null)
			return false;
	} else if (!name.equals(other.name))
		return false;
	return true;
}

1、HashMap 和 Hashtable 的区别?

解答: HashMap 和 Hashtable 都实现了 Map 接口,所以有所有Map接口的特性,比如:底层数据结构 针对键有效,键唯一、值可重复等。存在以下的不同点:(1)HashMap 允许 键值为 null,Hashtable 则不允许;(2)HashMap 是不同步的,单线程高效,Hashtable 是同步的,多线程安全;(3)HashMap 提供了可供应用迭代的键的集合,因此,HashMap 是快速失败的。另一方面,Hashtable 提供了对键的列举(Enumeration);【迭代器和枚举接口的不同点】(4)一般认为 Hashtable 是一个遗留的类。

2、Enumeration 接口和 Iterator 接口的区别有哪些?

解答: Enumeration 速度是 Iterator 的2倍,同时占用更少的内存。但是,Iterator 远远比 Enumeration 安全,因为其他线程不能够修改正在被 iterator 遍历的集合里面的对象,会导致 快速失败的。同时,Iterator 允许调用者删除底层集合里面的元素,这对 Enumeration 来说是不可能的。Iterator 提供了书写更加简单的方法名,并且额外提供了一个移除操作,Eunmeration 与 Iterator 重复功能。新的实现应该优先 考虑Iterator而不是Enumeration。

3、List、Set、Map是否都继承了Collection接口?

解答: List、Set 是,Map 不是。Map 是键值对映射容器,与 List 和 Set 有明显的区别,而 Set 存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List 是线性结构的容器,适用于按数值索引访问元素的情形。

4、Collection、Collections的区别?

解答: Collection 接口 是单列集合的顶层接口,有子接口 List、Set、Queue等;Collections 工具类 提供了对集合进行操作的一系列静态方法,诸如:排序和二分查找等。

集合工具类 Collections

0:类的描述

public class Collections extends Object

此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。它包含在 collection 上操作的多态算法,即“包装器”,包装器返回由指定 collection 支持的新 collection,以及少数其他内容。

如果为此类的方法所提供的 collection 或类对象为 null,则这些方法都将抛出 NullPointerException。

1:功能概述

Collections 会根据集合或元素的排序性,提供两个相同功能的方法。

  • 排序功能
    该排序算法是一个经过修改的合并排序算法。此算法提供可保证的 n log(n) 性能。 此实现将指定列表转储到一个数组中,并对数组进行排序,在重置数组中对相应位置上的每个元素的列表上进行迭代。这避免了由于试图原地对链接列表进行排序而产生的 n2 log(n) 性能。

    • public static <T extends Comparable<? super T>> void sort(List<T> list) 根据列表中元素本身的自然排序进行 升序排序。元素必须实现Comparable,否则抛出异常 ClassCastException 。
    • public static <T> void sort(List<T> list, Comparator<? super T> c) 根据为列表提供的比较器进行 升序排序。列表中元素必须可相互比较,否则抛出异常 ClassCastException 。
  • 查找功能

    • public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) 使用二分搜索法搜索指定列表,以获得指定对象。在进行此调用之前,必须根据列表元素的自然顺序对列表进行升序排序(通过 sort(List) 方法)。如果没有对列表进行排序,则结果是不确定的。如果列表包含多个等于指定对象的元素,则无法保证找到的是哪一个。

    • public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c) 使用二分搜索法搜索指定列表,以获得指定对象。在进行此调用之前,必须根据指定的比较器对列表进行升序排序(通过 sort(List, Comparator) 方法)。如果没有对列表进行排序,则结果是不确定的。如果列表包含多个等于指定对象的元素,则无法保证找到的是哪一个。

    • 上述方法返回值:(1)存在 key,返回索引;(2)不存在 key,将第一个大于key的元素的索引作为插入点,并返回 -(插入点)-1 、如果key大于所有的元素,则返回 -list.size()-1

  • 最值功能(同样两种方法,自然排序和比较器排序)

    • 最小值
    • 最大值
  • 置换功能

    • public static void reverse(List<?> list) 逆向置换(反转)列表,线性时间运行
    • public static void shuffle(List<?> list) 根据默认的随机源 随机置换列表,线性时间运行

案例1:ArrayList 存储自定义对象 排序查找最值

package com.rupeng.collections;

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

public class Demo1
{
	public static void main(String[] args)
	{
		List<Student> list = new ArrayList<Student>();
		Student stu1 = new Student("Cathy", 21);
		Student stu2 = new Student("Joian", 22);
		Student stu4 = new Student("Du", 29);
		Student stu3 = new Student("Yzk", 37);
		list.add(stu3);
		list.add(stu2);
		list.add(stu1);
		list.add(stu4);
		System.out.println("---初始列表---");
		for (Student stu : list)
		{
			System.out.println(stu);
		}
		System.out.println("---排序之后的列表---");
		/** 元素的自然排序 */
		System.out.println("元素的自然排序");
		Collections.sort(list);
		for (Student stu : list)
		{
			System.out.println(stu);
		}
		/** 二分查找之前必须先进性对应的排序 */
		System.out.println("--查找 Cathy_21 的学生(自然排序)--");
		int index = Collections.binarySearch(list, new Student("Cathy", 21));
		if (index >= 0)
		{
			System.out.println("所在位置是  " + index);
		}
		/** 自然排序和比较器排序同时存在时,以比较器排序 */
		System.out.println("列表的比较器排序");
		/** 自定义的Comparator实现类,实现按照姓名长度升序 */
		MyComparator mc = new MyComparator();
		Collections.sort(list, mc);
		for (Student stu : list)
		{
			System.out.println(stu);
		}
		/** 二分查找之前必须先进性对应的排序 */
		System.out.println("--查找 Cathy_21 的学生(比较器排序)--");
		index = Collections.binarySearch(list, new Student("Cathy", 21), mc);
		if (index >= 0)
		{
			System.out.println("所在位置是  " + index);
		}
		System.out.println("--查找 \"最大\"的学生(自然排序)--");
		Student stuMax = Collections.max(list);
		System.out.println(stuMax);
		System.out.println("--查找 \"最大\"的学生(比较器排序)--");
		stuMax = Collections.max(list, mc);
		System.out.println(stuMax);
	}
}

案例2:模拟斗地主的洗牌、发牌

package com.rupeng.collections;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.TreeSet;

/*
 * 思路:
 * 		A:创建一个HashMap集合
 * 		B:创建一个ArrayList集合
 * 		C:创建花色数组和点数数组
 * 		D:从0开始往HashMap里面存储编号,并存储对应的牌
 *        同时往ArrayList里面存储编号即可。
 *      E:洗牌(洗的是编号)
 *      F:发牌(发的也是编号,为了保证编号是排序的,就创建TreeSet集合接收)
 *      G:看牌(遍历TreeSet集合,获取编号,到HashMap集合找对应的牌)
 */
public class PokerDemo
{
	public static void main(String[] args)
	{
		// 创建一个HashMap集合
		HashMap<Integer, String> hm = new HashMap<Integer, String>();
		// 创建一个ArrayList集合
		ArrayList<Integer> array = new ArrayList<Integer>();
		// 创建花色数组和点数数组
		// 定义一个花色数组
		String[] colors = { "♠", "♥", "♣", "♦" };
		// 定义一个点数数组
		String[] numbers = { "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q","K", "A", "2", };

		// 从0开始往HashMap里面存储编号,并存储对应的牌,同时往ArrayList里面存储编号即可。
		int index = 0;

		for (String number : numbers)
		{
			for (String color : colors)
			{
				String poker = color.concat(number);
				hm.put(index, poker);
				array.add(index);
				index++;
			}
		}
		hm.put(index, "小王");
		array.add(index);
		index++;
		hm.put(index, "大王");
		array.add(index);
		// 洗牌(洗的是编号)
		Collections.shuffle(array);
		// 发牌(发的也是编号,为了保证编号是排序的,就创建TreeSet集合接收)
		TreeSet<Integer> player1 = new TreeSet<Integer>();
		TreeSet<Integer> player2 = new TreeSet<Integer>();
		TreeSet<Integer> player3 = new TreeSet<Integer>();
		TreeSet<Integer> remain = new TreeSet<Integer>();
		for (int x = 0; x < array.size(); x++)
		{
			if (x >= array.size() - 3)
			{
				remain.add(array.get(x));
			} else if (x % 3 == 0)
			{
				player1.add(array.get(x));
			} else if (x % 3 == 1)
			{
				player2.add(array.get(x));
			} else if (x % 3 == 2)
			{
				player3.add(array.get(x));
			}
		}
		// 看牌(遍历TreeSet集合,获取编号,到HashMap集合找对应的牌)
		lookPoker("Player1", player1, hm);
		lookPoker("Player2", player2, hm);
		lookPoker("Player3", player3, hm);
		lookPoker("Remain", remain, hm);
	}
	// 写看牌的功能
	public static void lookPoker(String name, TreeSet<Integer> ts,
			HashMap<Integer, String> hm)
	{
		System.out.print(name + ":");
		for (Integer key : ts)
		{
			String value = hm.get(key);
			System.out.print(value + " ");
		}
		System.out.println();
	}
}

集合框架

在这里插入图片描述

通用方法

在这里插入图片描述

性能测试

List性能比较

在这里插入图片描述

Set性能比较

在这里插入图片描述

Map性能比较

在这里插入图片描述

JavaSE-基础练习题 前言 部分练习题摘录自:Sun-Java程序员认证考试题库 部分练习题加入了自己的解析 还未整理完,待更 若有错误或补充,欢迎私信 一、Java入门 下列哪项不是JDK所包含的内容?(选一项)C A. Java编程语言 B. 工具及工具的API C. Java EE扩展API D. Java平台虚拟机 下列关于JDK、JRE和JVM的描述。哪项正确?D A. JDK中包含了JRE,JVM中包含了JRE B. JRE中包含了JDK,JDK中包含了JVM C. JRE中包含了JDK,JVM中包含了 阅读详情

相关推荐

JavaSE进阶基础这一篇就够了,你值得拥有!

本Java视频教程适合有一定编程语言基础的学员观看,在本Java视频教程中讲解了Java开发环境搭建、Java基础语法、面向对象、异常、数组、常用类、集合、IO流、线程、反射机制。该Java视频教程为讲师上课实录,共60个课时(1小时为1个课时),讲师语言表达流利,思路清晰。如果您是有一门编程语言基础的,或者您现在是一名Java程序员且想巩固一下JavaSE的,请打开Java视频教程观看吧!...

f5465245的博客 1万+

java集合替换集合_Java集合–您必须知道的13件事

java集合替换集合Java Collections Framework is one of the core parts of the Java programming language. Collections are used in almost every programming language. Most of the programming languages support var...

从零开始的教程世界 1576

JavaSE 集合进阶

JavaSE 集合进阶1. Collection1.1 集合知识回顾1.2 集合类体系结构1.3 Collection 集合概述和使用1.4 Collection 集合常用方法1.5 Collection 集合的遍历2. List3. Set4. 泛型5. Map6. Collections 1. Collection 1.1 集合知识回顾 集合类的特点: 提供一种存储空间可变的存储模型,存储的数据容量可以随时发生改变 1.2 集合类体系结构 1.3 Collection 集合概述和使用 Collecti

苦瓜的博客 1418

JavaSE基础(9)——集合框架、集合算法类、比较器

JavaSE基础(9)——集合框架、集合算法类、比较器

qq_64751004的博客 1827

相机标定-张正友棋盘格标定法

(X,Y,Z)为在世界坐标系下一点的物理坐标 ( u , v ) 为该点对应的在像素坐标系下的像素坐标 引入齐次坐标的原因:引入齐次坐标的目的是为了升维,将坐标从二维坐标变为三维坐标。 2.相机成像过程 相机成像系统中,共包含四个坐标系:世界坐标系、相机坐标系、图像坐标系、像素坐标系。 (1)世界坐标系(,,) 描述目标在真实世界中的位置引入的参考坐标系。 (2)相机坐标系(,,) ...

qq_41275123的博客 1万+

NVIDIA Jetson Orin系列 - 使用SDK Manager安装JetPack全流程解析

本文详细解析了使用NVIDIA官方SDK Manager为Jetson Orin系列开发板安装JetPack系统的完整流程。从环境准备、设备进入恢复模式,到SDK Manager图形化界面的分步操作指南,提供了新手友好的全流程解析,并分享了安装后的验证方法及常见问题解决方案,帮助开发者高效搭建稳定的边缘AI开发环境。

nn3456的博客 406

24-08-04 JavaSE java集合详解

传统的数组每次都要提前设置好容量,而且从某些方面来讲只能存储同一类型的元素,如果存储大小不够的话只能重新开辟一个新数组然后把元素移动到新数组中,非常麻烦*,java集合类分为两大类,一个是单列集合Collection,一个是双列集合Map

2301_79291071的博客 2490

学习笔记:CNN中模型复杂度

CNN有卷积层和全连接层构成。对于一个卷积层,设输入通道数未CinC_{in}Cin​,输出通道数未CoutC_{out}Cout​,卷积核kernel size为Kw×KhK_w\times K_hKw​×Kh​,输出的特征图尺寸是H×WH\times WH×W 。全连接层的输入通道数为NinN_{in}Nin​,输出通道数为NoutN_{out}Nout​. 时间复杂度 用每秒钟浮点数运算量衡量。 FLOPs指的是floating-point operations per second,每秒钟浮点数运算

keep用心的博客 623

JavaSE 集合基础

JavaSE 集合基础集合概述 集合概述

苦瓜的博客 701

详解APM的开方控制器sqrt_controller

前言 前面说过,sqrt_controller是对P项进行整定用途的,目的就是让P项的控制响应“软”下来,实际上就是一个经过改进的P控制器。读懂了sqrt_controller,那么你对APM的P控制器就了解得差不多了。 仅讨论P控制器,sqrt_controller的作用就是设定一个量linear_dist,以这个量的正负值为分界线划分出一个线性区域范围,在这个范围内表示误差量还很小,遵循Kp*error计算方式输出控制量。 超过linear_dist后,表明误差较大,此时不能由着误差单纯按照线性方式乘以

moumde的博客 4896

Qt开发中彻底解决中文乱码问题

中文乱码问题主要源于编码不一致需要同时设置控制台和Qt应用程序的编码确保项目文件和环境配置的编码一致文件操作时注意指定正确的编码。

qq_53832058的博客 3404

JavaSE - 集合类-单列集合框架

JavaSE - 集合类-单列集合框架 本节学习目标: 了解Java单列集合框架结构; 了解并掌握Collection接口及其方法; 了解并掌握List集合(接口)及其方法; 了解并掌握Set集合(接口)及其方法; 了解并掌握Queue集合(接口)及其方法; 了解并掌握单列集合实现类的特性和优缺点。 1. 单列集合框架结构 单列集合指只有一列数据的集合,单列集合中的每个数据被称为元素(Element),单列集合使用泛型E限制元素的数据类型。 Java单列集合框架由Collection根接口,继承Coll

adas5f1a51的博客 908

如何使用 ESP32 CAM 进行自动车牌识别 (ANPR)

用于提取 JSON 值并在 OLED 上显示文本的函数。有一个可用的函数,即 extractJsonStringValue 来提取字符串值。它是独立函数,但您仍然可以使用一些 JSON 库来执行相同的功能。

jkh920184196的博客 1304

JavaSE集合框架

JavaSE集合框架 Collection接口 子接口 List ArrayList 数组实现,线程不安全,查询快,增删慢 , 元素有序,有下标, 元素可重复 LinkedList 链表实现,线程不安全,查询慢,增删块,可以通过Collections.synchronizedList()实现线程安全 Vcetor 数组实现,线程安全,效率低,jdk1.0的古老类不推荐使用...

Java学习笔记 495

从数据库到数据仓库:数据仓库导论

本文为数据仓库导论,旨在介绍数据仓库的基本理念和应用场景,帮助读者理解数据仓库的重要性及其在企业中的实际应用。

探索大数据世界 - 深入解析数据存储、分布式计算与人工智能 2784

足式机器人全身 MPPI: 利用 MPPI 对足式机器人进行实时全身控制

本研究首次在真实四足机器人上成功部署了基于全身采样的模型预测控制(MPC)系统。采用模型预测路径积分(MPPI)算法,通过并行采样和评估控制轨迹实现实时运动规划。该系统能处理复杂接触场景,完成推箱子、爬越障碍及崎岖地形行走等任务。实验使用Unitree Go1机器人,结合MuJoCo仿真器快速生成运动策略,仅需简单控制策略即可实现稳健操作。硬件验证表明,该方法能有效完成目标导向的箱子推动任务。该成果标志着采样MPC在足式机器人实际应用中的重要突破。

2506_90492529的博客 1000

24-08-08 JavaSE Map集合

Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value(双列元素)Map 中的 key 和 value 可以是任何引用类型的数据,会封装到HashMap$Node 对象中Map 中的 value 可以重复Map 的key 可以为 null, value 也可以为null ,注意 key 为null,只能有一个,value 为null ,可以多个常用String类作为Map的 key。

2301_79291071的博客 876

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

JavaSE集合

无

weixin_71001270的博客 1350

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

JavaSE集合框架之Map集合

JavaSE集合框架之Map集合 1.Map集合概述和特点 A: 需求:根据学号获取学生姓名 B:Map接口概述 查看API可以知道: 将键映射到值的对象 一个映射不能包含重复的键 每个键最多只能映射到一个值 C:Map接口和Collection接口的不同 Map是双列的,Collection是单列的 Map的键唯一,Collection的子体系Set是唯一的 Map集合的数据结构针对键有效,跟值无关;Collection集合的数据结构是针对元素有效 2.Map集合的功能概述 A:Map

ITzhazhassc的博客 327

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

08-javase-集合框架

08-javase-集合框架-ydl-笔记 文章目录08-javase-集合框架-ydl-笔记一、集合大纲1.集合的继承结构2.常用接口介绍二、集合的增删改查三、源码分析1. ArrayList(1)成员变量(2)构造器(3)add方法(4)查找和删除2. LinkedList3. HashMap4. HastSet四、集合的遍历1.普通for循环2.迭代器3.增强for循环4.迭代中删除元素五、其他的集合类1. LinkedHashMap2. TreeMap3. Collection六、线程安全问题1.

cygqtt的博客 217

JavaSE 拾遗(8)——JavaSE 集合框架

集合概述 这篇博客记录 javase 集合相关的内容。javase 集合部分主要有 javase 集合框架相关的内容和 javase 泛型相关的内容。集合是用来做什么的呢,集合主要是用来对现实世界中多个对象在一起进行统一描述的。在现实世界中,常常我们会对多个在一起的对象进行操作描述,比如1000学生的资

分享美好的专栏 3042

【JavaSE系列】Java集合框架之Collection,Map接口

本篇文章带大家认识Java基础知识——集合框架,从本篇文章开始正式拉开数据结构的序幕,要深入了解一件事,那必然先得见个面,本文带大家认识Java集合框长什么样,以及怎么使用Collection和Map接口。

未见花闻的博客 1832

【JAVASE基础】集合框架

2.1集合框架 2.1集合框架由来 JDK1.2版本后,出现这个集合框架到DK1.5后,大幅度优化. ●集合本质上是存储对象的容器 ●数组也能存储对象,数组弊端就是定长 ●解决数组的问题开发出来集合框架,集合框架无需考虑长度 I ●集合和数组的区别与共同点 。集合,数组都是容器,都可以存储数据 。集合只存储引用数据类型,不存储基本数据类型 。数组可以存储基本类型,也可以存储引用类型 。数组定长,集合容器变成 牢记:数据多 了存数组,对象多了存集合 ●集合学习的关键点 。怎么存储数据 。怎么取出数据 。选择哪

yu_fu_a_bu的博客 490

【JavaSE】之基础语法

Java基础语法介绍,注释,标识符和关键字,数据类型,类型转换,变量与常量,基本运算等~

小新要变强的博客 7565

【JavaSE 集合框架的继承体系】

6. 集合框架的继承体系: ①第一个派系: Collection (集合) 顶级接口 单列集合(一次只能存一个对象) List (列表) 接口 ArrayList (数组列表) 实现类 LinkedList (链表) 实现类 Vector (数组列表) 实现类,已过时 Set (集) 接口 HashSet (哈希表) 实现类 LinkedHashSet (链表哈希表) 实现类,继承自 HashSet TreeSet (红黑树) 实现类 ②第二大派系: Map (

Jade_Heng_Star的博客 402

JavaSE.08.集合

JavaSE.08.集合 1.Collection Collection采用线性列表的储存方式,单列数据 1.集合,数组都是对多个数据进行存储操作的结构,简称Java容器 说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg...) 2.1数组在存储多个数据方面的特点 数组初始化以后长度就确定了 数组声明的类型,就决定了进行元素初始化的类型 2.2数组在存储多个数据方面的弊端 一旦初始化以后,其长度不可修改 数组中提供的方法非常有限

OrangeNotFat的博客 218
上一篇: 【JavaSE基础】07-集合框架(二)
下一篇: 【JavaSE基础】09-网络编程
老坛算粉
博客等级 码龄12年 708粉丝 73原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

老坛算粉

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

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

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

打赏作者

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

抵扣说明:

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

余额充值