Java 集合类是用途广泛的工具类,可用于存储数量不等的对象,并可以实现常用的数据结构。Java 集合大致分为 SetListMapQueue 四种体系

  • Set 代表无序、不可重复的集合
  • List 代表有序、可重复的集合
  • Map 代表具有 key-value 键值映射关系的集合
  • Queue 代表队列集合

    数组和集合的区别

    Java 提供了数组数据类型,可以充当集合,为何还需要其他集合类?因为数组存在很多使用限制:

  • 数组初始化后大小不可变

  • 数组无法保存具有映射关系的数据
  • 数组只能按索引顺序存取

    Java 集合 UML 继承树

    TreeSet.png

    List 集合

    List 是最基础的集合,List 集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List 集合默认按元素的添加顺序设置元素的索引,索引从 0 开始,可以通过索引来访问指定位置的集合元素。

    ArrayList

    ArrayList 是最常用的有序列表。ArrayList 是基于数组实现的 List 接口,它内部封装了一个动态的、允许再分配的 Object[] 数组,当向 ArrayList 中添加元素超出了该数组的长度时,其内部 Object[] 数组会先进行扩容,每次扩容增加原来的一半大小,扩容将原数组中的所有元素拷贝到新数组后再添加新元素。

    ArrayList 和 数组扩容区别

    从一个已有的数组 {'A', 'B', 'C', 'D', 'E'} 中删除索引为 2 的元素:
    1. ┌───┬───┬───┬───┬───┬───┐
    2. A B C D E
    3. └───┴───┴───┴───┴───┴───┘
    4. ┌───┘
    5. ┌───┘
    6. ┌───┬───┬───┬───┬───┬───┐
    7. A B D E
    8. └───┴───┴───┴───┴───┴───┘
    这个“删除”操作实际上是把 'C' 后面的元素依次往前挪一个位置,而“添加”操作实际上是把指定位置以后的元素都依次向后挪一个位置,腾出来的位置给新加的元素。删除和添加两种操作用数组实现非常麻烦。
    ArrayList 把内部数组添加和删除的操作进行封装,操作 List 类似于操作数组,却不用关心内部元素如何移动,其原理简介如下:
    一个 ArrayList 拥有 5 个元素,实际 Object[] 数组大小为 6 (即保留一个空位):
    1. size=5
    2. ┌───┬───┬───┬───┬───┬───┐
    3. A B C D E
    4. └───┴───┴───┴───┴───┴───┘
    当添加一个元素并指定索引到 ArrayList 时,ArrayList 自动移动需要的元素:
    1. size=5
    2. ┌───┬───┬───┬───┬───┬───┐
    3. A B C D E
    4. └───┴───┴───┴───┴───┴───┘
    然后,往内部指定索引的数组位置添加一个元素,然后把 size + 1
    1. size=6
    2. ┌───┬───┬───┬───┬───┬───┐
    3. A B F C D E
    4. └───┴───┴───┴───┴───┴───┘
    继续向 ArrayList 中添加元素,但是数组已满,没有空闲位置的时候,ArrayList 先创建一个更大的新数组,新数组大小为原数组 length/2 + 1,然后把原数组的所有元素复制到新数组,再用新数组取代原数组,执行扩容后继续添加一个元素到数组末尾,同时 size + 1
    1. size=7
    2. ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
    3. A B F C D E G
    4. └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

    LinkedList

    LinkeList 是通过“链表”实现 List 接口,在 LinkedList 中,它内部的每个元素都指向下一个元素:
    1. ┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
    2. HEAD ──>│ A ●─┼──>│ B ●─┼──>│ C ●─┼──>│ D
    3. └───┴───┘ └───┴───┘ └───┴───┘ └───┴───┘
    LinkedList 还实现了 Deque 接口,提供双向队列,栈的功能

    ArrayList 和 LinkedList 区别

    是否保证线程安全?

    ArrayListLinkedList 都是不同步的,也就是不保证线程安全;

    底层数据结构有何区别?

    ArrayList 底层使用的是 Object[] 数组;LinkedList 底层使用的是 双向链表 数据结构

    插入和删除是否受元素位置的影响?

  1. ArrayList 采用数组存储,插入和删除元素的时间复杂度受元素位置的影响。例如:指向 add(E e) 方法时,ArrayList 默认将指定元素追加到此列表的末尾,这种情况时间复杂度为 O(1)。但如果要在指定位置 i 处插入和删除元素时 add(int index,E e),因为在执行上述操作时集合中第 i 和 第 i 个元素之后的 n-i 个元素都要执行向后或向前位移一位的操作,时间复杂度为 O(n-i)
  2. LinkedList 采用链表存储,对于 add(E e) 方法的插入,删除元素时间复杂度不受元素位置的影响近似为 O(1),如果是要在指定位置 i 插入和删除元素时 add(int index,E e) ,因为需要先移动到指定个位置后在插入,时间复杂度近似为 O(n)

    是否支持快速随机访问?

    快速随机访问是指通过元素的序号快速获取元素对于 get(int index) 方法。LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。

    内存空间占用区别?

    ArrayList 的空间浪费主要体现在 List 列表的尾部会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArayList中元素更多的空间,每个元素都要存放直接后继(后一个)和直接前驱(前一个)以及数据
    通常情况下,总是优先使用 ArrayList
