Java geliştirici adayları için mülakatlar, teknik bilginin ve problem çözme becerilerinin test edildiği kritik aşamalardır. Bu kapsamlı rehberde, Java mülakatlarında en sık karşılaşılan soruları ve detaylı cevaplarını bulacak, temelden ileri düzeye kadar bilmeniz gereken tüm konuları keşfedeceksiniz. Kariyerinizde bir sonraki adımı atmaya hazır olun!
Günümüzün rekabetçi yazılım sektöründe, Java geliştiricilere olan talep hiç şüphesiz oldukça yüksek. Ancak bu yoğun talep, aynı zamanda mülakat süreçlerini de bir o kadar zorlayıcı hale getiriyor. Bir Java geliştirici olarak, sadece kod yazma yeteneğiniz değil, aynı zamanda temel kavramları derinlemesine anlama, karmaşık sorunlara mantıklı çözümler üretebilme ve projelerde etkin bir şekilde çalışabilme potansiyeliniz de değerlendirilir. İşte tam da bu noktada, iyi bir mülakat hazırlığı devreye giriyor. Hazırlık, sadece teknik sorulara doğru cevaplar vermekten ibaret değildir; aynı zamanda özgüveninizi artırır, bilginizi pekiştirir ve potansiyel işverenlere kendinizi en iyi şekilde ifade etmenizi sağlar.
Mülakatlar, adayın teorik bilgisini pratik uygulamalarla birleştirip birleştiremediğini görmenin en etkili yoludur. Örneğin, nesne yönelimli programlama (OOP) ilkelerini sadece ezbere bilmek yerine, bu ilkeleri gerçek dünya senaryolarında nasıl uygulayacağınızı göstermeniz beklenir. Bellek yönetimi, çoklu iş parçacığı (multithreading) veya koleksiyon frameworkleri gibi konular, sadece kavramsal olarak değil, performans optimizasyonu ve hata ayıklama bağlamında da ele alınmalıdır. Bu nedenle, mülakatlara hazırlanırken her konuyu sadece “bilgi” olarak değil, “uygulanabilir çözüm” olarak ele almanız büyük önem taşır. Ayrıca, teknik bilgiye ek olarak, problem çözme yeteneği, algoritmik düşünme, veri yapıları bilgisi ve hatta soft skill’ler (iletişim, takım çalışması) de mülakatların vazgeçilmez parçalarıdır. Bu makale, sizi bu kapsamlı sürece en iyi şekilde hazırlamak için tasarlandı.
İyi bir hazırlık, sadece mevcut bilginizi tazelemekle kalmaz, aynı zamanda yeni konular öğrenmenize ve eksiklerinizi gidermenize de olanak tanır. Genellikle mülakatlar, sizin öğrenme isteğinizi ve adaptasyon yeteneğinizi de ölçer. Dolayısıyla, “bilmiyorum” demek yerine, “şu anda bu konuda derinlemesine bilgim olmasa da, hızlıca öğrenip adapte olabilirim” mesajını vermek, çoğu zaman olumlu bir izlenim bırakabilir. Unutmayın, şirketler sadece “ne bildiğinizi” değil, aynı zamanda “nasıl düşündüğünüzü” ve “nasıl problem çözdüğünüzü” de merak ederler. Bu rehber, sizi hem teknik hem de stratejik olarak mülakatlara hazırlayacak ve Java dünyasındaki kariyer hedeflerinize ulaşmanızda size yol gösterecektir.
Java Temelleri: Nesne Yönelimli Programlamanın Kalbi
Java’nın en güçlü yanlarından biri, nesne yönelimli programlama (OOP) prensiplerini tam anlamıyla benimsemiş olmasıdır. OOP, yazılım tasarımını ve geliştirmesini daha düzenli, modüler ve yönetilebilir hale getiren bir programlama paradigmasıdır. Bir Java geliştirici adayı olarak, bu prensipleri sadece ismen bilmekle kalmayıp, her birini derinlemesine anlamanız ve gerçek senaryolarda nasıl uygulayacağınızı gösterebilmeniz kritik öneme sahiptir. Temel Java mülakat sorularının büyük bir kısmı genellikle OOP kavramları etrafında döner.
OOP Prensipleri Nelerdir?
Nesne Yönelimli Programlamanın dört ana ilkesi vardır:
- Kapsülleme (Encapsulation): Veriyi ve o veri üzerinde işlem yapan metotları tek bir birim (sınıf) içinde bir araya getirme ve dış dünyadan saklama prensibidir. Bu, veriye doğrudan erişimi kısıtlar ve veri tutarlılığını sağlar. Örneğin, bir banka hesabı sınıfında, hesap bakiyesine doğrudan erişmek yerine, para yatırma (deposit) ve çekme (withdraw) gibi metotlar aracılığıyla işlem yapılır. Bu, iç yapıyı korur ve kötüye kullanımı engeller.
- Kalıtım (Inheritance): Bir sınıfın (alt sınıf/türetilmiş sınıf), başka bir sınıftan (üst sınıf/temel sınıf) özelliklerini (metotlar ve alanlar) devralmasıdır. Bu, kod tekrarını azaltır ve hiyerarşik bir yapı oluşturulmasına olanak tanır. Örneğin, “Hayvan” sınıfından “Köpek” ve “Kedi” sınıfları türetilebilir; her ikisi de “Hayvan”ın genel özelliklerine (yeme, uyuma) sahip olurken, kendi özgün özelliklerini de (havlama, miyavlama) ekler.
- Polimorfizm (Polymorphism): “Çok biçimlilik” anlamına gelir. Bir nesnenin farklı türlerdeymiş gibi davranabilmesidir. Java’da iki ana polimorfizm türü vardır:
- Statik Polimorfizm (Metot Yükleme – Method Overloading): Aynı sınıf içinde aynı isme sahip ama farklı parametre listelerine (sayı, tür veya sıra) sahip metotlar tanımlamaktır. Derleme zamanında hangi metodun çağrılacağına karar verilir.
- Dinamik Polimorfizm (Metot Geçersiz Kılma – Method Overriding): Alt sınıfın, üst sınıftaki bir metodu kendi ihtiyacına göre yeniden tanımlamasıdır. Çalışma zamanında (runtime) hangi metodun çağrılacağına karar verilir.
- Soyutlama (Abstraction): Yalnızca temel ve gerekli bilgileri gösterip, gereksiz ayrıntıları gizleme prensibidir. Java’da soyut sınıflar (abstract classes) ve arayüzler (interfaces) aracılığıyla uygulanır. Bir araba örneğinde, sürücü sadece gaz pedalına basmanın arabayı hızlandıracağını bilir, ancak motorun iç işleyişinin karmaşık detaylarını bilmek zorunda değildir. Bu, sistemin karmaşıklığını azaltır ve kullanılabilirliği artırır.
Uzman İpucu: OOP prensiplerini sadece tanımlamakla kalmayın, her bir prensip için küçük kod örnekleri veya günlük hayattan analojilerle açıklayarak bilginizi pekiştirin. Bu, mülakatı yapan kişinin konuyu gerçekten kavradığınızı anlamasına yardımcı olur.
JVM, JRE ve JDK Arasındaki Fark Nedir?
Bu üç terim, Java ekosisteminin temel taşlarıdır ve birbirleriyle yakından ilişkilidirler, ancak farklı amaçlara hizmet ederler:
- JVM (Java Virtual Machine – Java Sanal Makinesi): Java kodunun çalıştığı soyut makinedir. Java kodunu çalıştırılabilir (makineye özgü) bayt koduna dönüştürür ve yürütür. JVM platformdan bağımsızdır, yani aynı bayt kodu farklı işletim sistemlerinde (Windows, Linux, macOS) çalışabilir. JVM, Garbage Collector (Çöp Toplayıcı) gibi bellek yönetimi özelliklerini de barındırır. Herhangi bir Java uygulamasını çalıştırmak için gereklidir.
- JRE (Java Runtime Environment – Java Çalışma Ortamı): Java uygulamalarını çalıştırmak için gerekli olan tüm bileşenleri içerir. Bu, JVM’in yanı sıra Java çekirdek sınıflarını (Java API’si) ve destekleyici dosyaları içerir. Eğer bir Java uygulamasını sadece çalıştırmak istiyorsanız, JRE yeterlidir. Geliştirme araçlarını içermez.
- JDK (Java Development Kit – Java Geliştirme Kiti): Java uygulamalarını geliştirmek için kullanılan tüm araçları içeren kapsamlı bir pakettir. JRE’yi (yani JVM ve Java API’lerini) içerir ve ek olarak derleyici (javac), hata ayıklayıcı (jdb), arşivleyici (jar) gibi geliştirme araçlarını barındırır. Eğer Java ile kod yazacak, derleyecek ve paketleyecekseniz, JDK’ya ihtiyacınız vardır.
Basitçe özetlemek gerekirse: JDK, Java uygulamalarını “yazmak” için; JRE, Java uygulamalarını “çalıştırmak” için; JVM ise Java bayt kodunu “yorumlamak” ve “yürütmek” için kullanılır.
Bellek Yönetimi ve Çöp Toplama (Garbage Collection): Optimizasyon Sırları
Java’da bellek yönetimi, özellikle diğer dillerden (C++ gibi) gelen geliştiriciler için önemli bir fark yaratır, zira Java’da manuel bellek yönetimine gerek yoktur. Bu görev, Java Virtual Machine (JVM) tarafından otomatik olarak yürütülen Çöp Toplayıcı (Garbage Collector – GC) tarafından üstlenilir. Ancak, bu otomasyon, bir geliştiricinin bellek yapısını ve GC’nin nasıl çalıştığını anlamasının gereksiz olduğu anlamına gelmez. Aksine, bellek sızıntılarını önlemek, performansı optimize etmek ve uygulamanın genel sağlığını korumak için bu konuları derinlemesine kavramak çok önemlidir.
Java’da Bellek Alanları Nasıl Çalışır?
JVM, çalışma zamanında farklı bellek alanları kullanarak program verilerini ve talimatlarını yönetir:
- Heap Bellek: Tüm nesneler ve diziler burada oluşturulur. Heap, paylaşımlı bir alandır ve tüm iş parçacıkları (threads) tarafından erişilebilir. Çöp toplayıcı tarafından yönetilir. Boyutu dinamik olarak artırılabilir veya azaltılabilir. Java’daki yavaş performans sorunlarının çoğu, genellikle Heap bellek kullanımıyla ilgilidir (bellek sızıntıları, gereksiz nesne oluşturma).
- Stack Bellek: Her iş parçacığının kendi özel Stack belleği vardır. Metot çağrıları, yerel değişkenler (primitif tipler) ve referans değişkenleri burada depolanır. Bir metot çağrıldığında, Stack’e bir “stack frame” eklenir; metot tamamlandığında ise bu frame Stack’ten çıkarılır. LIFO (Last-In, First-Out) prensibine göre çalışır. Stack belleği hızlıdır ancak boyutu sabittir (genellikle daha küçüktür).
- Metot Alanı (Method Area): Sınıf yapısı, metot kodları, statik değişkenler ve sabitler gibi sınıf seviyesindeki verileri saklar. Paylaşımlı bir alandır ve JVM başlatıldığında oluşturulur. Java 8 öncesinde “PermGen” olarak bilinirken, Java 8 ve sonrası “Metaspace” olarak adlandırılır.
- PC Kayıtları (Program Counter Registers): Her iş parçacığının kendi PC kaydedicisi vardır. Bir iş parçacığı tarafından yürütülen bir sonraki talimatın adresini tutar.
- Native Metot Yığınları (Native Method Stacks): Java dışındaki (C/C++ gibi) native metotlar için kullanılır. JNI (Java Native Interface) aracılığıyla çağrılan metotların bellek alanıdır.
Bu bellek alanlarının her biri, Java uygulamasının farklı yönlerini yöneterek sistemin sorunsuz çalışmasını sağlar. Heap belleği, özellikle büyük veri yapıları ve uzun ömürlü nesneler için hayati öneme sahipken, Stack bellek, metot yürütme hızını ve yerel değişken yönetimini optimize eder.
Çöp Toplama Süreci ve Çeşitleri
Çöp Toplama (Garbage Collection – GC), Java’da manuel bellek yönetiminin karmaşıklığını ortadan kaldıran otomatik bir süreçtir. GC’nin temel amacı, artık hiçbir referans tarafından erişilemeyen (yani kullanılmayan) nesneleri Heap bellekten temizleyerek yeni nesneler için yer açmaktır. Bu süreç, bellek sızıntılarını büyük ölçüde azaltır ve geliştiricinin verimli kod yazmaya odaklanmasını sağlar.
GC, genellikle “Generational Hypothesis” (Nesilsel Varsayım) prensibine dayanır: çoğu nesne kısa ömürlüdür ve çoğu uzun ömürlü nesne ise çok uzun süre yaşar. Bu varsayıma göre Heap bellek üç ana alana ayrılır:
- Young Generation (Genç Nesil): Yeni oluşturulan nesneler burada başlar. Çoğu nesne burada ölür.
- Eden Space: Nesnelerin ilk oluşturulduğu yer.
- Survivor Space (S0 ve S1): Eden’da GC’den kurtulan nesneler arasında geçiş yapar.
- Old Generation (Eski Nesil/Tenured Generation): Young Generation’da birden fazla GC döngüsünden kurtulan, yani daha uzun ömürlü olduğu düşünülen nesneler buraya taşınır.
- Permanent Generation (Kalıcı Nesil / Java 8 ve sonrası Metaspace): Sınıf meta verileri, statik değişkenler, metot bilgileri gibi verileri tutar.
Farklı GC algoritmaları bulunur, her birinin kendine göre avantajları ve dezavantajları vardır:
- Serial GC: Tek bir iş parçacığı kullanarak çöp toplama yapar. Küçük uygulamalar için uygundur, “stop-the-world” duraklamaları yaşatır.
- Parallel GC (Throughput Collector): Birden fazla iş parçacığı kullanarak genç nesilde çöp toplama yapar. Serbest bırakılan uygulama iş parçacıklarının (application threads) sayısını artırarak verimi (throughput) iyileştirir. Yine “stop-the-world” duraklamaları olur.
- CMS (Concurrent Mark-Sweep) GC: Genç nesil için paralel, eski nesil için eşzamanlı çalışır. Uygulama iş parçacıklarıyla eşzamanlı çalışmaya çalışarak duraklama sürelerini azaltmayı hedefler.
- G1 (Garbage-First) GC: Java 7 ile tanıtıldı ve Java 9’dan itibaren varsayılan GC’dir. Büyük Heap’lere sahip çok çekirdekli makineler için tasarlanmıştır. Belirli bir hedef duraklama süresi içinde çöp toplama yapmayı amaçlar. Heap’i küçük bölgelere ayırır ve en dolu bölgelerden başlayarak toplama yapar.
- ZGC ve Shenandoah GC: Modern GC algoritmalarıdır, çok düşük duraklama süreleri (genellikle milisaniyelerin altında) sunarak büyük ölçekli ve yüksek performanslı uygulamalar için tasarlanmışlardır.
// Basit bir nesne oluşturma ve referansın kaybolması
public class GcOrnegi {
public static void main(String[] args) {
String s1 = new String("Merhaba"); // Nesne 1 oluşturuldu
String s2 = new String("Dünya"); // Nesne 2 oluşturuldu
s1 = s2; // Nesne 1'e olan referans kayboldu. "Merhaba" nesnesi artık uygun (eligible) hale geldi.
// Ancak GC'nin ne zaman çalışacağı JVM'e bağlıdır.
s2 = null; // Nesne 2'ye olan referans da kayboldu. "Dünya" nesnesi de uygun hale geldi.
// System.gc(); // JVM'e GC çalıştırması için bir öneri göndeririz, garanti etmez.
}
}
Performansı optimize etmek için doğru GC algoritmasını seçmek ve JVM argümanlarıyla ayarlar yapmak (örneğin -Xms, -Xmx ile Heap boyutunu ayarlamak) kritik öneme sahiptir.
Çoklu İş Parçacığı (Multithreading) ve Eşzamanlılık: Performansı Artırma Yolları
Modern uygulamalar genellikle eşzamanlı ve paralel işlemlere ihtiyaç duyar. Java, bu ihtiyacı karşılamak için güçlü çoklu iş parçacığı (multithreading) ve eşzamanlılık (concurrency) mekanizmaları sunar. Birden fazla iş parçacığını kullanarak, bir uygulamanın aynı anda farklı görevleri yürütmesini sağlayabiliriz. Bu, özellikle UI tepkiselliğini artırmak, arka plan görevlerini işlemek veya yoğun hesaplama gerektiren işlemleri hızlandırmak için hayati öneme sahiptir. Ancak eşzamanlı programlama, beraberinde senkronizasyon ve veri tutarlılığı gibi yeni zorluklar da getirir.
Thread Oluşturma ve Yönetimi
Java'da bir iş parçacığı (thread) oluşturmanın ve çalıştırmanın iki temel yolu vardır:
ThreadSınıfını Extend Etmek: Kendi sınıfınızıjava.lang.Threadsınıfından türeterek verun()metodunu geçersiz kılarak (override) bir iş parçacığı oluşturabilirsiniz.RunnableArayüzünü Implement Etmek: Kendi sınıfınızıjava.lang.Runnablearayüzünü uygulayarak verun()metodunu implement ederek bir iş parçacığı oluşturabilirsiniz. Bu daha esnek bir yaklaşımdır çünkü Java'da sadece tek kalıtım desteklendiği için sınıfınız başka bir sınıftan da kalıtım alabilir.
// Runnable Arayüzünü Kullanan Örnek
public class MyRunnable implements Runnable {
private String threadName;
public MyRunnable(String name) {
this.threadName = name;
System.out.println(threadName + " oluşturuluyor...");
}
@Override
public void run() {
System.out.println(threadName + " çalıştırılıyor.");
try {
for (int i = 5; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
Thread.sleep(500); // 500 milisaniye bekle
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " kesintiye uğradı.");
}
System.out.println("Thread " + threadName + " sonlanıyor.");
}
public static void main(String[] args) {
MyRunnable runnable1 = new MyRunnable("Thread-1");
Thread t1 = new Thread(runnable1);
t1.start(); // Thread'i başlat
MyRunnable runnable2 = new MyRunnable("Thread-2");
Thread t2 = new Thread(runnable2);
t2.start(); // Thread'i başlat
}
}
start() metodu, JVM'in yeni bir iş parçacığı oluşturmasını ve bu iş parçacığının run() metodunu çağırmasını sağlar. Doğrudan run() metodunu çağırmak, kodu aynı iş parçacığı üzerinde çalıştırır ve yeni bir iş parçacığı oluşturmaz.
Eşzamanlılık Sorunları ve Çözümleri
Birden fazla iş parçacığı aynı paylaşılan kaynağa (değişken, dosya, veritabanı bağlantısı vb.) erişmeye çalıştığında çeşitli eşzamanlılık sorunları ortaya çıkabilir. En yaygın sorunlar şunlardır:
- Race Condition (Yarış Durumu): Birden fazla iş parçacığı paylaşılan bir kaynağı eşzamanlı olarak değiştirmeye çalıştığında, sonucun iş parçacıklarının çalışma sırasına bağlı olması durumudur. Bu, öngörülemeyen ve hatalı sonuçlara yol açabilir.
- Deadlock (Kilitlenme): İki veya daha fazla iş parçacığının, birbirlerinin salıvermesini beklediği ancak hiçbirinin asla salıvermeyeceği bir durumu ifade eder. Her iş parçacığı bir kaynağı tutar ve diğerinin tuttuğu kaynağı bekler.
- Livelock: İş parçacıklarının kilitlenmediği ancak ilerleme kaydedemediği bir durumdur. Sürekli olarak durumlarını değiştirirler ancak hiçbir iş yapamazlar.
- Starvation (Açlık): Bir veya daha fazla iş parçacığının, kaynaklara veya işlem süresine sürekli olarak erişememesi durumudur, çünkü diğer iş parçacıkları sürekli olarak bu kaynakları kullanır.
Bu sorunları çözmek için Java'da çeşitli senkronizasyon mekanizmaları mevcuttur:
synchronizedAnahtar Kelimesi: Metotları veya kod bloklarını senkronize etmek için kullanılır. Bir iş parçacığı senkronize bir metot veya bloğa girdiğinde, ilgili nesnenin (veya statik metotlar için sınıfın) kilidini alır. Başka bir iş parçacığı aynı kilidi almaya çalıştığında, kilit serbest bırakılana kadar bekler. Bu, Race Condition'ları önler.volatileAnahtar Kelimesi: Bir değişkene ait değerin her zaman ana bellekten okunup, ana belleğe yazılmasını sağlar. Bu, birden fazla iş parçacığının aynı değişkene eriştiği durumlarda görünürlük sorunlarını çözer. Ancak atomik operasyonları garanti etmez.java.util.concurrentPaketi: Java 5 ile birlikte gelen bu paket, eşzamanlı programlamayı basitleştiren ve daha verimli hale getiren gelişmiş araçlar sunar. Örnekler:ExecutorServiceveThreadPoolExecutor: İş parçacıklarını manuel olarak yönetmek yerine, bir havuzdan alıp kullanmayı sağlar.Semaphore: Belirli bir sayıda kaynağa eşzamanlı erişimi kontrol eder.CountDownLatchveCyclicBarrier: İş parçacıklarının birbirlerini beklemesini sağlayan senkronizasyon yardımcılarıdır.ConcurrentHashMap: Thread-safe ve yüksek performanslı birMapimplementasyonudur.AtomicSınıflar (AtomicInteger,AtomicLong): Küçük ölçekli atomik operasyonlar için kullanılır,synchronizedbloklarından daha hafif bir alternatiftir.
LockArayüzü (ReentrantLock):synchronizedanahtar kelimesine göre daha esnek kilit mekanizmaları sunar. Koşullu beklemeler (Condition), kilit zaman aşımı gibi özellikler sağlar.
// Synchronized Metot Örneği
public class Sayac {
private int count = 0;
public synchronized void increment() { // Metot senkronize edildi
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
Sayac sayac = new Sayac();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
sayac.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join(); // t1 bitene kadar bekle
t2.join(); // t2 bitene kadar bekle
System.out.println("Son Sayı: " + sayac.getCount()); // Her zaman 2000 olmalı
}
}
Eşzamanlı programlama karmaşıktır, ancak doğru araçlar ve yaklaşımlarla güvenli, verimli ve ölçeklenebilir uygulamalar geliştirmek mümkündür. Özellikle kritik bölümleri doğru bir şekilde senkronize etmek, hataları önlemek ve uygulamanın tutarlılığını sağlamak için temel bir beceridir.
Java Koleksiyon Çatısı ve Stream API: Veri İşlemenin Gücü
Java Koleksiyon Çatısı (Java Collections Framework - JCF), veri yapılarını yönetmek için bir dizi arayüz ve sınıf sunar. Bu çerçeve, geliştiricilere veri depolama, erişim, sıralama ve manipülasyon için güçlü ve esnek araçlar sağlar. JCF'in yanı sıra, Java 8 ile tanıtılan Stream API, veri kümeleri üzerinde fonksiyonel programlama tarzında işlemler yapmayı kolaylaştırır ve modern Java geliştirmenin vazgeçilmez bir parçası haline gelmiştir. Bu iki konu, mülakatlarda sıklıkla ele alınan ve Java geliştiricilerin günlük işlerinde yoğun olarak kullandığı alanlardır.
Java Koleksiyonları Nelerdir?
JCF, temel olarak üç ana arayüz etrafında döner: List, Set ve Map. Her birinin kendine özgü kullanım senaryoları ve davranışları vardır:
ListArayüzü:- Sıralı elemanlar koleksiyonudur. Elemanlar eklenme sırasına göre depolanır ve indeksleri aracılığıyla erişilebilir.
- Tekrarlayan elemanlara izin verir.
- Başlıca implementasyonlar:
ArrayList: Dinamik bir dizi olarak implement edilmiştir. Rastgele erişim hızlıdır (O(1)), ancak ortasına eleman ekleme veya çıkarma maliyetli olabilir (O(n)).LinkedList: Çift yönlü bağlı liste olarak implement edilmiştir. Başına veya sonuna eleman ekleme/çıkarma hızlıdır (O(1)), ancak rastgele erişim yavaştır (O(n)).Vector:ArrayList'e benzer ancak tüm metotlarısynchronized'dir, bu da onu thread-safe yapar ancak performans maliyetine neden olur. Genellikle daha modern eşzamanlı koleksiyonlar tercih edilir.
SetArayüzü:- Tekrarlayan elemanlara izin vermeyen bir koleksiyondur. Her eleman benzersiz olmalıdır.
- Elemanların belirli bir sırası garanti edilmez (
LinkedHashSethariç). - Başlıca implementasyonlar:
HashSet: Elemanların sıralamasını garanti etmez. Performans açısından genellikle en iyisidir, eleman ekleme, silme ve arama O(1) ortalama karmaşıklığa sahiptir.LinkedHashSet: Elemanların eklenme sırasını korur. PerformansıHashSet'e yakındır.TreeSet: Elemanları doğal sıralamalarına göre veya belirtilen birComparator'a göre sıralar. Operasyonları O(log n) karmaşıklığa sahiptir.
MapArayüzü:- Anahtar-değer çiftleri depolayan bir yapıdır. Her anahtar benzersiz olmalıdır, ancak değerler tekrarlanabilir.
- Anahtarlar aracılığıyla değerlere erişilir.
- Başlıca implementasyonlar:
HashMap: Anahtarların sırasını garanti etmez. PerformansıHashSet'e benzer, O(1) ortalama karmaşıklığa sahiptir.LinkedHashMap: Anahtarların eklenme sırasını korur.TreeMap: Anahtarları doğal sıralamalarına göre veya belirtilen birComparator'a göre sıralar. Operasyonları O(log n) karmaşıklığa sahiptir.HashTable:HashMap'e benzer ancak tüm metotlarısynchronized'dir (thread-safe). Eski bir implementasyondur ve genellikleConcurrentHashMaptercih edilir.
// Koleksiyon Örnekleri
import java.util.*;
public class KoleksiyonOrnekleri {
public static void main(String[] args) {
// List (Sıralı, Tekrarlayan Elemanlara İzin Verir)
List isimler = new ArrayList<>();
isimler.add("Ayşe");
isimler.add("Mehmet");
isimler.add("Ayşe");
System.out.println("List: " + isimler); // [Ayşe, Mehmet, Ayşe]
// Set (Sırasız, Benzersiz Elemanlar)
Set benzersizIsimler = new HashSet<>();
benzersizIsimler.add("Ayşe");
benzersizIsimler.add("Mehmet");
benzersizIsimler.add("Ayşe"); // Bu eklenmez
System.out.println("Set: " + benzersizIsimler); // [Ayşe, Mehmet] (sıra garanti değil)
// Map (Anahtar-Değer Çiftleri)
Map yaslar = new HashMap<>();
yaslar.put("Ayşe", 30);
yaslar.put("Mehmet", 25);
yaslar.put("Fatma", 30);
System.out.println("Map: " + yaslar); // {Ayşe=30, Mehmet=25, Fatma=30} (sıra garanti değil)
System.out.println("Mehmet'in Yaşı: " + yaslar.get("Mehmet"));
}
}
Stream API ile Veri Dönüşümü
Java 8 ile birlikte tanıtılan Stream API, koleksiyonlar üzerinde fonksiyonel tarzda işlem yapmayı sağlayan güçlü bir araçtır. Veri işleme operasyonlarını daha okunaklı, özlü ve çoğu durumda daha performanslı hale getirir. Bir Stream, bir veri kaynağı (koleksiyon, dizi, I/O kanalları vb.) üzerinde ardışık veya paralel olarak işlem yapmaya olanak tanır.
Stream operasyonları iki ana kategoriye ayrılır:
- Aracı Operasyonlar (Intermediate Operations): Bir Stream döndürürler ve genellikle başka Stream operasyonlarıyla zincirlenebilirler (
filter(),map(),sorted(),distinct(),limit()). Bu operasyonlar tembel (lazy)dir, yani bir sonlandırma operasyonu çağrılana kadar yürütülmezler. - Sonlandırma Operasyonları (Terminal Operations): Bir sonuç (
count(),sum()), bir yan etki (forEach()) veya bir koleksiyon (collect()) döndürürler. Bu operasyonlar Stream zincirini kapatır ve tüm aracı operasyonların yürütülmesini tetikler.
// Stream API Örneği
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamApiOrnegi {
public static void main(String[] args) {
List kelimeler = Arrays.asList("elma", "armut", "kiraz", "üzüm", "nar", "kivi", "erik");
// Kelimelerden 'k' ile başlayanları filtrele, büyük harfe çevir ve listeye topla
List kIleBaslayanlarBuyukHarf = kelimeler.stream()
.filter(s -> s.startsWith("k")) // 'k' ile başlayanları filtrele (intermediate)
.map(String::toUpperCase) // Büyük harfe çevir (intermediate)
.collect(Collectors.toList()); // Sonuçları yeni bir Listeye topla (terminal)
System.out.println("K ile başlayanlar (büyük harf): " + kIleBaslayanlarBuyukHarf); // [KİRAZ, KİVİ]
// Uzunluğu 4'ten büyük olan kelimeleri say
long uzunKelimelerSayisi = kelimeler.stream()
.filter(s -> s.length() > 4) // Uzunluğu 4'ten büyük olanları filtrele
.count(); // Say (terminal)
System.out.println("Uzunluğu 4'ten büyük kelime sayısı: " + uzunKelimelerSayisi); // 3 (armut, kiraz, üzüm)
// Benzersiz elemanları al ve alfabetik sırala
List benzersizVeSirali = Arrays.asList("elma", "armut", "elma", "kiraz", "armut").stream()
.distinct() // Benzersiz elemanları al
.sorted() // Alfabetik sırala
.collect(Collectors.toList());
System.out.println("Benzersiz ve Sıralı: " + benzersizVeSirali); // [armut, elma, kiraz]
}
}
Uzman İpucu: Stream API'nin paralel (
parallelStream()) versiyonunu kullanırken dikkatli olun. Küçük veri kümelerinde veya G/Ç yoğun işlemlerde performans artışı sağlamayabilir, hatta senkronizasyon maliyetleri nedeniyle performansı düşürebilir. Yalnızca CPU yoğun ve büyük veri kümeleri için paralel Stream'leri değerlendirin.
Koleksiyon Çatısı ve Stream API, Java uygulamalarında veri işleme ve manipülasyonun temelini oluşturur. Bu konuları iyi anlamak, daha temiz, daha etkili ve daha modern Java kodu yazmanız için size gerekli becerileri kazandıracaktır.
İleri Düzey Konular: API'ler, Frameworkler ve Mimari Yaklaşımlar
Java ekosistemi, temel dil özelliklerinin çok ötesine geçen zengin bir kütüphaneler, API'ler ve frameworkler dünyasına sahiptir. Bir Java geliştiricinin kariyerinde ilerlemesi için, bu ileri düzey konulara hakim olması ve gerçek dünya projelerinde bunları nasıl uygulayacağını bilmesi vazgeçilmezdir. Mülakatlarda, özellikle orta ve kıdemli seviye pozisyonlar için bu konulara dair derinlemesine bilgi beklenir. Veritabanı bağlantılarından modern web servislerine, büyük ölçekli uygulamaların mimari yaklaşımlarına kadar geniş bir yelpaze bu bölümde ele alınır.
JDBC ile Veritabanı Bağlantısı Nasıl Kurulur?
JDBC (Java Database Connectivity), Java uygulamalarının veritabanlarıyla etkileşim kurmasını sağlayan bir API'dir. Farklı veritabanı türleriyle (MySQL, PostgreSQL, Oracle, SQL Server vb.) standart bir arayüz üzerinden iletişim kurmayı mümkün kılar. JDBC kullanarak bir veritabanına bağlanmak için genel adımlar şunlardır:
- Sürücüyü Yükle: İlgili veritabanı için JDBC sürücüsünü (JAR dosyası) projenizin classpath'ine ekleyin.
- Bağlantı Kur:
DriverManager.getConnection()metodunu kullanarak veritabanı URL'si, kullanıcı adı ve şifre ile birConnectionnesnesi oluşturun. - Statement Oluştur: SQL sorgularını çalıştırmak için
Connectionnesnesinden birStatementveyaPreparedStatementnesnesi elde edin.PreparedStatement, SQL enjeksiyon saldırılarını önlemek ve performansı artırmak için parametreleştirilmiş sorgulara olanak tanır. - Sorguyu Çalıştır: SQL sorgularını (
executeQuery()için SELECT,executeUpdate()için INSERT, UPDATE, DELETE) çalıştırın. - Sonuçları İşle: SELECT sorguları için dönen
ResultSetnesnesini kullanarak verileri işleyin. - Kaynakları Kapat: En önemlisi, kullanılan tüm kaynakları (
ResultSet,Statement,Connection) ters sırada kapatın. Genellikletry-with-resourcesyapısı bu işlemi otomatik olarak halleder ve kaynak sızıntılarını önler.
// JDBC Bağlantı Örneği (try-with-resources ile)
import java.sql.*;
public class JdbcOrnek {
private static final String DB_URL = "jdbc:mysql://localhost:3306/testdb";
private static final String USER = "root";
private static final String PASS = "password";
public static void main(String[] args) {
// try-with-resources statement: Kaynaklar otomatik kapanır
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, name, email FROM users")) {
while (rs.next()) {
// ResultSet'ten verileri al
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
System.out.println("ID: " + id + ", İsim: " + name + ", Email: " + email);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Modern uygulamalarda genellikle JDBC'nin doğrudan kullanımı yerine, Spring Data JPA veya Hibernate gibi ORM (Object-Relational Mapping) frameworkleri tercih edilir. Ancak JDBC'nin temellerini anlamak, bu frameworklerin nasıl çalıştığını kavramak için önemlidir.
Spring ve Hibernate Neden Önemlidir?
Spring Framework: Java geliştirmenin de facto standardı haline gelmiş, kapsamlı bir uygulama geliştirme framework'üdür. Modüler yapısı sayesinde, web uygulamalarından mikroservislere, veri erişiminden güvenliğe kadar birçok alanda kullanılabilir. Spring'i önemli kılan temel özellikler:
- Bağımlılık Enjeksiyonu (Dependency Injection - DI) / Inversion of Control (IoC): Uygulama bileşenlerinin bağımlılıklarını yönetmeyi kolaylaştırır. Kodun daha modüler, test edilebilir ve yeniden kullanılabilir olmasını sağlar.
- Aspect-Oriented Programming (AOP): Logging, güvenlik, transaction yönetimi gibi cross-cutting (çapraz kesen) endişeleri ana iş mantığından ayırmayı sağlar.
- Spring Boot: Spring tabanlı uygulamaları hızlı bir şekilde kurmak ve çalıştırmak için bir araçtır. Otomatik yapılandırma, gömülü sunucular ve "starter" bağımlılıkları ile geliştirme sürecini büyük ölçüde basitleştirir.
- Spring MVC / Spring WebFlux: Web uygulamaları ve RESTful API'ler geliştirmek için güçlü modüller sunar.
- Spring Data JPA: Veritabanı işlemlerini ORM (Hibernate gibi) kullanarak çok daha az kodla yapmayı sağlayan bir abstraksiyon katmanıdır.
Hibernate: Java için önde gelen bir ORM (Object-Relational Mapping) aracıdır. Nesneye dayalı bir Java modelini ilişkisel bir veritabanı şemasıyla eşleştirerek, geliştiricilerin SQL sorguları yazmak yerine Java nesneleriyle çalışmasına olanak tanır. Hibernate'in faydaları:
- Veritabanı Abstraksiyonu: Veritabanı türünden bağımsız geliştirme imkanı sunar. Veritabanı değiştiğinde kodda büyük değişiklikler yapma gereksinimini azaltır.
- CRUD İşlemleri: Nesneleri veritabanına kaydetmek, güncellemek, silmek ve okumak için kolay bir API sağlar.
- Performans Optimizasyonları: Otomatik önbellekleme (caching) ve lazy loading gibi özelliklerle performansı artırabilir.
- HQL (Hibernate Query Language): SQL'e benzer, ancak Java nesneleri üzerinde çalışan nesneye yönelik bir sorgulama dili sunar.
Spring ve Hibernate genellikle birlikte kullanılır. Spring, uygulamanın genel yapısını ve bileşen yönetimini sağlarken, Hibernate veri kalıcılığı (persistence) katmanını yönetir. Bu ikilinin kombinasyonu, kurumsal düzeyde Java uygulamaları geliştirmek için standart bir çözüm haline gelmiştir.
RESTful Servisler ve Mikroservis Mimarisi
RESTful Servisler (Representational State Transfer - REST): Web servisleri oluşturmak için popüler bir mimari stildir. Durumsuz (stateless) olması, cache edilebilir olması ve HTTP metodlarını (GET, POST, PUT, DELETE) kaynaklar üzerinde CRUD (Create, Read, Update, Delete) işlemleri için kullanması temel özellikleridir. RESTful API'ler, farklı sistemler arasında platformdan bağımsız iletişimi mümkün kılar. Bir RESTful serviste, bir kaynağa (resource) URI (Uniform Resource Identifier) ile erişilir ve kaynağın temsili (representation) genellikle JSON veya XML formatında geri döndürülür.
// Basit bir RESTful API tasarımı örneği
// Bir kullanıcı kaynağı için tipik HTTP metotları
// GET /users - Tüm kullanıcıları getir
// GET /users/{id} - Belirli bir kullanıcıyı getir
// POST /users - Yeni bir kullanıcı oluştur
// PUT /users/{id} - Mevcut bir kullanıcıyı güncelle
// DELETE /users/{id} - Belirli bir kullanıcıyı sil
Mikroservis Mimarisi: Geleneksel monolitik mimarilere bir alternatif olarak ortaya çıkan bir yaklaşımdır. Büyük bir uygulamayı, her biri kendi iş mantığına sahip, bağımsız olarak konuşlandırılabilir ve yönetilebilir küçük, gevşek bağlı servisler kümesi olarak yapılandırır. Her mikroservis, kendi veritabanına sahip olabilir ve kendi teknolojisiyle geliştirilebilir.
Mikroservislerin avantajları:
- Modülerlik: Her servis, belirli bir iş alanına odaklanır ve kolayca anlaşılabilir.
- Bağımsız Geliştirme/Konuşlandırma: Servisler birbirinden bağımsız olarak geliştirilebilir, test edilebilir ve canlıya alınabilir. Bu, hızlı iterasyon ve CI/CD süreçlerini destekler.
- Teknoloji Çeşitliliği: Farklı servisler için farklı programlama dilleri ve teknolojiler kullanılabilir (poliglot geliştirme).
- Ölçeklenebilirlik: Yüksek trafik alan veya belirli bir fonksiyona daha fazla ihtiyaç duyan servisler, diğerlerinden bağımsız olarak ölçeklenebilir.
- Esneklik: Hata durumunda, bir servisin çökmesi tüm uygulamayı etkilemez.
Ancak mikroservislerin karmaşıklık, dağıtık işlem yönetimi, veri tutarlılığı ve iletişim gibi zorlukları da vardır. Bu nedenle, bir mikroservis mimarisine geçiş yapmadan önce iyi bir analiz ve planlama gereklidir. Spring Cloud gibi araçlar, mikroservis tabanlı uygulamaların geliştirilmesini ve yönetimini kolaylaştırır.
Bir geliştirici olarak, bu ileri düzey konulara hakim olmak, sadece bir mülakatı geçmekle kalmayacak, aynı zamanda modern ve ölçeklenebilir Java uygulamaları tasarlama ve geliştirme yeteneğinizi de önemli ölçüde artıracaktır.
Sonuç: Mülakat Başarısının Anahtarı
Bu makale boyunca, Java mülakatlarında karşılaşabileceğiniz temelden ileri düzeye kadar birçok önemli konuyu ele aldık. Nesne Yönelimli Programlama'nın (OOP) dört temel ilkesiyle başlayarak, Java'nın bellek yönetimi ve çöp toplama mekanizmalarını inceledik. Ardından, çoklu iş parçacığı (multithreading) ve eşzamanlılık kavramlarının performans optimizasyonundaki rolünü ve beraberindeki sorunları nasıl çözeceğimizi öğrendik. Java Koleksiyon Çatısı ve modern Stream API ile veri manipülasyonunun inceliklerine değindik. Son olarak, JDBC ile veritabanı bağlantısından, Spring ve Hibernate gibi popüler framework'lere ve RESTful servisler ile mikroservis mimarisi gibi ileri düzey mimari yaklaşımlara kadar geniş bir perspektif sunduk.
Unutmayın, Java mülakatlarında başarılı olmanın anahtarı sadece ezber bilgi değil, aynı zamanda bu bilgiyi gerçek dünya senaryolarına uygulama yeteneğidir. Her konuyu sadece ne olduğunu değil, neden önemli olduğunu ve nerede kullanıldığını anlamaya çalışın. Kod örneklerini kendiniz yazın, farklı senaryoları deneyin ve hata ayıklama süreçleriyle pratik yapın. Açıklamalarınızı somut örneklerle desteklemek, mülakatı yapan kişinin konuya olan hakimiyetinizi net bir şekilde görmesini sağlayacaktır.
Başarıya ulaşmak için sürekli öğrenmeye açık olmak, pratik yapmak ve analitik düşünme becerilerinizi geliştirmek kritik öneme sahiptir. Bu rehber, yolculuğunuzda size sağlam bir başlangıç noktası sunmayı amaçlamıştır. Şimdiden başarılar dileriz!
Sıkça Sorulan Sorular (SSS)
-
Java'da Overloading ve Overriding Arasındaki Fark Nedir?
Overloading (Metot Yükleme): Aynı sınıf içinde veya kalıtım ilişkisi olan sınıflarda, aynı isme sahip ancak farklı parametre listelerine (sayı, tip veya sıra) sahip metotlar tanımlamaktır. Derleme zamanı (compile-time) polimorfizmidir. Dönüş tipi farklı olabilir, ancak sadece dönüş tipi farklılığı overloading için yeterli değildir.
Overriding (Metot Geçersiz Kılma): Alt sınıfın, üst sınıftaki bir metodu (aynı isim, aynı parametre listesi ve aynı veya daha geniş erişim belirleyicisiyle) kendi ihtiyacına göre yeniden tanımlamasıdır. Çalışma zamanı (runtime) polimorfizmidir.
-
final, finally ve finalize Anahtar Kelimeleri Ne Anlama Gelir?
final: Değişkenler için sabit değer atamasını, metotlar için geçersiz kılmayı (override) önlemeyi ve sınıflar için kalıtımı (inheritance) önlemeyi sağlar. Bir kez atandıktan sonra değeri değiştirilemez.finally: Birtry-catchbloğundan sonra her zaman yürütülecek kodu tanımlayan bir bloktur. Hata oluşsa da oluşmasa da bu bloktaki kod çalışır; kaynakları serbest bırakmak için idealdir.finalize: Bir nesne çöp toplayıcı tarafından temizlenmeden önce JVM tarafından çağrılan bir metottur. Nadiren kullanılır ve performansı etkileyebilir. Genellikle kaynak serbest bırakma içintry-with-resourcesveyafinallyblokları tercih edilir. -
Checked ve Unchecked Exception'lar Arasındaki Fark Nedir?
Checked Exception'lar: Derleme zamanında (compile-time) kontrol edilen istisnalardır. Bunlar, programcının ele alması (
try-catchbloğu veyathrowsbildirimi ile) zorunlu olan istisnalardır. Örnek:IOException,SQLException.Unchecked Exception'lar (Runtime Exception'lar): Çalışma zamanında (runtime) ortaya çıkan istisnalardır ve derleyici tarafından kontrol edilmezler. Programcının ele alması zorunlu değildir, ancak genellikle programlama hatalarını (logic errors) işaret ederler. Örnek:
NullPointerException,ArrayIndexOutOfBoundsException. -
Java'da String Neden Immutable (Değiştirilemez)?
Java'da
Stringnesneleri değişmezdir çünkü birStringnesnesi oluşturulduktan sonra içeriği asla değiştirilemez. Herhangi bir "değişiklik" işlemi (örneğinconcat()metodu), aslında yeni birStringnesnesi oluşturur. Bunun temel nedenleri:- Güvenlik: Veritabanı bağlantı şifreleri, kullanıcı adları gibi hassas bilgiler String olarak saklandığında, değişmezlik güvenlik açıklarını önler.
- Thread-Safe Olma: Değişmez oldukları için, birden fazla iş parçacığı aynı String'i eşzamanlı olarak güvenle kullanabilir, senkronizasyona gerek kalmaz.
- Performans ve Hashing: String'ler sıkça anahtar olarak kullanılır (örneğin
HashMap'lerde). Değişmezlik, hash kodlarının bir kez hesaplanıp önbelleğe alınabilmesini sağlar, bu da performansı artırır. - String Pool: JVM, String değişmezliği sayesinde aynı literal değerlere sahip String'leri String Pool'da saklayarak bellek kullanımını optimize eder.
-
Java'da Mikroservis Mimarisi Nedir ve Avantajları Nelerdir?
Mikroservis mimarisi, büyük bir uygulamanın bağımsız, gevşek bağlı ve küçük servisler olarak tasarlanması yaklaşımıdır. Her servis kendi iş alanından sorumludur, kendi veritabanına sahip olabilir ve ayrı ayrı konuşlandırılabilir. Avantajları:
- Bağımsızlık: Servisler birbirinden bağımsız olarak geliştirilebilir, test edilebilir ve konuşlandırılabilir.
- Ölçeklenebilirlik: Yüksek talep gören servisler diğerlerinden bağımsız olarak ölçeklenebilir.
- Esneklik: Farklı servisler için farklı teknolojiler kullanılabilir (poliglot geliştirme). Bir servisteki hata diğerlerini etkilemez.
- Modülerlik: Her servis daha küçük ve yönetilebilir bir kod tabanına sahiptir.
- Hızlı Iterasyon: Daha küçük ekipler, daha hızlı geliştirme ve yayınlama döngüleri sağlayabilir.