Вопросы об интервью по Java Generics (ответы)

Интервью по Java Generics (+ ответы)

1. Вступление

В этой статье мы рассмотрим несколько примеров вопросов и ответов на собеседование с универсальными Java-шаблонами.

Обобщения являются основной концепцией в Java, впервые представленной в Java 5. Из-за этого почти все базы кода Java будут использовать их, почти гарантируя, что разработчик столкнется с ними в какой-то момент. Вот почему так важно правильно их понять, и именно поэтому они с большей вероятностью будут задавать вопросы во время собеседования.

2. Вопросы

Q1. Что такое параметр универсального типа?

Type - это имяclass илиinterface. Как следует из названия,a generic type parameter is when a type can be used as a parameter in a class, method or interface declaration.

Чтобы продемонстрировать это, давайте начнем с простого примера без обобщений:

public interface Consumer {
    public void consume(String parameter)
}

В этом случае тип параметра методаconsume() -String.. Он не параметризован и не настраивается.

Теперь давайте заменим наш типString на общий тип, который мы будем называтьT.. По соглашению он называется так:

public interface Consumer {
    public void consume(T parameter)
}

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

public class IntegerConsumer implements Consumer {
    public void consume(Integer parameter)
}

В этом случае теперь мы можем потреблять целые числа. Мы можем заменить этотtype на все, что нам нужно.

Q2. Каковы преимущества использования универсальных типов?

Одним из преимуществ использования дженериков является избежание затрат и обеспечение безопасности типов. Это особенно полезно при работе с коллекциями. Продемонстрируем это:

List list = new ArrayList();
list.add("foo");
Object o = list.get(1);
String foo = (String) o;

В нашем примере тип элемента в нашем списке неизвестен компилятору. Это означает, что единственное, что может быть гарантировано, это то, что это объект. Поэтому, когда мы получаем наш элемент, объект - это то, что мы получаем обратно. Как авторы кода, мы знаем, что этоString,, но мы должны привести наш объект к нему, чтобы устранить проблему явным образом. Это производит много шума и шаблон.

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

list.add(1)
Object o = list.get(1);
String foo = (String) o;

В этом случае мы получили быClassCastException во время выполнения, посколькуInteger не может быть преобразовано вString.

А теперь давайте попробуем повторить себя, на этот раз используя дженерики:

List list = new ArrayList<>();
list.add("foo");
String o = list.get(1);    // No cast
Integer foo = list.get(1); // Compilation error

Как видим,by using generics we have a compile type check which prevents ClassCastExceptions and removes the need for casting.

The other advantage is to avoid code duplication. Без дженериков мы должны копировать и вставлять один и тот же код, но для разных типов. С дженериками мы не обязаны это делать. Мы можем даже реализовать алгоритмы, которые применяются к универсальным типам.

Q3. Что такое стирание типа?

Важно понимать, что информация об общем типе доступна только компилятору, а не JVM. Другими словами, type erasure means that generic type information is not available to the JVM at runtime, only compile time.

Причина выбора основной реализации проста - сохранение обратной совместимости со старыми версиями Java. Когда универсальный код компилируется в байт-код, это будет так, как если бы универсальный тип никогда не существовал. Это означает, что компиляция будет:

  1. Заменить универсальные типы объектами

  2. Замените ограниченные типы (подробнее об этом в следующем вопросе) на первый связанный класс

  3. Вставьте эквивалент приведений при получении общих объектов.

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

public foo(Consumer consumer) {
   Type type = consumer.getGenericTypeParameter()
}

Приведенный выше пример является псевдокодовым эквивалентом того, что может выглядеть без стирания типа, но, к сожалению, это невозможно. И сноваthe generic type information is not available at runtime.

Q4. Если универсальный тип опущен при создании экземпляра объекта, будет ли код по-прежнему компилироваться?

Поскольку дженерики не существовали до Java 5, их можно вообще не использовать. Например, дженерики были модернизированы для большинства стандартных классов Java, таких как коллекции. Если мы посмотрим на наш список из первого вопроса, то увидим, что у нас уже есть пример пропуска универсального типа:

List list = new ArrayList();

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

Следует помнить, чтоwhile backward compatibility and type erasure make it possible to omit generic types, it is bad practice.

Q5. Чем общий метод отличается от универсального типа?

A generic method is where a type parameter is introduced to a method,living within the scope of that method. Давайте попробуем это на примере:

public static  T returnType(T argument) {
    return argument;
}

Мы использовали статический метод, но при желании могли бы использовать и нестатический. Используя вывод типа (рассмотренный в следующем вопросе), мы можем вызывать это как любой обычный метод, без необходимости указывать аргументы типа, когда мы это делаем.

Q6. Что такое вывод типа?

Вывод типа - это когда компилятор может посмотреть на тип аргумента метода, чтобы вывести обобщенный тип. Например, если мы передалиT методу, который возвращаетT,, компилятор сможет определить тип возвращаемого значения. Давайте попробуем это, вызвав наш общий метод из предыдущего вопроса:

Integer inferredInteger = returnType(1);
String inferredString = returnType("String");

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

Q7. Что такое параметр ограниченного типа?

До сих пор все наши вопросы охватывали аргументы обобщенных типов, которые не ограничены. Это означает, что наши аргументы универсального типа могут быть любого типа, который мы хотим.

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

В качестве примера предположим, что мы хотим, чтобы наш общий тип всегда был подклассом животных:

public abstract class Cage {
    abstract void addAnimal(T animal)
}

Используя extends,, мы заставляемT быть подклассом животного.. Тогда мы могли бы создать клетку для кошек:

Cage catCage;

Но у нас не могло быть клетки объектов, поскольку объект не является подклассом животного:

Cage objectCage; // Compilation error


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

public void firstAnimalJump() {
    T animal = animals.get(0);
    animal.jump();
}

Q8. Возможно ли объявить параметр с несколькими ограниченными типами?

Объявление нескольких границ для наших универсальных типов возможно. В нашем предыдущем примере мы указали одну границу, но мы могли бы также указать больше, если мы хотим:

public abstract class Cage

В нашем примере животное - это класс, а сопоставимый - это интерфейс. Теперь наш тип должен уважать обе эти верхние границы. Если бы наш тип был подклассом animal, но не реализовал сопоставимый, то код не скомпилировался бы. It’s also worth remembering that if one of the upper bounds is a class, it must be the first argument.с

Q9. Что такое подстановочный знак?

A wildcard type represents an unknown type. Он помечается знаком вопроса следующим образом:

public static consumeListOfWildcardType(List list)

Здесь мы указываем список, который может быть любымtype. Мы могли бы передать список чего угодно в этот метод.

Q10. Что такое подстановочный знак с ограничением сверху?

An upper bounded wildcard is when a wildcard type inherits from a concrete type. Это особенно полезно при работе с коллекциями и наследованием.

Давайте попробуем продемонстрировать это с помощью класса фермы, который будет хранить животных, сначала без подстановочного знака:

public class Farm {
  private List animals;

  public void addAnimals(Collection newAnimals) {
    animals.addAll(newAnimals);
  }
}

Если бы у нас было несколько подклассов животных,, таких как cat и dog,, мы могли бы сделать неверное предположение, что можем добавить их всех на нашу ферму:

farm.addAnimals(cats); // Compilation error
farm.addAnimals(dogs); // Compilation error

Это связано с тем, что компилятор ожидает коллекции конкретного типа animal,, а не одного подкласса.

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

public void addAnimals(Collection newAnimals)

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

Q11. Что такое неограниченный подстановочный знак?

Неограниченный подстановочный знак - это подстановочный знак без верхней или нижней границы, который может представлять любой тип.

Также важно знать, что подстановочный знак не является синонимом объекта. Это связано с тем, что подстановочный знак может быть любого типа, тогда как тип объекта является конкретно объектом (и не может быть подклассом объекта). Продемонстрируем это на примере:

List wildcardList = new ArrayList();
List objectList = new ArrayList(); // Compilation error


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

Q12. Что такое подстановочный знак с нижней границей?

Подстановочный знак с ограничением снизу - это когда вместо предоставления верхней границы мы указываем нижнюю границу с помощью ключевого словаsuper. Другими словами,a lower bounded wildcard means we are forcing the type to be a superclass of our bounded type. Давайте попробуем это на примере:

public static void addDogs(List list) {
   list.add(new Dog("tom"))
}

Используяsuper,, мы могли бы вызвать addDogs для списка объектов:

ArrayList objects = new ArrayList<>();
addDogs(objects);


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

Если мы подумаем об этом, мы не сможем добавить собаку в список любого подкласса животных, таких как кошки или даже собаки. Только суперкласс животного. Например, это не скомпилирует:

ArrayList objects = new ArrayList<>();
addDogs(objects);

Q13. Когда бы вы выбрали использование ограниченного типа против верхний ограниченный тип?

При работе с коллекциями общим правилом выбора между подстановочными знаками с верхним или нижним ограничением является PECS. PECS означаетproducer extends, consumer super.

Это можно легко продемонстрировать с помощью использования некоторых стандартных интерфейсов и классов Java.

Producer extends просто означает, что если вы создаете производителя универсального типа, используйте ключевое словоextends. Давайте попробуем применить этот принцип к коллекции, чтобы понять, почему это имеет смысл:

public static void makeLotsOfNoise(List animals) {
    animals.forEach(Animal::makeNoise);
}

Здесь мы хотим вызватьmakeNoise() для каждого животного в нашей коллекции. Это означает, что наша коллекция является производителем,, поскольку все, что мы делаем с ней, - это заставляем ее возвращать животных для выполнения нашей операции. Если бы мы избавились отextends, мы не смогли бы передавать в списках кошек собак, или любые другие подклассы животных. Применяя принцип продления производителя, мы получаем максимально возможную гибкость.

Consumer super означает противоположностьproducer extends.. Все это означает, что если мы имеем дело с чем-то, являющимся элементами потребителей, тогда мы должны использовать ключевое словоsuper. Мы можем продемонстрировать это, повторив наш предыдущий пример:

public static void addCats(List animals) {
    animals.add(new Cat());
}

Мы только добавляем в наш список животных, поэтому наш список животных является потребителем. Вот почему мы используем ключевое словоsuper. Это означает, что мы можем передать список любого суперкласса животных, но не подкласса. Например, если мы попытаемся передать список собак или кошек, код не скомпилируется.

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

Q14. Существуют ли ситуации, когда информация об общем типе доступна во время выполнения?

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

public class CatCage implements Cage

Используя отражение, мы получаем этот параметр типа:

(Class) ((ParameterizedType) getClass()
  .getGenericSuperclass()).getActualTypeArguments()[0];

Этот код несколько хрупкий. Например, это зависит от параметра типа, определенного в непосредственном суперклассе. Но это демонстрирует, что JVM имеет эту информацию типа.