Biraz Aynı, Ama Aslında Farklı: Java’da Öne Çıkan Karşılaştırmalar

Didem AĞDOĞAN
7 min readDec 16, 2024

--

Part 1 — Biraz aynı ama biraz daha aynı değil!

Bu kez yazıma, en sevdiğim videodaki popüler bir sözle başlamak istiyorum. (Videoda geçen; ‘Biraz güzel ama biraz daha güzel değil.’ 😊)

Peki bu aynı gibi görünen ama biraz da aynı olmayan konular neler? Bugün bunları inceliyoruz. İsim benzerlikleri veya görevlerdeki küçük farklar bazen kafa karıştırıcı olabilir. Ancak, bu yazıda bu karışıklıkları gidermek için detaylı bir inceleme yapacağız.

Comparable ve Comparator

Java’da, bir dizi veya listeyi sıralamak istediğimizde java.lang.Comparable ve java.util.Comparator arayüzlerinden faydalanırız. Bu iki arayüz, sıralama işlemleri için farklı ihtiyaçlara göre çözüm sunar.

Comparable Nedir?

Comparable arayüzü, sıralama mantığını doğrudan nesnenin kendisine dahil etmemizi sağlar. Bu arayüzü uygulayan bir sınıf, varsayılan bir sıralama düzeni belirler ve bu düzen, sınıfın compareTo(Object o) metodunda tanımlanır. Ancak, bu sıralama düzeni sabittir ve dinamik olarak değiştirilemez.

class Movie implements Comparable<Movie> {
private String name;
private int year;
@Override
public int compareTo(Movie m) {
return this.year - m.year; // Filmleri yapım yılına göre sıralar
}
}

Bu sınıfta, filmler varsayılan olarak yapım yılına göre sıralanır. Ancak başka bir kritere (örneğin, film adı) göre sıralama yapmak istersek, compareTo metodunu değiştirmemiz gerekir.

Comparator Nedir?

Comparator arayüzü, bir sınıfın sıralama mantığını dışarıdan tanımlamamıza olanak tanır. Bu, özellikle aşağıdaki durumlarda avantajlıdır:

  1. Sıralama mantığını birden fazla şekilde tanımlamak istiyorsak. Comparator ile bir sınıf için farklı sıralama stratejileri tanımlayabiliriz.
  2. Sınıfın kaynak kodunu değiştirme şansımız yoksa (örneğin, bir third-part kütüphanesindeki bir sınıf).

Maaşa göre sıralama:

public static Comparator<Employee> SalaryComparator = new Comparator<Employee>() {
@Override
public int compare(Employee e1, Employee e2) {
return (int) (e1.getSalary() - e2.getSalary());
}
};
Arrays.sort(empArr, Employee.SalaryComparator);

İsme göre sıralama:

public static Comparator<Employee> NameComparator = new Comparator<Employee>() {
@Override
public int compare(Employee e1, Employee e2) {
return e1.getName().compareTo(e2.getName());
}
};
Arrays.sort(empArr, Employee.NameComparator);

Peki Farkları Neler?

Bazen bir sınıfın kaynak kodunu değiştiremeyebiliriz. Bu durumda, Comparable kullanmak mümkün değildir. Ancak Comparator, bu gibi durumlarda esneklik sağlar. Örneğin, aynı sınıf için farklı Comparator’lar tanımlayarak bir listeyi maaşa, isme veya başka herhangi bir özelliğe göre sıralayabiliriz.

Serializable ve Externalization

Java’da Serializable ve Externalizable arayüzleri, nesneleri serileştirerek (serialization) depolama veya bir ağ üzerinden gönderme işlemlerinde kullanılır. Ancak, bu iki arayüzün işleyişi ve özellikleri arasında bazı önemli farklar vardır.

Serializable Arayüzü

  • Serializable arayüzü herhangi bir metot tanımlamaz ve serileştirme işlemi JVM tarafından otomatik olarak yönetilir. JVM, sınıfın tüm serileştirilebilir durumunu (non-transient ve non-static alanlar) yansıma (reflection) kullanarak işleme alır.
  • Daha basit bir yapı sağlar ve herhangi bir özel metot yazmamıza gerek kalmadan sınıf örneklerini kolayca serileştirebiliriz.
  • Yansıma ve meta veri kullanımı nedeniyle, Serializable arayüzü Externalizable’ a göre daha yavaş olabilir.
  • Özel serileştirme gerekiyorsa, alanları transient anahtar kelimesi ile işaretleyebiliriz. Transient bir alan, serileştirme işlemine dahil edilmez ve varsayılan değeriyle depolanır.
