Java Records
Java Records: Daha Sade, Güvenli ve Okunabilir Veri Modelleri Geliştirmek
Java 21 ile record kullanımını anlamak, immutable veri modelleri oluşturmak ve kurumsal uygulamalarda doğru tasarım kararları vermek.
Giriş: Her veri nesnesi için neden bu kadar fazla kod yazıyoruz?
Java ile geliştirme yaparken sık karşılaştığımız ihtiyaçlardan biri, belirli verileri bir arada taşımaktır. Bir müşterinin bilgilerini, bir API isteğini, bir servis yanıtını veya bir işlem sonucunu temsil etmek için genellikle sınıflar oluştururuz.
Örneğin basit bir müşteri veri nesnesi hazırlamak istediğimizi düşünelim:
public class CustomerDto {
private final Long id;
private final String firstName;
private final String lastName;
private final String email;
public CustomerDto(
Long id,
String firstName,
String lastName,
String email) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getEmail() {
return email;
}
// equals(), hashCode() ve toString()
// metotlarının da eklenmesi gerekir.
}Burada dikkat edersek sınıfın temel amacı yalnızca dört alanı bir arada taşımak. Buna rağmen constructor, getter, equals(), hashCode() ve toString() gibi tekrar eden kodlarla uğraşıyoruz.
Peki, bu tür veri nesnelerini çok daha az kodla tanımlayabilir miyiz?
Java 14 ile preview olarak sunulan ve Java 16 ile kalıcı hale gelen record özelliği tam olarak bu ihtiyaca cevap veriyor.
Record sayesinde veri taşıma amacıyla kullanılan sınıfları daha kısa, anlaşılır ve varsayılan olarak immutable olacak şekilde tanımlayabiliriz.
Java 21 ile birlikte record kullanımı artık modern Java uygulamalarında değerlendirilmesi gereken temel dil özelliklerinden biridir.
1. Record nedir ve nasıl çalışır?
Record, Java’da veri taşıyan nesneleri tanımlamak için kullanılan özel bir sınıf türüdür.
Yukarıdaki müşteri sınıfını record ile yeniden yazalım:
public record CustomerDto(
Long id,
String firstName,
String lastName,
String email) {
}Dikkat ederseniz daha önce onlarca satırda tanımladığımız veri nesnesini artık tek bir tanımlamayla oluşturabiliyoruz.
Java derleyicisi record için otomatik olarak aşağıdaki üyeleri oluşturur:
- Her bileşen için private ve final bir alan.
- Bütün bileşenleri parametre olarak alan canonical constructor.
- Her bileşen için erişim metodu.
- equals() ve hashCode() metotları.
- toString() metodu.
Kullanımı ise oldukça basittir:
CustomerDto customer = new CustomerDto(
1001L,
"Ahmet",
"Yılmaz",
"ahmet@example.com"
);
System.out.println(customer.firstName());
System.out.println(customer.email());Burada önemli bir farklılık var. Geleneksel Java sınıflarındaki getFirstName() yerine record bileşeninin adıyla tanımlanan firstName() metodunu kullanıyoruz.
Record bileşenlerine doğrudan erişim sağlanmaz; bu metotlar ilgili alanların değerini döndürür.
Peki, bu kadar az kod yazmamızın arkasında nasıl bir davranış var?
Record sınıfları java.lang.Record sınıfından örtük olarak türetilir. Tanımladığımız bileşenler constructor ve erişim metotlarıyla birlikte sınıfın temel veri modelini oluşturur.
2. Record neden immutable olarak kabul edilir?
Record kullanımının en önemli avantajlarından biri, veri nesnelerinin değiştirilemez (immutable) bir yapıda tanımlanmasını kolaylaştırmasıdır.
Örneğin:
CustomerDto customer = new CustomerDto(
1001L,
"Ahmet",
"Yılmaz",
"ahmet@example.com"
);
customer.firstName = "Mehmet"; // Derleme hatasıRecord bileşenlerine karşılık gelen alanlar private final olarak oluşturulduğu için doğrudan değer ataması yapılamaz.
Ayrıca record içerisinde bileşenlere karşılık gelen alanları yeniden tanımlayamayız veya bunlara setter metotları ekleyemeyiz.
Bu özellik özellikle çok katmanlı ve eşzamanlı çalışan kurumsal uygulamalarda faydalıdır. Bir servis tarafından oluşturulan veri nesnesinin başka bir katmanda beklenmedik biçimde değiştirilmesi engellenir.
Ancak burada önemli bir ayrıntı var: Record’un immutable olması, içerisindeki bütün nesnelerin de otomatik olarak immutable olduğu anlamına gelmez.
Örneğin:
public record OrderDto(
List<String> products) {
}
Bu record içerisindeki products alanına yeni bir liste atayamayız. Ancak record oluşturulduktan sonra listeye yeni eleman eklenebilir:
List<String> products = new ArrayList<>();
products.add(“Laptop”);
OrderDto order = new OrderDto(1L, products);
order.products().add(“Keyboard”);
Bu kod çalışır. Çünkü record alanının referansı değiştirilemez; ancak referansın gösterdiği liste değiştirilebilir.
Gerçek anlamda daha güvenli bir veri modeli oluşturmak için defensive copy kullanabiliriz:
public record OrderDto(
Long id,
List<String> products) {
public OrderDto {
products = List.copyOf(products);
}
}
Artık dışarıdan gönderilen liste kopyalanarak değiştirilemez bir listeye dönüştürülür.
List<String> products = new ArrayList<>();
products.add(“Laptop”);
OrderDto order = new OrderDto(1L, products);
products.add(“Keyboard”);
System.out.println(order.products());
// [Laptop]
Burada kullandığımız List.copyOf() yalnızca listenin yapısını korur. Liste elemanları mutable nesneler ise onların iç durumlarının değişmesini engellemez.
Dolayısıyla record kullanırken yalnızca alanların final olmasına değil, içerdikleri nesnelerin değiştirilebilir olup olmadığına da dikkat etmeliyiz.
3. Constructor ve validation kullanımı
Record’lar yalnızca veri taşımak için kullanılan basit yapılar olmak zorunda değildir. Nesne oluşturulurken geçerlilik kontrolleri de yapabiliriz.
Bunun için compact constructor özelliğini kullanabiliriz.
Örneğin bir müşterinin e-posta adresinin boş olmaması gerektiğini düşünelim:
public record CustomerDto(
Long id,
String firstName,
String lastName,
String email) {
public CustomerDto {
if (id == null || id <= 0) {
throw new IllegalArgumentException(
“Customer id must be positive”);
}
if (email == null || email.isBlank()) {
throw new IllegalArgumentException(
“Email cannot be empty”);
}
}
}
Burada constructor parametrelerini ayrıca tanımlamadığımıza dikkat edin. Compact constructor içerisinde record bileşenlerine doğrudan erişebiliriz.
Java, constructor tamamlandığında bu parametreleri ilgili alanlara atar.
Bu yaklaşım sayesinde nesnenin geçersiz bir durumda oluşturulmasını engelleyebiliriz.
Daha geleneksel bir constructor da tanımlayabiliriz:
public record Money(
BigDecimal amount,
String currency) {
public Money(BigDecimal amount, String currency) {
if (amount == null || amount.signum() < 0) {
throw new IllegalArgumentException(
“Invalid amount”);
}
if (currency == null || currency.isBlank()) {
throw new IllegalArgumentException(
“Currency is required”);
}
this.amount = amount;
this.currency = currency;
}
}
Burada canonical constructor’ın bütün bileşenleri aynı sırayla ve aynı türlerle karşılaması gerekir.
Peki, Spring Boot uygulamalarında Bean Validation kullanmak istersek ne yapmalıyız?
Record bileşenlerine standart validation anotasyonları ekleyebiliriz:
public record CreateCustomerRequest(
@NotBlank
String firstName,
@NotBlank
String lastName,
@NotBlank
String email) {
}
Controller tarafında ise normal DTO kullanımına benzer biçimde doğrulama yapabiliriz:
@RestController
@RequestMapping(“/api/customers”)
class CustomerController {
@PostMapping
public ResponseEntity<Void> create(
@Valid @RequestBody CreateCustomerRequest request) {
return ResponseEntity.accepted().build();
}
}
Bunun için Spring Boot uygulamasında uygun Bean Validation bağımlılığının bulunması gerekir.
Burada constructor doğrulaması ile Bean Validation arasında bir ayrım vardır. Constructor doğrulaması nesnenin her oluşturulma girişiminde çalışırken, Bean Validation ilgili nesne doğrulama mekanizması çağrıldığında devreye girer.
4. Record içerisinde metot tanımlayabilir miyiz?
Record yalnızca alanlardan oluşan bir veri yapısı değildir. Normal sınıflarda olduğu gibi metotlar da tanımlayabiliriz.
Örneğin müşterinin tam adını döndüren bir metot ekleyelim:
public record CustomerDto(
Long id,
String firstName,
String lastName,
String email) {
public String fullName() {
return firstName + “ “ + lastName;
}
}
Kullanımı:
CustomerDto customer = new CustomerDto(
1001L,
“Ahmet”,
“Yılmaz”,
“ahmet@example.com”
);
System.out.println(customer.fullName());
Ayrıca static metotlar ve static alanlar da tanımlayabiliriz.
public record Temperature(double value) {
public static Temperature fromCelsius(double value) {
return new Temperature(value);
}
public double toFahrenheit() {
return (value * 9 / 5) + 32;
}
}
Bu örnekte record hem veri taşımakta hem de veriye bağlı bir dönüşüm davranışı sunmaktadır.
Ancak record’u kapsamlı iş mantığı barındıran bir sınıfa dönüştürmek yerine, temel veri modelini ve onunla doğrudan ilişkili davranışları tanımlamak daha anlaşılır bir tasarım sağlar.
5. Record ile interface kullanabilir miyiz?
Evet. Record sınıfları interface implement edebilir.
Örneğin farklı ödeme türlerini ortak bir sözleşme üzerinden temsil etmek istediğimizi düşünelim:
public interface PaymentMethod {
String description();
}
Record kullanarak kredi kartı ödeme modelini tanımlayabiliriz:
public record CreditCardPayment(
String cardHolder,
String lastFourDigits)
implements PaymentMethod {
@Override
public String description() {
return “Credit card ending with “ + lastFourDigits;
}
}
Aynı interface’i başka bir record da uygulayabilir:
public record BankTransferPayment(
String iban)
implements PaymentMethod {
@Override
public String description() {
return “Bank transfer: “ + iban;
}
}
Get Muharrem Konca’s stories in your inbox
Join Medium for free to get updates from this writer.
Böylece farklı veri modellerini ortak bir interface üzerinden kullanabiliriz.
Record’lar başka bir sınıftan türetilemez; çünkü zaten örtük olarak java.lang.Record sınıfından türetilirler. Ancak interface implement edebilirler.
Bu özellik, özellikle farklı veri tiplerinin ortak bir sözleşme üzerinden yönetildiği uygulamalarda faydalıdır.
6. Record ile DTO tasarlamak
Record kullanımının en yaygın ve faydalı alanlarından biri DTO (Data Transfer Object) tasarımıdır.
Özellikle REST API geliştirdiğimiz Spring Boot uygulamalarında request ve response modellerini record ile tanımlayabiliriz.
Örneğin müşteri oluşturma isteği:
public record CreateCustomerRequest(
String firstName,
String lastName,
String email) {
}
Servis yanıtı:
public record CustomerResponse(
Long id,
String fullName,
String email) {
}
Controller:
@RestController
@RequestMapping(“/api/customers”)
class CustomerController {
private final CustomerService customerService;
CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@PostMapping
public ResponseEntity<CustomerResponse> create(
@Valid @RequestBody CreateCustomerRequest request) {
CustomerResponse response =
customerService.create(request);
return ResponseEntity.ok(response);
}
}
Bu kullanımda request ve response modelleri yalnızca veri taşımakla görevli oldukları için record oldukça uygun bir seçenektir.
Jackson gibi modern JSON kütüphaneleri record desteği sunar. Dolayısıyla uygun sürümlerde ekstra getter veya özel constructor yapılandırması olmadan JSON dönüşümü gerçekleştirilebilir.
Örneğin aşağıdaki JSON içeriği:
{
“id”: 1001,
“fullName”: “Ahmet Yılmaz”,
“email”: “ahmet@example.com”
}
CustomerResponse record nesnesine dönüştürülebilir.
Bununla birlikte kullanılan Jackson sürümünün record desteği ve özel JSON isimlendirme gereksinimleri kontrol edilmelidir.
7. Record ile JPA Entity oluşturabilir miyiz?
Bu konu özellikle kurumsal Java uygulamalarında dikkat edilmesi gereken noktalardan biridir.
İlk bakışta JPA entity sınıflarını da record olarak tanımlamak cazip gelebilir. Çünkü entity içerisinde de alanlar, constructor ve veri erişim metotları bulunur.
Ancak standart JPA entity gereksinimleri record yapısıyla uyumlu değildir.
JPA entity sınıfları genel olarak:
- Final olmamalıdır.
- Public veya protected parametresiz constructor içermelidir.
- Persistence provider tarafından yönetilmeye uygun olmalıdır.
- Proxy ve lazy loading gibi mekanizmalarla uyumlu olmalıdır.
Record sınıfları ise final’dır, final olmayan bir entity gibi genişletilemez ve record bileşenlerinin oluşturulma biçimi standart entity gereksinimleriyle uyuşmaz.
Bu nedenle standart JPA entity’leri record olarak tanımlamak doğru yaklaşım değildir.
Bunun yerine entity ve DTO ayrımını koruyabiliriz:
@Entity
@Table(name = “CUSTOMER”)
public class CustomerEntity {
@Id
private Long id;
private String firstName;
private String lastName;
private String email;
protected CustomerEntity() {
}
// Constructor, getter ve diğer metotlar
}
Veritabanından aldığımız veriyi API katmanına aktarırken record kullanabiliriz:
public record CustomerResponse(
Long id,
String fullName,
String email) {
}
Dönüşüm işlemi:
public CustomerResponse toResponse(CustomerEntity entity) {
return new CustomerResponse(
entity.getId(),
entity.getFirstName() + “ “ + entity.getLastName(),
entity.getEmail()
);
}
Bu yaklaşımda JPA entity yaşam döngüsü ile API veri modeli birbirinden ayrılmış olur.
Özetle record, DTO ve projection gibi veri taşıma modellerinde güçlü bir seçenektir; standart JPA entity sınıflarının yerine kullanılmamalıdır.
8. Record ile Generic veri modelleri oluşturmak
Record’ların bir diğer avantajı generic tiplerle birlikte kullanılabilmeleridir.
Örneğin API yanıtlarının ortak bir formatta dönmesini istediğimizi düşünelim.
Her servis için ayrı response sınıfı oluşturmak yerine generic bir record tanımlayabiliriz:
public record ApiResponse<T>(
boolean success,
T data,
String message) {
}
Bu modeli farklı veri türleriyle kullanabiliriz:
ApiResponse<CustomerResponse> customerResponse =
new ApiResponse<>(
true,
new CustomerResponse(
1001L,
“Ahmet Yılmaz”,
“ahmet@example.com”),
“Customer retrieved successfully”
);
Liste dönen bir servis için de kullanabiliriz:
ApiResponse<List<CustomerResponse>> response =
new ApiResponse<>(
true,
customerList,
“Customers retrieved successfully”
);
Bu yaklaşım API yanıtlarının standartlaştırılmasına yardımcı olur.
Ancak her API yanıtını aynı generic yapıya zorlamak da doğru olmayabilir. Hata yanıtları, sayfalama bilgileri ve farklı iş gereksinimleri için ayrı modeller daha anlaşılır olabilir.
9. Record ve equals/hashCode davranışı
Record kullanırken önemli avantajlardan biri, equals() ve hashCode() metotlarının otomatik oluşturulmasıdır.
Normal sınıflarda bu metotları çoğu zaman kendimiz yazmak veya IDE üzerinden üretmek zorunda kalırız.
Record içerisinde ise bileşenlerin değerleri karşılaştırılır.
public record Product(
Long id,
String name,
BigDecimal price) {
}
İki farklı nesne oluşturalım:
Product product1 =
new Product(1L, “Laptop”, new BigDecimal(“45000”));
Product product2 =
new Product(1L, “Laptop”, new BigDecimal(“45000”));
System.out.println(product1.equals(product2));
// true
Nesnelerin referansları farklı olsa da bileşen değerleri eşit olduğu için sonuç true olur.
Bu özellik record’ları özellikle Map anahtarı, Set elemanı ve değer nesnesi (Value Object) olarak kullanırken faydalı hale getirir.
Ancak bir ayrıntıya dikkat etmeliyiz: BigDecimal.equals() ölçek bilgisini de dikkate alır. Örneğin 10.0 ile 10.00 değerleri equals() açısından eşit değildir. Dolayısıyla record’un değer karşılaştırması, bileşenlerin kendi eşitlik davranışlarını kullanır.
10. Record kullanırken performans kazanır mıyız?
Record kullanımının temel amacı daha hızlı kod çalıştırmak değildir. Esas kazanım, daha sade ve güvenli veri modelleri oluşturmaktır.
Record kullanmak otomatik olarak daha az bellek tüketimi veya daha hızlı uygulama anlamına gelmez. Record nesneleri de JVM üzerinde normal nesneler gibi oluşturulur ve garbage collection süreçlerine tabidir.
Bununla birlikte record kullanımının dolaylı faydaları olabilir:
- Tekrarlanan constructor ve getter kodları ortadan kalkar.
- Veri modelleri daha kolay okunur.
- Gereksiz setter metotları oluşturulmaz.
- Değer tabanlı eşitlik davranışı hazır gelir.
- Immutable veri modeli oluşturmak kolaylaşır.
Özellikle yüksek hacimli veri işleyen batch uygulamalarında record kullanmak, veri taşıma modellerini sadeleştirebilir. Ancak performans kazanımı beklentisi varsa gerçek iş yükü üzerinden JMH gibi benchmark araçlarıyla ölçüm yapılmalıdır.
11. Java 21 ile Record kullanımını nasıl değerlendirmeliyiz?
Java 21, record kullanımının yerleşik ve olgun bir dil özelliği olarak değerlendirilebildiği bir sürümdür. Record’ların yanında Java 21 ile gelen pattern matching özellikleri de veri modellerinin kullanımını kolaylaştırır.
Örneğin instanceof pattern matching ile record türlerini daha sade biçimde kontrol edebiliriz:
public void process(Object value) {
if (value instanceof CustomerResponse customer) {
System.out.println(customer.fullName());
}
}
Java 21'de switch pattern matching de kalıcı bir özelliktir. Record pattern ise Java 21'de preview kapsamındadır ve kullanılabilmesi için preview özelliklerinin etkinleştirilmesi gerekir.
Bu nedenle üretim projelerinde record pattern kullanmadan önce derleme ve çalışma zamanı yapılandırması değerlendirilmelidir.
Özellikle Java 21'e geçiş yapan kurumsal uygulamalarda record kullanımı, DTO ve değer nesnelerinin sadeleştirilmesi için iyi bir başlangıç noktasıdır.
12. Hangi durumlarda Record kullanmalıyız?
Artık record’un ne olduğunu ve nasıl çalıştığını gördük. Peki, her Java sınıfını record olarak tanımlamak doğru mudur?
Elbette hayır.
Record özellikle nesnenin temel amacının veri taşımak olduğu durumlarda tercih edilmelidir.
Örneğin aşağıdaki kullanım alanları oldukça uygundur:
- REST API request ve response modelleri
- DTO sınıfları
- Değer nesneleri (Value Object)
- Servisler arasında taşınan immutable veri modelleri
- Raporlama ve sorgu sonuçları
- Event ve mesaj payload’ları
- Hesaplama sonuçlarını temsil eden veri yapıları
Buna karşılık aşağıdaki durumlarda normal sınıflar daha uygun olabilir:
- Durumu zaman içinde değişen nesneler
- Karmaşık yaşam döngüsüne sahip domain nesneleri
- Standart JPA entity sınıfları
- Kalıtım gerektiren sınıf hiyerarşileri
- Setter ve proxy davranışına ihtiyaç duyan nesneler
Buradaki temel tasarım kriteri basittir: Bir nesne esas olarak değerleri temsil ediyorsa record güçlü bir seçenektir. Ancak nesnenin değişen bir durumu, yaşam döngüsü veya karmaşık davranışları varsa normal sınıf kullanmak daha doğru olabilir.
Sonuç: Daha az kod, daha net veri modelleri
Java record özelliği, uzun süredir Java geliştiricilerinin karşılaştığı tekrar eden veri taşıma sınıfı problemlerine sade bir çözüm sunuyor.
Constructor, getter, equals(), hashCode() ve toString() gibi tekrar eden kodların otomatik oluşturulması sayesinde veri modelleri daha okunabilir hale geliyor. Ayrıca final alanlar ve değer tabanlı eşitlik davranışı, immutable tasarımları kolaylaştırıyor.
Özellikle Java 21 ve Spring Boot tabanlı kurumsal uygulamalarda DTO, API response, projection ve Value Object tasarımlarında record kullanımı önemli bir avantaj sağlayabilir.
Ancak her sınıfı record’a dönüştürmek doğru bir yaklaşım değildir. JPA entity’leri, değiştirilebilir domain nesneleri ve karmaşık yaşam döngüsüne sahip sınıflar için geleneksel sınıf yapısı hâlâ gereklidir.
Önemli olan daha az kod yazmak değil, nesnenin sorumluluğunu en doğru şekilde ifade eden Java yapısını seçmektir. Record, veri odaklı modeller için bu seçimi oldukça kolaylaştırır.
Kaynaklar


