relate toString with object creation

Viewed 86

I have a fairly basic Java class with some class variables. I have overwridden toString() to provide me with a string output (which will eventually be output to a text file).

I am trying to elegantly create a way for me to use this string output to recreate the object with all of the variables set as before. The class looks something like this:

public class Report {

    private String itemA;
    private String itemB;
    private String itemC;

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Items are::");
        sb.append("\nItem A is: ").append(itemA);
        sb.append("\nItem B is: ").append(itemB);
        sb.append("\nItem C is: ").append(itemC);
        return sb.toString();
    }
}

this is how I can potentially tackle it using reflection:

public class Report {

    private String itemA;
    private String itemB;
    private String itemC;

    private final Map<String, String> MAPPING = new HashMap<>();

    public Report(String itemA, String itemB, String itemC) {
        this.itemA = itemA;
        this.itemB = itemB;
        this.itemC = itemC;

        MAPPING.put("Item A is: ", "itemA");
        MAPPING.put("Item B is: ", "itemB");
        MAPPING.put("Item C is: ", "itemC");
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Items are::");

        MAPPING.entrySet().forEach(entry -> {
            sb.append("\n").append(entry.getKey()).append(BeanUtils.getProperty(this, entry.getValue()));
        });

        return sb.toString();
    }


    public Report createReportFromString(String reportString) {
        List<String> reportLines = Arrays.asList(reportString.split("\n"));
        HashMap<String, String> stringObjectRelationship = new HashMap<>();
        
        reportLines.forEach(reportLine -> {
            Optional<String> matchingKey = MAPPING.keySet().stream().filter(reportLine::contains).findFirst();
            matchingKey.ifPresent(key -> {stringObjectRelationship.put(MAPPING.get(key), reportLine.split(key)[1]);});
        });
        
        stringObjectRelationship.forEach((variableName, variableValue) -> BeanUtils.setProperty(this, variableName, variableValue));
        return this;
    }
}

I basically want to relate the key in the report ("Item A is: ") to the name of the corresponding variable ("itemA") and use this relationship in both the toString() method and the createReportFromString(String string) method. Now when doing this there are a lot of possible exceptions that can be thrown and need to either be handled or thrown - and it then looks a lot less elegant than I would like.

I don't know if this is possible to do without reflection - or perhaps I could rearrange this class to make this possible?

What I can`t change is the structure of the string output in the toString().

2 Answers

Reflection bears multiple features:

  1. Automatic discovery of features of a program at runtime
  2. Support for dealing with features unknown at compile-time
  3. Provide an abstraction of program features (e.g. methods or fields)

Your approach suggests that you don’t want an automatic discovery, as you are specifying the three elements explicitly. This is a good thing, as it makes your program more robust regarding future changes, as dealing with automatically discovered, potentially unknown program elements will destroy any help from the compiler, as it can’t tell you when there are mismatches.

You only want the third point, an abstraction over the elements of your report. You can create such an abstraction yourself, tailored to your use case, without Reflection, which will be more robust and even more efficient:

public class Report {
    static final class Element {
        final String header;
        final Function<Report,String> getter;
        final BiConsumer<Report,String> setter;
        final Pattern pattern;
        Element(String header,
                Function<Report, String> getter, BiConsumer<Report, String> setter) {
            this.header = header;
            this.getter = getter;
            this.setter = setter;
            pattern = Pattern.compile("^\\Q"+header+"\\E(.*?)$", Pattern.MULTILINE);
        }
    }
    static final List<Element> ELEMENTS = List.of(
        new Element("Item A is: ", Report::getItemA, Report::setItemA),
        new Element("Item B is: ", Report::getItemB, Report::setItemB),
        new Element("Item C is: ", Report::getItemC, Report::setItemC));

    private String itemA, itemB, itemC;

    public Report(String itemA, String itemB, String itemC) {
        this.itemA = itemA;
        this.itemB = itemB;
        this.itemC = itemC;
    }
    @Override public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Items are:");
        ELEMENTS.forEach(e ->
            sb.append('\n').append(e.header).append(e.getter.apply(this)));
        return sb.toString();
    }
    public static Report createReportFromString(String reportString) {
        return new Report("", "", "").setValuesFromString(reportString);
    }
    public Report setValuesFromString(String reportString) {
        Matcher m = null;
        for(Element e: ELEMENTS) {
            if(m == null) m = e.pattern.matcher(reportString);
            else m.usePattern(e.pattern).reset();

            if(!m.find())
                throw new IllegalArgumentException("missing \""+e.header+'"');
            e.setter.accept(this, m.group(1));
        }
        return this;
    }
    public String getItemA() {
        return itemA;
    }
    public void setItemA(String itemA) {
        this.itemA = itemA;
    }
    public String getItemB() {
        return itemB;
    }
    public void setItemB(String itemB) {
        this.itemB = itemB;
    }
    public String getItemC() {
        return itemC;
    }
    public void setItemC(String itemC) {
        this.itemC = itemC;
    }
}

This works with Java’s out-of-the-box features, not requiring another library to simplify the operation.

Note that I changed the code pattern, as createReportFromString is a misleading name for a method modifying an already existing object. I used the name for a factory method truly creating a new object and added a another method for setting the values of the object (as a direct counter-part to toString).

If you are still using Java 8, you can replace List.of(…) with Arrays.asList(…) or better Collections.unmodifiableList(Arrays.asList(…)).


You can also remove the .reset() call in the setValuesFromString method. When you remove it, the elements in the input string are required to be in the same order as the toString() method produces. This makes it a bit less flexible, but also more efficient if you expand the code to have a lot more elements.

@JimboMcHiggins assuming I can change the toString output how exactly would you tie together serialization and deserialization with some common mapping?

I would leave the toString unchanged and move the responsibility of serialization to java.io.Serializable. Correct me if this is not an acceptable approach. The mapping would be defined by the class fields of your Report pojo. This would also allow you to change your toString without breaking deserialization of existing objects.

import java.io.Serializable;

public class Report implements Serializable {
    private static final long serialVersionUID = 1L;

    private String itemA;
    private String itemB;
    private String itemC;

    public Report(String itemA, String itemB, String itemC) {
        this.itemA = itemA;
        this.itemB = itemB;
        this.itemC = itemC;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Items are::");
        sb.append("\nItem A is: ").append(itemA);
        sb.append("\nItem B is: ").append(itemB);
        sb.append("\nItem C is: ").append(itemC);
        return sb.toString();
    }
}

Example Usage

public class Test1 {
    public static void main(String[] args) {
        Report report = new Report("W", "O", "W");
        System.out.println(report);

        String filename = "file.ser";

        // Serialization
        try
        {
            //Saving of report in a file
            FileOutputStream file = new FileOutputStream(filename);
            ObjectOutputStream out = new ObjectOutputStream(file);

            // Method for serialization of report
            out.writeObject(report);

            out.close();
            file.close();

            System.out.println("Report has been serialized");

        }

        catch(IOException ex)
        {
            System.out.println("IOException is caught");
        }


        Report report1 = null;

        // Deserialization
        try
        {
            // Reading the report from a file
            FileInputStream file = new FileInputStream(filename);
            ObjectInputStream in = new ObjectInputStream(file);

            // Method for deserialization of report
            report1 = (Report)in.readObject();

            in.close();
            file.close();

            System.out.println("Report has been deserialized ");
            System.out.println(report1);
        }

        catch(IOException ex)
        {
            System.out.println("IOException is caught");
        }

        catch(ClassNotFoundException ex)
        {
            System.out.println("ClassNotFoundException is caught");
        }
    }
}

Output

Items are::
Item A is: W
Item B is: O
Item C is: W
Report has been serialized
Report has been deserialized 
Items are::
Item A is: W
Item B is: O
Item C is: W
Related