首页 文章 精选 留言 我的

精选列表

搜索[快直播],共10005篇文章
优秀的个人博客,低调大师

快使用 LocalDateTime 了!

云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 在项目开发过程中经常遇到时间处理,但是你真的用对了吗,理解阿里巴巴开发手册中禁用static修饰SimpleDateFormat吗?通过阅读本篇文章你将了解到: 为什么需要LocalDate、LocalTime、LocalDateTime【java8新提供的类】; java8新的时间API的使用方式,包括创建、格式化、解析、计算、修改。 为什么需要LocalDate、LocalTime、LocalDateTime 1.Date如果不格式化,打印出的日期可读性差 Tue Sep 10 09:34:04 CST 2019 2.使用SimpleDateFormat对时间进行格式化,但SimpleDateFormat是线程不安全的SimpleDateFormat的format方法最终调用代码: private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list calendar.setTime(date); boolean useDateFormatSymbols = useDateFormatSymbols(); 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: toAppendTo.append((char)count); break; case TAG_QUOTE_CHARS: toAppendTo.append(compiledPattern, i, count); i += count; break; default: subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols); break; } } return toAppendTo; } calendar是共享变量,并且这个共享变量没有做线程安全控制。关注微信公众号:互联网架构师,在后台回复:2T,可以获取教程,都是干货。 当多个线程同时使用相同的SimpleDateFormat对象【如用static修饰的SimpleDateFormat】调用format方法时,多个线程会同时调用calendar.setTime方法,可能一个线程刚设置好time值 另外的一个线程马上把设置的time值给修改了导致返回的格式化时间可能是错误的。 在多并发情况下使用SimpleDateFormat需格外注意 SimpleDateFormat除了format是线程不安全以外,parse方法也是线程不安全的。parse方法实际调用alb.establish(calendar).getTime()方法来解析,alb.establish(calendar)方法里主要完成了 a、重置日期对象cal的属性值b、使用calb中中属性设置calc、返回设置好的cal对象 但是这三步不是原子操作多线程并发如何保证线程安全 - 避免线程之间共享一个SimpleDateFormat对象,每个线程使用时都创建一次SimpleDateFormat对象 => 创建和销毁对象的开销大 - 对使用format和parse方法的地方进行加锁 => 线程阻塞性能差 - 使用ThreadLocal保证每个线程最多只创建一次SimpleDateFormat对象 => 较好的方法 1.Date对时间处理比较麻烦,比如想获取某年、某月、某星期,以及n天以后的时间,如果用Date来处理的话真是太难了,你可能会说Date类不是有getYear、getMonth这些方法吗,获取年月日很Easy,但都被弃用了啊 Come On 一起使用java8全新的日期和时间API LocalDate只会获取年月日创建LocalDate //获取当前年月日 LocalDate localDate = LocalDate.now(); //构造指定的年月日 LocalDate localDate1 = LocalDate.of(2019, 9, 10); 获取年、月、日、星期几 int year = localDate.getYear(); int year1 = localDate.get(ChronoField.YEAR); Month month = localDate.getMonth(); int month1 = localDate.get(ChronoField.MONTH_OF_YEAR); int day = localDate.getDayOfMonth(); int day1 = localDate.get(ChronoField.DAY_OF_MONTH); DayOfWeek dayOfWeek = localDate.getDayOfWeek(); int dayOfWeek1 = localDate.get(ChronoField.DAY_OF_WEEK); LocalTime只会获取几点几分几秒创建LocalTime LocalTime localTime = LocalTime.of(13, 51, 10); LocalTime localTime1 = LocalTime.now(); 获取时分秒 //获取小时 int hour = localTime.getHour(); int hour1 = localTime.get(ChronoField.HOUR_OF_DAY); //获取分 int minute = localTime.getMinute(); int minute1 = localTime.get(ChronoField.MINUTE_OF_HOUR); //获取秒 int second = localTime.getSecond(); int second1 = localTime.get(ChronoField.SECOND_OF_MINUTE); LocalDateTime获取年月日时分秒,等于LocalDate+LocalTime创建LocalDateTime LocalDateTime localDateTime = LocalDateTime.now(); LocalDateTime localDateTime1 = LocalDateTime.of(2019, Month.SEPTEMBER, 10, 14, 46, 56); LocalDateTime localDateTime2 = LocalDateTime.of(localDate, localTime); LocalDateTime localDateTime3 = localDate.atTime(localTime); LocalDateTime localDateTime4 = localTime.atDate(localDate); 获取LocalDate LocalDate localDate2 = localDateTime.toLocalDate(); 获取LocalTime LocalTime localTime2 = localDateTime.toLocalTime(); Instant获取秒数创建Instant对象 Instant instant = Instant.now(); 获取秒数 long currentSecond = instant.getEpochSecond(); 获取毫秒数 long currentMilli = instant.toEpochMilli(); 个人觉得如果只是为了获取秒数或者毫秒数,使用System.currentTimeMillis()来得更为方便修改LocalDate、LocalTime、LocalDateTime、InstantLocalDate、LocalTime、LocalDateTime、Instant为不可变对象,修改这些对象对象会返回一个副本增加、减少年数、月数、天数等 以LocalDateTime为例 LocalDateTime localDateTime = LocalDateTime.of(2019, Month.SEPTEMBER, 10, 14, 46, 56); //增加一年 localDateTime = localDateTime.plusYears(1); localDateTime = localDateTime.plus(1, ChronoUnit.YEARS); //减少一个月 localDateTime = localDateTime.minusMonths(1); localDateTime = localDateTime.minus(1, ChronoUnit.MONTHS); 通过with修改某些值 //修改年为2019 localDateTime = localDateTime.withYear(2020); //修改为2022 localDateTime = localDateTime.with(ChronoField.YEAR, 2022); 还可以修改月、日 时间计算 比如有些时候想知道这个月的最后一天是几号、下个周末是几号,通过提供的时间和日期API可以很快得到答案 比如通过firstDayOfYear()返回了当前日期的第一天日期,还有很多方法这里不在举例说明格式化时间 LocalDate localDate = LocalDate.of(2019, 9, 10); String s1 = localDate.format(DateTimeFormatter.BASIC_ISO_DATE); String s2 = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE); //自定义格式化 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); String s3 = localDate.format(dateTimeFormatter); DateTimeFormatter默认提供了多种格式化方式,如果默认提供的不能满足要求,可以通过DateTimeFormatter的ofPattern方法创建自定义格式化方式 解析时间 LocalDate localDate1 = LocalDate.parse("20190910", DateTimeFormatter.BASIC_ISO_DATE); LocalDate localDate2 = LocalDate.parse("2019-09-10", DateTimeFormatter.ISO_LOCAL_DATE); 和SimpleDateFormat相比,DateTimeFormatter是线程安全的 小结 LocalDateTime:Date有的我都有,Date没有的我也有,日期选择请Pick MeSpringBoot中应用LocalDateTime将LocalDateTime字段以时间戳的方式返回给前端添加日期转化类 public class LocalDateTimeConverter extends JsonSerializer{ @Override public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeNumber(value.toInstant(ZoneOffset.of("+8")).toEpochMilli()); } } 并在LocalDateTime字段上添加@JsonSerialize(using = LocalDateTimeConverter.class)注解,如下: @JsonSerialize(using = LocalDateTimeConverter.class) protected LocalDateTime gmtModified; 将LocalDateTime字段以指定格式化日期的方式返回给前端 在LocalDateTime字段上添加@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")注解即可,如下: @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss") protected LocalDateTime gmtModified; 对前端传入的日期进行格式化 在LocalDateTime字段上添加@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解即可,如下: @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") protected LocalDateTime gmtModified; 【云栖号在线课堂】每天都有产品技术专家分享!课程地址:https://yqh.aliyun.com/zhibo 立即加入社群,与专家面对面,及时了解课程最新动态!【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK 原文发布时间:2020-04-25本文作者: 何甜甜在吗本文来自:“互联网架构师 微信公众号”,了解相关信息可以关注“互联网架构师”

优秀的个人博客,低调大师

《快学 Go 语言》第 9 课 —— 接口

接口是一个对象的对外能力的展现,我们使用一个对象时,往往不需要知道一个对象的内部复杂实现,通过它暴露出来的接口,就知道了这个对象具备哪些能力以及如何使用这个能力。 我们常说「佛有千面」,不同的人看到的佛并不一样。一个复杂的复合对象常常也可以是一个多面手,它具备多种能力,在形式上实现了多种接口。「弱水三千,只取一瓢」,使用时我们根据不同的场合来挑选满足需要的接口能力来使用这个对象即可。 Go 语言的接口类型非常特别,它的作用和 Java 语言的接口一样,但是在形式上有很大的差别。Java 语言需要在类的定义上显式实现了某些接口,才可以说这个类具备了接口定义的能力。但是 Go 语言的接口是隐式的,只要结构体上定义的方法在形式上(名称、参数和返回值)和接口定义的一样,那么这个结构体就自动实现了这个接口,我们就可以使用这个接口变量来指向这个结构体对象。下面我们看个例子 package mainimport "fmt"// 可以闻type Smellable interface { smell() }// 可以吃type Eatable interface { eat() }// 苹果既可能闻又能吃type Apple struct {}func (a Apple) smell() { fmt.Println("apple can smell") }func (a Apple) eat() { fmt.Println("apple can eat") }// 花只可以闻type Flower struct {}func (f Flower) smell() { fmt.Println("flower can smell") }func main() {var s1 Smellablevar s2 Eatablevar apple = Apple{}var flower = Flower{} s1 = apple s1.smell() s1 = flower s1.smell() s2 = apple s2.eat() } -------------------- apple can smell flower can smell apple can eat 上面的代码定义了两种接口,Apple 结构体同时实现了这两个接口,而 Flower 结构体只实现了 Smellable 接口。我们并没有使用类似于 Java 语言的 implements 关键字,结构体和接口就自动产生了关联。 空接口 如果一个接口里面没有定义任何方法,那么它就是空接口,任意结构体都隐式地实现了空接口。 Go 语言为了避免用户重复定义很多空接口,它自己内置了一个,这个空接口的名字特别奇怪,叫 interface{} ,初学者会非常不习惯。之所以这个类型名带上了大括号,那是在告诉用户括号里什么也没有。我始终认为这种名字很古怪,它让代码看起来有点丑陋。 空接口里面没有方法,所以它也不具有任何能力,其作用相当于 Java 的 Object 类型,可以容纳任意对象,它是一个万能容器。比如一个字典的 key 是字符串,但是希望 value 可以容纳任意类型的对象,类似于 Java 语言的 Map 类型,这时候就可以使用空接口类型 interface{}。 package mainimport "fmt"func main() {// 连续两个大括号,是不是看起来很别扭var user = map[string]interface{}{"age": 30,"address": "Beijing Tongzhou","married": true, } fmt.Println(user)// 类型转换语法来了var age = user["age"].(int)var address = user["address"].(string)var married = user["married"].(bool) fmt.Println(age, address, married) } -------------map[age:30 address:Beijing Tongzhou married:true]30 Beijing Tongzhou true 代码中 user 字典变量的类型是 map[string]interface{},从这个字典中直接读取得到的 value 类型是 interface{},需要通过类型转换才能得到期望的变量。 接口变量的本质 在使用接口时,我们要将接口看成一个特殊的容器,这个容器只能容纳一个对象,只有实现了这个接口类型的对象才可以放进去。 图片 接口变量作为变量来说它也是需要占据内存空间的,通过翻阅 Go 语言的源码可以发现,接口变量也是由结构体来定义的,这个结构体包含两个指针字段,一个字段指向被容纳的对象内存,另一个字段指向一个特殊的结构体 itab,这个特殊的结构体包含了接口的类型信息和被容纳对象的数据类型信息。 // interface structure type iface struct { tab *itab // 类型指针 data unsafe.Pointer // 数据指针 } type itab struct { inter *interfacetype // 接口类型信息 _type *_type // 数据类型信息 ... } 既然接口变量只包含两个指针字段,那么它的内存占用应该是 2 个机器字,下面我们来编写代码验证一下 package mainimport "fmt"import "unsafe"func main() {var s interface{} fmt.Println(unsafe.Sizeof(s))var arr = [10]int {1,2,3,4,5,6,7,8,9,10} fmt.Println(unsafe.Sizeof(arr)) s = arr fmt.Println(unsafe.Sizeof(s)) } ----------168016 数组的内存占用是 10 个机器字,但是这丝毫不会影响到接口变量的内存占用。 用接口来模拟多态 前面我们说到,接口是一种特殊的容器,它可以容纳多种不同的对象,只要这些对象都同样实现了接口定义的方法。如果我们将容纳的对象替换成另一个对象,那不就可以完成上一节我们没有完成的多态功能了么?好,顺着这个思路,下面我们就来模拟一下多态 package mainimport "fmt"type Fruitable interface { eat() }type Fruit struct { Name string // 属性变量 Fruitable // 匿名内嵌接口变量 }func (f Fruit) want() { fmt.Printf("I like ") f.eat() // 外结构体会自动继承匿名内嵌变量的方法 }type Apple struct {}func (a Apple) eat() { fmt.Println("eating apple") }type Banana struct {}func (b Banana) eat() { fmt.Println("eating banana") }func main() {var f1 = Fruit{"Apple", Apple{}}var f2 = Fruit{"Banana", Banana{}} f1.want() f2.want() } --------- I like eating apple I like eating banana 使用这种方式模拟多态本质上是通过组合属性变量(Name)和接口变量(Fruitable)来做到的,属性变量是对象的数据,而接口变量是对象的功能,将它们组合到一块就形成了一个完整的多态性的结构体。 接口的组合继承 接口的定义也支持组合继承,比如我们可以将两个接口定义合并为一个接口如下 type Smellable interface { smell() } type Eatable interface { eat() } type Fruitable interface { Smellable Eatable } 这时 Fruitable 接口就自动包含了 smell() 和 eat() 两个方法,它和下面的定义是等价的。 type Fruitable interface { smell() eat() } 接口变量的赋值 变量赋值本质上是一次内存浅拷贝,切片的赋值是拷贝了切片头,字符串的赋值是拷贝了字符串的头部,而数组的赋值呢是直接拷贝整个数组。接口变量的赋值会不会不一样呢?接下来我们做一个实验 package mainimport "fmt"type Rect struct { Width int Height int }func main() {var a interface {}var r = Rect{50, 50} a = rvar rx = a.(Rect) r.Width = 100 r.Height = 100 fmt.Println(rx) } ------ {50 50} 从上面的输出结果中可以推断出结构体的内存发生了复制,这个复制可能是因为赋值(a = r)也可能是因为类型转换(rx = a.(Rect)),也可能是两者都进行了内存复制。那能不能判断出究竟在接口变量赋值时有没有发生内存复制呢?不好意思,就目前来说我们学到的知识点还办不到。到后面的高级阶段我们将会使用 unsafe 包来洞悉其中的更多细节。不过我可以提前告诉你们答案是什么,那就是两者都会发生数据内存的复制 —— 浅拷贝。 指向指针的接口变量 如果将上面的例子改成指针,将接口变量指向结构体指针,那结果就不一样了 package mainimport "fmt"type Rect struct { Width int Height int }func main() {var a interface {}var r = Rect{50, 50} a = &r // 指向了结构体指针var rx = a.(*Rect) // 转换成指针类型 r.Width = 100 r.Height = 100 fmt.Println(rx) } ------- {100 100} 从输出结果中可以看出指针变量 rx 指向的内存和变量 r 的内存是同一份。因为在类型转换的过程中只发生了指针变量的内存复制,而指针变量指向的内存是共享的。 下一节我们开讲 Go 语言的错误处理 原文发布时间为: 2018-11-27 本文作者:码洞 本文来自云栖社区合作伙伴“码洞”,了解相关信息可以关注“码洞”。

优秀的个人博客,低调大师

《快学 Go 语言》第 6 课 —— 字典

字典在数学上的词汇是映射,将一个集合中的所有元素关联到另一个集合中的部分或全部元素,并且只能是一一映射或者多对一映射。 数组切片让我们具备了可以操作一块连续内存的能力,它是对同质元素的统一管理。而字典则赋予了不连续不同类的内存变量的关联性,它表达的是一种因果关系,字典的 key 是因,字典的 value 是果。如果说数组和切片赋予了我们步行的能力,那么字典则让我们具备了跳跃的能力。 指针、数组切片和字典都是容器型变量,字典比数组切片在使用上要简单很多,但是内部结构却无比复杂。本节我们只专注字典的基础使用,在后续的高级章节再来分析它的内部结构。 字典的创建 关于 Go 语言有很多批评的声音,比如说它不支持范型。其实严格来说 Go 是支持范型的,只不过很弱,范型在 Go 语言里是一种很弱的存在。比如数组切片和字典类型都是支持范型的。在创建字典时,必须要给 key 和 value 指定类型。创建字典也可以使用 make 函数 package mainimport "fmt"func main() {var m map[int]string = make(map[int]string) fmt.Println(m, len(m)) } ----------map[] 0 使用 make 函数创建的字典是空的,长度为零,内部没有任何元素。如果需要给字典提供初始化的元素,就需要使用另一种创建字典的方式。 package mainimport "fmt"func main() {var m map[int]string = map[int]string{90: "优秀",80: "良好",60: "及格", // 注意这里逗号不可缺少,否则会报语法错误 } fmt.Println(m, len(m)) } ---------------map[90:优秀 80:良好 60:及格] 3 字典变量同样支持类型推导,上面的变量定义可以简写成 var m = map[int]string{90: "优秀",80: "良好",60: "及格", } 如果你可以预知字典内部键值对的数量,那么还可以给 make 函数传递一个整数值,通知运行时提前分配好相应的内存。这样可以避免字典在长大的过程中要经历的多次扩容操作。 var m = make(map[int]string, 16) 同 Python 语言一样,字典可以使用中括号来读写内部元素,使用 delete 函数来删除元素。 package mainimport "fmt"func main() {var fruits = map[string]int {"apple": 2,"banana": 5,"orange": 8, }// 读取元素var score = fruits["banana"] fmt.Println(score)// 增加或修改元素 fruits["pear"] = 3 fmt.Println(fruits)// 删除元素delete(fruits, "pear") fmt.Println(fruits) } -----------------------5map[apple:2 banana:5 orange:8 pear:3]map[orange:8 apple:2 banana:5] 删除操作时,如果对应的 key 不存在,delete 函数会静默处理。遗憾的是 delete 函数没有返回值,你无法直接得到 delete 操作是否真的删除了某个元素。你需要通过长度信息或者提前尝试读取 key 对应的 value 来得知。 读操作时,如果 key 不存在,也不会抛出异常。它会返回 value 类型对应的零值。如果是字符串,对应的零值是空串,如果是整数,对应的零值是 0,如果是布尔型,对应的零值是 false。 你不能通过返回的结果是否是零值来判断对应的 key 是否存在,因为 key 对应的 value 值可能恰好就是零值,比如下面的字典你就不能判断 "durin" 是否存在 var m = map[string]int {"durin": 0 // 举个栗子而已,其实我还是喜欢吃榴莲的 } 这时候必须使用字典的特殊语法,如下 package mainimport "fmt"func main() {var fruits = map[string]int {"apple": 2,"banana": 5,"orange": 8, }var score, ok = fruits["durin"]if ok { fmt.Println(score) } else { fmt.Println("durin not exists") } fruits["durin"] = 0 score, ok = fruits["durin"]if ok { fmt.Println(score) } else { fmt.Println("durin still not exists") } } ------------- durin not exists0 字典的下标读取可以返回两个值,使用第二个返回值都表示对应的 key 是否存在。初学者看到这种奇怪的用法是需要花时间来消化的,读者不需要想太多,它只是 Go 语言提供的语法糖,内部并没有太多的玄妙。正常的函数调用可以返回多个值,但是并不具备这种“随机应变”的特殊能力 —— 「多态返回值」。字典的遍历 字典的遍历提供了下面两种方式,一种是需要携带 value,另一种是只需要 key,需要使用到 Go 语言的 range 关键字。 package mainimport "fmt"func main() {var fruits = map[string]int {"apple": 2,"banana": 5,"orange": 8, }for name, score := range fruits { fmt.Println(name, score) }for name := range fruits { fmt.Println(name) } } ------------ orange 8 apple 2 banana 5 apple banana orange 奇怪的是,Go 语言的字典没有提供诸于 keys() 和 values() 这样的方法,意味着如果你要获取 key 列表,就得自己循环一下,如下 package mainimport "fmt"func main() {var fruits = map[string]int {"apple": 2,"banana": 5,"orange": 8, }var names = make([]string, 0, len(fruits))var scores = make([]int, 0, len(fruits))for name, score := range fruits { names = append(names, name) scores = append(scores, score) } fmt.Println(names, scores) } ---------- [apple banana orange] [2 5 8] 这会让代码写起来比较繁琐,不过 Go 语言官方就是没有提供,读者还是努力习惯一下吧 线程(协程)安全 Go 语言的内置字典不是线程安全的,如果需要线程安全,必须使用锁来控制。在后续锁的章节里,我们将会自己实现一个线程安全的字典。 字典变量里存的是什么? 字典变量里存的只是一个地址指针,这个指针指向字典的头部对象。所以字典变量占用的空间是一个字,也就是一个指针的大小,64 位机器是 8 字节,32 位机器是 4 字节。 图片 可以使用 unsafe 包提供的 Sizeof 函数来计算一个变量的大小 package mainimport ("fmt""unsafe" )func main() {var m = map[string]int{"apple": 2,"pear": 3,"banana": 5, } fmt.Println(unsafe.Sizeof(m)) } ------8 在遍历字典得到 keys 和 values 的例子里,我们分配了 names 和 scores 两个切片,如果把代码片断调整成下面这样,会有什么问题? var names = make([]string, len(fruits))var scores = make([]int, len(fruits)) 下一节我们开讲 Go 语言的字符串 原文发布时间为: 2018-11-16 本文作者:码洞 本文来自云栖社区合作伙伴“码洞”,了解相关信息可以关注“码洞”。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册