JavaRush /Java блог /Random UA /Історія однієї співбесіди: цікаві питання
GuitarFactor
30 рівень
Санкт-Петербург

Історія однієї співбесіди: цікаві питання

Стаття з групи Random UA
Нещодавно мені довелося відвідати співбесіду на позицію стажера в одній із великих IT-компаній. Історія однієї співбесіди: цікаві питання - 1Це була моя перша IT-співбесіда і, на мій погляд, вона видалася цікавою. Загалом мене "допитували" більше 3 годин (цьому передували ще домашні завдання та тест в офісі на комп'ютері). Хочу віддати належне співбесідному, який не ставив хрест, коли я відповідав на запитання невірно, а за допомогою своїх навідних питань змушував мене вдумуватись і приходити до вірної відповіді. Нижче я представлю кілька "замальовок" - на мій погляд, досить цікавих питань, деякі з яких дали мені глибше розуміння окремих аспектів Java. Можливо, комусь ці речі видадуться очевидними, але думаю, знайдеться ті, для кого це буде корисно. Нижче наведені нижче фрази: Закадрові пояснення і мої думки — курсивом Мої відповіді — звичайним шрифтом З передісторією закінчабо, перейдемо до справи)

Замальовка 1. "Здавалося б простий метод"

Напишіть, як би ви реалізували метод, що повертає результат поділу числа а на число b Співрозмовний пише на листку
int divide(int a, int b) {
}
*Я недовірливо зиркнув на листок із сигнатурою методу. У чому каверза?* Пишу:
int divide(int a, int b) {
    return a/b;
}
А якісь проблеми можуть бути з цим методом? *Ловлю дииииикий тупняк* Начебто ні.. Далі слідує законне питання: А якщо b = 0? *Воу, мене зараз виженуть з цього кабінету, якщо продовжу в тому ж дусі!* Ах так, зрозуміло. Тут у нас аргументи типу int, тому буде викинуто Arithmetic Exception. Якщо б аргументи були типу float або double, вийшла б Infinity. А що з цим робитимемо? Починаю писати try/catch
int divide(int a, int b) {
    try {
        return a/b;
    } catch (Exception e) {
        e.printStackTrace();
        return ... // ??? what the hack?
    }
}
*Доходжу до return і підвисаю: адже щось треба повернути у разі помилки. Але як це “щось” відрізнити від результату обчислення?* А що повертатимемо? Гм… Я б змінив тип змінної, що повертається на Integer і в разі ексепшна повертав би null. Уявімо, що не можемо змінювати тип. Чи можемо якось викрутитися? Може, можемо ще щось зробити з ексепшном? *Тут дійшло* Можемо ще прокинути у зухвалий метод! Правильно. Як він виглядатиме?
int divide(int a, int b) throws ArithmeticException{
    return a/b;
}

void callDivide(int a, int b) {
    try {
        divide(a, b);
    } catch (ArithmeticException e) {
        e.printStackTrace();
    }
}
А обробляти виняток обов'язково? Так, ми явно прокидаємо його з методу divide. (*Тут я помабовся! Далі слідують навідні питання співбесідуючого, щоб прийти до правильної відповіді*) А Arithmetic Exception - це який виняток – checked чи unchecked? Це Runtime exception, означає unchecked. *Тут слідує вбивче питання* Тобто виходить, за Вашими словами, якщо ми в сигнатурі методу вказали throws Arithmetic Exception, то воно стало checked винятком? * Ух е! * Напевно ... ні. Так, не стало. Якщо ми вказали в сигнатурі throws /unchecked exception/ ми всього лише попереджаємо, що метод може викидати виняток, але обробляти його в методі, що викликає, не обов'язково. Із цим розібралися. А ще щось можемо зробити, щоб уникнути помилок? * Після деяких роздумів * Так, можемо ще перевіряти, if (b = = 0). І виконувати якусь логіку. Правильно. Таким чином, ми можемо піти трьома шляхами:
  • try/catch
  • throws - прокид у викликальний метод
  • перевірка аргументів
У цьому випадку з divideяким із методів Вам здається кращим?
Я вибрав прокид виключення в зухвалий спосіб, т.к. У методі divide не зрозуміло, як цей ексепшн обробити і який результат типу intповернути у разі помилки. А в зухвалому методі використав би перевірку аргументу на рівність нулю. Начебто така відповідь задовольнила співбесідуючого, але якщо чесно, не впевнений, що ця відповідь однозначна))

Зарисовка 2. "Хто швидше?"

Після стандартного питання, чим ArrayList відрізняється від LinkedList, був такий: Що відбудеться швидше - вставка елемента в середину ArrayListабо в середину LinkedList? *Тут мене перемкнуло, я згадав, що скрізь читав щось на кшталт "використовуйте LinkedListдля вставки або видалення елементів у середину списку". Удома навіть перевіряв ще раз по лекціях JavaRush, там фраза: “якщо ти збираєшся вставляти (або видаляти) в середину колекції багато елементів, то тобі краще використовувати LinkedList. У решті випадків – ArrayList.” Відповів на автоматі* Швидше буде з LinkedList. Поясніть будь ласка
  1. Для того щоб вставити елемент у середину ArrayList, ми за константний час знаходимо елемент у списку, а потім перераховуємо індекси елементів праворуч від лінійного часу, що вставляється.
  2. Для LinkedList.. Ми спочатку за лінійний час доходимо до середини, а потім за константний час вставляємо елемент, змінюючи для сусідніх елементів посилання.
