- Set 接口
- 0:接口说明
- 1:功能概述
- 2、实现子类
- HashSet
- LinkedHashSet
- TreeSet
- Comparable 自然排序接口
- Comparator 比较器接口
- Map 接口
- 0:接口说明
- 1:功能概述
- 2:实现子类
- HashMap
- LinkedHashMap
- TreeMap
- 综合案例
- 面试题
- 集合工具类 Collections
- 0:类的描述
- 1:功能概述
- 集合框架
- 通用方法
- 性能测试
- List性能比较
- Set性能比较
- Map性能比较
public interface Set
Set 接口是对数学中的集合的抽象,满足集合的一般性质:
- 无序性(进出顺序)
- 唯一性(不包含重复元素)
- 至多一个null值元素(可以没有)
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上面)】
2、实现子类 HashSet相同点:
- 两者同时继承了 Collection 的15个基本操作方法,以及作为集合的特性(可变的数组:数据类型可变、元素个数可变)
- Set 没有 关于索引的操作,以及读写器,源于Set的无序性
public class HashSet
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 元素唯一性的原因:
- 先看 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 set = new HashSet();
set.add(stu1);
set.add(stu2);
set.add(stu3);
set.add(stu4);
for (Iterator 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())
public class LinkedHashSet
a、可预知迭代顺序的 Set接口,底层是哈希表和链表的实现
b、元素有序且唯一,允许 null 元素【哈希表保证元素的唯一性,链表保证元素的有序性】,增删效率高
c、按照链表定义的迭代顺序与其插入顺序相同
d、单线程高效,安全需要时,使用同步Set集合方式定义 Collections.synchronizedSet(new LinkedHashSet(...)); ,同样存在快速失败的 并发修改异常 ConcurrentModificationException。
TreeSetpublic class TreeSet
public interface NavigableSet
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 set = new TreeSet();
set.add("Javase");
set.add("Rupeng");
set.add("JoianSUN");
set.add("Shanhai");
set.add("20150705");
for (Iterator 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 set = new TreeSet();
set.add(new Student("Joian", 18));
set.add(new Student("Du", 27));
set.add(new Student("Yzk", 10));
for (Iterator 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{ @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 对象,并对代码进行了重构 Setset = new TreeSet (new Comparator () { @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;
public class TreeSetDemo3
{
public static void main(String[] args)
{
Set set = new TreeSet(new Comparator()
{
@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 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)
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;
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 set = new TreeSet(new Comparator()
{
public int compare(Student o1, Student o2)
{
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{ 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);
比较当前对象与指定对象的顺序。如果当前对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
实现整体排序,实际的底层 是 常见的排序算法。
Comparator 比较器接口注意:ClassCastException 异常,如果 o 无法转换为指定类的对象。
底层源码:Java 1.8 之前
public interface Comparator{ // 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
Map 接口是 键映射到值的对象。 键具有唯一性,值可重复 。 Map 实际对应的是 数学中函数。键为自变量,值为因变量。 Map 中的每一个键值对,实际就是 函数中每一个自变量与所得因变量的对应关系,这种对应关系在 Map 中的体现是 就是键值对 public static interface Map.Entry
public static interface Map.Entry
-
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
keySet() - 值的集合:值可重复 public Collection
values() - 映射项的集合:根据键值的点,键值对(映射项)具有唯一性 public Set
> 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 是可重复的;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
keySet() - 值的集合:值可重复 public Collection
values() - 映射项的集合:根据键值的点,键值对(映射项)具有唯一性 public Set
> entrySet()
获取映射中的项,必须使用 迭代器
- 键集合 + get 方法
- 键值集合 + Entry
获取功能
案例:遍历映射实例
- 方式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 map = new HashMap();
map.put(1, "Joian");
map.put(2, "Yzk");
map.put(3, "Cathy");
map.put(4, "Apple");
Set set = map.keySet();
for (Integer in : set)
{
System.out.println(in + " " + map.get(in));
}
}
}
- 方式2:键值集合 + Entry
获取功能
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 map = new HashMap();
map.put(1, "Joian");
map.put(2, "Yzk");
map.put(3, "Cathy");
map.put(4, "Apple");
Set> set = map.entrySet();
for (Iterator> it = set.iterator(); it.hasNext();)
{
Map.Entry 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
- 案例2:HashMap
自行完成
说明: 你会觉得,结果好像是 按照键 排序的,but you are wrong !只是存储的顺序而已。HashMap 不保证 映射的顺序,特别是不保证其顺序很久不变。插入和取出的顺序 是不一样的。说明了 HashMap 的无序性,你也可以随便测试一下 允许 null 键值。【Integer 和 String 重写了 Object 类的equals 和hashCode 方法】
案例3:HashMap
package com.rupeng.map;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class HashMapDemo3
{
public static void main(String[] args)
{
Map map = new HashMap();
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> set = map.entrySet();
for (Entry entry : set)
{
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
说明: 键相同 只添加一次,之后的操作是 进行 值的修改。 映射项的个数 不会改变 。
案例4:HashMap
package com.rupeng.map;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class HashMapDemo4
{
public static void main(String[] args)
{
Map map = new HashMap();
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 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 map = new LinkedHashMap();
map.put("2011", "软件技术 一年级");
map.put("2012", "软件技术 二年级");
map.put("2015", "软件工程 四年级");
map.put("2013", "软件技术 三年级");
map.put(null, null);
map.put(null, "我在逗你呢");
map.put("2014", "软件工程 三 年级");
Set> set = map.entrySet();
for (Map.Entry 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
m)
构造一个与指定有序映射具有相同映射关系和相同排序顺序的新的树映射。
分别使用 自然排序和比较器排序实现下面的案例(对比 TreeSet 案例需求 自行完成)
案例1:TreeMap
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 map = new TreeMap(
new Comparator()
{
@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
package com.rupeng.map;
import java.util.Map;
import java.util.TreeMap;
public class TreeMapDemo2
{
public static void main(String[] args)
{
Map 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 实例
public class Student implements Comparable{ @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; } }
给定了 “先按照姓名升序,姓名相同,按照年龄升序” 的排序
Mapmap = new TreeMap<>(new Comparator () { @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;
public class Demo1
{
public static void main(String[] args)
{
String str = "aasasdsadfasdasfgasewsfd";
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();
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]++;
}
}
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < chr.length; i++)
{
sb.append(chr[i] + "(" + nums[i] + ")");
}
String outStr = sb.toString();
System.out.println(outStr);
}
}
方式2:定义一个映射Map
package com.rupeng.map;
import java.util.TreeMap;
import java.util.Map;
import java.util.Set;
public class Demo2
{
public static void main(String[] args)
{
String str = "aasasdsadfasdasfgasewsfd";
Map map = new TreeMap();
char[] chr = str.toCharArray();
for (char c : chr)
{
if(map.get(c) == null)
{
map.put(c,1);
}
else {
map.put(c, map.get(c)+1);
}
}
StringBuilder sb = new StringBuilder();
Set keyset = map.keySet();
for (Character ch : keyset)
{
sb.append(ch).append("(").append(map.get(ch)).append(")");
}
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 方法
@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)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
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;
}
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
> void sort(List list) 根据列表中元素本身的自然排序进行 升序排序。元素必须实现Comparable,否则抛出异常 ClassCastException 。 - public static
void sort(List list, Comparator super T> c) 根据为列表提供的比较器进行 升序排序。列表中元素必须可相互比较,否则抛出异常 ClassCastException 。
- public static
-
查找功能
-
public static
int binarySearch(List extends Comparable super T>> list, T key) 使用二分搜索法搜索指定列表,以获得指定对象。在进行此调用之前,必须根据列表元素的自然顺序对列表进行升序排序(通过 sort(List) 方法)。如果没有对列表进行排序,则结果是不确定的。如果列表包含多个等于指定对象的元素,则无法保证找到的是哪一个。 -
public static
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 list = new ArrayList();
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("列表的比较器排序");
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;
public class PokerDemo
{
public static void main(String[] args)
{
// 创建一个HashMap集合
HashMap hm = new HashMap();
// 创建一个ArrayList集合
ArrayList array = new ArrayList();
// 创建花色数组和点数数组
// 定义一个花色数组
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 player1 = new TreeSet();
TreeSet player2 = new TreeSet();
TreeSet player3 = new TreeSet();
TreeSet remain = new TreeSet();
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 ts,
HashMap hm)
{
System.out.print(name + ":");
for (Integer key : ts)
{
String value = hm.get(key);
System.out.print(value + " ");
}
System.out.println();
}
}
集合框架
通用方法
性能测试
List性能比较
Set性能比较
Map性能比较



