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

Techboy

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

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

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

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

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

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

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

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

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

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

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

در هر مورد، فرزند یا زیر کلاس یک نسخه تخصصی از والد یا سوپرکلاس است. ارث بردن از سوپرکلاس نمونه ای از استفاده مجدد از کد است. برای درک بهتر این رابطه، کمی وقت بگذارید و کلاس 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();
    }
}

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

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

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

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

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


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);
    }

در این مورد، پاسخ منفی است. کلاس فرزند متدهای زیادی را به ارث می برد که هرگز از آنها استفاده نخواهد کرد، و در نتیجه کدهایی با هم جفت شده است که هم گیج کننده و هم نگهداری آن دشوار است. اگر به دقت نگاه کنید، همچنین مشخص می شود که این کد تست “is a” را قبول نمی کند.

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


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 استفاده کند، بدون اینکه همه آنها را به ارث ببرد. این منجر به کد ساده‌تر و کم‌تر همراه می‌شود که درک و نگهداری آن آسان‌تر خواهد بود.

نمونه های ارث بری در JDK

کیت توسعه جاوا پر از نمونه های خوب وراثت است:


class IndexOutOfBoundsException extends RuntimeException {...}
class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {...}
class FileWriter extends OutputStreamWriter {...}
class OutputStreamWriter extends Writer {...}
interface Stream<T> extends BaseStream<T, Stream<T>> {...}

توجه کنید که در هر یک از این مثال‌ها، کلاس فرزند یک نسخه تخصصی از والد آن است. برای مثال، IndexOutOfBoundsException نوعی RuntimeException است.

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

وراثت به ما امکان می دهد تا از متدها و سایر ویژگی های یک کلاس در یک کلاس جدید استفاده مجدد کنیم که بسیار راحت است.  اما برای اینکه وراثت واقعا کار کند، ما همچنین باید بتوانیم برخی از رفتارهای ارثی را در زیر کلاس جدید خود تغییر دهیم.  برای مثال، ممکن است بخواهیم صدایی را که 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 نمونه‌سازی می‌کنیم، میو گربه را دریافت می‌کنیم. 

با Sentry برنامه های Jetpack Compose بهتر بسازید

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

شاید از آخرین مقاله من به یاد داشته باشید که غیر از روش نمونه ای از چندشکلی یا فراخوانی روش مجازی است.

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

برخلاف برخی از زبان‌ها، مانند 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 به طور خودکار به سازنده اضافه می شود.  با این حال، اگر سوپرکلاس یک پارامتر در سازنده خود داشته باشد، باید به طور عمدی سازنده 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 و ClassCastException را تایپ کنید

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

Mastodon، Steampipe و RSS

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

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


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

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

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