Google Code Prettify

2020年11月7日 星期六

Java Stream: Collections

這一篇要整理的是 import static java.util.stream.Collectors.* 配合 stream 所進行的操作,它的作用就像 reduce 一樣,但是提供了更多高可讀性的操作,看一下實作 … 先定義資料如下:
@Data
public class Product implements Serializable {
	private String name;		//商品名稱
	private String brand;		//品牌
	private String kind;		//分類
	private int price;		//價格
	private int count;		//數量
	
	public Product() { }
	
	public Product(String name, String brand, String kind, int price, int count) {
		this.name = name;
		this.brand = brand;
		this.kind = kind;
		this.price = price;
		this.count = count;
	}
}

List<Product> products = Arrays.asList(
	//精油
	new Product("橙花純精油", "怡森氏", "精油", 1179, 10),
	new Product("快樂鼠尾草純精油", "怡森氏", "精油", 879, 8),
	new Product("純天然玫瑰純精油", "怡森氏", "精油", 1569, 5),
	new Product("薰衣草純精油", "奧地利KP", "精油", 680, 12),
	new Product("真正薰衣草", "ANDZEN", "精油", 288, 25),
	//鍵盤
	new Product("G512 RGB 機械遊戲鍵盤(青軸)", "羅技", "鍵盤", 3290, 2),
	new Product("黑寡婦蜘蛛幻彩版鍵盤", "Razer", "鍵盤", 2190, 1),
	new Product("Cynosa Lite 薩諾狼蛛Lite版鍵盤", "Razer", "鍵盤", 990, 7),
	new Product("Vigor GK50 Low Profile 短軸機械式鍵盤", "MSI微星", "鍵盤", 2990, 13),
	new Product("Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版)", "MSI微星", "鍵盤", 2999, 6),
	new Product("Vigor GK60 CL TC 電競鍵盤", "MSI微星", "鍵盤", 3290, 3),
	//滑鼠
	new Product("MX Master 2S 無線滑鼠", "羅技", "滑鼠", 1880, 25),
	new Product("MX Master 3 無線滑鼠", "羅技", "滑鼠", 3490, 18),
	new Product("M590 多工靜音無線滑鼠", "羅技", "滑鼠", 849, 30)
);
假設有家商店,有如上述的貨品,三類 (精油、鍵盤、滑鼠) 及多種品牌的商品,現在開始進行資料的操作:
  1. 所有商品按種類分組
  2. Map<String, List<Product>> groupByKind = products.stream()
    		.collect(groupingBy(Product::getKind));
    		
    groupByKind.forEach((k, v) -> {
    		log.info("Kind: " + k);
    		v.forEach(p -> log.info("  product: " + p.getName()));
    	});
    
    Kind: 滑鼠
      product: MX Master 2S 無線滑鼠
      product: MX Master 3 無線滑鼠
      product: M590 多工靜音無線滑鼠
    Kind: 精油
      product: 橙花純精油
      product: 快樂鼠尾草純精油
      product: 純天然玫瑰純精油
      product: 薰衣草純精油
      product: 真正薰衣草
    Kind: 鍵盤
      product: G512 RGB 機械遊戲鍵盤(青軸)
      product: 黑寡婦蜘蛛幻彩版鍵盤
      product: Cynosa Lite 薩諾狼蛛Lite版鍵盤
      product: Vigor GK50 Low Profile 短軸機械式鍵盤
      product: Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版)
      product: Vigor GK60 CL TC 電競鍵盤
    用 collect  可以展現出 functional programming 的優勢,從指令就可以看的出來,我們的需求是什麼,至於程式實際怎麼執行,不勞我們擔心,Java 會幫我們找出辦法。上述 groupingBy 指令指出以 kind (商品種類) 分組,匯整在 map 裡,後面我們將它依序輸出。
  3. 所有商品按種類分組後再依品牌分組
  4. Map<String, Map<String, List<Product>>> result = products.stream()
    	.collect(groupingBy(Product::getKind,
    		groupingBy(Product::getBrand)
    	));
    		
    result.forEach((kind, p1) -> {
    	log.info("Kind: " + kind);
    	p1.forEach((brand, p2) -> {
    		log.info("  Brand: " + brand);
    		p2.forEach(p3 -> log.info("    Product: " + p3.getName()));
    	});
    });
    
    Kind: 滑鼠
      Brand: 羅技
        Product: MX Master 2S 無線滑鼠
        Product: MX Master 3 無線滑鼠
        Product: M590 多工靜音無線滑鼠
    Kind: 精油
      Brand: 怡森氏
        Product: 橙花純精油
        Product: 快樂鼠尾草純精油
        Product: 純天然玫瑰純精油
      Brand: 奧地利KP
        Product: 薰衣草純精油
        Brand: ANDZEN
        Product: 真正薰衣草
    Kind: 鍵盤
      Brand: 羅技
        Product: G512 RGB 機械遊戲鍵盤(青軸)
      Brand: Razer
        Product: 黑寡婦蜘蛛幻彩版鍵盤
        Product: Cynosa Lite 薩諾狼蛛Lite版鍵盤
      Brand: MSI微星
        Product: Vigor GK50 Low Profile 短軸機械式鍵盤
        Product: Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版)
        Product: Vigor GK60 CL TC 電競鍵盤
    上面語法,我們要多層次分組,也是很明白的"說出"我們的需求,先以種類分組再以品牌分組,就得到我們要的結果了。
  5. 計算各種類中各品牌的商品樣式數量
  6. Map<String, Map<String, Long>> result = products.stream()
    	.collect(groupingBy(Product::getKind,
    		groupingBy(Product::getBrand, counting())
    	));
    
    result.forEach((kind, p1) -> {
    	log.info("Kind: " + kind);
    	p1.forEach((brand, count) -> {
    		log.info("  Brand: " + brand + " = " + count);
    	});
    });
    
    Kind: 滑鼠
      Brand: 羅技 = 3
    Kind: 精油
      Brand: 怡森氏 = 3
      Brand: 奧地利KP = 1
      Brand: ANDZEN = 1
    Kind: 鍵盤
      Brand: 羅技 = 1
      Brand: Razer = 2
      Brand: MSI微星 = 3
    利用 Collections 中的 counting() 就可以計算出數量了!
  7. 最貴的滑鼠
  8. Comparator<Product> priceComparator = Comparator.comparingInt(Product::getPrice);
    		
    Optional<Product> mouse = products.stream()
    	.filter(p -> p.getKind().equals("滑鼠"))
    	.collect(maxBy(priceComparator));
    		
    if (mouse.isPresent()) {
    	log.info(mouse.get().getName());
    }
    
    MX Master 3 無線滑鼠
    先定義 Comparator 讓 collect 的 maxBy 知道比較的方式,這裡就是比價格,在 stream 中配合 filter 找出滑鼠比一下價格,就找出最貴的滑鼠了! 這裡要注意的是,找出來的結果為 Optional, 因為有可能根本沒有滑鼠!
  9. 全部的鍵盤有幾個?
  10. int total = products.stream()
    	.filter(p -> p.getKind().equals("鍵盤"))
    	.collect(summingInt(Product::getCount));
    		
    log.info("鍵盤數量: " + total);
    
    鍵盤數量: 32
    非常簡單的,過濾出鍵盤後,用 summingInt 將數量加總。
  11. 全部的商品以種類先排序,再以分號分隔列出
  12. Comparator<Product> kindComparator = Comparator.comparing(Product::getKind);
    		
    String s = products.stream()
    	.sorted(kindComparator)
    	.map(Product::getName)
    	.collect(joining(","));
    		
    log.info(s);
    
    MX Master 2S 無線滑鼠,MX Master 3 無線滑鼠,M590 多工靜音無線滑鼠,橙花純精油,快樂鼠尾草純精油,純天然玫瑰純精油,薰衣草純精油,真正薰衣草,G512 RGB 機械遊戲鍵盤(青軸),黑寡婦蜘蛛幻彩版鍵盤,Cynosa Lite 薩諾狼蛛Lite版鍵盤,Vigor GK50 Low Profile 短軸機械式鍵盤,Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版),Vigor GK60 CL TC 電競鍵盤
    實務上資料常常要以 csv 檔產出,這時候可以用 joining,這是連接字串的 method,這裡在列出商品時,用 joining 連接並於中間加上逗點。
  13. 將精油分成怡森氏和非怡森氏兩組
  14. Map<Boolean, List<Product>> result = products.stream()
    	.filter(p -> p.getKind().equals("精油"))
    	.collect(partitioningBy(p -> p.getBrand().equals("怡森氏")));
    		
    result.forEach((k, v) -> {
    	log.info(k ? "怡森氏" : "非怡森氏");
    	v.forEach(p -> log.info("  " + p.getName()));
    });
    
    非怡森氏
      薰衣草純精油
      真正薰衣草
    怡森氏
      橙花純精油
      快樂鼠尾草純精油
      純天然玫瑰純精油
    分組除了可以用 groupingBy, 還有另一個選擇 -- partitioningBy,partitioningBy 要帶入的參數是 Predicate,只能分 true、false 兩組,但是效率比較高,如果剛好適用,partitioningBy 會是更好的選擇。
  15. 找出鍵盤中,微星與非微星品牌中最貴的
  16. Map<Boolean, Product> result = products.stream()
    	.filter(p -> p.getKind().equals("鍵盤"))
    	.collect(
    		partitioningBy(
    			p -> p.getBrand().equals("MSI微星"),
    			collectingAndThen(
    				maxBy(Comparator.comparingInt(Product::getPrice)), 
    				Optional::get)
    			)
    		);
    		
    result.forEach((k, v) -> {
    	log.info(k ? "MSI微星" : "非MSI微星");
    	log.info("  Product: " + v.getName() + ": " + v.getPrice());
    });
    
    非MSI微星
      Product: G512 RGB 機械遊戲鍵盤(青軸): 3290
    MSI微星
      Product: Vigor GK60 CL TC 電競鍵盤: 3290
    partitioningBy 還有個優點就是,在分組後,還可以使用 collectingAndThen 做進一步的處理,上面在分組後,我們在該組裡找出最貴的鍵盤。

