Into Java - Part XVI
Into Java, Part 16
Today we will explore a convenient way to store and retrieve your application data, something that can be really tiresome in other programming languages, but is a breeze with Java. In fact any data may be stored this way, basic data types or classes built by you or Java default classes. More than that, this is a rather fast way to store and read data from disk as well as from other streams. And it is by far the easiest technique to implement.
As we have said, streams may take several forms and today we will look into object streams, both input and output. To handle them we must learn about Serializable, but don't worry, that is only a declarative interface with no methods to take care of.
As our application example we will use our old PaintBox. Until today we have not been able to save our paintings, but now we will enable that using a menu bar item. Yes, we will add menu bars to our repertoire. Using the Swing JMenuBar that is also a piece of cake, as are most of the graphic tools in Swing.
The object stream saves the data to a particular file format not easily read by human eyes. The file format is not important, we do not need to know about its inner anatomy. In fact, we do not need to know that object streams use a file format of their own. But we do have to know that we must use the Serializable interface that resides in the java.io package. It is an empty interface that only tells the Java compiler that we know that this object may get serialized. We need to add implement Serializable to our class header. That is it, you are done!
Briefly though, when an object stream saves your object(s) it needs to save a description of the class, further it saves a seal and some more details, I will not go into the details of that. Important to know though, is that it is the object's state that is saved, that is, any instance variable is saved. This is a good reason to use instance variables sparsely, many times only a few are necessary. Think "do I need to save this variable to get an object back on its feet again?" If the answer is "yes" it needs to be an instance variable, but if the value may be computed from other instance variables it is not a good candidate.
Sometimes we do not want to save an instance variable in any case, perhaps for security reasons or if it holds vital transient information. Perhaps we can ask for it or recompute it, for example machine related information as window sizes, or a value of limited life time. In these cases we use the keyword transient, as
/* a value that is useless on another machine with other screen resolutions */ private transient int windowWidth;
Since it is possible, though tiresome, to exploit the information of the stream, it is good to use transient for any variable you do not want to reveal, or possibly that needs to be encrypted.
The observant reader will notice that the same reasoning must hold for variables declared static that is a class variable common to every instance of the class, while an instance variable holds the state of a distinct object. If we need to recall the value of a static variable we need to take care of that on our own. Having covered this use of static and transient, I consider this topic closed for now.
Finally, reading an object back from an object stream never calls any method nor the constructor, it simply reinstates the proper values in the reincarnated object. Hence, nothing is computed at the rebirth. If this is needed, find some deeper readings on this topic to find out how you can do that.
Reading and writing objects
The name "object stream" implies that only objects may be written and/or read to such streams. That is not true, the methods xxxInt, xxxDouble, etc., (where xxx is replaced by read or write) take care of the basic data types.
While basic data types are read and written with specific methods, reading and writing objects is done with the xxxObject methods. As with getting objects back from data structures, we must apply a cast to the returned value, which is of the basic Object type, to the type we know it should be.
If using this technique for reading and writing data one by one, it is important that the read-back is done in exactly the same order the writing was done, using the matching methods. When you have a lot of data, this is really tiresome. In a moment I will show you a more convenient way.
There is much more to tell on this topic, but I think that will be beyond the scope of this column. One good resource is "Core Java 2, Volume 1", by Horstmann & Cornell, starting at page 664. I would like to mention that there are several ways to twist both how data and what data should be written to the stream. Naturally in such cases we must use refined techniques to get the data back again. The door is open, you decide your direction.
ObjectOutputStream and ObjectInputStream
In our example application, the PaintBox, add two lines in the Paintshape class. First we must import java.io and then tell Java that we want to implement the behaviour of Serializable objects. In this class there are no transient variables or anything else to consider.
Now we are ready with the class PaintShape. Let us continue with PaintPanel and see how we use object streams.
As expected, both ObjectInputStream and ObjectOutputStream are located in java.io which needs to be imported. We will first look into how to save our PaintShape objects.
We will simply make a stream, and then write the objects to that stream. Add a small method saveFile, or any name you like, somewhere in the PaintPanel class, as the figure beneath shows. We do not have to implement Serializable in this class since this class is not going to be saved, we are only interested in saving the painted shapes, aren't we?
If you would like to test it so far, add a single line to the PaintBox class:
- find the constructor we named firstInit.
- as the very first line of the windowClosing method block, add paintArea.saveFile(); immediately before the System.exit(0) line.
This new line will save the painting you sketched when you are exiting the program. Of course you are still not happy with that, there is no way to get the painting back. But in any case, you may find the paint.dat file saved and you can inspect it a little. Some parts of it are readable, but most of it is not.
Please look back into the code. First we create the object output stream, this time wrapped around a FileOutputStream. Recall that an object stream shall be wrapped around anything extending a stream, InputStream or OutputStream.
Kindly note that we are writing the entire Vector storedShapes to the stream. We need not save one shape after another or use another similar scheme, it is possible to save a complete data structure. And if it is, for example, sorted in a certain way, upon reading it back later on, it will still be sorted in the very same way. If there are dependencies between the stored objects, they will be valid after they are read back. Convenient, huh?
Would you like to get the painting back? It is as simple as saving it. Still in the PaintPanel we add a little method like this one.
As with the former method, we have to catch IOException if anything goes wrong with the file. But another exception is introduced, ClassNotFoundException, that is thrown when a class referred to in the object file is not present as a class file. Hence we find it necessary to always bring the class files along with the object file, they need each other. Later on, erase the PaintShape.class file and try to read the data file.
After creating the object input stream around a FileInputStream we read from it. Again please note how conveniently it is done, we simply read an object from the stream and assign the object to an instance variable storedShapes, that is the Vector we used when saving the painting. But since readObject returns an Object we need to apply a cast to a Vector, as we know that storedShapes is declared to be of type Vector.
Assigning a new vector to the old variable is simply a replacement, we replace the old data structure (a Vector) with a new one.
Would you like to go for a test ride? Then simply add the line
as the very last line of the method init in the PaintBox class, after that the paintArea object is instantiated. That is, as soon as the canvas is ready we call the readFile method which replaces the still empty storedShapes Vector with a new stuffed one at start-up time. Try that, and say... Wow!
If you followed the instructions exactly, you now can save your paintings. Unfortunately only one, and you can not erase the data file since that will crash the program at start, due to an IOException. We need a better way to call the save and read methods, don't we?
Adding a menu bar is not hard, but adding a lot of menus and a lot of items to each menu can be tiresome (as the example shows). Today we will only add one menu with four menu items added to it. Here comes one example of what that might look like:
Setting up a menu bar is only done once, as there is only one menu bar in an application. The first two lines create one and set it to the frame, that is setJMenuBar(bar) is called from within the class extending JFrame. In our case that will be PaintBox. I have chosen to put the entire code within a method of its own, secondInit, which we can call from the PaintBox constructor as the second line, between firstInit and init.
The second part of the code is to create a menu, the "File" menu, which we simply add to the menu bar. Having more menus is done the same way, adding each to the same menu bar. Of course, menus may have fancy stuff like mnemonics and so forth, but I will omit that for now.
Then there is the first real menu item created, the "Open 'paint.dat'" item. So far there is nothing to worry about, but of course menus may have icons, mnemonics, key accelerators and so forth, too. But for now we must recall how to add a listener to that item. That is done by adding an ActionListener instantiated on the fly, supplying an actionPerformed method. Finally we add the menu item to the menu.
Each menu item contains an ActionListener object, but it would also be fine to make the whole class implement ActionListener and add the method actionPerformed, containing a few if - if else tests with the very same method calls. The choice is yours, and we have tested both ways in previous IntoJava instalments.
As seen in the code above, we do call the two methods we created in the PaintPanel class. We call System.exit(0) once. We added two separators, for sharpness, and in order to learn how. But we have not seen the method clearShapes that seems to be resident in PaintPanel. Here it comes, a convenient way to start over with a new painting:
It certainly is easy with Java, isn't it? Only a single call to the prefabricated Vector class removes every shape we have made, and we are ready for another sketch. Unfortunately I still do not know how to overcome that the last item is not always cleared, but vanishes as soon as you start to paint a new object. Anyone have an idea? Call me!
Do not forget to remove the lines you added before when testing the first two methods. Compile and go!
Here you have a little application capable of saving your small, cute paintings. And restoring them. Unfortunately you have to use your favourite file manager to copy them into other file names if you (or your kids) like to do a lot of them. So next time we will look into how to use the JFileChooser. At the same time we will briefly look into the handy dialogs that come with Swing, the JOptionPane.
As a convenience I have included all the necessary Java files as they looked before we started with this column:
We have learned how to make easy use of object streams, and that they can handle complete data structures in a pinch. Naturally we can use these streams a lot more, and we will. We will also, in a future column, see how convenient they are when sending data over a network, LAN or WAN.
We have also learned how to add menus to our applications, it looked tiresome, and it sure is most of the time. But not that hard, don't you think?
CU next month.