一個東西變兩個? 複視要看哪一科?

年紀過了一個歲數,身體也免不得開始有些狀況,這個月的瓊斯盃買了一張場邊席的票,但當天我的雙眼看出去的畫面是下圖這樣,真的是白白浪費錢了...


真的要說有什麼徵兆,應該是前陣子,每天一早寫待辦清單,眼睛就覺得有點對不上焦,以為是老花的關係就沒有放在心上。這個月去日本玩還帶上 macbook,每晚回到飯店繼續精實,又是看股票、又是剪輯影片上片,假期回到台灣是凌晨,隔天就直接上班,當天起床左腦就開始頭痛,又接著公司體檢三天低渣飲食(總共頭痛了四天),邊頭痛邊半夜起來拉肚子,想想應該都是讓自己太累的累積。

健檢完過了幾天好日子,但每每從辦公椅起來的時候,視覺上就會有種說不出的怪,還以為散光變嚴重了,但只要過一段時間就會慢慢恢復,還一度懷疑是不是眼鏡的鏡片位置跑掉了,直到某天下班發現怎麼調整,看到的景象都還是上下疊影才意識到代誌大條了。

不得不說現在有 AI 真的好用,真的能快速解答自己的疑問,先不管答案正確性與否,多少都有安定心情的效果。確認了自己單眼不會有散光現象,要雙眼同時打開才會散光,AI 給了我一個答案 - 複視,建議儘速先去看眼科,再去看神經內科。

說實在的,複視看到的景色完全沒法讓人定位遠近跟方向,靠著遮住一隻眼睛慢慢騎才能順利騎到眼科,初步檢查判定控制眼球的肌肉/神經異常,但視神經沒有腫脹,開了兩瓶眼藥水(乾澀/疲勞)請我趕快去看神經內科(還真的跟 AI 講的流程一樣)。隔天就殺去神經內科看診,初步檢查外加腦部斷層掃描後,醫生給了一個要住院檢查的結論,原本想說只是檢查,結果變成要住院,當下真的腦袋無法思考,不過現在哪家醫院病床都是滿的,我後續還是乖乖上了兩天班(賺住院費),才排到病床順利住院。

住院後補了一個腦部 MRI 檢查,檢查完後續查了一下網路,都是出現腦瘤、腦出血、動脈瘤這些詞,腦中滿滿的都是要被開顱手術的畫面,不過還好住院第二天,醫生就給了一個報告看起來都正常的好消息,不是視神經發炎,就是視神經感染,神經感染要做腰椎穿刺才能確認,但我沒發燒等感染症狀,就讓我免掉這一針之痛,先假設是神經發炎,打了五天的類固醇來來治療。

這五天住院的每日行程,6:30起床,7:00聽財經YT,8:00吃早餐,8:30打類固醇,9:30醫生關懷,11:30午餐,15:00午休,17:30晚餐,22:00睡覺。三餐就直接吃醫院待訂的便當,白蘿蔔默默地一直重複出現在便當裡頭,雖然蠻樸素的,但不用多一個人力還是自己下去買 - 無價。住院的病人大概就吃飯、洗碗、洗澡、上廁所這些狀況比較不方便時,才需要人力幫忙,其他時候照顧者也真的是把時間耗在旁邊,這是我看了旁邊三個大人跟一個小孩耗在病房一下午的小小心得,小孩玩手遊沒用耳機,大人跟病人也都沒法好好休息,沒休息好 > 情緒不好 > 唸小孩不要玩手機,根本無間地獄。

上面寫的行程中間都隔了一大段空白間隔,除了初剪去日本玩的影片、看財經電子書、刷刷 bilibili(看躺平被優化以片)/YT(AI 服務相關教學/財經),平常很忙的時候,會很想好好靜下來看部電影,但有了整整五天空檔,就只看了1/3的全面啟動,現在刷 NETFLIX 就跟刷 XBOX 遊戲特價商城一樣,滑一滑就有飽足感了,真的心酸,硬是要說,又開始看了停追很久的毒命藥師(出院後馬上又停追了),用了1.5倍速觀賞葬送的芙莉蓮(被推坑,出院馬上買了一套二手漫畫,但完全沒翻)。

出院的一些體悟吧,平常把自己的節奏過得太快,所有的空白都塞滿滿,也不管塞的東西有沒有營養價值,思維步調都預設為二倍速進行,活在當下體驗當下,把專注力跟心力留給自己,都是一直知道但卻很容易就飄掉的紙上談兵,另外寫到這的當下,也發現自己好久沒有好好寫些東西了,也許只是記錄的形式不一樣(進化史:文字 > 攝影 > 影片),有些人寫東西也許是為了別人的讚和認同,身為過來人的我,再回頭看我過去寫的東西,其實最大的受益人是未來當下的我,原來我以前這樣想過,但如果寫的東西能幫助到爬到此文章的你,那也是緣分啦。

目前出院一週,複視狀況還是有,持續口服類固醇中,日後再更新

小米65吋電視開箱(S Mini LED 2025 65型)

2021/2購入的第一台小米電視65"掛點了,18999元看了快四年(2024/12)也值得了,發生的問題跟下面影片一樣,螢幕畫面跟聲音都正常,但播放影片就是有聲無影,不管是外接的第四台/Chromecast/遊戲機,還是系統內建的 YouTube 都一樣狀況。

小米電視有聲無影影片

小米電視有聲無影

