CI/CD چیست؟ راهاندازی پایپلاین با GitLab CI و GitHub Actions
CI/CD یعنی خودکار کردن ساخت، تست و انتشار نرمافزار تا هر تغییر کوچک سریع و بیخطر بررسی و منتشر شود. …
وراثت در جاوا یعنی یک کلاس با کلمه extends ویژگیها و رفتار کلاس دیگری را به ارث ببرد و فقط تفاوتها را خودش بنویسد. چندریختی یعنی یک متغیر از نوع پدر بتواند به شیء هر فرزند اشاره کند و متد درست در زمان اجرا انتخاب شود. در این آموزش هر دو مفهوم را با یک مثال فروشگاهی یاد میگیرید؛ همه کدها را با JDK 25 اجرا کردهایم.
آخرین بهروزرسانی: ۲۱ آبان ۱۴۰۵ — منابع بررسیشده در این روز: آموزشهای رسمی اوراکل درباره وراثت، Override، super و چندریختی، فصل ۸ مشخصات زبان جاوا ۲۵، مستندات کلاس Object، JEP 513، JEP 394، JEP 409 و JEP 441، صفحه پروژه JDK 27 و نقشه پشتیبانی جاوا.
⚡ پاسخ کوتاه
در جاوا کلاس فرزند با extends از کلاس پدر ارث میبرد، با super سازنده یا متد پدر را صدا میزند و با @Override رفتار متد پدر را بازنویسی میکند. هر کلاس فقط یک پدر دارد ولی میتواند چند اینترفیس پیادهسازی کند. چندریختی یعنی وقتی متغیری از نوع پدر به شیء فرزند اشاره میکند، نسخه فرزندِ متد اجرا شود. از جاوا ۲۵ هم میشود پیش از super ورودی را بررسی کرد.
📑 فهرست مطالب
فرض کنید برای یک فروشگاه اینترنتی برنامه مینویسید. محصولات فیزیکی مثل دفترچه، هزینه ارسال دارند. محصولات دیجیتال مثل کتاب الکترونیکی، لینک دانلود دارند. اما همه آنها چیزهای مشترکی هم دارند: اسم، قیمت و یک توضیح کوتاه. اگر برای هر نوع محصول یک کلاس کاملاً جدا بنویسید، همان فیلدها و متدها چند بار تکرار میشوند و هر تغییر کوچک باید در چند جا انجام شود. وراثت (Inheritance) راهحل جاوا برای این مشکل است. یک کلاس کلی مثل Product مینویسید که ویژگیهای مشترک را دارد، و کلاسهای خاصتر مثل PhysicalProduct و DigitalProduct از آن ارث میبرند. کلاس فرزند همه فیلدها و متدهای قابلدسترس پدر را بهطور خودکار دارد و فقط چیزهای تازه یا متفاوت را خودش اضافه میکند. آموزش رسمی اوراکل درباره وراثت همین ایده را با اصطلاح کلاس پدر (superclass) و کلاس فرزند (subclass) توضیح میدهد. در این آموزش وراثت، extends، super، Override و چندریختی را با مثالهایی یاد میگیرید که همه را با JDK 25 کامپایل و اجرا کردهایم. اگر هنوز با کلاس، شیء و سازنده آشنا نیستید، اول آموزش کلاس و شیء در جاوا را بخوانید؛ این مقاله دقیقاً از جایی شروع میکند که آن مقاله تمام شد.
قبل از استفاده از وراثت، این سؤال را بپرسید: آیا کلاس فرزند واقعاً «یک نوع» از کلاس پدر است؟ محصول دیجیتال «یک محصول است»؛ پس وراثت منطقی است. اما سبد خرید «یک محصول نیست»، بلکه «محصول دارد». برای رابطه «دارد» (has-a) باید از ترکیب استفاده کنید، یعنی شیء را بهعنوان فیلد نگه دارید، نه اینکه از آن ارث ببرید. این آزمون ساده جلوی بیشتر طراحیهای اشتباه را میگیرد.

در جاوا کلاس فرزند با کلمه کلیدی extends ساخته میشود. بیایید کلاس پدر Product را بنویسیم:
class Product {
protected String name;
protected int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
int finalPrice() {
return price;
}
String describe() {
return name + ": " + finalPrice() + " toman";
}
}
حالا یک کلاس فرزند برای محصول فیزیکی که هزینه ارسال هم دارد:
class PhysicalProduct extends Product {
private int shippingCost;
PhysicalProduct(String name, int price, int shippingCost) {
super(name, price);
this.shippingCost = shippingCost;
}
@Override
int finalPrice() {
return price + shippingCost;
}
}
PhysicalProduct متد describe را ننوشته، اما آن را از Product به ارث برده است. فقط فیلد shippingCost را اضافه کرده و محاسبه قیمت نهایی را عوض کرده. این یعنی کد تکراری نداریم و هر تغییری در describe خودکار به همه فرزندها میرسد.
کلاس فرزند همه اعضای public و protected پدر را به ارث میبرد. اگر هر دو کلاس در یک پکیج باشند، اعضای بدون مشخصکننده دسترسی (package-private) هم به ارث میرسند. اما اعضای private مستقیم در دسترس فرزند نیستند. فیلد private پدر هنوز داخل شیء وجود دارد، فقط فرزند نمیتواند مستقیم آن را بخواند و باید از متدهای پدر استفاده کند. سازندهها هم به ارث نمیرسند؛ هر کلاس سازنده خودش را دارد. در مثال ما فیلدهای name و price را protected گذاشتیم تا فرزندها بتوانند در finalPrice از price استفاده کنند. در پروژههای بزرگتر بهتر است فیلدها private بمانند و پدر یک متد protected برای دسترسی فراهم کند. سطوح دسترسی را با جزئیات در بخش Access Modifier مقاله کلاس و شیء در جاوا توضیح دادهایم.
| مشخصکننده دسترسی | در کلاس فرزند همپکیج | در کلاس فرزند پکیج دیگر |
|---|---|---|
| public | به ارث میرسد | به ارث میرسد |
| protected | به ارث میرسد | به ارث میرسد |
| بدون مشخصکننده (package-private) | به ارث میرسد | در دسترس نیست |
| private | مستقیم در دسترس نیست | مستقیم در دسترس نیست |
در جاوا هر کلاس فقط میتواند از یک کلاس ارث ببرد. اگر بنویسید class C extends A, B کامپایلر خطا میدهد؛ ما امتحان کردیم و پیام '{' expected گرفتیم. دلیل این محدودیت، جلوگیری از ابهام است: اگر دو پدر متدی با یک اسم داشته باشند، معلوم نیست کدام باید اجرا شود. البته یک کلاس میتواند چند اینترفیس را پیادهسازی کند. تفاوت این دو راه را در مقایسه اینترفیس و کلاس انتزاعی در جاوا مفصل بررسی کردهایم. یک نکته جالب دیگر: همه کلاسهای جاوا در نهایت از کلاس Object ارث میبرند، حتی اگر extends ننوشته باشید. به همین دلیل همه شیءها متدهایی مثل toString، equals و hashCode دارند. فهرست کامل این متدها در مستندات کلاس Object در جاوا ۲۵ آمده است.
کلمه super دو کاربرد اصلی دارد: صدا زدن سازنده پدر و صدا زدن نسخه پدرِ یک متد. آموزش اوراکل درباره super هر دو را با مثال نشان میدهد.
وقتی شیئی از کلاس فرزند میسازید، اول باید بخش پدر آن ساخته شود. در PhysicalProduct، خط super(name, price) سازنده Product را صدا میزند تا name و price مقدار بگیرند. اگر super را ننویسید، جاوا خودکار super() بدون آرگومان را صدا میزند. اگر پدر چنین سازندهای نداشته باشد، کامپایلر خطا میدهد؛ این یکی از خطاهای رایج مبتدیهاست. ترتیب ساخته شدن را با این مثال ببینید:
class Animal {
Animal() {
System.out.println("1. Animal constructor");
}
}
class Dog extends Animal {
Dog() {
System.out.println("2. Dog constructor");
}
}
class Puppy extends Dog {
Puppy() {
System.out.println("3. Puppy constructor");
}
}
public class Order {
public static void main(String[] args) {
new Puppy();
}
}
خروجی اجرای ما:
1. Animal constructor
2. Dog constructor
3. Puppy constructor
با اینکه فقط new Puppy() نوشتیم، سازندهها از بالاترین پدر به پایین اجرا شدند. هر سازنده پیش از اجرای بدنه خودش، سازنده پدر را صدا میزند.
تا قبل از جاوا ۲۵، فراخوانی super باید حتماً اولین خط سازنده میبود. یعنی اگر میخواستید ورودی را قبل از دادن به پدر بررسی کنید، باید از روشهای پیچیده مثل متد کمکی static استفاده میکردید. JEP 513 با عنوان Flexible Constructor Bodies که در جاوا ۲۵ نهایی شد، این محدودیت را برداشت. حالا میشود قبل از super دستورهایی نوشت که به خود شیء در حال ساخت دسترسی ندارند؛ مثلاً بررسی ورودی:
class Account {
protected final String owner;
Account(String owner) {
this.owner = owner;
}
}
class SavingsAccount extends Account {
SavingsAccount(String owner) {
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("owner is required");
}
super(owner.strip());
}
}
در اجرای ما با JDK 25، ساخت حساب با ورودی " Sara " نام Sara را بدون فاصله ذخیره کرد و ورودی خالی، خطای owner is required داد. نکته: این کد روی جاوا ۲۱ و قدیمیتر کامپایل نمیشود. جاوا ۲۵ نسخه پشتیبانی بلندمدت (LTS) است و در ۲۱ آبان ۱۴۰۵ تازهترین نسخه جاوا ۲۷ است که ۲۴ شهریور ۱۴۰۵ (۱۵ سپتامبر ۲۰۲۶) منتشر شد. اگر پروژه شما روی نسخه قدیمیتر است، همان روش متد کمکی static را ادامه دهید. برای آشنایی با مدیریت خطا، آموزش مدیریت خطا در جاوا را ببینید.
در کلاس فرزند میتوانید نسخه پدر یک متد را با super.methodName() صدا بزنید. این وقتی مفید است که بخواهید رفتار پدر را کامل عوض نکنید، بلکه چیزی به آن اضافه کنید:
class DigitalProduct extends Product {
DigitalProduct(String name, int price) {
super(name, price);
}
@Override
String describe() {
return super.describe() + " (download link by email)";
}
}
DigitalProduct متن پدر را نگه میدارد و فقط یک توضیح به آخرش اضافه میکند.
Override یعنی کلاس فرزند متدی با همان اسم، همان پارامترها و نوع بازگشتی سازگار با پدر بنویسد تا رفتار متفاوتی داشته باشد. در مثالها PhysicalProduct متد finalPrice و DigitalProduct متد describe را Override کردند. طبق آموزش اوراکل درباره Override، فرزند میتواند نوع بازگشتی را به یک زیرنوع از نوع بازگشتی پدر تغییر دهد که به آن covariant return type میگویند.
حاشیهنویسی @Override اجباری نیست، اما همیشه آن را بنویسید. این علامت به کامپایلر میگوید «من قصد دارم متد پدر را بازنویسی کنم». اگر اشتباه تایپی داشته باشید یا پارامترها متفاوت باشند، کامپایلر خطا میدهد. ما این را امتحان کردیم: وقتی بالای متد equals(Point2 other) نوشتیم @Override، کامپایلر پیام method does not override or implement a method from a supertype داد، چون equals اصلی پارامتر Object میگیرد. بدون @Override این اشتباه بیصدا رد میشود و باگ پنهان میسازد. در یک آزمایش، کلاس Point را با equals(Point other) نوشتیم و دو نقطه یکسان را در HashSet گذاشتیم. خروجی Set size: 2 بود، یعنی HashSet آنها را دو شیء متفاوت دید؛ چون متد ما Overload شده بود، نه Override. نسخه درست این است:
@Override
public boolean equals(Object o) {
return o instanceof Point other && x == other.x && y == other.y;
}
با این تغییر خروجی Set size: 1 شد.
چند قانون را باید رعایت کنید. سطح دسترسی متد فرزند نمیتواند محدودتر از پدر باشد؛ متد public پدر را نمیشود در فرزند protected کرد. متدهای final را نمیشود Override کرد؛ ما امتحان کردیم و کامپایلر خطای cannot override داد. متدهای private هم Override نمیشوند، چون اصلاً به فرزند نمیرسند. متدهای static هم Override نمیشوند، بلکه پنهان (hide) میشوند که جلوتر میبینیم. فرق متد static و متد معمولی را در مقاله متد استاتیک و متد نمونه در جاوا توضیح دادهایم.
این دو اسم شبیهاند و در مصاحبهها زیاد پرسیده میشوند. Overload یعنی چند متد با یک اسم ولی پارامترهای متفاوت در یک کلاس؛ کامپایلر در زمان کامپایل تصمیم میگیرد کدام صدا زده شود. Override یعنی بازنویسی متد پدر در فرزند با همان امضا؛ تصمیم درباره اینکه کدام نسخه اجرا شود، در زمان اجرا گرفته میشود.
| ویژگی | Overload | Override |
|---|---|---|
| کجا؟ | معمولاً در یک کلاس | در کلاس فرزند |
| پارامترها | باید متفاوت باشند | باید یکسان باشند |
| زمان انتخاب متد | زمان کامپایل | زمان اجرا |
| @Override | استفاده نمیشود | توصیه جدی |
| متد static و final | قابل Overload | قابل Override نیست |
| مثال | print(int) و print(String) | finalPrice در PhysicalProduct |
چندریختی یعنی «یک اسم، چند شکل». در جاوا یعنی متغیری از نوع پدر میتواند به شیئی از هر کلاس فرزند اشاره کند و وقتی متدی را صدا میزنید، نسخه مربوط به نوع واقعی شیء اجرا میشود. این مفهوم در آموزش چندریختی در سایت dev.java هم با مثال آمده است.
همه کلاسهای قبلی را کنار هم بگذاریم و یک سبد خرید بسازیم:
import java.util.List;
public class Shop {
public static void main(String[] args) {
List<Product> cart = List.of(
new Product("Gift card", 500_000),
new PhysicalProduct("Notebook", 120_000, 45_000),
new DigitalProduct("Java e-book", 90_000)
);
int total = 0;
for (Product p : cart) {
System.out.println(p.describe());
total += p.finalPrice();
}
System.out.println("Total: " + total);
}
}
خروجی اجرای ما با JDK 25.0.5:
Gift card: 500000 toman
Notebook: 165000 toman
Java e-book: 90000 toman (download link by email)
Total: 755000
به خط دوم دقت کنید. متغیر p از نوع Product است و متد describe هم فقط در Product نوشته شده. اما describe داخل خودش finalPrice را صدا میزند و چون شیء واقعی PhysicalProduct است، نسخه فرزند اجرا شد و هزینه ارسال ۴۵ هزار تومانی اضافه شد. حلقه for اصلاً نمیداند با چه نوع محصولی کار میکند و لازم هم نیست بداند. اگر فردا نوع تازهای مثل SubscriptionProduct اضافه کنید، این حلقه بدون هیچ تغییری با آن کار میکند. این همان اصل باز-بسته است که در اصول SOLID توضیح دادهایم.

گاهی چندریختی را دو نوع میدانند. چندریختی زمان کامپایل همان Overload است: کامپایلر از روی نوع آرگومانها میفهمد کدام متد را صدا بزند. چندریختی زمان اجرا همان Override است که دیدیم: ماشین مجازی جاوا در لحظه اجرا از روی نوع واقعی شیء تصمیم میگیرد. وقتی در جاوا از «چندریختی» حرف میزنند، معمولاً منظور نوع دوم است. اگر کنجکاوید JVM پشت صحنه کلاسها را چطور بارگذاری و اجرا میکند، مقاله نحوه کار JVM را بخوانید.
یک نکته که خیلیها را غافلگیر میکند: چندریختی فقط برای متدهای معمولی (instance) کار میکند، نه برای فیلدها و متدهای static. این مثال را ببینید:
class Parent {
String label = "parent field";
static String kind() {
return "static in Parent";
}
String hello() {
return "hello from Parent";
}
}
class Child extends Parent {
String label = "child field";
static String kind() {
return "static in Child";
}
@Override
String hello() {
return "hello from Child";
}
}
وقتی نوشتیم Parent p = new Child()، فراخوانی p.hello() پیام hello from Child را داد؛ چون متد معمولی Override شده بود. اما p.label مقدار parent field را برگرداند، چون فیلدها بر اساس نوع متغیر انتخاب میشوند نه نوع شیء. متد static هم به کلاس تعلق دارد؛ Parent.kind() و Child.kind() هر کدام نسخه خودشان را دادند. توصیه عملی: در کلاس فرزند فیلدی با همان اسم فیلد پدر تعریف نکنید.
وقتی شیء فرزند را در متغیری از نوع پدر میگذارید، مثل Vehicle v = new Car()، به آن Upcasting میگویند. این کار همیشه امن است و خودکار انجام میشود. اما از طریق v فقط متدهایی را میتوانید صدا بزنید که در Vehicle تعریف شدهاند. اگر Car متد honk داشته باشد، v.honk() کامپایل نمیشود. برای دسترسی دوباره به متدهای فرزند باید Downcasting کنید. این کار خطرناک است، چون اگر شیء واقعاً از آن نوع نباشد، ClassCastException میگیرید. ما یک Bike را به Car تبدیل کردیم و پیام class Bike cannot be cast to class Car را گرفتیم. راه امن، pattern matching با instanceof است:
Vehicle v = new Car();
if (v instanceof Car car) {
System.out.println(car.honk());
}
این شکل از instanceof هم بررسی نوع را انجام میدهد و هم متغیر car را با نوع درست میسازد. این قابلیت از جاوا ۱۶ با JEP 394 رسمی شد. اگر در کدتان زیاد instanceof مینویسید، احتمالاً طراحی مشکل دارد و بهتر است رفتار را با Override به خود کلاسها بسپارید.
در جاوای امروزی ابزار تازهای برای کنترل وراثت داریم. با JEP 409 در جاوا ۱۷، کلاسها و اینترفیسهای sealed اضافه شدند. با sealed میگویید دقیقاً کدام کلاسها حق دارند از این نوع ارث ببرند. وقتی این را با switch الگویی (JEP 441) از جاوا ۲۱ ترکیب کنید، کامپایلر مطمئن میشود همه حالتها را پوشش دادهاید:
sealed interface Shape permits Circle, Square, Rectangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
public class Shapes {
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Rectangle r -> r.width() * r.height();
};
}
}
خروجی ما برای دایره با شعاع ۱، مربع با ضلع ۳ و مستطیل ۲ در ۵ به ترتیب 3.14، 9.00 و 10.00 بود. چون Shape بسته است، switch به default نیاز ندارد. اگر روزی نوع تازهای به permits اضافه کنید و case آن را ننویسید، کامپایلر خطا میدهد. این روش برای مدل کردن دادهها جایگزین خوبی برای زنجیره بلند instanceof است.

وراثت ابزار قدرتمندی است، اما زیادهروی در آن کد را شکننده میکند. وقتی کلاس پدر تغییر کند، همه فرزندها تحت تأثیر قرار میگیرند. سلسلهمراتبهای عمیق، مثلاً پنج یا شش سطح، فهم کد را سخت میکنند. به این مشکل «کلاس پدر شکننده» میگویند. قاعده معروف «ترکیب را به وراثت ترجیح بدهید» از همینجا میآید. اگر فقط میخواهید از کد یک کلاس دوباره استفاده کنید ولی رابطه «هست یک» برقرار نیست، شیء آن کلاس را بهعنوان فیلد نگه دارید. یک آزمون دیگر هم اصل جایگزینی لیسکوف است: هر جا شیء پدر استفاده میشود، باید بتوانید شیء فرزند را بگذارید بدون اینکه برنامه رفتار غیرمنتظره نشان دهد. مثال معروف «مربع از مستطیل ارث ببرد» دقیقاً این اصل را میشکند. اگر کلاس شما را نباید کسی گسترش دهد، آن را final کنید.
| اشتباه | نتیجه | راه درست |
|---|---|---|
| ننوشتن @Override | Overload اشتباهی بهجای Override و باگ پنهان | همیشه @Override بنویسید |
| equals با پارامتر نوع کلاس خود | HashSet و HashMap شیءهای برابر را جدا میبینند | equals(Object o) همراه hashCode |
| نداشتن سازنده بدون آرگومان در پدر و ننوشتن super | خطای کامپایل در سازنده فرزند | صدا زدن صریح super با آرگومانها |
| تعریف فیلد همنام در فرزند | فیلد پدر پنهان میشود و نتیجه گیجکننده است | اسم متفاوت یا استفاده از متد |
| Downcasting بدون بررسی | ClassCastException در زمان اجرا | instanceof با pattern matching |
| وراثت برای رابطه «دارد» | طراحی شکننده و کد پیچیده | ترکیب: شیء را فیلد کنید |
| سلسلهمراتب خیلی عمیق | فهم و تغییر کد سخت میشود | سطحهای کم و کلاسهای final |
کپسولهسازی، وراثت، چندریختی و انتزاع. کپسولهسازی را در مقاله کلاس و شیء دیدیم، وراثت و چندریختی را در همین مقاله و انتزاع را در مقایسه اینترفیس و کلاس انتزاعی.
برای جلوگیری از ابهام، وقتی دو پدر متدی با یک اسم داشته باشند. جاوا بهجای آن اجازه میدهد یک کلاس چند اینترفیس را پیادهسازی کند.
نه. سازندهها به ارث نمیرسند، اما سازنده فرزند همیشه سازنده پدر را صدا میزند؛ یا صریح با super یا خودکار با super() بدون آرگومان.
نه. اگر در فرزند متد static با همان امضا بنویسید، متد پدر پنهان میشود ولی چندریختی اتفاق نمیافتد و نسخهای اجرا میشود که به نوع اعلامشده مربوط است.
this به شیء فعلی اشاره میکند و super به بخش پدر همان شیء. this() سازنده دیگری از همین کلاس را صدا میزند و super() سازنده پدر را.
یک کلاس Employee با نام و حقوق پایه بسازید و متد monthlyPay را تعریف کنید. دو کلاس Manager با پاداش ثابت و Intern با حقوق نصف را از آن بسازید، monthlyPay را Override کنید و همه کارمندان را در یک لیست بگذارید و جمع حقوق را حساب کنید. برای کار با لیستها آموزش ArrayList و HashMap در جاوا را ببینید و برای مرور مبانی زبان آشنایی با جاوا را بخوانید.
نظرات 0
برای ارسال نظر وارد حساب کاربری شوید.
هنوز نظری ثبت نشده؛ اولین نفر باشید.