ArrayList LinkedList
获取指定元素 速度很快 需要从头开始查找元素
添加元素到末尾 速度很快 速度很快
在指定位置添加/删除元素 需要移动元素 不需要移动元素
内存占用 较大

List 和 Array 转换

第一种:调用 toArray() 方法直接返回一个 Object[] 数组

  1. public class Main {
  2. public static void main(String[] args) {
  3. List<String> list = List.of("apple", "pear", "banana");
  4. Object[] array = list.toArray();
  5. for (Object s : array) {
  6. System.out.println(s);
  7. }
  8. }
  9. }

第二种:调用 toArray(T[]) 方法传入一个类型相同的 ArrayList 内部自动把元素复制到传入的 Array

  1. public class Main {
  2. public static void main(String[] args) {
  3. List<Integer> list = List.of(12, 34, 56);
  4. Integer[] array = list.toArray(new Integer[3]);
  5. for (Integer n : array) {
  6. System.out.println(n);
  7. }
  8. }
  9. }

第三种:通过 List 接口定义的 T[] toArray(IntFuntion<T[]> generator) 方法

  1. Integer[] array = list.toArray(Integer[]::new);

ArrayList 通过 List.of(T...) 方法

  1. Integer[] array = { 1, 2, 3 };
  2. List<Integer> list = List.of(array);
  3. // 注意,返回的 List 不是 ArrayList 实现类,而是 Arrays 的内部类 ArrayList。并且返回的是只读的
  4. List<Integer> list = Arrays.asList(array);

equals() 方法

List 中查找元素时,List 接口的实现类通过元素的 equals() 方法比较两个元素是否相等,因此,放入的元素必须正确覆写 equals() 方法,Java 标准库提供的 StringInteger 等已经覆写了 equals() 方法

  1. public class Main {
  2. public static void main(String[] args) {
  3. List<String> list = List.of("A", "B", "C");
  4. System.out.println(list.contains(new String("C"))); // true
  5. System.out.println(list.indexOf(new String("C"))); // 2
  6. List<Person> list = List.of(
  7. new Person("Xiao Ming"),
  8. new Person("Xiao Hong"),
  9. new Person("Bob")
  10. );
  11. System.out.println(list.contains(new Person("Bob"))); // false
  12. }
  13. }
  14. class Person {
  15. String name;
  16. public Person(String name) {
  17. this.name = name;
  18. }
  19. }

Set 集合

Set 集合不允许包含相同的元素,如果视图把两个相同的元素加入同一个 Set 集合中,则添加操作失败,add() 方法返回 false,且新元素不会被加入。Set 集合多用于去除重复元素

  1. public class Main {
  2. public static void main(String[] args) {
  3. Set<String> set = new HashSet<>();
  4. System.out.println(set.add("abc")); // true
  5. System.out.println(set.add("xyz")); // true
  6. System.out.println(set.add("xyz")); // false,添加失败,因为元素已存在
  7. System.out.println(set.contains("xyz")); // true,元素存在
  8. System.out.println(set.contains("XYZ")); // false,元素不存在
  9. System.out.println(set.remove("hello")); // false,删除失败,因为元素不存在
  10. System.out.println(set.size()); // 2,一共两个元素
  11. }
  12. }

