4

我正在学习 Java 记录、预览功能,并且在运行以下代码时遇到 StackOverflow 异常。

    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;
    public class Example {
        public record Course(String name, Student topper) { }
        public record Student(String name, List<Course> enrolled) {}

        public static void main(String[] args) {
            Student john = new Student("John", new ArrayList<>());
            john.enrolled().add(new Course("Enrolled for Math", john));
            john.enrolled().add(new Course("Enrolled for History", john));
            System.out.println(john);
        }
    }


Below is the exception trace :


 java --enable-preview Example
 Exception in thread "main" java.lang.StackOverflowError
    at Example$Course.toString(Example.java:6)
    at java.base/java.lang.String.valueOf(String.java:3388)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:167)
    at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:457)

从异常中,我意识到它与 toString() 有关,当我在记录中覆盖 toString() 时,如下代码所示,我看不到 Exception 。

// code with implementation of toString()
public class Example {

    public record Course(String name, Student topper) {
    public String toString()
        {
            return name;
        }
   }
   public record Student(String name, List<Course> enrolled) {
   public String toString()
        {
             return this.name+" : "+enrolled.stream().map(s->s.toString()).collect(Collectors.joining(","));
        }
   }
   public static void main(String... args) {
       Student john = new Student("John", new ArrayList<>());
       john.enrolled().add(new Course("Enrolled for Math", john));
       john.enrolled().add(new Course("Enrolled for History", john));
       System.out.println(john);
   }
}

此代码打印 John : Enrolled for Math,Enrolled for History 。有人可以解释为什么如果我不覆盖 toString() 我会得到 StackOverflow?我在打印时也看到了 StackOverflowjohn.hashCode()

4

4 回答 4

2

它与特别无关Record#toString。仔细看看你的声明:

public record Course(String name, Student topper) {}
public record Student(String name, List<Course> enrolled) {}

Course#toString需要Student#toStringStudent#toString需要Course#toString。因此,如果您尝试打印,Student那么所有注册的Course都将被打印。那些Course需要Student,所以他们会在里面打印Student。这个循环将继续,直到你得到StackOverflow异常。

为了避免这种情况,你应该重新排列你的代码,这样它们就不会相互依赖。您可以为每个实例创建一个 id。一个例子是:

public record Course(String name, String studentId) {}
public record Student(String name, String studentId, List<Course> courses) {}
于 2020-06-02T08:57:13.713 回答
2

正如其他人指出的那样,问题在于您的记录中有循环引用。

您可以创建这样的循环引用,因为 Records 只是浅不可变的。
在你的情况下,enrolled是一个List<Course>,你用一个空初始化ArrayList<>

为避免这种情况,您应该在创建记录时制作一份防御性副本:

public record Student(String name, List<Course> enrolled) {
    public Student {
        enrolled = List.copyOf(enrolled);
    }
}

然后,您必须在创建学生时提供课程列表:

List<Course> courses = List.of(
        new Course("Enrolled for Math", null),
        new Course("Enrolled for History", null)
);
Student john = new Student("John", courses);

这使得不可能有循环引用。
但看起来您的模型需要那些循环引用。因此,您可能需要更改模型。

于 2020-06-02T09:19:30.153 回答
1

这是因为您正在尝试打印学生注册的课程列表,并且这些课程中的每一个都将同一名学生列为已注册。所以你有一个循环引用。课程 -> 学生 -> 课程 -> 学生等。

当您覆盖 Course toString() 时,您只打印了名称,因此这不再是问题。

于 2020-06-02T08:14:42.587 回答
1

您的结构如下Student->List<Course>Course->Student构建循环链并且该toString方法循环运行。

来自javadocs的记录:

Object.toString() 方法派生自所有组件字段。

当您System.out.println(john) toStringStudentjohn 调用 do 时,它将被委托给所有已注册的Course实例,toString并且作为一个实例再次Course引用一个Student实例......整个链运行直到 aStackOverflowException被抛出。

于 2020-06-02T08:15:37.607 回答