然後四年過去,上網更新了一下電視價格以及技術現況,小米電視用這幾年習慣了也沒什麼大問題,就繼續鎖定小米電視這系列,原本想捏一下上75吋,不過電視底座大小已經超過電視櫃只好放棄,2024/12當下有的產品如下:

價格 螢幕更新頻率 對比 亮度 備註
A 2025 65型 4K 12,995 60Hz - - 價格考量,只看第四台
A Pro 2025 65型 QLED 15,995 60Hz - - 會看高畫質影片
S Mini LED 2025 65型 24,995 144Hz 7000:1 600 nits (typ) / 1200 nits (peak) 會玩次世代遊戲機


小米型號蠻好區別,S Pro > S > A Pro > A,寫文當下台灣小米沒看到 S Pro 的65吋就沒研究了。A 系列就是最傳統的 LED TV,黑畫面不夠純黑。A Pro 系列就是 QLED 技術(藍光 LED 背光 + 量子點強化膜)讓顏色對比傳統 LED TV 更鮮豔。S 系列就是 Mini LED 技術(背光模組改用更小更多顆的 LED 發光),因為可以更精準控光,改善了黑畫面不夠純黑的問題。

在 momo 下標隔了一個六日,等了四五天後才來裝機,舊的小米電視就直接讓師傅回收了(買新機並使用廢四機回收政策:民眾購買新電視機、電冰箱、洗衣機及冷暖氣機(簡稱四機),當販賣業者交付新機至民眾家時,民眾可以請販賣業者免費協助回收舊機,但免費服務的部分限於搬(載)運,不包含為搬運或拆卸而動用大型機具、工程施工等。)

裝機後的看電視 POV 照片,拍起來還是好小,無預算的人建議還是買越大越好,人的眼睛真的不嫌螢幕大,整個外觀跟我2021買的那台設計風格一樣,不看螢幕表現真的沒感覺有換過電視。

隔了三年多的升級,幾個很有感的升級,電視開機速度變快,內建介面的切換速度變快,遙控器的按鍵有變好按。不放心資料回祖國,所以沒有用小米電視內建的 Google TV,依舊是沿用外接的 Chromecast 來看 YouTube / Netflix / Disney+ 等服務,另外兩個 HDMI 則是給第四台以及 Switch 使用。

每個 HDMI 的畫面設定都是獨立的,預設的亮度實在太亮了,三個 HDMI 介面都把亮度調低(20以下),第四台有開動態補償,Chromecst 和 Switch 就都關閉動態補償,原本幀數是多少就看多少。影片黑色的部分,真的有感受到 Mini LED 主打的高對比,不過不知道黑畫面佔比太大時就像看鏡子看到自己的反射,只能靠降低環境光的方式降低螢幕的反射了。

整體來說,眼睛有感升級,家中是掛壁或是電視櫃夠大的話,建議直接上75吋,追劇真的很爽。

Instagram(IG) 切換多個帳號、管理多個帳號、小帳

兩個人想要在自己的手機上(已經有各自的私人 Instagram 帳號),共同管理其他 Inatagram 帳號(品牌社群帳號、小孩帳號、寵物帳號)?看下面的截圖教學:

  1. 點擊自己 Instagram 主頁面右上角三明治圖示(三條線),開啟設定和動態
  2. 拉到最下方選擇『新增帳號』
  3. 選擇『切換帳號』
  4. 選擇『登入現有的帳號』
  5. 會跳出登入畫面,輸入想要共同管理的 Inatagram 帳號和密碼即可

 
1) 點擊自己 Instagram 主頁面右上角三明治圖示(三條線),開啟設定和動態

2) 拉到最下方選擇『新增帳號』

3) 選擇『切換帳號』

4) 選擇『登入現有的帳號』

Android File Transfer 無法與裝置連接

利用 Android File Transfer 這個官方工具,嘗試將 Pixel 6a 連接到我的 macbook,但一直發生『無法與裝置連接』的錯誤訊息。依照官方建議,重新安裝 Android File Transfer、電腦重新開機、手機重新開機、連接線材都更換了都沒用。

最後發現是 Google Drive 在搞鬼,將其結束後 Android File Transfer 就可以正常看到 Pixel 6a 的資料夾。


兩年 YouTuber 不藏私剪片流程 SOP 分享

最近一直有觀眾說我剪片很快,這邊就來分享一下我剪片的流程,也給各位有興趣當 YouTuber 或是已經有剪輯影片的人參考,沒有耐心或是已經有經驗者,就直接看圖就好,但以下流程不適用於手機剪輯的人。

流程圖其實已經說得很清楚,紅色的框框是直接提升硬體就能縮短時間的部分,硬碟要用 SSD、家裡網路上傳速度要夠快、剪片用的電腦推薦直接用 Apple M1/M2 系列筆電,原本我用 Apple i5 的筆電輸出25分鐘1080P的影片都要快半個小時,換了 Apple M1 筆電就只要3分鐘左右就輸出完了,以我的流程要輸出三次,就省了快一個半小時。

以我的感受,影片最耗時的第一個是剪片,除了一台剪片不會頓的電腦,熟記剪輯軟體的熱鍵也是一定要的,我不會想要第一次就看很仔細一次剪到位,而是第一次從頭看到尾,邊看邊把有趣的片段留下,第二次再重新檢視剛剛那些片段是否有必要留下(減法才是最難的),然後重新調整素材的先後次序,讓影片呈現的流程跟地點比較順暢,以我的例子都是場外活動、慢慢走近場內、球場裡頭、比賽、比賽結束結語。

