@
目录
- Java 集合(容器)
- 一、Java 集合框架概述
- 二、Collection 接口方法
- 三、Iterator 迭代器接口
- 1. 使用 Iterator 接口遍历集合元素
- 2. Iterator 接口的方法
- 3. 使用 foreach 循环遍历集合元素
- 四、Collection 子接口一:List
- 1. List 接口概述
- 2. List 接口方法
- 3. List 实现类之一:ArrayList(主要)
- 4. List 实现类之二:LinkedList
- 5. List 实现类之三:Vector
- 6. List 三种实现类的异同
- 五、Collection 子接口二:Set
- 1. Set 接口概述
- 2. Set 实现类之一:HashSet
- 3. Set 实现类之二:LinkedHashSet
- 4. Set 实现类之三:TreeSet
- 六、Map 接口
- 1. Map 接口概述
- 2. Map 实现类之一:HashMap
- 3. Map 实现类之二:LinkedHashMap
- 4. Map 实现类之三:TreeMap
- 5. Map 实现类之四:Hashtable
- 6. Map 实现类之五:Properties
- 七、Collections 工具类
- 1. 概述
- 2. Collections 常用方法
- 3. 同步控制
- 4. Enumeration
- 八、关于HashSet的一道面试题
Java 集合(容器)
一、Java 集合框架概述
-
一方面,面向对象语言对事务的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用 Array 存储对象放面具有一些弊端,而 Java 集合就像一种容器,可以动态的把多个对象的引用放入容器中。
- 数组在内存存储方面的特点:
- 数组初始化以后,长度就确定了
- 数组声明的类型,就决定了元素初始化时的类型
- 数组在存储数据方面的弊端:
- 数组初始化以后,长度不可变,不便于扩展
- 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储的元素的个数
- 数组存储的数据是有序的、可重复的。(存储数据的特点单一)
- Java 集合类可用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。
- 数组在内存存储方面的特点:
-
Java 集合可分为 Collection 和 Map 两种体系
- Collection 接口:单列数据,定义了存取一组对象的方法和集合
- List:元素有序、可重复的集合
- Set:元素无需、不可重复的集合
- Map 集合:双列数据,保存具有映射关系 “ key – value 对” 的集合
- Collection 接口:单列数据,定义了存取一组对象的方法和集合
-
Collection 接口继承树
-
Map 接口继承树
二、Collection 接口方法
Collection 接口中文文档(JDK 11)
- 添加
- add(Object obj)
- addAll(Collection coll):将集合 coll 中的所有元素添加到当前集合中
- 获取有效元素的个数
- int size()
- 清空合集
- void clear()
- 是否为空集合
- boolean isEmpty()
- 是否包含某个元素
- boolean contains(Object obj):是通过元素的 equals 方法来判断是否是同一个对象
- boolean containsAll(Collection c):也是调用 equals 方法来比较,用两个集合的元素挨个比较。
- 删除
- boolean remove(Object obj):通过 equals 方法定位,并删除,只会删除找到的第一个元素
- boolean removeAll(Collection coll):取当前集合的差集
- 取两个集合的交集
- boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响集合 c
- 集合是否相等
- boolean equals(Object obj)
- 转成对象数组
- Object[] toArray()
- 数组 转化成 集合:调用 Arrays 类的静态方法 asList()。
- 但要注意该方法直接写入 new int[](123, 456) 只是把整体当成一个元素存入,此时用包装类的方式即可解决。
- 获取集合对象的哈希值
- hashCode()
- 遍历
- iterator():返回迭代器对象,用于遍历集合
三、Iterator 迭代器接口
1. 使用 Iterator 接口遍历集合元素
- Iterator 对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
- GOF 给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需要暴露该对象的内部细节。迭代器模式,便是为容器而生。类似于”公交车上的售票员”、”火车上的乘务员“、”空姐“。
- Collection 接口继承了 java.lang.Iterator 接口,该接口有一个 Iterator() 方法,那么所有实现了 Collection 接口的集合类都有一个 Iterator() 方法,用以返回一个实现了 Iterator 接口的对象。
2. Iterator 接口的方法
boolean | hasNext(): Return true if the iterator has more elements. |
---|---|
E | next(): Return the next element int the iteration. |
void | remove(): Removes from the underlying collection the last element returned by the iterator(optional operation). |
-
可采取 hasNext() 和 next() 方法配合遍历集合
在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。
while(iterator.hasNext) { iterator.next(); }
两种常见的错误遍历方式:
//方式一: Iterator iterator = coll.iterator(); while(iterator.next() != null) { System.out.println(iterator.next()); } //while的条件会使 集合指针 移动一次,导致输出结果跳跃
//方式二: while(coll.iterator().hasNext()) { System.out.println(coll.iterator().next()); } //集合对象每次调用 iterator() 方法都会得到一个全新的迭代器对象,默认指针 都会从集合的第一个元素开始。
-
有关 remove()
Iterator iter = coll.iterator(); while(iter.hasNext()) { Object obj = iter.next(); if(obj,equals("Tom")) { iter.remove(); } }
- 注意
- Iterator 可以删除集合的元素,但是 是通过遍历过程中通过迭代器对象的 remove 方法,不是集合对象的 remove 方法。
- 如果还未调用 next() 或在上一次调用 next 方法之后已经调用了 remove 方法,再调用 remove 都会报IllegalStateException
- 注意
3. 使用 foreach 循环遍历集合元素
-
Java 5.0 提供了 foreach 循环迭代访问 Collection 和 数组
-
遍历操作不需要获取 Collection 或数组的长度,无需使用索引访问元素
-
遍历集合和底层调用 Iterator 完成操作
-
foreach 还可以用来遍历数组
四、Collection 子接口一:List
1. List 接口概述
- 鉴于 Java 中数组用来存储数据的局限性,我们通常使用List替代数组
- List 集合类中 元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
- JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。
2. List 接口方法
- List 除了从 Collection 集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
- void add(int index, Object ele):在 index 位置插入 ele 元素
- boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
- Object get(int index):获取指定 index 位置的元素
- int indexOf(Object obj):返回 obj 在集合中首次出现的位置
- int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
- Object remove(int index):移除指定 index 位置的元素,并返回此元素
- Object set(int index, Object ele):设置指定 index 位置的元素为 ele
- List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合
3. List 实现类之一:ArrayList(主要)
-
ArrayList 是 List 接口的典型实现类、主要实现类
-
本质上,ArrayList 是对象引用的一个“变长“数组
-
ArrayList 的 JDK 1.8 之前与之后的 空参构造器 的实现区别(其他方面无异)?
-
JDK 1.7:类似饿汉式,直接创建一个初始容量为 10 的数组
-
ArrayList list = new ArrayList(); list.add(123); // elementData[0] = new Integer(123);
-
当数组 elementData 数组容量不够时,默认情况下,扩容为原来的 1.5 倍,同时将原来数组中的数据复制到新的数组中。
-
建议开发中使用带参构造器:一开始人为的确定容量
-
-
JDK 1.8:类似懒汉式,一开始创建一个长度为 0 的数组,当添加第一个元素时再创建一个是容量为 10 的数组
-
-
Array.asList() 方法返回的 List 集合,既不是ArrayList 试里,也不是 Vector 实例。Array.asList() 返回值是一个固定长度的 List 集合。
-
源码分析传送门(转载)
4. List 实现类之二:LinkedList
-
适合频繁的插入或删除元素的操作,效率较高
-
双向链表
5. List 实现类之三:Vector
- Vector 很古老,JDK 1.0 就有了。大多数操作与 ArrayList 相同,区别在于 Vector 是线程安全的,但效率总是比 ArrayList 慢,一般不用。
6. List 三种实现类的异同
- ArrayList 与 LinkedList 区别:(参考javaGuide)
- 是否线程安全:二者线程不完全,相对线程安全的 Vector,执行效率更高
- 底层数据结构:ArrayList 和Vector 实现了基于动态数组的数据结构,LinkedList是基于链表的数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环)。
- 插入和删除元素:
- 对于 ArrayList ,由于是数组结构,故在指定位置 i 插入或删除元素的画,时间复杂度为 O(n-i) ,即操作位置之后的所有元素后移或前移。
- 对于 LinkedList ,由于采用链表结构,插入删除元素的时间复杂度近似为 O(1) , 若在指定位置插入元素,时间复杂度近似为 O(n) ,因为要先移动到指定位置,再进行操作。
- 是否支持快速随机访问:LinkedList 不支持高效的随即元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号开速获取元素对象。
- 内存空间占用:ArrayList 的空间浪费主要体现在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
- ArrayList 和 Vector 的区别
Vector 和 ArrayList 几乎完全相同,最大区别为 Vector 是同步类(synchronized),属于强同步类。因此开销比 ArrayList 要大,访问要慢。正常情况下,一般使用 ArrayList 而不是 Vector,因为同步完全可以由程序员自己来控制(如使用Collections的方法)。
Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
Vector还有一个子类 Stack
五、Collection 子接口二:Set
1. Set 接口概述
- Set 接口是 Collection 的子接口,Set 接口没有提供额外的方法。
- Set 集合不允许包含相同的元素
- Set 判断两个对象是否相同,使用的是 equals() 方法。
2. Set 实现类之一:HashSet
-
HashSet 是 Set 接口的典型实现,大多数使用 Set 集合时都使用该实现类。
-
HashSet 按 Hash 算法来存储集合种的元素,因此具有很好的存取、查找、删除性能。
-
HashSet 具有以下特点:
- 不能保证元素的排列顺序
- HashSet 不是线程同步的
- 集合元素可以时null
-
HashSet 集合判断两个元素相等的标准:
通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
-
对于存放在 Set 容器种的对象,对应的类一定要重写 equals() 和 hashCode(Object obj) 方法,以实现对象相等规则。即:”相等的对象必须具有相等的散列码“。
-
HashSet添加元素的过程:
添加元素a,首先调用元素 a 所在类的 hashcode() 方法,计算元素a的哈希值,通过此哈希值和某种散列函数计算出在 HashSet 底层数组的存放位置(即为:索引位置),判断此位置上是否已经又元素:
如果此位置上没有其他元素,则元素 a 添加成功(情况1)
如果此位置上有其他元素 b (或以链表形式存在的多个元素), 则比较元素 a 与元素 b 的哈希值:
若哈希值不同:则元素 a 添加成功(情况2)
若哈希值相同:需要调用元素 a 所在类的 equals() 方法
equals() 返回 true :添加失败
equals() 返回 false:添加成功(情况3)
-
对于添加成功的 情况2 和 情况3 而言:元素 a 与已经存在指定索引位置上数据以链表的方式存储。
-
jdk 7 :元素 a 放到数组中,指向原来的元素
-
jdk 8 :原来的元素在数组中,指向元素 a
(七上八下)
-
底层也是数组,初始容量为16,当如果使用率超过0.75(16*0.75=12)就会扩大容量为原来的2倍。(16扩容为32,依次为64,128….等)
- 上述提到的散列函数,会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好。
-
-
重写 hashCode() 方法的基本原则
- 在程序运行时,同时一个对象多次调用 hashCode() 方法应该返回相同的值。
- 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也相等
- 对象中作用 equals() 方法比较的 Field,都应该用来计算 hashCode 值
-
重写 equals() 方法的基本原则
- 当一个类有自己独特的 “逻辑相等” 概念,当该重写 equals() 的时候,总要重写 hashCode() ,根据一个类的 equals 方法(重写后),两个截然不同的实例有可能在逻辑上时相等的,但是根据 Object.hashCode() 方法,它们仅仅是两个对象。
- 因此,违反了 “相等的对象必须具有相等的散列码”。
- 结论:重写 equals 方法的时候一般都需要同时重写 hashCode 方法,通常参与计算 hashCode 的对象的属性也应该参与到 equals() 中进行计算。
- 为什么用 Eclipse/IDEA 重写的 hashCode 方法,有31这个数字?
- 选择系数的时候要尽量选择最大的系数,因为如果计算出来的 hash 地址越大,所谓的“冲突”就越少。查找起来效率也会提高(减少冲突)
- 31 只占用 5 bits,相乘造成数据溢出的概率较小
- 31 可以由 i * 31 == (i << 5) – 1,现在很多虚拟机里面都有相关优化。(提高算法效率)
- 31 是一个素数,某个数与素数相乘,结果只能被该素数,和被乘数还有 1 来整除(减少冲突)
3. Set 实现类之二:LinkedHashSet
-
LinkedHashSet 是 HashSet 的子类
-
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的
-
LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有更好的性能
-
LinkedHashSet 不允许集合元素重复
4. Set 实现类之三:TreeSet
- TreeSet 时 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
- TreeSet 底层使用 红黑树 结构存储数据
- TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
- 自然排序:
- 向 TreeSet 中添加元素时,只有第一个元素无须比较 compareTo() 方法,后面添加的元素都会调用 compareTo() 方法进行比较。
- 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该时同一个类的对象。
- 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值
- 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过 equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较 应返回 0 ,否则可读性较差。
- 定制排序:
- TreeSet 的自然排序要求元素所属的类实现 Comparable 接口,如果元素所属的类没有实现 Comparable 接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其他属性大小进行排序,则考虑使用定制排序。定制排序,通过 Comparator 接口来实现。需要重写 compae(T o1, T o2) 方法。
- 利用 int compare(T o1, T o2) 方法, 比较 o1 和 o2 的大小:如果方法返回正整数,则表示 o1 大于 o2;如果返回 0,表示相等;返回负整数,表示 o1 小于 o2。
- 要实现定制排序,需要将实现 Comparator 接口的实例作为参数传递给 TreeSet 的构造器
- 此时,仍然只能向 TreeSet 中添加类型相同的对象。否则发生 ClassCastException 异常。
- 使用定制排序判断两个元素相等的标准是:通过 Comparator 比较两个元素返回了 0。
- 自然排序:
六、Map 接口
1. Map 接口概述
-
Map 与 Collection 并列存在。用于保存具有映射关系的数据: key – value
-
Map 中的 key 和 value 都可以是任何引用类型的数据
-
Map 中的 key 用 Set 来存放,不允许重复,即同一个 Map 对象所对应的类,必须重写 hashCode() 和 equals() 方法
-
常用 String 类作为 Map 的 ”键“
-
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
-
Map 接口的常用实现类:HashMap、TreeMap、LinkedHashMap 和 Properties。其中,HashMap 是 Map 接口使用频率最高的实现类。
-
Map结构的理解:
- Map 中的 key 无序的、不可重复的,使用 Set 存储所有的 key (key 所在类要重写 equals() 和 hashCode() )
- Map 中的 value:无序的、可重复的,使用 Collection 存储所有的 value (value 所在的类要重写 equals() )
- 一个键值对:key – value 构成了一个 Entry 对象。
- Map 中的 Entry :无序的、不可重复的,使用 Set 存储所有的 Entry。
-
Map 接口常用方法:
2. Map 实现类之一:HashMap
-
Some Tips
- HashMap 是 Map接口使用频率最高的实现类
- 允许使用 null 键和 null 值,与 Hash Set 一样
- 所有的 key 构成的集合是 Set :无序的、不可重复的。所以,key 所在的类要重写 equals() 和 hashCode()
- 所有的 value 构成的集合是 Collection:无序的、可以重复的。所以,value所在类要重写 equals()
- 一个 key-value 构成一个 entry
- 所有的 entry 构成的集合是 Set:无需的、不可重复的
- HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 返回 true
- HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 返回 true
-
JDK 7 及以前
-
添加元素的过程(jdk7)
HashMap map = new HashMap();
在实例化后,底层创建了长度为 16 的一维数组 Entry[] table
(可能已经执行多次 put )
map.put(key1, value1);
首先,调用 key1 所在类的 hashCode() 计算 key1 哈希值,此哈希值经过某种算法计算后,得到在 Entry 数组中存放的位置
如果此位置上的数据为空,此时的 key1-value1 添加成功(情况1)
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较 key1 和已经存在的一个或多个数据的哈希值:
如果 key1 的哈希值与已经存在的数据的哈希值都不相同,key1-value1 添加成功(情况2)
如果 key1 的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,就继续比较:调用 key1 所在类的 equals(key2):
如果 equals() 返回 false:key1-value1添加成功。(情况3)
如果 equals() 返回 true:使用 value1 替换 value2
- (情况 2 与情况 3 :key1-value1 和原来的数据以链表的形式存储)
- 扩容(resize):转移到新的容量为原来的两倍的数组中。
-
扩容时机
当 HashMap 中的元素个数超过数组大小(数组总大小length,不是数组中个数 size ) * loadFactor 时 , 就会 进行数组扩容, loadFactor 的默认值( DEFAULT_LOAD_FACTOR)为 0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为 16,那么当 HashMap 中元素个数超过16 * 0.75=12(这个值就是代码中的 threshold 值,也叫做临界值)的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。
-
-
jdk 8 与jdk 7 的区别
-
new HashMap() 时:底层没有创建一个长度为 16 的数组
-
首次调用 put() 方法时,底层创建长度为 16 的数组
-
Entry() 改为 Node[]
-
JDK 8 形成链表结构时,新添加的 key – value 对在链表的尾部 (7 上 8 下 )
-
jdk 7 底层结构只有:数组 + 链表
jdk 8 底层结构:数组 + 链表 + 红黑树
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前这个数组的长度超过 64 时,这个链会变成树,结点类型由 Node 变成 TreeNode 类型。当然,如果当映射关系被移除后,下次 resize 方法时判断树的结点个数低于6个,也会把树再转为链表。
-
-
部分主要常量
- DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
- MAXIMUM_CAPACITY : HashMap的最大支持容量,2^30
- DEFAULT_LOAD_FACTOR :HashMap的默认加载因子:0.75
- TREEIFY_THRESHOLD :Bucket中链表长度大于该默认值,转化为红黑树(8)
- UNTREEIFY_THRESHOLD :Bucket中红黑树存储的Node小于该默认值,转化为链表
- MIN_TREEIFY_CAPACITY :桶中的Node被树化时最小的hash表容量(64)。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
- table :存储元素的数组,总是2的n次幂
- entrySet: :存储具体元素的集
- size :HashMap中存储的键值对的数量
- modCount :HashMap扩容和结构改变的次数。
- threshold :扩容的临界值 = 容量 * 填充因子 :16 * 0.75 = 12
- 提前扩容,可以让链表少一些
- loadFactor: :填充因子
-
LoadFactor(负载因子值、 填充因子值、填充比)的大小,对 HashMap的影响
- 决定 HashMap 的数据密度
- LoadFactor 越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能下降
- LoadFactor 越小,越容易触发扩容,数据密度也越小,碰撞几率越小,数组中的链表也越短,查询和插入时比较的次数也越小,性能会较高。但是会浪费更多的内存。而且经常扩容也会影响性能,故建议初始化预设大一点的空间。
- 设置为 0.7 ~ 0.75 较为常规:此时平均检索长度接近于常数。
3. Map 实现类之二:LinkedHashMap
-
是 HashMap 的子类
-
在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序。
-
与 LinkedHashSet 类似,其可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致。
-
HashMap 中的内部类:Node
static class Node<K, V> implements Map.Entry<K, V> { final int hash; final K key; V value; Node<K,V> next; }
LinkedHashMap 中的内部类:Entry
static class Entry<K, V> extends HashMap.Node<K, V> { Entry<K, V> before, after; Entry(int hash, K key, V value, Node<K, v> next) { super(hash, key, value, next); } }
4. Map 实现类之三:TreeMap
- TreeMap 存储 Key-Value 对时,需要根据 key-value 对进行排序
TreeMap 可以保证所有的 Key-Value 对处于有序状态。 - TreeMap 底层使用 红黑树 结构存储数据。
- TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClassCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
- TreeMap 判断两个 key 相等的标准:两个 key 通过 compareTo() 方法或者 compare() 方法返回 0 。
5. Map 实现类之四:Hashtable
- Hashtable 是个功劳的 Map 实现类(JDK 1.0),相比于 HashMap,Hashtable 时线程安全的。
- 实现原理和 HashMap 相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以相互用。
- 与 HashMap 不同,Hashtable 不允许使用 null 作为 key 和 value
- 与HashMap 一样, Hashtable 也不能保证其中 Key-Value 对的顺序。
- Hashtable 判断两个 key 相等、 两个 value 相等的标准,与 HashMap 一致。
6. Map 实现类之五:Properties
-
Properties 类是 Hashtable 的子类,该对象用于处理属性文件
-
由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
-
存储数据时,建议使用 setProperty (String key, String value)方法和 getProperty (String key) 方法
#jdbc.properties name = Tom passWord == 123abc
Properties pros = new Properties(); pros.load(new FileInputStream("jdbc.properties")); String user = pros.getProperty("user"); System.out.println(user)
七、Collections 工具类
1. 概述
- 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 处元素进行交换
2. Collections 常用方法
查找、替换
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection, Comparator): 根据 Comparator 指定的顺序,返回给定集合中的最大元素
- Object min (Collection)
- Object min (Collection, Comparator)
- int frequency(Collection, Object) : 返回指定集合中指定元素的出现次数
- void copy(List dest, List src) :将 src 中的内容赋值到 dest 中
- boolean replaceALl(List list, Object oldVal, Object newVal):使用新值替换 List 对象的所有旧值
3. 同步控制
- Collections 类提供了多个关于同步控制的方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
4. Enumeration
-
Enumeration 接口是 Iterator 迭代器的”古老版本“
Enumeration stringEnum = new StringTokenizer("a-b*c-d-e-g", "-"); while(stringEnum.hasMoreElements()) { Object obj = stringEnum.nextElement(); System.out.println(obj); }
八、关于HashSet的一道面试题
-
hashCode 理解
package com.exer03; import java.util.HashSet; import java.util.Objects; import java.util.Set; public class Test { @org.junit.Test public void test() { Person p1 = new Person(1001, "AA"); Person p2 = new Person(1002, "BB"); Set set = new HashSet(); set.add(p1); set.add(p2); System.out.println(set); // ① p1.name = "CC"; set.remove(p1); System.out.println(set); // ② set.add(new Person(1001, "CC")); System.out.println(set); // ③ set.add(new Person(1001, "AA")); System.out.println(set); } } class Person { int num; String name; public Person(int num, String name) { this.num = num; this.name = name; } public Person() { } @Override public String toString() { return "Person{" + "num=" + num + ", name='" + name + '\'' + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; if (num != person.num) return false; return name != null ? name.equals(person.name) : person.name == null; } @Override public int hashCode() { int result = num; result = 31 * result + (name != null ? name.hashCode() : 0); return result; } } // ① 输出结果: /* [Person{num=1002, name='BB'}, Person{num=1001, name='CC'}] 解析:关键是注意 hashCode 的重写算法,p1.name 改为 “CC” 后,remove()方法调用的hashCode()使用的name为CC的hashcode ,该位置和一开始存入p1位置不同(是AA和1001 hash编码),故remove() 过程实际上结果为,删除 1001 和 CC hash编码的位置上的元素。 */ // ② 输出结果: /* [Person{num=1002, name='BB'}, Person{num=1001, name='CC'}, Person{num=1001, name='CC'}] 解析;添加位置是1001和CC编码位置,p1的name虽然被改为CC,但位置在1001和AA编码位置。 向set里add元素的流程:首先靠 hashCode 寻找某位置是否存在元素,若有则调用 equals() 判断,若无则直接添加。 */ // ③ 输出结果: /* [Person{num=1002, name='BB'}, Person{num=1001, name='CC'}, Person{num=1001, name='CC'}, Person{num=1001, name='AA'}] 解析:虽然1001 和 AA 编码位置上有元素,但是经过 equals() 返回false,故可以添加成功。 */