目录
一、⭐️Java集合框架概述
二、⭐️Collection接口方法
三、⭐️Iterator迭代器接口
1. Iterator接口中的方法
2. foreach循环遍历集合元素
四、⭐️Collection子接口 : List
1. List的接口框架
2. ArrayList的源码分析
2.1 JDK 7中
2.2 JDK 8中
2.3 小结
3. LinkedList的源码分析
4. Vector的源码分析
5. List接口中的方法
五、⭐️Collection子接口 : Set
1. Set的接口框架
2. 如何理解Set接口特点
3. 添加元素的过程
4. hashCode()方法的重写
5. HashSet的使用
6. LinkedHashSet的使用
7. TreeSet的使用
7.1 TreeSet的自然排序
7.2 TreeSet的定制排序
六、⭐️Map接口
1. Map的接口框架
2. 关于Map结构的理解
3. HashMap的底层实现原理
3.1 JDK 7中
3.2 JDK 8中
4. LinkedHashMap的底层实现原理
5. Map接口中的方法
6. TreeMap的使用
6.1 TreeMap的自然排序
6.2 TreeMap的定制排序
7. Properties的使用
七、⭐️Collections工具类
1. 排序操作
2. 查找替换操作
3. 同步控制
集合、数组都是对多个数据进行存储操作的结构,简称Java容器
说明 : 此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)
数组在存储多个数据方面的特点 :
一旦初始化之后,其长度就确定了
数组一旦定义好,其元素的类型也就确定了,只能操作指定类型的数据。比如 : String[] arr,int[] arr;
数组在存储多个数据方面的缺点 :
一旦初始化之后,其长度就不可修改
数组中提供的方法非常有限,对于 : 添加、删除、插入数据等操作,非常不方便,同时效率不高
获取数组中实际元素的个数的需求,数组中没有现成的属性或方法可用
数组存储数据的特点是有序的,可以存储重复的数据(有序,可重复),对于无序,不可重复的需求不能满足
关于数组的优缺点,集合都能很好的满足,其使用场景有 : 数据库返回的为一个List
Java集合可分为Collection和Map两种体系 :
Collection接口 : 单列数据,定义了存取一组对象的方法的集合
List : 元素有序、可重复的集合
Set : 元素无序、不可重复的集合
Map接口 : 双列数据,保存具有映射关系 "key - value 对" 的集合
Collection接口继承树 :
说明 : 实线为继承,虚线为实现
Map接口继承树 :
|----Collection接口 : 单列集合,用来存储一个一个的对象
|----List接口 : 存储有序、可重复的数据 --> "动态"数组
|----ArrayList、LinkedList、Vector
|----Set接口 : 存储无序、不可重复的数据 --> 高中讲的 "集合"
|----HashSet、LinkedHashSet、TreeSet
|----Map接口 : 双列集合,用来存储一对 (key - value) 一对的数据 --> 高中函数 : y = f(x)
|----HashMap、LinkedHashMap、TreeMap、Hashtable、Properties
最基本的15种方法 :
add(Object obj)、addAll(Collection coll)、size()、isEmpty()、clear();
contains(Object obj)、containsAll(Collection coll)、remove(Object obj)、removeAll(Collection voll)、retainsAll(Collection coll)、equals(Object obj);
hashCode()、toArray()、iterator();
1、添加
add(Object obj)
addAll(Collection coll)
2、获取有效元素的个数
int size()
3、清空集合
void clear()
4、是否是空集合
boolean isEmpty()
/*
Collection接口中声明的方法测试
*/
@SuppressWarnings("rawtypes")
@Test
public void test01(){
Collection coll = new ArrayList();
//add(Object e) : 将元素e添加到集合coll中
coll.add("AA");
coll.add(123);
coll.add(new Object());
coll.add("BB");
//size() : 获取添加元素的个数
System.out.println(coll.size());//4
Collection coll1 = new ArrayList();
coll1.add("AA");
coll1.add(new Date());
//addAll(Collection c) : 将c集合中的元素全部添加到当前集合中
coll.addAll(coll1);
System.out.println(coll.size());//6
System.out.println(coll);
//[AA, 123, java.lang.Object@76fb509a, BB, AA, Fri Aug 12 17:38:17 CST 2022]
//clear() : 清空集合元素
coll.clear();
//isEmpty() : 判断当前集合是否为空
System.out.println(coll.isEmpty());//true
}
5、是否包含某个元素
boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
/*
Collection接口中声明的方法测试
向Collection接口的实现类中添加数据obj时,要求obj所在的类要重写equals()方法
*/
@SuppressWarnings("rawtypes")
@Test
public void test02(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
Person p = new Person("Jerry", 20);
coll.add(p);
//contains(Object obj) : 判断当前集合是否包含obj
//我们在判断时会调用obj对象所在类的equals()方法
boolean contains = coll.contains(123);
System.out.println(contains);//true
System.out.println(coll.contains(new String("Tom")));//true
// --> 判断的内容(重写的equals方法)
System.out.println(coll.contains(new Person("Jerry", 20)));//false
// --> 由于equals方法未重写 所以就是判断的地址
//containsAll(Collection coll) : 判断形参coll中的所有元素是否都存在于当前集合中
List list = Arrays.asList(123, 456);//返回list类型 list是collection的子接口
System.out.println(coll.containsAll(list));//true
}
6、删除
boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
boolean removeAll(Collection coll):取当前集合的差集
/*
Collection接口中声明的方法测试
*/
@SuppressWarnings("rawtypes")
@Test
public void test03(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry", 20));
//remove(Object obj) : 从当前集合中移除
System.out.println(coll.remove(123));//true
System.out.println(coll);//[456, Tom, false, Person{name='Jerry', age=20}]
System.out.println(coll.remove(new Person("Jerry", 20)));
System.out.println(coll);
//removeAll(Collection coll) : 从当前集合中移除coll当中的所有元素 (差集)
List list = Arrays.asList(123, 456);
System.out.println(coll.removeAll(list));//true
}
7、取两个集合的交集
boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
8、集合是否相等
boolean equals(Object obj)
/*
Collection接口中声明的方法测试
*/
@SuppressWarnings("rawtypes")
@Test
public void test04(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry", 20));
//retainAll(Collection coll) : 获取当前集合和coll集合之间的交集 修改当前集合的值为交集的值
// List list = Arrays.asList(123, 456, 789);
// System.out.println(coll.retainAll(list));//true
// System.out.println(coll);//[123, 456]
//equals(Object obj) : 判断当前集合中的每个元素和另一个集合是否相等(顺序元素都相等)
Collection coll1 = new ArrayList();
coll1.add(123);
coll1.add(456);
coll1.add(new String("Tom"));
coll1.add(false);
coll1.add(new Person("Jerry", 20));
boolean equals = coll.equals(coll1);
System.out.println(equals);//true
}
9、转成对象数组
Object[] toArray()
10、获取集合对象的哈希值
hashCode()
11、遍历
iterator():返回迭代器对象,用于集合遍历
/*
Collection接口中声明的方法测试
*/
@SuppressWarnings("rawtypes")
@Test
public void test05() {
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry", 20));
//hashCode() : 返回当前对象的哈希值
System.out.println(coll.hashCode());//-2061953047
//toArray() : 集合 --> 数组
Object[] arr = coll.toArray();//返回的是Object类型的数组
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
//拓展 : 数组 --> 集合 Arrays.asList(T...t) 可变形参不推荐new,直接写数据即可
List list = Arrays.asList(new String[]{"AA", "BB", "CC"});
System.out.println(list);//[AA, BB, CC]
List list1 = Arrays.asList(new int[]{123, 456});//认为是一个参数(一维数组)
System.out.println(list1);//[[I@300ffa5d]
System.out.println(list1.size());//1 --> 一个元素
List list2 = Arrays.asList(new Integer[]{123, 456});
System.out.println(list2);//[123, 456]
System.out.println(list2.size());//2 --> 两个元素
//iterator() : 返回Iterator接口的实例,用于遍历集合元素 放在IteratorTest.java中测试
}
Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合(不用于Map集合)中的元素。
GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”。
Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
@SuppressWarnings("rawtypes")
@Test
public void test(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry", 20));
Iterator iterator = coll.iterator();
//next() : 游标下移 返回游标对应的元素
//遍历方式一 :
System.out.println(iterator.next());//123
System.out.println(iterator.next());//456
System.out.println(iterator.next());//Tom
System.out.println(iterator.next());//false
System.out.println(iterator.next());//Person{name='Jerry', age=20}
//报异常 : NoSuchElementException
// System.out.println(iterator.next());
//遍历方式二 :
//注意游标不会重置 这里重新获取一个迭代器对象
Iterator iterator1 = coll.iterator();
for (int i = 0; i < coll.size(); i++) {
System.out.println(iterator1.next());
}
//hasNext() : 判断是否还有下一个元素
//遍历方式三 : 推荐
Iterator iterator2 = coll.iterator();
while (iterator2.hasNext()){// 判断是否还有下一个元素
//① 指针下移 ② 将下移以后集合位置上的元素返回
System.out.println(iterator2.next());
}
}
测试Iterator中的remove() :
@SuppressWarnings("rawtypes")
@Test
public void test2(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry", 20));
Iterator iterator = coll.iterator();
//remove() : 可以在遍历的时候 删除集合中的元素 (删除游标对应的元素)
while (iterator.hasNext()){
Object obj = iterator.next();
if ("Tom".equals(obj)){
iterator.remove();
//不能连续调用两次 会报异常 IllegalStateException
// iterator.remove();
}
}
Iterator iterator1 = coll.iterator();
while (iterator1.hasNext()){
System.out.println(iterator1.next());
}
}
注意 :
Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法。
如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。
Java 5.0 提供了 foreach 循环 (增强for循环) 迭代访问 Collection和数组。
遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
遍历集合的底层调用Iterator完成操作。
foreach还可以用来遍历数组。
for (集合元素的类型 局部变量 : 要遍历的集合对象){
//内部仍然调用了迭代器实现
}
局部变量 : 盛装对应的元素 不是索引
遍历集合 :
public class ForTest {
public static void main(String[] args) {
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry", 20));
//for(集合元素的类型 局部变量 : 集合对象)
/*
过程 : 内部用迭代器实现
先取coll中第一个元素赋值给obj
然后取第二个元素赋值给obj
*/
for(Object obj : coll){
System.out.println(obj);
}
}
}
遍历数组 :
@Test
public void test(){
int[] arr = new int[]{1, 2, 3, 4, 5, 6};
//for(数组元素的类型 局部变量 : 数组对象)
for (int i : arr){
System.out.println(i);//这里的 i 就是对应的元素 不是索引
}
}
练习题 :
@Test
public void test01() {
String[] arr = new String[]{"MM", "MM", "MM"};
//方式一 : 普通for赋值
// for (int i = 0; i < arr.length; i++) {
// arr[i] = "GG";
// }
//方式二 : 增强for赋值
for (String str : arr){
str = "GG";//此时的赋值不是修改的数组本身 而是修改的局部变量
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组,称作动态数组。
List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。
|----Collection接口 : 单列集合,用来存储一个一个的对象
|----List接口 : 存储有序、可重复的数据 --> "动态"数组,替换原有的数组
|----ArrayList
|----LinkedList
|----Vector
比较ArrayList、LinkedList、Vector三者的异同 ?
相同点 : 三个类都是实现了List接口,存储数据的特点相同 : 存储有序的、可重复的数据
不同点 :
ArrayList : 作为List接口的主要实现类,线程不安全的,效率高,底层使用Object[] elementData存储
Linkedlist : 对于频繁的增加和删除操作,使用此类效率比ArrayList高,底层使用双向链表存储
Vector : 作为List接口的古老实现类,线程安全的,效率低,底层使用Object[] elementData存储
ArrayList list = new ArrayList();
底层创建了长度是10的 Object[] 数组 elementData
list.add(123);//elementData[0] = new Integer(123);
...
list.add(456);//如果此次的添加导致底层elementData数组容量不够,则需要扩容
默认情况下,扩容为原来容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中 ( elementData = Arrays.copyOf(elementData, newCapacity) )。
结论 : 建议开发中使用带参的构造器 :
ArrayList list = new ArrayList(int capacity);
ArrayList list = new ArrayList();
底层Object[] elementData初始化为 {},并没有创建长度为10的数组
list.add(123);
当第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
后续的添加与扩容操作与JDK7相同。
JDK 7中的ArrayList的对象的创建类似于单例模式的饿汉式
JDK 8中的ArrayList的对象的创建类似于单例模式的懒汉式,延迟了数组的创建,节省内存。
LinkedList list = new LinkedList();
内部声明了Node类型的first和last属性,默认值为null
list.add(123);
将123封装到Node中,创建了Node对象,其中Node的定义为如下,体现了LinkedList双向链表的实现原理。
private static class Node {
E item;
Node next;
Node prev;
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
由于Vector容器是在JDK 1.0时就存在,而Collection接口框架在JDK 1.2时才有,是后才将Vector并入接口框架中的,所以对Vector的使用不多,Vector的特点是线程安全的。其底层实现与ArrayList的原理一样(Object[] 数组),在创建对象的时候默认也是创建一个容量为10的Object数组,但在扩容的时候与ArrayList不同的是默认扩容2倍而非1.5倍。
List除了从Collection集合继承的方法外,List集合里添加了一些根据索引来操作集合元素的方法 :
void add(int index, Object ele) : 在index位置插入ele元素
boolean addAll(int index, Collection eles) : 从index位置开始将eles中的所有元素添加进来
@SuppressWarnings("rawtypes")
@Test
public void test01(){
List list = new ArrayList
Object get(int index) : 获取指定index位置的元素
int indexOf(Object obj) : 返回obj在集合中首次出现的位置
int lastIndexOf(Object obj) : 返回obj在当前集合中末次出现的位置
@SuppressWarnings("rawtypes")
@Test
public void test02(){
List list = new ArrayList
Object remove(int index) : 移除指定index位置的元素,并返回此元素
Object set(int index, Object ele):设置指定index位置的元素为ele,并返回被修改的元素
@SuppressWarnings("rawtypes")
@Test
public void test03(){
List list = new ArrayList
List subList(int fromIndex, int toIndex) : 返回从fromIndex到toIndex位置的子集合
@SuppressWarnings("rawtypes")
@Test
public void test04(){
List list = new ArrayList
遍历 : ①迭代器 ②增强for循环 ③普通for循环
@SuppressWarnings("rawtypes")
@Test
public void test05(){
List list = new ArrayList();
list.add(123);
list.add(456);
list.add(new String("Tom"));
list.add(false);
list.add(456);
list.add(new Person("Jerry", 20));
//方式一 : 迭代器
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.out.print(iterator.next() + " ");
}
System.out.println();
//方式二 : 增强for循环
for (Object obj : list){
System.out.print(obj + " ");
}
System.out.println();
//方式三 : 普通for循环
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
}
总结,常用方法 :
增 : add(Object obj)
删 : remove(int index) / remove(Object obj)
改 : set(int index, Object ele)
查 : get(int index)
插 : add(int index, Object obj)
长度 : size()
遍历 : ①迭代器 ②增强for循环 ③普通for循环
区分remove中remove(int index) ,remove(Object obj)方法,例子 :
public class ListExer {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list);
System.out.println(list);//[1, 2]
}
public static void updateList(List list){
list.remove(2);
//list.remove(new Integer(2))
}
}
remove方法有 : remove(int index) ,remove(Object obj)两个,在方法外调用remove方法时首先默认调用的是不需要自动装箱的方法,如果想要调用remove(Object obj),则需要手动装箱。
Set接口是Collection的子接口,set接口没有提供额外的方法
Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
|----Collection接口 : 单列集合,用来存储一个一个的对象
|----Set接口 : 存储无序的、不可重复的数据 --> 高中讲的 "集合"
|----HashSet
|----LinkedHashSet
|----TreeSet
比较HashSet、LinkedHashSet、TreeSet三者的异同 ?
相同点 : 三个类都是实现了Set接口,存储数据的特点相同 : 存储无序的、不可重复的数据
不同点 :
HashSet : 作为Set接口的主要实现类,线程不安全的,可以存储null值
LinkedHashSet : 作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序遍历
TreeSet : 可以按照添加对象的指定属性,进行排序
以HashSet为例说明 :
(1) 无序性 : 不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。无序只是指在存储的时候不按索引顺序添加。
(2) 不可重复性 : 保证添加的元素按照equals()判断时,不能返回true,即相同的元素只能添加一个
@SuppressWarnings("rawtypes")
@Test
public void test01(){
Set set = new HashSet();
set.add(456);
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new User("Tom", 12));
set.add(new User("Tom", 12));
set.add(129);
System.out.println(set);
//如果未重写equals方法 判断的地址值 所以不同
//[AA, CC, User{name='Tom', age=12}, 129, User{name='Tom', age=12}, 456, 123]
System.out.println(set);//重写equals方法之后 还是有两个
//[AA, CC, User{name='Tom', age=12}, 129, User{name='Tom', age=12}, 456, 123]
System.out.println(set);//重写了equals方法和hashCode方法之后 只有一个了
//[AA, CC, 129, 456, 123, User{name='Tom', age=12}]
}
以HashSet为例 :
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法 (& (size - 1) 等操作) 计算出在HashSet底层数组中的存放位置 (即 : 索引位置) ,判断数组此位置上是否已经有元素 :
如果此位置上没有其他元素,则元素a添加成功 (情况1)。如果此位置上有其他元素b (或以链表形式存在的多个元素),则比较元素a与元素b的hash值 :
如果hash值不相同,则元素a添加成功 (情况2)。如果hash值相同,则进而需要调用元素a所在类的equals()方法 :
如果equals()返回false,则元素a添加成功 (情况3)。如果equals()返回true,则元素a添加失败。
对于添加成功的情况2和情况3而言 : 元素a与已经存在指定索引位置上数据以链表的方式存储 :
jdk 7 : 元素a放到数组中,指向原来的元素。
jdk 8 : 原来的元素在数组中,指向元素a。
HashSet底层 : 数组 + 链表的结构(前提JDK 7)
实际上HashSet的实现原理就是HashMap,HashSet的数据实际上就是HashMap中的key,而value存放的是一个静态Object类对象,没有意义。
当向Set集合中添加数据时,在重写equals()方法时,建议同时重写hashCode(),并且保证两个方法的一致性 (相同的属性参与运算) ,这是因为在当对象放到HashSet集合或HashMap集合时,保证去重后的正确性。
了解hashCode方法和equals方法的两个重要的规范 :
规范一 : 两个对象相等,其hashCode一定相等
规范二 : 两个对象不等(equals方法返回false),并不要求这两个对象的hashCode得到不同的数
则得到如下推论 :
推论一 : hashCode相同的两个对象,不一定相等
推论二 : hashCode不同的两个对象,一定不相等
hashCode方法在Object类中定义,Object类中定义的hashCode方法调用的底层的C语言来计算出来一个随机数,这个随机数基本保证了不同的对象有不同的hashCode。如果我们不重写hashCode的话,在往HashSet集合中存放对象时,如果存放内容相等的两个对象,其hashCode不同,那两个对象都会存放成功,达不到去重效果。
重写hashCode()方法的基本原则 :
在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?
选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
并且31只占用5bits,相乘造成数据溢出的概率较小。
31可以 由i * 31 == ( i << 5 ) - 1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)
关于HashSet的使用,有关remove等方法的说明 : 先调用hashCode再用equals
public class hashSetTest {
public static void main(String[] args) {
HashSet set = new HashSet();
Person p1 = new Person(1001, "AA");
Person p2 = new Person(1002, "BB");
set.add(p1);
set.add(p2);
System.out.println(set);
//[Person{id=1002, name='BB'}, Person{id=1001, name='AA'}]
p1.name = "CC";
set.remove(p1);
System.out.println(set);
//[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
set.add(new Person(1001, "CC"));
System.out.println(set);
//[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]
set.add(new Person(1001, "AA"));
System.out.println(set);
//[Person{id=1002, name='BB'}, Person{id=1001, name='CC'},
// Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]
}
}
以上测试体现了HashSet的特点,当我们向HashSet中添加数据时,按照hashCode计算索引位置的规则进行添加的。当我们将添加以后的数据内容更改以后,其计算的hashCode将会改变。当我们remove改数据时,通过计算hashCode来寻找改元素计算得到的索引位置大概率不会是原来的位置,所以remove操作将会失败。当我们再向HashSet中添加一个更新后的数据时,由于原来更改后的数据没有占着索引位置,所以将会添加成功。当我们向HashSet中添加一个更新前的数据时,这时由于原来的数据占着其索引位置,那么就会比较hashCode,两个数据内容不同,所以hashCode不同,则添加成功。
LinkedHashSet是HashSet的子类,特点是在遍历的时候,可以按照添加的顺序遍历出来。原理是,在添加元素的同时还记录下了上一个添加的元素的地址,上一个添加的元素同时也记录了本次添加元素的地址。相当于形成了一个双向链表。
优点 : 对于频繁的遍历操作,LinkedHashSet效率高于HashSet。
@SuppressWarnings("rawtypes")
@Test
public void test02(){
Set set = new LinkedHashSet();
set.add(456);
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new User("Tom", 12));
set.add(new User("Tom", 12));
set.add(129);
System.out.println(set);
//[456, 123, AA, CC, User{name='Tom', age=12}, 129]
}
TreeSet : 可以按照添加对象的指定属性,进行排序
TreeSet底层使用的红黑树实现的,利用二叉树存储数据。调用compareTo()或compare()来判断元素是否相同。
在后续的调用contains方法、remove方法等,都是调用的compareTo或者compare方法来判断的。
向TreeSet中添加的数据,要求是同一个类的对象,不能添加不同类的对象,否则报ClassCastException异常。
向TreeSet中添加Integer对象 :
@SuppressWarnings("rawtypes")
@Test
public void test01(){
TreeSet set = new TreeSet();
set.add(34);
set.add(-34);
set.add(43);
set.add(11);
set.add(8);
System.out.println(set);//[-34, 8, 11, 34, 43]
Iterator it = set.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.print(obj + " ");
}//-34 8 11 34 43 从小到大的顺序
}
在创建TreeSet对象时,调用空参构造器,那么就会按照对象的自然排序方式进行操作。
向空参构造的TreeSet中添加自定义类User对象,其自定义的对象必须实现Comparable接口,否则报异常
ClassCastException: com.atguigu.study.User cannot be cast to java.lang.Comparable
自然排序中,比较两个对象是否相同的标准为 : compareTo()返回 0,不再是equals方法。两个对象是否相同的标准,是一个属性相同就认为两个对象相同,还是所有属性相同才认为两个对象相同,就要看自己在compareTo()方法中实现的逻辑了。
@SuppressWarnings("rawtypes")
@Test
public void test02(){
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));
Iterator it = set.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.print(obj + " ");
}
//User{name='Jack', age=33} User{name='Jerry', age=32} User{name='Jim', age=2}
// User{name='Mike', age=65} User{name='Tom', age=12}
}
public class User implements Comparable{
private String name;
private int age;
public User(){}
public User(String name, int age){
this.age = age;
this.name = name;
}
//按照名字从小到大排
@Override
public int compareTo(Object o) {
if (o instanceof User) {
User user = (User) o;
return this.name.compareTo(user.name);
} else throw new RuntimeException("传入数据类型不一致");
}
}
在创建TreeSet对象时,调用传入比较器的构造器,那么就会按照对象的定制排序方式进行操作。
定制排序中,比较两个对象是否相同的标准为 : compare()返回 0,不再是equals方法。
@SuppressWarnings("rawtypes")
@Test
public void test03(){
//按照年龄从小到大排 生成一个比较器
Comparator com = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof User && o2 instanceof User) {
User user1 = (User) o1;
User user2 = (User) o2;
return Integer.compare(user1.getAge(), user2.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));
Iterator it = set.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.print(obj + " ");
}
//User{name='Jim', age=2} User{name='Tom', age=12} User{name='Jerry', age=32}
// User{name='Jack', age=33} User{name='Mike', age=65}
}
|----Map : 双列数据,存储key-value对的数据 ---->类似于高中的函数 : y = f(x)
|----HashMap : 作为Map的主要实现类,线程不安全,效率高,可以存储null的key和value
|----LinkedHashMap : 保证在遍历map元素时,可以按照添加的顺序实现遍历
原理 : 在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素
对于频繁的遍历操作,此类执行效率高于HashMap。
|----Hashtable : 作为古老的实现类,线程安全,效率低,不可以存储null的key和value
|----Properties : 常用来处理配置文件。key-value都是String类型
|----TreeMap : 保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
底层使用红黑树实现
HashMap的底层 : 数组 + 链表 (JDK 7及之前)
数组 + 链表 + 红黑树(JDK 8)
key-value拆分理解 :
Map中的key : 无序的、不可重复的,使用Set存储所有的key (以HashMap为例,key所在的类要重写equals()和hashCode())。
Map中的value : 无序的、可重复的,使用Collection存储所有的value(value所在的类要重写equals()方法)。
一个键值对 : key-value构成了一个Entry对象。
Map中的Entry : 无序的、不可重复的,使用Set存储所有的Entry。
HashMap map = new HashMap();
在实例化之后,底层创建了长度是16的一维数组 Entry[] table。
map.put()...
map.put(key1, value1);
已经执行多次put以后的put(一个普通的put) 的执行过程 :
首先,调用key1所在类的hashCode()方法计算key1的哈希值,此哈希值经过某种算法(&)计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1(Entry)添加成功 (情况1) 。如果此位置上的数据不为空(意味着此位置上存在一个或多个数据以链表形式存在),比较当前key1和已经存在的一个或多个数据的哈希值。
如果key1的哈希值与已经存在的数据都不相同,此时key1-value1添加成功 (情况2) 。如果key1的哈希值与已经存在的某个数据 (key2-value2) 的哈希值相同,继续比较,调用key1所在类的equals()方法比较。
如果equals()返回false,此时key1-value1添加成功 (情况3) 。如果equals()返回true,则将使用value1替换value2。
@SuppressWarnings("rawtypes")
@Test
public void test02(){
HashMap map = new HashMap();
//put具有修改功能
map.put("Tom", 12);
System.out.println(map);//{Tom=12}
map.put("Tom", 45);
System.out.println(map);//{Tom=45}
}
补充 : 关于情况2和情况3,此时key1-value1和原来的数据以链表的方式 (JDK 7新元素在上) 存储。
在添加过程中,涉及到扩容问题,当超出临界值时 (且要存放的位置非空) 默认的扩容方式 : 扩容为原来数组的二倍,并将原有的数据复制过来。在复制的时候会重新计算各个元素的位置,重新存放。
JDK 8相较于JDK 7在底层实现方面的不同 :
new HashMap() 时底层没有创建一个长度为16的数组
JDK 8底层的数组是Node[],而非Entry[]
首次调用put()方法时,底层创建一个长度为16的数组
JDK 7底层结构只有 : 数组 + 链表,JDK 8底层结构 : 数组 + 链表 + 红黑树。当数组的某一个索引位置上的以链表形式存在的数据个数大于8,且当前数组的长度大于64时,此时,此索引位置上的所有数据改为使用红黑树存储。
HashMap源码中的重要常量 :
DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子 (可以控制链表的多少),0.75 (比较稳定的数值)
threshold:扩容的临界值,容量 x 填充因子 = 0.75 x 16 = 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树 ,8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量,64
负载因子值的大小对HashMap有什么影响 ?
负载因子的大小决定了HashMap的数据密度。
负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
能够按照添加的顺序进行遍历 :
@SuppressWarnings("rawtypes")
@Test
public void test01(){
Map map1 = new HashMap();
map1.put(123, "AA");
map1.put(456, "BB");
map1.put(789, "CC");
System.out.println(map1);//{789=CC, 456=BB, 123=AA}
Map map2 = new LinkedHashMap();
map2.put(123, "AA");
map2.put(456, "BB");
map2.put(789, "CC");
System.out.println(map2);//{123=AA, 456=BB, 789=CC}
}
在源码中,内部类Entry :
static class Entry extends HashMap.Node {
Entry before, after;
Entry(int hash, K key, V value, Node next) {
super(hash, key, value, next);
}
}
增加了两个指针来记录上一个添加的元素和下一个添加的元素地址,遍历的时候通过该指针实现按照添加顺序遍历。
添加、删除、修改操作:
Object put(Object key,Object value) : 将指定key-value添加到(或修改)当前map对象中
void putAll(Map m) : 将m中的所有key-value对存放到当前map中
Object remove(Object key) : 移除指定key的key-value对,并返回value
void clear() : 清空当前map中的所有数据
/*
Map接口中的常用方法测试
*/
@SuppressWarnings("rawtypes")
@Test
public void test03(){
Map map = new HashMap();
//1. Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
//添加
map.put("AA", 123);
map.put(45, 123);
map.put("BB", 56);
//修改
map.put("AA", 87);
System.out.println(map);//{AA=87, BB=56, 45=123}
Map map1 = new HashMap();
map1.put("CC", 123);
map1.put("DD", 123);
//2. void putAll(Map m):将m中的所有key-value对存放到当前map中
map.putAll(map1);
System.out.println(map);
//{AA=87, BB=56, CC=123, DD=123, 45=123}
//3. Object remove(Object key):移除指定key的key-value对,并返回value
Object value = map.remove("CC");
Object ee = map.remove("EE");
System.out.println(value);//123
System.out.println(ee);//null remove的key不存在将返回null
System.out.println(map);//{AA=87, BB=56, DD=123, 45=123}
//4. void clear():清空当前map中的所有数据
map.clear();//与map = null操作不同
System.out.println(map.size());//0
System.out.println(map);//{}
}
元素查询的操作:
Object get(Object key) : 获取指定key对应的value
boolean containsKey(Object key) : 是否包含指定的key
boolean containsValue(Object value) : 是否包含指定的value
int size() : 返回map中key-value对的个数
boolean isEmpty() : 判断当前map是否为空
boolean equals(Object obj) : 判断当前map和参数对象obj是否相等
/*
Map接口的常用方法测试
*/
@SuppressWarnings("rawtypes")
@Test
public void test04(){
Map map = new HashMap();
map.put("AA", 123);
map.put(45, 123);
map.put("BB", 56);
//1. Object get(Object key):获取指定key对应的value
System.out.println(map.get("AA"));//123
System.out.println(map.get("CC"));//null
//2. boolean containsKey(Object key):是否包含指定的key
boolean isExistKey = map.containsKey("AA");
System.out.println(isExistKey);//true
//3. boolean containsValue(Object value):是否包含指定的value
boolean isExistVal = map.containsValue(123);
System.out.println(isExistVal);//true
//4. int size():返回map中key-value对的个数
System.out.println(map.size());//3
//5. boolean isEmpty():判断当前map是否为空
boolean isEmpty = map.isEmpty();
System.out.println(isEmpty);//false
//6. boolean equals(Object obj):判断当前map和参数对象obj是否相等
//要想为true则必须传入一个map且数据相同
}
元视图操作的方法:
Set keySet() : 返回所有key构成的Set集合
Collection values() : 返回所有value构成的Collection集合
Set entrySet() : 返回所有key-value对构成的Set集合
/*
Map接口中的常用方法测试
*/
@SuppressWarnings("rawtypes")
@Test
public void test05(){
Map map = new HashMap();
map.put("AA", 123);
map.put(45, 123);
map.put("BB", 56);
//1. 遍历所有的Key集合 :
Set keySet = map.keySet();
for (Object o : keySet) {
System.out.print(o + " ");
}//AA BB 45
System.out.println();
//2. 遍历所有的values集 :
Collection values = map.values();
Iterator it = values.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.print(obj + " ");
}//123 56 123
System.out.println();
//3. 遍历所有的Entry集 :
//方式一 :
Set entrySet = map.entrySet();
Iterator iterator = entrySet.iterator();
while(iterator.hasNext()){
Object obj = iterator.next();
//entrySet集合中的元素都是entry
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + " ---> " + entry.getValue());
}
/*
AA ---> 123
BB ---> 56
45 ---> 123
*/
System.out.println();
//方式二 :
Iterator iterator1 = keySet.iterator();
while(iterator1.hasNext()){
Object key = iterator1.next();
Object value = map.get(key);
System.out.println(key + " ---> " + value);
}
/*
AA ---> 123
BB ---> 56
45 ---> 123
*/
}
总结 : 常用方法
增 : put(Object key, Object value)
删 : remove(Object key)
改 : put(Object key, Object value)
查 : get(Object key)
长度 : size()
遍历 : keySet()/values()/entrySet()
向TreeMap中添加key-value,要求key必须是由同一个类创建的对象。
只能按照key来排,不能按照value排序。
@SuppressWarnings("rawtypes")
@Test
public void test01(){
TreeMap map = new TreeMap();
User u1 = new User("Tom", 23);
User u2 = new User("Jerry", 32);
User u3 = new User("Jack", 20);
User u4 = new User("Rose", 18);
map.put(u1, 98);
map.put(u2, 89);
map.put(u3, 76);
map.put(u4, 100);
Set entrySet = map.entrySet();
Iterator iterator = entrySet.iterator();
while(iterator.hasNext()){
Object obj = iterator.next();
//entrySet集合中的元素都是entry
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + " ---> " + entry.getValue());
}
/*
User{name='Jack', age=20} ---> 76
User{name='Jerry', age=32} ---> 89
User{name='Rose', age=18} ---> 100
User{name='Tom', age=23} ---> 98
*/
}
@SuppressWarnings("rawtypes")
@Test
public void test02(){
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());
} else throw new RuntimeException("传入的类型不一致");
}
});
User u1 = new User("Tom", 23);
User u2 = new User("Jerry", 32);
User u3 = new User("Jack", 20);
User u4 = new User("Rose", 18);
map.put(u1, 98);
map.put(u2, 89);
map.put(u3, 76);
map.put(u4, 100);
Set entrySet = map.entrySet();
Iterator iterator = entrySet.iterator();
while(iterator.hasNext()){
Object obj = iterator.next();
//entrySet集合中的元素都是entry
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + " ---> " + entry.getValue());
}
/*
User{name='Rose', age=18} ---> 100
User{name='Jack', age=20} ---> 76
User{name='Tom', age=23} ---> 98
User{name='Jerry', age=32} ---> 89
*/
}
Properties 类是 Hashtable 的子类,该对象用于处理属性文件
由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
public class PropertiesTest {
public static void main(String[] args) {
FileInputStream fis = null;
try {
Properties pros = new Properties();
fis = new FileInputStream("jdbc.properties");
pros.load(fis);//加载流对应的文件
String name = pros.getProperty("name");//获取value
String password = pros.getProperty("password");
System.out.println("name = " + name + " password = " + password);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null)
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Collections 是一个操作 Set、List 和 Map 等集合的工具类
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。
关于集合的排序操作,均为static方法 :
reverse(List) : 反转List 中元素的顺序
shuffle(List) : 对List集合元素进行随机排序
sort(List) : 根据元素的自然顺序对指定List集合元素按升序排序
sort(List,Comparator) : 根据指定的Comparator产生的顺序对List 集合元素进行排序
swap(List,int,int) : 将指定list 集合中的 i 处元素和 j 处元素进行交换
@SuppressWarnings("rawtypes")
@Test
public void test01(){
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(-97);
list.add(0);
System.out.println(list);//[123, 43, 765, -97, 0]
//1. reverse(List) : 反转List中元素的顺序
Collections.reverse(list);
System.out.println(list);//[0, -97, 765, 43, 123]
//2. shuffle(List) : 对List集合元素进行随机排序 (随机化)
Collections.shuffle(list);
System.out.println(list);//[-97, 0, 43, 123, 765]
//3. sort(List) : 根据元素的自然顺序对指定List集合元素按升序排序
Collections.sort(list);
System.out.println(list);//[-97, 0, 43, 123, 765]
//4. swap(List,int,int) : 将指定list集合中的i处元素和j处元素进行交换
Collections.swap(list, 0, 1);
System.out.println(list);//[0, -97, 43, 123, 765]
}
Object max(Collection) : 根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator) : 根据Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object) : 返回指定集合中指定元素的出现次数
@SuppressWarnings("rawtypes")
@Test
public void test02(){
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(765);
list.add(765);
list.add(-97);
list.add(0);
System.out.println(list);
//1. Object max(Collection) : 根据元素的自然顺序,返回给定集合中的最大元素
Comparable max = Collections.max(list);
System.out.println(max);//765
//2. Object min(Collection)
Comparable min = Collections.min(list);
System.out.println(min);//-97
//3. int frequency(Collection,Object) : 返回指定集合中指定元素的出现次数
int frequency = Collections.frequency(list, 765);
System.out.println(frequency);//3
}
void copy(List dest,List src) : 将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal) : 使用新值替换List对象的所有旧值
@SuppressWarnings("rawtypes")
@Test
public void test03(){
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(-97);
list.add(0);
System.out.println(list);
//1. void copy(List dest,List src) : 将src中的内容复制到dest中
//错误 报异常 底层要判断 src.size() > dest.size()
//java.lang.IndexOutOfBoundsException: Source does not fit in dest
// List dest = new ArrayList();
// Collections.copy(dest, list);
//利用Object数组把dest的size撑开
List dest = Arrays.asList(new Object[list.size()]);
Collections.copy(dest, list);
System.out.println(dest);//[123, 43, 765, -97, 0]
//2. boolean replaceAll(List list,Object oldVal,Object newVal) : 使用新值替换List对象的所有旧值
Collections.replaceAll(list, 123, 0);
System.out.println(list);//[0, 43, 765, -97, 0]
}
Collections 类中提供了多个synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
@SuppressWarnings("rawtypes")
@Test
public void test04(){
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(-97);
list.add(0);
//synchronizedXxx() : 该方法可使将指定集合包装成线程同步的集合
//返回的synchronizedList即为线程安全的List
List synchronizedList = Collections.synchronizedList(list);
System.out.println(synchronizedList);//[123, 43, 765, -97, 0]
}