1. Введение
Система типов Java — это тема, которую часто поднимают на технических собеседованиях с разработчиками Java. В этой статье рассматриваются некоторые важные вопросы, которые задают чаще всего, и на них может быть сложно ответить правильно.
2. Вопросы
Q1. Опишите место класса объекта в иерархии типов. Какие типы наследуются от объекта, а какие нет? Наследуются ли массивы от объекта? Можно ли присвоить лямбда-выражение объектной переменной?
java.lang.Object находится
на вершине иерархии классов в Java. Все классы наследуются от него либо явно, либо неявно (когда в определении класса опущено ключевое слово extends ), либо транзитивно через цепочку наследования.
Однако есть восемь примитивных типов, которые не наследуются от Object
, а именно: boolean
, byte
, short
, char
, int
, float
, long
и double
.
Согласно Спецификации языка Java, массивы также являются объектами. Они могут быть назначены ссылке на объект
, и для них могут быть вызваны все методы объекта .
Лямбда-выражения нельзя назначать непосредственно переменной объекта, поскольку объект
не
является функциональным интерфейсом. Но вы можете назначить лямбду переменной функционального интерфейса, а затем назначить ее переменной объекта
(или просто назначить ее переменной объекта, одновременно приведя ее к функциональному интерфейсу).
Q2. Объясните разницу между примитивными и ссылочными типами.
Ссылочные типы наследуются от верхнего класса java.lang.Object
и сами являются наследуемыми (за исключением конечных
классов). Примитивные типы не наследуются и не могут быть подклассами.
Значения аргументов с примитивным типом всегда передаются через стек, что означает, что они передаются по значению, а не по ссылке. Это имеет следующий смысл: изменения, внесенные в значение примитивного аргумента внутри метода, не распространяются на фактическое значение аргумента.
Примитивные типы обычно хранятся с использованием базовых аппаратных типов значений.
Например, для хранения значения int
может использоваться 32-битная ячейка памяти. Ссылочные типы вводят накладные расходы на заголовок объекта, который присутствует в каждом экземпляре ссылочного типа.
Размер заголовка объекта может быть весьма значительным по сравнению с размером простого числового значения. Вот почему в первую очередь были введены примитивные типы — для экономии места на служебных объектах. Недостатком является то, что не все в Java технически является объектом — примитивные значения не наследуются от класса Object .
Q3. Опишите различные типы примитивов и объем памяти, который они занимают.
Java имеет 8 примитивных типов:
boolean
— логическое значениеtrue
/false
. Размер логического значения не определяется спецификацией JVM и может различаться в разных реализациях.byte
— 8-битное значение со знаком,short
— 16-битное значение со знаком,char
— беззнаковое 16-битное значение,int
— 32-битное значение со знаком,long
— подписанное 64-битное значение,float
— 32-битное значение с плавающей запятой одинарной точности, соответствующее стандарту IEEE 754,double
— 64-битное значение с плавающей запятой двойной точности, соответствующее стандарту IEEE 754.
Q4. В чем разница между абстрактным классом и интерфейсом? Каковы варианты использования одного и другого?
Абстрактный класс — это класс
с модификатором abstract в его определении.
Его нельзя создать, но можно создать подкласс. Интерфейс — это тип, описываемый ключевым словом interface
. Его также нельзя реализовать, но можно реализовать.
Основное различие между абстрактным классом и интерфейсом заключается в том, что класс может реализовывать несколько интерфейсов, но расширять только один абстрактный класс.
Абстрактный класс
обычно используется в качестве базового типа в какой-либо иерархии классов и обозначает основное назначение всех классов, которые от него наследуются.
Абстрактный класс
также может реализовывать некоторые базовые методы, необходимые во всех подклассах. Например, большинство коллекций карт в JDK наследуются от класса AbstractMap
, который реализует многие методы, используемые подклассами (например, метод equals
).
Интерфейс определяет некоторый контракт, с которым соглашается класс. Реализованный интерфейс может обозначать не только основное назначение класса, но и некоторые дополнительные контракты.
Например, если класс реализует интерфейс Comparable
, это означает, что экземпляры этого класса можно сравнивать, какова бы ни была основная цель этого класса.
Q5. Каковы ограничения для членов (полей и методов) типа интерфейса?
Интерфейс может объявлять поля, но они неявно объявляются как public
, static
и final
, даже если вы не указываете эти модификаторы. Следовательно, вы не можете явно определить поле интерфейса как private
. По сути, интерфейс может иметь только поля-константы, а не поля-экземпляры.
Все методы интерфейса также неявно являются общедоступными
. Они также могут быть (неявно) abstract
или default
.
Q6. В чем разница между внутренним классом и статическим вложенным классом?
Проще говоря, вложенный класс — это класс, определенный внутри другого класса.
Вложенные классы делятся на две категории с очень разными свойствами. Внутренний класс — это класс, экземпляр которого не может быть создан без предварительного создания экземпляра охватывающего класса, т. е. любой экземпляр внутреннего класса неявно связан с некоторым экземпляром охватывающего класса.
Вот пример внутреннего класса — вы можете видеть, что он может получить доступ к ссылке на экземпляр внешнего класса в форме конструкции OuterClass1.this
:
public class OuterClass1 {
public class InnerClass {
public OuterClass1 getOuterInstance() {
return OuterClass1.this;
}
}
}
Чтобы создать такой внутренний класс, вам нужно иметь экземпляр внешнего класса:
OuterClass1 outerClass1 = new OuterClass1();
OuterClass1.InnerClass innerClass = outerClass1.new InnerClass();
Статический вложенный класс совсем другой. Синтаксически это просто вложенный класс с модификатором static в его определении.
На практике это означает, что этот класс может быть создан как любой другой класс, без привязки его к какому-либо экземпляру окружающего класса:
public class OuterClass2 {
public static class StaticNestedClass {
}
}
Чтобы создать экземпляр такого класса, вам не нужен экземпляр внешнего класса:
OuterClass2.StaticNestedClass staticNestedClass = new OuterClass2.StaticNestedClass();
Q7. Есть ли в Java множественное наследование?
Java не поддерживает множественное наследование для классов, что означает, что класс может наследоваться только от одного суперкласса.
Но вы можете реализовать несколько интерфейсов с помощью одного класса, и некоторые из методов этих интерфейсов могут быть определены как используемые по умолчанию
и иметь реализацию. Это позволяет вам иметь более безопасный способ смешивания различных функций в одном классе.
Q8. Что такое классы-оболочки? Что такое автобокс?
Для каждого из восьми примитивных типов в Java существует класс-оболочка, который можно использовать для обертывания примитивного значения и использования его как объекта. Эти классы, соответственно, Boolean
, Byte
, Short
, Character
, Integer
, Float
, Long
и Double
. Эти оболочки могут быть полезны, например, когда вам нужно поместить примитивное значение в общую коллекцию, которая принимает только ссылочные объекты.
List<Integer> list = new ArrayList<>();
list.add(new Integer(5));
Чтобы избежать необходимости вручную преобразовывать примитивы туда и обратно, компилятор Java обеспечивает автоматическое преобразование, известное как автоупаковка/автораспаковка.
List<Integer> list = new ArrayList<>();
list.add(5);
int value = list.get(0);
Q9. Опишите разницу между equals() и ==
Оператор == позволяет сравнивать два объекта на предмет «одинаковости» (т. е. того, что обе переменные ссылаются на один и тот же объект в памяти). Важно помнить, что ключевое слово new
всегда создает новый объект, который не будет передавать равенство ==
с любым другим объектом, даже если они имеют одинаковое значение:
String string1 = new String("Hello");
String string2 = new String("Hello");
assertFalse(string1 == string2);
Также оператор == позволяет сравнивать примитивные значения:
int i1 = 5;
int i2 = 5;
assertTrue(i1 == i2);
Метод equals()
определен в классе java.lang.Object
и поэтому доступен для любого ссылочного типа. По умолчанию он просто проверяет, что объект тот же, с помощью оператора ==. Но обычно это переопределяется в подклассах, чтобы обеспечить конкретную семантику сравнения для класса.
Например, для класса String
этот метод проверяет, содержат ли строки одинаковые символы:
String string1 = new String("Hello");
String string2 = new String("Hello");
assertTrue(string1.equals(string2));
Q10. Предположим, у вас есть переменная, которая ссылается на экземпляр типа класса. Как проверить, что объект является экземпляром этого класса?
Вы не можете использовать ключевое слово instanceof
в этом случае, потому что оно работает только в том случае, если вы предоставляете фактическое имя класса как литерал.
К счастью, в классе Class
есть метод isInstance
, который позволяет проверить, является ли объект экземпляром этого класса:
Class<?> integerClass = new Integer(5).getClass();
assertTrue(integerClass.isInstance(new Integer(4)));
Q11. Что такое анонимный класс? Опишите вариант его использования.
Анонимный класс — это одноразовый класс, который определяется в том же месте, где необходим его экземпляр. Этот класс определен и создан в одном и том же месте, поэтому ему не нужно имя.
До Java 8 вы часто использовали анонимный класс для определения реализации интерфейса с одним методом, например Runnable
. В Java 8 вместо отдельных интерфейсов абстрактных методов используются лямбда-выражения. Но у анонимных классов все еще есть варианты использования, например, когда вам нужен экземпляр интерфейса с несколькими методами или экземпляр класса с некоторыми дополнительными функциями.
Вот как вы можете создать и заполнить карту:
Map<String, Integer> ages = new HashMap<String, Integer>(){{
put("David", 30);
put("John", 25);
put("Mary", 29);
put("Sophie", 22);
}};