點贊再看,動力無限。 微信搜「 程序猿阿朗 」。
本文 Github.com/niumoo/JavaNotes 和 未讀代碼博客 已經收錄,有很多系列文章。
今天一個很簡單的功能,觸發了一個 BUG,處理後我想起了最近爆火的 ChatGPT,於是我嘗試測試 ChatGPT 能否發現這個 BUG。這篇文章會先介紹功能代碼,然後手動分析 BUG 原因;接着測試 ChatGPT 能否發現這個 BUG。
先說下結論,測試結束,ChatGPT 今後可能是我編程路上的好幫手。
前言
今天在測試一個準備上線的新功能時,裏面有一段簡單的函數,出現了一個 BUG。我先說一下這個函數的功能,看下你會怎麼寫。
功能描述
輸入:一個文件夾路徑。
輸出:這個文件夾中的文件夾數量。
這是一個實現起來比較簡單的函數,我順手寫了下面的代碼:
String pathString = "/Users/darcy/";
File fileDir = new File(pathString);
if (!fileDir.exists() || !fileDir.isDirectory()) {
throw new RuntimeException("xxx....");
}
String[] fileList = fileDir.list();
int count = 0;
for (String filePath : fileList) {
if (new File(pathString + filePath).isDirectory()) {
count++;
}
}
System.out.println(count);
功能是實現了,但是很明顯,代碼比較繁瑣,於是我又順手優化了一下。
Path path = Paths.get("/Users/darcy/");
if (!Files.exists(path) || !Files.isDirectory(path)) {
throw new RuntimeException("xxx....");
}
long dirCount = Files.list(path).filter(Files::isDirectory).count();
System.out.println(dirCount);
效果是一樣的,因爲使用了 JDK 7 引入的 Files 類,讓代碼簡單了不少。
不過正是這段代碼,觸發了 BUG,我先賣個關子,你先看下 BUG 在什麼地方。
問題分析
你看出問題了嗎?改造後的代碼運行少量次數的情況下,都是可以正常輸出的。但是它有一個問題,就是 Files.list 返回的 Stream 流沒有進行關閉,這就導致如果這段代碼被頻繁的調用,一段時間後將會因爲打開的文件過多而報錯,可以測試出這種異常情況。
問題復現
在循環中不斷運行這段代碼:
while (true){
Path path = Paths.get("/Users/darcy/");
if (!Files.exists(path) || !Files.isDirectory(path)) {
throw new RuntimeException("xxx....");
}
long dirCount = Files.list(path).filter(Files::isDirectory).count();
System.out.println(dirCount);
}
一段時間後控制檯收到報錯:Too many open files
問題原因
報錯的原因是因爲每個進程可以打開的文件數量是有限制的,如果一直打開不進行關閉,在達到限制時會報出這個錯誤。
不妨讓代碼運行速度降低,然後監控下隨着程序的運行,運行進程持有的文件數是否不斷增加。
while (true){
Path path = Paths.get("/Users/darcy/");
if (!Files.exists(path) || !Files.isDirectory(path)) {
throw new RuntimeException("xxx....");
}
long dirCount = Files.list(path).filter(Files::isDirectory).count();
System.out.println(dirCount);
Thread.sleep(1000);
}
下面是用到的命令。
## 查找進程 ID
➜ ~ jps -mlVv | grep testFileList
## 查看指定進程引用的文件數量
➜ ~ lsof -p 62248 | wc -l
從測試結果中可以看到隨着程序的運行,進程持有的文件數量不斷上升。
問題解決
原因已經知道了,既然 Files.list 返回的 Stream 流沒有進行關閉,那麼進行關閉就可以解決問題。
while (true){
Path path = Paths.get("/Users/darcy/");
if (!Files.exists(path) || !Files.isDirectory(path)) {
throw new RuntimeException("xxx....");
}
// 方式1:手動關閉流
// Stream<Path> pathStream = Files.list(path);
// long dirCount = pathStream.filter(Files::isDirectory).count();
// pathStream.close();
// 方式2:使用 try resource 自動關閉流
try (Stream<Path> pathStream = Files.list(path);){
long dirCount = pathStream.filter(Files::isDirectory).count();
System.out.println(dirCount);
}
Thread.sleep(1000);
}
再次運行後,引用的文件數量不再上漲,BUG 解決。
➜ ~ lsof -p 62879 | wc -l
321
➜ ~ lsof -p 62879 | wc -l
321
➜ ~ lsof -p 62879 | wc -l
321
➜ ~ lsof -p 62879 | wc -l
321
➜ ~ lsof -p 62879 | wc -l
321
➜ ~ lsof -p 62879 | wc -l
321
ChatGPT 分析
如果這段代碼使用 ChatGPT 分析,它能發現問題嗎?我先告訴你答案,它能發現,而且描述的很好,並且給出了修復建議。
ChatGPT 分析代碼
我問:
下面這段 Java 代碼有什麼問題嗎?
Path path = Paths.get("/Users/darcy/");
if (!Files.exists(path) || !Files.isDirectory(path)) {
throw new RuntimeException("fileDir.isDirectory() error");
}
long dirCount = Files.list(path).filter(Files::isDirectory).count();
System.out.println(dirCount);
ChatGPT:
這段代碼首先會檢查 /Users/darcy/
目錄是否存在並且是一個目錄,如果不是,則會拋出 RuntimeException
。接着,它會計算 /Users/darcy/
目錄下的子目錄數量並將其打印到控制檯。
這段代碼看起來沒有語法錯誤,但是在運行時可能會拋出 IOException
,因爲對文件系統進行操作時可能會出現各種異常情況。特別是,如果指定的路徑不存在,則會拋出 NoSuchFileException
。因此,在代碼中最好捕獲可能發生的異常並進行處理,或者在方法簽名中聲明拋出異常以通知調用方。
此外,還需要注意的是,Files.list(path)
方法返回的是一個 Stream<Path>
對象,需要注意其資源釋放問題,以避免潛在的資源泄漏問題。通常情況下,可以使用 try-with-resources
語句來自動關閉流,例如:
try (Stream<Path> stream = Files.list(path)) {
long dirCount = stream.filter(Files::isDirectory).count();
System.out.println(dirCount);
} catch (IOException e) {
// 處理異常
}
下面是對話截圖:
ChatGPT 實現功能
問:
幫我用 Java 寫一串代碼。 功能描述。
輸入:一個文件夾路徑。
輸出:這個文件夾中的文件夾數量。
ChatGPT 回答:
以下是 Java 代碼示例,用於計算指定文件夾路徑下的文件夾數量:
import java.io.File;
public class FolderCount {
public static void main(String[] args) {
String path = "path/to/folder"; // 替換成你的文件夾路徑
File folder = new File(path);
int count = 0;
if (folder.isDirectory()) {
File[] files = folder.listFiles();
for (File file : files) {
if (file.isDirectory()) {
count++;
}
}
}
System.out.println("該文件夾中的文件夾數量爲:" + count);
}
}
該程序首先接收一個文件夾路徑作爲輸入,然後創建一個 File 對象來代表該文件夾。然後它遍歷該文件夾中的所有文件,如果找到一個文件夾,則將計數器加一。最後,它輸出計數器的值作爲結果。
下面是對話截圖:
總結
在本文中,我們介紹了一個簡單功能的 Java 代碼 BUG,即未關閉 Files.list()
返回的流對象,導致程序運行時可能會打開過多的文件而出現異常。我們通過手動分析代碼和測試,找出了問題所在,並提供了兩種解決方案,一種是手動關閉流,另一種是使用 try-with-resources 自動關閉流。最後,我們還使用 ChatGPT 測試了是否可以用 AI 自動識別代碼中存在的問題,結果表明 ChatGPT 可以幫助我們發現這類問題,爲我們的編程工作提供了有力的支持。
上面這個總結,也是 ChatGPT 幫我寫的,相對於 ChatGPT 能給出具體的代碼,我更震驚於 ChatGPT 對於語義的理解,不僅能理解我的輸入,還能對代碼意思進行自然語言描述。今後你很可能分辨不出你所閱讀的文章是人類寫的,還是 ChatGPT 自動生成的。
附錄:ChatGPT 寫代碼測試
以下測試結果都是 ChatGPT 的具體回答,我沒有改動一個字符。
問1
幫我用 Java 寫一段代碼。
輸入:一個時間字符串,這個時間信息包含年月日時分秒。
處理:提取時間字符串中的月份
輸出:這個月份有多少天
ChatGPT 回答:
以下是一個示例Java代碼,它接受一個時間字符串作爲輸入,並從中提取出月份並計算該月有多少天:
import java.time.*;
import java.time.format.DateTimeFormatter;
public class MonthDays {
public static void main(String[] args) {
String dateString = "2023-02-14 12:34:56"; // 輸入時間字符串
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse(dateString, formatter);
int daysInMonth = YearMonth.from(dateTime).lengthOfMonth(); // 提取月份並計算該月有多少天
System.out.println(daysInMonth); // 輸出該月天數
}
}
在這個示例中,我們使用了Java 8引入的java.time
包來處理日期和時間。首先,我們將輸入時間字符串解析爲一個LocalDateTime
對象。然後,我們從LocalDateTime
對象中提取出年月,並使用YearMonth
類的lengthOfMonth()
方法計算該月份有多少天。最後,我們將該天數輸出到控制檯。
下面是測試截圖:
問2
幫我用 Java 寫一段代碼。
輸入:一個時間字符串,這個時間信息包含年月日時分秒。
輸出:把這個時間字符串進行轉換。獲取這個時間當天的第三小時第一秒
ChatGPT 回答:
以下是用 Java 實現獲取輸入時間當天第三小時第一秒的代碼示例:
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) {
String timeString = "2023-02-14 12:34:56"; // 代表輸入的時間字符串
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse(timeString, formatter); // 解析時間字符串
// 獲取當天的3:00:01
LocalDateTime targetDateTime = LocalDateTime.of(dateTime.toLocalDate(), LocalTime.of(3, 0, 1));
System.out.println(targetDateTime);
}
}
解釋:
- 首先,定義一個代表輸入時間的字符串
timeString
。 - 然後,使用
DateTimeFormatter
類創建一個日期時間格式化器,以便將時間字符串轉換爲LocalDateTime
對象。此處使用的格式化器模式字符串爲"yyyy-MM-dd HH:mm:ss"
,其中:
yyyy
代表四位數的年份,例如 2023。MM
代表兩位數的月份,例如 02。dd
代表兩位數的日期,例如 14。HH
代表兩位數的小時數(24 小時制),例如 12。mm
代表兩位數的分鐘數,例如 34。ss
代表兩位數的秒數,例如 56。
- 使用
LocalDateTime.parse()
方法將時間字符串解析爲LocalDateTime
對象。 - 接下來,使用
LocalDateTime.toLocalDate()
方法獲取輸入時間所在的日期,並使用LocalTime.of()
方法創建一個LocalTime
對象,代表目標時間當天的 3:00:01。 - 最後,使用
LocalDateTime.of()
方法將日期和時間組合成一個LocalDateTime
對象,即獲取了輸入時間當天的第三小時第一秒。打印該對象即可。
注意:這裏的代碼示例假設輸入時間字符串的格式是固定的。如果輸入時間的格式可能會發生變化,需要根據實際情況調整格式化器模式字符串。
問3
幫我用 Java 寫一段代碼。
輸入:一個長度爲10的全是小寫字母的字符串
處理1:把這個字符串按字母逆序
處理2:把這個字符串第3,6,9位字母轉換成大寫
處理3:截取字符串的前7位進行輸出
ChatGPT 回答:
以下是用 Java 實現對輸入字符串進行處理的代碼示例:
public class Main {
public static void main(String[] args) {
String inputStr = "abcdefghij"; // 輸入的字符串
// 處理1:按字母逆序
String reverseStr = new StringBuilder(inputStr).reverse().toString();
// 處理2:第3,6,9位字母轉換成大寫
StringBuilder resultStrBuilder = new StringBuilder(reverseStr);
resultStrBuilder.setCharAt(2, Character.toUpperCase(reverseStr.charAt(2)));
resultStrBuilder.setCharAt(5, Character.toUpperCase(reverseStr.charAt(5)));
resultStrBuilder.setCharAt(8, Character.toUpperCase(reverseStr.charAt(8)));
String resultStr = resultStrBuilder.toString();
// 處理3:截取前7位進行輸出
String outputStr = resultStr.substring(0, 7);
System.out.println(outputStr);
}
}
解釋:
- 首先,定義一個代表輸入字符串的變量
inputStr
。 - 處理1:使用
StringBuilder
類的reverse()
方法將字符串逆序,並使用toString()
方法轉換爲普通字符串。將逆序後的字符串保存在reverseStr
變量中。 - 處理2:使用
StringBuilder
類創建一個新的字符串構建器,將逆序後的字符串reverseStr
作爲初始字符串。
- 使用
setCharAt()
方法將第3,6,9位字母轉換成大寫字母。 - 注意:字符串下標從0開始,因此第3,6,9位分別對應下標2、5、8。
- 將處理後的字符串構建器轉換爲普通字符串,保存在
resultStr
變量中。 - 處理3:使用
substring()
方法從處理後的字符串resultStr
中截取前7位,保存在outputStr
變量中。 - 最後,使用
System.out.println()
方法輸出outputStr
變量的值。
<完>
文章持續更新,可以微信搜一搜「 程序猿阿朗 」或訪問「程序猿阿朗博客 」第一時間閱讀。本文 Github.com/niumoo/JavaNotes 已經收錄,有很多系列文章,歡迎Star。