尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Java面向对象编程核心:从类与对象到集合框架的实践指南

Java面向对象编程核心:从类与对象到集合框架的实践指南 1. 项目缘起为什么“面向对象”是Java的必修课如果你刚开始接触Java或者正在学校里上Java程序设计课大概率会听到“面向对象”这个词。老师会反复强调教材会花大篇幅讲解实验课更是绕不开它。但很多同学直到课程结束可能也只是记住了“封装、继承、多态”这三个词写出来的代码依然是面向过程的思维类与类之间的关系混乱一个main方法里塞了几百行代码。这其实非常可惜因为Java这门语言的灵魂或者说它区别于C语言这类过程式语言的核心就在于其纯粹的面向对象特性。我当年学Java时也经历过这个阶段总觉得“对象”这个概念很虚不如写几个函数调用来得直接。直到后来参与实际项目维护一个几千个类的系统时才真正体会到面向对象设计带来的好处清晰的职责划分、易于扩展的代码结构、以及团队协作时极高的可读性。这个“头歌面向对象程序设计Java实验”系列就是基于我多年的教学和开发经验为你系统梳理Java面向对象的核心知识点并通过一系列精心设计的实验带你从“知道”到“会用”最终到“理解其设计精髓”。本系列实验不追求覆盖Java所有语法而是聚焦于“面向对象”这个核心思想。我们会从最基础的类和对象开始一步步深入到继承与多态、抽象类与接口、异常处理、集合框架等核心主题。每个实验都配有明确的场景、可运行的代码示例以及我踩过的坑和总结的经验技巧。无论你是正在完成课程实验的学生还是希望夯实Java基础的自学者这个系列都能为你提供一条清晰、可实践的路径。2. 实验一从“物”到“类”——理解对象与类的本质很多教程一上来就教你写class Student { ... }但很少解释为什么要把学生定义成一个“类”。我们换个角度想在你写程序之前脑子里是不是已经有一个“学生”的概念了这个学生有名字、学号、年龄能进行选课、考试等行为。面向对象编程就是把我们脑中这个抽象的“概念”和具体的“个体”用代码的形式描述出来。2.1 类的定义蓝图是如何绘制的类Class就是对象的蓝图或模板。它定义了一类对象共有的属性成员变量和行为方法。定义类的过程就是进行抽象的过程。/** * 学生类 * 这是一个简单的类定义示例展示了成员变量和方法。 */ public class Student { // 成员变量属性描述对象的状态 private String name; // 姓名 private String studentId; // 学号 private int age; // 年龄 // 构造方法用于创建对象时初始化其状态 public Student(String name, String studentId, int age) { this.name name; this.studentId studentId; this.age age; } // 成员方法行为描述对象能做什么 public void selectCourse(String courseName) { System.out.println(name 选择了课程 courseName); } public void takeExam(String courseName) { System.out.println(name 正在参加 courseName 考试。); } // Getter和Setter方法提供对私有属性的安全访问 public String getName() { return name; } public void setName(String name) { this.name name; } // ... 其他Getter/Setter省略 }关键点解析与避坑指南private修饰符这是封装Encapsulation思想的体现。将成员变量声明为private意味着它们只能在类内部被直接访问。这保护了对象的数据防止外部代码随意修改保证了对象状态的完整性和安全性。初学时容易图省事把变量都写成public这在小型程序中可能没问题但一旦项目复杂数据被意外修改的bug会非常难查。this关键字在构造方法和setName中我们使用了this.name。this指向当前对象实例。当局部变量如构造方法的参数name与成员变量同名时this用于区分二者。这是一个必须养成的好习惯能极大提高代码清晰度。构造方法Constructor它的名字必须与类名完全相同没有返回类型连void都没有。它的核心作用就是在new关键字创建对象时为对象分配内存并初始化其成员变量。如果你不写任何构造方法Java会提供一个默认的无参构造方法但一旦你写了一个带参数的构造方法默认的无参构造方法就不再自动提供这点要特别注意。2.2 对象的创建与使用让蓝图变成大楼定义了Student类就像有了建筑设计图。现在我们要根据这张图来盖房子创建对象。public class SchoolManagement { public static void main(String[] args) { // 使用 new 关键字和构造方法创建对象实例化 Student student1 new Student(张三, 2023001, 20); Student student2 new Student(李四, 2023002, 19); // 调用对象的方法 student1.selectCourse(Java程序设计); student2.takeExam(数据结构); // 通过Setter修改对象状态通过Getter获取状态 student1.setAge(21); System.out.println(student1.getName() 的年龄是 student1.getAge()); } }内存模型浅析理解为什么这么写当你执行new Student(...)时Java虚拟机JVM会在堆内存中开辟一块空间用于存储这个Student对象的所有数据name,studentId,age。student1这个变量本身是一个引用可以理解为遥控器或地址标签它存储在栈内存中其值就是刚才在堆里开辟的那块内存的地址。通过student1.xxx()来调用方法就是根据这个地址找到堆里的对象然后执行它的方法。常见误区Student student1;这行代码只是声明了一个引用变量它还没有指向任何对象此时它的值是null。如果直接调用student1.selectCourse(...)就会抛出NullPointerException空指针异常。必须先new再使用。3. 实验二构建关系——深入继承与多态面向对象不仅关乎单个对象更关乎对象之间的关系。继承Inheritance和多态Polymorphism是构建这种关系的两大支柱它们让代码的复用性和扩展性得到质的提升。3.1 继承代码复用的艺术假设我们现在要处理学校里的教职工除了学生还有老师。老师和学生都是“人”都有姓名、年龄等属性。如果分别写Teacher和Student类会有大量重复代码。这时继承就派上用场了。// 父类基类、超类定义共通的属性和行为 public class Person { private String name; private int age; public Person(String name, int age) { this.name name; this.age age; } public void introduce() { System.out.println(大家好我是 name 今年 age 岁。); } // Getter/Setter 省略... } // 子类派生类继承父类并添加自己特有的属性和行为 public class Student extends Person { private String studentId; public Student(String name, int age, String studentId) { super(name, age); // 调用父类构造方法初始化继承来的属性 this.studentId studentId; } public void study() { System.out.println(getName() 正在努力学习。); } } public class Teacher extends Person { private String teacherId; private String course; public Teacher(String name, int age, String teacherId, String course) { super(name, age); this.teacherId teacherId; this.course course; } public void teach() { System.out.println(getName() 老师正在讲授《 course 》。); } }super关键字详解在子类构造方法中super(name, age)必须放在第一行如果显式调用的话。它的作用是调用父类的构造方法确保父类部分被正确初始化。子类对象在内存中包含了父类定义的所有字段super就是用来初始化这部分“父类区域”的。继承的“是”关系Student extends Person意味着“学生是一个人”。这是一种“is-a”关系。在设计时要谨慎使用继承只有确实存在这种一般与特殊的关系时才用。不要为了复用代码而滥用继承。3.2 多态同一接口不同实现多态是继承的延伸它允许父类的引用指向子类的对象并且在运行时能够自动调用子类重写的方法。这是面向对象最精妙、最强大的特性之一。public class PolymorphismDemo { public static void main(String[] args) { // 多态的典型体现父类引用指向子类对象 Person p1 new Student(王五, 21, 2023003); Person p2 new Teacher(赵老师, 35, T001, Java); // 调用的是父类Person中定义的方法 p1.introduce(); // 输出大家好我是王五今年21岁。 p2.introduce(); // 输出大家好我是赵老师今年35岁。 // 编译看左边运行看右边 // p1.study(); // 这行编译报错因为编译器认为p1是Person类型而Person没有study方法。 // 但如果我们在Person中定义一个抽象方法work并在子类中重写... } }要让多态发挥威力通常需要方法重写Override的配合。我们改造一下Person类public class Person { // ... 属性构造方法同上 public void work() { System.out.println(name 正在工作。); } } public class Student extends Person { // ... 其他同上 Override // 注解表明这是重写父类方法。建议加上让编译器帮助检查 public void work() { System.out.println(getName() 的工作是学习。); } } public class Teacher extends Person { // ... 其他同上 Override public void work() { System.out.println(getName() 的工作是教学。); } }再次运行测试public class Test { public static void main(String[] args) { Person[] people new Person[2]; people[0] new Student(王五, 21, 2023003); people[1] new Teacher(赵老师, 35, T001, Java); for (Person p : people) { p.work(); // 神奇的事情发生了 } } } // 输出 // 王五的工作是学习。 // 赵老师的工作是教学。多态的运行机制在编译时编译器只知道p是Person类型所以它检查Person类是否有work方法。在运行时JVM会查找p实际指向的对象可能是Student或Teacher的work方法并调用。这就是“编译看左边运行看右边”。多态的巨大优势提高代码可扩展性如果未来要新增一个Staff职员类也继承Person并重写work方法那么上面的for循环代码完全不需要修改就能处理新类型的对象。接口统一对于调用者main方法来说它不需要关心具体是学生还是老师它只需要知道它是一个“人”并且能“工作”就行了。这极大地降低了模块间的耦合度。4. 实验三抽象与契约——抽象类与接口的高阶应用继承和多态很好但有时父类太具体反而不合适。比如“图形”这个类它可以有计算面积的方法但“图形”本身无法计算一个具体的面积只有它的子类“圆形”、“矩形”才知道如何计算。这时就需要抽象类Abstract Class。而接口Interface则定义了一组纯粹的契约不关心你是谁只关心你能做什么。4.1 抽象类不完整的蓝图抽象类用于描述一种“概念”或“类别”它可能包含一些实现了的方法但至少包含一个抽象方法没有方法体只有声明。抽象类不能实例化。// 抽象类图形 public abstract class Shape { // 抽象方法计算面积。只有声明没有实现体。 public abstract double calculateArea(); // 具体方法非抽象方法可以有实现 public void display() { System.out.println(这是一个图形面积是 calculateArea()); } // 也可以有构造方法、成员变量等 private String color; public Shape(String color) { this.color color; } public String getColor() { return color; } } // 具体子类必须实现所有抽象方法 public class Circle extends Shape { private double radius; public Circle(String color, double radius) { super(color); // 调用父类构造方法 this.radius radius; } Override public double calculateArea() { return Math.PI * radius * radius; } } public class Rectangle extends Shape { private double width; private double height; public Rectangle(String color, double width, double height) { super(color); this.width width; this.height height; } Override public double calculateArea() { return width * height; } }使用场景当你发现几个类有共同的属性和行为但其中一些行为在父类中无法具体实现时就适合用抽象类。它强制子类去实现这些关键行为保证了设计的一致性。4.2 接口纯粹的能力契约接口比抽象类更“抽象”。在Java 8之前接口里只能有抽象方法和常量。Java 8之后允许有default方法和static方法。接口定义了一组能力或角色一个类可以实现多个接口。// 接口定义“可飞行”的能力 public interface Flyable { // 常量 (默认是 public static final) int MAX_SPEED 1000; // 抽象方法 (默认是 public abstract) void fly(); // Java 8 引入的默认方法有实现体 default void takeOff() { System.out.println(准备起飞...); } // Java 8 引入的静态方法 static void showMaxSpeed() { System.out.println(最大飞行速度 MAX_SPEED); } } // 接口定义“可鸣叫”的能力 public interface Honkable { void honk(); } // 一个类可以实现多个接口 public class Airplane implements Flyable, Honkable { private String model; public Airplane(String model) { this.model model; } Override public void fly() { System.out.println(model 飞机正在高空飞行。); } Override public void honk() { System.out.println(model 飞机嘀嘀); } } // 另一个类也可以实现Flyable public class Bird implements Flyable { Override public void fly() { System.out.println(小鸟扇动翅膀飞翔。); } // takeOff方法使用接口的默认实现可以不重写 }接口 vs 抽象类如何选择这是一个经典面试题也是设计时的关键决策。特性抽象类 (Abstract Class)接口 (Interface)定义对一类事物的抽象是“是什么”is-a对一组能力的抽象是“能做什么”has-a/can-do方法可以有抽象方法、具体方法、构造方法Java 8前只有抽象方法之后可有默认方法、静态方法、私有方法变量可以有各种成员变量只能是public static final常量继承单继承一个类只能有一个父类多实现一个类可实现多个接口设计理念代码复用提供模板强调所属关系定义契约解耦强调能力实现使用场景多个类有高度重合的代码和逻辑关系定义跨不同继承体系的对象共有的行为经验之谈在最初设计时优先考虑使用接口。因为接口更灵活耦合度更低。当你发现多个实现类有大量重复代码时再考虑将这些重复代码提取到一个抽象类中然后让这些类继承该抽象类并实现各自的接口。这种“接口定义行为抽象类提供部分实现”的模式非常常见。5. 实验四异常处理——让程序从容面对“意外”程序世界并非一帆风顺。用户可能输入错误格式的数据要读取的文件可能不存在网络连接可能会突然中断。这些运行时发生的问题在Java中被称为异常Exception。健壮的程序必须能够妥善处理异常。5.1 Java异常体系一张清晰的家族图谱Java中所有异常和错误的超类是Throwable。它有两个主要子类Error系统级错误如OutOfMemoryError内存溢出、StackOverflowError栈溢出。这类错误通常很严重程序一般无法处理只能尽量预防。Exception程序运行时异常是我们主要关注和处理的。它又分为两大类受检异常Checked Exception继承自Exception但不继承RuntimeException。如IOException、SQLException。编译器强制要求你必须处理try-catch或throws否则编译不通过。这体现了Java的“安全”设计哲学。非受检异常RuntimeException继承自RuntimeException。如NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException除零错误。编译器不强制要求处理通常由编程逻辑错误导致。5.2 异常处理机制try-catch-finally这是处理异常的核心语法块。import java.io.File; import java.io.FileNotFoundException; import java.util.Scanner; public class ExceptionHandlingDemo { public static void main(String[] args) { File file new File(nonexistent.txt); Scanner scanner null; try { // 尝试执行可能抛出异常的代码 scanner new Scanner(file); // 可能抛出 FileNotFoundException while (scanner.hasNextLine()) { System.out.println(scanner.nextLine()); } } catch (FileNotFoundException e) { // 捕获并处理特定的异常 System.err.println(错误文件未找到。请检查文件路径。); // 打印异常堆栈跟踪有助于调试生产环境通常记录到日志 // e.printStackTrace(); } catch (SecurityException e) { // 可以捕获多个不同类型的异常 System.err.println(错误没有读取该文件的权限。); } catch (Exception e) { // 兜底的异常捕获捕获所有Exception及其子类 System.err.println(发生了未知错误 e.getMessage()); } finally { // 无论是否发生异常finally块中的代码都会执行 // 常用于释放资源如关闭文件、网络连接、数据库连接等 if (scanner ! null) { scanner.close(); System.out.println(Scanner已关闭。); } } System.out.println(程序继续正常执行...); } }关键细节与最佳实践catch块的顺序必须从具体到一般。如果把catch (Exception e)放在第一个那么后面的catch (FileNotFoundException e)将永远捕获不到异常因为所有异常都是Exception的子类编译会报错。finally的作用它是释放资源的黄金位置。即使在try或catch块中遇到了return语句finally块也会在方法返回前执行。但如果在finally块中也有return它会覆盖try或catch中的return值这容易引发混淆应避免。不要“吞掉”异常最糟糕的做法是catch (Exception e) { }即空catch块。这会让错误悄无声息地消失给调试带来噩梦。至少应该打印日志或错误信息。使用try-with-resourcesJava 7对于实现了AutoCloseable接口的资源如Scanner,FileInputStream等强烈推荐使用此语法它能自动关闭资源代码更简洁安全。// 使用 try-with-resources 自动关闭资源 try (Scanner scanner new Scanner(new File(test.txt))) { while (scanner.hasNextLine()) { System.out.println(scanner.nextLine()); } } catch (FileNotFoundException e) { System.err.println(文件未找到。); } // 无需显式调用 scanner.close()离开try块后会自动调用5.3 抛出异常责任的传递除了捕获异常我们还可以抛出throw异常。当方法内部遇到无法处理或不应该由它处理的错误时可以选择将异常抛给上层调用者。public class BankAccount { private double balance; public void withdraw(double amount) throws InsufficientFundsException { // 检查业务逻辑 if (amount balance) { // 创建并抛出一个自定义的受检异常 throw new InsufficientFundsException(余额不足。当前余额 balance); } balance - amount; } } // 自定义一个受检异常 public class InsufficientFundsException extends Exception { public InsufficientFundsException(String message) { super(message); } }throwsvsthrowthrows用在方法声明上表示该方法可能抛出某种类型的异常提醒调用者需要处理。throw用在方法体内用于主动抛出一个异常对象。何时抛出异常一个简单的原则在当前方法所在的抽象层次上如果无法给出有意义的处理就应该抛出异常。例如在BankAccount.withdraw方法中遇到余额不足这个方法本身无法决定是提示用户、记录日志还是终止交易它只负责执行“取款”这个原子操作。因此将异常抛出由调用它的UI层或服务层去决定如何向用户反馈是更合理的职责划分。6. 实验五容器与泛型——用集合框架管理对象程序中的对象往往不是孤立的我们需要把它们组织起来。Java集合框架Java Collections Framework, JCF提供了一套性能优异、使用方便的容器类用于存储和操作对象组。而泛型Generics则像给这些容器贴上了类型标签保证了类型安全。6.1 核心集合接口与常用实现集合框架的根接口是Collection和Map。Collection接口代表一组对象。主要子接口有List列表有序、可重复。像排队每个人有固定位置索引。ArrayList基于动态数组随机访问快get(i)但在中间插入/删除慢。LinkedList基于双向链表在头部/尾部插入/删除快但随机访问慢。Set集无序、不可重复。像数学里的集合或者一个没有重复元素的袋子。HashSet基于哈希表存取最快但元素顺序不确定。TreeSet基于红黑树元素会自动排序需要元素实现Comparable接口或提供Comparator。Queue队列先进先出FIFO的队列。Map接口代表一组键值对Key-Value。像字典通过键Key来查找值Value。键不可重复。HashMap基于哈希表最常用的Map存取速度快。TreeMap基于红黑树键会自动排序。6.2 泛型让集合更安全在Java 5引入泛型之前集合里可以放任何类型的对象取出来时需要强制类型转换容易引发ClassCastException。// 没有泛型的时代已过时仅作对比 List list new ArrayList(); list.add(Hello); list.add(123); // 可以放整数类型混乱 String str (String) list.get(0); // 必须强制转换 // Integer num (Integer) list.get(0); // 运行时错误ClassCastException // 使用泛型 ListString stringList new ArrayList(); // 指定这个List只能放String stringList.add(Hello); // stringList.add(123); // 编译错误立刻发现类型问题 String str stringList.get(0); // 无需强制转换自动就是String类型菱形语法new ArrayList()中的在Java 7后可以省略类型编译器会根据前面ListString的声明进行推断这被称为“菱形语法”让代码更简洁。6.3 遍历集合迭代器与增强for循环遍历集合是常见操作有几种方式ListString fruits new ArrayList(); fruits.add(Apple); fruits.add(Banana); fruits.add(Orange); // 方法1传统for循环适用于List因为有索引 for (int i 0; i fruits.size(); i) { System.out.println(fruits.get(i)); } // 方法2迭代器 (Iterator) - 更通用的方式 IteratorString iterator fruits.iterator(); while (iterator.hasNext()) { String fruit iterator.next(); System.out.println(fruit); // 可以在遍历中使用iterator.remove()安全删除元素 } // 方法3增强for循环 (for-each) - 语法糖最简洁 // 底层实际上就是使用了迭代器 for (String fruit : fruits) { System.out.println(fruit); } // 方法4Java 8 的 Lambda 表达式和 Stream API (函数式风格) fruits.forEach(fruit - System.out.println(fruit)); // 或者使用方法引用 fruits.forEach(System.out::println);遍历时修改集合的坑绝对不要在增强for循环或迭代器遍历过程中直接调用集合自身的add或remove方法iterator.remove()除外。这会导致ConcurrentModificationException并发修改异常。如果需要遍历时删除正确做法是使用Iterator的remove方法或者使用Java 8的removeIf方法。// 错误示例 for (String fruit : fruits) { if (fruit.equals(Banana)) { fruits.remove(fruit); // 抛出 ConcurrentModificationException! } } // 正确做法1使用Iterator IteratorString it fruits.iterator(); while (it.hasNext()) { if (it.next().equals(Banana)) { it.remove(); // 安全删除 } } // 正确做法2使用Java 8的removeIf fruits.removeIf(fruit - fruit.equals(Banana));6.4 集合选型与性能考量选择哪种集合取决于你的具体需求需求推荐实现理由需要频繁按索引访问元素ArrayList基于数组get(int index)是O(1)时间复杂度。需要频繁在头部或中间插入/删除LinkedList基于链表插入/删除只需改变节点引用是O(1)如果已知位置。需要元素不重复不关心顺序HashSet基于哈希表add,contains操作平均是O(1)。需要元素不重复且自动排序TreeSet基于红黑树add,contains是O(log n)元素有序。需要键值对映射不关心顺序HashMap基于哈希表存取最快。需要键值对映射且键有序TreeMap基于红黑树键有序。需要线程安全的集合ConcurrentHashMap,CopyOnWriteArrayList专门的并发集合类性能优于古老的Vector和Hashtable。初始化容量对于ArrayList、HashMap等如果你能预估元素的大致数量最好在构造时指定初始容量如new ArrayList(1000)这可以减少内部数组扩容的次数提升性能。面向对象程序设计远不止于此还有内部类、枚举、注解、反射等高级主题以及基于面向对象思想的设计模式。但这个实验系列为你搭建了一个坚实而清晰的核心框架。理解类和对象是基石掌握继承与多态是构建复杂系统的钥匙善用抽象与接口能让设计更灵活稳健的异常处理是程序可靠的保障而熟练运用集合与泛型则是日常开发的效率之源。记住面向对象不仅仅是一种语法更是一种思维方式。多思考“这个对象应该有什么属性能做什么事它和别的对象是什么关系”并在编码中反复实践你才能真正驾驭Java这门语言写出优雅、健壮、易维护的代码。
返回列表