Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

Java 集合框架并不是一蹴而就写成的,也是经过了好多个版本迭代的演进与发展,才走到今天。本文总结下集合框架各版本的功能增强。

Java SE 9

ListSetMap 接口中,新的静态工厂方法可以创建这些集合的不可变实例(immutable),如下:

1
2
3
List<String> list = List.of("apple", "orange", "banana");
Set<String> set = Set.of("aggie", "alley", "steely");
Map<String, String> map = Map.of("A", "Apple", "B", "Boy", "C", "Cat");

参考:https://www.linuxidc.com/Linux/2017-10/147683.htm

Java SE 8

Java SE 7

  • 新增一个集合接口:TransferQueue,以及实现类 LinkedTransferQueue
  • Map 及其派生实现类引入了一个性能改进的替代版散列函数(但在 Java SE 8 已被移除并取代)。

Java SE 6

新增几个集合接口:

  • Deque
  • BlockingDeque
  • NavigableSet
  • NavigableMap
  • ConcurrentNavigableMap

新增几个集合实现类:

  • ArrayDeque
  • ConcurrentSkipListSet
  • ConcurrentSkipListMap
  • LinkedBlockingDeque
  • AbstractMap.SimpleEntry
  • AbstractMap.SimpleImmutableEntry

现有实现类增强:

  • LinkedList 实现 Deque 接口
  • TreeSet 实现 NavigableSet 接口
  • TreeMap 实现 NavigableMap 接口

Collections 工具类新增两个适配器方法:

  • newSetFromMap(Map) 根据 Map 的通用实现创建一个 Set 的通用实现
  • asLifoQueue(Deque) 以后进先出(Lifo)队列的形式返回 Deque 的视图。

Arrays 工具类新增两个方法:

  • copyOf
  • copyOfRange

Java SE 5

三个新增的语法糖显著增强了集合框架:

  • 泛型:为集合框架添加编译时类型安全,并在读取元素时不再需要做类型转换。

  • 自动装箱/拆箱:往集合插入元素时自动装箱(将原始数据类型转换为对应的包装类型),读取元素时自动拆箱。

  • 增强 for 循环:迭代集合时不再需要显式迭代器(Iterator)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 数组迭代
    String[] strArray = {"apple", "orange", "banana"};
    for (String s : strArray) {
    System.out.println(s);
    }

    // List 迭代
    List<String> strList = Arrays.asList("apple", "orange", "banana");
    for (String s : strList) {
    System.out.println(s);
    }

通用实现与并发实现:

  • 新增三个集合接口:
    • Queue
    • BlockingQueue
    • ConcurrentMap
  • 新增几个 Queue 实现类:
    • PriorityQueue
    • ConcurrentLinkedQueue
    • LinkedList 实现 Queue 接口
    • AbstractQueue 抽象类实现
  • 新增五个 BlockingQueue 实现类,位于 java.util.concurrent 包下:
    • LinkedBlockingQueue
    • ArrayBlockingQueue
    • PriorityBlockingQueue
    • DelayQueue
    • SynchronousQueue
  • 新增一个 ConcurrentMap 实现类:
    • ConcurrentHashMap

特殊实现:

  • 新增两个特殊用途的 ListSet 实现类,用于读远大于写以及迭代无法线程同步的情况:
    • CopyOnWriteArrayList
    • CopyOnWriteArraySet
  • 新增两个特殊用途的 SetMap 实现类,用于枚举:
    • EnumSet
    • EnumMap

包装器实现:

  • 新增一位包装器实现家族的新成员 Collections.checkedInterface ,主要用于通用集合。

Collections 工具类新增三个通用算法和一个 Comparator 转换器:

  • frequency(Collection<?> c, Object o) 计算指定元素在指定集合中出现的次数。
  • disjoint(Collection<?> c1, Collection<?> c2) 求两个集合是否不相交。
  • addAll(Collection<? super T> c, T... a) 将指定数组中的所有元素添加到指定集合的便捷方法。
  • Comparator<T> reverseOrder(Comparator<T> cmp) 反向排序。

Arrays 工具类新增下列方法:

  • hashCodetoString
  • deepEqualsdeepHashCodedeepToString 用于多维数组

Java SE 1.4

  • Collections 工具类新增几个新方法,例如 :
    • replaceAll(List list, Object oldVal, Object newVal) 查找替换。
  • 新增标记接口 RandomAccess
  • 新增集合实现类 LinkedHashMapLinkedHashSet。内部使用散列表 + 双向链表(按插入顺序排序)。

参考

http://openjdk.java.net/jeps/180

集合接口中的许多修改方法都被标记为可选(optional)。实现类允许按需实现,未实现的方法需抛出运行时异常 UnsupportedOperationException。每个实现类的文档必须指明支持哪些可选操作。集合框架引入下列术语来帮助阐述本规范:

定长/变长

长度保证不变(即使元素可以更改)的列表称为 fixed-size 定长列表。反之则称为 variable-size 变长列表。

开发中接触最多的定长集合是通过 Arrays.asList() 创建的,该方法是一个适配器接口,将数组适配为定长列表,返回的对象是一个 Arrays 内部类,源码如下:

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
/**
* Returns a fixed-size list backed by the specified array. (Changes to
* the returned list "write through" to the array.) This method acts
* as bridge between array-based and collection-based APIs, in
* combination with {@link Collection#toArray}. The returned list is
* serializable and implements {@link RandomAccess}.
*
* <p>This method also provides a convenient way to create a fixed-size
* list initialized to contain several elements:
* <pre>
* List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
* </pre>
*
* @param <T> the class of the objects in the array
* @param a the array by which the list will be backed
* @return a list view of the specified array
*/
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;

ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}

......

}

分析发现,涉及元素增删的操作(如 add()、remove()、clear())该内部类并没有实现,而是使用了父类 AbstractList 的方法,默认抛出 UnsupportedOperationException 异常:

1
2
3
4
5
6
7
8
9
10
11
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

public void add(int index, E element) {
throw new UnsupportedOperationException();
}

public E remove(int index) {
throw new UnsupportedOperationException();
}

}

参考手册:

arrays_aslist

可改/不可改

不支持修改操作(例如 addremoveclear)的集合称为 unmodifiable 不可修改集合。反之则称为 modifiable 可修改集合。Collections 工具类提供了一组静态工厂方法,用于包装并返回指定集合的不可修改视图(unmodifiable view),如果尝试修改,则会抛出 UnsupportedOperationException

1
2
3
4
5
6
7
8
Collections.unmodifiableCollection
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableNavigableSet
Collections.unmodifiableList
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
Collections.unmodifiableNavigableMap

从源码分析,该包装类覆盖了所有修改方法并抛出异常 UnsupportedOperationException,实现非常简单:

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
static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 1820017752578914078L;

final Collection<? extends E> c;

UnmodifiableCollection(Collection<? extends E> c) {
if (c==null)
throw new NullPointerException();
this.c = c;
}

public boolean add(E e) {
throw new UnsupportedOperationException();
}
public boolean remove(Object o) {
throw new UnsupportedOperationException();
}
public boolean addAll(Collection<? extends E> coll) {
throw new UnsupportedOperationException();
}
public boolean removeAll(Collection<?> coll) {
throw new UnsupportedOperationException();
}
public boolean retainAll(Collection<?> coll) {
throw new UnsupportedOperationException();
}
public void clear() {
throw new UnsupportedOperationException();
}

......

}

可变/不可变

unmodifiable 的基础上,加之保证 Collection 实现类的底层数据为 final 的集合称为 immutable 不可变集合。反之则称为 mutable 可变集合。

Java 9 为 ListSetMap 接口提供了新的静态工厂方法,可以创建这些集合的不可变实例,如下:

1
2
3
List<String> list = List.of("apple", "orange", "banana");
Set<String> set = Set.of("aggie", "alley", "steely");
Map<String, String> map = Map.of("A", "Apple", "B", "Boy", "C", "Cat");

而 Java 9 之前,要实现不可变集合只能通过第三方库,例如用 Guava 实现相同效果:

1
2
3
List<String> list = ImmutableList.of("apple", "orange", "banana");
Set<String> set = ImmutableSet.of("aggie", "alley", "steely");
Map<String, String> map = ImmutableMap.of("A", "Apple", "B", "Boy", "C", "Cat");

Guava 提供的不可变集合 API 如下:

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
ImmutableAsList
ImmutableBiMap
ImmutableClassToInstanceMap
ImmutableCollection
ImmutableEntry
ImmutableEnumMap
ImmutableEnumSet
ImmutableList
ImmutableListMultimap
ImmutableMap
ImmutableMapEntry
ImmutableMapEntrySet
ImmutableMapKeySet
ImmutableMapValues
ImmutableMultimap
ImmutableMultiset
ImmutableRangeMap
ImmutableRangeSet
ImmutableSet
ImmutableSetMultimap
ImmutableSortedAsList
ImmutableSortedMap
ImmutableSortedMapFauxverideShim
ImmutableSortedMultiset
ImmutableSortedMultisetFauxverideShim
ImmutableSortedSet
ImmutableSortedSetFauxverideShim
ImmutableTable

immutable_collections

使用如下:

1
2
3
4
5
List<String> list = ImmutableList.of("apple", "orange", "banana");
Set<String> set = ImmutableSet.of("aggie", "alley", "steely");
Map<String, String> map = ImmutableMap.of("A", "Apple", "B", "Boy", "C", "Cat");

log.info("list={}, set={}, map={}", fruits, marbles, map); // list=[apple, orange, banana], set=[aggie, alley, steely], map={A=Apple, B=Boy, C=Cat}

除此之外,Apache Commons Lang 也提供了两个好用的类 PairTriple,可用于存放指定个数的临时数据:

1
2
Triple.of("left", "middle", "right")
Pair.of("left", "right")

methods_of_Pair_and_Triple

线程同步/非同步

参考线程同步包装器

随机/顺序访问

支持根据下标索引快速(时间复杂度 0(1))访问元素的列表称为 random access 随机访问列表。反之则称为 sequential access 顺序访问列表。

标记接口 java.util.RandomAccess 用于标记列表类支持随机访问,其实现类如下:

RandomAccess

该标记接口使得 Collections 工具类中的通用算法实现能够据此更改其行为以提升性能:

  • binarySearch
  • reverse
  • shuffle
  • fill
  • copy
  • rotate
  • replaceAll
  • indexOfSubList
  • lastIndexOfSubList
  • checkedList

binarySearch 为例,源码判断如下:

1
2
3
4
5
6
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}

元素限制

某些集合实现限制了可以存储哪些元素。可能的限制包括:

  • 元素不能为 null
  • 元素必须属于特定类型。
  • 元素必须匹配某些断言。

尝试添加违反集合实现限制的元素将导致运行时异常,如 ClassCastExceptionIllegalArgumentExceptionNullPointerException

能否为 null

参考手册:

map_element_of_null

类型限制

泛型机制虽然为集合提供了编译期类型检查,但仍然可以在运行期绕过此机制(通过反射也能绕过编译期类型检查):

1
2
3
4
5
6
7
8
9
10
11
12
13
public void test4() {
List<Integer> intList = Lists.newArrayList(1, 2, 3);
add(intList);

// 循环到第四个元素时,报错:java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
for (int item : intList) {
log.info("result is {}", item);
}
}

private void add(List list) {
list.add("4");
}

集合框架提供了一组包装器实现:

1
2
3
4
5
6
7
8
9
Collections.checkedCollection
Collections.checkedQueue
Collections.checkedSet
Collections.checkedSortedSet
Collections.checkedNavigableSet
Collections.checkedList
Collections.checkedMap
Collections.checkedSortedMap
Collections.checkedNavigableMap

这些包装器实现用于返回指定集合的动态类型安全视图(dynamically type-safe view),核心源码如下:

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
static class CheckedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 1578914078182001775L;

final Collection<E> c;
final Class<E> type;

@SuppressWarnings("unchecked")
E typeCheck(Object o) {
if (o != null && !type.isInstance(o))
throw new ClassCastException(badElementMsg(o));
return (E) o;
}

private String badElementMsg(Object o) {
return "Attempt to insert " + o.getClass() +
" element into collection with element type " + type;
}

CheckedCollection(Collection<E> c, Class<E> type) {
this.c = Objects.requireNonNull(c, "c");
this.type = Objects.requireNonNull(type, "type");
}

public boolean add(E e) {
return c.add(typeCheck(e));
}

......

}

add 方法为例,每次添加元素时,都会调用 typeCheck 私有方法进行类型检查,如果尝试添加错误类型的元素,则会抛出 ClassCastException,通过 fail fast 防止后续出错:

1
2
3
4
5
6
7
8
9
10
11
12
public void test() {
List<Integer> intList = Collections.checkedList(Lists.newArrayList(1, 2, 3), Integer.class);
add(intList);
for (int item : intList) {
log.info("result is {}", item);
}
}

private void add(List list) {
// java.lang.ClassCastException: Attempt to insert class java.lang.String element into collection with element type class java.lang.Integer
list.add("4");
}

参考

Guava学习笔记:Immutable(不可变)集合

Java 中的 Mutable 和 Immutable》(en_US

https://stackoverflow.com/questions/7713274/java-immutable-collections

  • Unmodifiable collections are usually read-only views (wrappers) of other collections. You can’t add, remove or clear them, but the underlying collection can change.
  • Immutable collections can’t be changed at all - they don’t wrap another collection - they have their own final elements.

集合(collection)表示一组对象。Java SE 提供了集合框架(collections framework),是一个用于表示和操作集合的统一框架,使集合可以独立于实现细节进行操作。集合框架的主要优点如下:

  • 通过提供数据结构和算法实现,使用户无需自行编写,减少编程工作
  • 通过提供数据结构和算法的高性能实现来提高性能。由于每个接口的各种实现是可互换的,因此可以通过切换实现来调整和优化程序。
  • 通过提供一套标准接口来促进软件重用
  • 通过建立一种通用语言,为无关联的集合 API 之间提供互操作性
  • 减少学习成本,只须学习一些特设的集合 API。

集合框架的整体组成如下:

overview

下面分别来看下各组成部分。

集合接口

集合接口分为下面两组,这些接口构成了集合框架的基础:

  • java.util.Collection,表示一组对象集合

    A collection represents a group of objects, known as its elements.

    • Some collections allow duplicate elements and others do not.

    • Some are ordered and others unordered.

    The JDK does not provide any direct implementations of this interface: it provides implementations of more specific subinterfaces like Set and List.

    This interface is typically used to pass collections around and manipulate them where maximum generality is desired.

  • java.util.Map,用于存储键值对

    An object that maps keys to values.

    • A map cannot contain duplicate keys;

    • each key can map to at most one value.

Collection

Collection

最基础的集合接口 java.util.Collection 及其子接口如下:

Collection

其中,常用的五个重点接口的方法及使用要点如下:

methods_of_collection

Map

Map

其它集合接口基于 java.util.Map,不是真正的集合。但是,这些接口包含集合视图(collection-view)操作,使得它们可以作为集合进行操作。

Map

java.util.Map 接口的方法如下:

Map methods

集合实现类

抽象类实现

下列抽象类为核心集合接口提供了基本功能实现,以最小化用户自定义实现的成本。这些抽象类的 API 文档精确地描述了各个方法的实现方式,实现者能够参阅并了解哪些方法需要覆盖:

Collection_abstract_class

通用实现

java.util.Collection 的通用实现如下:

collection_impl

java.util.Map 的通用实现如下:

map_impl

集合接口的主要实现,命名通常形如 <*Implementation-style*><*Interface*>。通用实现类汇总如下(左列为接口,表头为数据结构):

Resizable Array Linked List Hash Table Hash Table + Linked List Balanced Tree Heap
List ArrayList LinkedList
Queue ArrayBlockingQueue LinkedList
LinkedBlockingQueue
LinkedTransferQueue
ConcurrentLinkedQueue
PriorityBlockingQueue
PriorityQueue
Deque ArrayDeque LinkedList
LinkedBlockingDeque
ConcurrentLinkedDeque
Set HashSet LinkedHashSet TreeSet
Map HashMap LinkedHashMap TreeMap

时间复杂度:

Resizable Array Linked List Hash Table Balanced Tree
插入 $O(n)$ $O(1)$ $O(1)$(平均情况)
$O(n)$(最坏情况,散列冲突时)
$O(log_{}{n})$
删除 $O(n)$ $O(1)$ $O(1)$(平均情况)
$O(n)$(最坏情况,散列冲突时)
$O(log_{}{n})$
查找 $O(n)$ $O(n)$ $O(1)$(平均情况)
$O(n)$(最坏情况,散列冲突时)
$O(log_{}{n})$
读取 $O(1)$ $O(n)$ $O(1)$(平均情况)
$O(n)$(最坏情况,散列冲突时)
$O(log_{}{n})$

通用实现的特性如下:

  • 通用实现类支持集合接口中的所有可选操作,并且对包含的元素没有限制。
  • 都是非线程同步的。Collections 工具类提供了称为同步包装器(synchronization wrappers)的静态工厂方法可用于添加同步行为。
  • 所有集合实现都具有快速失败的迭代器(fail-fast iterators),可以检测到无效的并发修改,然后快速失败,而不是表现异常。

遗留实现

早期版本的集合类,已被改进以实现新的集合接口:

  • java.util.Vector - List 接口的可变长数组实现,线程同步,包含其它遗留方法。
  • java.util.Hashtable - Map 接口的散列表实现,线程同步,键和值都不允许为 null,包含其它遗留方法。继承自抽象类 java.util.Dictionary

并发实现

为高并发使用而设计的实现。详见另一篇《并发实现总结》。

特殊实现

用于特殊情况的实现:

  • CopyOnWriteArrayList 写时复制列表
  • CopyOnWriteArraySet 写时复制列表
  • WeakHashMap
  • IdentityHashMap
  • EnumSet
  • EnumMap

适配器实现(Adaptor)

将某个集合接口适配成另一个:

  • 根据 Map 的通用实现创建一个 Set 的通用实现:

    1
    Collections.newSetFromMap(Map)
  • 以后进先出(Lifo)队列的形式返回 Deque 的视图:

    1
    Collections.asLifoQueue(Deque)
  • 将数组转换为 List 集合:

    1
    Arrays.asList(...)

包装器实现(Wrapper)

用于其它集合实现的功能增强:

  • 返回指定集合的不可修改视图(unmodifiable view),如果尝试修改,则会抛出 UnsupportedOperationException

    1
    2
    3
    4
    5
    6
    7
    8
    Collections.unmodifiableCollection
    Collections.unmodifiableSet
    Collections.unmodifiableSortedSet
    Collections.unmodifiableNavigableSet
    Collections.unmodifiableList
    Collections.unmodifiableMap
    Collections.unmodifiableSortedMap
    Collections.unmodifiableNavigableMap
  • 返回由指定集合支持的 synchronized 线程同步集合:

    1
    2
    3
    4
    5
    6
    7
    8
    Collections.synchronizedCollection
    Collections.synchronizedSet
    Collections.synchronizedSortedSet
    Collections.synchronizedNavigableSet
    Collections.synchronizedList
    Collections.synchronizedMap
    Collections.synchronizedSortedMap
    Collections.synchronizedNavigableMap
  • 返回指定集合的动态类型安全视图(dynamically type-safe view),如果尝试添加错误类型的元素,则会抛出 ClassCastException。泛型机制虽然提供了编译期类型检查,但可以绕过此机制。动态类型安全试图消除了这种可能性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Collections.checkedCollection
    Collections.checkedQueue
    Collections.checkedSet
    Collections.checkedSortedSet
    Collections.checkedNavigableSet
    Collections.checkedList
    Collections.checkedMap
    Collections.checkedSortedMap
    Collections.checkedNavigableMap

便利实现

集合接口的高性能版“迷你实现”:

  • 返回一个不可变集合(immutable),不包含任何元素:

    1
    2
    3
    4
    5
    6
    7
    Collections.emptySet
    Collections.emptySortedSet
    Collections.emptyNavigableSet
    Collections.emptyList
    Collections.emptyMap
    Collections.emptySortedMap
    Collections.emptyNavigableMap
  • 返回一个不可变集合(immutable),仅包含一个元素:

    1
    2
    3
    Collections.singleton
    Collections.singletonList
    Collections.singletonMap
  • 返回一个不可变集合(immutable),包含指定元素的 N 个拷贝:

    1
    Collections.nCopies
  • 返回一个由指定数组支持的定长集合(fixed-size):

    1
    Arrays.asList

基础设施

为集合接口提供必要支持的接口。例如:

  • 迭代器 IteratorListIterator
  • 排序接口 ComparableComparator
  • 运行时异常 UnsupportedOperationExceptionConcurrentModificationException
  • 标记接口 RandomAccess

算法和工具实现

算法实现。由工具类 Collections 提供,用于集合,提供了很多静态方法例如 sort 排序、binarySearch 查找、replaceAll 替换等。这些算法体现了多态性,因为相同的方法可以在相似的接口上有着不同的实现。

数组工具。由工具类 Arrays 提供,用于基本类型和引用类型数组,提供了很多静态方法例如 sort 排序、binarySearch 查找等。严格来说,这些工具不是集合框架的一部分,此功能在集合框架引入的同时被添加到 Java 平台,并依赖于一些相同的基础设施。

参考

https://docs.oracle.com/javase/8/docs/technotes/guides/collections/index.html

Why Java Collection Framework doesn’t contain Tree and Graph ?

https://www.baeldung.com/category/java/

例子

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import com.google.common.collect.Maps;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.util.Arrays;
import java.util.Map;

@RequiredArgsConstructor
@Getter
public enum PayMethodEnum {

/**
* 卡支付
*/
CARDS(0),

/**
* 借记支付
*/
BANK_DEBITS(1),

/**
* 网银支付
*/
BANK_REDIRECTS(2),

/**
* 银行转账
*/
BANK_TRANSFERS(3),

/**
* 分期付款
*/
PAY_LATER(4),

/**
* 现金支付
*/
VOUCHERS(5),

/**
* 电子钱包
*/
WALLETS(6);

/**
* 支付方式编码
*/
private final int code;
public static final Map<Integer, PayMethodEnum> MAP = Maps.newHashMapWithExpectedSize(PayMethodEnum.values().length);

static {
Arrays.stream(PayMethodEnum.values()).forEach(aEnum ->
MAP.put(aEnum.getCode(), aEnum)
);
}

public static PayMethodEnum valueOfCode(int code) {
return MAP.get(code);
}

}

本文介绍时区处理的两种方式。时区涉及的接口及实现类如下:

classes_of_time_zone

(上图简化掉了 Serializable 接口、Comparable 接口及 FunctionalInterface 注解)

推荐方式

ZoneId

时区的处理是新版日期与时间 API 新增的重要功能,且 API 被极大简化。新的 java.time.ZoneId 类是老版本 java.util.TimeZone 类的替代品。它的设计目标就是要让用户无需为时区处理的复杂和繁琐而操心,比如处理夏令时(DST)问题。

每个特定的 ZoneId 对象都有一个地区 ID 标识。地区 ID 格式为“{区域}/{城市}”,这些地区集合的设定都由 IANA 的时区数据库提供。可以输出如下:

1
ZoneId.getAvailableZoneIds().forEach(System.out::println);

ZoneId 的静态工厂方法构造如下:

1
2
3
4
5
// 获取服务器所在时区的 ZoneId,例如 Asia/Shanghai 为 UTC+8
ZoneId currentZone = ZoneId.systemDefault();

// 获取指定城市的 ZoneId,即 UTC+1
ZoneId zoneId = ZoneId.of("Europe/Paris");

一旦得到一个 ZoneId 对象,就可以与 LocalDateLocalDateTimeInstant 对象整合起来,构造一个 ZonedDateTime 实例,它代表了相对于指定时区的时间点

ZonedDateTime

https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html

A date-time with a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30+01:00 Europe/Paris.

Java 8 中 ZonedDateTime 基于 ISO-8601 实现,参考这里

底层实现

ZonedDateTime 的底层实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class ZonedDateTime
implements Temporal, ChronoZonedDateTime<LocalDate>, Serializable {
/**
* The local date-time.
*/
private final LocalDateTime dateTime;
/**
* The offset from UTC/Greenwich.
*/
private final ZoneOffset offset;
/**
* The time-zone.
*/
private final ZoneId zone;
}

ZonedDateTime 的实例如下图:

instance_of_ZonedDateTime

图中可见,原本 LocalDateTime 对象作为一个本地日期与时间,是不包含时区信息的,即没有时区概念。而在结合了 ZoneId 构造成一个 ZonedDateTime 实例之后,才有了时区概念。它代表了相对于指定时区的时间点

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1970-01-01
LocalDate date = LocalDate.ofEpochDay(0);
// 1970-01-01T00:00
LocalDateTime dateTime = date.atStartOfDay();
// 1970-01-01T00:00:00Z
Instant instant = Instant.ofEpochSecond(0);

// 1970-01-01T00:00+01:00[Europe/Paris],底层调用 ZonedDateTime.of(this, zoneId)
ZonedDateTime zonedDateTime = date.atStartOfDay(zoneId);
// 1970-01-01T00:00+01:00[Europe/Paris],底层调用 ZonedDateTime.of(this, zoneId)
ZonedDateTime zonedDateTime1 = dateTime.atZone(zoneId);
// 1970-01-01T01:00+01:00[Europe/Paris],底层调用 ZonedDateTime.ofInstant(this, zoneId)
ZonedDateTime zonedDateTime2 = instant.atZone(zoneId);
// 2015-12-03T10:15:30+08:00[Asia/Shanghai]
ZonedDateTime zonedDateTime3 = ZonedDateTime.parse("2015-12-03T10:15:30+05:30[Asia/Shanghai]");

LocalDateTime 与 Instant 互转

通过 ZoneId 可以将 LocalDateTimeInstant 进行互转,公式为 UTC + 时区差(东正西负)= 本地时间。

LocalDateTime > Instant

1
2
// 东八区的 1970-01-01T00:00,等于 UTC+0 的 1969-12-31T16:00:00Z
Instant instant2 = dateTime.atZone(ZoneId.systemDefault()).toInstant();

Instant > LocalDateTime

1
2
3
4
// 1970-01-01T00:00
LocalDateTime dateTime2 = LocalDateTime.ofInstant(instant2, ZoneId.systemDefault());
// 1970-01-01T08:00
LocalDateTime dateTime3 = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();

不推荐方式

ZoneOffset

另一种比较通用的表达时区的方式是利用当前时区和 UTC/格林尼治的固定偏差。可以使用 ZoneOffset 类,它是 ZoneId 的一个子类,表示的是当前时间和 UTC 的偏差:

1
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");

OffsetDateTime

底层实现

ZoneOffset 类可用于构造 OffsetDateTime 实例。OffsetDateTime 的底层实现如下:

1
2
3
4
5
6
7
8
9
10
11
public final class OffsetDateTime
implements Temporal, TemporalAdjuster, Comparable<OffsetDateTime>, Serializable {
/**
* The local date-time.
*/
private final LocalDateTime dateTime;
/**
* The offset from UTC/Greenwich.
*/
private final ZoneOffset offset;
}

使用方式

1
2
3
4
// 1970-01-01T00:00-05:00
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(dateTime1, newYorkOffset);
// 1970-01-01T00:00-05:00
OffsetDateTime dateTimeInNewYork2 = dateTime1.atOffset(newYorkOffset);

“-05:00” 的偏差实际上对应的是美国东部标准时间。注意,使用这种方式定义的 ZoneOffset 并未考虑任何夏令时的影响,所以在大多数情况下,不推荐使用。

常见问题

java.sql.Timestamp

有时开发会使用 java.sql.Timestamp 作为 PO 实体类的时间字段,java.sql.Timestamp 底层实现使用格里历(公历),并使用服务器所在时区(即本地时区),并受该时区影响。

这里看一段代码,以 2021-01-04 00:00:00 为例演示转换过程:

1
2
3
4
5
6
7
8
LocalDateTime localDateTime = LocalDateTime.parse(
"2021-01-04 00:00:00",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
);
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Jakarta"));

Timestamp.from(zonedDateTime.toInstant());
Timestamp.valueOf(localDateTime);

这里试验两个时区:

Europe/London (UTC) Asia/Shanghai (UTC+8)
ZoneId.systemDefault() Europe/London (UTC) Asia/Shanghai (UTC+8)
TimeZone.getDefaultRef() Europe/London (UTC) Asia/Shanghai (UTC+8)

下面分别看下 java.sql.Timestamp 两个 API 会有什么问题:

#valueOf(LocalDateTime)

转换过程:本地时间 > 系统时区的时间 > UTC-0 时区的时间戳

Europe/London (UTC) Asia/Shanghai (UTC+8)
2021-01-04T00:00 (LocalDateTime) →
2021-01-04T00:00:00.000Z / 1609718400000 (Timestamp)
2021-01-04T00:00 (LocalDateTime) →
2021-01-04T00:00:00.000+0800 / 1609689600000 (Timestamp)

可见,由于 LocalDateTime 本身不含时区信息,在经由 Timestamp#valueOf(LocalDateTime) 转换时,源码中使用了 TimeZone.getDefaultRef()受系统默认时区的影响,导致结果前后不一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Returns the reference to the default TimeZone object. This
* method doesn't create a clone.
*/
static TimeZone getDefaultRef() {
TimeZone defaultZone = defaultTimeZone;
if (defaultZone == null) {
// Need to initialize the default time zone.
defaultZone = setDefaultZone();
assert defaultZone != null;
}
// Don't clone here.
return defaultZone;
}

#from(Instant)

转换过程:本地时间 > 指定时区的时间 > UTC-0 时区的时间戳

Europe/London (UTC) Asia/Shanghai (UTC+8)
2021-01-04T00:00 (LocalDateTime) →
2021-01-04T00:00+07:00[Asia/Jakarta] (ZonedDateTime) →
2021-01-03T17:00:00Z / 1609693200 (Instant) →
2021-01-03T17:00:00.000Z / 1609693200000(Timestamp)
2021-01-04T00:00 (LocalDateTime) →
2021-01-04T00:00+07:00[Asia/Jakarta] (ZonedDateTime) →
2021-01-03T17:00:00Z / 1609693200 (Instant) →
2021-01-04T01:00:00.000+0800 / 1609693200000 (Timestamp)

这里看似结果没有问题,InstantTimestamp 对象在不同时区下都是相同时间戳。

但有一种场景,就是应用服务器与数据库的时区不一致导致的问题。假如应用服务器时区为 Asia/Shanghai (UTC+8),数据库时区为 Europe/London (UTC),当把上表 Timestamp 对象保存到 MySQL 数据库的 datetime 字段时,如果未经时区转换,会导致错误结果。

这里参考 mysql-connector-java-5.1.42.jar 源码如下,重点看 java.sql.PreparedStatement#setTimestamp 的方法实现,其使用了 SimpleDateFormatTimestamp 对象格式化成字符串,如果未经时区转换,结果如下表,导致前后不一致

格式化前 格式化后
2021-01-03T17:00:00.000Z 2021-01-03 17:00:00
2021-01-04T01:00:00.000+0800 2021-01-04 01:00:00
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
/**
* Set a parameter to a java.sql.Timestamp value. The driver converts this
* to a SQL TIMESTAMP value when it sends it to the database.
*
* @param parameterIndex
* the first parameter is 1, the second is 2, ...
* @param x
* the parameter value
* @param tz
* the timezone to use
*
* @throws SQLException
* if a database-access error occurs.
*/
private void setTimestampInternal(int parameterIndex, Timestamp x, Calendar targetCalendar, TimeZone tz, boolean rollForward) throws SQLException {
...

x = TimeUtil.changeTimezone(this.connection, sessionCalendar, targetCalendar, x, tz, this.connection.getServerTimezoneTZ(), rollForward);

...
synchronized (this) {
if (this.tsdf == null) {
this.tsdf = new SimpleDateFormat("''yyyy-MM-dd HH:mm:ss", Locale.US);
}

StringBuffer buf = new StringBuffer();
buf.append(this.tsdf.format(x));

...

setInternal(parameterIndex, buf.toString());
}
}

上述方法内部调用了 com.mysql.jdbc.TimeUtil#changeTimezone 方法,源码如下。

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
49
50
51
52
53
54
/**
* Change the given timestamp from one timezone to another
*
* @param conn
* the current connection to the MySQL server
* @param tstamp
* the timestamp to change
* @param fromTz
* the timezone to change from
* @param toTz
* the timezone to change to
*
* @return the timestamp changed to the timezone 'toTz'
*/
public static Timestamp changeTimezone(MySQLConnection conn, Calendar sessionCalendar, Calendar targetCalendar, Timestamp tstamp, TimeZone fromTz,
TimeZone toTz, boolean rollForward) {
if ((conn != null)) {
// 开启 useTimezone=true,才能进入下面的时区转换逻辑
if (conn.getUseTimezone()) {
// Convert the timestamp from GMT to the server's timezone
Calendar fromCal = Calendar.getInstance(fromTz);
fromCal.setTime(tstamp);

int fromOffset = fromCal.get(Calendar.ZONE_OFFSET) + fromCal.get(Calendar.DST_OFFSET);
Calendar toCal = Calendar.getInstance(toTz);
toCal.setTime(tstamp);

int toOffset = toCal.get(Calendar.ZONE_OFFSET) + toCal.get(Calendar.DST_OFFSET);
int offsetDiff = fromOffset - toOffset;
long toTime = toCal.getTime().getTime();

if (rollForward) {
toTime += offsetDiff;
} else {
toTime -= offsetDiff;
}

Timestamp changedTimestamp = new Timestamp(toTime);

return changedTimestamp;
} else if (conn.getUseJDBCCompliantTimezoneShift()) {
if (targetCalendar != null) {

Timestamp adjustedTimestamp = new Timestamp(jdbcCompliantZoneShift(sessionCalendar, targetCalendar, tstamp));

adjustedTimestamp.setNanos(tstamp.getNanos());

return adjustedTimestamp;
}
}
}

return tstamp;
}

如果 JDBC 连接参数未配置 useTimezone=true(默认值 false),会导致目标时区转换失效,从而产生上述问题。而如果开启之后,不管应用服务器设置什么时区,都能保证正确转换为数据库目标时区的时间值,反之亦然(数据库 -> 应用服务器)。这里给两个例子,如下表:

时区转换前 时区转换后
UTC+2 2021-01-03T19:00:00.000+0200 / 1609693200 2021-01-03T17:00:00.000Z / 1609693200
UTC+8 2021-01-04T01:00:00.000+0800 / 1609693200 2021-01-03T17:00:00.000Z / 1609693200

after_convert_time_zone+2

after_convert_time_zone+8

参考:

不指定时区会踩坑:MySQL JDBC 8.0.22 驱动升级遇到的 Bug 分析

参考

时区数据库:

  • IANA 的时区数据库

  • 《这个重要开源项目全靠一位低调的 “怪老头” 维护!他和比尔盖茨一样撑起了计算机世界》

    时区设置背后有一组大量关于全球许多代表性地点时间历史信息的代码和数据,这些代码和数据被称为时区数据库(即 tz、tzdata 或 zoneinfo),该数据库会定期进行更新以反映各政治实体对时区边界、UTC 差值和夏令时规则的更改。对 tz 的更新遵循 BCP 175 流程进行管理。

    尽管大多数计算机用户从未听说过时区数据库,但 tz 数据库对全世界的计算机非常重要。所有基于 Linux 和 Mac 的计算机都是从一个极其重要的数据库(时区数据库)中提取时区。目前,使用该数据库的项目包括:the GNU C Library (used in GNU/Linux), Android, FreeBSD, NetBSD, OpenBSD, Chromium OS, Cygwin, MariaDB, MINIX, MySQL, webOS, AIX, BlackBerry 10, iOS, macOS, Microsoft Windows, OpenVMS, Oracle Database, Oracle Solaris 等。

    tz 数据库背后,一个人在维护

    tz 数据库由 David Olson 创立,收集了自 1970 年以来被广泛认可的民用时钟的时区信息。2011 年,互联网域名与数字地址分配机构 ICANN 接管了这个被全球电脑和网站广泛使用的时区数据库,该机构通常只赞助对互联网发展非常重要的项目,

    现在,具体的维护工作由互联网分配号码管理局(Internet Assigned Numbers Authority, IANA)负责。Paul Eggert 是时区数据库的项目负责人,该职位被称为 TZ 协调员。

  • Get Time Zone info of the World Countries

  • 逐渐成为历史的 “新疆时间” —— Asia/Urumqi (UTC+06:00)

UTC:

时间协议:

其它:

现有问题

java.util.Date 存在的问题:

  • 正如类名所表达的,这个类无法表示“日期”(替代方案是 LocalDate),只能以毫秒的精度表示“时间”(替代方案是 LocalDateTime)。
  • 更糟糕的是它的易用性,比如:年份的起始选择是 1900 年,月份的起始从 0 开始。如果要表达 2014 年 3 月 18 日,需要创建以下 Date 实例:Date date = new Date(144,2,18)
  • 非线程安全,且所有的日期类都是可变的,这表示在运行期其值可以任意修改,这是 Java 日期类最大的问题之一。

java.util.Datejava.sql.Timestamp API 如下:

java time old api

Java 8 日期与时间

Java 8 引入了新的 java.time 类库,用于加强对日期与时间的操作,本文主要总结其 API 结构、具体实现和使用方式。

先看下涉及的包结构简介:

Package Description
java.time The main API for dates, times, instants, and durations.
java.time.temporal Access to date and time using fields and units, and date time adjusters.
java.time.chrono Generic API for calendar systems other than the default ISO.
java.time.zone Support for time-zones and their rules.
java.time.format Provides classes to print and parse dates and times.

java.time 包的常用类:

java.time核心类

1
2
3
4
5
6
7
8
9
10
11
12
13
Year              // 2007
YearMonth // 2007-12
MonthDay // --12-03

LocalDate // 2007-12-03
LocalTime // 10:15:30
LocalDateTime // 2007-12-03T10:15:30

OffsetTime // 10:15:30+01:00
OffsetDateTime // 2007-12-03T10:15:30+01:00
ZonedDateTime // 2007-12-03T10:15:30+01:00 Europe/Paris

Instant // 以 Unix 元年时间(UTC 时区 1970-01-01T00:00:00Z,“Z” 代表 UTC 时区)开始所经历的秒数

java.time.temporal 包的核心接口:

Interface Description
TemporalAccessor 框架级别的根接口,定义了对 temporal 对象的只读访问。
Temporal 框架级别的接口,定义了对 temporal 对象的读写访问。实现类有 LocalDateOffsetDateTimeInstant 等等。继承自 TemporalAccessor
TemporalAmount 框架级别的接口,定义了一个时间段,例如”6 小时“、”8 天“、”3 个月“。实现类有 DurationPeriod。可传入 temporal 对象的 plusminus 方法进行时间调整。
TemporalUnit 日期和时间的单元,ChronoUnit 枚举实现了该接口。可传入 temporal 对象的 plusminus 方法进行时间调整。
TemporalField 日期和时间的字段。ChronoField 枚举实现了该接口,可传入 temporal 对象的 getwith 方法获取或修改枚举对应的值。
TemporalAdjuster 函数式接口,定义了对 temporal 对象的调整策略。可以使用 TemporalAdjusters 工具类的静态工厂方法生成对象实例,并传入temporal 对象的 with 方法进行时间调整。

java.time.temporal 包的核心方法:

ChronoLocalDate核心接口方法

历法系统介绍

首先了解几个概念:

Calendar system 历法

https://en.wikipedia.org/wiki/Calendar

历法,或称日历,是用等时间单位计算时间的方法。Java 8 提供的历法实现如下:

历法 Java 8 实现
格里历(公历),即 Gregorian calendar java.time.LocalDate
和历 java.time.chrono.JapaneseDate
中华民国历 java.time.chrono.MinguoDate
泰国历 java.time.chrono.ThaiBuddhistDate
伊斯兰历(回历) java.time.chrono.HijrahDate

这些类都实现了 java.time.chrono.ChronoLocalDate 接口,能够对日期进行建模。

ChronoLocalDate实现类

1
2
3
4
5
6
7
8
9
10
// 格里历(公历)
LocalDate date = LocalDate.now();
// 和历
JapaneseDate japaneseDate = JapaneseDate.from(date);
// 中华民国历
MinguoDate minguoDate = MinguoDate.from(date);
// 泰国历
ThaiBuddhistDate thaiBuddhistDate = ThaiBuddhistDate.from(date);
// 伊斯兰历(回历)
HijrahDate hijrahDate = HijrahDate.from(date);

还有其它一些常见的历法,例如:

Calendar era 纪年

https://en.wikipedia.org/wiki/Calendar_era

https://docs.oracle.com/javase/8/docs/api/java/time/chrono/Era.html

纪年,或称纪元,是指历法中的年份命名体系,例如格里历(公历)所使用的基督纪年公元),中国农历使用的干支纪年等。世界各地曾存在过各种不同的纪年方法,其中一些至今仍在使用,例如日本现在仍在使用年号纪年

这里提供了一个常见的纪年对照表,可供参考。

年号是中国历史君主时代帝王纪年所立的名号,缘起于西汉汉武帝时期,后来朝鲜新罗在6世纪、日本在7世纪后期、越南在10世纪都因为中国的影响,开始使用年号;台湾岛的郑氏王朝与台湾民主国、朝鲜半岛大韩帝国与高丽、蒙古国建国初年受到中国影响,都还使用过年号,目前唯一使用年号的是仍保持君主制的日本(日本年号)。

值得一提,中华民国所用的民国纪年、以及朝鲜民主主义人民共和国使用的主体纪年,常被误认为是年号,实际上仅是单纯的纪年历法

一世一元制,指君主(国王、大君主、可汗、天皇、皇帝)在其在位期间只使用同一个年号,不进行改元的制度。例外:如果君主后来另行称帝或是重祚,会另建新年号,以示区别。

LocalDate、LocalTime

LocalDateLocalTime 是人类易读的日期和时间格式,表示一个本地时间点,基于 ISO 8601 历法系统,无时区信息

ISO 8601 日期和时间表示法

The ISO 8601 calendar system is the modern civil calendar system used today in most of the world. It is equivalent to the proleptic Gregorian calendar system :

In general, ISO 8601 applies to these representations and formats:

The standard does not assign specific meaning to any element of the dates/times represented: the meaning of any element depends on the context of its use.

Dates and times represented cannot use words that do not have a specified numerical meaning within the standard (thus excluding names of years in the Chinese calendar), or that do not use computer characters (excludes images or sounds).[2]

ISO 8601 的日期和时间表示法为:

In representations that adhere to the ISO 8601 interchange standard :

  • dates and times are arranged such that the greatest temporal term (typically a year) is placed at the left and each successively lesser term is placed to the right of the previous term.
  • Representations must be written in a combination of Arabic numerals and the specific computer characters (such as “-“, “:”, “T”, “W”, “Z”) that are assigned specific meanings within the standard; that is, such commonplace descriptors of dates (or parts of dates) as “January”, “Thursday”, or “New Year’s Day” are not allowed in interchange representations within the standard.

而这几个类都基于 ISO 8601实现的:

https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html

A date without a time-zone in the ISO-8601 calendar system, such as 2007-12-03.

https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html

A time without a time-zone in the ISO-8601 calendar system, such as 10:15:30.

https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html

A date-time without a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30.

底层实现

LocalDate 的底层实现包含不可变的年、月、日:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class LocalDate
implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
/**
* The year.
*/
private final int year;
/**
* The month-of-year.
*/
private final short month;
/**
* The day-of-month.
*/
private final short day;
}

LocalTime 的底层实现包含不可变的时、分、秒、纳秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class LocalTime
implements Temporal, TemporalAdjuster, Comparable<LocalTime>, Serializable {
/**
* The hour.
*/
private final byte hour;
/**
* The minute.
*/
private final byte minute;
/**
* The second.
*/
private final byte second;
/**
* The nanosecond.
*/
private final int nano;
}

LocalDateTime 的底层实现包含不可变的 LocalDateLocalTime

1
2
3
4
5
6
7
8
9
10
11
public final class LocalDateTime
implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable {
/**
* The date part.
*/
private final LocalDate date;
/**
* The time part.
*/
private final LocalTime time;
}

使用方式

方法名 是否静态方法 描述
of Temporal 对象的某个部分创建该对象。
now 依据系统时钟创建 Temporal 对象。
from 依据传入的 Temporal 对象创建对象。
parse 由字符串创建 Temporal 对象。
format 使用某个指定的 DateTimeFormatterTemporal 对象转换为字符串。
atOffset Temporal 对象和某个时区偏移相结合。
atZone Temporal 对象和某个时区相结合。
get 读取 Temporal 对象的某一部分值。
minus 创建 Temporal 对象的副本,通过将当前 Temporal 对象的值减去一定的时长创建该副本。
plus 创建 Temporal 对象的副本,通过将当前 Temporal 对象的值加上一定的时长创建该副本。
with 以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本。

例子:

创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过静态工厂方法创建实例
LocalTime time = LocalTime.of(12, 0);
LocalDate date = LocalDate.of(2007, 12, 3);
// 1970-01-01。epoch day(历元日)是一个简单的天数递增计数,其中第 0 天表示 1970-01-01。负数代表更早的日期。
LocalDate date1 = LocalDate.ofEpochDay(0);
// 通过静态方法创建实例
LocalDate date2 = LocalDate.parse("2007-12-03");

// 合并日期和时间
LocalDateTime dateTime = date.atTime(time);
LocalDateTime dateTime1 = time.atDate(date);
LocalDateTime dateTime2 = date.atStartOfDay();
LocalDateTime dateTime3 = LocalDateTime.of(date, time);

// 获取本地时区的当前日期与时间
LocalDateTime dateTime4 = LocalDateTime.now();

// 通过 unix_timestamp 创建实例
LocalDateTime dateTime5 = LocalDateTime.ofEpochSecond(timestamp, 0, ZoneOffset.of("+08:00"));

// java.time.LocalDateTime 转 java.sql.Timestamp
Timestamp timestamp = Timestamp.valueOf(localDateTime);

读取字段信息

读取信息使用 TemporalAccessor 接口,通过传递一个 ChronoField 枚举给 get 方法读取相关信息:

ChronoLocalDate核心接口方法

ChronoField 枚举提供的值如下图:

ChronoField枚举

1
2
3
4
5
6
7
8
9
10
// 使用 TemporalAccessor#get(TemporalField),传入 TemporalField 接口的实现类 ChronoField
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int dayOfMonth = date.get(ChronoField.DAY_OF_MONTH);

// LocalTime/LocalDate/LocalDateTime 各自也提供了一组 getter 快捷方法
int year1 = date.getYear();
int month1 = date.getMonthValue();
int dayOfMonth1 = date.getDayOfMonth();
boolean leapYear = date.isLeapYear();

创建副本并修改

创建副本使用 Temporal 接口,其提供了一组 with/plus/minus 方法:

ChronoLocalDate核心接口方法

1
2
3
4
5
6
7
8
9
// 使用 Temporal#with(TemporalField, long),传入 TemporalField 接口的实现类 ChronoField
LocalDate newDate = date.with(ChronoField.YEAR, 2019);
LocalDate newDate2 = date.with(ChronoField.MONTH_OF_YEAR, 1);
LocalDate newDate3 = date.with(ChronoField.DAY_OF_MONTH, 30);

// LocalTime/LocalDate/LocalDateTime 各自也提供了一组 with 快捷方法
LocalDate newDate4 = date.withYear(2019);
LocalDate newDate5 = date.withMonth(1);
LocalDate newDate6 = date.withDayOfMonth(30);

如果 with(TemporalField, long) 方法不满足需求,可以使用更灵活的 with(TemporalAdjuster) 方法,配合 TemporalAdjusters 工具类提供的静态工厂方法,方法名非常直观:

TemporalAdjusters静态工厂方法

1
2
3
// 使用 Temporal#with(TemporalAdjuster)
LocalDate newDate7 = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
LocalDate newDate8 = date.with(TemporalAdjusters.lastDayOfMonth());

如果在 TemporalAdjusters 工具类中没有找到符合要求的预定义静态工厂方法,可以自己实现 TemporalAdjuster 函数式接口,以定制一些更复杂的时间修改操作:

TemporalAdjuster

Instant

作为人,我们习惯于以星期几、几号、几点、几分这样的方式理解日期和时间。毫无疑问,这种方式对于计算机而言并不容易理解。从计算机的角度来看,建模时间最自然的格式是表示一个持续时间段上某个点的单一大整型数。这也是新的 java.time.Instant 类对时间建模的方式,它是以 Unix 元年时间(UTC 时区 1970-01-01T00:00:00Z,“Z” 代表 UTC 时区)开始所经历的秒数进行计算。

小数秒精度

参考《国际单位制词头表(Metric prefix)》,小数秒精度(fractional seconds precision)可以分为:

名称词头 符号词头 中文词头 英文 科学计数法 二进制存储所需位数 类型 名称示例 (以秒为例) 符号示例 (以秒为例)
milli m Thousandth 1×10⁻³ 2¹⁰ 小数单位 millisecond ms
micro μ Millionth 1×10⁻⁶ 2²⁰ 小数单位 microsecond μs
nano n 纳〔诺〕 Billionth 1×10⁻⁹ 2³⁰ 小数单位 nanosecond ns
pico p 皮〔可〕 Trillionth 1×10⁻¹² 2⁴⁰ 小数单位 picosecond ps
femto f 飞〔母托〕 Quadrillionth 1×10⁻¹⁵ 2⁵⁰ 小数单位 femtosecond fs
atto a 啊〔托〕 Quintillionth 1×10⁻¹⁸ 2⁶⁰ 小数单位 attosecond as
zepto z 仄〔普托〕 Sextillionth 1×10⁻²¹ 2⁷⁰ 小数单位 zeptosecond zs
yocto y 幺〔科托〕 Septillionth 1×10⁻²⁴ 2⁸⁰ 小数单位 yoctosecond ys

底层实现

从底层实现可见,java.time.Instant 仅包含不可变的秒、纳秒。由此可见,支持的最高存储精度为纳秒(10^-9 秒)

1
2
3
4
5
6
7
8
9
10
11
12
public final class Instant
implements Temporal, TemporalAdjuster, Comparable<Instant>, Serializable {
/**
* The number of seconds from the epoch of 1970-01-01T00:00:00Z.
*/
private final long seconds;
/**
* The number of nanoseconds, later along the time-line, from the seconds field.
* This is always positive, and never exceeds 999,999,999.
*/
private final int nanos;
}

可以通过 getter 方法获取这两个属性,例如:

1
2
3
4
5
Instant instant = Instant.now(); // 1562497662814 (2019-07-07T11:07:42.814Z)

// Instant 的设计初衷是为了便于机器使用,底层实现仅包含由秒和纳秒所构成的数字。
long seconds = instant.getEpochSecond(); // 1562497662
int nanoSeconds = instant.getNano(); // 814000000

使用方式

创建实例

java.time.Instant 提供了一系列静态工厂方法,用于创建实例,例如:

1
Instant instant = Instant.now();

unix_timestampjava.time.Instant

1
2
3
4
5
6
7
// // 自 1970-01-01T00:00:00Z 之后经过的秒数
// 1970-01-01T00:00:01Z
Instant instant = Instant.ofEpochSecond(1);

// 自 1970-01-01T00:00:00Z 之后经过的毫秒数
// 1970-01-01T00:00:01.000Z
Instant instant = Instant.ofEpochMilli(1000);

java.util.Datejava.time.Instant

1
Instant instant = new Date().toInstant();

java.time.LocalDateTimejava.time.ZonedDateTimejava.time.Instant

1
2
ZonedDateTime dt = LocalDateTime.now().atZone(zoneId);
Instant instant = Instant.from(dt);

类型转换

java.time.Instantunix_timestamp

1
2
long seconds = Instant.now().getEpochSecond();  // 秒
long milliSeconds = Instant.now().toEpochMilli(); // 毫秒

java.time.Instantjava.util.Date

1
Date date = Date.from(Instant.now());

读取字段信息

1
2
3
4
5
Instant instant = Instant.now(); // 1562497662814 (2019-07-07T11:07:42.814Z)

int milliOfSecond = instant.get(ChronoField.MILLI_OF_SECOND); // 814
int microOfSecond = instant.get(ChronoField.MICRO_OF_SECOND); // 814000
int nanoOfSecond = instant.get(ChronoField.NANO_OF_SECOND); // 814000000

java.time.Instant 无法处理那些人类非常容易理解的时间单位,例如下述操作将抛出异常:

1
2
// java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
Instant.now().get(ChronoField.DAY_OF_MONTH);

创建副本并修改

1
2
3
4
5
Instant newInstant = instant.with(ChronoField.INSTANT_SECONDS, 30);
Instant newInstant2 = instant.with(ChronoField.MILLI_OF_SECOND, 300);
Instant newInstant3 = instant.plus(30, ChronoUnit.SECONDS);
Instant newInstant4 = instant.plusSeconds(30);
Instant newInstant5 = instant.minusSeconds(30);

Duration、Period

DurationPeriod 类用于保存两个 Temporal 对象之间的时间段。

TemporalAmount实现类

底层实现

Duration 的底层实现仅包含不可变的秒、纳秒:

1
2
3
4
5
6
7
8
9
10
11
12
public final class Duration
implements TemporalAmount, Comparable<Duration>, Serializable {
/**
* The number of seconds in the duration.
*/
private final long seconds;
/**
* The number of nanoseconds in the duration, expressed as a fraction of the
* number of seconds. This is always positive, and never exceeds 999,999,999.
*/
private final int nanos;
}

Period 的底层实现包含不可变的年、月、日:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class Period
implements ChronoPeriod, Serializable {
/**
* The number of years.
*/
private final int years;
/**
* The number of months.
*/
private final int months;
/**
* The number of days.
*/
private final int days;
}

使用方式

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
// Duration 类的静态工厂方法 between 仅支持两个 LocalTime 或两个 LocalDateTime、或两个 Instant 对象之间的 duration
Duration duration = Duration.between(dateTime1, dateTime);
Duration.between(time, time);
Duration.between(instant, instant1);
// 由于 Duration 类主要用于以秒和纳秒衡量时间的长短,如果传 LocalDate 对象会抛异常:java.time.temporal.UnsupportedTemporalTypeException
// Duration.between(date1, date);

// Period 类的静态工厂方法 between 仅支持两个 LocalDates
Period period = Period.between(date1, date);

// Duration 和 Period 类都提供了很多非常方便的静态工厂方法,用于直接创建对应的实例
Duration threeDays = Duration.ofDays(3);
Duration threeHours = Duration.ofHours(3);
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes2 = Duration.of(3, ChronoUnit.MINUTES);
Duration threeSeconds = Duration.ofSeconds(3);
Duration threeMillis = Duration.ofMillis(3);
Duration twoDaysThreeHoursFourMinutes = Duration.parse("P2DT3H4M");

Period tenYears = Period.ofYears(10);
Period tenMonths = Period.ofMonths(10);
Period tenWeeks = Period.ofWeeks(10);
Period tenDays = Period.ofDays(10);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
Period oneYearTwoMonthsThreeDays = Period.parse("P1Y2M3D");

// 准确获取该时段的差值:日、时、分、秒、毫秒、纳秒
long days = duration.toDays();
long hours = duration.toHours();
long minutes = duration.toMinutes();
long seconds = duration.getSeconds();
long seconds2 = duration.get(ChronoUnit.SECONDS);
long millis = duration.toMillis();
long nanos2 = duration.toNanos();

// 获取指定部分的差值
int years = period.getYears();
int months = period.getMonths();
int days1 = period.getDays();
long days2 = period.get(ChronoUnit.DAYS);

ChronoUnit 枚举提供的值:

TemporalUnit枚举

日期格式化

DateTimeFormatter 用于日期格式化。和老的 java.util.DateFormat 相比,所有的 DateTimeFormatter 实例都是线程安全的。所以,你能够以单例模式创建格式器实例,并在多个线程间共享。

下面介绍创建格式器的三种方式:

预定义的格式器

创建格式器最简单的方法是通过它预定义的格式器常量,定义如下:

DateTimeFormatter常量

Formatter Description Example
ofLocalizedDate(dateStyle) Formatter with date style from the locale ‘2011-12-03’
ofLocalizedTime(timeStyle) Formatter with time style from the locale ‘10:15:30’
ofLocalizedDateTime(dateTimeStyle) Formatter with a style for date and time from the locale ‘3 Jun 2008 11:05:30’
ofLocalizedDateTime(dateStyle,timeStyle) Formatter with date and time styles from the locale ‘3 Jun 2008 11:05’
BASIC_ISO_DATE Basic ISO date ‘20111203’
ISO_LOCAL_DATE ISO Local Date ‘2011-12-03’
ISO_OFFSET_DATE ISO Date with offset ‘2011-12-03+01:00’
ISO_DATE ISO Date with or without offset ‘2011-12-03+01:00’; ‘2011-12-03’
ISO_LOCAL_TIME Time without offset ‘10:15:30’
ISO_OFFSET_TIME Time with offset ‘10:15:30+01:00’
ISO_TIME Time with or without offset ‘10:15:30+01:00’; ‘10:15:30’
ISO_LOCAL_DATE_TIME ISO Local Date and Time ‘2011-12-03T10:15:30’
ISO_OFFSET_DATE_TIME Date Time with Offset 2011-12-03T10:15:30+01:00’
ISO_ZONED_DATE_TIME Zoned Date Time ‘2011-12-03T10:15:30+01:00[Europe/Paris]’
ISO_DATE_TIME Date and time with ZoneId ‘2011-12-03T10:15:30+01:00[Europe/Paris]’
ISO_ORDINAL_DATE Year and day of year ‘2012-337’
ISO_WEEK_DATE Year and Week 2012-W48-6’
ISO_INSTANT Date and Time of an Instant ‘2011-12-03T10:15:30Z’
RFC_1123_DATE_TIME RFC 1123 / RFC 822 ‘Tue, 3 Jun 2008 11:05:30 GMT’

例子:

1
2
3
4
5
6
// 2021-02-01
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE);
// 2021-02-01T09:55:54.846Z(注意:UTC+00:00 输出为 Z,需要自行 replace)
ZonedDateTime.now(ZoneId.of("Europe/London")).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
// 2021-02-01T16:43:46+07:00
ZonedDateTime.now(ZoneId.of("Asia/Jakarta")).truncatedTo(ChronoUnit.SECONDS).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

自定义 Pattern

Patterns for Formatting and Parsing:

Patterns are based on a simple sequence of letters and symbols. A pattern is used to create a Formatter using the ofPattern(String) and ofPattern(String, Locale) methods.

A formatter created from a pattern can be used as many times as necessary, it is immutable and is thread-safe.

使用静态工厂方法 DateTimeFormatter#ofPattern(String) 创建日期格式器:

1
2
// 01/02/2021
LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));

使用静态工厂方法 DateTimeFormatter#ofPattern(String, Locale) 创建日期格式器:

1
2
3
4
5
6
7
8
9
10
// 2021 2 1
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy M d", Locale.US));
// 2021 02 1
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy MM d", Locale.US));
// 2021 Feb 1
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy MMM d", Locale.US));
// 2021 February 1
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy MMMM d", Locale.US));
// 2021 F 1
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy MMMMM d", Locale.US));

注意:Pattern 的字母数量决定格式,以月份为例:

  • Exactly 1 pattern letter will use the minimum number of digits and without padding.
  • Exactly 2 pattern letters, the count of digits is used as the width of the output field, with the value zero-padded as necessary.
  • Exactly 3 pattern letters will use the short form.
  • Exactly 4 pattern letters will use the full form.
  • Exactly 5 pattern letters will use the narrow form.

更灵活的构建器

如果还需要更加细粒度的控制,DateTimeFormatterBuilder 类还提供了更复杂的格式器构建,你可以选择恰当的方法,一步一步地构造自己的格式器:

1
2
3
4
5
6
7
8
9
10
DateTimeFormatter dtf = new DateTimeFormatterBuilder().appendPattern("dd/MM/yyyy[ [HH][:mm][:ss][.SSS]]")
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.toFormatter(Locale.US);

// 1999-01-01T02:04:06
LocalDateTime.parse("01/01/1999 02:04:06", dtf);
// 1999-01-01T00:00
LocalDateTime.parse("01/01/1999", dtf);

另外,DateTimeFormatterBuilder 类还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式)、填充,以及在格式器中指定可选节。

参考

标准:

API:

继承结构

methods_of_CharSequence

Appendable

CharSequence

字符串的实现

history_of_String

字符串的创建方式

通过字符串常量创建

通过构造方法创建

intern() 方法

字符串的处理

字符串拼接

字符串拼接的三种方式,区别如下:

since immutable ? thread-safe ?
java.lang.String Java SE 1.0 immutable no thread-safe
java.lang.StringBuffer Java SE 1.0 mutable thread-safe
java.lang.StringBuilder Java SE 1.5 mutable no thread-safe

字符串拼接的字节码分析,参考:《On Java 8》第十八章 字符串:+ 的重载与 StringBuilder 对比

除了上述方式之外, Java SE 8 还提供了新的工具类 java.util.StringJoiner,它是String.joinjava.util.stream.Collectors#joining(...) 的底层实现。结合 Stream API 使用如下:

1
2
3
// [a,b,c,d,e,f,g]
Arrays.asList("a", "b", "c", "d", "e", "f", "g").stream()
.collect(Collectors.joining(",", "[", "]"));

字符串格式化

java.util.Formatter —— C 语言 printf 风格的字符串格式化解释器。

Formatter

用法:

Returns a formatted string using the specified format string and args.

1
2
3
String s1 = new Formatter().format(format, args).toString();
// or simplied
String s2 = String.format(format, args);

Writes a formatted string to this output stream using the specified format string and args.

1
2
System.out.printf(format, args);
System.out.format(format, args);

参考:

https://linux.die.net/man/1/printf

https://linux.die.net/man/3/printf

https://en.wikipedia.org/wiki/Printf_format_string

https://www.baeldung.com/linux/printf-echo

1
2
3
4
$ printf "%s %s" hello world | hexyl
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 68 65 6c 6c 6f 20 77 6f ┊ 72 6c 64 │hello wo┊rld │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘

https://www.baeldung.com/java-printstream-printf

https://blog.csdn.net/quinnnorris/article/details/54614446

字符串转义

方式一:在线工具:https://www.freeformatter.com/string-escaper.html

HTML Escape
Escapes or unescapes an HTML file removing traces of offending characters that could be wrongfully interpreted as markup.

FEATURES
Escapes all reserverd characters with their corresponding HTML entities (‘, “, &, <, >)
Escapes ISO 8859-1 symbols and characters that have corresponding HTML entities

JSON Escape
Escapes or unescapes a JSON string removing traces of offending characters that could prevent parsing.

XML Escape
Escapes or unescapes an XML file removing traces of offending characters that could be wrongfully interpreted as markup.

CSV Escape
Escapes or unescapes a CSV string removing traces of offending characters that could prevent parsing.

JavaScript Escape
Escapes or unescapes a JavaScript string removing traces of offending characters that could prevent interpretation.

Java and .Net Escape
Escapes or unescapes a Java or .Net string removing traces of offending characters that could prevent compiling.

SQL Escape
Escapes or unescapes a SQL string removing traces of offending characters that could prevent execution.

方式二:代码处理

Apache Commons Lang 提供了字符串转义和反转义工具类 org.apache.commons.lang3.StringEscapeUtils,用于 Java、JavaScript、JSON、HTML、XML、CSV 等字符串:

StringEscapeUtils

例如:

1
2
3
4
5
// &lt;span&gt;hello world&lt;/span&gt;
StringEscapeUtils.escapeHtml4("<span>hello world</span>");

// {hello: "world"}
StringEscapeUtils.unescapeJson("{hello: \"world\"}");

例如,有些 HTML 字符实体在 PDF 是不支持的,需要先转义:

1
2
3
4
5
// flying saucer
ITextRenderer renderer = new ITextRenderer();

String escapedHtml = StringEscapeUtils.escapeHtml4(srcHtml);
renderer.setDocumentFromString(escapedHtml);

参考:

https://en.wikipedia.org/wiki/Escape_sequence

https://en.wikipedia.org/wiki/Escape_sequences_in_C

字符串操作

Apache Commons Lang 为字符串操作提供了 org.apache.commons.lang3.StringUtils 工具类,使用方式参考这里

StringUtils

参考

On Java 8 - 第十八章 字符串

Java String 对象,你真的了解了吗?

几张图轻松理解 String.intern()

再议String-字符串常量池与String.intern()

字符串常量池深入解析

JVM 常量池、运行时常量池、字符串常量池

Java8 对字符串连接的改进

HTML 字符实体

https://www.baeldung.com/category/java/tag/java-string/

包装类型作为日常开发最常用的数据载体,使用时有一些点需要特别注意,否则容易踩坑,本文总结下。

常见问题

值、引用比较问题

首先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Integer a = 1000;
Integer b = 1000;
Integer c = 100;
Integer d = 100;
Integer e = new Integer(100);
Integer f = new Integer(100);

// 值比较
System.out.println(("a equals b is " + (a.equals(b)))); // a equals b is true

// 引用比较,结果为 false
System.out.println("a == b is " + (a == b)); // a == b is false
// 引用比较,结果意外的为 true。这是由于 Integer 自动装箱时 [-128, 127] 使用了缓存,详见其 valueOf 方法的源码实现。
System.out.println(("c == d is " + (c == d))); // c == d is true
// 引用比较,结果为 false。因为 new Integer(int) 构造方法没有使用缓存。
System.out.println(("e == f is " + (e == f))); // e == f is false

值的比较务必使用 equals 方法。可以遵循《阿里巴巴 Java 开发规范》这条规范:

notes

自动装箱的性能问题

Java 语言提供了八种基本类型。其中包括:

  • 一种布尔类型
  • 一种字符类型
  • 六种数字类型(四种整数型,两种浮点型)

这些基本类型(primitive type)都有对应的包装类型(boxed primitive type),具有类的特性。基本类型和包装类型之间的转换通过装箱和拆箱方法:

基本数据类型 对应的包装类 拆箱方法 装箱方法 存储空间 取值范围 缓存范围
boolean Boolean booleanValue() valueOf(boolean) 1 bit true & false
char Character charValue() valueOf(char) 16 bit \u005Cu0000~\u005Cu007F
byte Byte byteValue() valueOf(byte) 8 bit -2^7~2^7-1 -2^7~2^7-1
short Short shortValue() valueOf(short) 16 bit -2^15~2^15-1 -2^7~2^7-1
int Integer intValue() valueOf(int) 32 bit -2^31~2^31-1 -2^7~2^7-1
long Long longValue() valueOf(long) 64 bit -2^63~2^63-1 -2^7~2^7-1
float Float floatValue() valueOf(float) 32 bit
double Double doubleValue() valueOf(double) 64 bit

其中数字类型的拆箱方法由共同的父类 java.lang.Number 定义:

Number方法

Number

自 JDK 1.5 版本后,Java 引入了自动装箱(autoboxing)、拆箱(auto-unboxing)作为语法糖,使基本类型和包装类型可以直接转换,减少使用包装类型的繁琐性。javac 编译会做相应处理,例如:

1
2
3
4
public static void main(String[] args) {
Integer i = 10;
int n = i;
}

反编译后代码如下:

1
2
3
4
public static void main(String args[]) {
Integer i = Integer.valueOf(10); // 自动装箱
int n = i.intValue(); // 自动拆箱
}

但在进行大批量数据操作时,装箱操作会有一定的性能损耗,看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 程序一:包装类型
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum); // 耗时 10390 ms

// 程序二:基本类型
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum); // 耗时 1101 ms

程序一运行起来比程序二慢了十倍,因为变量 sum 的每次计算结果都被反复装箱,导致不必要的对象创建和较高的资源消耗,从而影响性能。代码反编译如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 程序一:包装类型
Long sum = Long.valueOf(0L);
for (long i = 0L; i < 2147483647L; i++) {
sum = Long.valueOf(sum.longValue() + i);
}
System.out.println(sum);

// 程序二:基本类型
long sum = 0L;
long i;
for (i = 0L; i < 2147483647L; i++) {
sum += i;
}
System.out.println(sum);

这提醒我们,在进行大批量数据操作时,要优先使用基本类型。为此 Java 8 还专门引入了:

  • IntStreamLongStreamDoubleStream 作为泛型类 Stream 的补充;

  • OptionalIntOptionalLongOptionalDouble 作为泛型类 Optional 的补充;

  • 以及一堆配套的基本类型特化的函数式接口。

那么什么时候应该使用装箱类型呢?

  1. 必须使用装箱基本类型作为类型参数,因为 Java 不允许使用基本类型。例如作为泛型集合中的元素、键和值,由于不能将基本类型作为类型参数,因此必须使用装箱基本类型。又例如,不能将变量声明为 ThreadLocal<int> 类型,因此必须使用 ThreadLocal<Integer>
  2. 在进行反射的方法调用时,必须使用装箱基本类型。

自动拆箱的 NPE 风险

要注意,当程序进行涉及装箱和拆箱基本类型的混合类型计算时,它会进行自动拆箱。当程序进行拆箱时,会有 NPE 风险:

1
2
3
4
5
Long sum = null;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i; // java.lang.NullPointerException
}
System.out.println(sum);

在日常开发中,数据库字段定义、或查询结果都可能为 null,因为自动拆箱,用基本类型接收会有 NPE 风险。可以遵循《阿里巴巴 Java 开发规范》这条规范:

notes2

进制转换

https://en.wikipedia.org/wiki/Category:Numeral_systems

https://en.wikipedia.org/wiki/Numeral_system

https://en.wikipedia.org/wiki/List_of_numeral_systems

https://en.wikipedia.org/wiki/Positional_notation

Base/Radix
底/基数
Name 英文简写 字面量前缀 备注
2 Binary 二进制 BIN 0B0b 参考官方文档
8 Octal 八进制 OCT 0
10 Decimal 十进制 DEC
16 Hexadecimal 十六进制 HEX 0X0x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 十进制转成二进制:11111111
Integer.toBinaryString(255);
// 二进制转十进制:255
Integer.valueOf("11111111", 2);

// 十进制转成八进制:10
Integer.toOctalString(8);
// 八进制转成十进制:8
Integer.valueOf("10", 8);

// 十进制转成十六进制:F
Integer.toHexString(15);
// 十六进制转成十进制:15
Integer.valueOf("F", 16);

源码解析

最后,来总结下包装类型特点:

  • 所有包装类型都为 final,底层被包装的基本类型也都为 final。因此包装类型一旦初始化后,在运行期值是不可变的,非常安全。
  • ByteShortIntegerLong 装箱方法的某个数据段使用了缓存,因此 == 引用比较时可能会出现预期外的结果(true),详见源码。

java.lang.Byte

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
49
50
51
52
public final class Byte extends Number implements Comparable<Byte> {

/**
* The value of the {@code Byte}.
*
* @serial
*/
private final byte value;

/**
* Returns a {@code Byte} instance representing the specified
* {@code byte} value.
* If a new {@code Byte} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Byte(byte)}, as this method is likely to yield
* significantly better space and time performance since
* all byte values are cached.
*
* @param b a byte value.
* @return a {@code Byte} instance representing {@code b}.
* @since 1.5
*/
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}

/**
* Returns the value of this {@code Byte} as a
* {@code byte}.
*/
public byte byteValue() {
return value;
}

/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is a {@code Byte} object that
* contains the same {@code byte} value as this object.
*
* @param obj the object to compare with
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Byte) {
return value == ((Byte)obj).byteValue();
}
return false;
}
}

java.lang.Short

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
49
50
51
52
53
54
55
56
57
58
59
60
public final class Short extends Number implements Comparable<Short> {

/**
* The value of the {@code Short}.
*
* @serial
*/
private final short value;

/**
* Returns a {@code Short} instance representing the specified
* {@code short} value.
* If a new {@code Short} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Short(short)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param s a short value.
* @return a {@code Short} instance representing {@code s}.
* @since 1.5
*/
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}

/**
* Returns the value of this {@code Short} as a
* {@code short}.
*/
public short shortValue() {
return value;
}

/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is a {@code Short} object that
* contains the same {@code short} value as this object.
*
* @param obj the object to compare with
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Short) {
return value == ((Short)obj).shortValue();
}
return false;
}

}

java.lang.Integer

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
49
50
51
52
53
54
55
56
public final class Integer extends Number implements Comparable<Integer> {

/**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value;

/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

/**
* Returns the value of this {@code Integer} as an
* {@code int}.
*/
public int intValue() {
return value;
}

/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is an {@code Integer} object that
* contains the same {@code int} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}

}

java.lang.Long

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
49
50
51
52
53
54
55
56
57
58
59
60
61
public final class Long extends Number implements Comparable<Long> {

/**
* The value of the {@code Long}.
*
* @serial
*/
private final long value;

/**
* Returns a {@code Long} instance representing the specified
* {@code long} value.
* If a new {@code Long} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Long(long)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* Note that unlike the {@linkplain valueOf(int)
* corresponding method} in the {@code Integer} class, this method
* is <em>not</em> required to cache values within a particular
* range.
*
* @param l a long value.
* @return a {@code Long} instance representing {@code l}.
* @since 1.5
*/
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}

/**
* Returns the value of this {@code Long} as a
* {@code long} value.
*/
public long longValue() {
return value;
}

/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is a {@code Long} object that
* contains the same {@code long} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}

}

java.lang.Float

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public final class Float extends Number implements Comparable<Float> {

/**
* The value of the Float.
*
* @serial
*/
private final float value;

/**
* Returns a {@code Float} instance representing the specified
* {@code float} value.
* If a new {@code Float} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Float(float)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* @param f a float value.
* @return a {@code Float} instance representing {@code f}.
* @since 1.5
*/
public static Float valueOf(float f) {
return new Float(f);
}

/**

* Compares this object against the specified object. The result
* is {@code true} if and only if the argument is not
* {@code null} and is a {@code Float} object that
* represents a {@code float} with the same value as the
* {@code float} represented by this object. For this
* purpose, two {@code float} values are considered to be the
* same if and only if the method {@link #floatToIntBits(float)}
* returns the identical {@code int} value when applied to
* each.
*
* <p>Note that in most cases, for two instances of class
* {@code Float}, {@code f1} and {@code f2}, the value
* of {@code f1.equals(f2)} is {@code true} if and only if
*
* <blockquote><pre>
* f1.floatValue() == f2.floatValue()
* </pre></blockquote>
*
* <p>also has the value {@code true}. However, there are two exceptions:
* <ul>
* <li>If {@code f1} and {@code f2} both represent
* {@code Float.NaN}, then the {@code equals} method returns
* {@code true}, even though {@code Float.NaN==Float.NaN}
* has the value {@code false}.
* <li>If {@code f1} represents {@code +0.0f} while
* {@code f2} represents {@code -0.0f}, or vice
* versa, the {@code equal} test has the value
* {@code false}, even though {@code 0.0f==-0.0f}
* has the value {@code true}.
* </ul>
*
* This definition allows hash tables to operate properly.
*
* @param obj the object to be compared
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
* @see java.lang.floatToIntBits(float)
*/
public boolean equals(Object obj) {
return (obj instanceof Float)
&& (floatToIntBits(((Float)obj).value) == floatToIntBits(value));
}

}

java.lang.Double

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public final class Double extends Number implements Comparable<Double> {

/**
* The value of the Double.
*
* @serial
*/
private final double value;

/**
* Returns a {@code Double} instance representing the specified
* {@code double} value.
* If a new {@code Double} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Double(double)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* @param d a double value.
* @return a {@code Double} instance representing {@code d}.
* @since 1.5
*/
public static Double valueOf(double d) {
return new Double(d);
}

/**
* Compares this object against the specified object. The result
* is {@code true} if and only if the argument is not
* {@code null} and is a {@code Double} object that
* represents a {@code double} that has the same value as the
* {@code double} represented by this object. For this
* purpose, two {@code double} values are considered to be
* the same if and only if the method {@link
* #doubleToLongBits(double)} returns the identical
* {@code long} value when applied to each.
*
* <p>Note that in most cases, for two instances of class
* {@code Double}, {@code d1} and {@code d2}, the
* value of {@code d1.equals(d2)} is {@code true} if and
* only if
*
* <blockquote>
* {@code d1.doubleValue() == d2.doubleValue()}
* </blockquote>
*
* <p>also has the value {@code true}. However, there are two
* exceptions:
* <ul>
* <li>If {@code d1} and {@code d2} both represent
* {@code Double.NaN}, then the {@code equals} method
* returns {@code true}, even though
* {@code Double.NaN==Double.NaN} has the value
* {@code false}.
* <li>If {@code d1} represents {@code +0.0} while
* {@code d2} represents {@code -0.0}, or vice versa,
* the {@code equal} test has the value {@code false},
* even though {@code +0.0==-0.0} has the value {@code true}.
* </ul>
* This definition allows hash tables to operate properly.
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
* @see java.lang.Double#doubleToLongBits(double)
*/
public boolean equals(Object obj) {
return (obj instanceof Double)
&& (doubleToLongBits(((Double)obj).value) ==
doubleToLongBits(value));
}

}

java.lang.Boolean

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
49
50
51
52
53
54
public final class Boolean implements java.io.Serializable, Comparable<Boolean> {

/**
* The value of the Boolean.
*
* @serial
*/
private final boolean value;

/**
* Returns a {@code Boolean} instance representing the specified
* {@code boolean} value. If the specified {@code boolean} value
* is {@code true}, this method returns {@code Boolean.TRUE};
* if it is {@code false}, this method returns {@code Boolean.FALSE}.
* If a new {@code Boolean} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Boolean(boolean)}, as this method is likely to yield
* significantly better space and time performance.
*
* @param b a boolean value.
* @return a {@code Boolean} instance representing {@code b}.
* @since 1.4
*/
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

/**
* Returns the value of this {@code Boolean} object as a boolean
* primitive.
*
* @return the primitive {@code boolean} value of this object.
*/
public boolean booleanValue() {
return value;
}

/**
* Returns {@code true} if and only if the argument is not
* {@code null} and is a {@code Boolean} object that
* represents the same {@code boolean} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the Boolean objects represent the
* same value; {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Boolean) {
return value == ((Boolean)obj).booleanValue();
}
return false;
}

}

java.lang.Character

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
49
50
51
52
53
54
55
56
57
58
59
60
public final class Character implements java.io.Serializable, Comparable<Character> {

/**
* The value of the {@code Character}.
*
* @serial
*/
private final char value;

/**
* Returns a <tt>Character</tt> instance representing the specified
* <tt>char</tt> value.
* If a new <tt>Character</tt> instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Character(char)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* This method will always cache values in the range {@code
* '\u005Cu0000'} to {@code '\u005Cu007F'}, inclusive, and may
* cache other values outside of this range.
*
* @param c a char value.
* @return a <tt>Character</tt> instance representing <tt>c</tt>.
* @since 1.5
*/
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}

/**
* Returns the value of this {@code Character} object.
* @return the primitive {@code char} value represented by
* this object.
*/
public char charValue() {
return value;
}

/**
* Compares this object against the specified object.
* The result is {@code true} if and only if the argument is not
* {@code null} and is a {@code Character} object that
* represents the same {@code char} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Character) {
return value == ((Character)obj).charValue();
}
return false;
}

}

参考

《Effective Java 第三版》

  • 第 44 条:坚持使用标准的函数接口
  • 第 61 条:基本类型优先于装箱基本类型

《Java 8 函数式编程》

《Java 8 实战》

https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html

一文读懂什么是Java中的自动拆装箱

「传统的 OLTP 应用程序」和「现代的 WEB 应用程序」通常会执行许多小型数据更改操作,其中并发性能至关重要。本文记录如何优化这些批量插入与更新操作。

优化思路

https://dev.mysql.com/doc/refman/8.0/en/data-change-optimization.html

INSERT

If you are inserting many rows from the same client at the same time, use INSERT statements with multiple VALUES lists to insert several rows at a time. This is considerably faster (many times faster in some cases) than using separate single-row INSERT statements.

UPDATE

JDBC

使用 PreparedStatementaddBatch() 方法并执行 executeBatch() 批量发送多个操作给数据库,减少网络交互次数。

MyBatis

INSERT

MyBatis 批量插入几千条数据慎用 foreach。应使用 BATCH 模式,只发出一条 SQL 语句,预编译一次,但设置参数 N 次,执行一次。

1
2
3
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
...
}

ExecutorType.BATCH

相比之下,默认的 SIMPLE 模式,每一条语句都会发出一条 SQL,进行一次预编译,设置一次参数,执行一次。

ExecutorType.SIMPLE

两者性能相差很大。

参考:

UPDATE

MyBatis Plus

INSERT

UPDATE

MyBatis Plus 只提供了根据主键 ID 进行批量更新的 updateBatchById 的方法,以下是根据某个或者多个非 ID 字段进行批量更新的自定义方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean batchUpdate(Collection<SettlementDetailDO> entityList) {
String sqlStatement = getSqlStatement(SqlMethod.UPDATE);
return executeBatch(entityList, DEFAULT_BATCH_SIZE, (sqlSession, entity) -> {
MapperMethod.ParamMap<Object> param = new MapperMethod.ParamMap<>();
param.put(Constants.ENTITY, SettlementDetailDO.builder()
...
.build());
param.put(Constants.WRAPPER, Wrappers.<SettlementDetailDO>lambdaQuery()
.eq(SettlementDetailDO::getTransactionId, entity.getTransactionId())
.eq(SettlementDetailDO::getSettlementParty, entity.getSettlementParty())
.eq(SettlementDetailDO::getSettlementStatus, SettlementStatusEnum.INIT)
);
sqlSession.update(sqlStatement, param);
});

参考:《MyBatis Plus 根据某个指定字段批量更新数据库

参考

Configuration XML

TypeHandler

https://mybatis.org/mybatis-3/configuration.html#typeHandlers

Whenever MyBatis sets a parameter on a PreparedStatement or retrieves a value from a ResultSet, a TypeHandler is used to retrieve the value in a means appropriate to the Java type.

org.apache.ibatis.type.TypeHandler 接口实现类:

TypeHandler

TypeHandler 可用于以下场景:

参考例子:https://github.com/qidawu/mybatis-test/tree/master/src/main/java/org/mybatis/example/typehandler

使用方式:

1
2
<!-- 手动结果映射 -->
<result column="..." jdbcType="..." typehandler="..." />
1
2
// 自动结果映射(MyBatis Plus)
@TableField(typeHandler = ...class)

Mapper XML Files

https://mybatis.org/mybatis-3/sqlmap-xml.html

查询

1
2
3
4
5
<!-- 手动结果映射 -->
<select id="selectPage" resultMap="BaseResultMap">

<!-- 自动结果映射(automatic mapping) -->
<select id="selectPage" resultType="...">

手动结果映射

https://mybatis.org/mybatis-3/sqlmap-xml.html#Result_Maps

结果映射,用于自定义结果集(Result Maps):

参考源码:

DefaultResultSetHandler#handleRowValues

DefaultResultSetHandler#createResultObject

可变/不可变类的结果映射

如果是可变类(mutable classes),可以使用 Setter injection 进行结果映射(注意,必须指定 property 属性):

id – an ID result; flagging results as ID will help improve overall performance
result – a normal result injected into a field or JavaBean property

1
2
3
4
5
<resultMap id="BaseResultMap" type="...">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="create_time" jdbcType="BIGINT" property="createTime" />
<result column="update_time" jdbcType="BIGINT" property="updateTime" />
</resultMap>

但如果是不可变类(immutable classes),由于都是 final field,没有 Setter 方法,则只能通过 Constructor injection 进行结果映射:

constructor - used for injecting results into the constructor of a class upon instantiation

  • idArg - ID argument; flagging results as ID will help improve overall performance
  • arg - a normal result injected into the constructor

注意,arg 元素有两种设置方式:

  • 有序的 arg。必须指定 javaType 属性,否则构造方法参数默认使用 java.lang.Object 类型会构造报错。

    1
    2
    3
    4
    5
    6
    7
    <resultMap id="BaseResultMap" type="...">
    <constructor>
    <idArg column="id" javaType="java.lang.Long" />
    <arg column="create_time" javaType="java.lang.Long" />
    <arg column="update_time" javaType="java.lang.Long" />
    </constructor>
    </resultMap>
  • 无序的 arg。通过指定 name 属性,可以忽略 javaType 属性。

    1
    2
    3
    4
    5
    6
    7
    <resultMap id="BaseResultMap" type="...">
    <constructor>
    <idArg column="id" name="id" />
    <arg column="create_time" name="createTime" />
    <arg column="update_time" name="updateTime" />
    </constructor>
    </resultMap>

When you are dealing with a constructor with many parameters, maintaining the order of arg elements is error-prone.

Since 3.4.3, by specifying the name of each parameter, you can write arg elements in any order. To reference constructor parameters by their names, you can

  • either add @Param annotation to them

  • or compile the project with -parameters‘ compiler option and enable useActualParamName (this option is enabled by default).

    设置名 描述 有效值 默认值
    useActualParamName 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。 true, false true
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!-- https://maven.apache.org/plugins/maven-compiler-plugin/examples/pass-compiler-arguments.html -->
    <project>
    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
    <compilerArgs>
    <arg>-parameters</arg>
    </compilerArgs>
    </configuration>
    </plugin>
    </plugins>
    </build>
    </project>

javaType can be omitted if there is a property with the same name and type.

注意,如果抛出异常如下:

1
org.apache.ibatis.executor.ExecutorException: No constructor found in ...

解决方案:

  • 如果是可变类(mutable classes),为类添加无参构造方法。
  • 如果是不可变类(immutable classes),Mapper XML 结果映射 resultMap 必须使用正确参数的 constructor

嵌套结果映射

两种嵌套关系配置:

  • association “has-one” type relationship
  • collection “has many” type relationship

两种查询方式:

  • 嵌套查询(Nested Select),即分开多次查询。
  • 嵌套结果映射(Nested Results),即利用表连接语法进行一次性的连表查询;

The association element deals with a “has-one” type relationship. For example, in our example, a Blog has one Author. An association mapping works mostly like any other result. You specify the target property, the javaType of the property (which MyBatis can figure out most of the time), the jdbcType if necessary and a typeHandler if you want to override the retrieval of the result values.

Where the association differs is that you need to tell MyBatis how to load the association. MyBatis can do so in two different ways:

  • Nested Select: By executing another mapped SQL statement that returns the complex type desired.
  • Nested Results: By using nested result mappings to deal with repeating subsets of joined results.

总结:

嵌套查询(Nested Select) 嵌套结果映射(Nested Results)
association 多对一关联 禁用 可用
collection 一对多关联 可用 可用
  • association 多对一关联,嵌套查询(Nested Select)存在 N+1 次查询的性能问题,不建议使用。

    While this approach Nested Select is simple, it will not perform well for large data sets or lists. This problem is known as the “N+1 Selects Problem”. In a nutshell, the N+1 selects problem is caused like this:

    • You execute a single SQL statement to retrieve a list of records (the “+1”).
    • For each record returned, you execute a select statement to load details for each (the “N”).

    This problem could result in hundreds or thousands of SQL statements to be executed. This is not always desirable.

    The upside is that MyBatis can lazy load such queries, thus you might be spared the cost of these statements all at once. However, if you load such a list and then immediately iterate through it to access the nested data, you will invoke all of the lazy loads, and thus performance could be very bad.

    And so, there is another way Nested Results.

  • collection 一对多关联,两种配置方式的区别如下:

1
2
3
4
5
6
7
8
9
<!-- 一对多的嵌套结果映射(Nested Results)。注意表连接查询字段务必使用 AS 别名,避免手工映射时 column 取错列 -->
<resultMap id="ExtendResultMap" extends="BaseResultMap" type="...XxxQO">
<collection property="extendList" columnPrefix="sub_task_" resultMap="...XxxMapper.BaseResultMap" ofType="...XxxPO" />
</resultMap>

<!-- 一对多的嵌套查询(Nested Select),column 为嵌套查询的参数列,即“一”方的列 -->
<resultMap id="ExtendResultMap" extends="BaseResultMap" type="...XxxQO">
<collection property="extendList" column="task_no" select="...XxxMapper.getByTaskNo" ofType="...XxxPO" />
</resultMap>

下面演示一个例子,涉及的关联关系如下:学生多对一关联学校、一对多关联书本:

StudentMapper.java

1
List<StudentQO> listStudents(int age);

StudentQO.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
@Setter
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class StudentQO extends StudentPO {

/**
* 多对一关联学校
*/
private SchoolPO school;

