Java 入门指南:Map 接口

Map 接口是 Java 集合框架中的一个接口,它表示了一种键值对的映射关系Map 接口提供了一种以键为索引的数据结构,通过键可以快速查找对应的值。在 Map 中,每个键只能对应一个值,键是唯一的,但值可以重复

常用的实现类有 HashMapTreeMapLinkedHashMap 等。

![[Map 接口概览.png]]

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 进行排序,并且希望自定义排序逻辑,可以通过以下几种方式来实现:

  1. 使用 TreeMap:TreeMap 实现了 SortedMap 接口,它根据键的自然顺序或指定的 Comparator 对键进行排序。可以在创建 TreeMap 对象时,通过传入自定义的 Comparator 对象来实现排序

  2. 转换为 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());

自定义比较器

comparingByValuecomparingByKey 可以作为自定义比较器:

// 使用 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.EntryMap 接口的内部接口,它表示 Map 中的一个键值对。在 Map 中,每个键值对都由一个 Map.Entry 对象来表示。

Map.Entry 接口定义了以下几个常用方法:

  1. getKey():获取当前键值对的键。
  2. getValue():获取当前键值对的值。
  3. 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 接口的实现类常用的有 HashMapTreeMapLinkedHashMap 等。

HashMap

HashMap 是 Java 集合框架中的一个类,它实现了 Map 接口,是 Map 接口使用频率最高的实现类,允许存储键值对,并提供了快速的插入、访问和删除操作。

HashMap 使用哈希表数据结构来存储键值对构成的 Map.Entry通过键的哈希值来确定其在内部数组中的位置,以实现快速的检索性能。

HashMap的主要特点
  1. 键值对:HashMap 存储的数据是键值对,每个键唯一。可以根据键快速获取对应的值,但是不能保证键值对的顺序

  2. 线程不安全HashMap 不是线程安全的类,多个线程同时访问和修改 HashMap 可能导致数据不一致或异常。如果需要在多线程环境下使用,可以考虑使用 ConcurrentHashMap

    在并发环境中,可以使用 concurrentHashMap 或其他线程安全的实现类

  3. null 键和值:HashMap 允许存储 null 键(只能有一个)和 null 值(可以有多个)。

  4. HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。

  5. HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true。

HashMap 的四种构造方法
  1. 创建一个空的 HashMap 对象,默认初始容量为 16,负载因子为 0.75。
HashMap()
  1. 创建一个具有指定初始容量的 HashMap 对象。
HashMap(int initialCapacity)

初始容量是指 HashMap 最初可以存储的键值对数量上限。如果不确定初始容量,可以根据数据量和负载因子进行估算。

  1. 创建一个具有指定初始容量和加载因子的 HashMap 对象。
HashMap(int initialCapacity, float loadFactor)
  1. 创建一个包含指定映射的 HashMap 对象,其中映射的键值对会被复制到新的 HashMap 中。
HashMap(Map<? extends K, ? extends V> m)
  1. 用于创建一个具有指定初始容量、加载因子和访问顺序的 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 时,会初始化 initialCapacityloadFactor, 在 put 第一对映射关系时,系统会创建一个长度为 initialCapacityNode 数组。数组每个 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 = 12threshold 值,也叫做临界值)的时候,就把数组的大小扩展为 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 的主要特点
  1. 线程安全性: ConcurrentHashMap 内部使用了分段锁(Segment)机制,将整个哈希表拆分为多个独立的部分,每个部分拥有自己的锁,不同的线程可以同时操作不同的部分,提高了并发性能。

  2. 高并发性: 相较于 HashTableConcurrentHashMap 在多线程并发读写的情况下能够提供更高的性能,因为它允许多个线程同时读取而不需要加锁

  3. 可伸缩性: ConcurrentHashMap 允许在加载因子达到阈值时对其进行自动扩容,从而适应动态变化的数据大小。

  4. 不保证有序性: ConcurrentHashMap 不保证迭代顺序与插入顺序或访问顺序一致。

ConcurrentHashMap 的构造方法
  1. 创建一个默认初始容量为16ConcurrentHashMap 实例。
ConcurrentHashMap()
  1. 创建一个指定初始容量的 ConcurrentHashMap 实例。
ConcurrentHashMap(int initialCapacity)
  1. 创建一个包含指定映射中所有键值对的 ConcurrentHashMap 实例。
ConcurrentHashMap(Map< ? extends K, ? extends V> map)
  1. 创建一个指定初始容量和加载因子的 ConcurrentHashMap 实例。
ConcurrentHashMap(int initialCapacity, float loadFactor)
  1. 创建一个指定初始容量、加载因子和并发级别的 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 的主要特点
  1. 保持顺序:LinkedHashMap 保持元素的插入顺序或访问顺序。插入顺序通过默认构造函数实现,而访问顺序需要设置为 accessOrder = true

  2. 继承自 HashMapLinkedHashMap 继承自 HashMap,因此具备了 HashMap 的快速访问和删除操作等特点。但因此LinkedHashMap也是线程不安全

  3. 消耗内存:相比于 HashMapLinkedHashMap 需要额外的内存来维护内部的链表结构。

LinkedHashMap 的五种构造方法
  1. 创建一个空的 LinkedHashMap 对象,默认初始容量为 16,加载因子为 0.75。
LinkedHashMap()
  1. 创建一个具有指定初始容量的 LinkedHashMap 对象。
LinkedHashMap(int initialCapacity)
  1. 创建一个具有指定初始容量和加载因子的 LinkedHashMap 对象。
LinkedHashMap(int initialCapacity, float loadFactor)
  1. 创建一个具有指定初始容量、加载因子和访问顺序的 LinkedHashMap 对象。
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

accessOrder 参数为 true 表示按照访问顺序进行迭代,为 false 表示按照插入顺序进行迭代。

  1. 创建一个包含指定映射的 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 主要特点
  1. 排序:TreeMap 通过键的自然顺序或自定义比较器来对键进行排序,并保持键的有序状态。

  2. 基于红黑树TreeMap 使用红黑树来实现键值对的存储。红黑树是一种自平衡的二叉搜索树,可以保持较好的性能和平衡性。

  3. 线程不安全TreeMap 不是线程安全的数据结构。在多线程环境下,如果多个线程同时并发地对 TreeMap 进行读取和修改操作,可能会导致数据不一致或产生其他不可预测的错误。

TreeMap 的四种构造方法
  1. 创建一个空的 TreeMap 对象,并按照键的自然顺序进行排序。
TreeMap()
  1. 创建一个空的 TreeMap 对象,按照指定的比较器 comparator 进行排序。
TreeMap(Comparator< ? super K> comparator)
  1. 创建一个 TreeMap 对象,并将已存在的map中的键值对加入到新的 TreeMap 中。使用键的自然顺序进行排序。
TreeMap(Map< ? extends K, ? extends V> map)
  1. 创建一个 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 主要特点
  1. 哈希表:HashTable 内部使用一个哈希表来存储键值对。当插入或查找键值对时,会根据键的哈希码计算存储位置,以提高查找效率。

  2. 线程安全HashTable 是线程安全的类,这意味着多个线程可以同时读取和修改 HashTable 的内容。为了实现线程安全,HashTable 使用了同步(synchronization)机制,使得在并发环境下能够正确地处理读写操作。

  3. 性能相对较差:HashTable 的所有方法都是同步的,它通过对常用的操作加锁来保证线程安全。在单线程环境下,可以使用 HashMap ,多线程环境下,可以使用 ConcurrentHashMap 替代

HashTable 的四种构造方法
  1. 创建一个空的 HashTable 对象,默认初始容量为 11,加载因子为 0.75。
HashTable()
  1. 创建一个具有指定初始容量的 HashTable 对象。
HashTable(int initialCapacity)
  1. 创建一个具有指定初始容量和加载因子的 HashTable 对象。
HashTable(int initialCapacity, float loadFactor)
  1. 创建一个包含指定映射的 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 主要特点和常用方法
  1. 键值对存储:Properties 使用键值对的方式来存储数据,其中键和值都是字符串类型属性文件中的每一行都表示一个键值对,以 “键=值” 的形式存在。

  2. 加载和保存属性文件:Properties 提供了加载和保存属性文件的方法,可以从属性文件中读取键值对数据,或将键值对数据保存到属性文件中。常用的方法包括:

    • load(InputStream in): 从 输入流 中加载属性文件。

    • load(Reader reader): 从 字符流 中加载属性文件。

    • store(OutputStream out, String comments): 将属性文件保存到输出流中。

    • store(Writer writer, String comments): 将属性文件保存到字符流中。

  3. 获取和设置属性值Properties 提供了一系列方法来获取和设置属性值,如:

    • getProperty(String key): 根据键获取对应的属性值。

    • getProperty(String key, String defaultValue): 根据键获取对应的属性值,若不存在则返回默认值。

    • setProperty(String key, String value): 设置指定键的属性值。

  4. 遍历属性Properties 提供了遍历属性的方法,可以逐个获取所有的键值对。常用的方法有:

    • propertyNames(): 返回包含所有键的枚举对象,可用于迭代遍历。

    • stringPropertyNames(): 返回包含所有键的字符串集合。

    • forEach(BiConsumer< ? super Object, ? super Object> action): 遍历所有的键值对,并执行指定的操作。

Properties 的构造方法
  1. Properties(): 创建一个空的 Properties 对象。

  2. Properties(Properties defaults): 创建一个 Properties 对象,并指定默认的属性。当获取属性值时,如果在当前 Properties 中找不到对应的值,将返回默认属性中指定的值。

Properties defaults = new Properties();
defaults.setProperty("defaultKey", "defaultValue");
Properties properties2 = new Properties(defaults);

Properties 类不常主动实例化,通常会通过加载配置文件来获得 Properties 实例,例如使用 InputStream 来读取属性文件,然后通过 load 方法将属性加载到 Properties 对象中:

![[Pasted image 20231020154024.png]]

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();
    }
}

解析:

  1. 读取属性文件

    • 使用 readProperties 方法加载属性文件到 Properties 对象中。
    • 使用 FileInputStream 打开属性文件。
    • 使用 Properties 对象的 load 方法加载文件内容。
    • 使用 printProperties 方法打印原始属性。
  2. 修改属性文件

    • 使用 setProperty 方法更新属性值。
    • 使用 saveProperties 方法将修改后的属性保存回文件系统。
    • 使用 FileOutputStream 打开属性文件以写入模式。
    • 使用 Properties 对象的 store 方法保存修改后的属性。
  3. 再次读取属性文件验证修改

    • 重新读取属性文件到 Properties 对象中。
    • 使用 printProperties 方法打印更新后的属性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值