- [Instructor] So far, we've written
the locations and directions data as text,
as well as a binary stream.
Right now, we read and write each object field individually.
For example, we use readInt and writeInt methods
to read and write our location's ID,
and we use readUTF and writeUTF methods
to read and write a location's description.
Now, if an object had 20 fields,
we'd actually have to call 20 methods.
So instead of that, we can use
the ObjectInputStream and ObjectOutputStream classes
to read and write objects as a single unit.
Now, when we write an object to a file,
it has to be translated to a format
that can be stored to a file,
and then reassembled into an object later
when it's read by an application.
Now, the process of translating a data structure or object
into a format that can be stored and recreated
is called serialisation.
Now, in Java, when we want to use an object string
to write and read the objects of a class,
we have to make the class serializable.
We do so by making it implement the Serializable interface.
Now this interface has got no methods,
it's just used to give the JVM a heads up
that we may want to serialise the object to storage
and to read it back again at some point.
Now when we make a class serializable,
it's strongly recommended
that we declare a long field called serialVersionUID.
If we don't do so,
the compiler will give us a warning.
This warning can usually be turned off when using an IDE,
and are unfortunately sometimes off by default,
and it will let them calculate a default value for us.
However, different compiler implementations,
they calculate different default values,
and that can lead to problems down the road
if we change the compiler we're using.
Now, there are compilers, for example,
rather than the one included with the JDK.
For example, a class compiled
with the standard Oracle compiler
for a desktop application
would be compiled by a different compiler
if you were to add it to an android project.
So for that reason,
it's recommended that if we care about serialisation,
we actually set the serialVersionUID field ourselves.
So we set that value.
And it's recommended that the field be private
since no other class should have to use it.
So what is this serialVersionUID field used for?
Well, think of it as a sort of version number for the class.
Now if we don't explicitly set it,
the compiler will create it based on class details
such as the number of fields and methods, et cetera.
And if we ever change a class,
for example, by adding another field or method,
or change a field type,
we'd have to change the serialVersionUID value.
When we read an object from a stream,
the runtime checks the stored serialVersionUID
against the one contained within the compiled class file.
If they don't match, then there's a compatibility problem
and the runtime will throw an InvalidClassException.
Now, if we wanted later versions of an application
to be able to load files created by earlier versions,
we'd have to provide custom versions
of the writeObject and readObject methods
by overriding them,
which are methods we use to read and write objects.
The bottom line here is that
we don't have to worry about declaring
a serialVersionUID field unless A:
we explicitly use serialisation
as we do when we use ObjectInputStream
and the ObjectOutputStream classes, and B:
there may be a compatibility issue down the road.
But some developers and the Java documentation
recommend always declaring when A is true,
that's when we explicitly use serialisation,
or whether B also is true, and that's
if there may be compatibility issues down the road.
So with that theory out of the way,
let's actually go ahead and modify our code
to use the ObjectInputStream and ObjectOutputStream classes.
Our first step is to make the location class serializable
by having it implement the Serializable interface.
Let's go and have a look at that.
So location.
So we just want to change this now
public class Location,
we're going to do implements Serializable,
and noticing that's in the java.io package.
Now we don't get any errors about unimplemented methods
and that's because Serializable doesn't actually have any.
Right, so if we take a look at our Location class,
we notice that at the moment it's actually got three fields.
An int, a String, and a Map.
Now int and String are primitive types,
but Map is an object,
so will that be serialised
when a Location object is written to a file?
Well, in our case it will be,
because LinkedHashMap implements the serializable interface.
So we're actually lucky.
When we write a Location object,
the entire contents of the exits field
will also be serialised.
Now if LinkedHashMap didn't implement serializable,
we'd actually be responsible for writing the code
to store its contents to the file ourselves.
Now, ideally, when we want an object to be serializable,
all of its fields will also be serializable.
So you have to keep that in mind
when writing your own classes.
If a class we want to serialise
will have fields from other classes that you write,
then you need to make sure
those other classes are serializable as well.
But as I mentioned, we're in luck for this application
so serialising Location objects to a file
will be straightforward.
Alright, so our next step
is to declare and assign
the serialVersionUID field in Location.
So we're just going to start by doing that,
and we're going to assign it the number one,
because it's version one,
so we'll do that as private long,
and it's serialVersionUID,
making sure you get the exact spelling there with case,
equals 1L.
And at the moment if we have a look at that,
IntelliJ is complaining that this field is never used,
but unfortunately, that's because it doesn't recognise
the field as the recommended one
to include with serialisation.
So next what we're going to do
is modify the Locations.main method
to write the Location objects using an ObjectOutputStream.
So let's go and have a look at Locations.
So we got our main method here.
Now remember that when the class is loaded,
the static initializer
will run and read the Locations from the locations.dat file.
So that's this code down here
in the static initializer block.
So by the time the main method runs,
we know the Locations are loaded and available to use.
So looking at the code that's there now,
what I'm going to do is comment that out
and write the replacement code.
So I'm just gonna comment that out so we've got that there
if we need to refer back to it at some point,
and actually, what I'll do
is I'll take a copy of this first line,
cause that's going to be the same try with resources,
so I'll paste that in and remove the comment,
and in terms of writing the objects,
we just type for Location location,
it's going to be locations.values(),
and it's locFile.writeObject(Location).
But instead of using the DataOutputStream,
we're going to use the ObjectOutputStream,
like so.
So I think you'll agree that,
looking at the code that we've replaced,
that we've considerably simplified the code.
Now since Location is serializable,
and all of the non-primitive fields
in Location are serializable,
all we really have to do is call writeObject
for each Location object as we've done here on line 31,
and we're actually done.
And also you can see that
we can use a BufferedOutputStream to buffer the outputs,
I'm using that there still.
So literally all we have to do, which you saw me do,
was change the DataOutputStream object
to an ObjectOutputStream as we did there and there
on line 29.
Alright, so let's now run this Locations main method,
and this is not the application's main method,
this is the main method in Locations,
so I'm going to right-click that,
run Locations.main().
Alright, so that's run okay without any errors,
so let's actually have a look at the locations.dat file,
so I'm just going to double-click that.
Now you can see here that it's less human-readable
than it was before,
and in fact we're getting this error here
that IntelliJ is telling us
that the document contains very long lines
and soft wraps were forcibly enabled
to improve editor performance.
And also a note here about the wrong encoding.
But that's actually fine,
because a file containing serialised objects
is meant to be read by a Java application,
and not by a human or an IDE for that matter.
Now we notice that right at the top of the file here,
you can see that the package name is there.
If you look carefully, you can actually see
there's actually some class names as well.
So before we move onto reading the objects,
note that the ObjectOutputStream
implements the DataOutput interface
and the ObjectInputStream implements the DataInputStream.
Now the DataInput and DataOutput interfaces
contain the declarations for the writeInt, readInt,
as well as writeUTF and readUTF methods,
and other read/write specific type methods.
Because of that, object streams can contain
a mix of serialised objects and primitive types.
So in other words, they're not limited
to only containing serialised objects.
In our case, we don't need to mix objects and primitives
but we could do it if we actually needed to.
Alright, so let's close down this locations.dat file
because we really don't need it anymore.
The next thing we want to do, though,
is back in Locations.java,
we want to actually have a look
at modifying the static initializer block.
And again what I'm going to do here
for simplicity I'm going to actually just comment out
the whole while loop because
that's basically the major part that we're changing there,
and I'll just type that in again.
First thing we'll do
is we'll change the DataInputStream again
to an ObjectInputStream,
and just repeat it.
and I'll write it up here, the new code,
so we're gonna start with while not eof,
so that code doesn't change,
and you're going to add a try,
a new Location location equals, cast it as a location,
locFile.readObject(), and print out
Read location
plus location.getLocationID(),
plus a colon there to separate it,
plus location.getDescription(),
then on the next line,
we'll print Found
plus location.getExits().size() plus exits.
Then we'll do locations.put
and it's gonna be location.getLocationID(),
comma, and then location.
So the error you wanna catch here
will be an end of file exception,
so EOFException e,
and we'll set eof to be equal to true,
which will exit the while loop,
an attempt of catching errors,
and I might just put this code at the top here
so it's all together.
We're catching an IOException,
but also we need to catch this exception as well,
the ClassNotFoundException.
So we're gonna do a catch ClassNotFoundException,
we're just gonna print out a message if that happens,
ClassNotFoundException,
do e.getMessage().
Perhaps we'll do that as well if we get one.
Okay.
So a few things to note:
firstly, once again I'm sure you'd agree
the code has been greatly simplified,
we changed the DataInputStream
that was on line 39 to ObjectInputStream,
but the rest of the try statement
pretty much remained the same,
and that's because we're still
using this BufferedInputStream to buffer the data.
Now reading a Location object,
which we're doing here on line 43,
well that actually takes care
of reading all its fields and exits.
The readObject method returns an object,
so we have to cast it to a Location object,
which again we're doing on line 43,
now once the object is read,
all we have to do is add it to the locations map
in the Locations class
which we're doing right here on line 47.
We're also printing out the information
as you can see there,
on lines 44 and 45,
but this time we're getting the values
from the location object created
by the readObject call on line 43.
Now in addition to our other exception,
you saw me having to add another catch
which was the ClassNotFoundException,
and that's here on line 54.
Now this is gonna be thrown
when the runtime reads an object from the stream
and it can't find the corresponding class on the classpath.
Now for example, if another application
that didn't contain the Location class
tries to read the objects inside the locations.dat file,
then it will actually get a ClassNotFoundException.
Right, so let's end the video here
because it's getting a little bit long.
In the next video, we'll come back
and we'll run this Locations.main method
and check that the static initializer block code
is actually working as well.
So I'll see you in the next video.