怎樣用Java 8優雅的開發業務

switchvov發表於2020-12-20

怎樣用Java 8優雅的開發業務

函數語言程式設計

匿名函式

λ演算

流式程式設計

基本原理

Java中流式程式設計的基本原理有兩點。

  1. 構建流
  2. 資料流轉(流水線)
  3. 規約
IntStream.rangeClosed(1, 100) // 1. 構建流
    .mapToObj(String::valueOf)// 2. 資料流轉(流水線)
    .collect(joining());      // 3. 規約

案例

  • 英雄的主位置一共有幾類,分別是什麼
@Test
fun t1() {
    // 英雄的主位置一共有幾類,分別是什麼
    // 對映
    val roleMains = heroes.map(Hero::getRoleMain)
        // 過濾為空的資料
        .filter(Objects::nonNull)
        // 去重
        .distinct()
    println(roleMains.size)
    println(roleMains)
}
@Test
public void t1() {
    // 英雄的主位置一共有幾類,分別是什麼
    List<String> roleMains = heroes.stream()
            // 對映
            .map(Hero::getRoleMain)
            // 過濾為空的資料
            .filter(Objects::nonNull)
            // 去重
            .distinct()
            // 收集列表
            .collect(toList());
    System.out.println(roleMains.size());
    System.out.println(roleMains);
}

  • 英雄按主次位置分組後,輸出每個分組有多少英雄,其中:近戰英雄有多少位,遠端英雄有多少位
@Test
fun t2() {
    // 英雄按主次位置分組後,輸出每個分組有多少英雄,其中:近戰英雄有多少位,遠端英雄有多少位

    // 主次位置分組的英雄數量
    val groupHeroCount = heroes.groupingBy {
        Pair.of(it.roleMain, it.roleAssist)
    }.eachCount()

    // 主次分組後,再按攻擊範圍分組的英雄數量
    val groupThenGroupCount = heroes.groupBy {
        Pair.of(it.roleMain, it.roleAssist)
    }.map {
        val value = it.value.groupingBy(Hero::getAttackRange).eachCount()
        Pair.of(it.key, value)
    }.associateBy({ it.left }, { it.value })

    // 遍歷輸出
    groupThenGroupCount.forEach { (groupKey, groupValue) ->
        val groupingCount = groupHeroCount[groupKey]
        print("英雄分組key為:$groupKey;英雄數量:$groupingCount;")
        groupValue.forEach { (countKey, countValue) ->
            print("英雄攻擊範圍:$countKey;英雄數量:$countValue;")
        }
        println()
    }
}
@Test
public void t2() {
    // 英雄按主次位置分組後,輸出每個分組有多少英雄,其中:近戰英雄有多少位,遠端英雄有多少位

    // 主次位置分組的英雄數量
    Map<Pair<String, String>, Long> groupHeroCount = heroes.stream()
            .collect(groupingBy(hero -> Pair.of(hero.getRoleMain(), hero.getRoleAssist()), counting()));

    // 主次分組後,再按攻擊範圍分組的英雄數量
    Map<Pair<String, String>, Map<String, Long>> groupThenGroupCount = heroes.stream()
            .collect(groupingBy(hero -> Pair.of(hero.getRoleMain(), hero.getRoleAssist()),
                    groupingBy(Hero::getAttackRange, counting())));

    // 遍歷輸出
    groupThenGroupCount.forEach((groupKey, groupValue) -> {
        Long groupingCount = groupHeroCount.get(groupKey);
        System.out.print("英雄分組key為:" + groupKey + ";英雄數量:" + groupingCount + ";");
        groupValue.forEach((countKey, countValue) -> System.out.print("英雄攻擊範圍:" + countKey + ";英雄數量:" + countValue + ";"));
        System.out.println();
    });
}

  • 求近戰英雄HP初始值的加總
@Test
fun t3() {
    // 求近戰英雄HP初始值的加總
    val sum = heroes.filter { "近戰" == it.attackRange }
        .map(Hero::getHpStart)
        .filter(Objects::nonNull)
        .reduce(BigDecimal::add)
    println("近戰英雄HP初始值的加總為:$sum")
}
@Test
public void t3() {
    // 求近戰英雄HP初始值的加總
    BigDecimal sum = heroes.stream()
            .filter(hero -> "近戰".equals(hero.getAttackRange()))
            .map(Hero::getHpStart)
            .filter(Objects::nonNull)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    System.out.println("近戰英雄HP初始值的加總為:" + sum);
}

  • 通過最小列表收集器獲取最小列表
@Test
public void t4() {
    // 通過最小列表收集器獲取最小列表
    List<BigDecimal> minAttackGrowth = heroes.stream()
            .map(Hero::getAttackGrowth)
            .collect(new MinListCollector<>());
    System.out.println(minAttackGrowth);
    List<Hero> minHero = heroes.stream()
            .collect(new MinListCollector<>());
    System.out.println(minHero);
}
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;

import static java.util.stream.Collector.Characteristics.*;

/**
 * 最小列表收集器
 *
 * @author switch
 * @since 2020/8/18
 */
public class MinListCollector<T extends Comparable<? super T>> implements Collector<T, List<T>, List<T>> {
    /**
     * 收集器的特性
     *
     * @see Characteristics
     */
    private final static Set<Characteristics> CHARACTERISTICS = Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
    private final static int ZERO = 0;

    /**
     * 最小值
     */
    private final AtomicReference<T> min = new AtomicReference<>();

    @Override
    public Supplier<List<T>> supplier() {
        // supplier引數用於生成結果容器,容器型別為A
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        // accumulator用於消費元素,也就是歸納元素,這裡的T就是元素,它會將流中的元素一個一個與結果容器A發生操作
        return (list, element) -> {
            // 獲取最小值
            T minValue = min.get();
            if (Objects.isNull(minValue)) {
                // 第一次比較
                list.add(element);
                min.set(element);
            } else if (element.compareTo(minValue) < ZERO) {
                // 發現更小的值
                list.clear();
                list.add(element);
                min.compareAndSet(minValue, element);
            } else if (element.compareTo(minValue) == ZERO) {
                // 與最小值相等
                list.add(element);
            }
        };
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        // combiner用於兩個兩個合併並行執行的執行緒的執行結果,將其合併為一個最終結果A
        return (left, right) -> {
            // 最小值列表合併
            List<T> leftList = getMinList(left);
            List<T> rightList = getMinList(right);
            leftList.addAll(rightList);
            return leftList;
        };
    }

    private List<T> getMinList(List<T> list) {
        return list.stream()
                .filter(element -> element.compareTo(min.get()) == ZERO)
                .collect(Collectors.toList());
    }

    @Override
    public Function<List<T>, List<T>> finisher() {
        // finisher用於將之前整合完的結果R轉換成為A
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        // characteristics表示當前Collector的特徵值,這是個不可變Set
        return CHARACTERISTICS;
    }
}

優雅的空處理

file

import org.junit.Test;

import java.util.Optional;

/**
 * @author switch
 * @since 2020/8/18
 */
public class OptionalTests {
    @Test
    public void t1() {
        // orElse
        System.out.println(Optional.ofNullable(null).orElse("張三"));
        System.out.println(Optional.ofNullable(null).orElseGet(() -> "李四"));
        System.out.println(Optional.ofNullable("王五").orElseThrow(NullPointerException::new));
    }

    @Test
    public void t2() {
        // isPresent
        Optional<String> name = Optional.ofNullable("張三");
        if (name.isPresent()) {
            System.out.println(name.get());
        }
    }

    @Test
    public void t3() {
        // map
        Optional<Integer> number = Optional.of("123456").map(Integer::valueOf);
        if (number.isPresent()) {
            System.out.println(number.get());
        }
    }

    @Test
    public void t4() {
        // flatMap
        Optional<Integer> number = Optional.of("123456").flatMap(s -> Optional.of(Integer.valueOf(s)));
        if (number.isPresent()) {
            System.out.println(number.get());
        }
    }

    @Test
    public void t5() {
        // 過濾
        String number = "123456";
        String filterNumber = Optional.of(number).filter(s -> !s.equals(number)).orElse("654321");
        System.out.println(filterNumber);
    }
}

新的併發工具類CompletableFuture

file

單機批處理多執行緒執行模型

該模型適用於百萬級量級的任務。超過千萬資料,可以考慮分組,多機器並行執行。
基本流程:

  1. 從資料庫獲取Id列表
  2. 拆分成n個子Id列表
  3. 通過子Id列表獲取關聯資料(注意:都需要提供批量查詢介面)
  4. 對映到需要處理的Model(提交到CompletableFuture)->處理資料->收整合list)(java 8流式處理)
  5. 收集的list進行join操作
  6. 收集list
模型

模型原理:Stream+CompletableFuture+lambda

簡要解釋:

  • CompletableFuture是java8提供的一個工具類,主要是用於非同步處理流程編排的。
  • Stream是java8提供的一個集合流式處理工具類,主要用於資料的流水線處理。
  • lambda在java中是基於內部匿名類實現的,可以大幅減少重複程式碼。
  • 總結:在該模型中Stream用於集合流水線處理、CompletableFuture解決非同步編排問題(非阻塞)、lambda簡化程式碼。
  • 資料流動
List<List<String>> -> 
Stream<List<String>> -> 
Stream<List<Model>> -> 
Stream<CompletableFuture<List<Model>>> -> 
Stream<CompletableFuture<List<對映型別>>> -> 
List<CompletableFuture<Void>>
案例
  • ThreadPoolUtil
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

public final class ThreadPoolUtil {
    public static ThreadPoolTaskExecutor getDefaultExecutor(Integer poolSize, Integer maxPoolSize, Integer queueCapacity) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setAllowCoreThreadTimeOut(true);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setCorePoolSize(poolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}
  • ThreadPoolConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class ThreadPoolConfig {
    /**
     * 計算規則:N(thread) = N(cpu) * U(cpu) * (1 + w/c)
     * N(thread):執行緒池大小
     * N(cpu):處理器核數
     * U(cpu):期望CPU利用率(該值應該介於0和1之間)
     * w/c:是等待時間與計算時間的比率,比如說IO操作即為等待時間,計算處理即為計算時間
     */
    private static final Integer TASK_POOL_SIZE = 50;
    private static final Integer TASK_MAX_POOL_SIZE = 100;
    private static final Integer TASK_QUEUE_CAPACITY = 1000;

    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        return ThreadPoolUtil.getDefaultExecutor(TASK_POOL_SIZE, TASK_MAX_POOL_SIZE, TASK_QUEUE_CAPACITY);
    }
}
  • #getFuturesStream
public Stream<CompletableFuture<List<Model>>> getFuturesStream(List<List<String>> idSubLists) {
    return idSubLists.stream()
        .map(ids -> 
            CompletableFuture.supplyAsync(() -> modelService.listByIds(ids), taskExecutor)
        );
}
  • #standardisation
public void standardisation() {
    List<CompletableFuture<Void>> batchFutures = getFuturesStream(idSubLists)
            .map(future -> future.thenApply(this::listByNormalize))
            .map(future -> future.thenAccept(modelService::batchUpdateData))
            .collect(Collectors.toList());
    List<Void> results = batchFutures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
}

調整執行緒池的大小

《Java併發程式設計實戰》一書中,Brian Goetz和合著者們為執行緒池大小的優化提供了不少中肯的建議。這非常重要,如果執行緒池中執行緒的數量過多,最終它們會競爭稀缺的處理器和記憶體資源,浪費大量的時間在上下文切換上。反之,如果執行緒的數目過少,正如你的應用所面臨的情況,處理器的一些核可能就無法充分利用。Brian Goetz建議,執行緒池大小與處理器的利用率之比可以使用下面的公式進行估算:
$$N_{threads} = N_{CPU} * U_{CPU} * (1 + \frac{W}{C})$$

其中:

  • $N_{CPU}$是處理器的核的數目,可以通過Runtime.getRuntime().availableProcessors()得到
  • $U_{CPU}$是期望的CPU利用率(該值應該介於0和1之間)
  • $\frac{W}{C}$是等待時間與計算時間的比率,比如說IO操作即為等待時間,計算處理即為計算時間

並行——使用流還是CompletableFutures?

對集合進行平行計算有兩種方式:要麼將其轉化為並行流,利用map這樣的操作開展工作,要麼列舉出集合中的每一個元素,建立新的執行緒,在CompletableFuture內對其進行操作。後者提供了更多的靈活性,可以調整執行緒池的大小,而這能幫助確保整體的計算不會因為執行緒都在等待I/O而發生阻塞。

使用這些API的建議如下:

  • 如果進行的是計算密集型的操作,並且沒有I/O,那麼推薦使用Stream介面,因為實現簡單,同時效率也可能是最高的(如果所有的執行緒都是計算密集型的,那就沒有必要建立比處理器核數更多的執行緒)。
  • 反之,如果並行的工作單元還涉及等待I/O的操作(包括網路連線等待),那麼使用CompletableFuture靈活性更好,可以依據等待/計算,或者$\frac{W}{C}$的比率設定需要使用的執行緒數。這種情況不使用並行流的另一個原因是,處理流的流水線中如果發生I/O等待,流的延遲特性很難判斷到底什麼時候觸發了等待。

日期和時間API

file

使用指南:https://www.yuque.com/docs/share/ee5ef8a7-d261-4593-bd08-2a7a7d2c11ca?#(密碼:gtag) 《時區工具類使用指南》

專案地址

GitHub:java8-fluent

參考

分享並記錄所學所見

相關文章