İçeriğe geç
academia.sh

Ders 14 / 15

Tarih ve Zaman API'si

Üç tipin (anlık, yerel tarih-saat, bölgesel tarih-saat) 'aynı anı mı gösteriyor' sorusunu doğru yanıtlayıp yanıtlayamadığı koşturularak ölçülür. Saat dilimi verilmeden karşılaştırmanın sessizce yanlış sonuç verdiği ve yaz saati geçişindeki boşluk ile çakışmanın API'nin kural gereği seçtiği yer olduğu gösterilir.

İçindekiler

Önceki ders bir nesnenin bayta çevrildiğinde yapıcının hiç çalışmadığını ölçtü — orada zaman hiç karışmadı, çünkü ölçülen her şey belleğe alınmış sabit bir bayt dizisiydi. Bir sistemin taşıdığı en yaygın veri türlerinden biri zamanın kendisidir, ve zaman üç ayrı tiple temsil edilir: Instant bir anlıktır, LocalDateTime bir yerel tarih-saattir, ZonedDateTime bir bölgesel tarih-saattir. Bu üç tip aynı bilgiyi taşımaz — biri saat dilimini hiç bilmez, biri her zaman bilir — ve bu ders o farkın nerede önemli olduğunu ölçer.

Kursun sorusu değişmedi: bir garanti — burada “bu iki değer aynı anı mı gösteriyor” sorusuna doğru yanıt verme garantisi — hangi tarafın elinde? Yanıt bu kez bir sınıf seçimi değil, bir tip seçimidir. Üç bölüm sırayla ilerliyor. İlki üç tipi aynı soruyla sınıyor. İkincisi bu sorunun yanlış yanıtlandığı somut bir çağıran hatasını ölçüyor. Üçüncüsü ise saat dilimi eklemenin bile her soruyu yanıtlamadığı bir sınır durumu — yaz saati geçişini — ölçüyor.

Bu üçü, bir önceki dersin ölçtüğü şeyin bir başka biçimidir. Dizileştirme dersinde bir bayt dizisinin hangi bilgiyi taşıdığı ölçülmüştü; burada da aynı soru soruluyor, ama nesne değil zaman temsil ediliyor. Bir LocalDateTime, tıpkı alan adı değiştirilmiş bir kayıt gibi, sahip olmadığı bir bilgiyi taşıyormuş gibi kullanıldığında sessizce yanlış sonuç üretiyor — fark, orada bir dizileştirme sürüm kimliğinin, burada bir saat diliminin eksik olması.

Belirlenimcilik notu: bu derste gerçek saat hiç okunmaz. Bütün örnekler sabit bir Instant üzerine ya da sabit bir metinden ayrıştırılmış bir tarihe kurulur; yaz saati geçişleri de sistemin kendi saat dilimi verisinden programatik olarak keşfedilir, tarihi düzyazıda iddia edilmez.

Üç Tip, Aynı Soru

  • IO22 — Tek bir Instant iki ayrı saat dilimine (İstanbul, New York) uygulanıp iki ZonedDateTime üretiliyor; bu iki değer aynı anı gösteriyor ama farklı yerel saatler taşıyor. Dört ayrı yöntem aynı soruyu (“bu ikisi aynı an mı”) yanıtlamaya çalışıyor: Instant.equals, LocalDateTime.equals, ZonedDateTime.equals, ZonedDateTime.isEqual.
// UcTip.java — uc tipten hangisi "ayni ani mi gosteriyor" sorusunu dogru yanitliyor
import java.time.*;

public class UcTip {
    public static void main(String[] args) {
        Instant an = Instant.parse("2026-06-15T13:00:00Z");

        ZonedDateTime istanbul = an.atZone(ZoneId.of("Europe/Istanbul"));
        ZonedDateTime newYork = an.atZone(ZoneId.of("America/New_York"));
        LocalDateTime istanbulYerel = istanbul.toLocalDateTime();
        LocalDateTime newYorkYerel = newYork.toLocalDateTime();

        System.out.println("istanbul yerel saat: " + istanbulYerel);
        System.out.println("newyork  yerel saat: " + newYorkYerel);
        System.out.println();

        System.out.printf("%-40s%s%n", "soru: ayni ani mi gosteriyor", "yanit");
        System.out.printf("%-40s%s%n", "Instant.equals",
                an.equals(Instant.parse("2026-06-15T13:00:00Z")));
        System.out.printf("%-40s%s%n", "LocalDateTime.equals (yerel saatler)",
                istanbulYerel.equals(newYorkYerel));
        System.out.printf("%-40s%s%n", "ZonedDateTime.equals (bolge+yerel+ofset)",
                istanbul.equals(newYork));
        System.out.printf("%-40s%s%n", "ZonedDateTime.isEqual (yalniz an)",
                istanbul.isEqual(newYork));
    }
}
istanbul yerel saat: 2026-06-15T16:00
newyork  yerel saat: 2026-06-15T09:00

