SimpleDateFormat源码分析及简单运用(Java时间格式化)

SimpleDateFormat源码分析及简单运用

原创博客不易,如若转载请注明来源
本文内容分为3部分:部分源码说明基本使用方式仿写 SimpleTimeFormat

随便说点

java.text.SimpleDateFormat在数据采集、数据清洗的中是一个非常常用的类。
它能做什么?将不同网站、app数据中五花八门的时间格式,统一、转化、格式化为你需要的相同的格式。存入我们的库中。

它是一个线程不安全的类。为什么?那么我们应该怎么用它?下文会一一介绍。

源码分析

  • 基本原理图
format
parse
日期字段
分隔符
日期字段
分隔符
构造方法传入pattern
编译
applyPattern方法传入pattern
存储在变量compiledPattern中
遍历compiledPattern
遍历compiledPattern
从传入的Date对象中取出对应的内容
直接使用pattern中的分隔符
根据pattern的日期类型抽取字段
跳过
  • 这里先对基本原理做一个介绍:

    大家都知道,在使用SimpleDateFormat的时候需要先传入字符串形式的Pattern

    并向外提供两类方法:

    • parse

      根据给定的pattern,从指定的字符串中解析出 java.util.Date 对象,也就是时间对象。

    • format

      按照给定的pattern,把指定的java.util.Date对象,格式化你想要的格式。

  • 然后会得到3个疑问:

    1. 代码中是怎样解析并存储传入的pattern字符串?
    2. parse的时候是怎样利用解析后pattern,解析一段字符串的?
    3. format的时候是怎么利用pattern,将一个Date对象,格式化为你想要的格式的?
  • 我们带着问题去看:

compile

这里解释问题一:怎样解析并存储 pattern?

在 SimpleDateFormat 中,会将传入的原始 pattern 和解析后的 compiledPattern,放在成员变量中。

这个 compiledPattern 在使用中 至关重要。

有两个位置可以传入原始 pattern:

  1. 构造方法。
  2. applyPattern

我们看一下这两个地方的代码:

  • 构造方法:

    可以看到,pattern 直接赋值。而compiledPattern则通过 另一个方法compile编译得到。

    public SimpleDateFormat(String pattern, Locale locale)
    {
        if (pattern == null || locale == null) {
            throw new NullPointerException();
        }
    
        initializeCalendar(locale);
        this.pattern = pattern;
        this.formatData = DateFormatSymbols.getInstanceRef(locale);
        this.locale = locale;
        initialize(locale);
    }
    
    private void initialize(Locale loc) {
      // Verify and compile the given pattern.
      compiledPattern = compile(pattern);
    
      /* try the cache first */
      numberFormat = cachedNumberFormatData.get(loc);
      if (numberFormat == null) { /* cache miss */
        numberFormat = NumberFormat.getIntegerInstance(loc);
        numberFormat.setGroupingUsed(false);
    
        /* update cache */
        cachedNumberFormatData.putIfAbsent(loc, numberFormat);
      }
      numberFormat = (NumberFormat) numberFormat.clone();
    
      initializeDefaultCentury();
    }
    
  • applyPattern

    这个方法里就更明显了,pattern直接赋值,compiledPattern 通过 compile 得到。

        public void applyPattern(String pattern)
        {
            applyPatternImpl(pattern);
        }
    
        private void applyPatternImpl(String pattern) {
            compiledPattern = compile(pattern);
            this.pattern = pattern;
        }
    

那么我们简单看一个 compile 这个方法:

