Just Do Java

Java 's Blog


  • 首页

  • 分类

  • 作者

  • 归档

  • 关于

分享几个 Markdown 写作软件 Typora 的骚操作

发表于 2020-07-18 | 分类于 java

Hello 大家好,我是阿粉,Markdown 语法想必很多写作的朋友都了解(不了解的可以网上找找,绝对是写作装逼必备神器),Typora 软件必定是 Markdown 写作的一大利器(阿粉不接受反驳),今天阿粉给大家分享一下 Typora 的几个骚操作,我们来一起看下吧。

阅读全文 »

不藏着掖着了,阿粉的各种利器都在这儿了

发表于 2020-07-15 | 分类于 nginx

全是阿粉想要推荐给你的

阅读全文 »

阿粉一个循环引起的生产事故

发表于 2020-07-15 | 分类于 java

Hello 大家好,我是阿粉,工作这么多年虽然经历过风风雨雨,但是每次线上发布版本的时候都是一场硬战,这不最近发布了一个版本,一不小心写了个 bug,差点造成了生产事故,幸好运维老大发现及时。

love2

阅读全文 »

手把手教你在 CentOS7 上搭建 Nginx

发表于 2020-07-14 | 分类于 nginx

是阿粉用了心写的搭建教程

阅读全文 »

Java容器集合经典面试题集

发表于 2020-07-13 | 分类于 java容器集合

本文总结了Java集合容器的经典面试题,所有题目阿粉都给出了自己思考,适合面试前复习扫盲使用。阿粉不能保证里面包含了所有集合面试题,但只要认真深挖好每一道题,做到触类旁通,就能以不变应万变。

  • 大纲:
  • 概述型面试题
  • List
  • Map
  • 小结

概述类面试题

1. 请说一下Java容器集合的分类,各自的继承结构

  • Java中的容器集合分为两大阵营,一个是Collection,一个是Map
  • Collection下分为Set,List,Queue
  • Set的常用实现类有HashSet,TreeSet等
  • List的常用实现类有ArrayList,LinkedList等
  • Queue的常用实现类有LinkedList,ArrayBlockingQueue等
  • Map下没有进一步分类,它的常用实现类有HashMap,ConcurrentHashMap等

能把上面的基本框架答出来基本就没问题了,对于各种类型我只列举了一些实际工作中常用的实现类。但其实在Set,List和Queue下还有更细的划分,如果想要在面试时表现一番,那得对着JDK好好背一背了>_<

2. 请谈一谈Java集合中的fail-fast和fail-safe机制

fail-fast是一种错误检测机制,Java在适合单线程使用的集合容器中很好地实现了fail-fast机制,举一个简单的例子:在多线程并发环境下,A线程在通过迭代器遍历一个ArrayList集合,B线程同时对该集合进行增删元素操作,这个时候线程A就会抛出并发修改异常,中断正常执行的逻辑。

而fail-safe机制更像是一种对fail-fast机制的补充,它被广泛地实现在各种并发容器集合中。回头看上面的例子,如果线程A遍历的不是一个ArrayList,而是一个CopyOnWriteArrayList,则符合fail-safe机制,线程B可以同时对该集合的元素进行增删操作,线程A不会抛出任何异常。

要理解这两种机制的表象,我们得了解这两种机制背后的实现原理:

我们同样用ArrayList解释fail-fast背后的原理:首先ArrayList自身会维护一个modCount变量,每当进行增删元素等操作时,modCount变量都会进行自增。当使用迭代器遍历ArrayList时,迭代器会新维护一个初始值等于modCount的expectedModCount变量,每次获取下一个元素的时候都会去检查expectModCount和modCount是否相等。在上面举的例子中,由于B线程增删元素会导致modCount自增,当A线程遍历元素时就会发现两个变量不等,从而抛出异常。

CopyOnWriteArrayList所实现的fail-safe在上述情况下没有抛出异常,它的原理是:当使用迭代器遍历集合时,会基于原数组拷贝出一个新的数组(ArrayList的底层是数组),后续的遍历行为在新数组上进行。所以线程B同时进行增删操作不会影响到线程A的遍历行为。

这种题目我觉得要先答出核心原理,如果你对多线程和单线程下容器的使用有自己的见解,可以考虑多聊点。

3. 如何一边遍历一边删除Collection中的元素?

使用集合迭代器自身的remove方法进行删除

1
2
3
4
5
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();
}

可能笔试考的更多,算是Java的基本常识吧

List类面试题

4. 谈谈ArrayList和LinkedList的区别

本质的区别来源于两者的底层实现:ArrayList的底层是数组,LinkedList的底层是双向链表。

数组拥有O(1)的查询效率,可以通过下标直接定位元素;链表在查询元素的时候只能通过遍历的方式查询,效率比数组低。

数组增删元素的效率比较低,通常要伴随拷贝数组的操作;链表增删元素的效率很高,只需要调整对应位置的指针即可。

以上是数组和链表的通俗对比,在日常的使用中,两者都能很好地在自己的适用场景发挥作用。

比如说我们常常用ArrayList代替数组,因为封装了许多易用的api,而且它内部实现了自动扩容机制,由于它内部维护了一个当前容量的指针size,直接往ArrayList中添加元素的时间复杂度是O(1)的,使用非常方便。

而LinkedList常常被用作Queue队列的实现类,由于底层是双向链表,能够轻松地提供先入先出的操作。

我觉得可以分两部分答,一个是数组与链表底层实现的不同,另一个是答ArrayList和LinkedList的实现细节。

5. 谈谈ArrayList和Vector的区别

两者的底层实现相似,关键的不同在于Vector的对外提供操作的方法都是用synchronized修饰的,也就是说Vector在并发环境下是线程安全的,而ArrayList在并发环境下可能会出现线程安全问题。

由于Vector的方法都是同步方法,执行起来会在同步上消耗一定的性能,所以在单线程环境下,Vector的性能是不如ArrayList的

除了线程安全这点本质区别外,还有一个实现上的小细节区别:ArrayList每次扩容的大小为原来的1.5倍;Vector可以指定扩容的大小,默认是原来大小的两倍。

感觉可以顺带谈谈多线程环境下ArrayList的替代品,比如CopyOnWriteArrayList,但是要谈谈优缺点。

6. 为什么ArrayList的elementData数组要加上transient修饰

由于ArrayList有自动扩容机制,所以ArrayList的elementData数组大小往往比现有的元素数量大,如果不加transient直接序列化的话会把数组中空余的位置也序列化了,浪费不少的空间。

ArrayList中重写了序列化和反序列化对应的writeObject和readObject方法,在遍历数组元素时,以size作为结束标志,只序列化ArrayList中已经存在的元素。

细节题

Map类面试题

HashMap死亡连环Call即将来临,看爽了记得点个赞啊

7. 请介绍一下HashMap的实现原理

  1. 我们一般用HashMap存储key-value类型的数据,它的底层是一个数组,当我们调用put方法的时候,首先会对key进行计算得出一个hash值,然后根据hash值计算出存放在数组上的位置
  2. 这个时候我们会遇到两种情况:一是数组上该位置为空,可以直接放入数据;还有一种情况是该位置已经存放值了,这就发生了哈希冲突。
  3. 在现在使用较为普遍的JDK1.8中是这样处理哈希冲突的:先用链表把冲突的元素串起来,如果链表的长度达到了8,并且哈希表的长度大于64,则把链表转为红黑树。(在JDK1.7中没有转化为红黑树这一步,只用链表解决冲突)

先热身

8. HashMap是怎样确定key存放在数组的哪个位置的?

JDK1.8

首先计算key的hash值,计算过程是:先得到key的hashCode(int类型,4字节),然后把hashCode的高16位与低16位进行异或,得到key的hash值。

接下来用key的hash值与数组长度减一的值进行按位与操作,得到key在数组中对应的下标。

追问:为什么计算key的hash时要把hashCode的高16位与低16位进行异或?(变式:为什么不直接用key的hashCode)

计算key在数组中的下标时,是通过hash值与数组长度减一的值进行按位与操作的。由于数组的长度通常不会超过2^16,所以hash值的高16位通常参与不了这个按位与操作。

为了让hashCode的高16位能够参与到按位与操作中,所以把hashCode的高16位与低16位进行异或操作,使得高16位的影响能够均匀稀释到低16位中,使得计算key位置的操作能够充分散列均匀。

9. 为什么要把链表转为红黑树,阈值为什么是8?

在极端情况下,比如说key的hashCode()返回的值不合理,或者多个密钥共享一个hashCode,很有可能会在同一个数组位置产生严重的哈希冲突。

这种情况下,如果我们仍然使用使用链表把多个冲突的元素串起来,这些元素的查询效率就会从O(1)下降为O(N)。为了能够在这种极端情况下仍保证较为高效的查询效率,HashMap选择把链表转换为红黑树,红黑树是一种常用的平衡二叉搜索树,添加,删除,查找元素等操作的时间复杂度均为O(logN)

至于阈值为什么是8,这是HashMap的作者根据概率论的知识得到的。当key的哈希码分布均匀时,数组同一个位置上的元素数量是成泊松分布的,同一个位置上出现8个元素的概率已经接近千分之一了,这侧面说明如果链表的长度达到了8,key的hashCode()肯定是出了大问题,这个时候需要红黑树来保证性能,所以选择8作为阈值。

追问:为什么红黑树转换回链表的阈值不是7而是6呢?

如果是7的话,那么链表和红黑树之间的切换范围值就太小了。如果我的链表长度不停地在7和8之间切换,那岂不是得来回变换形态?所以选择6是一种折中的考虑。

10. 请说一下HashMap的扩容原理

  1. 首先得到新的容量值和新的扩容阈值,默认都是原来大小的两倍。
  2. 然后根据新容量创建新的数组
  3. 最后把元素从旧数组中迁移到新数组中

在JDK1.7中,迁移数据的时候所有元素都重新计算了hash,并根据新的hash重新计算数组中的位置。

在JDK1.8中,这个过程进行了优化:如果当前节点是单独节点(后面没有接着链表),则根据该节点的hash值与新容量减一的值按位与得到新的地址。

如果当前节点后面带有链表,则根据每个节点的hash值与旧数组容量进行按位与的结果进行划分。如果得到的值为0,这些元素会被分配回原来的位置;如果得到的结果不为0,则分配到新位置,新位置的下标为当前位置下标加上旧数组容量。

还有一种情况是当前节点是树节点,那么会调用一个专门的拆分方法进行拆分。

追问:为什么HashMap不支持动态缩容?

开放性题目?以下是个人见解:

如果要支持动态缩容,可能就要把缩容安排在remove方法里,这样可能会导致remove方法的时间复杂度从O(1)上升为O(N)。

还有一点可能和我们编写Java代码的习惯有关:由于Java有自动垃圾回收机制,让我们得以可劲地new对象,Java也默认了我们这种吃饭不收拾盘子的行为。既然对象会被回收,HashMap动态缩容在这样的大环境下似乎就显得没那么重要了,这可以说是一种空间换时间的策略吧。

11. 为什么HashMap中适合用Integer,String这样的基础类型作为key?

因为这些基础类内部已经重写了hashCode和equals方法,遵守了HashMap内部的规范。

追问:如果要用我们自己实现的类作为key,要注意什么?

一定要重写hashCode()和equals()方法,而且要遵从以下规则:

equals()是我们判断两个对象是否相同的依据,如果我们重写了equals方法,用自己的逻辑去判断两个对象是否相同,那么一定要保证:

两个equals()返回true的对象,一定要返回相同的hashCode。

这样,在HashMap的put方法中才能正确判断key是否相同。

不是经常有一个问题嘛,两个对象hashCode相同,equals一定返回true吗?答案肯定是否的,这和你的设计密切相关:如果在你的编程思路中这两个对象是不同的,那么就算恰巧两个对象的hashCode相同,equals也应该返回false。

12. 为什么HashMap数组的长度是2的幂次方?

因为这样能够提高根据key计算数组位置的效率。

HashMap根据key计算数组位置的算法是:用key的hash值与数组长度减1的值进行按位与操作。

在我们正常人的思维中,获取数组的某个位置最直接的方法是对数组的长度取余数。但是如果被除数是2的幂次方,那么这个对数组长度取余的方法就等价于对数组长度减一的值进行按位与操作。

在计算机中,位运算的效率远高于取模运算,所以为了提高效率,把数组的长度设为2的幂次方。

13. HashMap与HashTable有什么区别?

在JDK1.7之前,两者的实现极为相似,最大的区别在于HashTable的方法都用synchronized关键字修饰起来了,表明它是线程安全的。

但是由于直接在方法上加synchronized关键字的同步效率较低,在并发情况下,官方推荐我们使用ConcurrentHashMap。

所以我们看到在JDK1.8中,官方甚至没有对HashTable进行链表转树这样的优化,HashTable已经不被推荐使用了。

14. 请说一下ConcurrentHashMap的实现原理

在JDK1.7中ConcurrentHashMap采用了一种分段锁的机制,它的底层实现是一个segment数组,每个segment的底层结构和HashMap相似,也是数组加链表。

当对segment里面的元素进行操作之前,需要获得该segment独有的一把ReentrantLock。ConcurrentHashMap如果不进行手动设置的话,默认有16个segment,可以支持16个线程对16个不同的segment进行并发写操作。

在JDK1.8之后摒弃了segment这种臃肿的设计,新的实现和HashMap非常相似,底层用的也是数组加链表加红黑树。

在新实现中,在put方法里使用了CAS + synchronized进行同步。如果插入元素的位置为空,则使用CAS进行插入。如果插入的位置不为空,则对当前位置的对象进行加锁,也就链表或红黑树的头节点,加锁后再进行后续的插入操作。

这样设计的好处是:

  1. CAS是十分轻量的加锁操作,如果能够直接插入,用CAS能够大幅度节省加锁的开销。
  2. 如果发生冲突,只用锁住当前位置的头结点,理论上数组的长度有多大,并发操作的线程数就能有多少,比原来只能有16个线程效率更高。

这道题如果想深挖扩展可以开始往Java多线程并发方面扯:synchronized,CAS。Java多线程方面我也会出一份总结,有兴趣的不妨先点赞关注一波

小结

我感觉面试的时候对集合的考察会偏向实现原理多一些,所以一定要看一遍源码,相比于框架的源码,集合的源码简直太友好了。在笔试的时候可能还会考一些集合的使用,比如遍历,排序,比较等等,这些算是Java基础了,用得多也就熟了。

最后如果你觉得阿粉的回答有问题,欢迎指正!

阅读全文 »

学会这些超实用的谷歌浏览器技巧,妹子随便撩

发表于 2020-07-11 | 分类于 tool

不夸张的说,遇到的问题, 80% 都可以通过浏览器搜索解决,但是呢,有时候你会发现,我搜索的内容挺对的呀,为什么我就找不到想要的内容,别人就可以找到呢

阅读全文 »

再记一次止于三面的阿里面试之旅

发表于 2020-07-05 | 分类于 面试

Hello 大家好,我是阿粉,最近心情不是很好,因为阿粉面试阿里三面挂掉了, 当收到下面这封邮件的时候阿粉内心是拔凉拔凉的。阿粉被 “Unfortunately”,“another candidate” 这几个词深深的伤害到了。不过伤心归伤心,该自我总结还是得自我总结的,有机会再战。

阅读全文 »

源码级分析 ThreadPoolExecutor ,可能是最详细的一篇

发表于 2020-07-05 | 分类于 java并发

阿粉万字长文带你解析 ThreadPoolExecutor

阅读全文 »

面试官没想到,一个 Java 线程生命周期,我可以扯半小时

发表于 2020-07-04 | 分类于 java并发

面试官:你不是精通 Java 并发吗?从基础的 Java 线程生命周期开始讲讲吧。

好的,面试官。吧啦啦啦…

阅读全文 »

同事被几个面试题,阻挡在了京东的门外

发表于 2020-07-04

最近疫情稳定了,跳槽的换工作这块,又开始了,这不,阿粉公司的同事因为公司不涨工资的事情,已经开始踏上了重新面试,寻找更高级别的出路了,而前几天的面试,让他一次大好的机会被京东的线程面试题阻挡在了门外。

阅读全文 »

面试官:你来讲讲一条查询语句的具体执行过程

发表于 2020-07-01 | 分类于 mysql

对于一个开发工程师来说,了解一下 MySQL 是如何执行一条查询语句的,不是一件坏事,阿粉带你来瞅瞅它是怎么执行的

阅读全文 »

手把手教你搞定菜单权限设计,精确到按钮级别,建议收藏

发表于 2020-06-30 | 分类于 springboot

菜单权限管理,一直都是后端管理系统必不可少的一个模块,今天我们就一起来瞅瞅,如何精准的控制到按钮。

阅读全文 »

这可能是阿粉见过最详细的一份 Spring 异步任务教程

发表于 2020-06-30 | 分类于 Java

阿粉最近碰到一个场景,用户注册之后需要发送邮件给其邮箱。原先设计中,这是一个同步过程,注册方法需要等待邮件发送成功才能返回。

由于邮件发送流程对于注册来说并不是一个关键节点,我们可以将邮件发送异步执行,减少注册方法执行时间。

我们可以自己创建线程池,然后执行异步任务,示例代码如下:

阅读全文 »

mybatis系列之获取mapper.xml配置文件中的sql

发表于 2020-06-27 | 分类于 Java

hello~各位读者好,我是鸭血粉丝(大家可以称呼我为「阿粉」)。今天,阿粉带着大家来了解一下获取 mapper.xml 配置文件中的sql

阅读全文 »

SpringBoot2.x 配合 Redis 操作

发表于 2020-06-24 | 分类于 SpringBoot

我们都知道,把首页数据放到Redis里,能够加快首页数据的访问速度。但是我们要如何准确又快速的将 Redis 整合到自己的 SpringBoot2.x 项目中呢?今天阿粉就带大家爬一爬其中的门门道道。

阅读全文 »

序列化到底是什么

发表于 2020-06-23 | 分类于 java

我们都知道,新建一个对象的时候实现 Serializeable 接口,但为什么要这么做?什么时候这样子做?这样子做会不会出现幺蛾子?阿粉一个三连差点把自己都问懵逼了……

阅读全文 »

灵魂拷问,如何防止重复提交?

发表于 2020-06-21 | 分类于 springboot

平时开发项目的时候,你是否遇到这样的困惑,用户不停的点击按钮向后端提交数据,而你却束手无策!

阅读全文 »

周六特辑:星球作业之最有意义的技术成长故事

发表于 2020-06-20 | 分类于 感悟

Hello 大家好,我是鸭血粉丝,月初的时候我们再星球发布了一个作业,作业的题目是分享一段对自己最有意义的技术成长故事,并且会对相关回答点赞人数最多的几位星友进行赞赏奖励。作业如下图所示:

阅读全文 »

阿粉给大家总结了如此多的面试题,总有一款你需要的!

发表于 2020-06-20

阿粉最近在Boss上面公开了自己的简历,好多人在疯狂的私聊阿粉,最近是不是有换工作的意向了,要不要考虑一下?于是阿粉躁动的心难以平静,分分钟准备出了大量的面试题,狂刷面试题恶补自己的知识储备,准备新一轮的面试。但是阿粉肯定是不会那么自私的,肯定要给大家也放送上这么多的面试题,让大家一起找到一份高质量高薪资的工作。跟着阿粉来刷一下吧!

阅读全文 »

有的人 28 岁已经退休,而阿粉 28 岁还在“搬砖”

发表于 2020-06-18 | 分类于 感悟

Hello 大家好,我是阿粉,想必大家这几天都被 28 岁字节程序员退休的话题刷屏了。同样作为程序员,阿粉只能默默的表示实名羡慕,但是别人的人生毕竟是别人的人生,我们能做的还是过好自己的人生。

layoff03

最近在跟朋友聊天,说到从毕业开始已经深漂五年了。五年的时光说长不长说短不短,在这五年里的各种艰难只有在一线城市经历过的才懂,有过泪,有过喜,熬过夜,通过宵,有得到也有失去。

阿粉还清晰的记得高考结束填志愿的时候,填的都是计算机相关的专业,那个时候也不知道啥时互联网,只知道计算机听上去挺高大上的。然后就稀里糊涂的进入了计算机学院网络工程专业,刚开始我以为大家都跟我一样是因为对计算机的热爱才选了这个专业,但是慢慢的才发现很多人是调剂过来的,都是因为其他专门满了才被调剂到计算机学院的。虽然阿粉作为科班出身,但是接触电脑还是比较晚的,接触编程也是上大学才开始的,不像有些朋友从小就接触电脑接触编程。

接触的第一门编程语言毫无疑问《谭浩强的 C 语言》,相信很多人都跟阿粉一样编程生涯是从这本书开始的。对于这本书的褒贬阿粉不做评价,有人说不好,有人说好,但是既然能出书并能被纳入大学教材自然有它的道理。

刚接触编程语言的时候,第一感觉就是,这也太神奇好玩了吧!那个时候刚上大学,还没有买电脑,每次都是很期待上机课的到来,因为这样就可以去玩电脑了,可以把书上的代码在机器上运行一遍。还记得那个时候有高年级的学长来给我们辅导,当时在屏幕上输出一个乘法口诀表都很开心。最有成长的是大三的时候,那个时候买了一台笔记本,天天待在老是实验室里面看各种技术视频,跟着视频一行一行敲代码,然后做了一个小网站自己开心了好久。

phone1

后来实验室老师接了项目,让我们帮他做,每个月挣点零花钱,那个时候既能学东西又能挣钱,不用跟家里要生活费,感觉还是很棒的。

现在想想,校园的时光就是那么美好,那个时候的心思也很单纯,真好。

后来大三到深圳的一家公司实习,实习完后公司还不错就就顺利的签了意向书,毕业后可以直接入职,相当于还没毕业就找到了工作,可开心坏了。实习后既然工作已经定了,就没有再找过其他公司,那个时候班里同学整体都在外面跑来跑去,各种面试,而自己则是一门心思的在实验室提升技能,而没有出去面试。(其实这个现在想想挺后悔的,还是要去大公司试试~~)。

刚开始工作的时候,那个时候的自己没有什么其他想法,每天只要好好写自己的代码就可以了,不需要管什么乱七八糟的事情,老大安排什么就做什么,闲暇时候自己写写博客,学习一些新的东西。那个时候考虑的只是如果将老大安排的任务用最好的方式进行实现,保证系统的功能能正常使用和快速提升自己的能力。

那个时候每天都是充实的,因为刚毕业,项目里面能学习的东西很多,天天都是满满的收获。每天十点十一点下班,但是并不觉得累,反正住公司员工宿舍,回去早了也没有事情做,还不如在公司多学习点东西。那个时候跟自己一起进来的小伙伴们还有好多,大家一起学习,一起进步;周末的时候一起加班,一起徒步,一起吃石锅鱼,一起看电影,大家还是很开心的。

慢慢的到后来随着自己参与的项目越来越多,接触的模块和业务也越来越复杂,渐渐的成为小组内的核心人员,这个时候考虑的和思考的已经从个人变成团队了。需要考虑一个模块怎么样设计才合理,框架的选型怎么样才更符合业务需求,系统的功能怎么样设计才能更方便用户使用。很多时候因为一个小小的改动都要纠结很久,因为虽然是系统上的一个小小改动,但是影响的却是几百万的用户。

工作了这么久有很多当时一起工作的小伙伴有的创业了,有的转行了,有的离开深圳回老家发展了。如果要问是什么让阿粉坚持下来,那应该是热爱吧,自己本身还是挺喜欢互联网行业的,而且阿粉觉得程序开发还是很有意思的,喜欢自己折腾完成一个东西。It 这一行学无止境,未来的路还有很长,继续加油。

程序员这行技术的更新迭代太快,我们要时刻准备接受新的知识,才能不被时代淘汰。

jdk01

前段时间看了微笑哥送的《学会写作》, 书中提到在《孙子兵法》中有一句:“胜者先胜而后求战,败者先战而后求胜”。

胜者之所以能常胜是因为在开战之前就已经洞悉了一切,做好了充分的准备,有着百分百必胜的把握,而后才会开战,自然必胜。而败者在开战前没有任何准备,直到战争开始才想着怎样取胜,这样失败的概率必然很大。

总结给我们的知识就是在做任何事情的时候都要做好准备,只有自己准备的充分,才能取得最大的胜利,也就是我们常说的知己知彼,百战不殆。不仅是对待某件事情,对待我们的人生都应该这样,前期做好准备,集聚能量,蓄势待发,在关键时刻必然一飞冲天。

阅读全文 »
1 … 11 12 13 … 32
Java Geek Tech

Java Geek Tech

一群热爱 Java 的技术人

633 日志
116 分类
24 作者
RSS
GitHub 知乎
Links
  • 纯洁的微笑
  • 沉默王二
  • 子悠
  • 江南一点雨
  • 炸鸡可乐
  • 郑璐璐
  • 程序通事
  • 懿
© 2019 - 2022 Java Geek Tech
由 Jekyll 强力驱动
主题 - NexT.Mist