Collection 翻下詞典,有許多含義:
收集;聚集;(常指同類的)收藏品;募捐,募集;作品集;聚積;取走;一群人;拿走;(常為季節性推出的)系列時裝(或家用品);一批物品
選擇“集合”作為翻譯名,我覺得可行,除非我們現在重新創造一個漢語片語。
對於CRUD和非CRUD,集合都是一個無比重要的東西,因為計算機的本質是對資訊的處理。
資訊一般不是單個,是一堆,一堆堆,一塊塊,一個個....
網上關於集合的資料無比多,所以本文主要是做一個簡要的介紹,並新增一些注意事項和個人感悟。
一、簡介
不過Collection的子孫過於多,用現有詞彙命名這些子孫並不容易,有待建立新的詞彙。
常用知名子孫有:
List -- 列表,javaDoc的釋義是:有序集合。
--ArrayList 動態大小列表 ,這是crud中最常用的型別 。不保證順序
--LinkedList 雙鏈列表,可以固定成員順序。本身實現了Deque的介面,可用於輔助實現FiLo的演算法
Set - 無重複集合,允許有一個null成員
---TreeSet 有序集合
-- HastSet 雜湊集合 ,主要是操作的效能好一些
-- LinkedHashSet 雙向鏈雜湊集合,保持了插入順序,又具有對應的效能
Queue -佇列
--Deque 雙端操作佇列。它有一個著名的實現 LinkedList
Buffer --緩衝
不過這個主要是阿帕奇的實現org.apache.commons.collections.Buffer,算不得java的基礎型別
如果是初級程式設計師,或者以CRUD為主的,那麼只要學些掌握ArrayList就差不多了,因為現在的大部分的ORM或者JDBC的上級實現都適用ArrayList來儲存資料集。
二、集合的基本方法
僅僅介紹Collection的介面方法,為了便於理解,以LinkedList為例子。
這些方法都極其簡單,也沒有什麼特別好解釋的,直接上例子吧!
package study.base.types.collection.list; import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; /** * 演示Collection介面的基本操作和LinkedList的一些典型操作 * @author lto */ public class TestLinkedList { private LinkedList<MoneyJar> list; private String[] givers = new String[]{"爸爸","媽媽","哥哥","姐姐","爺爺","奶奶"}; private Random random = new Random(); private Map<String,Long> realGivers; public TestLinkedList(int size) { this.list = new LinkedList<>(); this.realGivers = new HashMap<>(); //插入100個MoneyJar,金額和日期都是隨機的,giver是隨機 for (int i = 0; i < size; i++) { String giver = givers[random.nextInt(givers.length)]; int amount = random.nextInt(100); this.list.add(new MoneyJar(giver, amount, new Date())); } //按照giver分組統計個數,並賦值給realGivers this.list.stream().collect(Collectors.groupingBy(MoneyJar::giver, Collectors.counting())).forEach((k,v)->{ realGivers.put(k,(long)v);}); //列印realGivers this.realGivers.forEach((k,v)->{System.out.println(String.format("%s共有%d個", k, v));}); } public void count(){ long start = System.currentTimeMillis(); final long[] total = {0}; this.list.spliterator().forEachRemaining(mj-> total[0] += mj.amount()); System.out.println(String.format("總共%d元",total[0])); System.out.println("耗費時間:"+(System.currentTimeMillis()-start)); } public void sortByAmount(){ this.list.sort((o1, o2) -> o1.amount().compareTo(o2.amount())); } /** * 統計每個giver給的錢,並列印結果 */ public void sumByGiver(){ System.out.println("--------------------****************-----------------------------"); //根據giver分組統計每個giver給的錢,並返回一個ListMap long start = System.currentTimeMillis(); Map<String, Integer> result= this.list.stream().collect(Collectors.groupingBy(MoneyJar::giver, Collectors.summingInt(MoneyJar::amount))); //列印統計結果 result.forEach((k,v)->{System.out.println(String.format("%s給的錢是%d", k, v));}); System.out.println("耗費時間:"+(System.currentTimeMillis()-start)); //採用for迴圈的方式,分組統計 System.out.println("採用for迴圈的方式,分組統計-----------------------------"); long start1 = System.currentTimeMillis(); Map<String,List<Integer>> result1= new HashMap<>(); //初始化result1,把realGivers的每個元素作為key,初始值為0 this.realGivers.forEach((k,v)->{ result1.put(k,new ArrayList<>()); }); //遍歷list,計算每個giver給的錢 for (MoneyJar moneyJar : list) { result1.get(moneyJar.giver()).add(moneyJar.amount()); } //根據result1的成員個數,建立對應的執行緒,然後線上程中計算每個giver給的錢,並計算總和 int numThreads=result1.size(); CountDownLatch latch = new CountDownLatch(numThreads); ExecutorService executor = Executors.newFixedThreadPool(numThreads); result1.forEach((k,v)->{ Runnable worker = () -> { try { long sum=0; for (int i : v) { sum+=i; } System.out.println(String.format("%s給的錢是%d", k, sum)); } finally { latch.countDown(); // 計數減一 } }; // 使用executor提交任務,而不是直接啟動Thread executor.submit(worker); }); try { // 等待所有執行緒完成 latch.await(); System.out.println("All threads have finished."); } catch (InterruptedException e) { e.printStackTrace(); } // 關閉executor,釋放資源 executor.shutdown(); System.out.println("耗費時間:"+(System.currentTimeMillis()-start1)); } public void splitToSum(){ //把錢罐的錢分為n份,分別統計,然後再合併總的金額,並統計耗費時間 System.out.println("-- 採用並行流的方法"); long start = System.currentTimeMillis(); Long total=list.parallelStream().mapToLong(MoneyJar::amount).sum(); System.out.println("耗費時間:"+(System.currentTimeMillis()-start)); System.out.println("總金額是"+total.toString()); //採用傳統的for迴圈方式累積 System.out.println("-- 採用傳統的for迴圈的方法"); start = System.currentTimeMillis(); long sum=0; for (MoneyJar moneyJar : list) { sum+=moneyJar.amount(); } System.out.println("總金額是"+sum); System.out.println("耗費時間:"+(System.currentTimeMillis()-start)); } /** * 把小於等於指定的金額的錢都清理掉 * @param amount */ public void purgeSmallMoney(int amount){ this.list.removeIf(moneyJar -> moneyJar.amount()<=amount); } record MoneyJar(String giver,Integer amount, Date putDay){ } public static void main(String[] args) { //當10萬個的時候,並行的速度反而是for的3倍左右。 TestLinkedList test = new TestLinkedList(200); test.splitToSum(); test.sortByAmount(); System.out.println("-- 排序後 -----"); for (MoneyJar moneyJar : test.list) { System.out.println(moneyJar); } //測試100萬的情況 TestLinkedList test100 = new TestLinkedList(1000000); test100.splitToSum(); //測試2000萬的情況 TestLinkedList test1000 = new TestLinkedList(20000000); test1000.splitToSum(); //以上三個例子,哈無例外,都是簡單的迴圈勝出。那麼parametrizedStream的效率就值得懷疑了。 //是否因為沒有正確設定並行度,還是計算機的環境存在問題 test1000.sumByGiver(); test1000.count(); } }
以上例子並沒有測試每一個介面方法,是因為有些太簡單不值得浪費篇幅。
三、並行處理和流處理
在J8之前,如果把一個集合,以ArrayList為例子,進行並行處理,那麼必須自己來動手,過程可能是這樣的:
1.分隔集合為n個子集
2.建立n個執行緒,用於分別處理n個子集
3.如果需要合併處理,還需要特定注意執行緒的等待和合並
寫起來還是相對比較麻煩的。當然,現在藉助於ai,沒有那麼複雜。但和J8之後提供的特性相比,自然還是麻煩一些。
至於流,更不用說了,J8之前並沒有這個概念。
在JDK17中,可以看到Collection介面和併發以及流有關的方法:
default Stream<E> parallelStream() { return StreamSupport.stream(spliterator(), true); } @Override default Spliterator<E> spliterator() { return Spliterators.spliterator(this, 0); } default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); }
這三個都是預設方法,可以直接使用
parallelStream可以提供並行流處理。
--
根據已知的一些報告和我幾次不是很嚴謹的測試,Stream和for相比並沒有什麼優勢。
由此可以得出一個不是很嚴謹的結論:
在相當大的業務場景(crud為主的資訊系統)中,甚至可以說,在大部分的業務場景中,Stream其實居於下風。
stream的作用僅僅是為了節約工程師的精力和體力。
只有資料集巨大,且cpu充足的情況下,例如千萬級別左右,並行流才會有一些可見的優勢。但是,又有多少面向
企業基別的資訊系統,會在應用級別這樣瘋狂地處理千萬級別的資料,難道不怕jvm爆了嗎?
用資料庫的集合運算功能不是更好更簡單嗎?
四、工具類
4.1官定工具- Collections
這是集合最重要的工具類。
全路徑:java.util.Collections
需要特別申明的是,Collections不僅僅會處理Colletion的子子孫孫,也會處理Map,所以不能被它的名稱騙了。
由於存在JAVADOC,且這個Colllections的成員巨多,所以不逐一列出,避免浪費篇幅。
Collections方法大體包含三類:
1.運算
例如排序(sort)、翻轉(reverse)、打亂(shuffle)、交換元素(swap)、填充元素(fill),經典聚集(min,max),集合運算等等
其中和經典集合運算有關的:
frequency -頻率
disjoint-判斷是否有交集
總之,結合Collecion自身的實現和Collections工具,要實現兩個集合的並集、交集、差集、是否包含等等都是可以的,只不過有點麻煩。
2.構造特定型別的物件
a.不可修改集合(含map)
b.執行緒同步集合(含map)
c.鎖定型別集合
d.空集合(無元素集合)
e.單元素集合(Singleton)
前四個都容易理解,最後一個Singleton有點迷惑,就是為了返回只有一個成員的集合?
3.其它雜項
諸如複製、替換等等。
不過沒有提供深度複製的方法。
4.2阿帕奇集合工具(CollectionUtils)
相比java自帶的集合工具,阿帕奇的工具主要集中在以下幾個用途:
1.集合運算
這個比java官方的強大多了,所以還是用這個把。看看都有什麼:
union(並集),intersection(交集),disjunction(!交集,或者獨立並集),substract(移除子集),containAny(是否有交集)
isSubCollection(是否子集),isEqualCollection(是否相等),retainAll(交集),以及其它。
注意:retainAll和intersection都可以用於獲取交集,但是二者還是有明顯區別的,後者(intersection)會給出不重複的結果,而前者(retainAll)會給出重複的結果。
以下是關於這些本人重視的集合運算方法的示例:
public void testApacheCollectionUtils(){ List<Integer> me = Arrays.asList(90, 80, 70,90,92,88); List<Integer> mother = Arrays.asList(90, 80, 70,90,92,88); List<Integer> auntScore = Arrays.asList(90, 80, 70,90,92,88); List<Integer> fatherScore = Arrays.asList(99, 81, 71,90,98,88); List<Integer> 趙雲 = Arrays.asList(90,80); List<Integer> 崔顥 = Arrays.asList(77); List<Integer> myNewScore = (List<Integer>) CollectionUtils.union(me, 趙雲); System.out.println("我和趙雲的合併∪="+myNewScore); List<Integer> myIntersectionScore = (List<Integer>) CollectionUtils.intersection(me, fatherScore); System.out.println("我和爸爸交集="+myIntersectionScore); //差集 List<Integer> myDifferenceScore = (List<Integer>) CollectionUtils.subtract(me, fatherScore); System.out.println("我和爸爸的差集="+myDifferenceScore); //非公共部分 List<Integer> myDisJointScore = (List<Integer>) CollectionUtils.disjunction(me, fatherScore); System.out.println("我和爸爸的非公共部分="+myDisJointScore); //我和爸爸是否有交集 if(!CollectionUtils.containsAny(me, fatherScore)) { System.out.println("我和爸爸沒有交集"); } else { System.out.println("我和爸爸有交集"); } //我和崔顥是否有交集 if(!CollectionUtils.containsAny(me, 崔顥)) { System.out.println("我和崔顥沒有交集"); } else { System.out.println("我和崔顥有交集"); } //我和趙雲的交集 List<Integer> myIntersectionScore2 = (List<Integer>) CollectionUtils.retainAll(me, 趙雲); System.out.println("我和趙雲的交集(retainAll)="+myIntersectionScore2); System.out.println("我和趙雲的交集(inter)="+ CollectionUtils.intersection(me, 趙雲)); //和崔顥Score的交集 List<Integer> myIntersectionScore3 = (List<Integer>) CollectionUtils.retainAll(崔顥, 趙雲); System.out.println("和崔顥的交集="+myIntersectionScore3); //趙雲是否是me的子集 if(CollectionUtils.isSubCollection(趙雲, me)) { System.out.println("趙雲是me的子集"); } else { System.out.println("趙雲不是me的子集"); } //崔顥Score是否是me的子集 if(CollectionUtils.isSubCollection(崔顥, me)) { System.out.println("崔顥是me的子集"); } else { System.out.println("崔顥不是me的子集"); } //媽媽和阿姨是否一致 if(CollectionUtils.isEqualCollection(mother, auntScore)) { System.out.println("媽媽和阿姨一致"); } else { System.out.println("媽媽和阿姨不一致"); } }
輸出結果:
我和趙雲的合併∪=[80, 70, 88, 90, 90, 92] 我和爸爸交集=[88, 90] 我和爸爸的差集=[80, 70, 90, 92] 我和爸爸的非公共部分=[80, 81, 98, 99, 70, 71, 90, 92] 我和爸爸有交集 我和崔顥沒有交集 我和趙雲的交集(retainAll)=[90, 80, 90] 我和趙雲的交集(inter)=[80, 90] 和崔顥的交集=[] 趙雲是me的子集 崔顥不是me的子集 媽媽和阿姨一致
2.元素處理
find,filter,exists,countMatches、select、collect、get、
3.構造特定型別集合
- synchronizedCollection
- unmodifiableCollection
- predicatedCollection
- typedCollection
需要注意的是,這裡的幾個方法,個人傾向於少用,儘量用java標準的Collections。
4.雜項
isEmpty,isNotEmpty,cardinality...
4.3其它雜項工具
現在工具有點氾濫了。這是因為複製工具程式碼已經很簡單,再加上實在有一些個性化的需要,所以越做越多。
Spring有,JSON有,mybatis有...
這些已經氾濫的就不提了,它們主要用於一些極其個性化的,或者自認為更有效率更安全(存疑)。
4.4 小結
為安全起見,我個人都是儘量用官方的Collections和阿帕奇的CollectionUtils。
從工程角度出發,儘量少依賴也是一個大體正確的選擇。
其它的不是萬不得已不要用。當然各個組織也完全可以自行建立工具。
只不過,這兩個工具集已包含絕大部分集合有關的操作,再結合Stream和Colllection自有的功能,應該很夠用了。
五、CRUD和集合
編寫crud的時候,我們可能會常常使用以下幾種基於jdbc的方式建立集合:
1.使用基於jdbc的orm,例如典型的mybatis
2.基於sping的jdbcTemplate
實際是對原生jdbc的封裝
3.基於原生jdbc
現在已經很少人用jpa來訪問處理資料。
在絕大部分CRUD專案中,一般都用mytabis之類的Orm
所以,這裡主要討論mybatis(或者類似的框架工具即可)。
當返回集合的時候,mytais支援返回List(ArrayList),Set ,對這兩個型別的支援是很友好的。
以下是方法(org.apache.ibatis.jdbc.SqlRunner#getResults,selectAll)的部分
public List<Map<String, Object>> selectAll(String sql, Object... args) throws SQLException {
try (PreparedStatement ps = connection.prepareStatement(sql)) {
setParameters(ps, args);
try (ResultSet rs = ps.executeQuery()) {
return getResults(rs);
}
}
}
private List<Map<String, Object>> getResults(ResultSet rs) throws SQLException { List<Map<String, Object>> list = new ArrayList<>(); ..... while (rs.next()) { Map<String, Object> row = new HashMap<>(); for (int i = 0, n = columns.size(); i < n; i++) { String name = columns.get(i); TypeHandler<?> handler = typeHandlers.get(i); row.put(name.toUpperCase(Locale.ENGLISH), handler.getResult(rs, name)); } list.add(row); } return list; }
可以看出,在mybatis的底層是用ArrayList來承接原生資料集的結果的。用ArrayList是因為一個效能較好,另外一個是因為集合的數量不可測的緣故。
在不考慮極端效能的要求下,用mybatis還是不錯的,因為它提供了主要的型別轉換和spring的整合。
很少有人考慮使用LinkedList等其它集合來承接資料即可。
由於List實現了Collection介面,所以可以使用mybatis在獲得List之後,再做流處理。
六、適用場景和挑戰
集合的子孫巨多,有不同的業務場景對應,以最常見的來說:
ArrayList -- crud,隨機訪問效能高。但crud很少隨機訪問某個,一般都丟到前端處理了。
如前,Colletions提供了大量構建特定用途的集合的方法,可以讓動態列表用於執行緒安全等場景。
LinkedList -- 雙向連結串列用途很廣,一般不CRUD的時候,常常會考慮用它,它的優缺點:
頻繁進行插入和刪除更高效;可以用作用作棧(Stack)和佇列(Queue);保持元素插入順序的場景;實現雙向遍歷
缺點:隨機訪問慢
Set -大量的非crud的,需要保持元素唯一的情況
Queue -佇列,主要用於需要堆疊操作的情況
再結合執行緒同步、不可修改、指定型別等等,可以細分為更多的子場景。
由於子孫太多,如果個人對每個型別的優缺點不是太明白,那麼至少要知道大類的適用場景,然後再檢視javaDoc/ai即可。
6.1 挑戰-執行緒安全
如果,java工具Colleections已經提供了適用於大部分業務場景的併發集合物件,以便線上程操作情況下,能夠保證安全。
以非常典型的java.util.Collections.synchronizedList(List<T>)為例子,下面是相關程式碼:
public static <T> List<T> synchronizedList(List<T> list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); } static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> { @java.io.Serial private static final long serialVersionUID = -7754090372962971524L; @SuppressWarnings("serial") // Conditionally serializable final List<E> list; SynchronizedList(List<E> list) { super(list); this.list = list; } SynchronizedList(List<E> list, Object mutex) { super(list, mutex); this.list = list; } public boolean equals(Object o) { if (this == o) return true; synchronized (mutex) {return list.equals(o);} } public int hashCode() { synchronized (mutex) {return list.hashCode();} } public E get(int index) { synchronized (mutex) {return list.get(index);} } public E set(int index, E element) { synchronized (mutex) {return list.set(index, element);} } public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} } public E remove(int index) { synchronized (mutex) {return list.remove(index);} } public int indexOf(Object o) { synchronized (mutex) {return list.indexOf(o);} } public int lastIndexOf(Object o) { synchronized (mutex) {return list.lastIndexOf(o);} } public boolean addAll(int index, Collection<? extends E> c) { synchronized (mutex) {return list.addAll(index, c);} } public ListIterator<E> listIterator() { return list.listIterator(); // Must be manually synched by user } public ListIterator<E> listIterator(int index) { return list.listIterator(index); // Must be manually synched by user } public List<E> subList(int fromIndex, int toIndex) { synchronized (mutex) { return new SynchronizedList<>(list.subList(fromIndex, toIndex), mutex); } } @Override public void replaceAll(UnaryOperator<E> operator) { synchronized (mutex) {list.replaceAll(operator);} } @Override public void sort(Comparator<? super E> c) { synchronized (mutex) {list.sort(c);} } @java.io.Serial private Object readResolve() { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : this); } }
從程式碼可以看出,這個SynchronizedList對大部分的集合操作都使用關鍵字synchronized,包括基本的get,add,indexOf...
但是需要注意,並不是所有的操作都是上同步鎖,例如獲得迭代器(iterator())就不會。具體哪些不會,需要工程師自己去閱讀程式碼。
實現單個jvm內的執行緒安全問題不大,工程師主要的調整來自於效能要求,需要謹慎地分辨這些上鎖的代價是否過於大,大到不如直接使用序列的
方式進行處理。
通常而言,如果鎖內操作很短,而鎖外的操作相對長的多,那麼還是值得那樣進行操作的。
七、小結
1.集合的子孫比較多,建議先認識一遍,這樣有助於開發,不要浪費自己的時間2.應付一般的CRUD,依靠JAVA和阿帕奇的已經基本夠了用了。
如果實在不夠可以自己額外編寫工具集,不推薦採用三方的工具集(存在安全和更新問題)當然類似阿帕奇這樣的可以例外。
如果是開發產品,更不推薦採用非知名的小組織/個人的工具包。
3.需要注意執行緒安全情況下的用法,這個有賴於個人實踐之後的體驗,雖然JAVADOC有一些說明,但是不夠。
4.使用ai輔助編寫程式碼的時候,應該有適當的辨別能力,避免每個集合都是stream()之後再操作
最簡單的,例如 list.filter(),沒有必要list.stream().filter,除非filter後還掛著其它操作。sort()也是類似。
不能太機械。