4

Why does Files.list throw an IOException whereas File.listFiles doesn't?

Looking at the source code of Files.list (Java 8) I am even more curious why there wasn't thrown an UncheckedIOException as it is also thrown inside the iterator.

If I replace File.listFiles-code with Files.list I now need to handle an exception where I didn't really handle one before. Needless to say, that most developer don't even know what they would have to handle at that point ;-) or just put a // TODO and/or e.printStackTrace() there.

That makes the use of the Stream here rather cumbersome as you need to surround it with a try/catch or rethrow the exception, which might not even be possible in legacy code.

So why was this decision taken?

Roland
  • 22,259
  • 4
  • 57
  • 84
  • So don't replace it if you don't need to. The only reason to replace it is really to get the exception: if you don't care, don't. – user207421 Mar 27 '17 at 23:50
  • 1
    well... some of the nio-utils make my life easier, that's why I switched nonetheless. I handle the exception the same way as was done before (so I nearly don't). Additionally I like the "`Stream`-way" of solving things (of course where appropriate) ;-) Just for the `Stream` I could also have gone for `Arrays.stream(file.listFiles())`... but then again I would have to transform it to a path later for other nio-util-calls... So better catching that exception once and have the convenience of using the path rightaway... – Roland Mar 28 '17 at 07:34
  • @Roland Searched exactly what you raised here and it seems to me that this is a mistake by the API developers especially when you consider the older File.listFiles() does not throw a checked exception. When they wrote this API, I'm pretty sure the debate on checked exceptions was settled and agreed that it is no more a good idea. – Srikanth Oct 23 '20 at 10:10

1 Answers1

6

First of all, the developer always has to handle erroneous situations that might occur.

  • File.list():

    Returns null if this abstract pathname does not denote a directory, or if an I/O error occurs.

  • Files.list(Path):

    Throws:

    • NotDirectoryException - if the file could not otherwise be opened because it is not a directory (optional specific exception)
    • IOException - if an I/O error occurs when opening the directory

So the difference is that the developer might easily forget the check for null, which won’t ever be noticed as long as there is no erroneous condition. You will learn it the hard way, when the problem occurs at the customer and your application throws a NullPointerException instead of handling a possibly trivial issue.

This is perfectly illustrated by your statement “Needless to say, that most developer don't even know what they would have to handle at that point”. Indeed, but the compiler will tell you already at compile time, that you have to handle the IOException. Unlike File.list(), where the failure to check for null may be ignored. You still may handle it poorly in either case, but there’s no way to prevent this.

Of course, once you understood that you have to handle the problem, you might ask how you want to handle it, which depends on the kind of problem. Having a return value of null doesn’t tell you anything about the problem. You may check for the “not a directory” condition via File.isDirectory() and hope that it didn’t change in-between, but if the file is a directory, you don’t have any hint at where to start.

In contrast, the thrown IOException does not only allow you to differentiate between NotDirectoryException and other error conditions, IOException is the base class of a forest of specific exceptions which can precisely describe the problem, e.g. AccessDeniedException. Even if the exception has an unspecific type, it might have a meaningful message, you can present to the user.

Note that this is a general pattern with these two APIs:

  • File.renameTo(File)

    Returns:

    true if and only if the renaming succeeded; false otherwise

  • File.delete()

    Returns:

    true if and only if the file or directory is successfully deleted; false otherwise

So what do you do when either of these methods returns false?

  • Files.move(Path,Path,CopyOption...)

    Throws:

    • FileAlreadyExistsException - if the target file exists but cannot be replaced because the REPLACE_EXISTING option is not specified (optional specific exception)
    • DirectoryNotEmptyException - the REPLACE_EXISTING option is specified but the file cannot be replaced because it is a non-empty directory (optional specific exception) AtomicMoveNotSupportedException - if the options array contains the ATOMIC_MOVE option but the file cannot be moved as an atomic file system operation.
    • IOException - if an I/O error occurs

That’s what I call helpful…

  • Files.delete(Path)
    Throws:
    • NoSuchFileException - if the file does not exist (optional specific exception)
    • DirectoryNotEmptyException - if the file is a directory and could not otherwise be deleted because the directory is not empty (optional specific exception)
    • IOException - if an I/O error occurs

Again, much more helpful than a boolean. But note that there’s also boolean deleteIfExists(Path), in case you want to handle that one trivial condition non-exceptionally. Since all other non-trivial conditions are still handled as exception, you can’t confuse them.

Of course, the API designers could have used unchecked exceptions, but where would that lead to?

That makes the use of the Stream here rather cumbersome as you need to surround it with a try/catch or rethrow the exception, which might not even be possible in legacy code.

Exactly. You would change code that never threw such an exception (because File.list() returns null in the erroneous case) to call the new method that might throw the unchecked exception, because that’s “possible in legacy code”—but the legacy caller isn’t expecting that exception and would never handle it.

Catching the exceptions and behave exactly like before when null was returned (if you ever checked that in the old code), might indeed be cumbersome, but that’s not the intended way to deal with such situations, so why should that made comfortable…

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Agreed. Erroneous situations should be dealt with and the checked exception forces one to do so. I also agree, that specific exceptions are more helpful than a return value of `null` or `false` for anything that might have gone wrong. But if it's legacy code, I now have to catch an exception and rethrow it as a runtime exception to get a similar behavior as before? The benefit of doing this is obvious but it didn't interest anyone so far in that specific code I am dealing with anyhow ;-) – Roland Mar 18 '17 at 02:03
  • I could also just use `Arrays.stream(file.listFiles())` and can have streams and the same bad exception-handling-behaviour as before. Why no `UncheckedIOException` was chosen instead of `IOException` isn't that clear to me. The ones who deal with erroneous situations, do it regardless of whether an exception is checked or unchecked. The others may not even handle the checked one correctly. So maybe the question rather becomes: why was checked preferred over unchecked? That however might be a philosophical question? Thanks in any case for your helpful answer. – Roland Mar 18 '17 at 02:12
  • 1
    Well, catching a checked exception and rethrowing it wrapped in an unchecked exception makes it explicit. In case of `Files.list()`, an `UncheckedIOException` is thrown when an `IOException` happens in the middle of the stream operation, as the underlying code has to face the same issue, handle it while the calling API doesn’t allow checked exceptions. That’s what it for, as a last resort, not as a general pattern. The better solution would be a type system that allows declaring constructs like “this method might throw whatever this function argument may throw”, but Java is not ready for this… – Holger Mar 20 '17 at 09:19
  • Can't praise this enough. And my problem wasn't even the one of the original poster. People need to appreciate the advantages of nio! Files.move() is a great improvement over File.renameTo() and the even worse commons-io moveFile() - which has no REPLACE-EXISTING option and throws an exception in that case. Thanks again! – Steve Cohen Jan 25 '18 at 21:40