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
Instantiki ayrı saat dilimine (İstanbul, New York) uygulanıp ikiZonedDateTimeü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
Instantbir anlıktır ve saat dilimi taşımaz;LocalDateTimebir yerel tarih-saattir ve saat dilimi taşımaz;ZonedDateTimebir bölgesel tarih-saattir ve saat dilimi taşır.- “Aynı anı mı gösteriyor” sorusunu
Instant.equalsveZonedDateTime.isEqualdoğru yanıtlar;LocalDateTime.equalsbu soruyu hiç yanıtlayamaz,ZonedDateTime.equalsise 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/isOverlapgibi 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.