import java.io.Serializable;

class Employee implements Serializable {
private static final long serialVersionUID = 1L; // Serileştirme uyumluluğu için
private String name;
private int age;
private transient double salary; // Serileştirilmez

// Constructor, Getter ve Setter'lar
}

Externalizable Arayüzü

  • Externalizable arayüzünü uygulayan sınıflar, writeExternal() ve readExternal() metotlarını override etmek zorundadır. Bu metotlar, serileştirme ve deserialization işlemlerini tamamen programcının kontrolüne bırakır.
  • Tüm serileştirme işlemini özelleştirmek istiyorsak Externalizable daha uygundur. Hangi alanların serileştirilip serileştirilmeyeceğini veya nasıl bir formatla depolanacağını kontrol edebiliriz.
  • Daha iyi performans sağlayabilir, çünkü reflection kullanılmaz ve yalnızca belirtilen alanlar işleme alınır.
  • Serileştirilen alanların, okuma sırası aynı sırayla geri alınması gereklidir. Aksi takdirde, bir java.io.EOFException gibi hatalar alınabilir.
import java.io.Externalizable;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.IOException;

class Country implements Externalizable {
private String code;
private String name;

public Country() {} // Varsayılan Constructor zorunlu

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(code);
out.writeUTF(name);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
code = in.readUTF();
name = in.readUTF();
}
}

Hangi Durumda Hangisi Kullanılmalı?

  • Tüm nesne serileştirmek isteniyorsa: Serializable tercih edilmelidir.
  • Serileştirme işlemi özelleştirilecekse (örneğin, yalnızca belirli alanlar serileştirilecekse): Externalizable daha uygun bir seçimdir.
  • Performans kritikse ve serileştirme işlemi üzerinde tam kontrol gerekiyorsa: Externalizable kullanılmalıdır.

Checked ve Unchecked

Java’da istisnalar, hataları yönetmek ve programın beklenmedik durumlarla karşılaştığında düzgün bir şekilde çalışmaya devam etmesini sağlamak için kullanılır. İstisnalar iki temel gruba ayrılır: Checked Exception ve Unchecked Exception. Bu iki tür arasındaki temel farklar, derleme zamanı kontrolleri ve işlenme şekillerinde görülür.

Checked Exception Nedir?

Checked Exception, derleme (compile-time) sırasında kontrol edilen istisnalardır. Bu tür bir istisna, programın mantığı içinde ele alınabilir ve genellikle dosya okuma/yazma, veritabanı işlemleri gibi dış kaynaklara bağlı işlemlerde oluşur.

Özellikler:

  1. Derleyici, checked exception’ ların ele alınmasını zorunlu kılar. Eğer bir checked exception ele alınmazsa (try-catch bloğu veya throws anahtar kelimesiyle), program derlenmez.
  2. Genellikle dış kaynaklardan kaynaklanan sorunları temsil eder (örneğin, dosyanın bulunamaması veya bir sınıfın yüklenememesi).

Örnekler: ClassNotFoundException, IOException, SQLException, FileNotFoundException

import java.io.*;
public class CheckedExceptionExample {
public static void main(String[] args) {
try {
FileReader file = new FileReader("nonexistentfile.txt");
} catch (FileNotFoundException e) {
System.out.println("Dosya bulunamadı: " + e.getMessage());
}
}
}

FileNotFoundException bir checked exception’ dır. Dosyanın var olup olmadığını kontrol etmek zorunda olduğumuz için bir try-catch bloğu veya throws anahtar kelimesi kullanmamız gerekir.

Unchecked Exception Nedir?

Unchecked Exception, çalışma zamanı (runtime) sırasında oluşur ve derleyici tarafından kontrol edilmez. Bu tür istisnalar, genellikle programlama hatalarıdır ve programın yanlış davranışından kaynaklanır.

Özellikler:

  1. Derleme zamanı kontrolü yoktur, yani bu istisnaların ele alınması zorunlu değildir.
  2. Genellikle programcı hatalarıyla ilişkilidir (örneğin, null bir nesneye erişim veya dizinin sınırları dışına çıkma).
  3. Ölümcül olabilir ve programın çökmesine neden olabilir.

