14 Eylül 2011 Çarşamba

Değişken Uzunluklu Diziler: java.util.Vector

Dizi yapısının belki de en büyük artısı, eleman erişim ve eleman güncellemenin sabit maliyetli olmasıdır; hangi indisteki eleman söz konusu olursa olsun, doğrudan erişim sayesinde her iki işlem de sabit (ve yüksek) hızlı bir şekilde yapılabilmektedir. Ancak, bunu mümkün kılan "elemanların dizinin yaratıldığı noktada ayrılan ardışık bellek konumlarına yerleştirilmesi" özelliği, yapının statik olması sonucunu da doğurur. Yani, dizinin yaratılması sonrasında uzunluğunun değiştirilmesi olanaklı değildir. Dolayısıyla, kullanım sırasında bir taşmanın olmaması için, yer ayrımının olası en yüksek eleman sayısı düşünülerek yapılması gerekir. Bunun sonucu da kimi zaman, ayrılan yerin tümünden yararlanılmadığından, belleğin verimsiz kullanımı olacaktır.

Bu olumsuzluğu gidermek için, dizi tutacağını yeri yeni ayrılmış ve baş kısmında eski dizi nesnesinin kopyasına sahip daha büyük bir dizi nesnesini gösterecek şekilde güncellemeyi düşünebilirsiniz. Java karşılığı aşağıda sunulan bu yaklaşımın iki zaafı vardır: i) dizinin büyümesi için gerekli olan kod programcı tarafından yazılmak zorundadır ve ii) programcı dizinin ne zaman büyümesi gerektiğini bilmelidir.
int[] dizi = {0, 1, 4};
...
dizi = Arrays.copyOf(dizi, dizi.length * 2);
dizi[3] = 9;
İşte bu yazımızda, Java kitaplığı tarafından değişken uzunluklu dizileri desteklemek amacıyla sağlanan java.util.Vector sınıfına bakacağız. Vector nesnelerinin sahip olması gereken öznitelikler ile başlayalım: sığa, eleman sayısı ve sığa artımı. Sığa, bir Vector nesnesinin büyümeye gerek olmadan kaç değer tutabileceğini gösterirken; eleman sayısı, sorgulandığı anda Vector'de kaç değer tutulmakta olduğunu gösterir. Vector nesnesinin dolması sonrasında—yani eleman sayısı ile sığanın eşit olması halinde—yeni bir elemanın eklenmesi istenecek olursa, taşmayı önlemek adına Vector nesnesinin sığası, sığa artımı özelliğinde öngörüldüğü miktarda artırılarak yeni eleman için yer açılır ve ekleme yapılır.

Yapıcılar


Vector nesnelerine gönderilebilecek iletilere geçmeden önce, sağlanan yapıcılara bir göz atalım. Aşağıda verilen örneklerden de görülebileceği gibi, Vector sınıfı soysaldır; tutacaklarının tanımlandığı ve nesnelerinin yaratıldığı noktalarda ilişkin kapta tutulacak elemanların türü bilgisini tür argümanı olarak ister. Tür argümanının es geçilmesi, Vector sınıfının J2SE 5.0 öncesinde olduğu gibi tür güvenliği olmaksızın kullanılacağı anlamına gelir.
Vector<Integer> intVec1 = new Vector<Integer>(50, 10);
Vector<Integer> intVec2 = new Vector<Integer>(50);
Vector<Integer> intVec3 = new Vector<Integer>();
intVec1.add(1);
...
Vector<Integer> intVec4 = new Vector<Integer>(intVec1);
boolean aynıMı = intVec4 == intVec1; // aynıMı ← false
boolean eşitMi = intVec4.equals(intVec1); // eşitMi ← true
intVec4.set(0, 11);
eşitMi = intVec4.equals(intVec1); // eşitMi ← false
İlk yapıcı çağrısı, başlangıç sığası 50 olan ve taşma noktalarında 10 elemanlık artımla genişletilecek bir Vector nesnesi yaratır. Sığa artım argümanı olarak pozitif olmayan bir değer geçirilmesi veya ikinci satırdaki yapıcı çağrısında olduğu gibi artım değerinin sağlanmaması durumunda, Vector nesnesinin sığası her taşma noktasında ikiye katlanarak artırılacaktır. Buna göre, intVec2 tutacağı ile gösterilen nesne 51. elemanın eklenmesi istendiği noktada 100, 101. elemanın eklenmek istendiği noktada ise 200 eleman tutabilecek şekilde genişletilecektir. Benzer bir sığa artırım politikası izlenecek üçüncü satırdaki varsayılan yapıcı çağrısı, on elemanlık başlangıç sığasına sahip bir Vector nesnesi yaratacaktır. Son satırdaki çağrı ise, kopyalayan yapıcı görevini görecek ve kendisine geçirilen Collection<E> kategorisindeki bir kabın elemanlarını gezicinin döndürdüğü sırada yaratılmakta olan Vector nesnesine kopyalayacaktır. Kopyalamanın sonrasında eşit fakat birbirlerinden farklı (ve bağımsız) iki Vector nesnesinin var olacağı ve bunlardan birisine yapılacak değişikliğin diğerini etkilemeyeceği unutulmamalıdır. Ayrıca, yeni yaratılan Vector nesnesinin ilk sığası diğerinin eleman sayısı ile sınırlı olacaktır.

Desteklenen Arayüzler


Vector sınıfının dokümantasyonuna baktığınızda, desteklenen iletilerin çokluğu gözünüzü korkutabilir. Ancak, bunların bir bölümünün bir diğer ileti ile eşdeğer olduğu, bir bölümünün ise bildiğiniz sınıflardan tanıdık geleceği düşünüldüğünde, işimizin o kadar da zor olmadığı görülecektir. Ne de olsa, aşağıda verilen soy ağacına sahip olan Vector sınıfı, gerçekleştirilen ortak arayüzler ve kalıtlanılan ortak üstsınıflar nedeniyle Veri Kapları Çerçevesi'ndeki diğer sınıflarla büyük benzerlikler gösterir.
Iterable arayüzü ile başlayalım. Genelde programcı tarafından doğrudan kullanılmayacak olan iterator iletisini içeren bu arayüz, derleyiciye söz konusu sınıfın gezici for döngüsü ile [baştan sona] dolaşılabileceğini garanti eder. Mesela, aşağıdaki kod parçası, hiç gezici nesne yaratıp, bu nesneye ileti göndermek zahmetine katlanmadan intVec4'in gösterdiği kaptaki tüm elemanların kareköklerini standart çıktıya basar.
for (Integer eleman : intVec4)
  System.out.println(Math.sqrt(eleman));
Gerçekleştirilen arayüzler listesindeki Cloneable, Vector nesnelerinin kopyalarının çıkarılabileceği anlamına gelir. Object sınıfındaki clone metodunun gösterge arayüz olan Cloneable'ı gerçekleştiren sınıflarda özel bir biçimde ezilmesiyle yerine getirilen sözleşme sonucunda, clone iletisi Object türlü bir tutacakla dahi gönderilebilir ve yaratılan kopya Object tutacağı aracılığıyla döndürülür. Dolayısıyla, yeni kopyaya Vector sınıfına has iletiler gönderilmesi öncesinde sonucun biçimlendirilmesi gerekecektir. Ayrıca; işlem sırasında elemanları tutan kap kopyalandığı için hedef nesne ile çıkarılan kopya ayrı nesneler olacaktır. Bu, nesnelerin atıflarının (tutacak) geçirildiği Java'da, değer geçirme yönteminin benzetimi için kullanılabilir.
...
public void mtt(Vector<Integer> vec) {
  Vector<Integer> vecKopya = 
    (Vector<Integer>) vec.clone();
  // vecKopya'yı kullan.
  ...
} // void mtt(Vector<Integer>) sonu
java.util.RandomAccess de gösterge arayüz olup herhangi bir ileti içermez. İşaret görevini gören bu arayüzün varlığı, kimi zaman doğrudan erişimli kimi zamansa doğrudan erişimli olmayan kapları işleyerek işini gören soysal kod parçalarında, doğrudan erişimliliğin avantajlarından yararlanılabilecek noktalarda bu özelliğin kullanılmasıyla performansın artmasını olanaklı kılar. Bunun için, nesnenin RandomAccess arayüzünü destekleyip desteklemediğinin aşağıdaki gibi denetlenmesi yeterli olacaktır.
...
if (kap instanceof RandomAccess) {
  // Doğrudan erişimin avantajlarından yararlan.
  ...
} else { ... }
...
Desteklenen gösterge arayüzlerden bir diğeri olan java.io.Serializable, Vector nesnelerinin disk dosyası, ağ gibi çevre aygıtlara dışsallaştırılıp, aynı aygıtlardan içselleştirilebileceğini gösterir. Bu, Vector nesnelerinin java.io.ObjectOutputStream türünden akaklara writeObject ile yazılıp, java.io.ObjectInputStream türlü akaklardan readObject ile okunabileceği anlamını taşır.

Değineceğimiz son arayüzler, Vector nesnelerini ekleme, güncelleme, silme, sorgulama gibi işlemlerle manipüle eden java.util.Collection ve java.util.List arayüzleridir. Collection, Veri Kapları Çerçevesi'ndeki kapların pek çoğu için geçerli olan işlemlerin genel sözleşmelerini sağlar. Bu yapılırken, ne çeşit bir kabın ele alındığı konusunda—aynı değerden iki veya daha fazlası tutulabilir mi, uygulanan işlemlerin sıra ve sonucuna göre elemanların yapı içindeki sırası bilinebilir mi?—bir varsayımda bulunulmaz. Buna karşılık, List arayüzü, kaptaki değerlerin tekrarlanabileceğini ve uygulanan işlemlerin sonrasında yapı içindeki sıranın bilinebileceğini varsayar.

Desteklenen İletiler


İletilere öznitelikleri işlemlemek için yararlanabileceklerimizle başlayalım. Argümansız size ve capacity iletileri, sırasıyla, hedef nesnenin kaç eleman içerdiğini ve sığasını döndürürken, isEmpty boşluk yüklemi olarak görev görür. Hedef nesnenin içeriğini eleman sayısına eşit sığalı bir kaba koyarak bellekten tasarruf sağlayan trimToSize, eleman sayısının artmayacağından emin olduğumuz durumlarda kullanılabilir. Sığanın değiştirilmesine yarayan bir diğer ileti olan ensureCapacity, hedef nesneyi argümanda geçirilen değere eşit veya daha büyük bir sığaya sahip olacak şekilde değiştirir. Bu işlemin icra edilmesi esnasında, hedef nesne yaratılırken sağlanan/varsayılan sığa artım değerinden yararlanılacaktır. Son olarak, setSize, hedef nesnedeki eleman sayısı özniteliğini günceller. Bu iletinin kullanımında dikkat edilmesi gereken bir nokta, geçirilen argümana göre eleman sayısının artması gibi azalmasının da mümkün olduğudur: argümanın ileti gönderimi anındaki eleman sayısından büyük olması eleman sayısının null değerine sahip elemanlar eklenerek artırılmasına neden olurken, küçük olması kabın sonuna doğru bazı değerlerin kırpılmasına, yani eleman sayısının azaltılmasına neden olacaktır.

Sınıflarını yazarken titiz davrananlara tanıdık gelecek iletilerle devam edelim. equals ve toString, beklendiği gibi, sırasıyla, eşitlik denetimi ve hoş yazım işlemlerini karşılarken hashCode, hedef nesneyi kıyarak özet değeri görevini gören bir int döndürür. Bilmeyenler için, bunun kulağa geldiği kadar vahşi bir şey olmadığını, nesneyi değiştirerek zarar vermediğini söyleyelim ve ne işe yaradığını biraz açalım. Potansiyel olarak pek çok eleman içerebilecek kapların eşitlik denetimi oldukça uzun bir zaman alabilir; ilk elemandan başlayarak eşit olmayan eleman çiftine kadar kontrol edilen iki kabın aynı indislerdeki elemanları birbirleriyle karşılaştırılır. Bu, eleman sayısı ile doğru orantılı olarak artan oldukça pahalı bir işlemin söz konusu olduğu anlamına gelir. Maliyet kap içeriğinin özeti olarak bir değerin tutulması ile azaltılabilir; kap değiştikçe—bu, eleman ekleme, güncelleme ve eleman silme ile mümkün olabilir—özet değer yeniden hesaplanır. Bir diğer kap ile eşitlik denetiminin yapılması istendiği durumlarda ise, önce eleman sayıları sonra ise hashCode ile öğrenilebilecek özet değerler karşılaştırılır. Özet değerler eşit değilse, kaplar da eşit değildir; aksi takdirde, eşitsiz kapların özetlerinin eşit olması olanaklı olduğundan, yukarıda bahsettiğmiz pahalı yöntemle eşitlik denetimi yapılır.
public class Vector<E> extends AbstractList<E>
  implements List<E>, Cloneable, RandomAccess, Serializable {
  ...
  public boolean equals(Object sağ) {
    Vector<E> sağTaraf = (Vector<E>) sağ;
    if (size() != sağTaraf.size()) return false;
    if (hashCode() != sağTaraf.hashCode()) return false;
    // Kapları dolaşarak karşılıklı elemanları denetle.
    ...
  } // boolean equals(Object) sonu
  ...
} // Vector<E> sınıfının sonu
Vector sınıfı, içerik güncellenmesi ve sorgulanması için pek çok ileti sunar. Ancak, kimi iletilerin bir diğer Java sürümünde eklenen eşdeğerleri bulunduğu için, ileti listesini anlamlandırmak işinin altından kalkmak görünenden kolay olacaktır. Bu gruba giren iletilerden aşağıdaki tablonun ilk sütununda verilmiş olanları kullanmanız daha akıllıca olacaktır. Zira, Collection ve List arayüzlerinde tanımlanmış olan bu iletilerin kullanımı, Vector yerine Veri Kapları Çerçevesi'ndeki diğer sınıfların kodunuzda değişiklik yapılmadan kullanılmasını sağlayacak ve yeniden kullanımı olanaklı kılacaktır.

