Google Code Prettify

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 兩個檔案裡,可以查到撥接的帳號、密碼。

2019年4月5日 星期五

部署 RESTful service 到 docker

我用 spring boot 寫了一個 hello 的 RESTful service,如下,傳入名稱,服務就會回覆 Hello $name。
@RestController
public class HelloController {

@RequestMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return "Hello " + name + " !";
}
}
這個 RESTful service 我打包成 jar 檔,要部署到 docker 上執行。打包好的 jar 檔名為 Hello-1.0.0.jar,在 docker 中要安裝 open JDK 11 (openjdk-11_linux-x64_bin.tar.gz),Dockerfile 是 docker 提供的自動化部署方式,繼續往下看之前,可以先看一下這一篇 -- 使用 Dockerfile 創建 docker 鏡像。
  • Dockerfile
FROM centos:latest
MAINTAINER "Steven Shi"<hi.steven@gmail.com>
ADD openjdk-11_linux-x64_bin.tar.gz /usr/local
COPY Hello-1.0.0.jar /home
RUN ln -s /usr/local/jdk-11 /usr/bin/java
ENV JAVA_HOME /usr/local/jdk-11
ENV PATH $JAVA_HOME/bin:$PATH
EXPOSE 9080
CMD java -jar /home/Hello-1.0.0.jar
    • FROM centos:latest: 指定這個 image 的基礎鏡像為 centos 的最新版。
    • ADD openjdk-11_linux-x64_bin.tar.gz /usr/local: 把 jdk 部署到 /usr/local 目錄下,ADD 指令不只會 copy 指定的檔案到指定目錄,還具有解壓縮的功能。
    • COPY Hello-1.0.0.jar /home: 把 RESTful service 的 jar 檔部署到 /home 目錄下。
    • EXPOSE 9080: 這個 container 對外開放出 9080 這個 port。
    • CMD java -jar /home/Hello-1.0.0.jar: 在 container 啟動時,讓 hello 服務啟動。
  • 部署服務到 docker
docker build -t steven/hello .
在上面那三個檔案所在目錄執行上述指令,就會在 docker 產生鏡像,名稱為 steven/hello,注意,最後面有個句點,指出是在目前目錄。
  • 啟動 container
docker run -p 9080:9080 --name hello-container -d steven/hello
    • -p 9080:9080 : container 外要連入 container 的 port 也設為 9080 和 container 開放給外界的 port 相連通 (port 號不需要一樣)。
    • --name : container 啟動後的名稱。
    • -d : 啟動為 daemon 狀態,後面接的是鏡像名稱。
  • 測試
curl localhost:9080/hello/Steven
用 curl 發一個 http request 如上,會傳回 Hello Steven,確認服務部署成功。