ο髮?shí)驗(yàn)代碼全解析:從類到異常處理)
簡(jiǎn)介這份資源是東北大學(xué)Java面向?qū)ο髮?shí)驗(yàn)課程第1至6次的完整代碼合集面向正在學(xué)習(xí)Java OOP的高校學(xué)生與自學(xué)者幫助解決實(shí)驗(yàn)無從下手、缺少參考實(shí)現(xiàn)的問題。壓縮包共124個(gè)文件約51KB以java源碼與class字節(jié)碼為主輔以html說明頁面、dat數(shù)據(jù)文件、xml配置及少量gif與jar覆蓋類與對(duì)象、封裝、繼承、多態(tài)、構(gòu)造器、接口、異常處理、集合框架、文件流、數(shù)據(jù)結(jié)構(gòu)與設(shè)計(jì)模式等核心知識(shí)點(diǎn)從內(nèi)容預(yù)覽可見還包含GourmetCoffeeGUI等圖形界面與目錄加載相關(guān)類實(shí)驗(yàn)場(chǎng)景較為完整。目前已有2002人學(xué)習(xí)下載說明該套代碼在課程學(xué)習(xí)者中認(rèn)可度較高。讀者可據(jù)此對(duì)照實(shí)驗(yàn)要求梳理類結(jié)構(gòu)與方法調(diào)用關(guān)系理解私有變量與公共接口的封裝思路、子類對(duì)父類的擴(kuò)展方式以及接口實(shí)現(xiàn)與多態(tài)調(diào)用并參考異常捕獲與集合存儲(chǔ)的寫法快速搭建可運(yùn)行工程、排查編譯與運(yùn)行錯(cuò)誤為后續(xù)復(fù)雜Java應(yīng)用開發(fā)打下扎實(shí)基礎(chǔ)。1. 從六份 Java 實(shí)驗(yàn)代碼里我拆出了面向?qū)ο笞钤摼毜膸准聨н^幾輪 Java 實(shí)驗(yàn)的人都有個(gè)共識(shí)面向?qū)ο筮@東西光看語法是學(xué)不會(huì)的必須動(dòng)手把類、繼承、多態(tài)、接口、集合、異常這些點(diǎn)一個(gè)個(gè)敲進(jìn)代碼里才能形成肌肉記憶。這份「Java 面向?qū)ο髮?shí)驗(yàn)全部實(shí)驗(yàn)代碼 1 到 6」就是一套按實(shí)驗(yàn)順序排好的完整代碼包覆蓋了從類與對(duì)象基礎(chǔ)、封裝繼承、多態(tài)與抽象類、接口與內(nèi)部類到集合框架、異常處理與文件 IO 的典型練習(xí)。它解決的不是「教你 Java 語法」的問題而是「給你一套能跑、能改、能對(duì)照的參考實(shí)現(xiàn)」讓你在做實(shí)驗(yàn)卡殼時(shí)有個(gè)能翻的底本。適合正在上 Java 面向?qū)ο笳n、需要交實(shí)驗(yàn)報(bào)告的學(xué)生也適合想系統(tǒng)補(bǔ)一遍 OOP 基本功的轉(zhuǎn)行者。下面我按實(shí)驗(yàn)順序拆開講重點(diǎn)放在每個(gè)實(shí)驗(yàn)的設(shè)計(jì)意圖、代碼結(jié)構(gòu)、參數(shù)怎么調(diào)以及我踩過的那些坑。2. 實(shí)驗(yàn) 1 到 3類、封裝、繼承的代碼骨架怎么搭前三個(gè)實(shí)驗(yàn)是整個(gè)面向?qū)ο蟮牡鼗芏嗳擞X得簡(jiǎn)單就跳過結(jié)果后面寫多態(tài)和接口時(shí)連構(gòu)造方法鏈都理不清。這三個(gè)實(shí)驗(yàn)的代碼量不大但每一行都在訓(xùn)練「把現(xiàn)實(shí)問題抽象成類」的思維。2.1 實(shí)驗(yàn) 1類與對(duì)象的最小閉環(huán)實(shí)驗(yàn) 1 通常要求定義一個(gè)簡(jiǎn)單的類比如學(xué)生類或矩形類包含私有屬性、構(gòu)造方法、getter/setter再在 main 里創(chuàng)建對(duì)象并調(diào)用方法。這份代碼里用的是「學(xué)生成績(jī)管理」的場(chǎng)景核心結(jié)構(gòu)如下// Student.java public class Student { private String name; // 姓名私有屬性 private int score; // 成績(jī)私有屬性 // 無參構(gòu)造 public Student() { this(未知, 0); // 調(diào)用有參構(gòu)造避免重復(fù)賦值 } // 有參構(gòu)造 public Student(String name, int score) { this.name name; this.score score; } public String getName() { return name; } public void setName(String name) { this.name name; } public int getScore() { return score; } // 設(shè)置成績(jī)時(shí)做合法性校驗(yàn) public void setScore(int score) { if (score 0 || score 100) { throw new IllegalArgumentException(成績(jī)必須在 0-100 之間); } this.score score; } Override public String toString() { return Student{name name , score score }; } }這段代碼的關(guān)鍵點(diǎn)有三個(gè)一是this(未知, 0)這種構(gòu)造方法鏈能減少重復(fù)賦值邏輯二是setScore里加了范圍校驗(yàn)這是封裝的意義——不讓外部隨意改壞內(nèi)部狀態(tài)三是重寫了toString()方便打印調(diào)試。參數(shù)方面score用int而不是String是為了后續(xù)排序和比較方便如果你實(shí)驗(yàn)里要求成績(jī)帶小數(shù)改成double即可但要注意浮點(diǎn)數(shù)比較的精度問題。2.2 實(shí)驗(yàn) 2繼承與方法重寫實(shí)驗(yàn) 2 一般會(huì)引入繼承關(guān)系比如讓GraduateStudent繼承Student增加導(dǎo)師、研究方向等字段。這份代碼里的做法是// GraduateStudent.java public class GraduateStudent extends Student { private String supervisor; // 導(dǎo)師 private String researchArea; // 研究方向 public GraduateStudent(String name, int score, String supervisor, String researchArea) { super(name, score); // 必須先調(diào)用父類構(gòu)造 this.supervisor supervisor; this.researchArea researchArea; } Override public String toString() { return super.toString() , supervisor supervisor ; } }這里最容易翻車的地方是super(name, score)的位置——它必須是子類構(gòu)造方法的第一條語句否則編譯不過。另外如果父類沒有無參構(gòu)造子類就必須顯式調(diào)用super(...)這個(gè)規(guī)則在實(shí)驗(yàn) 3 里會(huì)反復(fù)出現(xiàn)。參數(shù)上supervisor和researchArea用String存儲(chǔ)如果實(shí)驗(yàn)要求關(guān)聯(lián)到某個(gè)導(dǎo)師對(duì)象那就得再定義一個(gè)Teacher類把這里改成Teacher supervisor這就是從「字符串建?!沟健笇?duì)象建?!沟纳?jí)。2.3 實(shí)驗(yàn) 3多態(tài)與抽象類的落地實(shí)驗(yàn) 3 通常要求把父類改成抽象類或者定義抽象方法讓不同子類給出不同實(shí)現(xiàn)。這份代碼里把Student改成了抽象類增加了一個(gè)抽象方法getDescription()// Student.java 改為抽象類 public abstract class Student { protected String name; protected int score; public Student(String name, int score) { this.name name; this.score score; } // 抽象方法子類必須實(shí)現(xiàn) public abstract String getDescription(); public int getScore() { return score; } }然后在GraduateStudent和UndergraduateStudent里分別實(shí)現(xiàn)// UndergraduateStudent.java public class UndergraduateStudent extends Student { private String major; public UndergraduateStudent(String name, int score, String major) { super(name, score); this.major major; } Override public String getDescription() { return 本科生 name 專業(yè) major; } }多態(tài)的價(jià)值在這里體現(xiàn)得很直接你可以寫一個(gè)ListStudent往里放本科生和研究生遍歷時(shí)調(diào)用getDescription()運(yùn)行時(shí)會(huì)自動(dòng)找到對(duì)應(yīng)子類的實(shí)現(xiàn)。參數(shù)上要注意抽象類不能直接new必須通過子類實(shí)例化如果某個(gè)子類沒實(shí)現(xiàn)抽象方法編譯器會(huì)直接報(bào)錯(cuò)這是好事能提前暴露遺漏。提示實(shí)驗(yàn) 1 到 3 的代碼建議按順序跑一遍每跑完一個(gè)實(shí)驗(yàn)就手動(dòng)改幾個(gè)參數(shù)比如把成績(jī)改成負(fù)數(shù)看校驗(yàn)是否生效把super調(diào)用刪掉看編譯報(bào)錯(cuò)這種「故意改壞」的練習(xí)比只看代碼有效得多。3. 實(shí)驗(yàn) 4 到 5接口、內(nèi)部類與集合框架的配合實(shí)驗(yàn) 4 和 5 開始進(jìn)入 Java 面向?qū)ο罄锔腹こ袒沟牟糠帧涌诙x契約、內(nèi)部類簡(jiǎn)化回調(diào)、集合管理批量數(shù)據(jù)。這三個(gè)東西單獨(dú)學(xué)都不難但放在一起用的時(shí)候初學(xué)者很容易寫出「接口當(dāng)類用、內(nèi)部類亂嵌套、集合遍歷時(shí)刪元素」這類問題。3.1 實(shí)驗(yàn) 4接口與內(nèi)部類的典型寫法實(shí)驗(yàn) 4 一般要求定義一個(gè)接口比如Sortable或Comparable的自定義版本然后讓某個(gè)類實(shí)現(xiàn)它。這份代碼里定義了一個(gè)Scorable接口// Scorable.java public interface Scorable { int getScore(); default boolean isPass() { return getScore() 60; // 默認(rèn)方法子類可覆蓋 } }然后Student實(shí)現(xiàn)這個(gè)接口并在內(nèi)部用一個(gè)靜態(tài)內(nèi)部類做比較器// Student.java 片段 public class Student implements Scorable, ComparableStudent { private String name; private int score; // ... 構(gòu)造和 getter 省略 Override public int getScore() { return score; } Override public int compareTo(Student other) { return Integer.compare(this.score, other.score); } // 靜態(tài)內(nèi)部類按姓名排序的比較器 public static class NameComparator implements ComparatorStudent { Override public int compare(Student s1, Student s2) { return s1.name.compareTo(s2.name); } } }這里有兩個(gè)關(guān)鍵設(shè)計(jì)一是default方法讓接口有了默認(rèn)實(shí)現(xiàn)子類不必強(qiáng)制重寫二是靜態(tài)內(nèi)部類NameComparator不持有外部類實(shí)例適合做工具型比較器。參數(shù)上Integer.compare比手寫this.score - other.score更安全因?yàn)楹笳咴跇O端值下可能溢出。如果你實(shí)驗(yàn)里要求匿名內(nèi)部類可以把NameComparator改成new ComparatorStudent() { ... }的形式效果一樣只是不能復(fù)用。3.2 實(shí)驗(yàn) 5集合框架的增刪改查與遍歷實(shí)驗(yàn) 5 通常要求用ArrayList、HashMap或HashSet管理一批學(xué)生數(shù)據(jù)并實(shí)現(xiàn)按條件篩選、排序、統(tǒng)計(jì)。這份代碼里用的是ArrayList加HashMap的組合import java.util.*; public class StudentManager { private ListStudent students new ArrayList(); private MapString, Student indexByName new HashMap(); public void addStudent(Student s) { students.add(s); indexByName.put(s.getName(), s); // 建立姓名索引 } // 按成績(jī)篩選 public ListStudent filterByScore(int minScore) { ListStudent result new ArrayList(); for (Student s : students) { if (s.getScore() minScore) { result.add(s); } } return result; } // 按成績(jī)排序 public void sortByScore() { Collections.sort(students); // 依賴 Comparable 實(shí)現(xiàn) } // 按姓名排序 public void sortByName() { students.sort(new Student.NameComparator()); } }這段代碼里最值得說的是indexByName這個(gè)HashMap——它讓按姓名查找從 O(n) 降到 O(1)這是集合選型的實(shí)際意義。參數(shù)上filterByScore的minScore是包含邊界的如果你要改成不包含把改成即可。另外Collections.sort和List.sort的區(qū)別在于前者是靜態(tài)工具方法后者是 Java 8 引入的默認(rèn)方法兩者底層都是歸并排序穩(wěn)定性有保證。注意遍歷ArrayList時(shí)如果同時(shí)刪除元素必須用Iterator.remove()或removeIf()直接調(diào)list.remove()會(huì)拋ConcurrentModificationException這是實(shí)驗(yàn) 5 里最常見的翻車點(diǎn)。4. 實(shí)驗(yàn) 6異常處理與文件 IO 的完整鏈路實(shí)驗(yàn) 6 通常是綜合練習(xí)要求把前面定義的學(xué)生對(duì)象寫入文件再從文件讀回來同時(shí)處理各種異常。這份代碼里用的是「文本文件 緩沖流」的方案沒有上序列化因?yàn)閷?shí)驗(yàn)環(huán)境里更看重對(duì)try-catch-finally和try-with-resources的掌握。4.1 自定義異常與校驗(yàn)邏輯先定義一個(gè)業(yè)務(wù)異常用來區(qū)分「文件格式錯(cuò)誤」和「IO 錯(cuò)誤」// InvalidStudentFormatException.java public class InvalidStudentFormatException extends Exception { public InvalidStudentFormatException(String message) { super(message); } }然后在解析每一行時(shí)拋出private Student parseLine(String line) throws InvalidStudentFormatException { String[] parts line.split(,); if (parts.length ! 2) { throw new InvalidStudentFormatException(字段數(shù)量不對(duì) line); } try { String name parts[0].trim(); int score Integer.parseInt(parts[1].trim()); return new Student(name, score); } catch (NumberFormatException e) { throw new InvalidStudentFormatException(成績(jī)不是數(shù)字 line); } }這里的設(shè)計(jì)意圖是NumberFormatException是運(yùn)行時(shí)異常直接往外拋會(huì)讓調(diào)用方難以區(qū)分「格式問題」和「文件不存在」包一層自定義異常后上層就能用catch (InvalidStudentFormatException e)單獨(dú)處理。4.2 文件讀寫與資源關(guān)閉寫文件和讀文件都用try-with-resources避免忘記關(guān)流// 寫入 public void saveToFile(String path) throws IOException { try (BufferedWriter writer new BufferedWriter(new FileWriter(path))) { for (Student s : students) { writer.write(s.getName() , s.getScore()); writer.newLine(); } } } // 讀取 public void loadFromFile(String path) throws IOException, InvalidStudentFormatException { try (BufferedReader reader new BufferedReader(new FileReader(path))) { String line; while ((line reader.readLine()) ! null) { if (line.trim().isEmpty()) continue; // 跳過空行 students.add(parseLine(line)); } } }參數(shù)上FileWriter默認(rèn)是覆蓋模式如果要追加需要傳第二個(gè)參數(shù)true。BufferedReader的readLine()返回null表示文件結(jié)束這個(gè)判斷不能少。另外文件路徑建議用Paths.get(data, students.txt)這種跨平臺(tái)寫法直接拼/在 Windows 上雖然也能跑但不夠規(guī)范。提示實(shí)驗(yàn) 6 跑完后手動(dòng)把數(shù)據(jù)文件里的某一行改成「張三,abc」再運(yùn)行讀取程序看自定義異常是否被正確捕獲并打印出可讀信息。這個(gè)驗(yàn)證步驟能幫你確認(rèn)異常鏈路是通的。5. 六個(gè)實(shí)驗(yàn)跑下來這幾個(gè)坑我替你踩過了5.1 構(gòu)造方法鏈斷裂導(dǎo)致編譯失敗現(xiàn)象子類構(gòu)造方法里寫了super(name, score)但父類只有無參構(gòu)造編譯器報(bào)「無法將類中的構(gòu)造器應(yīng)用到給定類型」。原因Java 規(guī)定子類構(gòu)造必須調(diào)用父類某個(gè)構(gòu)造如果父類沒有無參構(gòu)造子類必須顯式super(...)且必須是第一句。解決要么給父類補(bǔ)一個(gè)無參構(gòu)造要么在子類第一行寫super(...)。我一般建議父類保留無參構(gòu)造減少子類負(fù)擔(dān)。5.2 重寫 equals 沒重寫 hashCode現(xiàn)象把Student對(duì)象放進(jìn)HashSet后明明兩個(gè)對(duì)象屬性一樣卻都被保留沒去重。原因HashSet先用hashCode定位桶再用equals比較。只重寫equals不重寫hashCode兩個(gè)相等對(duì)象的哈希值不同會(huì)被分到不同桶。解決用 IDE 自動(dòng)生成或手寫hashCode確保equals為 true 的兩個(gè)對(duì)象hashCode也相等。實(shí)驗(yàn) 5 里如果用到HashSet這個(gè)坑必踩。5.3 遍歷集合時(shí)刪除元素拋異?,F(xiàn)象for (Student s : students) { if (s.getScore() 60) students.remove(s); }運(yùn)行時(shí)報(bào)ConcurrentModificationException。原因增強(qiáng) for 循環(huán)底層用Iterator但remove調(diào)的是集合自己的方法導(dǎo)致迭代器狀態(tài)和集合狀態(tài)不一致。解決改用Iterator.remove()或者直接用students.removeIf(s - s.getScore() 60)后者更簡(jiǎn)潔。5.4 文件路徑寫死導(dǎo)致?lián)Q環(huán)境就找不到文件現(xiàn)象在本機(jī)跑得好好的換臺(tái)機(jī)器或換個(gè)目錄就報(bào)FileNotFoundException。原因用了絕對(duì)路徑如D:/code/data.txt或者用了相對(duì)路徑但工作目錄變了。解決用Paths.get(System.getProperty(user.dir), data, students.txt)獲取當(dāng)前工作目錄下的路徑或者把文件放在src同級(jí)目錄并用相對(duì)路徑。實(shí)驗(yàn) 6 里建議把數(shù)據(jù)文件放在項(xiàng)目根目錄的data文件夾下。5.5 抽象類里寫了構(gòu)造方法卻以為不能有現(xiàn)象看到抽象類里有構(gòu)造方法以為寫錯(cuò)了刪掉后子類編譯報(bào)錯(cuò)。原因抽象類可以有構(gòu)造方法只是不能直接new它的構(gòu)造方法是給子類super()調(diào)用的。解決保留抽象類的構(gòu)造方法用來初始化父類字段。實(shí)驗(yàn) 3 里如果父類有name和score就應(yīng)該在抽象類構(gòu)造里賦值子類通過super(name, score)傳遞。6. 把實(shí)驗(yàn)代碼改成自己的項(xiàng)目三個(gè)進(jìn)階改法這套代碼如果只是跑一遍交作業(yè)有點(diǎn)浪費(fèi)。我一般會(huì)做三件事把它變成能寫進(jìn)簡(jiǎn)歷的小項(xiàng)目。第一把Student換成你熟悉的業(yè)務(wù)對(duì)象比如「訂單」「商品」「?jìng)鞲衅髯x數(shù)」然后重新設(shè)計(jì)字段和校驗(yàn)規(guī)則。這一步練的是抽象能力——同樣的繼承、接口、集合結(jié)構(gòu)換一個(gè)領(lǐng)域能不能快速套上去。第二給StudentManager加一個(gè)命令行菜單用Scanner讀取用戶輸入實(shí)現(xiàn)增刪改查的交互。這個(gè)改動(dòng)會(huì)逼你處理輸入校驗(yàn)、循環(huán)控制、異常捕獲比單純寫類更有工程感。代碼結(jié)構(gòu)大概是這樣public static void main(String[] args) { StudentManager manager new StudentManager(); Scanner scanner new Scanner(System.in); while (true) { System.out.println(1.添加 2.按成績(jī)篩選 3.保存 4.讀取 0.退出); int choice scanner.nextInt(); scanner.nextLine(); // 吃掉換行符 switch (choice) { case 1: System.out.print(姓名); String name scanner.nextLine(); System.out.print(成績(jī)); int score scanner.nextInt(); manager.addStudent(new Student(name, score)); break; case 2: System.out.print(最低分); int min scanner.nextInt(); manager.filterByScore(min).forEach(System.out::println); break; case 3: try { manager.saveToFile(data/students.txt); } catch (IOException e) { System.out.println(保存失敗 e.getMessage()); } break; case 0: return; } } }第三把文件存儲(chǔ)從文本改成 JSON 或 CSV用第三方庫如 Jackson 或 OpenCSV 做序列化。這一步會(huì)接觸到依賴管理Maven/Gradle和庫的 API 設(shè)計(jì)是從「實(shí)驗(yàn)代碼」到「真實(shí)項(xiàng)目」的關(guān)鍵一跳。參數(shù)上Jackson 的ObjectMapper默認(rèn)要求類有無參構(gòu)造和 getter/setter如果字段是private且沒有 getter需要加JsonProperty注解。從那以后我每次拿到一套實(shí)驗(yàn)代碼都會(huì)先跑通、再改壞、最后換場(chǎng)景重寫一遍這三步走完代碼才真正變成自己的。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取