Effective Java Madde 5: Bağımlılıkları Kendiniz Yaratmak Yerine Dependency Injection Kullanın

Birçok sınıf işlevlerini yerine getirebilmek için çeşitli kaynaklara ihtiyaç duyar. Bu kaynaklar, başka bir deyişle ihtiyaç duyulan diğer sınıflar yazılımda bağımlılık (dependency) olarak adlandırılır. Örneğin, bir yazım denetleme uygulaması (spell checker) işlevlerini yerine getirebilmek için bir sözlüğe bağımlı olabilir. Bu tarz sınıfların statik yardımcı sınıf (utility class) olarak gerçekleştirildiğini zaman zaman görmekteyiz. (Madde 4)

// Statik yardımcı sınıfların yanlış kullanımı 
// esnek değildir ve test edilemez!
public class SpellChecker {
    private static final Lexicon dictionary = ...;
    private SpellChecker() {} 
    public static boolean isValid(String word) { 
        ... 
    }
    public static List<String> suggestions(String typo) { 
        ... 
    }
}

Benzer olarak, bunların Singleton sınıflar olarak gerçekleştirildiğini görmek de mümkündür. (Madde 3)

// Yanlış singleton kullanımı, esnek değildir ve test edilemez!
public class SpellChecker {
    private final Lexicon dictionary = ...;
    private SpellChecker(...) {}
    public static INSTANCE = new SpellChecker(...);
    public boolean isValid(String word) { 
        ... 
    }
    public List<String> suggestions(String typo) { 
        ... 
    }
}

Bu iki yaklaşım da tatmin edici değildir, çünkü ikisi de kullanmaya değer sadece bir tane sözlük (dictionary) olduğu varsayımında bulunmaktadır. Ancak pratikte her bir dilin farklı bir sözlüğü bulunmaktadır, hatta aynı dil içinde farklı amaçlar için farklı sözlükler de kullanılabilir (deyimler sözlüğü, bilişim terimleri sözlüğü gibi). Bu sebeple tek bir sözlüğün yeterli olacağını düşünmek yanlış olur.

Bunu çözmek için SpellChecker sınıfına kullanılan sözlüğü değiştirmeyi sağlayan bir metot eklemeyi düşünebilirsiniz. Ancak bu son derece hata yapmaya müsait ve kırılgan bir uygulamaya sebebiyet verecektir. Daha ötesi birden çok thread kullanılan (concurrent) uygulamalarda başınızı çok ağrıtacaktır. Statik yardımcı sınıflar ve singleton sınıflar, davranışlarını parametre olarak geçilen bir bağımlılığa (dependency) göre değiştiriyorlarsa, istenmeyen sonuçlar doğabilir.

Burada yapılması gereken şey SpellChecker sınıfını, birden fazla nesnesini oluşturacak şekilde tasarlamak ve her nesne için kullanılacak sözlüğün istemci tarafından belirlenmesini istemek olacaktır. Bunu yapmanın en basit yolu istemcinin nesneyi yaratırken kullanılmasını istediği sözlüğü yapıcı metoda göndermesidir. Bu, dependency injection dediğimiz prensibi uygulamanın bir biçimidir. Sözlük, burada SpellChecker sınıfı için bir bağımlılıktır (dependency). İstemci ise, kullanılmasını istediği sözlük referansını SpellChecker sınıfına dışarıdan ”enjekte” etmektedir. (injection)

// Dependency injection esneklik ve test edilebilirliği sağlar
public class SpellChecker {
    private final Lexicon dictionary;
    public SpellChecker(Lexicon dictionary) { 
        this.dictionary = dictionary;
    }
    public boolean isValid(String word) { 
        ... 
    }
    public List<String> suggestions(String typo) { 
        ... 
    }
}

Dependency injection prensibi o kadar basit ve yaygındır ki birçok yazılımcı bunu kullanır ama bir ismi olduğunu bilmez. Her ne kadar bizim örneğimizde tek bir bağımlılık olsa da (sözlük), bu prensibi kaç tane bağımlılık olursa olsun kullanabilirsiniz. Değişebilirliği (mutability) kısıtlama prensibini de sağladığı için (Madde 17) istendiği taktirde birden fazla istemci aynı bağımlılığı kullanabilir. Dependency injection sadece yapıcı metotlara değil, statik fabrika metotlarına (Madde 1) ve builder kullanan sınıflara (Madde 2) da uygulanabilir.

Bu prensibin bir başka uygulama şekli ise yapıcı metoda bir fabrika referansı geçmektir. Fabrika, çağırdıkça belirli bir sınıfın nesnelerini üretip döndüren sınıflara verilen addır. Java 8 ile kullanıma sunulan Supplier<T> arayüzü, fabrika sınıflarını ifade etmek için çok uygundur. Bir Supplier<T> referansını girdi olarak kabul eden fabrika metotlarının, tür parametresini sınırlı joker türü (bounded wildcard type) kullanarak kısıtlamaları gerekir (Madde 31). Böylece istemci sadece istenen türü değil, bu türün alt türlerini üreten fabrika referanslarını da parametre olarak geçebilir. Örneğin aşağıda, istemcinin gönderdiği ve gerekli taşları üreten bir fabrika referansı kullanarak mozaik döndüren bir metodun imzasını görüyorsunuz:

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

Dependency injection tekniği, her ne kadar uygulamanızın test edilebilirliğini ve esnekliğini çok büyük oranda artırsa da, büyük projeler birbirine bağımlı binlerce sınıftan oluştuğu için uygulaması kolay olmayabilir. Bu sorunu çözmek için Dagger, Guice, Spring gibi dependency injection framework denilen uygulama çatıları geliştirilmiştir. Bu çatıların kullanımı bu yazının kapsamında değildir. Ancak şunu söyleyebiliriz ki, bu prensibi kendiniz uygulamış olsanız bile daha sonra bu çatılardan birine geçiş yapmak son derece kolaydır.

Özetle, sınıfınızın bağımlılıkları varsa ve bu bağımlılıklar sınıfın davranışını etkiliyorsa, bu sınıfı statik yardımcı (utility) veya singleton olacak şekilde yazmayın! Bu sınıfın kendi bağımlıklıklarını yaratmasına da izin vermeyin! Bunun yerine istemci tarafında bu bağımlılıkları yaratıp sınıfa gönderin veya bu bağımlılıkları yaratan bir fabrika referansı gönderin. Bunları sadece yapıcı metodu çağırırken değil, statik fabrika veya builder kullanan sınıflara da gönderebilirsiniz. Bu şekilde uyguladığınız taktirde, dependency injection sınıfların esnekliğini, yeniden kullanılabilirliğini ve test edilebilirliğini çok büyük oranda artırır.

Share

Effective Java Madde 4: Nesne Yaratılmasını İstemediğiniz Sınıfları Private Yapıcı Metot İle Güçlendirin

Bazen sadece statik metotları ve değişkenleri bir arada tutmak için bir sınıf yazmak isteyebilirsiniz. Bu tür sınıflar, bazı geliştiriciler tarafından nesne bazlı düşünmekten kaçınmak için suistimal edildiklerinden ötürü kötü bir üne kavuşmuşlardır. Buna rağmen, geçerli kullanım alanları bulunmaktadır. İlkel türler veya diziler üzerinde işlem yapan, birbiriyle alakalı metotları gruplamak için kullanılabilirler. java.lang.Math ve java.util.Arrays bu kullanıma örnek gösterilebilir. Bunun yanında java.util.Collections örneğinde olduğu gibi, belli bir arayüzü gerçekleştiren nesneler için tasarlanmış statik metotları – statik fabrika metotları da dahil (Madde 1) – bir arada tutmak için de kullanılabilirler. (Java 8 ile birlikte artık bu metotları arayüz içerisine de yazabilirsiniz, tabi arayüze erişiminiz varsa.) Son olarak, bu sınıflar final bir sınıf içerisindeki metotları gruplamak için de kullanılabilir, çünkü bunlar bir çocuk sınıfta tanımlanamaz.

Bu tarz yardımcı sınıflardan (utility class) nesne oluşturulması istenmez. Ancak herhangi bir yapıcı metot tanımlanmadığı zaman, Java derleyicisi otomatik olarak parametre almayan public bir varsayılan yapıcı metot (default constructor) tanımlayacaktır. Bir istemci açısından bu varsayılan yapıcı metodun, açıkça tanımlanmış diğer yapıcı metotlardan hiçbir farkı yoktur. Bu sebeple, birçok API içerisinde istemeden de olsa nesne yaratılabilen sınıflar bulunmaktadır.

Bunu engellemek için bir sınıfı abstract tanımlamak işe yaramaz. Bu sınıf katılılarak çocuk sınıftan bir nesne yaratılabilir. Dahası, kullanıcı bu sınıfın kalıtım amaçlı tasarlandığı yanılgısına kapılabilir. (Madde 19) Aslında bir sınıftan nesne yaratılmasını engellemek için çok basit bir yöntem vardır, private bir yapıcı metot tanımlamak. Biz açıkça bir yapıcı metot tanımladığımız zaman, derleyici varsayılan public yapıcı metodu tanımlamaz.

// Nesne yaratılamayan yardımcı sınıf
public class UtilityClass {
    // Varsayılan yapıcı metodu engelle
    private UtilityClass() {
        throw new AssertionError();
    }
    
    ...  // diğer kısımlar çıkartılmıştır
}

Tanımladığımız yapıcı metot private olduğu için sınıfın dışından görülebilmesi veya çalıştırılabilmesi mümkün değildir. Fırlatılan AssertionError ise yapıcı metodun sınıfın içerisinden yanlışlıkla çağırılması ihtimaline karşı önlem almak içindir. Böylece bu sınıftan asla bir nesne oluşturulamayacağını garanti etmiş oluruz.

Bir yan etki olarak, bu private yapıcı metot aynı zamanda sınıfın kalıtılmasını da engeller. Bütün yapıcı metotlar ilk önce kalıttıkları ata sınıfın (açıkça belirtilen bir kalıtım yoksa Object sınıfının) yapıcı metodunu çağırmak zorundadır, ata sınıftaki yapıcı metot açıkça tanımlanmış veya derleyici tarafından eklenen varsayılan yapıcı metot olabilir. Biz yukarıdaki yardımcı sınıfta hem varsayılan yapıcı metodun tanımlanmasını engellediğimiz için hem de kendi yapıcı metodumuzu private tanımladığımız için, bu sınıfı kalıtacak bir çocuk sınıfın çağırabileceği bir yapıcı metot bulunmamaktadır.

Share

Effective Java Madde 3: Singleton Sınıfları Private Yapıcı Metot veya Enum Türüyle Güçlendirin

Singleton en basit anlamıyla sadece bir kez somutlaştırılabilen (instantiate) sınıf anlamına gelir. Diğer bir değişle, singleton sınıflardan sadece bir kez nesne oluşturulabilir. Bu nesneler ya fonksiyon gibi durum içermeyen nesneleri, (Madde 24) ya da doğası itibariyle eşsiz olan bileşenleri temsil ederler. Bir sınıfı singleton yapmak, onu kullanan istemcileri test etmeyi zorlaştırır çünkü singleton nesneyi bir mock gerçekleştirim ile değiştirmek eğer bu nesne bir arayüzü gerçekleştirmiyorsa imkansızdır.

Singleton nesne oluşturmak için iki yöntem vardır. Her ikisi de yapıcı metotları private tutarak, singleton nesneyi public static bir üye olarak dışarı açma esasına dayanmaktadır. Bu yöntemlerden ilkinde, singleton nesne final olarak tanımlanmaktadır:

// public final olarak tanımlanmış singleton alan
public class Elvis {

public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}

>> DEVAMINI OKU
Share

Effective Java Madde 18: Kalıtım Yerine Komposizyonu Tercih Edin

Kalıtım kodların yeniden kullanımını sağlayabilen güçlü bir araçtır, ancak her zaman yapacağınız iş için en doğru araç olmayabilir. Yanlış kullanıldığında kırılgan yazılımlara yol açar. Kalıtımı, aynı paket içerisinde, ata sınıf ve çocuk sınıfın aynı programcının kontrolünde olduğu durumlarda kullanmak güvenlidir. Bunun yanında, özellikle kalıtılmak için tasarlanmış ve belgelenmiş sınıfları kalıtmak da güvenlidir (Madde 19). Ancak, farklı paketlerdeki sıradan somut sınıfları kalıtmak tehlikelidir. Hatırlatma olarak söyleyelim, burada kalıtım dediğimiz zaman bir sınıfın başka bir sınıfı kalıtmasından bahsetmekteyiz. Bahsedilen problemler arayüz (interface) kalıtırken geçerli değildir.

Metot çağırmanın aksine, kalıtım yapmak sarmalamayı (encapsulation) ihlal eder. Başka bir deyişle, bir çocuk sınıf işlevini gerçekleştirebilmek için ata sınıfın gerçekleştirim detaylarına bağımlıdır. Ata sınıfın içeriği zaman içerisinde yeni sürümlerle birlikte değişebilir, ve bu durumda çocuk sınıfın kodu hiç değişmemiş olsa bile çalışmayabilir. Sonuç olarak, eğer ata sınıf özel olarak kalıtılmak için tasarlanmamışsa, çocuk sınıf değişikliğe ihtiyacı olmasa bile ata sınıfla birlikte gelen değişikliklere uyum sağlayacak şekilde değiştirilmelidir.

Bunu somutlaştırmak için, farzedelim ki HashSet kullanan bir programımız olsun ve uygulamaya bu HashSet yaratıldığından beri kaç kere eleman eklendiğini döndüren bir metot eklemek istediğimizi düşünelim. (HashSet’in o andaki eleman sayısı ile karıştırmayın, eleman sayısı eleman sildikçe azalır.) Bunu sağlamak için HashSet sınıfını kalıtıp, her eleman eklendiğinde bir sayacı artıralım ve bu sayaç değerini döndüren bir metot ekleyelim. HashSet sınıfında add() ve addAll() olmak üzere iki tane eleman ekleyen metot vardır, dolayısıyla bunları geçersiz kılmalıyız.

// Bozuk - Yanlış kalıtım kullanımı!
public class InstrumentedHashSet<E> extends HashSet<E> {

    // Eklenen eleman sayısı
    private int addCount = 0;

    public InstrumentedHashSet() { }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

Yukarıdaki sınıf akla yatkın görünse de aslında bozuktur. Diyelim ki bu sınıftan bir nesne yarattık ve addAll() metodunu kullanarak 3 eleman ekledik. Burada, Java 9 ile eklenen List.of statik metodunu kullanıyoruz, daha önceki versiyonlar için Arrays.asList kullanın.

InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("Snap", "Crackle", "Pop"));

Bu noktada getAddCount() metodunun 3 değeri döndürmesini bekleriz ama aslında 6 döndürecektir. Yanlış giden nedir? HashSet sınıfındaki addAll() metodu aslında kendi içerisinde add() metodunu çağıracak şekilde gerçekleştirilmiştir. Dolayısıyla, InstrumentedHashSet çocuk sınıfındaki addAll() metodu addCount değerine 3 ekledikten sonra ata sınıftaki aynı isimli metodu super.addAll() ile çağırmaktadır. Ata sınıf kendi gerçekleştirimi gereği addAll() içerisinde add() metodunu çağırmakta, bu metot da çocuk sınıfta geçersiz kılındığı için çocuk sınıftaki add() metodu işletilmektedir, bu yüzden de her bir eleman için 2 defa addCount sayacına ekleme yapılmaktadır.

Bu problemi addAll() metodunu geçersiz kılmayarak (override etmeyerek) çözmek mümkündür. Sonuçta elde edeceğimiz sınıf çalışacaktır ancak bu sefer de sınıfın doğru çalışması ata sınıfın addAll() içerisinde add() metodunu çağırmasına bağımlı olacaktır. Bu tamamen HashSet sınıfını yazanların tercihidir ve ileriki Java sürümlerinde öyle kalacağının hiçbir garantisi yoktur. Bu sebeple de yazdığımız çocuk sınıf o an için çalışsa da ata sınıftaki değişikliklere karşı çok kırılgan olacaktır.