2020年11月2日 星期一

Java Stream: operation

 本篇參見【Modern Java in Action】 2nd-edition


Stream 提供了如上表的 operation,要怎麼使用呢? 直接看例子吧~

假設一個場景,客戶進行證券交易,這裡記錄下客戶在某年交易的金額及客戶的基本資料 (姓名、年齡、居住城市),資料如下:

Customer steven = new Customer("Steven", 50, "New Taipei");
Customer shelley = new Customer("Shelley", 48, "Taichung");
Customer mark = new Customer("Mark", 53, "Taipei");
Customer kfc = new Customer("KFC", 38, "Taipei");
Customer clair = new Customer("Clair", 26, "Yunlin");
	
List<Trade> trades = Arrays.asList(
		new Trade(steven, 2018, 350000),
		new Trade(shelley, 2018, 535000),
		new Trade(mark, 2018, 1005000),
		new Trade(kfc, 2018, 810000),
		new Trade(clair, 2018, 559000),
		new Trade(steven, 2019, 67000),
		new Trade(mark, 2019, 890100),
		new Trade(clair, 2019, 90900)
	);
  1. 2018年的所有交易,按交易額排序:
  2. trades.stream()
    	.filter(t -> t.getYear() == 2018)
    	.sorted(comparing(Trade::getAmount))
    	.forEach(t -> log.info(t.toString()));
    
    得到如下結果,filter 過濾出 2018 年的交易 (Trade),以 amount 排序,從結果可以看到這些交易是那個客戶交易的。
    Trade(cTrade(cstm=Customer(name=Steven, age=50, city=New Taipei), year=2018, amount=350000) Trade(cstm=Customer(name=Shelley, age=48, city=Taichung), year=2018, amount=535000) Trade(cstm=Customer(name=Clair, age=26, city=Yunlin), year=2018, amount=559000) Trade(cstm=Customer(name=KFC, age=38, city=Taipei), year=2018, amount=810000) Trade(cstm=Customer(name=Mark, age=53, city=Taipei), year=2018, amount=1005000)
  3. 所有客戶分布於那些城市?
  4. trades.stream()
    	.map(t -> t.getCstm().getCity())
    	.distinct()
    	.forEach(t -> log.info(t.toString()));
    
    用 map 指出要保留的欄位,distinct() 表示重複的只會列出一筆。
    New Taipei
    Taichung
    Taipei
    Yunlin
  5. 列出住台北市的客戶,並以年齡排序:
  6. trades.stream()
    	.filter(t -> t.getCstm().getCity().equals("Taipei"))
    	.map(t -> t.getCstm())
    	.distinct()
    	.sorted(comparing(Customer::getAge))
    	.forEach(t -> log.info(t.toString()));
    
    Customer(name=KFC, age=38, city=Taipei)
    Customer(name=Mark, age=53, city=Taipei)
  7. 住台北市的客戶的交易量總和?
  8. int total = trades.stream()
    	.filter(t -> t.getCstm().getCity().equals("Taipei"))
    	.map(t -> t.getAmount())
    	.reduce(0, Integer::sum);
    log.info("total = " + total);
    
    total = 2705100
    上面的寫法會有個「封裝」的成本,int 被封裝成 Integer,或 Integer 拆裝成 int 都會需要一點點時間。為了解決這個問題,Stream 提供了 IntStream、DoubleStream、LongStream 三個原始型別的串流,那麼上面的程式可以改寫成如下:
    int total = trades.stream()
    	.filter(t -> t.getCstm().getCity().equals("Taipei"))
    	.mapToInt(Trade::getAmount)
    	.sum();
    
  9. 歷年來所有交易中,最大的一筆交易的交易量?
  10. Optional<Integer> optTotal = trades.stream()
    	.map(t -> t.getAmount())
    	.reduce(Integer::max);
    	
    if (optTotal.isPresent()) {
    	int total = optTotal.get();
    	log.info("total = " + total);
    }
    else {
    	log.info("data not found");
    }
    
    使用 reduce() 可以將資料歸納,這裡指出要資料中最大的。
    total = 1005000

【番外篇】
Arrays.asList(...) 產生的 List 會是固定大小,上面的 trades 中的元素可以更改,但是,如果要加入新元素,例如: trades.add(new Trade()); 會拋出 UnsupportedModificationException

2020年11月1日 星期日

Java Lambda: getting started

這篇會整理的是,在傳統語法下及 Lambda 語法,以排序為例進行說明:

在開始說明前,先定義一個類別如下:

@Data
public class Country implements Serializable {
	private String name;       //國名
	private String capital;    //首都
	private String area;       //位於那個洲?
	private int gdp;           //人均GDP
	private int population;    //人口總數
	
	public Country() { }
	
	public Country(String name, String capital, String area, int gdp, int population) {
		this.name = name;
		this.capital = capital;
		this.area = area;
		this.gdp = gdp;
		this.population = population;
	}
}
程式一開始會進建立一個命名為 world 的 List 如下:
List<Country> world = Arrays.asList(		
		new Country("Taiwan", "Taipei", "Asia", 24500, 23500000),
		new Country("Korea", "Seoul", "Asia", 31430, 51709098),
		new Country("Philippines", "Manila", "Asia", 3117, 107225000),
		new Country("Nederland", "Amsterdam", "Europe", 53106, 17291000),
		new Country("Canada", "Ottawa", "America", 47931, 37281000),
		new Country("Australia", "Canberra", "Oceania", 51592, 25220000)
	);
下面的例子就用人口數來排序 world 裡的國家。
  1. 定義實作 Comparator 介面的類別
  2. private class CountryComparator implements Comparator<Country> {
    	@Override
    	public int compare(Country c1, Country c2) {
    		return c1.getPopulation() - c2.getPopulation();
    	}
    }
    
    使用上述的類別規範了比較的方式,程式會如下:
    world.sort(new CountryComparator());
    
  3. 定義實作 Comparator 介面的匿名類別
  4. world.sort(
    	new Comparator<Country>() {
    		@Override
    		public int compare(Country c1, Country c2) {
    			return c1.getPopulation() - c2.getPopulation();
    		}
    	});
    
    這兩種寫法,都是 Java 7 以前的寫法,當這個用來比較的類別很短,又只會使用一次,採用匿名類別可讀性會比較高。
  5. 使用 Lambda
  6. world.sort((c1, c2) -> c1.getPopulation() - c2.getPopulation());
    
    程式怎麼會變得這麼簡單,而且清楚明白,Lambda 怎麼辦到的? Lambda 的使用有一些限制,sort 接受的 Comparator 介面只有一個 method,這是它的重要限制之一! 因為只有一個 method,Lambda expression 就看的出來是要傳入 c1、c2 兩個 Country 類別的物件進入 compare() method,compare() method 在這裡實作為 return c1.getPopulation() - c2.getPopulation()。
  7. 使用「方法引用」
  8. 這是 Lambda 的語法糖,這裡假設我們有靜態導入 java.util.Comparator.comparing (import static java.util.Comparator.comparing)。
    world.sort(comparing(Country::getPopulation));
    
    直接指明要調用 Country 裡的 getPopulation() 的值進行比較。這裡有個疑問,怎麼 Comparator 類別又多出了 comparing 這個 method 了? 而且這個 method 還不只是定義了介面,還實作了 method ! 這是 Java 8 之後才有的,interface 裡也可以實作 method,但是,因為裡面只有一個未實作的 method,仍是合乎 Lambda 的限制,Lambda 知道要使用那個 method。
    有興趣的話,再觀察一下 Comparator 的原始碼,多了不少 static method,上面只比較人口數,事實上我們可以複合比較,如下:
    world.sort(
    	comparing(Country::getArea).thenComparing(Country::getPopulation)
    );