Java作业-面向对象设计原则——单一职责原则与开闭原则

第7章 面向对象设计原则——单一职责原则与开闭原则

耿祥义、张跃平《Java面向对象程序设计(第4版)》第7章

一、什么是面向对象设计原则?

在面向对象程序设计中,除了掌握继承、多态、接口等语法特性外,更重要的是理解面向对象设计的原则。这些原则是前人总结出的经验法则,指导我们如何设计出可维护、可扩展、可复用的软件系统。

本章介绍两个最基本、最重要的原则:单一职责原则开闭原则

二、单一职责原则

2.1 定义

单一职责原则(Single Responsibility Principle,SRP) 是最简单的面向对象设计原则,也是最难正确运用的原则之一。

它的核心思想是:一个类只负责一个功能领域中的相应职责

通俗地说:一个类,只有一个引起它变化的原因。

2.2 如何理解”职责”?

一个类的”职责”可以理解为”它为什么存在”或”它做什么事情”。

  • 如果一个类有多个引起它变化的原因,就说明它承担了多个职责
  • 职责过多,就会导致类变得臃肿、复杂、难以维护

2.3 违反单一职责原则的后果

当一个类承担了多项职责时,会出现以下问题:

  • 可维护性差:修改某一职责的代码可能会影响其他职责的正常功能
  • 可复用性差:无法单独使用某个职责的功能,因为所有功能都耦合在一个类中
  • 可测试性差:测试一个职责时,必须依赖其他职责的代码
  • 代码臃肿:一个类包含了大量无关的方法,代码量过大

2.4 如何识别违反单一职责原则的类?

可以通过以下几个问题来判断:

  1. 这个类的方法是否涉及多个不同的功能领域?
  2. 修改这个类的某个方法,是否会影响其他不相关的方法?
  3. 这个类的名称是否过于宽泛(如”Manager”、”Util”、”Handler”)?
  4. 这个类是否有多个不同的”使用者”,每个使用者只用到其中一部分功能?

如果上述问题多数为”是”,说明该类违反了单一职责原则。

2.5 课堂作业

图片描述
图片描述

三、开闭原则

3.1 定义

开闭原则(Open-Closed Principle,OCP) 是面向对象设计中最重要的原则,由Bertrand Meyer于1988年提出。

它的核心思想是:对扩展开放,对修改关闭。

  • 对扩展开放:当系统需要增加新功能时,可以通过添加新代码来扩展系统的行为
  • 对修改关闭:在添加新功能时,不应修改已有的源代码

通俗地说:写好的代码,不要再动了;要加新功能,就写新类。

3.2 为什么开闭原则如此重要?

在软件工程中,修改已有代码是有风险的

风险 说明
引入新Bug 修改一处代码可能破坏原本正常的功能
影响其他模块 被修改的类可能被多个其他类依赖,修改后影响范围难以预估
增加测试成本 每次修改都需要重新测试所有相关功能
难以维护 频繁修改的代码会变得混乱,难以理解

开闭原则通过”不修改已有代码,只添加新代码”的方式,有效规避了上述风险。

3.3 如何实现开闭原则?

实现开闭原则的关键是抽象化

  1. 定义抽象层:使用抽象类或接口定义系统的核心规范
  2. 实现具体类:让不同的具体类实现这个抽象层
  3. 面向抽象编程:客户端代码针对抽象层编程,不依赖具体实现类
  4. 扩展新功能:当需要新增功能时,只需添加一个新的具体类,无需修改已有代码

这一过程依赖Java的多态机制——父类引用可以指向任意子类对象,从而实现”新增子类即可扩展功能”。

3.4 违反开闭原则的后果

当系统不遵循开闭原则时,会出现以下问题:

  • 分支泛滥:大量使用if-else或switch来判断类型,每增加一种类型都要修改判断逻辑
  • 代码重复:相似的功能无法复用,只能复制粘贴后再修改
  • 扩展困难:每次新增功能都要修改多处已有代码,且改动的类和数量不断增加
  • 回归风险:每次改动都可能引入新的Bug,需要重新测试整个系统

3.5 课堂作业

图片描述
图片描述

四、两个原则的对比总结

对比项 单一职责原则 开闭原则
核心思想 一个类只做一件事 对扩展开放,对修改关闭
关注点 类的内部职责分配 系统的可扩展性
解决问题 类的臃肿和高耦合 新增功能时被迫修改已有代码
实现方式 将多个职责拆分到不同类中 引入抽象层,利用多态扩展
目标 提高可维护性、可复用性 提高可扩展性、降低修改风险

单一职责原则解决的是类的设计质量问题,开闭原则解决的是系统的可扩展性问题。两者相辅相成——遵循单一职责原则设计的类,通常也更容易符合开闭原则。

参考资料:耿祥义、张跃平《Java面向对象程序设计(第4版)》第7章

Java作业:两种创建线程方式的对比

一、作业概述
在Java中,创建线程主要有两种方式:

方式 核心类/接口 实现方法
方式一 继承Thread类 重写run()方法
方式二 实现Runnable接口 实现run()方法,传入Thread对象
这两种方式都可以创建并启动线程,但在设计理念、使用场景和灵活性 上存在显著差异。

核心问题:我们应该在什么时候选择继承Thread类?什么时候选择实现Runnable接口?

二、基础知识回顾
2.1 什么是线程?
线程(Thread)是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务。

2.2 Java中的线程模型
text

Java线程模型
├── Thread类(java.lang.Thread)
│ └── 代表一个线程对象
└── Runnable接口(java.lang.Runnable)
└── 定义可执行的任务代码
2.3 线程的生命周期
text

新建(New) → 就绪(Runnable) → 运行(Running) → 阻塞(Blocked) → 终止(Terminated)
↑ ↓
└── 时间片用完 ────┘
三、方式一:继承Thread类
3.1 实现步骤
定义一个类继承Thread类

重写run()方法,将线程要执行的任务代码放入其中

创建该类的实例(即创建线程对象)

调用线程对象的start()方法启动线程

3.2 代码实现
/**

  • 方式一:继承Thread类创建线程
    */
    public class ThreadDemo {
    public static void main(String[] args) {
    // 3. 创建线程对象
    MyThread t1 = new MyThread(“线程A”);
    MyThread t2 = new MyThread(“线程B”);

    // 4. 启动线程(会自动调用run方法)
    t1.start();
    t2.start();

    // 主线程也执行任务
    for (int i = 0; i < 5; i++) {
    System.out.println(“主线程正在执行:” + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

/**

    1. 继承Thread类
    1. 重写run()方法
      */
      class MyThread extends Thread {
      private String name;

    public MyThread(String name) {
    this.name = name;
    }

    @Override
    public void run() {
    for (int i = 0; i < 5; i++) {
    System.out.println(name + “正在执行:” + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

AI写代码
java
运行

3.3 运行结果(示例)

text

线程A正在执行:0
主线程正在执行:0
线程B正在执行:0
线程A正在执行:1
主线程正在执行:1
线程B正在执行:1
线程A正在执行:2
主线程正在执行:2

3.4 继承Thread类的特点
特点 说明
简单直接 代码结构简单,易于理解
线程独立 每个线程对象都是独立的Thread实例
单继承限制 Java不支持多继承,继承Thread后无法再继承其他类
四、方式二:实现Runnable接口
4.1 实现步骤
定义一个类实现Runnable接口

实现run()方法,将线程要执行的任务代码放入其中

创建该类的实例(即任务对象)

创建Thread对象,将任务对象作为参数传入

调用Thread对象的start()方法启动线程

4.2 代码实现
/**

  • 方式二:实现Runnable接口创建线程
    */
    public class RunnableDemo {
    public static void main(String[] args) {
    // 3. 创建任务对象
    MyRunnable task1 = new MyRunnable(“任务A”);
    MyRunnable task2 = new MyRunnable(“任务B”);

    // 4. 创建Thread对象,将任务对象传入
    Thread t1 = new Thread(task1);
    Thread t2 = new Thread(task2);

    // 5. 启动线程
    t1.start();
    t2.start();

    // 主线程执行
    for (int i = 0; i < 5; i++) {
    System.out.println(“主线程正在执行:” + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

/**

    1. 实现Runnable接口
    1. 实现run()方法
      */
      class MyRunnable implements Runnable {
      private String name;

    public MyRunnable(String name) {
    this.name = name;
    }

    @Override
    public void run() {
    for (int i = 0; i < 5; i++) {
    System.out.println(name + “正在执行:” + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

AI写代码
java
运行

4.3 Lambda表达式简化(Java 8+)
/**

  • 使用Lambda表达式简化Runnable创建
    */
    public class RunnableLambdaDemo {
    public static void main(String[] args) {
    // 使用Lambda表达式创建Runnable任务
    Runnable task = () -> {
    for (int i = 0; i < 5; i++) {
    System.out.println(Thread.currentThread().getName() + “: “ + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    };

    // 创建并启动线程
    new Thread(task, “线程A”).start();
    new Thread(task, “线程B”).start();
    }

}

AI写代码
java
运行

4.4 实现Runnable接口的特点

特点 说明
避免单继承限制 类可以实现多个接口,更灵活
任务与线程分离 Runnable定义任务,Thread定义线程,职责清晰
便于资源共享 多个线程可以共享同一个Runnable任务对象
符合面向接口编程 推荐的设计模式
五、两种方式的核心区别
5.1 代码层面对比
对比项 继承Thread类 实现Runnable接口
实现方式 class MyThread extends Thread class MyTask implements Runnable
启动方式 new MyThread().start() new Thread(new MyTask()).start()
资源共享 每个线程独立的对象 多个线程可共享同一个任务对象
单继承限制 受限制(无法继承其他类) 无限制(可实现多个接口)
获取当前线程 this(直接使用) Thread.currentThread()
线程名字设置 setName()或构造方法 new Thread(task, name)
5.2 设计理念对比
text

继承Thread类:
┌─────────────────┐
│ MyThread │
│ (is-a Thread) │ ← 线程就是任务本身
└─────────────────┘

实现Runnable接口:
┌─────────────┐ ┌─────────────┐
│ MyTask │ │ Thread │ ← 线程和任务分离
│ (is-a 任务) │ ─────→│ (is-a 线程) │ (组合关系)
└─────────────┘ └─────────────┘
5.3 资源共享示例对比
继承Thread类(无法共享资源):

class CounterThread extends Thread {
private int count = 0;

@Override
public void run() {
    for (int i = 0; i < 5; i++) {
        count++;
        System.out.println(getName() + " count=" + count);
    }
}

}

// 测试
public class ThreadShareTest {
public static void main(String[] args) {
// 创建两个独立的对象,每个有自己的count
CounterThread t1 = new CounterThread();
CounterThread t2 = new CounterThread();

    t1.start();  // 输出:Thread-0 count=1,2,3,4,5
    t2.start();  // 输出:Thread-1 count=1,2,3,4,5
    // 两个线程各自计数,互不影响
}

}

AI写代码
java
运行

实现Runnable接口(可以共享资源):

class CounterRunnable implements Runnable {
private int count = 0; // 共享的资源

@Override
public void run() {
    for (int i = 0; i < 5; i++) {
        count++;
        System.out.println(Thread.currentThread().getName() + " count=" + count);
    }
}

}

// 测试
public class RunnableShareTest {
public static void main(String[] args) {
// 创建一个任务对象
CounterRunnable task = new CounterRunnable();

    // 多个线程共享同一个任务对象
    Thread t1 = new Thread(task, "线程A");
    Thread t2 = new Thread(task, "线程B");
    
    t1.start();
    t2.start();
    // 两个线程共享同一个count,会产生竞态条件
    // 输出:线程A count=1,2,3,4,5;线程B count=6,7,8,9,10
}

}

AI写代码
java
运行

六、详细区别对比表

对比维度 继承Thread类 实现Runnable接口
灵活性 低(单继承限制) 高(可实现多接口)
代码耦合度 高(任务与线程绑定) 低(任务与线程分离)
资源共享 困难(需用静态变量) 容易(自然共享)
适用场景 简单的独立任务 复杂任务、资源共享场景
面向对象原则 违反“组合优于继承” 符合“组合优于继承”
线程池支持 不友好 友好(Runnable可放入线程池)
多次执行同一任务 需创建多个对象 可多次传入不同线程
可读性 简单直观 稍复杂但更清晰
七、适用场景分析
7.1 继承Thread类适用场景
场景 说明
简单临时线程 只需执行一次,不需要复用
需要直接覆盖Thread的其他方法 如重写run()以外的interrupt()、isAlive()等
代码量很小 不需要复杂的任务拆分
// 示例:简单的一次性线程
new Thread() {
@Override
public void run() {
System.out.println(“执行一次的任务”);
}
}.start();

AI写代码
java
运行
7.2 实现Runnable接口适用场景
场景 说明
多个线程执行相同任务 资源共享,如卖票系统
类已继承其他类 无法再继承Thread
需要配合线程池使用 ExecutorService提交Runnable
任务需要复用 同一个Runnable可多次执行
追求高内聚低耦合设计 任务与线程分离
// 示例:多个线程共享任务(模拟卖票)
class TicketTask implements Runnable {
private int tickets = 100;

@Override
public void run() {
    while (tickets > 0) {
        System.out.println(Thread.currentThread().getName() + "卖出第" + tickets-- + "张票");
    }
}

}

// 启动多个卖票窗口
TicketTask task = new TicketTask();
new Thread(task, “窗口1”).start();
new Thread(task, “窗口2”).start();
new Thread(task, “窗口3”).start();

AI写代码
java
运行

八、面试常考对比

8.1 为什么推荐使用Runnable而不是Thread?
避免单继承限制:Java只支持单继承,继承Thread后就无法继承其他类了

任务与线程解耦:Runnable专注于定义任务,Thread专注于执行任务

便于资源共享:多个线程可以共享同一个Runnable实例

便于配合线程池:线程池接受Runnable和Callable,而不是Thread

符合面向接口编程原则:面向接口编程比面向具体类编程更灵活

8.2 两者可以混用吗?
可以。Runnable最终还是要交给Thread来执行:

线程创建的两种方式
Lambda表达式实现Runnable接口

Runnable task = () -> System.out.println(“Hello”);
new Thread(task).start();
AI写代码
java
运行
通过Lambda表达式创建Runnable实现类,简洁地定义任务逻辑。Thread构造函数接收Runnable参数,调用start()启动新线程执行。

继承Thread类重写run方法

Thread t = new Thread() {
@Override
public void run() {
System.out.println(“Thread中的run”);
}
};
new Thread(t).start();
AI写代码
java
运行
Thread类本身实现了Runnable接口,其实例可作为Runnable对象传递给其他Thread。这种嵌套方式虽然合法,但通常直接调用t.start()更合理。

关键区别
Runnable是函数式接口,适合与Lambda结合实现任务与线程控制的分离
Thread作为Runnable的实现类,重写run()时可直接访问线程方法,但会失去Java单继承的灵活性
嵌套Thread实例作为参数的方式在实际开发中较少使用,容易造成理解困难
注意事项
线程启动必须调用start()而非run(),后者会在当前线程同步执行。两种方式创建的线程在功能上没有本质区别,但实现接口的方式更符合面向对象设计原则。

九、完整对比示例:银行取款
/**

  • 综合示例:模拟银行取款

  • 演示两种方式的区别
    */
    // 方式一:继承Thread类
    class ATMThread extends Thread {
    private static int balance = 1000; // 静态变量实现共享
    private String customerName;

    public ATMThread(String customerName) {
    this.customerName = customerName;
    }

    @Override
    public void run() {
    for (int i = 0; i < 3; i++) {
    synchronized (ATMThread.class) {
    if (balance >= 100) {
    balance -= 100;
    System.out.println(customerName + “取款100元,余额:” + balance);
    } else {
    System.out.println(customerName + “取款失败,余额不足!”);
    }
    }
    try {
    Thread.sleep(50);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

// 方式二:实现Runnable接口
class ATMRunnable implements Runnable {
private int balance = 1000; // 实例变量,自然共享
private String customerName;

public ATMRunnable(String customerName) {
    this.customerName = customerName;
}

@Override
public void run() {
    for (int i = 0; i < 3; i++) {
        synchronized (this) {
            if (balance >= 100) {
                balance -= 100;
                System.out.println(customerName + "取款100元,余额:" + balance);
            } else {
                System.out.println(customerName + "取款失败,余额不足!");
            }
        }
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

}

public class BankDemo {
public static void main(String[] args) {
System.out.println(“========== 方式一:继承Thread类 ==========”);
ATMThread t1 = new ATMThread(“小明”);
ATMThread t2 = new ATMThread(“小红”);
t1.start();
t2.start();

    try {
        Thread.sleep(1000);  // 等待线程执行完
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    System.out.println("\n========== 方式二:实现Runnable接口 ==========");
    ATMRunnable task = new ATMRunnable("张三");
    new Thread(task, "线程A").start();
    new Thread(task, "线程B").start();
}

}

AI写代码
java
运行

十、总结

10.1 核心知识点回顾
知识点 要点
继承Thread类 简单直接,但受单继承限制
实现Runnable接口 灵活、解耦、可资源共享
启动线程 必须调用start(),不能直接调用run()
资源共享 Runnable天然支持,Thread需静态变量
推荐方式 优先使用Runnable
10.2 选择建议
text

是否需要资源共享?
├── 是 → 使用Runnable接口
└── 否 → 继续判断
├── 类是否已继承其他类?
│ ├── 是 → 使用Runnable接口
│ └── 否 → 两种都可以,但Runnable更推荐
└── 是否需要重写Thread的其他方法?
├── 是 → 使用Thread子类
└── 否 → 使用Runnable接口(推荐)
————————————————
版权声明:本文为CSDN博主「江城墨雨xs」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2401_87661118/article/details/161711152

Java作业:文件复制四种实现方式

一、作业概述
本次作业要求实现文件复制的四种不同方式:

序号 方式 核心类 适用场景
1 文本文件复制 FileReader / FileWriter 仅文本文件
2 字符缓冲流 BufferedReader / BufferedWriter 文本文件(最常用)
3 任意文件复制 FileInputStream / FileOutputStream 任意类型文件
4 字节缓冲流 BufferedInputStream / BufferedOutputStream 任意文件(万能复制)
核心原理:从一个文件中读取数据,写入到另一个文件中。

二、基础知识回顾
2.1 Java IO流分类
text

Java IO流
├── 字节流(处理所有文件)
│ ├── InputStream(输入流)—— FileInputStream
│ ├── OutputStream(输出流)—— FileOutputStream
│ ├── BufferedInputStream(缓冲输入流)
│ └── BufferedOutputStream(缓冲输出流)
└── 字符流(仅处理文本文件)
├── Reader(输入流)—— FileReader
├── Writer(输出流)—— FileWriter
├── BufferedReader(缓冲输入流)
└── BufferedWriter(缓冲输出流)
2.2 字符流 vs 字节流
对比项 字符流 字节流
处理单位 字符(16-bit) 字节(8-bit)
适用文件 文本文件(.txt, .java, .csv) 任意文件(图片、视频、音频等)
核心类 Reader, Writer InputStream, OutputStream
是否支持中文 是 需要手动处理编码
2.3 缓冲流的作用
特性 无缓冲流 有缓冲流
读取方式 每次读一个字节/字符 一次读一批数据到内存缓冲区
IO次数 频繁 减少
性能 较低 较高
特有方法 无 readLine()(字符缓冲流)
三、方式一:文本文件复制(字符流)
3.1 实现原理
使用 FileReader 读取文本文件,FileWriter 写入到目标文件。一次读取一个字符数组,提高效率。

3.2 代码实现
以下是整理后的代码块,符合Markdown 格式要求:

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**

  • 方式一:文本文件复制

  • 使用字符流 FileReader/FileWriter

  • 适用于:.txt, .java, .csv 等文本文件
    */
    public class TextFileCopy {
    public static void main(String[] args) {
    String sourceFile = “source.txt”;
    String destFile = “dest_text.txt”;

    try {
    copyFile(sourceFile, destFile);
    System.out.println(“文件复制成功!”);
    System.out.println(“源文件:” + sourceFile);
    System.out.println(“目标文件:” + destFile);
    } catch (IOException e) {
    System.out.println(“文件复制失败:” + e.getMessage());
    e.printStackTrace();
    }
    }

    public static void copyFile(String sourcePath, String destPath) throws IOException {
    try (FileReader fr = new FileReader(sourcePath);
    FileWriter fw = new FileWriter(destPath)) {

    char[] buffer = new char[1024];
    int len;

    while ((len = fr.read(buffer)) != -1) {
    fw.write(buffer, 0, len);
    }

    System.out.println(“已复制 “ + len + “ 个字符”);
    }
    }

}
AI写代码
java
运行

3.3 代码解释

代码 说明
FileReader fr = new FileReader(sourcePath) 创建文件字符输入流,用于读取源文件
FileWriter fw = new FileWriter(destPath) 创建文件字符输出流,用于写入目标文件
char[] buffer = new char[1024] 创建字符数组缓冲区,一次读取1024个字符
fr.read(buffer) 从源文件读取数据到缓冲区,返回实际读取的字符数
fw.write(buffer, 0, len) 将缓冲区中的数据写入目标文件
try-with-resources 自动关闭流资源,不需要手动调用close()
3.4 运行效果
假设 source.txt 内容为:

text

Hello Java!
你好,世界!
这是文件复制测试。
复制后的 dest_text.txt 内容完全相同。

四、方式二:字符缓冲流(最常用)
4.1 实现原理
使用 BufferedReader 和 BufferedWriter 包装基本的字符流,提供缓冲功能,提高读写效率。BufferedReader 提供了 readLine() 方法,可以一次读取一行文本,非常方便。

4.2 代码实现
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**

  • 方式二:字符缓冲流复制文本文件

  • 使用 BufferedReader/BufferedWriter

  • 特点:最常用、支持按行读取、性能高
    */
    public class BufferedTextFileCopy {

    public static void main(String[] args) {
    String sourceFile = “source.txt”;
    String destFile = “dest_buffered.txt”;

    try {
    copyFile(sourceFile, destFile);
    System.out.println(“文件复制成功!”);
    } catch (IOException e) {
    System.out.println(“文件复制失败:” + e.getMessage());
    }
    }

    /**

    • 使用缓冲流复制文本文件
    • @param sourcePath 源文件路径
    • @param destPath 目标文件路径
    • @throws IOException IO异常
      */
      public static void copyFile(String sourcePath, String destPath) throws IOException {
      // 使用 try-with-resources 自动关闭资源
      try (BufferedReader br = new BufferedReader(new FileReader(sourcePath));
      BufferedWriter bw = new BufferedWriter(new FileWriter(destPath))) {

      String line;
      int lineCount = 0;

      // 按行读取,逐行写入
      while ((line = br.readLine()) != null) {
      bw.write(line);
      bw.newLine(); // 写入换行符
      lineCount++;
      }

      System.out.println(“共复制 “ + lineCount + “ 行”);
      }

    }

}
AI写代码
java
运行

4.3 为什么是最常用的?
优点 说明
按行读取 readLine() 方法非常方便处理文本
缓冲性能 减少实际IO次数,提高效率
编码友好 正确处理中文字符
代码简洁 逻辑清晰,易于理解和维护
4.4 代码解释
代码 说明
new BufferedReader(new FileReader(…)) 在 FileReader 外层包装缓冲流
br.readLine() 读取一行文本(不包含换行符),返回 null 表示读到文件末尾
bw.write(line) 写入一行文本
bw.newLine() 写入系统相关的换行符(Windows: \r\n, Linux: \n)
五、方式三:任意文件复制(字节流)
5.1 实现原理
使用 FileInputStream 和 FileOutputStream 处理字节数据。这种方式可以复制任意类型的文件,因为所有文件在计算机中本质上都是字节序列。

5.2 代码实现
以下是转换后的代码块格式:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**

  • 方式三:任意文件复制

  • 使用字节流 FileInputStream/FileOutputStream

  • 适用于:图片、视频、音频、压缩包等任意文件
    */
    public class AnyFileCopy {
    public static void main(String[] args) {
    // 示例:复制图片文件
    String sourceFile = “photo.jpg”;
    String destFile = “photo_copy.jpg”;

    // 示例:复制视频文件
    // String sourceFile = “video.mp4”;
    // String destFile = “video_copy.mp4”;

    try {
    copyFile(sourceFile, destFile);
    System.out.println(“文件复制成功!”);
    } catch (IOException e) {
    System.out.println(“文件复制失败:” + e.getMessage());
    }
    }

    /**

    • 使用字节流复制任意文件
    • @param sourcePath 源文件路径
    • @param destPath 目标文件路径
    • @throws IOException IO异常
      */
      public static void copyFile(String sourcePath, String destPath) throws IOException {
      try (FileInputStream fis = new FileInputStream(sourcePath);
      FileOutputStream fos = new FileOutputStream(destPath)) {

      // 字节数组缓冲区,一次读取1024个字节
      byte[] buffer = new byte[1024];
      int len;
      long totalBytes = 0;

      while ((len = fis.read(buffer)) != -1) {
      fos.write(buffer, 0, len);
      totalBytes += len;
      }

      System.out.println(“共复制 “ + totalBytes + “ 字节”);
      System.out.println(“约 “ + (totalBytes / 1024) + “ KB”);
      }

    }

}
AI写代码
java
运行

5.3 支持的任意文件类型

文件类型 扩展名 是否支持
文本文件 .txt, .java, .xml, .json 支持
图片 .jpg, .png, .gif, .bmp 支持
视频 .mp4, .avi, .mkv, .flv 支持
音频 .mp3, .wav, .flac 支持
压缩包 .zip, .rar, .7z 支持
可执行文件 .exe, .dll 支持
5.4 字节流 vs 字符流选择
if (fileType == 文本文件) {
// 使用字符流(方便处理文本)
useReaderWriter();
} else {
// 使用字节流(万能)
useInputStreamOutputStream();
}

AI写代码
java
运行
六、方式四:字节缓冲流(万能复制)

6.1 实现原理
使用 BufferedInputStream 和 BufferedOutputStream 包装基本的字节流,提供内部缓冲区,减少实际IO次数,是最通用、性能最好的文件复制方式。

6.2 代码实现
以下是转换后的代码块格式:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedAnyFileCopy {
public static void main(String[] args) {
copyFile(“photo.jpg”, “photo_buffered_copy.jpg”);
}

public static boolean copyFile(String sourcePath, String destPath) {
    if (sourcePath == null || destPath == null) {
        System.err.println("文件路径不能为空");
        return false;
    }

    long startTime = System.currentTimeMillis();

    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourcePath));
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPath))) {
        
        byte[] buffer = new byte[8192];
        int len;
        long totalBytes = 0;

        while ((len = bis.read(buffer)) != -1) {
            bos.write(buffer, 0, len);
            totalBytes += len;
        }

        bos.flush();

        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;

        System.out.println("========== 复制完成 ==========");
        System.out.println("源文件:" + sourcePath);
        System.out.println("目标文件:" + destPath);
        System.out.println("文件大小:" + formatSize(totalBytes));
        System.out.println("耗时:" + duration + " 毫秒");
        System.out.println("================================");

        return true;
    } catch (IOException e) {
        System.err.println("文件复制失败:" + e.getMessage());
        return false;
    }
}

private static String formatSize(long bytes) {
    if (bytes < 1024) {
        return bytes + " B";
    } else if (bytes < 1024 * 1024) {
        return String.format("%.2f KB", bytes / 1024.0);
    } else if (bytes < 1024 * 1024 * 1024) {
        return String.format("%.2f MB", bytes / (1024.0 * 1024));
    } else {
        return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
    }
}

}
AI写代码
java
运行

6.3 为什么是“万能复制”?

特点 说明
文件类型无关 无论是文本、图片、视频还是可执行文件,都能正确处理
编码无关 按字节处理,不涉及编码问题
性能优化 内置缓冲区,减少IO次数
大文件支持 边读边写,不会一次性加载整个文件到内存
6.4 性能对比测试
/**

  • 性能对比测试

  • 比较四种复制方式的效率
    */
    public class PerformanceTest {
    public static void main(String[] args) {
    String testFile = “large_file.txt”; // 假设有一个10MB的文件

    // 测试四种方式的复制时间
    testMethod(“方式一:字符流”, () -> {
    TextFileCopy.copyFile(testFile, “test1.txt”);
    });

    testMethod(“方式二:字符缓冲流”, () -> {
    BufferedTextFileCopy.copyFile(testFile, “test2.txt”);
    });

    testMethod(“方式三:字节流”, () -> {
    AnyFileCopy.copyFile(testFile, “test3.txt”);
    });

    testMethod(“方式四:字节缓冲流”, () -> {
    BufferedAnyFileCopy.copyFile(testFile, “test4.txt”);
    });
    }

    private static void testMethod(String name, Runnable method) {
    long start = System.nanoTime();
    method.run();
    long end = System.nanoTime();
    System.out.println(name + “ 耗时:” + (end - start) / 1000000 + “ ms”);
    }

}

AI写代码
java
运行

典型运行结果(复制10MB文件):

text

方式一:字符流 耗时:450 ms
方式二:字符缓冲流 耗时:120 ms ← 比无缓冲快3-4倍
方式三:字节流 耗时:480 ms
方式四:字节缓冲流 耗时:100 ms ← 最快,万能选择
七、四种方式对比总结
对比项 方式一 方式二 方式三 方式四
核心类 FileReader/Writer BufferedReader/Writer FileInput/OutputStream BufferedInput/OutputStream
处理单位 字符 字符(按行) 字节 字节
适用文件 仅文本 仅文本 任意文件 任意文件
缓冲支持 手动数组 内置缓冲 手动数组 内置缓冲
性能 中 高 中 最高
编码问题 自动处理 自动处理 需注意 需注意
推荐场景 小文本文件 文本文件(最常用) 任意小文件 万能复制(推荐)
选择建议:

复制文本文件 → 用方式二(字符缓冲流)

复制任意文件(图片、视频等) → 用方式四(字节缓冲流)

简单小文件测试 → 方式一或方式三

生产环境 → 方式四

八、完整代码整合版
以下是一个完整的工具类,整合了四种复制方式:

import java.io.*;

/**

  • 文件复制工具类

  • 提供四种不同的文件复制方式
    */
    public class FileCopyUtil {

    /**

    • 方式一:字符流复制文本文件
      */
      public static void copyByCharStream(String src, String dest) throws IOException {
      try (FileReader fr = new FileReader(src);
      FileWriter fw = new FileWriter(dest)) {
      char[] buffer = new char[1024];
      int len;
      while ((len = fr.read(buffer)) != -1) {
      fw.write(buffer, 0, len);
      }
      }
      }

    /**

    • 方式二:字符缓冲流复制文本文件(推荐文本文件使用)
      */
      public static void copyByBufferedCharStream(String src, String dest) throws IOException {
      try (BufferedReader br = new BufferedReader(new FileReader(src));
      BufferedWriter bw = new BufferedWriter(new FileWriter(dest))) {
      String line;
      while ((line = br.readLine()) != null) {
      bw.write(line);
      bw.newLine();
      }
      }
      }

    /**

    • 方式三:字节流复制任意文件
      */
      public static void copyByByteStream(String src, String dest) throws IOException {
      try (FileInputStream fis = new FileInputStream(src);
      FileOutputStream fos = new FileOutputStream(dest)) {
      byte[] buffer = new byte[1024];
      int len;
      while ((len = fis.read(buffer)) != -1) {
      fos.write(buffer, 0, len);
      }
      }
      }

    /**

    • 方式四:字节缓冲流复制任意文件(万能复制,推荐)
      */
      public static void copyByBufferedByteStream(String src, String dest) throws IOException {
      try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
      BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest))) {
      byte[] buffer = new byte[8192];
      int len;
      while ((len = bis.read(buffer)) != -1) {
      bos.write(buffer, 0, len);
      }
      bos.flush();
      }
      }

    // 测试
    public static void main(String[] args) {
    try {
    // 文本文件复制测试
    copyByBufferedCharStream(“source.txt”, “dest.txt”);

         // 图片文件复制测试
         copyByBufferedByteStream("photo.jpg", "photo_copy.jpg");
    
         System.out.println("所有文件复制完成!");
     } catch (IOException e) {
         e.printStackTrace();
     }
    

    }

}

AI写代码
java
运行

九、常见问题与解决方案
9.1 文件找不到异常(FileNotFoundException)
// 错误示例
FileReader fr = new FileReader(“nonexistent.txt”);

// 解决方案1:检查文件路径是否正确
// 解决方案2:使用绝对路径
FileReader fr = new FileReader(“D:/project/files/source.txt”);

// 解决方案3:先判断文件是否存在
File file = new File(“source.txt”);
if (!file.exists()) {
System.out.println(“文件不存在:” + file.getAbsolutePath());
return;
}

AI写代码
java
运行

9.2 中文乱码问题

// 问题:使用字节流读取中文文本文件可能出现乱码
// 解决:指定正确的字符集编码
// 方式一:使用字符流(默认使用系统编码)
BufferedReader br = new BufferedReader(new FileReader(“source.txt”));

// 方式二:指定编码(推荐)
BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(“source.txt”), “UTF-8”));

AI写代码
java
运行
9.3 大文件复制内存 溢出
// 错误做法:一次性读取整个文件
byte[] allBytes = fis.readAllBytes(); // 大文件会内存溢出

// 正确做法:边读边写,使用缓冲区
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}

AI写代码
java
运行

十、总结

10.1 核心知识点回顾
知识点 要点
字符流 用于文本文件,处理字符数据
字节流 用于任意文件,处理字节数据
缓冲流 提高IO性能,减少实际读写次数
try-with-resources 自动关闭流资源
缓冲区大小 通常 1024、2048、8192 字节

JAVA作业:了解Java的反射机制

一、什么是反射?
1.1 定义
反射(Reflection) 是Java语言提供的一种强大机制,允许程序在运行时动态地获取类的完整信息(如成员变量、方法、构造方法等),并且可以动态地创建对象、调用方法、访问属性。

通俗地说:反射就是在程序运行时,让Java程序能够“ introspection”(自省),看清自己的结构,甚至修改自己的行为。

1.2 正常方式 vs 反射方式
对比项 正常方式 反射方式
时机 编译时确定 运行时动态确定
类的信息 写代码时已知 运行时才获取
对象创建 new 类名() Class.forName(“类名”).newInstance()
方法调用 对象.方法名() method.invoke(对象, 参数)
特点 高效、类型安全 灵活、但有一定性能开销
简单示例:

// 正常方式:编译时就知道要使用Student类
Student s = new Student();
s.study();

// 反射方式:运行时才知道要使用哪个类
Class<?> clazz = Class.forName(“com.example.Student”);
Object obj = clazz.newInstance();
Method method = clazz.getMethod(“study”);
method.invoke(obj);

AI写代码
java
运行

二、为什么需要反射?

2.1 核心价值
反射机制使得Java程序从“静态”变为“动态”:

能力 说明
动态加载类 运行时才决定加载哪个类
动态创建对象 类名作为字符串传入,无需硬编码
动态调用方法 方法名作为字符串传入
访问私有成员 可以绕过访问权限检查
2.2 典型应用场景
场景 说明
IDE自动提示 编辑器通过反射获取类的方法和属性
Spring框架 IoC容器通过反射创建和管理Bean
JUnit框架 通过反射识别@Test注解的方法并执行
Tomcat等Web容器 动态加载Servlet类
JSON解析库 通过反射将JSON数据映射到Java对象
注解处理器 运行时读取注解信息
三、反射的核心API
3.1 Class类
Class类是反射的入口,每个Java类在运行时都会对应一个Class对象。

获取Class对象的三种方式:

// 方式1:通过类名.class
Class<?> clazz1 = String.class;

// 方式2:通过对象.getClass()
String str = “hello”;
Class<?> clazz2 = str.getClass();

// 方式3:通过Class.forName()(最常用,动态加载)
Class<?> clazz3 = Class.forName(“java.lang.String”);
AI写代码
java
运行
3.2 反射核心类

类名 作用
Class 代表一个类或接口
Field 代表类的成员变量
Method 代表类的方法
Constructor 代表类的构造方法
Modifier 访问修饰符的工具类
3.3 常用方法一览
Class类常用方法:

方法 说明
getName() 获取类全名(包名+类名)
getSimpleName() 获取类简单名称
getFields() 获取所有public成员变量
getDeclaredFields() 获取所有声明的成员变量(包括private)
getMethods() 获取所有public方法(包括继承的)
getDeclaredMethods() 获取所有声明的方法(不包括继承的)
getConstructors() 获取所有public构造方法
getDeclaredConstructors() 获取所有声明的构造方法
newInstance() 创建实例(调用无参构造)
四、反射的基本使用
4.1 准备工作:定义一个示例类
/**

  • 示例类:用于演示反射机制
    */
    public class Person {
    // 成员变量
    private String name;
    public int age;

    // 构造方法
    public Person() {
    System.out.println(“无参构造方法被调用”);
    }

    private Person(String name) {
    this.name = name;
    System.out.println(“私有构造方法被调用,name=” + name);
    }

    public Person(String name, int age) {
    this.name = name;
    this.age = age;
    System.out.println(“有参构造方法被调用:” + name + “,” + age);
    }

    // 成员方法
    public void sayHello() {
    System.out.println(“Hello, I’m “ + name);
    }

    private void secret() {
    System.out.println(“这是一个私有方法”);
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    @Override
    public String toString() {
    return “Person{name=’” + name + “‘, age=” + age + “}”;
    }

}
AI写代码
java
运行

4.2 获取类的信息

import java.lang.reflect.*;

public class ReflectionInfoDemo {
public static void main(String[] args) throws ClassNotFoundException {
// 1. 获取Class对象
Class<?> clazz = Class.forName(“Person”);

    // 2. 获取类名
    System.out.println("类全名:" + clazz.getName());
    System.out.println("简单类名:" + clazz.getSimpleName());
    
    // 3. 获取所有public方法(包括继承的)
    System.out.println("\n=== 所有public方法 ===");
    Method[] methods = clazz.getMethods();
    for (Method m : methods) {
        System.out.println("  " + m.getName());
    }
    
    // 4. 获取所有声明的成员变量
    System.out.println("\n=== 所有声明的成员变量 ===");
    Field[] fields = clazz.getDeclaredFields();
    for (Field f : fields) {
        System.out.println("  " + Modifier.toString(f.getModifiers()) + " " + f.getName());
    }
    
    // 5. 获取所有构造方法
    System.out.println("\n=== 所有构造方法 ===");
    Constructor<?>[] constructors = clazz.getDeclaredConstructors();
    for (Constructor<?> c : constructors) {
        System.out.println("  " + c);
    }
}

}
AI写代码
java
运行

4.3 通过反射创建对象

import java.lang.reflect.*;

public class ReflectionCreateDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName(“Person”);

    // 方式1:调用无参构造方法
    System.out.println("=== 方式1:无参构造 ===");
    Person p1 = (Person) clazz.newInstance();  // JDK9后可能过时
    p1.setName("张三");
    p1.sayHello();
    
    // 方式2:调用有参构造方法(推荐)
    System.out.println("\n=== 方式2:有参构造 ===");
    Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
    Person p2 = (Person) constructor.newInstance("李四", 20);
    p2.sayHello();
    
    // 方式3:调用私有构造方法
    System.out.println("\n=== 方式3:私有构造 ===");
    Constructor<?> privateCon = clazz.getDeclaredConstructor(String.class);
    privateCon.setAccessible(true);  // 突破私有访问限制
    Person p3 = (Person) privateCon.newInstance("王五");
    p3.sayHello();
}

}
AI写代码
java
运行

4.4 通过反射调用方法

import java.lang.reflect.*;

public class ReflectionMethodDemo {
public static void main(String[] args) throws Exception {
// 创建对象
Class clazz = Class.forName("Person"); Constructor con = clazz.getConstructor(String.class, int.class);
Person person = (Person) con.newInstance(“小明”, 18);

    // 调用public方法
    System.out.println("=== 调用public方法 ===");
    Method sayHello = clazz.getMethod("sayHello");
    sayHello.invoke(person);
    
    // 调用带参数的方法
    Method setName = clazz.getMethod("setName", String.class);
    setName.invoke(person, "小红");
    sayHello.invoke(person);
    
    // 调用私有方法(需要setAccessible)
    System.out.println("\n=== 调用私有方法 ===");
    Method secret = clazz.getDeclaredMethod("secret");
    secret.setAccessible(true);  // 突破私有权限
    secret.invoke(person);
    
    // 调用有返回值的方法
    Method getName = clazz.getMethod("getName");
    String name = (String) getName.invoke(person);
    System.out.println("getName返回值:" + name);
}

}
AI写代码
java
运行

4.5 通过反射访问成员变量
以下是整理后的代码块格式:

import java.lang.reflect.*;

public class ReflectionFieldDemo {
public static void main(String[] args) throws Exception {
// 创建对象
Class clazz = Class.forName("Person"); Constructor con = clazz.getConstructor(String.class, int.class);
Person person = (Person) con.newInstance(“张三”, 25);

    System.out.println("原始对象:" + person);
    
    // 访问public成员变量
    System.out.println("\n=== 访问public成员变量 ===");
    Field ageField = clazz.getField("age");
    System.out.println("当前age:" + ageField.get(person));
    ageField.set(person, 30);
    System.out.println("修改后age:" + ageField.get(person));
    
    // 访问私有成员变量(需要setAccessible)
    System.out.println("\n=== 访问私有成员变量 ===");
    Field nameField = clazz.getDeclaredField("name");
    nameField.setAccessible(true);
    System.out.println("当前name:" + nameField.get(person));
    nameField.set(person, "李四");
    System.out.println("修改后name:" + nameField.get(person));
    System.out.println("最终对象:" + person);
}

}
AI写代码
java
运行

五、反射的实际应用示例

示例1:通用的toString方法
import java.lang.reflect.*;

class ObjectUtil {
public static String toString(Object obj) {
if (obj == null) return “null”;

    Class<?> clazz = obj.getClass();
    StringBuilder sb = new StringBuilder();
    sb.append(clazz.getSimpleName()).append(" { ");
    
    Field[] fields = clazz.getDeclaredFields();
    for (int i = 0; i < fields.length; i++) {
        fields[i].setAccessible(true);
        try {
            sb.append(fields[i].getName()).append("=");
            sb.append(fields[i].get(obj));
            if (i < fields.length - 1) {
                sb.append(", ");
            }
        } catch (IllegalAccessException e) {
            sb.append("?");
        }
    }
    sb.append(" }");
    return sb.toString();
}

}

class Student {
private String name;
private int score;

Student(String name, int score) {
    this.name = name;
    this.score = score;
}

}

public class GenericToStringDemo {
public static void main(String[] args) {
Student s = new Student(“张三”, 95);
System.out.println(ObjectUtil.toString(s));
}
}
AI写代码
java
运行

示例2:动态调用任意方法
import java.lang.reflect.*;

class DynamicInvoker {
public static Object invoke(Object obj, String methodName, Object… args) {
try {
Class clazz = obj.getClass(); Class[] paramTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
paramTypes[i] = args[i].getClass();
}
Method method = clazz.getMethod(methodName, paramTypes);
return method.invoke(obj, args);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

class Calculator {
public int add(int a, int b) {
return a + b;
}

public String greet(String name) {
    return "Hello, " + name;
}

}

public class DynamicInvokeDemo {
public static void main(String[] args) {
Calculator calc = new Calculator();
Object result1 = DynamicInvoker.invoke(calc, “add”, 10, 20);
System.out.println(“add结果:” + result1);
Object result2 = DynamicInvoker.invoke(calc, “greet”, “张三”);
System.out.println(“greet结果:” + result2);
}
}
AI写代码
java
运行

示例3:模拟JUnit的@Test注解
import java.lang.annotation.;
import java.lang.reflect.
;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Test {}

class MyTestClass {
@Test
public void testMethod1() {
System.out.println(“执行测试方法1”);
}

@Test
public void testMethod2() {
    System.out.println("执行测试方法2");
}

public void normalMethod() {
    System.out.println("普通方法不会被自动执行");
}

}

class TestRunner {
public static void run(Class<?> testClass) throws Exception {
Object instance = testClass.newInstance();
Method[] methods = testClass.getDeclaredMethods();

    for (Method method : methods) {
        if (method.isAnnotationPresent(Test.class)) {
            System.out.print("正在执行:");
            method.invoke(instance);
        }
    }
}

}

public class AnnotationDemo {
public static void main(String[] args) throws Exception {
TestRunner.run(MyTestClass.class);
}
}
AI写代码
java
运行

运行结果
正在执行:执行测试方法1
正在执行:执行测试方法2
AI写代码
六、反射的优缺点
6.1 优点
优点 说明
动态性 运行时动态加载类,提高程序灵活性
通用性 可以编写通用的代码处理各种类型的对象
框架基础 几乎所有Java框架(Spring、Hibernate等)都依赖反射
绕过限制 可以访问私有成员,用于特殊场景(如调试工具)
6.2 缺点
缺点 说明
性能开销 反射调用比直接调用慢,因为需要动态解析
安全问题 可以绕过访问权限,破坏封装性
代码可读性 反射代码不如直接调用直观
编译时检查缺失 错误只能在运行时暴露
七、性能对比示例
以下是整理后的代码块格式:

/**

  • 对比直接调用与反射调用的性能
    */
    public class PerformanceCompare {
    public static void main(String[] args) throws Exception {
    Person person = new Person(“张三”, 20);

    // 1. 直接调用
    long start1 = System.nanoTime();
    for (int i = 0; i < 1000000; i++) {
    person.sayHello();
    }
    long end1 = System.nanoTime();
    System.out.println(“直接调用耗时:” + (end1 - start1) / 1000000 + “ ms”);

    // 2. 反射调用
    Method method = Person.class.getMethod(“sayHello”);
    long start2 = System.nanoTime();
    for (int i = 0; i < 1000000; i++) {
    method.invoke(person);
    }
    long end2 = System.nanoTime();
    System.out.println(“反射调用耗时:” + (end2 - start2) / 1000000 + “ ms”);

    // 3. 反射调用(启用访问检查关闭)
    method.setAccessible(true);
    long start3 = System.nanoTime();
    for (int i = 0; i < 1000000; i++) {
    method.invoke(person);
    }
    long end3 = System.nanoTime();
    System.out.println(“反射(setAccessible)耗时:” + (end3 - start3) / 1000000 + “ ms”);
    }

}
AI写代码
java
运行

代码结构说明:

完整保留了原始注释和逻辑结构
使用标准的Java代码块格式
保持了原有的三个测试部分(直接调用、反射调用、反射调用优化)
时间统计单位统一转换为毫秒(ms)
结论:反射调用通常比直接调用慢2-3倍,setAccessible(true)可以略微提升性能。

八、总结
8.1 核心知识点回顾
知识点 要点
反射的定义 运行时动态获取类信息并操作对象
获取Class对象 .class、getClass()、Class.forName()
核心API Class、Field、Method、Constructor
访问私有成员 setAccessible(true)
主要应用 Spring框架、JUnit、JSON解析等
8.2 使用建议
优先使用直接调用:除非确实需要动态性,否则不要滥用反射

缓存反射对象:如果需要频繁调用,缓存Method、Field等对象

注意安全性:反射可以破坏封装,谨慎使用

考虑性能影响:对性能敏感的代码避免使用反射

8.3 作业完成情况
理解反射机制的定义和作用

掌握获取Class对象的三种方式

能够通过反射创建对象、调用方法、访问属性

了解反射的实际应用场景

了解反射的优缺点

九、课后练习
编写代码,通过反射获取ArrayList类的所有方法名称

使用反射调用String类的toUpperCase()方法

通过反射创建一个HashMap对象,并调用put和get方法

尝试修改一个String对象的内容(利用反射突破不可变性)

Java作业:使用匿名内部类计算方法执行时间

一、作业需求

计算一个方法执行了多少秒(或毫秒),要求使用匿名内部类实现。

这是一个典型的应用场景:在不修改原有方法代码的情况下,为方法添加执行时间统计功能。

二、什么是匿名内部类?

2.1 定义

匿名内部类(Anonymous Inner Class)是没有名字的内部类,通常用于一次性使用的场景。

2.2 基本语法
匿名内部类 语法示例
匿名内部类的标准语法格式如下,适用于直接实现接口或继承父类并重写方法:

父类/接口 变量名 = new 父类/接口() {
@Override
返回类型 方法名(参数列表) {
// 方法实现
}
};
AI写代码
java
运行
具体实现案例
实现接口示例(Runnable):

Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(“匿名内部类执行任务”);
}
};
AI写代码
java
运行
继承抽象类示例(Thread):

Thread thread = new Thread() {
@Override
public void run() {
System.out.println(“重写Thread的run方法”);
}
};
AI写代码
java
运行
使用场景说明
适用于需要一次性使用的类实现,避免单独创建子类文件
常用于事件监听、线程实现等需要快速重写方法的场景
匿名内部类会隐式持有外部类的引用,需注意内存泄漏问题
语法要点
必须实现父类或接口的所有抽象方法
可以访问外部类的final变量或等效final变量
编译后会生成独立的.class文件,命名格式为外部类$数字.class
Lambda简化场景
当接口仅含单个抽象方法时(函数式接口),可用Lambda表达式替代:

Runnable task = () -> System.out.println(“Lambda替代匿名类”);
AI写代码
java
运行
2.3 特点
特点 说明
没有类名 创建时直接定义类体
一次性使用 通常只使用一次
简化代码 避免单独定义一个类
必须实现所有抽象方法 与普通类相同
三、设计思路
3.1 核心问题
如何在不修改原有方法代码的情况下,统计方法的执行时间?

3.2 解决方案
使用模板方法设计模式 + 匿名内部类:

定义一个接口或抽象类,包含需要执行的方法

在工具类中编写计时方法,接收接口/抽象类的实现

调用时使用匿名内部类传入具体逻辑

3.3 流程图
以下是基于文本描述的计时流程实现方案,采用Java代码示例和流程图说明:

代码实现方案
// 定义函数式接口,便于匿名内部类实现
@FunctionalInterface
interface TargetMethod {
void execute();
}

public class TimeRecorder {
public static void recordExecutionTime(TargetMethod method) {
long startTime = System.nanoTime();
method.execute();
long endTime = System.nanoTime();
long duration = endTime - startTime;
System.out.println(“Execution time: “ + duration + “ ns”);
}

public static void main(String[] args) {
    // 通过匿名内部类实现目标方法
    recordExecutionTime(new TargetMethod() {
        @Override
        public void execute() {
            // 替换为实际需要计时的代码
            int sum = 0;
            for (int i = 0; i < 1000000; i++) {
                sum += i;
            }
        }
    });
}

}
AI写代码
java
运行

流程图表示
text

开始计时

记录开始时间 (startTime)

执行目标方法 ←──── 匿名内部类提供具体实现

记录结束时间 (endTime)

计算耗时 = endTime - startTime

输出结果
关键说明
时间精度:使用System.nanoTime()获取纳秒级时间戳,比currentTimeMillis()精度更高
函数式接口:通过接口设计使计时逻辑与业务逻辑解耦
Lambda优化:Java 8+可使用Lambda简化匿名内部类写法:
recordExecutionTime(() -> {
int sum = IntStream.range(0, 1000000).sum();
});
AI写代码
java
运行
注意事项
避免在测试方法中包含IO操作或随机因素
考虑JVM预热问题,正式测试前应先执行几次预热运行
对于微基准测试建议使用JMH等专业框架
四、代码实现
4.1 方式一:使用接口
以下是整理后的代码块格式:

/**

  • 定义一个接口,包含需要计时的方法
    */
    interface Task {
    void execute(); // 需要执行的任务
    }

/**

  • 计时器工具类
    /
    class TimerUtil {
    /
    *
    • 计算任务执行时间(毫秒)
    • @param task 要执行的任务
      */
      public static void measureTime(Task task) {
      long startTime = System.currentTimeMillis();
      task.execute();
      long endTime = System.currentTimeMillis();
      long duration = endTime - startTime;
      System.out.println(“方法执行耗时:” + duration + “ 毫秒”);
      System.out.println(“转换为秒:” + duration / 1000.0 + “ 秒”);
      }
      }

/**

  • 测试类
    */
    public class TimeTest {
    public static void main(String[] args) {
    TimerUtil.measureTime(new Task() {
    @Override
    public void execute() {
    System.out.println(“开始执行任务…”);
    for (int i = 0; i < 100000000; i++) {
    Math.sqrt(i);
    }
    System.out.println(“任务执行完毕”);
    }
    });
    }
    }
    AI写代码
    java
    运行

运行结果:

开始执行任务…
任务执行完毕
方法执行耗时:12 毫秒
转换为秒:0.012 秒
AI写代码
text
4.2 方式二:使用抽象类
/**

  • 抽象类:需要计时的任务
    */
    abstract class AbstractTask {
    public abstract void run();
    }

/**

  • 计时器工具类(支持抽象类)
    */
    class TimerUtil2 {
    public static void measureTime(AbstractTask task) {
    long startTime = System.nanoTime(); // 纳秒,更精确

    task.run();

    long endTime = System.nanoTime();
    long duration = endTime - startTime;

    System.out.println(“执行耗时:” + duration + “ 纳秒”);
    System.out.println(“转换为毫秒:” + duration / 1000000.0 + “ 毫秒”);
    }

}

/**

  • 测试类
    */
    public class TimeTest2 {
    public static void main(String[] args) {
    // 使用匿名内部类继承AbstractTask
    TimerUtil2.measureTime(new AbstractTask() {
    @Override
    public void run() {
    // 需要计时的业务逻辑
    System.out.println(“正在计算100以内素数…”);
    for (int i = 2; i <= 100; i++) {
    boolean isPrime = true;
    for (int j = 2; j <= Math.sqrt(i); j++) {
    if (i % j == 0) {
    isPrime = false;
    break;
    }
    }
    if (isPrime) {
    System.out.print(i + “ “);
    }
    }
    System.out.println();
    }
    });
    }
    }

AI写代码
java
运行

4.3 方式三:带返回值的方法计时

如果需要统计的方法有返回值,可以使用泛型:

/**

  • 带返回值的任务接口
  • @param 返回值类型
    */
    interface CallableTask {
    T execute();
    }

/**

  • 计时器工具类(支持返回值)
    */
    class TimerUtil3 {
    public static T measureTime(CallableTask task, String taskName) {
    System.out.println(“========== “ + taskName + “ ==========”);

    long startTime = System.currentTimeMillis();
    T result = task.execute();
    long endTime = System.currentTimeMillis();
    long duration = endTime - startTime;

    System.out.println(“执行耗时:” + duration + “ 毫秒(” + duration / 1000.0 + “ 秒)”);
    System.out.println(“================================”);

    return result;
    }

}

/**

  • 测试类
    */
    public class TimeTest3 {
    public static void main(String[] args) {
    // 计算1到100的和,并返回结果
    Integer sum = TimerUtil3.measureTime(new CallableTask() {
    @Override
    public Integer execute() {
    int total = 0;
    for (int i = 1; i <= 100; i++) {
    total += i;
    }
    System.out.println(“计算结果:” + total);
    return total;
    }
    }, “计算1-100的和”);

    System.out.println(“返回结果:” + sum);

    // 计算斐波那契数列
    TimerUtil3.measureTime(new CallableTask() {
    @Override
    public Void execute() {
    System.out.println(“斐波那契数列前20项:”);
    long a = 0, b = 1;
    for (int i = 0; i < 20; i++) {
    System.out.print(a + “ “);
    long temp = a + b;
    a = b;
    b = temp;
    }
    System.out.println();
    return null;
    }
    }, “斐波那契数列”);
    }

}

AI写代码
java
运行

五、多种时间单位说明

方法 返回单位 说明
System.currentTimeMillis() 毫秒 从1970-01-01到现在的毫秒数
System.nanoTime() 纳秒 高精度计时,适合短时间测量
以下是将时间单位转换的Java代码示例:

毫秒转秒
long ms = 1234;
double seconds = ms / 1000.0; // 结果为1.234秒
AI写代码
java
运行
纳秒转毫秒
long ns = 12345678;
double ms = ns / 1000000.0; // 结果为12.345678毫秒
AI写代码
java
运行
秒转毫秒
double seconds = 1.5;
long ms = (long)(seconds * 1000); // 结果为1500毫秒
AI写代码
java
运行
毫秒转纳秒
long ms = 15;
long ns = ms * 1000000L; // 结果为15000000纳秒
AI写代码
java
运行
注意事项:

使用浮点数除法(1000.0)确保精度不丢失
大数值计算时使用L后缀防止整数溢出
从浮点转整型时需要进行显式类型转换
六、不使用匿名内部类的对比
6.1 传统方式(单独定义一个类)
// 需要单独定义一个类
class MyTask implements Task {
@Override
public void execute() {
// 业务逻辑
}
}

// 使用时
TimerUtil.measureTime(new MyTask());
AI写代码
java
运行

6.2 使用匿名内部类的方式

TimerUtil.measureTime(new Task() {
@Override
public void execute() {
// 业务逻辑
}
});
AI写代码
java
运行
代码说明
使用匿名内部类实现Task接口
measureTime方法接收Task实例并测量其execute方法的执行时间
业务逻辑需在execute方法中实现
格式要点
代码块使用三个反引号加语言类型标记
保持原代码缩进结构
注释保留在原有位置
6.3 Lambda表达式方式(Java 8+)
// 更简洁的Lambda写法(接口只有一个抽象方法时)
TimerUtil.measureTime(() -> {
// 业务逻辑
});
AI写代码
java
运行
代码说明
使用Lambda表达式简化了匿名内部类的写法
适用于函数式接口(只有一个抽象方法的接口)
大括号内可编写需要计时的业务逻辑代码
代码块格式便于在Markdown文档中突出显示
注意事项
确保TimerUtil类已正确导入
measureTime方法需接收Runnable或Callable等函数式接口参数
Lambda表达式中的业务逻辑不应过于复杂
方式 代码量 可读性 适用场景
单独定义类 多 一般 需要多处复用
匿名内部类 中 较好 一次性使用
Lambda 少 好 接口只有一个抽象方法
七、完整示例:多个方法计时
以下是转换后的代码块格式:

/**

  • 综合示例:统计多个排序算法的执行时间
    */
    public class SortTimeTest {
    public static void main(String[] args) {
    // 准备测试数据
    int[] arr1 = generateRandomArray(10000);
    int[] arr2 = arr1.clone();

    // 测试冒泡排序时间
    TimerUtil.measureTime(new Task() {
    @Override
    public void execute() {
    bubbleSort(arr1.clone());
    System.out.println(“冒泡排序完成”);
    }
    });

    // 测试快速排序时间
    TimerUtil.measureTime(new Task() {
    @Override
    public void execute() {
    quickSort(arr2.clone(), 0, arr2.length - 1);
    System.out.println(“快速排序完成”);
    }
    });
    }

    /**

    • 生成随机数组
      */
      private static int[] generateRandomArray(int size) {
      int[] arr = new int[size];
      for (int i = 0; i < size; i++) {
      arr[i] = (int) (Math.random() * size);
      }
      return arr;
      }

    /**

    • 冒泡排序
      */
      private static void bubbleSort(int[] arr) {
      for (int i = 0; i < arr.length - 1; i++) {
      for (int j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
      int temp = arr[j];
      arr[j] = arr[j + 1];
      arr[j + 1] = temp;
      }
      }
      }
      }

    /**

    • 快速排序
      */
      private static void quickSort(int[] arr, int left, int right) {
      if (left >= right) return;
      int pivot = partition(arr, left, right);
      quickSort(arr, left, pivot - 1);
      quickSort(arr, pivot + 1, right);
      }

    private static int partition(int[] arr, int left, int right) {
    int pivot = arr[left];
    int i = left, j = right;
    while (i < j) {
    while (i < j && arr[j] >= pivot) j–;
    arr[i] = arr[j];
    while (i < j && arr[i] <= pivot) i++;
    arr[j] = arr[i];
    }
    arr[i] = pivot;
    return i;
    }

}
AI写代码
java
运行

八、总结

8.1 核心知识点
知识点 应用
匿名内部类 简化代码,一次性传入任务逻辑
接口/抽象类 定义任务规范
System.currentTimeMillis() 获取系统时间,计算耗时
模板方法模式 固定计时流程,任务逻辑可变
8.2 匿名内部类的使用场景
回调函数:如本作业的方法计时

事件监听:GUI编程中的按钮点击事件

线程创建:new Thread(new Runnable() {…})

集合排序:Collections.sort(list, new Comparator() {…})

8.3 作业完成情况
实现了方法执行时间计算功能

使用了匿名内部类

支持毫秒/纳秒两种精度

支持带返回值的场景

JAVA作业:形式化方法学习与《大象——Thinking in UML》阅读推荐

一、形式化方法概述
1.1 定义
形式化方法(Formal Methods)是一种基于数学的软件和硬件系统开发技术,采用严格的形式化规约语言描述系统,并通过数学推理 验证系统性质是否正确。

计算机科学家Edsger Dijkstra曾指出:“程序测试只能表明错误的存在,而不能表明错误的不存在。”这一论断揭示了传统测试方法的根本局限——测试无法穷尽所有可能的输入状态。形式化方法正是解决这一局限的重要途径。

1.2 软件工程 方法的分类
方法类型 特征 优势 局限性
非形式化方法 自然语言描述 通俗易懂,便于沟通 存在歧义,缺乏精确性
半形式化方法 图形化表示(如UML) 直观清晰,结构明确 语义不够精确
形式化方法 数学符号描述 精确无歧义,可验证 学习门槛高,开发成本高
1.3 形式化方法的核心构成
形式化方法由两个核心部分组成:

(1)形式化模型
具有严格数学定义的抽象模型,常见的有:

有穷自动机(Finite Automaton)

Petri网(Petri Net)

迁移系统(Transition System)

Z语言规范

(2)形式化分析
对模型进行数学验证的技术,主要包括:

技术 原理 特点
模型检查 遍历系统所有状态空间 自动化程度高,但存在状态爆炸问题
演绎证明 采用数学公理和推理规则证明 可处理无限状态,但需要人工引导
抽象解释 对程序行为进行静态近似分析 适用于大型程序,存在精度损失
1.4 主要应用领域
由于形式化方法具有较高的技术门槛和实施成本,其主要应用于对安全性、可靠性要求极高的关键系统:

航空航天:飞行控制系统、导航系统

轨道交通:列车信号控制系统

医疗设备:生命支持系统、放射治疗设备

核工业:核反应堆安全控制系统

金融交易:高频交易系统、清算系统

1.5 形式化方法的优势与局限
优势 局限
规约精确,无二义性 学习曲线陡峭
可进行数学正确性证明 开发周期较长
早期发现设计缺陷 需要专门的工具支持
提高系统可靠性和安全性 不适用于所有类型的项目
1.6 形式化方法与UML的互补关系
对比维度 形式化方法 UML
表达方式 数学符号 图形化语言
语义精确性 高(无歧义) 中等(存在解释空间)
可验证性 强(可数学证明) 弱(主要依赖评审)
学习门槛 高 中等
适用场景 安全关键系统 一般软件项目
主要用途 验证系统正确性 沟通系统设计
两者并非对立关系,而是互补关系:UML提供直观的系统视图用于团队沟通,形式化方法提供严格的语义和验证手段用于保证系统可靠性。在实践中,可将UML模型转换为形式化规约进行验证,实现优势互补。

二、《大象——Thinking in UML》阅读推荐
2.1 书籍基本信息
项目 内容
书名 《大象——Thinking in UML》
作者 谭云杰
出版社 中国水利水电出版社
版本 第2版
出版时间 2012年
豆瓣评分 8.1分
2.2 内容概述
本书并非一本简单的UML符号参考手册,而是一部系统讲解面向对象分析与设计思想的专业著作。

书名“大象”取意于“盲人摸象”的寓言——软件开发涉及需求、分析、设计、编码、测试等多个环节,参与者往往只了解局部而难以把握全貌。UML作为一种标准化 的建模语言,其价值在于帮助开发团队建立对软件系统的完整、一致的理解。

2.3 全书结构
部分 主题 核心内容
准备篇 面向对象基础 面向对象基本概念、建模原理
基础篇 UML核心元素 用例、类、接口、组件等及相互关系
进阶篇 统一开发过程 需求获取、系统分析、架构设计
总结篇 深入探讨 常见疑难问题与实践经验
2.4 核心价值
(1)培养面向对象思维方式

许多学习者虽然掌握了Java等语言的语法,但习惯于用面向对象 的语言编写面向过程的代码。本书从认知层面帮助读者理解:

何为抽象?

如何从业务领域中识别对象?

如何建立类之间的合理关系?

(2)建立用例驱动的开发流程

书中系统阐述了用例驱动的开发方法论:

用例作为分析单元

用例作为设计单元

用例作为开发单元

用例作为测试单元

(3)理论与实践相结合

区别于仅介绍UML符号的教材,本书以实际项目为背景,完整展示了从需求获取到架构设计 的过程,具有较强的实践指导意义。

(4)语言通俗,降低学习门槛

本书写作风格贴近开发者实际认知,避免了学术化的晦涩表达,适合初学者系统学习。

2.5 适用读者
读者群体 推荐程度
已完成Java基础学习的学生 ★★★★★ 强烈推荐
正在学习UML建模的读者 ★★★★★ 推荐
初次参与软件项目的开发者 ★★★★★ 推荐
希望提升设计能力的初级工程师 ★★★★☆ 推荐
2.6 阅读建议
建议按以下顺序阅读:

第一遍通读:把握全书框架,理解核心思想,不纠结技术细节

第二遍精读:重点研读用例图、类图、时序图等相关章节

边读边实践:选取一个小型项目(如图书管理系统),同步进行建模练习

2.7 关联参考
参考书目 推荐理由
《UML用户指南》(Grady Booch) UML创始人著作,经典权威
《形式化方法导论》(张广泉) 清华教材,系统学习形式化方法
《设计模式:可复用面向对象软件的基础》(GoF) 面向对象设计进阶读物
三、总结
本次作业的主要收获如下:

理解形式化方法:形式化方法以数学为理论基础,能够精确描述系统行为并验证其正确性,但实施成本较高,主要应用于安全关键领域。

UML的定位与价值:作为半形式化方法,UML通过图形化方式表达软件设计,在直观性与规范性之间取得平衡,是当前工业界主流的建模工具。

理论与实践的关系:形式化方法提供理论严谨性,UML提供实践可操作性,二者相互补充。

《大象——Thinking in UML》的阅读价值:该书有助于建立面向对象思维方式、掌握UML建模方法、提升软件设计能力,适合作为UML入门的首选读物。

第五章 继承与接口

一、本章知识回顾

知识点 说明
继承 子类继承父类的成员变量和方法
方法重写 子类重新定义父类的方法
super关键字 访问父类的成员变量、方法和构造方法
多态 父类引用指向子类对象
final关键字 防止继承、防止重写、定义常量
抽象类 包含抽象方法的类,不能实例化
接口 完全抽象的规范,支持多实现
二、为什么要继承?

2.1 问题引入

假设要设计学生和教师两个类:

学生类:姓名、年龄、学号、学习()

教师类:姓名、年龄、工号、教学()

你会发现,学生和教师都有相同的属性(姓名、年龄)。如果每个类都重复写一遍,代码会非常冗余。

继承的解决方案:把共同的特征提取到“人类”中,让学生和教师继承这个类。

2.2 继承的概念

  • 父类(超类):被继承的类
  • 子类(派生类):继承父类的类
  • 关键字:extends

// 父类
class Person {
String name;
int age;

void eat() {
    System.out.println(name + "正在吃饭");
}

}

// 子类继承父类
class Student extends Person {
String studentId; // 子类特有的属性

void study() {   // 子类特有的方法
    System.out.println(name + "正在学习");
}

}

AI写代码
java
运行

2.3 继承的好处

好处 说明
代码复用 子类自动拥有父类的属性和方法
易于维护 修改父类即可影响所有子类
体现is-a关系 Student is a Person(学生是一个人)
AI写代码
markdown
三、子类与父类

3.1 继承的基本语法

class 子类名 extends 父类名 {
// 子类特有的成员变量
// 子类特有的成员方法
}
AI写代码
java
运行
3.2 继承示例:银行账户

代码解析
/**

  • 父类:银行账户
    */
    class BankAccount {
    String accountNo; // 账号
    double balance; // 余额

    // 构造方法
    BankAccount(String accountNo, double balance) {
    this.accountNo = accountNo;
    this.balance = balance;
    }

    // 存款
    void deposit(double amount) {
    balance += amount;
    System.out.println(“存入 “ + amount + “ 元,余额:” + balance);
    }

    // 取款
    void withdraw(double amount) {
    if (balance >= amount) {
    balance -= amount;
    System.out.println(“取出 “ + amount + “ 元,余额:” + balance);
    } else {
    System.out.println(“余额不足!”);
    }
    }

    void showInfo() {
    System.out.println(“账号:” + accountNo + “,余额:” + balance);
    }

}

/**

  • 子类:储蓄账户(继承BankAccount)

  • 特点:有利息
    */
    class SavingsAccount extends BankAccount {
    double interestRate; // 子类特有的属性:利率

    SavingsAccount(String accountNo, double balance, double interestRate) {
    super(accountNo, balance); // 调用父类构造方法
    this.interestRate = interestRate;
    }

    // 子类特有的方法:计算利息
    void addInterest() {
    double interest = balance * interestRate;
    balance += interest;
    System.out.println(“增加利息:” + interest + “ 元,余额:” + balance);
    }

}

public class InheritanceDemo {
public static void main(String[] args) {
System.out.println(“=== 普通账户 ===”);
BankAccount acc1 = new BankAccount(“1001”, 1000);
acc1.deposit(500);
acc1.withdraw(200);

    System.out.println("\n=== 储蓄账户 ===");
    SavingsAccount acc2 = new SavingsAccount("2001", 2000, 0.03);
    acc2.deposit(300);
    acc2.addInterest();
}

}
AI写代码
java
运行

运行结果
=== 普通账户 ===
存入 500.0 元,余额:1500.0
取出 200.0 元,余额:1300.0

=== 储蓄账户 ===
存入 300.0 元,余额:2300.0
增加利息:69.0 元,余额:2369.0
AI写代码
关键点说明
父类 BankAccount 定义了银行账户的基本属性和方法,包括账号、余额、存款、取款和显示账户信息。

子类 SavingsAccount 继承了 BankAccount,并增加了特有的属性 interestRate 和方法 addInterest(),用于计算并添加利息。

在 main 方法中,分别创建了普通账户和储蓄账户的实例,演示了存款、取款和利息计算的功能。
四、方法重写(Override)

4.1 什么是方法重写?

子类可以重新定义(覆盖)父类中已有的方法,这叫做方法重写

重写规则:

  • 方法名相同
  • 参数列表相同
  • 返回值类型相同(或是其子类)
  • 访问权限不能比父类更严格

4.2 示例:信用卡账户(重写取款方法)

信用卡账户实现代码
/**

  • 子类:信用卡账户(继承BankAccount)

  • 特点:可以透支
    */
    class CreditAccount extends BankAccount {
    double creditLimit; // 透支额度

    CreditAccount(String accountNo, double balance, double creditLimit) {
    super(accountNo, balance);
    this.creditLimit = creditLimit;
    }

    // 重写取款方法(支持透支)
    @Override
    void withdraw(double amount) {
    if (balance + creditLimit >= amount) {
    balance -= amount;
    System.out.println(“取出 “ + amount + “ 元,余额:” + balance);
    } else {
    System.out.println(“超过透支额度!”);
    }
    }

}

public class OverrideDemo {
public static void main(String[] args) {
CreditAccount acc = new CreditAccount(“3001”, 0, 5000);
acc.withdraw(2000); // 可以透支
acc.withdraw(4000); // 超出额度
}
}
AI写代码
java
运行

运行结果示例
取出 2000.0 元,余额:-2000.0
超过透支额度!
AI写代码
text
代码说明
信用卡账户类继承自银行账户基类,通过重写取款方法实现透支功能。当取款金额不超过账户余额与透支额度之和时允许交易,否则拒绝交易并提示超额。

构造方法接收账户号、初始余额和透支额度三个参数,初始化时需调用父类构造方法。透支额度作为信用卡账户特有的属性单独存储。

4.3 @Override注解

@Override是可选的,但推荐加上:

  • 让编译器 帮忙检查是否正确的重写了父类方法
  • 提高代码可读性

4.4 重写 vs 重载

方法重写(Override)与方法重载(Overload)对比
比较项 方法重写(Override) 方法重载(Overload)
发生位置 子类和父类之间 同一个类中
方法名 必须相同 必须相同
参数列表 必须相同 必须不同
返回值 相同或子类(协变返回类型) 无关
访问修饰符 不能比父类更严格(如父类为 protected,子类可为 public,但不能为 private) 无限制
异常抛出 不能抛出比父类更宽泛的检查异常 无限制
作用 实现多态,子类提供特定实现 提供同名方法的不同参数版本
关键区别说明
方法重写(Override)

发生在继承关系中,子类重新定义父类的方法。
方法签名(名称和参数列表)必须完全一致。
返回类型可以是原类型的子类型(协变返回)。
访问权限不能比父类方法更严格。
方法重载(Overload)

发生在同一类中,通过不同参数列表区分同名方法。
返回值类型、访问修饰符和异常声明可自由变化。
仅需参数列表不同(类型、顺序或数量)。
示例代码
方法重写示例
class Parent {
protected void show() {
System.out.println(“Parent’s show”);
}
}

class Child extends Parent {
@Override
public void show() { // 访问修饰符更宽松,参数列表相同
System.out.println(“Child’s show”);
}
}
AI写代码
java
运行

方法重载示例
class Calculator {
int add(int a, int b) {
return a + b;
}

double add(double a, double b) {  // 参数类型不同
    return a + b;
}

int add(int a, int b, int c) {  // 参数数量不同
    return a + b + c;
}

}
AI写代码
java
运行

五、super关键字

5.1 super的作用

super用于在子类中访问父类的成员。

表格转换结果
用法 说明
super.成员变量 访问父类成员变量
super.成员方法() 调用父类成员方法
super(参数) 调用父类构造方法(必须在第一行)
注意事项
表格已按照原始内容格式转换,保持清晰对齐
每列内容准确对应原始数据
表格边框采用标准Markdown表格语法
列宽根据内容自动调整
5.2 示例:电脑

代码解析
/**

  • 父类:电脑
    */
    class Computer {
    String cpu;
    int ram;

    Computer(String cpu, int ram) {
    this.cpu = cpu;
    this.ram = ram;
    }

    void showSpec() {
    System.out.println(“CPU:” + cpu + “,内存:” + ram + “GB”);
    }

}

/**

  • 子类:游戏电脑
    */
    class GamingComputer extends Computer {
    String gpu; // 显卡

    GamingComputer(String cpu, int ram, String gpu) {
    super(cpu, ram); // 调用父类构造方法
    this.gpu = gpu;
    }

    @Override
    void showSpec() {
    super.showSpec(); // 调用父类的方法
    System.out.println(“显卡:” + gpu);
    }

}

public class SuperDemo {
public static void main(String[] args) {
GamingComputer gc = new GamingComputer(“i7-13700K”, 32, “RTX 4080”);
gc.showSpec();
}
}
AI写代码
java
运行

运行结果
CPU:i7-13700K,内存:32GB
显卡:RTX 4080
AI写代码
关键点说明
super关键字用于在子类中访问父类的成员变量或方法。在构造方法中,super()用于调用父类的构造方法,必须放在子类构造方法的第一行。

方法重写时,使用super.方法名()可以调用父类被重写的方法,实现功能的扩展而非完全覆盖。

继承关系中,子类可以继承父类的非私有成员,同时可以添加自己的特有成员,形成更具体的类。

5.3 注意事项

子类构造方法必须先调用父类构造方法

class Parent {
Parent(String name) { // 有参构造
System.out.println(“Parent: “ + name);
}
}

// 正确写法
class Child extends Parent {
Child(String name) {
super(name); // 必须手动调用父类有参构造
}
}
AI写代码
java
运行

六、多态

6.1 什么是多态?

多态:同一个方法调用,作用于不同的对象,产生不同的行为。

实现多态的三个条件:

  1. 有继承关系
  2. 子类重写父类的方法
  3. 父类引用指向子类对象6.2 多态示例

/*** 父类:动物*/
class Animal {
void sound() {
System.out.println(“动物发出声音”);
}
}

/*** 子类:狗*/
class Dog extends Animal {
@Override
void sound() {
System.out.println(“狗:汪汪汪!”);
}
}

/*** 子类:猫*/
class Cat extends Animal {
@Override
void sound() {
System.out.println(“猫:喵喵喵!”);
}
}

public class PolymorphismDemo {
public static void main(String[] args) {
// 多态:父类引用指向子类对象
Animal animal1 = new Dog();
Animal animal2 = new Cat();

    // 同一个方法调用,产生不同行为
    animal1.sound(); // 狗:汪汪汪!
    animal2.sound(); // 猫:喵喵喵!
    
    // 多态的方便之处:用一个数组存放多种动物
    Animal[] animals = {new Dog(), new Cat(), new Dog()};
    for (Animal animal : animals) {
        animal.sound(); // 不需要知道具体是哪种动物
    }
}

}

AI写代码
java
运行

6.3 对象类型转换

// 向上转型(自动,安全)
Animal animal = new Dog();

// 向下转型(强制,有风险)
Dog dog = (Dog) animal;

// instanceof 判断类型(避免转型异常)
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
}
AI写代码
java
运行

七、final关键字

7.1 final的三种用法

修饰符作用示例表
修饰符 作用 示例
final 类 不能被继承 final class String
final 方法 不能被重写 public final void print()
final 变量 只能赋值一次(常量) final double PI = 3.14
7.2 示例

// 1. final变量:常量
class Constants {
final double PI = 3.14159; // 声明时初始化
void test() {
// PI = 3.14; // 编译错误:不能修改
}
}

// 2. final方法:子类不能重写
class Parent {
final void finalMethod() {
System.out.println(“这个方法不能被重写”);
}
}

// 3. final类:不能有子类
final class FinalClass {
// 类体
}
// class SubClass extends FinalClass {} // 编译错误

AI写代码
java
运行

八、抽象类

8.1 什么是抽象类?

当一个父类只知道子类应该有什么方法,但不知道如何实现时,使用抽象类。

  • abstract 关键字修饰
  • 抽象方法没有方法体
  • 抽象类不能实例化(不能 new)

8.2 示例:图形面积计算

/**

  • 抽象类:图形
    */
    abstract class Shape {
    String color;

    Shape(String color) {
    this.color = color;
    }

    // 抽象方法:只有声明,没有实现
    abstract double getArea();
    abstract double getPerimeter();

    // 普通方法(可以被子类继承)
    void showColor() {
    System.out.println(“颜色:” + color);
    }

}

/**

  • 子类:圆形
    */
    class Circle extends Shape {
    double radius;

    Circle(String color, double radius) {
    super(color);
    this.radius = radius;
    }

    @Override
    double getArea() {
    return Math.PI * radius * radius;
    }

    @Override
    double getPerimeter() {
    return 2 * Math.PI * radius;
    }

}

/**

  • 子类:矩形
    */
    class Rectangle extends Shape {
    double width;
    double height;

    Rectangle(String color, double width, double height) {
    super(color);
    this.width = width;
    this.height = height;
    }

    @Override
    double getArea() {
    return width * height;
    }

    @Override
    double getPerimeter() {
    return 2 * (width + height);
    }

}

public class AbstractDemo {
public static void main(String[] args) {
// Shape shape = new Shape(“红色”); // 错误:抽象类不能实例化

    Shape circle = new Circle("红色", 5);
    Shape rectangle = new Rectangle("蓝色", 4, 6);
    
    System.out.println("=== 圆形 ===");
    circle.showColor();
    System.out.println("面积:" + circle.getArea());
    System.out.println("周长:" + circle.getPerimeter());
    
    System.out.println("\n=== 矩形 ===");
    rectangle.showColor();
    System.out.println("面积:" + rectangle.getArea());
    System.out.println("周长:" + rectangle.getPerimeter());
}

}

AI写代码
java
运行

抽象类要点:

  • 含有抽象方法的类必须是抽象类
  • 抽象类可以没有抽象方法
  • 子类必须实现所有抽象方法,否则子类也是抽象类

九、接口

9.1 什么是接口?

接口是完全抽象的规范,定义了“能做什么”,但不关心“怎么做”。

  • interface 关键字定义
  • implements 关键字实现

9.2 接口的特性

成员类型与默认修饰符对照表
成员类型 默认修饰符 说明
常量 public static final 接口中定义的变量自动是常量
抽象方法 public abstract 接口中的方法自动是抽象的
9.3 示例:USB接口

代码解析
/**

  • USB接口规范
    */
    interface USB {
    // 常量(自动是 public static final)
    String STANDARD = “USB 3.0”;

    // 抽象方法(自动是 public abstract)
    void plugIn(); // 插入
    void work(); // 工作
    void plugOut(); // 拔出

}

/**

  • 鼠标实现USB接口
    */
    class Mouse implements USB {
    @Override
    public void plugIn() {
    System.out.println(“鼠标已插入”);
    }

    @Override
    public void work() {
    System.out.println(“鼠标:移动光标、点击”);
    }

    @Override
    public void plugOut() {
    System.out.println(“鼠标已拔出”);
    }

}

/**

  • 键盘实现USB接口
    */
    class Keyboard implements USB {
    @Override
    public void plugIn() {
    System.out.println(“键盘已插入”);
    }

    @Override
    public void work() {
    System.out.println(“键盘:输入文字”);
    }

    @Override
    public void plugOut() {
    System.out.println(“键盘已拔出”);
    }

}

public class InterfaceDemo {
public static void main(String[] args) {
USB mouse = new Mouse();
USB keyboard = new Keyboard();

    System.out.println("=== 鼠标 ===");
    mouse.plugIn();
    mouse.work();
    mouse.plugOut();
    
    System.out.println("\n=== 键盘 ===");
    keyboard.plugIn();
    keyboard.work();
    keyboard.plugOut();
    
    System.out.println("\nUSB标准:" + USB.STANDARD);
}

}
AI写代码
java
运行

运行结果
代码执行后的输出结果如下:

=== 鼠标 ===
鼠标已插入
鼠标:移动光标、点击
鼠标已拔出

=== 键盘 ===
键盘已插入
键盘:输入文字
键盘已拔出

USB标准:USB 3.0
AI写代码

关键点说明
接口USB定义了三个抽象方法和一个常量。Mouse和Keyboard类实现了USB接口,并提供了具体的方法实现。

在InterfaceDemo类的main方法中,创建了Mouse和Keyboard的实例,并通过接口类型引用调用各自的方法。最后打印了接口中定义的常量STANDARD。

该示例展示了Java接口的基本特性,包括抽象方法的定义、实现类的编写以及接口常量的使用。

9.4 接口的多实现

一个类可以实现多个接口,这是Java实现多继承的方式。

interface A {
void methodA();
}

interface B {
void methodB();
}

class MultipleImpl implements A, B {
@Override
public void methodA() {
System.out.println(“实现A的方法”);
}

@Override
public void methodB() {
System.out.println(“实现B的方法”);
}
}

AI写代码
java
运行

9.5 接口与抽象类的对比

抽象类与接口对比
比较项 抽象类 接口
关键字 abstract class interface
继承/实现 extends implements
多继承 不支持 支持
成员变量 可以有各种类型 只能是常量
构造方法 有 没有
普通方法 可以有 只有抽象方法(Java 8前)
使用场景 有共同代码的基类 定义规范/能力
注:Java 8及以后版本允许接口包含默认方法(default方法)和静态方法。

十、综合示例:教材“银行与利息”案例

/*** 抽象类:银行账户*/
abstract class BankAccount {
String accountNo;
double balance;

BankAccount(String accountNo, double balance) {
    this.accountNo = accountNo;
    this.balance = balance;
}

// 抽象方法:计算利息
abstract double calculateInterest();

void showInfo() {
    System.out.println("账号:" + accountNo + ",余额:" + balance);
    System.out.println("利息:" + calculateInterest());
}

}

/*** 定期账户*/
class FixedAccount extends BankAccount {
double rate; // 利率

FixedAccount(String accountNo, double balance, double rate) {
    super(accountNo, balance);
    this.rate = rate;
}

@Override
double calculateInterest() {
    return balance * rate;
}

}

/*** 活期账户*/
class CurrentAccount extends BankAccount {
double rate;
int days; // 存款天数

CurrentAccount(String accountNo, double balance, double rate, int days) {
    super(accountNo, balance);
    this.rate = rate;
    this.days = days;
}

@Override
double calculateInterest() {
    return balance * rate * days / 365;
}

}

public class BankDemo {
public static void main(String[] args) {
BankAccount account1 = new FixedAccount(“1001”, 10000, 0.035);
BankAccount account2 = new CurrentAccount(“2001”, 10000, 0.003, 180);

    System.out.println("=== 定期账户 ===");
    account1.showInfo();
    
    System.out.println("\n=== 活期账户 ===");
    account2.showInfo();
}

}

AI写代码
java
运行

十一、常见错误与注意点

常见Java继承与接口错误对照表
错误写法 正确写法 关键说明
class Child extends Parent1, Parent2 class Child implements Interface1, Interface2 Java单继承限制,类只能继承一个父类,但可实现多个接口
子类构造方法未调用super() 显式调用super(参数) 父类不存在默认构造器时,子类必须显式调用父类构造器
误用@Override注解 保持方法签名完全一致 包括方法名、参数列表、返回类型,否则触发编译错误
抽象类遗漏抽象方法声明 使用abstract修饰无实现的方法 抽象方法必须以分号结尾,不能包含方法体
接口实现类未声明public 显式添加public修饰符 接口方法默认public,但实现类必须显式声明为public
典型问题深度解析
多继承限制 Java通过单继承+多接口实现解决菱形继承问题。当需要复用多个父类功能时,应改用组合模式或接口默认方法。

构造器链规则 子类构造器必须调用父类构造器,若未显式调用且父类无默认构造器,编译器报错。调用语句必须作为构造器首行代码。

注解校验机制 @Override严格校验重写规范,包括协变返回类型等细节。现代IDE通常能实时提示签名不匹配问题。

抽象方法特征 抽象类可以包含具体方法,但抽象方法必须由子类实现。注意与接口方法的区别:抽象方法可受保护,接口方法始终公开。

访问控制一致性 接口方法默认公开且不可降低可见性。实现类若省略public修饰符会导致访问权限收缩,违反里氏替换原则。

十二、小结

本章学习了:

  • 继承:子类继承父类,实现代码复用
    -方法重写:子类重新定义父类的方法
  • super关键字:访问父类的成员
  • 多态:父类引用指向子类对象
  • final关键字:防止继承、防止重写、定义常量
  • 抽象类:包含抽象方法的类,不能实例化
  • 接口:完全抽象的规范,支持多实现

第四章 类与对象——面向对象编程入门

一、本章知识回顾

面向对象基础概念表格
知识点 说明
类 对象的模板,描述一类事物的共同特征
对象 类的具体实例
成员变量 对象的属性(状态)
成员方法 对象的行为(功能)
构造方法 创建对象时初始化成员变量
this关键字 指向当前对象的引用
对象的组合 一个类把其他类的对象作为成员

二、为什么需要类与对象?

2.1 问题引入

假设要处理小明养的两只狗的信息:

狗名 年龄 颜色
小白 3岁 白色
小花 10岁 花色

表格转换结果
狗名 年龄 颜色
小白 3岁 白色
小花 10岁 花色
如果用之前学过的数组来处理:

String[] dog1 = {“小白”, “3”, “白色”};
String[] dog2 = {“小花”, “10”, “花色”};

AI写代码
java
运行
数组方式的问题:

数据类型不明确(年龄”3”是字符串,不方便计算)

通过下标获取信息(dog1[0]是名字,dog1[1]是年龄,容易混淆)

不能体现狗的行为(如”叫”、”跑”等)

解决方式:用类来定义狗的特征和行为,用对象来表示具体的某只狗。

三、类的定义
3.1 类的基本结构
类是Java中最基本的”模板”,用来描述一类事物的属性(成员变量)和行为(成员方法)。

java

class Dog {
// 1. 成员变量(属性)
String name; // 名字
int age; // 年龄
String color; // 颜色

// 2. 成员方法(行为)
void bark() {
    System.out.println(name + ":汪汪汪!");
}

void run() {
    System.out.println(name + "正在奔跑");
}

void showInfo() {
    System.out.println("名字:" + name + ",年龄:" + age + "岁,颜色:" + color);
}

}
3.2 成员变量
位置:在类中、方法外部定义

作用:描述对象的属性(状态)

默认值:即使不赋值,也有默认初始值

类型 默认值
int 0
double 0.0
boolean false
char ‘\u0000’
引用类型(String等) null
3.3 成员方法
位置:在类中定义

作用:描述对象的行为(功能)

结构:修饰符 + 返回值类型 + 方法名 + 参数列表 + 方法体

java

// 方法结构示例
public void bark() { // 无返回值,无参数
System.out.println(“叫”);
}

public int add(int a, int b) { // 有返回值,有参数
return a + b;
}
四、对象的创建与使用
4.1 创建对象(实例化)
使用 new 关键字 + 构造方法创建对象:

java

public class DogTest {
public static void main(String[] args) {
// 创建Dog类的对象
Dog dog1 = new Dog(); // 声明 + 创建 + 赋值

    // 或者分两步写
    Dog dog2;              // 1.声明对象变量
    dog2 = new Dog();      // 2.创建对象(分配内存)
}

}
4.2 使用对象
通过 对象名.成员变量 和 对象名.成员方法() 来访问:

java

public class DogTest {
public static void main(String[] args) {
// 创建对象
Dog dog1 = new Dog();
Dog dog2 = new Dog();

    // 给成员变量赋值
    dog1.name = "小白";
    dog1.age = 3;
    dog1.color = "白色";
    
    dog2.name = "小花";
    dog2.age = 10;
    dog2.color = "花色";
    
    // 调用成员方法
    dog1.showInfo();   // 输出:名字:小白,年龄:3岁,颜色:白色
    dog1.bark();       // 输出:小白:汪汪汪!
    
    dog2.showInfo();   // 输出:名字:小花,年龄:10岁,颜色:花色
    dog2.run();        // 输出:小花正在奔跑
}

}
运行结果:

text

名字:小白,年龄:3岁,颜色:白色
小白:汪汪汪!
名字:小花,年龄:10岁,颜色:花色
小花正在奔跑
4.3 类与对象的关系图
text

┌─────────────────────────────────────┐
│ 类(模板) │
│ ┌─────────────────────────────────┐ │
│ │ class Dog { │ │
│ │ String name; // 成员变量 │ │
│ │ int age; │ │
│ │ void bark() {…} // 方法 │ │
│ │ } │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘

│ 实例化(new)

┌───────────────────────┐ ┌───────────────────────┐
│ 对象1(实例1) │ │ 对象2(实例2) │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ name = “小白” │ │ │ │ name = “小花” │ │
│ │ age = 3 │ │ │ │ age = 10 │ │
│ │ color = “白色” │ │ │ │ color = “花色” │ │
│ └─────────────────┘ │ │ └─────────────────┘ │
└───────────────────────┘ └───────────────────────┘
核心理解:

类:图纸(抽象)

对象:根据图纸造出的房子(具体)

同一个类可以创建多个对象,每个对象有自己的属性值

五、构造方法
5.1 什么是构造方法?
构造方法是专门用来创建对象时初始化成员变量的特殊方法。

特点:

方法名与类名相同

没有返回值类型(连void都不写)

创建对象时自动调用

5.2 无参构造方法
java

class Dog {
String name;
int age;

// 无参构造方法
Dog() {
    System.out.println("一只新的狗被创建了");
}

}

public class Test {
public static void main(String[] args) {
Dog dog = new Dog(); // 自动调用构造方法,输出:一只新的狗被创建了
}
}
5.3 有参构造方法
java

class Dog {
String name;
int age;
String color;

// 有参构造方法
Dog(String n, int a, String c) {
    name = n;
    age = a;
    color = c;
    System.out.println("创建了:" + name);
}

void showInfo() {
    System.out.println("名字:" + name + ",年龄:" + age + ",颜色:" + color);
}

}

public class Test {
public static void main(String[] args) {
// 创建对象时直接传入参数
Dog dog1 = new Dog(“小白”, 3, “白色”);
Dog dog2 = new Dog(“小花”, 10, “花色”);

    dog1.showInfo();  // 名字:小白,年龄:3,颜色:白色
    dog2.showInfo();  // 名字:小花,年龄:10,颜色:花色
}

}
5.4 注意事项
情况 说明
没有定义任何构造方法 系统自动提供无参构造方法
定义了有参构造方法 系统不再提供无参构造方法
想同时使用两种 需要重载构造方法(手动写两个)
六、this关键字
6.1 解决命名冲突
当成员变量和局部变量(包括参数)同名时,this.成员变量 表示成员变量。

java

class Dog {
String name;
int age;

// 参数名和成员变量名相同
Dog(String name, int age) {
    this.name = name;  // this.name 是成员变量,name 是参数
    this.age = age;
}

void showInfo() {
    System.out.println("名字:" + name + ",年龄:" + age);
}

}
为什么需要this?

如果不写this,会发生”局部变量覆盖成员变量”:

java

// 错误写法
Dog(String name, int age) {
name = name; // 左边和右边都是参数name,成员变量name没有被赋值
age = age; // 同样的问题
}
6.2 this的其他用法
java

class Dog {
String name;

Dog() {
    this("无名");  // 调用本类的有参构造方法
}

Dog(String name) {
    this.name = name;
}

}
七、对象的组合
一个类可以把其他类的对象作为自己的成员变量,这叫对象的组合。

示例:人养狗
java

// 狗类
class Dog {
String name;
int age;

Dog(String name, int age) {
    this.name = name;
    this.age = age;
}

void bark() {
    System.out.println(name + ":汪汪!");
}

}

// 人类(组合了Dog类)
class Person {
String name;
Dog pet; // 把Dog对象作为成员变量

Person(String name, Dog pet) {
    this.name = name;
    this.pet = pet;
}

void walkPet() {
    System.out.println(name + "正在遛 " + pet.name);
    pet.bark();
}

void showInfo() {
    System.out.print(name + "的宠物:");
    System.out.println("名字:" + pet.name + ",年龄:" + pet.age + "岁");
}

}

public class Test {
public static void main(String[] args) {
// 先创建狗
Dog dog = new Dog(“小白”, 3);

    // 再创建人(把狗作为参数传入)
    Person person = new Person("小明", dog);
    
    person.showInfo();   // 小明的宠物:名字:小白,年龄:3岁
    person.walkPet();    // 小明正在遛小白 \n 小白:汪汪!
}

}
运行结果:

text

小明的宠物:名字:小白,年龄:3岁
小明正在遛小白
小白:汪汪!
八、完整示例:有理数类(教材例题)
根据教材第4章的有理数类例题,以下是一个完整的封装示例:

java

/**

  • 有理数类(封装分数)

  • 成员变量:分子、分母

  • 成员方法:加法、减法、乘法、除法、化简
    */
    class Rational {
    int numerator; // 分子
    int denominator; // 分母

    // 构造方法
    Rational(int numerator, int denominator) {
    this.numerator = numerator;
    this.denominator = denominator;
    reduce(); // 创建时自动化简
    }

    // 求最大公约数(用于化简)
    int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
    }

    // 化简分数
    void reduce() {
    int g = gcd(numerator, denominator);
    numerator /= g;
    denominator /= g;
    }

    // 加法
    Rational add(Rational r) {
    int newNum = this.numerator * r.denominator + r.numerator * this.denominator;
    int newDen = this.denominator * r.denominator;
    return new Rational(newNum, newDen);
    }

    // 减法
    Rational subtract(Rational r) {
    int newNum = this.numerator * r.denominator - r.numerator * this.denominator;
    int newDen = this.denominator * r.denominator;
    return new Rational(newNum, newDen);
    }

    // 输出分数
    void display() {
    if (denominator == 1) {
    System.out.println(numerator);
    } else {
    System.out.println(numerator + “/“ + denominator);
    }
    }

}

public class RationalTest {
public static void main(String[] args) {
Rational r1 = new Rational(1, 3); // 1/3
Rational r2 = new Rational(2, 5); // 2/5

    System.out.print("1/3 + 2/5 = ");
    r1.add(r2).display();                // 11/15
    
    System.out.print("1/3 - 2/5 = ");
    r1.subtract(r2).display();           // -1/15
}

}
九、常见错误与注意点
错误写法 正确写法 说明
Dog dog; dog.name = “小白”; Dog dog = new Dog(); 必须先创建对象才能使用
class dog { … } class Dog { … } 类名首字母大写(规范)
构造方法写了返回值类型 Dog() { }(无void) 构造方法不能有返回值
this 在静态方法中使用 只能在非静态方法中使用 static方法中不能用this
忘记写构造方法导致无法初始化 提供合适的构造方法 或使用setter方法赋值
十、小结
本章学习了:

类的定义:成员变量 + 成员方法

对象的创建:使用 new 关键字

构造方法:与类同名,无返回值,用于初始化

this关键字:区分成员变量和局部变量

对象的组合:一个类包含其他类的对象

核心代码模板:

java

class 类名 {
// 1. 成员变量
数据类型 变量名;

// 2. 构造方法
类名(参数列表) {
    this.成员变量 = 参数;
}

// 3. 成员方法
返回值类型 方法名(参数列表) {
    // 方法体
}

}

// 使用
类名 对象名 = new 类名(参数);
对象名.方法名();

JAVA作业:素数查找——用循环和分支解决问题

一、问题引入

什么是素数?

素数(质数):大于1的自然数,除了1和它本身以外,不能被其他自然数整除。

例如:

  • 7是素数(只能被1和7整除)
  • 8不是素数(还能被2和4整除)

二、判断一个数是否为素数

2.1 核心思路

要判断一个数num是否为素数,只需要检查:

  • 从2开始,到num-1结束
  • 用每个数去除num
  • 如果发现任何一个能整除,说明num不是素数

2.2 基础代码

public class IsPrime {
    public static void main(String[] args) {
        int num = 17;
        boolean isPrime = true;  // 先假设是素数
        
        // for循环:从2检查到num-1
        for (int i = 2; i < num; i++) {
            if (num % i == 0) {      // 如果能整除
                isPrime = false;      // 不是素数
                break;                // 提前结束循环
            }
        }
        
        if (isPrime) {
            System.out.println(num + "是素数");
        } else {
            System.out.println(num + "不是素数");
        }
    }
}
2.3 代码逐行解释
代码	知识点	说明
for (int i = 2; i < num; i++)	for循环	初始化i=2;条件i<num;每次i+1
num % i	算术运算符%	取余运算,得到num除以i的余数
num % i == 0	关系运算符==	判断余数是否等于0
if (num % i == 0)	if条件语句	条件成立时执行
isPrime = false	赋值运算符=	把false赋值给isPrime
break	跳转语句	跳出整个循环
2.4 程序流程图
text

开始
  ↓
输入num = 17
  ↓
假设isPrime = true
  ↓
i从2开始 ──────→ i=2
  ↓              ↓
i < num? ──→ 2<17? 是
  ↓              ↓
num % i == 0?  17%2==1? 否
  ↓              ↓
i++ → i=3       (继续循环)
  ↓
... 一直检查到i=16
  ↓
i=17时,17<17? 否,退出循环
  ↓
isPrime仍为true
  ↓
输出"17是素数"
  ↓
结束
三、找出某个范围内的所有素数
3.1 需求
找出101到200之间所有的素数。

3.2 思路
外层循环:遍历101到200的每个数

内层循环:判断当前数是否为素数

计数器:统计素数个数

3.3 完整代码
java

public class FindPrimes {
    public static void main(String[] args) {
        int start = 101;
        int end = 200;
        int count = 0;
        
        System.out.println(start + "到" + end + "之间的素数有:");
        
        // 外层循环:遍历每个要判断的数
        for (int num = start; num <= end; num++) {
            
            boolean isPrime = true;  // 假设当前数是素数
            
            // 内层循环:检查是否有因数
            // 只需要检查到 num-1
            for (int i = 2; i < num; i++) {
                if (num % i == 0) {
                    isPrime = false;  // 找到因数,不是素数
                    break;            // 跳出内层循环
                }
            }
            
            // 如果是素数,输出并计数
            if (isPrime) {
                System.out.print(num + " ");
                count++;
            }
        }
        
        System.out.println("\n共有 " + count + " 个素数");
    }
}
3.4 运行结果
text

101到200之间的素数有:
101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 
共有 21 个素数
四、优化:平方根优化
4.1 为什么要优化?
上面的代码中,判断一个数num,需要检查2到num-1。如果num=199,需要检查197次,效率很低。

4.2 优化原理
如果num不是素数,那么它一定可以写成:num = a × b

其中a和b中,至少有一个 ≤ √num

因此,我们只需要检查到√num就可以了。

4.3 优化后的代码
java

public class FindPrimesOptimized {
    public static void main(String[] args) {
        int start = 101;
        int end = 200;
        int count = 0;
        
        System.out.println(start + "到" + end + "之间的素数有:");
        
        for (int num = start; num <= end; num++) {
            if (isPrime(num)) {
                System.out.print(num + " ");
                count++;
            }
        }
        
        System.out.println("\n共有 " + count + " 个素数");
    }
    
    /**
     * 判断素数(平方根优化版)
     */
    public static boolean isPrime(int num) {
        // 小于2的不是素数
        if (num < 2) {
            return false;
        }
        
        // 只需要检查到平方根
        for (int i = 2; i <= Math.sqrt(num); i++) {
            if (num % i == 0) {
                return false;  // 有因数,不是素数
            }
        }
        return true;  // 没有因数,是素数
    }
}
4.4 优化效果对比
方法	判断199需要检查的次数
原始方法(检查到num-1)	197次
平方根优化(检查到√199≈14)	13次
快了约15倍!

五、常见错误提醒
错误写法	正确写法	说明
if (num % i = 0)	if (num % i == 0)	判断相等要用==,单个=是赋值
for (int i=2; i<num; i++) 不加大括号	建议加大括号 {}	循环体多条语句时需要大括号
忘记写break	找到因数后写break	不写会继续循环,浪费性能
内层循环从1开始	从2开始	任何数都能被1整除,从1开始会误判
六、小结
本章通过“查找素数”这个案例,实践了以下知识点:

for循环:遍历范围、嵌套使用

if语句:条件判断

运算符%:判断整除

关系运算符:<、≤(用<=表示)、==

break语句:提前跳出循环

平方根优化:提升算法效率

核心代码模板(记住这个就够了):

java

public static boolean isPrime(int num) {
    if (num < 2) return false;
    for (int i = 2; i <= Math.sqrt(num); i++) {
        if (num % i == 0) return false;
    }
    return true;
}
  • Copyrights © 2026 Shipanxs
  • 访问人数: | 浏览次数:

如果这篇文章对你有帮助,可以请我喝杯茶

支付宝

支付宝扫一扫