During software development a class goes through several modifications or versions. Normally this is no problem. There is one situation, however, in which you need to take special action whenever you change a class.
If your class is Serializable and you write objects of the class to an ObjectOutputStream and then later read them back in with an ObjectInputStream, you need the same version of the class to read with that you used to write it. If you want to use one version of a class to save something that acts like an object data base, and then change the class (new version) you won't be able to read your file with the new version and will be forced to reconstruct it from scratch unless you take special precautions when you first define the class and take special action when you update the class. This note tells you how to update such a class.
This note will only cover the most difficult case, in which the class to be versioned contains some private data that has no accessor function.
You first need to define a new interface and have the class that needs to be updated implement this interface.
interface Versionable { Object newVersion(Object params); }
Your class needs to implement this interface, but can do so trivially. Here we take a simple example that we will carry through with.
class Filer implements Serializable, Versionable //Original { public String toString(){return "" + x ;} private int x = 999; public Object newVersion(Object o) { return null; } }
Suppose that we have written these objects to a file. We will only consider the simple case in which a single object has been written.
Filer f = new Filer(); try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream (filename)); out.writeObject(f); } catch (IOException io) { System.out.println("cant open output file"); }
At some later time we need to read these objects back in.
try { ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename)); Filer newf = (Filer)in.readObject(); System.out.println(newf); System.out.println(newf.getClass().getName()); } catch (ClassNotFoundException e) { System.out.println("Illegal Class def"); } catch(StreamCorruptedException sc) { System.out.println("Stream corrupted"); } catch(IOException io) { System.out.println("IO exception reading"); }
If the class Filer has changed in the interim, the readObject will fail with a ClassNotFoundException.
To change the class Filer, write a new class instead that has a new name and that you can build by starting with a copy of Filer and then making suitable modifications. Suppose we want to modify Filer by adding a new variable named y. Here we will call the new class FilerTemp and will add the new private variable. This class will need a constructor that can set all relevant instance variables. The new class must also implement Versionable as it will eventually replace Filer.
public class FilerTemp implements Serializable, Versionable { FilerTemp(int x, int y) { this.x = x; this.y = y; } public String toString(){return "" + x + " " + y;} public Object newVersion(Object o) { return null; } private int x; private int y; }
You now have both the original class Filer and the new class FilerTemp. You can read the original file using Filer and you want to write it using FilerTemp. The problem is to create a new object of type FilerTemp for each object of type Filer. To do this you make the following modification to the body of the newVersion method of class Filer (the original class). This change in the class won't affect readability as it doesn't change the class signature.
class Filer implements Serializable, Versionable //Original { public String toString(){return "" + x ;} private int x = 999; public Object newVersion(Object o) { return new FilerTemp(this.x, ...whatever value is needed); } }
Here you can use the parameter o of newVersion pass in arbitrary data needed for the extra parameters of the constructor.
Next you read the old file and write a new file with the objects obtained from calling this versioning method.
public static void updateFileForNewVersion(String filenameIn, String filenameOut) { try { ObjectInputStream in = new ObjectInputStream(new FileInputStream(filenameIn)); /* read file */ Versionable f = (Versionable)in.readObject(); /* transform version */ Object f2 = f.newVersion(null); //This is the version transfomation /* write file */ try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream (filenameOut)); out.writeObject(f2); } catch (IOException io) { System.out.println("cant open output file for new version"); } } catch (ClassNotFoundException e) { System.out.println("Illegal Class def"); } catch(StreamCorruptedException sc) { System.out.println("Stream corrupted"); } catch(IOException io) { System.out.println("IO exception in transform"); } finally { try { System.in.read(); // prevent console window from going away } catch (java.io.IOException e) {} } }
This function will read the original file, transform the results using the newVersion method and then write the result back to a new file. The input file name should be that of the file originally written. The output file name should probably be new to prevent disasters. Since you read the original file and it has a Filer object in it, the variable f will be a Filer. Therefore its newVersion function is the one written above. Here we have not passed in a parameter but you may need to.We also then know that the F2 object is a FilerTemp.
Also, if the situation is more complex and (say) your file has a vector of Filers in it instead of a single one, then you can read in the vector and transform each of the elements in the vector, creating a new vector with the results. Finally you would then write that new vector, instead of a single object. Therfore all three steps (read, transform, write) might need to be more complex. Note that the Vector itself need not be Versionable, just its contents.
Now you have updated the file with objects of a new type, but that type has a new name. If you are happy with the new name (here FilerTemp) then you are done at this point. You can however, make an additional change so that you can continue to use the same name (Filer) for the new class.
To "recapture" the name Filer for our class you must do this. Make an exact copy of FilerTemp and call it Filer. You now have lost the original class named Filer. In the class FilerTemp reimplement the newVersion method as follows.
public class FilerTemp implements Serializable, Versionable { FilerTemp(int x, int y) { this.x = x; this.y = y; } public String toString(){return "" + x + " " + y;} public Object newVersion(Object o) { return new Filer(x,y); } private int x; private int y; }
You can now reexecute updateFileForNewVersion reading the file written above and writing a new file. At this point the file just written has the same data in it as the one read, except for the name of the class of the object stored in it. This is because the transform step creates Filer objects for the new class Filer.
At this time you can abandon the class FilerTemp and continue with your work.
Note that this sequence of changes must take place in steps with runs of updateFileForNewVersion in between. You need to do this each time you make a change in Filer that changes its signature. The signature will change if you add (or remove) an instance variable or method, rename anything, or change the parameters or return type of any method. Adding an inner class may or may not change the signature, depending on how that new inner class interacts with the containing class.
Filer.java (both versions)
FilerTemp.java (latest version)
NewVersion.java (contains a tester and updateFileForNewVersion)
NOTE: Part of the necessity for this complex mechanism is the fact that we are saving and restoring private variables. There are security issues here, of course. However, if your class has accessors for all variables, then a simpler mechanism can be used and you don't need the Versionable interface or the newVersion method. We need it here because we need some mechanism within the class to be modified so that it has access to those private variables. With accessors for all fields we can just assure that the constructor for the new version transfers all of the data from the object of the old version to that of the new version by calling those accessors as appropriate.
Another mechanism that might be made to work is to use sub packages for the various versions and just keep all old versions available. Thus you could have
package filer.version1;
originally and then use
package filer.version2;
and so on when classes need to be changed. You would still need to implement Versionable and move the old objects to the new format, but the classes could have the same names in the different packages.