JavaRush /وبلاگ جاوا /Random-FA /ژنریک برای گربه ها
Viacheslav
مرحله

ژنریک برای گربه ها

در گروه منتشر شد
ژنریک برای گربه ها - 1

معرفی

امروز یک روز عالی برای یادآوری آنچه در مورد جاوا می دانیم است. طبق مهمترین سند، یعنی. مشخصات زبان جاوا (JLS - مشخصات زبان جاوا)، جاوا یک زبان با تایپ قوی است، همانطور که در فصل " فصل 4. انواع، مقادیر و متغیرها " توضیح داده شده است. این یعنی چی؟ فرض کنید یک روش اصلی داریم:
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
تایپ قوی تضمین می‌کند که وقتی این کد کامپایل می‌شود، کامپایلر بررسی می‌کند که اگر نوع متغیر متنی را به‌عنوان String مشخص کرده‌ایم، در آن صورت سعی نمی‌کنیم از آن به عنوان متغیری از نوع دیگری (مثلاً به عنوان یک عدد صحیح) استفاده کنیم. . به عنوان مثال، اگر بخواهیم یک مقدار را به جای متن ذخیره کنیم 2L(یعنی long به جای String)، در زمان کامپایل با خطا مواجه خواهیم شد:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
آن ها تایپ قوی به شما امکان می دهد اطمینان حاصل کنید که عملیات روی اشیاء تنها زمانی انجام می شود که آن عملیات برای آن اشیا قانونی باشد. به این ایمنی نوع نیز می گویند. همانطور که در JLS بیان شد، دو دسته از انواع در جاوا وجود دارد: انواع اولیه و انواع مرجع. می توانید انواع اولیه را از مقاله مروری به یاد بیاورید: " انواع اولیه در جاوا: آنها چندان ابتدایی نیستند ." انواع مرجع را می توان با یک کلاس، رابط یا آرایه نشان داد. و امروز ما به انواع مرجع علاقه مند خواهیم شد. و بیایید با آرایه ها شروع کنیم:
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
این کد بدون خطا اجرا می شود. همانطور که می دانیم (به عنوان مثال، از " آموزش اوراکل جاوا: آرایه ها ")، آرایه محفظه ای است که داده ها را فقط از یک نوع ذخیره می کند. در این مورد - فقط خطوط. بیایید سعی کنیم به جای String طولانی را به آرایه اضافه کنیم:
text[1] = 4L;
بیایید این کد را اجرا کنیم (مثلاً در Repl.it Online Java Compiler ) و یک خطا دریافت کنیم:
error: incompatible types: long cannot be converted to String
آرایه و ایمنی نوع زبان به ما اجازه نمی دهد آنچه را که با نوع آن مطابقت ندارد در یک آرایه ذخیره کنیم. این جلوه ای از ایمنی نوع است. به ما گفته شد: "خطا را برطرف کنید، اما تا آن زمان من کد را کامپایل نخواهم کرد." و مهمترین چیز در این مورد این است که این اتفاق در زمان کامپایل رخ می دهد و نه زمانی که برنامه راه اندازی می شود. یعنی ما فورا خطاها را می بینیم و نه «روزی». و از آنجایی که آرایه‌ها را به یاد آوردیم، اجازه دهید چارچوب مجموعه‌های جاوا را نیز به خاطر بسپاریم . ما در آنجا ساختارهای مختلفی داشتیم. به عنوان مثال، لیست ها. بیایید مثال را دوباره بنویسیم:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
testهنگام کامپایل آن، یک خطا در خط مقداردهی اولیه متغیر دریافت خواهیم کرد :
incompatible types: Object cannot be converted to String
در مورد ما، List می تواند هر شی (به عنوان مثال یک شی از نوع Object) را ذخیره کند. بنابراین، گردآورنده می گوید که نمی تواند چنین بار مسئولیتی را بر عهده بگیرد. بنابراین، باید به صراحت نوع مورد نظر را از لیست مشخص کنیم:
String test = (String) text.get(0);
این نشانه تبدیل نوع یا ریخته گری نوع نامیده می شود. و همه چیز به خوبی کار می کند تا زمانی که سعی کنیم عنصر را در شاخص 1 بدست آوریم، زیرا از نوع Long است. و ما یک خطای منصفانه دریافت خواهیم کرد، اما در حال حاضر در حالی که برنامه در حال اجرا است (در Runtime):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
همانطور که می بینیم، چندین معایب مهم در اینجا وجود دارد. در مرحله اول، ما مجبوریم مقدار به دست آمده از لیست را به کلاس String "cast" کنیم. موافقم، این زشت است. ثانیاً در صورت بروز خطا، فقط زمانی که برنامه اجرا شود، آن را مشاهده خواهیم کرد. اگر کد ما پیچیده‌تر بود، ممکن بود فوراً چنین خطایی را شناسایی نکنیم. و توسعه دهندگان شروع کردند به فکر کردن در مورد اینکه چگونه کار را در چنین شرایطی آسان تر و کد را واضح تر کنند. و آنها متولد شدند - ژنریک.
ژنریک برای گربه ها - 2

ژنریک ها

بنابراین، ژنریک. چیست؟ ژنریک یک روش خاص برای توصیف انواع مورد استفاده است که کامپایلر کد می تواند در کار خود برای اطمینان از ایمنی نوع استفاده کند. چیزی شبیه این به نظر می رسد:
ژنریک برای گربه ها - 3
در اینجا یک مثال و توضیح کوتاه آورده شده است:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
در این مثال، می گوییم که ما نه فقط List، بلکه داریم Listکه فقط با اشیایی از نوع String کار می کند. و نه دیگران. چیزی که فقط در پرانتز نشان داده شده است، می توانیم آن را ذخیره کنیم. به چنین "براکت ها" "براکت های زاویه ای" می گویند. براکت های زاویه کامپایلر با مهربانی برای ما بررسی می کند که آیا هنگام کار با لیست رشته ها اشتباهی مرتکب شده ایم (لیست به نام متن است). کامپایلر می بیند که ما بی پروا در حال تلاش برای قرار دادن Long در لیست String هستیم. و در زمان کامپایل خطا می دهد:
error: no suitable method found for add(long)
شاید به خاطر داشته باشید که String از نسل CharSequence است. و تصمیم به انجام کاری مانند:
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
اما این امکان پذیر نیست و ما این خطا را دریافت خواهیم کرد: error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> عجیب به نظر می رسد، زیرا. خط CharSequence sec = "test";حاوی هیچ خطایی نیست. بیایید آن را بفهمیم. آنها در مورد این رفتار می گویند: "ژنریک ها ثابت هستند." "نامتغییر" چیست؟ من دوست دارم که چگونه در مورد این در ویکی پدیا در مقاله " کوواریانس و تضاد " گفته شده است:
ژنریک برای گربه ها - 4
بنابراین، Invariance عدم وجود وراثت بین انواع مشتق شده است. اگر Cat یک زیرگروه از Animals باشد، پس Set<Cats> زیرگروه Set<Animals> و Set<Animals> زیرگروه Set<Cats> نیست. به هر حال، شایان ذکر است که با شروع Java SE 7، به اصطلاح " اپراتور الماس " ظاهر شد. زیرا دو براکت زاویه <> مانند الماس هستند. این به ما امکان می دهد از ژنریک ها مانند زیر استفاده کنیم:
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
بر اساس این کد، کامپایلر می‌فهمد که اگر در سمت چپ نشان دادیم که Listشامل اشیایی از نوع String است، در سمت راست منظور ما این است که می‌خواهیم linesیک ArrayList جدید را در یک متغیر ذخیره کنیم، که یک شی را نیز ذخیره می‌کند. از نوع مشخص شده در سمت چپ. بنابراین کامپایلر از سمت چپ نوع سمت راست را می فهمد یا استنباط می کند. به همین دلیل است که به این رفتار در انگلیسی نوع استنتاج یا «استنتاج نوع» می گویند. یکی دیگر از موارد جالب توجه، انواع RAW یا "انواع خام" است. زیرا ژنریک ها همیشه وجود نداشته اند، و جاوا سعی می کند تا زمانی که امکان دارد سازگاری با عقب را حفظ کند، سپس ژنریک ها مجبور می شوند به نحوی با کدهایی کار کنند که در آن هیچ عمومی مشخص نشده است. بیایید یک مثال را ببینیم:
List<CharSequence> lines = new ArrayList<String>();
همانطور که به یاد داریم، چنین خطی به دلیل تغییر ناپذیری ژنریک ها کامپایل نمی شود.
List<Object> lines = new ArrayList<String>();
و این یکی هم به همین دلیل کامپایل نمی شود.
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
چنین خطوطی کامپایل می شوند و کار می کنند. در آنها است که از Raw Types استفاده می شود، i.e. انواع نامشخص بار دیگر، شایان ذکر است که انواع خام نباید در کدهای مدرن استفاده شوند.
ژنریک برای گربه ها - 5

کلاس های تایپ شده

بنابراین، کلاس های تایپ شده است. بیایید ببینیم چگونه می توانیم کلاس تایپ شده خودمان را بنویسیم. به عنوان مثال، ما یک سلسله مراتب کلاس داریم:
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
ما می خواهیم کلاسی ایجاد کنیم که یک کانتینر حیوانی را پیاده سازی کند. می توان کلاسی نوشت که حاوی هر کدام باشد Animal. این ساده، قابل درک است، اما... اختلاط سگ و گربه بد است، آنها با یکدیگر دوست نیستند. علاوه بر این، اگر شخصی چنین ظرفی را دریافت کند، ممکن است به اشتباه گربه ها را از ظرف به گله سگ بیندازد... و این کار خیری در پی نخواهد داشت. و در اینجا ژنریک ها به ما کمک خواهند کرد. برای مثال پیاده سازی را به صورت زیر بنویسیم:
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
کلاس ما با اشیایی از نوع مشخص شده توسط یک ژنریک به نام T کار خواهد کرد. این یک نوع نام مستعار است. زیرا ژنریک در نام کلاس مشخص شده است، سپس هنگام اعلام کلاس آن را دریافت خواهیم کرد:
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
همانطور که می بینیم، ما نشان دادیم که داریم Box، که فقط با کار می کند Cat. کامپایلر متوجه شد که catBoxبه جای یک ژنریک، Tباید نوع را Catدر هر جایی که نام ژنریک مشخص شده است جایگزین کنید T:
ژنریک برای گربه ها - 6
آن ها Box<Cat>به لطف کامپایلر است که می فهمد slotsدر واقع چه چیزی باید باشد List<Cat>. زیرا Box<Dog>در داخل وجود خواهد داشت slots, حاوی List<Dog>. در یک اعلان نوع می تواند چندین ژنریک وجود داشته باشد، به عنوان مثال:
public static class Box<T, V> {
نام عمومی می‌تواند هر چیزی باشد، اگرچه توصیه می‌شود به برخی از قوانین ناگفته پایبند باشید - "شرایط نامگذاری پارامتر نوع": نوع عنصر - E، نوع کلید - K، نوع عدد - N، T - برای نوع، V - برای نوع مقدار . به هر حال، به یاد داشته باشید که گفتیم که ژنریک ها ثابت هستند، یعنی. سلسله مراتب وراثت را حفظ نکنید. در واقع ما می توانیم روی این موضوع تاثیر بگذاریم. یعنی ما این فرصت را داریم که ژنریک ها را COvariant کنیم، یعنی. حفظ ارث به همان ترتیب این رفتار "نوع محدود" نامیده می شود. انواع محدود به عنوان مثال، کلاس ما Boxمی تواند شامل همه حیوانات باشد، سپس ما یک عمومی مانند این را اعلام می کنیم:
public static class Box<T extends Animal> {
یعنی حد بالایی را برای کلاس تعیین می کنیم Animal. ما همچنین می توانیم چندین نوع را بعد از کلمه کلیدی مشخص کنیم extends. این بدان معنی است که نوع مورد نظر ما باید از یک کلاس باشد و در عین حال برخی از اینترفیس ها را پیاده سازی کند. مثلا:
public static class Box<T extends Animal & Comparable> {
در این صورت، اگر بخواهیم Boxچیزی را در آن قرار دهیم که ارثی نباشد Animalو اجرا نشود Comparable، در حین کامپایل با خطا مواجه خواهیم شد:
error: type argument Cat is not within bounds of type-variable T
ژنریک برای گربه ها - 7

روش تایپ

ژنریک ها نه تنها در انواع، بلکه در روش های فردی نیز استفاده می شوند. کاربرد روش ها را می توان در آموزش رسمی " روش های ژنریک " مشاهده کرد.

زمینه:

ژنریک برای گربه ها - 8
بیایید به این تصویر نگاه کنیم. همانطور که می بینید، کامپایلر به امضای متد نگاه می کند و می بیند که ما مقداری کلاس تعریف نشده را به عنوان ورودی می گیریم. با امضا مشخص نمی شود که ما نوعی شی را برمی گردانیم، یعنی. هدف - شی. بنابراین، اگر بخواهیم مثلاً یک ArrayList ایجاد کنیم، باید این کار را انجام دهیم:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
شما باید به صراحت بنویسید که خروجی یک ArrayList خواهد بود که زشت است و احتمال اشتباه را اضافه می کند. به عنوان مثال، می توانیم چنین مزخرفاتی بنویسیم و آن را جمع آوری می کند:
ArrayList object = (ArrayList) createObject(LinkedList.class);
آیا می توانیم به کامپایلر کمک کنیم؟ بله، ژنریک ها این امکان را به ما می دهند. بیایید به همین مثال نگاه کنیم:
ژنریک برای گربه ها - 9
سپس، می‌توانیم یک شی به سادگی مانند زیر ایجاد کنیم:
ArrayList<String> object = createObject(ArrayList.class);
ژنریک برای گربه ها - 10

WildCard

با توجه به آموزش Oracle در مورد Generics، به طور خاص بخش " Waldcards "، ما می توانیم یک "نوع ناشناخته" را با علامت سوال توصیف کنیم. Wildcard یک ابزار مفید برای کاهش برخی از محدودیت های ژنریک است. به عنوان مثال، همانطور که قبلاً بحث کردیم، ژنریک ها ثابت هستند. این بدان معناست که اگرچه همه کلاس‌ها از نوادگان (زیرگروه‌ها) از نوع Object هستند، اما List<любой тип>یک زیرگروه نیست List<Object>. اما، List<любой тип>این یک نوع فرعی است List<?>. بنابراین می توانیم کد زیر را بنویسیم:
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
مانند ژنریک های معمولی (یعنی بدون استفاده از حروف عام)، ژنریک های دارای عام می توانند محدود شوند. حروف با کران بالا آشنا به نظر می رسد:
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
اما شما همچنین می توانید آن را با علامت عام کران پایین محدود کنید:
public static void printCatList(List<? super Cat> list) {
بنابراین، این روش شروع به پذیرش همه گربه‌ها و همچنین بالاتر در سلسله مراتب (تا Object) می‌کند.
ژنریک برای گربه ها - 11

پاک کردن را تایپ کنید

در مورد ژنریک ها، ارزش دانستن در مورد "پاک کردن نوع" را دارد. در واقع پاک کردن نوع مربوط به این واقعیت است که ژنریک ها اطلاعاتی برای کامپایلر هستند. در طول اجرای برنامه، اطلاعات بیشتری در مورد ژنریک وجود ندارد، به این "پاک کردن" می گویند. این پاک کردن باعث می شود که نوع عمومی با نوع خاص جایگزین شود. اگر ژنریک مرزی نداشت، نوع Object جایگزین می شود. اگر مرز مشخص شده بود (به عنوان مثال <T extends Comparable>)، آنگاه جایگزین خواهد شد. در اینجا یک مثال از آموزش Oracle آورده شده است: " Erasure of Generic Types ":
ژنریک برای گربه ها - 12
همانطور که در بالا گفته شد، در این مثال کلی Tتا مرز خود پاک می شود، یعنی. قبل از Comparable.
ژنریک برای گربه ها - 13

نتیجه

ژنریک موضوع بسیار جالبی است. امیدوارم این موضوع مورد توجه شما قرار گرفته باشد. به طور خلاصه، می‌توان گفت که ژنریک‌ها ابزار بسیار خوبی هستند که توسعه‌دهندگان دریافت کرده‌اند تا اطلاعات اضافی را برای اطمینان از ایمنی نوع از یک سو و انعطاف‌پذیری از سوی دیگر به کامپایلر ارائه دهند. و اگر علاقه مند هستید، پیشنهاد می کنم منابعی را که دوست داشتم بررسی کنید: #ویاچسلاو
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION