۲۹ شهریور ۱۴۰۳

Techboy

اخبار و اطلاعات روز تکنولوژی

وراثت جاوا در مقابل ترکیب: نحوه انتخاب

وراثت و ترکیب، دو روش اساسی برای ارتباط کلاس‌های جاوا را مقایسه کنید، سپس اشکال‌زدایی ClassCastExceptions را در وراثت جاوا تمرین کنید.

وراثت و ترکیب، دو روش اساسی برای ارتباط کلاس‌های جاوا را مقایسه کنید، سپس اشکال‌زدایی ClassCastExceptions را در وراثت جاوا تمرین کنید.

وراثت و ترکیب دو تکنیک برنامه نویسی هستند که توسعه دهندگان برای ایجاد رابطه بین کلاس ها و اشیاء استفاده می کنند. در حالی که وراثت یک کلاس را از کلاس دیگر مشتق می کند، ترکیب یک کلاس را به عنوان مجموع اجزای آن تعریف می کند.

کلاس‌ها و اشیاء ایجاد شده از طریق وراثت هستند همبستگی محکم زیرا تغییر والد یا سوپرکلاس در یک رابطه ارثی خطر شکستن کد شما را به همراه دارد. کلاس‌ها و اشیاء ایجاد شده از طریق ترکیب به‌طور آزاد با هم مرتبط هستند، به این معنی که می‌توانید به راحتی اجزای سازنده را بدون شکستن کد خود تغییر دهید.

از آنجایی که کدهای جفت آزاد انعطاف پذیری بیشتری ارائه می دهند، بسیاری از توسعه دهندگان بر این باورند که ترکیب تکنیکی بهتر از وراثت است، اما حقیقت پیچیده تر است. انتخاب یک ابزار برنامه نویسی شبیه به انتخاب ابزار آشپزخانه مناسب است: شما از چاقوی کره برای برش سبزیجات استفاده نمی کنید و به همین ترتیب، نباید ترکیب را برای هر سناریو برنامه نویسی انتخاب کنید. 

وارث و ترکیب در جاوا

در این مقاله، تفاوت‌های بین وراثت و ترکیب و نحوه تصمیم‌گیری صحیح برای برنامه‌های جاوا را خواهید آموخت:

  • زمان استفاده از وراثت در جاوا
  • زمان استفاده از ترکیب در جاوا
  • تفاوت های بین وراثت و ترکیب
  • رویکردن روش با وراثت جاوا
  • استفاده از سازنده با وراثت
  • casting و ClassCastException را تایپ کنید
  • اشتباهات رایج وراثت در جاوا
  • چه چیزهایی را در مورد وراثت در جاوا به خاطر بسپارید

زمان استفاده از وراثت در جاوا

در برنامه نویسی شی گرا، زمانی می توانیم از وراثت استفاده کنیم که بدانیم بین یک فرزند و کلاس والد آن رابطه “is a” وجود دارد. برخی از نمونه ها عبارتند از:

  • یک فرد یک انسان است.
  • گربه یک حیوان است.
  • خودرو یک وسیله نقلیه است.

در هر مورد، فرزند یا زیر کلاس یک نسخه تخصصی از والد یا سوپرکلاس است. به ارث بردن از superclass نمونه ای از استفاده مجدد از کد است. برای درک بهتر این رابطه، کمی وقت بگذارید و کلاس Car را مطالعه کنید، که از Vehicle به ارث می رسد:


class Vehicle {

    String brand;
    String color;
    double weight;
    double speed;

    void move() {
        System.out.println("The vehicle is moving");
    }
}

public class Car extends Vehicle {
    String licensePlateNumber;
    String owner;
    String bodyStyle;

    public static void main(String... inheritanceExample) {
        System.out.println(new Vehicle().brand);
        System.out.println(new Car().brand);
        new Car().move();
    }
}

هنگامی که قصد استفاده از وراثت را دارید، از خود بپرسید که آیا زیر کلاس واقعاً نسخه تخصصی تری از سوپرکلاس است یا خیر. در این صورت خودرو یک نوع وسیله نقلیه است، بنابراین رابطه ارث معنا پیدا می کند. 

زمان استفاده از ترکیب در جاوا

در برنامه نویسی شی گرا، می توانیم از ترکیب در مواردی استفاده کنیم که یک شی “دارای” (یا بخشی از) شی دیگری است. برخی از نمونه ها عبارتند از:

  • یک ماشین دارای باتری است (باتری بخشی از خودرو است).
  • یک فرد یک قلب دارد  (قلب بخشی از یک شخص است).
  • یک خانه دارای یک اتاق نشیمن است (اتاق نشیمن بخشی از خانه است).

برای درک بهتر این نوع رابطه، ترکیب یک خانه را در نظر بگیرید:


public class CompositionExample {

    public static void main(String... houseComposition) {
        new House(new Bedroom(), new LivingRoom());
        // The house now is composed with a Bedroom and a LivingRoom
    }

    static class House {

        Bedroom bedroom;
        LivingRoom livingRoom;

        House(Bedroom bedroom, LivingRoom livingRoom) {
            this.bedroom = bedroom;
            this.livingRoom = livingRoom;
        }
    }
    static class Bedroom { }
    static class LivingRoom { }
}

در این مورد، ما می دانیم که یک خانه دارای یک اتاق نشیمن و یک اتاق خواب است، بنابراین می توانیم از اشیاء Bedroom و  LivingRoom در ترکیب یک استفاده کنیم. >خانه

کد را دریافت کنید

کد منبع را برای مثال در این مقاله دریافت کنید. می‌توانید همزمان با دنبال کردن مثال‌ها، آزمایش‌های خود را اجرا کنید.

تفاوت بین وراثت و ترکیب

اکنون اجازه دهید وراثت و ترکیب را در جاوا مقایسه کنیم. آیا موارد زیر نمونه خوبی از ارث است؟


import java.util.HashSet;

public class CharacterBadExampleInheritance extends HashSet<Object> {

    public static void main(String... badExampleOfInheritance) {
        BadExampleInheritance badExampleInheritance = new BadExampleInheritance();
        badExampleInheritance.add("Homer");
        badExampleInheritance.forEach(System.out::println);
    }

در این مورد، پاسخ منفی است. کلاس فرزند متدهای زیادی را به ارث می برد که هرگز از آنها استفاده نخواهد کرد و در نتیجه کدهایی به هم پیوسته است که گیج کننده و نگهداری آن دشوار است. اگر دقت کنید، همچنین مشخص می‌شود که این کد تست «است» را قبول نمی‌کند.

اکنون بیایید همان مثال را با استفاده از ترکیب امتحان کنیم:


import java.util.HashSet;
import java.util.Set;

public class CharacterCompositionExample {
    static Set<String> set = new HashSet<>();

