İçeriğe geç
academia.sh

Ders 13 / 15

Dizileştirme

Dizileştirmenin yapıcıyı hiç çalıştırmadığı ve yapıcının kurduğu değişmezlerin bu yüzden dışarıdan gelen bir bayt dizisiyle atlanabildiği ölçülür. Bir geri okuma kancasının denetimi nasıl geri getirdiği ve alan adıyla sürüm kimliğinin nasıl bir sözleşmeye dönüştüğü gösterilir.

İçindekiler

Önceki ders bir dosyanın var olup olmadığını ve hangi baytları taşıdığını ölçtü — ama bir nesnenin kendisi bayta çevrildiğinde ne olduğu hiç sorulmadı. Bu ders dizileştirmeye bakar. Nesneye Dayalı Java kursu (M08/K02, java-siniflar/01, 02) yapıcı zincirini ve sınıf değişmezlerini kurdu: bir nesne ancak yapıcısından geçerek var olabilir, ve yapıcı değişmezi zorlayan yerdir. Dizileştirme bu kuralın dışında durur — bir nesneyi bayta çevirip geri okumak, yapıcıyı hiç çağırmadan bir nesne üretmenin standart kitaplıktaki yoludur.

Kursun sorusu burada yeniden sorulur: yapıcının kurduğu bir değişmez, geri okumada hâlâ geçerli mi? Yanıtı bulmak için tek bir sınıf yeter, çünkü karşılaştırılan iki gerçekleştirim değil, aynı sınıfın iki farklı yoldan — yapıcıdan ve dizileştirmeden — geçerek var olmasıdır.

Üç bölüm sırayla ilerliyor. İlk bölüm yapıcının atlandığını ve bunun bedelini sayıyor. İkincisi bu bedelin nasıl geri kazanılabildiğini gösteriyor. Üçüncüsü ise değişmezlerin ötesine geçip sınıfın kendi şeklinin — alan adlarının ve sürüm kimliğinin — geri okumada nasıl bir sözleşmeye dönüştüğünü ölçüyor.

Bu, K03’ün “çağıranın yükümlülüğü” dediği kaynaklardan biridir, ama alışılmadık bir biçimde: çağıran burada değişmezi kıran taraf değil, değişmezi geri kazanmakla yükümlü taraftır. Kütüphane bir söz vermedi ve tutmadığı bir sözü de yok; boşluğu dolduracak satırı yazmak baştan beri sınıfı tasarlayana aitti.

Yapıcı Hiç Çalışmıyor

  • IO17Hesap sınıfı yapıcısında iki değişmez zorluyor: sahip adı boş olamaz, bakiye negatif olamaz. İkisi de tek bir if satırıyla, yapıcının içinde denetleniyor.
  • IO18 — Elde, yapıcı hiç çağrılmadan üretilmiş bir bayt dizisi var: sahibi boş, bakiyesi negatif bir Hesap kaydı. Bu derste o bayt dizisinin nasıl üretildiği önemli değil; önemli olan geri okunduğunda ne olduğu. Risk, yapıcının kurduğu değişmezlerden kaçının geri okumada geçerli kaldığı sayısıyla ölçülüyor.

Serializable’ı uygulayan her sınıf bu ölçümün konusu olabilir; arayüz hiçbir yöntem istemez, yalnız bir işarettir. Hesap burada seçilmiş özel bir örnek değil, kütüphanenin herhangi bir dizileştirilebilir sınıfa uyguladığı genel bir davranışın tek bir örneğidir. Aşağıdaki ölçüm bu davranışı elde bulunan bir bayt dizisiyle gösteriyor; o dizinin bir dosyadan mı, bir ağ bağlantısından mı, yoksa programın kendi önceki bir çalışmasından mı geldiği ölçüm açısından fark etmiyor — hangi kaynaktan geldiğine bakmaksızın, geri okuma aynı yolu izliyor.

// YapiciAtlanir.java — dizilestirme yapiciyi hic calistirmaz, yapicinin kurdugu degismezler korunmaz
import java.io.*;
import java.util.Base64;

class Hesap implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String sahip;
    private final long bakiye;

    Hesap(String sahip, long bakiye) {
        if (sahip == null || sahip.isBlank()) throw new IllegalArgumentException("bos sahip");
        if (bakiye < 0) throw new IllegalArgumentException("negatif bakiye");
        this.sahip = sahip;
        this.bakiye = bakiye;
    }

    String sahip() { return sahip; }
    long bakiye() { return bakiye; }
}

public class YapiciAtlanir {
    // Yapici hic calistirilmadan uretilmis bir bayt dizisi: sahip bos, bakiye negatif.
    static final String BOZUK_KAYIT =
            "rO0ABXNyAAVIZXNhcAAAAAAAAAABAgACSgAGYmFraXllTAAFc2FoaXB0ABJMamF2YS9sYW5nL1N0cmluZzt4cP/////////OdAAA";

    static int gecerliDegismez(Hesap h) {
        int sayi = 0;
        if (h.sahip() != null && !h.sahip().isBlank()) sayi++;
        if (h.bakiye() >= 0) sayi++;
        return sayi;
    }

    public static void main(String[] args) throws Exception {
        int yapiciEngeli = 0;
        try {
            new Hesap("", -50L);
        } catch (IllegalArgumentException e) {
            yapiciEngeli = 1;
        }
        System.out.println("yapici iki degismezi de zorluyor mu: " + (yapiciEngeli == 1));

        byte[] bayt = Base64.getDecoder().decode(BOZUK_KAYIT);
        Hesap geriGelen;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bayt))) {
            geriGelen = (Hesap) i.readObject();
        }
        System.out.println("geri okuma yapiciyi hic calistirmadan basarili mi: true");
        System.out.printf("yapicinin kurdugu iki degismezden gecerli kalan: %d / 2%n",
                gecerliDegismez(geriGelen));
    }
}
yapici iki degismezi de zorluyor mu: true
geri okuma yapiciyi hic calistirmadan basarili mi: true
yapicinin kurdugu iki degismezden gecerli kalan: 0 / 2

İlk satır, doğrudan yapıcıdan geçmeye çalışan bozuk bir çağrının reddedildiğini doğruluyor: denetim orada çalışıyor. İkinci satır aynı sınıfın aynı bozukluğu taşıyan bir kaydı geri okuyabildiğini gösteriyor — hiçbir istisna düşmüyor, readObject sorunsuzca tamamlanıyor. Üçüncü satır bunun bedelini sayıyor: yapıcının kurduğu iki değişmezden sıfırı geri okumada geçerli kalıyor.

Bunun nedeni ObjectInputStream‘in nasıl çalıştığıdır. Varsayılan geri okuma, sınıfın alanlarına girdi-çıktı akışındaki değerleri doğrudan yazar; bunu yaparken hiçbir yapıcı çağırmaz, hiçbir if satırından geçmez. final alanlar bile bu yoldan yazılabilir, çünkü dizileştirme dilin kendi düzeneğidir ve yapıcının önündeki kapıdan girmez, nesneyi doğrudan bellekte kurar. Bu, K02’de kurulan “bir nesne ancak yapıcısından geçerek var olabilir” kuralının tek istisnasıdır — ve istisna kuralın bir zayıflığı değil, dizileştirmenin tanımının kendisidir: geri okumanın işi bir yapıcı çağırmak değil, bir nesneyi aynen yeniden kurmaktır.

Bu yol tamamen kapalı da değildir: Hesap Serializable’ı hiç uygulamasaydı, writeObject çağrısının kendisi NotSerializableException ile düşerdi ve geri okuma sorusu hiç gündeme gelmezdi. Arayüzü konuşmak, sınıfı bu ikinci yola açan karardır — ve bu karar sınıfı tasarlayanın kendisine aittir, dizileştirmenin dayattığı bir şey değildir.

K02’nin kapsülleme dersi, bir alanı private yapmanın onu yalnız sınıfın kendi yöntemleri aracılığıyla değiştirilebilir kıldığını kurmuştu. Dizileştirme bu duvarı yıkmaz ama yanından dolaşır: private değiştirici hâlâ yoktur, hiçbir sıradan çağrı bakiye alanına dışarıdan doğrudan atama yapamaz — ama geri okuma sıradan bir çağrı değil, sanal makinenin kendi düzeneğidir ve erişim belirtecine hiç takılmaz. Kapsülleme dışarıdan gelen yöntem çağrılarını kısıtlar; dizileştirme bir yöntem çağrısı değil, dilin kendisine ait bir yeniden kurma işlemidir.

Geri Okuma Kancası Denetimi Geri Getiriyor

ObjectInputStream’in belgesi bir kaçış yolu tanımlar: bir sınıf, imzası tam olarak private void readObject(ObjectInputStream) olan bir yöntem tanımlarsa, geri okuma varsayılan alanı doldurma davranışını bırakıp bu yöntemi çağırır. İmza harfiyen uyulması gereken bir sözleşmedir — yöntem public olursa, parametre tipi değişirse ya da adı farklı yazılırsa geri okuma onu hiç görmez ve sessizce varsayılan davranışa döner.

  • IO19Hesap’a readObject adında özel bir yöntem eklenir. Bu yöntem önce defaultReadObject ile alanları her zamanki gibi doldurur, sonra yapıcıdaki aynı iki denetimi tekrarlar.
// GeriOkumaKancasi.java — readObject kancasi yazildiginda iki degismez de geri kontrol ediliyor
import java.io.*;
import java.util.Base64;

class Hesap implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String sahip;
    private final long bakiye;

    Hesap(String sahip, long bakiye) {
        if (sahip == null || sahip.isBlank()) throw new IllegalArgumentException("bos sahip");
        if (bakiye < 0) throw new IllegalArgumentException("negatif bakiye");
        this.sahip = sahip;
        this.bakiye = bakiye;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        if (sahip == null || sahip.isBlank()) throw new InvalidObjectException("bos sahip");
        if (bakiye < 0) throw new InvalidObjectException("negatif bakiye: " + bakiye);
    }
}

public class GeriOkumaKancasi {
    static final String BOZUK_KAYIT =
            "rO0ABXNyAAVIZXNhcAAAAAAAAAABAgACSgAGYmFraXllTAAFc2FoaXB0ABJMamF2YS9sYW5nL1N0cmluZzt4cP/////////OdAAA";

    public static void main(String[] args) throws Exception {
        byte[] bayt = Base64.getDecoder().decode(BOZUK_KAYIT);
        String sonuc;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bayt))) {
            i.readObject();
            sonuc = "acildi";
        } catch (InvalidObjectException e) {
            sonuc = "reddedildi: " + e.getMessage();
        }
        System.out.println("kanca yazildiktan sonra ayni bozuk kayit: " + sonuc);
    }
}
kanca yazildiktan sonra ayni bozuk kayit: reddedildi: bos sahip

Bir önceki bölümde okunan aynı bayt dizisi bu kez reddediliyor. Değişen tek şey sınıfın içine iki satırlık bir yöntem eklenmesi: defaultReadObject çağrısı alanları her zamanki gibi dolduruyor, ardından gelen iki if yapıcıdakiyle birebir aynı denetimi tekrar kuruyor. ObjectInputStream, bir sınıfta readObject adında böyle bir yöntem bulursa varsayılan davranış yerine onu çağırıyor; yöntem bir istisna fırlatırsa geri okuma da yarım kalıyor ve nesne hiç üretilmiyor.

Bu ölçümün söylediği şey şudur: dizileştirmenin sözü eksik değildi. Söz zaten “bir nesneyi aynen yeniden kurarım, alanlarını doğrudan doldururum” biçimindeydi ve bu sözde hiçbir değişmez denetimi yoktu — çünkü o denetim yapıcının işiydi, dizileştirmenin değil. Denetimin geri gelmesi için gereken şey kütüphaneden bir düzeltme değil, sınıfın kendi readObject’ini yazmasıydı; bu satırı yazmak yine çağırana, yani sınıfı tasarlayan tarafa kalıyor.

Kursun üçüncü iddiası burada da doğrulanıyor: bu boşluğun hiçbirini derleyici yakalamıyor. Kancasız Hesap de, kancalı Hesap de aynı sorunsuzlukla derleniyor; derleyici bir sınıfın readObject’i olup olmadığına, olan readObject’in yapıcıdaki denetimleri tekrarlayıp tekrarlamadığına hiç bakmıyor. Serializable bir işaretleyici arayüzdür — hiçbir soyut yöntem taşımaz — ve bu yüzden derleme zamanında zorlanabilecek hiçbir şey yoktur; bütün denetim isteğe bağlı bir yöntemin isteğe bağlı gövdesine bırakılmıştır.

Alan Adı ve Sürüm Kimliği Bir Sözleşmedir

  • IO20 — Elde, sürüm kimliği 42 olan eski bir Hesap kaydı var; o zamanki sınıfta alanın adı bakiye idi. Şimdiki sınıfta aynı alan miktar olarak yeniden adlandırıldı, ama sürüm kimliği hâlâ 42.
  • IO21 — Aynı eski kayıt, bu kez sürüm kimliği 43 olan bir sınıf tanımına karşı okunur — yani sürüm kimliği de değişmiş olsaydı ne olacağı ölçülüyor.
// AlanAdiSozlesmesi.java — alan adi ve dizilestirme surum kimligi sozlesmeye donusuyor
import java.io.*;
import java.util.Base64;

class Hesap implements Serializable {
    private static final long serialVersionUID = 42L;
    private final String sahip;
    private final long miktar;
    Hesap(String sahip, long miktar) {
        if (miktar < 0) throw new IllegalArgumentException("negatif miktar");
        this.sahip = sahip;
        this.miktar = miktar;
    }
    long miktar() { return miktar; }
    String sahip() { return sahip; }
}

public class AlanAdiSozlesmesi {
    // Eski surumde ayni sinif "bakiye" adinda bir alan tasiyordu, "miktar" degil;
    // dizilestirme surum kimligi (42L) o zamandan bu yana degistirilmedi.
    static final String ESKI_KAYIT =
            "rO0ABXNyAAVIZXNhcAAAAAAAAAAqAgACSgAGYmFraXllTAAFc2FoaXB0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAAAAAAABkdAADYWRh";

    public static void main(String[] args) throws Exception {
        byte[] bayt = Base64.getDecoder().decode(ESKI_KAYIT);
        Hesap h;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bayt))) {
            h = (Hesap) i.readObject();
        }
        System.out.println("gecti mi (istisna yok): true");
        System.out.println("sahip alani (adi degismedi): " + h.sahip());
        System.out.println("miktar alani (eskiden bakiye idi): " + h.miktar());
    }
}
gecti mi (istisna yok): true
sahip alani (adi degismedi): ada
miktar alani (eskiden bakiye idi): 0

Geri okuma hiç itiraz etmiyor. sahip alanı adı değişmediği için sorunsuz doluyor: ada. Ama miktar alanı, akıştaki kaydın bakiye diye taşıdığı değeri hiç görmüyorlong tipinin varsayılan değeriyle, sıfırla dolduruluyor. ObjectInputStream akıştaki her alanı adına göre eşliyor; akışta miktar adında bir alan yoksa, sınıftaki miktar hiç dokunulmamış sayılıyor. Sürüm kimliği aynı kaldığı için hiçbir uyarı da çıkmıyor: kütüphane açısından bu geçerli bir geri okumadır.

// SurumKimligiUyusmazligi.java — surum kimligi degistiginde ayni bayt dizisi reddedilir
import java.io.*;
import java.util.Base64;

class Hesap implements Serializable {
    private static final long serialVersionUID = 43L;
    private final String sahip;
    private final long miktar;
    Hesap(String sahip, long miktar) {
        if (miktar < 0) throw new IllegalArgumentException("negatif miktar");
        this.sahip = sahip;
        this.miktar = miktar;
    }
}

public class SurumKimligiUyusmazligi {
    static final String ESKI_KAYIT =
            "rO0ABXNyAAVIZXNhcAAAAAAAAAAqAgACSgAGYmFraXllTAAFc2FoaXB0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAAAAAAABkdAADYWRh";

    public static void main(String[] args) throws Exception {
        byte[] bayt = Base64.getDecoder().decode(ESKI_KAYIT);
        String sonuc;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bayt))) {
            i.readObject();
            sonuc = "gecti";
        } catch (InvalidClassException e) {
            sonuc = "reddedildi: " + e.getClass().getSimpleName();
        }
        System.out.println("surum kimligi 43 iken ayni eski kayit: " + sonuc);
    }
}
surum kimligi 43 iken ayni eski kayit: reddedildi: InvalidClassException

Bu kez aynı eski kayıt daha derine bile inmiyor: sürüm kimliği eşleşmediği için geri okuma alanlara hiç bakmadan reddediliyor. İki ölçüm yan yana konduğunda ters bir sonuç çıkıyor. Sürüm kimliğini değiştirmek, uyumsuz bir kaydı gürültülü biçimde durduruyor. Sürüm kimliğini sabit tutmak — sanki sınıf hiç değişmemiş gibi davranmak — yapı gerçekten değiştiğinde sessizce yanlış bir değer üretiyor. Dizileştirme sürüm kimliği bu yüzden bir sürüm numarası değil, “bu sınıf tanımıyla o akış birbirini tanıyor mu” sorusuna verilen tek bitlik bir yanıttır; alan adlarının kendisi de bu yanıtın bir parçasıdır, çünkü bağlama adla yapılır.

Sürüm kimliği hiç yazılmasaydı sonuç değişmezdi, çünkü derleyici o durumda kimliği sınıfın o anki şekline bakarak kendisi hesaplar; iki farklı şekilden hesaplanan iki kimlik neredeyse her zaman farklı çıkar ve sonuç yine InvalidClassException olurdu. Sürüm kimliğini elle ve sabit yazmak bu otomatik korumayı bilinçli olarak kapatmaktır — geriye dönük uyumluluk isteniyorsa gereklidir, ama isteniyorsa her alan değişikliğinin geri okumaya ne yapacağını da düşünmeyi gerektirir. Aynı mantık eklenen bir alan için de geçerlidir: akışta hiç bulunmayan yeni bir alan, tıpkı miktar gibi, sessizce kendi varsayılan değerini alır.

Özet

  • Dizileştirme yapıcıyı hiç çalıştırmaz; ObjectInputStream alanları doğrudan doldurur ve final alanlar bile bu yoldan yazılabilir.
  • Yapıcının kurduğu iki değişmezden hiçbiri geri okumada kendiliğinden korunmaz: elde bulunan bozuk bir kayıt hiçbir istisna vermeden geri okunur.
  • Sınıfa özel bir readObject yöntemi eklenip yapıcıdaki denetimler tekrarlandığında, aynı bozuk kayıt artık reddedilir; denetim geri gelir çünkü çağıran onu yazmıştır.
  • Alan adı geri okumada bağlama anahtarıdır: akıştaki bir alan sınıfta bulunmuyorsa yok sayılır, sınıftaki bir alan akışta bulunmuyorsa varsayılan değerle sessizce doldurulur.
  • Dizileştirme sürüm kimliği sabit tutulduğunda yapı değişikliği sessizce geçer; sürüm kimliği değiştirildiğinde aynı uyumsuzluk gürültülü biçimde reddedilir.
  • Serializable işaretleyici bir arayüzdür ve hiçbir soyut yöntem taşımaz; bu yüzden bir sınıfın değişmezlerini geri okumada da koruyup korumadığını derleyici hiçbir zaman denetlemez — denetim tamamen isteğe bağlı bir readObject’in gövdesine bağlıdır.

Sonraki Adım

Bu derste ölçülen her şey belleğe alınmış, sabit bir bayt dizisiydi — zaman hiç karışmadı. Ama bir sistemin taşıdığı en yaygın veri türlerinden biri zamanın kendisidir. Sıradaki ders tarih ve zaman API’sine bakar: bir anlık noktanın, bir yerel tarih-saatin ve bir bölgesel tarih-saatin hangi bilgiyi taşıyıp hangisini taşımadığını, ve bu farkın iki değeri karşılaştırırken nasıl sessizce yanlış bir sonuca yol açabildiğini ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat