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)
    );
    

2020年10月4日 星期日

Java Stream: Creation

繼上一篇「Java Stream: getting started」後,這裡整理 stream 生成的幾種方式。

  1. of
  2. Stream week = Stream.of("日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日");
    log.debug("count = " + week.count());
    
    如上,使用 of 產生 stream,上面經過 count 方法後,會 output 出 7。
  3. Arrays
  4. Arrays.stream(new String[] { "日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日" }, 2, 4)
    	.forEach(w -> log.debug(w));
    
    這個方法,可以從陣列中截取一段產生 stream,上面會輸出 "火曜日"、"水曜日"。
  5. generate
  6. Stream.generate(Math::random).forEach(s -> log.info("number = " + s));
    
    使用 generate 會生成無限長度的 stream,如上,會一直輸出亂數。所以,通常會配合 limit 使用。
    Stream.iterate(BigInteger.ZERO, n -> n.compareTo(BigInteger.valueOf(10)) < 0, n -> n.add(BigInteger.ONE)).forEach(i -> log.debug("integer = " + i));
    Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)).limit(10).forEach(i -> log.debug("integer = " + i));
    
    上面兩種方法可以得到相同結果,都是輸出 0 ~ 9。

2020年10月3日 星期六

Java Stream: getting started

 stream 是 Java 8 新增的功能,現在 Java 已經到 15 了,但是 ... stream 似乎也沒有很普及,這裡試著整理一些基礎用法。

File resource = new ClassPathResource("data/employee.txt").getFile();
var lines = Files.readAllLines(resource.toPath(), StandardCharsets.UTF_8);
lines.stream().filter(s -> s.compareTo("1000000") > 0).forEach(s -> log.info(s));
【說明】
  1. 在 src/main/resources 目錄裡,我建了一個子目錄 data,裡面放的 employee.txt 只是一些員工編號。
  2. Files.readAllLines 可以將文字檔資料讀入,並以每一行一個 String 的方式,存在 List<String> 裡,在 Java 10 開始提供了 var 這個關鍵字,程式員可以不需要寫明型別由 java 自行判斷,將滑鼠移到 var 上,eclipse 就會顯示出正確的型別了。
  3. 所有資料存於 lines,先以 stream() 將它轉成串流,接著就可以用 stream 提供的一些方法操作。第 3 行是先透過 filter 篩選出員編大於 "1000000" 的資料,再透過 forEach 印出來。
如果資料量很大,上面的 stream() 可以改為 parallelStream(),就會以多執行緒的方式處理。一般寫 stream 的程式,步驟如下:
  1. create 一個 stream
  2. 使用 stream 的一些方法操作 stream 的內容 (可能會有多個方法一連串的操作)
  3. 在最後一個方法中,產生出結果。
感覺上 stream 很像 collection ? 以下是主要的差異:
  1. stream 沒有儲存這些資料,它只是在處理資料,以上面的例子來說,資料儲存在 List<String> 裡,轉換成 stream 並沒有另外存一份。
  2. stream 處理資料的過程不會變更原有資料,如上面,log 顯示出結果後,原本的 lines 裡的資料不會被改變。
  3. 上面的例子比較簡單,常常看到的會是串接好幾個方法,stream 會試圖延遲這些方法的執行,直到最後真的得執行時才全部的方法一次執行,如上例就是在 forEach 才會執行 filter。

2020年5月1日 星期五

Ubuntu 上的 PPPoE 設定

中華電信的光世代 ADSL,沒有特別申請的話,連接上去是動態 IP,如果想要固定 IP 可以到官網申請,申請核可後,電腦需以 PPPoE 的方式撥接上去取得該固定 IP,在 Ubuntu 中,方法如下:
  1. 安裝
  2. sudo apt install net-tools
    sudo apt-get install pppoeconf
    
  3. 設定
  4. sudo pppoeconf
    
    除了帳號、密碼外,每個選項都用預設值,直接按【ENTER】,帳號的部份要特別注意,中華電信給的帳號可能為 74669091,在這裡要輸入 74669091@ip.hinet.net
  5. 撥接
  6. sudo pon dsl-provider
    
  7. 中斷連線
  8. sudo poff -a
    
  9. 觀察
  10. 撥接上去後,透過 ifconfig -a 可以看到類似下面的內容,顯示有張網卡綁定了固定 IP。
    另外在 /etc/ppp 目錄下,chap-secrets 和 pap-secrets 兩個檔案裡,可以查到撥接的帳號、密碼。