5
\$\begingroup\$

See the previous iteration. I have incorporated the answer of h.j.k.. Now I have:

LineStringSerializationFactory.java:

package net.coderodde.lists.serial;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Scanner;

/**
 * This class contains static methods for serializing the lists of elements to
 * a textual representation and deserializing it back to the list of elements.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.61
 */
public class LineStringSerializationFactory {

    /**
     * The alias for the new line string.
     */
    private static final String ENDL = "\n";

    // Do not instantiate this class.
    private LineStringSerializationFactory() {}

    /**
     * Serializes the elements in <code>list</code>. The <code>serializer</code>
     * is supposed to return a single line of text with no new line characters.
     * That string is supposed to encode the state of the serialized element. 
     * The order of encoding lines is dictated by the iteration order of the 
     * input list.
     * 
     * <b>Note:</b> this serialization procedure assumes that
     * <code>serializer</code> does not map any element to a string containing
     * a new line character as it is used for separating the element encodings.
     * 
     * @param <E>        the actual type of elements to serialize.
     * @param collection the collection of elements to serialize.
     * @param serializer the serializer returning a line of text encoding the 
     *                   state of the input element.
     * @return           a string each line of which encodes a single element.
     * 
     * @throws IllegalArgumentException if <code>serializer</code> returns a
     *                                  string containing a new line character.
     */
    public static <E> String serialize(Collection<E> collection, 
                                       LineStringSerializer<E> serializer) {
        StringBuilder sb = new StringBuilder();

        for (E element : collection) {
            String line = serializer.serialize(element);

            if (line.contains(ENDL)) {
                throw new IllegalArgumentException(
                        "The line serializer may not return the new line " +
                        "character in its output.");
            }

            sb.append(line).append(ENDL);
        }

        return sb.toString();
    }

    /**
     * Deserializes the list from the input text <code>text</code>. Each line 
     * is expected to produce exactly one element.
     * 
     * @param  <E>          the actual deserialized element type.
     * @param  text         the entire string holding the encoding of the entire
     *                      list.
     * @param  deserializer the deserializer converting each input line to an
     *                      element whose state is encoded by that line.
     * @return              the list of elements encoded by <code>text</code> in
     *                      the same order as their respective encoding lines.
     */
    public static <E> List<E> 
        deserialize(String text, LineStringDeserializer<E> deserializer) {
        List<E> ret = new ArrayList<>();
        Scanner scanner = new Scanner(text);

        while (scanner.hasNextLine()) {
            ret.add(deserializer.deserialize(scanner.nextLine()));
        }

        scanner.close();
        return ret;
    }
}

LineStringSerializer.java:

package net.coderodde.lists.serial;

/**
 * This interface defines the API for serializing an object to a string.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.61
 * @param <E> the object type.
 */
@FunctionalInterface
public interface LineStringSerializer<E> {

    /**
     * Returns the textual representation of the input object.
     * 
     * @param  object the object to serialize.
     * @return the textual representation of the input object.
     */
    public String serialize(E object);
}

LineStringDeserializer.java:

package net.coderodde.lists.serial;

/**
 * This interface defines the API for deserializing the objects from their 
 * textual representation.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.61
 * @param <E> the object type.
 */
@FunctionalInterface
public interface LineStringDeserializer<E> {

    /**
     * Deserializes an object from its textual representation.
     * 
     * @param  text the string representing the state of the object.
     * @return the actual, deserialized object.
     */
    public E deserialize(String text);
}

LineStringSerializationFactoryTest.java:

package net.coderodde.lists.serial;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.BeforeClass;

public class LineStringSerializationFactoryTest {

    private static Random random;

    @BeforeClass
    public static void init() {
        long seed = System.currentTimeMillis();
        System.out.println("Seed: " + seed);
        random = new Random(seed);
    }

    @Test
    public void testSimpleIntegers() {
        List<Integer> input = getRandomIntegerList(1000, random);
        String text = LineStringSerializationFactory
                      .serialize(input, Object::toString);
        List<Integer> output = LineStringSerializationFactory
                               .deserialize(text, Integer::parseInt);
        assertTrue(input.equals(output));
    }

    @Test
    public void testListOfLists() {
        // Create data to serialize.
        List<List<Integer>> input = getRandomListOfLists(1000, random);
        // Serialize it.
        String text = LineStringSerializationFactory
                      .serialize(input, new IntListSerializer());
        // Deserialize it.
        List<List<Integer>> output = 
                LineStringSerializationFactory
                .deserialize(text, new IntListDeserializer());
        // Compare.
        assertTrue(input.equals(output));
    }

    /**
     * Constructs a random integer array.
     * 
     * @param  size   the length of the integer array.
     * @param  random the random number generator.
     * @return        the integer array.
     */
    private static List<Integer> 
        getRandomIntegerList(int size, Random random) {
        return random.ints(size).boxed().collect(Collectors.toList());
    }

    /**
     * Constructs a random list of integer lists.
     * 
     * @param  size   the length of the outer list.
     * @param  random the random number generator.
     * @return        the random list of integer lists.
     */
    private static List<List<Integer>> 
        getRandomListOfLists(int size, Random random) {
        List<List<Integer>> ret = new ArrayList<>(size);

        for (int i = 0; i < size; ++i) {
            ret.add(getRandomIntegerList(random.nextInt(50), random));
        }

        return ret;
    }

    private static class IntListSerializer
    implements LineStringSerializer<List<Integer>> {

        @Override
        public String serialize(List<Integer> list) {
            StringBuilder sb = new StringBuilder();
            int index = 0;

            for (Integer i : list) {
                sb.append(i.toString());

                if (index < list.size() - 1) {
                    sb.append(",");
                }
            }

            return sb.toString();
        }
    }

    private static class IntListDeserializer 
    implements LineStringDeserializer<List<Integer>> {

        @Override
        public List<Integer> deserialize(String text) {
            return Stream.of(text.split(","))
                         .map(String::trim)
                         .filter(s -> !s.isEmpty())
                         .map(LineStringSerializationFactoryTest::asInteger)
                         .filter(Objects::nonNull)
                         .collect(Collectors.toList());
        }
    }

    private static Integer asInteger(String value) {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException ex) {
            return null;
        }
    }
}

So, what do you think?

\$\endgroup\$
0
4
\$\begingroup\$

I'm suggesting this serialize-method:

return input

        // convert items to strings
        .map(serializer::serialize)

        // check for illegal values
        .peek(line -> {
            if (line.contains(ENDL)) {
                throw new IllegalArgumentException(
                        "The line serializer may not return the new line " +
                        "character in its output.");
            }
        })

        // join them together to one text
        .collect(Collectors.joining(ENDL));

And this deserialize-method:

// split text at line breaks
String[] elements = text.split(ENDL);

return Arrays.stream(elements)

        // turn every item into a java object
        .map(deserializer::deserialize);

My changes:

  • Use Streams in method signature (both parameter and return value)
    • this hinders other code to expose encapsulated Collections
  • Use Stream syntax instead of old foreach loop
    • this will make your code shorter and therefore better readable
  • Use split instead of a Scanner
    • this will make your code shorter and more focused on functionality than on technical implementation

The complete factory class will look like this:

package net.coderodde.lists.serial;

import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * This class contains static methods for serializing the lists of elements to
 * a textual representation and deserializing it back to the list of elements.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.61
 */
public class LineStringSerializationFactory {

    /**
     * The alias for the new line string.
     */
    private static final String ENDL = "\n";

    // Do not instantiate this class.
    private LineStringSerializationFactory() {}

    /**
     * Serializes the elements in <code>input</code>. The <code>serializer</code>
     * is supposed to return a single line of text with no new line characters.
     * That string is supposed to encode the state of the serialized element. 
     * The order of encoding lines is dictated by the iteration order of the 
     * input stream.
     * 
     * <b>Note:</b> this serialization procedure assumes that
     * <code>serializer</code> does not map any element to a string containing
     * a new line character as it is used for separating the element encodings.
     * 
     * @param <E>        the actual type of elements to serialize.
     * @param collection the stream of elements to serialize.
     * @param serializer the serializer returning a line of text encoding the 
     *                   state of the input element.
     * @return           a string each line of which encodes a single element.
     * 
     * @throws IllegalArgumentException if <code>serializer</code> returns a
     *                                  string containing a new line character.
     */
    public static <E> String serialize(Stream<E> input, 
            LineStringSerializer<E> serializer) {
        return input

                // convert items to strings
                .map(serializer::serialize)

                // check for illegal values
                .peek(line -> {
                    if (line.contains(ENDL)) {
                        throw new IllegalArgumentException(
                                "The line serializer may not return the new line " +
                                "character in its output.");
                    }
                })

                // join them together to one text
                .collect(Collectors.joining(ENDL));
    }

    /**
     * Deserializes the list from the input text <code>text</code>. Each line 
     * is expected to produce exactly one element.
     * 
     * @param  <E>          the actual deserialized element type.
     * @param  text         the entire string holding the encoding of the entire
     *                      list.
     * @param  deserializer the deserializer converting each input line to an
     *                      element whose state is encoded by that line.
     * @return              the stream of elements encoded by <code>text</code> in
     *                      the same order as their respective encoding lines.
     */
    public static <E> Stream<E> 
    deserialize(String text, LineStringDeserializer<E> deserializer) {

        // split text at line breaks
        String[] elements = text.split(ENDL);

        return Arrays.stream(elements)

                // turn every item into a java object
                .map(deserializer::deserialize);
    }
}

For the test class you will need only minor changes. For completness - here is the code:

package net.coderodde.lists.serial;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.BeforeClass;

public class LineStringSerializationFactoryTest {

    private static Random random;

    @BeforeClass
    public static void init() {
        long seed = System.currentTimeMillis();
        System.out.println("Seed: " + seed);
        random = new Random(seed);
    }

    @Test
    public void testSimpleIntegers() {
        List<Integer> input = getRandomIntegerList(1000, random);
        String text = LineStringSerializationFactory
                      .serialize(input.stream(), Object::toString);
        List<Integer> output = LineStringSerializationFactory
                               .deserialize(text, Integer::parseInt).collect(Collectors.toList());
        assertTrue(input.equals(output));
    }

    @Test
    public void testListOfLists() {
        // Create data to serialize.
        List<List<Integer>> input = getRandomListOfLists(1000, random);
        // Serialize it.
        String text = LineStringSerializationFactory
                      .serialize(input.stream(), new IntListSerializer());
        // Deserialize it.
        List<List<Integer>> output = 
                LineStringSerializationFactory
                .deserialize(text, new IntListDeserializer()).collect(Collectors.toList());
        // Compare.
        assertTrue(input.equals(output));
    }

    /**
     * Constructs a random integer array.
     * 
     * @param  size   the length of the integer array.
     * @param  random the random number generator.
     * @return        the integer array.
     */
    private static List<Integer> 
        getRandomIntegerList(int size, Random random) {
        return random.ints(size).boxed().collect(Collectors.toList());
    }

    /**
     * Constructs a random list of integer lists.
     * 
     * @param  size   the length of the outer list.
     * @param  random the random number generator.
     * @return        the random list of integer lists.
     */
    private static List<List<Integer>> 
        getRandomListOfLists(int size, Random random) {
        List<List<Integer>> ret = new ArrayList<>(size);

        for (int i = 0; i < size; ++i) {
            ret.add(getRandomIntegerList(random.nextInt(50), random));
        }

        return ret;
    }

    private static class IntListSerializer
    implements LineStringSerializer<List<Integer>> {

        @Override
        public String serialize(List<Integer> list) {
            StringBuilder sb = new StringBuilder();
            int index = 0;

            for (Integer i : list) {
                sb.append(i.toString());

                if (index < list.size() - 1) {
                    sb.append(",");
                }
            }

            return sb.toString();
        }
    }

    private static class IntListDeserializer 
    implements LineStringDeserializer<List<Integer>> {

        @Override
        public List<Integer> deserialize(String text) {
            return Stream.of(text.split(","))
                         .map(String::trim)
                         .filter(s -> !s.isEmpty())
                         .map(LineStringSerializationFactoryTest::asInteger)
                         .filter(Objects::nonNull)
                         .collect(Collectors.toList());
        }
    }

    private static Integer asInteger(String value) {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException ex) {
            return null;
        }
    }
}

No changes required on both interfaces.

\$\endgroup\$
1
\$\begingroup\$

This may sound really picky, but if this is actually some library code that you would like to reuse in different environments, then you might run into minor issues because of the line :

private static final String ENDL = "\n";

This works fine on Linux machines, but Windows uses \r\n to denote line endings, and Mac traditionally uses \r for line endings (though I believe this is relaxed a little). If you were to run this code in those environments, your outputs may look strange or not separated at all. You can save yourself from some confusion down the road if you modify that line to be :

private static final String ENDL = System.getProperty("line.separator");

or if you're using Java 7 or later :

private static final String ENDL = System.lineSeparator();

This could be an important issue if you were looking to manage a cross platform serialization. If you make a data file on Linux and copy it to Windows, it may not display completely correctly. Even if Windows handles the display correctly (many apps are becoming more line separator agnostic these days), it's still technically bad code hygiene. I have two suggestions for the above code:

  1. What if you eventually want to serialize data that contains newlines? If the data you're about to serialize contains a newline, instead of throwing, you could simply replace it with a magic character that transforms back into the correct newline character for your system when you deserialize.
  2. Using \n is technically fine in the scenario that you're only looking for a character to delimit values, but if you're expecting that the serialized data is human readable on any system that it's created on (or copied to), then you might want to consider changing your delimiter to be something more platform friendly like a comma (,) or pipe (|) character. Alternatively, if making it readable isn't that important, possibly a non printing character? The 0x1E Character is noted as a "Record Separator" in this symbol table.

That being said, take into account how complex and encompassing you want the final solution to be. If you're not comfortable with making those sorts of edge-case resiliency guarantees in your library then these suggestions are pretty moot.

\$\endgroup\$
2
  • \$\begingroup\$ "Mac traditionally uses \r" I'm not sure in what sense you're using "traditionally", but if you mean "has for a while and still does", that's for the old Mac OSes. The new ones use \n. See this answer. \$\endgroup\$
    – Nic
    Dec 10 '15 at 3:14
  • \$\begingroup\$ @QPaysTaxes - That should probably be "Mac traditionally used \r" instead of "uses" \$\endgroup\$
    – C0M37
    Dec 11 '15 at 18:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.