Logo
Ana Sayfa
ÖZEL KODLAMA DERSLERİ
Yazılım Özel Ders
Tüm birebir programlara genel bakış
Python Yazılım Kursu
Sıfırdan ileri seviyeye birebir Python
Java Yazılım Kursu
OOP odaklı birebir Java eğitimi
AP Computer Science Principles
AP CSP sınav hazırlığı
GRUP DERSLERİ
Python & Django Masterclass
SINIRLI KONTENJAN
Java & Spring Boot Masterclass
SINIRLI KONTENJAN
C# .NET Masterclass
SINIRLI KONTENJAN
VİDEO DERSLER
Sıfırdan Temel Python Kursu
Kendi Hızında Öğren
ÜCRETSİZ
Seviye Testi
Ücretsiz — Python, Java, algoritma seviye testleri
Kariyerini Keşfet
Sertifika Doğrula
Belge numarası ve soyad ile doğrulama

Java'da Serializable Arayüzü ile Nesne Serileştirme Nasıl Yapılır?

Yazar: Berk Keskin 29.08.2026 ~12 dk okuma 2 Okunma
java-serializable-nesne-serilestirme

Java'da nesne serileştirme (serialization), bir nesnenin o anki bellek durumunun bayt dizisine (byte stream) dönüştürülüp dosyaya yazılabilmesini veya ağ üzerinden gönderilebilmesini sağlayan mekanizmadır. Bunun için ilgili sınıfın Serializable arayüzünü uygulaması, nesneyi diske yazmak için ObjectOutputStream, geri okumak için ise ObjectInputStream kullanılması gerekir. Süreç boyunca transient anahtar kelimesiyle işaretlenen alanlar kaydedilmez, sınıf sürüm uyumluluğu ise serialVersionUID ile kontrol edilir. Aşağıda bu mekanizmanın her adımını çalışan örneklerle inceliyoruz.

Java'da Nesne Serileştirme (Serialization) Nedir?

Bir Java programı çalışırken oluşturduğunuz her nesne, JVM'in yönettiği bellek (heap) alanında yaşar ve program kapandığı anda o nesneye ait tüm veriler kaybolur. Nesne serileştirme, işte tam bu noktada devreye girer: bir nesnenin alan değerlerini (state) alıp, diske yazılabilen veya ağ üzerinden iletilebilen doğrusal bir bayt dizisine çevirme işlemidir. Bu bayt dizisi, nesnenin sınıf bilgisini ve alan değerlerini belirli bir formatta taşır; dolayısıyla aynı yapıya sahip bir nesne, bu veriden yeniden inşa edilebilir.

Bu mekanizmanın pratikte üç temel kullanım alanı vardır. Birincisi kalıcılıktır: bir oyun kaydının, kullanıcı ayarlarının veya bir hesaplama sonucunun dosyaya yazılıp program yeniden başlatıldığında aynen geri yüklenmesi gerekir. İkincisi ağ iletişimidir; dağıtık sistemlerde bir nesnenin bir JVM'den başka bir JVM'e, örneğin RMI (Remote Method Invocation) üzerinden gönderilmesi serileştirme sayesinde mümkün olur. Üçüncüsü ise önbellekleme (caching) senaryolarıdır; hesaplanması maliyetli bir nesne, tekrar tekrar üretilmek yerine serileştirilip bellek dışı bir ortamda saklanabilir ve gerektiğinde geri yüklenebilir.

Serileştirmenin tersi işleme deserialization (geri serileştirme) denir: bayt dizisinin okunup orijinal nesne grafiğine dönüştürülmesi sürecidir. Bir nesne serileştirildiğinde sınıf adı, alan isimleri ve değerleri saklanır; deserialization sırasında JVM bu bilgiyi kullanarak nesneyi bellekte yeniden oluşturur. Burada şunu belirtmek gerekir: Java'nın yerleşik serileştirme mekanizması ikili (binary) bir format üretir ve JVM'e özgüdür; buna karşılık Gson veya Jackson gibi kütüphaneler nesneyi insan tarafından okunabilir JSON formatına çevirerek farklı bir amaca hizmet eder. Bu yazının odağı, dilin kendi Serializable altyapısıdır.

Serializable Arayüzü: İşaretleyici (Marker) Arayüz Mantığı

OOP temellerini bilen bir geliştirici için arayüz denince akla genellikle imzası olan, alt sınıfın uygulamak zorunda kaldığı metotlar gelir. Ancak java.io.Serializable bu kalıbın dışında kalan özel bir yapıdır: içinde hiçbir metot bildirimi bulunmaz. Tamamen boş bir arayüzdür ve bu yüzden "işaretleyici arayüz" (marker interface) olarak adlandırılır. Amacı davranış dayatmak değil, sınıfa bir meta veri etiketi iliştirmektir.

Bir sınıf implements Serializable yazdığında, derleyici açısından hiçbir yeni metot uygulanması zorunlu hâle gelmez; kod derleme zamanında bu satırdan dolayı ekstra bir davranış kazanmaz. Asıl etki çalışma zamanında ortaya çıkar: ObjectOutputStream, bir nesneyi yazmadan önce o nesnenin sınıfının Serializable arayüzünü uygulayıp uygulamadığını Java Reflection API üzerinden kontrol eder. Eğer bu işaret yoksa işlem daha ileride bir istisna ile durur. Yani arayüz burada bir sözleşme değil, JVM'in tanıması için bırakılmış bir bayrak görevi görür.

Bu ayrım, normal arayüzlerle (ör. Comparable, Runnable) karşılaştırıldığında daha net anlaşılır: Comparable uygularsanız compareTo metodunu yazmak zorundasınızdır, çünkü arayüz bir davranış sözü verir. Serializable ise davranış değil, izin verir; sınıfın alanlarına dokunmadan, mevcut yapıyı olduğu gibi bırakarak sadece "bu sınıf serileştirilebilir" bilgisini JVM'e iletir. Kalıtım hiyerarşisinde bir üst sınıf bu arayüzü uyguladığında, alt sınıflar da otomatik olarak serileştirilebilir hâle gelir; bu davranışa ileride ayrı bir başlıkta değineceğiz.

Bu tür ince OOP ayrımlarını uygulamalı örneklerle pekiştirmek isteyen öğrenciler için birebir Java özel dersi içeriğinde arayüz, kalıtım ve nesne yönelimli tasarım konuları adım adım işlenir. Marker interface mantığını bir kez kavradığınızda, Java standart kütüphanesindeki benzer yapıları (ör. Cloneable) tanımak da kolaylaşır.

ObjectOutputStream ve ObjectInputStream ile Nesne Yazma/Okuma

Bir nesneyi diske yazmak ve geri okumak, Java'da akış (stream) tabanlı bir zincirleme işlemdir. ObjectOutputStream, ham bayt akışı sağlayan FileOutputStream'ı sarmalayarak ona nesne yazma yeteneği katar; ObjectInputStream ise aynı mantıkla FileInputStream'ı sarmalar. Süreç şu adımlarla ilerler:

  1. Hedef dosyayı temsil eden bir FileOutputStream nesnesi oluşturulur.
  2. Bu akış, nesne yazma yeteneği için ObjectOutputStream ile sarmalanır.
  3. writeObject(nesne) metodu çağrılarak nesnenin durumu bayt dizisine dönüştürülüp akışa yazılır.
  4. Akış kapatılarak veri diske kesin olarak işlenir.
  5. Okuma tarafında bir FileInputStream açılır ve ObjectInputStream ile sarmalanır.
  6. readObject() çağrılır, dönen sonuç ilgili sınıfa cast edilerek orijinal nesne geri elde edilir.

writeObject metodunun rolü, nesnenin sınıf bilgisini ve alan değerlerini belirlenmiş ikili formata çevirip akışa aktarmaktır; readObject ise bu formatı çözüp bellekte yeni bir nesne örneği inşa eder. Dönen değer Object tipinde olduğu için, geri alırken açık bir tür dönüşümü (cast) yapılması gerekir. Aşağıdaki örnek, basit bir dize nesnesini bu akışlarla nasıl yazıp okuyabileceğinizi gösterir:

import java.io.*;

public class SerilestirmeOrnegi {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("veri.ser"))) {
            oos.writeObject("Merhaba Serileştirme");
        }

        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("veri.ser"))) {
            String okunan = (String) ois.readObject();
            System.out.println(okunan);
        }
    }
}

Örnekte dikkat edilmesi gereken nokta, akışların try-with-resources yapısıyla açılmasıdır. Bu yapı, işlem bittiğinde veya bir istisna fırlatıldığında akışın otomatik olarak kapatılmasını garanti eder; manuel close() çağrısını unutmak, dosyanın eksik yazılmasına ya da kaynağın açık kalmasına yol açabilir. Bu tür akış yönetimi pratiklerini gerçek proje senaryolarıyla görmek isteyenler canlı Java video eğitimleri içinde dosya işlemleri ve I/O konularını uçtan uca uygulamalı olarak inceleyebilir.

transient Anahtar Kelimesi ve Kalıcı Olmaması Gereken Alanlar

Bir sınıf Serializable arayüzünü uygulasa bile, içindeki her alanın diskte veya ağ üzerinde saklanması gerekmez. transient anahtar kelimesi tam olarak bu noktada devreye girer: bir alanın başına eklendiğinde, o alan serileştirme sürecine hiç dahil edilmez. ObjectOutputStream, byte akışını oluştururken transient alanları görmezden gelir; bu alanlar dosyaya ya da akışa hiç yazılmaz.

Peki hangi alanlar transient yapılmalıdır? Genel kural şudur: değeri güvenlik açısından hassas olan, çalışma zamanına özgü olan veya yeniden oluşturulması gereken her alan transient adayıdır. Örneğin bir kullanıcı sınıfındaki şifre alanı düz metin ya da hash olarak diske yazılmamalıdır; bir Thread nesnesi zaten kendi yaşam döngüsüne sahiptir ve serileştirilmesi mantıksızdır; bir dosya veya soket bağlantısı (FileInputStream, Socket gibi) işletim sistemi kaynaklarına bağlı olduğu için başka bir JVM'de veya başka bir zamanda anlamsız hale gelir. Bu tür alanları serileştirmeye çalışmak hem gereksiz veri şişmesine hem de NotSerializableException riskine yol açar.

Aşağıdaki tablo, farklı alan tiplerinin serileştirme sürecindeki davranışını özetler:

Alan Tipi Serializable mi? transient mi? Serileştirme Sonucu Davranışı
String (ad, soyad) Evet Hayır Normal şekilde yazılır ve okunur
String (şifre) Evet Evet Akışa yazılmaz, geri okununca varsayılan değere döner
Thread nesnesi Hayır Evet Yazılmaz; transient olmasaydı istisna fırlatırdı
Socket bağlantısı Hayır Evet Yazılmaz; geri okunan nesnede null olarak gelir
int, double gibi ilkel tipler Doğası gereği uyumlu Genelde hayır Değer aynen korunur

Burada dikkat edilmesi gereken önemli bir nokta şudur: transient işaretlenen bir alan geri okunduğunda otomatik olarak eski değerine dönmez. Nesne, ObjectInputStream ile yeniden oluşturulduğunda transient alanlar ilkel tiplerde varsayılan değerlerini (int için 0, boolean için false), referans tiplerinde ise null değerini alır. Bu davranışın somut bir kod örneğiyle nasıl gözlemlendiğini bir sonraki bölümde ayrıntılı olarak göreceğiz.

Örnek Uygulama: Ogrenci Sınıfı ile Uçtan Uca Serileştirme

Teoriyi somutlaştırmak için tek bir sınıf üzerinden baştan sona çalışan bir örnek kuralım. Aşağıdaki Ogrenci sınıfı Serializable arayüzünü uygular, bir serialVersionUID tanımlar ve sifre alanını transient yapar:

import java.io.Serializable;

public class Ogrenci implements Serializable {

    // Sınıfın sürüm kimliğini sabitliyoruz.
    // Bu değer olmadan sınıfa sonradan yeni bir alan eklenirse
    // (örneğin "eposta" alanı), JVM otomatik ürettiği UID'yi
    // değiştirebilir ve eski serileştirilmiş dosyalar okunamaz hale gelir.
    private static final long serialVersionUID = 1L;

    private String ad;
    private int numara;
    private transient String sifre; // diske yazılmayacak alan

    public Ogrenci(String ad, int numara, String sifre) {
        this.ad = ad;
        this.numara = numara;
        this.sifre = sifre;
    }

    public String getAd() { return ad; }
    public int getNumara() { return numara; }
    public String getSifre() { return sifre; }
}

Şimdi bu nesneyi ObjectOutputStream ile bir dosyaya yazalım ve ObjectInputStream ile geri okuyalım:

import java.io.*;

public class SerilestirmeTest {
    public static void main(String[] args) {
        Ogrenci ogrenci = new Ogrenci("Ayşe Yılmaz", 1024, "gizliSifre123");

        // Nesneyi dosyaya yazma
        try (ObjectOutputStream oos =
                new ObjectOutputStream(new FileOutputStream("ogrenci.ser"))) {
            oos.writeObject(ogrenci);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Nesneyi dosyadan geri okuma
        try (ObjectInputStream ois =
                new ObjectInputStream(new FileInputStream("ogrenci.ser"))) {
            Ogrenci okunan = (Ogrenci) ois.readObject();

            System.out.println("Ad: " + okunan.getAd());       // Ayşe Yılmaz
            System.out.println("Numara: " + okunan.getNumara()); // 1024
            System.out.println("Şifre: " + okunan.getSifre());   // null
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Programı çalıştırdığınızda konsolda ad ve numara alanlarının aynen korunduğunu, ancak transient olan sifre alanının null olarak geldiğini göreceksiniz. Bu, bir önceki bölümde açıklanan davranışın doğrudan kanıtıdır. Konuyu pekiştirmek ve OOP temellerinizdeki eksikleri görmek isterseniz Java bilgi seviyeni ölç testiyle kendinizi sınayabilirsiniz.

Kod içindeki yorum satırında da belirtildiği gibi, serialVersionUID tanımlanmadığında sınıfa sonradan alan eklemek ciddi uyumluluk sorunlarına yol açabilir; bu konuyu ilerleyen bölümde ayrıntılı ele alacağız.

NotSerializableException Hatası Ne Zaman Oluşur?

NotSerializableException, bir sınıf Serializable arayüzünü uygulasa bile içindeki alanlardan biri Serializable olmayan bir tipe sahipse ortaya çıkar. Java derleyicisi bu durumu tespit edemez; hata yalnızca serileştirme fiilen çalıştığında görülür. Aşağıdaki örnekte Ogrenci sınıfına Serializable olmayan bir Danisman referansı eklenirse ne olacağını görelim:

class Danisman {
    private String isim;
    public Danisman(String isim) { this.isim = isim; }
}

class Ogrenci implements Serializable {
    private static final long serialVersionUID = 1L;
    private String ad;
    private Danisman danisman; // Serializable DEĞİL!

    public Ogrenci(String ad, Danisman danisman) {
        this.ad = ad;
        this.danisman = danisman;
    }
}

// writeObject çağrıldığı an fırlatılır:
// java.io.NotSerializableException: Danisman

Burada kritik nokta şudur: bu kod sorunsuz derlenir. Derleyici, alan tiplerinin Serializable olup olmadığını kontrol etmez; kontrol yalnızca ObjectOutputStream.writeObject() metodu çalışma zamanında (runtime) nesne grafiğini gezerken yapılır. JVM, serileştirilecek nesnenin içindeki her referans alanı sırayla dolaşır ve Serializable olmayan bir tipe rastladığı anda istisnayı fırlatır.

Bu hatayı çözmenin iki temel yolu vardır:

  • İlgili alanı transient yaparak serileştirme sürecinin dışında bırakmak (alanın değeri gerçekten gerekli değilse).
  • Alanın tipini de Serializable arayüzünü uygulayacak şekilde güncellemek (örneğin Danisman sınıfına implements Serializable eklemek).

Hangi yolun seçileceği, o alanın nesnenin işlevselliği için gerçekten kalıcı olması gerekip gerekmediğine bağlıdır; kalıcı olmayan referanslar için transient, kalıcı olması gereken ama şu ana kadar Serializable yapılmamış sınıflar için ise arayüzü uygulamak doğru yaklaşımdır.

serialVersionUID ve Sürüm Uyumluluğu

serialVersionUID, bir sınıfın serileştirme sürecinde taşıdığı sürüm kimliğidir. JVM bir nesneyi geri okurken, akıştaki (stream) sınıf tanımının serialVersionUID değeri ile o an classpath'te yüklü olan sınıfın serialVersionUID değerini karşılaştırır. Bu iki değer eşleşmezse JVM, sınıfların uyumsuz olduğuna karar verir ve nesneyi geri yüklemeyi reddeder.

Bir sınıfta serialVersionUID alanı açıkça tanımlanmazsa, JVM bunu sınıfın adı, alanları, metotları ve arayüz listesi gibi bilgilerden yola çıkarak otomatik olarak hesaplar. Sorun tam da burada başlar: sınıfa yeni bir alan eklemek, bir metodun imzasını değiştirmek ya da derleyici sürümünü değiştirmek bile bu otomatik hesaplanan değeri farklılaştırabilir. Dolayısıyla eski bir sınıf sürümüyle diske yazılmış bir nesne, sınıfa küçük bir alan eklenmiş yeni sürümüyle okunmaya çalışıldığında InvalidClassException fırlatılır ve hata mesajında iki serialVersionUID değerinin uyuşmadığı açıkça görülür.

Bu riski ortadan kaldırmanın yolu, serialVersionUID'yi JVM'e bırakmak yerine sabit bir long değeri olarak manuel tanımlamaktır:

public class Ogrenci implements Serializable {
    private static final long serialVersionUID = 1L;
    private String ad;
    private int numara;
    // yeni alan eklense bile bu değer sabit kalır
}

Bu şekilde sınıfa sonradan alan eklense dahi serialVersionUID sabit kaldığı için JVM sınıfı uyumlu kabul eder ve eski verinin okunmasına (elbette yeni alan varsayılan değerini alır) izin verir. Bu tür sürüm yönetimi detayları, özellikle uzun ömürlü kurumsal projelerde ciddi hatalara yol açabildiğinden, konuyu uygulamalı örneklerle pekiştirmek isteyenler Java özel ders kapsamında birebir çalışarak bu tür sınıf tasarım kararlarını canlı örnekler üzerinde deneyimleyebilir.

Kalıtımda Serileştirme Davranışı

Kalıtım hiyerarşisinde serileştirme davranışı, hangi sınıfın Serializable arayüzünü implemente ettiğine göre değişir. Eğer bir alt sınıf Serializable ise ancak üst sınıfı değilse, üst sınıftan gelen alanlar serileştirme akışına yazılmaz. Nesne geri okunurken JVM, bu alanları eski değerleriyle geri getiremez; bunun yerine üst sınıfın no-arg (parametresiz) constructor'ını çağırarak o alanları constructor'da atanan başlangıç değerlerine sıfırlar.

Aşağıdaki örnek bu davranışı somut şekilde gösterir:

class Kisi {
    protected String ad;
    public Kisi() {
        this.ad = "Bilinmiyor";
    }
}

class OgrenciAlt extends Kisi implements Serializable {
    private static final long serialVersionUID = 1L;
    private int numara;

    public OgrenciAlt(String ad, int numara) {
        this.ad = ad;
        this.numara = numara;
    }
}

Bu sınıflarda Kisi Serializable değildir. "Ayşe" adıyla oluşturulan bir OgrenciAlt nesnesi serileştirilip geri okunduğunda, numara alanı doğru şekilde korunur; ancak ad alanı serileştirme akışına hiç yazılmadığından, geri okuma sırasında Kisi sınıfının no-arg constructor'ı devreye girer ve ad değeri tekrar "Bilinmiyor" olur. Yani orijinal "Ayşe" değeri kalıcı olarak kaybolur.

Buna karşılık üst sınıf da Serializable arayüzünü implemente ediyorsa, hiyerarşideki tüm sınıfların alanları normal akışla serileştirilir ve geri okunurken hiçbir üst sınıf constructor'ı çağrılmadan nesne doğrudan bellek üzerinde yeniden inşa edilir. Bu nedenle bir sınıf hiyerarşisi tasarlarken, kalıtım zincirindeki hangi sınıfların kalıcı hale getirilmesi gerektiğine baştan karar vermek, ileride veri kaybı yaşanmasını önler.

Sık Sorulan Sorular

Bir Java sınıfı Serializable yapılmazsa ne olur?

Bu sınıftan üretilen bir nesne ObjectOutputStream ile diske veya ağa yazılmaya çalışıldığında JVM NotSerializableException fırlatır ve işlem çalışma zamanında (runtime) hemen durur. Derleme aşamasında herhangi bir uyarı verilmez, çünkü bu kontrol yalnızca serileştirme anında yapılır.

transient ile static alanlar arasındaki fark nedir?

transient, bir örnek (instance) alanının serileştirme sürecine dahil edilmemesini sağlar; nesneye özgüdür ve her nesne için ayrı ayrı işaretlenebilir. static alanlar ise zaten sınıfa aittir, nesneye değil; bu yüzden serileştirme mekanizması static alanları hiçbir zaman yazmaz, transient anahtar kelimesine ihtiyaç duymadan otomatik olarak dışarıda kalırlar.

serialVersionUID tanımlamak zorunlu mudur?

Hayır, zorunlu değildir; tanımlanmazsa JVM otomatik bir değer hesaplar ve sınıf hiç değişmediği sürece bu çalışır. Ancak sınıf yapısında ileride değişiklik yapılacaksa (yeni alan, metot değişikliği vb.) sürüm uyumsuzluğu riskini önlemek için manuel tanımlamak güçlü bir mühendislik pratiğidir.

ObjectOutputStream yerine JSON tabanlı kütüphaneler kullanılabilir mi?

Evet, Gson veya Jackson gibi kütüphaneler nesneleri JSON formatında dosyaya yazıp okumak için kullanılabilir; bu yaklaşım özellikle farklı platformlar arası veri paylaşımında Java'ya özgü ikili serileştirmeye göre daha esnek bir alternatiftir.

NotSerializableException hatası nasıl çözülür?

Öncelikle hata mesajında belirtilen sınıfın Serializable arayüzünü implemente edip etmediği kontrol edilmelidir. Eğer sorun sınıfın kendisi değil de içindeki bir alanın tipiyse (örneğin bir Socket veya Thread referansı), bu alan transient olarak işaretlenerek serileştirme akışının dışında bırakılabilir.

Kalıtımda üst sınıf Serializable değilse alt sınıf yine de serileştirilebilir mi?

Evet, alt sınıf Serializable olduğu sürece serileştirme işlemi çalışır; ancak üst sınıftan gelen alanlar korunmaz ve geri okuma sırasında üst sınıfın no-arg constructor'ı çalıştırılarak bu alanlar başlangıç değerlerine döner.

Serileştirilmiş bir dosya farklı bir Java sürümünde okunabilir mi?

Genel olarak evet, ancak bunun garantisi sınıfın serialVersionUID değerinin ve alan yapısının iki sürüm arasında tutarlı kalmasına bağlıdır. Sınıf yapısı değiştiyse veya serialVersionUID uyuşmuyorsa InvalidClassException alınır.

Java'da nesne serileştirme, OOP temellerini bilen bir geliştirici için basit bir arayüz uygulamasından çok daha fazlasıdır; sınıf tasarımı, sürüm yönetimi ve kalıtım kararlarının bir araya geldiği pratik bir mühendislik konusudur. Bu tür konuları küçük kod parçalarıyla değil, gerçek proje senaryolarıyla pekiştirmek isteyenler Java yazılım kursu içeriğine göz atarak konuyu daha derinlemesine çalışabilir.

Bu içeriğin üretilmesinde yapay zeka araçlarından destek alınmıştır.

İlgili Eğitimler

Berk Keskin — Yazılım Geliştirici ve Eğitmen
Yazar

Berk Keskin Kimdir?

Yazılıma 12 yaşında başladı; bugün öğrencinin seviyesine ve hedefine göre şekillenen sürdürülebilir öğrenme sistemleri tasarlıyor. 300'den fazla kişiye ezber değil, düşünerek kod yazmayı öğretti — Berk Akademi'de izlemeye değil üretmeye dayalı öğrenme kültürünü o kuruyor.

WhatsApp Hemen Ara