Debapriya Biswas
Debapriya Biswas

Reputation: 1349

Java Records StackOverflow RuntimeException

I am learning Java Records, preview feature and I am getting a StackOverflow exception when I run the below piece of code .

    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)

From the exception I realize that it has to do with toString() and when I have override toString() in the records as below code I don't see the 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);
   }
}

This code prints John : Enrolled for Math,Enrolled for History . Can someone please explain why if I don't override toString() I get StackOverflow? Also I see StackOverflow when I print john.hashCode()

Upvotes: 4

Views: 1735

Answers (4)

Johannes Kuhn
Johannes Kuhn

Reputation: 15173

As others pointed out, the issue is that you have a circular reference in your record.

You are able to create such a circular reference because Records are only shallowly immutable.
In your case, enrolled is a List<Course>, that you initialize with an empty ArrayList<>.

To avoid that, you should make a defensive copy when creating the record:

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

You then have to supply a list of the courses when you create a Student:

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

This makes it impossible to have a circular reference.
But it looks like your model requires those circular references. So you might need to change the model.

Upvotes: 2

Aniket Sahrawat
Aniket Sahrawat

Reputation: 12937

It has nothing to do with Record#toString in particular. Take a closer look at your declaration:

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

Course#toString needs Student#toString and Student#toString needs Course#toString. So if you try to print Student then all enrolled Course will be printed along. Those Courses needs Student so they will print Student inside. This loop will continue till you get StackOverflow exception.

To avoid this kind of scenario, you should rearrange your code so they don't depend on each other. You can create an id per instance. An example would be:

public record Course(String name, String studentId) {}
public record Student(String name, String studentId, List<Course> courses) {}

Upvotes: 2

pero_hero
pero_hero

Reputation: 3194

Your structure is the following Student-> List<Course> and Course-> Student which builds a circular chain and the toString method runs in loop.

From the javadocs for records:

Object.toString() methods are derived from all of the component fields.

When you do System.out.println(john) toString is called for the Student john then it is delegated to all the enrolled Course instances for which toString is called and as a Course instance has a reference to a Student instance again...the whole chain runs until a StackOverflowException is thrown.

Upvotes: 1

Harry Jones
Harry Jones

Reputation: 342

It's because you're trying to print a list of courses that the student is enrolled in, and each of those courses has that same student listed as enrolled. So you have a circular reference. Course -> Student -> Course -> Student etc.

When you overrode Course toString(), you only printed the name so this was no longer an issue.

Upvotes: 1

Related Questions