Mengapa java.util.Optional tidak dapat diserialisasi, bagaimana membuat serial objek dengan bidang seperti itu

107

Kelas Enum adalah Serializable sehingga tidak ada masalah untuk membuat serial objek dengan enum. Kasus lainnya adalah di mana kelas memiliki bidang kelas java.util.Optional. Dalam kasus ini, pengecualian berikut akan ditampilkan: java.io.NotSerializableException: java.util.Optional

Bagaimana menangani kelas-kelas seperti itu, bagaimana cara membuatnya bersambung? Apakah mungkin mengirim objek tersebut ke Remote EJB atau melalui RMI?

Ini contohnya:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Optional;

import org.junit.Test;

public class SerializationTest {

    static class My implements Serializable {

        private static final long serialVersionUID = 1L;
        Optional<Integer> value = Optional.empty();

        public void setValue(Integer i) {
            this.i = Optional.of(i);
        }

        public Optional<Integer> getValue() {
            return value;
        }
    }

    //java.io.NotSerializableException is thrown

    @Test
    public void serialize() {
        My my = new My();
        byte[] bytes = toBytes(my);
    }

    public static <T extends Serializable> byte[] toBytes(T reportInfo) {
        try (ByteArrayOutputStream bstream = new ByteArrayOutputStream()) {
            try (ObjectOutputStream ostream = new ObjectOutputStream(bstream)) {
                ostream.writeObject(reportInfo);
            }
            return bstream.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
vanarchi
sumber
Jika Optionalditandai sebagai Serializable, lalu apa yang akan terjadi jika get()mengembalikan sesuatu yang tidak dapat diserialkan?
WW.
10
@WW. Anda akan mendapatkan NotSerializableException,kursus.
Marquis dari Lorne
7
@WW. Ini seperti koleksi. Sebagian besar kelas koleksi dapat diserialkan, tetapi contoh koleksi sebenarnya hanya dapat diserialkan jika setiap objek yang terdapat dalam koleksi juga dapat diserialkan.
Stuart Marks
Pengambilan pribadi saya di sini: bukan untuk mengingatkan orang untuk menguji unit dengan benar bahkan serialisasi objek. Baru saja berlari ke java.io.NotSerializableException (java.util.Optional) sendiri ;-(
GhostCat

Jawaban:

171

Jawaban ini untuk menjawab pertanyaan pada judul "Shouldn't Opsional menjadi Serializable?" Jawaban singkatnya adalah bahwa kelompok ahli Java Lambda (JSR-335) mempertimbangkan dan menolaknya . Catatan itu, dan yang satu ini dan yang ini menunjukkan bahwa tujuan desain utama untuk Optionaldigunakan sebagai nilai hasil fungsi ketika nilai yang dikembalikan mungkin tidak ada. Maksudnya adalah agar pemanggil segera memeriksa Optionaldan mengekstrak nilai sebenarnya jika ada. Jika nilainya tidak ada, pemanggil dapat mengganti nilai default, membuat pengecualian, atau menerapkan beberapa kebijakan lain. Ini biasanya dilakukan dengan merangkai panggilan metode yang lancar dari ujung pipa aliran (atau metode lain) yang mengembalikan Optionalnilai.

Itu tidak pernah dimaksudkan untuk Optionaldigunakan dengan cara lain, seperti untuk argumen metode opsional atau untuk disimpan sebagai bidang dalam suatu objek . Dan dengan ekstensi, membuat Optionalserializable akan memungkinkannya untuk disimpan secara terus-menerus atau ditransmisikan melalui jaringan, yang keduanya mendorong penggunaan jauh melampaui tujuan desain aslinya.

Biasanya ada cara yang lebih baik untuk mengatur data daripada menyimpan Optionaldi lapangan. Jika getter (seperti getValuemetode dalam pertanyaan) mengembalikan aktual Optionaldari lapangan, itu memaksa setiap pemanggil untuk mengimplementasikan beberapa kebijakan untuk menangani nilai kosong. Ini kemungkinan akan menyebabkan perilaku tidak konsisten di seluruh penelepon. Seringkali lebih baik memiliki kumpulan kode apa pun yang bidang itu menerapkan beberapa kebijakan pada saat disetel.

Terkadang orang ingin memasukkan Optionalke dalam koleksi, seperti List<Optional<X>>atau Map<Key,Optional<Value>>. Ini juga biasanya merupakan ide yang buruk. Seringkali lebih baik mengganti penggunaan ini Optionaldengan nilai Null-Object (bukan nullreferensi sebenarnya ), atau hanya menghilangkan entri ini dari koleksi seluruhnya.

Stuart Marks
sumber
26
Kerja bagus Stuart. Saya harus mengatakan itu adalah rantai yang luar biasa jika bernalar, dan itu adalah hal yang luar biasa dilakukan untuk merancang kelas yang tidak dimaksudkan untuk digunakan sebagai tipe anggota contoh. Apalagi jika itu tidak disebutkan dalam kontrak kelas di Javadoc. Mungkin mereka seharusnya mendesain anotasi, bukan kelas.
Marquis dari Lorne
1
Ini adalah entri blog yang sangat bagus tentang alasan di balik jawaban Sutart
Wesley Hartford
2
Untung saya tidak perlu menggunakan bidang serializable. Jika tidak, ini akan menjadi bencana
Kurru
48
Jawaban yang menarik, bagi saya pilihan desain itu sama sekali tidak bisa diterima dan salah arah. Anda mengatakan "Biasanya ada cara yang lebih baik untuk mengatur data daripada menyimpan Opsional di bidang", tentu, mungkin, mengapa tidak, tapi itu harus menjadi pilihan perancang, bukan bahasanya. Ini adalah salah satu dari kasus-kasus ini di mana saya sangat merindukan pilihan Scala di Java (pilihan Scala adalah Serializable, dan mereka mengikuti pedoman Monad)
Guillaume
37
"tujuan desain utama untuk Opsional adalah untuk digunakan sebagai nilai hasil dari fungsi ketika nilai yang dikembalikan mungkin tidak ada.". Nah, tampaknya Anda tidak dapat menggunakannya untuk mengembalikan nilai di EJB jarak jauh. Hebat ...
Thilo
15

Banyak Serializationmasalah terkait dapat diatasi dengan memisahkan formulir serial yang persisten dari implementasi runtime aktual tempat Anda beroperasi.

/** The class you work with in your runtime */
public class My implements Serializable {
    private static final long serialVersionUID = 1L;

    Optional<Integer> value = Optional.empty();

    public void setValue(Integer i) {
        this.value = Optional.ofNullable(i);
    }

    public Optional<Integer> getValue() {
        return value;
    }
    private Object writeReplace() throws ObjectStreamException
    {
        return new MySerialized(this);
    }
}
/** The persistent representation which exists in bytestreams only */
final class MySerialized implements Serializable {
    private final Integer value;

    MySerialized(My my) {
        value=my.getValue().orElse(null);
    }
    private Object readResolve() throws ObjectStreamException {
        My my=new My();
        my.setValue(value);
        return my;
    }
}

Kelas Optionalmengimplementasikan perilaku yang memungkinkan untuk menulis kode yang baik ketika berhadapan dengan nilai yang mungkin tidak ada (dibandingkan dengan penggunaan null). Tapi itu tidak menambah manfaat apa pun untuk representasi data Anda yang persisten. Itu hanya akan membuat data serial Anda lebih besar…

Sketsa di atas mungkin terlihat rumit tetapi itu karena ini menunjukkan pola hanya dengan satu properti. Semakin banyak properti yang dimiliki kelas Anda, semakin banyak kesederhanaannya yang harus diungkapkan.

Dan jangan lupa, kemungkinan untuk mengubah implementasi Mysepenuhnya tanpa perlu menyesuaikan bentuk persisten…

Holger
sumber
2
+1, tetapi dengan lebih banyak bidang akan ada lebih banyak boilerplate untuk menyalinnya.
Marko Topolnik
1
@ Marko Topolnik: maksimal satu baris per properti dan arah. Namun, dengan menyediakan konstruktor yang sesuai class My, yang biasanya Anda lakukan karena berguna untuk penggunaan lain juga, readResolvebisa menjadi implementasi satu baris sehingga mengurangi pelat boiler menjadi satu baris per properti. Yang tidak banyak mengingat fakta bahwa setiap properti yang dapat berubah memiliki setidaknya tujuh baris kode di kelas My.
Holger
Saya menulis posting yang membahas topik yang sama. Ini pada dasarnya adalah versi teks panjang dari jawaban ini: Serialize Opsional
Nicolai
Kelemahannya adalah: Anda perlu melakukan itu untuk kacang / pojo yang menggunakan pilihan. Tapi tetap saja, ide yang bagus.
GhostCat
12

Jika Anda menginginkan opsional yang dapat diserialkan, pertimbangkan untuk menggunakan opsi jambu biji yang dapat diserialkan.

Eric Hartford
sumber
4

Ini kelalaian yang aneh.

Anda harus menandai bidang sebagai transientdan memberikan writeObject()metode kustom Anda sendiri yang menulis get()hasil itu sendiri, dan readObject()metode yang memulihkan Optionaldengan membaca hasil itu dari aliran. Tidak lupa menelepon defaultWriteObject()dan defaultReadObject()masing - masing.

Marquis dari Lorne
sumber
Jika saya memiliki kode kelas, akan lebih mudah untuk menyimpan objek di bidang. Kelas opsional dalam kasus seperti itu akan dibatasi untuk antarmuka kelas (metode get akan mengembalikan Opsional.ofNullable (bidang)). Namun untuk representasi internal, tidak mungkin menggunakan Opsional untuk dengan jelas bermaksud bahwa nilai itu opsional.
vanarchi
Aku baru saja menunjukkan bahwa adalah mungkin. Jika Anda berpikir sebaliknya karena suatu alasan, pertanyaan Anda tentang apa sebenarnya?
Marquis dari Lorne
Terima kasih atas jawaban Anda, ini menambah opsi bagi saya. Dalam komentar saya, saya ingin menunjukkan pemikiran lebih lanjut tentang topik ini, pertimbangkan Pro dan kontra dari setiap solusi. Dalam metode penggunaan writeObject / readObject kita memiliki maksud yang jelas dari Opsional dalam representasi negara, tetapi implementasi serialisasi menjadi lebih rumit. Jika bidang digunakan secara intensif dalam penghitungan / aliran - lebih nyaman menggunakan writeObject / readObject.
vanarchi
Komentar harus relevan dengan jawaban yang muncul di bawah. Relevansi renungan Anda luput dari saya.
Marquis dari Lorne
3

Library Vavr.io (sebelumnya Javaslang) juga memiliki Optionkelas yang dapat diserialkan:

public interface Option<T> extends Value<T>, Serializable { ... }
Przemek Nowak
sumber
0

Jika Anda ingin mempertahankan daftar tipe yang lebih konsisten dan menghindari penggunaan null, ada satu alternatif yang kooky.

Anda dapat menyimpan nilai menggunakan perpotongan tipe . Digabungkan dengan lambda, ini memungkinkan sesuatu seperti:

private final Supplier<Optional<Integer>> suppValue;
....
List<Integer> temp = value
        .map(v -> v.map(Arrays::asList).orElseGet(ArrayList::new))
        .orElse(null);
this.suppValue = (Supplier<Optional<Integer>> & Serializable)() -> temp==null ? Optional.empty() : temp.stream().findFirst();

Memiliki tempvariabel terpisah menghindari penutupan pemilik valueanggota dan dengan demikian terlalu banyak melakukan seri .

Dan Gravell
sumber