Reputation: 11
I'm creating an image gallery, which contains photo albums and each album contains pictures. These items are stored in a HashMap like this
HashMap<Album, ArrayList<Picture>> albums=new HashMap<>();
the problem starts when trying to serialize the map, because every Picture object contains an Image Object, so I can pick this Image and create an ImageView more easily for my app, the Picture constructor looks like this:
public Picture(String name,String place, String description, Image image)
I always get this exception:
java.io.NotSerializableException: javafx.scene.image.Image
Is there any way to make my Pictures serializable?
Upvotes: 1
Views: 643
Reputation: 46136
You need to customize the serialization of Picture
. To customize serialization of an object you use the following two methods:
void readObject(ObjectInputStream) throws ClassNotFoundException, IOException
void writeObject(ObjectOutputStream) throws IOException
These methods can have any access modifier but are typically(?) private
.
If you have the following class:
public class Picture implements Serializable {
private final String name;
private final String place;
private final String description;
private transient Image image;
public Picture(String name, String place, String description, Image image) {
this.name = name;
this.place = place;
this.description = description;
this.image = image;
}
public String getName() {
return name;
}
public String getPlace() {
return place;
}
public String getDescription() {
return description;
}
public Image getImage() {
return image;
}
}
You have at least three options regarding the serialization of the Image
.
Serialize the location of the image.
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
in.defaultReadObject();
String url = (String) in.readObject();
if (url != null) {
image = new Image(url);
}
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObjet();
out.writeObject(image == null ? null : image.getUrl());
}
The Image#getUrl()
method was added in JavaFX 9.
Serialize the pixel data of the Image
via the PixelReader
. When deserializing you'll use a PixelWriter
.
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
in.defaultReadObject();
if (in.readBoolean()) {
int w = in.readInt();
int h = in.readInt();
byte[] b = new byte[w * h * 4];
in.readFully(b);
WritableImage wImage = new WritableImage(w, h);
wImage.getPixelWriter().setPixels(0, 0, w, h, PixelFormat.getByteBgraInstance(), b, 0, w * 4);
image = wImage;
}
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeBoolean(image != null);
if (image != null) {
int w = (int) image.getWidth();
int h = (int) image.getHeight();
byte[] b = new byte[w * h * 4];
image.getPixelReader().getPixels(0, 0, w, h, PixelFormat.getByteBgraInstance(), b, 0, w * 4);
out.writeInt(w);
out.writeInt(h);
out.write(b);
}
}
WARNING: Serializing the pixel data in this way is saving the image in an uncompressed format. Images can be quite large which may cause problems for you when using this approach.
This is dependent on the PixelReader
being available, which is not always the case. If you read the documentation of Image#getPixelReader()
you'll see (emphasis mine):
This method returns a
PixelReader
that provides access to read the pixels of the image, if the image is readable. If this method returns null then this image does not support reading at this time. This method will return null if the image is being loaded from a source and is still incomplete {the progress is still <1.0) or if there was an error. This method may also return null for some images in a format that is not supported for reading and writing pixels to.
Outside still-loading and errors, some non-exhaustive testing indicates animated GIFs do not have an associated PixelReader
.
Serialize the actual image file (I don't recommend this one, reasons below).
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
in.defaultReadObject();
if (in.readBoolean()) {
byte[] bytes = new byte[in.readInt()];
in.readFully(bytes);
image = new Image(new ByteArrayInputStream(bytes));
}
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeBoolean(image != null);
if (image != null) {
byte[] bytes;
try (InputStream is = new URL(image.getUrl()).openStream()) {
bytes = is.readAllBytes();
}
out.writeInt(bytes.length);
out.write(bytes);
}
}
This assumes the Image
wasn't loaded from a resource (or at least that the URL has a scheme).
However, as I said I don't recommend this approach. For one, it only allows you to serialize the Picture
instance one time because the original URL is lost after deserialization. You could get around this by storing the location separately but then you might as well use option #1. Then there's also the fact you open up an InputStream
and read from it during serialization; this could be quite unexpected behavior for a developer serializing Picture
instances.
Some notes:
There may be room for optimizations in the code above.
Options #1 and #3 don't take the requested width and height of the image into account. This may lead to having a larger image in memory after deserialization. You can modify the code to fix this.
Your Picture
class seems like a model class. If that's the case, it may be better to simply store the location of the image in a field rather than the Image
itself (this can also make customizing the serialization unneeded); then have other code responsible for loading the actual Image
(e.g. a cache) based on the location stored in Picture
. Either that or allow for lazily loading the Image
in the Picture
instance.
The point is to avoid loading Image
s when you don't need them. For instance, what if part of your UI only wants to display a list of available pictures by name. If you have thousands of Picture
s you'll want to avoid loading thousands of Image
s as you could very easily run out of memory (at even just a few dozen images).
Upvotes: 2