soru: ayni ani mi gosteriyor            yanit
Instant.equals                          true
LocalDateTime.equals (yerel saatler)    false
ZonedDateTime.equals (bolge+yerel+ofset)false
ZonedDateTime.isEqual (yalniz an)       true

İki ZonedDateTime aynı Instant’tan üretildi, yani ikisi de birebir aynı anı gösteriyor — biri saat 16.00, öteki saat 09.00 gösterse bile. Dört yöntemden ikisi bunu doğru söylüyor: Instant.equals (zaten aynı anlığı karşılaştırıyor) ve ZonedDateTime.isEqual (yalnız temsil ettiği anı karşılaştırmak için özel olarak yazılmış bir yöntem). LocalDateTime.equals false diyor, ama bu soruyu yanlış yanıtlamıyor — hiç yanıtlayamıyor, çünkü elindeki değerlerin hangi saat dilimine ait olduğuna dair hiçbir bilgi yok; yalnız iki takvim okumasının birbirine eşit olup olmadığını söylüyor. En şaşırtıcı satır üçüncüsü: ZonedDateTime.equals, elinde saat dilimi bilgisi olmasına rağmen false diyor, çünkü o yöntem “aynı an mı” sorusunu değil, “bölge, yerel saat ve ofset birebir aynı mı” sorusunu yanıtlıyor. Aynı tipte iki yöntem var ve ikisi farklı sorulara yanıt veriyor; doğru yöntemi seçmek çağırana kalıyor.

Bu dördü, üç tipin taşıdığı bilginin doğrudan bir sonucudur. Instant bir sayı çizgisindeki tek bir noktadır ve iki nokta aynı yerdeyse equals bunu doğrudan söyler; başka bir sorusu yoktur. ZonedDateTime bu noktayı bir de takvim okumasıyla birlikte taşır — hangi bölgede, hangi yerel saatte olduğunu — ve equals bu ek bilgiyi de karşılaştırmaya katar; iki temsil aynı anı gösterse bile bölgeleri farklıysa equals onları ayrı sayar, çünkü o iki değeri birebir aynı temsil olarak tanımlar, aynı an olarak değil. isEqual bilerek bu ek bilgiyi göz ardı eder ve yalnız temsil edilen ana bakar. LocalDateTime ise bu ek bilginin kendisini hiç taşımaz; elinde karşılaştıracağı bir bölge, bir ofset yoktur, bu yüzden sorulan soruyu değil, sorabileceği tek soruyu — iki takvim okuması eşit mi — yanıtlar.

Çağıranın Kuralı: Saat Dilimi Vermeden Karşılaştırma

  • IO23 — İki kayıt aynı yerel saat değerini taşıyor (09:00) ama biri İstanbul’da, biri New York’ta girilmiş; hangi saat diliminde girildiği bilgisi kayıtla birlikte tutulmuyor. Bu bilgi olmadan yapılan bir karşılaştırma, bilgi verilerek yapılan bir karşılaştırmayla yan yana koşturuluyor.

Bu durum uydurma değildir: bir randevu sistemi, bir günlük kaydı, bir zamanlanmış görev — zamanı kullanıcının kendi yerel saatiyle alan her sistem aynı seçimle karşılaşır. Saat dilimini kayıtla birlikte saklamak bir tasarım kararıdır, dilin kendisi bunu dayatmaz; LocalDateTime bilinçli olarak saat dilimsiz bir tip olarak var eder, çünkü çoğu zaman gerçekten de yalnız bir takvim okuması yeterlidir. Bedel yalnız o okumayı “aynı an” anlamında kullanmaya kalkışıldığında ortaya çıkıyor.

// SaatDilimiEksik.java — saat dilimi verilmeden karsilastirma sessizce yanlis sonuc verir
import java.time.*;

public class SaatDilimiEksik {
    static boolean naifAyniZaman(LocalDateTime a, LocalDateTime b) {
        return a.equals(b);
    }

    static boolean dogruAyniZaman(LocalDateTime a, ZoneId aBolge, LocalDateTime b, ZoneId bBolge) {
        return a.atZone(aBolge).toInstant().equals(b.atZone(bBolge).toInstant());
    }

    public static void main(String[] args) {
        LocalDateTime aKaydi = LocalDateTime.of(2026, 6, 15, 9, 0);
        LocalDateTime bKaydi = LocalDateTime.of(2026, 6, 15, 9, 0);
        ZoneId aBolge = ZoneId.of("Europe/Istanbul");
        ZoneId bBolge = ZoneId.of("America/New_York");

        System.out.println("a kaydi (yerel): " + aKaydi + " (Istanbul)");
        System.out.println("b kaydi (yerel): " + bKaydi + " (New York)");
        System.out.println("naif karsilastirma (saat dilimi vermeden): "
                + naifAyniZaman(aKaydi, bKaydi));
        System.out.println("dogru karsilastirma (saat dilimi verilerek): "
                + dogruAyniZaman(aKaydi, aBolge, bKaydi, bBolge));

        Duration fark = Duration.between(aKaydi.atZone(aBolge).toInstant(),
                bKaydi.atZone(bBolge).toInstant());
        System.out.println("gercek fark: " + fark.toHours() + " saat");
    }
}
a kaydi (yerel): 2026-06-15T09:00 (Istanbul)
b kaydi (yerel): 2026-06-15T09:00 (New York)
naif karsilastirma (saat dilimi vermeden): true
dogru karsilastirma (saat dilimi verilerek): false
gercek fark: 7 saat

Naif karşılaştırma “aynı zaman” diyor, çünkü iki LocalDateTime değeri gerçekten eşit — ikisi de 09:00 okuyor. Ama gerçekte aralarında yedi saatlik bir fark var. Bu kütüphanenin bir kusuru değil: LocalDateTime.equals zaten yalnız iki takvim okumasını karşılaştırdığını söylüyor, başka bir söz vermiyor. Kusur, saat dilimi bilgisini hiç taşımayan bir tipi, sanki taşıyormuş gibi kullanmakta. Doğru karşılaştırma her iki tarafa da saat dilimi ekleyip Instant’a çevirdikten sonra karşılaştırıyor ve doğru yanıtı — false, ve altında yatan yedi saatlik farkı — veriyor. Bu, bir önceki bölümün doğal sonucu: LocalDateTime “aynı an mı” sorusunu yanıtlayamayan tipti; burada aynı eksiklik bir kayıt karşılaştırma hatası olarak somutlaşıyor.

Bu kural derleyici tarafından hiç zorlanmıyor. naifAyniZaman iki LocalDateTime alıp bir boolean döndüren, tamamen geçerli, tipi uyan bir yöntemdir; derleyici onun anlamsal olarak yanlış bir soruya yanlış bir yanıt verdiğini bilemez, çünkü elindeki tek bilgi iki parametrenin de aynı sınıftan olduğudur. Sonuç, önceki derste ölçülen kusurla aynı biçimdedir: hata çalışma zamanında bile bir istisna olarak görünmez, yalnız yanlış bir boolean olarak sessizce geçer. Duration.between çağrısı bu farkı görünür kılmak için her iki tarafın da açıkça bir Instant’a çevrilmesini gerektiriyor — yani doğru karşılaştırmayı yazmanın bedeli, saat dilimini unutmamaktır, unutulduğunda derleyicinin hiçbir uyarı vermeyeceğini bilerek unutmamaktır.

Yaz Saati Geçişinde Boşluk ve Çakışma

  • IO24 — Sistemin New York saat dilimi kuralları, sabit bir başlangıç anından sonraki ilk iki geçiş için sorgulanıyor. İlk geçiş bir boşluktur (bir saatlik bir yerel saat aralığı hiç yaşanmaz), ikincisi bir çakışmadır (bir saatlik bir yerel saat aralığı iki kez yaşanır). Hangi tarihte olduğu düzyazıda iddia edilmiyor; program kendi bulduğu tarihi kullanıyor.

Önceki iki bölüm saat dilimini eksik bir durumu ele aldı; bu bölüm saat dilimi tamsa bile sınırın nerede olduğunu ölçüyor. ZoneId.getRules() bir bölgenin bütün geçiş bilgisini taşıyan bir ZoneRules nesnesi döndürüyor; bu bilgi kodun içinde yazılı bir sabit değil, sanal makinenin kendi saat dilimi verisinden okunuyor. nextTransition bir andan sonraki ilk geçişi buluyor — sonucu hangi anın verildiğine bağlı, ama bu bağımlılık ölçümü belirsizleştirmiyor, çünkü başlangıç anı burada da sabit bir metinden ayrıştırılıyor.

// KuralKesif.java — yaz saati gecisleri sistemin kendi saat dilimi verisinden kesfedilir
import java.time.*;
import java.time.zone.*;

public class KuralKesif {
    public static void main(String[] args) {
        ZoneId bolge = ZoneId.of("America/New_York");
        ZoneRules kurallar = bolge.getRules();
        Instant baslangic = Instant.parse("2020-01-01T00:00:00Z");

        ZoneOffsetTransition ilkGecis = kurallar.nextTransition(baslangic);
        ZoneOffsetTransition ikinciGecis = kurallar.nextTransition(ilkGecis.getInstant());

        System.out.println("ilk gecis bosluk mu: " + ilkGecis.isGap());
        System.out.println("ilk gecis oncesi/sonrasi yerel saat: "
                + ilkGecis.getDateTimeBefore() + " / " + ilkGecis.getDateTimeAfter());
        System.out.println("ikinci gecis cakisma mi: " + ikinciGecis.isOverlap());
        System.out.println("ikinci gecis oncesi/sonrasi yerel saat: "
                + ikinciGecis.getDateTimeBefore() + " / " + ikinciGecis.getDateTimeAfter());

        LocalDateTime boslukIcinde = ilkGecis.getDateTimeBefore().plusMinutes(30);
        ZonedDateTime boslukSonuc = boslukIcinde.atZone(bolge);
        System.out.println();
        System.out.println("bosluktaki yerel saat: " + boslukIcinde);
        System.out.println("varsayilan cozum: " + boslukSonuc);
        System.out.println("yerel saat ileri kaydirildi mi: "
                + !boslukSonuc.toLocalDateTime().equals(boslukIcinde));

        LocalDateTime cakismaIcinde = ikinciGecis.getDateTimeAfter();
        ZonedDateTime erkenOfset = cakismaIcinde.atZone(bolge);
        ZonedDateTime gecOfset = erkenOfset.withLaterOffsetAtOverlap();
        System.out.println();
        System.out.println("cakismadaki yerel saat: " + cakismaIcinde);
        System.out.println("varsayilan cozum (erken ofset): " + erkenOfset);
        System.out.println("gec ofset istendiginde: " + gecOfset);
        System.out.println("yerel saat ayni, aninlar ayni mi: "
                + erkenOfset.toLocalDateTime().equals(gecOfset.toLocalDateTime()) + ", "
                + erkenOfset.toInstant().equals(gecOfset.toInstant()));
    }
}
ilk gecis bosluk mu: true
ilk gecis oncesi/sonrasi yerel saat: 2020-03-08T02:00 / 2020-03-08T03:00
ikinci gecis cakisma mi: true
ikinci gecis oncesi/sonrasi yerel saat: 2020-11-01T02:00 / 2020-11-01T01:00

bosluktaki yerel saat: 2020-03-08T02:30
varsayilan cozum: 2020-03-08T03:30-04:00[America/New_York]
yerel saat ileri kaydirildi mi: true

cakismadaki yerel saat: 2020-11-01T01:00
varsayilan cozum (erken ofset): 2020-11-01T01:00-04:00[America/New_York]
gec ofset istendiginde: 2020-11-01T01:00-05:00[America/New_York]
yerel saat ayni, aninlar ayni mi: true, false

Program kendi başlangıç anından itibaren sistemin saat dilimi kurallarını tarayıp iki geçiş buluyor: biri boşluk, biri çakışma. Boşlukta saat 02.00’den 03.00’e doğrudan atlıyor — 02:30 diye bir yerel saat o bölgede o gün hiç yaşanmıyor. Bu yüzden 02:30’u o bölgeye bağladığımızda API onu olduğu gibi kabul etmiyor; boşluğun uzunluğu kadar ileri kaydırıp 03:30’a taşıyor. Çakışmada ise tam tersi olur: 01:00 o gün iki kez yaşanıyor, biri yaz saatiyle (ofset −4), biri kışla (ofset −5). API bir seçim yapmak zorunda ve varsayılan olarak erken ofseti seçiyor; withLaterOffsetAtOverlap çağrıldığında aynı yerel saat bu kez bir saat sonraki gerçek ana bağlanıyor — yerel saat değişmiyor, ama gösterdiği an değişiyor.

İki durumun neden farklı sonuçlandığı geçişin kendi tanımından çıkıyor. Boşlukta saatler gerçekten atlıyor — 02.00’den sonra bir sonraki gerçek an 03.00 olarak okunuyor — bu yüzden aradaki yerel saatlerin karşılığı olan tek gerçek an, atlamanın bittiği yerdir; API bu yüzden ileri kaydırıyor, geri değil. Çakışmada ise saatler geri sarılıyor: aynı yerel okuma iki farklı gerçek anda beliriyor, ve aralarında bir tercih yapılması gerekiyor çünkü her ikisi de eşit derecede geçerli. getDateTimeBefore ve getDateTimeAfter, bu ölçümde kullanılan iki yöntem, geçişin hangi yönde olduğunu söylüyor: boşlukta ikinci değer birinciden büyük, çakışmada küçük — geçişin bosluk mu çakışma mı olduğu buradan da çıkarılabilir, isGap/isOverlap yalnız bunu adlandırıyor.

Bu bölümün sınırlayıcı ölçümü şudur: saat dilimi eklemek önceki bölümdeki hatayı çözüyor, ama her soruyu yanıtlamıyor. Çakışma durumunda bölgeyle birlikte gelen yerel saat bile tek başına yetmiyor — aynı yerel saat, aynı bölgede, iki ayrı gerçek ana karşılık gelebiliyor, ve hangisinin kastedildiğini API’nin kendisi seçmek zorunda kalıyor. Bu seçim keyfi değil: sistemin saat dilimi kuralları hangi yerel saatlerin var olmadığını ve hangilerinin iki kez var olduğunu tanımlıyor, API da bu kurala göre bir tarafı seçiyor. Seçimin kendisi sabit ve belgelenmiş; ama çağıranın istediği taraf bu olmayabilir, ve bunu fark etmenin tek yolu isGap/isOverlap gibi sorguları kullanmaktır.

Özet

  • Instant bir anlıktır ve saat dilimi taşımaz; LocalDateTime bir yerel tarih-saattir ve saat dilimi taşımaz; ZonedDateTime bir bölgesel tarih-saattir ve saat dilimi taşır.
  • “Aynı anı mı gösteriyor” sorusunu Instant.equals ve ZonedDateTime.isEqual doğru yanıtlar; LocalDateTime.equals bu soruyu hiç yanıtlayamaz, ZonedDateTime.equals ise farklı bir soruyu (bölge, yerel saat, ofset birebir aynı mı) yanıtlar.
  • Saat dilimi verilmeden karşılaştırılan iki yerel tarih-saat, aralarında gerçek bir fark varken bile eşit çıkabilir; kusur kütüphanede değil, tipin taşımadığı bilgiyi varsaymakta.
  • Yaz saati geçişindeki boşlukta bir yerel saat aralığı hiç yaşanmaz ve API onu geçişin uzunluğu kadar ileri kaydırır; çakışmada bir yerel saat aralığı iki kez yaşanır ve API varsayılan olarak erken ofseti seçer.
  • Sınırlayıcı ölçüm: saat dilimi eklemek her soruyu yanıtlamaz. Çakışan bir yerel saat, aynı bölgede bile iki ayrı gerçek ana karşılık gelebilir ve hangisinin kastedildiği yalnız isGap/isOverlap gibi sorgularla anlaşılabilir.
  • Boşluk ile çakışma geçişin yönünden gelir: saatler ileri atladığında ortada kalan yerel saatler hiç yaşanmaz, saatler geri sarıldığında bir yerel saat aralığı iki kez yaşanır.

Sonraki Adım

Bu derste ölçülen her şey sayısal ve takvimseldi. Ama standart kütüphanenin girdi-çıktı katmanında bir de metin eşleştirme sorusu var: bir dizginin belli bir örüntüye uyup uymadığını sormak. Sıradaki ve kursun son dersi düzenli ifadelere bakar — örüntünün derlenmiş bir değer olduğunu, açgözlü ve isteksiz niceleyicinin aynı girdide ayrı yakalama ürettiğini ve geri izlemenin adım sayısıyla ölçüldüğünü gösterir.

İ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