PS:最近开发遇到时区的一些问题,简记如下:
时区基本概念
-
为了克服时间上的混乱,全球划分为24个时区(东、西各12个时区)。规定英国(格林尼治天文台旧址)为中时区(零时区)、东1-12区,西1-12区。每个时区横跨经度15度,时间正好是1小时。最后的东、西第12区各跨经度7.5度,以东、西经180度为界。并且规定英国(格林尼治天文台旧址)为本初子午线,即零时(24时)经线。
-
每个时区的中央经线上的时间就是这个时区内统一采用的时间,称为区时,相邻两个时区的时间相差1小时。例如,中国东8区的时间总比泰国东7区的时间早1小时,而比日本东9区的时间迟1小时。因此,出国旅行的人,必须随时调整自己的手表,才能和当地时间相一致。凡向西走,每过一个时区,就要把表拨慢1小时(比如2点拨到1点);凡向东走,每过一个时区,就要把表拨快1小时(比如1点拨到2点)。
-
时间纪元epoch: 所谓的"时间纪元"就是1970-01-01 00:00:00(GMT),指的是开始的时间。
-
夏令时,又称“日光节约时制”或“夏时制”,是一种为节约能源而人为规定地方时间的制度,在这一制度实行期间所采用的统一时间称为“夏令时间”。夏令时会导致某一天多出一个小时,或者少出一个小时。冬时令时间(非夏时令时间)也称标准时间。
-
1986年至1991年,中华人民共和国在全国范围实行了六年夏令时,每年从4月中旬的第一个星期日2时整(北京时间)到9月中旬第一个星期日的凌晨2时 整(北京夏令时)。除1986年因是实行夏令时的第一年,从5月4日开始到9月14日结束外,其它年份均按规定的时段施行。1992年4月5日后不再实 行。
Date
-
主要记录当前时刻相对于epoch的毫秒数。通过getTime()方法获得,Date类的其他方法已经不建议使用。
-
toString()方法会将日期对应的TimeInMills值转换为默认时区(一般是操作系统设置的时区)的时间,例如:Mon Feb 01 15:29:19 CST 2016。 同样,toGMTString()方法会将日期对应的TimeInMills值转换为GMT零时区的时间。注意:不建议这样将日期转换为字符串,应该用SimpleDateFormate。
TimeZone
-
TimeZone represents a time zone offset, and also figures out daylight savings. 描述了一个时区对应的一些信息(如相对零时区的偏移数rawoffset,有没有使用夏时令)
-
TimeInMills是实现世界当前时刻相对January 1, 1970, 00:00:00 GMT(Greenwich Mean Time)的毫秒数(可正可负)。
-
同一个TimeInMills值在不同的时区取值也不一样,比如TimeInTills=0表示世界当前时刻相对1970-01-01 00:00:00(GMT)的毫秒数,在零时区UTC(0)为1970-01-01 00:00:00,在东八区UTC+8为1970-01-01 08:00:00
-
SimpleTimeZone is a concrete subclass of TimeZone that represents a time zone for use with a Gregorian calendar.
SimpleDateFormate
-
format:日期Date 转换为 字符串。会转换为SmipleDateFormate对象设置对应时区的字符串。
-
parse:字符串转日期函数。字符串本身没有时区概念,会转换为SmipleDateFormate对象设置时区对应的日期Date。
-
我的操作系统是"Asia/Shanghai",即GMT+8的北京时间,那么执行日期转字符串的format方法时,由于日期生成时默认是操作系统时区,因此2013-1-31 22:17:14是北京时间,那么推算到GMT时区,自然是要减8个小时的
-
而执行字符串转日期的parse方法时,由于字符串本身没有时区的概念,因此2013-1-31 22:17:14就是指GMT(UTC)时间【ps:所有字符串都看做是GMT时间】,那么当转化为日期时要加上默认时区,即"Asia/Shanghai",因此要加上8个小时。
Calendar:
Calendar不涉及到日期与字符串的转化,因此不像SimpleDateFormat那么复杂,与日期转字符串的思路类似。但是需要注意的是,设置完时区后,我们不能用calendar.getTime()来直接获取Date日期,因此此时的日期与一开始setTime时是相同值,要想获取某时区的时间,正确的做法是用calendar.get(field)方法
Java实践注意事项:
-
Java中不是每天都是标准的24个小时,可能是23,也可能是25。23小时和25小时就是夏令时、冬令时引起的。
-
日期的计算,使用Calendar提供的API,是不会出差错的,简单的new Date(long milliseconds)并不靠谱
-
来自多方协作的项目,最好使用统一的时间标准,例如系统时间,或是统一时区(如GMT)
-
CST缩写表示下面四个时区,尽量不要用基于这样缩写的时间解析(Central Standard Time (USA) UT-6:00 , Central Standard Time (Australia) UT+9:30 ,
China Standard Time UT+8:00 , Cuba Standard Time UT-4:00 )
-
涉及到夏时令的算法会复杂很多,使用时要注意。
参考资源:
-
java处理时区的注意事项 http://blog.csdn.net/wangpeng047/article/details/8560690
-
Java时区处理之夏令时,冬令时 - 美国的6个时区 http://josh-persistence.iteye.com/blog/2230341
实战代码:
package edu.ustc.java.basic.date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
/**
* 类TimeZoneTest.java的实现描述:TODO 类实现描述
*
* @author Ace Jan 25, 2016 2:06:04 PM
*/
public class TimeZoneTest {
/** 日期格式 */
public static final String FORMAT_DATE = "yyyy-MM-dd";
public static final String FORMAT_TIME = "HH:mm:ss";
public static final String FORMAT_DATETIME = "yyyy-MM-dd HH:mm:ss";
/**
* 测试主函数
* @param args
*/
public static void main(String[] args) {
testDate();
testSimpleDateFormate();
testTimeZone();
testCalendar();
testDaylightSavingTime();
testUseDST(TimeZone.getDefault());
}
public static void testDate() {
System.out.println("====> testDate()");
Date now = new Date();
System.out.println("now -date: year=" + now.getYear() + ", month=" + now.getMonth() + ", day=" + now.getDay());
System.out.println(
"now -time: hour=" + now.getHours() + ",minute=" + now.getMinutes() + ", sec=" + now.getSeconds());
System.out.println("the number of milliseconds=" + now.getTime());
System.out.println(" Default dateTime String = " + now.toString());
System.out.println(" GMT dateTime String = " + now.toGMTString());
System.out.println(" Local dateTime String = " + now.toLocaleString());
}
/**
* SimpleDateFormate类测试
*/
public static void testSimpleDateFormate() {
System.out.println("====> testSimpleDateFormate()");
SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_DATETIME);
formatter.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
Date now = new Date();
System.out.println(" now :" + formatter.format(now));
Date halfYearAgo = new Date(now.getTime() - 1000L * 3600 * 24 * 180);
System.out.println("half year ago:" + formatter.format(halfYearAgo));
SimpleDateFormat sdf = new SimpleDateFormat(FORMAT_DATETIME);
sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
SimpleDateFormat sdf2 = new SimpleDateFormat(FORMAT_DATETIME);
sdf2.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
try {
String dateStr = "1970-01-01 00:00:00";
System.out.println("Local America/New_York = " + sdf.parse(dateStr));
System.out.println(" TS = " + sdf.parse(dateStr).getTime());
System.out.println("Local Asia/Shanghai = " + sdf2.parse(dateStr));
System.out.println(" TS = " + sdf2.parse(dateStr).getTime());
System.out.println(sdf2.format(now));
} catch (ParseException e) {
e.printStackTrace();
}
try {
System.out.println("====> 同一时间经过转换,结果不同");
Date date = new Date(1359641834000L);// 2013-1-31 22:17:14
String dateStr = "2013-1-31 22:17:14";
SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT_DATETIME);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
// 字符串转日期
System.out.println("字符串转日期,转换为本地时区显示");
Date dateTmp = dateFormat.parse(dateStr);
System.out.println("本地时区显示 parse result = " + dateTmp.toString());
System.out.println(" // TimeInMills = " + dateTmp.getTime() + " ");
System.out.println(
" // rawOffset = " + (dateTmp.getTime() - date.getTime()) / 3600000 + " h (单位毫秒转换为小时)");
System.out.println("GMT时区显示 parse result = " + dateFormat.format(dateTmp));
System.out.println(" // TimeInMills = " + dateTmp.getTime());
System.out.println(
" // rawOffset = " + (dateTmp.getTime() - date.getTime()) / 3600000 + " h (单位毫秒转换为小时)");
// 日期转字符串
System.out.println("日期转字符串,转换为设置的时区显示");
String dateStrTmp = dateFormat.format(date);
System.out.println("format result = " + dateStrTmp);
} catch (ParseException e) {
e.printStackTrace();
}
}
/**
* TimeZone类使用测试
*/
public static void testTimeZone() {
System.out.println("====> testTimeZone()");
// 输出TimeZone支持的时区ID
System.out.println(Arrays.asList(TimeZone.getAvailableIDs()));
System.out.println("==== 测试0 - 基本使用测试");
// Gets the default TimeZone for this host.
System.out.println("Default TimeZone ID : " + TimeZone.getDefault().getID());
System.out.println("Default TimeZone ZoneId: " + TimeZone.getDefault().toZoneId());
System.out.println("Default TimeZone Display Name: " + TimeZone.getDefault().getDisplayName());
System.out.println("Default TimeZone getDSTSavings: " + TimeZone.getDefault().getDSTSavings());
System.out.println("Default raw offset: " + TimeZone.getDefault().getRawOffset());
// Create TimeZone by the time zone ID
System.out.println("getId : " + TimeZone.getTimeZone("Asia/Shanghai").getID());
System.out.println("toZoneId : " + TimeZone.getTimeZone("Asia/Shanghai").toZoneId());
System.out.println("toString : " + TimeZone.getTimeZone("America/New_York").toString());
// Create TimeZone by the specified custom time zone ID
System.out.println("getId : " + TimeZone.getTimeZone("GMT+0800").getID());
System.out.println("toZoneId : " + TimeZone.getTimeZone("GMT+0800").toZoneId());
System.out.println("toString : " + TimeZone.getTimeZone("GMT+0800").toString());
System.out.println("raw offset: " + TimeZone.getTimeZone("GMT+0800").getRawOffset());
System.out.println("==== 测试1 - 夏时令问题");
System.out.println("====> testSimpleTimeZone()");
int timeZoneOffset = +2;
TimeZone timeZone = new SimpleTimeZone(timeZoneOffset * 60 * 60 * 1000, "Asia/Shanghai");
System.out.println(timeZone.getID());
System.out.println(
"SimpleTimeZone is a concrete subclass of TimeZone that represents a time zone for use with a Gregorian calendar. ");
}
/**
* Calendar类使用测试
*/
public static void testCalendar() {
System.out.println("====> testCalendar()");
System.out.println("==== 测试0 - TimeInMills值的意义");
long begin = 0;
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("Europe/Pairs"));
calendar.setTimeInMillis(begin);
System.out.println("calendar TimeInMillis= " + calendar.getTimeInMillis());
System.out.println(" Local Time= " + calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH)
+ "-" + calendar.get(Calendar.DAY_OF_MONTH) + " " + calendar.get(Calendar.HOUR_OF_DAY) + ":"
+ calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND));
System.out.println(" get Time= " + calendar.getTime()); // 设置完时区后,我们不能用calendar.getTime()来直接获取Date日期
calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"));
calendar.setTimeInMillis(begin);
System.out.println("calendar TimeInMillis= " + calendar.getTimeInMillis());
System.out.println(" Local Time= " + calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH)
+ "-" + calendar.get(Calendar.DAY_OF_MONTH) + " " + calendar.get(Calendar.HOUR_OF_DAY) + ":"
+ calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND));
System.out.println(" get Time= " + calendar.getTime());// 设置完时区后,我们不能用calendar.getTime()来直接获取Date日期
System.out.println(
"Conclusion 0: TimeInMills是世界当前时刻相对January 1, 1970, 00:00:00 GMT(Greenwich Mean Time)的毫秒数(可正可负)。");
System.out.println("-----------------------------");
System.out.println("==== 测试1 - TimeInMills值相同,但在各个时区的本地时间不一样 ");
Date date = new Date();
System.out.println("long : ms = " + date.getTime());
begin = date.getTime();
calendar = Calendar.getInstance(TimeZone.getTimeZone("Europe/Pairs"));
calendar.setTimeInMillis(begin); // 这个函数很耗时
System.out.println("calendar TimeInMillis= " + calendar.getTimeInMillis());
System.out.println(" Local Time= " + calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH)
+ "-" + calendar.get(Calendar.DAY_OF_MONTH) + " " + calendar.get(Calendar.HOUR_OF_DAY) + ":"
+ calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND));
System.out.println(" get Time= " + calendar.getTime());
calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"));
calendar.setTimeInMillis(begin);
System.out.println("calendar TimeInMillis= " + calendar.getTimeInMillis());
System.out.println(" Local Time= " + calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH)
+ "-" + calendar.get(Calendar.DAY_OF_MONTH) + " " + calendar.get(Calendar.HOUR_OF_DAY) + ":"
+ calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND));
System.out.println(" get Time= " + calendar.getTime());
// 转换
calendar.setTimeZone(TimeZone.getTimeZone("Europe/Pairs"));
System.out.println("change calendar TimeZone=" + calendar.getTimeZone().getID());
System.out.println("calendar TimeInMillis= " + calendar.getTimeInMillis());
System.out.println(" Local Time= " + calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH)
+ "-" + calendar.get(Calendar.DAY_OF_MONTH) + " " + calendar.get(Calendar.HOUR_OF_DAY) + ":"
+ calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND));
System.out.println("Conclusion 1: 同一个calendar对象,TimeInMillis一样,但set不同的TimeZone,输出的本地时间不一样。");
System.out.println("-----------------------------");
System.out.println("==== 测试2 - calendar对象对日期的灵活操作 ");
calendar.add(Calendar.DAY_OF_MONTH, 2);
System.out.println(" Local Time= " + calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH)
+ "-" + calendar.get(Calendar.DAY_OF_MONTH) + " " + calendar.get(Calendar.HOUR_OF_DAY) + ":"
+ calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND));
calendar.add(Calendar.MONTH, 2);
System.out.println(" Local Time= " + calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH)
+ "-" + calendar.get(Calendar.DAY_OF_MONTH) + " " + calendar.get(Calendar.HOUR_OF_DAY) + ":"
+ calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND));
}
/**
* 夏时令测试
*/
public static void testDaylightSavingTime() {
try {
System.out.println("====> 夏时令 (Daylight Saving Time)");
SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT_DATETIME);
// dateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
String dateStr = "1986-05-06 22:17:14";
Date tmpDate = dateFormat.parse(dateStr);
System.out.println("tmpDate = " + tmpDate);
// 夏令时会导致某一天多出一个小时,或者少出一个小时。
System.out.println("==== 测试1 - 1986年5月4号0点不见了");
Date d = dateFormat.parse("1986-5-4 0:0:0");
System.out.println(d + " //中国在当天还在使用夏令时,时间被拨快了1个小时(1986年5月4号0点不见了)");
System.out.println("==== 测试2 - 预期的0点也没有了");
String sTime = "1991-04-07 00:00:00";
Date time = dateFormat.parse(sTime);
Calendar cd = Calendar.getInstance();
cd.setTime(time);
cd.add(Calendar.DATE, 7);
time = cd.getTime();
System.out.println(dateFormat.format(time));
} catch (ParseException e) {
e.printStackTrace();
}
}
/*
* 判断一个时区从1970年开始是否使用过夏时令
*/
public static void testUseDST(TimeZone timeZone) {
System.out.println("====> testUseDST 判断一个时区ID是否使用过夏时令");
System.out.println("Time Zone is : " + timeZone.getDisplayName() + " : " + timeZone.getID());
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar start = Calendar.getInstance(timeZone);
start.setTime(new Date(0)); // UTC 1970-01-01
System.out.println("start=" + df.format(start.getTime())); // will
// print:
// start=1970-01-01
// 08:00:00
Calendar end = Calendar.getInstance(timeZone);
// end.add(Calendar.YEAR, 1);
System.out.println("end=" + df.format(end.getTime()));
boolean find = false;
for (long i = start.getTimeInMillis(); i < end.getTimeInMillis(); i = start.getTimeInMillis()) {
start.add(Calendar.DATE, 1); // add one day
if ((start.getTimeInMillis() - i) % (24 * 3600 * 1000L) != 0) { // 是否能被24整除
find = true;
System.out.println("from " + df.format(new Date(i)) + " to " + df.format(start.getTime()) + " has "
+ (start.getTimeInMillis() - i) + "ms" + "[" + (start.getTimeInMillis() - i) / (3600 * 1000L)
+ "hours]");
}
}
if (!find) {
System.out.println("Every day is ok.");
}
}
}