
本文深入探讨了java中因构造器职责混淆导致的无限循环问题。通过分析一个实际案例,揭示了在父类构造器中包含用户输入和子类实例化逻辑,并通过 `super()` 调用形成递归的机制。文章提供了将输入逻辑从构造器中分离的解决方案,并强调了构造器单一职责、分离关注点以及健壮的输入处理等java编程最佳实践,旨在帮助开发者构建更清晰、可维护的代码。
问题现象与分析
在提供的Java代码中,开发者遇到了一个意外的“循环”现象:即使只输入一次选择,程序却反复显示“[1]AGENT”和“[2]CUSTOMER”的提示。
仔细分析代码可以发现问题的根源:
- Person 类有一个构造器,其中包含用户输入逻辑,用于选择角色(AGENT 或 CUSTOMER)。如果用户选择 1 (AGENT),它会在 Person 构造器内部创建一个 Agent 对象:Agent agent = new Agent("Niel", "diko alam", "umay");
- Agent 类继承自 Person 类,其构造器会通过 super(agentId, password, address); 调用 Person 类的构造器。
- 在 main 方法中,首先创建了一个 Person 对象:Person person = new Person("20860132", "h208f32", "San luis"); 这会执行 Person 构造器中的用户输入逻辑。
- 如果用户在 Person 构造器中选择 1 (AGENT),那么 Person 构造器会尝试创建一个 Agent 对象。创建 Agent 对象时,又会调用 Agent 的构造器,而 Agent 的构造器又会调用 super(),即再次调用 Person 的构造器。
- 这样就形成了一个递归调用链:main -> Person 构造器 -> Agent 构造器 -> Person 构造器 -> ... 导致用户输入提示无限次地重复出现。
这种“循环”并非由显式的 for 或 while 循环引起,而是由构造器之间的递归调用所致。
根本原因:构造器的职责混淆
问题的根本原因在于 Person 类的构造器承担了过多的职责。构造器的主要作用是初始化对象的状态,确保对象在创建后处于一个有效且一致的状态。然而,在示例代码中,Person 构造器不仅初始化了属性,还包含了:
立即学习“Java免费学习笔记(深入)”;
- 用户界面(UI)输出(System.out.println)。
- 用户输入处理(Scanner)。
- 基于用户输入的业务逻辑(创建 Agent 或 Customer 子类实例)。
当这些复杂的逻辑被放置在构造器中时,特别是当子类构造器又调用父类构造器时,很容易导致意料之外的行为,例如本例中的无限递归。这违反了软件设计中的“单一职责原则”(Single Responsibility Principle)。
解决方案:重构输入与对象创建逻辑
解决此问题的核心思想是将用户交互和基于用户选择的对象创建逻辑从构造器中分离出来,让构造器回归其初始化对象状态的本职。
核心重构步骤
- 将用户选择逻辑移出构造器: 把显示角色选项和获取用户输入的代码从 Person 构造器中移除。
- 在 main 方法或工厂方法中处理对象创建: 根据用户在 main 方法中或一个专门的工厂方法中的选择,直接实例化相应的 Person、Agent 或 Customer 对象。
- 构造器仅负责初始化: Person、Agent 和 Customer 的构造器只接收必要的参数,并用这些参数初始化对象的属性。
代码示例
以下是修改后的代码,展示了如何将输入逻辑从构造器中分离:
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner; // 确保 Scanner 被正确导入
// Person 类,构造器仅用于初始化属性
class Person {
protected String agentId;
protected String password;
protected String address;
public Person(String agentId, String password, String address) {
this.agentId = agentId;
this.password = password;
this.address = address;
// 构造器中不再包含用户输入逻辑
}
// 可以添加 getter/setter 方法
}
// Agent 类,构造器仅用于初始化属性
class Agent extends Person {
public Agent(String agentId, String password, String address) {
super(agentId, password, address);
// 代理登录和操作逻辑应在对象创建后调用,而不是构造器内
}
// 登录方法
public boolean login(int id, int pass) {
return id == 20860132 && pass == 20020729;
}
// 代理操作菜单
public void showAgentMenu(Scanner input) {
System.out.println("[1]ADD CAR");
System.out.println("[2]SCHEDULE");
System.out.println("[3]RECORDS");
System.out.print("Enter your choice: ");
int choice2 = input.nextInt();
input.nextLine(); // 消费掉换行符
switch (choice2) {
case 1:
addCarWorkflow(input);
break;
case 2:
System.out.print("Enter schedule details: ");
String scheduleDetails = input.nextLine();
schedule(scheduleDetails);
System.out.println("Schedule added.");
break;
case 3:
System.out.print("Enter record details: ");
String recordDetails = input.nextLine();
records(recordDetails);
System.out.println("Record added.");
break;
default:
System.out.println("Invalid choice.");
}
}
// 添加汽车流程
private void addCarWorkflow(Scanner input) {
boolean stopFlag = false;
List cars = new ArrayList<>();
cars.add("Tayota");
cars.add("Hillux");
cars.add("Bugatti");
do {
System.out.println("[Current Cars]: " + cars);
System.out.print("Enter Car to add: ");
String car = input.nextLine();
cars.add(car);
addCar(cars); // 将当前列表写入文件
System.out.println("Would you like to add more?");
System.out.println("[1]YES");
System.out.println("[2]NO");
System.out.print("Enter your choice: ");
String choice3 = input.nextLine(); // 注意这里是 String
if (!choice3.equals("1")) { // 比较字符串
stopFlag = true;
}
} while (!stopFlag);
}
public void addCar(List cars) {
try (FileWriter fw = new FileWriter("cars.txt", true);
PrintWriter pw = new PrintWriter(fw)) {
pw.println(cars);
} catch (IOException e) {
e.printStackTrace();
}
}
public void schedule(String schedule) {
try (FileWriter fw = new FileWriter("schedule.txt", true);
PrintWriter pw = new PrintWriter(fw)) {
pw.println(schedule);
} catch (IOException e) {
e.printStackTrace();
}
}
public void records(String record) {
try (FileWriter fw = new FileWriter("records.txt", true);
PrintWriter pw = new PrintWriter(fw)) {
pw.println(record);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Customer 类,构造器仅用于初始化属性
class Customer extends Person {
private String customerId;
public Customer(String agentId, String password, String address, String customerId) {
super(agentId, password, address);
this.customerId = customerId;
}
public void setCustomerId(String customerId) {
this.customerId = customerId;
}
public String getCustomerId() {
return customerId;
}
// 客户操作菜单
public void showCustomerMenu(Scanner input) {
System.out.println("[1]RENT CAR");
System.out.println("[2]VIEW SCHEDULE");
System.out.println("[3]EXTEND RENTAL");
System.out.print("Enter your choice: ");
int choice2 = input.nextInt();
input.nextLine(); // 消费掉换行符
switch (choice2) {
case 1:
System.out.print("Enter car to rent: ");
String carToRent = input.nextLine();
rentCar(carToRent);
System.out.println("Car rented.");
break;
case 2:
System.out.print("Enter schedule to view: ");
String scheduleToView = input.nextLine();
viewSchedule(scheduleToView);
System.out.println("Schedule viewed.");
break;
case 3:
System.out.print("Enter record to extend: ");
String recordToExtend = input.nextLine();
extend(recordToExtend);
System.out.println("Rental extended.");
break;
default:
System.out.println("Invalid choice.");
}
}
public void rentCar(String car) {
try (FileWriter fw = new FileWriter("rented_cars.txt", true); // 使用不同的文件名
PrintWriter pw = new PrintWriter(fw)) {
pw.println(car);
} catch (IOException e) {
e.printStackTrace();
}
}
public void viewSchedule(String schedule) {
try (FileWriter fw = new FileWriter("viewed_schedules.txt", true); // 使用不同的文件名
PrintWriter pw = new PrintWriter(fw)) {
pw.println(schedule);
} catch (IOException e) {
e.printStackTrace();
}
}
public void extend(String record) {
try (FileWriter fw = new FileWriter("extended_records.txt", true); // 使用不同的文件名
PrintWriter pw = new PrintWriter(fw)) {
pw.println(record);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 主类,包含 main 方法
public class Finals {
public static void main(String[] args) {
Scanner input = new Scanner(System.in); // 在 main 方法中创建 Scanner
System.out.println("[1]AGENT");
System.out.println("[2]CUSTOMER");
System.out.print("Enter your choice: ");
int choice = input.nextInt();
input.nextLine(); // 消费掉 nextInt() 留下的换行符
Person user = null; // 定义一个 Person 引用
if (choice == 1) {
// 代理登录流程
System.out.println("[AGENT LOGIN]");
System.out.print("ENTER AGENT ID:");
int id = input.nextInt();
input.nextLine(); // 消费掉换行符
System.out.print("ENTER PASSWORD:");
int pass = input.nextInt();
input.nextLine(); // 消费掉换行符
Agent agent = new Agent("20860132", "h208f32", "San luis"); // 创建 Agent 对象
if (agent.login(id, pass)) {
System.out.println("Login successful!");
user = agent; // 将 agent 赋值给 user
((Agent)user).showAgentMenu(input); // 显示代理菜单
} else {
System.out.println("INCORRECT PLEASE TRY AGAIN.");
}
} else if (choice == 2) {
// 客户流程 (此处仅为示例,可根据需求添加客户登录/注册逻辑)
System.out.println("[CUSTOMER SECTION]");
// 假设直接创建一个客户对象,或者通过某种方式获取客户ID
Customer customer = new Customer("defaultAgentId", "defaultPassword", "defaultAddress", "CUST001");
user = customer; // 将 customer 赋值给 user
((Customer)user).showCustomerMenu(input); // 显示客户菜单
} else {
System.out.println("Invalid choice. Exiting.");
}
input.close(); // 关闭 Scanner
}
} 关键改进点:
- 构造器职责清晰: Person、Agent 和 Customer 的构造器现在只负责初始化对象的基本属性,不再处理用户输入或复杂的业务逻辑。
- 输入逻辑集中化: 用户选择角色、登录验证等输入和决策逻辑被移到了 main 方法中,使其成为程序的入口和控制流中心。
- 避免递归调用: Agent 构造器不再间接导致 Person 构造器递归创建 Agent 对象,从而消除了无限循环。
- 操作方法化: 将代理和客户的具体操作(如 showAgentMenu, showCustomerMenu, login, addCarWorkflow 等)封装成独立的方法,提高了代码的可读性和模块化。
- 资源管理: 在 main 方法结束时关闭了 Scanner 对象,避免资源泄露。文件写入也使用了 try-with-resources 语句,确保 FileWriter 和 PrintWriter 被正确关闭。
- Scanner.nextInt() 后跟 nextLine() 的处理: 在 nextInt() 后立即调用 input.nextLine() 来消费掉 nextInt() 遗留的换行符,避免后续 nextLine() 读取空字符串的问题。
- 字符串比较: 将 if(!choice3.equals(1)) 改为 if (!choice3.equals("1")),确保字符串与字符串进行比较。
Java编程最佳实践
此次问题及解决方案引出了几个重要的Java编程最佳实践:
- 构造器的单一职责原则: 构造器应专注于创建和初始化对象,使其处于一个有效状态。避免在构造器中执行耗时操作、复杂的业务逻辑、用户交互或文件I/O等。
- 分离关注点(Separation of Concerns): 将不同的功能模块(如用户界面、业务逻辑、数据存储、对象创建)分离到不同的类或方法中。这使得代码更易于理解、测试和维护。例如,用户输入和对象创建逻辑应与对象本身的初始化逻辑分离。
- 使用工厂方法模式(可选): 对于需要根据不同输入创建不同类型对象的情况,可以考虑使用工厂方法模式。例如,可以创建一个 PersonFactory 类,其中包含一个静态方法 createPerson(int choice, Scanner input),负责根据用户的选择返回 Agent 或 Customer 实例。这进一步解耦了对象创建逻辑。
-
健壮的输入处理:
- 关闭 Scanner: 确保在不再需要 Scanner 对象时调用其 close() 方法,释放系统资源。
- 处理 nextInt() 后遗留的换行符: 当使用 Scanner.nextInt()、nextDouble() 等方法读取数字后,缓冲区会留下一个换行符。如果紧接着调用 Scanner.nextLine(),它会立即读取这个换行符并返回一个空字符串。为了避免此问题,应在 nextInt() 后立即调用 input.nextLine() 来消费掉这个换行符。
- 输入验证: 考虑用户输入非预期数据(例如,当期望数字时输入了文本)的情况,使用 try-catch 块处理 InputMismatchException,或使用 hasNextInt() 等方法进行预检查。
- 资源管理: 对于文件I/O等需要外部资源的场景,应使用 try-with-resources 语句(Java 7+)确保资源被正确关闭,即使发生异常。
总结
本教程通过一个具体的Java代码案例,详细阐述了因构造器职责混淆导致的递归调用和无限循环问题。核心解决方案在于将用户交互和对象创建的决策逻辑从构造器中剥离,使其专注于对象初始化。通过遵循构造器单一职责、分离关注点等编程最佳实践,并注意输入处理的细节,开发者可以构建出更加健壮、可维护且易于理解的Java应用程序。










