← Academy Blog

Марафон підготовки 2021

Стартував марафон підготовки до тестів - Java трек - Binary Studio Academy 2021! Щотижневі питання з відповідями та пояснення чекають на нашому блозі.

  • [Java] Що буде виведено на екран після виконання цього коду?
    public class Test {
      public static void main(String[] args) {
        int x = 1;
        Integer y = new Integer(x);
        int [] z = {x};func(x, y, z);System.out.print(x);
        System.out.print(y);
        System.out.println(z[0]);
      }static void func (int x, Integer y, int[] z) {
        x++;
        y++;
        z[0]++;
      }
    }
    

    a) 122
    b) 111
    c) 112
    d) 11undefined


    Розгорнути правильну відповідь з поясненням

    Це питання покриває дві теми: Value та Reference типи, та autoboxing-autounboxing. ​ Почнемо з відкидання найбільш очевидного варіанту: в Java немає undefined, при звертанні до неіснуючого елементу массиву буде викинуто IndexOutOfRangeException, тому варіант e) відпадає. ​ ​ Тепер розглянемо наступний варіант на вибування - d). Оскільки int - це value type, то при виклику метода буде передано копію значення, зміна якої ніяк не вплине на оригінальне значення. ​ Також знаючи, що масив - reference тип, можна відкинути варіант b). Оскільки при передачі reference параметрів передається не копія значення, а посилання на купу, де знаходиться значення, зміна значень параметра в методі призводить до зміни оригінального значення. ​ Залишаються два варіанти - a і c. Оскільки типи-обгортки примітивів є об'єктами, відповідно вони є reference типами, можна було б зробити висновок, що варіант a) - правильний. Але слід пригадати, що типи-обгортки примітивів є іммутабельними. Розглянемо невеликий приклад:

    package com.binary_studio_academy;public class Main{
      public static void main(String[] args) {
        Integer a = 0;
        Integer b = a;
        a++;
        System.out.println(a + " " + b);
      }
    }

  • [Java] Який результат виконання цього коду?
    package com.learning;public class Main {
      public static void main(String[] args) {
        BaseLogger l1 = new BaseLogger();
        ChildLogger l2 = new ChildLogger();
        BaseLogger l3 = new ChildLogger();consoleLog(l1);
        consoleLog(l2);
        consoleLog(l3);
      }public static void consoleLog(BaseLogger logger) {
        logger.log();
      }
    }class BaseLogger {
      public void log() {
        System.out.println("Base");
      }
    }class ChildLogger extends BaseLogger {
      @Override
      final public void log() {
        System.out.println("Child");
      }
    }

    a) RuntimeException
    b) BaseChildBase
    c) Помилка компіляції
    d) BaseChildChild


    Розгорнути правильну відповідь з поясненням

    Для розв'язання задачі скористаємось методом виключення:

    • Код успішно скомпілюється, тому варіант 3 відпадає;
    • Runtime виключень також немає, тому варіант 1 - теж не правильний;
    • Залишається або BaseChildBase, або BaseChildChild. Оскільки при створенні змінної l3 ми створюємо екземпляр класу ChildLogger, у якому метод log перевизначено, у консоль буде виведено BaseChildChild. Отже, правильна відповідь 4.

    Це питання покриває реалізацію двох аспектів ООП у Java - subtype polymorphism та наслідування. Розглянемо більш цікавий приклад використання наслідування:

    package com.learning;
    ​
    ​
    public class Main {
      public static void main(String[] args) {
        BaseValueCalculator l1 = new BaseValueCalculator();
        BaseValueCalculator l2 = new AdvancedValueCalculator();System.out.println(l1.calculate().value());
        System.out.println(l2.calculate().value());
      }
    }class BaseValue{
      protected final String val;
      public BaseValue(String val){
        this.val = val;
      }
      public String value(){
        return this.val;
      }
    }class CapitalizedValue extends BaseValue{
      public CapitalizedValue(String val){
        super(val);
      }
      @Override
      public String value(){
        return this.val.toUpperCase();
      }
    }class BaseValueCalculator {
      public BaseValue calculate() {
        return new BaseValue("I'm simple calculator!");
      }
    }class AdvancedValueCalculator extends BaseValueCalculator {
      @Override
      final public CapitalizedValue calculate() {
        return new CapitalizedValue("I'm advanced calculator!");
      }
    }

    Який результат виконання цього коду?

    1. RuntimeException
    2. I'm simple calculator!I'M ADVANCED CALCULATOR!
    3. Помилка компіляції
    4. I'm simple calculator!I'm advanced calculator!

    Головним питанням у цьому прикладі є те, чи цей код скомпілюється. Return type методів у Java [коваріантний](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science), отже, його можна перевизначити у класах нащадках. Розглянемо щє один приклад, цього разу спробуємо погратись з типами параметрів:

    package com.learning;
    ​
    ​
    public class Main {
      public static void main(String[] args) {
        BaseLogger l1 = new BaseLogger();
        ChildLogger l2 = new ChildLogger();
        BaseValue message = new BaseValue("lsp is a myth");
        l1.log(message);
        l2.log(message);
      }
    }class BaseLogger {
      public void log(BaseValue value) {
        System.out.println(value.value());
      }
    }class ChildLogger extends BaseLogger {
      final public void log(CapitalizedValue value) {
        System.out.println(String.format("<<<%s>>>", value.value()));
      }
    }class BaseValue {
      protected final String val;public BaseValue(String val) {
        this.val = val;
      }public String value() {
        return this.val;
      }
    }class CapitalizedValue extends BaseValue {
      public CapitalizedValue(String val) {
        super(val);
      }@Override
      public String value() {
        return this.val.toUpperCase();
      }
    }

    ​ Який результат виконання даного коду?

    1. RuntimeException
    2. lsp is a myth <<<lsp is a myth>>>
    3. Помилка компіляції
    4. lsp is a myth lsp is a myth

    ​ Якщо ви відповіли “помилка компіляції”, то вітаю вас: ви помилились. Правильна відповідь - 4, привітаємо всіх, хто так відповів. На відміну від return значень, параметри методу є інваріантними. У прикладі ми спробували зробити параметр нащадка коваріантним, але навіть якщо ми використаємо контраваріантний тип параметру(BaseLogger приймав би CapitalizedValue, а ChildLogger - BaseValue), все одно перевизначення методу log не відбулось би. Тому у ChildLogger відбувається не overriding, а overloading методу log, і при виклику методу log з параметром типу BaseValue буде викликано реалізацію методу log батьківського класу, а при виклику з параметром типу CapitalizedValue буде викликано перевантажену реалізацію з класу ChildLogger. На практиці, хоч анотація @Override не є обов'язковою, її слід завжди використовувати для запобігання неочікуваним перевантаженням замість перевизначень. ​

    На цьому на сьогодні все, дякуємо за увагу. Якщо хочеш більше дізнатись про наслідування та варіантність у Java, тицяй на посилання знизу. Побачимось в Академії ;) ​