本文介绍了 Java 中常用的数据结构与算法类。
1. String/StringBuffer/StringBuilder
String
类即字符串,在 Java 中 String
类是不可改变的,如果修改 String
对象,那么其实是开一个新的空间保存,而原空间中的字符串还存在于内存中。String
类的用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| String str = "Hello World!";
char[] charArray = { 'A', 'B', 'C' }; String strFromChar = new String(charArray);
for (int i = 0; i < str.length(); i++) System.out.print(str.charAt(i)); System.out.println();
for (char c: str.toCharArray()) System.out.print(c); System.out.println();
str.charAt(0); str.compareTo("G"); str.equals("Hello World!"); str.startsWith("He"); str.endsWith("d!"); str.indexOf('o'); str.indexOf('o', 5); str.lastIndexOf('o'); str.lastIndexOf('o', 6); str.replace('o', 'x'); str.split(" "); str.substring(6, 9); str.toLowerCase(); str.toUpperCase();
|
当需要对字符串进行频繁修改时,要用 StringBuffer
或 StringBuilder
类。这两个类的对象能够被多次修改,并且不产生新的未使用对象。
StringBuffer
和 StringBuilder
之间最大的不同在于 StringBuilder
的方法不是线程安全的(不能同步访问)。但是 StringBuilder
相较于 StringBuffer
有速度优势,所以多数情况下建议使用 StringBuilder
类。以 StringBuilder
为例,用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| StringBuilder strBuilder = new StringBuilder(str);
for (int i = 0; i < strBuilder.length(); i++) System.out.print(strBuilder.charAt(i)); System.out.println();
strBuilder.append("A"); strBuilder.reverse(); strBuilder.delete(0, 1); strBuilder.insert(6, "A"); strBuilder.replace(0, 7, "Hello"); strBuilder.toString();
|
2. Arrays
Arrays
类定义在 java.util
包中,该类能够方便地操作数组,用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| int[] arr = { 2, 4, 3, 1, 5 }; int[][] arr2d = {{ 1, 2 }, { 3, 4 }}; Integer[] arrInteger = { 2, 4, 3, 1, 5 };
for (int i = 0; i < arr.length; i++) System.out.print(arr[i] + " "); System.out.println();
for (int x: arr) System.out.print(x + " "); System.out.println();
Arrays.sort(arr); Arrays.sort(arrInteger, (x, y) -> y - x); Arrays.binarySearch(arr, 3); Arrays.equals(arr, arr); Arrays.fill(arr, 0); Arrays.toString(arr); Arrays.deepToString(arr2d);
|
3. ArrayList
Java 的集合框架主要包括两种类型的容器:
- 集合(Collection):存储一个元素集合。
- 图(Map):存储键/值对映射。
其中,Collection
接口又包含了 List
接口和 Set
接口两大分支,前者为允许存在重复元素的有序集合,后者为不允许存在重复元素的无序集合;Map
是一个映射接口,其中的每一个元素包含一个 key
和对应的 value
。
ArrayList
类实现了 List
接口,继承自 AbstractList
类,位于 java.util
包中(迭代器也位于该包中),可以用于动态地调整存储在其中的元素的大小。ArrayList
类是基于数组的数据结构,它提供了一组用于操作元素的方法,包括添加、删除、插入、搜索和排序等。
ArrayList
用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| ArrayList<Integer> v = new ArrayList<>(); ArrayList<Integer> v = new ArrayList<>(Arrays.asList(1, 2, 3)); List<Integer> v = Arrays.stream(new int[] { 1, 2, 3 }).boxed().collect(Collectors.toList());
for (int i = 0; i < v.size(); i++) System.out.printf("%d ", v.get(i)); System.out.println();
for (int x: v) System.out.printf("%d ", x); System.out.println();
for (Iterator<Integer> it = v.iterator(); it.hasNext();) System.out.printf("%d ", it.next()); System.out.println();
for (ListIterator<Integer> it = v.listIterator(); it.hasNext();) System.out.printf("%d ", it.next()); System.out.println();
v.stream().forEach(x -> System.out.printf("%d ", x)); System.out.println();
v.add(4); v.add(1, 5); v.remove(4); v.set(0, 4); v.subList(0, 2); v.indexOf(5); v.isEmpty(); v.contains(2); v.sort(Comparator.naturalOrder()); v.sort(Comparator.reverseOrder()); v.addAll(Arrays.asList(1, 2)); Collections.addAll(v, 7, 8, 9); Collections.reverse(v); ArrayList<Integer> t = (ArrayList<Integer>)v.clone();
Integer[] a = new Integer[v.size()]; v.toArray(a);
Integer[] b = v.toArray(new Integer[0]);
int[] c = v.stream().mapToInt(Integer::valueOf).toArray();
|
4. LinkedList
Java 的 LinkedList
类是一个实现了 List
接口和 Deque
接口的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
与 ArrayList
相比,LinkedList
的增加和删除的操作效率更高,而查找和修改的操作效率较低。因此需要频繁访问列表中的某一个元素且只需要在列表末尾进行添加和删除元素操作推荐使用 ArrayList
,而需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作推荐使用 LinkedList
。
LinkedList
的定义与遍历方式与 ArrayList
相同,且上述的 ArrayList
常用方法在 LinkedList
中均可使用,额外添加的部分方法如下:
1 2 3 4 5 6 7 8 9 10
| LinkedList<Integer> v = new LinkedList<>(Arrays.asList(1, 2, 3));
v.addLast(4); v.addFirst(5); v.removeLast(); v.removeFirst(); v.getFirst(); v.getLast();
|
5. HashSet/TreeSet
Java 的 HashSet
类是一个实现了 Set
接口的集合类,它使用 HashMap
作为基础,因此其特性和 HashMap
类似:
- 不重复:是一个没有重复元素的集合。
- 无序:它不保证元素的顺序。
- 允许
null
:允许使用 null
元素。
- 非同步:即不是线程安全的,如果多个线程尝试同时修改
HashSet
,则最终结果是不确定的。 必须在多线程访问时显式同步对 HashSet
的并发访问。
HashSet
用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| HashSet<Integer> st = new HashSet<>(); HashSet<Integer> st = new HashSet<>(Arrays.asList(1, 2, 3));
for (int x: st) System.out.printf("%d ", x); System.out.println();
for (Iterator<Integer> it = st.iterator(); it.hasNext();) System.out.printf("%d ", it.next()); System.out.println();
st.stream().forEach(x -> System.out.printf("%d ", x)); System.out.println();
st.add(4); st.remove(3); st.contains(3); st.size(); st.isEmpty(); st.clear();
|
TreeSet
与 HashSet
的用法基本一致,区别在于 TreeSet
中的元素是有序的:
1 2 3 4 5 6 7 8
| TreeSet<Integer> st = new TreeSet<>(); TreeSet<Integer> st = new TreeSet<>(Arrays.asList(3, 2, 1, 2));
st.ceiling(3); st.floor(3); ...其余同HashSet
|
6. HashMap/TreeMap
Java 的 HashMap
是一个实现了 Map
接口的哈希表,它存储的内容是键值对(key-value)映射。HashMap
的特性如下:
- 键值对存储:
HashMap
是以 key-value
存储形式存在,主要用来存放键值对。
- 非同步:
HashMap
的实现不是同步的,这意味着它不是线程安全的。
- 允许
null
:它的 key
、value
都可以为 null
。
- 无序:此外,
HashMap
中的映射不是有序的。
HashMap
的底层数据结构为:
- Java8 之前:
HashMap
由数组+链表组成,数组是 HashMap
的主体,链表则主要用于解决哈希碰撞。
- Java8 之后:在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。
HashMap
的保存顺序与插入顺序不同,这是因为 HashMap
使用了一种非常快速的算法,该算法会控制顺序。LinkedHashMap
则是按照插入顺序来保存键值对,同时保留了 HashMap
的查找速度。
HashMap
用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| HashMap<String, Integer> mp = new HashMap<>(); HashMap<String, Integer> mp = new HashMap<String, Integer>() {{ put("ABC", 1); put("DEF", 2); }};
for (String k: mp.keySet()) System.out.println(k);
for (int v: mp.values()) System.out.println(v);
for (Map.Entry<String, Integer> e: mp.entrySet()) System.out.println(e.getKey() + " " + e.getValue());
for (Iterator it = mp.entrySet().iterator(); it.hasNext();) { Map.Entry e = (Map.Entry)it.next(); System.out.println(e.getKey() + " " + e.getValue()); }
mp.put("YYJ", 666); mp.get("YYJ"); mp.remove("YYJ"); mp.containsKey("YYJ"); mp.containsValue(1); mp.size(); mp.clear(); mp.isEmpty();
|
与 Set
同理,TreeMap
与 HashMap
的用法基本一致,区别在于 TreeMap
中的元素是根据键的升序排好序的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| TreeMap<String, Integer> mp = new TreeMap<>(); TreeMap<String, Integer> mp = new TreeMap<String, Integer>() {{ put("DEF", 1); put("ABC", 2); }};
Map.Entry<String, Integer> ceiling = mp.ceilingEntry("AAA"); System.out.println(ceiling.getKey() + ": " + ceiling.getValue()); Map.Entry<String, Integer> floor = mp.floorEntry("DEG"); System.out.println(floor.getKey() + ": " + floor.getValue()); mp.ceilingKey("A"); mp.floorKey("A"); ...其余同HashMap
|
7. Stack
栈(Stack)是一个后进先出(LIFO)的集合,它有时被称作下推栈,因为不管我们最后压入的是什么,它都会第一个弹出。
Java 1.0 就提供了 Stack
类,结果这个类的设计非常糟糕,不过因为要向后兼容,所以我们永远也无法摆脱 Java 过去的设计错误了。Java 6 加入了 ArrayDeque
,提供了直接实现栈功能的方法:
1 2 3 4 5 6 7
| Deque<String> stk = new ArrayDeque<>();
for (String s: "This is stack".split(" ")) stk.push(s);
while (!stk.isEmpty()) System.out.print(stk.pop() + " ");
|
8. Queue/PriorityQueue
队列(Queue)是一个典型的先进先岀(FIFO)的集合。换言之,我们在一端放入在另一端拿出来,放入的顺序和取出的顺序是一样的。队列常用来将对象从程序的一个区域可靠地转移到另一个区域,队列在并发编程中特别重要,因为它们可以安全地将对象从一个任务转移到另一个任务。
LinkedList
实现了 Queue
接口,提供了支持队列行为的方法,因此 LinkedList
可以作为 Queue
的一种实现来使用,通过将 LinkedList
向上转型为 Queue
,下面这个示例使用了 Queue
接口特有的方法:
1 2 3 4 5 6 7 8 9
| Queue<Integer> Q = new LinkedList<>();
for (int i = 0; i < 5; i++) Q.offer(i);
System.out.println(Q);
while (Q.peek() != null) System.out.print(Q.remove() + " ");
|
其中:
offer()
是 Queue
特有的方法之一,负责在队列尾部插入一个元素,如果无法插入则返回 false
。
peek()
和 element()
都会返回队头元素,不会将其删除,但是如果队列为空,peek()
返回 null
,而 element()
抛出 NoSuchElementException
异常。
poll()
和 remove()
会将队头元素删除,同时返回该元素,但是如果队列为空,poll()
返回 null
,而 remove()
抛出 NoSuchElementException
异常。
优先级队列(Priority Queue)是指下一个要拿出的元素是需求最强烈的元素(最高优先级)。Java 5 中添加了 PriorityQueue
,为这种行为提供了一个自动化的实现。
PriorityQueue
默认的排序方法使用的是对象在队列中的自然顺序,但我们可以通过提供自己的 Comparator
来修改顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| PriorityQueue<Integer> Q = new PriorityQueue<>();
for (int i = 5; i > 0; i--) Q.offer(i);
while (Q.peek() != null) System.out.print(Q.remove() + " "); System.out.println();
List<Integer> nums = Arrays.asList(2, 5, 1, 3, 7); PriorityQueue<Integer> RQ = new PriorityQueue<>(Collections.reverseOrder()); RQ.addAll(nums);
while (RQ.peek() != null) System.out.print(RQ.remove() + " "); System.out.println();
|
9. 深入Iterator与Collection
Collection
是所有序列集合共同的根接口,可以认为它是一个为表示其他接口之间的共性而出现的附属接口。支持使用这样一个接口的理由是,它可以创建更通用的代码,通过面向接口而不是面向实现来编写代码,我们的代码可以应用于更多对象类型。因此,如果我们编写一个以 Collection
为参数的方法,那么该方法可以应用于任何实现了 Collection
的类型。
在 Java 中,看上去遵循 C++ 的方式比较明智,即使用迭代器而非 Collection
来表示集合之间的共性。但是,因为在 Java 中实现 Collection
也就意味着提供了 iterator()
方法,所以这两种方式其实是绑在一起了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package com.yyj;
import java.util.*;
public class Main { public static void main(String[] args) { List<Integer> numsList = new ArrayList<>(Arrays.asList(1, 2, 3, 3)); Set<Integer> numsSet = new HashSet<>(Arrays.asList(1, 2, 3, 3));
display(numsList.iterator()); display(numsSet.iterator()); display(numsList); display(numsSet); }
public static void display(Iterator<Integer> it) { while (it.hasNext()) System.out.print(it.next() + " "); System.out.println(); }
public static void display(Collection<Integer> nums) { for (Integer x: nums) System.out.print(x + " "); System.out.println(); } }
|
10. 自定义对象数组排序
我们需要让进行排序的类实现 Comparable
接口,重写其中的 compareTo()
方法,在其中定义排序规则,那么就可以直接调用 java.util.Arrays.sort()
来排序对象数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import java.util.Arrays;
class Data implements Comparable<Data> { int x; String s;
Data(int x, String s) { this.x = x; this.s = s; }
@Override public int compareTo(Data t) { return t.x - x; }
@Override public String toString() { return String.format("[%d, %s]", x, s); } }
public class SortObjectArrays { public static void main(String[] args) { Data[] datas = { new Data(4, "Hello"), new Data(1, "Java"), new Data(6, "C++") }; Arrays.sort(datas); Arrays.stream(datas).forEach(System.out::println);
} }
|