開發(fā)實戰(zhàn):從Swing界面到JDBC持久化)
簡介針對備忘錄管理信息化需求開發(fā)的畢業(yè)設(shè)計文檔面向計算機相關(guān)專業(yè)學(xué)生及需要快速理解SSM框架MySQL開發(fā)流程的開發(fā)者。內(nèi)容涵蓋系統(tǒng)用戶管理、備忘錄管理、日志管理、登錄與退出等核心模塊的設(shè)計思路并完整呈現(xiàn)緒論、技術(shù)介紹、系統(tǒng)分析、測試等章節(jié)可作為課程設(shè)計或畢業(yè)設(shè)計的參考模板。資源包僅含1個docx文檔大小690KB已有78人學(xué)習(xí)。文檔基于Java語言采用Spring、SpringMVC、MyBatis三層架構(gòu)結(jié)合Eclipse工具與MySQL數(shù)據(jù)庫詳細說明了從需求分析到系統(tǒng)測試的全過程對比傳統(tǒng)管理模式突出了信息化管理在效率與經(jīng)濟成本上的優(yōu)勢有助于讀者快速掌握SSM框架在小型管理系統(tǒng)中的實際應(yīng)用。1. 為什么“課程設(shè)計級”的備忘錄系統(tǒng)反而值得認真拆解很多人看到“基于Java的備忘錄管理系統(tǒng)”這種題目第一反應(yīng)是“這不就是個增刪改查嗎有什么好寫的”。這個反應(yīng)我太熟悉了因為我自己帶過不少新人也評審過大量類似的課設(shè)、畢設(shè)項目。但我想說的是正因為這類系統(tǒng)看起來簡單它才是一個絕佳的試金石——它能檢驗?zāi)銓ava基礎(chǔ)、面向?qū)ο笤O(shè)計、數(shù)據(jù)持久化、異常處理、用戶交互這一整條鏈路是否真的吃透了。備忘錄管理系統(tǒng)的核心價值不在于“記事”本身而在于它幾乎覆蓋了Java后臺開發(fā)的全部基本功JavaBean的封裝、集合框架的使用、文件或數(shù)據(jù)庫的讀寫、分層架構(gòu)的拆解、時間日期的處理、界面與邏輯的分離。這些能力是通用的今天你把它用在備忘錄上明天換成圖書管理、日程管理、個人財務(wù)架構(gòu)思路完全一致。我在實戰(zhàn)中見過兩種極端一種是純面向過程的寫法所有邏輯堆在一個類里界面代碼和業(yè)務(wù)代碼糾纏在一起改一個按鈕就要翻幾百行代碼另一種是過度設(shè)計為了一個500行的系統(tǒng)引入了Spring Boot、MyBatis、Maven多模塊結(jié)果環(huán)境配置花了兩天還沒寫一行業(yè)務(wù)代碼。這兩種都很可惜。這篇博文我會按一個“剛好合適”的粒度來拆解——既保留分層設(shè)計帶來的可維護性又不至于用大炮打蚊子。適合正在做課程設(shè)計、畢業(yè)設(shè)計或者想用一個小項目把Java基礎(chǔ)串起來的朋友參考。2. 系統(tǒng)的功能定位與核心需求分析2.1 備忘錄系統(tǒng)“應(yīng)該”做成什么樣在動手寫代碼之前先想清楚系統(tǒng)邊界。我見過太多人一上來就寫代碼寫到一半發(fā)現(xiàn)需求含糊又開始返工。備忘錄系統(tǒng)的核心功能其實非常聚焦就三類記事新增、編輯、刪除備忘錄條目記錄標題、正文、創(chuàng)建時間、最后修改時間。分類與檢索按類別或關(guān)鍵詞篩選備忘錄否則記事越來越多之后根本無法使用。提醒與狀態(tài)管理給備忘錄設(shè)置提醒時間或優(yōu)先級把“已完成”“待辦”狀態(tài)區(qū)分開。從用戶角度來說一個能用的備忘錄系統(tǒng)不需要花哨但一定要邏輯閉環(huán)。新增的記錄能查到改過的內(nèi)容能反映刪除之前有確認搜索時能命中。這些流程打通之后系統(tǒng)才是“可用”的——很多課程設(shè)計恰恰就敗在這上面功能按鈕擺了一排但數(shù)據(jù)流是斷的新增完去列表頁看不到改完狀態(tài)刷新還是舊值。2.2 典型用戶畫像與使用場景我建議在需求分析階段做一次用戶場景的推演。比如用戶在早上創(chuàng)建一個“下午三點給客戶回電”的備忘設(shè)置優(yōu)先級為高類別為工作下午處理完以后把狀態(tài)改為已完成晚上查看時能過濾“只看未完成事項”。這整個流程就是系統(tǒng)設(shè)計的驗收標準。這樣的場景推導(dǎo)看起來很基礎(chǔ)但它的作用非常大它會決定你的類怎么切、方法怎么命名、異常怎么處理。例如你會發(fā)現(xiàn)需要一條“查詢所有未完成備忘”的方法那代碼結(jié)構(gòu)里就必須有findByStatus這類接口而不是每次查出來再在內(nèi)存里循環(huán)過濾——后者在小數(shù)據(jù)量下能跑但做法是錯的而且一旦數(shù)據(jù)量上來就暴露出性能問題。2.3 技術(shù)選型Swing還是JavaFX文件還是數(shù)據(jù)庫技術(shù)選型是每個Java桌面項目繞不開的問題而很多人都選錯了。界面方面Java Swing和JavaFX是目前主流的兩條路線。對于課程設(shè)計我更推薦 Swing理由很實際學(xué)習(xí)資料多、教材覆蓋率極高、JDK自帶無需額外配置、代碼示例隨便一搜就有。JavaFX 雖然界面更現(xiàn)代但它從JDK 11開始被剝離出標準JDK需要單獨引入依賴對于只想專注業(yè)務(wù)邏輯的初學(xué)者反而多了一道坎。數(shù)據(jù)存儲方面我建議分階段先做文件存儲如TXT或CSV把序列化和IO讀寫練熟再進階到SQLite或H2嵌入式數(shù)據(jù)庫。不要一上來就裝MySQL桌面級備忘錄工具用獨立的數(shù)據(jù)庫服務(wù)器太重了而且部署和移植都麻煩。SQLite是單文件數(shù)據(jù)庫Java里通過JDBC驅(qū)動就能操作體驗和文件存儲幾乎一樣簡單但已經(jīng)能把SQL語句練上手了。架構(gòu)方面不管數(shù)據(jù)層用什么分層是最重要的界面層只負責(zé)展示和收集輸入業(yè)務(wù)層處理規(guī)則和流轉(zhuǎn)數(shù)據(jù)層管持久化。這樣分層之后你把文件存儲換成數(shù)據(jù)庫界面和業(yè)務(wù)代碼幾乎不用動。這個收益在寫代碼時感受不明顯等需求變化、出bug排查時你就知道分層的價值了。3. 核心模塊設(shè)計與關(guān)鍵代碼實現(xiàn)3.1 實體層設(shè)計備忘錄對象該怎么建模實體類是整個系統(tǒng)的地基地基沒打好后面的代碼處處別扭。我的建議是使用一個Memo類來承載核心數(shù)據(jù)字段設(shè)計如下public class Memo implements Serializable { private Integer id; // 唯一標識自增 private String title; // 標題必填 private String content; // 正文內(nèi)容 private String category; // 分類工作/生活/學(xué)習(xí)等 private Integer priority; // 優(yōu)先級1高 2中 3低 private Integer status; // 狀態(tài)0待辦 1已完成 private LocalDateTime createdAt; // 創(chuàng)建時間 private LocalDateTime remindAt; // 提醒時間可為空 private LocalDateTime updatedAt; // 最后修改時間 }有幾個設(shè)計細節(jié)值得展開說。第一id字段建議用包裝類型Integer而不是基本類型int因為后面從數(shù)據(jù)源讀取時可能為空自動拆箱容易拋NullPointerException。第二時間字段統(tǒng)一使用LocalDateTime而不是DateLocalDateTime是Java 8引入的新時間API線程安全且操作方法豐富格式化解析也更直觀。第三priority和status我建議用int而不是直接存字符串這樣查詢排序和過濾時比較方便顯示層再根據(jù)數(shù)值映射成文字。實體類要重寫toString()方法方便開發(fā)階段打印日志排查問題同時實現(xiàn)Serializable接口如果第一版用文件存對象這個接口是必須的。這是很多資料里不會強調(diào)但實際開發(fā)中非常實用的細節(jié)。3.2 數(shù)據(jù)訪問層從文件存儲到JDBC的完整演進數(shù)據(jù)訪問層我建議按兩條路線來寫不是二選一而是循序漸進。第一版文件存儲練序列化和IO。public class MemoFileDao { private static final String FILE_PATH memos.dat; public void save(ListMemo memos) throws IOException { try (ObjectOutputStream oos new ObjectOutputStream( new FileOutputStream(FILE_PATH))) { oos.writeObject(memos); } } SuppressWarnings(unchecked) public ListMemo load() throws IOException, ClassNotFoundException { File file new File(FILE_PATH); if (!file.exists()) { return new ArrayList(); } try (ObjectInputStream ois new ObjectInputStream( new FileInputStream(file))) { return (ListMemo) ois.readObject(); } } }這段代碼看著簡單但這套思路很關(guān)鍵try-with-resources語法保證流自動關(guān)閉避免內(nèi)存泄漏先判斷文件是否存在再讀取避免首次運行拋異常用ArrayList作為默認返回值避免上層拿到null再去判空。這些都是實戰(zhàn)中非常常見的坑。第二版SQLite數(shù)據(jù)庫練JDBC和SQL。public class MemoDao { private Connection getConnection() throws SQLException { return DriverManager.getConnection(jdbc:sqlite:memo.db); } public void createTable() { String sql CREATE TABLE IF NOT EXISTS memo ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, category TEXT, priority INTEGER DEFAULT 2, status INTEGER DEFAULT 0, created_at TEXT, remind_at TEXT, updated_at TEXT); try (Connection conn getConnection(); Statement stmt conn.createStatement()) { stmt.execute(sql); } catch (SQLException e) { e.printStackTrace(); } } }這里有一個特別容易踩的坑SQLite沒有專門的日期時間類型所以時間字段用TEXT字符串存儲。那么在寫入和讀取時就必須統(tǒng)一格式。我建議在DAO層做轉(zhuǎn)換用DateTimeFormatter.ISO_LOCAL_DATE_TIME格式化后再存入數(shù)據(jù)庫取出時再解析回LocalDateTime。如果有人在寫入時不格式化直接拼接對象toString取出時就解析不回來。再強調(diào)一個點JDBC操作里PreparedStatement是必須的千萬不要用字符串拼接SQL來傳參。既防SQL注入又不用手動處理引號轉(zhuǎn)義代碼還更清晰public void insert(Memo memo) { String sql INSERT INTO memo(title, content, category, priority, status, created_at, remind_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, ?); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, memo.getTitle()); ps.setString(2, memo.getContent()); ps.setString(3, memo.getCategory()); ps.setInt(4, memo.getPriority()); ps.setInt(5, memo.getStatus()); ps.setString(6, memo.getCreatedAt() null ? null : memo.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); ps.setString(7, memo.getRemindAt() null ? null : memo.getRemindAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); ps.setString(8, memo.getUpdatedAt() null ? null : memo.getUpdatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }注意兩個細節(jié)setInt和setString之后如果字段為null要用setNull或者像上面這樣提前判空否則LocalDateTime.format會直接空指針執(zhí)行增刪改用executeUpdate()執(zhí)行查詢用executeQuery()這兩個方法絕對不能混用。3.3 業(yè)務(wù)層設(shè)計把“規(guī)則”從界面中剝離出來業(yè)務(wù)層是很多課設(shè)忽略的但恰恰是拉開水平差距的地方。比如“刪除備忘錄時需要二次確認”“已完成狀態(tài)不能修改內(nèi)容只允許刪除”“統(tǒng)計不同分類的數(shù)量”——這些都是業(yè)務(wù)規(guī)則處理它們的代碼應(yīng)該統(tǒng)一放在MemoService而不是散落在各個窗口的監(jiān)聽器里。public class MemoService { private MemoDao memoDao; public MemoService() { this.memoDao new MemoDao(); } public boolean addMemo(String title, String content, String category, int priority, LocalDateTime remindAt) { if (title null || title.trim().isEmpty()) { throw new IllegalArgumentException(標題不能為空); } Memo memo new Memo(); memo.setTitle(title.trim()); // 其余字段set... memo.setStatus(0); memo.setCreatedAt(LocalDateTime.now()); memo.setUpdatedAt(LocalDateTime.now()); memoDao.insert(memo); return true; } public ListMemo findUnfinished() { return memoDao.findByStatus(0); } }業(yè)務(wù)層的好處體現(xiàn)在兩個場景。第一是參數(shù)校驗的統(tǒng)一收斂標題為空這種錯誤不管用戶是在哪個窗口按下保存都觸發(fā)同一套校驗邏輯不會出現(xiàn)“主窗口能存空標題、編輯窗口就不行”這種詭異現(xiàn)象。第二是擴展的便利性以后想加一個“到期備忘錄自動標記為超期”的規(guī)則只需在Service里增加一個方法界面層完全不用改。3.4 界面層設(shè)計Java Swing布局要點與事件綁定Swing界面層很容易寫成一坨亂麻核心原因是布局管理器沒有選對。我的建議是主界面用BorderLayout北邊放搜索和篩選工具欄中間放JTable列表南邊放操作按鈕。編輯界面用GridBagLayout或GroupLayout雖然這兩個布局寫起來繁瑣但效果和伸縮性遠超null布局。我這里要嚴厲警告一點絕對不要用setLayout(null)絕對坐標定位。這樣做的后果是窗口一旦拉伸控件全部擠在一起在不同分辨率的屏幕上效果還會不同后期加一個按鈕所有坐標全部要重新算。我看到過太多課設(shè)代碼就是這么寫的雖然能截圖交差但實際一運行全是問題。表格的構(gòu)建是界面層的核心建議使用DefaultTableModel包裝數(shù)據(jù)DefaultTableModel model new DefaultTableModel( new Object[]{ID, 標題, 分類, 優(yōu)先級, 狀態(tài), 提醒時間}, 0); public void refreshTable(ListMemo memos) { model.setRowCount(0); for (Memo m : memos) { model.addRow(new Object[]{ m.getId(), m.getTitle(), m.getCategory(), priorityText(m.getPriority()), statusText(m.getStatus()), m.getRemindAt() null ? — : m.getRemindAt().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm)) }); } }表格刷新有個常見問題直接model.setRowCount(0)再重新addRow會閃爍得很厲害。數(shù)據(jù)量小的時候看不出來一旦超過幾百條體驗就很糟糕。印象中比較高效的做法是使用TableModel的事件機制或者直接重新setModel一個新的DefaultTableModel這兩者刷新效率都更好。事件綁定方面每個按鈕的ActionListener里只做三件事收集界面輸入、調(diào)用Service方法、刷新表格。不要在監(jiān)聽器里寫任何業(yè)務(wù)邏輯比如“判斷標題是否為空”這種邏輯必須放到Service層。這樣監(jiān)聽器基本就是三行代碼saveBtn.addActionListener(e - { try { service.addMemo(titleField.getText(), contentArea.getText(), categoryBox.getSelectedItem().toString(), priorityBox.getSelectedIndex() 1, remindTimeField.getText().isEmpty() ? null : LocalDateTime.parse(remindTimeField.getText(), formatter)); refreshTable(service.findAll()); clearInputs(); } catch (IllegalArgumentException ex) { JOptionPane.showMessageDialog(this, ex.getMessage(), 提示, JOptionPane.WARNING_MESSAGE); } });4. 關(guān)鍵功能實現(xiàn)提醒機制與事務(wù)邊界4.1 提醒功能的實時性與輪詢方案取舍備忘錄要“有點用”提醒功能是加分項。Swing是單線程模型所有界面操作都在**EDT事件分發(fā)線程**上執(zhí)行所以在EDT里寫死循環(huán)等待就會卡死界面。最樸素的方案是用一個定時器輪詢數(shù)據(jù)庫看看有沒有到點的未提醒備忘。我的做法是使用javax.swing.Timer而不是java.util.Timer原因在于Swing版本的回調(diào)方法會在EDT線程上執(zhí)行可以直接安全地更新界面而無須手動SwingUtilities.invokeLater。示例代碼如下Timer reminderTimer new Timer(1000 * 30, e - checkReminders()); reminderTimer.start(); private void checkReminders() { ListMemo dueMemos service.findDueAndUnnotified(); for (Memo memo : dueMemos) { JOptionPane.showMessageDialog(mainFrame, 提醒 memo.getTitle(), 備忘錄提醒, JOptionPane.INFORMATION_MESSAGE); service.markNotified(memo.getId()); } }這里的Timer構(gòu)造函數(shù)第一個參數(shù)是毫秒間隔30秒檢查一次即可滿足大多數(shù)場景。不要設(shè)成1秒一次純屬浪費CPU而且彈窗頻率過密會嚴重干擾使用。這里有個設(shè)計細節(jié)service.findDueAndUnnotified()在數(shù)據(jù)庫層對應(yīng)的SQL是WHERE remind_at ? AND notified 0所以實體類中還需要加一個notified字段或者復(fù)用status字段來標記“已提醒”。我的經(jīng)驗是單獨加notified字段更清晰免得狀態(tài)語義混淆。4.2 時間比較的精度問題從毫秒到分鐘時間提醒有一個典型陷阱——LocalDateTime存的是納秒精度而界面展示和用戶認知通常精確到分鐘。你在數(shù)據(jù)庫里存了2025-06-08T15:30:00用戶看到的是15:30但在程序內(nèi)部比較時會帶上納秒部分導(dǎo)致兩個“看起來一樣”的時間equals返回false。所以在解析用戶輸入時我建議統(tǒng)一做一次truncatedTo(ChronoUnit.MINUTES)處理把秒和納秒全部截斷。這個動作既能讓時間的相等比較符合直覺還能避免數(shù)據(jù)庫里存出一堆莫名其妙的秒級時間數(shù)據(jù)LocalDateTime remindAt LocalDateTime.parse(text, formatter) .truncatedTo(ChronoUnit.MINUTES);另外提醒時間是過去的時間該怎么辦用戶在新增備忘時已經(jīng)把提醒時間設(shè)在昨天這種數(shù)據(jù)存進去沒有任何意義還可能導(dǎo)致啟動程序時瞬間彈出無數(shù)個歷史提醒窗口。所以Service的addMemo方法里必須有這個校驗remindAt.isBefore(LocalDateTime.now())就拋IllegalArgumentException(提醒時間不能早于當前時間)。4.3 刪除操作的二次確認與事務(wù)邊界刪除備忘錄時彈一個確認框是對用戶的基本尊重。但真正容易忽略的是刪除關(guān)聯(lián)數(shù)據(jù)的事務(wù)邊界。如果你的系統(tǒng)有“備忘錄和提醒記錄是兩張表”的關(guān)系刪除備忘錄時必須同時刪除它對應(yīng)的提醒記錄這兩個操作必須在一個事務(wù)里完成。JDBC事務(wù)的寫法要牢記先conn.setAutoCommit(false)然后執(zhí)行多個SQL全部成功再commit()任何一步失敗都要rollback()最后在finally里恢復(fù)autoCommit并關(guān)閉連接。很多新人只會寫單個SQL不知道Connection默認是自動提交的結(jié)果總以為兩條SQL之間天然就是原子的一旦第二條執(zhí)行失敗數(shù)據(jù)就出現(xiàn)了臟數(shù)據(jù)。不過話又說回來對于備忘錄管理系統(tǒng)這種體量的項目單表就能解決問題不一定要設(shè)計成多表關(guān)聯(lián)。表拆分得越多代碼復(fù)雜度上升越明顯即使有事務(wù)兜底也要考慮值不值。我的建議是把提醒狀態(tài)作為備忘錄表的一個字段放在同一張表里這樣根本不需要事務(wù)一條UPDATE就能解決簡單可靠。5. 踩過的坑從運行環(huán)境到代碼細節(jié)的實戰(zhàn)排查5.1 JDK環(huán)境變量配置的經(jīng)典問題界面、業(yè)務(wù)、數(shù)據(jù)層都寫完以后最尷尬的事情往往發(fā)生在運行環(huán)境上。很多同學(xué)的代碼在IDE里點“運行”完全正常但雙擊Jar包就報錯或者命令行java -jar啟動不了。排查下來十有八九是環(huán)境變量的問題。我在熱詞里看到“java環(huán)境變量配置詳細教程”“java安裝教程詳細”是高頻搜索說明這確實是攔路虎。環(huán)境變量配置的核心只有兩件事JAVA_HOME指向JDK安裝目錄Path里加上%JAVA_HOME%\bin。配置完以后在命令行輸入java -version和javac -version驗證兩個都能正常輸出版本號才算成功。如果安裝了多個JDK版本一定要把高版本的Path條目移到前面否則命令行用的還是舊版本。還有一個容易忽略的坑JDK和JRE混用?,F(xiàn)在JDK自帶了JRE但如果你的機器上單獨裝了JRE并且Path里JRE的bin排在JDK前面那么java命令用的是JRE。而JRE沒有javac所以就會出現(xiàn)“能運行不能編譯”的怪象。這屬于排查了半天都找不出原因、最后發(fā)現(xiàn)就是路徑順序問題的典型case。5.2 編譯期“軟件包不存在”與Lombok沖突問題有很多人在項目里用了LombokData注解寫得很爽結(jié)果換一臺電腦或者用命令行編譯時就報“程序包lombok不存在”。這個問題我在熱詞里也看到了“you arent using a compiler supported by lombok, so lombok will not work”。這說明編譯環(huán)境和Lombok的適配出了問題。對于備忘錄管理系統(tǒng)這個體量我的建議是放棄Lombok手寫getter/setter。雖然多打個十幾行字但它省掉的麻煩是實打?qū)嵉牟挥醚bIDE插件、不用配注解處理器、不用擔(dān)心編譯環(huán)境之間的兼容性、生成的class文件也絕對不會有問題。一個課程設(shè)計項目把精力花在環(huán)境適配和奇怪報錯上太不值得了。如果非要用Lombok至少確認兩件事IDEA里安裝Lombok插件并開啟Enable annotation processing項目的編譯方式用Maven或Gradle管理依賴而不是手動引入jar包。否則在別人電腦上拉下來代碼編譯報錯分分鐘讓人崩潰。還有一個坑和編碼相關(guān)Windows環(huán)境下命令行編譯Java源文件時如果代碼里有中文而源文件是UTF-8編碼直接javac編譯很可能報“未結(jié)束的字符串文字”或亂碼。解決方案是編譯時顯式指定javac -encoding UTF-8 *.java。這個參數(shù)在IDE里通常默認配好了但命令行編譯時非常容易踩到尤其是從Windows的cmd窗口直接操作時。備忘錄系統(tǒng)里到處都是中文字符串編碼問題可以說是必修課。5.3 JVM內(nèi)存不足與數(shù)組越界的高發(fā)場景“java: outofmemoryerror: insufficient memory”這個報錯我在查看JVM類項目時沒少見過。備忘錄系統(tǒng)雖然是輕量級應(yīng)用但如果你一次性把整個文件讀進來處理或者表格里的數(shù)據(jù)不翻頁全量加載數(shù)據(jù)量到達一定級別時同樣能撐爆堆內(nèi)存。我的經(jīng)驗是做桌面工具時養(yǎng)成兩個習(xí)慣一是在主函數(shù)入口給JVM設(shè)合理的初始堆大小java -Xms64m -Xmx256m -jar memo.jar預(yù)留比實際需求稍高的空間但也不要無腦給幾個G二是數(shù)據(jù)加載盡量分批比如查詢列表時默認只加載前500條用戶搜索時再加條件減少單次內(nèi)存壓力。數(shù)組越界異常也高發(fā)在界面層。比如表格選擇行時沒有判斷getSelectedRow()的返回值是不是-1用戶沒選中任何行就直接點刪除代碼就會崩。這個異常在明明“不可能”的情況下就發(fā)生了因為在快速點擊時表格的選中狀態(tài)可能已經(jīng)變化了。我的防御性寫法是所有從界面控件取值的地方都先判空或判斷下標范圍int selectedRow table.getSelectedRow(); if (selectedRow 0) { JOptionPane.showMessageDialog(this, 請先選擇一條記錄, 提示, JOptionPane.WARNING_MESSAGE); return; }這種處理看起來啰嗦但正是這些防御性判斷讓你的程序從“能跑”變成“不會莫名其妙崩”。一個無人維護的課設(shè)項目最怕的就是用戶在沒有任何提示的情況下看到程序異常退出這種體驗非常差。6. 讓系統(tǒng)更“值錢”的三個進階方向6.1 從文件存儲升級到數(shù)據(jù)庫的完整遷移如果第一版用文件存儲升級到SQLite其實非常順滑因為DAO接口的兩個實現(xiàn)對外暴露的方法是一致的。操作步驟無非是新寫一個滿足相同方法簽名的MemoDao實現(xiàn)類把內(nèi)部邏輯換成JDBC再做一個數(shù)據(jù)遷移方法讀取原文件數(shù)據(jù)批量寫入數(shù)據(jù)庫。界面層和業(yè)務(wù)層的代碼完全不用動。這就是分層設(shè)計帶來的核心優(yōu)勢——數(shù)據(jù)源替換對上層透明。遷移時要注意主鍵的連續(xù)性。文件存儲時你可能自己用一個AtomicInteger生成id但SQLite的自增主鍵是由數(shù)據(jù)庫管理的。遷移時要么顯式插入舊id要么重新分配id。如果備忘之間有關(guān)聯(lián)關(guān)系比如提醒記錄引用備忘id就一定要保留舊id否則關(guān)聯(lián)就斷了。備忘錄系統(tǒng)通常沒有這種關(guān)系但培養(yǎng)這種意識很重要。6.2 導(dǎo)出功能與Java反射的初步結(jié)合給系統(tǒng)加一個“導(dǎo)出為CSV”功能是性價比很高的一個加分項。用戶可以把備忘錄列表導(dǎo)出成表格文件用Excel打開。CSV本身就是一個純文本格式每一行是按逗號分隔的字段。生成的要點有兩個字段中含有逗號或換行時要加雙引號包裹文件寫入時用UTF-8 BOM否則Windows下的Excel打開中文會亂碼。代碼非常簡單public void exportCsv(ListMemo memos, Path target) throws IOException { try (BufferedWriter writer Files.newBufferedWriter(target, StandardCharsets.UTF_8)) { writer.write(\uFEFF); // UTF-8 BOM防止Excel中文亂碼 writer.write(ID,標題,分類,優(yōu)先級,狀態(tài),提醒時間\n); for (Memo m : memos) { writer.write(String.join(,, String.valueOf(m.getId()), escapeCsv(m.getTitle()), m.getCategory(), String.valueOf(m.getPriority()), String.valueOf(m.getStatus()), m.getRemindAt() null ? : m.getRemindAt().toString() )); writer.newLine(); } } }escapeCsv這個輔助方法的邏輯是如果字段包含逗號、雙引號或換行就用雙引號包起來并把字段內(nèi)部的雙引號替換成兩個雙引號。這個規(guī)則是CSV格式的標準不處理就會出現(xiàn)導(dǎo)出文件錯位。這個小功能可以引導(dǎo)你了解一種輕量級的數(shù)據(jù)交換格式也為以后對接別的系統(tǒng)打下基礎(chǔ)。6.3 面向?qū)ο笏枷氲倪M階場代碼重構(gòu)備忘錄系統(tǒng)寫完之后一個很好的自我訓(xùn)練方式是“重構(gòu)”。重構(gòu)不是改bug而是不改變外部功能的前提下讓代碼結(jié)構(gòu)更合理。比如你發(fā)現(xiàn)Memo類的方法越寫越多可以按職責(zé)拆出MemoValidator類統(tǒng)一管理字段校驗比如你發(fā)現(xiàn)MemoService里同時處理了業(yè)務(wù)邏輯和提醒時間解析可以把事件解析工具類抽出來。這些重構(gòu)動作本質(zhì)上都是在練習(xí)“識別壞味道、找到更優(yōu)設(shè)計”的肌肉記憶。以后你面對真實的后臺系統(tǒng)時面對一堆屎山代碼才不至于手足無措——因為你在備忘錄這種小項目上已經(jīng)積累過“化繁為簡”的經(jīng)驗了。根據(jù)我個人經(jīng)驗來說完成一個備忘錄系統(tǒng)最好的方式不是悶頭一天寫完而是先花半小時把類和接口的關(guān)系畫在紙上再動手代碼寫完核心流程以后運行一次觀察哪里交互不順手再迭代一版。這個“設(shè)計-實現(xiàn)-反思-重構(gòu)”的循環(huán)才是Java基礎(chǔ)真正長在身上的過程。系統(tǒng)本身做得好不好是其次你在整個過程中建立起來的工程直覺才是未來能帶走的財富。本文還有配套的精品資源點擊獲取