求求你了,不要再自己實現這些邏輯了,開源工具類不香嗎?

最近公司來了一批實習生,小黑哥負責帶一個。這位小師弟說實話,基本功很紮實,做事也非常靠譜,深得小黑哥真傳。

不過最近給其 Review 程式碼的時候,小黑哥發現小師弟有些程式碼邏輯有些繁瑣,有些程式碼小黑哥看來可以用一些開源工具類實現,不需要自己重複實現。

不過這也是正常的,小黑哥剛入行的時候寫的程式碼也是這樣,這幾年慢慢接觸了一些開源工具類,逐漸積累。現在寫程式碼才會直接用工具類替換自己實現的這些繁瑣的邏輯。

於是小黑哥給小師弟分享了幾個自己常用的開源工具類,小師弟學完直呼:『666』。

這裡小黑哥拋磚引玉,分享幾個常用的工具類,希望幫助到剛入行的同學們。其他編程老司機如果還有其他好用的工具類,歡迎評論區分享。

下文主要分享這幾個方向的常用工具類:

字元串相關工具類

Java 中 String 應該是日常用的最多一個類吧,平常我們很多程式碼需要圍繞 String ,做一些處理。

JDK 提供 String API 雖然比較多,但是功能比較基礎,通常我們需要結合 String 多個方法才能完成一個業務功能。

下面介紹一下 Apache 提供的一個工具類 StringUtils.

Maven Pom 資訊如下:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.10</version>
</dependency>

commons-lang 有兩個版本,一個是 commons-lang3 ,一個是 commons-lang 。

commons-lang 是老版本,已經很久沒有維護了。

commons-lang3 是一直在維護的版本,推薦直接使用這個版本。

注意:如果你系統已經有 commons-lang,注意如果直接替換成 commons-lang3,將會編譯錯誤。commons-lang3 中相關類與 commons-lang 一樣,但是包名不一樣。

判斷字元串是否為空

判斷字元串是否為空,想必每個人應該都寫過吧:

if (null == str || str.isEmpty()) {

}

雖然這段程式碼非常簡單,但是說實話,小黑哥以前還是在這裡犯過空指針的異常的。

img

使用 StringUtils ,上面程式碼可以替換下面這樣:

if (StringUtils.isEmpty(str)) {

}

StringUtils 內部還有一個方法 isBlank,也是用來判斷字元串是否為空,兩個方法比較相近,比較搞混,主要區別如下:

// 如果字元串都是空格的話,
StringUtils.isBlank(" ")       = true;
StringUtils.isEmpty(" ")       = false;   

判斷字元串是否為空,使用頻率非常高,這裡大家可以使用 IDEA Prefix 的功能,輸入直接生成判空語句。

字元串固定長度

這個通常用於字元串需要固定長度的場景,比如需要固定長度字元串作為流水號,若流水號長度不足,,左邊補 0 。

這裡當然可以使用 String#format 方法,不過小黑哥覺得比較麻煩,這裡可以這樣使用:

// 字元串固定長度 8位,若不足,忘左補 0
StringUtils.leftPad("test", 8, "0");

另外還有一個 StringUtils#rightPad,這個方法與上面方法正好相反。

字元串關鍵字替換

StringUtils 提供一些列的方法,可以替換某些關鍵字:

// 默認替換所有關鍵字
StringUtils.replace("aba", "a", "z")   = "zbz";
// 替換關鍵字,僅替換一次
StringUtils.replaceOnce("aba", "a", "z")   = "zba";
// 使用正則表達式替換
StringUtils.replacePattern("ABCabc123", "[^A-Z0-9]+", "")   = "ABC123";
....   

字元串拼接

字元串拼接是個常見的需求,簡單辦法使用 StringBuilder 循環遍歷拼接:

String[] array = new String[]{"test", "1234", "5678"};
StringBuilder stringBuilder = new StringBuilder();

for (String s : array) {
    stringBuilder.append(s).append(";");
}
// 防止最終拼接字元串為空 
if (stringBuilder.length() > 0) {
    stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
System.out.println(stringBuilder.toString());

上面業務程式碼不太難,但是需要注意一下上面這段程式碼非常容易出錯,容易拋出 StringIndexOutOfBoundsException

這裡我們可以直接使用以下方法獲取拼接之後字元串:

StringUtils.join(["a", "b", "c"], ",")    = "a,b,c"

StringUtils 只能傳入數組拼接字元串,不過我比較喜歡集合拼接,所以再推薦下 Guava 的 Joiner

實例程式碼如下:

String[] array = new String[]{"test", "1234", "5678"};
List<String> list=new ArrayList<>();
list.add("test");
list.add("1234");
list.add("5678");
StringUtils.join(array, ",");

// 逗號分隔符,跳過 null
Joiner joiner=Joiner.on(",").skipNulls();
joiner.join(array);
joiner.join(list);

字元串拆分

有字元串拼接,就會有拆分字元串的需求,同樣的 StringUtils 也有拆分字元串的方法。

StringUtils.split("a..b.c", '.')   = ["a", "b", "c"]
StringUtils.splitByWholeSeparatorPreserveAllTokens("a..b.c", ".")= ["a","", "b", "c"]

ps:注意以上兩個方法區別。

StringUtils 拆分之後得到是一個數組,我們可以使用 Guava 的

Splitter splitter = Splitter.on(",");
// 返回是一個 List 集合,結果:[ab, , b, c]
splitter.splitToList("ab,,b,c");
// 忽略空字元串,輸出結果 [ab, b, c]
splitter.omitEmptyStrings().splitToList("ab,,b,c")

StringUtils 內部還有其他常用的方法,小夥伴可以自行查看其 API。

日期相關工具類

DateUtils/DateFormatUtils

JDK8 之前,Java 只提供一個 Date 類,平常我們需要將 Date 按照一定格式轉化成字元串,我們需要使用 SimpleDateFormat

SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// Date 轉 字元串
simpleDateFormat.format(new Date());
// 字元串 轉 Date
simpleDateFormat.parse("2020-05-07 22:00:00");

程式碼雖然簡單,但是這裡需要注意 SimpleDateFormat,不是執行緒安全的,多執行緒環境一定要注意使用安全。

這裡小黑哥推薦 commons-lang3 下的時間工具類DateUtils/DateFormatUtils,解決 Date 與字元串轉化問題。

ps:吐槽一下,你們工程中有沒有多個叫 DateUtils 類?小黑哥發現我們現有工程,多個模組有提供這個類,每個實現大同小異。

使用方法非常簡單:

// Date 轉化為字元串
DateFormatUtils.format(new Date(),"yyyy-MM-dd HH:mm:ss");
// 字元串 轉 Date
DateUtils.parseDate("2020-05-07 22:00:00","yyyy-MM-dd HH:mm:ss");

除了格式轉化之外,DateUtils 還提供時間計算的相關功能。

Date now = new Date();
// Date 加 1 天
Date addDays = DateUtils.addDays(now, 1);
// Date 加 33 分鐘
Date addMinutes = DateUtils.addMinutes(now, 33);
// Date 減去 233 秒
Date addSeconds = DateUtils.addSeconds(now, -233);
// 判斷是否 Wie 同一天
boolean sameDay = DateUtils.isSameDay(addDays, addMinutes);
// 過濾時分秒,若 now 為 2020-05-07 22:13:00 調用 truncate 方法以後
// 返回時間為 2020-05-07 00:00:00
Date truncate = DateUtils.truncate(now, Calendar.DATE);

JDK8 時間類

JDK8 之後,Java 將日期與時間分為 LocalDateLocalTime,功能定義更加清晰,當然其也提供一個 LocalDateTime,包含日期與時間。這些類相對於 Date 類優點在於,這些類與 String 類一樣都是不變類型,不但執行緒安全,而且不能修改。

ps:仔細對比 mysql 時間日期類型 DATETIMEDATETIME,有沒有感覺差不多

現在 mybatis 等 ORM 框架已經支援 LocalDate 與 JDBC 時間類型轉化,所以大家可以直接將時間欄位實際類型定義為 JDK8 時間類型,然後再進行相關轉化。

如果依然使用的是 Date 類型,如果需要使用新的時間類型,我們需要進行相關轉化。兩者之間進行轉化, 稍微複雜一點,我們需要顯示指定當前時區。

Date now = new Date();
// Date-----> LocalDateTime 這裡指定使用當前系統默認時區
LocalDateTime localDateTime = now.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
// LocalDateTime------> Date 這裡指定使用當前系統默認時區
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());

接下來我們使用 LocalDateTime 進行字元串格式化。

// 按照 yyyy-MM-dd HH:mm:ss 轉化時間
LocalDateTime dateTime = LocalDateTime.parse("2020-05-07 22:34:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 將 LocalDateTime 格式化字元串
String format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(dateTime);

另外我們使用 LocalDateTime 獲取當前時間年份,月份特別簡單:

LocalDateTime now = LocalDateTime.now();
// 年
int year = now.getYear();
// 月
int month = now.getMonthValue();
// 日
int day = now.getDayOfMonth();

最後我們還可以使用 LocalDateTime 進行日期加減,獲取下一天的時間:

LocalDateTime now = LocalDateTime.now();
// 當前時間加一天
LocalDateTime plusDays = now.plusDays(1l);
// 當前時間減一個小時
LocalDateTime minusHours = now.minusHours(1l);
// 還有很多其他方法

總之 JDK8 提供的時間類非常好用,還沒用過小夥伴,可以嘗試下。

集合/數組相關

集合與數組我們日常也需要經常使用,也需要對其進行判空:

if (null == list || list.isEmpty()) {

}

ps: 數組、Map 集合與其類似

上面程式碼如字元串判空一樣寫起來都非常簡單,但是也比較容易寫出會拋出空指針異常的程式碼。這裡我們可以使用 commons-collections 提供工具類。

pom 資訊:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</vesion>
</dependency>

ps: 還有一個低版本的 ,artifactId 為 commons-collections

我們可以使用 CollectionUtils/MapUtils進行判空判斷。

// List/Set 集合判空
if(CollectionUtils.isEmpty(list)){

}
// Map 等集合進行判空
if (MapUtils.isEmpty(map)) {
    
}

至於數組判空判斷需要使用 commons-lang 下的 ArrayUtils進行判斷:

// 數組判空
if (ArrayUtils.isEmpty(array)) {
    
}

除此之外還有一些列的對於集合增強方法,比如快速將數組加入到現有集合中:

List<String> listA = new ArrayList<>();
listA.add("1");
listA.add("2");
listA.add("3");
String[] arrays = new String[]{"a", "b", "c"};
CollectionUtils.addAll(listA, arrays);

其他方法感興趣同學可以再自行研究下,另外 Guava 中也有提供對於集合的操作增強類 Lists/Maps,這個可以看下小黑哥之前寫的:老司機小黑哥帶你玩轉 Guava 集合類

I/O 相關

JDK 有提供一系列的類可以讀取文件等,不過小黑哥覺得那些類有些晦澀難懂,實現一個小功能可能還要寫好多程式碼,而且還不一定能寫對。

小黑哥推薦一下 Apache 提供的 commons-io 庫,增強 I/O 操作,簡化操作難度。pom 資訊:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

FileUtils-文件操作工具類

文件操作工具類提供一系列方法,可以讓我們快速讀取寫入文件。

快速實現文件/文件夾拷貝操作 ,FileUtils.copyDirectory/FileUtils.copyFile

// 拷貝文件
File fileA = new File("E:\\test\\test.txt");
File fileB = new File("E:\\test1\\test.txt");
FileUtils.copyFile(fileA,fileB);

使用 FileUtils.listFiles 獲取指定文件夾上所有文件

// 按照指定文件後綴如java,txt等去查找指定文件夾的文件
File directory = new File("E:\\test");
FileUtils.listFiles(directory, new String[]{"txt"}, false);

使用 FileUtils.readLines 讀取該文件所有行。

// 讀取指定文件所有行 不需要使用 while 循環讀取流了
List<String> lines = FileUtils.readLines(fileA)

有讀就存在寫,可以使用 FileUtils.writeLines,直接將集合中數據,一行行寫入文本。

// 可以一行行寫入文本
List<String> lines = new ArrayList<>();
.....
FileUtils.writeLines(lines)

IOUtils-I/O 操作相關工具類

FileUtils 主要針對相關文件操作,IOUtils 更加針對底層 I/O,可以快速讀取 InputStream。實際上 FileUtils 底層操作依賴就是 IOUtils

IOUtils可以適用於一個比較試用的場景,比如支付場景下,HTTP 非同步通知場景。如果我們使用 JDK 原生方法寫:

從 Servlet 獲取非同步通知內容

byte[] b = null;
ByteArrayOutputStream baos = null;
String respMsg = null;
try {
    byte[] buffer = new byte[1024];
    baos = new ByteArrayOutputStream();
  	// 獲取輸入流
    InputStream in = request.getInputStream();
    for (int len = 0; (len = in.read(buffer)) > 0; ) {
        baos.write(buffer, 0, len);
    }
    b = baos.toByteArray();
    baos.close();
  	// 位元組數組轉化成字元串
    String reqMessage = new String(b, "utf-8");
} catch (IOException e) {
  
} finally {
    if (baos != null) {
        try {
            baos.close();
        } catch (IOException e) {
           
        }
    }
}

上面程式碼說起來還是挺複雜的。不過我們使用 IOUtils,一個方法就可以簡單搞定:

// 將輸入流資訊全部輸出到位元組數組中
byte[] b = IOUtils.toByteArray(request.getInputStream());
// 將輸入流資訊轉化為字元串
String resMsg = IOUtils.toString(request.getInputStream());

ps: InputStream 不能被重複讀取

計時

編程中有時需要統計程式碼的的執行耗時,當然執行程式碼非常簡單,結束時間與開始時間相減即可。

long start = System.currentTimeMillis();   //獲取開始時間

//其他程式碼
//...
long end = System.currentTimeMillis(); //獲取結束時間

System.out.println("程式運行時間: " + (end - start) + "ms");

雖然程式碼很簡單,但是非常不靈活,默認情況我們只能獲取 ms 單位,如果需要轉換為秒,分鐘,就需要另外再計算。

這裡我們介紹 Guava Stopwatch 計時工具類,藉助他統計程式執行時間,使用方式非常靈活。

commons-lang3 與 Spring-core 也有這個工具類,使用方式大同小異,大家根據情況選擇。

// 創建之後立刻計時,若想主動開始計時
Stopwatch stopwatch = Stopwatch.createStarted();
// 創建計時器,但是需要主動調用 start 方法開始計時
// Stopwatch stopwatch = Stopwatch.createUnstarted();
// stopWatch.start();
// 模擬其他程式碼耗時
TimeUnit.SECONDS.sleep(2l);

// 當前已經消耗的時間
System.out.println(stopwatch.elapsed(TimeUnit.SECONDS));;

TimeUnit.SECONDS.sleep(2l);

// 停止計時 未開始的計時器調用 stop 將會拋錯 IllegalStateException
stopwatch.stop();
// 再次統計總耗時
System.out.println(stopwatch.elapsed(TimeUnit.SECONDS));;
// 重新開始,將會在原來時間基礎計算,若想重新從 0開始計算,需要調用 stopwatch.reset()
stopwatch.start();
TimeUnit.SECONDS.sleep(2l);
System.out.println(stopwatch.elapsed(TimeUnit.SECONDS));

輸出結果為:

2
4
6

總結

今天小黑哥拋磚引玉,介紹了字元串、日期、數組/集合、I/O、計時等工具類,簡化日常業務程式碼。大家看完可以嘗試一下,不得不說,這些工具類真香!

歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:studyidea.cn

Tags: