动态代理笔记

以前我眼里动态代理一直是个很神秘的东西,面试一旦问到必然摇头回答不知道

想办法理一下,目前动态代理有使用JDK类库 和使用CGLIB 两种方式

JDK类库的大概用法是这样的:

写一个类 写一个接口 叫MyClassMyClassImpl

写一个Handler类,实现InvocationHandler这个JDK反射包里的接口,构造器传入一个MyClass的实例,并且覆盖invoke方法,然后在invoke方法中实现具体的逻辑

如果要使用动态代理,就需要使用JDK的Proxy.newProxyInstance()方法,传入类加载器、代理需要实现的接口、你写的Handler类的一个新实例,并且传入一个MyClassImpl作为这个新实例的构造函数参数。

就可以在方法调用时 动态生成Proxy类,并且实际执行Handler中的逻辑了。

这一套看起来很眼熟,Spring的AOP相关类库 就是对使用动态代理的简化。


但是JDK动态代理 要求被代理的类必须实现接口,碰到没有接口的类 就必须使用CGLIB来操作字节码了。

其实和JDK库类似,只不过实际使用的类库不太一样:

首先写一个MyClass类,再写一个Interceptor类 实现MethodInterceptor这个CGLIB的接口,覆盖intercept方法,然后在intercept方法里实现具体的逻辑

而实际使用动态代理时,new 一个 Enhancer对象(CGLIB提供这个类),设置要代理的类和Interceptor对象,再使用create()方法创建被代理后的对象。

这样做,所有非final方法都会被代理。而由于生成的代理类是继承了原类的,所以final类被代理时会抛出异常。

对于Object类,JDK只代理hashCode()equals()toString(),按官方文档的说法,是“和接口方法一样处理”;

而CGLIB则是代理wait(),notify()notifyAll(),getClass()四个方法,因为它们是final的。

面经读后感-类加载顺序

讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字

段,当new的时候,他们的执行顺序。

死板答案:父类静态字段/静态块 子类静态字段/静态块 父类字段/代码块 父类构造器 子类字段/代码块 子类构造器

解释:
编译器会收集所有字段赋值和静态块 搞成一个方法,顺序由源码顺序决定,并且保证父类的比子类的先执行。
但是常量赋值会在之前执行。
所以必然是父类的静态字段/块,子类静态字段/块。
然后是方法,初始化所有字段,并且调用其他方法。
分几种情况:
如果对应的源码构造器内明确从调用另一个构造器开始,则该方法会先调用对应的方法。
否则该方法会调用父类的方法,初始化字段,然后是该方法本体的字节码)

简要回顾各垃圾回收器的核心思想

人一老 就容易忘事


Serial 新生代单线程 复制到老年代

ParNew 新生代多线程 复制到老年代

Parallel Scavenge 新生代多线程 复制到老年代
关注点在 停顿时间 ,因此可以设置最大回收停顿时间,自动调节各代大小
和G1都没有用传统的GC框架,因此无法和老年代CMS共用


Serial Old 老年代单线程 标记整理

Parallel Old Parallel Scavenge的老年版,标记整理
配合Parallel Scavenge 可以只关注吞吐量

CMS 老年代并发 标记清除 1.4.1
初始标记,标记GC Root(初始标记的root并不包括年轻代,结合并发标记阶段看实际上年轻代也是GC Root的一部分)直达对象,阻塞
并发标记,从初始标记的结果开始爬可达性树,非阻塞
重新标记,并发标记完成后,修正在并发标记期间而变化的记录,阻塞
并发清除

缺点:
需要多核环境
并发清除时产生的垃圾只能留到下次,如果并发清除时内存不足只能Full GC
标记清除带来空间碎片,可以设置在Full GC前整理(默认开启)


G1 管理整个GC堆 但是依然有分代概念 6u14面世 7u4正式推出
准备替代CMS
G1暂停时需要复制对象,CMS暂停时只需要扫描对象,因此6G以下CMS不一定比G1差。推荐6G以上堆,可达到0.5s以下GC时间。

可用对象占用50%以上堆时
对象分配/变老速率显著变化时(CMS并发标记时,如果依然在高速分配内存,会导致很久的remark),
较长时间的GC/压缩发生时(0.5-1s以上)
建议切到G1,可以获得收益

设计理念:
停顿可预测:可以避免收集整个堆,而是跟踪每个Region中的垃圾大小和回收所需时间(也就是价值),优先回收价值高的Region,也是名字的来由
无碎片:内存分为等大的Region,整体为标记整理,实际是复制Region。
并行:扫描对象和复制对象分开运行,并且扫描对象的【初始标记】会借用复制对象的【年轻代复制】步骤。
空间换时间:使用Remembered Set记录老年代指向年轻代的指针,以及使用Collection Set记录需要收集的Region。

收集器部分

复制对象的两种运行模式:
Young gc:新生代满时运行,扫描所有年轻代的Region,找出半死的和全死的Region构成Collection Set。
并行的干掉死透的,并且把半死的Region中活对象复制到别的Region中。通过控制年轻代个数来控制开销

Mixed gc:扫描所有年轻代Region,和 全局并发标记 得出的高价值老年代Region构成Collection Set
。根据用户指定开销来调节价值范围,当mixed gc也清不出足够内存,老年代填满,就会用Searial Old 的核心代码 来Full GC。System.gc()也是Full GC,XX:+ExplicitGCInvokesConcurrent 会改为强行启动一次全局并发标记。

每个Region有对应的Remembered Set,只记录老年代到年轻代的 别的Region对本Region对象的引用,在写引用的时候阻塞,如果Region改变就把引用信息改到新Region的RSet中

全局并发标记的步骤:
初始标记,同CMS只扫描GC Root直达对象,压入扫描栈,阻塞

并发标记,弹栈,递归扫描对象图,还会扫描Write Barrier记录的引用(这些引用是每次改变引用时的老引用),非阻塞

重新标记,标记每个线程各自的Write Barrier(这个Barrier满了之后会丢到全局的去,而每个线程还有一个没满的Barrier),顺带处理弱引用,阻塞。只扫描SATB Buffer,而不是和CMS一样重新扫描整个GC Root,整个年轻代都会被扫描,可能会很慢

清理,类似于标记清理的清理阶段,但是不清理实际对象,而是计算每个Region的价值,根据用户要求的性能水平( -XX:MaxGCPauseMillis)优先清理价值高的,阻塞,所以如果要求的性能太高,反而容易造成垃圾堆积进而Full GC。

CSet永远包括年轻代,因此G1不维护年轻代出发的引用涉及的RSet更新

记录引用变化的部分

SATB Barrier
G1的设计思路是,一次GC开始时 活的对象认为是活的,并且记录为SATB(理解成快照) ,GC过程中新分配的都当做活对象。

GC中新分配的对象容易找,每个Region会记录两个TAMS指针(top-at-mark-start),此后的对象视为新分配的。

但是全局并发标记的并发标记过程中,由于和其他线程并行执行,会出现这种情况:

首先假设有一个对象的引用没有被标记过,记为A(其实就是白色状态)
前提1:给一个已经标记过、并且所有字段被标记完(黑色状态)的对象的字段赋值为A
前提2:并且所有 字段没有被标记完的引用(灰色状态) 到A的引用被删除了

这时候会出现A明明活着 却没有被标记到的情况,因此G1引入了两个WriteBarrier,在改变引用 前后 都会把老引用记下来,哪怕发生了前提2的事,A也会被标记下来。

Logging Barrier

G1为了尽量避免降低改变引用的性能,改变引用时 其实是将老引用加入一个队列,满了之后会被移到一个全局的SATB队列集合,然后换一个新的空队列。

而并发标记会定期检查全局SATB队列集合,当超过一定量时就把它们全部标记上,并且把它们压到标记栈上等后面进一步标记。

Remember Set

传统GC的
G1给每个Region维护一个Remembered Set,它记录别的Region指向本体的指针,并且这些指针分别在哪些Card Table的范围内。

维护Remembered Set的逻辑在改变引用 做,过滤掉从年轻代出发的引用 涉及的RSet维护。

维护RSet时也会采用Logging Barrier的设计思路,在全局队列集合超过一定量时,会取出若干个队列,并且更新RSet。


ZGC
并不是新货,而是Azul很久之前的Pauseless GC,而不如Zing VM的C4。
所有阶段都可以并发,很容易最大暂停控制在1ms内。

不标记对象,而是标记引用,访问引用时有Read Barrier,消耗读取引用时的性能,而干掉了STW。
Region有多种尺寸,根据对象大小分配
每次清理整个Region,因此没有RSet
支持NUMA,提高整体效率
没有分代(暂不完善,还在考虑是分代还是Thread Local GC作为前端),因此只有PGC的水平,遇到高速分配对象只能调大堆内存来喘息

初始扫描:只扫描全局变量和线程栈的指针,不扫描GC堆的指针
并发标记:递归对象图
移动对象:在移动过程中有forward table记录移动,并且活的对象移走后可以立即被释放,可以被其他扫描过程用来复制
修正指针:在修正时同时进行标记

面经读后感-双亲委派模型和ClassLoader部分源码阅读

发现很多看似艰深的面试题 原来在以前看不懂的书里有讲解
代码写多了,以前看不懂的东西慢慢的能理解了


原题:谈谈双亲委派模型,以及怎么被破坏的
扩展:能否自己写一个java.lang.System


双亲委派模型:

每个类加载器应该有一个成员变量作为父加载器,在加载时先让父加载器加载,父加载器抛出异常再调用本体的findClass()方法。

它是Java设计者推荐的实现,直接体现在抽象类ClassLoaderloadClass()方法里。

好处在于符合Java语言的设计,例如自行编写的java.lang.Object()类必然会被BootStrap加载器加载,并且会报错“找不到方法”。

(即使自己编写类加载器尝试加载,也会由于包名开头是java.而抛出SecurityException,体现在抽象类ClassLoaderpreDefineClass()方法)

重点在于父加载器是成员变量而不是父类,采用的是组合而不是继承。


被破坏:

第一次是由于存在一些老代码。JDK1.2前,自定义类加载器只能覆写loadClass()方法,当时没有这个模型,自然也没有父类优先的逻辑了。

在实现双亲委派时,为了兼容老代码,JDK的做法是添加一个findClass()方法,并且在loadClass()方法中调用,并且提倡用户覆写findClass()方法。

(问题,如果不需要兼容老代码可以怎么做呢?可以直接修改loadClassInternal()方法吗,这么做会有什么后果呢?)

第二次是由于模型缺陷(需求变更),由父加载器加载的代码需要调用子加载器的代码。

书中给的例子是JNDI,JNDI作为Java标准服务,代码在rt.jar中,并且由启动类加载器加载。但是它需要调用应用程序ClassPath的代码。

为了解决问题,Java团队只能引入线程上下文加载器,可以在创建线程时设一个类加载器,就可以逆向请求子加载器了。

第三次是技术迭代,OGSi的类加载器比双亲委派多了很多规则……

面经读后感-双重检查笔记

感觉看面经是个提高自己的好方式……
代码:

1
2
3
4
5
6
7
8
9
10
public static Singleton instance;
public static Singleton getInstance(){
if (instance == null){ //1
synchronized(Singleton.class) { //2
if (instance == null) //3
instance = new Singleton(); //4
}
}
return instance;
}

问题:
双线程同时执行getInstance()方法,线程1执行到第4步,而实例化对象在JVM中分为两步:分配内存+创建对象
如果在创建对象之前,线程2执行到第1步,发现内存已经分配了,返回这个引用就会出现问题。
解决:

1
public volatile static Singleton instance;

加上volatile就可以保证语句的有序性(1.4之前不行),强制实例化对象先创建 再分配内存,其他线程执行到1时,如果instance不是null则对象必然创建完成。

记吃到的Kotlin语法糖总结

最近在着手写一个TornadoFx的项目,接触了之前从未写过的Kotlin,吃到了大量的语法糖,给我一种“到处充满了Lambda”的既视感。

很多东西一个花括号就能搞定,隐藏了很多“当场实现接口”、“覆写方法”的繁琐语法,确实写起来非常的舒服,写篇文章总结一下。

目前来说,我体会到的主要有这些语法糖:

  • 构造块
    替代了构造函数,而Java中的有参构造被移到了类名右边的圆括号内:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//java
public class XXXBo{
public XXXBo(){

}
public XXXBo(int i){

}
}

//kotlin
class XXXBo(var i: Int) {
init{

}
}

顺带提一下延迟加载,Kotlin要求显式声明在声明时不赋值的变量:
(读起来可能有点拗口,意思是默认情况下如果变量声明不赋值,是一个编译期错误)

1
2
//编译错误:Property must be initialized or be abstract
//var bo:XXXBo;

只有加入lateinit 关键字才允许之后在其他地方赋值:

1
lateinit var bo:XXXBo;

延迟加载机制能和DI框架很好的配合,虽然Spring的写法是这样的:

1
var bo:XXXBo by di();

需要和延迟加载区分的一个机制是懒加载,代码是这样的:

1
2
3
val bo by lazy{
XXXBo(param1);
}

注意,懒加载用于val变量,一个非常对口的应用场景就是单例模式,如果在单例模式中使用lateinit虽然也可以实现,但是会增加不必要的逻辑。

  • 分号省略

代码略;每一行代码的分号可以省略,(不知道编译那边是怎么做的……)

  • 类型推断+使用var来声明变量

这个在类名很长的时候兼职就是拯救世界……

1
2
3
4
5
6
//java
XXXBo bo = new XXXBo();
final XXXBo bo = new XXXBo();
//kotlin
var bo = XXXBo();
val bo = XXXBo();
  • 进一步简化的try-with-resource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//java6
try{
InputStream is = FileInputStream(filename);
}catch(Exception e){

}finally{
is.close();
}
//java7+
try (InputStream is = FileInputStream(filename)) {

}catch(Exception e){

}
//kotlin
FileInputStream(filename).use {
//使用it对象来使用这个流本身
var properties:Properties = Properties();
properties.load(it)
}

这个use()方法本质是调用close()方法,见这个回答

  • 对NPE进行了最大限度的消除:强制null检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//java
XXXBo bo = null;
bo.getXXX();//NPE

//kotlin
//编译错误:Null can not be a value of a non-null type XXXBo
//var bo:XXXBo = null;

var bo:XXXBo? = null;

//编译错误:Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type XXXBo
//bo.count;

if(bo !=null){
bo.count;
}
  • 对Get/Set进行最大程度的消除(数据类):

Java里其实有个叫Lombok的东西干了差不多的事,在编译器自动生成Getter Setter equals() toString()这么些方法。
不过Kotlin里调用get/set方法也可以不用方法名,而是直接.属性名。
顺带一提,Kotlin的主方法由于没有static方法,用的是伴生对象,并且方法加入@JvmStatic注解。
不过IDEA似乎没法用psvm来快速生成了,也不知道是复杂了还是简单了(

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//java
public class XXXBo{
private Integer id;
//getter setter
}
psvm{
new XXXBo().getId();
}
//kotlin
data class XXXBo(val id:Int){

}
companion object {
@JvmStatic
fun main(args: Array<String>) {
XXXBo().id;
}
}
  • 语言层面的TODO支持:
1
2
3
4
5
6
7
8
9
10
11
12
13
//java

//TODO xxx
//完全依赖IDE语法高亮,对实际编译的代码来说就是空代码

//kotlin
//标准库提供TODO函数,执行此处代码会抛出一个异常

TODO()

//@kotlin.internal.InlineOnly
//public inline fun TODO(): Nothing = throw NotImplementedError()

  • KDoc(对位JavaDoc)支持Markdown语法

详见链接

jOOQ比起MyBatis的便利之处

单位新项目用了新的持久化框架,叫jOOQ

用惯MyBatis的我一开始有些不适应,但是写过新项目之后,回去维护老项目,确实感觉基于XML的MyBatis编码效率低下。

首先,两边同样有自动生成代码的工具。
MyBatis生成的有Dao层和pojo,需要配置生成哪张表的pojo,(随口一提,公司的生成器配置可能有bug,生成XML时并不会清空之前的XML内容,需要手动清空/删除一次。)

生成的dao提供的方法有:

  • 根据主键获取
  • 插入
  • 可选插入(pojo对应字段不是null的时候才设值)
  • 更新
  • 可选更新(不是null的时候才更新值)
  • 逻辑删除

pojo则是数据库的各字段以及get/set方法,值得一提的是每个字段上会有DDL里的列备注,但是会充斥大量空行,阅读起来较为困难。

而jOOQ的代码生成器一次性生成所有表的dao层和pojo。dao层全部继承了一个叫DAOImpl的抽象类,提供了以下方法:

  • 新增单个pojo
  • 新增多个pojo(可变参数)
  • 新增多个pojo(集合)
  • 用以上三种方式更新、物理删除pojo
  • 根据主键判断是否存在
  • 获取表中记录总数
  • 获取整张表
  • 利用Java8的Optional来获取(大概是允许参数为null?)
  • 获取主键

还有一些看不太懂的方法:

  • private /* non-final */ Condition equal(Field<?>[] pk, Collection<T> ids)
  • private /* non-final */ List<R> records(Collection<P> objects, boolean forUpdate)
  • private /* non-final */ RecordListenerProvider[] providers(final RecordListenerProvider[] providers, final Object object)

而每个表不同的dao实现类也有各自的方法:

  • 根据主键(项目里就是id)获取
  • 根据多个id获取多个记录(可变参数)
  • 根据每个唯一索引获取记录
  • 根据每个列获取多条记录,也支持可变参数

比MyBatis丰富很多,唯一的缺陷是没有自动生成的逻辑删除方法,初次维护项目很容易根据直觉使用delete方法,需要自动生成Service层代码进行封装。

生成的pojo则兼容了JPA的注解(目前没有在项目里用上),这方面有点hibernate的画风? 较MyBatis生成的pojo要紧凑很多,少了很多无谓的空行。
当业务变化,建立新表,jOOQ生成的代码可以快速满足大部分业务。

说完自动生成的代码,接下来谈谈编写业务代码的复杂度。
贴一段使用MyBatis的传统项目里的单表分页查询代码:

首先需要编写XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="对应的dao接口类名">
<select id="selectByPageQuery"
resultMap="自动生成的Mapper里的ResultMap">
select * from 表
where is_deleted = 0
order by id desc
LIMIT #{itemIndex}, #{pageSize}
</select>

<select id="countByPageQuery" resultType="java.lang.Integer">
select count(*) from 表
where is_deleted = 0
LIMIT #{itemIndex}, #{pageSize}
</select>
</mapper>

由于多个Mapper需要公用LIMIT #{itemIndex}, #{pageSize} 这段语句(还有一些权限控制的语句),事实上代码是这样的:

1
<include refid="com.***.dao.mapper.fragment.PageMapper.pageLimit"/>

所以无法使用注解方式的SQL。
接下来是接口:

1
2
3
4
public interface 表SimpleSelectDao {
int countByPageQuery(PageQuery query);
List<自动生成的实体类> selectByPageQuery(PageQuery query);
}

其中PageQuery是公司封装的分页类,不贴代码了,可以从SQL里看出用到的字段。
然后再在Service层使用:

1
2
3
4
5
...
int count = parcelTransferRecordSimpleSelectDao.countByPageQuery(query);
query.setItemTotal(count);
List<TxParcelTransferRecord> records = parcelTransferRecordSimpleSelectDao.selectByPageQuery(query);
...

相对于基于XML的MyBatis,jOOQ在业务变动时编写代码的效率更高,同样是分页获取代码,只需要7行:

说明:
Pageable是Spring 5的分页相关类,专门解决从前端传参到数据库查询的分页问题,不需要自己实现分页框架了。
Tuple2是公司内部实现的数据结构,用来返回两个不同类型的数据。
SelectConditionStepdsl是jOOQ提供的类和对象,其中dsl的类是DSLContext,用于执行SQL,改写生成器后可以使用Spring注入。

1
2
3
4
5
6
7
8
9
public Tuple2<List<表实体类>, Integer> pageVoById(Pageable pageable) {
SelectConditionStep<表Record类> step = dsl.selectFrom(表的枚举)
.where(表的枚举.IS_DELETED.eq(false));
List<表实体类> list = step.orderBy(表的枚举.ID.desc())
.limit((int) pageable.getOffset(), pageable.getPageSize())
.fetchInto(表实体类.class);
int count = dsl.fetchCount(step);
return new Tuple2<>(list, count);
}

这段代码出现在Service层,用惯之后很符合人类直觉,发挥了SQL人类可读性好的优势。

jOOQ封装了sql,将所有的字段和SQL语句化为Java代码,当然也可以传入完整或者部分的sql执行,自由度很高,同时有Java强类型的优势加持,屏蔽掉了MyBatis的ResultMapper环节,每一步SQL执行虽然要使用一些复杂的类和对象(例如SelectConditionStep),对习惯使用强类型语言的java码狗来说很有“安全感”。

相比jOOQ,基于XML的MyBatis代码编写繁琐,手写SQL经常漏掉order by id desc(页面上刚刚新增的记录在最前面)和is_deleted = 0(其实这是低级失误),查找SQL需要跳转两次(这还是装了IDE插件的情况),在多表查询时还要编写复杂的ResultMap,体验确实差很多。

记一次IDEA项目报红

今天上午,IDEA突然提示Maven的依赖有更新,点下Enable Auto-Import之后,项目中所有引入的依赖全部报红。
尝试点击Re-Import无效,重装IDEA,删除.idea文件夹,重新Clone项目均无效。

后来同事说远程Maven仓库没法访问,同时在调节设置时发现,在设置中指定了Maven路径(下载公司内部依赖库需要定制的Maven,其实就是改过配置)后,本地Repo路径会自动变成一个不存在的路径,改为~/.m2/repository之后,Re-Import后开始下载jar包,最后恢复正常。

记录一次调通七牛云存储接口的经历

换了新东家,单位配了Mac,由于hexo只会推送生成好的HTML,没有存Markdown源文件,没法写博客(强行给偷懒找借口)。

最近需要同时开多个项目,低压U承受不住,平时操作卡顿,因此换回台式。

新项目中有用到七牛云存储,需求是给存储的图片、视频加上水印,视频需要有缩略图。

于是开始研究七牛云的文档,历时两天终于调通接口,特此记一笔。

调图像水印的接口时遇到的问题是我的低级失误,某个Config类输出七牛云格式的参数时 忘记输出StringBuilder.toString()了,而是用了默认的null。

调视频水印的时候,七牛云有一个坑,调用预处理持久化接口生成带水印的视频,并且使用saveas命令覆盖原视频文件后,无法再用vthumb命令一次性同时创建水印图片,必须请求两次。。

遇到的问题则是由于我忽视了视频转码需要时间的问题,上传成功后在文件管理里没有看到带水印的视频,就以为参数错误,直接把文件删了…

直到我用了七牛云存储实验室,在查询结果时看到了正在转码的信息,才意识到确实需要创建一个私有的多媒体处理队列。

虽说浪费了时间,但是熟悉了七牛云转码和水印相关接口,而且是SpringBoot环境下的应用,算是一段有价值的经历吧。

图解白菜用到的循环队列

实现了用循环队列存QQ消息后,就七八个月没有管(能用的代码才是好代码),以至于写上简历被问到之后支支吾吾说不出……

这个队列比较特殊,只有入队和遍历,没有写出队(业务用不到)。
代码如下(CqMsg为反序列化出来的实体类,记载着QQ号、消息体、发送时间等信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class MsgQueue {

private int start = 0;
private int end = 0;
private int len = 0;
private int N=100;

private CqMsg[] msgs = new CqMsg[N];
public MsgQueue(){}
public MsgQueue(int N) {
this.N=N;
msgs = new CqMsg[N];
}
public void addMsg(CqMsg msg) {
len++;
if (len >= N) {
len = N;
start++;
}
if (end == N) {
end = 0;
}
if (start == N) {
start = 0;
}
msgs[end] = msg;
this.msg = msg;
end++;
}


public ArrayList<CqMsg> getMsgsByQQ(Long QQ) {
ArrayList<CqMsg> result = new ArrayList<>();
if (start < end) {
for (int i = 0; i < end; i++) {
if (QQ.equals(msgs[i].getQQ())) {
result.add(msgs[i]);
}
}
} else {
for (int i = end; i < msgs.length; i++) {
if (QQ.equals(msgs[i].getQQ())) {
result.add(msgs[i]);
}
}
for (int i = 0; i < start - 1; i++) {
if (QQ.equals(msgs[i].getQQ())) {
result.add(msgs[i]);
}
}
}
return result;

}
}

一开始我创建了一个数组(容量由构造器指定),同时定义两个整数作为坐标变量,再加一个表示队列当前长度的变量,大概是这样的:

1
length = 0
null null null null null null
Start
End

之后每当收到新消息,都将消息插入到end坐标上,之后将end++,同时length++,直到数组装满。

在数组装满后,队列是这样的:

1
length = 5
消息1 消息2 消息3 消息4 消息5 消息6
Start
End

当有下一条消息进入队列时,尝试将队列长度增加:

1
length++

随即满足下方if语句块的判定,将length重置为数组长度,并且将起始点右移:

1
2
3
4
if (len >= N) {
len = N;
start++;
}
1
length = 5

进入下一个if块:

1
2
3
if (end == N) {
end = 0;
}

最后把结果插入到end上,之后end++:

消息7 消息2 消息3 消息4 消息5 消息6
Start
End

不断的插入消息后,Start也会达到数组最右端,此时的队列如图:

消息7 消息8 消息9 消息10 消息11 消息6
Start
End

此时插入消息的逻辑如下:

首先依旧将length重置为当前数组长度;

但是不会进第二个if块,直接进第三个:

1
2
3
if (start == N) {
start = 0;
}

这之后执行end++,会回到数组第一次装满时候队列的样子:

消息7 消息8 消息9 消息10 消息11 消息12
Start
End

每次遍历时,只需要从end→length,再从0→start就行,是一次O(n)的操作。

而插入时,则只需要O(1)(判断几个数字的大小、给数组某个位置赋值)。

如改用纯数组实现,在数组满后,插入消息需要O(n)的时间。

若改用链表,则可以使用迭代器达到与循环队列相同的复杂度:

有新消息时,丢弃头部消息,在尾部追加消息,也是O(1)的操作。

遍历时, 并不使用传统的for+get方法(每次获取链表的某个位置的值代价都是O(n)),而是使用迭代器,可以达到O(n)的时间复杂度。