Örnekler : ArithmeticException, ArrayIndexOutOfBoundsException, NullPointerException, NumberFormatException

public class UncheckedExceptionExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // ArrayIndexOutOfBoundsException
}
}

Bu örnekte, ArrayIndexOutOfBoundsException bir unchecked exception’ dır. Dizinin sınırları dışında bir indekse erişmeye çalıştığı için bu hata oluşur.

Failsafe vs Failfast

Java’da yineleyiciler (iterators), koleksiyonlarla çalışırken iki farklı yaklaşım sergiler: Fail-Fast ve Fail-Safe. Bu iki mekanizma, hata yönetimi ve veri tutarlılığı konusunda farklı yaklaşımlar sunar.

Fail-Fast

Fail-Fast iteratörleri, bir hata tespit edildiğinde işlemi olabildiğince hızlı bir şekilde durdurur. Bu sayede hatalar hemen görünür hale gelir ve tüm işlem durdurulur. Koleksiyon üzerinde iterasyon yaparken, koleksiyonda yapısal bir değişiklik (örneğin, bir elemanın eklenmesi veya silinmesi) gerçekleşirse, ConcurrentModificationException fırlatılır.

Özellikler:

  • Daha az bellek kullanır çünkü koleksiyonun kopyasını almaz.
  • Hataları hızlı bir şekilde tespit eder.

Örnekler: ArrayList, HashMap gibi java.util paketindeki varsayılan koleksiyonlar.

Fail-Safe

Fail-Safe iteratörleri, bir hata durumunda işlemi durdurmaz ve mümkün olduğunca hataları engellemeye çalışır. Koleksiyon üzerinde iterasyon yaparken, iteratör koleksiyonun bir kopyasını kullanır. Bu nedenle, yapısal değişiklikler orijinal koleksiyonu etkilemez.

Özellikler:

  • Koleksiyonun kopyası üzerinde çalıştığı için daha fazla bellek kullanır.
  • Daha güvenli bir iterasyon sağlar, ancak orijinal koleksiyondaki değişiklikleri yansıtmaz.
  • Örnekler: ConcurrentHashMap, CopyOnWriteArrayList gibi koleksiyonlar.

Message-Driven Mimari ile Asenkron İletişim

Message-Driven mimaride, servisler arasında doğrudan iletişim yerine bir mesajlaşma altyapısı (örneğin, message queue ya da message bus) kullanılarak iletişim kurulur. Bu yaklaşım, Publish/Subscribe (Yayınla/Abone Ol) ilkesi temelinde çalışır.

  • Servisler birbirleriyle doğrudan bağlı değildir. Bir servis bir mesaj gönderir (publish eder), başka bir servis bu mesajı işlemek için abone olabilir.
  • Servisler birbirinin varlığından haberdar olmak zorunda değildir. Bu, sistemin bakımını kolaylaştırır ve bağımsız geliştirme süreçlerini destekler.
  • Mesajlar kuyruğa eklenir ve diğer servisler tarafından alınıp işlenir. Gönderen servis, mesajın hemen işlenip işlenmediğini bilmek zorunda değildir.
  • Eğer bir servis çalışmıyorsa, mesajlar kuyruğa alınır ve servis tekrar çalışmaya başladığında işlenir.

Mesajlaşma Araçları: RabbitMQ, Apache Kafka, ActiveMQ

Protokoller: AMQP (Advanced Message Queuing Protocol), MQTT

Örnek: Bir e-ticaret sisteminde, bir kullanıcı sipariş verdiğinde; sipariş hizmeti, sipariş oluşturma mesajını bir kuyruğa gönderir. Farklı servisler (örneğin, stok güncelleme servisi, ödeme servisi) bu mesaja abone olur ve mesajı işleyerek ilgili işlemleri gerçekleştirir.

Event-Driven Mimari ile Asenkron İletişim

Event-Driven mimaride, sistem bileşenleri arasında iletişim, olaylar (events) üzerinden gerçekleştirilir. Bu yaklaşımda, her bir bileşen kendi iş mantığını yürütürken, oluşan olayları bir message bus aracılığıyla iletir.

  • Bir olay gerçekleştiğinde, bu olay sistem genelinde yayınlanır. Olayı dinleyen bileşenler (event listeners), bu olayı işleyebilir. Her bileşen yalnızca ilgilendiği olaylarla ilgilenir ve buna göre tepki verir.
  • Event-Driven mimaride de bir message bus ya da event broker (örneğin Kafka) kullanılır.

Mesajlaşma Araçları: Apache Kafka, AWS SNS (Simple Notification Service), Google Pub/Sub

Protokoller: Webhooks, MQTT

Örnek: Bir E-ticaret sisteminde; ürün satın alındığında; sipariş servisi “OrderPlaced” eventi yayınlar. Bu olay bir mesaj kuyruğu (ör. Kafka, RabbitMQ) veya olay yayınlama sistemi (ör. AWS SNS, Azure Event Grid) ile diğer servislere iletilir. Ödeme Servisi, “OrderPlaced” olayını dinler ve ödeme işlemini başlatır.

Her iki yaklaşım da modern mikro servis mimarilerinde asenkron iletişim için kritik öneme sahiptir ve kullanım alanına göre seçilmelidir.

  • Message-Driven Mimari: Yüksek ayrışma ve hata toleransı gerektiğinde tercih edilir.
  • Event-Driven Mimari: Gerçek zamanlı işlemler veya olay bazlı süreç yönetimi gerektiğinde daha uygundur.

ReentrantLock ve ReadWriteLock

ReentrantLock ve ReadWriteLock, Java’nın çoklu thread senaryolarında veri tutarlılığını sağlamak için kullanılan önemli senkronizasyon araçlarıdır. ReentrantLock, bir thread’in aynı kilidi birden fazla kez alabilmesine olanak tanırken, ReadWriteLock, okuma-yazma işlemleri arasında daha verimli bir paylaşım mekanizması sunar. Her iki yapı da farklı kullanım senaryolarında, performansı artırmak ve kaynakların doğru şekilde yönetilmesini sağlamak için tasarlanmıştır.

ReentrantLock

ReentrantLock, bir thread’in aynı lock’u birden fazla kez almasına olanak tanıyan bir karşılıklı dışlama (mutual exclusion) mekanizmasıdır. Bir seferde yalnızca bir thread’ in erişmesi gereken kritik bölgeyi korur.

Özellikler:

  • Lock, adil bir şekilde thread’ lere sırayla atanabilir veya herhangi bir sıraya bağlı kalmadan atanabilir.
  • Aynı anda yalnızca bir thread, lock’a sahip olabilir.
  • Reentrancy: Aynı thread, aynı lock’u tekrar alabilir (recursive locking).
  • Try-Lock Desteği: Lock’u almak için beklemek zorunda kalmadan deneme yapılabilir (tryLock() metodu).

Örnek Kullanım:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();

public void criticalSection() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " is executing critical section");
// Kritik kod
} finally {
lock.unlock();
}
}
}

ReadWriteLock

  • ReadWriteLock, bir okuma-yazma senaryosunda kullanılır. Aynı anda birden fazla thread, okuma işlemi gerçekleştirebilir ancak yalnızca bir thread yazma işlemi yapabilir.
  • Okuma (Read) işlemleri sırasında veri üzerinde değişiklik yapılmazken, birden fazla thread’in aynı anda veri okuyabilmesine olanak tanır. Ancak, yazma (Write) işlemleri sırasında tüm erişim kilitlenir.

Özellikler:

  • Aynı anda yalnızca bir thread yazma işlemi yapabilir.
  • Okuma işlemleri için readLock(), yazma işlemleri için writeLock() metotları bulunur.

Örnek Kullanım:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int sharedResource = 0;

public void read() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " is reading: " + sharedResource);
} finally {
lock.readLock().unlock();
}
}

public void write(int value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " is writing: " + value);
sharedResource = value;
} finally {
lock.writeLock().unlock();
}
}
}

Ne Zaman Kullanılır?

  • ReentrantLock: Sadece bir thread’in erişmesi gereken durumlarda kullanılır (örneğin, banka hesap işlemleri).
  • ReadWriteLock: Okuma işlemlerinin çok olduğu, ancak yazma işlemlerinin daha az olduğu senaryolarda tercih edilir (örneğin, bir cache uygulaması).

--

--

Didem AĞDOĞAN
Didem AĞDOĞAN

Written by Didem AĞDOĞAN

Software Developer, Traveller, Curious

No responses yet