本教程旨在解析java中因构造器内包含用户输入逻辑,并通过`super()`调用导致意外循环的问题。文章将深入分析问题根源,提供清晰的重构方案,将输入处理从构造器中分离,并探讨构造器设计、工厂模式及`scanner`管理等最佳实践,帮助开发者构建更健壮、可维护的Java应用。
在Java面向对象编程中,构造器的主要职责是初始化新创建对象的状态。然而,当构造器中包含用户输入或复杂的业务逻辑,并且与继承机制中的super()调用结合时,可能会导致难以察觉的意外行为,例如无限循环。本文将详细分析此类问题,并提供专业的解决方案和最佳实践。
问题分析:构造器中的循环陷阱
给定的代码片段中,核心问题源于父类Person的构造器中包含了用户选择逻辑,而子类Agent的构造器又通过super()关键字调用了Person的构造器。
让我们逐步分析这个过程:
-
Finals类中的main方法:
立即学习“Java免费学习笔记(深入)”;
public class Finals { public static void main(String[] args) { Person person = new Person("20860132", "h208f32", "San luis"); Agent agent = new Agent("20860132", "h208f32", "San luis"); // 第一次问题触发点 } }
当程序执行到 new Agent(…) 时,会尝试创建 Agent 类的实例。
-
Agent类的构造器:
class Agent extends Person { public Agent(String agentId, String password, String address) { super(agentId, password, address); // 调用父类Person的构造器 // ... 后续逻辑 ... } // ... }
Agent 构造器的第一行 super(agentId, password, address); 会立即调用 Person 类的构造器。
-
Person类的构造器:
class Person { // ... 成员变量 ... public Person(String agentId, String password, String address) { this.agentId = agentId; this.password = password; this.address = address; Scanner input = new Scanner(System.in); // 创建Scanner System.out.println("[1]AGENT"); System.out.println("[2]CUSTOMER"); int choice = input.nextInt(); // 等待用户输入 if(choice == 1) { Agent agent = new Agent("Niel", "diko alam", "umay"); // 第二次问题触发点 } else if(choice == 2) { System.out.println("POTANGINA"); } } // ... }
这是问题的核心所在。当 Person 构造器被调用时,它会:
- 打印 “[1]AGENT” 和 “[2]CUSTOMER”。
- 等待用户输入一个整数 (choice)。
如果用户在此时输入 1,Person 构造器会再次尝试创建一个新的 Agent 对象:new Agent(“Niel”, “diko alam”, “umay”);。 这又会回到第2步,即调用 Agent 的构造器,而 Agent 的构造器又会再次调用 Person 的构造器,形成一个无限递归的循环。用户每次选择 1,都会导致一个新的 Agent 对象的创建尝试,从而陷入死循环。
即使 main 方法中仅创建 Person 对象,并在 Person 构造器中选择 1,也会导致循环。如果选择 2,则不会触发 Agent 对象的创建,从而看似“没有循环”,但实际上设计上已经存在缺陷。
解决方案:重构构造器与输入逻辑
解决这个问题的关键在于遵循面向对象设计的“单一职责原则”:构造器应专注于对象的初始化,而用户交互和业务逻辑应放在其他方法或主程序流程中。
1. 移除构造器中的用户输入和业务逻辑
首先,将Person构造器中的用户选择和对象创建逻辑移除。构造器应该只负责接收参数并初始化对象的成员变量。
修改后的Person类:
import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.Scanner; import java.util.ArrayList; import java.util.List; 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; } // 可以添加获取字段的方法 public String getAgentId() { return agentId; } public String getPassword() { return password; } public String getAddress() { return address; } }
修改后的Agent类:Agent类的构造器也应简化,只负责调用父类构造器和初始化自己的特有字段(如果有)。
class Agent extends Person { // Agent类特有的字段(如果存在) // private String agentSpecificField; public Agent(String agentId, String password, String address) { super(agentId, password, address); // 只调用父类构造器 // this.agentSpecificField = agentSpecificField; // 初始化Agent特有字段 } // 示例:登录和操作逻辑应放在方法中,而不是构造器 public void performAgentActions(Scanner input) { System.out.println("[LOGIN]"); System.out.print("ENTER AGENT ID:"); int id = input.nextInt(); System.out.print("ENTER PASSWORD:"); int pass = input.nextInt(); input.nextLine(); // 消费掉nextInt()留下的换行符 if (id == 20860132 && pass == 20020729) { System.out.println("[1]ADD CAR"); System.out.println("[2]SCHEDULE"); System.out.println("[3]RECORDS"); int choice2 = input.nextInt(); input.nextLine(); // 消费掉nextInt()留下的换行符 if (choice2 == 1) { addCarWorkflow(input); // 委托给一个私有方法处理添加汽车的流程 } else if (choice2 == 2) { // schedule logic } else if (choice2 == 3) { // records logic } } else { System.out.println("INCORRECT PLEASE TRY AGAIN."); } } private void addCarWorkflow(Scanner input) { boolean stopFlag = false; List<String> cars = new ArrayList<>(); cars.add("Tayota"); cars.add("Hillux"); cars.add("bugatti"); do { System.out.println("[CARS]"); System.out.println(cars); System.out.print("Enter Car:"); String car = input.nextLine(); cars.add(car); System.out.println("Would you like to add more?"); System.out.println("[1]YES"); System.out.println("[2]NO"); String choice3 = input.nextLine(); // 注意这里是nextLine() addCar(cars); // 将当前汽车列表写入文件 if (!choice3.equals("1")) { // 比较字符串使用equals() stopFlag = true; } } while (!stopFlag); } public void addCar(List<String> 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类: 类似地,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 performCustomerActions(Scanner input) { System.out.println("Customer actions for ID: " + customerId); // ... 其他客户特有逻辑 ... } // 文件写入方法保持不变 public void rentCar(String car) { try (FileWriter fw = new FileWriter("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("schedule.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("records.txt", true); PrintWriter pw = new PrintWriter(fw)) { pw.println(record); } catch (IOException e) { e.printStackTrace(); } } }
2. 在main方法中集中处理用户选择和对象创建
将用户选择是创建Agent还是Customer的逻辑放在main方法中,或者一个独立的工厂方法中。这样可以避免递归调用。
修改后的Finals类:
public class Finals { public static void main(String[] args) { // 推荐只创建一个Scanner实例并在程序中传递使用或作为静态成员 Scanner input = new Scanner(System.in); System.out.println("欢迎使用系统!"); System.out.println("[1] 代理 (Agent)"); System.out.println("[2] 客户 (Customer)"); System.out.print("请选择您的角色:"); int choice = -1; try { choice = input.nextInt(); input.nextLine(); // 消费掉nextInt()留下的换行符 } catch (java.util.InputMismatchException e) { System.out.println("输入无效,请输入数字1或2。"); input.nextLine(); // 清除错误输入 return; // 退出程序或重新提示 } Person user = null; // 定义一个Person类型的引用 if (choice == 1) { // 在这里创建Agent对象 Agent agent = new Agent("20860132", "h208f32", "San luis"); user = agent; // 将Agent对象赋值给Person引用 System.out.println("您选择了代理角色。"); // 代理的后续操作 agent.performAgentActions(input); } else if (choice == 2) { // 在这里创建Customer对象 System.out.print("请输入客户ID:"); String customerId = input.nextLine(); Customer customer = new Customer("默认代理ID", "默认密码", "默认地址", customerId); user = customer; // 将Customer对象赋值给Person引用 System.out.println("您选择了客户角色。"); // 客户的后续操作 customer.performCustomerActions(input); } else { System.out.println("无效的选择。"); } // 确保关闭Scanner input.close(); } }
最佳实践与注意事项
-
构造器的单一职责原则: 构造器应该只负责初始化对象的状态,不应包含复杂的业务逻辑、用户交互或文件I/O操作。这些操作会使构造器变得难以理解、测试和维护,并且容易引发本文所述的递归调用问题。
-
避免在构造器中创建新的对象(尤其是同类型或父类型): 尤其是在父类构造器中创建子类对象,或者子类构造器中创建同类型对象,很容易导致无限递归。对象创建的决策应该由外部调用者(如main方法或工厂方法)来完成。
-
使用工厂方法(Factory Method)模式: 当对象的创建逻辑比较复杂,需要根据不同条件创建不同类型的对象时,可以考虑使用工厂方法模式。这可以将对象创建的逻辑从main方法中进一步抽象出来,提高代码的可读性和可维护性。
// 示例:一个简单的工厂类 class PersonFactory { public static Person createPerson(int choice, Scanner input) { if (choice == 1) { return new Agent("20860132", "h208f32", "San luis"); } else if (choice == 2) { System.out.print("请输入客户ID:"); String customerId = input.nextLine(); return new Customer("默认代理ID", "默认密码", "默认地址", customerId); } else { System.out.println("无效的选择。"); return null; } } } // main方法中调用 // Person user = PersonFactory.createPerson(choice, input); // if (user instanceof Agent) { ((Agent)user).performAgentActions(input); } // else if (user instanceof Customer) { ((Customer)user).performCustomerActions(input); }
-
Scanner对象的管理: 频繁地创建Scanner对象(new Scanner(System.in))不是一个好习惯。System.in是一个系统资源,应该只创建一个Scanner实例并在整个程序中重用。在程序结束时,务必调用scanner.close()来释放资源。
-
nextInt()和nextLine()的混合使用: 当使用nextInt()、nextDouble()等方法读取数字后,如果紧接着使用nextLine()读取字符串,nextLine()会立即读取nextInt()留下的换行符,导致读取到一个空字符串。解决办法是在nextInt()之后立即调用一个input.nextLine()来消费掉这个换行符。
总结
通过将用户输入和对象创建的决策逻辑从构造器中分离出来,并将其集中到main方法或专门的工厂方法中,我们成功解决了Java中因构造器递归调用导致的意外循环问题。这不仅消除了bug,更重要的是,它促使我们遵循了面向对象设计的核心原则,如单一职责原则,从而提升了代码的模块化、可读性和可维护性。在未来的开发中,请务必记住,构造器应是简洁且专注于对象初始化的,而复杂的交互和业务逻辑则应由方法或外部流程来处理。
评论(已关闭)
评论已关闭