- [Tim] In the last video,
we learned how to create a directory.
We're now gonna have a look
at how to read that existing directory.
Now, a directory's content
is the list of files that it contains.
So, to make sure that we're on the same page,
I've created a new project called Directories,
as you can see on screen.
And inside the project, you can see
that I've created this project structure,
and I'll get you to do the same so you can follow along.
Now, the actual file names here can all be empty
or can have any contents.
That doesn't really matter, so long as they're there.
So you can see that I've made a sub-folder called FileTree,
and starting out, a sub-folder from FileTree, DIR1,
and it's got two files, file1 and file2.txt.
We've got a Dir2 folder.
The Dir2 folder,
again, that's a sub-folder of the FileTree folder.
That's got file1, file2, and file3.txt.
So we'll take our subfolder, Dir3,
that's got file1 and file2.txt, and then moving back up,
back on the FileTree folder, we've got a Dir4 folder
or subfolder that's got the filename
or the folder file3.txt,
and then finally, the FileTree folder itself
has actually got two files in it, file1.txt, file2.txt,
in addition to the various folders,
and we can just collapse those.
And you can see that we've got those two files as well.
So, you basically wanna create
that same directory structure,
and you've seen in a previous video
how to go about the directory structure within IntelliJ.
All right, so now that we've actually got that,
we're going to read the entries in our directory
using the files.newDirectoryStream method.
Now, this method actually returns a stream
which needs to be closed once it's finished,
and consequently, we're going to use
a try-with-resources statement to do that,
and then we'll catch an IOException
just in case there is one along the way.
So, to start that, we're going to start
with Path directory is equal to FileSystems.
Accept those imports.
.getDefault.getPath, as we've done in the past,
and we're going to use the FileTree, which is our subfolder,
and in my case, I'm a Mac.
I'm going to put a forward slash,
Dir2, and I need to change the level to 1.8 here,
so I'm just going to go into Module Settings >
Project, and make sure that I've got Java 8 specified here.
If I do that, that should fix that error.
But again, if you're on a PC, the file path will be
FileTree\\Dir2, and I'll just put that in Windows.
All right, so that's the path, and then we wanna do this
try-with-resources that I talked about.
We'll do try, then in parentheses,
it's going to be DirectoryStream,
and contents is equal to Files,
dot, and it's newDirectoryStream(directory)
in parentheses as you can see there.
And we'll leave that code for now,
but we'll actually catch an IOException,
and if we get one, let's just print out the message.
So e.getMessage.
So firstly, on line 12, we're actually creating
the path to the directory.
Now, we're using FileSystems.getDefault
to get the working directory
so that we don't have to specify the absolute path,
and of course we've talked about that
quite a few times in the past.
Now, when running the application,
the working directory will be the IntelliJ ID project
directory, and so you have to start up the file directory
tree when you get to tack on the rest of the path.
So that's why we've got FileTree here in the path.
Once we've got our path, after also tacking on Dir2,
we'll be going onto line 13.
We're creating the DirectoryStream.
Now, notice that the new DirectoryStream method
returns a DirectoryStream of paths.
You can see that there,
so I'll hover over that.
DirectoryStream of Path is getting returned
from newDirectoryStream.
Now, DirectoryStream is an interface
that implements the Iterable interface, so consequently,
you can iterate over the return paths
to get the contents of the Dir2 folder.
So let's actually put that Iterable code in,
or some code that it'll iterate through.
So it'll for, in parentheses, (Path file: contents),
and let's just print it out.
So print System.out to println(file.getFileName).
So if we do that, we should get the output,
so let's run that.
Let's put this output down the bottom.
And ignoring that first line in red, which is
the Mac warning that should probably go away shortly
once they fix it in a later version of the JDK.
You can see that we've got a Dir entry there
that's been returned, as well as file one, two, and three.
So basically, we've passed the Dir2 folder there,
and it's given us an entry for Dir3,
which is a folder, obviously, but also the three file names.
But notice that only the contents directly
within the Dir2 directory are within the stream.
So in other words, the method only returns
the first level of content, also referred to
as the directory's direct descendants.
Now, it doesn't go into the Dir3 directory in this case
and return its files as well,
but you will see how to walk
an entire directory tree a little bit later on.
Now, when iterating over a DirectoryStream,
a DirectoryIteratorException may be thrown, so
it's actually good practise to explicitly catch that,
and we can actually do that by using the pipe character,
so here, in the catch(IOException), we can put
the pipe character and DirectoryIteratorException,
which is another way to catch both.
So we haven't really used that much in the course.
We normally use a logical or.
The logical or has got the two pipe characters,
but using one like this, well, this is known
as a bitwise-inclusive or.
So when you're actually chaining together exceptions
like this, you can use that
to put them all on the same line.
So that's just a bit of a shorthand way
to avoid you having to create two separate catch statements.
Now, what if you only wanted to list or work
on specific types of files in a directory?
So, for example, let's say the application
works with .DAT files.
Now, sure, you could retrieve everything in the directory
and then check in each pathname,
but there's actually a better way to do it.
Now, the newDirectoryStream method accepts a filter
as a second parameter, and only paths that match the filter
will actually be included in the DirectoryStream.
Now, the second filtering parameter
is referred to as a glob.
Now, what a glob is is a pattern that's similar
to a regular expression, but the syntax is more geared
towards the types of things
you'd wanna do when working with paths.
And I've got a few examples up on the screen now
as you can see there.
We've got a star character to match any string,
which can contain any number of characters.
We've got *.dat there, an example,
will match any path with a .dat extension.
You can see another example there is *.
And then in left and right curly braces.
Well, that'll match any path
that's got the extension .dat or .txt.
The question mark matches exactly one character.
For example, the glob ??? will actually match
any path that contains exactly three characters.
Another example, myfile*, matches any paths
that begin with the text myfile,
and the last one, b?*.text,
well, that will match any path that is at least
two characters long and begin with the character b.
Now, the question mark here forces a second character,
and the star then matches zero or more characters.
Now, there's a lot more than you can do
than what I've shown there.
Now, for the complete glob syntax,
you can actually check the documentation
for the FileSystem.getPathMatcher method,
and I'll just bring that up on the screen quickly
so you can see that.
And it's this one here, the getPathMatcher.
Now, if the glob syntax isn't sufficient for you to describe
the paths you wanna retrieve, what you can also do
is pass a regular expression, and therefore
really take full advantage of the power that they offer.
But the majority of the time, I think you'll find
that the globs should be enough.
All right, so let's go back to IntelliJ.
So if you wanted to actually make a change here and say
to retrieve only the filenames
with one of the files that have got a .dat extension,
on the newDirectoryStream method, as the second argument,
we could put *.dat,
and what I'll do is, just for that to work,
at the moment, we've got all of these .txt files,
but let's just change one.
I'm going to actually refactor and rename
this one in the Dir2 folder.
Let's refactor that to a .dat file extension.
So if we actually run this now,
we should find that it will return only the .dat file.
You can see only file3.dat has returned now,
because that's the only file that meets our criteria,
including the directories as well.
Now, I said earlier that when you retrieve
the entire contents of a directory, you can use
the isDirectory and isRegularFile methods
to separate files from directories.
I said that in a previous video.
But there's actually an easier way to do it.
But this time, a glob won't help,
and that's because globs are used to match patterns
against filenames.
So in other words, they don't operate on file attributes,
but there's a third version of the newDirectoryStream method
that does accept a DirectoryStream.Filter parameter,
and that allows you to write your own filters.
So, let's just have a go at writing a filter
that only returns files and not directories.
Now, the DirectoryStream.Filter interface
has only got one method called accept,
so that's the method that we have to implement.
Now, when accept returns true for a path,
the path will be included in the DirectoryStream results.
So let's go ahead and do that.
So we'll start with this code
up above the path directory code here on line 10.
We'll do DirectoryStream filter
is equal to, and we'll put new DirectoryStream,
and as I'm typing, I should be able
to select DirectoryStream.Filter, like so,
and then I can type in here,
and I'll have to implement the method myself,
so it's going to be public boolean accept(Path path)
throws IOException, and then I'm just going to do
a return Files.isRegularFile(path).
Just tidy that up a bit.
Now, I need to add that other parenthesis
there at the start as well.
And up here, DirectoryStream should actually be
DirectoryStream.Filter, 'cause that's actually
what we're working on, and that keeps it happy.
Now that I've done that, I can actually change
the newDirectoryStream method call.
We'll get rid of the *.dat there,
and we'll change that instead to Filter.
And again, what we've done here
is with the DirectoryStream.Filter.
That interface has only got one method called accept,
and that's the method that I've implemented,
and you have to implement that.
And when accept returns true for a path, the path
is going to be included in the DirectoryStream results.
So what we're doing here is we're actually testing to see
whether the particular path is a regular file.
If that returns true, then that means
it's going to be shown in the lists when we go down
to print it out here on lines 20, on line 20.
Otherwise, it won't be shown.
So if we actually run this now,
you can see, we just get our three files there,
file1.txt, file2.txt, and file3.dat; in other words,
the Dir3 subfolder is now not showing,
because of course that's not a regular file.
So basically, that filter
has now eliminated the Dir directory.
And keep in mind that since there's only one method
to implement, we could also have used
a Lambda expression instead, so let's just quickly see
how we could have done that as well.
So I'm just going to just temporarily copy these out.
Comment these out, rather.
So to do the equivalent with a Lambda,
we will do DirectoryStream.Filter
and then filter equals,
and then write our regular expression,
so p, dash, greater than signs we've one before
for regular expressions, Files.isRegularFile,
and p in parentheses.
And by the way, depending on the order of this course,
you may not actually have got up to Lambda expressions yet.
There's an entire section on Lambda expressions in this
course, and the reason that that's happened like that
is that I've come back to this I/O section
and started adding new content to it,
and I did that because students voted
for me to stop working on the I/O section on the time
and go back and work on the Lamdba expressions
and other sections of the course,
and then I've come back here and finished it.
So, consequently, there's probably a few things
that are out of order.
So if you're not understanding what a Lambda expression is,
keep in mind that there's a future section of the course
where we go through that in detail.
There's a number of videos that explain what it is,
but for now, this is just a shortcut, another way
of actually achieving the same goal.
So if I just do that as I've done there,
using a Lambda expression, run it again,
we should get the same result.
Same result, as you can see on the screen.
So it's basically more concise, but again,
you get exactly the same results as before.
All right, so let's finish the video here.
See you in the next video.