JDK源码剖析之java.util.Calendar

点击查看JDK源码剖析系列


目录

一、Calendar介绍

Calendar类是一个日历抽象类,提供了一组对年月日时分秒星期等日期信息的操作的函数,并针对不同国家和地区的日历提供了相应的子类,即本地化。比如公历GregorianCalendar,佛历BuddhistCalendar,日本帝国历JapaneseImperialCalendar等。从JDK1.1版本开始,在处理日期和时间时,系统推荐使用Calendar类进行实现(Date的一些方法都过时了)。在设计上,Calendar类的功能要比Date类强大很多,而且在实现方式上也比Date类要复杂一些,下面就介绍一下Calendar类的使用。

二、获取实例方式

1.getInstance()

Calendar类和Date类不同,它是一个抽象类,如下:

public abstract class Calendar implements Serializable,Cloneable,Comparable<Calendar> 

因此,我们并不能通过new Calendar()的方式创建Calendar的实例。但是该类提供了getInstance方法让我们获取实例(设计模式-工厂模式),如:

Calendar cal = Calendar.getInstance();

这里的getInstance方法有四种重载,具体如下:

(1)getInstance()

public static Calendar getInstance(){ 
    return createCalendar(TimeZone.getDefault(),Locale.getDefault(Locale.Category.FORMAT)); 
} 

(2)getInstance(TimeZone zone)

  public static Calendar getInstance(TimeZone zone){ 
      return createCalendar(zone,Locale.getDefault(Locale.Category.FORMAT)); 
  } 

(3)getInstance(Locale aLocale)

public static Calendar getInstance(Locale aLocale){ 
    return createCalendar(TimeZone.getDefault(),aLocale); 
} 

(4)getInstance(TimeZone zone,Locale aLocale)

public static Calendar getInstance(TimeZone zone,Locale aLocale){ 
    return createCalendar(zone,aLocale); 
}

可以看到每种getInstance方法都是通过createCalendar方法创建了 Calendar类型对象并返回给我们。 这里createCalendar方法需要两个参数,分别是TimeZone时区类型和Locale地区类型。 如果getInstance方法传入了相应类型的参数(具体参考java.util.Timezonejava.util.Locale这两个类),那么参数会继续传递给createCalendar;如果没有某个对应的参数,那么程序会自动获取机器的时区或者地区。createCalendar方法内部实现如下:

private static Calendar createCalendar(TimeZone zone, Locale aLocale){
    CalendarProvider provider = LocaleProviderAdapter
        .getAdapter(CalendarProvider.class, aLocale)
        .getCalendarProvider();
    if (provider != null) {
        try {
            return provider.getInstance(zone, aLocale);
        } catch (IllegalArgumentException iae) {
            // fall back to the default instantiation
        }
    }

    Calendar cal = null;

    if (aLocale.hasExtensions()) {
        String caltype = aLocale.getUnicodeLocaleType("ca");
        if (caltype != null) {
            switch (caltype) {
            case "buddhist":
            cal = new BuddhistCalendar(zone, aLocale);
                break;
            case "japanese":
                cal = new JapaneseImperialCalendar(zone, aLocale);
                break;
            case "gregory":
                cal = new GregorianCalendar(zone, aLocale);
                break;
            }
        }
    }
    if (cal == null) {
        // If no known calendar type is explicitly specified,
        // perform the traditional way to create a Calendar:
        // create a BuddhistCalendar for th_TH locale,
        // a JapaneseImperialCalendar for ja_JP_JP locale, or
        // a GregorianCalendar for any other locales.
        // NOTE: The language, country and variant strings are interned.
        if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
            cal = new BuddhistCalendar(zone, aLocale);
        } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
                   && aLocale.getCountry() == "JP") {
            cal = new JapaneseImperialCalendar(zone, aLocale);
        } else {
            cal = new GregorianCalendar(zone, aLocale);
        }
    }
    return cal;
}

这个方法内部非常的长,还用到了其他类的API。简而言之,程序会通过TimeZone(时区)和Locale(地区)来返回不同的Calendar子类对象。其实就是三种,如果是泰国地区就返回BuddhistCalendar(佛历),日本地区就返回JapaneseImperialCalendar(日本帝国历),其他国家和地区就返回GregorianCalendar(格里高利历,即公历)。 P.S. JDK居然专门为泰国和日本做了符合当地民俗的日历,怎么没做中国的阴历呢。。。

2.通过new子类来获取实例

除此之外,还有一个能够实例化的方式——用子类来实例化。Calendar的子类有GregorianCalendar,BuddhistCalendar还有JapaneseImperialCalendar等,但由于JapaneseImperialCalendar这个类的修饰符不是public(真奇怪的设计),所以我们不能访问,因此只有另外俩可用。具体实例化方式如下:

Calendar calendar = new GregorianCalendar(); 
Calendar calendar = new BuddhistCalendar();  

三、常用函数

1.get(int field)

用来获取实例化的Calendar对象储存的年月日时分秒星期等等信息,complete方法主要是对所有日期字段的值进行计算并赋值给相应变量。方法的参数通过Calendar.XXXX的形式填写,比如要想获取年份信息就用Calendar.YEAR、月份Calendar.MONTH、日期Calendar.Date、时Calendar.HOUR、分Calendar.MINUTE、秒Calendar.SECOND等等。get方法内部细节如下:

/**
 * 返回给定日历字段的值。 在宽松模式下,
 * 所有日历字段都已标准化。 在非宽松模式下,全部
 * 日历字段进行验证,并且此方法将引发一个
 * 如果任何日历字段超出范围值,则为例外。该
 * 规范化和验证由处理
 * @param field 给定的日历字段。
 * @return 给定日历字段的值。
 * 如果指定的字段超出范围,则抛出ArrayIndexOutOfBoundsException
 */
public int get(int filed){ 
     complete(); 
     return internalGet(field); 
} 

其中complete()方法是用来自动设置Calendar对象中的任何未设置的年月日时分秒星期等字段。这里不再继续往下挖。
再来看internalGet方法,用来返回需要获取的值,方法内部实现如下:

protected final int internalGet(int field){
    return fields[field];
}

Calendar对象的时间信息如年月日时分秒星期等都是储存在名为fields的int数组里的,internalGet其实就是通过Calendar.XXXX作为下标来访问数组。

2.set

set方法有四种重载,分别如下:

public void set(int field, int value){
    // If the fields are partially normalized, calculate all the
    // fields before changing any fields.
    if (areFieldsSet && !areAllFieldsSet) {
        computeFields();
    }
    internalSet(field, value);
    isTimeSet = false;
    areFieldsSet = false;
    isSet[field] = true;
    stamp[field] = nextStamp++;
    if (nextStamp == Integer.MAX_VALUE) {
        adjustStamp();
    } 
}
public final void set(int year, int month, int date){
    set(YEAR, year);
    set(MONTH, month);
    set(DATE, date);
}
public final void set(int year, int month, int date, int hourOfDay, int minute){
    set(YEAR, year);
    set(MONTH, month);
    set(DATE, date);
    set(HOUR_OF_DAY, hourOfDay);
    set(MINUTE, minute);
}
public final void set(int year, int month, int date, int hourOfDay, int minute,int second){
    set(YEAR, year);
    set(MONTH, month);
    set(DATE, date);
    set(HOUR_OF_DAY, hourOfDay);
    set(MINUTE, minute);
    set(SECOND, second); 
}

可以看出后三种重载其实都是对第一种set(int field,int value)进行的简单调用,核心逻辑都在第一种里面。因此我们只看第一种。
首先看这一段:

if (areFieldsSet && !areAllFieldsSet) {
    computeFields();
}

其中areFieldsSet是用来标记日期字段是否和当前设置的时间保持同步,areAllFieldsSet用来标记是否所有的时间字段都被设置了。computeFields()是一个抽象方法,由Calendar的子类实现,是用来将当前毫秒时间值转换为特定种类的Calendar的字段值。
这一段整体意义就是,如果日期字段有且仅有部分被规范化了,那么在更改任何字段前先计算所有字段。
再看下一行:

internalSet(field, value);

其实前面已经提到了Calendar对象的时间信息如年月日时分秒星期等都是储存在名为fields的int数组里的,前面的internalGet通过Calendar.XXXX作为下标来访问数组,这里的internalSet对应地向数组中存数据。
后面的代码:

isTimeSet = false;
areFieldsSet = false;
isSet[field] = true;
stamp[field] = nextStamp++;
if (nextStamp == Integer.MAX_VALUE) {
    adjustStamp();
}

这部分是对该日期对象的一些状态进行设置。

3.getTime()

/**
 * 返回一个Date对象
 * <code>Calendar</code>'s time value (millisecond offset from the <a
 * href="#Epoch">Epoch</a>").
 *
 * @return a <code>Date</code> representing the time value.
 * @see #setTime(Date)
 * @see #getTimeInMillis()
 */
public final Date getTime() {
    return new Date(getTimeInMillis());
}

利用该Calendar对象生成一个Date对象,调用了Date(long mills)构造方法。
其中getTimeInMills()是获取该Calendar对象保存的时间距1970.1.1的毫秒数。

4.after(Object when)

public boolean after(Object when) {
    return when instanceof Calendar
        && compareTo((Calendar)when) > 0;
}

功能与Date类的after方法一致,测试该Calendar对象代表的时间是否比传入时间晚。
先判断传入的Object对象when是否属于Calendar类型,不属于直接返回false (为啥不直接设计成参数类型为Calendar?),否则调用compareTo方法比较时间的早晚。compareTo内部如下:

public int compareTo(Calendar anotherCalendar) {
    return compareTo(getMillisOf(anotherCalendar));
}

内部调用了另一种重载的compareTo方法:

private int compareTo(long t) {
    long thisTime = getMillisOf(this);
    return (thisTime > t) ? 1 : (thisTime == t) ? 0 : -1;
}

可以发现实质上还是比较谁的时间距离1970.1.1的毫秒数更大。更大的时间肯定更晚。

4.before(Object when)

public boolean before(Object when) {
    return when instanceof Calendar
        && compareTo((Calendar)when) < 0;
}

只是功能和逻辑和after(Object when)反过来,不做赘述。

猜你喜欢

转载自blog.csdn.net/qq_33829547/article/details/80242596