İçeriğe geç
academia.sh

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 Pattern nesnesi dört ayrı girdi üzerinde hem matches (tam eşleşme) hem find (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

  • Pattern derlenmiş bir değerdir; matches (tam eşleşme) ve find (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 etkisiz kural 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 CharSequence ile ö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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat