JavaRush /وبلاگ جاوا /Random-FA /حروف عام در Java Generics

حروف عام در Java Generics

در گروه منتشر شد
سلام! ما به مطالعه موضوع ژنریک ادامه می دهیم. شما قبلاً اطلاعات خوبی در مورد آنها از سخنرانی‌های قبلی دارید (در مورد استفاده از varargs هنگام کار با ژنریک و در مورد پاک کردن نوع )، اما ما هنوز یک موضوع مهم را پوشش نداده‌ایم: wildcards . این یک ویژگی بسیار مهم ژنریک است. آنقدر که یک سخنرانی جداگانه برای آن اختصاص داده ایم! با این حال، هیچ چیز پیچیده ای در مورد عام وجود ندارد، اکنون آن را خواهید دید :) حروف عام در ژنریک - 1بیایید به یک مثال نگاه کنیم:
public class Main {

   public static void main(String[] args) {

       String str = new String("Test!");
       // ниHowих проблем
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // ошибка компиляции!
       List<Object> objects = strings;
   }
}
اینجا چه خبره؟ ما دو موقعیت بسیار مشابه را می بینیم. در اولین مورد، ما سعی می کنیم یک شی را Stringبرای تایپ ریخته کنیم Object. هیچ مشکلی با این وجود ندارد، همه چیز همانطور که باید کار می کند. اما در حالت دوم کامپایلر خطا می دهد. اگرچه به نظر می رسد که ما هم همین کار را انجام می دهیم. فقط در حال حاضر ما از مجموعه ای از چندین شی استفاده می کنیم. اما چرا خطا رخ می دهد؟ اساساً چه تفاوتی وجود دارد که یک شی را Stringبه یک نوع Objectیا 20 شیء می اندازیم؟ تفاوت مهمی بین یک شی و مجموعه ای از اشیاء وجود دارد . اگر کلاسی Bجانشین یک کلاس باشد Аپس Collection<B>جانشین نیست Collection<A>. به همین دلیل است که ما نتوانستیم مال خود List<String>را به List<Object>. Stringوارث است Objectولی List<String>وارث نیست List<Object>. از نظر شهودی، این خیلی منطقی به نظر نمی رسد. چرا سازندگان زبان بر اساس این اصل هدایت می شدند؟ بیایید تصور کنیم که کامپایلر در اینجا به ما خطایی نمی دهد:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
در این مورد، به عنوان مثال، می توانیم موارد زیر را انجام دهیم:
objects.add(new Object());
String s = strings.get(0);
از آنجایی که کامپایلر به ما خطایی نداد و به ما اجازه داد تا List<Object> objectبه مجموعه‌ای از رشته‌ها ارجاع دهیم strings، می‌توانیم stringsنه یک رشته، بلکه به سادگی هر شی را اضافه کنیم Object! بنابراین، ما این تضمین را از دست داده ایم که مجموعه ما فقط شامل اشیاء مشخص شده در عمومی استString . یعنی ما مزیت اصلی ژنریک ها - ایمنی نوع را از دست داده ایم. و از آنجایی که کامپایلر به ما اجازه انجام همه اینها را داده است، به این معنی است که ما فقط در هنگام اجرای برنامه با خطا مواجه خواهیم شد که همیشه بسیار بدتر از خطای کامپایل است. برای جلوگیری از چنین شرایطی، کامپایلر به ما یک خطا می دهد:
// ошибка компиляции
List<Object> objects = strings;
...و به او یادآوری می کند که List<String>وارث نیست List<Object>. این یک قانون آهنین برای عملکرد ژنریک ها است و هنگام استفاده از آنها باید به خاطر داشت. بیایید ادامه دهیم. بیایید بگوییم که ما یک سلسله مراتب کلاس کوچک داریم:
public class Animal {

   public void feed() {

       System.out.println("Animal.feed()");
   }
}

public class Pet extends Animal {

   public void call() {

       System.out.println("Pet.call()");
   }
}

public class Cat extends Pet {

   public void meow() {

       System.out.println("Cat.meow()");
   }
}
در رأس این سلسله مراتب به سادگی حیوانات قرار دارند: حیوانات خانگی از آنها به ارث می رسد. حیوانات خانگی به 2 نوع سگ و گربه تقسیم می شوند. حال تصور کنید که باید یک روش ساده ایجاد کنیم iterateAnimals(). این روش باید مجموعه‌ای از حیوانات ( Animal, Pet, Cat, Dog) را بپذیرد، همه عناصر را تکرار کند و هر بار چیزی را به کنسول ارسال کند. بیایید سعی کنیم این روش را بنویسیم:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
به نظر می رسد که مشکل حل شده است! با این حال، همانطور که اخیرا متوجه شدیم، List<Cat>یا وارث List<Dog>نیستند ! بنابراین، هنگامی که می خواهیم متدی را با لیستی از cat ها فراخوانی کنیم، یک خطای کامپایلر دریافت خواهیم کرد: List<Pet>List<Animal>iterateAnimals()
import java.util.*;

public class Main3 {


   public static void iterateAnimals(Collection<Animal> animals) {

       for(Animal animal: animals) {

           System.out.println("Еще один шаг в цикле пройден!");
       }
   }

   public static void main(String[] args) {


       List<Cat> cats = new ArrayList<>();
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());

       //ошибка компилятора!
       iterateAnimals(cats);
   }
}
اوضاع برای ما خوب به نظر نمی رسد! معلوم می شود که باید روش های جداگانه ای برای شمارش انواع حیوانات بنویسیم؟ در واقع، نه، شما مجبور نیستید :) و حروف عام به ما در این امر کمک خواهند کرد ! ما با استفاده از ساختار زیر مشکل را با یک روش ساده حل خواهیم کرد:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
این وایلدکارت است. به‌طور دقیق‌تر، این اولین نوع از چندین نوع عام است - " exts " (نام دیگر Wildcards با کران بالا است ). این طراحی به ما چه می گوید؟ این بدان معنی است که متد مجموعه ای از اشیاء کلاس Animalیا اشیاء هر کلاس نزولی را به عنوان ورودی می گیرد Animal (? extends Animal). به عبارت دیگر ، متد می تواند مجموعه Animal، یا به عنوان ورودی را بپذیرد - تفاوتی ندارد. بیایید مطمئن شویم که این کار می کند: PetDogCat
public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);
   iterateAnimals(dogs);
}
خروجی کنسول:

Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
ما در مجموع 4 مجموعه و 8 آبجکت ایجاد کرده ایم و دقیقاً 8 ورودی در کنسول وجود دارد. همه چیز عالی کار می کند! :) Wildcard به ما این امکان را داد که به راحتی منطق لازم را با اتصال به انواع خاص در یک روش قرار دهیم. ما از نیاز به نوشتن یک روش جداگانه برای هر نوع حیوان خلاص شدیم. تصور کنید اگر برنامه ما در باغ وحش یا کلینیک دامپزشکی استفاده می شد چند روش خواهیم داشت :) حالا بیایید به یک وضعیت متفاوت نگاه کنیم. سلسله مراتب وراثت ما به همان صورت باقی خواهد ماند: کلاس سطح بالا، Animalپایین تر، کلاس Pets Petو در سطح بعدی، Catو است Dog. حالا باید روش رو دوباره بنویسید iretateAnimals()تا بتونه با هر نوع حیوانی بجز سگ کار کنه . Collection<Animal>یعنی باید بپذیرد یا به عنوان ورودی باشد Collection<Pet>، Collection<Cat>اما نباید با آن کار کند Collection<Dog>. ما چگونه می توانیم به این دست پیدا کنیم؟ به نظر می رسد که دوباره چشم انداز نوشتن یک روش جداگانه برای هر نوع در مقابل ما ظاهر می شود :/ چگونه می توانیم منطق خود را برای کامپایلر توضیح دهیم؟ و این را می توان خیلی ساده انجام داد! در اینجا حروف عامیانه دوباره به کمک ما خواهند آمد. اما این بار از نوع دیگری استفاده خواهیم کرد - " super " (نام دیگر Lower Bounded Wildcards است ).
public static void iterateAnimals(Collection<? super Cat> animals) {

   for(int i = 0; i < animals.size(); i++) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
اصل در اینجا مشابه است. ساختار <? super Cat>به کامپایلر می گوید که متد iterateAnimals()می تواند مجموعه ای از اشیاء کلاس Catیا هر کلاس اجداد دیگری را به عنوان ورودی دریافت کند Cat. Catدر مورد ما، خود کلاس ، اجداد آن - Petsو جد جد - با این توصیف مطابقت دارند Animal. کلاس Dogدر این محدودیت نمی گنجد، و بنابراین تلاش برای استفاده از یک متد با لیست List<Dog>منجر به خطای کامپایل می شود:
public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);

   //ошибка компиляции!
   iterateAnimals(dogs);
}
مشکل ما حل شد، و دوباره وایلد کارت ها بسیار مفید بودند :) این سخنرانی را به پایان می رساند. اکنون می بینید که موضوع ژنریک در یادگیری جاوا چقدر مهم است - ما 4 سخنرانی روی آن صرف کردیم! اما اکنون شما درک خوبی از موضوع دارید و می توانید خود را در مصاحبه ثابت کنید :) و اکنون زمان بازگشت به وظایف است! امیدوارم در مطالعه موفق باشی! :)
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION