2018-03-31

Java Lambda 初體驗,就遇到 Project Reactor 的 GroupedFlux

在練習書上的(不負責任的)範例時,遇到了「流中流」的問題,就是 Stream of Stream,一般都是用 flatMap 去打平,但這個 GroupedFlux 不太一樣。

先來說說需求,就是輸入一堆單字,然後統計每個字母(不分大小寫)的出現次數。

舉例來說,輸入「java」、「"hibernate」和「spring」三個單字,要得到以下的結果:
A => 3
B => 1
E => 2
G => 1
H => 1
I => 2
J => 1
N => 2
P => 1
R => 2
S => 1
T => 1
V => 1
第一次嘗試,試了好久才免強得到結果,但不符合我目前理解的標準。
Flux.just("java", "hibernate", "spring")// 1
        .map(String::toUpperCase)// 2
        .flatMap(s -> Flux.fromArray(s.split("")))// 3
        .groupBy(String::toString)// 4
        .sort((o1, o2) -> o1.key().compareTo(o2.key()))// 5
        .map(t -> t.key() + " => " + t.count().block())// 6
        .subscribe(System.out::println);// 7
  1. 先用 Flux.just() 這個 static 方法建立 String 流(在 Java 8 叫做 Stream,但在 Project Reactor 叫做 Flux,本質雖然類似,但實際上有很大的差異,就是非同步和非阻塞)。
  2. 為了不分大小寫,先用 Flux.map() 轉成大寫。
  3. 為了統計每個字母的數量,先用 String.split() 將字串拆成字母陣列,再用 Flux.fromArray() 將字母陣列轉換成 Flux<string>,這時候就是簡單的流中流(Flux of Flux),最後用 Flux.flapMap() 打平成單一個 Flux<string>,此時的 String 是單一字母。
  4. 使用 Flux.groupBy() 將 Flux<string>依照字母做統計,最終得到 GroupedFlux<string, String>,這時就是最棘手的流中流,Flux of GroupedFlux。
  5. 使用 Flux.sort() 將字母排序。
  6. 使用 Flux.map() 將 GroupedFlux 轉成目標字串,就是「字母 => 數量」,但問題來了,GroupedFlux.count() 回傳的是 Mono<long>,可以說是另一種流,Flux  可以看作是容納 0 到 無限多物件的容器,而 Mono 是容納 0 到 1 個物件的容器,有點像是 Java 8 的 Optional,目前找不到方法可以得到 Mono<long> 裡的 Long,最終用了我覺得不該用的 Mono.block(),雖然可以得到數量,但 Mono.block() 就像是 Flux.subscribe(),是流的終點,是不是不應該在流的中間呼叫這樣的 API 呢?
  7. 最後呼叫 Flux.subscribe() 將內容輸出。
第二次嘗試,將 Mono.block() 的呼叫放到最後的 Flux.subscribe(),不過好像還是怪怪的。
Flux.just("java", "hibernate", "spring")// 1
        .map(String::toUpperCase)// 2
        .flatMap(s -> Flux.fromArray(s.split("")))// 3
        .groupBy(String::toString)// 4
        .sort((o1, o2) -> o1.key().compareTo(o2.key()))// 5
        .subscribe(g -> System.out.println(g.key() + " => " + g.count().block()));// 6
最後終於試出一種比較像樣的結果。
Flux.just("java", "hibernate", "spring")// 1
        .map(String::toUpperCase)// 2
        .flatMap(s -> Flux.fromArray(s.split("")))// 3
        .groupBy(String::toString)// 4
        .sort((o1, o2) -> o1.key().compareTo(o2.key()))// 5
        .flatMap(g -> g.count().map(count -> g.key() + " => " + count))// 6
        .subscribe(System.out::println); // 7
差別在第 6 步驟用 Flux.flatMap() 去打平 Flux of GroupedFlux,但特別的地方在對 GroupedFlux.count()  回傳的 Mono<Long> 去呼叫它的 map(),因為 Mono<Long> 也是流,所以可以呼叫 Mono.map() 來得到內容(也就是數量)再組成字串,不過因為是流的關係,所以最後要用 flatMap 去打平。
---
---
---

2017-12-28

com.fasterxml.jackson.core 的 Jackson Annotations 2.9.3

蠻常用 Jackson 將 Java 物件轉成 JSON(writeValueAsString),或者將 JSON 轉回 Java 物件(readValue)。
public class Book implements Serializable {

  private String title;
  private int year;

  public String getTitle() {
    return this.title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public int getYear() {
    return this.year;
  }

  public void setYear(int year) {
    this.year = year;
  }

  @SuppressWarnings("unchecked")
  public static void main(String[] args) throws IOException {
    Book b = new Book();
    b.setTitle("Jackson");
    b.setYear(2017);
    String json = new ObjectMapper().writeValueAsString(b);
    System.out.println(json); // {"title":"Jackson","year":2017}
    Map<String, String> m = new ObjectMapper().readValue(json, Map.class);
    System.out.println(m); // {title=Jackson, year=2017}
  }
}
但聽說它有非常多特別的 Annotation。

2017-06-03

Java 腦袋學 Python Regular Expression

Python Regular Expression 的套件為 re。

compile(), search() & findall()

#!python3
import re
regex = re.compile(r'\d{4}-?\d{6}') # r 表示使用 Raw string,即不 escape 反斜線 

s = '手機號碼有兩種,第一種是 0912-345678,第二種是 0912345678。'

# search 只會找到第一個符合的
mo = regex.search(s)
if mo != None:
    print(mo.group())
# 0912-345678

# findall 會找到全部符合的    
mo = regex.findall(s)
for mo in mobile_regex.findall(s):
    print(mo)
# 0912-345678
# 0912345678
Raw string 請參考 Java 腦袋學 Python 字串,如果不用 Raw string,那所有反斜線都要改用兩個反斜線,這樣變得很麻煩而且讓原本就難以閱讀的 RE 語法變得更加外星語。

2017-06-02

Python 執行檔

指定 Python 版本

由於可以同時安裝多個版本的 Python,因此 Python 須指名使用的 Python 版本。

在 Windows 上,每個 Python 檔案都要加上以下的檔頭,表示使用 Python 3。
#!python3
而在 Linux 上,則是加上以下的檔頭。
#!/usr/lib/python3
這是從命令提示字元下執行 Python 檔案才需要指定版本的檔頭,如果是從 IDLE 或者直接執行 python.exe,就不需要指名版本,因為已經在特定版本的 Python 環境裡了。

hello.py
#!python3
print('Hello Python!')
之前都是用 python.exe 執行 py 檔,加上檔頭後可以改用 py.exe 執行 py 檔,py.exe 的特色在於會讀取 #! 並使用對應的 Python 版本來執行。

2017-05-31

Java 腦袋學 Python OOP

Python OOP 基本概念與 java 類似,但不需要預先定義 API。
class Point:
    '''This is a doc string'''

# 不需要 new 關鍵字
p = Point()
print(p) # <__main__.Point object at 0x01C5BD70>
print(p.__doc__) # This is a doc string

# 動態建立欄位
p.x = 3
p.y = 4

print('(%d, %d)' % (p.x, p.y)) # (3, 4)
import math
print(math.sqrt(p.x**2 + p.y**2)) # 5.0

__init__() 與 __str__()

當然也可以預先定義屬性(API),這裡的__init__() 就是 Java 的 constructor,並定義 __str__() 回傳的字串,也就是 Java 裡的 toString()。
class Point:
    '''This is a doc string'''
    def __init__(self, x=0, y=0): # 可以給預設值,也可以不要
        self.x = x
        self.y = y
    def __str__(self):
        return 'Point (%d, %d)' % (self.x, self.y)

p = Point() # 使用預設值
print(p) # Point (0, 0)
p.x = 3
p.y = 4
print(p) # Point (3, 4)

print(Point(1, 2)) # Point (1, 2) 使用自訂值

物件特徵

和 Java 一樣,Python 物也是 pass by reference 與 mutable。