Java 集合框架系列(六)线性表之 List 总结
常用方法
List partition
Array to List
方法一:转两次
1 | List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c")) // from varargs |
方法二:Java 8
1 | // 包装类型 |
方法三:Guava
1 | // 不可变集合 |
方法一:转两次
1 | List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c")) // from varargs |
方法二:Java 8
1 | // 包装类型 |
方法三:Guava
1 | // 不可变集合 |
本文总结下集合元素排序的常用 API:
如果集合元素已实现 Comparable
接口,可以直接使用 naturalOrder
、reverseOrder
方法进行排序:
1 | List<Integer> integers = Arrays.asList(3, 1, 2, 4); |
上述排序不支持 null
值(会抛 NPE 异常),如果自定义实现的话,代码比较冗余,容易出错:
1 | List<Integer> integers = Arrays.asList(3, 1, null, 2, 4); |
可采用 nullsFirst
、nullsLast
方法兼容 null
值情况:
1 | List<Integer> integers = Arrays.asList(3, 1, null, 2, 4); |
例子:
1 | @Data |
如果集合元素未实现 Comparable
接口,需要抽取关键字(关键字需实现 Comparable
接口)排序:
1 | List<IdName> idNames = Arrays.asList( |
《java comparator 升序、降序、倒序从源码角度理解》
https://blog.csdn.net/weixin_44270183/article/details/87026995
本文总结下集合元素迭代的常用 API。
迭代器模式(Iterator)是一种行为型设计模式,让你能在不暴露集合底层表现形式(列表、栈和树等)的情况下遍历集合中所有的元素。
在 Java 中,迭代器模式的实现有以下几种:
java.util.Enumeration<E>
:Java 1.0 引入,用于枚举集合元素。这种传统接口已被 Iterator
迭代器取代,虽然 Enumeration
还未被废弃,但在现代代码中已经被很少使用了。主要用于诸如 java.util.Vector
和 java.util.Properties
这些传统集合类。java.util.Iterator<E>
:Java 1.2 引入。作为 Java 集合框架的成员,迭代器取代了枚举。迭代器与枚举有两个不同之处:remove
方法,允许调用者在迭代期间从集合中删除元素。java.lang.Iterable<T>
:Java 1.5 引入。For-each Loop 语法糖的底层实现,实现这个接口的对象可以用于 “For-each Loop”语句,简化迭代器繁琐的使用语法。上述三种迭代器实现都属于命令式编程范式,即使访问值的方法仅由迭代器负责实现。但实际上,是由开发者来决定何时访问序列中的 next()
项。
java.util.stream.Stream<T>
:Java 8 引入,用于实现 Stream API:
与迭代器的区别在于:
Iterator
外部迭代,使用命令式编程范式,完全由用户来决定”做什么“和”怎么做“,例如:
1 | @Test |
Stream
内部迭代,使用声明式编程范式 > 函数式编程,用户仅需要决定“做什么”,而把“怎么做”的任务交给 JVM:
1 | @Test |
使用内部迭代的优势在于:
响应式编程范式通常在面向对象语言中作为观察者模式的扩展出现。可以将其与大家熟知的迭代器模式作对比,主要区别在于:
参考:响应式编程总结
Java 集合框架并不是一蹴而就写成的,也是经过了好多个版本迭代的演进与发展,才走到今天。本文总结下集合框架各版本的功能增强。
List
、Set
和 Map
接口中,新的静态工厂方法可以创建这些集合的不可变实例(immutable),如下:
1 | List<String> list = List.of("apple", "orange", "banana"); |
参考:https://www.linuxidc.com/Linux/2017-10/147683.htm
forEach
HashMap
、LinkedHashMap
、ConcurrentHashMap
的性能得到提升。当出现大量散列冲突时,值将存储在红黑树而不是链表,以提升查找性能。TransferQueue
,以及实现类 LinkedTransferQueue
。Map
及其派生实现类引入了一个性能改进的替代版散列函数(但在 Java SE 8 已被移除并取代)。新增几个集合接口:
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
三个新增的语法糖显著增强了集合框架:
泛型:为集合框架添加编译时类型安全,并在读取元素时不再需要做类型转换。
自动装箱/拆箱:往集合插入元素时自动装箱(将原始数据类型转换为对应的包装类型),读取元素时自动拆箱。
增强 for
循环:迭代集合时不再需要显式迭代器(Iterator
)。
1 | // 数组迭代 |
通用实现与并发实现:
Queue
BlockingQueue
ConcurrentMap
Queue
实现类:PriorityQueue
ConcurrentLinkedQueue
LinkedList
实现 Queue
接口AbstractQueue
抽象类实现BlockingQueue
实现类,位于 java.util.concurrent
包下:LinkedBlockingQueue
ArrayBlockingQueue
PriorityBlockingQueue
DelayQueue
SynchronousQueue
ConcurrentMap
实现类:ConcurrentHashMap
特殊实现:
List
和 Set
实现类,用于读远大于写以及迭代无法线程同步的情况:CopyOnWriteArrayList
CopyOnWriteArraySet
Set
和 Map
实现类,用于枚举: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
工具类新增下列方法:
hashCode
、toString
deepEquals
、deepHashCode
、deepToString
用于多维数组Collections
工具类新增几个新方法,例如 :replaceAll(List list, Object oldVal, Object newVal)
查找替换。RandomAccess
。LinkedHashMap
、LinkedHashSet
。内部使用散列表 + 双向链表(按插入顺序排序)。集合接口中的许多修改方法都被标记为可选(optional)。实现类允许按需实现,未实现的方法需抛出运行时异常 UnsupportedOperationException
。每个实现类的文档必须指明支持哪些可选操作。集合框架引入下列术语来帮助阐述本规范:
长度保证不变(即使元素可以更改)的列表称为 fixed-size 定长列表。反之则称为 variable-size 变长列表。
开发中接触最多的定长集合是通过 Arrays.asList()
创建的,该方法是一个适配器接口,将数组适配为定长列表,返回的对象是一个 Arrays
内部类,源码如下:
1 | /** |
分析发现,涉及元素增删的操作(如 add()、remove()、clear()
)该内部类并没有实现,而是使用了父类 AbstractList
的方法,默认抛出 UnsupportedOperationException
异常:
1 | public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> { |
参考手册:
不支持修改操作(例如 add
、remove
和 clear
)的集合称为 unmodifiable 不可修改集合。反之则称为 modifiable 可修改集合。Collections
工具类提供了一组静态工厂方法,用于包装并返回指定集合的不可修改视图(unmodifiable view),如果尝试修改,则会抛出 UnsupportedOperationException
:
1 | Collections.unmodifiableCollection |
从源码分析,该包装类覆盖了所有修改方法并抛出异常 UnsupportedOperationException
,实现非常简单:
1 | static class UnmodifiableCollection<E> implements Collection<E>, Serializable { |
在 unmodifiable 的基础上,加之保证 Collection
实现类的底层数据为 final
的集合称为 immutable 不可变集合。反之则称为 mutable 可变集合。
Java 9 为 List
、Set
和 Map
接口提供了新的静态工厂方法,可以创建这些集合的不可变实例,如下:
1 | List<String> list = List.of("apple", "orange", "banana"); |
而 Java 9 之前,要实现不可变集合只能通过第三方库,例如用 Guava 实现相同效果:
1 | List<String> list = ImmutableList.of("apple", "orange", "banana"); |
Guava 提供的不可变集合 API 如下:
1 | ImmutableAsList |
使用如下:
1 | List<String> list = ImmutableList.of("apple", "orange", "banana"); |
除此之外,Apache Commons Lang 也提供了两个好用的类 Pair
和 Triple
,可用于存放指定个数的临时数据:
1 | Triple.of("left", "middle", "right") |
参考线程同步包装器
支持根据下标索引快速(时间复杂度 0(1)
)访问元素的列表称为 random access 随机访问列表。反之则称为 sequential access 顺序访问列表。
标记接口 java.util.RandomAccess
用于标记列表类支持随机访问,其实现类如下:
该标记接口使得 Collections
工具类中的通用算法实现能够据此更改其行为以提升性能:
binarySearch
reverse
shuffle
fill
copy
rotate
replaceAll
indexOfSubList
lastIndexOfSubList
checkedList
以 binarySearch
为例,源码判断如下:
1 | public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) { |
某些集合实现限制了可以存储哪些元素。可能的限制包括:
null
。尝试添加违反集合实现限制的元素将导致运行时异常,如 ClassCastException
、IllegalArgumentException
或 NullPointerException
。
参考手册:
泛型机制虽然为集合提供了编译期类型检查,但仍然可以在运行期绕过此机制(通过反射也能绕过编译期类型检查):
1 | public void test4() { |
集合框架提供了一组包装器实现:
1 | Collections.checkedCollection |
这些包装器实现用于返回指定集合的动态类型安全视图(dynamically type-safe view),核心源码如下:
1 | static class CheckedCollection<E> implements Collection<E>, Serializable { |
以 add
方法为例,每次添加元素时,都会调用 typeCheck
私有方法进行类型检查,如果尝试添加错误类型的元素,则会抛出 ClassCastException
,通过 fail fast 防止后续出错:
1 | public void test() { |
《Java 中的 Mutable 和 Immutable》(en_US)
https://stackoverflow.com/questions/7713274/java-immutable-collections
final
elements.集合(collection)表示一组对象。Java SE 提供了集合框架(collections framework),是一个用于表示和操作集合的统一框架,使集合可以独立于实现细节进行操作。集合框架的主要优点如下:
集合框架的整体组成如下:
下面分别来看下各组成部分。
集合接口分为下面两组,这些接口构成了集合框架的基础:
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
andList
.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.
最基础的集合接口 java.util.Collection
及其子接口如下:
其中,常用的五个重点接口的方法及使用要点如下:
其它集合接口基于 java.util.Map
,不是真正的集合。但是,这些接口包含集合视图(collection-view)操作,使得它们可以作为集合进行操作。
java.util.Map
接口的方法如下:
下列抽象类为核心集合接口提供了基本功能实现,以最小化用户自定义实现的成本。这些抽象类的 API 文档精确地描述了各个方法的实现方式,实现者能够参阅并了解哪些方法需要覆盖:
java.util.Collection
的通用实现如下:
java.util.Map
的通用实现如下:
集合接口的主要实现,命名通常形如 <*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)的静态工厂方法可用于添加同步行为。早期版本的集合类,已被改进以实现新的集合接口:
java.util.Vector
- List
接口的可变长数组实现,线程同步,包含其它遗留方法。java.util.Hashtable
- Map
接口的散列表实现,线程同步,键和值都不允许为 null
,包含其它遗留方法。继承自抽象类 java.util.Dictionary
。为高并发使用而设计的实现。详见另一篇《并发实现总结》。
用于特殊情况的实现:
CopyOnWriteArrayList
写时复制列表CopyOnWriteArraySet
写时复制列表WeakHashMap
IdentityHashMap
EnumSet
EnumMap
将某个集合接口适配成另一个:
根据 Map
的通用实现创建一个 Set
的通用实现:
1 | Collections.newSetFromMap(Map) |
以后进先出(Lifo)队列的形式返回 Deque
的视图:
1 | Collections.asLifoQueue(Deque) |
将数组转换为 List
集合:
1 | Arrays.asList(...) |
用于其它集合实现的功能增强:
返回指定集合的不可修改视图(unmodifiable view),如果尝试修改,则会抛出 UnsupportedOperationException
:
1 | Collections.unmodifiableCollection |
返回由指定集合支持的 synchronized
线程同步集合:
1 | Collections.synchronizedCollection |
返回指定集合的动态类型安全视图(dynamically type-safe view),如果尝试添加错误类型的元素,则会抛出 ClassCastException
。泛型机制虽然提供了编译期类型检查,但可以绕过此机制。动态类型安全试图消除了这种可能性:
1 | Collections.checkedCollection |
集合接口的高性能版“迷你实现”:
返回一个不可变集合(immutable),不包含任何元素:
1 | Collections.emptySet |
返回一个不可变集合(immutable),仅包含一个元素:
1 | Collections.singleton |
返回一个不可变集合(immutable),包含指定元素的 N 个拷贝:
1 | Collections.nCopies |
返回一个由指定数组支持的定长集合(fixed-size):
1 | Arrays.asList |
为集合接口提供必要支持的接口。例如:
Iterator
、ListIterator
Comparable
、Comparator
UnsupportedOperationException
、ConcurrentModificationException
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 ?
1 | import com.google.common.collect.Maps; |
本文介绍时区处理的两种方式。时区涉及的接口及实现类如下:
(上图简化掉了 Serializable
接口、Comparable
接口及 FunctionalInterface
注解)
时区的处理是新版日期与时间 API 新增的重要功能,且 API 被极大简化。新的 java.time.ZoneId
类是老版本 java.util.TimeZone
类的替代品。它的设计目标就是要让用户无需为时区处理的复杂和繁琐而操心,比如处理夏令时(DST)问题。
每个特定的 ZoneId
对象都有一个地区 ID 标识。地区 ID 格式为“{区域}/{城市}
”,这些地区集合的设定都由 IANA 的时区数据库提供。可以输出如下:
1 | ZoneId.getAvailableZoneIds().forEach(System.out::println); |
ZoneId
的静态工厂方法构造如下:
1 | // 获取服务器所在时区的 ZoneId,例如 Asia/Shanghai 为 UTC+8 |
一旦得到一个 ZoneId
对象,就可以与 LocalDate
、LocalDateTime
、Instant
对象整合起来,构造一个 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 | public final class ZonedDateTime |
ZonedDateTime
的实例如下图:
图中可见,原本 LocalDateTime
对象作为一个本地日期与时间,是不包含时区信息的,即没有时区概念。而在结合了 ZoneId
构造成一个 ZonedDateTime
实例之后,才有了时区概念。它代表了相对于指定时区的时间点。
1 | // 1970-01-01 |
通过 ZoneId
可以将 LocalDateTime
和 Instant
进行互转,公式为 UTC + 时区差(东正西负)= 本地时间。
LocalDateTime
> Instant
:
1 | // 东八区的 1970-01-01T00:00,等于 UTC+0 的 1969-12-31T16:00:00Z |
Instant
> LocalDateTime
:
1 | // 1970-01-01T00:00 |
另一种比较通用的表达时区的方式是利用当前时区和 UTC/格林尼治的固定偏差。可以使用 ZoneOffset
类,它是 ZoneId
的一个子类,表示的是当前时间和 UTC 的偏差:
1 | ZoneOffset newYorkOffset = ZoneOffset.of("-05:00"); |
ZoneOffset
类可用于构造 OffsetDateTime
实例。OffsetDateTime
的底层实现如下:
1 | public final class OffsetDateTime |
1 | // 1970-01-01T00:00-05:00 |
“-05:00” 的偏差实际上对应的是美国东部标准时间。注意,使用这种方式定义的 ZoneOffset
并未考虑任何夏令时的影响,所以在大多数情况下,不推荐使用。
有时开发会使用 java.sql.Timestamp
作为 PO 实体类的时间字段,java.sql.Timestamp
底层实现使用格里历(公历),并使用服务器所在时区(即本地时区),并受该时区影响。
这里看一段代码,以 2021-01-04 00:00:00 为例演示转换过程:
1 | LocalDateTime localDateTime = LocalDateTime.parse( |
这里试验两个时区:
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 会有什么问题:
转换过程:本地时间 > 系统时区的时间 > 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 | /** |
转换过程:本地时间 > 指定时区的时间 > 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 ) |
这里看似结果没有问题,Instant
和 Timestamp
对象在不同时区下都是相同时间戳。
但有一种场景,就是应用服务器与数据库的时区不一致导致的问题。假如应用服务器时区为 Asia/Shanghai (UTC+8)
,数据库时区为 Europe/London (UTC)
,当把上表 Timestamp
对象保存到 MySQL 数据库的 datetime
字段时,如果未经时区转换,会导致错误结果。
这里参考 mysql-connector-java-5.1.42.jar 源码如下,重点看 java.sql.PreparedStatement#setTimestamp
的方法实现,其使用了 SimpleDateFormat
将 Timestamp
对象格式化成字符串,如果未经时区转换,结果如下表,导致前后不一致:
格式化前 | 格式化后 |
---|---|
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 | /** |
上述方法内部调用了 com.mysql.jdbc.TimeUtil#changeTimezone
方法,源码如下。
1 | /** |
如果 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 |
参考:
不指定时区会踩坑:MySQL JDBC 8.0.22 驱动升级遇到的 Bug 分析
时区数据库:
《这个重要开源项目全靠一位低调的 “怪老头” 维护!他和比尔盖茨一样撑起了计算机世界》
时区设置背后有一组大量关于全球许多代表性地点时间历史信息的代码和数据,这些代码和数据被称为时区数据库(即 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 协调员。
UTC:
闰秒终于要被取消了!
2022 年 11 月 20 日消息,负责协调世界时的国际计量局(BIPM)表示,科学家和政府代表 18 日在法国举行的一次会议上投票决定到 2035 年取消闰秒。BIPM 时间部门负责人帕特里齐亚·塔维拉表示,这项“历史性决定”将允许“秒数连续流动,而不会出现目前由不规则闰秒造成的不连续性。”
更多阅读:
正式宣布取消!能让 Linus 本人同谷歌微软达成一致的,只有它了!
让大厂抓狂的“额外一秒”:谷歌、微软、Meta 和亚马逊纷纷提议放弃
当闰秒发生时,就需要通过网络时间协议 NTP (Network time protocol) 来进行时间同步,NTP 服务器会一级一级地下发闰秒事件通知直到最边缘的 NTP 服务器,然后 NTP 服务器就会把闰秒通知发给客户端的操作系统,由操作系统来处理闰秒通知。
如果你的计算机系统没有开启 NTP 服务,那么导致的问题就是你的计算机上的机器时间就会比世界时间慢 1 秒。
如果开了 NTP 服务的话,就需要操作系统来处理这个闰秒。
时间协议:
ntpdate
》其它:
java.util.Date
存在的问题:
LocalDate
),只能以毫秒的精度表示“时间”(替代方案是 LocalDateTime
)。Date
实例:Date date = new Date(144,2,18)
java.util.Date
及 java.sql.Timestamp
API 如下:
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
包的常用类:
1 | Year // 2007 |
java.time.temporal
包的核心接口:
Interface | Description |
---|---|
TemporalAccessor |
框架级别的根接口,定义了对 temporal 对象的只读访问。 |
Temporal |
框架级别的接口,定义了对 temporal 对象的读写访问。实现类有 LocalDate 、OffsetDateTime 、Instant 等等。继承自 TemporalAccessor 。 |
TemporalAmount |
框架级别的接口,定义了一个时间段,例如”6 小时“、”8 天“、”3 个月“。实现类有 Duration 、Period 。可传入 temporal 对象的 plus 或 minus 方法进行时间调整。 |
TemporalUnit |
日期和时间的单元,ChronoUnit 枚举实现了该接口。可传入 temporal 对象的 plus 或 minus 方法进行时间调整。 |
TemporalField |
日期和时间的字段。ChronoField 枚举实现了该接口,可传入 temporal 对象的 get 或 with 方法获取或修改枚举对应的值。 |
TemporalAdjuster |
函数式接口,定义了对 temporal 对象的调整策略。可以使用 TemporalAdjusters 工具类的静态工厂方法生成对象实例,并传入temporal 对象的 with 方法进行时间调整。 |
java.time.temporal
包的核心方法:
首先了解几个概念:
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
接口,能够对日期进行建模。
1 | // 格里历(公历) |
还有其它一些常见的历法,例如:
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
是人类易读的日期和时间格式,表示一个本地时间点,基于 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:
- dates, in the Gregorian calendar (including the proleptic Gregorian calendar);
- times, based on the 24-hour timekeeping system, with optional UTC offset; time intervals; and combinations thereof.[2]
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 | public final class LocalDate |
LocalTime
的底层实现包含不可变的时、分、秒、纳秒:
1 | public final class LocalTime |
LocalDateTime
的底层实现包含不可变的 LocalDate
和 LocalTime
:
1 | public final class LocalDateTime |
方法名 | 是否静态方法 | 描述 |
---|---|---|
of |
是 | 由 Temporal 对象的某个部分创建该对象。 |
now |
是 | 依据系统时钟创建 Temporal 对象。 |
from |
是 | 依据传入的 Temporal 对象创建对象。 |
parse |
是 | 由字符串创建 Temporal 对象。 |
format |
否 | 使用某个指定的 DateTimeFormatter 将 Temporal 对象转换为字符串。 |
atOffset |
否 | 将 Temporal 对象和某个时区偏移相结合。 |
atZone |
否 | 将 Temporal 对象和某个时区相结合。 |
get |
否 | 读取 Temporal 对象的某一部分值。 |
minus |
否 | 创建 Temporal 对象的副本,通过将当前 Temporal 对象的值减去一定的时长创建该副本。 |
plus |
否 | 创建 Temporal 对象的副本,通过将当前 Temporal 对象的值加上一定的时长创建该副本。 |
with |
否 | 以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本。 |
例子:
1 | // 通过静态工厂方法创建实例 |
读取信息使用 TemporalAccessor
接口,通过传递一个 ChronoField
枚举给 get
方法读取相关信息:
ChronoField
枚举提供的值如下图:
:
1 | // 使用 TemporalAccessor#get(TemporalField),传入 TemporalField 接口的实现类 ChronoField |
创建副本使用 Temporal
接口,其提供了一组 with
/plus
/minus
方法:
1 | // 使用 Temporal#with(TemporalField, long),传入 TemporalField 接口的实现类 ChronoField |
如果 with(TemporalField, long)
方法不满足需求,可以使用更灵活的 with(TemporalAdjuster)
方法,配合 TemporalAdjusters
工具类提供的静态工厂方法,方法名非常直观:
1 | // 使用 Temporal#with(TemporalAdjuster) |
如果在 TemporalAdjusters
工具类中没有找到符合要求的预定义静态工厂方法,可以自己实现 TemporalAdjuster
函数式接口,以定制一些更复杂的时间修改操作:
作为人,我们习惯于以星期几、几号、几点、几分这样的方式理解日期和时间。毫无疑问,这种方式对于计算机而言并不容易理解。从计算机的角度来看,建模时间最自然的格式是表示一个持续时间段上某个点的单一大整型数。这也是新的 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 | public final class Instant |
可以通过 getter 方法获取这两个属性,例如:
1 | Instant instant = Instant.now(); // 1562497662814 (2019-07-07T11:07:42.814Z) |
java.time.Instant
提供了一系列静态工厂方法,用于创建实例,例如:
1 | Instant instant = Instant.now(); |
unix_timestamp → java.time.Instant
:
1 | // // 自 1970-01-01T00:00:00Z 之后经过的秒数 |
java.util.Date
→ java.time.Instant
:
1 | Instant instant = new Date().toInstant(); |
java.time.LocalDateTime
→ java.time.ZonedDateTime
→ java.time.Instant
:
1 | ZonedDateTime dt = LocalDateTime.now().atZone(zoneId); |
java.time.Instant
→ unix_timestamp:
1 | long seconds = Instant.now().getEpochSecond(); // 秒 |
java.time.Instant
→ java.util.Date
:
1 | Date date = Date.from(Instant.now()); |
1 | Instant instant = Instant.now(); // 1562497662814 (2019-07-07T11:07:42.814Z) |
但 java.time.Instant
无法处理那些人类非常容易理解的时间单位,例如下述操作将抛出异常:
1 | // java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth |
1 | Instant newInstant = instant.with(ChronoField.INSTANT_SECONDS, 30); |
Duration
和 Period
类用于保存两个 Temporal
对象之间的时间段。
Duration
的底层实现仅包含不可变的秒、纳秒:
1 | public final class Duration |
Period
的底层实现包含不可变的年、月、日:
1 | public final class Period |
1 | // Duration 类的静态工厂方法 between 仅支持两个 LocalTime 或两个 LocalDateTime、或两个 Instant 对象之间的 duration |
ChronoUnit
枚举提供的值:
DateTimeFormatter
用于日期格式化。和老的 java.util.DateFormat
相比,所有的 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 | // 2021-02-01 |
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)
andofPattern(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 | // 01/02/2021 |
使用静态工厂方法 DateTimeFormatter#ofPattern(String, Locale)
创建日期格式器:
1 | // 2021 2 1 |
注意:Pattern 的字母数量决定格式,以月份为例:
short form
.full form
. narrow form
.如果还需要更加细粒度的控制,DateTimeFormatterBuilder
类还提供了更复杂的格式器构建,你可以选择恰当的方法,一步一步地构造自己的格式器:
1 | DateTimeFormatter dtf = new DateTimeFormatterBuilder().appendPattern("dd/MM/yyyy[ [HH][:mm][:ss][.SSS]]") |
另外,DateTimeFormatterBuilder
类还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式)、填充,以及在格式器中指定可选节。
标准:
API:
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.join
和 java.util.stream.Collectors#joining(...)
的底层实现。结合 Stream API 使用如下:
1 | // [a,b,c,d,e,f,g] |
java.util.Formatter
—— C 语言 printf
风格的字符串格式化解释器。
用法:
Returns a formatted string using the specified format
string and args
.
1 | String s1 = new Formatter().format(format, args).toString(); |
Writes a formatted string to this output stream using the specified format
string and args
.
1 | System.out.printf(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 | $ printf "%s %s" hello world | hexyl |
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 entitiesJSON 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 等字符串:
例如:
1 | // <span>hello world</span> |
例如,有些 HTML 字符实体在 PDF 是不支持的,需要先转义:
1 | // flying saucer |
参考:
https://en.wikipedia.org/wiki/Escape_sequence
https://en.wikipedia.org/wiki/Escape_sequences_in_C
Apache Commons Lang 为字符串操作提供了 org.apache.commons.lang3.StringUtils
工具类,使用方式参考这里。