Vector sınıfındaki eşdeğer iletiler
add(e)addElement(e)e'yi kabın sonuna ekler
add(i, e)insertElementAt(e, i)e'yi i indisli konuma ekler
clear()removeAllElements()Hedef nesneyi boşaltır
get(i)elementAt(i)i indisli elemanı döndürür
remove(e)removeElement(e)e ile eşit ilk konumdaki değeri siler
remove(i)removeElementAt(i)i indisli elemanı siler
set(i, e)setElementAt(e, i)i indisli elemanı e ile değiştirir ve eski değeri döndürür.

İçerik sorgulama ve güncelleme iletilerinin anlatımına geçmeden önce, eleman ekleme ve silme iletilerinin kullanımı sırasında akılda tutulması yararlı olacak şu noktayı hatırlatalım: ekleme sırasında, eklemenin yapıldığı indis sonrasındaki elemanlar sona doğru, silme sırasında, silinen elemanın sonrasındaki elemanlar öne doğru kaydırılacaktır. Dolayısıyla, bu işlemlerin etkilediği indis kap önlerine yaklaştıkça maliyet artacaktır.

İki uyarlaması bulunan add, hedef nesneye yeni eleman eklemeye yarar. Bu iletilerden, tek argümanlı olanı, argümanındaki değeri Vector nesnesinin sonuna ekleyip true döndürürken, iki argümanlı olanı, ikinci argümandaki değeri ilk argümanda belirtilen indisteki konuma ekler. Eklemenin toptan yapılması istendiğinde, bir döngü veya pek çok add iletisi kullanmaktansa addAll iletisi kullanılabilir. add iletisine koşut iki uyarlaması bulunan bu ileti, kendisine geçirilen Collection arayüzünü destekleyen kabın içindeki elemanları gezicisinin döndürdüğü sırada hedef nesneye ekler.

get iletisi, yegâne argümanında belirtilen indisteki elemanı döndürür. Bu iletinin özel kullanımları için, sırasıyla ilk ve son elemanları döndüren firstElement ve lastElement iletileri tercih edilebilir. Ancak, kodun yeniden kullanım kaygıları ağır basıyorsa, List arayüzünde tanımlanmış get'i yeğlemek daha yerinde olacaktır. Bir grup ardışık elemanın döndürülmesi ise, List arayüzü türündeki tutacakla gösterilen bir Vector nesnesi döndüren subList iletisi ile olanaklıdır. Bu ileti, hedef nesnenin ilk argümandaki indisten başlayıp ikinci argümandaki indisin bir öncesinde sonlanan dilimini döndürür.

indexOf ve lastIndexOf iletileri de sorgulama amacıyla kullanılablir. get verilen bir indisteki elemanı döndürürken, indexOf ve lastIndexOf eleman türündeki bir değer alıp bu değerin hedef nesnede geçtiği indisi döndürür. İki uyarlaması var olan indexOf, tek argümanlı kullanılacak olursa argümanda geçirilen nesnenin bulunduğu ilk indisi döndürürken, iki argümanlı kullanılırsa, ilk argümanda geçirilen nesnenin ikinci argümanda belirtilen indisten sonra bulunduğu ilk yerin indisini döndürür. Benzer şekilde; lastIndexOf, tek argümanlı kullanılacak olursa argümanda geçirilen nesnenin bulunduğu son indisi döndürürken, iki argümanlı kullanılırsa, ilk argümanda geçirilen nesnenin ikinci argümanda belirtilen indisten önce bulunduğu ilk yerin indisini döndürür. Aranan nesnenin hedef nesnede bulunmaması kullanıcıya -1 döndürülerek bildirilecektir.

Sorgulama amacıyla yararlanabileceğimiz bir diğer ileti çifti, yegâne argümanlarında geçirilen nesnenin veya Collection arayüzünü destekleyen kaptaki elemanların hedef nesnede geçip geçmediğinin yanıtını veren contains ve containsAll yüklemleridir.

Hedef nesneden eleman silme işlemi, argüman olarak indis veya silinmesi istenen değere eşit bir nesne bekleyen iki remove iletisi ile karşılanır. İndis alan uyarlama istenen konumdaki elemanı silip sonucu olarak döndürürken, diğer ileti argümanındaki nesnenin hedef nesne içinde, varsa, geçtiği ilk noktadaki elemanı siler ve silmenin gerçekleşmesi durumunda true, aksi halde false döndürür.

Çoklu silme, removeAll, retainAll veya clear iletilerinden biri kullanılarak yapılabilir. removeAll, argümanındaki Collection arayüzünü destekleyen kap içindeki elemanların hedef nesnedeki bütün kopyalarını silerken, retainAll argümandaki kabın elemanlarının hedef nesnedeki tüm kopyalarının korunması ve geri kalan elemanların silinmesini sağlar. Her iki ileti de, işleyişleri sırasında hedef nesneyi değiştirecek olurlarsa true, aksi takdirde false döndürür. Son olarak, clear komutu, hedef nesnedeki tüm elemanları siler.

Eleman güncellemede kullanılan set iletisi, ilk argümanında sağlanan indisteki elemanı ikinci argümandaki değer ile değiştirir ve değişim öncesindeki eleman değerini sonucu olarak döndürür.

Değineceğimiz bir sonraki ileti grubu, gezici nesnesi döndürenler.1 listIterator adlı bu iletiler, hedef nesneyi çift yönlü dolaşıp, güncellememizi sağlayan ListIterator türlü bir gezici nesne döndürür.2 Bunlardan argümansız olanı, dolaşmaya hedef nesnemizin ilk indisinden başlarken, tek argümanlı olanı, dolaşmaya argümanda belirtilen indisten başlar. İstenen yere konuşlanılmasını takiben, gezici nesneye sonraki/önceki elemanı döndürme (next, previous), o anki konuma ekleme (add), o anki konumdaki elemanı güncelleme (set) ve silme (remove) imkanını veren iletiler gönderilebilir. Geziciye gönderilen iletiler esas etkilerini dolaşılmakta olan kap üzerinde gösterecektir. Bunu, Vector nesnesi içindeki 3 değerine sahip elemanları gezici vasıtasıyla silen aşağıdaki kod parçasından görebilirsiniz.
import java.util.ListIterator;
...
ListIterator<Integer> gezici = intVec4.listIterator();
while (gezici.hasNext())
  if (gezici.next() == 3) gezici.remove();
Göz atacağımız son ileti grubu, Vector yerine bir başka yapının gerektiği veya daha uygun olduğu zamanlarda ihtiyacını duyduğumuz dönüşümü sağlar. Öncelikle, Veri Kapları Çerçevesi'nde ArrayList, HashSet ve LinkedList'in de içinde olduğu pek çok sınıf, Collection arayüzünü gerçekleştiren bir kap bekleyen yapıcıya sahiptir. Bu yapıcılar, geçirilen kabı baştan sona dolaşarak elemanları yaratılmakta olan yeni kaba eklerler. Dolayısıyla, Vector ve diğer pek çok sınıfın Collection arayüzünü gerçekleştirdiği anımsanacak olursa, Vector nesnelerinin pek çok diğer türden kaba ve diğer türden kapların Vector nesnesine çevrilmesi kolaylıkla mümkün olacaktır.

Vector nesneleri dizi nesnelerine copyInto ve toArray iletileri kullanılarak dönüştürülebilir. copyInto hedef nesnenin içerdiği elemanları argümanında sağlanan dizinin içine doldururken dizinin yeterli uzunlukta olmamasını kullanıcıya IndexOutOfBoundsException ayrıksı durumu ile bildirir. Buna karşılık, benzer imzaya sahip toArray iletisi, argümanda geçirilen dizinin yetersiz kalması durumunda sızlanmaz ve gerekli uzunluğa sahip yeni bir dizi nesnesi yaratıp işini bu yeni dizi üstünde tamamladıktan sonra bu diziyi döndürür. toArray'in argümansız ikinci uyarlaması da, hedef nesnenin eleman sayısına sahip bir dizi döndürerek işini görür.3

  1. elements iletisi iterator ve listIterator iletilerinin eklenmesi ile kullanımdan düşmüştür. Dolayısıyla, anlatımımız bu iletiyi kapsamayacaktır.
  2. İletilerin adı olarak vectorIterator yerine listIterator (liste gezici) seçilmiş olması sizi şaşırtmasın. Ne de olsa baştan sona dolaşılmak tüm doğrusal veri yapıları için makul bir işlem ve bu yüzden bu işleme karşılık gelen iletiler List arayüzüne konulmuş.
  3. Bu iletinin Vector sınıfındaki gerçekleştirimi Arrays.asList metodu ile bir bütün olarak düşünülmelidir.