1. Set集合
1.1 基本概念
- 是Collection接口的子接口,和List接口平级
- 元素不能重复,且无序
- 主要实现类:
- HashSet:底层是哈希表
- TreeSet:底层是红黑树
- LinkedHashSet:底层是哈希表+双向链表
- 双向链表:用于记录元素放入集合中的先后顺序,便于迭代
1.2 Set的常用方法
- 参考Collection集合中的方法即可
1.3 HashSet
-
是set接口的主要实现类 ;非线程安全;可以存储null值
-
HashSet 底层的实现其实是一个 java.util.HashMap 支持
// 构建一个hashSet Set<String> hashSet = new HashSet<String>(); // 调用的构造方法,实际上是创建了一个HashMap public HashSet() { map = new HashMap<>(); }
-
HashSet存储
-
根据 HashSet 中元素的
哈希值
,来确定元素在集合中的位置 -
底层是希表存
-
必须重写hashCode、equals方法
-
重写equals方法:
用equals判断两个对象是否相等,比较的是地址值。重写equals方法后,判断的是这两个对象的属性是否相等
-
重写hashCode方法:
保证同一个元素能得到唯一的哈希码。这样,同一个元素只有一个哈希值,只需要和索引位置的元素进行比较即可,从而提高了效率,避免元素重复
-
-
哈希表的大致结构图:
-
在
JDK1.8
之前,哈希表底层采用数组+链表实现 (数组中存放的是链表头结点的地址) -
元素通过
HashCode()
,得到一个哈希码。该哈希码通过哈希算法,得到哈希值 -
哈希值通过一系列算法,得到哈希值在数组中索引,并将元素添加到到该数组中
-
若数组中已经存放链表了,
-
先用该元素的哈希值和整条链表的所有节点进行比较,若没有相同,则将该元素添加到尾节点
-
如果有节点,与之哈希值相同!!那么调用
equal()
,判断这两个元素是否相同。若是没有,则将该元素添加到尾节点 -
如果有节点,与之哈希值相同!!那么插入失败( 所谓的元素不能重复 )
-
-
-
举例:用HashSet存储用户自定义的类
-
自定义用户类
public class Student { private String name; private int age; // ......(还有get、set、构造方法,就省略了) @Override public boolean equals(Object o) { // ...... (重写equals方法) } @Override public int hashCode() { return Objects.hash(name, age); } }
-
构建哈希表,并进行存储
-
在下面的代码中,构建了 HashSet集合,并进行添加元素
-
但是我们发现,第二次没有添加成功 ( 是因为自定义的Student类重写了hashCode、equals方法 )
// 构建HashSet来存储Student类型对象 HashSet<Student> stuSet = new HashSet<Student>(); //存储测试 Student stu = new Student("于谦", 43); stuSet.add(stu); stuSet.add(new Student("于谦", 43));
-
-
添加不同元素
stuSet.add(new Student("郭德纲", 44)); stuSet.add(new Student("郭麒麟", 23));
-
遍历HashSet
// 遍历列表 for (Student stu2 : stuSet) { System.out.println(stu2); }
-
执行结果:
Student [name=郭德纲, age=44]
Student [name=于谦, age=43]
Student [name=郭麒麟, age=23]
案例总结:
- 根据执行结果,我们可以看到,相同的name=于谦的类,没有添加成功
- HashSet集合是无序的
- 自定义类,一定要重写hashCode、equals方法
1.4 TreeSet
-
二叉树:
-
基本概念:
-
有序二叉树:
- 左子树中任意一个节点 < 根节点(30) < 右子树中的任意一个节点
- 左、右子树内部,也遵循 左< 根 < 右
-
-
红黑树,就是有序二叉树
- 所以,当有新元素插入TreeSet时,实际上就是把新元素和集合中元素依次比较,来确定合理的位置,保证插入后的集合,还是有序二叉树
-
比较元素大小的规则,有两种方式:
-
自然排序:
-
让集合中的元素,实现
java.lang.Comparable
接口 ( 重写compareTo方法 )// Comparable接口 public interface Comparable<T> { public int compareTo(T o); }
-
-
比较器排序:
-
在TreeSet的构造方法中,将
java.util.Comparator
接口作为参数传入public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); }
-
准备一个比较器对象,作为参数传入构造方法:
- 使用Comparator接口的匿名内部类
- 使用Comparator接口的实现类
-
-
两种排序方式的一个对比:
- 自然排序方式,比较单一
- 比较器排序,则多元化 ( 优先使用比较器排序 )
-
-
举例说明:
- 下列代码执行结果为: [a, b, c]
- [a, b, c] ,满足有序二叉树的规则
// 构建HashSet Set<String> strSet = new HashSet<String>(); strSet.add("a"); strSet.add("c"); strSet.add("b"); System.out.println(strSet.toString());
思考:将元素插入二叉树中,实际上是将元素同集合中的元素进行比较,确保二叉树有序。但是上述代码中,并没有指定比较类型,那他们是如何进行比较的?
-
查看源码:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
我们发现String类型,实现了
java.lang.Comparable
接口。原来,默认的是使用自然排序。同时,Integer、Short…等都实现了public final class Integer extends Number implements Comparable<Integer>
-
结论:
除了自定义类,默认都是实现了
java.lang.Comparable
接口。所以,自定义类想要使用TreeSet集合,那么必须在自然排序,比较器排序中,选择一种实现。
- 下列代码执行结果为: [a, b, c]
-
使用 TreeSet集合,存储自定义类Student (一定要选择排序方式)
- 第一种方式:实现Comparable接口
public class Student implements Comparable<Student>{ private String name; private int age; // ......(还有get、set、构造方法,就省略了) // 实现了Comparable接口那么就要重写compareTo public int compareTo(Student obj){ // 先根据姓名比较。如果姓名相等,那么比较年龄 int i = this.getName().compareTo(obj.getName(); return 0 != i ? i : this.getAge() - obj.getAge(); } }
compareTo方法,返回值说明:
- 0: 待插入的对像 - 参数对象 = 0 (插入失败)
- 负数:待插入的对像 - 参数对象 < 0 , 说明待插入的对像 < 参数对象
- 正数:待插入的对像 - 参数对象 > 0 , 说明待插入的对像 > 参数对象
Set<String> trSet = new TreeSet<String>(); stuSet.add(new Student("悟空", 43)); stuSet.add(new Student("八戒", 43));
-
第二种方式:在TreeSet的构造方法中,将Comparator 接口作为参数传入
public class Student { private String name; private int age; // ......(还有get、set、构造方法,就省略了) // 重写hashCode和qeuals @Override public boolean equals(Object o){ // ...略 } @Override public int hashCode() { return Objects.hash(name, age); } }
在构建TreeSet时,将Comparator接口使用匿名内部类,作为参数传入:
Set<Student> stuSet = new TreeSet<Student>(new Comparator<Student>() { @Override // o1,表示待插入集合的元素对象;o2,表示集合中已有的对象 public int compare(Student o1, Student o2) { int i = o1.getName().compareTo(o2.getName()); return 0 != i ? i : o1.getAge() - o2.getAge(); } }); stuSet.add(new Student("悟空", 43)); stuSet.add(new Student("八戒", 43));
而且从Java8开始,支持Lambda表达式:( 参数列表 ) -> { 方法体 }
Set<Student> trSet = new TreeSet<Student>((Student o1, Student o2) -> { int i = o1.getName().compareTo(o2.getName()); return 0 != i ? i : o1.getAge() - o2.getAge(); });
2. Map集合
2.1 基本概念
-
Map集合中,存取基本单位是键值对
-
Map在大量的数据下,具有优良的查询性能 (先找Key,Key找不着,就不用去找Value了)
-
key是不能重复的 ( Key:目录,value:目录对应的内容 。目录是不能重复的)
-
Map接口的主要实现类:
-
HashMap类:底层是,哈希表
- LinkedHashMap是HashMap的子类
- LinkedHashMap类:底层是,哈希表 + 双向链表
-
TreeMap类:底层是,红黑树
-
HashTable类:
-
古老的Map实现类,key、value的值不能为null
-
线程安全,效率低下
-
Properties 类是Hashtable 的子类:
-
用于处理配置文件
-
Properties 里的key和value 都是字符串类型
-
存取数据时,建议使用
setProperty(String key,String value)
方法和getProperty(String key)
方法
-
-
-
-
面试题:
为什么有HashMap了,还要有LinkedHashMap?
- HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的
- LinkedHashMap,询速度快,而且存放的顺序有序
LinkedHashMap为什么存放有序?
- 在HashMap的基础上,每一个键值对对象引用的一前一后,都添加了一对指针,指向了前一个元素的引用和后一个元素的引用。
HashMap 和 Hashtable 的区别
- Hashtable 线程安全效率低下,HashMap 与之相反
Set集合和Map集合的关系
-
Set集合底层是Map集合
-
Set集合在添加元素的时候,实际上是往Map的key中添加,而Value则添加默认值:
new Object()
-
以TreeSet为例
// 底层是Map集合 public TreeSet() { this(new TreeMap<E,Object>()); }
add方法
private static final Object PRESENT = new Object(); public boolean add(E e) { // 元素是往 key添加,而Value值是一个默认值PRESENT return m.put(e, PRESENT)==null; }
-
1.2 Map集合中的常用方法
方法声明 | 功能介绍 |
---|---|
put(K key, V value) | 向key值对应的value中添加元素。若value存在,则替换掉 |
get(Object key) | 返回与参数key对应的value值,所示不存在,返回null |
public boolean containsKey(Object key) | 判断集合中是否包含指定的key |
public boolean containsValue(Object value) | 判断集合中,是否包含指定的value值 |
remove | 对指定key进行删除 |
keySet | 获取集合的所有键,存放到一个set集合 |
values | |
Set<Map.Entry<K,V>> entrySet() | 获取Map集合中所有 key-value对象,存放到set集合 |
1.2.1 Map集合的遍历
-
方法1:遍历key
// 将key存放在set集合 Set<Integer> stuKeys = studentMap.keySet(); // 遍历key,来获取对应的value for (Integer key: stuKeys){ // 根据键值,来求 value String value = studentMap.get(key); System.out.print(value + ","); }
-
方法2:遍历key-value
// 将所有键值对对象存放在set集合 Set<Map.Entry<Integer, String>> keyValueObj = studentMap.entrySet(); // 遍历键值对 for (Map.Entry<Integer, String> objEntry : keyValueObj) { // 获取单个键值对的key、value Integer key = objEntry.getKey(); String value = objEntry.getValue(); System.out.println(key + " - " + value); }
1.3 Map 集合扩容分析
-
构建Map,调用构造方法
HashMap<String, Integer> s = new HashMap<String, Integer>(); // 源码: static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; static final float DEFAULT_LOAD_FACTOR = 0.75f; int threshold; public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
参数说明:
DEFAULT_LOAD_FACTOR
: 加载因子,默认是 0.75DEFAULT_INITIAL_CAPACITY
:默认容量16 。是指哈希表中数组的长度threshold
:扩容的临界值,threshold = 加载因子 * 默认容量 = 16 * 0.75 = 12。也就是说,数组中有12个元素,我们就对数组进行扩容
3.Collections工具类
方法声明 (注:参数 o 指集合 ) | 功能简介 |
---|---|
Collections.max(Object o); | 返回集合中的最大元素 |
Collections.min(Object o); | 返回集合中的最小元素 |
Collections.reverse(Object o); | 反转集合中的元素 |
Collections.swap(Object o, int startIndex, int endIndex); | 实现两个元素的交换。在集合o中,把startIndex索引、endIndex索引两处的元素进行交换 |
Collections.sort(Object o); | 实现元素的排序 |
Collections.shuffle(Object o); | 打乱集合中元素的顺序 |
Collections.copy(Object o1, Object o2); | 两个集合之间,元素的拷贝。将 o2中的元素,拷贝到o1中 |
注意:
- 在使用copy方法时,源集合的中现有的元素个数 > 目标集合中的先有个数。不然会索引越界异常
- 值得注意的是,这里并不是指容量,而是指集合汇总存在的个数
- 那么我们新建一个空集合,想要使用copy,最好用
Arrays.asList
的方式创建
List<Integer> list = Arrays.asList(new Integer[10]);
System.out.println(list);
结果为: