Рубрики
Без рубрики

Ограниченные универсальные типы и подстановочные знаки – [ООП и Java #6]

Продолжая тему дженериков: Ограниченные универсальные типы расширяет строку> void print(… С пометкой computer science, java, ооп. расширяет строку> void print(… С пометкой computer science, java, ооп.

Продолжая тему дженериков:

  • Ограниченные универсальные типы
 void print(T t){}
  • Подстановочные знаки
Drawer drawer = new Drawer("abc"1);

Ограниченные Универсальные типы

Воспринимайте слово “ограниченный” буквально – ограничивайте что-либо диапазоном возможных значений.

Теперь вернемся к замечанию, сделанному в предыдущей статье, о том, что всякий раз, когда мы используем дженерики, нам все равно требуется как можно больше проверки типов во время компиляции. Это полезная стратегия, потому что ошибки во время компиляции легче обнаружить, чем ошибки во время выполнения. Если мы убедимся, что объекты правильного типа передаются по кругу, у нас будет на одну вещь меньше поводов для беспокойства после компиляции.

// for illustration purposes
class Clothing {};
class Shirt extends Clothing {};
class Tshirt extends Shirt {};

class Drawer {
  T obj;
  Drawer(T obj) {
    this.obj = obj;
  }
  T get() {
    return this.obj;
  }
}

Повторно используя пример объекта Drawer , а также классы, представляющие Одежда , Рубашка и Футболка , давайте посмотрим, как работают параметры ограниченного типа.

Исходная реализация не имеет привязки. Заполнитель T технически может принимать объекты любого возможного типа.

new Drawer(new Clothing());
new Drawer(new String());
new Drawer(new Double());

Однако это может быть не идеально, потому что

Могут быть случаи, когда вы захотите ограничить типы, которые могут использоваться в качестве аргументов типа в параметризованном типе.

Размышляя в русле ООП, мы можем не захотеть, чтобы наш Ящик принимал что-то, что ему не принадлежит (например, ящик Human ?!). Таким образом, мы вполне можем намереваться привязать заполнитель к верхней границе, такой как Одежда , и, следовательно, принимать только замены указанной границы и любого из подклассов. Например, Поскольку расширяет Одежду> , расширяет Одежду> , T может быть заменен Одеждой или Рубашка или даже Футболка (Потому что оба Рубашка и Футболка являются/| Одеждой в порядке наследования).

class Drawer {
  T obj;
  Drawer(T obj) {
    this.obj = obj;
  }
  T get() {
    return this.obj;
  }
}

// usage
new Drawer(new Double()); // ERROR
new Drawer(new Clothing()); // SAFE
new Drawer(new Shirt()); // SAFE
new Drawer(new Tshirt()); // SAFE

Ограниченные универсальные типы реализуются с помощью ключевого слова extends . Обратите внимание, что также возможны множественные границы.

// has the same effect of only extending Clothing
Drawer {
  T obj;
  Drawer(T obj) {
    this.obj = obj;
  }
  T get() {
    return this.obj;
  }
}

Еще один момент, касающийся ограниченного типа, заключается в том, что границы увеличивают количество разрешенных вызовов методов во время компиляции. Без привязки компилятор знает только то, что заполнитель может быть одним из возможных типов. Следовательно, успешный вызов любых методов объекта с типом заполнителя не может быть гарантирован.

Представьте, что вы указали метод, который принимает в качестве аргумента t и вы пытались написать t.length() . Не все объекты имеют метод length() и, следовательно, компилятору небезопасно компилировать код. ЕСЛИ только, учитывая, что все объекты являются подклассами Object , любой объект в Java не будет поддерживать методы, указанные в классе Object . Это означает вызов toString() или equals() on t в порядке.

С ограниченными типами теперь компилятор знает, что объект, помещаемый в заполнитель, будет одним из типов/подтипов. Затем можно безопасно вызывать любые методы, указанные в привязке.

Подстановочные знаки

Прежде чем мы углубимся в подстановочные знаки, нам нужно рассмотреть концепцию сложных и простых типов

// examples of simple
String s;
Clothing c;
Shirt st;

// examples of complex
String[] s;
Clothing[] c;
Shirt[] st;

Массивы Java сложны, и поскольку массивы ковариантны, мы получаем следующие характеристики:

Clothing c = new Shirt(); // SAFE
Clothing[] c = new Shirt[1](); // SAFE

Когда мы используем пример |/Ящика , мы также работаем с этим понятием контейнера. Итак, точно так же, как массивы, которые хранят элементы в коллекции , работает ли следующее?

Clothing c = new Shirt();
Drawer c = new Drawer(new Shirt());

Ранее мы упоминали, что дженерики инвариантны, что означает, что отношение подтипов простых типов не распространяется на сложные типы. Именно поэтому мы также можем опустить тип в правой части знака равенства.

// SAFE
Drawer c = new Drawer(new Clothing); 
// ALSO SAFE
Drawer c = new Drawer<>(new Clothing): 

Если обсуждение на этом закончилось, то нам не понадобятся подстановочные знаки. Тем не менее, отношение подтипов кажется достаточно разумным. Учитывая, что у нас есть это для массивов Java, и они во многих случаях полезны для поддержки полиморфизма, мы можем использовать подстановочные знаки для достижения тех же эффектов для дженериков.

Ящик для одежды, в конечном счете, также является ящиком для рубашек. Мы будем использовать подстановочные знаки или выраженные как ? ,

// SAFE
Drawer d = new Drawer(new Clothing); 
d = new Drawer(new Shirt());

// ALSO SAFE
Drawer s = new Drawer(new Shirt());
Drawer anyDrawer = s;

Для понимания, ? можно рассматривать как “ЛЮБОЙ”. Таким образом, любой ящик может быть ящиком для одежды, и любой ящик также может быть ящиком для рубашек.

Ограниченные подстановочные знаки

Когда мы используем > , мы теряем количество вызовов методов, которые мы можем выполнить, когда мы вынимаем вещи из ящика. Поскольку ящик может быть любого типа, единственная гарантия, которую мы получаем от вещей, которые выходят из ящика, – это то, что они имеют тип Объект . Следовательно, мы можем вызывать только методы уровня Object , такие как toString() или equals() . Знакомая фраза?

Добавление привязки к подстановочным знакам дает нам возможность сначала ограничить диапазон типов, которые могут входить в заполнитель, а затем увеличить количество разрешенных вызовов методов.

// upper-bounded wildcard
// SAFE
Drawer drawerOfShirt;
drawerOfShirt = new Drawer(new Shirt());
Shirt s = drawerOfShirt.get();
drawerOfShirt = new Drawer(new Tshirt());
Shirt s = drawerOfShirt.get();

// ERROR
drawerOfShirt = new Drawer(new Clothing());
Shirt s = drawerOfShirt.get();

Ящик расширяет рубашку> разрешает только объекты типа расширяет рубашку> разрешает только объекты типа Рубашка

Поскольку Одежда не относится к типу/| Рубашка или дочерний элемент |/Рубашка , по ограничению bound это не сработает. Логически, мы не можем допустить, чтобы это было правдой, потому что, если пара брюк вынимается из Ящика <Одежда> , это все равно предмет одежды, но это не рубашка.

Мы также можем сделать то же самое для нижней границы, используя ключевое слово super .

// suppose Drawer contains an update method
class Drawer {
  T obj;
  //...
  void update(T obj) {
    this.obj = obj;
  }
}

// lower-bounded wildcard
// SAFE
Drawer drawer= new Drawer(new Shirt());
drawer.update(new Shirt());
drawer = new Drawer(new Clothing);
drawer.update(new Shirt()); // still safe

Теперь ящик может быть ящиком для рубашки , Одежда и даже Объект .

Рубашка может попасть в ящик независимо от того, на какую ссылку она указывает. Это потому, что Рубашка – это предмет одежды, и залезать в ящик с одеждой тоже разумно.

Принцип Получения И Размещения

Некоторые замечания:

Ковариация

  • Рубашка является подтипом/| Одежды Ящик<Рубашка>
  • является подтипом Ящика расширяет Одежду>

Контравариантность

  • Рубашка является подтипом/| Одежды Ящик<Одежда>
  • является подтипом Ящика супер рубашка>

Обсуждение метода равных

Типичный @Override метода equals/| в Java:

  • Проверьте, является ли объект аргумента точно таким же объектом, как и он сам
  • Если он того же типа, выполните настраиваемое сравнение на основе определенных доступных методов, требуйте приведения типов перед вызовом доступных методов
  • Если они не одного типа, то не равны
// suppose a string is equal to another string if 
// they have the same length
@Override
public boolean equals(Object obj) {
  if (this == obj) {
    return true;
  } else if (obj instanceof String) {
    String s = (String) obj;
    return this.length() == s.length();
  } else {
    return false;
  }
}

С дженериками логично следовать следующей реализации, которая не совсем корректна.

// suppose a drawer is equal to another drawer if
// they have the same content
@Override
public boolean equals(Object obj) {
  if (this == obj) {
    return true;
  } else if (obj instanceof Drawer) {
    Drawer anyDrawer = (Drawer) obj;
    return this.get().equals(anyDrawer.get());
  } else {
    return false;
  }
}

У нас есть две ошибки для приведенного выше кода:

  • недопустимый универсальный тип для instanceof

    • Универсальные типы недоступны во время выполнения из-за стирания типов, которое заключается в том, что универсальные типы удаляются после проверки. Достаточно сказать, что во время выполнения Drawer совпадает с Drawer без типа (также известного как raw type). Причина этого заключается в обеспечении обратной совместимости с кодом Java, который существовал до введения дженериков. Подробнее об этом читайте здесь
  • непроверенные или небезопасные операции
    • Предыдущая ошибка сообщает нам, что мы не можем проверить, имеет ли сравниваемый объект тип Drawer , который можно рассматривать как определенный тип. Если условие if является действительным, и мы выполняем приведение
Drawer otherDrawer = (Drawer) obj;

Правую часть можно рассматривать как приведение Объекта к необработанному типу Выдвижной ящик вместо Выдвижной ящик (опять же из-за стирания типа). Это может быть опасно, потому что предположим, что у нас есть Ящик<Стационарный> , тогда мы знаем , что он имеет тип Ящик , выполнив |/instanceof Drawer . Теперь, если мы приведем его к Drawer , когда T равно Clothing , компилятор может просто продолжить и выполнить приведение, потому что для компилятора мы приводим Выдвижной ящик в Выдвижной ящик/| . Однако у нас есть Ящик <Одежда> другой ящик , указывающий на Ящик<Стационарный> , что не нормально. Обратите внимание, что мы не использовали никаких границ, поэтому должны применяться стандартные инвариантные характеристики дженериков.

Решение здесь заключается в использовании подстановочных знаков.

@Override
public boolean equals(Object obj) {
  if (this == obj) {
    return true;
  } else if (obj instanceof Drawer) {
    Drawer anyDrawer = (Drawer) obj;
    return this.get().equals(anyDrawer.get());
  } else {
    return false;
  }
}

Причина, по которой это работает сейчас, заключается в том, что ? здесь означает любой тип, начиная от Object и заканчивая любым из ваших пользовательских типов. Утверждение, что он приводит к любому типу, немного отличается от утверждения, что он приводит к определенному типу. Можно с уверенностью сказать, что конкретный ящик является одним из возможных ящиков. Любой из возможных ящиков может не быть этим конкретным ящиком.

Опять же, сосредоточив внимание на Правой стороне, мы знаем, что объект является экземпляром Выдвижной ящик . Мы понятия не имеем, что это за ящик. Но в приведении мы позволяем ему приводить к ящику, который может содержать любые возможные типы, что почти похоже на высказывание: “пусть он приводит к чему-то, пока это что-то является ящиком”. Это разумное преобразование. Компилятор не будет жаловаться сейчас, потому что вы здесь очень обобщаете. Побочным эффектом является то, что после приведения вы можете вызывать только наиболее распространенные методы ( Методы уровня объекта , такие как toString() и равно() ) на любом ящике .

Упражнения

Компилируются ли они для следующих утверждений?

// for illustration purposes
class Clothing {};
class Shirt extends Clothing {};
class Tshirt extends Shirt {};

class Drawer {
  Drawer(){};
}

1. Drawer s = new Drawer();
2. Drawer s = new Drawer();
3. Drawer s = new Drawer();
4. Drawer s = new Drawer<>();
5. Drawer s = new Drawer();
6a. Drawer ts = new Drawer();
6b. ts = new Drawer();
7a. Drawer ws = new Drawer();
7b. ws = new Drawer();

Ответы следующие:

  1. Да, 6а. Да
  2. № 6b. Нет
  3. Да, 7а. Да
  4. Да, 7b. Да
  5. Нет

Заключительные мысли

В этой статье многое предстоит осветить, и еще многое осталось не упомянутым. Я думаю, что понимание дженериков придет с увеличением контактов. Я не вдавался в подробности об удалении типов и необработанных типах. Кроме того, объяснение дженериков в контексте Java Collections Framework, возможно, очень полезно, и я намерен поговорить об этом позже.

Одним из ресурсов для дальнейшего чтения является Часто задаваемые вопросы по Java Generics

Оригинал: “https://dev.to/tlylt/bounded-generic-types-wildcards-oop-java-6-1mcc”