Ders 08 / 10
Diziler
Java'da dizi bir nesnedir ve ölçüm bunu üç yerde gösterir: üretim tek bir komut bırakır, `d.length` alan okuması değil kendi başına bir komuttur, sınır denetimi ile dizi kovaryansının tip denetimi ise sınıf dosyasında hiç görünmeden çalışma zamanına bırakılır.
İçindekiler
Önceki ders akış biçimlerinin hangi komuta derlendiğini ölçtü: aynı seçim iki ayrı arama
komutuna, aynı gelişmiş for döngüsü dizide ve listede ayrı adıma dönüşüyordu. Geriye
üzerinde gezinilen şeyin kendisi kaldı.
Dizi, Veri Yapıları kursunda bir veri yapısı olarak kuruldu: sabit boyut, bitişik yerleşim,
konumdan adrese giden aritmetik. Bu ders onu tekrarlamaz. Buradaki soru şudur: new int[3]
satırı sınıf dosyasında kaç komut bırakır, d.length yazımı bir alan okuması mıdır, ve
dizinin sınırını denetleyen adım kaynağın neresinde durur? Üçünün de yanıtı, dizinin Java’da
bir nesne olmasından çıkar.
Dizi Bir Nesnedir
- SD21 — Ölçülen kaynak metni dersin içinde yazılır. Ne yazdığımızı bildiğimiz için, sınıf dosyasında bulunan her fazlalık derleyicinin eklediğidir.
- SD22 —
Olcekçekirdeği ortak tanımdan gelir. Bu ders ona yalnız bir ölçüm ekler: bir yöntemin komut adlarını sırayla döndürenkomutlar. Var olan ölçümlerin davranışı değişmez. - SD23 — Komut adları sınıf dosyasındaki adlardır ve
Locale.ROOTile küçük harfe çevrilerek basılır; yerel ayar sonucu etkilemez. - SD24 — Derleyici sıfırdan farklı bir sonuç döndürürse çekirdek istisna fırlatır. Ders bu istisnayı yakalayıp yalnız “derlenmedi” satırını basar; derleyicinin ileti metni basılmaz.
// Olcek.java — ortak ölçüm çekirdeği: derleyicinin sınıf dosyasına ne koyduğunu okur
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Olcek {
record Yontem(String ad, int komut, List<String> eklenen) {}
record Sinif(List<Yontem> yontem, List<String> alan) {}
static ClassModel derle(String kaynak, String sinif) throws Exception {
Path d = Files.createTempDirectory("olcek");
Path k = d.resolve(sinif + ".java");
Files.writeString(k, kaynak);
PrintWriter bos = new PrintWriter(Writer.nullWriter());
if (ToolProvider.findFirst("javac").orElseThrow()
.run(bos, bos, "-d", d.toString(), k.toString()) != 0)
throw new IllegalStateException("derlenmedi: " + sinif);
return ClassFile.of().parse(d.resolve(sinif + ".class"));
}
static Sinif oku(String kaynak, String sinif) throws Exception {
ClassModel cm = derle(kaynak, sinif);
List<Yontem> yontemler = new ArrayList<>();
for (MethodModel m : cm.methods()) {
if (m.code().isEmpty()) continue;
List<String> eklenen = new ArrayList<>();
int komut = 0;
for (CodeElement e : m.code().get()) {
if (e instanceof Instruction) komut++;
if (e instanceof InvokeInstruction iv)
eklenen.add(kisa(iv.owner().asInternalName()) + "." + iv.name().stringValue());
else if (e instanceof NewPrimitiveArrayInstruction a)
eklenen.add("yeni dizi " + a.typeKind().name().toLowerCase(Locale.ROOT));
else if (e instanceof NewReferenceArrayInstruction a)
eklenen.add("yeni dizi " + kisa(a.componentType().asInternalName()));
}
yontemler.add(new Yontem(m.methodName().stringValue(), komut, eklenen));
}
List<String> alanlar = new ArrayList<>();
for (FieldModel f : cm.fields()) alanlar.add(f.fieldName().stringValue());
return new Sinif(yontemler, alanlar);
}
static List<String> komutlar(String kaynak, String sinif, String yontem) throws Exception {
List<String> adlar = new ArrayList<>();
for (MethodModel m : derle(kaynak, sinif).methods())
if (m.methodName().stringValue().equals(yontem))
for (CodeElement e : m.code().orElseThrow())
if (e instanceof Instruction i)
adlar.add(i.opcode().name().toLowerCase(Locale.ROOT));
return adlar;
}
static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}
// Dizi.java — dizi üretimi sınıf dosyasında ne bırakıyor
import java.util.Arrays;
class Kayit {
String ad;
int adet;
long agirlik;
}
public class Dizi {
static final String K = """
class DiziOrnek {
static class Kayit { String ad; int adet; long agirlik; }
static int[] bos() { return new int[3]; }
static Kayit[] kayitlar() { return new Kayit[3]; }
static int[] sabitli() { return new int[] {12, 18, 7}; }
static int uzunluk(int[] d) { return d.length; }
static int adet(Kayit k) { return k.adet; }
}
""";
public static void main(String[] args) throws Exception {
System.out.printf("%-10s %6s %s%n", "yontem", "komut", "kaynakta yazilmayan adim");
for (Olcek.Yontem y : Olcek.oku(K, "DiziOrnek").yontem())
if (!y.ad().equals("<init>"))
System.out.printf("%-10s %6d %s%n", y.ad(), y.komut(),
y.eklenen().isEmpty() ? "-" : String.join(", ", y.eklenen()));
System.out.println();
System.out.println("d.length okuma: " + Olcek.komutlar(K, "DiziOrnek", "uzunluk"));
System.out.println("k.adet okuma: " + Olcek.komutlar(K, "DiziOrnek", "adet"));
int[] sayilar = new int[3];
Kayit[] kayitlar = new Kayit[3];
System.out.println();
System.out.println("int[] sinifi: " + sayilar.getClass().getName());
System.out.println("Kayit[] sinifi: " + kayitlar.getClass().getName());
System.out.println("dizinin ust sinifi: " + sayilar.getClass().getSuperclass().getName());
System.out.println("baslangic degerleri: " + Arrays.toString(sayilar)
+ " " + Arrays.toString(kayitlar));
try {
Olcek.oku(K.replace("return d.length;", "d.length = 5; return 0;"), "DiziOrnek");
} catch (IllegalStateException e) {
System.out.println("d.length = 5 -> " + e.getMessage());
}
}
}
yontem komut kaynakta yazilmayan adim bos 3 yeni dizi int kayitlar 3 yeni dizi DiziOrnek$Kayit sabitli 15 yeni dizi int uzunluk 3 - adet 3 - d.length okuma: [aload_0, arraylength, ireturn] k.adet okuma: [aload_0, getfield, ireturn] int[] sinifi: [I Kayit[] sinifi: [LKayit; dizinin ust sinifi: java.lang.Object baslangic degerleri: [0, 0, 0] [null, null, null] d.length = 5 -> derlenmedi: DiziOrnek
İlk iki satır dizi üretiminin üç komut olduğunu söylüyor: boyutu yükle, diziyi üret,
döndür. Ölçek bu üretimi yeni dizi diye kaydediyor, çünkü sınıf dosyasında dizi üretimi
nesne üretimiyle aynı sınıfa girer. Kayit[3] satırının kaydında sınıfın tam adı
duruyor, öğe tipi diziye yazılmıştır.
Üçüncü satır bir fark koyuyor. Köşeli ayraç içinde üç değer yazan sabitli yöntemi
15 komut üretiyor; boyu aynı olan boş dizi 3 komut. Aradaki on iki komut, üç öğenin
tek tek yazılmasıdır: her öğe için diziyi çoğalt, konumu yükle, değeri yükle, yaz. Kaynakta
tek bir sabit gibi duran {12, 18, 7} yazımının sınıf dosyasında sabit karşılığı yoktur.
İlk derste ölçülen sabit katlama sayılarda işler; dizide işlemez, çünkü katlanacak şey bir
değer değil, çalışma zamanında üretilen bir nesnedir.
Son üç satır bunu adıyla koyuyor. Dizinin bir sınıfı var — int[] için [I, Kayit[]
için [LKayit; — ve o sınıfın üst sınıfı java.lang.Object. Dizi bir nesnedir: öbekte
üretilir, bir referansla tutulur, Object bekleyen her yere geçer. Üretim öğeleri üretmez;
sayı dizisi sıfırlarla, kayıt dizisi null ile başlar. Kayıt dizisi kurmak üç kayıt
kurmaz, üç boş yer kurar.
Uzunluk satırları ayrımın ikincisini veriyor. d.length yazımı arraylength komutuna,
k.adet yazımı getfield komutuna derleniyor. Sözdizim ikisinde de aynı — nokta ve bir ad
— ama karşılıkları ayrı komutlardır. length bir alan değildir; sınıf dosyasında alan
listesinde bulunmaz, kendi komutu vardır. Bunun görünen sonucu son satırda: d.length = 5
yazan kaynak derlenmiyor. Bir diziyi büyütmek bir yasak yüzünden değil, karşılığı olan
bir komut bulunmadığı için olanaksızdır.
Sınırı Denetleyen Adım Kaynakta Yok
- SD25 — Sınır ölçümünde yalnız istisnanın sınıf adı basılır. İstisna iletisi konumu ve uzunluğu içerir; o metin basılmaz, çünkü ölçülen şey denetimin varlığıdır.
// Erisim.java — sınır denetimi hangi komutun içinde
public class Erisim {
static final String K = """
class ErisimOrnek {
static int oku(int[] d, int i) { return d[i]; }
static void yaz(int[] d, int i, int v) { d[i] = v; }
}
""";
public static void main(String[] args) throws Exception {
System.out.println("d[i] okuma: " + Olcek.komutlar(K, "ErisimOrnek", "oku"));
System.out.println("d[i] yazma: " + Olcek.komutlar(K, "ErisimOrnek", "yaz"));
int[] d = {12, 18, 7};
System.out.println();
for (int i : new int[] {0, 2, 3, -1}) {
try {
System.out.printf("d[%d] -> %d%n", i, d[i]);
} catch (RuntimeException e) {
System.out.printf("d[%d] -> %s%n", i, e.getClass().getSimpleName());
}
}
int boy = -1;
try {
System.out.println("new int[boy] -> " + new int[boy].length);
} catch (RuntimeException e) {
System.out.println("new int[boy] -> " + e.getClass().getSimpleName() + " (boy = " + boy + ")");
}
}
}
d[i] okuma: [aload_0, iload_1, iaload, ireturn] d[i] yazma: [aload_0, iload_1, iload_2, iastore, return] d[0] -> 12 d[2] -> 7 d[3] -> ArrayIndexOutOfBoundsException d[-1] -> ArrayIndexOutOfBoundsException new int[boy] -> NegativeArraySizeException (boy = -1)
Okuma dört, yazma beş komut. İkisinde de bir karşılaştırma, bir dallanma ya da bir çağrı
yok: diziyi yükle, konumu yükle, oku ya da yaz. Sınır denetimi kaynakta yazılmamıştır ve
sınıf dosyasında da ayrı bir komut olarak durmaz. Denetim iaload ve iastore
komutlarının tanımının içindedir; sanal makine her erişimde onu yapar ve konum aralık
dışındaysa istisna fırlatır.
Bu, eklenen adımın üçüncü biçimidir. Birincisi sınıf dosyasına konan bir komuttur — çevirme, çağrı, üretim. İkincisi kaynakta hiç yazılmamış bir üyedir. Üçüncüsü ise komutun kendi tanımına gömülüdür ve komut sayısını hiç değiştirmez; ölçek onu sayamaz, ancak çalışma zamanındaki sonucundan okur. Ölçümdeki dört erişimin ikisi değer veriyor, ikisi istisna; eksi konum ayrı bir kural değil, aynı kuralın sonucudur.
Son satır denetimin ikinci yerini gösteriyor: boyutu eksi olan üretim isteği de çalışma zamanında düşer ve ayrı bir istisna verir, çünkü boyut derleme zamanında bilinmek zorunda değildir.
Çok Boyutlu Dizi Bir Dizi Dizisidir
- SD26 — Satır uzunlukları ölçümde tek tek yazılır; hiçbiri rastgele üretilmez, hiçbiri ortamdan okunmaz.
// CokBoyut.java — çok boyutlu dizi bir dizi dizisidir
public class CokBoyut {
static final String K = """
class BoyutOrnek {
static int[][] duzgun() { return new int[2][3]; }
static int[][] tirtikli() { return new int[2][]; }
static int satirUzunlugu(int[][] d, int i) { return d[i].length; }
}
""";
public static void main(String[] args) throws Exception {
for (String y : new String[] {"duzgun", "tirtikli", "satirUzunlugu"})
System.out.printf("%-14s %s%n", y, Olcek.komutlar(K, "BoyutOrnek", y));
int[][] t = new int[3][];
t[0] = new int[] {12};
t[1] = new int[] {18, 7, 4};
t[2] = new int[0];
System.out.println();
System.out.println("dis dizinin sinifi: " + t.getClass().getName());
System.out.println("bir satirin sinifi: " + t[0].getClass().getName());
System.out.printf("dis uzunluk %d, satir uzunluklari %d %d %d%n",
t.length, t[0].length, t[1].length, t[2].length);
int[][] d = new int[2][3];
System.out.println("duzgun dizide satirlar ayni nesne mi: " + (d[0] == d[1]));
d[0][0] = 12;
System.out.println("d[0][0] = 12 sonrasi d[1][0]: " + d[1][0]);
}
}
duzgun [iconst_2, iconst_3, multianewarray, areturn] tirtikli [iconst_2, anewarray, areturn] satirUzunlugu [aload_0, iload_1, aaload, arraylength, ireturn] dis dizinin sinifi: [[I bir satirin sinifi: [I dis uzunluk 3, satir uzunluklari 1 3 0 duzgun dizide satirlar ayni nesne mi: false d[0][0] = 12 sonrasi d[1][0]: 0
Üçüncü satır dersin cümlesini yazıyor: d[i].length okuması iki adımdır. Önce aaload
ile satır yüklenir — çünkü satır bir referanstır, bir nesnedir — sonra o nesnenin uzunluğu
arraylength ile okunur. İki boyutlu dizi tek bir bitişik blok değildir; dış dizinin
öğeleri iç dizilere giden referanslardır. Sınıf adı da bunu yazıyor: dış dizinin sınıfı
[[I, bir satırınki [I.
İlk iki satır iki ayrı üretimi ayırıyor. Bütün boyutları verilen new int[2][3] tek bir
multianewarray komutudur ve dış diziyle birlikte satırları da üretir. Yalnız dış boyutu
verilen new int[2][] ise anewarray komutudur; dış dizi üretilir, satırları null
kalır. İkincisi satırların ayrı uzunlukta kurulmasına izin verir: ölçümdeki üç satırın
uzunlukları 1, 3 ve 0. Son iki satır ise düzgün dizide bile satırların ayrı nesneler
olduğunu doğruluyor — d[0] ile d[1] aynı nesne değil, ve birinci satıra yazmak ikinciyi
değiştirmiyor. Java’da ayrı bir çok boyutlu dizi tipi yoktur.
Yazma Başına Bir Tip Denetimi
- SD27 — Aynı yazma üç ayrı kaynakta denenir. İkisi derlenir ve komutları basılır, biri derlenmez ve “derlenmedi” satırıyla geçer; hangisinin hangi zamanda düştüğü ölçümün kendisidir.
// Kovaryans.java — dizi kovaryansı: derlenen yazma, düşen yazma
public class Kovaryans {
static final String DIZGI = "class KovOrnek { static void yaz(String[] d) { d[0] = %s; } }";
static final String NESNE = "class KovOrnek { static void yaz(Object[] d) { d[0] = %s; } }";
static void dene(String ad, String kaynak) {
try {
System.out.printf("%-28s -> %s%n", ad, Olcek.komutlar(kaynak, "KovOrnek", "yaz"));
} catch (Exception e) {
System.out.printf("%-28s -> %s%n", ad, e.getMessage());
}
}
public static void main(String[] args) throws Exception {
dene("String[] d; d[0] = \"kuzey\"", DIZGI.formatted("\"kuzey\""));
dene("String[] d; d[0] = 12", DIZGI.formatted("12"));
dene("Object[] d; d[0] = 12", NESNE.formatted("12"));
String[] adlar = new String[2];
Object[] gorunum = adlar;
System.out.println();
System.out.println("Object[] gorunum = adlar; ayni nesne mi: " + (gorunum == adlar));
System.out.println("gorunum sinifi: " + gorunum.getClass().getName());
gorunum[0] = "kuzey";
System.out.println("gorunum[0] = \"kuzey\" -> adlar[0] = " + adlar[0]);
try {
gorunum[1] = 12;
System.out.println("gorunum[1] = 12 -> " + adlar[1]);
} catch (RuntimeException e) {
System.out.println("gorunum[1] = 12 -> " + e.getClass().getSimpleName()
+ ", adlar[1] = " + adlar[1]);
}
int[] sayilar = new int[2];
sayilar[1] = 12;
System.out.println("int[] tarafinda ayni yazma -> " + sayilar[1]);
}
}
String[] d; d[0] = "kuzey" -> [aload_0, iconst_0, ldc, aastore, return] String[] d; d[0] = 12 -> derlenmedi: KovOrnek Object[] d; d[0] = 12 -> [aload_0, iconst_0, bipush, invokestatic, aastore, return] Object[] gorunum = adlar; ayni nesne mi: true gorunum sinifi: [Ljava.lang.String; gorunum[0] = "kuzey" -> adlar[0] = kuzey gorunum[1] = 12 -> ArrayStoreException, adlar[1] = null int[] tarafinda ayni yazma -> 12
Java’da diziler kovaryanttır (array covariance): öğe tipi bir üst tipin altındaysa,
dizi de o üst tipin dizisi yerine geçer. String[] bir Object[] yerine geçer, ve
ölçümün dördüncü satırı bunu doğruluyor — iki ad aynı nesneyi tutuyor, sınıfı hâlâ
[Ljava.lang.String;.
İlk üç satır kovaryansın bedelini yazıyor. String[] olarak adlandırılan diziye sayı
yazan kaynak derlenmiyor: derleyici bildirilen tipe bakar ve hatayı derleme zamanında
verir. Aynı dizi Object[] olarak adlandırıldığında aynı yazma derleniyor — üstelik
sayıyı sarmalayıcıya çeviren bir çağrı da eklenerek. Yazma sözdizim düzeyinde geçerlidir,
çünkü Object[] içine her nesne yazılabilir.
Kararı çalışma zamanı veriyor: dizgi yazması geçiyor ve çağırana görünüyor, sayı yazması
ArrayStoreException ile düşüyor ve adlar[1] hâlâ null — yazma yapılmadı, yarım
da kalmadı. Sanal makine aastore komutunu yürütürken değerin dizinin gerçek öğe
tipine uyup uymadığını denetler; uymuyorsa yazmaz.
Buradaki sınırlayıcı ölçüm iki yönlüdür. Birincisi: bu denetim komut sayısına hiç
yansımaz. aastore tek komuttur ve iastore da tek komuttur; sınıf dosyasına bakarak
denetimin varlığını sayamayız. İkincisi: son satırdaki int[] yazmasında böyle bir denetim
yoktur. Öğe tipi ilkelse dizi kovaryansı diye bir soru doğmaz, çünkü ilkel tipli bir
dizi başka bir dizi tipinin yerine geçmez. Eklenen adım burada kaybolur — ve kaybolduğu
yer, tam olarak alt tip ilişkisinin bittiği yerdir.
Özet
- Dizi bir nesnedir: kendi sınıfı vardır, üst sınıfı
java.lang.Object’tir ve üretimi sınıf dosyasında 3 komuttur; köşeli ayraçlı{12, 18, 7}yazımı ise 15 komuttur, çünkü öğeler tek tek yazılır. Üretim öğeleri üretmez: sayı dizisi sıfırlarla, referans dizisinulldeğerlerle başlar. d.lengthbir alan okuması değildir;arraylengthkomutuna derlenir,k.adetisegetfieldkomutuna. Uzunluğa yazan kaynak derlenmez.- Sınır denetimi ne kaynakta ne de ayrı bir komutta durur; erişim komutunun tanımının içindedir, komut sayısını değiştirmez ve aralık dışı erişimde istisna fırlatır.
int[][]ayrı bir tip değil, birint[]dizisidir:d[i].lengthiki komuttur, satırlar ayrı nesnelerdir ve ayrı uzunlukta olabilir.- Dizi kovaryansı yüzünden
Object[]olarak adlandırılan bir dizgi dizisine sayı yazmak derlenir ama çalışma zamanındaArrayStoreExceptionile düşer; aynı denetim ilkel tipli dizide hiç yoktur.
Sonraki Adım
Bu derste String[] dizisine yazmanın çalışma zamanında denetlendiği ölçüldü; yazılan
değer her seferinde hazır bir dizgi nesnesiydi. Sıradaki ders o nesneye bakar: bir dizgi
sabiti sınıf dosyasında nerede durur, aynı sabiti iki kez yazmak kaç nesne üretir, ve
döngü içinde birleştirilen bir metin yol boyunca kaç nesne bırakır? Dizginin
değiştirilemez olması da, dizinin sabit boyutlu olması gibi, kaynakta görünmeyen adımları
belirler.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.