    public static void main(String... goodExampleOfComposition) {
        set.add("Homer");
        set.forEach(System.out::println);
    }

استفاده از ترکیب برای این سناریو به کلاس  CharacterCompositionExample اجازه می‌دهد فقط از دو روش از HashSet استفاده کند، بدون اینکه همه آنها را به ارث ببرد. در نهایت کد ساده‌تر و کم‌تر جفت‌شده‌ای داریم که درک و نگهداری آن آسان‌تر است.

روی کردن روش با وراثت جاوا

وراثت به ما اجازه می دهد تا از متدها و سایر ویژگی های یک کلاس در یک کلاس جدید استفاده مجدد کنیم که بسیار راحت است. اما برای اینکه وراثت واقعاً کار کند، ما همچنین باید بتوانیم برخی از رفتارهای ارثی را در زیر کلاس جدید خود تغییر دهیم. برای مثال، ممکن است بخواهیم صدایی را که Cat تولید می کند، تخصصی کنیم:


class Animal {
    void emitSound() {
        System.out.println("The animal emitted a sound");
    }
}
class Cat extends Animal {
    @Override
    void emitSound() {
        System.out.println("Meow");
    }
}
class Dog extends Animal {
}

public class Main {
    public static void main(String... doYourBest) {
        Animal cat = new Cat(); // Meow
        Animal dog = new Dog(); // The animal emitted a sound
        Animal animal = new Animal(); // The animal emitted a sound
        cat.emitSound();
        dog.emitSound();
        animal.emitSound();
    }
}

این نمونه ای از وراثت جاوا با نادیده گرفتن متد است. ابتدا، کلاس Animal را توسعه می کنیم تا یک کلاس Cat جدید ایجاد کنیم. در مرحله بعد، روش emitSound() کلاس Animal را لغو می‌کنیم تا صدای خاصی را که Cat می‌سازد به دست آوریم. حتی اگر نوع کلاس را به عنوان Animal اعلام کرده‌ایم، وقتی آن را به‌عنوان Cat نمونه‌سازی می‌کنیم، میو گربه را دریافت می‌کنیم. 

اغلب روش چندشکلی است

شایان ذکر است که نسخ روش نمونه ای از چندشکلی است< /a> یا فراخوانی روش مجازی.

آیا جاوا چندین وراثت دارد؟

برخلاف برخی از زبان‌ها، مانند C++، جاوا اجازه وراثت چندگانه با کلاس‌ها را نمی‌دهد. با این حال، می توانید از ارث بری چندگانه با رابط ها استفاده کنید. تفاوت بین یک کلاس و یک رابط، در این مورد، این است که رابط ها حالت را حفظ نمی کنند.

اگر سعی کنید چندین وراثت را مانند من در زیر انجام دهید، کد کامپایل نمی شود:


class Animal {}
class Mammal {}
class Dog extends Animal, Mammal {}

یک راه حل با استفاده از کلاس ها، ارث بردن یک به یک است:


class Animal {}
class Mammal extends Animal {}
class Dog extends Mammal {}

یک راه حل دیگر جایگزینی کلاس ها با رابط ها است:


interface Animal {}
interface Mammal {}
class Dog implements Animal, Mammal {}

استفاده از “super” برای دسترسی به متدهای کلاس والد

وقتی دو کلاس از طریق وراثت به هم مرتبط هستند، کلاس فرزند باید بتواند به هر فیلد، متد یا سازنده کلاس والد قابل دسترسی دسترسی داشته باشد. در جاوا، از کلمه رزرو شده super استفاده می‌کنیم تا اطمینان حاصل کنیم که کلاس فرزند همچنان می‌تواند به روش لغو شده والد خود دسترسی داشته باشد:


public class SuperWordExample {
    class Character {
        Character() {
            System.out.println("A Character has been created");
        }
        void move() {
            System.out.println("Character walking...");
        }
    }
    class Moe extends Character {
        Moe() {
            super();
        }
        void giveBeer() {
            super.move();
            System.out.println("Give beer");
        }
    }
}

در این مثال، Character کلاس والد Moe است. با استفاده از super، می‌توانیم به روش move() Character برای دادن آبجو به Moe دسترسی داشته باشیم.

استفاده از سازنده با وراثت

وقتی یک کلاس از کلاس دیگر ارث می برد، سازنده superclass ابتدا قبل از بارگیری زیر کلاس آن بارگذاری می شود. در بیشتر موارد، کلمه رزرو شده super به طور خودکار به سازنده اضافه می شود. با این حال، اگر superclass یک پارامتر در سازنده خود داشته باشد، باید عمداً سازنده super را فراخوانی کنیم، همانطور که در زیر نشان داده شده است:


public class ConstructorSuper {
    class Character {
        Character() {
            System.out.println("The super constructor was invoked");
        }
    }
    class Barney extends Character {
        // No need to declare the constructor or to invoke the super constructor
        // The JVM will to that
    }
}

اگر کلاس والد سازنده ای با حداقل یک پارامتر داشته باشد، باید سازنده را در زیر کلاس اعلام کنیم و از super برای فراخوانی صریح سازنده والد استفاده کنیم. کلمه رزرو شده super به طور خودکار اضافه نمی شود و کد بدون آن کامپایل نمی شود. به عنوان مثال:


public class CustomizedConstructorSuper {
    class Character {
        Character(String name) {
            System.out.println(name + "was invoked");
        }
    }
    class Barney extends Character {
        // We will have compilation error if we don't invoke the constructor explicitly
        // We need to add it
        Barney() {
            super("Barney Gumble");
        }
    }
}

Casting و ClassCastExceptions را تایپ کنید

Casting راهی برای برقراری ارتباط صریح با کامپایلر است که شما واقعاً قصد دارید یک نوع مشخص را تبدیل کنید. مانند این است که بگویید: “هی، JVM، من می دانم که دارم چه کار می کنم، پس لطفاً این کلاس را با این نوع انتخاب کنید.” اگر کلاسی که شما ارسال کرده اید با نوع کلاسی که اعلام کرده اید سازگار نباشد، یک ClassCastException دریافت خواهید کرد.

در وراثت، می‌توانیم کلاس فرزند را بدون ارسال محتوا به کلاس والد اختصاص دهیم، اما نمی‌توانیم بدون استفاده از ارسال محتوا، کلاس والد را به کلاس فرزند اختصاص دهیم.

مثال زیر را در نظر بگیرید:


public class CastingExample {
    public static void main(String... castingExample) {
        Animal animal = new Animal();
        Dog dogAnimal = (Dog) animal; // We will get ClassCastException
        Dog dog = new Dog();
        Animal dogWithAnimalType = new Dog();
        Dog specificDog = (Dog) dogWithAnimalType;
        specificDog.bark();
        Animal anotherDog = dog; // It's fine here, no need for casting
        System.out.println(((Dog)anotherDog)); // This is another way to cast the object
    }
}
class Animal { }
class Dog extends Animal { void bark() { System.out.println("Au au"); } }

وقتی می‌خواهیم یک نمونه حیوان را به سگ ارسال کنیم، یک استثنا دریافت می‌کنیم. این به این دلیل است که حیوان چیزی در مورد فرزند خود نمی داند. این می تواند یک گربه، یک پرنده، یک مارمولک و غیره باشد. هیچ اطلاعاتی در مورد حیوان خاص وجود ندارد. 

مشکل در این مورد این است که ما Animal را به این صورت نمونه‌سازی کرده‌ایم:


Animal animal = new Animal();

سپس سعی کرد آن را به این شکل پخش کند:


Dog dogAnimal = (Dog) animal;

از آنجایی که ما نمونه سگ نداریم، نمی‌توان حیوان را به سگ اختصاص داد. اگر تلاش کنیم، یک ClassCastException دریافت خواهیم کرد.

برای اجتناب از استثناء، باید سگ را به این صورت نمونه سازی کنیم:


Dog dog = new Dog();

سپس آن را به حیوان اختصاص دهید:


Animal anotherDog = dog;

در این مورد، چون  کلاس Animal را گسترش داده‌ایم، نمونه Dog حتی نیازی به نمایش داده نمی‌شود. نوع کلاس والد Animal به سادگی تخصیص را می پذیرد.

بازیگری با سوپرتایپ

می‌توان سگ را با سوپرتایپ حیوان اعلام کرد، اما اگر بخواهیم روش خاصی را از Dog فراخوانی کنیم، باید آن را ریخته شود. به عنوان مثال، اگر بخواهیم متد bark() را فراخوانی کنیم، چه؟  سوپرتایپ Animal هیچ راهی برای دانستن اینکه دقیقاً چه نمونه حیوانی را فرا می‌خوانیم ندارد، بنابراین قبل از اینکه بتوانیم bark()bark را فراخوانی کنیم، باید Dog را به صورت دستی ارسال کنیم. روش /code>:


Animal dogWithAnimalType = new Dog();
Dog specificDog = (Dog) dogWithAnimalType;
specificDog.bark();

همچنین می‌توانید از Casting بدون اختصاص دادن شی به یک نوع کلاس استفاده کنید. این روش زمانی مفید است که نمی خواهید متغیر دیگری را اعلام کنید:


System.out.println(((Dog)anotherDog)); // This is another way to cast the object

چالش وراثت جاوا را قبول کنید!

شما مفاهیم مهم وراثت را یاد گرفته‌اید، بنابراین اکنون وقت آن است که چالش ارث را امتحان کنید. برای شروع، کد زیر را مطالعه کنید:


public class InheritanceCompositionChallenge {
    private static int result;
    public static void main(String... doYourBest) {
        Character homer = new Homer();
        homer.drink();
        new Character().drink();
        ((Homer)homer).strangleBart();
        Character character = new Character();
        System.out.println(result);
        ((Homer)character).strangleBart();
    }
    static class Character {
        Character() {
            result++;
        }
        void drink() {
            System.out.println("Drink");
        }
    }
    static class Homer extends Character {
        Lung lung = new Lung();
        void strangleBart() {
            System.out.println("Why you little!");
        }
        void drink() {
            System.out.println("Drink beer");
            lung.damageLungs();
        }
    }
    static class Lung {
        void damageLungs() {
            System.out.println("Soon you will need a transplant");
        }
    }
}

کدام یک از اینها خروجی پس از اجرای روش اصلی است؟

A)


Drink
Drink
Why you little!
۲
Exception in thread "main" java.lang.ClassCastException:....

B)


Drink beer
Soon you will need a transplant
Drink
Why you little!
Exception in thread "main" java.lang.ClassCastException:....

C)


Drink beer
Soon you will need a transplant
Drink
Why you little!
۳
Exception in thread "main" java.lang.ClassCastException:....

D)


Drink beer
Soon you will need a transplant
Drink
Why you little!
۲
Why you little!

حل چالش وراثت جاوا

خروجی صحیح از چالش وراثت C:

است


Drink beer
Soon you will need a transplant
Drink
Why you little!
۳
Exception in thread "main" java.lang.ClassCastException:....

برای درک دلیل، کافی است کد را از بالا شروع کنید:


Character homer = new Homer();
 homer.drink();

از آنجایی که ما شی را با هومر نمونه‌سازی می‌کنیم، پیاده‌سازی روش هومر اجرا می‌شود و خروجی زیر را تولید می‌کند:


Drink beer
Soon you will need a transplant

سپس، متد drink() را مستقیماً از کلاس Character فراخوانی می‌کنیم:


new Character().drink();

که خروجی زیر را به ما می دهد:


Drink beer

در این خط، ما از ریخته گری استفاده می کنیم و روش strangleBart() را به طور معمول فراخوانی می کنیم:


 ((Homer)homer).strangleBart();

سپس خروجی حاصل را می‌خواهیم:


System.out.println(result);

از آنجایی که می دانیم سازنده super همیشه باید فراخوانی شود، فقط باید تعداد دفعات شخصیت یا Homer را بشماریم. نمونه سازی شد. اکنون می دانیم که خروجی ۳ خواهد بود. 

در نهایت، ما سعی می‌کنیم روشی را از ریخته‌گری که استفاده نادرست است، فراخوانی کنیم:


((Homer) character).strangleBart();

ما سعی کردیم نوع کلاس و اجرای شخصیت را تبدیل کنیم، بنابراین یک ClassCastException پرتاب می‌شود. (این به این دلیل است که هیچ اطلاعات Homer در کلاس Character وجود ندارد.)

چالش ویدیویی! اشکال زدایی وراثت جاوا

اشکال‌زدایی یکی از بهترین راه‌ها برای جذب کامل مفاهیم برنامه‌نویسی و در عین حال بهبود کد شماست. در این ویدیو، می‌توانید در حین رفع اشکال و توضیح چالش وراثت جاوا، همراه باشید.

اشتباهات رایج در وراثت جاوا

چه چیزی را در مورد وراثت جاوا به خاطر بسپارید

  • هنگام نمونه سازی یک سوپرکلاس با زیر کلاس، نمونه سازی زیر کلاس در نظر گرفته می شود.
  • برای تصمیم گیری در مورد استفاده از وراثت، از خود بپرسید که آیا کلاس فرزند واقعاً یک نوع تخصصی از والدین است (آزمون "است").
  • اگر خودتان آن را اعلام نکنید، کلمه رزرو شده super به طور خودکار در سازنده شما اضافه می شود.
  • اگر نوع کلاس والد و نمونه زیر کلاس را برای این نوع دارید، می توانید آن را به زیر کلاس ارسال کنید.
  • اگر superclass سازنده ای دارد که حداقل یک پارامتر دریافت می کند، باید این سازنده super را در کلاس فرعی فراخوانی کنید که پارامتر مورد نیاز را ارسال می کند.

درباره وراثت جاوا بیشتر بیاموزید