То виходить, що швидше? Гм… Виходить однаково. А коли все LinkedListж таки швидше? Виходить, що колись вставляємо в першу половину списку. Наприклад, якщо вставляти в самий початок, ArrayListдоведеться перерахувати всі індекси до самого хвоста, а LinkedListвсього лише поміняти посилання у першого елемента. Мораль: не вірте дослівно всьому, що написано, навіть на JavaRush!)

Замальовка 3. “Куди ж без equals та hashcode!”

Розмова про equals і hashcode була дуже довга – як перевизначаємо, яка реалізація в Object, що відбувається під капотом, коли вставляється елемент HashMapі т.д. Наведу лише низку цікавих на мій погляд моментів* Уявіть, що ми створабо клас
public class A {
    int id;

    public A(int id) {
        this.id = id;
    }
}
І не перевизначабо equalsі hashcode. Опишіть, що станеться під час виконання коду
A a1 = new A(1);
A a2 = new A(1);
Map<A, String> hash = new HashMap<>();
hash.put(a1, "1");
hash.get(a2);
*Добре що перед співбесідою спеціально приділив пару днів на розуміння основних алгоритмів, їх складності та структур даних – дуже допомогло, дякую CS50!*
  1. Створюємо два екземпляри класу A

  2. Створюємо порожню карту, яка за замовчуванням має 16 кошиків. Як ключ виступає об'єкт класу A, в якому не перевизначені методи equalsі hashcode.

  3. Кладемо a1в карту. Для цього спочатку обчислюємо хеш a1.

    Чому дорівнюватиме хеш?

    Адреса осередку в пам'яті – це реалізація методу з класуObject

  4. Виходячи з хеша, обчислюємо індекс корзини.

    А як ми можемо його вирахувати?

    *Тут на жаль я не дав зрозумілу відповідь. У вас є довге число - хеш, і є 16 кошиків - як визначити індекс, щоб об'єкти з різним хеш рівномірно розподілялися по кошиках? Я міг би припустити, що індекс обчислюється так:

    int index = hash % buckets.length

    Вже вдома подивився, що оригінальна реалізація у вихідниках дещо відрізняється.

    static int indexFor(int h, int length)
    {
        return h & (length - 1);
    }
  5. Перевіряємо, що не сталося колізій та вставляємо a1.

  6. Переходимо до методу get. Примірники a1 і a2 гарантовано мають різну hash(різну адресау в пам'яті), тому ми нічого не знайдемо за цим ключем

    А якщо перевизначимо тільки hashcodeв класі A і спробуємо вставити в хешмап спочатку пару з ключем a1, а потім a2?

    Тоді спочатку знайдемо потрібний кошик hashcode- ця операція буде виконана коректно. Далі почнемо йти по об'єктах Entryу прикріпленому до кошика ЛінкедЛисту та порівнювати ключі по equals. Т.к. equalsне перевизначено, то береться базова реалізація з класу Object– порівняння за посиланням. a1 і a2 гарантовано мають різні посилання, тому ми “промахнемося” повз вставлений елемент a1, і a2 буде поміщений в ЛінкедЛіст як новий вузл.

    Який висновок? Чи можна використовувати як ключ в HashMapоб'єкт із неперевизначеними equalshashcode?

    Ні, не можна.

Замальовка 4. "Давайте навмисне зламаємо!"

Після запитань про Error і Exception виникло таке запитання: Напишіть простий приклад, коли функція викине StackOverflow. * Тут я згадав, як мене кумарив цей ерор, коли я намагався написати якусь рекурсивну функцію . *Далі я став щось мудрувати, в результаті співбесідующий допоміг, все виявилося просто*
void sof() {
    sof();
}
А чим цей ерор відрізняється від OutOfMemory? *Тут я не відповів, вже потім зрозумів, що це було питання на знання Stackта Heapпам'яті Java (у Stack зберігаються виклики та посилання на об'єкти, а в Heap пам'яті – самі об'єкти). Відповідно, StackOverflow викидається, коли більше немає місця в Stackпам'яті для чергового виклику методу, а OutOfMemoryмісце під об'єкти закінчилося в Heapпам'яті*
Ось такі моменти із співбесіди мені запам'яталися. На стажування в результаті мене взяли, так що попереду у мене 2,5 місяці навчання і, якщо все складеться добре, робота в компанії) Якщо буде цікаво, можу написати ще одну статейку, вже менше, з розбором простого, але показового завдання, яке мені дали на співбесіді до іншої компанії. На цьому у мене все, сподіваюся, комусь ця стаття допоможе поглибити або розставити свої знання по поличках. Усім приємне навчання!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