0

0

Java构造器中的意外循环:理解super()调用与输入逻辑的陷阱

碧海醫心

碧海醫心

发布时间:2025-10-12 13:52:02

|

406人浏览过

|

来源于php中文网

原创

Java构造器中的意外循环:理解super()调用与输入逻辑的陷阱

本教程旨在解析java中因构造器内包含用户输入逻辑,并通过`super()`调用导致意外循环的问题。文章将深入分析问题根源,提供清晰的重构方案,将输入处理从构造器中分离,并探讨构造器设计、工厂模式及`scanner`管理等最佳实践,帮助开发者构建更健壮、可维护的java应用。

在Java面向对象编程中,构造器的主要职责是初始化新创建对象的状态。然而,当构造器中包含用户输入或复杂的业务逻辑,并且与继承机制中的super()调用结合时,可能会导致难以察觉的意外行为,例如无限循环。本文将详细分析此类问题,并提供专业的解决方案和最佳实践。

问题分析:构造器中的循环陷阱

给定的代码片段中,核心问题源于父类Person的构造器中包含了用户选择逻辑,而子类Agent的构造器又通过super()关键字调用了Person的构造器。

让我们逐步分析这个过程:

  1. 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 类的实例。

  2. Agent类的构造器:

    class Agent extends Person {
        public Agent(String agentId, String password, String address) {
            super(agentId, password, address); // 调用父类Person的构造器
            // ... 后续逻辑 ...
        }
        // ...
    }

    Agent 构造器的第一行 super(agentId, password, address); 会立即调用 Person 类的构造器。

  3. Person类的构造器:

    Favird No-Code Tools
    Favird No-Code Tools

    无代码工具的聚合器

    下载
    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();
    }
}

最佳实践与注意事项

  1. 构造器的单一职责原则: 构造器应该只负责初始化对象的状态,不应包含复杂的业务逻辑、用户交互或文件I/O操作。这些操作会使构造器变得难以理解、测试和维护,并且容易引发本文所述的递归调用问题。

  2. 避免在构造器中创建新的对象(尤其是同类型或父类型): 尤其是在父类构造器中创建子类对象,或者子类构造器中创建同类型对象,很容易导致无限递归。对象创建的决策应该由外部调用者(如main方法或工厂方法)来完成。

  3. 使用工厂方法(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); }
  4. Scanner对象的管理: 频繁地创建Scanner对象(new Scanner(System.in))不是一个好习惯。System.in是一个系统资源,应该只创建一个Scanner实例并在整个程序中重用。在程序结束时,务必调用scanner.close()来释放资源。

  5. nextInt()和nextLine()的混合使用: 当使用nextInt()、nextDouble()等方法读取数字后,如果紧接着使用nextLine()读取字符串,nextLine()会立即读取nextInt()留下的换行符,导致读取到一个空字符串。解决办法是在nextInt()之后立即调用一个input.nextLine()来消费掉这个换行符。

总结

通过将用户输入和对象创建的决策逻辑从构造器中分离出来,并将其集中到main方法或专门的工厂方法中,我们成功解决了Java中因构造器递归调用导致的意外循环问题。这不仅消除了bug,更重要的是,它促使我们遵循了面向对象设计的核心原则,如单一职责原则,从而提升了代码的模块化、可读性和可维护性。在未来的开发中,请务必记住,构造器应是简洁且专注于对象初始化的,而复杂的交互和业务逻辑则应由方法或外部流程来处理。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
go语言 面向对象
go语言 面向对象

本专题整合了go语言面向对象相关内容,阅读专题下面的文章了解更多详细内容。

58

2025.09.05

java面向对象
java面向对象

本专题整合了java面向对象相关内容,阅读专题下面的文章了解更多详细内容。

63

2025.11.27

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

760

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

221

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1566

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

649

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

1228

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

1184

2024.04.29

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.2万人学习

Java 教程
Java 教程

共578课时 | 81万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号