JavaRush /Java Blog /Random-TL /Generics para sa mga pusa

Generics para sa mga pusa

Nai-publish sa grupo
Дженерики на котиках - 1

Panimula

Ngayon ay isang magandang araw para alalahanin ang nalalaman natin tungkol sa Java. Ayon sa pinakamahalagang dokumento, i.e. Java Language Specification (JLS - Java Language Specifiaction), ang Java ay isang malakas na pag-type ng wika, gaya ng inilarawan sa kabanata " Kabanata 4. Mga Uri, Halaga, at Variable ". Ano ang ibig sabihin nito? Sabihin nating mayroon tayong pangunahing pamamaraan:
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
Tinitiyak ng malakas na pag-type na kapag pinagsama-sama ang code na ito, susuriin ng compiler na kung tinukoy namin ang uri ng variable ng text bilang String, hindi namin ito sinusubukang gamitin kahit saan bilang variable ng ibang uri (halimbawa, bilang isang Integer) . Halimbawa, kung susubukan naming mag-save ng value sa halip na text 2L(ibig sabihin, mahaba sa halip na String), magkakaroon kami ng error sa oras ng pag-compile:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
Yung. Ang malakas na pag-type ay nagbibigay-daan sa iyo upang matiyak na ang mga pagpapatakbo sa mga bagay ay ginagawa lamang kapag ang mga pagpapatakbong iyon ay legal para sa mga bagay na iyon. Ito ay tinatawag ding uri ng kaligtasan. Gaya ng nakasaad sa JLS, mayroong dalawang kategorya ng mga uri sa Java: mga primitive na uri at mga uri ng sanggunian. Maaalala mo ang tungkol sa mga primitive na uri mula sa review na artikulo: " Primitive na mga uri sa Java: Hindi sila masyadong primitive ." Ang mga uri ng sanggunian ay maaaring katawanin ng isang klase, interface, o array. At ngayon kami ay magiging interesado sa mga uri ng sanggunian. At magsimula tayo sa mga arrays:
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
Ang code na ito ay tumatakbo nang walang error. Tulad ng alam natin (halimbawa, mula sa " Oracle Java Tutorial: Arrays "), ang array ay isang lalagyan na nag-iimbak ng data ng isang uri lang. Sa kasong ito - mga linya lamang. Subukan nating magdagdag ng mahaba sa array sa halip na String:
text[1] = 4L;
Patakbuhin natin ang code na ito (halimbawa, sa Repl.it Online Java Compiler ) at magkaroon ng error:
error: incompatible types: long cannot be converted to String
Ang array at ang uri ng kaligtasan ng wika ay hindi nagpapahintulot sa amin na i-save sa isang array kung ano ang hindi akma sa uri. Ito ay isang pagpapakita ng uri ng kaligtasan. Sinabihan kami: "Ayusin ang error, ngunit hanggang doon ay hindi ko isasama ang code." At ang pinakamahalagang bagay tungkol dito ay nangyayari ito sa oras ng compilation, at hindi kapag inilunsad ang programa. Ibig sabihin, nakikita natin kaagad ang mga pagkakamali, at hindi “balang araw.” At dahil naalala natin ang tungkol sa mga array, tandaan din natin ang tungkol sa Java Collections Framework . Nagkaroon kami ng iba't ibang mga istraktura doon. Halimbawa, mga listahan. Isulat muli natin ang halimbawa:
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);
  }
}
Kapag kino-compile ito, makakatanggap kami testng error sa variable initialization line:
incompatible types: Object cannot be converted to String
Sa aming kaso, ang List ay maaaring mag-imbak ng anumang bagay (i.e. isang bagay na may uri ng Bagay). Samakatuwid, sinabi ng tagatala na hindi ito maaaring kumuha ng gayong pasanin ng responsibilidad. Samakatuwid, kailangan nating tahasang tukuyin ang uri na makukuha natin mula sa listahan:
String test = (String) text.get(0);
Такое указание называется приведением типов (type conversion or type casting). И всё будет работать теперь замечательно, пока мы не попробуем достать элемент по индексу 1, т.к. он имеет тип Long. И мы получим справедливую ошибку, но уже во время работы программы (в Runtime):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
Как мы видим, здесь сразу несколько важных минусов. Во-первых, мы вынуждены сами "кастить" meaning, полученное из списка, к классу String. Согласитесь, это некрасиво. Во-вторых, в случае ошибки, мы её увидим только тогда, когда программа будет выполнятся. Будь наш code сложнее, мы могли бы обнаружить такую ошибку далеко не сразу. И стали разработчики думать, How сделать работу в таких ситуациях проще, а code нагляднее. И на свет родorсь они — Дженерики (Generics).
Дженерики на котиках - 2

Generics

Итак, дженерики. What же это такое? Дженерик — это особый способ описание используемых типов, который сможет использовать в своей работе компилятор codeа для обеспечения типобезопасности. Выглядит это примерно так:
Дженерики на котиках - 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, который работает ТОЛЬКО с an objectми типа String. И ниHowими другими. Вот только что указано в скобках, то и можем хранить. Такие "скобки" называются "angle brackets", т.е. угловые (angle) скобки (brackets). Компилятор любезно за нас проверит, не допустor ли мы ошибок при работе со списком строк (список имеет Name text). Компилятор увидит, что мы пытаемся в список String положить наглым образом Long. И в момент компиляции выдаст ошибку:
error: no suitable method found for add(long)
Возможно, Вы вспомнor про то, что 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"; не содержит ошибок. Давайте разбираться. Про такое поведение говорят: "Дженерики являются инвариантными". What такое "инвариант"? Мне нравится, How про это сказано на википедии в статье "Covariance and contravariance":
Дженерики на котиках - 4
Таким образом, Инвариантность — это отсутствие наследования между производными типами. Если Кошка — это подтип Животные, то Множество<Кошки> не является подтипом Множество<Животные> и Множество<Животные> не является подтипом Множество<Кошки>. Кстати, стоит сказать, что начиная с Java SE 7 появился так называемый "Diamond Operator". Потому что две угловые скобки <> похожи на бриллиант. Это позволяет нам использовать дженерики следующим образом:
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
Компилятор по данному codeу понимает, что если мы указали в левой части, что List будет содержать an objectы типа String, то в правой части мы подразумеваем, что хотим в переменную lines сохранить новый ArrayList, который будет хранить тоже an object того типа, который указан в левой части. Таким образом, компилятор из левой части понимает or выводит тип для правой части. Именно поэтому такое поведение называется выведением типа or "Type Inference" на английском. Стоит отметить ещё такую интересную вещь, How RAW Types or же "сырые типы". Т.к. дженерики были не всегда, а Java старается по возможности поддерживать обратную совместимость, то дженерики вынуждены How-то работать с codeом, где не указан ниHowой дженерик. Посмотрим пример:
List<CharSequence> lines = new ArrayList<String>();
Как мы помним, такая строчка не скомпorруется из-за инвариантности дженериков.
List<Object> lines = new ArrayList<String>();
И такая тоже не скомпorруется, по той же самой причине.
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
Такие строчки скомпorруется и будут работать. Именно в них используется Raw Types, т.е. не указанные типы. Ещё раз стоит указать, что Raw Types в современном codeе НЕ ДОЛЖНЫ быть использованы.
Дженерики на котиках - 5

Типизированные классы

