دانستنی‌ها

هوشمندی در پیاده‌سازی Generic‌ها با Bridge Method

داده‌های عام یا Generic‌ها یکی از ویژگی‌هایی هستند که از ابتدا در زبان جاوا پیش‌بینی نشده بود. پیاده‌سازی آن در میانۀ راه به کمک Type Erasure با مشکلاتی همراه بود که یکی از آن‌ها به کمک ایدۀ Bridge Method حل شده است.

در طراحی و به‌روز‌رسانی‌ جاوا، همیشه در نظر گرفته می‌شود که بایت کد (Byte Code) کامپایل شده با JDK‌های پایین‌تر، با JDK جدید نیز قابل اجرا باشد.

این مسئله که از آن با نام Backward Compatibility یاد می‌شود، یکی از نقاط قوت جاوا نیز به حساب می‌آید. حمایت از کد‌های تولید‌شده با نسخه‌های قدیمی‌تر چندان بی‌هزینه هم نیست؛ چرا که افزودن ویژگی‌های جدید به زبان، رفع مشکلات نسخه‌های قدیمی‌تر و بهینه‌سازی‌ها را سخت‌تر می‌کند.

یکی از تغییرات بزرگ جاوا در نسخۀ 5 اضافه شدن Generic‌ها بوده است. توسعه‌دهندگان هنگام پیاده‌سازی این ویژگی، ناچار بودند از روشی به نام Type Erasure استفاده کنند تا بتوانند JDK 5 را با نسخۀ قدیمی‌تر سازگار نگه‌دارند. برای مرور مفاهیم مربوط به داده‌های نوع عام (Generic Type) می‌توانید به اسلاید و ویدیوهای آموزشی جاوا‌کاپ مراجعه کنید. در ادامه، مثال ساده‌ای از عملکرد Type Erasure را مرور کرده و بعد به ایدۀ جالبی که در این طراحی به کار گرفته شده، خواهیم پرداخت.

می دانید جاوا یک زبانِ به اصطلاح Strongly Typed است. یعنی:

اولا: نوع متغیر‌های یک برنامه پیش از استفاده تعریف می‌شود.
ثانیا: همیشه کنترل می‌شود داده‌ای که در متغیر‌ها ریخته می‌شود، با نوع آن‌ها سازگار باشد.

گرامر تعریف یک متغیر عام دارای یک بخش به نام «پارامتر نوع» (Type Parameter) است که داخل کاراکتر‌های <> قرار می‌گیرد. به عنوان مثال اگر بخواهیم نوعِ عامِ Stack را تعریف کنیم، این کار را به شکل زیر انجام می دهیم:

class Stack<E> {
    // Generic Type Definition Goes Here
}

پس از تعریف «کلاس» فوق می‌توانیم متغیر‌هایی از این قسم را ساخته و مورد استفاده قرار دهیم:

Stack<Plate> plateStack = new Stack<Plate>();

یا:

Stack<Paper> paperStack = new Stack<Paper>();
Stack<Plate> Stack<Paper>

Type Erasure

به صورت خلاصه، Type Erasure به این معنی است که محدودیت‌های نوع در زمان کامپایل اعمال شده و در زمان اجرا کنترل نمی‌شوند. به عبارت دیگر، در زمان اجرا محدودیت ذکر‌شده در بخشِ نوع، در دسترس نیست. به این مثال دقت کنید:

public class Stack<E> {
    private E[] stackContent;
 
    public Stack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }
 
    public void push(E data) {
        // ..
    }
 
    public E pop() {
        // ..
    }
}

با کامپایل کد فوق و اعمال Type Erasure، نوع E با Object جای‌گذاری شده و کد زیر تولید و آماده اجرا می‌شود:

public class Stack {
    private Object[] stackContent;
 
    public Stack(int capacity) {
        this.stackContent = (Object[]) new Object[capacity];
    }
 
    public void push(Object data) {//<<
        // ..
    }
 
    public Object pop() {
        // ..
    }
}

مشکلات ناشی از Type Erasure

خب، تا این جای کار به نظر خوب می‌آید. اما گاهی در فرایند Type Erasure متد‌هایی به وجود می‌آید که وجود آن‌ها چندان معقول نیست! اجازه دهید مسئله را با یک مثال نشان دهیم. تصور کنید می‌خواهیم کلاس فوق را برای Integer توسعه دهیم و یک زیر-نوع از آن بسازیم:

public class IntegerStack extends Stack<Integer> {
 
    public IntegerStack(int capacity) {
        super(capacity);
    }
 
    public void push(Integer value) {
        super.push(value);
    }
}

دقت کنید، کلاس IntegerStack متدِ (puch(Object data را به ارث برده و متدِ (puch(Integer value را تعریف کرده است؛ در نتیجه کلاس IntegerStack  (در عمل) حاوی متد‌های زیر خواهد بود:

public class IntegerStack extends Stack<Integer> {
//Other Methods and Constructors
 
	public void push(Object value) {//<<Inherited
        //
    }
	public void push(Integer value) {//New Defined
        //
    }
}

ولی به نظر می‌آید این مسئله مشکل‌ساز باشد. دقت کنید:

IntegerStack integerStack = new IntegerStack(5);
Stack stack = integerStack;
stack.push("Hello");//This is Source of Problem
//Now it’s time to convert “hello” to Integer!
Integer data = integerStack.pop();//ClassCastException!!!

در تکه کد فوق، از متدِ به ارث رسیده استفاده کردیم و یک شیء با نوع دادۀ ناسازگار، به لیستِ دارائی Stack اضافه کردیم. عملکرد درست این است که با استفاده از متد‌های push کلاسِ IntegerStack فقط بتوان دادۀ از نوع Integer به آن اضافه کرد؛ چرا که IntegerStack، کلاسی از نوع زیر را به ارث برده است. طبیعی است در تکه کد فوق، تلاش برای pop کردن یک String و قرار دادن آن در یک متغیر Integer باعث پرتاب ClassCastException می‌شود.

Stack<Integer>

Bridge Method

به نظر شما برای حل این مسئله چه باید کرد؟

برای این که Type Erasure باعث به وجود آمدن مشکلات این چنینی نشود، از تدبیری به نام Bridge Method استفاده شد. ایده به زبان ساده این است: عملکردِ متدِ به ارث رسیده و متدِ تعریف شده را به هم گره بزنیم تا متدِ به ارث رسیده نتواند به اشتباه مورد استفاده قرار گیرد. به این منظور، از متدِ به ارث رسیده می خواهند برای انجام کار، متدِ تعریف شده در کلاس جدید را فراخوانی کند:

public class IntegerStack extends Stack {

    // Bridge method generated by the compiler
    public void push(Object value) {
        push((Integer)value);
    }
 
    public void push(Integer value) {
        super.push(value);
    }
}

در حالت فوق متد push انجام کار را به متدِ تازه تعریف شده واگذار[1] می کند. در نتیجه عملکرد معقولی را شاهد خواهیم بود.

جمع‌بندی: هنگامی که یک کلاس یا واسطِ زیرنوع از یک کلاس یا واسطِ نوع‌عام را کامپایل می‌کنیم، ممکن است کامپایلر در روال Type Erasure متد جدیدی به عنوان Bridge Method بسازد. این کار به صورت اتوماتیک انجام می‌شود در نتیجه نیازی نیست برنامه‌نویس نگران آن باشد. اما ممکن است هنگام پرتاب Exception و مواجهه با Stack Trace باعث سردرگمی گردد. در این مواقع آگاهی از آنچه پشت پرده اتفاق می‌افتد برای برنامه‌نویس هم مفید خواهد بود.

[1]  Delegate

منابع

Oracle.com

Baeldung.com

docs.oracle.com

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا