Ders 15 / 15
Düzenli İfadeler
Örüntünün derlenmiş bir değer olduğu, tam eşleşme ile aramanın ayrı sözleşmeler taşıdığı ve açgözlü ile isteksiz niceleyicinin aynı girdide ayrı yakalama ürettiği ölçülür. Örüntüyü her çağrıda yeniden derlemenin etkisiz olduğu ve geri izlemeli eşleştirmede adım sayısının girdiyle doğrusal artmadığı gösterilir.
İçindekiler
Önceki ders üç zaman tipinin hangi bilgiyi taşıyıp hangisini taşımadığını ölçtü. Bu ders,
kursun ve kursun son konusunun sorusunu bu kez metin üzerinde sorar: bir dizginin bir
örüntüye uyup uymadığını sormak. Java’nın düzenli ifade API’si iki parçadan kurulur —
Pattern, örüntünün kendisi; Matcher, bir örüntüyü belirli bir girdiye uygulayan taraf —
ve bu ayrımın kendisi kursun sorusuna zaten bir ipucu verir: örüntü bir kere kurulup tekrar
tekrar kullanılabilecek bir değerdir, tek seferlik bir çağrı değil.
Dört bölüm sırayla ilerliyor: örüntünün bir değer oluşu ve iki eşleştirme yönteminin ayrı sözleşmesi, iki niceleyici biçiminin aynı girdide ayrı sonucu, kursun tek etkisiz satırı (kural kırılıyor, sonuç değişmiyor, yalnız bedel değişiyor) ve son sınırlayıcı ölçüm — geri izlemenin adım sayısı girdiyle doğrusal artmıyor.
Düzenli ifadelerin sözdizimi Linux müfredatındaki Kabuk Programlama kursunda ve Python Temelleri kursunda zaten kurulmuştu; o sözdizim burada tekrar anlatılmıyor. Bu dersin payı Java’nın kendi düzeneğidir: örüntünün derlenmiş bir nesne olarak var olması, eşleştirmenin bu nesne üzerinden ayrı yöntemlerle çağrılması ve bu yöntemlerin çağıranı hangi kararlara zorladığı.
Örüntü Derlenmiş Bir Değerdir
- IO25 — Tek bir
Patternnesnesi dört ayrı girdi üzerinde hemmatches(tam eşleşme) hemfind(arama) ile sınanıyor. Aynı örüntü, aynı girdi, iki ayrı yöntem.
// TamEslesmeVeArama.java — oruntu derlenmis bir degerdir, matches ve find ayri sozlesme tasir
import java.util.regex.*;
public class TamEslesmeVeArama {
public static void main(String[] args) {
Pattern oruntu = Pattern.compile("[0-9]+");
String[] girdiler = { "12345", "kod-12345", "12345-son", "abc" };
System.out.printf("%-16s%-10s%s%n", "girdi", "matches", "find");
for (String g : girdiler) {
Matcher m1 = oruntu.matcher(g);
boolean tamEslesme = m1.matches();
Matcher m2 = oruntu.matcher(g);
boolean arama = m2.find();
System.out.printf("%-16s%-10s%s%n", g, tamEslesme, arama);
}
}
}
girdi matches find 12345 true true kod-12345 false true 12345-son false true abc false false
Pattern.compile çağrısı bir kere yapılıyor ve dönen oruntu dört ayrı Matcher’a temel
oluyor — örüntünün kendisi bir çağrı değil, bir değer. Bu değer üzerinde çağrılan iki
yöntem farklı sorular soruyor. matches girdinin tamamının örüntüye uyup uymadığını
soruyor; kod-12345 girdisinde rakamlar var ama başında kod- fazlalığı olduğu için tüm
girdi uymuyor, matches false dönüyor. find ise girdinin bir yerinde örüntüye uyan
bir alt dizgi arıyor; aynı girdide 12345 alt dizgisi bulunduğu için find true dönüyor.
Yalnız son satırda ikisi de aynı yanıtı veriyor, çünkü abc girdisinde hiçbir rakam yok —
ne tam eşleşme ne arama bir şey bulabiliyor. Pattern aynı kalırken sözleşmeyi değiştiren
şey çağrılan yöntemdir, örüntünün kendisi değil.
Açgözlü ve İsteksiz Niceleyici Ayrı Yakalama Üretir
- IO26 — Aynı yakalama grubu iki niceleyici biçimiyle yazılıyor:
(.*)açgözlü,(.*?)isteksiz. İkisi de aynı girdiye, aynı köşeli parantez çiftlerine uygulanıyor.
// AcgozluVeIsteksiz.java — acgozlu ve isteksiz niceleyici ayni girdide ayri yakalama uretir
import java.util.regex.*;
public class AcgozluVeIsteksiz {
public static void main(String[] args) {
String girdi = "[kayit-1][kayit-2]";
Pattern acgozlu = Pattern.compile("\\[(.*)\\]");
Pattern isteksiz = Pattern.compile("\\[(.*?)\\]");
Matcher m1 = acgozlu.matcher(girdi);
m1.find();
Matcher m2 = isteksiz.matcher(girdi);
m2.find();
System.out.println("girdi: " + girdi);
System.out.println("acgozlu [(.*)] yakalanan: " + m1.group(1));
System.out.println("isteksiz [(.*?)] yakalanan: " + m2.group(1));
int acgozluAdet = 0;
Matcher m3 = acgozlu.matcher(girdi);
while (m3.find()) acgozluAdet++;
int isteksizAdet = 0;
Matcher m4 = isteksiz.matcher(girdi);
while (m4.find()) isteksizAdet++;
System.out.println("acgozlu toplam eslesme sayisi: " + acgozluAdet);
System.out.println("isteksiz toplam eslesme sayisi: " + isteksizAdet);
}
}
girdi: [kayit-1][kayit-2] acgozlu [(.*)] yakalanan: kayit-1][kayit-2 isteksiz [(.*?)] yakalanan: kayit-1 acgozlu toplam eslesme sayisi: 1 isteksiz toplam eslesme sayisi: 2
İki niceleyici de “sıfır ya da daha çok karakter” anlamına gelir; ikisi de aynı kümeyi
kabul eder. Aralarındaki fark kabul ettikleri küme değil, o kümeyi hangi sırayla dener.
Açgözlü niceleyici önce mümkün olan en uzun diziyi dener ve eşleşme başarısız olursa geri
çekilir; girdinin sonuna kadar giden .* ile başlıyor, sonra kapanış parantezi bulunana
kadar geriye doğru bırakıyor. Girdide iki kapanış parantezi olduğu için en son bulduğu
parantez kazanıyor — yakalanan dizgi iki kaydın arasındaki her şeyi, kayit-1][kayit-2’yi
kapsıyor. İsteksiz niceleyici tam tersini yapıyor: önce en kısa diziyi, boş dizgiyi dener ve
yalnız gerektiğinde uzatır; ilk kapanış parantezinde durur. Bu farkın somut sonucu eşleşme
sayısında görünüyor: açgözlü örüntü girdinin tamamını bir eşleşmede tüketirken, isteksiz
örüntü aynı girdide iki ayrı eşleşme buluyor.
Çağıranın Kuralı: Yeniden Derleme Etkisizdir
- IO27 — Beş girdi iki yoldan işleniyor: her seferinde yeniden derlenen bir örüntüyle ve bir kere derlenip beş kez kullanılan bir örüntüyle. Derleme çağrılarının sayısı ayrıca sayılıyor.
// YenidenDerleme.java — oruntuyu her cagrida yeniden derlemek etkisizdir: sonuc ayni, bedel ayri
import java.util.List;
import java.util.regex.*;
public class YenidenDerleme {
static int derlemeSayaci = 0;
static Pattern derle(String duzenli) {
derlemeSayaci++;
return Pattern.compile(duzenli);
}
static boolean herCagridaDerleyerek(String duzenli, String girdi) {
return derle(duzenli).matcher(girdi).find();
}
public static void main(String[] args) {
List<String> girdiler = List.of("alfa-01", "beta-02", "gamma-03", "delta-04", "epsilon-05");
String duzenli = "^[a-z]+-[0-9]{2}$";
derlemeSayaci = 0;
boolean[] sonuc1 = new boolean[girdiler.size()];
for (int i = 0; i < girdiler.size(); i++)
sonuc1[i] = herCagridaDerleyerek(duzenli, girdiler.get(i));
int herCagridaDerlemeSayisi = derlemeSayaci;
derlemeSayaci = 0;
Pattern tekOruntu = derle(duzenli);
boolean[] sonuc2 = new boolean[girdiler.size()];
for (int i = 0; i < girdiler.size(); i++)
sonuc2[i] = tekOruntu.matcher(girdiler.get(i)).find();
int birKereDerlemeSayisi = derlemeSayaci;
System.out.println("sonuclar ayni mi: " + java.util.Arrays.equals(sonuc1, sonuc2));
System.out.println("her cagrida derleyerek - derleme sayisi: " + herCagridaDerlemeSayisi);
System.out.println("bir kere derleyerek - derleme sayisi: " + birKereDerlemeSayisi);
}
}
sonuclar ayni mi: true her cagrida derleyerek - derleme sayisi: 5 bir kere derleyerek - derleme sayisi: 1
Bu ölçüm kursun kurduğu üçüncü sınıfı örnekliyor: etkisiz kural kırılması. Beş girdi de
iki yoldan birebir aynı sonuca ulaşıyor — hangi girdinin örüntüye uyduğu, hangisinin
uymadığı iki yolda da özdeş. Yeniden derleme çağıranın kuralını kırıyor (“aynı örüntü tekrar
tekrar kullanılacaksa bir kere derlenmeli”) ama bu kırılma ne bir istisna fırlatıyor ne
sessizce yanlış bir sonuç üretiyor; doğruluk hiç bozulmuyor. Bozulan şey yalnız bedel:
beş girdilik bir döngüde derleme çağrısı bire karşı beş oluyor. Pattern.compile küçük bir
örüntü için ucuz görünebilir, ama derleme işi örüntü metnini bir durum makinesine çeviren
gerçek bir çalışmadır ve her çağrıda yeniden yapılır. Kuralın kırılması burada görünmez
kalır, çünkü hiçbir çıktı satırı yanlış değildir — yalnız harcanan iş fazladır.
Kursun ortak tanımı üç sınıf tanımlıyordu: istisna (hemen ve adıyla düşer), sessiz
(sonuç yanlıştır, kimse şikâyet etmez), etkisiz (kural doğruluğa değil bedele ilişkindir).
On beş gözlemin on dördü ilk ikisine düşüyordu; bu ölçüm on beşincisi ve kursun tek
etkisiz satırı — en kolay gözden kaçan kırılma, çünkü görünür hiçbir izi yoktur;
yalnız aynı işi yapan iki kod arasındaki iş miktarı farkıdır.
Sınırlayıcı Ölçüm: Geri İzleme Adım Sayısı Doğrusal Artmaz
- IO28 — Girdi, kendi
charAtçağrılarını sayan bir dizi ile sarmalanıyor. Aynı örüntü, gitgide uzayan ama hiçbir zaman eşleşmeyen bir girdiye uygulanıyor; her denemede toplam okuma sayısı kaydediliyor. Ölçü adım sayısıdır, süre değildir.
// SayanDizi.java — girdiyi sarmalayip charAt cagrisini sayan CharSequence (yardimci sinif, main yok)
class SayanDizi implements CharSequence {
private final String temel;
long okumaSayisi = 0;
SayanDizi(String temel) { this.temel = temel; }
@Override public int length() { return temel.length(); }
@Override public char charAt(int i) {
okumaSayisi++;
return temel.charAt(i);
}
@Override public CharSequence subSequence(int b, int s) { return temel.subSequence(b, s); }
@Override public String toString() { return temel; }
}
// GeriIzlemeAdimlari.java — geri izlemeli eslestirmede adim sayisi girdiyle dogrusal artmaz
import java.util.Locale;
import java.util.regex.Pattern;
public class GeriIzlemeAdimlari {
static long adimSay(Pattern p, String girdi) {
SayanDizi d = new SayanDizi(girdi);
p.matcher(d).matches();
return d.okumaSayisi;
}
public static void main(String[] args) {
Pattern oruntu = Pattern.compile("(a+)+b");
System.out.printf(Locale.ROOT, "%-10s%s%n", "uzunluk", "charAt cagri sayisi");
long onceki = 0;
for (int uzunluk = 10; uzunluk <= 25; uzunluk += 5) {
String girdi = "a".repeat(uzunluk);
long adim = adimSay(oruntu, girdi);
System.out.printf(Locale.ROOT, "%-10d%d%n", uzunluk, adim);
if (onceki > 0) {
double oran = (double) adim / onceki;
System.out.printf(Locale.ROOT, " (bir onceki uzunluga gore artis orani: %.1f)%n", oran);
}
onceki = adim;
}
}
}
uzunluk charAt cagri sayisi 10 100 15 225 (bir onceki uzunluga gore artis orani: 2.3) 20 400 (bir onceki uzunluga gore artis orani: 1.8) 25 625 (bir onceki uzunluga gore artis orani: 1.6)
Girdi a harflerinden oluşuyor ve b hiç içermiyor; örüntü (a+)+b ise sonunda mutlaka bir
b istiyor. Eşleşme baştan başarısız olacak, ama motor bunu anlayana kadar iç grup (a+)‘yı
girdiyi kaç harfte bir bölerek deneyeceğini araştırıyor — bir harf mi, iki mi, üç mü —
ve her bölünüşte b bulunamadığı için bir öncekine geri dönüp başka bir bölünüşü deniyor.
Girdi uzunluğu 10’dan 25’e, yani iki buçuk katına çıktığında, okuma sayısı 100’den 625’e,
altı buçuk katına çıkıyor: 10 uzunlukta 100, 15’te 225, 20’de 400, 25’te 625 — her satır bir
öncekinin karesiyle orantılı büyüyor. Girdi doğrusal (sabit adım sabit artış) uzarken
adım sayısı karesel büyüyor; bu, “girdiyle doğrusal artmaz” iddiasının ölçülmüş hâli. Ölçü
gerçek bir kronometre değil, charAt çağrısı sayan bir sarmalayıcı — bu yüzden sonuç bu
belgeyi kim, hangi makinede çalıştırırsa çalıştırsın birebir aynı çıkıyor.
(a+)+b örüntüsünün kendisi geçerli, sıradan bir düzenli ifade; onu tehlikeli yapan bir
kaçış ya da özel bir kurgu değil, iç içe geçmiş bir niceleyicinin aynı karakterleri birden
çok yoldan gruplayabilmesidir — bu ders bu geometriyi kurmuyor, yalnız sayıyor.
Özet
Patternderlenmiş bir değerdir;matches(tam eşleşme) vefind(arama) aynı örüntü üzerinde ayrı sözleşmeler taşır ve aynı girdide ayrı sonuç verebilir.- Açgözlü niceleyici (
.*) önce en uzun diziyi dener ve geriye doğru daralır; isteksiz niceleyici (.*?) önce en kısa diziyi dener ve gerektikçe uzar; aynı girdide ayrı yakalama ve ayrı eşleşme sayısı üretirler. - Örüntüyü her çağrıda yeniden derlemek kursun tek
etkisizkural kırılmasıdır: eşleşme sonucu değişmez, yalnız derleme çağrısı sayısı artar. - Geri izlemeli bir eşleştirmede adım sayısı girdiyle doğrusal artmayabilir; ölçülen örnekte girdi uzunluğu iki buçuk katına çıkarken adım sayısı altı buçuk katına, karesel olarak çıktı.
- Adım sayısı gerçek bir kronometreyle değil, girdiyi sarmalayıp okuma çağrısını sayan bir
CharSequenceile ölçüldü; sonuç bu yüzden makineden bağımsız ve yeniden üretilebilir.
Kurs Kapanışı
Kurs tek bir soruyla açıldı: yazılmış bir kütüphaneyi çağırdığın anda karar dilin dışına
çıkar — peki o kütüphanenin sana verdiği sözü kim veriyor? On beş ders bu soruyu on beş ayrı
davranışa sordu ve yanıtı her seferinde en az iki gerçekleştirimi ya da en az iki çağrı
biçimini yan yana koşturarak aldı. Ana iddia doğrulandı: on beş gözlemin yedisi arayüz
garantisiydi, sekizi yalnız seçilen gerçekleştirimin davranışıydı — tek bir sınıfı gözleyerek
çıkarılan bir davranış listesinin çoğunluğu arayüzün sözü değildi. İkinci iddia da doğrulandı:
çağıranın kuralı kırıldığında en sık düşen şey bir istisna değil, sessizce yanlış bir
sonuçtu. Üçüncü iddia sınırıyla birlikte doğrulandı: derleyicinin koruması burada tümüyle
bitmiyor, koruduğu şey değişiyor. Tek soyut yöntem kısıtı ile etkin final kuralı hâlâ
derleme zamanında düşüyor; ikisi de bir tipin şekline ilişkin sözlerdir. Ama kursun
ölçtüğü çağıran yükümlülüklerinin hiçbiri derleme zamanında görünmedi — hepsi derlendi ve
ayrım çalışma zamanına kaldı. Derleyici tipin şeklini doğruluyor, sözün tutulduğunu değil.
| Ders | Ölçülen davranış | Garantiyi veren | Sınırlayıcı ölçüm |
|---|---|---|---|
| Koleksiyon Çerçevesi | 15 gözlemin 7’si arayüz garantisi, 8’i gerçekleştirim davranışı | arayüz (7) + gerçekleştirim (8) | 4 ögeli kaynakta PriorityQueue’nun gezinme sırası çıkış sırasıyla rastlantı eseri örtüşür; 5. öge ayrımı açar |
| Liste, Küme ve Kuyruk | 6 ögelik girdiden liste hiçbir bilgi, küme 3 bilgi, kuyruk yalnız konum erişimini düşürür | arayüz (düşen bilgi sözün bedeli) + çağıran (remove(1)) |
LinkedList iki arayüzü birden konuşur; aynı remove(1) iki anlam taşır, kırılma sessizdir |
| Sözlükler | Üç söz (anahtar tekilliği, canlı görünüm, yok anahtarda boş değer) üç gerçekleştirimde aynı; sıra ve boş anahtar ayrışır | arayüz (3 söz) + gerçekleştirim (sıra) + çağıran (equals/hashCode) |
Aynı kusurlu anahtar sıralı sözlükte bulunur; orada kullanılan söz compareTo’dur |
| Yineleyiciler ve Eşzamanlı Değişiklik | Tükenmede NoSuchElementException üç gerçekleştirimde aynı; gezinirken değişiklikte üç ayrı yanıt |
arayüz (tükenme) + gerçekleştirim (değişiklik yanıtı) | Iterator.remove kuralı kırmaz; kural “yineleyicinin dışından değiştirme”dir |
| Karşılaştırma ve Sıralama | Doğal sıra tipin kendi sözü, karşılaştırıcı çağıranın verdiği ayrı nesne; kararlılık iki gerçekleştirimde aynı | arayüz (kararlılık) + çağıran (karşılaştırıcı tutarlılığı) | Uyumsuz karşılaştırıcı listede hiç öge düşürmez; kusuru üreten onu tekillik kararı olarak kullanan kaptır |
| Fonksiyonel Arayüzler | Tek soyut yöntem derleme zamanı sözüdür; davranış sözü hiçbir yerde denetlenmez | arayüz (tipin şekli); davranış sözü kimsenin | İşaret garantiyi eklemez, ihlali erken yakalar; işaretsiz arayüz de fonksiyoneldir |
| Lambda İfadeleri | Yerel değişken değeriyle yakalanır, etkin final derleme zamanında zorunlu; alanda kısıt yok |
çağıran (hangi biçimi seçtiği) | Kısıt değişkeni dondurur, gösterdiği nesneyi değil; yakalanan liste değişirse lambda yeni içeriği görür |
| Akış API’si | On ölçümün 3’ü kaynaktan bağımsız garanti, 5’i kaynağın sırasına bağlı, 2’si çağıranın kuralı | arayüz (3) + kaynak (5) + çağıran (2) | “Veri akışı sırayı korur” cümlesi kaynağın sözüdür, veri akışının değil |
| Toplayıcılar | Dört parça (üretici, birikeç, birleştirici, bitirici); sonuç kabının tipi arayüzün sözü değildir | gerçekleştirim (kap tipi) + çağıran (yinelenen anahtar) | Çağıran kap üreticisini verdiğinde belirsizlik kapanır; bırakılmış söz geri alınabilir |
Optional |
Denetimsiz bırakılan çağrı sayısı iki imzada da eşit (4/4); kazanç imzada görünürlüktür | çağıran (denetim çalışma zamanında kalır) | Alanda ve parametrede kazanç kaybolur; of(null) hemen düşer, ofNullable(null) düşmez |
| Akış Tabanlı Girdi/Çıktı | write(int)in düşük sekiz bit ve read()in 0–255 artı -1 sözü iki gerçekleştirimde aynı; tamponlama yalnız çağrı sayısını değiştirir |
arayüz (bayt sözü) + çağıran (boşaltma) | Boşaltılmamış tampon veriyi eksik bırakır; boşaltma kararı çağıranındır |
| Dosya Sistemi API’si | Yol cebiri iki sağlayıcıda da aynı; exists ve size sağlayıcının deposuna sorar |
arayüz (yol cebiri) + gerçekleştirim (depo) + çağıran (iki adım) | Varlık denetimi ile açma iki ayrı işlemdir; arada dosya silinirse denetim geçer, açma düşer |
| Dizileştirme | Yapıcı hiç çalışmaz; kurduğu iki değişmezden sıfırı geri okumada kendiliğinden geçerli kalır | çağıran (readObject yazılmazsa denetim yok) |
Sürüm kimliği sabit tutulup alan adı değişince eski kayıt sessizce varsayılanla geçer |
| Tarih ve Zaman API’si | Instant.equals ve ZonedDateTime.isEqual “aynı an mı” sorusunu yanıtlar; LocalDateTime.equals yanıtlayamaz |
tip (taşınan bilgi) + çağıran (saat dilimi vermeden karşılaştırma) | Yaz saati geçişinde çakışan yerel saat iki ayrı ana karşılık gelir; API erken ofseti seçer |
| Düzenli İfadeler | Pattern derlenmiş bir değerdir; matches ile find ayrı sözleşme, açgözlü ile isteksiz ayrı yakalama üretir |
arayüz (Pattern sözleşmesi) + çağıran (yeniden derleme: tek etkisiz satır) |
Geri izlemede adım sayısı girdiyle doğrusal artmaz; ölçü adım sayısıdır, süre değildir |
Standart Kütüphane ve Akışlar burada kapanıyor. Sıradaki kurs olan JVM, Eşzamanlılık ve Başarım aynı sorulara üçüncü bir katmanda döner: bir garantinin arkasında artık tek bir iş parçacığı değil, birden çok iş parçacığının aynı nesneye aynı anda eriştiği bir çalışma zamanı vardır — ve o zaman “kim garanti veriyor” sorusunun yanıtı zamanlamaya bağlı hâle gelebilir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.