HashSet

HashSetSet 接口的典型实现。当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置。

当把某个类的对象放入 HashSet 集合中时,重写该对象对应类的 equals() 方法和 hashCode() 方法时,应该尽量保证两个对象通过 equals() 方法比较返回 true 时,它们的 hashCode() 方法返回值也相等。

观察 HashSet 源码可发现 HashSet 仅仅是对 HashMap 的一个简单封装,它的核心代码如下:

  1. public class HashSet<E> implements Set<E> {
  2. // 持有一个HashMap:
  3. private transient HashMap<E, Object> map;
  4. // 放入HashMap的value:
  5. private static final Object PRESENT = new Object();
  6. public HashSet() {
  7. map = new HashMap<>();
  8. }
  9. public boolean add(E e) {
  10. return map.put(e, PRESENT) == null;
  11. }
  12. public boolean contains(Object o) {
  13. return map.containsKey(o);
  14. }
  15. public boolean remove(Object o) {
  16. return map.remove(o) == PRESENT;
  17. }
  18. }

TreeSet

Set 接口并不保证有序,而 SortedSet 接口则保证元素是有序的。TreeSet 集合实现了 SortedSet 接口,TreeSet 采用红黑树的数据结构来存储集合元素。TreeSet 支持两种排序方法:自然排序和定制排序。在默认情况下,TreeSet 采用自然排序

Comparable 比较接口

Java 提供了 Comparable 接口,它负责比较传入的两个元素 ab,如果 a<b,则返回 -1,如果 a==b,则返回 0,如果 a>b,则返回 1StringInteger 等常见类型已经实现了 Comparable 接口,JDK 也为常见类型提供了内置比较器

  1. Set<Integer> demoBeans = new TreeSet<>(Comparator.comparingInt(q -> q));

HashSet 和 TreeSet 区别

HashSetTreeSetSet 的两个典型实现,而 HashSet 的性能总是比 TreeSet 好,尤其是最常用的添加、查询元素等操作。TreeSet 需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的 Set 时,才应该使用 TreeSet,否则都应该使用 HashSet

Map 集合

Map<K,V> 是一种键-值映射表,当调用 put(K key,V value) 方法时,就把 keyvalue 做了映射并放入 Map。当调用 V get(K key) 方法时,通过 key 获取到对应的 value。如果 key 不存在,则返回 null

调用 put() 方法插入 Map 时,若放入的 key 已经存在,put() 方法会返回被删除的旧的 value,否则返回 nullMap 中不存在重复的 key,因为放入相同的 key,只会把原油的 key-value 对应的 value 给替换掉

遍历 Map

  • 仅遍历 Map 中的 key 可使用 for each 循环遍历 Map 实例的 keySet() 方法返回的 Set 集合,它包含不重复的 key 的集合
  • 同时遍历 keyvalue 可使用 for each 循环遍历 Map 实例的 entrySet() 集合,它包含每一个 key-value 映射

    遍历 Map 集合时,不可假设输出的 key 是有序的,Map 不保证顺序

HashMap

HashMap 内部通过空间换时间的方法,用一个大数组存储所有 value,并根据 key 直接计算出 value 应用存储在哪个索引:

  1. Map<String, Person> map = new HashMap<>();
  2. map.put("a", new Person("Xiao Ming"));
  3. map.put("b", new Person("Xiao Hong"));
  4. map.put("c", new Person("Xiao Jun"));
  5. map.get("a"); // Person("Xiao Ming")
  6. map.get("x"); // null
  1. ┌───┐
  2. 0
  3. ├───┤
  4. 1 ●─┼───> Person("Xiao Ming")
  5. ├───┤
  6. 2
  7. ├───┤
  8. 3
  9. ├───┤
  10. 4
  11. ├───┤
  12. 5 ●─┼───> Person("Xiao Hong")
  13. ├───┤
  14. 6 ●─┼───> Person("Xiao Jun")
  15. ├───┤
  16. 7
  17. └───┘

equals() 和 hashCode()

HashMap 内部,对 key 做比较是通过 equals() 方法实现的,如果传入的 key 的内容相同但对象不同时,两个不同对象的 key 可能获取到的 value 是一样的

  1. public class Main {
  2. public static void main(String[] args) {
  3. String key1 = "a";
  4. Map<String, Integer> map = new HashMap<>();
  5. map.put(key1, 123);
  6. String key2 = new String("a");
  7. map.get(key2); // 123
  8. System.out.println(key1 == key2); // false
  9. System.out.println(key1.equals(key2)); // true
  10. }
  11. }

为避免上述情况发生,正确使用 Map 必须保证:

  1. 要求作为 key 的对象必须正确覆写 equals() 方法,相等的两个 key 实例的 equals() 必须返回 true。常用的 String 类型默认已经覆写了 equals() 方法。
  2. 如果两个对象相等,则两个对象的 hashCode() 必须相等
  3. 如果两个对象不相等,则两个对象的 hashCode 尽量不要相等

Q:HashMap 内部使用了数组,并通过计算 keyhashCode() 直接定位 value 所在的索引,hashCode() 方法返回的 int 范围高达 ±21 亿,HashMap 内部使用的数组大小如何确定的?
A:观察 HashMap 源码,HaspMap 初始化时默认的数组大小只有 16 位。往 HashMap 中插入任何 key ,无论它的 hashCode() 结果多大,计算出的索引范围一定在 0~15

  1. public class HashMap<K,V> {
  2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  3. }
  4. int index = key.hashCode() & 0xf; // 0xf = 15

Q:如果添加超过 16 个元素到 HashMap 中是,内部数组不够用时如何扩容?
A:HashMap 中添加超过一定数量的元素时,HashMap 会在内部自动扩容,每次扩容一倍,即长度为 16 的数组扩容为 32。大小扩容后会重新计算 hashCode() 方法生成新的索引位置。

  1. int index = key.hashCode() & 0x1f; // 0x1f = 31

Q:如果两个不同的 key,如 "a""b",假设它们的 hashCode() 恰好是相同的,此时往 Map 中分别插入两个元素会造成覆盖吗?
A:结果是不会造成覆盖。使用 Map 时,只要 key 不相同,它们的 value 就互不干扰。在 HashMap 内部数组中使用 List 存储 value。假设两者计算得到索引为 5,List 中将包含两个 Entry,一个是 "a" 的映射,另一个是 "b" 的映射

  1. ┌───┐
  2. 0
  3. ├───┤
  4. 1
  5. ├───┤
  6. 2
  7. ├───┤
  8. 3
  9. ├───┤
  10. 4
  11. ├───┤
  12. 5 ●─┼───> List<Entry<String, Person>>
  13. ├───┤
  14. 6
  15. ├───┤
  16. 7
  17. └───┘

EnumMap

如果作为 key 的对象是 enum 类型,最佳实践是使用 EnumMap,它在内部以一个非常紧凑的数组存储 value,并且根据 enum 类型的 key 直接定位到内部数组的索引,并不需要计算 hashCode(),不但效果高,而且没有额外的空间浪费

  1. public class Main {
  2. public static void main(String[] args) {
  3. Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
  4. map.put(DayOfWeek.MONDAY, "星期一");
  5. map.put(DayOfWeek.TUESDAY, "星期二");
  6. map.put(DayOfWeek.WEDNESDAY, "星期三");
  7. map.put(DayOfWeek.THURSDAY, "星期四");
  8. map.put(DayOfWeek.FRIDAY, "星期五");
  9. map.put(DayOfWeek.SATURDAY, "星期六");
  10. map.put(DayOfWeek.SUNDAY, "星期日");
  11. System.out.println(map);
  12. System.out.println(map.get(DayOfWeek.MONDAY));
  13. }
  14. }

TreeMap

TreeMap 实现了排序键值对接口 SortedMap 接口,保证遍历 Map 时按 key 的顺序进行排序。
String 类型默认字母排序

  1. public class Main {
  2. public static void main(String[] args) {
  3. Map<String, Integer> map = new TreeMap<>();
  4. map.put("orange", 1);
  5. map.put("apple", 2);
  6. map.put("pear", 3);
  7. for (String key : map.keySet()) {
  8. System.out.println(key);
  9. }
  10. // apple, orange, pear
  11. }
  12. }

SortedMap 要求作为 key 的对象必须实现 Comparable 接口或者传入 Comparator 实例,并且实现方案必须严格遵循 compare() 规范,否则将不能正常工作

  1. public class Main {
  2. public static void main(String[] args) {
  3. Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
  4. public int compare(Student p1, Student p2) {
  5. return p1.score > p2.score ? -1 : 1;
  6. }
  7. });
  8. map.put(new Student("Tom", 77), 1);
  9. map.put(new Student("Bob", 66), 2);
  10. map.put(new Student("Lily", 99), 3);
  11. for (Student key : map.keySet()) {
  12. System.out.println(key);
  13. }
  14. System.out.println(map.get(new Student("Bob", 66))); // null。因为 compare 在两者相等时没有返回 0
  15. }
  16. }
  17. class Student {
  18. public String name;
  19. public int score;
  20. Student(String name, int score) {
  21. this.name = name;
  22. this.score = score;
  23. }
  24. public String toString() {
  25. return String.format("{%s: score=%d}", name, score);
  26. }
  27. }

Properties

读取配置文件类,是特殊的键值对集合,它内部继承了 Hashtable
典型的配置文件如下:

  1. # setting.properties
  2. last_open_file=/data/hello.txt
  3. auto_save_interval=60

使用 Properties 读取配置文件:

  1. String f = "setting.properties";
  2. Properties props = new Properties();
  3. // 1.从文件系统中读取
  4. props.load(new java.io.FileInputStream(f));
  5. // 2.从 classpath 中读取
  6. props.load(getClass().getResourceAsStream("/common/setting.properties"));
  7. // 3.使用 Reader 指定编码格式读取
  8. props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
  9. String filepath = props.getProperty("last_open_file");
  10. String interval = props.getProperty("auto_save_interval", "120");

使用 Properties 写入配置文件:

  1. Properties props = new Properties();
  2. props.setProperty("url", "http://www.liaoxuefeng.com");
  3. props.setProperty("language", "Java");
  4. props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");

Queue

“先进先出”(FIFO:First In First Out)队列,Queue 与其他容器不同,它仅有两个操作:

  • 把元素添加到队列末尾
  • 从队列头部取出元素

超市的收银台就是一个典型的队列:
image.png
对于具体的实现类,有的Queue有最大队列长度限制,有的 Queue 没有。注意到添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,这两个方法的行为是不同的。我们用一个表格总结如下:

throw Exception 返回false或null
添加元素到队尾 add(E e) boolean offer(E e)
取队首元素并删除 E remove() E poll()
取队首元素但不删除 E element() E peek()

Queue 中添加和取出元素示例代码:

  1. public static void main(String[] args) {
  2. Queue<String> queue = new ArrayDeque<>();
  3. queue.offer("1");
  4. queue.offer("2");
  5. queue.offer("3");
  6. queue.offer("4");
  7. queue.offer("5");
  8. queue.offer("6");
  9. boolean hasNext = true;
  10. do {
  11. final String poll = queue.poll();
  12. log.debug(poll);
  13. hasNext = StrUtil.isNotEmpty(poll);
  14. } while (hasNext);
  15. }
  16. // 依次输出
  17. // 1 2 3 4 5 6

PriorityQueue

PriorityQueue 优先队列实现了 Queue 接口,Queue 队列只能按入列顺序逐个取出元素,而 PriorityQueue 队列的出队顺序与元素的优先级有关,调用 remove()poll() 方法返回的总是优先级最高的元素
PriorityQueue 默认按元素比较的顺序排序,也可以通过 Comparator 自定义排算法

  1. public static void main(String[] args) {
  2. // 按照大小,较小的在前面
  3. final Queue<Integer> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(s -> s));
  4. priorityQueue.offer(6);
  5. priorityQueue.offer(5);
  6. priorityQueue.offer(4);
  7. priorityQueue.offer(3);
  8. priorityQueue.offer(2);
  9. priorityQueue.offer(1);
  10. do {
  11. final Integer poll = priorityQueue.poll();
  12. if (poll == null) {
  13. break;
  14. }
  15. log.debug(poll.toString());
  16. } while (true);
  17. }
  18. // 依次输出
  19. // 1 2 3 4 5 6

Deque

Queue 队列只能一头进,另一头出。而 Deque 队列允许两头都进,两头都出,称之为双向对列

  • 既可以添加到队尾,也可以添加到对首
  • 既可以从对首获取,又可以从队尾获取

DequeQueue 出对和入队的区别

Queue Deque
添加元素到队尾 add(E e) / offer(E e) addLast(E e) / offerLast(E e)
取队首元素并删除 E remove() / E poll() E removeFirst() / E pollFirst()
取队首元素但不删除 E element() / E peek() E getFirst() / E peekFirst()
添加元素到队首 addFirst(E e) / offerFirst(E e)
取队尾元素并删除 E removeLast() / E pollLast()
取队尾元素但不删除 E getLast() / E peekLast()

Stack

Stack 栈是一种后进先出的数据结构,只能不断的往 Stack 中压入 push 元素,最后进去的元素必须最早弹出 pop。Java 集合类中没有单独的 Stack 接口,使用 Deque 接口来“模拟” Stack

  1. public static void main(String[] args) {
  2. Deque<Integer> deque = new ArrayDeque<>();
  3. deque.push(1);
  4. deque.push(2);
  5. deque.push(3);
  6. do {
  7. final Integer poll = deque.poll();
  8. if (poll == null) {
  9. break;
  10. }
  11. log.debug(poll.toString());
  12. } while (true);
  13. // 依次输出
  14. // 3 2 1
  15. }

当把 Deque 用作 Stack 使用时,注意只使用 push()/pop()/peek() 方法,不用调用 addFirst()/removeFirst()/peekFirst() 方法,这样使代码更清晰

Stack 用法

一、JVM 使用 Stack 维护方法调用的层次

  1. static void main(String[] args) {
  2. foo(123);
  3. }
  4. static String foo(x) {
  5. return "F-" + bar(x + 1);
  6. }
  7. static int bar(int x) {
  8. return x << 2;
  9. }

JVM 会创建方法调用栈,每调用一个方法时,先将参数压入栈,然后指向对应的方法;当方法返回时,返回值压入栈,调用方法通过出栈操作获得方法返回值。
方法调用栈存在容量限制,嵌套调用过多会造成栈溢出,即引发 StackOverflowError

  1. static int inc(int data){
  2. return inc(data)+1;
  3. }
  4. public static void main(String[] args) {
  5. inc(1);
  6. }

二、用 Stack 对整数进行进制转换

int 整数 12500 转换为十六进制表示的字符串:

  1. 准备一个空栈

    1. └───┘
  2. 计算 12500÷16=781…4,余数是4,把余数4压栈:

    1. 4
    2. └───┘
  3. 计算 781÷16=48…13,余数是1313的十六进制用字母D表示,把余数D压栈:

    1. D
    2. 4
    3. └───┘
  4. 计算 48÷16=3…0,余数是0,把余数0压栈:

    1. 0
    2. D
    3. 4
    4. └───┘
  5. 计算 3÷16=0…3,余数是3,把余数3压栈:

    1. 3
    2. 0
    3. D
    4. 4
    5. └───┘
  6. 当商是 0 的时候,结束计算。将栈中的所有元素依次弹出,组成字符串 30D4,这就是十进制整数 12500 的十六进制表示的字符串 ```java static final String[] HEX_CHARS = new String[]{“0”, “1”, “2”, “3”, “4”, “5”, “6”, “7”, “8”, “9”, “A”, “B”, “C”, “D”, “E”, “F”}; static String toHex(int data) { Deque stack = new ArrayDeque<>(); do {

    1. int result = data % HEX_CHARS.length;
    2. stack.push(HEX_CHARS[result]);
    3. data = data / HEX_CHARS.length;

    } while (data != 0);

    StringBuilder sb = new StringBuilder(stack.size()); do {

    1. sb.append(stack.poll());

    } while (stack.size() > 0);

    return sb.toString(); }

public static void main(String[] args) { var hex = toHex(12500); log.debug(“{} toHex = {}”, 12500, hex); // 30D4 // JDK 写法 final String hexString = Integer.toHexString(12500).toUpperCase(); log.debug(“{} toHexString = {}”, 12500, hexString); // 30D4 } ```