Alternatif olarak, addAll() metodunu ata sınıfı hiç çağırmadan verilen veri yapısı üzerinde dolaşıp her eleman için çocuk sınıftaki add() metodunu çağıracak şekilde geçersiz kılabiliriz. Bu durumda ata sınıfın addAll() içerisinde add() metodunu çağırdığı varsayımından kurtularak sınıfın her şekilde doğru çalışmasını sağlayabilir, kırılganlık problemini çözebiliriz. Çünkü ata sınıftaki addAll() metodu devreden tamamen çıkmış olacaktır. Ancak bu yöntem de bütün problemleri çözmez. Ata sınıf içerisindeki detaylar her zaman bu kadar açık olmayacaktır ve bulması zaman alacak, kolay hata yapmaya sebebiyet verecektir. Hatta bazen bu yöntemi uygulamak mümkün olmayacaktır çünkü bazı metotlar çocuk sınıfın erişemediği, ata sınıf içerisindeki private alanlara ihtiyaç duyacaktır.

Çocuk sınıflarda kırılganlığa sebebiyet veren bir başka unsur da ata sınıfların yeni sürümlerle birlikte yeni metotlar eklemesidir. Farzedelim ki bir uygulama güvenlik gerekçeleriyle bir veri yapısına eklenen her bir elemanı ön kontrolden geçiriyor olsun. Bunu sağlamak için veri yapısını temsil eden sınıf kalıtılarak eleman ekleyen her metot geçersiz kılınıp, eleman eklemeden önce güvenlik kontrolünü yapan kod parçası çalıştırılabilir. Bu çocuk sınıf o an için çalışacaktır ancak ata sınıf bir sonraki sürümde eleman ekleyen yeni bir metot tanımlarsa çocuk sınıfta var olmayan bu metot güvenlik kontrolü yapmadan veri yapısına eleman eklemeye başlayacaktır. Bu anlatılan teorik bir problemden ibaret değildir, Hashtable ve Vector sınıfları Collections çatısına eklenirken bu şekilde birkaç güvenlik açığı oluşmuş ve sonradan fark edilerek düzeltilmiştir.

Bu iki problem de ata sınıftaki metotları geçersiz kılmaktan dolayı çıkmaktadır. Bu yüzden bir sınıfı kalıtıp metotlarını geçersiz kılmadan, sadece çocuk sınıfa eklemeler yaptığınızda daha güvenli olacağını düşünebilirsiniz. Bu şekilde yapılan bir kalıtım daha güvenli olsa da, tamamen risksiz değildir. Ata sınıf ileriki bir sürümde, sizin çocuk sınıfa eklediğiniz bir metodu aynı imza ama farklı bir dönüş türüyle tanımlarsa çocuk sınıf derleme hatası verecektir. Eğer dönüş türü de aynı olursa bu sefer çocuk sınıf ata sınıftaki metodu siz farkında olmadan geçersiz kılmış olacaktır, dolayısıyla yukarıda bahsedilen iki problem tekrar karşınıza çıkacaktır. Dahası, çocuk sınıfta var olan metodun ata sınıftaki yeni metodun sözleşmesini karşılayacağını bilemezsiniz, çünkü siz çocuk sınıfı yazarken sözleşme belirlenmemişti bile.

Neyse ki, burada anlatılan bütün problemlerden sakınmanın bir yolu var. Var olan bir sınıfı kalıtmak yerine, yeni yazdığınız sınıfın içine ihtiyacınız olan sınıfın referansını private bir alan olarak ekleyin. Bu teknik kompozisyon olarak bilinmektedir çünkü bir sınıf diğerinin bileşeni durumuna gelmektedir. Burada yeni yazdığınız sınıf, referans olarak eklediğiniz sınıfın metotlarını çağırarak oradan gelen sonuçları döndürecektir. Buna “iletme” de denir çünkü yeni sınıfta tanımlı metotlar gelen isteği referans sınıfa iletmektedir. Sonuç olarak ortaya çıkan sınıf sapasağlam ve içerdiği sınıfın gerçekleştirim detaylarından tamamen bağımsız olacaktır. Referans olarak eklenen sınıfa yeni metotlar eklense bile sizi etkilemeyecektir. Bunu somutlaştırmak için InstrumentedHashSet sınıfını komposizyon ve iletme uygulayarak tekrar yazalım. Burada dikkat edilmesi gereken nokta, sınıfın iki parçaya bölünmüş olmasıdır: sınıfın kendisi ve sadece iletim yapan yardımcı bir gömülü sınıf.

// Kalıtım yerine komposizyon kullanılan çözüm
public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

// Yeniden kullanılabilir iletim sınıfı
public class ForwardingSet<E> implements Set<E> {

    private final Set<E> s;
    public ForwardingSet(Set<E> s) {
        this.s = s;
    }

    public void clear() {
        s.clear();
    }
    public boolean contains(Object o) {
        return s.contains(o);
    }
    public boolean isEmpty() {
        return s.isEmpty();
    }
    public int size() {
        return s.size();
    }
    public Iterator<E> iterator() {
        return s.iterator();
    }
    public boolean add(E e) {
        return s.add(e);
    }
    public boolean remove(Object o) {
        return s.remove(o);
    }
    public boolean containsAll(Collection<?> c) {
        return s.containsAll(c);
    }
    public boolean addAll(Collection<? extends E> c) {
        return s.addAll(c);
    }
    public boolean removeAll(Collection<?> c) {
        return s.removeAll(c);
    }
    public boolean retainAll(Collection<?> c) {
        return s.retainAll(c);
    }
    public Object[] toArray() {
        return s.toArray();
    }
    public <T> T[] toArray(T[] a) {
        return s.toArray(a);
    }

    @Override
    public boolean equals(Object o) {
        return s.equals(o);
    }

    @Override
    public int hashCode() {
        return s.hashCode();
    }

    @Override
    public String toString() {
        return s.toString();
    }
}

Yukarıdaki sınıfı yazabilmemize olanak sağlayan şey HashSet tarafından da uygulanan Set arayüzünün varlığıdır. Sağlam olmasının yanında bu sınıf aynı zamanda çok da esnektir. InstrumentedSet sınıfı Set arayüzünü uygulamaktadır ve yine Set türünde parametresi olan bir yapıcı metot içermektedir. Aslında bu sınıfın yaptığı iş, bir Set nesnesini başka bir Set nesnesine dönüştürüp çeşitli eklemeler yapmaktan ibarettir. Tek bir somut sınıf ile kullanılabilen kalıtım tabanlı yaklaşımın aksine, bu çözüm daha esnektir çünkü Set arayüzünü gerçekleştiren herhangi bir sınıf ile kullanılabilir.

Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));

Hatta daha önceden yaratılmış ve kullanılmış olan Set türünden bir nesneyi bile dönüştürüp yeni haliyle kullanmaya devam edebilirsiniz.

static void walk(Set<Dog> dogs) {
    InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs);
    ... // bu metot içerisinde dogs yerine iDogs kullanabilirsiniz
}

Bu örnekteki InstrumentedSet sınıfı sarmalayan (wrapper) sınıf olarak tanımlanabilir çünkü içerisinde başka bir Set nesnesi barındırmakta, başka bir deyişle sarmalamaktadır. Bu yaklaşım aynı zamanda bu dekoratör tasarım deseni olarak da adlandırılmaktadır çünkü bizim örneğimizde InstrumentedSet bir Set nesnesini alıp onu dekore etmektedir.

İletim sınıfı içerisindeki bütün iletim metotlarını yazmak biraz zahmetli olsa da bir kere yazıldıktan sonra yeniden kullanılabilir olması avantajlıdır. Örneğin, Guava kütüphanesi bütün collections arayüzleri için iletim sınıfları tanımlamaktadır.

Kalıtım kullanmak sadece çocuk sınıf ata sınıfın bir alt türü ise mantıklıdır. Eğer B sınıfının A sınıfını kalıtması gerektiğini düşünüyorsanız, bunu yapmadan önce mutlaka “B gerçekten bir A mı?” sorusunu sorun. Eğer bu soruya gerçekten evet cevabını veremiyorsanız o zaman B sınıfının A’yı kalıtması yanlış olacaktır.
Eğer cevap hayır ise, B sınıfı A türünde private bir referans tanımlamalı ve dışarıya daha küçük ve basit bir API sunmalıdır. Bu durumda A sınıfı B’nin gerekli bir parçası değil gerçekleştirim detayı olarak kalacaktır.

Java kütüphaneleri içerisinde bu kurala aykırı olan birçok sınıf vardır.
Mesela stack (yığın) bir vektör olmamasına rağmen Stack sınıfı Vector sınıfını kalıtmaktadır. Benzer olarak Properties sınıfının Hashtable sınıfını kalıtması da yanlış bir tercihtir ve bu iki durumda da kompozisyon kullanmak daha yararlı olurdu diyebiliriz.