Итак, типизированные классы. Давайте увидим, How мы можем написать свой типизированный класс. Например, у нас есть иерархия классов:
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;
  }
}
Наш класс будет работать с an objectми типа, указанные дженериком с именем T. Это своего рода псевдоним. Т.к. дженерик указан у имени класса, то и получать его будем при объявлении класса:
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
Как мы видим, мы указали что у нас Box, который работает только с Cat. Компилятор понял, что для catBox instead of дженерика T нужно подставить тип Cat везде, где указано Name дженерика T:
Дженерики на котиках - 6
Т.е. именно благодаря Box<Cat> компилятор понимает, что slots на самом деле должен быть List<Cat>. Для Box<Dog> внутри будет slots, содержащий List<Dog>. Дженериков в объявлении типа может быть несколько, например:
public static class Box<T, V> {
Название дженерика может быть любым, хотя рекомендуется придерживаться некоторым негласным правилам — "Type Parameter Naming Conventions": Тип element — E, тип ключа — K, тип числа — N, T — для обозначения типа, V — для типа значения. Кстати, помните мы с вами говорor, что дженерики инвариантны, т.е. не сохраняют иерархию наследования. На самом деле, мы можем на это повлиять. То есть у нас есть возможность сделать дженерики КОвариантными, т.е. сохраняющими наследования в том же порядке. Такое поведение называется "Bounded Type", т.е. ограниченные типы. Например, наш класс Box мог бы содержать всех животных, тогда бы мы объявor дженерик следующим образом:
public static class Box<T extends Animal> {
То есть мы задали ограничor верхнюю границу классом 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

Типизация методов

Дженерики применяются не только в типах, но и в отдельных методах. Применение в методах можно увидеть в официальном tutorial: "Generics Methods".

Предыстория:

Дженерики на котиках - 8
Посмотрим на эту картинку. Как видно, компилятор смотрит на сигнатуру метода и видит, что на вход мы принимаем Howой-то неопределённый класс. Не по сигнатуре он определяет, что мы возвращаем Howой-то an object, т.е. Object. Следовательно, если мы хотим создать, скажем, ArrayList, то нам надо делать так:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
Приходится самим явно писать, что на выходе будет ArrayList, что некрасиво и добавляет шанс сделать ошибку. Например, мы можем написать такой бред и это скомпorруется:
ArrayList object = (ArrayList) createObject(LinkedList.class);
Можем ли мы помочь компилятору? Да, дженерики нам это позволяют. Рассмотрим тот же пример:
Дженерики на котиках - 9
Тогда, мы можем создать an object просто вот так:
ArrayList<String> object = createObject(ArrayList.class);
Дженерики на котиках - 10

WildCard

Согласно Tutorial от Oracle по дженерикам, а именно разделу "Wildcards", мы можем описать "неизвестный тип" символом вопроса, так называемого question mark. Wildcard — удобный инструмент, чтобы смягчить некоторый ограничения дженериков. Например, How мы ранее разбирали, дженерики инвариантны. Это значит что хотя все классы являются наследниками (подтипами, subtypes) типа Object, но List<любой тип> не является подтипом List<Object>. НО, List<любой тип> является подтипом List<?>. Таким образом, мы можем написать следующий code:
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
Как и обычные дженерики (т.е. без использования wildcard'ов), дженерики с wildcard могут быть ограничены. Ограничение по верхней границе (Upper bounded wildcard) выглядит привычно:
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
Но можно ограничить и по нижней границе (Lower bound wildcard):
public static void printCatList(List<? super Cat> list) {
Таким образом метод начнёт принимать всех котов, а так же по иерархии выше (вплоть до Object).
Дженерики на котиках - 11

Стирание типов or Type Erasure

Говоря про дженерики стоит знать про "Стирание типов". На самом деле стирание типов это про то, что дженерики — это информация для компилятора. Во время выполнения программы информации о дженериках больше нет, это и называется "стирание". Это стирание имеет тот эффект, что тип дженерика заменяется конкретным типом. Если дженерик не имел границы, то будет подставлен тип Object. Если же граница была указана (например <T extends Comparable>), то она и будет подставлена. Вот пример из Tutorial от Oracle: "Erasure of Generic Types":
Дженерики на котиках - 12
Как выше и было сказано, в данном примере дженерик T стёрт до своей границы, т.е. до Comparable.
Дженерики на котиках - 13

Заключение

Дженерики — очень интересная тема. Надеюсь, данная тема Вас заинтересовала. Подводя итоги, можно сказать, что дженерики — прекрасное средство, которые получor разработчики, чтобы подсказывать компилятору дополнительную информацию для обеспечения типобезопасности с одной стороны и гибкости с другой. И если заинтересовала, то предлагаю к ознакомлению ресурсы, которые понравorсь самому: #Viacheslav
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION