StringBuild
如果字符串修改操作比较频繁,应该使用StringBuild或StirngBuffer,这两个类的实现基本上是一致的,不同的是StringBuild非线程安全,而StringBuffer线程安全。
基本用法
StringBuild sb = new StringBuild("老马");
sb.append("说");
sb.append("编程");
System.out.println(sb.toString());
基本实现
与String类似,StringBuild类也封装了一个字符数组,定义如下:
char[] value;
与String不同的是,它不是final的,可以修改。另外,与String不同,字符数组中不一定所有位置都有被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:
int count;
StringBuilder继承自AbstractStringBuild,它的默认构造方法是:
public StringBuild(){
super(16);
}
调用父类的构造方法,父类对应的构造方法是:
AbstractStringBuild(int capacity){
value = new char[capacity];
}
也就是说,new StringBuild代码内部会创建一个长度为16的字符串,count的默认值为0。append方法的代码:
public AbstractStringBuild append(String str){
if(str == null) str = "null";
int len = str.length();
ensureCapacityIntenal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
append会直接复制字符到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度用count体现。具体来说,ensureCapacityInternal(count + len)会确保数组的长度足以容纳新添加的字符,str.getChars会复制新添加的字符到字符数组中,count+=len会增加实际使用的长度。
ensureCapacityInternal的代码如下:
private void ensureCapacityInternal(int minimunCapacity){
if(minimunCapacity - value.length > 0){
expandCapacity(minimunCapcity);
}
}
如果字符数组的长度小于需要的长度,则调用expandCapacity(int minimunCapacity)
进行扩展,其代码为:
void expandCapacity(int minimunCapacity){
int newCapacity = (value.length << 1) + 2;
if(newCapacity - minimunCapacity < 0){
newCapacity = minimunCapacity;
}
if(newCapacity < 0){
if(minimunCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
扩展的逻辑:分配一个足够长度的新数组,然后将原内容复制到这个新数组中,最后让内部的字符数组指向这个新数组,这个逻辑主要靠下面代码实现:
value = Arrays.copyOf(value, newCapacity);
参数minimunCapacity表示需要的最小长度,需要多少分配多少不就行了吗?不行,因为拿就跟String一样了,没append一次,都会进行一次内存分配,效率低下。这里的扩展策略是跟当前长度相关的,当前长度乘以2,再加上2,如果这个长度不够最小需要的长度,才用minimunCapacity。
比如,默认长度为16,长度不够时,会先扩展到16*2+2即34,然后扩展到34乘2+2即70,这是一种指数扩展策略。为什那么要加2?这样,在原长度为0的时也可以工作。
为什么要这么扩展呢?这是一种折中策略,以方面要减少内存分配的次数,另一方面要避免空间浪费。在不知道最终需要多长的情况下,指数扩展是一种常见的策略,广泛应用于各种内存分配相关的计算机程序中。不过,如果预先就知道需要多少,那么可以调用StringBuilder的另一个构造方法。
public StringBuilder(int capacity){}
字符串构建完后,我们来看toString
方法:
public String toString(){
return new String(value, 0, count);
}
基于内部新建了一个String。注意,这个String构造方法不会直接用value数组,而会新建一个,以保证String的不可变性。
除了append和toString方法,StringBuild还有很多其他方法,包括更多构造方法,更多append方法、插入、删除、替换、翻转、长度有关的方法。我们主要看下插入方法,在指定索引offset处插入字符串str:
public StringBuild insert(int offset, String str);
原来的字符后移,offset为0表示在开头插,为length()表示在结尾插,比如:
StringBuilder sb = new StringBuilder();
sb.append("老马说编程");
sb.insert(0, "关注");
sb.insert(sb.length(),"老马和你一起探索编程本质");
sb.insrt(7,",");
print(sb.toString()); // 关注老马说编程,老马和你一起探索编程本质
了解了用法,我们来看下insert的实现代码:
public AbstractStringBuilder insert(int offset, String str){
if(offset < 0 || offset > length()){
throw new StringIndexOutOfBoundsException(offset);
}
if(str == null){
str = "null";
}
int len = str.length();
ensureCapacityInternal(count + len);
Systen.arrayCopy(value, offset, value, offset + len, count - offset);
str.getChars(value, offset);
coutn += len;
return this;
}
这个实现思路是:在确保有足够长度时,首先将原数组中offset开始的内容向后移动n个位置,n为待插入字符的长度,然后将待插入的字符串复制进offset位置。
移动位置调用了System.arrayCopy()
方法,这是个比较常用的方法,它的声明如下:
public static native void arrayCopy(Object src, int srcPos, Object dest, int destPos, int length);
将数组src中srcPos开始的length个元素复制到数组dest中destPos处。这个方法有个有点:即src和dest是同一个数组,它也可以正确处理。比如如下代码:
int[] arr = new int[]{1, 2, 3, 4};
System.arrayCopy(arr, 1, arr, 0, 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。比如,如下代码:
String hello = "hello";
hello += "world";
print(hello);
背后,Java编译器一般会转换为:
StringBuilder hello = new StringBuilder("hello");
hello.append("world");
print(hello.toString());
既然直接使用+和+=就相当于使用StringBuilder和append,那还有必要直接使用StringBilder吗?在简单的情况下,确实没必要。不过,在稍微复杂的情况下,Java编译器可能没那么智能,它可能会生成过多的StringBuilder,尤其是在有循环的情况下,比如,如下代码:
String hello = "hello";
for(int i = 0; i < 3; i++){
hello += "world";
}
print(hello.toString());
Java编译器转换后的代码大致如下所示:
String hello = "hello";
for(int i = 0; i < 3; i++){
StringBuilder sb = new StringBuilder(hello);
sb.append("world");
hello = sb.toString();
}
print(hello);
在循环内部,每一次+=操作,都会生成一个StringBuilder。
所以,对于简单情况,可以直接使用String的+和+=,对于复杂情况,应该直接使用StringBuilder。
Arrays
数组是存储同种类型固定长度的基本数据结构,数组中的元素在内存中连续存放,可以通过索引下标直接定位数组中的元素,相比其他容器而言效率非常高。
数组操作是计算机程序中的常见操作。Java中有一个类Arrays,包含了一些对数组操作的静态方法。
用法
Arrays类中有很多方法,主要介绍toString、排序、查找、对于一些其他方法、如复制、比较、批量设置值和计算哈希值,也进行简单介绍。
toString
Arrays的toString方法可以方便的输出一个数组的字符串形式,以便查看。它有9个重载方法,包括8个基本类型数组和一个对象类型数组:
public static String toString(int[] a);
public static String toString(Object[] a);
int[] a = new int[]{1, 2, 3, 4};
String[] strArr = new String[]{"a", "b", "c", "d"};
print(Arrays.toString(a));
print(Arrays.toString(strArr));
// 如果不使用Arrays.toString(),直接输出数组自身,则输出变为 I@1224b90 Ljava.lang.String@728e84
// @ 后面表示的是内存的地址
排序
排序是一种比较常见的操作。同toString一样,对每种基本数据类型,Arrays都有sort方法(boolean除外),例如:
public static void sort(int[] a);
public static void sort(double[] a);
// 排序按照从小到大排序
int[] arr = new int[]{1,4,5,3,5,6,8,10,8,5};
print(Array.toString(Arrays.sort(arr)));
除了基本数据类型,sort还可以直接接受对象类型,但对象需要实现Comparable接口。
public static void srot(Object[] a);
public static void sort(Object[] a, int fromIndex, int toIndex);
String[] strArr = new String[]{"hello", "world", "Break", "abc"};
Arrays.sort(strArr);
print(Arrays.toString(strArr));
// Break abc hello world
“Break”之所以排在最前面,是因为大写字母的ASCII码比小写字母都小。那如果排序的时候希望忽略大小写呢?sort还有另外两个重载方法,可以接受一个比较器作为参数:
public static <T> void sort(<T> a, Comparator<? super T> c);
public static <T> void srot(<T> a, int fromIndex, int toIndex, Comparator<? super T> c);
方法中的T表示泛型,这里表示的是,这个方法可以支持所有类型对象,只要传递这个类型对应的比较器就行了。Comparator就是比较器,它是一个接口,Java7的定义是:
public interface Comparator<T>{
int compare(T o1, T o2);
boolean equals(Object obj);
}
最主要的是compare
方法,它比较两个对象,返回一个比较结果的的值,>=1表示o1>o2,<0表示o2大于o1其他情况表示两个数相等。排序是通过比较来实现的,sort方法在排序的过程中需要对对象进行比较的时候,就调用比较器的compare方法。Java 8中Comparator增加了多个静态和默认方法。
String类有一个public静态成员,表示忽略大小写的比较器:
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
我们通过这个比较器再来对上面的String数组排序:
String[] arr = {"hello", "world", "Break", "abc"};
Arrays.sort(arr, Stirng.CASE_INSENSITIVE_ORDER);
print(Arrars.toString(arr));
// 这样,大小写就忽略了,输出变为:
// abc Break hello world
为了进一步理解Comparator,我们来看下String的这个比较器的只要实现代码:
private static class CaseInsensitiveComparator implements Comparator<String>{
public int compare(String s1, String s2){
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for(int i = 0; i < min; i++){
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if(c1 != c2){
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if(c1 != c2){
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if(c1 != c2){
return c1 - c2;
}
}
}
}
return n1 - n2;
}
}
sort方法默认是按从小到大排序,如果希望按照从大到小排序呢?对于对象类型,可以指定一个不同的Comparator,可以用匿名内部类来实现Comparator,比如:
String[] arr = {"hello", "world", "Break", "abc"};
Arrays.sort(arr, new Comparator<String>(){
@Override
public int compare(String o1, String o2){
return o2.compareToIgnoreCase(o1);
}
});
print(Arrays.toString(arr));
// world hello Break abc
以上代码使用一个匿名内部类实现Comparator接口,返回o2与o1进行忽略大小写比较的结果,这样就能实现忽略大小写且按从大到小排序。
Collections类中有两个静态方法,可以返回逆序的Comparator,例如:
public static <T> Comparator<T> reverseOrder();
public static <T> Comparator<T> reverseOrder(Comparator<T> cmp);
这样,上面字符串忽略大小写逆序排序的代码可以改为:
String[] arr = {"hello", "world", "Break", "abc"};
Arrays.sort(arr, Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER));
print(Arrays.toString(arr));
传递比较器Comparator给sort方法,体现了程序设计中一种重要的思维方式。将不变和变化相分离,排序的基本步骤和算法是不变的,但按什么排序是变化的,sort方法将不变的算法设计为主体逻辑,而将变化的排序方式设计为参数,允许调用者动态指定,这也是一种常见的设计模式,称为策略模式,不同的排序方式就是不同的策略。
查找
Arrays包含很多sort对应的查找方法,可以在已排序的数组中进行二分查找。所谓二分查找就是从中间开始查找,如果小于中间元素,则在前半部分找,否则在后半部分找,每比较一次,要么找到,要么将查找的范围缩小到一半,所以查找效率非常高。
二分查找既可以针对基本类型数组,也可以针对对象数组,对对象数组,可以传递Comparator,也可以指定查找范围。比如,针对int数组:
public static int binarySearch(int[] a, int key);
public static int binarySearch(int[] a, int fromIndex, int toIndex, int key);
针对对象数组:
public static int binarySearch(Object[] a,Object key);
指定自定义比较器:
public static <T> int binarySearch(T[] a, T key, Comparator<? super T> c);
如果能找到,binarySearch返回找到的元素索引,比如:
int[] arr = {3, 5, 7, 9, 11};
print(Arrays.binarySearch(arr, 7));
输出为2。如果没找到,返回一个负数,这个负数等于-(插入点+1)。插入点表示,如果在这个位置插入没找到的元素,可以保持数组有序,比如:
int[] arr = {3, 5, 7, 9, 11};
print(Arrays.binarySearch(arr, 6));
输出为-3,表示插入点为2,如果在2这个索引位置处插入6,可以保持数组有序,即数组为:{3, 5, 6, 7, 9, 11}。
需要注意的是,binarySearch针对的必须是以排序的数组,如果指定了Comparator,需要和排序时指定的Comparator保持一致。另外,如果数组中有多个匹配的元素,则返回哪一个是不确定的。
更多用法
除了常用的toString、排序、查找,Arrays中还有复制、比较、批量设置值和计算哈希值等方法。
基于元素数组,赋值一个新数组,与toString一样,也有多种重载方式,例如:
public static long[] copyOf(long[] original, int newLength);
public static <T> T[] copyOf(T[] original, int newLength);
判断两个数组是否相同,支持基本数据类型和对象类型,如:
public static boolean equals(boolean[] a, boolean[] a2);
public static boolean equals(Object[] a, Object[] a2);
只有数组长度相同,且每个元素相同,才返回true,否者返回false。对于对象,相同是指equals返回true。
Arrays包含很多fill方法,可以给数组中的每个元素设置一个相同的值:
public static void fill(int[] a,int val);
也可以给数组中一个给定范围的每个元素设置一个相同的值:
public static void fill(int[] a, int fromIndex, int toIndex, int val);
针对数组,计算一个数组的哈希值:
public static int hashCode(int[] a){
if(a == null)
return 0;
int result = 1;
for(int element : a){
result = 31 * result + element;
}
return result;
}
和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类型的值,如下所示:
private transient long fastTime;
fasetTime表示距离纪元时的毫秒数。
Date有两个构造方法:
public Date(long date){
fastTime = date;
}
public Date(){
this(System.currentTimeMillis());
}
第一个构造方法是根据传入的毫秒数进行初始化,第二个构造方法是默认构造方法,它根据System.currentTimeMIllis()的返回值进行初始化。System.currentTimeMillis()是一个常用的方法,它返回当前距离纪元时的毫秒数。
Date中大部分方法都已经过时了,其中没有过时的主要方法有下面这些:
// 返回毫秒数
public long getTime();
// 主要就是比较内部的毫秒数是否相同
public boolean equals(Object equals);
//与其他Date比较,如果当前Date的毫秒数大于参数返回1 相等返回0 否者返回-1
public int compareTo(Date anotherDate);
// 判断是否在给定日期之前
public boolean before(Date when);
// 判断是否在给定日期之后
public boolean after(Date when);
// 哈希值与Long类似
public int hashCode();
TimeZone
TimeZone表示时区,它是一个抽象类,有静态方法用于获取实例。获取当前默认时区,代码为:
// 获取默认时区
TimeZone tz = TimeZone.getDefault();
// 获取ID
print(tz.getID()); // Asia/Shanghai
Java中有一个系统属性user.timezone,保存的就是默认时区。系统属性可以通过System.getProperty获得,如下所示:
print(System.getProperties.getProperty("user.timezone"));
系统属性可以在Java启动的时候传入参数进行更改,如:
java -Duser.timezone=Asia/Shanghai
TimeZone也有静态方法,可以获得任意给定时区的实例。例如,获取美国东部时区:
Timezone timezone = TimeZone.getTimeZone("US/Eastern");
ID除了可以是名称外,还可以是GMT形式表示的时区,如:
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也有静态方法获取默认值,如:
Locale locale = Locale.getDefault();
print(loale.toString()); // zh_CN
Calendar
Calendar类是日期和时间操作中的主要类,它表示TimeZone和Locale相关的日历信息,可以进行各种相关的运算。与Date类似,Calendar内部也有一个表示时刻的毫秒数,定义为:
protected long time;
除此之外,Calendar内部还有一个数组,表示日历中各个字段的值,定义为:
protected int fields[];
这个数组的长度为17,保存一个日期中各个字段的值,Calendar类中定义了一些静态变量,表示这些字段,主要有:
// 表示年
public final static int YEAR = 1;
// 表示月 1月是0,Calendar同样定义了常量表示各个月份的静态变量 如Calendar.JULY表示7月
public final static int MONTH;
// 表示日,每月的第一天是1
public final static int DAY_OF_MONTH;
// 表示小时 0~23
public final static int HOUR_OF_DAY;
// 表示分钟 0~59
public final static int MINUTE;
// 表示秒 0~59
public final static int SECOND;
// 表示毫秒 0~999
public final static int MILLISECOND;
// 表示星期几 周日是1,周一是2 周六是7 Calendar同样定义了常量表示各个月份的静态变量 如Calendar.SUNDAY表示周日
public final static int DAY_OF_WEEK;
Calendar是抽象类,不能直接创建对象,它提供了多个静态方法,可以获取Calendar实例,比如:
public static Calendar getInstance();
public static Calendar getInstance(TimeZone zone);
public static Calendar getInstance(Locale aLocale);
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对象可以方便地获取年月日等信息。输出当前时间的各种信息:
Calendar calendar = Calendar.getInstance();
System.out.println("calendar.get(Calendar.YEAR) = " + calendar.get(Calendar.YEAR));
System.out.println("calendar.get(Calendar.DAY_OF_YEAR) = " + calendar.get(Calendar.DAY_OF_YEAR));
System.out.println("calendar.get(Calendar.WEEK_OF_YEAR) = " + calendar.get(Calendar.WEEK_OF_YEAR));
System.out.println("calendar.get(Calendar.MONTH) = " + calendar.get(Calendar.MONTH));
System.out.println("calendar.get(Calendar.DAY_OF_MONTH) = " + calendar.get(Calendar.DAY_OF_MONTH));
System.out.println("calendar.get(Calendar.HOUR_OF_DAY) = " + calendar.get(Calendar.HOUR_OF_DAY));
System.out.println("calendar.get(Calendar.MINUTE) = " + calendar.get(Calendar.MINUTE));
System.out.println("calendar.get(Calendar.SECOND) = " + calendar.get(Calendar.SECOND));
System.out.println("calendar.get(Calendar.MILLISECOND) = " + calendar.get(Calendar.MILLISECOND));
System.out.println("calendar.get(Calendar.DAY_OF_WEEK) = " + calendar.get(Calendar.DAY_OF_WEEK));
具体输出与执行的时间和默认的TimeZone以及Locale有关,输出:
calendar.get(Calendar.YEAR) = 2020
calendar.get(Calendar.DAY_OF_YEAR) = 345
calendar.get(Calendar.WEEK_OF_YEAR) = 50
calendar.get(Calendar.MONTH) = 11
calendar.get(Calendar.DAY_OF_MONTH) = 10
calendar.get(Calendar.HOUR_OF_DAY) = 21
calendar.get(Calendar.MINUTE) = 52
calendar.get(Calendar.SECOND) = 6
calendar.get(Calendar.MILLISECOND) = 374
calendar.get(Calendar.DAY_OF_WEEK) = 5
内部,Calendar会将表示时刻的毫秒数,按照TimeZone和Local对应的年历,计算各个日历字段的值,存放在fields数组中,Calendar.get方法获取的就是fields数组对应字段的值。
Calendar支持根据Date或毫秒数设置时间:
public final void setTime(Date date);
public void setTimeMillis(long millis);
也支持根据年月日等日历字段设置时间,比如:
public final void set(int year, int month, int date);
public final void set(int year, int month int date, int hourOfDay, int minute, int second);
public void set(int field, int value);
除了直接设置,Calendar支持根据字段增加或减少时间:
public void add(int field, int amount);
amount为正数表示增加,负数表示减少。
比如,如果想设置Calendar为第二天的下午2点15,代码可以为:
// 第二天下午2点15
calendar.add(Calendar.DAY_OF_YEAR, 1);
// 24小时制
calendar.set(Calendar.HOUR_OF_DAY, 14);
calendar.set(Calendar.MINUTE, 15);
calendar.set(Calendar.MILLISECOND, 0);
calendar.set(Calendar.SECOND, 0);
// 2020-12-11 14:15:00
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会自动更新其他值,如:
Calendar calendar = Calendar.getInstance();
claendar.add(Calendar.HOUR_OF_DAY,48);
calendar.add(Calendar.MINUTE, -120);
相当于增加了46小时。
内部,根据字段设置或修改时间时,Calendar会更新fields数组对应字段的值,但一般不会立即更新其他相关字段或内部毫秒数的值,不过在获取时间或字段值的时候,Calendar会重新计算并更新相关字段。
Calendar做了一项非常繁琐的工作,根据TimeZone和Locale,在绝对时间毫秒数和日历字段之间自动进行转换,且对不同日历字段的修改进行自动同步更新。
除了add方法,Calendarhi有一个类似方法:
public void roll(int field, int amount);
与add方法的区别是,roll方法不影响时间范围更大的字段值。比如:
// Calendar now = Calendar.getInstance(TimeZone.getDefault, Locale.getDefault().toString());
// add 与 roll方法的区别
Calendar now = Calendar.getInstance(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
now.set(Calendar.HOUR_OF_DAY, 13);
now.set(Calendar.MINUTE,59);
now.add(Calendar.MINUTE, 3);
// 首先设置为13:59 然后将分钟+3,执行后calendar的时间为14:02
// 如果add改为roll即now.roll(Calendar.MINUTE, 3);
// 则执行后的calendar时间会变为13:02,在分钟字段上执行roll方法不会改变小时的值
Calendar可以方便地转换为Date或毫秒数,:
public final Date getTime();
public long getTimeInMillis();
与Date类似,Calendar之间也可以进行比较,也实现了Comparator接口,相关方法有:
public boolean equals(Object obj);
public int compareTo(Calendar anotherCalendar);
public boolean before(Object when);
public boolean after(Object when);