← Academy Blog

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

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

  • [Java] Яким буде результат виконання даного коду?
    package com.learning;
    
    public class Main {
      public static void main(String[] args) {
        try {
          // business logic
        }
        catch (ArithmeticException e) {
          // error handler logic
        }
        catch (Exception e) {
          // error handler logic
        }
        catch (RuntimeException e) {
          // error handler logic
        }
      }
    }

    a) Код скомпілюється без помилок b) Помилка компіляції через те, що класу ArithmeticException не існує c) Помилка компіляції через те, що клас Exception перехоплюється раніше, ніж RuntimeException d) Помилка компіляції через те, що блок try {} не містить коду

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

    Уважно прочитавши варіанти відповідей, ми зрозуміємо, що в цьому питанні якість коду може бути досить поганою, тому що в більшості варіантів згадується помилка компіляції. Спочатку необхідно зрозуміти, чи код написаний синтаксично правильно, і якщо є сумнів, то визначитися, яка підтема зачіпається, та в чому полягає помилка. З контексту стає зрозуміло, що перевіряються знання особливостей порядку обробки виключень в Java.

    Спочатку варто згадати, що стандартний клас-виключення ArithmeticException існує :)

    Для того, щоб правильно відповісти, необхідно знати базову ієрархію класів винятків і нюанс використання цієї ієрархії в послідовності блоків "catch". У нашому випадку ієрархія виглядає таким чином (...-> означає успадковується від):

    ArithmeticException -> RuntimeException -> Exception

    Сама ж помилка компіляції все-таки буде, оскільки ієрархія блоків "catch" в коді вибудувана неправильно. Правильним є варіант с). Така логіка забезпечує те, що виключення будуть оброблятися в послідовності від нащадка до предку (from child to parent), що дозволяє описати алгоритм обробки більш прямолінійно, рухаючись як би від приватного до більш загального. В цілому це також зробить код більш читабельним.

    Додатково:

    https://airbrake.io/blog/java-exception-handling/the-java-exception-class-hierarchy

    https://codegym.cc/groups/posts/exceptions-in-java

  • [Java] Приклад якого патерну проектування ООП представлений в коді?
    package com.learning;
    
    abstract class Smartphone {
    
      protected final String name;
    
      public Smartphone(String name) {
        this.name = name;
      }
    
      public String getName() {
        return name;
      }
    }
    
    class Iphone extends Smartphone {
      public Iphone() {
        super("Iphone");
      }
    }
    
    class Nokia extends Smartphone {
      public Nokia() {
        super("Nokia");
      }
    }
    
    interface SmartphoneProducer {
      Smartphone produce();
    }
    
    final class ChinaProducer implements SmartphoneProducer {
      public Smartphone produce() {
        return new Iphone();
      }
    }
    
    final class NonExistentProducer implements SmartphoneProducer {
      public Smartphone produce() {
        return new Nokia();
      }
    }

    a) Декоратор
    b) Стратегія
    c) Фабричний метод
    d) Абстрактна фабрика

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

    Питання є чисто теоретичним. Жодних помилок компіляції та інших каверз тут на нас не чекає (круто ж :)). Необхідно відразу зробити невеликий дисклеймер, що відповісти правильно на це питання допоможе або кмітливість рівня 80, або ваші знання теорії і прикладів патернів проектування, які знаходяться безпосередньо у вашій пам'яті. Практика показує, що покладатися все ж варто на 2-й варіант.

    Самі патерни, як і будь-які інші сучасні, загальноприйняті поняття та підходи в розробці (існує також термін "best practices"), усталені і протестовані протягом чималого часу, з'явилися на світ, звичайно ж, не випадково. Програмісти, щодня виконуючи завдання з автоматизації і спрощення якихось бізнес-процесів, часто стикалися з досить схожим колом завдань день за днем, тиждень за тижнем і т.д. Звідси і почали з'являтися різні прийоми і методики, що описують рішення якоїсь конкретної задачі, а також практики для спрощення повсякденного життя розробника і, відповідно, економії часу на написання коду і уникнення потреби винаходити так звані "велосипеди". Надалі ці підходи і практики були описані в книгах, статтях, відеокурсах, і неважливо, чи стосується це написання коду, розгортання серверів для веб додатків, оптимізації структури та роботи з базами даних, чи навіть навчання свого власного штучного інтелекту.

    Резюмуючи сказане вище, можна зробити висновок, що є ряд речей, які добре було б знати, і на які варто опиратися при вирішенні більшості повсякденних задач, оскільки дуже ймовірно, що в більшості випадків їх знання допоможе спростити процес написання програмного коду та забезпечити необхідну якість остаточного рішення.

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

    Тому раджу вам все ж ознайомитися з класифікацією патернів і декількома базовими, які найчастіше зустрічаються в повсякденному житті:

    • Singleton
    • Factory method
    • Abstract Factory
    • Decorator
    • Adapter
    • Builder
    • Strategy

    І, звісно, відповідь на запитання - варіант d).

    Додатково вам допоможуть:

    https://refactoring.guru/

    https://github.com/RefactoringGuru/design-patterns-java

    https://github.com/iluwatar/java-design-patterns

    https://github.com/kamranahmedse/design-patterns-for-humans (приклади на PHP, але все досить доступно)

  • [Java] Які типи аргументів буде приймати метод?
    package com.learning;
    
    public class Main {
      public static <T extends Vacation> void method(T param) {
        // ...
      }
    }
    
    class Vacation {}
    
    class Quarantine extends Vacation {}

    a) Object, Vacation
    b) Тільки нащадки Vacation
    c) Vacation і його нащадки
    d) Будь-які типи

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

    Традиційно на початку варто визначити, що перевіряється в даному питанні. Які підтеми зачіпаються? У цьому випадку точно зрозуміло, що це Generics.

    Що ж собою являє цей механізм? Узагальнені типи (вони ж Generics) є дуже потужною особливістю інструментарію деяких мов програмування. Багато статично типізованих мов (Java, Kotlin, C++, C#) імплементують цю фічу. Якщо говорити дуже коротко, вони існують для того, щоб мати можливість описувати функціональність і алгоритми обробки даних, які не залежать від конкретного типу цих даних. Адже є чималий ряд ситуацій, коли логіка алгоритму є універсальною незалежно від того, чи працюємо ми з числами, рядками, об'єктами, колекціями об'єктів або чисел і т.д. Хорошим прикладом будуть мабуть стандартні структури даних List, Set, Map та ін.

    Найцікавіша частина в питанні полягає в фрагменті коду "". Щоб правильно на нього відповісти, необхідно точно знати, що таке "wildcards" та особливість поведінки ключового слова "extends" в цій конструкції. Згідно зі специфікацією мови Java правильним буде варіант с), який говорить про те, що або сам клас Vacation, або його спадкоємці можуть передаватися в якості узагальненого типу T.

    Також радимо звернути увагу і вивчити нюанси схожого використання узагальнених типів, наприклад "<? super Vacation>".

    Додатково можна подивитися тут

    https://javarush.ru/groups/posts/2324-wildcards-v-generics

    https://www.baeldung.com/java-generics

  • [Java] Яке значення матиме змінна message?
    package com.learning;
    
    import java.util.Queue;
    import java.util.LinkedList;
    
    public class Main {
      public static void main(String[] args) {
        Queue<String> messages = new LinkedList();
    
        messages.add("Message 1");
        messages.add("Message 2");
        messages.add("Message 3");
        messages.add("Message 4");
        messages.add("Message 5");
    
        RemoveQueueMessage lambda = msgPool -> msgPool.remove();
    
        String message = lambda.remove(messages);
    
        message.toLowerCase();
      }
    }
    
    @FunctionalInterface
    interface RemoveQueueMessage {
      String remove(Queue<String> messages);
    }

    a) Message 5
    b) message 5
    c) Message 1
    d) message 1
    e) Compilation error

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

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

    Спочатку нам слід визначити, чи виникне помилка компіляції. Для цього треба знати ієрархію основних класів структур даних в так званому Java collections framework (основні структури як List, Queue, Set, Map, Collection), а також особливість визначення лямбда-виразів в Java 8.

    Спробуємо розібратися :)

    Класс LinkedList імплементує інтерфейс Deque, який своєю чергою наслідується від інтерфейсу Queue. Відповідно, рядок “Queue messages = new LinkedList();” ніяких помилок не викличе.

    Наступний потенційно небезпечний крок - це оголошення об’єкту типу “RemoveQueueMessage”. Тіло інтерфейсу описане без помилок, але цікавою для нас є анотація “@FunctionalInterface”. Вона вказує на те, що оголошений нами лямбда-вираз типу “RemoveQueueMessage” може містити лише один метод, і в нашому разі це “remove”. Саме тіло лямбда-виразу описане рядком “RemoveQueueMessage lambda = msgPool -> msgPool.remove();” відповідно до специфікації і не містить помилок. Виклик цього виразу в рядку “lambda.remove(messages);” здійснюється з правильними параметрами. Можемо зробити висновок, що помилки компіляції не буде.

    Все, що залишилося - визначитися з правильною відповіддю. В змінну message запишеться значення методу “msgPool.remove()”, і цим значенням є “Message 1” відповідно до принципу FIFO, на якому грунтується структура даних Queue.

    Це значення не зміниться і не переведеться у нижній регістр в останньому рядку. Варто пам’ятати, що деякі об’єкти (String, Int та ін.) є незмінними (immutable). Тобто, будь-яка зміна створює новий об’єкт такого ж типу, але з новим значенням. Якщо уважно проаналізуємо код, то помітимо, що змінна message просто викликає метод “message.toLowerCase()”, але її значення не переприсвоюється, і отже не буде змінене.

    Тепер ми можемо точно визначити правильну відповідь, і нею є варіант c.

    Додатково можна прочитати тут:

    https://habr.com/ru/post/237043/

    https://javarush.ru/groups/posts/845-lambda-vihrazhenija-na-primerakh

    https://www.baeldung.com/java-8-lambda-expressions-tips

    https://www.tutorialspoint.com/data_structures_algorithms/dsa_queue.htm

  • [Java] Яким буде результат виконання даного коду?
    package com.learning;
    
    public class Main {
      public static void main(String[] args) {
        Logger.console("Hello!");
      }
    }
    
    interface Logger {
    
      default void file(String str) {
        // file logging logic
      }
    
      static void console(String msg) {
        System.out.println(msg);
      }
    }

    a) Помилка компіляції, тому що метод інтерфейсу не може містити ключового слова default.
    b) Буде виведений рядок "Hello!".
    c) Помилка компіляції, тому що метод інтерфейсу не може бути статичним.
    d) Помилка компіляції, тому що не можна звертатися до статичного методу через інтерфейс Logger. Спочатку потрібно описати клас, який імплементує цей інтерфейс, і звертатися вже до нього.

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

    У цьому питанні перевіряється знання синтаксису і нюансів версії Java 8 і вище. Правильним є варіант "b" - виведеться рядок "Hello!". Спробуємо розібратися, чому саме цей варіант правильний, а для цього проаналізуємо інші варіанти і спробуємо зрозуміти, чому ж саме вони не підходять.

    Варіант "а" говорить про те, що ключове слово default викличе помилку компіляції. Це неправильно, адже починаючи з версії Java 8 в специфікацію мови була додана можливість описати реалізацію методу інтерфейсу за замовчуванням, і для цього використовується це ключове слово. Нова функціональність дає можливість описати логіку методу всередині інтерфейсу і класи, які в свою чергу будуть імплементувати цей інтерфейс, можуть не перевизначати цей метод.

    У варіанті “c” стверджується, що помилку компіляції викличе ключове слово "static" перед методом. Цей варіант також є помилковим. Починаючи з 8-ї версії дозволяється оголошувати статичні методи всередині інтерфейсу і використовувати їх звертаючись безпосередньо до імені цього інтерфейсу. Зважаючи на це, варіант "d" також є некоректний.

    Підводячи підсумки, варто згадати, що мова Java стрімко розвивається, і стежити за оновленням специфікації і додаванням / видаленням можливостей в синтаксисі мови доводиться постійно (з певною періодичністю звичайно). Це потрібно для того, щоб бути в курсі останніх змін в екосистемі мови і мати змогу перейти на нову версію зі "смачними" нововведеннями, які дозволяють спростити життя розробника і заощадити час на написання коду час від часу.

    Додатково кілька прикладів тут:

  • [Java] Чому дорівнюватиме значення змінної "a" в результаті виконання наступного коду при початковому значенні a = 10?
    int a = 10;
    try {
      try {
        if (a < 20) {
          throw new Exception();
        }
      } catch (Exception ex) {
        a += 10;
        throw new RuntimeException();
      }
    } catch (Throwable th) {
      a += 5;
    } finally {
      a += 5;
    }

    a) 25
    b) 20
    c) 15
    d) 30


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

    Це питання стосується механізму обробки винятків в Java. Спробуємо проаналізувати логіку роботи цього коду.

    Як ми бачимо, мається вкладений блок try {} catch () {}, і наша основна мета - визначити, в якій саме послідовності виконуватимуться інструкції, а отже яке фінальне значення отримає змінна "a".

    Варто згадати, що в Java логіка вкладених try {} catch () {} не має в собі ніякої магії. Інструкції будуть виконуватися в порядку опису програми або ж "зверху вниз".

    Спочатку відпрацює блок if (a < 20) всередині вкладеного блоку "try". Умова є істинною, адже значення "a" в цей момент - число 10. Далі буде виключення типу "Exception", яке одразу обробляється блоком "catch (Exception ex)", який і є замикаючим для вкладеного "try". Всередині нього значення змінної "a" стане рівним 20. Після цього буде виключення типу "RuntimeException".

    Чи буде цей виняток оброблений далі? Так, адже в нас є зовнішній блок "catch (Throwable th)", в якому вказано, що ми ловимо виняток типу "Throwable". Тип "Throwable" - це клас, який є базовим для всіх класів винятків (java.lang.Exception), а також класів помилок (java.lang.Error). Відповідно, ми можемо зловити і обробити будь-яке виключення / помилку додатка, якщо вкажемо цей тип (RuntimeException успадковує Exception).

    Спочатку також потрібно розібратись, чи виконається "catch (Throwable th)" перед блоком "finally".

    Зв'язка try {} catch () {} finally {} влаштована таким чином, що блок "finally" буде виконаний завжди, незважаючи на те, чи був виконаний блок "catch" раніше. Важливим моментом є те, що "finally" завжди виконується після блоку "catch", якщо той присутній в конструкції (конструкція try {} finally {} теж вважається припустимою), і якщо всередині блоку "try {}" викинуто виключення, яке може перехопити відповідний блок "catch" (і в нашому випадку це так). Сміливо робимо висновок, що, потрапляючи всередину зовнішнього блоку "catch (Throwable th)", змінна "а" матиме нове значення "a + = 5; // 25". Наступним буде виконаний блок "finally", після якого змінна "а" вже матиме значення 30, що є правильною відповіддю.

    Додатково можна почитати тут https://www.geeksforgeeks.org/exceptions-in-java/ або https://javarush.ru/groups/posts/isklyucheniya-java.

  • [Java] Який результат виконання наведеного коду?
    package com.learning;
    
    public class Main {
    
      public static void main(String[] args) {
        BaseLogger l1 = new BaseLogger();
        ChildLogger l3 = new ChildLogger();
        BaseLogger l2 = 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)

      Base
      Child
      Base

    c) Помилка компіляції
    d)

      Base
      Child
      Child

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

    У цьому питанні акцент — на використання фундаментальних принципів ООП, зокрема — наслідування і поліморфізму, а також синтаксис перевизначення методів. Помилки компіляції та виключень не буде, оскільки код написаний відповідно до усіх правил. Правильний варіант: d — відображення рядків у послідовності:

    Base
    Child
    Child

    Спочатку буде виведено стрічку "Base", оскільки змінній l1 присвоюється екземпляр об’єкта BaseLogger. У цьому випадку клас ні від кого не наслідується, логіка цілком прямолінійна. Клас "ChildLogger" — нащадок "BaseLogger", і у ньому визначений наперед метод "log", котрий, у свою чергу, виведе в консоль стрічку "Child".

    Найцікавішим є рядок BaseLogger l2 = new ChildLogger();, що додає певної невизначеності.

    Статичний метод “consoleLog” приймає аргумент типу BaseLogger, як і будь-який його похідний клас-нащадок відповідно. Однією з ключових особливостей поліморфізму є можливість підставити потрібну реалізацію під час виконання (Runtime). Спираючись на цю особливість, можна стверджувати, що у рядку "consoleLog(l2);" також буде виведено стрічку "Child".

    Непоганий приклад також наведено тут: https://www.geeksforgeeks.org/polymorphism-in-java/.