Map
接口是 Java 集合框架中的一个接口,它表示了一种键值对的映射关系。Map
接口提供了一种以键为索引的数据结构,通过键可以快速查找对应的值。在 Map
中,每个键只能对应一个值,键是唯一的,但值可以重复。
常用的实现类有 HashMap
、TreeMap
、LinkedHashMap
等。
Map 常用方法
-
put(key, value)
:将指定的键值对添加到 Map 中。 -
putAll(Map<? extends K, ? extends V> m)
:将指定的映射中的所有键值对添加到当前的Map
中 -
get(key)
:根据键获取对应的值。 -
containsKey(key)
:判断指定的键是否存在于 Map 中。 -
containsValue(value)
:判断指定的值是否存在于 Map 中。 -
remove(key)
:根据键移除对应的键值对。 -
size()
:获取 Map 中键值对的数量。 -
keySet()
:获取 Map 中所有键,存入到集合。 -
values()
:获取 Map 中所有值,存入到集合。 -
Set<Map.Entry<K,V>> entrySet()
:获取 Map 中所有键值对,存入到集合。
Set<Map.Entry(String, Integer)> entries = map.entrySet();
-
isEmpty()
:判断 Map 是否为空 -
clear()
:清除 Map 中所有数据
Map 中的排序方法
在 Java 中,Map
接口本身并没有直接提供 Comparator 接口相关的方法。Comparator
接口主要用于对集合中的元素进行比较排序,而 Map
接口表示的是键值对的集合,其排序通常是基于键或值进行的。
对 Map
进行排序,并且希望自定义排序逻辑,可以通过以下几种方式来实现:
-
使用
TreeMap
:TreeMap 实现了SortedMap
接口,它根据键的自然顺序或指定的Comparator
对键进行排序。可以在创建TreeMap
对象时,通过传入自定义的Comparator
对象来实现排序 -
转换为 List 进行排序:先将
Map
的键值对转换成一个List
,然后使用Collections.sort()
方法对List
进行排序,并且可以提供自定义的Comparator
对象。
基于 键 排序
使用 comparingByKey()
方法可以很方便地对 Map
的键进行排序
comparingByKey()
是 java.util.Map
类中的一个静态方法,在 Java 8 中引入的函数式接口 java.util.Comparator
中有一个 comparingByKey()
方法的默认实现。
comparingByKey()
方法返回一个基于键的比较器(Comparator
),用于对 Map
中的键进行排序。它可以用于对 Map
的键值对进行排序操作。
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
实例:
Map<String, Integer> sortedMap = new TreeMap<>(Map.Entry.comparingByKey());
sortedMap.putAll(map);
基于 值 排序
使用 comparingByValue()
方法可以方便地对 Map
的值进行排序
comparingByValue()
是 java.util.Map
类中的一个静态方法,在 Java 8 中引入的函数式接口 java.util.Comparator
中有一个 comparingByValue()
方法的默认实现。
comparingByValue()
方法返回一个基于值的比较器(Comparator
),用于对 Map
中的值进行排序。它可以用于对 Map
的键值对进行排序操作。
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
实例:
List<Map.Entry<String, Integer>> sortedList = new ArrayList<>(map.entrySet());
sortedList.sort(Map.Entry.comparingByValue());
自定义比较器
comparingByValue
和 comparingByKey
可以作为自定义比较器:
// 使用 comparingByKey 方法创建自定义比较器
Comparator<String> keyComparator =
Comparator.comparingByKey();
也可以自定义比较器传入两方法中,使用自行定义的比较器对 Map 中的键值对进行排序:
Comparator<String> keyComparator = (k1, k2) -> {
// 自定义比较逻辑,这里假设按照键的长度进行比较
return Integer.compare(k1.length(), k2.length());
};
// 使用自定义的比较器对键进行排序
map.entrySet()
// 将集合转换为流
.stream()
.sorted(Map.Entry.comparingByKey(keyComparator))
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
}
Map 中的默认方法
-
default
V getOrDefault(Object key, V defaultValue)
:
通过键获取一个值,如果不存在设置默认值 defaultValue -
default
void forEach(BiConsumer<? super K, ? super V> action)
遍历 Map,通过 函数式接口的 lambda 表达式实现遍历操作:打印、求和等 -
void replaceAll(BiFunction<? super K, ? super V, extends V> function)
:替换所有的键值对 -
default
V putIfAbsent(K key, V value)
:如果不存在键值对就添加键值对,否则就不添加
-
default
boolean remove(Object key, Object value)
移除键值对,键值对必须全部匹配,否则失败
-
default
boolean replace(K key, V oldValue, V newValue)
类似于乐观锁,如果根据键值得到的键值和期望的旧值oldValue 相等,就替换为新值,否则失败
-
default
V replace(K key, V value)
替换已经存在的键值对 -
default
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
如果对应键不存在键值对,则使用自定义
lambda [Function<K,V>]
指定根据键返回值,并且存放入 Map;如果返回 null,就不放入 Map ,并且返回 null -
default
V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
如果对应键存在键值对,使用自定义
lambda [BiFunction<T,U,R>]
,根据key 和 U 生成 对应值,并将键值对放入 Map;如果不存在键值对,返回null;如果生成对应值为 null ,会将 key 对应键值对删除 -
default
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
根据键(key)获取旧值,自定义
lambda [BiFunction<T,U,R>]
,通过 key 和 旧值 生成新值,将新键值对放入Map;如果生成新值为 null,并且旧值存在则删除旧的键值对,旧的键值对不存在就返回 null -
default
V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction)
如果存在旧的键值对,设置新值为新的键值对,并返回新值(value);如果九的键值对不存在,自定义
lambda [BiFunction<T,U,R>]
,根据旧值和 value 生成新值,新值不为 null 就将新的键值对放入Map,新的键值为 null 就删除旧的键值对
Map.Entry
Map.Entry
是 Map
接口的内部接口,它表示 Map
中的一个键值对。在 Map
中,每个键值对都由一个 Map.Entry
对象来表示。
Map.Entry
接口定义了以下几个常用方法:
getKey()
:获取当前键值对的键。getValue()
:获取当前键值对的值。setValue(V value)
:设置当前键值对的值。
通过 Map.Entry
接口,可以方便地遍历 Map
中的键值对。
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
// 修改键值对的值
entry.setValue(value * 2);
}
Map 常见实现类
Map
接口的实现类常用的有 HashMap
、TreeMap
、LinkedHashMap
等。
HashMap
HashMap
是 Java 集合框架中的一个类,它实现了 Map
接口,是 Map 接口使用频率最高的实现类,允许存储键值对,并提供了快速的插入、访问和删除操作。
HashMap
使用哈希表数据结构来存储键值对构成的 Map.Entry
,通过键的哈希值来确定其在内部数组中的位置,以实现快速的检索性能。
HashMap的主要特点
-
键值对:
HashMap
存储的数据是键值对,每个键唯一。可以根据键快速获取对应的值,但是不能保证键值对的顺序。 -
线程不安全:
HashMap
不是线程安全的类,多个线程同时访问和修改HashMap
可能导致数据不一致或异常。如果需要在多线程环境下使用,可以考虑使用ConcurrentHashMap
。在并发环境中,可以使用
concurrentHashMap
或其他线程安全的实现类 -
null 键和值:
HashMap
允许存储 null 键(只能有一个)和 null 值(可以有多个)。 -
HashMap
判断两个 key 相等的标准是:两个 key 通过equals()
方法返回 true,hashCode 值也相等。 -
HashMap
判断两个 value 相等的标准是:两个 value 通过equals()
方法返回 true。
HashMap 的四种构造方法
- 创建一个空的
HashMap
对象,默认初始容量为 16,负载因子为 0.75。
HashMap()
- 创建一个具有指定初始容量的
HashMap
对象。
HashMap(int initialCapacity)
初始容量是指 HashMap
最初可以存储的键值对数量上限。如果不确定初始容量,可以根据数据量和负载因子进行估算。
- 创建一个具有指定初始容量和加载因子的
HashMap
对象。
HashMap(int initialCapacity, float loadFactor)
- 创建一个包含指定映射的
HashMap
对象,其中映射的键值对会被复制到新的HashMap
中。
HashMap(Map<? extends K, ? extends V> m)
- 用于创建一个具有指定初始容量、加载因子和访问顺序的
HashMap
对象。
HashMap(int initialCapacity, float loadFactor, boolean accessOrder)
accessOrder
:指定的访问顺序,为 true
表示按照访问顺序(LRU 链表顺序)进行迭代,即最近访问的元素会放在链表尾部;为 false
表示按照插入顺序进行迭代。
访问顺序的设定对于实现 LRU(最近最少使用)缓存等应用场景非常有用,可以方便地实现基于访问频率的数据清除策略。
HashMap 的存储结构
JDK 1.7及以前版本:HashMap
是 数组+链表 结构(即为链地址法)
数组是
HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)
JDK 1.8版本发布以后:HashMap
是 数组+链表+红黑树 实现。
当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
JDK 1.7及之前
当实例化一个 HashMap
时,系统会创建一个长度为 Capacity 的 Entry 数组,这个长度在哈希表中被称为容量(Capacity),数组中可以存放元素的位置称为“桶”(bucket),每个 bucket
都有自己的索引,系统可以根据索引快速的查找 bucket
中的元素。
每个 bucket 中存储一个元素,即一个 Entry 对象,每一个 Entry 对象可以带一个引用变量,用于指向下一个元素。在一个桶中,就有可能生成一个Entry链。新添加的元素作为链表的 head。
JDK 1.8及之后
当实例化一个 HashMap
时,会初始化 initialCapacity
和 loadFactor
, 在 put 第一对映射关系时,系统会创建一个长度为 initialCapacity
的Node 数组。数组每个 bucket
都有自己的索引,系统可以根据索引快速的查找 bucket
中的元素。
每个 bucket
中存储一个元素,即一个 Node 对象,每一个 Node 对象可以带一个引用变量 next
,用于指向下一个元素,在一个桶中,就有可能生成一个 Node 链。
也可能是一个个 TreeNode 对象,每一个 TreeNode
对象可以有两个叶子结点 left 和 right。在一个桶中,就有可能生成一个 TreeNode 树。而新添加的元素作为链表的 last,或树的叶子结点。
HashMap 扩容机制
当 HashMap
中的元素越来越多的时候,hash冲突的几率也就越来越高,
因为数组的长度是固定的。为了提高查询的效率,就要对HashMap的数组进行扩容
在 HashMap
数组扩容之后,最消耗性能的点 resize()
就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去。
JDK 1.7 及之前
当 HashMap
中的元素个数超过数组大小 loadFactor(数组总大小length,不是数组中个数 size)时,就会进行数组扩容。
默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为 16,那么当 HashMap
中元素个数超过 16 * 0.75 = 12
(threshold
值,也叫做临界值)的时候,就把数组的大小扩展为 2 * 16 = 32
,即扩大一倍,然后重新计算每个元素在数组中的位置
这是一个非常消耗性能的操作,如果已经预知 HashMap
中元素的个数,那么预设元素的个数能够有效的提高 HashMap
的性能。
JDK 1.8及之后
当 HashMap
中的元素个数超过数组大小(length,不是数组中个数size) loadFactor 时,就会进行数组扩容。
默认情况下,当 HashMap
中元素个数超过 16 * 0.75 = 12
(threshold值)的时候,就把数组的大小扩展为 2 * 16 = 32
,即扩大一倍,然后重新计算每个元素在数组中的位置。
当 HashMap
中的其中一个链的对象个数如果达到了 8 个,此时如果capacity 没有达到 64,那么 HashMap
会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由 Node
变成 TreeNode
类型。
(当数组指定索引位置的链表长度 >8 时,且 map 中的数组的长度 >64 时,此索引位置上的所有key-value对使用红黑树进行存储。)
如果当映射关系被移除后,下次 resize
方法时判断树的结点个数低于6个,也会把树再转为链表。
HashMap 使用示例
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
// 创建一个 HashMap
Map<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Charlie", 35);
// 访问键值对
System.out.println("Age of Alice: " + map.get("Alice")); // 输出: Age of Alice: 25
// 更新键值对
map.put("Alice", 26);
System.out.println("New age of Alice: " + map.get("Alice")); // 输出: New age of Alice: 26
// 检查键是否存在
System.out.println("Does 'Alice' exist? " + map.containsKey("Alice")); // 输出: Does 'Alice' exist? true
// 检查值是否存在
System.out.println("Does 26 exist? " + map.containsValue(26)); // 输出: Does 26 exist? true
// 遍历映射中的所有键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 移除键值对
map.remove("Bob");
System.out.println("After removing Bob: " + map); // 输出: After removing Bob: {Alice=26, Charlie=35}
// 清空映射
map.clear();
System.out.println("Is the map empty? " + map.isEmpty()); // 输出: Is the map empty? true
}
}
ConcurrentHashMap
ConcurrentHashMap
继承自 AbstractMap
,它是线程安全的哈希表实现,用于存储键值对。它提供了高效的并发操作,多个线程可以同时进行读取和写入操作,而不会发生数据不一致的情况。
ConcurrentHashMap
提供了与 HashMap
相似的接口和方法,主要包括添加、获取、删除元素的操作,以及迭代等。
ConcurrentHashMap 的主要特点
-
线程安全性:
ConcurrentHashMap
内部使用了分段锁(Segment)机制,将整个哈希表拆分为多个独立的部分,每个部分拥有自己的锁,不同的线程可以同时操作不同的部分,提高了并发性能。 -
高并发性: 相较于
HashTable
,ConcurrentHashMap
在多线程并发读写的情况下能够提供更高的性能,因为它允许多个线程同时读取而不需要加锁。 -
可伸缩性:
ConcurrentHashMap
允许在加载因子达到阈值时对其进行自动扩容,从而适应动态变化的数据大小。 -
不保证有序性:
ConcurrentHashMap
不保证迭代顺序与插入顺序或访问顺序一致。
ConcurrentHashMap 的构造方法
- 创建一个默认初始容量为16的
ConcurrentHashMap
实例。
ConcurrentHashMap()
- 创建一个指定初始容量的
ConcurrentHashMap
实例。
ConcurrentHashMap(int initialCapacity)
- 创建一个包含指定映射中所有键值对的
ConcurrentHashMap
实例。
ConcurrentHashMap(Map< ? extends K, ? extends V> map)
- 创建一个指定初始容量和加载因子的
ConcurrentHashMap
实例。
ConcurrentHashMap(int initialCapacity, float loadFactor)
- 创建一个指定初始容量、加载因子和并发级别的
ConcurrentHashMap
实例。并发级别是哈希表内部的并发更新线程数(预期值)。
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
并发更新线程实际上是由锁的数量来决定的,而不是由
concurrencyLevel
参数明确限制的。
根据 Oracle 官方文档的建议,如果不确定应该将
concurrencyLevel
设置为多少,可以使用默认值16
,在大多数情况下都能够提供良好的性能。
ConcurrentHashMap 使用示例
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建一个 ConcurrentHashMap
Map<String, Integer> map = new ConcurrentHashMap<>();
// 添加键值对
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Charlie", 35);
// 访问键值对
System.out.println("Age of Alice: " + map.get("Alice")); // 输出: Age of Alice: 25
// 更新键值对
map.put("Alice", 26);
System.out.println("New age of Alice: " + map.get("Alice")); // 输出: New age of Alice: 26
// 检查键是否存在
System.out.println("Does 'Alice' exist? " + map.containsKey("Alice")); // 输出: Does 'Alice' exist? true
// 检查值是否存在
System.out.println("Does 26 exist? " + map.containsValue(26)); // 输出: Does 26 exist? true
// 遍历映射中的所有键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 移除键值对
map.remove("Bob");
System.out.println("After removing Bob: " + map); // 输出: After removing Bob: {Alice=26, Charlie=35}
// 清空映射
map.clear();
System.out.println("Is the map empty? " + map.isEmpty()); // 输出: Is the map empty? true
}
}
LinkedHashMap
LinkedHashMap
是 Java 集合框架中的一个类,它是 HashMap
的一个子类,具有与 HashMap
相同的功能,并且保持了插入顺序或访问顺序。
LinkedHashMap
内部使用一个双向链表来维护键值对的顺序,而不像 HashMap
那样是无序的。这意味着当遍历 LinkedHashMap
时,键值对会按照插入顺序或访问顺序进行迭代。
LinkedHashMap 的主要特点
-
保持顺序:
LinkedHashMap
保持元素的插入顺序或访问顺序。插入顺序通过默认构造函数实现,而访问顺序需要设置为accessOrder = true
。 -
继承自
HashMap
:LinkedHashMap
继承自HashMap
,因此具备了HashMap
的快速访问和删除操作等特点。但因此LinkedHashMap也是线程不安全的 -
消耗内存:相比于
HashMap
,LinkedHashMap
需要额外的内存来维护内部的链表结构。
LinkedHashMap 的五种构造方法
- 创建一个空的
LinkedHashMap
对象,默认初始容量为 16,加载因子为 0.75。
LinkedHashMap()
- 创建一个具有指定初始容量的
LinkedHashMap
对象。
LinkedHashMap(int initialCapacity)
- 创建一个具有指定初始容量和加载因子的
LinkedHashMap
对象。
LinkedHashMap(int initialCapacity, float loadFactor)
- 创建一个具有指定初始容量、加载因子和访问顺序的
LinkedHashMap
对象。
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
accessOrder
参数为 true 表示按照访问顺序进行迭代,为 false 表示按照插入顺序进行迭代。
- 创建一个包含指定映射的
LinkedHashMap
对象,其中映射的键值对会被复制到新的LinkedHashMap
中。
LinkedHashMap(Map< ? extends K, ? extends V> m)
LinkedHashMap 使用示例
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapExample {
public static void main(String[] args) {
// 创建一个 LinkedHashMap
Map<String, Integer> map = new LinkedHashMap<>();
// 添加键值对
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Charlie", 35);
// 访问键值对
System.out.println("Age of Alice: " + map.get("Alice")); // 输出: Age of Alice: 25
// 更新键值对
map.put("Alice", 26);
System.out.println("New age of Alice: " + map.get("Alice")); // 输出: New age of Alice: 26
// 检查键是否存在
System.out.println("Does 'Alice' exist? " + map.containsKey("Alice")); // 输出: Does 'Alice' exist? true
// 检查值是否存在
System.out.println("Does 26 exist? " + map.containsValue(26)); // 输出: Does 26 exist? true
// 遍历映射中的所有键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 移除键值对
map.remove("Bob");
System.out.println("After removing Bob: " + map); // 输出: After removing Bob: {Alice=26, Charlie=35}
// 清空映射
map.clear();
System.out.println("Is the map empty? " + map.isEmpty()); // 输出: Is the map empty? true
}
}
TreeMap
TreeMap
实现了 NavigableMap
接口,使用红黑树(Red-Black Tree)数据结构来存储键值对。
TreeMap
可以根据键的自然排序或自定义比较器对键进行排序,并提供了快速的查找、插入和删除操作。
TreeSet 主要特点
-
排序:
TreeMap
通过键的自然顺序或自定义比较器来对键进行排序,并保持键的有序状态。 -
基于红黑树:
TreeMap
使用红黑树来实现键值对的存储。红黑树是一种自平衡的二叉搜索树,可以保持较好的性能和平衡性。 -
线程不安全:
TreeMap
不是线程安全的数据结构。在多线程环境下,如果多个线程同时并发地对TreeMap
进行读取和修改操作,可能会导致数据不一致或产生其他不可预测的错误。
TreeMap 的四种构造方法
- 创建一个空的
TreeMap
对象,并按照键的自然顺序进行排序。
TreeMap()
- 创建一个空的
TreeMap
对象,按照指定的比较器comparator
进行排序。
TreeMap(Comparator< ? super K> comparator)
- 创建一个
TreeMap
对象,并将已存在的map中的键值对加入到新的TreeMap
中。使用键的自然顺序进行排序。
TreeMap(Map< ? extends K, ? extends V> map)
- 创建一个
TreeMap
对象,并将已存在的排序映射表sortedMap
的键值对加入到新的TreeMap
中。
TreeMap(SortedMap<K, ? extends V> sortedMap)
TreeMap 使用示例
import java.util.TreeMap;
import java.util.Map;
public class TreeMapExample {
public static void main(String[] args) {
// 创建一个 TreeMap
Map<String, Integer> map = new TreeMap<>();
// 添加键值对
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Charlie", 35);
// 访问键值对
System.out.println("Age of Alice: " + map.get("Alice")); // 输出: Age of Alice: 25
// 更新键值对
map.put("Alice", 26);
System.out.println("New age of Alice: " + map.get("Alice")); // 输出: New age of Alice: 26
// 检查键是否存在
System.out.println("Does 'Alice' exist? " + map.containsKey("Alice")); // 输出: Does 'Alice' exist? true
// 检查值是否存在
System.out.println("Does 26 exist? " + map.containsValue(26)); // 输出: Does 26 exist? true
// 遍历映射中的所有键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 移除键值对
map.remove("Bob");
System.out.println("After removing Bob: " + map); // 输出: After removing Bob: {Alice=26, Charlie=35}
// 清空映射
map.clear();
System.out.println("Is the map empty? " + map.isEmpty()); // 输出: Is the map empty? true
}
}
HashTable
HashTable
实现了 Map
接口, 用于存储键值对,并根据键的哈希码(hash code)进行查找。HashTable
是线程安全的,但也因此在性能上较 HashMap
稍慢。
HashTable 主要特点
-
哈希表:
HashTable
内部使用一个哈希表来存储键值对。当插入或查找键值对时,会根据键的哈希码计算存储位置,以提高查找效率。 -
线程安全:
HashTable
是线程安全的类,这意味着多个线程可以同时读取和修改HashTable
的内容。为了实现线程安全,HashTable
使用了同步(synchronization)机制,使得在并发环境下能够正确地处理读写操作。 -
性能相对较差:
HashTable
的所有方法都是同步的,它通过对常用的操作加锁来保证线程安全。在单线程环境下,可以使用HashMap
,多线程环境下,可以使用ConcurrentHashMap
替代
HashTable 的四种构造方法
- 创建一个空的
HashTable
对象,默认初始容量为 11,加载因子为 0.75。
HashTable()
- 创建一个具有指定初始容量的
HashTable
对象。
HashTable(int initialCapacity)
- 创建一个具有指定初始容量和加载因子的
HashTable
对象。
HashTable(int initialCapacity, float loadFactor)
- 创建一个包含指定映射的
HashTable
对象,其中映射的键值对会被复制到新的HashTable
中。
HashTable(Map< ? extends K, ? extends V> m)
HashTable 使用示例
import java.util.Hashtable;
import java.util.Map;
public class HashTableExample {
public static void main(String[] args) {
// 创建一个 HashTable
Map<String, Integer> map = new Hashtable<>();
// 添加键值对
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Charlie", 35);
// 访问键值对
System.out.println("Age of Alice: " + map.get("Alice")); // 输出: Age of Alice: 25
// 更新键值对
map.put("Alice", 26);
System.out.println("New age of Alice: " + map.get("Alice")); // 输出: New age of Alice: 26
// 检查键是否存在
System.out.println("Does 'Alice' exist? " + map.containsKey("Alice")); // 输出: Does 'Alice' exist? true
// 检查值是否存在
System.out.println("Does 26 exist? " + map.containsValue(26)); // 输出: Does 26 exist? true
// 遍历映射中的所有键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 移除键值对
map.remove("Bob");
System.out.println("After removing Bob: " + map); // 输出: After removing Bob: {Alice=26, Charlie=35}
// 清空映射
map.clear();
System.out.println("Is the map empty? " + map.isEmpty()); // 输出: Is the map empty? true
}
}
Properties
Properties
继承自 HashTable
,它用于处理属性文件(.properties)中的键值对数据。属性文件是一种常见的配置文件格式,在 Java 开发中经常被用来存储配置信息。
Properties 主要特点和常用方法
-
键值对存储:
Properties
使用键值对的方式来存储数据,其中键和值都是字符串类型。属性文件中的每一行都表示一个键值对,以 “键=值” 的形式存在。 -
加载和保存属性文件:Properties 提供了加载和保存属性文件的方法,可以从属性文件中读取键值对数据,或将键值对数据保存到属性文件中。常用的方法包括:
-
load(InputStream in)
: 从 输入流 中加载属性文件。 -
load(Reader reader)
: 从 字符流 中加载属性文件。 -
store(OutputStream out, String comments)
: 将属性文件保存到输出流中。 -
store(Writer writer, String comments)
: 将属性文件保存到字符流中。
-
-
获取和设置属性值:
Properties
提供了一系列方法来获取和设置属性值,如:-
getProperty(String key)
: 根据键获取对应的属性值。 -
getProperty(String key, String defaultValue)
: 根据键获取对应的属性值,若不存在则返回默认值。 -
setProperty(String key, String value)
: 设置指定键的属性值。
-
-
遍历属性:
Properties
提供了遍历属性的方法,可以逐个获取所有的键值对。常用的方法有:-
propertyNames()
: 返回包含所有键的枚举对象,可用于迭代遍历。 -
stringPropertyNames()
: 返回包含所有键的字符串集合。 -
forEach(BiConsumer< ? super Object, ? super Object> action)
: 遍历所有的键值对,并执行指定的操作。
-
Properties 的构造方法
-
Properties()
: 创建一个空的Properties
对象。 -
Properties(Properties defaults)
: 创建一个Properties
对象,并指定默认的属性。当获取属性值时,如果在当前Properties
中找不到对应的值,将返回默认属性中指定的值。
Properties defaults = new Properties();
defaults.setProperty("defaultKey", "defaultValue");
Properties properties2 = new Properties(defaults);
Properties
类不常主动实例化,通常会通过加载配置文件来获得 Properties
实例,例如使用 InputStream
来读取属性文件,然后通过 load
方法将属性加载到 Properties
对象中:
Properties 使用示例
假设我们有一个名为 app.properties
的属性文件,内容如下:
database.url=jdbc:mysql://localhost:3306/mydb
database.user=root
database.password=secret
server.port=8080
server.host=localhost
示例代码
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
public class PropertiesExample {
public static void main(String[] args) {
// 读取属性文件
Properties properties = readProperties("app.properties");
// 输出原始属性
printProperties(properties);
// 修改属性文件
properties.setProperty("database.password", "newpassword");
properties.setProperty("server.port", "9090");
// 保存修改后的属性到文件
saveProperties("app.properties", properties);
// 再次读取属性文件验证修改
Properties updatedProperties = readProperties("app.properties");
printProperties(updatedProperties);
}
/**
* 读取属性文件到 Properties 对象。
*
* @param fileName 属性文件的名称
* @return 包含属性的 Properties 对象
*/
private static Properties readProperties(String fileName) {
Properties properties = new Properties();
try (FileInputStream fileInputStream = new FileInputStream(fileName)) {
properties.load(fileInputStream);
} catch (IOException e) {
e.printStackTrace();
}
return properties;
}
/**
* 将 Properties 对象保存到属性文件。
*
* @param fileName 属性文件的名称
* @param properties 包含属性的 Properties 对象
*/
private static void saveProperties(String fileName, Properties properties) {
try (FileOutputStream fileOutputStream = new FileOutputStream(fileName)) {
properties.store(fileOutputStream, "Updated properties file.");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 打印 Properties 对象中的所有属性。
*
* @param properties 包含属性的 Properties 对象
*/
private static void printProperties(Properties properties) {
properties.list(System.out);
System.out.println();
}
}
解析:
-
读取属性文件:
- 使用
readProperties
方法加载属性文件到Properties
对象中。 - 使用
FileInputStream
打开属性文件。 - 使用
Properties
对象的load
方法加载文件内容。 - 使用
printProperties
方法打印原始属性。
- 使用
-
修改属性文件:
- 使用
setProperty
方法更新属性值。 - 使用
saveProperties
方法将修改后的属性保存回文件系统。 - 使用
FileOutputStream
打开属性文件以写入模式。 - 使用
Properties
对象的store
方法保存修改后的属性。
- 使用
-
再次读取属性文件验证修改:
- 重新读取属性文件到
Properties
对象中。 - 使用
printProperties
方法打印更新后的属性。
- 重新读取属性文件到