Android(Java)日期和时间处理完全解析 (下)——使用 Gson 优雅地处理日常开发中关于时间处理的问题

1,285 阅读10分钟

原创声明: 该文章为原创文章,未经博主同意严禁转载。

简介:原本关于时间处理问题笔者只是打算写具体的介绍文章的,但是有读者指出这个上篇文章与代码不够完善(因为写这系列文章的时候只是想为大家提供一个解决问题的思路,并没有想提供完成的代码,所以删减掉部分不重要的代码了)。为了让这系列文章对得起完全解析与优雅这两个主题,笔者决定再深入介绍下如何通过Gson更进一步简化时间处理的流程。

使用TypeAdapter使Gson支持对DateTime的转换

如果熟悉Gson的朋友肯定应该TypeAdapter作用,TypeAdapter是用来接管某个类(在本文中是是指DateTIme类)的序列化与反序列化过程的。下面先来看看笔者所写的DateTimeTypeAdapter类:

public class DateTimeTypeAdapter extends TypeAdapter{  
    private final DateTimeFormatter dateTimeFormatter;  
  
    public DateTimeTypeAdapter(){  
        this(ISODateTimeFormat.dateTimeNoMillis());  
    }  
  
    public DateTimeTypeAdapter(DateTimeFormatter dateTimeFormatter){  
        this.dateTimeFormatter = dateTimeFormatter;  
    }  
  
    @Override  
    public DateTime read(JsonReader in) throws IOException {  
        JsonToken peek = in.peek();  
        if (peek == JsonToken.NULL) {  
            in.nextNull();  
            return null;  
        }  
  
        return deserializeToDate(in.nextString());  
  
    }  
  
    @Override  
    public synchronized void write(JsonWriter out, DateTime value) throws IOException {  
        if (value == null){  
            out.nullValue();  
  
        }  
        String dateTimeFormatStr = value.toString(dateTimeFormatter);  
        out.value(dateTimeFormatStr);  
}  
  
    private synchronized DateTime deserializeToDate(String json) {  
        try {  
            return DateTime.parse(json,dateTimeFormatter);  
        } catch (IllegalArgumentException e) {  
            throw new JsonSyntaxException(json, e);  
        }  
  
    }  
}

我们通过这个DateTimeTypeAdapter类就能实现Gson对DateTime类的支持了,DateTimeTypeAdapter使用方式如下。

Gson gson = new GsonBuilder()  
        .registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())  
        .create();

在DateTimeTypeAdapter的实现中我们可以看到有一个叫DateTimeFormatter的类,这个类的作用和SimpleDateFormat的作用类似,都是用于处理时间的处理/显示格式的。我们可以看到,在DateTimeTypeAdapter有两个构造方法。

  • public DateTimeTypeAdapter()构造方法的作用是创建一个支持ISO8061时间格式的DateTimeTypeAdapter。
  • public DateTimeTypeAdapter(DateTimeFormatter dateTimeFormatter)构造方法的作用是创建一个你需要支持的格式的DateTimeTypeAdapter。

代码的细节我们就不详细解析了,下面先了解下DateTimeFormatter这个类。

DateTimeFormatter

这个类是Joda-Time提供的用于支持时间格式的一个类,这个类的使用方式和SimpleDateFormat类似但是它的功能更加强大,下面来简单介绍下如何使用这个类。
Joda-Time提供了一系列的工具类与方法让我们快速创建常用的DateTimeFormatter。

ISODateTimeFormat

这个类的作用是提供一系列快速创建遵循ISO 8061标准的DateTimeFormatter的工厂方法。如上面的例子就使用了ISODateTimeFormat.dateTimeNoMillis()来创建一个忽略了毫秒的DateTimeFormatter实例。至于其它方法的作用读者可以自己验证或者去阅读Joda-TIme的API文档。

DateTimeFormat

这个类也提供了一系列的工厂方法用于快速创建DateTimeFormatter实例,具体的工厂方法的作用读者有兴趣的话可以去查阅API文档。这里我们主要介绍其中一个方法:forPattern(String s)这个方法的作用是创建一个我们需要的格式的DateTimeFormatter。

使用DateTimeFormatter解析ISO 8061标准的时间

好了,当我们创建了一个DateTimeFormatter之后,我们应该如何使用呢?笔者先来演示下如何解析ISO 8061标准的时间。

DateTimeFormatter dateTimeFormatter = ISODateTimeFormat.dateTimeNoMillis();  
DateTime dateTime = DateTime.parse("2017-05-05T06:44:16Z",dateTimeFormatter);  
System.out.println(dateTime);

我们来看看输出的结果:
2017-05-05T06:44:16.000Z

或者我们可以直接使用DateTimeFormatter.parseDateTime(String s)方法来获得一个DateTime实例,这个方法的效果和DateTime.parse(String s, DateTimeFormatter format)的效果是一样的。

使用DateTimeFormatter解析自定义格式的时间

在实际的开发中,并不是所有的开发者都遵守IOS 8061标准的,我们在这里不讨论使用哪种时间格式的效果更加好,我们主要讨论的是,如何解析自定义格式的时间,如上一章中我们提到的微博回传的时间格式。微博回传的时间格式在上一章中我们已经分析过了,现在我们知道微博回传的时间的格式是:
EEE MMM dd HH:mm:ss Z yyyy
现在我们只需要创建一个支持上面这种格式的DateTimeFormatter即可,下面是代码:

DateTimeFormatter dateTimeFormatter1 = DateTimeFormat.forPattern("EEE MMM dd HH:mm:ss Z yyyy");  
DateTime dateTime1 = dateTimeFormatter1.parseDateTime("Sun Apr 16 06:00:19 +0800 2017");  
System.out.println(dateTime1);

我们运行上面这段代码输出的结果是:

java.lang.IllegalArgumentException: Invalid format: "Sun Apr 16 06:00:19 +0800 2017"

很意外,居然报错了,为什么会报错呢?这里是因为DateTime支持的时间格式是和手机的语言有关的。因为笔者手机的语言设置是中文的,所以中文格式下当然是无法识别Sun和Apr这两个单词啦。那么要怎么做才能解决这个问题呢?我们不妨试试吧Sun和Apr改成星期六和四月试试。
现在我们的代码变成了:

DateTimeFormatter dateTimeFormatter1 = DateTimeFormat.forPattern("EEE MMM dd HH:mm:ss Z yyyy");  
DateTime dateTime1 = dateTimeFormatter1.parseDateTime("星期日 四月 16 06:00:19 +0800 2017");  
System.out.println(dateTime1);

运行结果是:
2017-04-15T22:00:19.000Z
从上面的结果我们可以得出一个结论:DateTime默认的toString方法是遵守ISO 8061标准的。那么我们想让DateTime转换成我们需要的格式的字符串需要怎么做呢?很简单,我们只需要把输出代码改成:

System.out.println(dateTime1.toString(dateTimeFormatter1));

现在的输出结果是:
星期六 四月 15 22:00:19 +0000 2017

我们已经解决我们的问题了吗?并没有!上面这个输出还存在两个问题:

  1. 我们无法控制微博回传给我们的时间格式,所以传入的字符串只能是Sun Apr 16 06:00:19 +0800 2017这个形式。
  2. 上面输出的时间结果和我们输入的时间并不一致,所以上面的结果是有问题的。

我们如何解决呢?第一个问题我们经过分析得知是和系统的时间格式有关,而在Java 的API中提供了一个叫Locale的类,这个类的作用是控制语言环境。那么我们是不是通过这个Locale把DateTime的语言环境转换成美国的呢?
我们通过查阅DateTimeFormatter的源码可以发现又个这样的方法:withLocale(Locale local),如无意外,这就是设置语言环境的方法了。我们来试试效果:

DateTimeFormatter dateTimeFormatter1 = DateTimeFormat  
        .forPattern("EEE MMM dd HH:mm:ss Z yyyy")  
        .withLocale(Locale.US);  
DateTime dateTime1 = dateTimeFormatter1.parseDateTime("Sun Apr 16 06:00:19 +0800 2017");  
System.out.println(dateTime1);

运行的结果是:
Sat Apr 15 22:00:19 +0000 2017

现在我们来分析导致第二个问题的原因,一般来说,导致时间出现误差的原因很可能是因为时区的问题。我们通过输入和输出的时间差可以大概知道相差的时间是8个小时。这恰好是美国时间和中国时间的时间差,那么按照这个规律,我们只需要把时区设置为中国的时区就可以解决这个问题了。这个问题同样是通过DateTimeFormatter来解决的。我们来看看更改后生成DateTimeFormatter实例的代码:

DateTimeFormatter dateTimeFormatter1 = DateTimeFormat  
        .forPattern("EEE MMM dd HH:mm:ss Z yyyy")  
        .withLocale(Locale.US)  
        .withZone(DateTimeZone.forID("+08:00"));

我们重点看最后一行代码,看起来的意思很像是:把输出的时间增加8个小时,就是这么简单。如果需要减少8小时,我们只需要把‘+’换成‘-’就可以了。通过这系列的代码可以看到,我们通过DateTimeFormatter可以轻易的实现对时间格式的转换。
现在我们来运行看看效果:
Sun Apr 16 06:00:19 +0800 2017

到目前为止算是完全解决了我们上面提到的需求与出现的问题了,而其它更加有趣的用法,读者可以参考源码或API文档自行尝试。

DateFormatUtils工具类

因为上篇文章为了简洁易懂,把大部分关于时间转换的代码都删除掉了,造成了部分读者理解上的问题。所以这次补上完整的代码。

public class DateFormatUtils {  
  
    private static final String ONE_SECOND_AGO = "秒前";  
    private static final String ONE_MINUTE_AGO = "分钟前";  
  
    public static String format(Date date) {  
        DateTime dateTime = new DateTime(date);  
        return realFormat(DateTime.now(),dateTime);  
    }  
  
    public static String format(DateTime dateTime){  
        return realFormat(DateTime.now(),dateTime);  
    }  
  
    /**  
     * @Method: realFormat  
     * @author create by Tang  
     * @date 2017/5/5 下午3:20  
     * @Description: 实现时间转换函数  
     */  
    @SuppressLint("SimpleDateFormat")  
    private static String realFormat(DateTime nowDateTime,DateTime dateTime){  
        int seconds = Seconds.secondsBetween(dateTime,nowDateTime).getSeconds();  
        if (seconds < 60) {  
            return seconds + ONE_SECOND_AGO;  
        }  
  
        int minutes = Minutes.minutesBetween(dateTime,nowDateTime).getMinutes();  
        if (minutes < 60) {  
            return minutes + ONE_MINUTE_AGO;  
        }  
  
        int day = nowDateTime.getDayOfYear() - dateTime.getDayOfYear();  
        int year = nowDateTime.getYear() - dateTime.getYear();  
        if (year < 1 && day < 1) {  
            DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("今天 HH:mm");  
            return dateTime.toString(dateTimeFormatter);  
        }  
  
        if (year < 1 && day < 2) {  
            DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("昨天 HH:mm");  
            return dateTime.toString(dateTimeFormatter);  
        }  
        if (year < 1) {  
            DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("MM月dd日 HH:mm");  
            return dateTime.toString(dateTimeFormatter);  
        }  
  
        DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy年MM月dd日 HH:mm");  
        return dateTime.toString(dateTimeFormatter);  
    }  
}

DateFormatUtils这个工具类同时支持对Date和DateTime实现时间转换,我们也可以在代码中看到,时间格式的转换是通过DateTimeFormat这个类来完成了。至于这个类的实现原理上一章中已经解析了,这里就不多说了。

Gson与DateTimeUtils的测试

说了这么多,现在测试下上文的代码,假设现在的时间是2017年5月5日 16:46:46。

测试通过Gson解析ISO 8061格式的时间:
测试实例是:

String ISO8061_STR = "\"2017-05-05T06:44:16Z\"";

测试代码:

Gson gson = new GsonBuilder()  
        .registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())  
        .create();  
DateTime isoDateTime = gson.fromJson(ISO8061STR,DateTime.class);  
System.out.println(DateFormatUtils.format(isoDateTime));

测试结果是:
今天 06:44

通过Gson解析微博回传的时间数据:

测试实例:

private static final String WEIBO_STR1 = "\"Fri May 05 16:47:19 +0800 2017\"";  
private static final String WEIBO_STR2 = "\"Fri May 05 16:40:22 +0800 2017\"";  
private static final String WEIBO_STR3 = "\"Fri May 05 04:22:19 +0800 2017\"";  
private static final String WEIBO_STR4 = "\"Thu May 04 12:00:19 +0800 2017\"";  
private static final String WEIBO_STR5 = "\"Sun Apr 16 06:00:19 +0800 2017\"";  
private static final String WEIBO_STR6 = "\"Thu Nov 05 23:17:19 +0800 2015\"";

测试代码:

DateTimeFormatter weiboFormat =  DateTimeFormat.forPattern("EEE MMM dd HH:mm:ss Z yyyy")  
        .withLocale(Locale.US)  
                 .withZone(DateTimeZone.forID("+08:00"));  
Gson gson1 = new GsonBuilder()  
        .registerTypeAdapter(DateTime.class  
                ,new DateTimeTypeAdapter(weiboFormat))  
        .create();  
  
System.out.println(DateFormatUtils.format(gson1.fromJson(WEIBO_STR1,DateTime.class)));  
System.out.println(DateFormatUtils.format(gson1.fromJson(WEIBO_STR2,DateTime.class)));  
System.out.println(DateFormatUtils.format(gson1.fromJson(WEIBO_STR3,DateTime.class)));  
System.out.println(DateFormatUtils.format(gson1.fromJson(WEIBO_STR4,DateTime.class)));  
System.out.println(DateFormatUtils.format(gson1.fromJson(WEIBO_STR5,DateTime.class)));  
System.out.println(DateFormatUtils.format(gson1.fromJson(WEIBO_STR6,DateTime.class)));

测试结果
33秒前
7分钟前
今天 04:22
昨天 12:00
04月16日 06:00
2015年11月05日 23:17

小结:

对于时间的处理到这里就真正的结束了,上一篇由于笔者的疏忽可能会导致一些读者理解上有某些偏差。这篇的主要是作为上一篇文章的补偿而写的。希望大家看完这两篇博客后能够对时间的处理有更深一步的理解。最后还是那句话,有时候我们觉得理所当然的东西,在实现上的复杂度可能会超出你的预料的。所以在学习的过程成,更进一步对自己的成长是很有帮助的。最后附上这篇文章的源码,觉得这系列文章对你有所帮助或启发的话,可以star这篇文章的源码哦,当作是对博主的小小支持吧。
源码GitHub地址:DateFormatUtils