你真的懂计算机中的时间吗
时间是大家自从出生就开始接触的东西,但是你真的足够了解时间、时区还有计算机中的时间吗?
时间和时区
在介绍计算时间之前先简单介绍下有关时间和时区的基础知识。“北京时间2021年11月25日18点11分50秒”,这个是当前时间,这个时间是包含了日期和时区的,“北京时间”是时区,“2021年11月25日18点11分50秒”这个是时间。正式的时区划分包括24个时区,每一时区由一个英文字母表示。每隔经度15°划分一个时区,有一个例外,每个时区有一条中央子午线,这个最早是在1881年10月1日,国际子午线会议上确定的,和时区相关的名词有三个:GMT、UT、UTC:
•GMT的意思是格林尼治标准时间(旧译格林威治平均时间或格林威治标准时间;英语:Greenwich Mean Time,GMT)是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线,除了GMT还有UTC、UT;•UTC叫协调世界时。因为地球自转越来越慢,每年都会比前一年多出零点几秒,每隔几年协调世界时组织都会给世界时+1秒,让基于原子钟的世界时和基于天文学(人类感知)的格林尼治标准时间相差不至于太大。并将得到的时间称为UTC,这是现在使用的世界标准时间。•UT是Universal Time 世界时。根据原子钟计算出来的时间。
协调世界时不与任何地区位置相关,也不代表此刻某地的时间,所以在说明某地时间时要加上时区,也就是说GMT并不等于UTC,而是等于UTC+0,只是格林尼治刚好在0时区上。GMT = UTC+0
我们国家是在东八区,一般叫北京时间,标准上叫“GMT+8:00”或者“UTC+8”,这是全国统一的时区,在新中国成立之前我们国家一共有五个时区:中原时区、陇蜀时区、新藏时区、昆仑时区、长白时区。统一成一个时区应该也是为了工作生活的方便,但是我们国家实际上也是横跨了时区的,比如新疆晚上9点天还没黑但是成都晚上九点天都黑透了。与之对应的美国就是横跨四个时区,所以美国有四个时区:东部时间(EST)(西五区时间)、中部时间(CST)(西六区时间)、山地时间(MST)(西七区时间)、太平洋时间(西部时间)(PST)(西八区时间)。还有两个特殊的时区:[阿拉斯加时间(AKST)(西九区时间)和夏威夷时间(HST)(西十区时间)。
和时区有关的概念介绍完了,再简单介绍下几个和时间有关系的名词:
•公历公元,即公历纪年法,是一种源自于西方社会的纪年方法。原称基督纪元,又称西厉或者西元),是由意大利医生兼哲学家Aloysius Lilius对儒略历加以改革而制成的一种历法——《格里历》。1582年,时任罗马教皇的格列高利十三世予以批准颁行(所以周杰伦的歌《爱在西元前》的意思是在1582年之前的爱)。1949年9月27日,经过中国人民政治协商会议第一届全体会议通过,新成立的中华人民共和国使用国际社会上大多数国家通用的公历和公元作为历法与纪年•夏令时,表示为了节约能源,人为规定时间的意思。也叫夏时制,夏令时(Daylight Saving Time:DST),又称“日光节约时制”和“夏令时间”,在这一制度实行期间所采用的统一时间称为“夏令时间”。一般在天亮早的夏季人为将时间调快一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。各个采纳夏时制的国家具体规定不同。全世界有近110个国家每年要实行夏令时,我们国家1992年正式废除了夏令时,与之对应的就是冬令时,总之夏令时就是拨快一个小时冬令时就是拨慢一小时•闰秒闰秒,是指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中(也可能在季末)对协调世界时增加或减少1秒的调整。由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),会使世界时(民用时)和原子时之间相差超过到±0.9秒时,就把协调世界时向前拨1秒(负闰秒,最后一分钟为59秒)或向后拨1秒(正闰秒,最后一分钟为61秒);闰秒一般加在公历年末或公历六月末。•ISO 8601,国际标准化组织的国际标准ISO 8601是日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》。最新为第三版ISO8601:2004,第一版为ISO8601:1988,第二版为ISO8601:2000。这个太长了就自己去看吧:https://baike.baidu.com/item/ISO%208601/3910715?fr=aladdin;总之这个就是国际上如何表示时间的国际标准
计算机中的时间
1946年2月14日,人类历史上公认的第一台现代电子计算机“埃尼阿克”(ENIAC)诞生。在计算机里“2021-11-25 18:11:11”、“Thu Nov 25 18:45:26 CST 2021”、“1637837049”,以上三个都可以表示为时间。前两个还好理解,那么第三个是啥?实际上第三个才是真的计算机上的时间;第三个叫unix时间戳,unix时间戳是指格林威治时间自1970年1月1日(00:00:00 GMT)至当前时间的总秒数。它也被称为Unix时间戳。1970年1月1日叫”UNIX TIME的纪元时间“,那么为什么是1970年1月1日呢?因为最初计算机操作系统是32位,而时间也是用32位表示。而32位能表示的最大值是2147483647。另外1年365天的总秒数是31536000。2147483647/31536000 = 68.1,也就是说32位能表示的最长时间是68年,而实际上到2038年01月19日03时14分07秒,便会到达最大时间,过了这个时间点,所有的32位操作系统时间便会变为10000000 00000000 00000000 00000000,也就是1901年12月13日20时45分52秒,这样便会出现时间回归的现象,很多软件便会运行异常了。因为用32位来表示时间的最大间隔是68年,而最早出现的UNIX操作系统考虑到计算机产生的年代和应用的实现综合取了1970年1月1日作为UNIX TIME的纪元时间(开始时间),而java自然也遵循了这一约束。时间回归的现象相信随着64位操作系统的产生逐渐得到解决,因为64位操作系统可以表示到292,277,026,596年12月4日15时30分08秒,相信我们的N代子孙,哪怕地球毁灭那天都不用愁不够用了,因为这个时间已经是千亿年以后了。
对了计算机时间还有Y2K问题也就是千年虫,简单来说千年虫问题的起因就是因为“bug之母”——Grace Murray Hopper(为什么她叫bug之母呢?因为她在一次工作中解决了由一只飞蛾造成的继电器短路问题,事后她将这只蛾子的尸体镶在了记事本上,并声明这次事故是因为一个“bug”(英语有小虫子的意思)。而这个bug就是计算机历史上的第一个bug,Grace因此也被称为“bug之母),发明了六位数字来储存时间,这一方法在后来被延续了下来,其表现形式就类似“95.11.02”,意思就是1995年11月2号,这是因为当时的内存可是一个精贵的东西,数据处理还需要穿孔卡片来完成,所以为了进一步的节省空间,采用最简单的方法来储存看似不太重要的时间数据。但是这样有个很大的问题,就是如果到了2000年的话,那么就是00.11.02了,但是计算机并不知道这个00是2000还是1900还是1800,这就意味着你在1999年12月31日23:59分打了三分钟的电话,电话局的账单却可能显示为(-100年+3分钟)因为他并不知道00是多少年。
Java中和时间
说了时间、时区的概念和计算机中的时间再来介绍下java中的时间api。Java中的时间相关的api要分成两个时代,jdk8之前和之后。
先看看jdk8之前:
jdk1.0开始就有了处理时间日期的类——Date(所以Date类是jdk的元老啊,以后说话要注意点了)在这个时候,Date既要承载日期信息,又要做日期之间的转换,还要做不同日期格式的显示,职责较繁杂,于是在jdk1.1中有了新的成员:
•使用Calendar类实现日期和时间字段之间转换;•使用DateFormat类来格式化和分析日期字符串;•而Date只用来承载日期和时间信息。
这些类知道jdk8之前都是java官方的处理时间的类,相信大家也不会陌生。怎么使用的就不详细说了,就说说他几个坑爹的地方:
1、以下代码:
Date date = new Date(2021,11,25);
System.out.println(date);
输出结果是:Sun Dec 25 00:00:00 CST 3921
3921年12月25日,咿,这个好像和我输入的不对啊。好了这里有人会说Date的这个构造方法已经被弃用了,应该用Calendar,好了我们换成Calendar试试:
Calendar calendar = Calendar.getInstance();
calendar.set(2021, 11, 25);
System.out.println(calendar.getTime());
结果是:Sat Dec 25 20:09:37 CST 2021
年份对了,月份还是21月。原因是Date的年份需要传递的是你要的年份减去1900的数字,月份和Calendar一样都是从0开始,是不是很崩溃?如果你不知道这个坑的话,那麻烦就大了。
2、java.util.Date与java.util.Calendar中的所有属性都是可变的,以下代码,计算两个日期之间的天数:
public static void main(String[] args) {
Calendar birth = Calendar.getInstance();
birth.set(1975, Calendar.MAY, 26);
Calendar now = Calendar.getInstance();
System.out.println(daysBetween(birth, now));
System.out.println(daysBetween(birth, now));
}
public static long daysBetween(Calendar begin, Calendar end) {
long daysBetween = 0;
while(begin.before(end)) {
begin.add(Calendar.DAY_OF_MONTH, 1);
daysBetween++;
}
return daysBetween;
}
daysBetween有点问题,如果连续计算两个Date实例的话,第二次会取得0,因为Calendar状态是可变的,考虑到重复计算的场合,最好复制一个新的Calendar:
public static long daysBetween(Calendar begin, Calendar end) {
Calendar calendar = (Calendar) begin.clone();
long daysBetween = 0;
while(calendar.before(end)) {
calendar.add(Calendar.DAY_OF_MONTH, 1);
daysBetween++;
}
return daysBetween;
}
3、SimpleDateFormat
是线程不安全的,原因其实就是2造成的::在多线程环境下,当多个线程同时使用相同的SimpleDateFormat对象(如static修饰)的话,如调用format方法时,多个线程会同时调用calender.setTime方法,导致time被别的线程修改,因此线程是不安全的。比如:
public class SimpleDateFormatTest {
private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000), new MyThreadFactory("SimpleDateFormatTest"));
public void test() {
while (true) {
poolExecutor.execute(new Runnable() {
@Override
public void run() {
String dateString = simpleDateFormat.format(new Date());
try {
Date parseDate = simpleDateFormat.parse(dateString);
String dateString2 = simpleDateFormat.format(parseDate);
System.out.println(dateString.equals(dateString2));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
}
}
输出结果是:
true
false
true
true
false
出现了false,说明线程不安全。
对了说个有意思的,在jdk8以前,以下代码:
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
String str3 = "1927-12-31 23:54:07";
String str4 = "1927-12-31 23:54:08";
Date sDt3 = sf.parse(str3);
Date sDt4 = sf.parse(str4);
long ld3 = sDt3.getTime() /1000;
long ld4 = sDt4.getTime() /1000;
System.out.println(ld4-ld3);
打印的结果不是1,是353,也就是说1927年12月13日晚上十一点,在中国发生了一次时间回退(查不到为啥,应该是和近代历史有关系),当然了jdk8开始输出就是1了。
综上所述jdk自带的时间类,真的很难用很难用,Tiago Fernandez做过一次投票,选举最烂的JAVA API,排第一的EJB2.X,第二的就是日期API,所以有了很多第三方的时间处理包,比如大名鼎鼎的joda。这一切在jdk8之后都解决了或者说是在实现了JSR310之后都解决了,JSR310,地址是:https://jcp.org/en/jsr/detail?id=310;就是关于Java的日期和时间的。JSR 310实际上有两个日期概念。第一个是Instant,它大致对应于java.util.Date类,因为它代表了一个确定的时间点,即相对于标准Java纪元(1970年1月1日)的偏移量;但与java.util.Date类不同的是其精确到了纳秒级别。
第二个对应于人类自身的观念,比如LocalDate和LocalTime。他们代表了一般的时区概念,要么是日期(不包含时间),要么是时间(不包含日期),类似于java.sql的表示方式。此外,还有一个MonthDay,它可以存储某人的生日(不包含年份)。每个类都在内部存储正确的数据而不是像java.util.Date那样利用午夜12点来区分日期,利用1970-01-01来表示时间。
目前Java8已经实现了JSR310的全部内容。新增了java.time包定义的类表示了日期-时间概念的规则,包括instants, durations, dates, times, time-zones and periods。这些都是基于ISO日历系统,它又是遵循 Gregorian规则的。最重要的一点是值不可变,且线程安全。实际上JSR310和joda有千丝万缕的关系,因为JSR310的领导者就是joda的创建者。
JDK8新的时间api和老的对应关系:

介绍几个用的很多的:
1、Instant,瞬时时间,等价于以前的System.currentTimeMillis()
//瞬时时间 相当于以前的System.currentTimeMillis()
Instant instant1 = Instant.now();
System.out.println(instant1.getEpochSecond());//精确到秒 得到相对于1970-01-01 00:00:00 UTC的一个时间
System.out.println(instant1.toEpochMilli()); //精确到毫秒
Clock clock1 = Clock.systemUTC(); //获取系统UTC默认时钟
Instant instant2 = Instant.now(clock1);//得到时钟的瞬时时间
System.out.println(instant2.toEpochMilli());
Clock clock2 = Clock.fixed(instant1, ZoneId.systemDefault()); //固定瞬时时间时钟
Instant instant3 = Instant.now(clock2);//得到时钟的瞬时时间
System.out.println(instant3.toEpochMilli());//equals instant1
2、LocalDateTime、LocalDate、LocalTime ,提供了对java.util.Date的替代,另外还提供了新的DateTimeFormatter用于对格式化/解析的支持
//使用默认时区时钟瞬时时间创建 Clock.systemDefaultZone() -->即相对于 ZoneId.systemDefault()默认时区
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
//自定义时区
LocalDateTime now2= LocalDateTime.now(ZoneId.of("Europe/Paris"));
System.out.println(now2);//会以相应的时区显示日期
//自定义时钟
Clock clock = Clock.system(ZoneId.of("Asia/Dhaka"));
LocalDateTime now3= LocalDateTime.now(clock);
System.out.println(now3);//会以相应的时区显示日期
//不需要写什么相对时间 如java.util.Date 年是相对于1900 月是从0开始
//2013-12-31 23:59
LocalDateTime d1 = LocalDateTime.of(2013, 12, 31, 23, 59);
//年月日 时分秒 纳秒
LocalDateTime d2 = LocalDateTime.of(2013, 12, 31, 23, 59,59, 11);
//使用瞬时时间 + 时区
Instant instant = Instant.now();
LocalDateTime d3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
System.out.println(d3);
//解析String--->LocalDateTime
LocalDateTime d4 = LocalDateTime.parse("2013-12-31T23:59");
System.out.println(d4);
LocalDateTime d5 = LocalDateTime.parse("2013-12-31T23:59:59.999");//999毫秒 等价于999000000纳秒
System.out.println(d5);
//使用DateTimeFormatter API 解析 和 格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
LocalDateTime d6 = LocalDateTime.parse("2013/12/31 23:59:59", formatter);
System.out.println(formatter.format(d6));
//时间获取
System.out.println(d6.getYear());
//时间增减
LocalDateTime d7 = d6.minusDays(1);
LocalDateTime d8 = d7.plus(1, IsoFields.QUARTER_YEARS);
//LocalDate 即年月日 无时分秒
//LocalTime即时分秒 无年月日
//API和LocalDateTime类似就不演示了
3、Chronology,用于对年历系统的支持,是java.util.Calendar的替代者
//提供对java.util.Calendar的替换,提供对年历系统的支持
Chronology c = HijrahChronology.INSTANCE;
ChronoLocalDateTime d = c.localDateTime(LocalDateTime.now());
System.out.println(d);
4、ZonedDateTime,带有时区的date-time 存储纳秒、时区和时差(避免与本地date-time歧义);API和LocalDateTime类似,只是多了时差(如2013-12-20T10:35:50.711+08:00[Asia/Shanghai])
/即带有时区的date-time 存储纳秒、时区和时差(避免与本地date-time歧义)。
//API和LocalDateTime类似,只是多了时差(如2013-12-20T10:35:50.711+08:00[Asia/Shanghai])
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now);
ZonedDateTime now2= ZonedDateTime.now(ZoneId.of("Europe/Paris"));
System.out.println(now2);
//其他的用法也是类似的 就不介绍了
ZonedDateTime z1 = ZonedDateTime.parse("2013-12-31T23:59:59Z[Europe/Paris]");
System.out.println(z1);
5、Duration,表示两个瞬时时间的时间段
//表示两个瞬时时间的时间段
Duration d1 = Duration.between(Instant.ofEpochMilli(System.currentTimeMillis() - 12323123), Instant.now());
//得到相应的时差
System.out.println(d1.toDays());
System.out.println(d1.toHours());
System.out.println(d1.toMinutes());
System.out.println(d1.toMillis());
System.out.println(d1.toNanos());
//1天时差 类似的还有如ofHours()
Duration d2 = Duration.ofDays(1);
System.out.println(d2.toDays());
对比旧的日期API:
Java.time | java.util.Calendar以及Date |
流畅的API | 不流畅的API |
实例不可变 | 实例可变 |
线程安全 | 非线程安全 |
日期与时间处理API,在各种语言中,可能都只是个不起眼的API,如果你没有较复杂的时间处理需求,可能只是利用日期与时间处理API取得系统时间,简单做些显示罢了,然而如果认真看待日期与时间,其复杂程度可能会远超过你的想象,天文、地理、历史、政治、文化等因素,都会影响到你对时间的处理。所以在处理时间上,最好选用JSR310(如果你用java8的话就实现310了),或者Joda-Time。
不止是java面临时间处理的尴尬,其他语言同样也遇到过类似的问题,比如
Arrow:Python 中更好的日期与时间处理库
Moment.js:JavaScript 中的日期库
参考:
深入解析日期和时间:JSR310:(https://www.kancloud.cn/wizardforcel/java8-new-features/148349)
你确信你了解时间吗?:(https://coolshell.cn/articles/5075.html)
“千年虫”是什么?一个在计算机诞生之初,遗留下来的巨大BUG:(https://baijiahao.baidu.com/s?id=1678895122100583824&wfr=spider&for=pc)
原文始发于微信公众号(六道轮回菠萝):你真的懂计算机中的时间吗
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/25373.html