JavaRush /Java блогы /Random-KK /Java Generics ішіндегі қойылмалы таңбалар

Java Generics ішіндегі қойылмалы таңбалар

Топта жарияланған
Сәлеметсіз бе! Біз генериктер тақырыбын зерттеуді жалғастырамыз. Сізде алдыңғы лекциялардан олар туралы жақсы білімдер бар ( генериктермен жұмыс істеу кезінде varargs пайдалану және типті өшіру туралы ), бірақ біз әлі бір маңызды тақырыпты қарастырған жоқпыз: қойылмалы таңбалар . Бұл генериктердің өте маңызды қасиеті. Біз оған бөлек дәріс арнағанымыз сонша! Дегенмен, қойылмалы таңбаларда күрделі ештеңе жоқ, оны қазір көресіз :) Жалпы таңбалар - 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 нысанға шығарамыз ба, айырмашылығы неде ? Нысан мен an objectілер жиынтығы арасында маңызды айырмашылық бар . Егер сынып 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қоса аламыз ! Осылайша, коллекциямыз тек generic ішінде көрсетілген нысандарды қамтитынына кепілдікті жоғалттық . Яғни, біз генериктердің басты артықшылығы – қауіпсіздік түрін жоғалттық. Ал компилятор мұның бәрін жасауға мүмкіндік бергендіктен, бұл біз тек бағдарламаны орындау кезінде қате аламыз дегенді білдіреді, бұл әрқашан компиляция қателігінен әлдеқайда нашар. Мұндай жағдайларды болдырмау үшін компилятор бізге қате береді: stringsObjectString
// ошибка компиляции
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>емеспіз ! Сондықтан, біз мысықтар тізімі бар әдісті шақыруға тырысқанда , біз компилятор қатесін аламыз: 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("Еще один шаг в цикле пройден!");
   }
}
Бұл қойылмалы таңба. Дәлірек айтсақ, бұл қойылмалы таңбаның бірнеше түрлерінің біріншісі – “ кеңейтеді ” (басқа атауы – Жоғарғы шектелген қойылмалы таңбалар ). Бұл дизайн бізге не айтады? AnimalБұл әдіс кіріс ретінде сынып an objectілерінің немесе кез келген ұрпақ класының an objectілерінің жиынын қабылдайтынын білдіреді Animal (? extends Animal). AnimalБасқаша айтқанда, әдіс , коллекциясын Petнемесе кіріс ретінде Dogқабылдай алады Cat- бұл ешқандай айырмашылық жоқ. Бұл жұмыс істейтініне көз жеткізейік:
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 жазба бар. Барлығы тамаша жұмыс істейді! :) Қойылмалы таңба бізге белгілі бір түрлерге байланыстыру арқылы қажетті логиканы бір әдіске оңай орналастыруға мүмкіндік берді. Жануарлардың әр түріне жеке әдістеме жазу қажеттілігінен құтылдық. Біздің қолданба хайуанаттар бағында немесе ветеринарлық клиникада қолданылса, қанша әдіске ие болатынын елестетіп көріңізші :) Енді басқа жағдайды қарастырайық. Біздің мұра иерархиямыз өзгеріссіз қалады: жоғарғы деңгейлі сынып - Animal, одан төменірек - Үй жануарлары класы Petжәне келесі деңгейде - Catжәне Dog. Енді иттерден басқаiretateAnimals() жануарлардың кез келген түрімен жұмыс істей алатындай әдісті қайта жазу керек . Яғни, ол , немесе кіріс ретінде қабылдауы керек , бірақ онымен жұмыс істемеуі керек . Бұған қалай қол жеткізе аламыз? Әрбір түрге жеке әдіс жазу мүмкіндігі тағы да алдымызда тұрған сияқты:/ Компиляторға логикамызды тағы қалай түсіндіре аламыз? Және мұны өте оңай жасауға болады! Мұнда тағы да қойылмалы таңбалар көмекке келеді. Бірақ бұл жолы біз басқа түрін қолданамыз - « super » (басқа атауы - Төменгі шектелген қойылмалы таңбалар ). Collection<Animal>Collection<Pet>Collection<Cat>Collection<Dog>
public static void iterateAnimals(Collection<? super Cat> animals) {

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

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Мұндағы принцип ұқсас. Конструкция <? super Cat>компиляторға әдіс iterateAnimals()кіріс ретінде сынып an objectілерінің жиынын 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);
}
Біздің мәселе шешілді, тағы да қойылмалы таңбалар өте пайдалы болып шықты :) Осымен лекция аяқталды. Енді сіз Java тілін үйрену кезінде генериктер тақырыбының қаншалықты маңызды екенін көресіз - біз оған 4 лекция өткіздік! Бірақ қазір сіз тақырыпты жақсы түсіндіңіз және сұхбатта өзіңізді дәлелдей аласыз :) Ал енді тапсырмаларға қайта оралатын уақыт! Оқуларыңызға сәттілік! :)
Пікірлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION