diff --git a/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/CopyOnWriteArrayListCacheStore.java b/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/CopyOnWriteArrayListCacheStore.java
new file mode 100644
index 0000000..23fb17f
--- /dev/null
+++ b/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/CopyOnWriteArrayListCacheStore.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2020 LamGC
+ *
+ * ContentGrabbingJi is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * ContentGrabbingJi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package net.lamgc.cgj.bot.cache.local;
+
+import net.lamgc.cgj.bot.cache.ListCacheStore;
+
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 基于 {@link CopyOnWriteArrayList} 的有序列表缓存存储容器.
+ * @param 元素类型.
+ * @author LamGC
+ */
+public class CopyOnWriteArrayListCacheStore extends LocalCollectionCacheStore> implements ListCacheStore {
+
+ @Override
+ public E getElement(String key, int index) {
+ List itemCollection = getCacheItemCollection(key, false);
+ try {
+ return itemCollection == null ? null : itemCollection.get(index);
+ } catch (IndexOutOfBoundsException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public List getElementsByRange(String key, int index, int length) {
+ int listLength = elementsLength(key);
+ if (listLength == -1) {
+ return null;
+ }
+ List itemCollection = getCacheItemCollection(key, false);
+ List result = new ArrayList<>();
+
+ try {
+ ListIterator iterator = itemCollection.listIterator(index);
+ for (int i = 0; i < length && iterator.hasNext(); i++) {
+ result.add(iterator.next());
+ }
+ } catch (IndexOutOfBoundsException ignored) {
+ // 正常情况来讲, 该 try-catch 块只有 listIterator 会抛出 IndexOutOfBoundsException,
+ // 而一旦抛出 IndexOutOfBoundsException, 就代表 index 溢出了, try 块后面代码没有继续执行,
+ // 既然抛出异常时, result 并没有添加任何元素, 为何要再 new 一个 List 浪费内存呢? :D
+ }
+ return result;
+ }
+
+ @Override
+ public boolean removeElement(String key, int index) {
+ List itemCollection = getCacheItemCollection(key, false);
+ if (itemCollection != null) {
+ try {
+ itemCollection.remove(index);
+ return true;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected List getCacheItemCollection(String key, boolean create) {
+ Objects.requireNonNull(key);
+ Map>> cacheMap = getCacheMap();
+ if (!cacheMap.containsKey(key)) {
+ if (create) {
+ cacheMap.put(key, new CacheItem<>(new CopyOnWriteArrayList<>()));
+ } else {
+ return null;
+ }
+ }
+ return cacheMap.get(key).getValue();
+ }
+}
diff --git a/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/LocalCollectionCacheStore.java b/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/LocalCollectionCacheStore.java
new file mode 100644
index 0000000..cb6f3a4
--- /dev/null
+++ b/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/LocalCollectionCacheStore.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 LamGC
+ *
+ * ContentGrabbingJi is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * ContentGrabbingJi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package net.lamgc.cgj.bot.cache.local;
+
+import net.lamgc.cgj.bot.cache.CollectionCacheStore;
+
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * 本地集合缓存存储容器.
+ * @param 元素类型
+ * @author LamGC
+ * @see net.lamgc.cgj.bot.cache.CacheStore
+ * @see net.lamgc.cgj.bot.cache.CollectionCacheStore
+ */
+public abstract class LocalCollectionCacheStore>
+extends HashCacheStore
+implements CollectionCacheStore {
+
+ /**
+ * 获取缓存项集合对象.
+ * @param key 缓存项键名
+ * @param create 如果不存在, 是否创建.
+ * @return 如果不存在且 create 为 false, 或添加失败, 返回 false, 添加成功返回 true.
+ */
+ protected abstract C getCacheItemCollection(String key, boolean create);
+
+ @Override
+ public boolean addElement(String key, E element) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(element);
+ Collection itemCollection = getCacheItemCollection(key, true);
+ return itemCollection.add(element);
+ }
+
+ @Override
+ public boolean addElements(String key, Collection elements) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(elements);
+ Collection itemCollection = getCacheItemCollection(key, true);
+ return itemCollection.addAll(elements);
+ }
+
+ @Override
+ public boolean containsElement(String key, E value) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(value);
+ Collection itemCollection = getCacheItemCollection(key, false);
+ if (itemCollection == null) {
+ return false;
+ }
+ return itemCollection.contains(value);
+ }
+
+ @Override
+ public boolean isEmpty(String key) {
+ Collection itemCollection = getCacheItemCollection(Objects.requireNonNull(key), false);
+ if (itemCollection == null) {
+ return false;
+ }
+ return itemCollection.isEmpty();
+ }
+
+ @Override
+ public int elementsLength(String key) {
+ Collection itemCollection = getCacheItemCollection(Objects.requireNonNull(key), false);
+ if (itemCollection == null) {
+ return -1;
+ }
+ return itemCollection.size();
+ }
+
+ @Override
+ public boolean clearCollection(String key) {
+ Collection itemCollection = getCacheItemCollection(Objects.requireNonNull(key), false);
+ if (itemCollection == null) {
+ return false;
+ }
+ itemCollection.clear();
+ return true;
+ }
+
+ @Override
+ public boolean removeElement(String key, E element) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(element);
+ Collection itemCollection = getCacheItemCollection(key, false);
+ if (itemCollection == null) {
+ return false;
+ }
+ return itemCollection.remove(element);
+ }
+
+}
diff --git a/ContentGrabbingJi-CacheStore-local/src/test/java/net/lamgc/cgj/bot/cache/local/ListCacheStoreTest.java b/ContentGrabbingJi-CacheStore-local/src/test/java/net/lamgc/cgj/bot/cache/local/ListCacheStoreTest.java
new file mode 100644
index 0000000..910689d
--- /dev/null
+++ b/ContentGrabbingJi-CacheStore-local/src/test/java/net/lamgc/cgj/bot/cache/local/ListCacheStoreTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2020 LamGC
+ *
+ * ContentGrabbingJi is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * ContentGrabbingJi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package net.lamgc.cgj.bot.cache.local;
+
+import com.google.common.collect.Lists;
+import net.lamgc.cgj.bot.cache.ListCacheStore;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * @see CopyOnWriteArrayListCacheStore
+ * @see LocalCollectionCacheStore
+ */
+public class ListCacheStoreTest {
+
+ @Test
+ public void nullThrowTest() {
+ final ListCacheStore cacheStore = new CopyOnWriteArrayListCacheStore<>();
+
+ // LocalCollectionCacheStore
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.addElement(null, "testValue"));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.addElement("testKey", null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.addElements(null, new ArrayList<>()));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.addElements("testKey", null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.elementsLength(null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.containsElement(null, "testValue"));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.containsElement("testKey", null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.isEmpty(null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.clearCollection(null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.removeElement(null, "testValue"));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.removeElement("testKey", null));
+
+ // CopyOnWriteArrayListCacheStore
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.getElement(null, 0));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.getElementsByRange(null, 0, 0));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.removeElement(null, 0));
+
+ }
+
+ @Test
+ public void notExistCacheTest() {
+ final ListCacheStore cacheStore = new CopyOnWriteArrayListCacheStore<>();
+ final String key = "testKey";
+ Assert.assertFalse(cacheStore.clearCollection(key));
+ Assert.assertFalse(cacheStore.isEmpty(key));
+ Assert.assertEquals(-1, cacheStore.elementsLength(key));
+ Assert.assertFalse(cacheStore.containsElement(key, "testValue"));
+ Assert.assertFalse(cacheStore.removeElement(key, "testValue"));
+ }
+
+ @Test
+ public void addAndGetTest() {
+ final ListCacheStore cacheStore = new CopyOnWriteArrayListCacheStore<>();
+ final String key = "test01";
+ List numbers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9);
+ // getElement/getElementsByRange Cache不存在测试
+ Assert.assertNull(cacheStore.getElement(key, 0));
+ Assert.assertNull(cacheStore.getElementsByRange(key, 0, 1));
+
+ // addElement/getElement 正常情况测试
+ Assert.assertTrue("addElement operation failed!", cacheStore.addElement(key, 0));
+ Assert.assertEquals(new Integer(0), cacheStore.getElement(key, 0));
+ // 超出范围的 null 测试
+ Assert.assertNull(cacheStore.getElement(key, cacheStore.elementsLength(key)));
+
+ // addElements/getElementsByRange 正常情况测试
+ Assert.assertTrue("addElements operation failed!", cacheStore.addElements(key, numbers));
+ Assert.assertEquals(Lists.newArrayList(0, 1, 2), cacheStore.getElementsByRange(key, 0, 3));
+
+ // 不足长度的 getElementsByRange
+ Assert.assertEquals(Lists.newArrayList(7, 8, 9), cacheStore.getElementsByRange(key, 7, 8));
+
+ // 超出索引的 getElementsByRange
+ List result = cacheStore.getElementsByRange(key, cacheStore.elementsLength(key) + 1, 8);
+ Assert.assertNotNull("getElementsByRange returned null if index is out of range", result);
+ Assert.assertEquals("getElementsByRange returned a non empty list when the index was out of range",
+ 0, result.size());
+
+ // 不足长度的 getElementsByRange
+ Assert.assertEquals(Lists.newArrayList(), cacheStore.getElementsByRange(key, cacheStore.elementsLength(key), 0));
+ }
+
+ @Test
+ public void removeElementTest() {
+ // removeElement(String, E) / removeElement(String, int)
+ final ListCacheStore cacheStore = new CopyOnWriteArrayListCacheStore<>();
+ final String key = "test01";
+ Random random = new Random();
+ List numbers = Lists.newArrayList("1", "2", "3", "4", "5", "6", "7", "8", "9");
+
+ // 删除不存在 Cache 返回 false
+ Assert.assertFalse(cacheStore.removeElement(key, 0));
+
+ Assert.assertTrue("addElements operation failed!", cacheStore.addElements(key, numbers));
+
+ int removeIndex = random.nextInt(cacheStore.elementsLength(key));
+ numbers.remove(removeIndex);
+ Assert.assertTrue("removeElement operation failed!", cacheStore.removeElement(key, removeIndex));
+ Assert.assertEquals(numbers, cacheStore.getElementsByRange(key, 0, cacheStore.elementsLength(key)));
+
+ String removeTarget = cacheStore.getElement(key, random.nextInt(cacheStore.elementsLength(key)));
+ Assert.assertNotNull(removeTarget);
+ Assert.assertTrue(cacheStore.containsElement(key, removeTarget));
+ numbers.remove(removeTarget);
+ Assert.assertTrue("removeElement operation failed!", cacheStore.removeElement(key, removeTarget));
+ Assert.assertEquals(numbers, cacheStore.getElementsByRange(key, 0, cacheStore.elementsLength(key)));
+
+ Assert.assertTrue("clearCollection operation failed!", cacheStore.clearCollection(key));
+ Assert.assertTrue(cacheStore.exists(key));
+ Assert.assertEquals(0, cacheStore.elementsLength(key));
+ Assert.assertTrue(cacheStore.isEmpty(key));
+
+ // 删除不存在元素返回 false
+ Assert.assertFalse(cacheStore.removeElement(key, cacheStore.elementsLength(key)));
+ }
+
+}
\ No newline at end of file