javafiletry-catchtry-with-resourcesautocloseable

How to create a file only once when it's needed?


I'm working on a Java utility. This utility should read some input files (one or several). Each input file contains three types of strings, representing an integer, a float, and a string. The utility has to filter input strings according to their type, and write all of them in 3 separate files (ints, floats, strings).

The utility should not create an output file, if there is no specific representing string in the input. In other words, if none of the input files contain an integer-type string, no "integer_output.txt" file should be created.

So we have to create/open output file only if it's needed, but this "needed" clause can only be discovered at a random moment while reading input lines. And you can't know, when exactly it'll happen, and if it will even happen.

How can I open an output file only if it's needed, not leading to a huge resources overhead when opening/closing this file?

My current code version is (simplified):

for (String inputFile : validInputFiles) {
    try (
            BufferedReader reader = Files.newBufferedReader(Paths.get(inputFile))
    ) {
        String line;
        while ((line = reader.readLine()) != null) {
            if (line.matches("[+-]?\\d+")) { // integer
                BigInteger num = new BigInteger(line);
                try {
                    if (integerCount == 0) {
                        integerWriter = new FileWriter(fullIntegerFileName);
                    }
                    integerCount++;
                    integerWriter.write(line);
                    integerWriter.write(System.lineSeparator());
                    integerWriter.flush();
                } catch (IOException e) {
                    System.out.println("Error while writing to file: " + inputFile);
                }

            } else if (line.matches("[+-]?(\\d+(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?")) { // float
                BigDecimal num = new BigDecimal(line);
                try {
                    if (floatCount == 0) {
                        floatWriter = new FileWriter(fullFloatFileName);
                    }
                    floatCount++;
                    floatWriter.write(line);
                    floatWriter.write(System.lineSeparator());
                    floatWriter.flush();
                } catch (IOException e) {
                    System.out.println("Error while writing to file: " + inputFile);
                }

            } else { // string
                try {
                    if (stats.stringCount == 0) {
                        stringWriter = new FileWriter(fullStringFileName);
                    }
                    stringCount++;
                    stringWriter.write(line);
                    stringWriter.write(System.lineSeparator());
                    stringWriter.flush();
                } catch (IOException e) {
                    System.out.println("Error while writing to file: " + inputFile);
                }
            }
        }

    } catch (IOException e) {
        System.err.println("Error while reading input file: " + e.getMessage());
    }
}

try {
    if (integerWriter != null) {
        integerWriter.close();
    }
} catch (IOException e) {
    System.out.println("Error while closing file: " + fullIntegerFileName);
}

try {
    if (floatWriter != null) {
        floatWriter.close();
    }
} catch (IOException e) {
    System.out.println("Error while closing file: " + fullFloatFileName);
}

try {
    if (stringWriter != null) {
        stringWriter.close();
    }
} catch (IOException e) {
    System.out.println("Error while closing file: " + fullStringFileName);
}

Main steps here:

  1. Read a line from input
  2. Determine if a line represents integer / float / string.
  3. When determined:
    • check if it's a first int/float/string, then we need to open output file
    • write the line to specified output file (assuming it was opened earlier)
  4. Close all output files (if needed) after all input files were filtered.

The problem is, I know it's recommended to use try-with-resources (TWR) with AutoCloseable, but if I do so, then:

At this point, I decided not to use TWR, but to open output files with try-catch only if needed, and close all output files after all filtering actions.

Isn't that a bad way to do? Because now I have to manage opening/closing manually, and my IntelliJ IDEA warns me, that I should use TWR. Seems like it's bad practice.

I thought of another way to do this:

But this way leads to double-reading, which I think is bad too.

And last way I thought of:

But I think it's kinda the same - some sort of double-work.

Taking all that into account, how can I solve this?


Solution

  • I would write a dedicated AutoCloseable class for writing these output files. It would manage a collection of FileWriters and its close method would close them all.

    Here is an example:

    class OutputFilesWriter implements AutoCloseable {
        private final HashMap<String, BufferedWriter> writers = new HashMap<>();
    
        public void writeToFile(String file, String line) throws IOException {
            // not convenient to use Map.computeIfAbsent here because 'newBufferedWriter' throws a checked exception
            var writer = writers.get(file);
            if (writer == null) {
                writer = Files.newBufferedWriter(
                        Paths.get(file),
                        StandardOpenOption.CREATE,
                        // StandardOpenOption.APPEND, // if you like
                        StandardOpenOption.WRITE
                );
                writers.put(file, writer);
            }
            writer.write(line);
            writer.write(System.lineSeparator());
        }
    
        @Override
        public void close() throws Exception {
            closeAll(writers.values()).close();
        }
    
        // adapted from https://stackoverflow.com/a/41019032/5133585, by Holger
        private static AutoCloseable closeBoth(AutoCloseable a, AutoCloseable b) {
            if(a==null) return b;
            if(b==null) return a;
            return () -> { try(AutoCloseable first=a) { b.close(); } };
        }
    
        private static AutoCloseable closeAll(Collection<? extends AutoCloseable> c) {
            return c.stream().map(x -> (AutoCloseable)x)
                    .reduce(null, OutputFilesWriter::closeBoth);
        }
    }
    

    The idea is to manage a hMap<String, BufferedWriter>. Each call to writeToFile specifies a file path and the line to write. If there is already an open writer to that file path, use that. Otherwise create a new writer.

    You can just wrap everything with a big TWR like this:

    try (var writer = new OutputFilesWriter()) {
        // ...
    
        // at some point you'd call one of these
        writer.writeToFile(fullIntegerFileName, line);
        writer.writeToFile(fullFloatFileName, line);
        writer.writeToFile(fullStringFileName, line);
    }