Java'da OOP: Sınıflar, Nesneler, Kapsülleme, Kalıtım ve Soyutlama
Pratik örneklerle Java’da Nesne Yönelimli Programlamayı öğrenin. Bir restoran menü sistemi kullanarak sınıflar, nesneler, kalıtım, kapsülleme ve soyut sınıflarda uzmanlaşın.
Java, dünya genelinde en popüler üç dil arasında istikrarlı şekilde yer alır. Kurumsal yazılım geliştirme, Android mobil uygulamaları ve büyük ölçekli web uygulamaları gibi alanlardaki benimsenmesi benzersizdir. Güçlü tip sistemi, geniş ekosistemi ve “bir kez yaz, her yerde çalıştır” özelliği, sağlam ve ölçeklenebilir sistemler kurmak için onu özellikle cazip kılar. Bu yazıda, Java’nın nesne yönelimli programlama özelliklerinin geliştiricilerin bu yeteneklerden etkin şekilde yararlanmasına nasıl olanak tanıdığını, doğru kod organizasyonu ve yeniden kullanım yoluyla bakımı kolay ve ölçeklenebilir uygulamalar geliştirmelerini nasıl sağladığını inceleyeceğiz.
Java Kodunu Düzenleme ve Çalıştırma Üzerine Bir Not
Herhangi bir kod yazmaya başlamadan önce biraz kurulum yapalım.
Sözdiziminde olduğu gibi, Java’nın kod organizasyonu konusunda da katı kuralları vardır.
Öncelikle, her public sınıf kendi dosyasında olmalı ve dosya adı sınıf adıyla birebir aynı olup .java uzantısına sahip olmalıdır. Örneğin, bir Laptop sınıfı yazmak istersem, dosya adı mutlaka Laptop.java—büyük/küçük harf duyarlı olmalıdır. Aynı dosyada public olmayan sınıflar bulunabilir ancak bunları ayırmak en iyisidir. Farkındayım, daha sınıfları yazmadan önce nasıl organize edileceğinden bahsederek önden gidiyoruz; ancak önceden neleri nereye koyacağımıza dair kabaca bir fikir edinmek iyi bir fikirdir.
Tüm Java projelerinde Main.java dosyası ve içinde Main sınıfı bulunur. Sınıflarınızı buradan oluşturup nesneler yaratarak test edersiniz.
Java kodunu çalıştırmak için popüler bir Java IDE’si olanIntelliJ IDEA’yı kullanacağız. IntelliJ’i kurduktan sonra:
Yeni bir Java projesi oluşturun (File > New > Project)
src klasörüne sağ tıklayarak Main.java dosyasını oluşturun ve aşağıdaki içeriği yapıştırın:
publicclassMain{publicstaticvoidmain(String[] args){// Create and tests objects here}}Altyapı:
Sınıflardan bahsederken, kodu Main.java dışındaki dosyalara yazarız. Ancak nesneler oluşturma ve test etmeden bahsediyorsak Main.java’ya geçeriz.
Programı çalıştırmak için main metodunun yanındaki yeşil oynat düğmesine tıklayabilirsiniz:
Çıktı altta, Run araç penceresinde gösterilecektir.
Java’ya tamamen yeniyseniz, lütfen devam etmeden önce Java veri tipleri ve kontrol akışının temellerini ele alanIntroduction to Java kursumuza göz atın.
Aksi halde, doğrudan konuya girelim.
Java Sınıfları ve Nesneleri
Peki, sınıflar tam olarak nedir?
Sınıflar, gerçek dünyadaki kavramları temsil etmek için Java’daki programlama yapılarıdır. Örneğin, şu MenuItem sınıfını düşünün (IDE’nizde bu sınıfı yazmak için bir dosya oluşturun):
Sınıf, bir restorandaki çeşitli menü öğelerini temsil etmek için bize bir plan ya da şablon sunar. Sınıfın iki özelliğini, name, ve price, değiştirerek burger veya salata gibi sayısız menü nesnesi oluşturabiliriz.
Java’da bir sınıf oluşturmak için, sınıfın erişim seviyesini (private, public veya protected) belirten bir satır yazar, ardından sınıf adını verirsiniz. Süslü parantezlerden hemen sonra, sınıfınızın özniteliklerini tanımlarsınız.
Peki, bu sınıfa ait nesneleri nasıl oluştururuz? Java bunu kurucu (constructor) metodlarla sağlar:
Kurucu, bir sınıftan yeni bir nesne oluşturduğumuzda çağrılan özel bir metottur. Nesnenin özniteliklerini sağladığımız değerlerle başlatır. Yukarıdaki örnekte kurucu, name ve price parametrelerini alır ve ‘this’ anahtar kelimesini kullanarak bunları gelecekteki nesne örneğine atar.
Kurucunun sözdizimi diğer sınıf metotlarından farklıdır çünkü dönüş tipi belirtmenizi gerektirmez. Ayrıca, kurucu sınıfla aynı ada sahip olmalı ve sınıf tanımından sonra tanımladığınız özniteliklerle uyumlu olmalıdır. Yukarıda, kurucu iki öznitelik oluşturuyor çünkü sınıf tanımından sonra iki öznitelik ilan ettik: name ve price.
Sınıfınızı ve kurucusunu yazdıktan sonra, main metodunuzda bu sınıfın örneklerini (nesnelerini) oluşturabilirsiniz:
Yukarıda, iki MenuItem nesnesini burger ve salad değişkenlerine atıyoruz. Java’da zorunlu olduğu üzere, değişkenin tipi ilan edilmelidir; burada bu tip MenuItem’dır. Ardından, sınıfımızın bir örneğini oluşturmak için new anahtar kelimesini yazar ve kurucu metodu çağırırız.
Kurucuya ek olarak, sınıfınıza davranış kazandıran normal metotlar da oluşturabilirsiniz. Örneğin, aşağıda vergi sonrası toplam fiyatı hesaplayan bir metot ekliyoruz:
Sınıfların amacı, nesneler oluşturmak için bir şablon sağlamaktır. Bu nesneler daha sonra diğer betikler veya programlar tarafından kullanılacaktır. Örneğin, MenuItem nesnelerimiz, adlarını, fiyatlarını ve görsellerini ekranda gösteren bir kullanıcı arayüzü tarafından kullanılabilir.
Bu nedenle, sınıflarımızı, örneklerinin yalnızca bizim amaçladığımız şekilde kullanılabileceği biçimde tasarlamalıyız. Şu anda MenuItem sınıfımız çok temel ve hataya açık. Biri saçma özniteliklerle nesneler oluşturabilir; örneğin negatif fiyatlı elmalı turta veya milyon dolarlık sandviç:
Dolayısıyla, bir sınıf yazdıktan sonra yapılacak ilk iş, özniteliklerinin nasıl oluşturulup erişildiğini sınırlayarak onları korumaktır. Başlangıç olarak, price için yalnızca pozitif değerler kabul etmek ve yanlışlıkla aşırı pahalı öğelerin görüntülenmesini önlemek için bir üst sınır belirlemek istiyoruz.
Java bunu setter metotlarıyla yapmamıza izin verir:
Yukarıdaki kod bloğunda nelerin yeni olduğuna bakalım:
1. private anahtar kelimesini ekleyerek öznitelikleri private yaptık. Bu, onlara yalnızca MenuItem sınıfı içinde erişilebileceği anlamına gelir. Kapsülleme bu kritik adımla başlar.
2. Şu yeni sabiti ekledik: MAX_PRICE. Bu sabit şunları taşır:
private (yalnızca sınıf içinde erişilebilir)
static (tüm örnekler arasında ortak)
final (başlatıldıktan sonra değiştirilemez)
makul bir üst sınır olarak 100,0 $’a ayarlanmıştır
3. setPrice() metodunu ekledik; bu metot:
Bir price parametresi alır
Fiyatın negatif olmadığını doğrular
Fiyatın MAX_PRICE’ı aşmadığını doğrular
Doğrulama başarısız olursa açıklayıcı mesajlarla IllegalArgumentException fırlatır
Tüm doğrulamalar geçerse fiyatı ayarlar
4. Kurucuyu, fiyatı doğrudan atamak yerine setPrice() kullanacak şekilde değiştirdik. Bu, nesne oluşturma sırasında fiyat doğrulamasının yapılmasını sağlar.
Az önce iyi nesne yönelimli tasarımın temel sütunlarından birini uyguladık — kapsülleme. Bu paradigma, veri gizliliğini ve nesne özniteliklerine kontrollü erişimi dayatır; böylece dahili uygulama ayrıntıları dış müdahaleden korunur ve yalnızca iyi tanımlanmış arayüzler aracılığıyla değiştirilebilir.
Kapsüllemeyi name özniteliğine uygulayarak konuyu pekiştirelim. Sadece latte, cappuccino, espresso, americano ve mocha servis eden bir kahve dükkânımız olduğunu hayal edin.
Dolayısıyla menü öğesi adlarımız yalnızca bu listedeki öğelerden biri olabilir. Bunu koda şöyle dökebiliriz:
// Rest of the class here...privatestaticfinalString[]VALID_NAMES={"latte","cappuccino","espresso","americano","mocha"};privateString name;publicvoidsetName(String name){String lowercaseName = name.toLowerCase();for(String validName :VALID_NAMES){if(validName.equals(lowercaseName)){this.name = name;return;}}thrownewIllegalArgumentException("Invalid drink name. Must be one of: "+String.join(", ",VALID_NAMES));}Altyapı:
Yukarıdaki kod, bir kahve dükkânındaki menü öğeleri için ad doğrulamasını uygular. Bunu parçalayalım:
1. Önce, yalnızca izin verilen içecek adlarını içeren private static final bir dizi olan VALID_NAMES tanımlanır: latte, cappuccino, espresso, americano ve mocha. Bu dizinin özellikleri şunlardır:
private: yalnızca sınıf içinde erişilebilir
static: tüm örnekler için ortaktır
final: başlatıldıktan sonra değiştirilemez
2. İçecek adını depolamak için private bir String name alanı ilan edilir
3. setName() metodu doğrulama mantığını uygular:
String türünde bir name parametresi alır
Karşılaştırmayı büyük-küçük harf duyarsız yapmak için küçük harfe çevirir
VALID_NAMES dizisi üzerinde döngü kurar
Eşleşme bulunursa adı ayarlar ve döner
Eşleşme bulunamazsa, tüm geçerli seçenekleri listeleyen açıklayıcı bir mesajla IllegalArgumentException fırlatır
İşte şu ana kadarki tam sınıf:
publicclassMenuItem{privateString name;privatedouble price;privatestaticfinalString[]VALID_NAMES={"latte","cappuccino","espresso","americano","mocha"};privatestaticfinaldoubleMAX_PRICE=100.0;publicMenuItem(String name,double price){setName(name);setPrice(price);}publicvoidsetName(String name){String lowercaseName = name.toLowerCase();for(String validName :VALID_NAMES){if(validName.equals(lowercaseName)){this.name = name;return;}}thrownewIllegalArgumentException("Invalid drink name. Must be one of: "+String.join(", ",VALID_NAMES));}publicvoidsetPrice(double price){if(price <0){thrownewIllegalArgumentException("Price cannot be negative");}if(price >MAX_PRICE){thrownewIllegalArgumentException("Price cannot exceed $"+MAX_PRICE);}this.price = price;}}Altyapı:
Özniteliklerin nasıl oluşturulduğunu koruduktan sonra, onlara nasıl erişildiğini de korumak isteriz. Bu, getter metotları kullanılarak yapılır:
publicclassMenuItem{// Rest of the code here...publicStringgetName(){return name;}publicdoublegetPrice(){return price;}}Altyapı:
Getter metotları, bir sınıfın private özniteliklerine kontrollü erişim sağlar. Doğrudan öznitelik erişiminin yol açabileceği istenmeyen değişiklikleri önleyerek kapsüllemenin bozulmasını engellerler.
Örneğin, getter olmadan özniteliklere doğrudan erişebiliriz:
MenuItem item =newMenuItem("Latte",4.99);String name = item.name;// Direct access to attribute
item.name ="INVALID";// Can modify directly, bypassing validationAltyapı:
Getter’larla doğru erişimi zorunlu kılarız:
MenuItem item =newMenuItem("Latte",4.99);String name = item.getName();// Controlled access through getter// item.name = "INVALID"; // Not allowed - must use setName() which validatesAltyapı:
Bu kapsülleme şunları sağlar:
Geçersiz değişiklikleri önleyerek veri bütünlüğünü korur
Sınıfı kullanan kodu etkilemeden dahili uygulamayı değiştirmemize imkân tanır
Gerektiğinde ek mantık içerebilecek tek bir erişim noktası sunar
Kodu daha bakımı kolay ve hatalara daha az yatkın hale getirir
Kalıtım
Sınıfımız iyi görünmeye başladı ama oldukça fazla sorun var. Örneğin, birçok türde yemek ve içecek servis eden büyük bir restoran için bu sınıf yeterince esnek değil.
Farklı türde yiyecek öğeleri eklemek istersek çeşitli zorluklarla karşılaşırız. Bazı yemekler paket servise uygundur, bazıları anında tüketilmelidir. Menü öğelerinin farklı fiyat ve indirimleri olabilir. Yemeklerin sıcaklık takibi veya özel depolama gerektirebilir. İçecekler sıcak ya da soğuk olabilir ve özelleştirilebilir malzemeler içerebilir. Öğeler alerjen bilgisi ve porsiyon seçenekleri gerektirebilir. Mevcut sistem bu farklı gereksinimleri karşılamıyor.
Kalıtım bu sorunların tümüne zarif bir çözüm sunar. Ortak özniteliklere sahip temel bir MenuItem sınıfı tanımlamamıza ve ardından bu temelleri miras alırken kendine özgü özellikler ekleyen alt sınıflar oluşturmamıza olanak tanır.
Örneğin, sıcaklık seçenekleri olan içecekler için bir Drink sınıfı, anında tüketilmesi gereken öğeler için bir Food sınıfı ve özel depolama gerektiren öğeler için bir Dessert sınıfımız olabilir — hepsi çekirdek menü öğesi işlevselliğini miras alır.
Sınıfları genişletme
Bu fikirleri Drink ile başlayarak hayata geçirelim:
Bir ebeveynden miras alan bir alt sınıf tanımlamak için, alt sınıf adından sonra extends anahtar kelimesini ve ardından ebeveyn sınıfı yazarız. Sınıf tanımından sonra, bu alt sınıfın sahip olduğu yeni öznitelikleri tanımlar ve kurucusunu uygularız.
Ancak dikkat edin, name ve price başlatmalarını isCold ile birlikte tekrarlamak zorunda kalıyoruz. Bu ideal değildir çünkü ebeveyn sınıfın yüzlerce özniteliği olabilir. Ayrıca, yukarıdaki kod derleme sırasında hata verecektir çünkü ebeveyn sınıfın özniteliklerini başlatmanın doğru yolu bu değildir. Doğru yol super anahtar kelimesini kullanmaktır:
super anahtar kelimesi, ebeveyn sınıfın kurucusunu çağırmak için kullanılır. Bu durumda super(name, price), bu öznitelikleri başlatmak için MenuItem’ın kurucusunu çağırır ve kod tekrarını önler. Yalnızca Drink sınıfına özgü yeni isCold özniteliğini başlatmamız gerekir.
Bu anahtar kelime oldukça esnektir çünkü alt sınıfın herhangi bir kısmında ebeveyn sınıfa atıfta bulunmak için kullanılabilir. Örneğin, bir ebeveyn metodunu çağırmak için super.methodName(), bir öznitelik için ise super.attributeName kullanılır.
Metot geçersiz kılma (overriding)
Şimdi, sınıflarımıza vergi sonrası toplam fiyatı hesaplayan yeni bir metot eklemek istediğimizi düşünelim. Farklı menü öğelerinin farklı vergi oranları olabileceğinden (örneğin, hazırlanmış yiyecekler ve paketli içecekler), ebeveyn sınıfta ortak bir metot adı korurken her alt sınıfta belirli vergi hesaplamalarını uygulamak için metot geçersiz kılmayı kullanabiliriz.
Bu şöyle görünür:
publicclassMenuItem{// Rest of the MenuItem classpublicdoublecalculateTotalPrice(){// Default tax rate of 10%return price *1.10;}}publicclassFoodextendsMenuItem{privateboolean isVegetarian;publicFood(String name,double price,boolean isVegetarian){super(name, price);this.isVegetarian = isVegetarian;}@OverridepublicdoublecalculateTotalPrice(){// Food has 15% taxreturnsuper.getPrice()*1.15;}}publicclassDrinkextendsMenuItem{privateboolean isCold;publicDrink(String name,double price,boolean isCold){super(name, price);this.isCold = isCold;}@OverridepublicdoublecalculateTotalPrice(){// Drinks have 8% taxreturnsuper.getPrice()*1.08;}}Altyapı:
Bu örnekte metot geçersiz kılma, her alt sınıfın calculateTotalPrice() metoduna kendi uygulamasını sağlamasına olanak tanır:
Temel MenuItem sınıfı varsayılan olarak %10 vergi hesabı tanımlar.
Food ve Drink sınıfları MenuItem’ı genişlettiğinde, bu metodu kendi vergi oranlarını uygulamak üzere geçersiz kılar:
Yiyecek öğelerinde %15 gibi daha yüksek vergi oranı
İçeceklerde %8 gibi daha düşük vergi oranı
@Override açıklama satırı, bu metotların ebeveyn sınıf metodunu geçersiz kıldığını açıkça belirtmek için kullanılır. Bu, metot imzası ebeveyn sınıfla eşleşmezse hataları yakalamaya yardımcı olur.
Her alt sınıf yine de ebeveyn sınıfın fiyatına super.getPrice() ile erişebilir; bu da geçersiz kılınan metotların, kendi davranışlarını eklerken ebeveyn sınıf işlevselliğinden nasıl yararlanabileceğini gösterir.
Kısacası, metot geçersiz kılma, alt sınıfların ebeveyn sınıfta tanımlanan metotlara kendi uygulamalarını sağlamasına olanak tanıyan ve aynı metot imzasını koruyan kalıtımın ayrılmaz bir parçasıdır.
Soyut Sınıflar
MenuItem sınıf hiyerarşimiz çalışıyor, ancak bir sorun var: herkes düz bir MenuItem nesnesi oluşturabilmeli mi? Sonuçta restoranımızda her menü öğesi ya Food ya da Drink’tir — “genel bir menü öğesi” diye bir şey yok.
Bunu, MenuItem’ı soyut bir sınıf yaparak önleyebiliriz. Soyut bir sınıf yalnızca bir temel şablon sunar — yalnızca kalıtım için ebeveyn sınıf olarak kullanılabilir, doğrudan örneklenemez.
MenuItem’ı soyut yapmak için erişim belirleyicisinden sonra abstract anahtar kelimesini ekleriz:
publicabstractclassMenuItem{privateString name;privatedouble price;publicMenuItem(String name,double price){setName(name);setPrice(price);}// Existing getters/setters remain the same// Make this method abstract - every subclass MUST implement itpublicabstractdoublecalculateTotalPrice();}Altyapı:
Soyut sınıflar, yukarıdaki calculateTotalPrice() gibi soyut metotlar da içerebilir. Bu soyut metotlar, alt sınıfları kendi uygulamalarını sağlamaya zorlayan bir sözleşme işlevi görür. Başka bir deyişle, soyut bir sınıftaki her soyut metot, alt sınıflar tarafından uygulanmalıdır.
Öyleyse, bu değişiklikleri dikkate alarak Food ve Drink sınıflarını yeniden yazalım:
Bu menü sistemi uygulamasıyla, soyutlama ve kalıtımın farklı iş gereksinimlerine kolayca uyum sağlayabilen esnek ve bakımı kolay kod oluşturmak için nasıl birlikte çalıştığını gördük.
Sonuç
Bugün, Java’nın bir nesne yönelimli programlama dili olarak neler yapabildiğine kısa bir bakış attık. Bir restoran menü sistemi üzerinden sınıfların, nesnelerin ve OOP’nin birkaç temel sütununun: kapsülleme, kalıtım ve soyutlamanın temellerini ele aldık.
Bu sistemi üretime hazır hale getirmek için hâlâ arayüzler (soyutlamanın bir parçası), çok biçimlilik (polymorphism) ve OOP tasarım kalıpları gibi öğrenecek pek çok şey var. Bu kavramlar hakkında daha fazla bilgi edinmek için Introduction to OOP in Java kursumuza bakın.
Bu Java OOP eğitimini takip etmek için önkoşullar nelerdir?⌄
Veri tipleri, değişkenler, kontrol akışı (if/else, döngüler) ve temel sözdizimi dahil olmak üzere Java programlamaya dair temel bilgilere sahip olmalısınız. Bir hatırlatmaya ihtiyaç duyarsanız, Introduction to Java kursumuz bu temelleri kapsar.
OOP kavramlarını açıklamak için neden bir restoran menü sistemi kullanılıyor?⌄
Bir restoran menü sistemi, OOP ilkelerinin gerçek dünyadaki uygulamalarını gösteren sezgisel bir örnektir. Doğal olarak kalıtımı (farklı menü öğesi türleri), kapsüllemeyi (fiyat ve ad değerlerinin korunması) ve arayüzleri (sadakat programları, sıcaklık kontrolü) sergiler. Çoğu insan restoranların nasıl çalıştığını bildiğinden, kavramlar daha kolay anlaşılır.
Java'da soyut sınıflar ile arayüzler arasındaki fark nedir?⌄
Bir sınıf yalnızca bir soyut sınıfı genişletebilir ancak birden çok arayüzü uygulayabilir.
Soyut sınıflar alanlara ve kuruculara sahip olabilir; arayüzler olamaz.
Soyut sınıflar hem soyut hem somut metotlara sahip olabilir; arayüzler (Java 8’den önce) geleneksel olarak yalnızca soyut metotlara sahiptir.
Soyut sınıflar bir temel uygulama sağlar, arayüzler ise bir sözleşme tanımlar.
2 yılı aşkın deneyime sahip bir veri bilimi içerik üreticisiyim ve Medium’da en büyük takipçi kitlelerinden birine sahibim. Yapay zekâ ve makine öğrenimi üzerine, biraz da alaycı bir üslupla yazılmış, ayrıntılı makaleler kaleme almayı seviyorum; çünkü onları biraz daha az sıkıcı hale getirmek için bir şeyler yapmak gerekiyor. 130’un üzerinde makale ve bir de DataCamp kursu ürettim; bir diğeri de hazırlık aşamasında. İçeriklerim 5 milyondan fazla kişi tarafından görüldü; bunların 20 bini Medium ve LinkedIn’de takipçim oldu.
Algoritmalar, sistem tasarımı ve davranışsal senaryoları kapsayan bu temel sorularla teknik mülakat sürecine hakim olun. Uzman cevapları, kod örnekleri ve kanıtlanmış hazırlık stratejileri edinin.
Git deponuzu temiz tutmak için .gitignore’u nasıl kullanacağınızı öğrenin. Bu eğitim; temelleri, yaygın kullanım durumlarını ve başlamanıza yardımcı olacak pratik örnekleri kapsar!
Java, dünya genelinde en popüler üç dil arasında istikrarlı şekilde yer alır. Kurumsal yazılım geliştirme, Android mobil uygulamaları ve büyük ölçekli web uygulamaları gibi alanlardaki benimsenmesi benzersizdir. Güçlü tip sistemi, geniş ekosistemi ve “bir kez yaz, her yerde çalıştır” özelliği, sağlam ve ölçeklenebilir sistemler kurmak için onu özellikle cazip kılar. Bu yazıda, Java’nın nesne yönelimli programlama özelliklerinin geliştiricilerin bu yeteneklerden etkin şekilde yararlanmasına nasıl olanak tanıdığını, doğru kod organizasyonu ve yeniden kullanım yoluyla bakımı kolay ve ölçeklenebilir uygulamalar geliştirmelerini nasıl sağladığını inceleyeceğiz.
Java Kodunu Düzenleme ve Çalıştırma Üzerine Bir Not
Herhangi bir kod yazmaya başlamadan önce biraz kurulum yapalım.
Sözdiziminde olduğu gibi, Java’nın kod organizasyonu konusunda da katı kuralları vardır.
Öncelikle, her public sınıf kendi dosyasında olmalı ve dosya adı sınıf adıyla birebir aynı olup
.javauzantısına sahip olmalıdır. Örneğin, bir Laptop sınıfı yazmak istersem, dosya adı mutlakaLaptop.java—büyük/küçük harf duyarlı olmalıdır. Aynı dosyada public olmayan sınıflar bulunabilir ancak bunları ayırmak en iyisidir. Farkındayım, daha sınıfları yazmadan önce nasıl organize edileceğinden bahsederek önden gidiyoruz; ancak önceden neleri nereye koyacağımıza dair kabaca bir fikir edinmek iyi bir fikirdir.Tüm Java projelerinde
Main.javadosyası ve içinde Main sınıfı bulunur. Sınıflarınızı buradan oluşturup nesneler yaratarak test edersiniz.Java kodunu çalıştırmak için popüler bir Java IDE’si olan IntelliJ IDEA’yı kullanacağız. IntelliJ’i kurduktan sonra:
Main.javadosyasını oluşturun ve aşağıdaki içeriği yapıştırın:Sınıflardan bahsederken, kodu
Main.javadışındaki dosyalara yazarız. Ancak nesneler oluşturma ve test etmeden bahsediyorsakMain.java’ya geçeriz.Programı çalıştırmak için main metodunun yanındaki yeşil oynat düğmesine tıklayabilirsiniz:
Çıktı altta, Run araç penceresinde gösterilecektir.
Java’ya tamamen yeniyseniz, lütfen devam etmeden önce Java veri tipleri ve kontrol akışının temellerini ele alan Introduction to Java kursumuza göz atın.
Aksi halde, doğrudan konuya girelim.
Java Sınıfları ve Nesneleri
Peki, sınıflar tam olarak nedir?
Sınıflar, gerçek dünyadaki kavramları temsil etmek için Java’daki programlama yapılarıdır. Örneğin, şu MenuItem sınıfını düşünün (IDE’nizde bu sınıfı yazmak için bir dosya oluşturun):
Sınıf, bir restorandaki çeşitli menü öğelerini temsil etmek için bize bir plan ya da şablon sunar. Sınıfın iki özelliğini,
name, veprice, değiştirerek burger veya salata gibi sayısız menü nesnesi oluşturabiliriz.Java’da bir sınıf oluşturmak için, sınıfın erişim seviyesini (
private,publicveyaprotected) belirten bir satır yazar, ardından sınıf adını verirsiniz. Süslü parantezlerden hemen sonra, sınıfınızın özniteliklerini tanımlarsınız.Peki, bu sınıfa ait nesneleri nasıl oluştururuz? Java bunu kurucu (constructor) metodlarla sağlar:
Kurucu, bir sınıftan yeni bir nesne oluşturduğumuzda çağrılan özel bir metottur. Nesnenin özniteliklerini sağladığımız değerlerle başlatır. Yukarıdaki örnekte kurucu, name ve price parametrelerini alır ve ‘this’ anahtar kelimesini kullanarak bunları gelecekteki nesne örneğine atar.
Kurucunun sözdizimi diğer sınıf metotlarından farklıdır çünkü dönüş tipi belirtmenizi gerektirmez. Ayrıca, kurucu sınıfla aynı ada sahip olmalı ve sınıf tanımından sonra tanımladığınız özniteliklerle uyumlu olmalıdır. Yukarıda, kurucu iki öznitelik oluşturuyor çünkü sınıf tanımından sonra iki öznitelik ilan ettik:
nameveprice.Sınıfınızı ve kurucusunu yazdıktan sonra, main metodunuzda bu sınıfın örneklerini (nesnelerini) oluşturabilirsiniz:
Çıktı:
Yukarıda, iki
MenuItemnesnesiniburgervesaladdeğişkenlerine atıyoruz. Java’da zorunlu olduğu üzere, değişkenin tipi ilan edilmelidir; burada bu tipMenuItem’dır. Ardından, sınıfımızın bir örneğini oluşturmak için new anahtar kelimesini yazar ve kurucu metodu çağırırız.Kurucuya ek olarak, sınıfınıza davranış kazandıran normal metotlar da oluşturabilirsiniz. Örneğin, aşağıda vergi sonrası toplam fiyatı hesaplayan bir metot ekliyoruz:
Artık vergi dahil toplam fiyatı hesaplayabiliriz:
Çıktı:
Kapsülleme
Sınıfların amacı, nesneler oluşturmak için bir şablon sağlamaktır. Bu nesneler daha sonra diğer betikler veya programlar tarafından kullanılacaktır. Örneğin,
MenuItemnesnelerimiz, adlarını, fiyatlarını ve görsellerini ekranda gösteren bir kullanıcı arayüzü tarafından kullanılabilir.Bu nedenle, sınıflarımızı, örneklerinin yalnızca bizim amaçladığımız şekilde kullanılabileceği biçimde tasarlamalıyız. Şu anda
MenuItemsınıfımız çok temel ve hataya açık. Biri saçma özniteliklerle nesneler oluşturabilir; örneğin negatif fiyatlı elmalı turta veya milyon dolarlık sandviç:Dolayısıyla, bir sınıf yazdıktan sonra yapılacak ilk iş, özniteliklerinin nasıl oluşturulup erişildiğini sınırlayarak onları korumaktır. Başlangıç olarak, price için yalnızca pozitif değerler kabul etmek ve yanlışlıkla aşırı pahalı öğelerin görüntülenmesini önlemek için bir üst sınır belirlemek istiyoruz.
Java bunu setter metotlarıyla yapmamıza izin verir:
Yukarıdaki kod bloğunda nelerin yeni olduğuna bakalım:
1.
privateanahtar kelimesini ekleyerek öznitelikleri private yaptık. Bu, onlara yalnızca MenuItem sınıfı içinde erişilebileceği anlamına gelir. Kapsülleme bu kritik adımla başlar.2. Şu yeni sabiti ekledik:
MAX_PRICE. Bu sabit şunları taşır:3.
setPrice()metodunu ekledik; bu metot:4. Kurucuyu, fiyatı doğrudan atamak yerine
setPrice()kullanacak şekilde değiştirdik. Bu, nesne oluşturma sırasında fiyat doğrulamasının yapılmasını sağlar.Az önce iyi nesne yönelimli tasarımın temel sütunlarından birini uyguladık — kapsülleme. Bu paradigma, veri gizliliğini ve nesne özniteliklerine kontrollü erişimi dayatır; böylece dahili uygulama ayrıntıları dış müdahaleden korunur ve yalnızca iyi tanımlanmış arayüzler aracılığıyla değiştirilebilir.
Kapsüllemeyi name özniteliğine uygulayarak konuyu pekiştirelim. Sadece latte, cappuccino, espresso, americano ve mocha servis eden bir kahve dükkânımız olduğunu hayal edin.
Dolayısıyla menü öğesi adlarımız yalnızca bu listedeki öğelerden biri olabilir. Bunu koda şöyle dökebiliriz:
Yukarıdaki kod, bir kahve dükkânındaki menü öğeleri için ad doğrulamasını uygular. Bunu parçalayalım:
1. Önce, yalnızca izin verilen içecek adlarını içeren private static final bir dizi olan
VALID_NAMEStanımlanır: latte, cappuccino, espresso, americano ve mocha. Bu dizinin özellikleri şunlardır:2. İçecek adını depolamak için private bir String name alanı ilan edilir
3.
setName()metodu doğrulama mantığını uygular:VALID_NAMESdizisi üzerinde döngü kurarİşte şu ana kadarki tam sınıf:
Özniteliklerin nasıl oluşturulduğunu koruduktan sonra, onlara nasıl erişildiğini de korumak isteriz. Bu, getter metotları kullanılarak yapılır:
Getter metotları, bir sınıfın private özniteliklerine kontrollü erişim sağlar. Doğrudan öznitelik erişiminin yol açabileceği istenmeyen değişiklikleri önleyerek kapsüllemenin bozulmasını engellerler.
Örneğin, getter olmadan özniteliklere doğrudan erişebiliriz:
Getter’larla doğru erişimi zorunlu kılarız:
Bu kapsülleme şunları sağlar:
Kalıtım
Sınıfımız iyi görünmeye başladı ama oldukça fazla sorun var. Örneğin, birçok türde yemek ve içecek servis eden büyük bir restoran için bu sınıf yeterince esnek değil.
Farklı türde yiyecek öğeleri eklemek istersek çeşitli zorluklarla karşılaşırız. Bazı yemekler paket servise uygundur, bazıları anında tüketilmelidir. Menü öğelerinin farklı fiyat ve indirimleri olabilir. Yemeklerin sıcaklık takibi veya özel depolama gerektirebilir. İçecekler sıcak ya da soğuk olabilir ve özelleştirilebilir malzemeler içerebilir. Öğeler alerjen bilgisi ve porsiyon seçenekleri gerektirebilir. Mevcut sistem bu farklı gereksinimleri karşılamıyor.
Kalıtım bu sorunların tümüne zarif bir çözüm sunar. Ortak özniteliklere sahip temel bir
MenuItemsınıfı tanımlamamıza ve ardından bu temelleri miras alırken kendine özgü özellikler ekleyen alt sınıflar oluşturmamıza olanak tanır.Örneğin, sıcaklık seçenekleri olan içecekler için bir
Drinksınıfı, anında tüketilmesi gereken öğeler için birFoodsınıfı ve özel depolama gerektiren öğeler için birDessertsınıfımız olabilir — hepsi çekirdek menü öğesi işlevselliğini miras alır.Sınıfları genişletme
Bu fikirleri
Drinkile başlayarak hayata geçirelim:Bir ebeveynden miras alan bir alt sınıf tanımlamak için, alt sınıf adından sonra
extendsanahtar kelimesini ve ardından ebeveyn sınıfı yazarız. Sınıf tanımından sonra, bu alt sınıfın sahip olduğu yeni öznitelikleri tanımlar ve kurucusunu uygularız.Ancak dikkat edin,
namevepricebaşlatmalarınıisColdile birlikte tekrarlamak zorunda kalıyoruz. Bu ideal değildir çünkü ebeveyn sınıfın yüzlerce özniteliği olabilir. Ayrıca, yukarıdaki kod derleme sırasında hata verecektir çünkü ebeveyn sınıfın özniteliklerini başlatmanın doğru yolu bu değildir. Doğru yolsuperanahtar kelimesini kullanmaktır:superanahtar kelimesi, ebeveyn sınıfın kurucusunu çağırmak için kullanılır. Bu durumdasuper(name, price), bu öznitelikleri başlatmak içinMenuItem’ın kurucusunu çağırır ve kod tekrarını önler. YalnızcaDrinksınıfına özgü yeniisColdözniteliğini başlatmamız gerekir.Bu anahtar kelime oldukça esnektir çünkü alt sınıfın herhangi bir kısmında ebeveyn sınıfa atıfta bulunmak için kullanılabilir. Örneğin, bir ebeveyn metodunu çağırmak için
super.methodName(), bir öznitelik için isesuper.attributeNamekullanılır.Metot geçersiz kılma (overriding)
Şimdi, sınıflarımıza vergi sonrası toplam fiyatı hesaplayan yeni bir metot eklemek istediğimizi düşünelim. Farklı menü öğelerinin farklı vergi oranları olabileceğinden (örneğin, hazırlanmış yiyecekler ve paketli içecekler), ebeveyn sınıfta ortak bir metot adı korurken her alt sınıfta belirli vergi hesaplamalarını uygulamak için metot geçersiz kılmayı kullanabiliriz.
Bu şöyle görünür:
Bu örnekte metot geçersiz kılma, her alt sınıfın
calculateTotalPrice()metoduna kendi uygulamasını sağlamasına olanak tanır:Temel
MenuItemsınıfı varsayılan olarak %10 vergi hesabı tanımlar.FoodveDrinksınıflarıMenuItem’ı genişlettiğinde, bu metodu kendi vergi oranlarını uygulamak üzere geçersiz kılar:@Overrideaçıklama satırı, bu metotların ebeveyn sınıf metodunu geçersiz kıldığını açıkça belirtmek için kullanılır. Bu, metot imzası ebeveyn sınıfla eşleşmezse hataları yakalamaya yardımcı olur.Her alt sınıf yine de ebeveyn sınıfın fiyatına
super.getPrice()ile erişebilir; bu da geçersiz kılınan metotların, kendi davranışlarını eklerken ebeveyn sınıf işlevselliğinden nasıl yararlanabileceğini gösterir.Kısacası, metot geçersiz kılma, alt sınıfların ebeveyn sınıfta tanımlanan metotlara kendi uygulamalarını sağlamasına olanak tanıyan ve aynı metot imzasını koruyan kalıtımın ayrılmaz bir parçasıdır.
Soyut Sınıflar
MenuItemsınıf hiyerarşimiz çalışıyor, ancak bir sorun var: herkes düz birMenuItemnesnesi oluşturabilmeli mi? Sonuçta restoranımızda her menü öğesi ya Food ya da Drink’tir — “genel bir menü öğesi” diye bir şey yok.Bunu,
MenuItem’ı soyut bir sınıf yaparak önleyebiliriz. Soyut bir sınıf yalnızca bir temel şablon sunar — yalnızca kalıtım için ebeveyn sınıf olarak kullanılabilir, doğrudan örneklenemez.MenuItem’ı soyut yapmak için erişim belirleyicisinden sonraabstractanahtar kelimesini ekleriz:Soyut sınıflar, yukarıdaki
calculateTotalPrice()gibi soyut metotlar da içerebilir. Bu soyut metotlar, alt sınıfları kendi uygulamalarını sağlamaya zorlayan bir sözleşme işlevi görür. Başka bir deyişle, soyut bir sınıftaki her soyut metot, alt sınıflar tarafından uygulanmalıdır.Öyleyse, bu değişiklikleri dikkate alarak
FoodveDrinksınıflarını yeniden yazalım:Bu menü sistemi uygulamasıyla, soyutlama ve kalıtımın farklı iş gereksinimlerine kolayca uyum sağlayabilen esnek ve bakımı kolay kod oluşturmak için nasıl birlikte çalıştığını gördük.
Sonuç
Bugün, Java’nın bir nesne yönelimli programlama dili olarak neler yapabildiğine kısa bir bakış attık. Bir restoran menü sistemi üzerinden sınıfların, nesnelerin ve OOP’nin birkaç temel sütununun: kapsülleme, kalıtım ve soyutlamanın temellerini ele aldık.
Bu sistemi üretime hazır hale getirmek için hâlâ arayüzler (soyutlamanın bir parçası), çok biçimlilik (polymorphism) ve OOP tasarım kalıpları gibi öğrenecek pek çok şey var. Bu kavramlar hakkında daha fazla bilgi edinmek için Introduction to OOP in Java kursumuza bakın.
Java bilginizi test etmek isterseniz, Java Mülakat Soruları makalemizde yer alan bazı soruları yanıtlamayı deneyin.