1. Overview

When developing software, it’s often necessary to write an object in memory to a file and, conversely, to read file contents into an object. This is simple to do with primitive and String values but becomes more complicated when dealing with data structures and objects.

One common Java data structure is the HashMap. In this tutorial, we’ll cover three methods for reading and writing files with HashMap data: Java Properties, Java object serialization, and JSON serialization using third-party libraries.

2. Using the Java Properties Class

A common application of a map is a properties file, which contains key-value pairs of Strings representing application configuration. The Java Properties class is well-suited to working with String-based HashMaps. For example, let’s create a map of student data:

Map<String, String> studentData = new HashMap<>();
studentData.put("student.firstName", "Henry");
studentData.put("student.lastName", "Winter");

The Properties class implements Map<Object, Object**>, so it’s easy to read in all values from a HashMap:

Properties props = new Properties();
props.putAll(studentData);

We can create a temporary file and use the store method to write the Properties object to the file:

File file = File.createTempFile("student", ".data");
try (OutputStream output = Files.newOutputStream(file.toPath())) {
    props.store(output, null);
}

This method takes an OutputStream (or Writer) and an optional String for adding comments to the file. Here, we can pass in null. If we view the file we created, we can see our student data:

student.firstName: Henry
student.lastName: Winter

To read the file content back into a Properties object, we can use the load method:

Properties propsFromFile = new Properties();
try (InputStream input = Files.newInputStream(file.toPath())) {
    propsFromFile.load(input);
}

Since Properties implements Map<Object, Object> (but contains only String keys and values), we can get our original map back by streaming stringPropertyNames and collecting the results back into a map:

HashMap<String, String> studentDataFromProps = propsFromFile.stringPropertyNames()
  .stream()
  .collect(Collectors.toMap(key -> key, props::getProperty));
assertThat(studentDataFromProps).isEqualTo(studentData);

Using Properties is straightforward, but only if we’re dealing with a HashMap that has String keys and values. For any other mapping, we’ll need to use another strategy.

3. Working with Object Serialization

Java provides Serializable, an interface for converting objects to and from a byte stream. Let’s define a custom class, Student, that contains student data. We’ll have it implement Serializable *(*and set a serialVersionUID as recommended in the documentation):

public class Student implements Serializable {
    private static final long serialVersionUID = 1L;
    private String firstName;
    private String lastName;

    // Standard getters, setters, equals and hashCode methods
}

Next, we’ll create a mapping of student ID to a Student instance:

Map<Integer, Student> studentData = new HashMap<>();
studentData.put(1234, new Student("Henry", "Winter"));
studentData.put(5678, new Student("Richard", "Papen"));

Then, we can use an ObjectOutputStream to write the HashMap to a file:

File file = File.createTempFile("student", ".data");
try (FileOutputStream fileOutput = new FileOutputStream(file); 
  ObjectOutputStream objectStream = new  ObjectOutputStream(fileOutput)) {
    objectStream.writeObject(studentData);
}

This file will contain binary data and is not human-readable. To verify our file is correct, we can read it back into a HashMap using an ObjectInputStream:

Map<Integer, Student> studentsFromFile;
try (FileInputStream fileReader = new FileInputStream(file);
  ObjectInputStream objectStream = new ObjectInputStream(fileReader)) {
    studentsFromFile = (HashMap<Integer, Student>) objectStream.readObject();
}
assertThat(studentsFromFile).isEqualTo(studentData);

Note that we had to cast the result of readObject back to a HashMap<Integer, Student>. We ignore the unchecked cast warning here since the file will only ever contain our HashMap, but type safety should be considered for operational code.

Serializable provides a relatively simple way to serialize and deserialize a HashMap from a file, but it may not always be possible to serialize a class in this way – particularly if we’re dealing with a class that we can’t modify. Luckily, there is still another option.

4. Using JSON Libraries

JSON is a widely-used format for specifying key-value pairs of data. There are several open-source libraries available for working with JSON in Java. One advantage of JSON is that it’s human-readable – a JSON representation of our map of student data from above looks like this:

{
    1234: {
        "firstName": "Henry",
        "lastName": "Winter"
    },
    5678: {
        "firstName": "Richard",
        "lastName": "Papen"
    }
}

Here, we’ll use two of the most well-known libraries, Jackson and Gson, to convert our HashMap to and from a JSON file.

4.1. Jackson

Jackson is a common option for JSON serialization in Java. We’ll only cover the basics necessary to serialize our simple data structure – for more information, see our Jackson tutorials.

Using the same map of student data above, we can create a Jackson ObjectMapper and use it to write our HashMap as JSON to a file:

ObjectMapper mapper = new ObjectMapper();
File file = File.createTempFile("student", ".data");
try (FileOutputStream fileOutput = new FileOutputStream(file)) {
    mapper.writeValue(fileOutput, studentData);
}

Similarly, we can use ObjectMapper to read our file back into a new HashMap instance:

Map<Integer, Student> mapFromFile;
try (FileInputStream fileInput = new FileInputStream(file)) {
    TypeReference<HashMap<Integer, Student>> mapType 
      = new TypeReference<HashMap<Integer, Student>>() {};
    mapFromFile = mapper.readValue(fileInput, mapType);
}
assertThat(mapFromFile).isEqualTo(studentData);

Because HashMap is a parameterized type, we had to create a TypeReference so Jackson knows how to deserialize our JSON file back into a HashMap<Integer, Student>.

No special interfaces or class modifications were necessary to convert our Student class to JSON – we could even drop the use of the Serializable interface. However, it’s also important to note here that during deserialization, Jackson requires the class to have a default no-argument constructor. Though many classes provide one, this requirement can be an issue if the class can’t be changed.

4.2. Gson

Gson is another common choice for JSON serialization in Java.

We’ll again use our map from above and define a Gson instance to serialize it to a JSON file:

Gson gson = new Gson();
File file = File.createTempFile("student", ".data");
try (FileWriter writer = new FileWriter(file)) {
    gson.toJson(studentData, writer);
}

Reading the file back into a HashMap instance is simple:

Map<Integer, Student> studentsFromFile;
try (FileReader reader = new FileReader(file)) {
    Type mapType = new TypeToken<HashMap<Integer, Student>>() {}.getType();
    studentsFromFile = gson.fromJson(reader, mapType);
}
assertThat(studentsFromFile).isEqualTo(studentData);

Similar to Jackson, Gson needs type information to deserialize the parameterized HashMap – this is provided in the form of a Java Reflection API Type using Gson’s TypeToken.

Gson has the same requirement of a default constructor but provides the InstanceCreator interface to assist in the case where one is not provided.

5. Summary

In this tutorial, we discussed three methods for writing to and reading from a file with HashMap data.

For simple mappings of Strings, Java Properties offers us a straightforward solution. Object serialization is another core Java feature that provides us with more flexibility for classes that we can modify. For cases where we can’t edit the class in question (or if we need a human-readable format), open-source libraries like Jackson and Gson provide useful tools for JSON serialization.

As always, all code is available over on GitHub.


» 下一篇: Round the Date in Java