Eğer kompozisyonun uygun olduğu yerde kalıtım kullanıyorsanız sınıfın gerçekleştirim detaylarını gereksiz bir biçimde dışarıya açıyorsunuz demektir. Sonuçta ortaya çıkan API sınıfın gerçekleştirimine bağımlı kalacak ve ileride değişiklik yapılmasını kısıtlayacaktır. Daha da önemlisi, istemcilerin gerçekleştirim detaylarına direk olarak erişebilmesine neden olursunuz, bu da en iyi ihtimalle istemcilerin sınıfınızı yanlış kullanmasına sebep olabilir. Örneğin, p referansı Properties türünden bir nesneyi gösteriyorsa, p.getProperty(key) ile p.get(key) farklı sonuçlar döndürebilir çünkü birinci metot varsayılan değerleri de hesaba katarken Hashtable sınıfından kalıtılan ikinci metot bunu yapmamaktadır. Kötü ihtimali düşünecek olursak da istemcilerin gerçekleştirim detaylarına erişebiliyor olması sizin sınıfınızı bozmalarına bile neden olabilir. Properties sınıfını tasarlayanlar aslında sadece String türünden anahtarlar ve değerler kullanılmasını istemişlerdir ancak kalıtım kullanılması ile arka taraftaki Hashtable metotlarına da ulaşabilen istemciler bu kuralı kolaylıkla ihlal edebilirler. Bu kural bir kere ihlal edildiğinde ise Properties sınıfının diğer kısımları (load, store gibi) kullanılamaz hale gelmektedir. Bu problem fark edildiğinde düzeltmek için çok geçti çünkü istemciler String olmayan anahtarlar ve değerler kullanmaya çoktan başlamışlardı.

Kalıtım kullanmaya karar vermeden önce kendinize sormanız gereken son soru da şudur: Kalıtmak istediğim sınıfta bir API hatası var mı? Var ise benim aynı hatayı kendi sınıfıma yansıtmam kabul edilebilir mi?
Kalıtım kullandığınızda ata sınıfın API’ını da kalıtmış oluyorsunuz yani hatalar varsa onlar da beraberinde gelecektir, ancak kompozisyon kullanırsanız diğer sınıfın bütün detaylarını saklayıp sıfırdan bir API oluşturabilirsiniz.

Özetleyecek olursak, kalıtım güçlüdür ancak sarmalamayı (encapsulation) ihlal ettiği için problemlidir. Sadece çocuk sınıf ile ata sınıf arasında bir tür-alt tür ilişkisi varsa kullanılması uygun olur. Ancak o zaman bile, eğer ata sınıf ile çocuk sınıf farklı paketlerde tanımlanmışsa ve ata sınıf kalıtılmak için tasarlanmamışsa yazılımda kırılganlığa yol açar. Bu kırılganlığı önlemek için kompozisyon ve iletim sınıfı kullanmayı tercih edin. Bu şekilde tasarlanan sınıflar hem daha esnek hem de daha sağlam olacaktır.

Share

Effective Java Madde 17: Değişebilirliği (Mutability) Kısıtlayın

Basit olarak tanımlamak gerekirse değişmez (immutable) sınıflar, nesneleri üzerinde değişiklik yapılamayan sınıflardır. Her bir nesne içinde tanımlı olan değerler, nesne yaratılırken belirlenir ve nesnenin ömrü boyunca aynı kalır. Java platformu içerisinde tanımlı String, Integer, Boolean, BigInteger, BigDecimal gibi birçok değişmez sınıf bulunmaktadır. Bunun sebebi değişmez sınıfların daha kolay tasarlanabilmesi, yazılabilmesi ve kullanılabilmesidir. Ayrıca, değişmez sınıflar hata yapmaya daha az olanak verir ve daha güvenlidirler.

Bir sınıfı değişmez yapmak için aşağıdaki 5 kuralı uygulamalısınız:

1. Nesnenin durumunu değiştiren hiçbir metot tanımlamayın.
2. Sınıfı kalıtılamaz hale getirin. Böylece yazdığınız sınıfın değişmezliği, kötü niyetli veya dikkatsizce yazılmış çocuk sınıflar tarafından etkilenmemiş olur. Bunu yapmak için genellikle sınıf tanımlanırken final anahtar kelimesi kullanılır, ancak başka bir yol daha var ve buna ileride değineceğiz.
3. Sınıftaki bütün alanları (field) final olarak tanımlayın. Böylece alanlara ilk değerlerini atadıktan sonra değiştirilmesi mümkün olmayacaktır.
4. Sınıftaki bütün alanları private olarak tanımlayın. Böylece sınıf içerisinde değişebilir nesnelere referanslar varsa, bu nesnelerin değiştirilmesini engellemiş olursunuz. Her ne kadar değişmez sınıflar içerisinde temel değerler taşıyan (başka nesnelere referans olmayan) public final alanlar tanımlamak mümkün olsa da, ileride sınıfın iç yapısının değiştirilmesini kısıtlayacağı için önerilmez. (Madde 15 ve Madde 16)
5. Değişebilir alanlara olan erişimi kısıtlayın. Eğer değişmez sınıf içerisinde değişebilir nesnelere referanslar varsa, istemcilerinizin bu referanslara erişemediğinden emin olun. Bu tür referansları istemciye döndürmeniz gerekiyorsa, gösterdiği nesnenin bir kopyasını oluşturup onu döndürün. Aynı şekilde, örneğin bir yapıcı metot aracılığıyla istemciden gelen bir referansı değişmez sınıf içerisinde kullanmanız gerekiyorsa, yine nesnenin bir kopyasını oluşturup onu kullanın. Böylece istemci nesnede değişiklik yapsa bile değişmez sınıftaki nesneyi değil kopyasını değiştirmiş olacaktır. (Madde 50, Madde 88)

Daha önceki maddelerde yazdığımız sınıfların birçoğu değişmezdir. Buna örnek olarak Madde 11’de tanımladığımız PhoneNumber sınıfı verilebilir. PhoneNumber sınıfında her bir alan için erişim metodu olsa da, değer değişikliği yapabilen hiçbir metot yazılmamıştır. Aşağıda daha karmaşık bir örnek verilmiştir:

public final class Complex {
    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    // erişim sağlayan (getter) metotlar
    public double realPart() {
        return re;
    }

    public double imaginaryPart() {
        return im;
    } 

    public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    public Complex subtract(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex multiply(Complex c) {
        return new Complex(re * c.re - im * c.im, 
                           re * c.im + im * c.re);
    }

    public Complex divide(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp, 
                           (im * c.re - re * c.im) / tmp);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Complex)) {
            return false;
        }

        Complex c = (Complex) o;
        return Double.compare(re, c.re) == 0 && 
               Double.compare(im, c.im) == 0;
    }

    @Override
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

Yukarıdaki sınıf sanal (imaginery) ve gerçel (real) olmak üzere iki parçadan oluşan bir karmaşık sayıyı ifade etmektedir. Bu sınıf Object ata sınıfından gelen metotlar haricinde, sanal ve gerçel değerleri döndüren erişim metotları ve toplama, çıkarma, çarpma ve bölme işlemlerini yapan 4 farklı metot tanımlamaktadır. Aritmetik işlemleri yapan metotların, var olan nesneyi değiştirmek yerine her seferinde yeni bir nesne yaratıp döndürmesi dikkat edilmesi gereken bir noktadır. Bu yöntem birçok değişmez sınıf içerisinde kullanılmaktadır ve fonksiyonel yaklaşım olarak bilinmektedir, çünkü metotlar değer değişikliği yapmadan bir fonksiyonun sonucunu direk olarak istemciye döndürmektedirler. Bunun tersi olan prosedürel yaklaşımda ise metotlar, gelen parametreler üzerinde değişiklik yaparak durumlarını değişmesine neden olurlar.

Eğer alışık değilseniz fonksiyonel yaklaşım ilk başta biraz garip görünebilir, ancak sınıflarda değişmezliği sağladığı için çok avantajlıdır. Değişmez nesneler çok basittir ve durumları yaratıldıktan sonra değiştirilemez, dolayısıyla sonradan nesne üzerinde değişiklik yapılıp yapılmadığıyla ilgilenmek zorunda kalmazsınız. Diğer taraftan değişebilir nesneler ise herhangi bir zamanda herhangi bir durumda bulunabilirler. Eğer sınıfta değişime neden olan metotlar nesnenin durumları arasındaki geçişleri iyi belgelemezse, bu nesneleri kullanmak güvenli bir biçimde kullanmak çok zorlaşır.

Değişmez nesneler doğası gereği thread güvenlidir (thread safe), senkronizasyon gerektirmezler. Bir başka deyişle birden fazla thread (iş parçacığı) aynı nesne üzerinde güvenli bir biçimde işlem yapabilir. Değişmez nesneler kullanmak thread güvenliğini sağlamak için açık ara en kolay yöntemdir. Nesne üzerinde değişiklik yapılamadığı için aynı nesne birçok thread arasında güvenli bir biçimde paylaşılabilir. Değişmez sınıflar bu özelliklerini kullanmaları için istemcileri teşvik etmelidirler. Bunu yapmanın bir yolu, sık kullanılan nesneleri public static final olarak aynı sınıf içerisinde tanımlamaktır. Örneğin, yukarıda tanımladığımız Complex sınıfı aşağıdaki nesneleri de içerisinde barındırabilir.

public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

Bu yaklaşım bir adım daha ileriye götürülebilir. Değişmez sınıf statik fabrika metotları (Madde 1) tanımlayarak sık kullanılan nesneleri tekrar tekrar yaratmak yerine bir kere yaratıp her istemciye aynı nesneyi döndürebilir. Java’da Integer, Boolean gibi temel türleri temsil eden sınıflar (boxed primitive types) ve BigInteger sınıfı bunu yapmaktadır. Bu yöntemi kullanmak gereksiz nesne yaratılmasını önlediği için bellek kullanımını azaltır ve çöp toplayıcının (garbage collector) işini kolaylaştırır. Bir sınıfı tasarlarken public yapıcı metotlar (constructor) yerine statik fabrika metotları kullanmak, daha sonra istemcileri hiç etkilemeden önbellek (cache) eklemenize de olanak sağlar.

A consequence of the fact that immutable objects can be shared freely is that you never have to make defensive copies of them (Item 50). In fact, you never have to make any copies at all because the copies would be forever equivalent to the originals. Therefore, you need not and should not provide a clone method or copy constructor (Item 13) on an immutable class. This was not well understood in the early days of the Java platform, so the String class does have a copy constructor, but it should rarely, if ever, be used (Item 6).

Değişmez nesnelerin bir başka avantajı da kopyalamaya asla gerek kalmamasıdır. (Madde 50) Sonuç olarak kopyalanan nesne de orijinalinin aynısı olacak ve üzerinde değişiklik yapılamayacaktır. Bu sebeple, değişmez bir sınıf içerisinde clone metodu veya kopya yapıcı metot (copy constructor) tanımlamaya gerek yoktur (Madde 13). Bu durum Java’nın ilk zamanlarında çok iyi anlaşılmamış ve String sınıfı içerisinde bir kopya yapıcı metot tanımlanmıştır, bu yapıcı metodun kullanılması tavsiye edilmez. (Madde 6)

Değişmez nesneler başka nesnelerin oluşturulmasında yapı taşı görevi görürler.
Kendisi değişmez olsa da olmasa da, içerisinde değişmez bir nesne bulunduran karmaşık bir nesnenin durumu (state), değişebilir bir nesne bulunduran bir nesnenin durumuna göre çok daha tutarlı olacaktır. Örneğin, değişmez nesneler Map yapılarında harika bir anahtar görevi görürler ve Set yapılarına eleman olarak güvenle eklenebilirler. Bu değerler çalışma zamanında değişmeyeceği için, veri yapısına eklendikten sonra değer değişikliğinden kaynaklı bozulmalar yaşanmayacaktır.

Değişmez sınıfların tek dezavantajı, her bir değer için farklı bir nesne oluşturulması gerekmesidir. Bu yüzden büyük nesneler söz konusu ise değişmez nesneler kullanmak pahalı olabilir. Örneğin 1 milyon bitten oluşan bir BigInteger nesnesi üzerinde tek bir biti değiştirmek istediğimizi düşünelim:

BigInteger moby = ...;
moby = moby.flipBit(0);

2. satırdaki flipBit() metodu yine 1 milyon bit uzunluğunda başka bir BigInteger nesnesi yaratacaktır ve aradaki tek fark 1 bittir. BigSet sınıfı ise, BigInteger gibi istediğiniz uzunlukta bir bit kümesi tanımlamanızı sağlar ancak değişebilirdir. Bu sınıf, bit değiştirme işlemlerini aynı nesne üzerinde yaptığı için yukarıdaki aynı işlemi çok daha çabuk gerçekleştirir.

BitSet moby = ...;
moby.flip(0);

Bu performans problemi, her biri yeni bir nesne yaratılmasına neden olan birkaç farklı işlemi art arda yapıyorsanız daha da belirgin olacaktır. Çünkü en son yaptığınız işlem haricinde yaratılan bütün nesneler geçersiz kalacaktır. Bu durumla başa çıkmanın en pratik yolu değişebilir bir yardımcı sınıf yazmaktır. Bunun en güzel örneği Java platformu içerisinde bulunan String ve StringBuilder sınıflarıdır. String değişmez bir sınıf olarak tasarlanmış, ancak birden çok adımda oluşturulan String nesneleri performans kaybına yol açmasın diye değişebilir StringBuilder sınıfı da yazılımcılara sunulmuştur.

Artık bir sınıfı nasıl değişmez yapabileceğinizi ve bu yaklaşımın iyi ve kötü taraflarını öğrendiğinize göre, birkaç değişik tasarım üzerinde konuşabiliriz. Değişmezliği sağlayabilmek için sınıfın kalıtılmasını engellemek gerektiğini hatırlayın. Genel olarak bu sınıfı final tanımlayarak yapılabilir ancak ikinci ve daha esnek bir yol daha var. Sınıfı final tanımlamak yerine bütün yapıcı metotları private veya package private yazabilir, public yapıcı metotlar yerine de public statik fabrika metotları tanımlayabiliriz. (Madde 1)

Bu durumu örneklemek için aşağıdaki kodu verebiliriz:

// Yapıcı metot yerine statik fabrika metotu kullanılmış değişmez (immutable) sınıf
public class Complex {
    private final double re;
    private final double im;

    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }

    ... // Sınıfın geri kalanında değişiklik yok..
}

Bu yaklaşım çok sık kullanılmasa da aslında çoğu zaman en iyi seçenektir. Aynı zamanda esnektir çünkü yapıcı metodu package private tanımlarsanız, kendiniz paket içerisinde sınıfı kalıtabilir ancak istemcilerinizin kalıtmasını engelleyebilirsiniz. Paket dışında kalan istemciler açısından bakıldığında bu sınıf final tanımlanmış gibidir, çünkü başka bir paketten gelen sınıfı eğer public veya protected bir yapıcı metodu yoksa kalıtmak mümkün değildir. Bu avantajın yanında, bu yaklaşım sonraki versiyonlarda statik fabrikanın geliştirilerek, istemci koduna dokunmadan önbellek (cache) eklenebilmesine de olanak sağlar.

Statik fabrika metotları Madde 1’de anlatıldığı üzere yapıcı metotlara göre birçok fayda sağlarlar. Örneğin yukarıdaki Complex sınıfına, karmaşık sayıları kutup koordinatları kullanarak yaratma özelliğini eklemek istediğimizi düşünelim. Bunu yapıcı metot kullanarak yapmak işleri çok karıştıracaktır çünkü kullanacağımız yapıcı metot imzası sınıfta zaten var olan yapıcı metotla çakışacaktır: Complex(double, double). Statik fabrika kullandığımızda ise çok kolaydır, tek yapmamız gereken farklı bir isimle ikinci bir metot eklemek olacaktır.

public static Complex valueOfPolar(double r, double theta) {
    return new Complex(r * Math.cos(theta), r * Math.sin(theta));
}

Değişmez sınıfların kalıtılmaması kuralı malesef BigInteger ve BigDecimal sınıfları yazılırken ihmal edilmiştir ve bugün bu sınıfların bütün metotları kalıtılabilir durumdadır. Bu durumun düzeltilmesi geriye uyumluluğu (backwards compatibility) bozacağı için mümkün değildir. Dolayısıyla, güvenmediğiniz istemcilerden gelen BigInteger veya BigDecimal türünden argümanları, eğer değişmezlik kriteri sizin için önemliyse gerçekten BigInteger veya BigDecimal türünden mi yoksa kalıtılmış bir tür mü olduğunu kontrol etmelisiniz. Eğer argüman kalıtılmış bir tür ise kendinize ata sınıftan yeni bir kopya oluşturmalısınız.

public static BigInteger safeInstance(BigInteger val) {
    if (val.getClass() != BigInteger.class)
        return new BigInteger(val.toByteArray());
    return val;

Yukarıda bahsettiğimiz değişmezlik kurallarında sınıfın durum değişikliği yapan metot bulundurmaması ve bütün alanların final olması gerektiğinden bahsetmiştik. Bu kurallar performası artışı sağlayabileceğimiz yerlerde esnetilebilir. Değişmez sınıf, hiçbir koşulda istemcilerin görebileceği bir değişikliğe izin vermemelidir, ancak kendi içerisinde performansı artırmak için belli hesaplamalar yapıp saklayabilir. Aynı değer tekrar lazım olduğunda tekrar hesaplamak yerine sakladığı değeri döndürülebilir. Bu tekniğin işe yaramasının sebebi sınıfın değişmez olmasıdır, çünkü aynı hesaplama tekrar tekrar yapılsa bile her seferinde aynı sonucu üretecektir.

Örneğin Madde 11’da yazdığımız PhoneNumber sınıfının hashCode() metodu, ilk çağrıldığında hash kodunu hesaplamakta ve tekrar çağrıldığında daha önce hesapladığı değeri döndürmektedir. Bu teknik String sınıfı tarafından da kullanılmaktadır. (Madde 83)

Serileştirme ile ilgili bir uyarıdan da bahsedelim. Eğer değişmez sınıf değişebilir bir nesneye referans içeriyorsa, Serializable arayüzünü gerçekleştirirken bir readObject veya readResolve metodu tanımlamanız, veya ObjectOutputStream.writeUnshared ve ObjectInputStream.readUnshared metotlarını kullanmanız gerekmektedir. Aksi taktirde bir saldırgan sınıfınızdan değişebilir bir nesne yaratabilir. Bu konu Madde 88’de detaylı ele alınmaktadır.

Özetlemek gerekirse, alanların değerlerini değiştiren metotları mecbur değilseniz yazmayın. (setter metotlar) Sınıflar, değişebilir olması için mantıklı bir sebep yoksa değişmez yazılmalıdır. Değişmez sınıflar birçok yönden avantaj sağlarlar ve tek kötü tarafları bazı şartlar altında performans kaybına sebebiyet vermeleridir. Birkaç değer taşımaktan ibaret olan PhoneNumber ve Complex gibi sınıfları her zaman değişmez yapmalısınız. (Java platformu içerisinde java.util.Date ve java.awt.Point gibi değişmez olması gereken ama olmayan sınıflar vardır.) Büyük veriler taşıyabilen String ve BigInteger gibi sınıfları değişmez yapıp yapmama konusunda dikkatli karar vermelisiniz. Eğer performans kaybını engellemek için gerçekten gerekliyse, ek olarak bir de yardımcı değişebilir sınıf yazmalısınız. (Madde 67)

Bazı sınıfları değişmez yapmak pratik olarak mümkün olmayabilir. Böyle durumlarda değişebilirliği mümkün olduğunca kısıtlamalısınız. Dolayısıyla, nesnelerin alanlarını aksi bir sebep yoksa final tanımlamalısınız.

Bunun dışında, yapıcı metotların nesneleri bütünüyle yaratması gerekmektedir. Yapıcı metot veya statik fabrika dışında, nesneye ilk değer atamaları yapan başka bir metot daha eklemeyin. Aynı şekilde nesneleri yeniden kullanabilmek için içindeki değerleri sıfırlayıp sanki sıfırdan üretiliyormuş gibi davranan metotlar da yazmayın. Bu tarz metotlar çok az bir performans katkısı verip karmaşıklığı çok artırmaktadırlar.

CountDownLatch sınıfı bu prensipleri başarıyla uygulamaktadır. Değişebilir bir sınıf olmasına rağmen, durum uzayı kasten küçük tutulmuştur. Nesne bir kere yaratılır, kullanılır ve işe yaramaz hale gelir. Değeri 0’a ulaştığında tekrar aynı nesneyi kullanmak mümkün değildir.

Son olarak, yukarıda yazdığımız Complex sınıfı sadece değişmezliği örneklemek için kabataslak yazılmıştır. Gerçek uygulamalarda kullanılabilecek sağlamlıkta ve güvenilebilirlikte değildir.

Share

Effective Java Madde 14: public sınıflarda erişim metotları kullanın, public alanlar değil

Zaman zaman birkaç tane alanı bir arada tutmaktan başka bir iş yapmayan sınıflar yazma eğiliminde olabilirsiniz:

// Bunun gibi sınıflar public olmamalıdır!
class Point {
    public double x;
    public double y;
}

Bu tür sınıfların veri alanları direk erişime açık olduğu için, kapsülleme prensibinin (encapsulation) faydalarını kullanamazlar. UPA’yı (API) değiştirmeden kod içerisinde değişiklik yapmak zorlaşır, sabit değerler tanımlayamazsınız ve bir alana erişildiğinde müdahale etme şansınız olmaz. Katı görüşlü programcılar bu tür sınıfları lanetli olarak görürler ve bunların yerine private alanları ve public erişim metotları olan sınıflar yazılması gerektiğini savunurlar.

Read more “Effective Java Madde 14: public sınıflarda erişim metotları kullanın, public alanlar değil”

Share

Effective Java Madde 13: Sınıfların ve Üyelerinin Erişilebilirliğini Kısıtlayın

İyi tasarlanmış bir yazılım modülünü kötü tasarlanmış bir modülden ayıran en önemli faktör, içindeki verileri ve gerçekleştirim (implementation) detaylarını diğer modüllerden ne kadar iyi saklayabildiğidir. İyi tasarlanmış bir modül, gerçekleştirim detaylarını net bir biçimde diğer modüllerden gizler. Modüller birbirleriyle tanımladıkları API (Uygulama Programlama Arabirimi, UPA) üzerinden konuşurlar ve birbirlerinin iç dünyasından habersiz olmaları gerekir. Bu konsept, çok temel bir yazılım tasarım ilkesidir ve bilgi saklama yada kapsülleme (encapsulation) olarak bilinir.

Bilgi saklama konsepti birçok açıdan önemlidir. En önemli faydası ise yazılım modüllerini birbirinden ayrıştırarak (decoupling) birbirinden bağımsız bir şekilde geliştirilebilmesini, test edilebilmesini ve kullanılabilmesini sağlamaktır. Bu durum, bir sistemin geliştirilmesini hızlandırır çünkü farklı modüller paralel bir biçimde aynı anda geliştirilebilir. Ayrıca bakım yapmayı ve modülün anlaşılmasını kolaylaştırır, bir problem olduğunda diğer modülleri bozma riski olmadan hata araştırılıp, çeşitli çözümler denenebilir. Bilgi saklama kendi başına bir sistemin yüksek performansla çalışmasını sağlamasa da, birbirinden bağımsız modüllerin diğerlerini etkilemeden optimize edilebilmesini sağlar. Dolayısıyla sistemi test ederek hangi modülün yavaş çalıştığını tespit edebilir, sistemin diğer parçalarına zarar vermeden yavaş çalışan modülü optimize edebilirsiniz. Birbirinden bağımsız modüller aynı zamanda tekrar kullanılabilirliği (reusability) yüksek olan modüllerdir. Bilgi saklama tekniği büyük sistemler geliştirmenin riskini önemli ölçüde azaltır, çünkü sistem bir bütün olarak doğru çalışmasa da bağımsız modüllerin doğru çalıştığı kanıtlanabilir.

Read more “Effective Java Madde 13: Sınıfların ve Üyelerinin Erişilebilirliğini Kısıtlayın”

Share

Effective Java Madde 12: Comparable Arayüzünü Gerektiğinde Uygulayın

Bu bölümde gördüğümüz diğer metotların aksine, compareTo() metodu Object içerisinde tanımlanmış değildir. Bunun yerine Comparable arayüzünün tek metodu olarak karşımıza çıkmaktadır. Karakter olarak Object sınıfındaki equals() metoduna benzer ancak eşitlik karşılaştırması yanında sıralama da yapabilir ve üreyseldir (generic). Comparable arayüzünü uygulayarak, sınıfınızın nesneleri arasında sıralama yapılabileceğini belirtmiş olursunuz. Bu arayüzü uygulayan nesnelerden oluşan bir diziyi (array) sıralamak aşağıdaki gibi son derece kolaydır:

Arrays.sort(a);

Benzer şekilde, Comparable arayüzü uygulandığında arama yapma, uç değerleri bulma, kendiliğinden sıralı veri yapılarını yönetme gibi problemler de çok kolaylaşır. Örneğin, aşağıdaki program komut satırından girilen parametreleri alfabetik sırada yazdırmakta ve tekrar edilen değerleri elemektedir. Bu kodun doğru çalışması String sınıfının Comparable arayüzünü uygulamasıyla mümkün olmaktadır.

public class WordList {
        public static void main(String[] args) {
        Set<String> s = new TreeSet<String>();
        Collections.addAll(s, args);
        System.out.println(s);
    }
}

Read more “Effective Java Madde 12: Comparable Arayüzünü Gerektiğinde Uygulayın”

Share

Effective Java Madde 10: toString() Metodunu Her Zaman Geçersiz Kılın

Her ne kadar java.lang.Object bir toString() gerçekleştirimi sunsa da, geri döndürdüğü karakter dizisi genellikle sizin sınıfınızı kullanmak isteyen bir yazılımcının görmek istediği şey değildir. Bu değer sınıfın adı, ardından gelen ‘@’ karakteri ve nesnenin hash kodunun onaltılık sistemde (hexadecimal) ifadesini içermektedir, örneğin “PhoneNumber@163b91”. toString sözleşmesi, bu metottan döndürülen değerin “kolay okunup anlaşılabilen, kısa ama bilgi verici” olması gerektiğini söyler. “PhoneNumber@163b91” değerinin kısa ve okuması kolay olduğunu savunabilirsiniz ancak “(707) 867-5309” ile karşılaştırıldığında bilgi verici olmadığı açıktır. toString sözleşmesi bunların haricinde “Bu metodu bütün alt sınıfların geçersiz kılması tavsiye edilir” demektedir. Kesinlikle uyulması gereken bir tavsiye!

Her ne kadar equals (Madde 8) ve hashCode (Madde 9) sözleşmeleri kadar kritik olmasa da, güzel bir toString gerçekleştirimi sağlamak sınıfınızı çok daha kolay kullanılabilir hale getirir. toString metodu, bir nesne printf, println gibi metotlara parametre olarak geçildiğinde, karakter dizilerine eklendiğinde veya assert ile birlikte kullanıldığında otomatik olarak işletilir.

Read more “Effective Java Madde 10: toString() Metodunu Her Zaman Geçersiz Kılın”

Share

Effective Java Madde 11: equals() ile Birlikte Mutlaka hashCode() Metodunu da Geçersiz Kılın

Önemli Not: Hash tabanlı veri yapılarının Java’da nasıl çalıştığını bilmiyorsanız bu yazıyı okumadan önce araştırmanızı şiddetle tavsiye ederim.

Object sınıfından gelen hashCode metodunu gerektiği yerde geçersiz kılmamak (override) birçok hatanın kaynağını oluşturur. equals metodunu geçersiz kıldığınız her sınıfta hashCode metodunu da geçersiz kılmanız gerekir. Bunu yapmamak Object.hashCode metodunun sözleşmesini ihlal etmek demektir, bu da sınıfınızın hash tabanlı HashMap, HashSet, HashTable gibi veri yapılarıyla birlikte kullandığında yanlış çalışmasına yol açar. Bunun sebebi ise Object sınıfından gelen hashCode metodunun hash kodunu hesaplarken nesnenin o anda bulunduğu bellek adresini kullanmasıdır. Her nesnenin bellek adresi farklı olacağı için hesaplanan hash kodu da farklı olacaktır. Object sınıfı belirtiminde tarif edildiği üzere sözleşme genel olarak şu şekildedir:

  • equals karşılaştırmasında kullanılan alanlar sabit kaldığı sürece, hashCode metodu aynı uygulama içerisinde üst üste çağrıldığında her zaman aynı sonucu üretmelidir.
  • Eğer iki nesne equals metoduna göre birbirine eşitse, bu iki nesnenin hashCode metotları da aynı integer değerini üretmelidir.
  • Eğer iki nesne equals metoduna göre eşit değilse, hashCode metodu bu iki nesne için farklı integer sonuçları üretmek zorunda değildir. Ancak yazılımcı bilmelidir ki eşit olmayan nesneler için farklı hash kodları üretmek hash table performansını artırabilir.

equals ile birlikte hashCode metodunu geçersiz kılmadığınız zaman 2. koşulu ihlal etmiş olursunuz: eşit nesneler eşit hash kodu üretmelidir. Bu durumda birbirinden farklı iki nesne equals metoduna göre eşit olsa da, hashCode metoduna göre aralarında pek de bir benzerlik olmayacaktır. Bu yüzden de hashCode metodu sözleşmenin aksine bu iki eşit nesne için çok farklı değerler üretecektir.

Aşağıda equals metodu sözleşmeye uygun bir biçimde yazılmış (Madde 10) PhoneNumber sınıfını düşünecek olursak:

 public final class PhoneNumber {

    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode,    999, "area code");
        rangeCheck(prefix,      999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");

        this.areaCode  = (short) areaCode;
        this.prefix  = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    private static void rangeCheck(int arg, int max, String name) {
        if (arg < 0 || arg > max)
            throw new IllegalArgumentException(name + ": " + arg);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;

        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNumber == lineNumber 
            &amp;&amp; pn.prefix  == prefix 
            &amp;&amp; pn.areaCode == areaCode;
    }

    // Bozuk - hashCode() metodu yok!
}

Şimdi bu sınıfı bir HashMap ile kullandığımızı düşünelim:

Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");

Bu noktada m.get(new PhoneNumber(707, 867, 5309)) çağrısının “Jenny” değerini döndürmesini bekleyebilirsiniz ancak null dönecektir. Burada iki PhoneNumber nesnesinin olduğuna dikkat edin, bir tanesi HashMap içerisine eklenirken ikinci ve eşit olan nesne HashMap üzerinden değeri geri almak için kullanılıyor. PhoneNumber sınıfının hashCode metodunu geçersiz kılmaması, eşit iki nesnenin farklı hash kodları üretmesine neden olmaktadır. Bu yüzden de get() metodu, aradığımız değeri put() metodunun kullandığı hash bölmesinde (bucket) değil başka bir bölmede arayacaktır ve bulamayacaktır. Tesadüfen bu iki nesnenin hash kodları aynı bölmelere denk gelse bile yine de null değeri dönecektir çünkü Java’da hash tabanlı veri yapıları hash kodları farklı olan nesnelerin eşit olmadığını varsayar ve equals metoduna bakmadan null döndürürler.

Bu problemi çözmek için PhoneNumber sınıfı içerisine uygun bir hashCode metodu yazmak gereklidir. Peki hashCode metodu nasıl olmalıdır? Sözleşmeye uyan ama doğru olmayan bir hashCode metodu yazmak çok basittir. Aşağıdaki örnek hashCode sözleşmesine uysa da asla kullanılmamalıdır:

// Sözleşmeye uyarak yazılabilecek en kötü hashCode(), asla kullanmayın!
@Override
public int hashCode() {
    return 42;
}

Yukarıdaki metod hashCode sözleşmesine uygundur çünkü eşit nesneler için aynı değeri üretecektir. Ancak yine de çok kötü yazılmıştır çünkü sadece eşit nesneler için değil bütün nesneler için aynı değeri üretecektir. Bu yüzden de hash tabanlı veri yapılarıyla birlikte kullanıldığında bütün nesneler aynı hash bölmesine (bucket) eklenecek ve örneğin bir HashTable bağlı liste (linked list) gibi çalışacaktır. Bu da erişim performansını çok büyük oranda olumsuz etkileyecektir. Büyük veri yapıları için bu performans farkı hiç çalışmamakla eşdeğerdir.

İyi bir hashCode metodu eşit olmayan nesneler için büyük oranda farklı değerler üretmelidir. hashCode sözleşmesinde bulunan 3. madde ile anlatılmak istenen tam olarak budur. İdeal bir hash fonksiyonu, makul sayıdaki eşit olmayan bir nesne yığınını, mümkün olan bütün hash kod değerleri üzerinde eşit olarak dağıtabilmelidir. Bu ideal fonksiyonu yazabilmek çok zor olabilir ama yaklaşmak mümkündür. İşte sizlere bir yöntem:

  1. result adında bir int değişken tanımlayın ve ilk değer olarak sınıftaki ilk anlamlı alanın hash kodunu 2.a adımında belirdiği gibi hesaplayarak atayın. (Madde 10’dan hatırlayacağınız üzere, anlamlı alanlar equals hesaplamasında kullanılan, mantıksal eşitlik hesabında kullanılan alanlardır.)
  2. Geriye kalan her anlamlı f alanı için, aşağıdakileri yapın
    1. Bu alan için değeri c olacak şekilde hash kodunu hesaplayın
      1. Eğer anlamlı alan bir ilkel tür ise, IlkelTur.hashCode(f) kullanın, burada tür anlamlı alanın ilkel türüne göre değişecektir, örneğin Short.hashCode(f), Integer.hashCode(f)
      2. Alan bir nesne referansı ise ve sınıfın equals metodu bu alanı karşılaştırırken özyineli olarak referans üzerinden equals metodunu çağırıyorsa, siz de referans nesnenin hashCode metodunu çağırın. Eğer referans null ise 0 değerini döndürün.
      3. Eğer anlamlı alan bir dizi ise, dizinin her elemanı ayrı bir alanmış gibi hesaplama yapın. Yani, dizideki her anlamlı alan için yukarıdaki kuralları uygulayın ve 2.b’de anlatıldığı gibi bunları birleştirin. Eğer dizide anlamlı eleman yoksa 0 haricinde herhangi bir sabit değer, bütün alanları anlamlı ise, Arrays.hashCode() metotlarından birini kullananın.
    2. Yukarıda hesaplanan c hash kodunu, aşağıdaki formülü kullanarak result değişkenine ekleyin
      result = 31 * result + c;
  3. result değerini döndürün.

Yazmayı tamamladığınızda kendinize şu soruyu sorun: Eşit nesneler aynı hash kodunu üretiyor mu? Bunu doğrulamak için birim testler yazın. AutoValue kütüphanesini kullanarak equals ve hashCode metotlarını otomatik ürettiyseniz bu testlere gerek kalmayabilir. Eğer eşit nesneler farklı hash kodları üretiyorsa, problemi bulun ve giderin.

Eşitlik hesaplamasında kullanmadığınız anlamlı alanları hash kod hesabından da mutlaka çıkartmalısınız, aksi taktirde birbirine eşit nesnelerin farklı hash kodları üretmesine sebep olabilirsiniz. Değeri başka anlamlı alanlar kullanılarak hesaplanabilen alanları da isterseniz hash kod hesaplamasından çıkartabilirsiniz.

2.b’de yapılan çarpma işlemi ise anlamlı alanları sırasının da üretilen hash koduna etki etmesini sağlar, bu da eğer sınıf içerisinde çok sayıda benzer alan varsa hash metodunun çok daha iyi çalışmasını sağlar. Örneğin, bu çarpma işlemi String sınıfının hashCode metodunda olmasaydı, bütün anagramlar aynı hash koduna sahip olurlardı. Çarpma işleminde 31 sayısının kullanılmasının sebebi ise asal bir tek sayı olmasıdır. Çift bir sayı seçersek ve çarpma işleminin sonucu maksimum saklayabileceğimiz değeri geçerse veri kaybetmiş oluruz, çünkü sayıyı 2 ile çarpmak bitlerini kaydırmakla eşdeğerdir. Burada asal bir sayı kullanmanın avantajı çok belirgin olmasa da, geleneksel olarak asal tek sayılar kullanılır. 31 sayısının güzel bir özelliği ise, daha iyi performans alabilmek için kaydırma ve çıkarma işlemleri ile ifade edilebilmesidir: 31 * i == (i << 5) - i Bu dönüştürme işlemi çoğu zaman sanal makina tarafından otomatik olarak yapılmaktadır.

Şimdi yukarıdaki tarifi PhoneNumber sınıfına uygulayalım. Bu sınıfta 3 adet anlamlı alan var ve hepsi de short türünde:

@Override 
public int hashCode() {
    int result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}

Bu metod 3 anlamlı alanı da hesaba katarak basit bir hesaplama ile hash kodu ürettiği için, birbirine eşit PhoneNumber nesnelerinin aynı hash kodunu üreteceği de gayet açıktır. Aslına bakarsanız bu metod PhoneNumber sınıfı için harika bir hashCode metodudur. Basit, hızlı ve eşit olmayan nesneleri farklı hash bölmelerine atamak konusunda gayet başarılı bir iş çıkarmaktadır.

Bu yazıda tarif edilen yöntemle, çoğu durumda yeterli olacak çok iyi hash fonksiyonları yazılabilir, ancak her zaman kusursuz sonuçlar alamayabilirsiniz. Daha üst düzeyde hash fonksiyonları üretebilmek için Guava kütüphanesinin com.google.common.hash.Hashing sınıfına bakabilirsiniz.

Objects sınıfı, istediğiniz sayıda nesneyi geçip bir hash kodu üretebileceğiniz, statik Objects.hash() metodunu barındırmaktadır. Bu metodu kullanarak, tek satırdan oluşan ve bu yazıdaki önerileri uygulayan bir hashCode metodunu kolayca yazabilirsiniz. Dezavantajı ise, geçilen argümanları diziye çevirdiği için ve ilkel türler kullanıldığında boxing ve unboxing yaptığı için biraz daha yavaş çalışmasıdır. Eğer performans uygulamanız için çok kritik değilse bu yolu deneyebilirsiniz. Aşağıda, PhoneNumber sınıfı için bu yöntemle yazılmış bir hash metodu görebilirsiniz:

// Tek satırlık hashCode metodu, vasat performans
@Override 
public int hashCode() {
    return Objects.hash(lineNum, prefix, areaCode);
} 

Eğer bir sınıf değişmez (immutable) ise ve hash kodunu hesaplamak zaman alıyorsa, hash kodunu nesnenin içerisinde saklayıp her defasında hesaplamadan kolayca döndürebilirsiniz. Eğer bu türdeki nesnelerin hash anahtarı olarak kullanılacağını öngörüyorsanız, nesne yaratıldığında hash kodunu hesaplayın, aksi taktirde hashCode metodu ilk defa çağrıldığında hesaplayın. PhoneNumber sınıfı için bu çok gerekli değildir ancak nasıl yapılacağını göstermek için aşağıdaki kodu yazabiliriz:

// Tembel ilklendirme (lazy initialization), ön bellekte saklanan hashCode
private int hashCode;  // ilk değer 0 olacaktır

@Override
public int hashCode() {
    int result = hashCode;
    if (result == 0) {
        result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        hashCode = result;
    }
    return result;
}

Performansı artırmak için sınıfınızdaki anlamlı alanları hash kod hesaplamasından çıkarmaya çalışmayın. Hash fonksiyonunun daha hızlı çalışmasını sağlayabilirsiniz ancak hash tabanlı veri yapılarınız kullanılamayacak kadar yavaş çalışabilir. Hash fonksiyonu, sizin ihmal ettiğiniz alanlarda farklılaşan nesnelerle karşı karşıya kalırsa, bütün bu nesneleri çok küçük bir hash kod kümesine eşleştirebilir ve bu da hash tabanlı veri yapılarının performansını kötü etkiler. Bu, teoride kalmış bir problem değildir. Java 1.2 öncesinde yazılmış olan String sınıfının hashCode metodu, ilk karakterden başlayıp eşit aralıklarla devam ederek en fazla 16 karakteri dikkate alıyordu. Bu hesaplama yöntemi URL gibi hiyerarşik yapıya sahip karakter dizilerinin için kullanıldığında tam olarak burada anlatılan probleme yol açmıştır.

Özet olarak, equals metodunu geçersiz kıldığınız her durumda hashCode metodunu da geçersiz kılmanız şarttır. Bunu yapmadığınız taktirde uygulamanız yanlış çalışacaktır. Bunu yaparken de, yukarıda anlatıldığı gibi belirtilen kurallara uymanız gerekmektedir. Ancak bunu kendimiz yazmaktansa, Madde 10‘da belirtildiği üzere AutoValue kütüphanesini veya IDE’lerin otomatik kod üretme özelliklerini kullanmak, iyi bir alternatif olacaktır.

Share