- [Tim] All right, so in the previous video
we wrote this code and made some changes
and talked about catching this ClassNotFoundException.
So let's actually now try running this
to make sure that our main method,
again, this is in Locations.main, and in the main class,
making sure this still actually runs.
So we'll actually run this again.
And we can scroll up there.
You can see that all the
locations are printed to the console,
so that means we're still reading the
objects from the file correctly.
So now that we can see that that's still working okay
with our Locations.java main method,
let's go back and run the application's main method
to make sure the game's still working.
It seems to be working fine still.
Quit out of that.
The game's still working, and our code,
I think you'd agree, looking at
it now, is a lot easier to read.
And because I've saved this in the
previous video, now, and commented that code,
I'll remove this from the source code
and likewise for this commented out code
in our static initializer block.
Let's also remove that code as well
just to clear things up a bit.
The point is, though, looking at that code now,
is our main method and locations class
and the static initializer block
and it's far more concise, I think
you'd agree, and also easier to read.
Now, for fun, let's actually temporarily
comment out the declaration of the
serial version UID in the location class.
Come up here and just comment that out.
You go back to the main method and run it again.
You see, we've now got an error.
In this case, we've got, originally, an
IOException, and then ultimately
we've got a NullPointerException as a result of that one.
And so that's coming back from the other class,
and that's because we didn't
explicitly set the serial version ID.
Because we didn't do that, the compiler
assigned one when it recompiled the location
class, but this means that it no
longer matches the value we wrote to the file.
Which, if you recall, was 1L.
Now because of that, the exception is thrown
when we try to read objects from the file.
So in order to get this to work again,
and if I just run it again, we'll
get the same result, it'll crash.
We actually have to then uncomment
it from the Location class to get
our application to run again.
And here's our application running again.
But actually, what I should do
is just show you something else.
I'm gonna go back and just comment that out again.
Now, in the previous video, I talked about
the fact that an InvalidCLassException
would be thrown when the serial version UIDs don't match.
InvalidClassException is a subclass
of IOException, and so the existing
catch block also catches the InvalidClassException.
So let's actually go back and add
the InvalidClassException to the static
initializer code in the Locations class.
We'll catch that, so we've got IOException here,
so let's actually catch that first.
Catch InvalidClassException, and then
we can just do a print out InvalidClassException
plus e.getMessage(), and then I'll just put a
space there, 'cause that didn't
have one in the output before.
So now if we run that, we actually
see the InvalidClassException's showing
up there because the serial version UIDs no longer match.
Right, and again we'll just go back
to the Location class and we'll uncomment
the serial version UID, run it
again, we should find that it works.
Okay.
Now before we leave this topic,
let's touch on what happens when
two objects that we serialise point to the same object.
And as an example, let's say we want
to serialise a class called Foo, which
contains a field for a Bar object.
Now if two Foo instances point
to the exact same Bar instance,
and we serializer the two Foo instances
to a file, the question is, will
the file contain two Bar objects or just one?
Well, the answer is that it'll only contain one.
Since both Foo instances referred to the same
Bar instance, only one Bar instance is serialised.
So in other words, a serialised file will
only ever contain one copy of the same instance.
So that's pretty cool.
But now imagine that we want to do the
same thing, but for some reason,
we want to create two files.
So we write both Foo instances
to file 1, and then both to file 2.
So how many instances of Bar will be written this time?
Well, the answer in that case is two.
Object instances will be unique
within a file, but not across files.
So in this case, when the two files
are read back into an application,
two distinct instances of Bar would be created.
Now this may not matter, but it
could, which is why it's important
to understand that that can happen in this case.
Of course, in the two file case,
there could also be two instances
of each Foo object read back in as well.
All right, so to close the section
on the Java.IO package, we're gonna
take a look at the Java.IO.RandomAccessFile class.
Now so far, we've been reading
and writing files sequentially.
We start at byte 0 and we read or write bytes,
one after the other until we reach
the end of the file, or until we finish writing the data.
So we don't jump around in the file, in other words.
For example, we don't read bytes 20 to 35
in the file, and then read bytes 50
to 100 and then bytes 10 to 15.
And likewise, we don't write out bytes
100 to 150 and then bytes 20 to 30.
So far, we've only moved forward and we've
done so without skipping any bytes.
Now, let's actually say that we have
thousands of locations in our file,
so many that we don't want to store
the locations in memory because
they'd take up too much room.
So instead, every time a player moved to a new location,
we'd read that location from the file.
So to do that, we'd need to jump to the place
in the file where the information
for that location is stored, rather
than reading the file sequentially,
we'd need to read it in a random fashion in other words.
So this is where the random access file class comes in.
Now honestly, it would be far more efficient
if in that scenario to use an embedded
database like SQLite, especially
when we've got two datasets that are related
to each other, as we do in this case
with the locations and the exits.
'Cause, of course, every location has a set of exits.
But let's just pretend that for
whatever reason, we have to use a flat file.
Now when accessing a file in a random fashion,
it's important to understand the concept of a file pointer.
Now the file pointer is an offset
in the file where the next read or write will start from.
Now for example, if the file pointer
is set to 100, then the next time
we call a read or write method,
the read or write will start at
byte position 100 in the file.
Now if we were reading an int,
the byte set positions 100 to 103 would
be read, since an int is 4 bytes.
The byte position is zero-based.
So the first byte in the file is at position 0.
Whenever we read or write the file, the
file pointer is advanced by the
number of bytes we read or wrote.
So for example, if the file pointer is
equal to 100, and we read five bytes,
then the file pointer will be equal to
105 after the read/write has taken place.
Now also we use the term offset
when randomly accessing files.
So an offset is a byte location in the file.
So for example, if the value has an offset of 100,
that would mean that the value is
located at byte 100 in the file,
so we would want the file pointer
to be set to 100 when we read/write the value.
Now the value could, of course, occupy more than one byte.
But the offset is whether the value's first
byte is located -- the remaining bytes
would follow that first byte sequentially.
Now when using random access files,
we refer to each set of related data as a record.
In our application, the location ID,
description, and exits, make up the record for a location.
When the user moves to a location,
how will we know which bytes to be read from the file?
Well, if all of the locations occupy the same links,
and we wrote them out in order,
the location one followed by location two
followed by location three, etc., well that would be easy.
We could figure out a location's offset
from its location ID and the fixed location length.
For example, let's say every location
occupied 30 bytes of our file.
So the first location's offset would be
zero, and its data would occupy bytes 0 through 29,
and the second location would begin
at byte zero, and end at byte 59, etc.
Then when a played moved to location end,
we'd figure out which bytes to read
using a formula, assuming locations IDs were
one based, so the formula would be
something like -- let's put this in a comment.
Start byte equals, and it'd be intake 1 times 30.
Again, that assumes that each location occupied 30
bytes in the file and end its location here.
So that would be great if we knew that
each location was of a fixed length.
But unfortunately, our records don't have the same length.
The length for each location is different,
and that's because their description
length and the number of X's differ
from location to location.
So because of that, we have to include an index in our file.
So I'm just gonna delete that comment.
And that index stores the offset and
record length for each location.
Now we also have to include the location ID in the index.
Now, given that, reading a location's
going to be a two step process.
Firstly, we're gonna get the index
record for the location, and secondly,
we're gonna use the index values and read the location data.
Now I'm gonna go through a bit more theory in a bit
what we're going to do here, and then
we'll end this video and in the next one,
we'll actually start the coding.
So getting back to what we're trying to do
here with this two step process.
Firstly, we're going to get the index
record for the location, and secondly,
use the index values to read the location data.
So, every index record has to be the same length.
We wouldn't want to have an index for the index, after all.
Now, typically, an index contains a unique
identifier for each data record, the
offset of the record in the file,
and the length of the record.
Now in Java, that's 12 bytes.
The index for our application's going
to contain the location ID, the location's
offset, and its record length,
which matches the typical case.
Now because of the description, the
location record will always be longer
than 12 bytes, so the index will
be much smaller than the location's data.
Now, we can access the index record
we need by reading it from the file
when the user moves to a new location,
or we could load the entire index
into memory when the application starts.
Ideally, we'd want to load the index into memory.
And the obvious reason for that is that
accessing memory is much faster than accessing
files on disc, so loading the index
into memory will always provide better performance.
Now usually, the size of each index record
will be smaller than the size of the data records.
But when that isn't the case, then of course
it wouldn't make sense to load the index
into memory if we're using a random
access file, because memory's tight.
So if we're using a random access
file for other reasons, then it'll
really depend on the specific case.
Another important point.
I've been saying that the first data
record starts at byte 0, but that
actually won't be the case if you
write the index before the data.
We have to save the offset of the
data section, which in our case
is the locations, to the file.
And that's typically saved near the top of the file.
Now sometimes, the number of data
records could also be saved there.
Now for our application, we're actually
gonna load the index into memory.
So our file format will look like the following,
and I'm just going to put some comments in here as well.
So first thing, gonna have the first four bytes
is gonna contain the number of locations.
So that's bytes 0 through 3.
And notice I put this in the right place,
so I'm just comment at the moment to in the main class.
Let's go back to locations and put it in there.
And we'll just put it -- the comment just
above the static initializer block.
So firstly, the first fourt bytes are
gonna contain the number of locations in their game.
Secondly, the next four bytes, we are going to --
it's gonna contain the staff offset of the location section.
That's bytes 4 through 7.
Thirdly, the next section of the
file's gonna contain the index.
Now the index is 1,692 bytes long,
so it'll start at byte 8 and end at byte 1699.
Okay, and then lastly, the final
section of the file is gonna contain
the location records, the actual data from the game.
And that's gonna start at byte 1700.
So when we want to move to a specific offset,
what we'll do is we'll use the random
access file dot sink method to move the file pointer.
Now we won't always have to do this.
When we do, we want to do a bunch
of sequential read/writes, we'll
start by positioning the file pointer
to the offset of where the first read/write will take place.
Now if the next read/write we wanna do will
immediately follow the previous one,
we can just use the readInt, the readLong, the
readUTF etc. and the corresponding functions,
writeInt, writeLong, writeUTF etc. and we've
seen those methods that everyone will
be using those, that's where we've used them before.
The file pointer is always advanced by the number
of bytes read or written, so if the next
piece of data we want to read/write
immediately followed the last piece that
we read or wrote, then the file pointer
will be already positioned correctly.
So in that case, we wouldn't need to call seek() first.
So we only really have to call the seek() method
when we wanna jump to a different offset in the file.
And we'll see examples of this as we move forward.
All right, so let's finish the video here now.
In the next video, we're gonna start by changing
our application to use a random access file and
to load these locations on demand.
So I'll see you in the next video.