代码较长,具体分析看代码中我写的注解。

    private char[] compile(String pattern) {
        int length = pattern.length();
        boolean inQuote = false;
        // 缓存buffer用来存储编译过的每一个字节
        StringBuilder compiledCode = new StringBuilder(length * 2);  
        StringBuilder tmpBuffer = null;
        int count = 0, tagcount = 0;
        int lastTag = -1, prevTag = -1;

        for (int i = 0; i < length; i++) {
            char c = pattern.charAt(i);
			// 在引号中的特殊字符处理,例如 'mm'
            if (c == '\'') {
                // '' is treated as a single quote regardless of being
                // in a quoted section.
                if ((i + 1) < length) {
                    c = pattern.charAt(i + 1);
                    if (c == '\'') {
                        i++;
                        if (count != 0) {
                            encode(lastTag, count, compiledCode);
                            tagcount++;
                            prevTag = lastTag;
                            lastTag = -1;
                            count = 0;
                        }
                        if (inQuote) {
                            tmpBuffer.append(c);
                        } else {
                            compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | c));
                        }
                        continue;
                    }
                }
                if (!inQuote) {
                    if (count != 0) {
                        encode(lastTag, count, compiledCode);
                        tagcount++;
                        prevTag = lastTag;
                        lastTag = -1;
                        count = 0;
                    }
                    if (tmpBuffer == null) {
                        tmpBuffer = new StringBuilder(length);
                    } else {
                        tmpBuffer.setLength(0);
                    }
                    inQuote = true;
                } else {
                    int len = tmpBuffer.length();
                    if (len == 1) {
                        char ch = tmpBuffer.charAt(0);
                        if (ch < 128) {
                            compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | ch));
                        } else {
                            compiledCode.append((char)(TAG_QUOTE_CHARS << 8 | 1));
                            compiledCode.append(ch);
                        }
                    } else {
                        encode(TAG_QUOTE_CHARS, len, compiledCode);
                        compiledCode.append(tmpBuffer);
                    }
                    inQuote = false;
                }
                continue;
            }
            if (inQuote) {
                tmpBuffer.append(c);
                continue;
            }
            // 分隔符处理,非ymhs等字符
            if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
                if (count != 0) {
                    encode(lastTag, count, compiledCode);
                    tagcount++;
                    prevTag = lastTag;
                    lastTag = -1;
                    count = 0;
                }
                if (c < 128) {
                    // ascii 字符处理
                    // In most cases, c would be a delimiter, such as ':'.
                    compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | c));
                } else {
                    // 非 ascii 处理,存储字符长度,非ascii字符拼接在后面
                    // Take any contiguous non-ASCII alphabet characters and
                    // put them in a single TAG_QUOTE_CHARS.
                    int j;
                    for (j = i + 1; j < length; j++) {
                        char d = pattern.charAt(j);
                        if (d == '\'' || (d >= 'a' && d <= 'z' || d >= 'A' && d <= 'Z')) {
                            break;
                        }
                    }
                    compiledCode.append((char)(TAG_QUOTE_CHARS << 8 | (j - i)));
                    for (; i < j; i++) {
                        compiledCode.append(pattern.charAt(i));
                    }
                    i--;
                }
                continue;
            }
			// 日期占位符处理,例如yhms
            int tag;
            if ((tag = DateFormatSymbols.patternChars.indexOf(c)) == -1) {
                throw new IllegalArgumentException("Illegal pattern character " +
                                                   "'" + c + "'");
            }
            if (lastTag == -1 || lastTag == tag) {
                lastTag = tag;
                count++;
                continue;
            }
            encode(lastTag, count, compiledCode);
            tagcount++;
            prevTag = lastTag;
            lastTag = tag;
            count = 1;
        }

        if (inQuote) {
            throw new IllegalArgumentException("Unterminated quote");
        }

        if (count != 0) {
            encode(lastTag, count, compiledCode);
            tagcount++;
            prevTag = lastTag;
        }

        forceStandaloneForm = (tagcount == 1 && prevTag == PATTERN_MONTH);

        // Copy the compiled pattern to a char array
        int len = compiledCode.length();
        char[] r = new char[len];
        compiledCode.getChars(0, len, r, 0);
        return r;
    }

emmmmm 注释看完了,就是这么解析的。

format

这里解释问题三:format 是怎么 format 的?

具体看代码中的注释。

private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // 日期缓存在Calendar对象中,供下文使用
    	// Convert input date to time field list
        calendar.setTime(date);

        boolean useDateFormatSymbols = useDateFormatSymbols();
		// 遍历 compiledPattern
        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;  // 右移8位,得到标签数字
            int count = compiledPattern[i++] & 0xff;  // 得到附加内容,标签长度 or ascii字符 or 特殊字符长度
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }

            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                // ascii 字符直接追加
                toAppendTo.append((char)count);
                break;

            case TAG_QUOTE_CHARS:
                // 特殊字符,根据上文长度,截取、追加
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;

            default:
                // 日期占位符,取Calendar对象中的对应字段,追加
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
    }

emmm。就是这样 format的。

parse