第二個耗時的工作就是上字幕了,以2023現在,我覺得最快的上字幕方式就是利用剪映這個編輯軟體提供自動產生字幕功能(YouTube 搜尋剪映+字幕 有很多教學影片),輸出成 srt 在用文字編輯程式轉繁體中文、大量文字替換來校正錯別字,最後 srt 匯入剪映,跟著影片播放再從頭到尾校正一次。

此外,我的文案還是影片的封面圖,都會保留之前的範本,可以再利用產生新的文案跟封面。影片上常用的特效/音效/音樂/短影片都有另外保留下來,就不用每次都要重新找素材。

整體來說,如果時間允許跟狀態很好的話,大概5~6小時就能剪完上傳一支影片,但身為非正職而且家裡又有小孩的不專業人士,時間就是得切割成很多段使用,也就是為什麼我會畫出這張流程圖,讓我可以切割剪片的流程成一個個小任務,分多天慢慢完成,也不會忘記自己少做了什麼!

以上就是我目前剪片的流程,有任何問題也歡迎下方留言問我!

PS:FCP 是指 Apple 出的影片剪輯軟體 - Final Cut Pro

 

Eclipse 啟動發生 libjvm.dylib does not contain the JNI_CreateJavaVM symbol

Eclipse/SpringToolSuites4 啟動發生 libjvm.dylib does not contain the JNI_CreateJavaVM symbol

更新 openjdk 後,無法開啟 Eclipse/SpringToolSuites4,錯誤訊息如下 :

The JVM shared library "/Users/foo/Library/Java/JavaVirtualMachines/adopt-openjdk-11.0.12/Contents/Home/bin/../lib/server/libjvm.dylib" does not contain the JNI_CreateJavaVM symbol.

看起來應該是更新的 openjdk(adopt-openjdk-11.0.12)有問題,編輯 Eclipse/SpringToolSuites4 的 Info.plist,指定要執行的 Java 路徑(原先正常的 adopt-openjdk-11.0.11)即可。

<!-- Info.plist 搜尋 -vm 字串 --> 

<string>-vm</string><string>/Users/foo/Library/Java/JavaVirtualMachines/adopt-openjdk-11.0.11/Contents/Home/bin/java</string>

為什麼要使用 Java Optional 以及 Optional 常用的方法有哪些

你用過 Java Optional 嗎?

本文假設讀者你已經用過 Optional 或是介接過回傳 Optional 的 API,知道 Optional 主要是解決 java.lang.NullPointerException(剛好是本站用的網址 XD),但還是不太知道為什麼要用 Optional!

換個角度看 Java Optional

我覺得站在開發 API 的角度來看這個問題,就很清楚為何要使用 Optional!

  • 如果回傳的結果可能為 null 就該用 Optional<T>。
  • 反之,不可能回傳 null 就跟以前一樣就用 T。

使用你 API 的人,就可以根據這個資訊,知道呼叫這個方法需不需要處理 null 的問題,而不是每個方法都要多寫處理 null 的邏輯,或是沒處裡就會發生 java.lang.NullPointerException。

// 回傳 People 代表這方法絕對會回傳一個我爸
public People getMyFather()

// 回傳 <Optional>People 代表可能回傳空物件
public <Optional>People getMyFather()

如果今天你寫的系統,每個人都必定會有個爸爸,那回傳 People 的 getMyFather() 符合你的意圖,而回傳 <Optional>People 的 getMyFather() 因為有可能回傳空物件的語意,所以就不適合用此方法。

若你寫的系統允許父不詳的狀況,那回傳 <Optional>People 的 getMyFather() 就是比較好的寫法。

我看到第一個回傳 People 的 getMyFather(),我很清楚我不用處理 null 的狀況,也就不用多寫處理 null 的邏輯。而第二個方法我就需要用 isPresent() 檢查回傳的 Optional<People>是否為一個空物件。

Java Optional 常用的方法整理

下面程式碼說明 of、ofNullable 這兩個方法:

Foo foo = null;

// of()不接受傳入 null,此處會發生 NullPointerException
Optional<Foo> optionalFoo1 = Optional.of(foo);

// ofNullable()可以傳入 null,此處會得到一個空物件 
Optional<Foo> optionalFoo2 = Optional.ofNullable(foo);

下面程式碼說明 ifPresent、get、getElse、orElseGet、orElseThrow:

// 從 getBar 取得一個 Optional<Bar>
// 由回傳 Optional<Bar> 來看
// 我們預期有可能收到空物件
Optional<Bar> optionalBar = getBar();

// 用 ifPresent + get 的寫法跟原本檢查 null 的寫法一樣差
if (optionalBar.ifPresent()) // 檢查是否為空物件
{
  Bar bar = optionalBar.get(); // 取得包在裏頭的 bar
  bar.doSometing();
}

// 當 optionalBar 裏頭包的是 null,會建立一個新的 Bar 物件並回傳
Bar bar = optionalBar.getElse(new Bar());

// 當 optionalBar 裏頭包的是 null,會建立一個新的 Bar 物件並回傳
Bar bar = optionalBar.orElseGet(() -> { 
    // 這邊可以多做一些事,比如印 Log
    return new Bar();
}));

// 當 optionalBar 裏頭包的是 null,會丟出一個自訂的例外
Bar bar = optionalBar.orElseThrow(() -> new MyException("WTF")));

結語

開發 API 的人使用 Optional 來說明回傳的物件是不是有可能為 null,讓使用 API 的人看到介面就知道是否要處理 null。

而 getElse() 能改善以往檢查是否為空,如果不為空就...的寫法,讓程式碼更簡潔更容易閱讀。

三分鐘讀懂 Encode 編碼、Encrypt 加密、Hash 雜湊

Encode、Encrypt、Hash 的差別

Encode 編碼Encrypt 加密Hash 雜湊
簡單定義原本的資料用不同的詮釋方式產生新的資料利用金鑰來保護資料 將資料用公式算出一個無法反推回原本資料的結果
是否能還原資料?知道編碼方式即可還原資料知道加密方式
+ 取得金鑰即可還原資料
無法還原資料
舉例摩斯密碼
網址編碼
Base64
AES(對稱加密)
RSA(非對稱加密)
md5
SHA1
SHA256
Encode、Encrypt、Hash 對照表

密碼要怎麼保存

如果今天在資料庫要保存一個密碼:

  1. 將密碼編碼後保存:被駭客知道編碼方式,就能還原成原本密碼。
  2. 將密碼加密後保存:被駭客取得金鑰以及加密方式,就能還原成原本密碼。
  3. 將密碼雜湊後保存:網路上有人用字典搭配各種雜湊演算法計算過後產生對照表,如果使用者用的密碼是人可閱讀的、常見密碼,就可以從對照表反查出原本密碼。

你可以制定密碼規則,強迫使用者建立比較難想到的密碼(比如要有大小寫、特殊符號、數字),這樣的密碼就比較不會從對照表被反查出來,但會造成使用者要建立自己也記不太住的密碼。

另外一種方式,就是使用者可以建立他們記得住的密碼,然後系統會在密碼後方加上由系統產生的特殊字串(就是所謂的 Salt),達到不造成使用者困擾,又能建立少見密碼的效果。

結論:

將密碼加鹽,雜湊後保存。

Windows 工作排程器如何處理還在執行中的工作

Windows 工作排程器 > 設定 > 如果工作已在執行中,下列規則將會套用:(If the task is already running, then the following rule applies)

之前用 Windows 工作排程器 都是用來執行會自動關閉的程式/Script,所以沒有設定上圖的這個選項『如果工作已在執行中,下列規則將會套用:(If the task is already running, then the following rule applies)』都沒有關係,因為舊工作很快就執行完成,執行新工作的時候不會看到還在執行中的舊工作。

這次嘗試要每天重啟一個會持續執行的程式,因為該程式發生問題並不會自己關閉,在設定上遇到了一些問題,先來解釋一下上面圖片的四個選項:

  • 不要啟動新執行個體(Do not start a new instance):預設選項。準備執行新工作時,如果舊工作還在執行,那就不會執行新工作。
  • 以平行方式執行新執行個體(Run a new instance in parallel):不管舊工作是否還在執行,都會執行新工作。
  • 佇列新執行個體(Queue a new instance):準備執行新工作時,如果舊工作還在執行,會將新工作放入一個佇列等待,等舊工作執行完畢才會執行新工作。
  • 停止現有的執行個體(Stop the existing instance):準備執行新工作時,如果舊工作還在執行,會先將舊工作關閉再執行新工作。

首先要注意的是,不要用手動執行工作的方式來驗證,你會發現每次新工作都會執行成功,但實際上自動跑的時候,行為結果都跟你想的不一樣。可以利用反覆設定僅一次的觸發時間,等到觸發時間到來驗證看看新工作是否有順利執行。

以下針對我這次的問題做紀錄,我想要定期重新執行一個 exe 檔案(因為它會發生錯誤且不會結束),四個選項觀察到的結果如下:

  • 不要啟動新執行個體(Do not start a new instance):不會執行新工作,所以排程時間過了,還是只會看到原先執行的 exe 檔案。
  • 以平行方式執行新執行個體(Run a new instance in parallel):會執行新工作,所以會看到越來越多 exe 檔案被執行。
  • 佇列新執行個體(Queue a new instance):會將新工作放入一個佇列等待,但因為原先執行的 exe 檔案不會結束,所以排程時間過了,還是只會看到原先執行的 exe 檔案。
  • 停止現有的執行個體(Stop the existing instance):沒有正確關閉原先執行的 exe 檔案(bug?),所以會看到越來越多 exe 檔案被執行。

最終的做法是,在排程工作中加入 taskkill 指令(用來刪除原先執行的 exe 檔案) + 佇列新執行個體(Queue a new instance),確保新工作總是會被執行,而執行新工作的第一個步驟就是刪除執行中的 exe 檔案,再重新執行 exe 檔案。

MultiBio zkemkeeper.CZKEMClass 無法監聽即時事件

MultiBio700 門禁考勤人臉指紋機

介接 MultiBio 門禁考勤人臉指紋機時,利用官網提供的 zkemkeeper SDK,能正確讀取設備上的打卡資料,但想要即時接收打卡事件時,完全照抄官方範例,卻怎樣都收不到設備即時回傳的事件。最大的差別在於官方範例是一個 Windows Forms 程式,我正在開發的是一個主控台(Console)程式。

可能原因是 zkemkeeper SDK 收到設備即時回傳事件後,使用的訊息派送機制跟 Windows Forms 程式會用到的 Message Loop 相關。所以嘗試在一開始就用 Application 提供的 Run() 方法,執行目前執行緒的標準應用程式 Message Loop。

記得要用 .NET Framwork 專案,而不是 .NET / .NET Core 專案(C# 工程師可能才知道我在說什麼),要不然沒有 System.Windows.Forms.Application 可以用,完整程式碼如下:

Thread thread = new Thread(() =>
{
  zkemkeeper.CZKEMClass axCZKEM1 = new zkemkeeper.CZKEMClass();

  bool bIsConnected = false; // the boolean value identifies whether the device is connected
  int idwErrorCode = 0;
  bIsConnected = axCZKEM1.Connect_Net("x.x.x.x", Convert.ToInt32("4370"));
  if (bIsConnected == true)
  {
    iMachineNumber = 1;// In fact,when you are using the tcp/ip communication,this parameter will be ignored,that is any integer will all right.Here we use 1.
    bool regEventResult = axCZKEM1.RegEvent(iMachineNumber, 65535);//Here you can register the realtime events that you want to be triggered(the parameters 65535 means registering all)
  }
  else
  {
    axCZKEM1.GetLastError(ref idwErrorCode);
    Console.WriteLine(String.Format("Unable to connect the device, ErrorCode={1}", idwErrorCode.ToString()));          
  }

  axCZKEM1.OnAttTransactionEx += new    zkemkeeper._IZKEMEvents_OnAttTransactionExEventHandler(axCZKEM1_OnAttTransactionEx); // 註冊監聽事件

  Application.Run(); 
});

thread.IsBackground = true;
//thread.SetApartmentState(ApartmentState.STA); // 我註解掉也可以運作!
thread.Start();

Console.ReadLine(); // 接收到輸入才會結束程式

參考

Java 常用的 Functional Interface:Consumer、Function、Predicate、 Supplier

在看 Functional Interface 之前,先看看以下例子,如果我們只能寫一個方法,要在一個字串集合內,找到預期的字串,同時要能支援完全比對/忽略大小寫的話,寫出來的程式碼大概長的是以下這個樣子:


/*
* expect:是要比對的字串。
* ignoreCase:true 代表比對可以忽略大小寫。
*/
public boolean findAnyMatch(List<String> list, String expect, boolean ignoreCase)
{
  if (ignoreCase) // 若任一個字串符合傳入的字串(忽略大小寫),就回傳 true。
  {
    for (String string : list)
    {
      if (expect.equalsIgnoreCase(string))
      {
        return true;
      }
    }
  }
  else // 若任一個字串符合傳入的字串(要完全一樣),就回傳 true。 
  {
    for (String string : list)
    {
      if (expect.equals(string))
      {
        return true;
      }
    }
  }
		
  return false;
}

...

System.out.println(foo.findAnyMatch(list, "Moe", false)); // true
System.out.println(foo.findAnyMatch(list, "Jack", false)); // false
System.out.println(foo.findAnyMatch(list, "moe", true)); // true

原本的寫法除了又臭又長之外,還有多重巢狀結構,不容易閱讀。

同樣的方法改用 lamda 表達式作為 findAnyMatch 傳入參數的寫法,程式碼如下,是不是短了很多?甚至還可以在不更動 findAnyMatch 的情況下,找出是否有包含 "o" 的字串。


/*
* predicate:是一個只定義一個抽象方法 interface,告訴傳入的 Lambda 表達式會收到一個參數,然後要回傳一個 boolean。
*/
public boolean findAnyMatch(List<String> list, Predicate<String> predicate)
{
  // 若任一個字串符合傳入的 predicate 檢查的條件,就回傳 true。
  return list.stream()
    .filter(predicate) // 傳入的 lambda 會收到一個字串,回傳 boolean
    .findFirst()
    .isPresent();
}

...

System.out.println(foo.findAnyMatch(list, (s) -> s.equals("Moe"))); // true
System.out.println(foo.findAnyMatch(list, (s) -> s.equals("Jack"))); // false
System.out.println(foo.findAnyMatch(list, (s) -> s.equalsIgnoreCase("moe"))); // true
System.out.println(foo.findAnyMatch(list, (s) -> s.contains("o"))); // true

所以大致的演進流程是:用傳入的參數來影響方法內的行為 > 直接把行為(lambda)當作參數。同樣的效果用傳入匿名類別當作參數也可以做到,但傳入 lambda 的寫法看起來比較短、比較好閱讀。

Java 把 lambda 寫法常用到的 Interface 先定義出來,我們在導入 lamda 寫法改寫原有方法時,就不用重複定義這些常出現的介面。比如上面用到的 Predicate 介面,它預期接收一個字串類型物件,然後返回一個 boolean 值。

以下整理出程式碼常見到的 Functional Interface:

Functional Interface輸入回傳
PredicateT 類型物件boolean
ConsumerT 類型物件不用回傳
FuctionT 類型物件R 類型物件
SupplierT 類型物件
BiPredicateT 類型物件 + U 類型物件boolean
BiConsumerT 類型物件 + U 類型物件不用回傳
BiFuctionT 類型物件 + U 類型物件R 類型物件

Intellij IDEA 啟動跳出 Unsupported Java Version

升級完 Intellij IDEA 後,要開啟 Intellij IDEA 就跳出了 Unsupported Java Version 這個警告,在 Console 下查看 Java 版本是 openjdk version "11.0.2" 2019-01-15,版本大於 Java 11,應該不是環境的問題。

執行以下指令排除此問題:

rm ~/Library/Application\ Support/JetBrains/IntelliJIdea2020.3/idea.jdk

特殊註解:TODO、FIXME、XXX

最早接觸到這些特殊註解是在寫 Eclipse 的時候,有些自動產生的程式碼就會自己補上 //TODO,在 Tasks 分頁就能顯示出這些備註,點擊就直接跳到程式碼上相當方便。

實際上在 Preferences > Java > Compiler > Task Tags 下可以看到預設只支援 TODO、FIXME 和 XXX 這三種特殊註釋。

除了 TODO、FIXME 和 XXX 外,我也列出了我比較少用,但在其他地方有看到的特殊註釋:

  • TODO:知道要實作什麼功能,但還沒開始寫的程式碼。比如://TODO 接上 LDAP 認證
  • FIXME:發現比較少發生的 bug,但當下沒時間改。比如://FIXME 對方回傳結果缺乏 foo 欄位,會造成 NullPointerException。
  • XXX:很醜但可以正常運作的程式碼,我會先用這個註釋標註,未來有空回來優化。比如://XXX 目前用 iterator 的方式掃描集合,可改用 Stream 的方式。
  • 少用
    • NOTE:說明這段程式碼怎麼運作。因為等於在寫一般的單行註解,所以我沒用過。
    • HACK:這裡用非正規的方法實作功能(Android 開發常用 reflection 機制呼叫沒開放的功能)。因為跟 XXX 類似,所以我很少用。
    • BUG:這裡有 bug。因為跟 FIXME 類似,所以我很少用。

Eclipse 啟動發生 Caused by: java.lang.ClassNotFoundException: javax.annotation.PreDestroy

因為開發環境的 SpringToolSuite4 要求更新 Java,更新到 Java 11 之後,反而 SpringToolSuite3 就打不開了,啟動就跳出下圖:

Eclipse 無法啟動

按照指示查看 log,看起來是跟 Java 9 引入的 module 功能相關,造成找不到這個 PreDestroy 這個類別。


java.lang.NoClassDefFoundError: javax/annotation/PreDestroy
        at org.eclipse.e4.core.internal.di.InjectorImpl.disposed(InjectorImpl.java:426)
        at org.eclipse.e4.core.internal.di.Requestor.disposed(Requestor.java:154)
        at org.eclipse.e4.core.internal.contexts.ContextObjectSupplier$ContextInjectionListener.update(ContextObjectSupplier.java:78)
        at org.eclipse.e4.core.internal.contexts.TrackableComputationExt.update(TrackableComputationExt.java:111)
        at org.eclipse.e4.core.internal.contexts.TrackableComputationExt.handleInvalid(TrackableComputationExt.java:74)
        at org.eclipse.e4.core.internal.contexts.EclipseContext.dispose(EclipseContext.java:176)
        at org.eclipse.e4.core.internal.contexts.osgi.EclipseContextOSGi.dispose(EclipseContextOSGi.java:106)
        at org.eclipse.e4.core.internal.contexts.osgi.EclipseContextOSGi.bundleChanged(EclipseContextOSGi.java:139)
        at org.eclipse.osgi.internal.framework.BundleContextImpl.dispatchEvent(BundleContextImpl.java:903)
        at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
        at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:148)
        at org.eclipse.osgi.internal.framework.EquinoxEventPublisher.publishBundleEventPrivileged(EquinoxEventPublisher.java:213)
        at org.eclipse.osgi.internal.framework.EquinoxEventPublisher.publishBundleEvent(EquinoxEventPublisher.java:120)
        at org.eclipse.osgi.internal.framework.EquinoxEventPublisher.publishBundleEvent(EquinoxEventPublisher.java:112)
        at org.eclipse.osgi.internal.framework.EquinoxContainerAdaptor.publishModuleEvent(EquinoxContainerAdaptor.java:156)
        at org.eclipse.osgi.container.Module.publishEvent(Module.java:476)
        at org.eclipse.osgi.container.Module.doStop(Module.java:634)
        at org.eclipse.osgi.container.Module.stop(Module.java:498)
        at org.eclipse.osgi.container.SystemModule.stop(SystemModule.java:202)
        at org.eclipse.osgi.internal.framework.EquinoxBundle$SystemBundle$EquinoxSystemModule$1.run(EquinoxBundle.java:165)
        at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.ClassNotFoundException: javax.annotation.PreDestroy cannot be found by org.eclipse.e4.core.di_1.6.1.v20160712-0927
        at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:410)
        at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:372)
        at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:364)
        at org.eclipse.osgi.internal.loader.ModuleClassLoader.loadClass(ModuleClassLoader.java:161)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        ... 21 more

解決方法:以下方法為 Eclipse / SpringToolSuite 通用,到 Eclipse / SpringToolSuite 資料夾下編輯 eclipse.ini 或 STS.ini,在 -vmargs 之前加上 -vm 參數指定啟動使用的 Java,下方範例我指定到我原本安裝的 Java 8 其 bin 資料夾下的 java 檔案。

另外要注意,-vm 是一列,指定路徑也是單獨一列,記得換行!


... 忽略
-vm
/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/bin/java
-vmargs
... 忽略

iTerm2 管理 SSH 帳號密碼

在 Mac 上用 iterm2 管理所有會連線的主機,這樣就不用每次在記 IP / 帳號 / 密碼,首先先安裝 sshpass,再去 iterm2 設定 Profile(一個 Profile 可以想成一個主機),詳細步驟如下:

安裝 sshpass

打開命令列,利用 homebrew 來安裝 sshpass:


brew install https://raw.githubusercontent.com/kadwanev/bigboybrew/master/Library/Formula/sshpass.rb

設定 iTerm2

打開 iTerm2,Preferences > Profiles 分頁,在 Command 區域將選項設為 Command ,旁邊文字欄填入連線資訊。


# 連線資訊格式說明
# /usr/local/bin/sshpass -p 密碼 ssh -p Port 帳號@IP

# 範例:連線到 192.168.8.8,Port 是 22,帳號是 root,密碼是 iampassword
/usr/local/bin/sshpass -p iampassword ssh -p 22 root@192.168.8.8

設定畫面如下:

Mock 與 Stub 的差別

假設我們要測試資料庫,除了連接真正的資料庫或是本地端的測試資料庫以外,我們也可以生成假的物件來測試,以下列出 Stub / Mock 這兩種做法的測試案例,大家可以自行體會其中的差別。

Stub


/** 能連接至 MySQL 的實作 */
public class DatabaseMySql implements Database
{
  public boolean connect()
  {
    // 省略
    return true; // 連接真正的的資料庫,真的連上才回傳 true。
  }
}

/** 為了測試用的實作 */
public class DatabaseStub implements Database
{
  public boolean connect()
  {
    return true; // 直接回傳 true。
  }
}

// 分隔線

public void testStub()
{
  Database databaseStub = new DatabaseStub(); // 建立 stub

  System.out.println(databaseStub.connect()); // 印出 true
}

Mock


public void testMock()
{
  Database databaseMock = mock(Database.class); // 使用 mockito 建立 mock
  when(databaseMock.connect()).thenReturn(true); // 當呼叫 connect() 就回傳 true

  System.out.println(databaseStub.connect()); // 印出 true
}

Stub vs Mock

對我來說如果用 Mock 寫法的話:

  • 要替每個測試定義其 databaseMock 物件的輸入輸出。
  • 無需定義 Database 介面。
  • 無需實作 DatabaseStub 類別,複寫所有 Database 介面定義的方法。

我實務上比較常用 Stub 的做法:

  • 因為不用特別引入第三方函式庫就可以實現了。
  • 而且對介面開發而非對實作開發也本來就是一個好的習慣。

關於更專業的解釋,可以參考 Mocks Aren't Stubs 這一篇經典文章。

Java Stream API Cheat Sheet

在 Java 8 之後,可以用 Stream API 來處理資料集合,最大的好處就是巢狀結構消失了,比較容易閱讀、看出商業邏輯,以下紀錄比較常用的操作:

建立 Stream

  • of
  • empty
  • concat
  • generate:產生無限串流 的方法之一。
  • iterate:產生無限串流的方法之二。
// 示範 of
Stream<String> fooBarBazStream = Stream.of("foo", "bar", "baz");

// 示範 empty
Stream<String> emptyStream = Stream.empty(); 

// 示範 concat
Stream<String> stream1 = Stream.of("foo", "bar");
Stream<String> stream2 = Stream.of("baz");
Stream<String> stream3 = Stream.concat(stream1, stream2);

// 示範 generate
Stream<String> helloStream = Stream.generate(() -> "hello"); // 總是回傳 "hello"
String<Double> randomStream = Stream.generate(Math::random); // 每次回傳一個亂數

// 示範 iterate
Integer seed = 1;
Stream<Integer> oneTwoFourEghitStream = Stream.iterate(seed, n -> n*2); // 1, 2, 4, 8...

中間操作

  • peek:常用來 debug,印出經過元素。
  • limit
  • skip
  • distinct:去除重複
  • sorted
  • filter
  • map:將經過的元素類型轉換成別種元素類型,比如:員工物件轉成姓名字串。
    • mapToInt
    • mapToLong
    • mapToDouble
  • flatMap:將經過元素轉換成該元素內資料集合的元素,比如:一個員工有兩個電話(手機和住家電話),將傳入的三位員工轉成六個電話字串。
    • flatMapToInt
    • flatMapToLong
    • flatMapToDouble
// 示範 distinct
Stream.of("a", "b", "a", "b")
  .distinct()
  .forEach(e -> System.out.print(e)); // 印出 "ab"

// 示範 map
employees.stream()
  .map(e -> e.getName())
  .forEach(name -> System.out.println(name)); // 印出所有員工的姓名

// 示範 flatMap
employees.stream()
  .flatMap(e -> e.getPhoneList().stream()) // 傳入一位員工,發送出多筆電話
  .forEach(phone -> System.out.println(phone)); // 印出所有員工的電話

終止操作

終止操作完了以後,Stream 就不能再使用了。

  • max
  • min
  • findFirst
  • findAny
  • anyMatch:有符合條件的元素回傳 true。
  • allMatch
  • noneMatch
  • orElse
  • count
  • forEach:很像 peek,但 peek 是中間操作,需要一個終止操作才能執行。
  • collect:返回的是集合。
  • toArray:返回的會是陣列。
  • reduce:可以去看一下 Google 提出的 MapReduce 概念
// 示範 collect
List<String> nameList = 
  employees.stream()
    .map(e -> e.getName())
    .collect(Collectors.toList());

Java 安裝在 Docker 容器的記憶體問題

Docker 容器佔用大量記憶體空間

Docker VM 的記憶體使用量很高,進去看16G 的記憶體的機器,才裝12個 Docker 容器,一個一個檢查才發現幾個是跑 Java APP 的容器吃的記憶體特別高(1G~3G 之間),但有些容器都是 Serverless 類型的服務,不應該佔用那麼多記憶體。

Java 支援 Docker 的記憶體限制

在容器內部執行 JVM,預設 Max Heap Size 會是主機記憶體的1/4,而非容器記憶體的1/4(所以我 docker run 設定 memory 參數也沒用)。

在Java 8u131 之後支援了 Docker CPU 和記憶體的限制,在 Java 8 上要使用以下參數:

  • UnlockExperimentalVMOptions:開啟實驗性質的選項。
  • UseCGroupMemoryLimitForHeap:使用 cgroup(control group) 記憶體大小來計算 Max Heap Size ,白話就是偵測到 Docker 容器的記憶體。
  • MaxRAMFraction:將偵測到的記憶體除以這個數字,就是 Max Heap Size。這邊設為1,搭配 UseCGroupMemoryLimitForHeap 的效果就是 Docker 容器的記憶體大小除以1就是 Max Heap Size,白話就是不會超過 Docker 容器設定的記憶體。

# 建立一個記憶體500 MB 有 JRE 8 的容器,執行 Java 指令印出 Max. Heap Size
# 調整 docker run 的 memory 參數,可以影響容器內 JVM 使用的記憶體大小。
docker run --memory 500MB openjdk:8-jre java \
-XX:+UnlockExperimentalVMOptions \
-XX:+UseCGroupMemoryLimitForHeap \
-XX:MaxRAMFraction=1 \
-XshowSettings:vm \
-version

參考:

修正 Dockerfile 以及 docker run

了解上一章節的參數後,首先記得 docker run 一定要加上 memory 參數來限制容器的記憶體。

再來就是將原本執行 Java 程式的命令列(看是寫在 docker run 指令還是寫在 Dockerfile 裡頭)加上參數。這樣就能解決 Docker 容器跑 Java 程式佔用太多記憶體的問題。


# 執行 app.jar
docker run --memory 500MB openjdk:8-jre java \
-XX:+UnlockExperimentalVMOptions \
-XX:+UseCGroupMemoryLimitForHeap \
-XX:MaxRAMFraction=1 \
-jar path/to/your/app.jar

# Dockerfile
# 省略
CMD ["java", "-Dspring.profiles.active=test", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:MaxRAMFraction=1", "-XshowSettings:vm",  "-jar", "path/to/your/app.jar"]

Intellij IDEA 控制台(Console)無法 Search/Find

Console 無法搜尋字串

在 Console 視窗按 ⌘ + F 不會跳出搜尋功能。

因為開發環境會使用 Eclipse 和 Intellij IDEA,不想記兩套快捷鍵,所以 keymap 就設定為 Eclipse(macOS),Eclipse(macOS) 預設 Find 和 Replace 都是空的,原本只替 Replace(通常找到就是想要替代)設定 ⌘ + F,Find 沒有設定熱鍵所以就沒作用。

替 Find 指定快捷鍵

Preferences > Keymap ,選取 Main menu > Edit > Find,指定 Find... 和 Replace... 的熱鍵即可。

  • Find...:⌘ + F
  • Replace...:⌥ + F

MacBook Pro 14"(2021) M1 Max 入手

上次換機是2020/8,距今約兩年半,上一台 MacBook Pro 13" 是未代的 intel i5 / 16G Ram / 500G SSD,當初沒有等一下換 M1 版本,主要是怕程式開發工具有支援度問題,另外我有外接三螢幕需求,需要買到最貴的 M1 Max 才支援三螢幕輸出,這次買的是整修品(M1 Max / 64G Ram / 1T SSD)價格為85300元。

現在這台 MacBook Pro 13" 開發上很夠用,缺點大概是沒用的 touch bar 還有比較容易熱,然後這一兩年有了剪片需求,整個剪輯流程大概要輸出長度20分鐘的1080P影片三次,每次都要等20分鐘以上,現在換上 M1 Max 大概兩分鐘左右就輸出完成了,大大減少我等待的時間。

這次兩台電腦轉移的體驗也很好,同個網段下開啟系統移轉輔助程式,兩個多小時完成,大部分的 APP 跟個人資料都轉移過去了,我是碰到 Chrome 轉移失敗、MS Offiece 沒有轉移這兩個問題而已,重新安裝就解決了!

關於多螢幕部分,完全沒問題可以輸出三個螢幕,目前外接的做法是:

  • MacBook Pro 14" 上的 HDMI 輸出 > 螢幕*1
  • HyperDrive 9-in-1 USB-C HUB 上的 HDMI 輸出 > 螢幕*1
  • JUA365 USB3.0 to HDMI雙外接顯卡 > 螢幕*1

以上供有想要升級 M1 的朋友參考,有問題也可以留言問看看,我懂就幫忙測看看。


2023/2/16 更新

VirtualBox 6.1 不支援 M1/M2,VirtualBox 7.0 才有 Developer preview for macOS / Arm64 (M1/M2) hosts,但根據如何评价 VirtualBox 7.0 Beta 1 支持Apple Silicon?和我實際所有 VM (CentOS、Ubuntu)都無法啟動看來,目前 VirtualBox 並不支援! 

2023/6/30 更新

VirtualBox-7.0.8_BETA4-156879-macOSArm64 不支援 M1/M2