JAVA基礎之七-Collection和它的並行和流處理

正在战斗中發表於2024-09-23

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()也是類似。

不能太機械。

相關文章