StringBuild

如果字符串修改操作比较频繁,应该使用StringBuild或StirngBuffer,这两个类的实现基本上是一致的,不同的是StringBuild非线程安全,而StringBuffer线程安全。

基本用法
  1. StringBuild sb = new StringBuild("老马");
  2. sb.append("说");
  3. sb.append("编程");
  4. System.out.println(sb.toString());

基本实现

与String类似,StringBuild类也封装了一个字符数组,定义如下:

  1. char[] value;

与String不同的是,它不是final的,可以修改。另外,与String不同,字符数组中不一定所有位置都有被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

  1. int count;

StringBuilder继承自AbstractStringBuild,它的默认构造方法是:

  1. public StringBuild(){
  2. super(16);
  3. }

调用父类的构造方法,父类对应的构造方法是:

  1. AbstractStringBuild(int capacity){
  2. value = new char[capacity];
  3. }

也就是说,new StringBuild代码内部会创建一个长度为16的字符串,count的默认值为0。append方法的代码:

  1. public AbstractStringBuild append(String str){
  2. if(str == null) str = "null";
  3. int len = str.length();
  4. ensureCapacityIntenal(count + len);
  5. str.getChars(0, len, value, count);
  6. count += len;
  7. return this;
  8. }

append会直接复制字符到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度用count体现。具体来说,ensureCapacityInternal(count + len)会确保数组的长度足以容纳新添加的字符,str.getChars会复制新添加的字符到字符数组中,count+=len会增加实际使用的长度。

ensureCapacityInternal的代码如下:

  1. private void ensureCapacityInternal(int minimunCapacity){
  2. if(minimunCapacity - value.length > 0){
  3. expandCapacity(minimunCapcity);
  4. }
  5. }

如果字符数组的长度小于需要的长度,则调用expandCapacity(int minimunCapacity)进行扩展,其代码为:

  1. void expandCapacity(int minimunCapacity){
  2. int newCapacity = (value.length << 1) + 2;
  3. if(newCapacity - minimunCapacity < 0){
  4. newCapacity = minimunCapacity;
  5. }
  6. if(newCapacity < 0){
  7. if(minimunCapacity < 0) // overflow
  8. throw new OutOfMemoryError();
  9. newCapacity = Integer.MAX_VALUE;
  10. }
  11. value = Arrays.copyOf(value, newCapacity);
  12. }

扩展的逻辑:分配一个足够长度的新数组,然后将原内容复制到这个新数组中,最后让内部的字符数组指向这个新数组,这个逻辑主要靠下面代码实现:

  1. value = Arrays.copyOf(value, newCapacity);

参数minimunCapacity表示需要的最小长度,需要多少分配多少不就行了吗?不行,因为拿就跟String一样了,没append一次,都会进行一次内存分配,效率低下。这里的扩展策略是跟当前长度相关的,当前长度乘以2,再加上2,如果这个长度不够最小需要的长度,才用minimunCapacity。

比如,默认长度为16,长度不够时,会先扩展到16*2+2即34,然后扩展到34乘2+2即70,这是一种指数扩展策略。为什那么要加2?这样,在原长度为0的时也可以工作。

为什么要这么扩展呢?这是一种折中策略,以方面要减少内存分配的次数,另一方面要避免空间浪费。在不知道最终需要多长的情况下,指数扩展是一种常见的策略,广泛应用于各种内存分配相关的计算机程序中。不过,如果预先就知道需要多少,那么可以调用StringBuilder的另一个构造方法。

  1. public StringBuilder(int capacity){}

字符串构建完后,我们来看toString方法:

  1. public String toString(){
  2. return new String(value, 0, count);
  3. }

基于内部新建了一个String。注意,这个String构造方法不会直接用value数组,而会新建一个,以保证String的不可变性。

除了append和toString方法,StringBuild还有很多其他方法,包括更多构造方法,更多append方法、插入、删除、替换、翻转、长度有关的方法。我们主要看下插入方法,在指定索引offset处插入字符串str:

  1. public StringBuild insert(int offset, String str);

原来的字符后移,offset为0表示在开头插,为length()表示在结尾插,比如:

  1. StringBuilder sb = new StringBuilder();
  2. sb.append("老马说编程");
  3. sb.insert(0, "关注");
  4. sb.insert(sb.length(),"老马和你一起探索编程本质");
  5. sb.insrt(7,",");
  6. print(sb.toString()); // 关注老马说编程,老马和你一起探索编程本质

了解了用法,我们来看下insert的实现代码:

  1. public AbstractStringBuilder insert(int offset, String str){
  2. if(offset < 0 || offset > length()){
  3. throw new StringIndexOutOfBoundsException(offset);
  4. }
  5. if(str == null){
  6. str = "null";
  7. }
  8. int len = str.length();
  9. ensureCapacityInternal(count + len);
  10. Systen.arrayCopy(value, offset, value, offset + len, count - offset);
  11. str.getChars(value, offset);
  12. coutn += len;
  13. return this;
  14. }

这个实现思路是:在确保有足够长度时,首先将原数组中offset开始的内容向后移动n个位置,n为待插入字符的长度,然后将待插入的字符串复制进offset位置。

移动位置调用了System.arrayCopy()方法,这是个比较常用的方法,它的声明如下:

  1. public static native void arrayCopy(Object src, int srcPos, Object dest, int destPos, int length);

将数组src中srcPos开始的length个元素复制到数组dest中destPos处。这个方法有个有点:即src和dest是同一个数组,它也可以正确处理。比如如下代码:

  1. int[] arr = new int[]{1, 2, 3, 4};
  2. System.arrayCopy(arr, 1, arr, 0, 3);
  3. System.out.println(Arrays.toString(arr));// 2 3 4

arrayCopy的声明有个修饰符native,表示它的实现是通过Java本地接口实现的。Java本地接口是Java提供的一种技术,用于在Java中调用非Java实现的代码,实际上,array-copy是用c实现的。因为这个功能非常常用,而C的实现效率要远高于Java。

String的+和+=运算符

Java中,String可以直接使用+和+=运算符,这是Java编译器提供的支持,背后,Java编译器一般会生成StringBuilder,+和+=操作会转换为append。比如,如下代码:

  1. String hello = "hello";
  2. hello += "world";
  3. print(hello);

背后,Java编译器一般会转换为:

  1. StringBuilder hello = new StringBuilder("hello");
  2. hello.append("world");
  3. print(hello.toString());

既然直接使用+和+=就相当于使用StringBuilder和append,那还有必要直接使用StringBilder吗?在简单的情况下,确实没必要。不过,在稍微复杂的情况下,Java编译器可能没那么智能,它可能会生成过多的StringBuilder,尤其是在有循环的情况下,比如,如下代码:

  1. String hello = "hello";
  2. for(int i = 0; i < 3; i++){
  3. hello += "world";
  4. }
  5. print(hello.toString());

Java编译器转换后的代码大致如下所示:

  1. String hello = "hello";
  2. for(int i = 0; i < 3; i++){
  3. StringBuilder sb = new StringBuilder(hello);
  4. sb.append("world");
  5. hello = sb.toString();
  6. }
  7. print(hello);

在循环内部,每一次+=操作,都会生成一个StringBuilder。

所以,对于简单情况,可以直接使用String的+和+=,对于复杂情况,应该直接使用StringBuilder。

Arrays

数组是存储同种类型固定长度的基本数据结构,数组中的元素在内存中连续存放,可以通过索引下标直接定位数组中的元素,相比其他容器而言效率非常高。

数组操作是计算机程序中的常见操作。Java中有一个类Arrays,包含了一些对数组操作的静态方法。

用法

Arrays类中有很多方法,主要介绍toString、排序、查找、对于一些其他方法、如复制、比较、批量设置值和计算哈希值,也进行简单介绍。

toString

Arrays的toString方法可以方便的输出一个数组的字符串形式,以便查看。它有9个重载方法,包括8个基本类型数组和一个对象类型数组:

  1. public static String toString(int[] a);
  2. public static String toString(Object[] a);
  3. int[] a = new int[]{1, 2, 3, 4};
  4. String[] strArr = new String[]{"a", "b", "c", "d"};
  5. print(Arrays.toString(a));
  6. print(Arrays.toString(strArr));
  7. // 如果不使用Arrays.toString(),直接输出数组自身,则输出变为 I@1224b90 Ljava.lang.String@728e84
  8. // @ 后面表示的是内存的地址

排序

排序是一种比较常见的操作。同toString一样,对每种基本数据类型,Arrays都有sort方法(boolean除外),例如:

  1. public static void sort(int[] a);
  2. public static void sort(double[] a);
  3. // 排序按照从小到大排序
  4. int[] arr = new int[]{1,4,5,3,5,6,8,10,8,5};
  5. print(Array.toString(Arrays.sort(arr)));

除了基本数据类型,sort还可以直接接受对象类型,但对象需要实现Comparable接口。

  1. public static void srot(Object[] a);
  2. public static void sort(Object[] a, int fromIndex, int toIndex);
  3. String[] strArr = new String[]{"hello", "world", "Break", "abc"};
  4. Arrays.sort(strArr);
  5. print(Arrays.toString(strArr));
  6. // Break abc hello world

“Break”之所以排在最前面,是因为大写字母的ASCII码比小写字母都小。那如果排序的时候希望忽略大小写呢?sort还有另外两个重载方法,可以接受一个比较器作为参数:

  1. public static <T> void sort(<T> a, Comparator<? super T> c);
  2. public static <T> void srot(<T> a, int fromIndex, int toIndex, Comparator<? super T> c);

方法中的T表示泛型,这里表示的是,这个方法可以支持所有类型对象,只要传递这个类型对应的比较器就行了。Comparator就是比较器,它是一个接口,Java7的定义是:

  1. public interface Comparator<T>{
  2. int compare(T o1, T o2);
  3. boolean equals(Object obj);
  4. }

最主要的是compare方法,它比较两个对象,返回一个比较结果的的值,>=1表示o1>o2,<0表示o2大于o1其他情况表示两个数相等。排序是通过比较来实现的,sort方法在排序的过程中需要对对象进行比较的时候,就调用比较器的compare方法。Java 8中Comparator增加了多个静态和默认方法。

String类有一个public静态成员,表示忽略大小写的比较器:

  1. public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();

我们通过这个比较器再来对上面的String数组排序:

  1. String[] arr = {"hello", "world", "Break", "abc"};
  2. Arrays.sort(arr, Stirng.CASE_INSENSITIVE_ORDER);
  3. print(Arrars.toString(arr));
  4. // 这样,大小写就忽略了,输出变为:
  5. // abc Break hello world

为了进一步理解Comparator,我们来看下String的这个比较器的只要实现代码:

  1. private static class CaseInsensitiveComparator implements Comparator<String>{
  2. public int compare(String s1, String s2){
  3. int n1 = s1.length();
  4. int n2 = s2.length();
  5. int min = Math.min(n1, n2);
  6. for(int i = 0; i < min; i++){
  7. char c1 = s1.charAt(i);
  8. char c2 = s2.charAt(i);
  9. if(c1 != c2){
  10. c1 = Character.toUpperCase(c1);
  11. c2 = Character.toUpperCase(c2);
  12. if(c1 != c2){
  13. c1 = Character.toLowerCase(c1);
  14. c2 = Character.toLowerCase(c2);
  15. if(c1 != c2){
  16. return c1 - c2;
  17. }
  18. }
  19. }
  20. }
  21. return n1 - n2;
  22. }
  23. }

sort方法默认是按从小到大排序,如果希望按照从大到小排序呢?对于对象类型,可以指定一个不同的Comparator,可以用匿名内部类来实现Comparator,比如:

  1. String[] arr = {"hello", "world", "Break", "abc"};
  2. Arrays.sort(arr, new Comparator<String>(){
  3. @Override
  4. public int compare(String o1, String o2){
  5. return o2.compareToIgnoreCase(o1);
  6. }
  7. });
  8. print(Arrays.toString(arr));
  9. // world hello Break abc

以上代码使用一个匿名内部类实现Comparator接口,返回o2与o1进行忽略大小写比较的结果,这样就能实现忽略大小写且按从大到小排序。

Collections类中有两个静态方法,可以返回逆序的Comparator,例如:

  1. public static <T> Comparator<T> reverseOrder();
  2. public static <T> Comparator<T> reverseOrder(Comparator<T> cmp);

这样,上面字符串忽略大小写逆序排序的代码可以改为:

  1. String[] arr = {"hello", "world", "Break", "abc"};
  2. Arrays.sort(arr, Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER));
  3. print(Arrays.toString(arr));

传递比较器Comparator给sort方法,体现了程序设计中一种重要的思维方式。将不变和变化相分离,排序的基本步骤和算法是不变的,但按什么排序是变化的,sort方法将不变的算法设计为主体逻辑,而将变化的排序方式设计为参数,允许调用者动态指定,这也是一种常见的设计模式,称为策略模式,不同的排序方式就是不同的策略。

查找

Arrays包含很多sort对应的查找方法,可以在已排序的数组中进行二分查找。所谓二分查找就是从中间开始查找,如果小于中间元素,则在前半部分找,否则在后半部分找,每比较一次,要么找到,要么将查找的范围缩小到一半,所以查找效率非常高。

二分查找既可以针对基本类型数组,也可以针对对象数组,对对象数组,可以传递Comparator,也可以指定查找范围。比如,针对int数组:

  1. public static int binarySearch(int[] a, int key);
  2. public static int binarySearch(int[] a, int fromIndex, int toIndex, int key);

针对对象数组:

  1. public static int binarySearch(Object[] a,Object key);

指定自定义比较器:

  1. public static <T> int binarySearch(T[] a, T key, Comparator<? super T> c);

如果能找到,binarySearch返回找到的元素索引,比如:

  1. int[] arr = {3, 5, 7, 9, 11};
  2. print(Arrays.binarySearch(arr, 7));

输出为2。如果没找到,返回一个负数,这个负数等于-(插入点+1)。插入点表示,如果在这个位置插入没找到的元素,可以保持数组有序,比如:

  1. int[] arr = {3, 5, 7, 9, 11};
  2. print(Arrays.binarySearch(arr, 6));

输出为-3,表示插入点为2,如果在2这个索引位置处插入6,可以保持数组有序,即数组为:{3, 5, 6, 7, 9, 11}。

需要注意的是,binarySearch针对的必须是以排序的数组,如果指定了Comparator,需要和排序时指定的Comparator保持一致。另外,如果数组中有多个匹配的元素,则返回哪一个是不确定的。

更多用法

除了常用的toString、排序、查找,Arrays中还有复制、比较、批量设置值和计算哈希值等方法。

基于元素数组,赋值一个新数组,与toString一样,也有多种重载方式,例如:

  1. public static long[] copyOf(long[] original, int newLength);
  2. public static <T> T[] copyOf(T[] original, int newLength);

判断两个数组是否相同,支持基本数据类型和对象类型,如:

  1. public static boolean equals(boolean[] a, boolean[] a2);
  2. public static boolean equals(Object[] a, Object[] a2);

只有数组长度相同,且每个元素相同,才返回true,否者返回false。对于对象,相同是指equals返回true。

Arrays包含很多fill方法,可以给数组中的每个元素设置一个相同的值:

  1. public static void fill(int[] a,int val);

也可以给数组中一个给定范围的每个元素设置一个相同的值:

  1. public static void fill(int[] a, int fromIndex, int toIndex, int val);

针对数组,计算一个数组的哈希值:

  1. public static int hashCode(int[] a){
  2. if(a == null)
  3. return 0;
  4. int result = 1;
  5. for(int element : a){
  6. result = 31 * result + element;
  7. }
  8. return result;
  9. }

和String计算hashCode是类似的,数组中每个元素都影响到hashCode,位置不同,影响也不同,使用31一方面希望产生更分散的hash值,一方面计算效率也比较高。

Java8和Java9对Arrays类又增加了一些方法,比如将数组转换为流、并行排序、数组比较等。

多维数组

实现原理

二分查找

时间和日期

日期和时间是一个比较复杂的概念,Java 8之前的设计有一些不足,业界有一个广泛使用的第三方类库Joda-Time,Java受Joda-Time的影响,重新设计了日期和时间的API,新增了一个包java.time。

基本概念

时区

同一时刻,世界上各个地区的时间可能是不一样的,具体时间与地区有关。 全球共有24个时区,英国格林尼治是0时区,北京是东八区,也就是说格林尼治凌晨1点,背景是早上9点。0时区的时间也称GMT +0时间,GMT是格林尼治标准时间,北京时间就是GMT +8:00。

时刻和纪元时

所有计算机系统内部都用一个整数表示时刻,这个整数是距离格林尼治标准时间1970年1月1日0时0分0秒的毫秒数。

格林尼治标准时间1970年1月1日0时0分0秒也称为(Epoch Time)纪元时。

这个整数表示的是一个时刻,与时区无关,世界上各个地方都是统一时刻,但各个地区对这个时刻的解读(如年月日时分秒)可能是不一样的。

对于1970年以前的时间,使用负数表示。

年历

中国有公历和农历之分,公历和农历都是年历,不同的年历,一年有多少月,每月有多少天,甚至一天有多少小时,这些可能都是不一样的。

比如,公历有闰年,闰年2月29天,而其他年份则是28天,其他月份,有的是30天,有的是31天。农历有闰月,比如闰7月,一年就会有两个7月,一共13个月。

公历是世界上广泛采用的年历,除了公历,还有一些其他年历,比如日本也有自己的年历。Java API的设计思想是支持国际化的,支持多种年历,但没有直接支持中国的农历。

简单来说,时刻是一个绝对时间,对时刻的解读,则是相对的,与年历和时区有关。

日期和时间API

Java API中关于日期和时间,有三个主要的类。

  • Date:表示时刻,即绝对时间,与年月日无关。
  • Calendar:表示年历,Calendar是一个抽象类,其中表示公历的子类是Gregorian-Calendar。
  • DateFormat:表示格式化,能够将日期和时间与字符串进行相互转换,DateFormat也是一个抽象类,其中最常用的子类是SimpleDateFormat。

还有两个相关的类:

  • TimeZone:表示时区。
  • Local:表示国家(或地区)和语言。

Date

Date是Java API中最早引入的关于日期的类,一开始,Date也承担了关于年历的角色,但由于不能支持国际化,其中很多方法都已经过时了,被标记为了@Deprecated,不再建议使用。

Date表示时刻,内部主要是一个long类型的值,如下所示:

  1. private transient long fastTime;

fasetTime表示距离纪元时的毫秒数。

Date有两个构造方法:

  1. public Date(long date){
  2. fastTime = date;
  3. }
  4. public Date(){
  5. this(System.currentTimeMillis());
  6. }

第一个构造方法是根据传入的毫秒数进行初始化,第二个构造方法是默认构造方法,它根据System.currentTimeMIllis()的返回值进行初始化。System.currentTimeMillis()是一个常用的方法,它返回当前距离纪元时的毫秒数。

Date中大部分方法都已经过时了,其中没有过时的主要方法有下面这些:

  1. // 返回毫秒数
  2. public long getTime();
  3. // 主要就是比较内部的毫秒数是否相同
  4. public boolean equals(Object equals);
  5. //与其他Date比较,如果当前Date的毫秒数大于参数返回1 相等返回0 否者返回-1
  6. public int compareTo(Date anotherDate);
  7. // 判断是否在给定日期之前
  8. public boolean before(Date when);
  9. // 判断是否在给定日期之后
  10. public boolean after(Date when);
  11. // 哈希值与Long类似
  12. public int hashCode();

TimeZone

TimeZone表示时区,它是一个抽象类,有静态方法用于获取实例。获取当前默认时区,代码为:

  1. // 获取默认时区
  2. TimeZone tz = TimeZone.getDefault();
  3. // 获取ID
  4. print(tz.getID()); // Asia/Shanghai

Java中有一个系统属性user.timezone,保存的就是默认时区。系统属性可以通过System.getProperty获得,如下所示:

  1. print(System.getProperties.getProperty("user.timezone"));

系统属性可以在Java启动的时候传入参数进行更改,如:

  1. java -Duser.timezone=Asia/Shanghai

TimeZone也有静态方法,可以获得任意给定时区的实例。例如,获取美国东部时区:

  1. Timezone timezone = TimeZone.getTimeZone("US/Eastern");

ID除了可以是名称外,还可以是GMT形式表示的时区,如:

  1. TimeZone timezone = TimeZone.getTimeZone("GMT +08:00");

Locale

Locale表示国家(或地区)和语言,它主要有两个参数:一个是国家(或地区);另一个是语言,每个参数都有一个代码,不过国家(或地区)并不是必须的。比如,中国内地的代码是CN,中国台湾地区的代码是TW,美国的代码是US,中文语言的代码是zh,英文语言的代码是en。

Locale类中定义了一些静态变量,表示常见的Locale,比如:

  • Locale.US:表示美国英语
  • Locale.ENGLISH:表示所有英语
  • Locale.TAIWAN:表示中国台湾地区所用的中文
  • Local.CHINESE:表示所有中文
  • Local.SIMPLFIED_CHINESE:表示中国内地所用的中文

与TimeZone类似,Locale也有静态方法获取默认值,如:

  1. Locale locale = Locale.getDefault();
  2. print(loale.toString()); // zh_CN

Calendar

Calendar类是日期和时间操作中的主要类,它表示TimeZone和Locale相关的日历信息,可以进行各种相关的运算。与Date类似,Calendar内部也有一个表示时刻的毫秒数,定义为:

  1. protected long time;

除此之外,Calendar内部还有一个数组,表示日历中各个字段的值,定义为:

  1. protected int fields[];

这个数组的长度为17,保存一个日期中各个字段的值,Calendar类中定义了一些静态变量,表示这些字段,主要有:

  1. // 表示年
  2. public final static int YEAR = 1;
  3. // 表示月 1月是0,Calendar同样定义了常量表示各个月份的静态变量 如Calendar.JULY表示7月
  4. public final static int MONTH;
  5. // 表示日,每月的第一天是1
  6. public final static int DAY_OF_MONTH;
  7. // 表示小时 0~23
  8. public final static int HOUR_OF_DAY;
  9. // 表示分钟 0~59
  10. public final static int MINUTE;
  11. // 表示秒 0~59
  12. public final static int SECOND;
  13. // 表示毫秒 0~999
  14. public final static int MILLISECOND;
  15. // 表示星期几 周日是1,周一是2 周六是7 Calendar同样定义了常量表示各个月份的静态变量 如Calendar.SUNDAY表示周日
  16. public final static int DAY_OF_WEEK;

Calendar是抽象类,不能直接创建对象,它提供了多个静态方法,可以获取Calendar实例,比如:

  1. public static Calendar getInstance();
  2. public static Calendar getInstance(TimeZone zone);
  3. public static Calendar getInstance(Locale aLocale);
  4. public static Calendar getInstance(TimeZone zone, Locale aLocale);

最终调用的方法都是需要TimeZone和Locale的,如果没有,则会使用TimeZone和Locale的默认值。getInstance方法会根据TimeZone和Locale创建对应的Calendar子类对象,在中文系统中,子类一般是表示公历的GregorianCalendar。

getInstance方法封装了Calendar对象创建的细节。TimeZone和Locale不同,具体的子类可能不同,但都是Calendar。这种隐藏对象创建细节的方式,是计算机程序中一种常见的设计模式,它有一个名字,叫工厂方法 。getInstance就是一个工厂方法,它生产对象。与new Date()类似,新创建的Calendar对象表示的也是当前时间,与Date不同的是,Calendar对象可以方便地获取年月日等信息。输出当前时间的各种信息:

  1. Calendar calendar = Calendar.getInstance();
  2. System.out.println("calendar.get(Calendar.YEAR) = " + calendar.get(Calendar.YEAR));
  3. System.out.println("calendar.get(Calendar.DAY_OF_YEAR) = " + calendar.get(Calendar.DAY_OF_YEAR));
  4. System.out.println("calendar.get(Calendar.WEEK_OF_YEAR) = " + calendar.get(Calendar.WEEK_OF_YEAR));
  5. System.out.println("calendar.get(Calendar.MONTH) = " + calendar.get(Calendar.MONTH));
  6. System.out.println("calendar.get(Calendar.DAY_OF_MONTH) = " + calendar.get(Calendar.DAY_OF_MONTH));
  7. System.out.println("calendar.get(Calendar.HOUR_OF_DAY) = " + calendar.get(Calendar.HOUR_OF_DAY));
  8. System.out.println("calendar.get(Calendar.MINUTE) = " + calendar.get(Calendar.MINUTE));
  9. System.out.println("calendar.get(Calendar.SECOND) = " + calendar.get(Calendar.SECOND));
  10. System.out.println("calendar.get(Calendar.MILLISECOND) = " + calendar.get(Calendar.MILLISECOND));
  11. System.out.println("calendar.get(Calendar.DAY_OF_WEEK) = " + calendar.get(Calendar.DAY_OF_WEEK));

具体输出与执行的时间和默认的TimeZone以及Locale有关,输出:

  1. calendar.get(Calendar.YEAR) = 2020
  2. calendar.get(Calendar.DAY_OF_YEAR) = 345
  3. calendar.get(Calendar.WEEK_OF_YEAR) = 50
  4. calendar.get(Calendar.MONTH) = 11
  5. calendar.get(Calendar.DAY_OF_MONTH) = 10
  6. calendar.get(Calendar.HOUR_OF_DAY) = 21
  7. calendar.get(Calendar.MINUTE) = 52
  8. calendar.get(Calendar.SECOND) = 6
  9. calendar.get(Calendar.MILLISECOND) = 374
  10. calendar.get(Calendar.DAY_OF_WEEK) = 5

内部,Calendar会将表示时刻的毫秒数,按照TimeZone和Local对应的年历,计算各个日历字段的值,存放在fields数组中,Calendar.get方法获取的就是fields数组对应字段的值。

Calendar支持根据Date或毫秒数设置时间:

  1. public final void setTime(Date date);
  2. public void setTimeMillis(long millis);

也支持根据年月日等日历字段设置时间,比如:

  1. public final void set(int year, int month, int date);
  2. public final void set(int year, int month int date, int hourOfDay, int minute, int second);
  3. public void set(int field, int value);

除了直接设置,Calendar支持根据字段增加或减少时间:

  1. public void add(int field, int amount);

amount为正数表示增加,负数表示减少。

比如,如果想设置Calendar为第二天的下午2点15,代码可以为:

  1. // 第二天下午2点15
  2. calendar.add(Calendar.DAY_OF_YEAR, 1);
  3. // 24小时制
  4. calendar.set(Calendar.HOUR_OF_DAY, 14);
  5. calendar.set(Calendar.MINUTE, 15);
  6. calendar.set(Calendar.MILLISECOND, 0);
  7. calendar.set(Calendar.SECOND, 0);
  8. // 2020-12-11 14:15:00
  9. System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime()));

Calendar的这些方法中一个比较方便和强大的地方在于,它能够自动调整相关的字段。比如,我们知道2月最多有29天,如果当前时间为1月30号,对Calendar.MONTH字段加1,即增加一月,Calendar不是简单的只对月字段加1,那样日期是2月30号,是无效的,Calendar会自动调整为2月最后天,即2月28号或29号。

再如,设置的值可以超出其字段最大范围,Calendar会自动更新其他值,如:

  1. Calendar calendar = Calendar.getInstance();
  2. claendar.add(Calendar.HOUR_OF_DAY,48);
  3. calendar.add(Calendar.MINUTE, -120);

相当于增加了46小时。

内部,根据字段设置或修改时间时,Calendar会更新fields数组对应字段的值,但一般不会立即更新其他相关字段或内部毫秒数的值,不过在获取时间或字段值的时候,Calendar会重新计算并更新相关字段。

Calendar做了一项非常繁琐的工作,根据TimeZone和Locale,在绝对时间毫秒数和日历字段之间自动进行转换,且对不同日历字段的修改进行自动同步更新。

除了add方法,Calendarhi有一个类似方法:

  1. public void roll(int field, int amount);

与add方法的区别是,roll方法不影响时间范围更大的字段值。比如:

  1. // Calendar now = Calendar.getInstance(TimeZone.getDefault, Locale.getDefault().toString());
  2. // add 与 roll方法的区别
  3. Calendar now = Calendar.getInstance(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
  4. now.set(Calendar.HOUR_OF_DAY, 13);
  5. now.set(Calendar.MINUTE,59);
  6. now.add(Calendar.MINUTE, 3);
  7. // 首先设置为13:59 然后将分钟+3,执行后calendar的时间为14:02
  8. // 如果add改为roll即now.roll(Calendar.MINUTE, 3);
  9. // 则执行后的calendar时间会变为13:02,在分钟字段上执行roll方法不会改变小时的值

Calendar可以方便地转换为Date或毫秒数,:

  1. public final Date getTime();
  2. public long getTimeInMillis();

与Date类似,Calendar之间也可以进行比较,也实现了Comparator接口,相关方法有:

  1. public boolean equals(Object obj);
  2. public int compareTo(Calendar anotherCalendar);
  3. public boolean before(Object when);
  4. public boolean after(Object when);

DateFormat

局限性

随机

Math.random

Random

随机的基本原理

随机密码

洗牌

带权重的随机

抢红包算法

北京购车摇号算法

小结