/**
* 一对多关联书本
*/
private List<BookPO> books;

}

StudentMapper.xml

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
<sql id="Base_column_list">
T.id,
......
T.create_time,
T.update_time,
T.version
</sql>

<sql id="Extend_column_list">
<include refid="Base_column_list" />
,
<!-- 见 SchoolMapper.xml -->
<include refid="com.test.school.mapper.SchoolMapper.Base_Column_List" />
</sql>

<resultMap id="BaseResultMap" type="com.test.student.po.StudentPO">
<id column="id" property="id" jdbcType="BIGINT"/>
......
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="version" property="version" jdbcType="TINYINT"/>
</resultMap>

<resultMap id="ExtendResultMap" extends="BaseResultMap" type="com.test.student.qo.StudentQO">
<!-- 使用嵌套结果映射,即利用表连接语法进行一次性的连表查询。resultMap 配置见 SchoolMapper.xml -->
<association property="school" columnPrefix="school_" resultMap="com.test.school.mapper.SchoolMapper.BaseResultMap" />
<!-- 使用嵌套查询,即分开多次查询。column 为嵌套查询的参数列,select 引用见 BookMapper.xml -->
<collection property="books" column="student_no" select="com.test.book.mapper.BookMapper.getByStudentNo" ofType="com.test.book.po.BookPO" />
</resultMap>

<!-- 使用表连接嵌套查询学生、学校 -->
<select id="listStudents" resultMap="ExtendResultMap">
SELECT
<include refid="Extend_column_list" />
FROM t_student T
INNER JOIN t_school E
ON T.school_no = E.school_no
WHERE T.age = #{age}
</select>

SchoolMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<sql id="Base_Column_List">
E.id AS school_id,
......
E.create_time AS school_create_time,
E.update_time AS school_update_time
</sql>

<resultMap id="BaseResultMap" type="com.test.school.po.SchoolPO">
<id column="id" property="id" jdbcType="BIGINT"/>
......
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>

自动结果映射

https://mybatis.org/mybatis-3/sqlmap-xml.html#Auto-mapping

相关配置

设置名 描述 有效值 默认值
autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。
* NONE 表示关闭自动映射
* PARTIAL 只会自动映射没有定义嵌套结果映射的字段
* FULL 会自动映射任何复杂的结果集(无论是否嵌套)
NONE, PARTIAL, FULL PARTIAL
autoMappingUnknownColumnBehavior 指定发现自动映射目标未知列(或未知属性类型)的行为。
* NONE: 不做任何反应
* WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN
* FAILING: 映射失败 (抛出 SqlSessionException)
NONE, WARNING, FAILING NONE
mapUnderscoreToCamelCase 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 true, false false
argNameBasedConstructorAutoMapping 当应用构造器自动映射时,参数名称被用来搜索要映射的列,而不再依赖列的顺序。(新增于 3.5.10) true, false false

自动结果映射常用于搭配 MyBatis 注解 @Select 使用。

MyBatis 自动结果映射时,如果有多个构造方法,可以通过 @AutomapConstructor 指定自动映射使用哪个。例如搭配 lombok 使用:

1
@AllArgsConstructor(onConstructor=@__({@AutomapConstructor}))

如果构造方法未指定 @AutomapConstructor,会按字段类型查找匹配的构造方法。如果找不到该构造方法,则会报错。

缓存

一级缓存

也称为本地缓存,与 SqlSession 绑定,只存在于 SqlSession 生命周期。

二级缓存

https://github.com/mybatis/memcached-cache

https://github.com/mybatis/ignite-cache

https://github.com/mybatis/redis-cache

https://github.com/mybatis/hazelcast-cache

https://github.com/mybatis/caffeine-cache

https://github.com/mybatis/couchbase-cache

https://github.com/mybatis/oscache-cache

https://github.com/mybatis/ehcache-cache

参考:

MyBatis 二级缓存 关联刷新实现

插入

https://mybatis.org/mybatis-3/sqlmap-xml.html#insert_update_and_delete

回写主键

MyBatis 回写主键利用了 JDBC 的特性,适用于支持自动生成主键的数据库,比如 MySQL 和 SQL Server。

First, if your database supports auto-generated key fields (e.g. MySQL and SQL Server), then you can simply set useGeneratedKeys="true" and set the keyProperty to the target property and you’re done.

1
2
3
4
<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>

keyProperty attribute:

(insert and update only) Identifies a property into which MyBatis will set the key value returned by getGeneratedKeys, or by a selectKey child element of the insert statement. Default: unset.

自定义生成主键

适用于不支持自动生成主键的数据库,比如 Oracle。

MyBatis has another way to deal with key generation for databases that don’t support auto-generated column types, or perhaps don’t yet support the JDBC driver support for auto-generated keys.

Here’s a simple (silly) example that would generate a random ID (something you’d likely never do, but this demonstrates the flexibility and how MyBatis really doesn’t mind).

The selectKey statement would be run first, the Author id property would be set, and then the insert statement would be called. This gives you a similar behavior to an auto-generated key in your database without complicating your Java code.

1
2
3
4
5
6
7
8
9
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>
insert into Author
(id, username, password, email,bio, favourite_section)
values
(#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

The selectKey element is described as follows:

1
2
3
4
5
<selectKey
keyProperty="id"
resultType="int"
order="BEFORE"
statementType="PREPARED">

注解方式:

1
2
@Insert("insert into Author(id, username, password, email,bio, favourite_section) values (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})")
@SelectKey(keyProperty = "id", resultType = Long.class, before = false, statement = "SELECT LAST_INSERT_ID()")

Dynamic SQL

https://mybatis.org/mybatis-3/dynamic-sql.html

script

For using dynamic SQL in annotated mapper class, script element can be used.

与 Spring 整合

依赖安装

基础依赖安装:

1
2
3
4
5
6
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>x.x.x</version>
</dependency>

Spring 整合依赖安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- MyBatis Spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>x.x.x</version>
</dependency>

<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>x.x.x</version>
</dependency>

Spring Bean 配置

In base MyBatis, the SqlSessionFactory is built using SqlSessionFactoryBuilder. In MyBatis-Spring, SqlSessionFactoryBean is used instead.

MyBatis-Spring 中,SqlSessionFactoryBean 用于创建 SqlSessionFactory。主要的配置参数如下:

  • dataSource 数据源类
  • configLocation 配置文件的位置
  • mapperLocations Mapper XML 文件的位置
  • typeAliasesPackage

MyBatis-Spring 中,Mapper API 的扫描查找有两种方式:

  • 配置 org.mybatis.spring.mapper.MapperScannerConfigurer 主动扫描指定 basePackage 路径
  • 为 Mapper API 加上类注解:org.mybatis.spring.annotation.MapperScan

下面演示如何通过 Spring 的 XML 配置文件或 Java Config 进行配置。

XML

1
2
3
4
5
6
7
8
9
10
11
<!-- 配置 MyBatis 的 sqlSessionFactory。MyBatis Plus 使用 com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>

<!-- DAO 接口所在包名,Spring 会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

Java Config

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
import javax.sql.DataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;

@MapperScan(basePackages = {"..."})
public class MyBatisConfig {

/**
* 用于创建 SqlSessionFactory,以便获取 SqlSession
**/
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setConfigLocation(configLocation);
factoryBean.setMapperLocations(resource);
return factoryBean;
}

/**
* @MapperScan 或 MapperScannerConfigurer 二选一配置
**/
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage(basePackage);
return mapperScannerConfigurer;
}

}

事务

SpringManagedTransactionFactory

与 Spring Boot 整合

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>x.x.x</version>
</dependency>

http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

插件

深入理解 Mybatis 插件开发

扩展

MyBatis Generator

https://github.com/mybatis/generator

https://github.com/zouzg/mybatis-generator-gui

MyBatis PageHelper

https://github.com/pagehelper/Mybatis-PageHelper

https://pagehelper.github.io/docs/howtouse/

Mybatis Plus

https://baomidou.com/

https://github.com/baomidou/mybatis-plus

https://github.com/yulichang/mybatis-plus-join

https://www.jianshu.com/p/ceb1df475021

IDEA Plugins

https://plugins.jetbrains.com/plugin/7293-mybatis-plugin

https://plugins.jetbrains.com/plugin/8321-free-mybatis-plugin