这里解释问题二:parse 是怎么 parse 的?

关键在于 parse 方法,代码较长,具体内容看以下代码中的注视。

    // 根据 ParsePosition 记录的index,进行字符串截取。根据tag的单位,对截取的字符串进行换算
    public Date parse(String text, ParsePosition pos)
    {
        checkNegativeNumberExpression();

        int start = pos.index;
        int oldStart = start;
        int textLength = text.length();

        boolean[] ambiguousYear = {false};

        CalendarBuilder calb = new CalendarBuilder();
   		// 遍历compiledPattern
        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }

            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                // ascii 字符,index++
                if (start >= textLength || text.charAt(start) != (char)count) {
                    pos.index = oldStart;
                    pos.errorIndex = start;
                    return null;
                }
                start++;
                break;

            case TAG_QUOTE_CHARS:
                // 非ascii特殊字符,index递增,所需要增加的数值
                while (count-- > 0) {
                    if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
                        pos.index = oldStart;
                        pos.errorIndex = start;
                        return null;
                    }
                    start++;
                }
                break;

            default:
                // Peek the next pattern to determine if we need to
                // obey the number of pattern letters for
                // parsing. It's required when parsing contiguous
                // digit text (e.g., "20010704") with a pattern which
                // has no delimiters between fields, like "yyyyMMdd".
                boolean obeyCount = false;

                // In Arabic, a minus sign for a negative number is put after
                // the number. Even in another locale, a minus sign can be
                // put after a number using DateFormat.setNumberFormat().
                // If both the minus sign and the field-delimiter are '-',
                // subParse() needs to determine whether a '-' after a number
                // in the given text is a delimiter or is a minus sign for the
                // preceding number. We give subParse() a clue based on the
                // information in compiledPattern.
                boolean useFollowingMinusSignAsDelimiter = false;

                if (i < compiledPattern.length) {
                    int nextTag = compiledPattern[i] >>> 8;
                    if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||
                          nextTag == TAG_QUOTE_CHARS)) {
                        obeyCount = true;
                    }

                    if (hasFollowingMinusSign &&
                        (nextTag == TAG_QUOTE_ASCII_CHAR ||
                         nextTag == TAG_QUOTE_CHARS)) {
                        int c;
                        if (nextTag == TAG_QUOTE_ASCII_CHAR) {
                            c = compiledPattern[i] & 0xff;
                        } else {
                            c = compiledPattern[i+1];
                        }

                        if (c == minusSign) {
                            useFollowingMinusSignAsDelimiter = true;
                        }
                    }
                }
                // 根据position的index进行字符串截取。并根据tag单位换算时间
                start = subParse(text, start, tag, count, obeyCount,
                                 ambiguousYear, pos,
                                 useFollowingMinusSignAsDelimiter, calb);
                if (start < 0) {
                    pos.index = oldStart;
                    return null;
                }
            }
        }

        // At this point the fields of Calendar have been set.  Calendar
        // will fill in default values for missing fields when the time
        // is computed.

        pos.index = start;

        Date parsedDate;
        try {
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }
        // An IllegalArgumentException will be thrown by Calendar.getTime()
        // if any fields are out of range, e.g., MONTH == 17.
        catch (IllegalArgumentException e) {
            pos.errorIndex = start;
            pos.index = oldStart;
            return null;
        }

        return parsedDate;
    }

emmmmm. 看完了。就是这么parse的。

简单使用

本文来自hu-jinwen,请勿随意转载

时间占位符含义

字符含义
y
M
d
h小时(12小时制)
H小时(24小时制)
m
s
S毫秒
// 构造方法
// 无参构造方法
SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
// 构造,并且传入pattern格式
SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

// 切换 pattern
simpleDateFormat.applyPattern("yyyy-MM-dd hh:mm:ss");
// 格式化时间
String timeStr = simpleDateFormat.format(new Date());
// 解析时间
Date resultDate = simpleDateFormat.parse("2018年12月21日 18时21分00秒");

多线程环境下的使用

不难发现,SimpleDateFormat 是线程不安全的。内部将 compiledPattern 保存在成员变量中,没有做任何线程同步。且用到了 Calendar。

要么每次都 new 一个对象,效率太慢了。

要么自己做线程同步,效率更慢。

这里推荐使用ThreadLocal

在用 ThreadLocal 的时候,需要注意 ThreadLocal 容易发生的内存泄漏的问题,这里不细讲了

private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT_HOLDER = ThreadLocal.withInitial(SimpleDateFormat::new);

仿写SimpleTimeFormat

有意思的来了,

我们可以使用 SimpleDateFormat 将Date格式化想要的格式、可以从字符串中解析出Date。

但是缺少一种这样的工具,

  1. 23分12秒 转化为 1392秒

  2. 5小时4分钟23秒 转化为 5:4:23

  3. 5小时4分钟23秒 转化为毫秒时间戳。

这里仿照SimpleDateFormat的模式,写了一个SimpleTimeFormat,作为其补充。

使用方式与 SimpleDateFormat 相同,经过一段时间的使用和测试……

源代码

import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Created by hu-jinwen on 2018/12/18
 * <p>
 * 此类语法同 SimpleDateFormat
 * h = 小时
 * m = 分钟
 * s = 秒
 * S = 毫秒
 * 此类非线程安全
 */
public class SimpleTimeFormat {

    /**
     * 原始pattern
     */
    transient private String pattern;

    /**
     * 编译后的pattern
     */
    transient private char[] compiledPattern;

    /**
     * ascii字符标签
     */
    private static final int TAG_ASCII_CODE = 100;

    /**
     * 非 ascii 字符标签
     */
    private static final int TAG_QUOTE_CODE = 101;

    /**
     * numberFormat 对象
     */
    private final DecimalFormat numberFormat = new DecimalFormat("##");


    public SimpleTimeFormat() {
        this("hh:mm:ss");
    }

    public SimpleTimeFormat(String pattern) {
        if (pattern == null || pattern.length() == 0) {
            throw new NullPointerException("+++++>> The pattern must not be null!");
        }
        applyPattern(pattern);
    }

    /**
     * 将时长、时间戳(毫秒),转化为指定格式
     */
    public String format(Long duration) {
        return format(duration, new StringBuilder()).toString();
    }

    public String format(int duration) {
        return format((long) duration);
    }

    /**
     * 从字符串中解析时间戳
     */
    public Long parse(String source) {
        return parse(source, new ParsePosition(0));
    }

    /**
     * 应用指定pattern
     */
    public void applyPattern(String pattern) {
        this.compiledPattern = compile(pattern);
        this.pattern = pattern;
    }

    /**
     * 时间格式编译
     */
    private char[] compile(String pattern) {
        int length = pattern.length();
        StringBuilder compiledBuffer = new StringBuilder(length * 2);
        int count = 0;
        int lastTag = -1;

        for (int i = 0; i < length; i++) {
            char c = pattern.charAt(i);
            // 特殊字符处理(特殊字符使用引号括起来)
            if (c == '\'') {
                if (count != 0) {
                    compiledBuffer.append((char) (lastTag << 8 | count));
                    lastTag = -1;
                    count = 0;
                }
                int j;
                for (j = ++i; j < length; j++) {
                    char d = pattern.charAt(j);
                    if (d == '\'') {
                        break;
                    }
                }
                compiledBuffer.append((char) (TAG_QUOTE_CODE << 8 | (j - i)));
                for (; i < j; i++) {
                    compiledBuffer.append(pattern.charAt(i));
                }
                continue;
            }
            // 分隔符处理
            if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
                if (count != 0) {
                    compiledBuffer.append((char) (lastTag << 8 | count));
                    lastTag = -1;
                    count = 0;
                }
                if (c < 128) {
                    compiledBuffer.append((char) (TAG_ASCII_CODE << 8 | c));
                } else {
                    // 非 ascii 字符处理
                    int j;
                    for (j = i + 1; j < length; j++) {
                        char d = pattern.charAt(j);
                        if (d == '\'' || (d >= 'a' && d <= 'z' || d >= 'A' && d <= 'Z')) {
                            break;
                        }
                    }
                    compiledBuffer.append((char) (TAG_QUOTE_CODE << 8 | (j - i)));
                    for (; i < j; i++) {
                        compiledBuffer.append(pattern.charAt(i));
                    }
                    i--;
                }
                continue;
            }
            // 非分隔符处理
            int tag;
            if ((tag = TimeFormatSymBols.patternChars.indexOf(c)) == -1) {
                throw new IllegalArgumentException(
                        MessageFormat.format("Illegal pattern character -> {0}", c));
            }
            if (lastTag == -1 || lastTag == tag) {
                lastTag = tag;
                count++;
                continue;
            }
            compiledBuffer.append((char) (tag << 8 | count));
            lastTag = tag;
            count = 1;
        }

        if (count != 0) {
            compiledBuffer.append((char) (lastTag << 8 | count));
        }

        int bufLen = compiledBuffer.length();
        char[] r = new char[bufLen];
        compiledBuffer.getChars(0, bufLen, r, 0);
        return r;
    }

    /**
     * 格式化传入的毫秒时间
     */
    private StringBuilder format(Long duration, StringBuilder resultBuffer) {
        Long[] timeCache = getTimeCache(duration);

        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;

            switch (tag) {
                case TAG_ASCII_CODE:
                    resultBuffer.append((char) count);
                    break;
                case TAG_QUOTE_CODE:
                    resultBuffer.append(compiledPattern, i, count);
                    i += count;
                    break;
                default:
                    resultBuffer.append(timeCache[tag]);
                    break;
            }
        }
        return resultBuffer;
    }

    /**
     * 解析字符串时间
     */
    private Long parse(String source, ParsePosition position) {
        long result = 0L;

        if (this.pattern.contains(".")) {
            source = source.replace(".", ",");
        }

        for (int i = 0; i < compiledPattern.length; i++) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i] & 0xff;

            switch (tag) {
                case TAG_ASCII_CODE:
                    position.setIndex(position.getIndex() + 1);
                    break;
                case TAG_QUOTE_CODE:
                    position.setIndex(position.getIndex() + count);
                    i += count;
                    break;
                default:
                    // 处理数值前可能出现的空格和制表符
                    while (true) {
                        char c = source.charAt(position.getIndex());
                        if (c != ' ' && c != '\t') {
                            break;
                        }
                        position.setIndex(position.getIndex() + 1);
                    }
                    // 前瞻, 处理真实数字位数与pattern中的位数不匹配
                    while (true) {
                        int nextCharIndex = position.getIndex() + count;
                        char c = nextCharIndex >= source.length() ? 'n' : source.charAt(nextCharIndex);
                        if (!Character.isDigit(c)) {
                            if (nextCharIndex > source.length()) {
                                count--;
                                continue;
                            }
                            break;
                        }
                        count++;
                    }
                    Number number = numberFormat.parse(source.substring(0, position.getIndex() + count), position);
                    result += (TimeFormatSymBols.UNITS_MILLS[tag] * number.doubleValue());
                    break;
            }

        }
        return result;
    }

    private Long[] getTimeCache(Long duration) {
        Long[] results = new Long[TimeFormatSymBols.patternChars.length()];
        List<Long> units = new ArrayList<>();

        // 时间格式提取
        for (char c : compiledPattern) {
            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
                continue;
            }
            int tag = c >>> 8;
            if (tag < TimeFormatSymBols.patternChars.length()) {
                units.add((long) tag);
            }
        }
        Collections.sort(units);

        for (Long tag : units) {
            long value = duration / TimeFormatSymBols.UNITS_MILLS[tag.intValue()];
            duration = duration % TimeFormatSymBols.UNITS_MILLS[tag.intValue()];
            results[tag.intValue()] = value;
        }
        return results;
    }

    private static class TimeFormatSymBols {
        /**
         * 时间格式通配符
         * <p>
         * h = 小时
         * m = 分钟
         * s = 秒
         * S = 毫秒
         */
        static final String patternChars = "hmsS";

        /**
         * 单位毫秒数
         */
        static final long[] UNITS_MILLS = {60 * 60 * 1000, 60 * 1000, 1000, 1};
    }

}

测试代码

@Test
void formatTest() {
    // 初始化可通过构造方法传入指定 pattern
    SimpleTimeFormat simpleTimeFormat = new SimpleTimeFormat("hh小时mm分钟ss");
    // 根据 pattern 将指定字符串解析为时间戳
    Long timestamp = simpleTimeFormat.parse("5小时4分钟23秒");
    // 也可手动修改 pattern
    simpleTimeFormat.applyPattern("ss:mm:hh");
    // 根据 pattern 格式化时间戳
    String format = simpleTimeFormat.format(timestamp);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值