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

Ми розпочали марафон підготовки до вступного тестування Binary Studio Academy 2020! Раз на тиждень ми публікуватимемо на нашому блозі питання подібні тим, що будуть на тестуванні, з правильними відповідями та поясненнями.

-
[JS] Будут ли ссылаться obj1.prop1 и obj2.prop1 на один и тот же объект?

const obj0 = {};
const obj1 = {
    prop1: obj0
};
const obj2 = Object.assign({}, obj1);

a) Да
b) Нет
c) Да, но не в strict mode
d) Фрагмент кода невозможно интерпретировать

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

Тема иммутабельности - одна из важнейших тем в программировании, так как этот подход позволяет избегать сайд-эффектов и создавать реиспользуемые модули. Объекты в JS передаются по ссылке, т.е. если перезаписать объект из переменной в переменную, то при изменении любой из этих переменных будут изменяться все переменные, ссылаемые на этот объект:

const obj0 = {};
const obj1 = obj0;
obj0.prop = 'a';
console.log(obj1.prop); // "a"

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

function copy(obj0) {
   return Object.keys(obj0).reduce((result, key) => {
       result[key] = obj0[key];
       return result;
   }, {});
}

Но чтобы не изобретать велосипед, в JS есть для этого метод Object.assign(dist, src, ...), который первым параметром принимает объект (dist), на который копируются все свойства с объекта, переданного вторым параметром (src). Другим способом копирования может быть spread оператор, т.е. следующие две записи абсолютно идентичны:

const obj1 = Object.assign({}, obj0);
const obj1 = {...obj0};

Обратите внимание на функцию copy, описанную выше,: значения просто перезаписываются на новый объект, т.е. если какой-то из ключей из obj0 будет ссылаться на объект или массив, то в результирующем объекте эти ключи будут ссылаться на этот же объект. Чтобы изменить значение в объекте внутри объекта, нужно делать глубокое копирование:

const obj0 = {};
const obj1 = {
 prop1: obj0
};
const obj2 = Object.assign({}, obj1, {
   prop1: Object.assign({}, obj1.prop1, {
       someData: 'a'
   })
});
 
console.log(obj2); // { prop1: { someData: "a" } }
console.log(obj1); // { prop1: {} }
console.log(obj0); // {}

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

Исходя из этого можно с полной уверенностью сказать, что код валидный, а значит, вариант d) - неправильный. Режим strict также не имеет никакого влияния на выполнение следующего кода, а значит, вариант c) тоже неправильный. А так как мы разобрались, что при копировании объектов методом Object.assign значения не копируются, то правильный ответ a), ведь свойства prop1 в обоих объектах будут ссылаться на один и тот же объект в памяти.

-
[.NET] Что выведет следующий код?

public interface I
{
        void Go();
}

public class A: I
{
        public void Go() {
                Console.Write("A.Go() ");
        }
}

public class B: A
{

}

public class C: B, I
{
        public new void Go()
{
                Console.Write("C.Go ");
        }
}

class Program
{
        static void Main(string[] args)
{
                B b1 = new B();
                C c1 = new C();
                B b2 = c1;
                b1.Go();
                c1.Go();
                b2.Go();
                ((I) b2).Go();
                Console.ReadLine();
        }
}

a) A.Go() C.Go() A.Go() С.Go()
b) A.Go() С.Go() С.Go() С.Go()
c) A.Go() C.Go() C.Go() A.Go()
d) A.Go() C.Go() A.Go() A.Go()

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

Правильный ответ: a) A.Go() C.Go() A.Go() С.Go()

Решение: Сегодня мы поговорим о достаточно интересной и для многих не до конца понятной теме - о полиморфизме. Полиморфизм — это одна из трех основных парадигм объектно-ориентированного программирования, в основе которой, грубо говоря, лежит использование общего интерфейса, что дает возможность использования метода производного класса, даже если его еще не существует на момент написания базового. Таким образом избегаются зависимости от конкретных реализаций, что позволяет делать модули наших программ более самостоятельными и обобщенными. Вот что говорит Майкрософт об особенностях полиморфизма в C#:

  • Во время выполнения объекты производного класса могут обрабатываться как объекты базового класса в таких местах, как параметры методов или массивы. Когда возникает полиморфизм, объявленный тип объекта перестает соответствовать своему типу во время выполнения.
  • Базовые классы могут определять и реализовывать виртуальные методы, а производные классы — переопределять их, т. е. предоставлять свою собственною реализацию. В исходном коде можно вызвать метод в базовом классе и обеспечить выполнение версии метода, относящейся к производному классу.

Давайте теперь рассмотрим нашу программу. Итак, мы имеем базовый для всех классов интерфейс I, который нуждается в реализации метода Go. Дальше у нас есть класс А, который уже имеет конкретную реализацию интерфейса I и просто выводит строку "A.Go () ". Следующим в цепочке наследования идет класс B, который не имеет собственной реализации метода, а использует унаследованную версию. И последний, и самый интересный в этой иерархии, класс - C, он имеет метод Go, в сигнатуре которого содержится ключевое слово new. Это ключевое слово используется, чтобы скрыть базовую реализацию члена класса с таким же именем. Модификатор new приведет к созданию нового метода с таким же именем и скроет унаследованную реализацию. Давайте теперь пошагово разберем все вызовы метода Go:

  1. b1.Go () - переменная b1 является объектом класса B, так что вызовется ближайшая унаследованная реализация, то есть из класса А: "A.Go () "
  2. c1.Go () - вызовет свою реализацию: "C.Go () "
  3. b2.Go () - переменная b2 - это переменная типа C, приведена к классу B, но поскольку реализация в классе C скрыта ключевым словом new, то правила полиморфизма для него не работают, поскольку базовая реализация не была переопределена. Поэтому результат этого вызова - ближайшая унаследованная версия, а именно: "A.Go ()"
  4. ((I) b2) .Go () - помним, что b2 - это экземпляр класса C, хотя и приведен к базовому классу B, который затем приводится к интерфейсу I. Чтобы правильно определить результат этого вызова, нужно внимательно посмотреть на определение классов. Если внимательно посмотреть, можно увидеть, что класс C явно наследует интерфейс I, то есть получается, что данный класс наследует этот интерфейс не явно от базового класса B и скрывает его реализацию, но также имеет собственную прямую реализации. Поэтому результатом этого вызова являются: "C.Go () "

Сложив результаты каждого вызова, получаем: A.Go () C.Go () A.Go () C.Go ()

-
[JAVA] Вы создаете приложение, которое управляет информацией о продукции вашей компании. Приложение включает в себя класс с именем Product и метод с именем save. Метод save () должен быть строго типизирован. Он должен разрешать только типы, унаследованные от класса Product. Вам необходимо реализовать метод save (). Какой сегмент кода необходимо использовать?

a) public static <T extends Product> void save(T prod) { }
b) public static void save(Product prod) { }
c) public static <T implements Product> void save(T prod) { }
d) public static void save(<T extends Product> prod) { }


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

Всем привет и добро пожаловать на финального босса этого марафона. Сегодня будем смотреть на дженерики. Сам разбор очень простой - варианты c и d даже не скомпилируются. Вариант b валидный, но не строго типизированный. Остается вариант a, который и признаем правильным.

Разбор закончен, всем спасибо за внимание.

...Шутка. Давайте копаться детальней. Этот вопрос был выбран потому, что это пример довольно странного кода. Если так подумать, то варианты a и b не особо отличаются. Оба принимают параметр типа Product или его сабклассы и возвращают void, так что особого преимущества мы здесь не получим. А вот если немного поменять сигнатуру на public static <T extends Product> T save(T prod), то разница становится ощутима - она позволяет сохранить конкретный тип у возвращаемого значения, более специфичный, чем просто Product. ​ Где можно это применить? Ну, например, можно сделать ковариантные параметры у методов! Вспомните первый разбор, где мы узнали, что возвращаемый тип метода - ковариантный , а параметры - нет? Пора сделать параметры ковариантными! Рассмотрим пример:

abstract class AbstractCalculator<T extends Number>{
   abstract protected T cast(double val);
   public T add(T a, T b){
       return cast(a.doubleValue() + b.doubleValue());
   }
   public T mult(T a, T b){
       return cast(a.doubleValue() * b.doubleValue());
   }
}abstract class DoubleCalculator extends AbstractCalculator<Double>{
   @Override
   protected Double cast(double val){
       return val;
   }
}abstract class IntegerCeilingCalculator extends AbstractCalculator<Integer>{
   @Override
   protected Double cast(double val){
       return Math.ceil(val);
   }
}class Main{
   public static void main(String[] args) {
       var dCalc = new DoubleCalculator();
       var intCalc = new IntegerCeilingCalculator();
​
       Integer a = intCalc.add(5, 2);
       Double b = dCalc.add(3.0, 2.5);
​
       Integer c = intCalc.add(1, 2.5);//fails typecheck
   }
}

Мы смогли сделать параметры методов AbstractCalculator ковариантными и переопределить их в классах-наследниках. Неплохо. Давайте посмотрим, что мы ещё можем делать с ковариантностью.

Давайте взглянем на следующий пример кода:

interface PurchaceableProduct{
   Integer getCount();
   BigDecimal getPricePerItem();
}class Product implements PurchacableProduct{/* implementation */}class ShoppingCartTotalCalculator{
   public BigDecimal calculateTotal(Stream<PurchaceableProduct> items){
       return items.reduce(
           new BigDecimal(0),
           (sum, item) -> sum.add(item.getPricePerItem().multiply(item.getCount())),
           (a, b) -> sum.add(a, b)
       );
   }
}//in shopping cart service
class ShoppingCartService{
   //Don't put main into your services, it's just an example!
   public static void main(String[] args){
       List<Product> products;
       var total = new ShoppingCartTotalCalculator().calculateTotal(products.stream());//won't compile
   }
}

Данный код не скомпилируется из-за того, что calculateTotal принимает тип Stream<PurchaceableProduct>, который не ковариантный. Мы можем это исправить, поменяв его сигнатуру: public BigDecimal calculateTotal(Stream<? extends PurchaceableProduct> items). Что за ?, вы спросите? Это wildcard тип. Он позволяет контролировать вариантность на уровне использования(use-site) обобщенного параметра, в отличии от definition-site вариативности, которая используется в Scala и C#. Вместо ? может быть подставлен абсолютно любой тип, но на него можно наложить ограничения нижней и верхней границы, что мы и сделали в нашем случае.

Давайте также поговорим о менее известном аспекте генериков - контравариантность. Используется она довольно редко и имеет смысл в ограниченных ситуациях. Пример ее использования можно увидеть в стандартной библиотеке Java, а именно методе Stream.sorted, который имеет такую сигнатуру: sorted(Comparator<? super T> comparator). Для того, чтобы понять, почему так, рассмотрим синтетический пример: у нас есть класс Shape, у которого есть метод getArea. От него наследуется класс Circle, у которого есть методы getRadius и getCenterCoordinates. Мы хотим его отсортировать, и для этого нам подойдет любой компаратор, который может работать с Circle(сортировка по радиусу или координатам центра) ИЛИ с одним из его супертипов (в нашем случае - класс Shape и сортировка по площади). Если обобщенный параметр у компаратора в sorted будет не контравариантным, мы не сможем использовать компараторы, которые работают с Shape, например:

Comparator<Shape> shapeComp = (a,b) -> a.getArea().compareTo(b.getArea());
List<Circle> circles;//somehow get circles/* sorted(Comparator<\? super T> comparator) */
circles.stream().sorted(shapeComp);//compiles ok/* sorted(Comparator<T> comparator) */
circles.stream().sorted(shapeComp);//fails to compile - T is not varaint

Вот такие интересные применения есть у дженериков. На этом на сегодня все, рекомендуем почитать фоллоу-ап статью:

-
[QA] Какое из выражений лучше описывает "парадокс пестицида" в тестировании?

a) С помощью имеющихся тестовых сценариев можно обнаружить дефекты в фичах, которые подверглись изменениям
b) Даже для тестирования старых фич, в которые уже не вносятся изменения, стоит изменять тест кейсы
c) Парадокс пестицида возникает вследствие упущения важных тестовых сценариев
d) Хорошо написанные тесты никогда не подвергнутся парадоксу пестицида

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

Для ответа на этот вопрос достаточно понять, что такое “парадокс пестицида”. Это понятие впервые появилось в книге Бориса Бейзера “Software Testing Techniques” в 1983 году. Автором было проведено аналогию между выполнением тестов и обработкой полей пестицидом, который уже применялся. После первой обработки часть вредителей погибла, но не все, потому что организм некоторых оказался стойким к яду. Велика вероятность того, что они выдержат и во второй раз. Бейзер провел аналогию, что повторное использование одних и тех же тестов и даже методов тестирования может привести к тому, что некоторые баги не будут выявлены. Парадокс пестицида является одним из семи основных принципов тестирования.

Следовательно, правильный ответ b) Даже для тестирования старых фич, в которые уже не вносятся изменения, стоит изменять тест кейсы.

-
[JS] Какое из утверждений верно для следующего кода?

class User {
   constructor(username, password) {
       Object.assign(this, {
           username,
           password
       });
   }
}
 
class Admin extends User {
   constructor(username, password, level) {
       Object.assign(this, {
           username,
           password,
           level
       });
   }
 
   static doSomething() {
       // ...
   }
}

a) Отсутствует вызов конструктора User
b) Object.assign не применим для this
c) Метод doSomething инкапсулирован и не доступен вне класса

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

В этом вопросе разберемся с одним из столпов объектно-ориентированного программирования - наследованием. В частности, в JS. Для того, чтобы унаследовать класс от другого класса, в JS используется ключевое слово extends. Но так было не всегда, ключевые слова extends и class пришли в javascript в стандарте ES6 или ES2015, но в ядре осталось все то же прототипное наследование.

До ES2015 классы создавались и наследовались следующим образом:

function Person(name) {
   this.name = name;
}
 
function Student(name, track) {
   Person.call(this, name);
   this.track = track;
}
 
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
 
Student.prototype.getTrack = function () {
   return this.track;
};
 
const jsStudent = new Student('petro', 'JS');
const dotNetStudent = new Student('ivan', '.NET');

Т.е. тело функции выполняет роль конструктора, а методы объявляются на прототипе этой функции. А для того, чтобы наследоваться, необходимо скопировать прототип наследуемого класса и восстановить конструктор. При этом, чтобы корректно работало наследование, надо не забыть вызвать конструктор родительского класса, иначе поведение будет неправильное, но исключения никакого не будет.

Обратите внимание, что для копирования используется метод Object.create, который копирует все свойства объекта, записывает их в прототип и возвращает новый объект. При этом аналогичный ему Object.assign копирует собственные свойства объекта (не из прототипа) на другой объект и возвращает измененный объект, переданный первым параметром. Object.assign можно применять на любом доступном для записи объекте, поэтому вариант b) можно исключить сразу.

Пример выше можно переписать следующим образом:

class Person {
   constructor(name) {
       this.name = name;
   }
}
 
class Student extends Person {
   constructor(name, track) {
       super(name);
       this.track = track;
   }
}

Такой синтаксис не только лучше читается, но и накладывает ряд ограничений. Например, нельзя вызвать класс как функцию, несмотря на то, что имеет тип функции:

console.log(typeof Student) // function
Student() //  TypeError: Class constructor Student cannot be invoked without 'new'

А также, если при наследовании от другого класса не вызвать его конструктор через встроенную функцию super(), вы получите исключение:

new Student() // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructo

В задании также объявлен статический метод doSomething, который недоступен на экземпляре (объекте) класса, но доступен на классе:

Admin.doSomething()

Но он никак не влияет на указанное поведение. Кроме того, в варианте c) сказано, что метод инкапсулирован, но static - это не модификатор доступа, ведь метод остается публичным, поэтому этот вариант ответа точно неправильный. А знаете ли вы, как задать приватный модификатор доступа в современном стандарте ECMAScript? Пишите свой вариант в комментариях.

И как вы могли догадаться, правильный ответ a), так как конструктор класса User не вызывается в конструкторе класса Admin, и при попытке создать экземпляр класса Admin мы получим исключение.

-
[.NET] Что будет выведено в консоль?

class Program {
        public static void Main(string[] args) {
                string s1 = "Test String";
                string s2 = "Another String";
                s2.Replace("String", "Another");
                Console.WriteLine(s1 + " " + s2);
        }
}

a) Test String String
b) Test String String String
c) Test String Another String
d) Test String Another Another

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

Правильный ответ: c) Test String Another String

Решение: Работа со строками является одной из самых частых задач в веб разработке, и не только в ней. Это может быть как парсинг путей, извлечения некоторой информации из текста или простые преобразования перед возвращением ответа с контроллера. Поэтому очень важно уметь правильно работать с типом string (System.String) и знать его особенности. Сама строка в .NET представляет собой последовательность Unicode символов, которые представлены типом данных char.

Одной из важнейших особенностей типа string является то, что объекты этого типа - неизменны, то есть их нельзя изменить после создания. Этот тип имеет богатый перечень методов, и может показаться, что они меняют изначальною строку, но на самом деле каждый такой метод возвращает новый объект. Поэтому, если в вашем коде много различных манипуляций над строчками, особенно если они большого размера, то лучше использовать специальный класс для работы со строками - StringBuilder, иначе будут нежелательные утечки памяти. Данный класс под капотом содержит массив символов, и операции над объектом StringBuilder модифицируют этот массив, а не пересоздают его заново, как это происходит с string, исключением является лишь ситуация, когда емкость массива недостаточна. Его длину можно проверить свойством Capacity. Когда длина массива недостаточна, тогда он пересоздается с большим размером. По умолчанию его начальная емкость - 16 символов, и если при добавления новых элементов этой емкости не хватает, то массив увеличивается вдвое, но если новых символов больше этой длины, то новый размер будет таким же, как количество символов. При достижении лимита каждый раз массив будет расширяться по той же методике. Это значительно экономит ресурсы при трудоемких операциях со строками. Но это не панацея, не стоит использовать его везде. Майкрософт дает собственные рекомендации касательно того, когда и какой из классов для работы со строками использовать. Итак, StringBuilder они рекомендуют в таких случаях:

  • При неизвестном количестве операции на период жизни строки
  • Когда ожидается, что программа сделает много операций над строкой

И советы, когда использовать обычный класс string:

  • При небольшом количестве операций
  • При выполнении фиксированного количества операций. В этом случае компилятор может объединить все операции в одну. Например, объявления большой строки, разбитой на подстроки и сконкатенированой оператором +
  • Когда нужно выполнить масштабные операции поиска (IndexOf, StartWith) при построении строки, StringBuilder не содержит таких методов.

Отсюда следует: какую бы мы операцию не сделали над s2, пока мы не переприсвоим ей значения, до тех пор оно не изменится, поэтому результат будет без изменений, а именно: Test String Another String.

-
[JAVA] Какой результат компиляции данного кода?

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) Ошибка компиляции из-за того, что блок try {} не содержит кода
d) Ошибка компиляции из-за того, что класс Exception перехватывается раньше, чем RuntimeException


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

Перед тем, как приступать к новому разбору, давайте вспомним задачу в конце предыдущего:

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

  1. Можно ли остановить fall through в switch не используя ключевое слово break?
  2. Какой будет результат исполнения этого кода?
public class Main {
      public static void main(String[] args) {
          //created on heap, not interned
          String operation = new String("+");
              
           int a = 30;
           int b = 10;
           int c = 0;
              
           switch (operation) {
               case "+": c -= a + b;
               case "-": c += a - b;
               default : c *= 3;
           }
​
           System.out.println(c);
       }
}
  1. Да, можно. Ключевое слово throw позволяет выкинуть исключение, и тем самым прервать fall through, если не обработать его в том же кейсе. Также можно использовать ключевое слово return для возврата из метода, например:
public static int op(String op, int a, int b){
   switch(op){
       case "+": return a + b;
       case "-": return a - b;
       case "*": return a * b;
       case "/": return a / b;
       default: throw new RuntimeException("Unknown operator");
   }
}

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

Также существует ещё один ситуативный способ прервать switch/case, находясь в цикле - использовав ключевое слово continue.

Но не стоит забывать, что это все синтетические примеры, и лучшие способы остановить fall through - это именно return и break, и реже - throw.

  1. Ответ -60. Как и в основной задаче. Значение в switch() сравнивается со значениями в case, используя метод equals. ​ С микроразбором закончили, переходим в макро. Сегодня вопрос на спецификацию и знание языка, но на следующий раз мы готовим вопрос, связанный с обобщенным программированием, будет интересно. ​ Начнем разбор с размышления: для того, чтобы вариант a был правильным, нам необходимо, чтобы все 3 варианта с ошибками компиляции оказались неправильными. Вооружившись спецификацией java, начнем откидывать варианты b, c, d по очереди. ​ b - Ошибка компиляции из-за того, что класса ArithmeticException не существует. Но такое исключение существует, и существует очень давно. Откидываем этот вариант и идем дальше. ​ c - Ошибка компиляции из-за того, что блок try {} не содержит кода. Тут вариант интересней, давайте смотреть спецификацию.

Семантика try блока выглядит так:

TryStatement:
   try Block Catches
   try Block [Catches] Finally
   TryWithResourcesStatement

В нашем случае у нас нет finally, и это не try with resources, соответственно, наш случай подпадает под схему 1 - try Block Catches. В данном случае нас интересует блок сразу после try, поэтому идем смотреть спецификацию Block:

Block:
   { [BlockStatements] }

​ Как мы видим, Block состоит из '{', '}' и нуля и более BlockStatements между ними. Соответственно, {} - валидный блок, и вариант c мы можем откинуть.

d - Ошибка компиляции из-за того, что класс Exception перехватывается раньше, чем RuntimeException. Это место, где все становится действительно интересно. Если читать спецификацию try-catch, то мы не найдем ничего, что говорило бы о некорректности кода. Но нам нужно вспомнить про unreachable код. It is a compile-time error if a statement cannot be executed because it is unreachable.. В каком случае try-catch будет unreachable? Смотрим документацию:

A try statement can complete normally if both of the following are true:
​
- The try block can complete normally or any catch block can complete normally.
- If the try statement has a finally block, then the finally block can complete normally.
​
The try block is reachable if the try statement is reachable.
​
A catch block C is reachable if both of the following are true:
- Either the type of C's parameter is an unchecked exception type or Exception or a superclass of Exception, or some expression or throw statement in the try block is reachable and can throw a checked exception whose type is assignable to the type of C's parameter. (An expression is reachable if the innermost statement containing it is reachable.) See §15.6 for normal and abrupt completion of expressions.
- There is no earlier catch block A in the try statement such that the type of C's parameter is the same as or a subclass of the type of A's parameter.

There is no earlier catch block A in the try statement such that the type of C's parameter is the same as or a subclass of the type of A's parameter., говорите? Какой у нас порядок исключений? ArithmeticException, Exception, RuntimeException. Вспоминаем иерархию: ArithmeticException -> RuntimeException -> Exception. Первые два блока catch правильные, а вот третий как раз-таки нарушает это правило: Тип параметра С(RuntimeException) является сабкласом A(Exception), который был встречен ранее. Бинго. Теперь мы знаем, что этот код не скомпилируется, соответственно, ответ d. ​ Было не просто, но мы справились и смогли раскопать ответ в документации. Вот вам небольшая подводочка к следующему разбору: эксепшены - лишь один из видов обработки ошибок, существует ряд других подходов. Посмотрите, как в других языках без механизма исключений обрабатывают ошибки и исключительные ситуации. Какие преимущества и недостатки у этих подходов? Полного ответа в следующий раз мы не дадим, но несколько механизмов рассмотрим.

Также ловите ссылки на документацию, которая использовалась в текущем разборе:

-
[QA] Выберите баги, пропущенные во время тестирования GUI

a) Поле ввода пропускает SQL injection
b) Кнопка "Reload" не выполняет функцию загрузки данных из базы
c) Ошибка в методе калькуляции суммы товаров приводит к неверному результату
d) Кнопка "Вернуться на домашнюю страницу" переадресовывает на страницу "Список товаров".

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

Для ответа на этот вопрос разберемся с определением, что такое тестирование GUI. Тестирование GUI (Graphical User Interface) - это тестирование графического пользовательского интерфейса. Этот вид тестирования выполняется для проверки соответствия графического интерфейса требованиям. GUI обеспечивает взаимодействие с системой через графические элементы (кнопки, меню, иконки, списки и т.д.)

Следовательно, можно понять, что правильные ответы b) Кнопка "Reload" не выполняет функцию загрузки данных из базы и d) Кнопка "Вернуться на домашнюю страницу" переадресовывает на страницу "Список товаров".

Баг, описанный в ответе “a) Поле ввода пропускает SQL injection” поможет выявить security testing, а описанный в ответе “c) Ошибка в методе калькуляции суммы товаров приводит к неверному результату” можно найти во время функционального тестирования.

-
[JS] Какой из приведенных методов способен заменить функцию transformer?

function transformer(arr, func) {
   for (let i = 0; i < arr.length; i++) {
       if (!(func(arr[i], i, arr))) {
           return false;
       }
   }
   return true;
}

a) arr.map(func)
b) arr.sort(func)
c) arr.reduce(func)
d) arr.filter(func)
e) arr.every(func)

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

В этом вопросе разберемся со встроенными методами обработки массивов.

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

const arr = [4, 9, 16];
arr.map((n) => {
   return Math.sqrt(n);
}); // [2, 3, 4]

Т.е. ее упрощенная реализация будет выглядеть следующим образом:

function transformer(arr, func) {
   const result = [];
 
   for (let i = 0; i < arr.length; i++) {
       result.push(
           func(arr[i], i, arr)
       );
   }
 
   return result;
}

sort - сортирует массив в порядке, который указывается переданной функцией. Если функция возвращает число больше нуля, то значение больше, 0 - если значения равны и меньше нуля, то значение меньше. При этом функция sort - мутирующая и изменяет массив, на котором она вызывается.

const arr = [0, -2, 10, 4];
arr.sort((a, b) => a - b); // [-2, 0, 10, 4]
console.log(arr); //  [-2, 0, 10, 4]

Т.е. мы отсортировали массив в порядке возрастания, и при этом массив arr также изменился. Упрощенная реализация метода sort могла бы быть следующей:

 function transformer(arr, func) {
   for (let i = 0; i < arr.length - 1; i++) {
       for (let j = i + 1; j < arr.length; j++) {
           const result = func(arr[i], arr[j]);
           if (result > 0) {
               let buf = arr[i];
               arr[i] = arr[j];
               arr[j] = buf;
           }
       }
   }
   return arr;
}

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

const arr = [1, 2, 3, 4];
arr.reduce((result, n) => result + n); // 10
arr.reduce((result, n) => result + n, -1); // 9

Заметьте, что во втором случае мы передали инициализирующее значение -1, поэтому результат на 1 меньше. А в первом случае инициализирующего значения нет - значит, в первой итерации result равен 1, а n - 2. Реализация reduce может быть следующей:

function transformer(arr, func, init) {
   let result = init;
   let i = 0;
 
   if (init === undefined) {
       result = arr[0];
       i = 1;
   }
 
   for (;i < arr.length; i++) {
       result = func(result, arr[i], i, arr);
   }
 
   return result;
}

filter - возвращает новый массив из значений, для которых переданная функция возвращает true. Т.е. filter вызывает на каждом элементе переданную функцию, и если она возвращает true, то записывает значение в результирующий массив. Так следующий пример отфильтровывает все положительные числа массиве:

const arr = [-2, -1, 0, 1, 2];
arr.filter(n => n >= 0); // [0, 1, 2]

Реализация filter выглядит следующим образом:

function transformer(arr, func) {
   const result = [];
 
   for (let i = 0; i < arr.length; i++) {
       if (func(arr[i], i, arr)) {
           result.push(arr[i]);
       }
   }
 
   return result;
}

every - возвращает true, если для всех значений массива переданная функция вернула true. Т.е. если нам нужно определить, состоит ли массив из положительных чисел, то мы воспользуемся every():

Реализация же этого метода выглядит следующим образом:

function transformer(arr, func) {
   for (let i = 0; i < arr.length; i++) {
       if (!(func(arr[i], i, arr))) {
           return false;
       }
   }
   return true;
}

Т.е. реализация метода every подходит под заданную - значит, правильный ответ e).

Кстати, если нам необходимо найти хотя бы один элемент, для которого переданная функция вернула true, есть встроенный метод some. Например:

const arr = [2, 5, 4, -1];
arr.some((n) => n > 0)

-
[.NET] Что означает данная сигнатура класса?

public class Generic<T> where T: new()

a) T должен быть ссылочным типом
b) T должен быть классом
c) T должен быть анонимным типом
d) T должен иметь конструктор без параметров

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

Правильный ответ: c) T должен иметь конструктор без параметров

Решение: Давайте начнем с самого начала и разберемся, что такое обобщенные типы (generic) в C #. Итак, обобщенные типы данных — это такие типы данных, которые позволяют разработчикам писать типизированные структуры данных, не привязываясь к конкретному типу. Чтобы понять, что это значит, давайте рассмотрим, что привело к их созданию. Обобщенные типы появились только во второй версии .NET Framework и стали большим прорывом, чтобы C # и в дальнейшем развивался как безопасный и строго типизированном язык. Лучшим примером продемонстрировать это будет привести пример с коллекциями. Сейчас мы привыкли, что у нас есть List <T>, или любая другая коллекция, которая содержится в пространстве имен System.Collections.Generic и позволяет определить тип ее элементов на момент объявления коллекции. Раньше нам бы пришлось писать свой собственный список для каждого нужного нам типа отдельно или пользоваться списком ArrayList, который хранит свои элементы как objects. Проблема ArrayList в том, что, во-первых, он выполняет упаковку-распаковку и приведения типов, что негативно влияет на производительность нашей программы, а во-вторых, теряется типизация, и с этим - надежность программы. Такие коллекции позволяют записывать туда различные типы данных, что влияет и на понимание того, над каким именно типом мы работаем в данный момент времени. Обобщенные типы данных выглядят следующим образом:

class Entity<T>
{
    public T Id { get; set; }
    public DateTime CreatedOn { get; set; }
}

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

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

Если вы внимательно просмотрели ограничения по ссылке выше, то должны увидеть, что ограничение new() - говорит компилятору, что тип, который заполняет нашу структуру данных, должен иметь конструктор без параметров и не обязательно, что это - структура или класс.

-
[JAVA] Чему будет равна переменная "c" после выполнения данного кода?

public class Main {
      public static void main(String[] args) {
               String operation = "+";
              
               int a = 30;
               int b = 10;
               int c = 0;
              
               switch (operation) {
                   case "+": c -= a + b;
                   case "-": c += a - b;
                   default : c *= 2;
               }
       }
}

a) 20
b) -20
c) -40
d) -60


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

Интересные вопросы подходят к концу, но один мы все-же прибережем на потом, поэтому пока рассмотрим менее интересный, но более коварный вопрос. Это вопрос на внимательность и знание работы switch в Java.

Интуитивно хотелось бы сказать, что ответ 0 - (30 + 10) = -40 -> c), но внимательные читатели уже заметили отсутствие ключевого слова break в case-ах. А это значит, что сработает механизм fall through. Согласно его правилам, после того, как значение переменной, на которой делают switch, совпало со значением в блоке case, все утверждения, следующие за данным блоком case, будут исполняться до тех пор, пока не будет встречено ключевое слово break, после чего исполнение switch прервется.

Давайте взглянем как fall through отработает в нашем случае:

//substitute operation for it's value
switch ("+") {
   case "+": c -= a + b;
   case "-": c += a - b;
   default : c *= 3;
}
  1. Сравниваем + с +;
  2. Значение совпало, начинаем fall through;
  3. c = c - a + b;//c = 0 - (10 + 30) == -40;
  4. break нет, продолжаем исполнение;
  5. c = c + (a - b);//c = -40 + (30 - 10) == -20;
  6. break нет, продолжаем исполнение;
  7. c = c * 3;//c = -20 * 3 == -60;
  8. больше стейтментов нет, завершаем исполнение switch;

Соответственно, ответ d) - -60.

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

  1. Можно ли остановить fall through в switch, не используя ключевое слово break?
  2. Какой будет результат исполнения этого кода?
public class Main {
      public static void main(String[] args) {
          //created on heap, not interned
          String operation = new String("+");
              
           int a = 30;
           int b = 10;
           int c = 0;
              
           switch (operation) {
               case "+": c -= a + b;
               case "-": c += a - b;
               default : c *= 3;
           }
​
           System.out.println(c);
       }
}

-
[QA] Выберите техники измерения тестового покрытия для метода черного ящика.

a) Покрытие кода
b) Покрытие требований
c) Тестовое покрытие на базе анализа потока управления

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

Для начала вспомним, что такое тестирование методом черного (black box testing) и белого ящика (white box testing).

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

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

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

Следовательно, единственный правильный ответ - b) Покрытие требований.

-
[JS] Как узнать разницу между двумя датами в миллисекундах?

const dateStart = new Date(1976, 2, 20);
const dateEnd = new Date(2017, 6, 20);

a) Date.now(dateEnd) - Date.now(dateStart)
b) Невозможно без использования сторонних библиотек
c) dateEnd.valueOf() - dateStart.valueOf()
d) dateEnd - dateStart

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

Работа с датой и временем - одна из важнейших тем в программировании, независимо от языка. В JS для работы с датой и временем существует класс Date. Базовым значением объекта этого класса является количество миллисекунд, прошедших с 1 января 1970 без учета таймзоны, т.е. в UTC. Важно понимать, что в отличие от unix timestamp, в Date отсчет идет в миллисекундах, а не в секундах.

Но чтобы ответить на этот вопрос, также нужно знать о встроенных методах конвертации объектов в JS. Я хотел бы выделить следующие 3: valueOf, toString, toJSON.

toString - вызывается при попытке конвертировать объект к строке

String({
   toString() {
       return 'yahoo!!!';
   }
}) // 'yahoo!!!'

toJSON - этот метод вызывается при сериализации объекта в JSON методом JSON.stringify

JSON.stringify({
   toJSON() {
       return 'yahoo!!!';
   }
}) // '"yahoo!!!"'

valueOf - возвращает примитивное значение объекта. Т.е. если выполняется какая-либо операция над объектом (напр. -, +, /, * и т.д.), то вызывается этот метод, если он определен.

({
   valueOf() {
       return 1;
   }
}) + 1 // 2

При этом, если определены оба toString и valueOf, то приоритет будет у valueOf над toString, независимо от второго операнда:

({
   toString() {
       return '1';
   },
   valueOf() {
       return 2;
   }
}) + '2' // '22'

А если valueOf не определен, то будет вызван toString.

({
   toString() {
       return '1';
   }
}) + '2' // '12'

Таким образом, примитивным значением класса Date является количество миллисекунд и метод valueOf возвращает это значение. Значит, вариант с) - правильный. Но так как в d) мы выполняем операцию вычитания, то valueOf вызовется автоматически, а значит, и этот вариант правильный. Вариант b) неправильный, потому что задачу можно выполнить стандартными средствами JS. А вариант a) неправильный, потому что метод Date.now() не принимает аргументов и всегда возвращает количество миллисекунд, прошедших с 1 января 1970 года.

-
[.NET] Что будет выведено на экран в результате выполнения следующего кода?

static void Main(string[] args) {
        int a = 10;
        increment(a);
        Console.WriteLine(a++);
}
static void increment(int p) {
        ++p;
}

a) 9
b) 10
c) 11
d) 12

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

Правильный ответ: c) 10

Решение: Чтобы правильно решить задачу, нужно знать 2 вещи, а именно: приоритет операций и особенности работы со значимыми типами (value types). Итак, давайте сначала рассмотрим операции, применяемые в приведенном коде. Единственная арифметическая операция, которая выполняется здесь, - это инкремент, причем он здесь выражен в двух формах, а именно:

  • Постфиксный инкремент (a ++) - сначала возвращается значение переменной как результат операции, а затем переменная увеличивается на 1.
  • Префиксный инкремент (++ p) - сначала переменная увеличивается на 1, а затем возвращается ее значение как результат операции (уже увеличенное!).

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

Давайте теперь попробуем воспроизвести порядок действий нашей программы:

  1. Сначала инициализируется переменная a, и ей присваивается значение 10.
  2. Значение переменной передается в функцию increment(a), но так как передается значение, а не ссылки, то нам не важно, что выполняется в середине функции, поэтому идем дальше.
  3. Вызывается функция Console.WriteLine (a ++), и поскольку здесь применен постфиксный инкремент, то стоит выделить 2 шага. В упрощенном виде этот вызов можно переписать на идентичный, но который наглядно иллюстрирует, что за чем выполняется:
Console.WriteLine(a);
a = a + 1;

Проанализировав программу, видим, что значение переменной a не изменяется до момента ее вывода, поэтому и результатом будет начальное значение, а именно 10.

-
[JAVA] Что будет выведено на экран?

public class Main {
       public static void main(String[] args) {
               Book book = new Book(1101, "John Doe");
              
               book.complete();
              
               boolean isCompleted = book.isCompleted();
               System.out.println(isCompleted);
       }
}class Book {
   private long id;
   private String author;
   private boolean isCompleted = false;public Book(long id, String author) {
       this.id = id;
       this.author = author;
   }private void complete() {
       this.isCompleted = true;
   }public boolean isCompleted() {
       return this.isCompleted;
   }
}

a) true
b) false
c) compilation error
d) null


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

​ И перед тем, как мы начнем разбирать сегодняшний вопрос, давайте вернемся во времени на неделю назад и вспомним задачу в конце предыдущего разбора: Можно ли не изменяя уже написанный код, дописать что-то вместо <your code>, чтобы получить true?

  var literal = "BSA";
  var wrapped = new String(literal);
  System.out.println(literal == wrapped<your code>);

Ответ - да, можно. Строки предоставляют метод для получения иконического(интернированного) представления с использованием метода intern. Если вместо <your code> подставить .intern(), то в консоли мы увидим true. Можете это проверить, выполнив следующий код:

   var literal = "BSA";
   var wrapped = new String(literal);
   System.out.println(literal == wrapped.intern());

​ Теперь перейдем к сегодняшнему вопросу. Это, наверное, один из моих самых нелюбимых типов вопросов - вопрос на внимательность. Сразу откинем вариант null, boolean - примитивный тип, ему нельзя присвоить null. false тоже неоткуда взяться в таком флоу, можем также его откинуть. Остаются варианты true и ошибка компиляции, и тут, казалось бы, что код семантически правильный и видимых причин для ошибки компиляции нет... Но они есть. Если мы взглянем на модификаторы доступа в классе Book, мы увидим, что метод complete - приватный, соответственно, вызов его не из непосредственного класса, где его объявили, вызовет ошибку компиляции. Правильный вариант - c, молодцы, если были внимательны и сразу заметили подвох.

Сегодняшний разбор вышел довольно коротким, поэтому компенсируем его, поигравшись с модификатором доступа у метода complete.

Начнем с пустого(default или же package-private) модификатора. Этот модификатор - второй по строгости. Метод complete выглядел бы так:

void complete() {
       this.isCompleted = true;
}

В данном случае у нас недостаточно информации, чтобы дать однозначный ответ. Пустой модификатор видим внутри класса И внутри пакета, т.е. если класс Main находятся в одном файле(или в разных файлах, но в одном пакете) с классом Book, то код скомпилируется, и в консоли мы увидим true. Иначе код просто не скомпилируется.

С protected модификатором ситуация аналогичная. Разница между этими модификаторами в том, что члены класса, помеченные данным модификатором, будут видны классам-наследникам вне зависимости от пакета, где они находятся.

С модификатором public ответ однозначный - в консоли мы увидим true.

А на сегодня всё, stay tuned for more. Как всегда, советуем посмотреть дополнительные материалы:

-
[QA] Выберите баги, пропущенные во время нагрузочного тестирования странички регистрации, которая по спецификации должна была бы поддерживать до 1000 одновременных подключений.

a) При 900-1000 одновременных подключениях сервер падает
b) При 10000 одновременных подключениях сервер падает
c) Сервер падает при переходе на страничку регистрации
d) Сервер возвращает неверную информацию при перезагрузке странички регистрации

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

В спецификации указано, что страница регистрации должна поддерживать ДО 1000 одновременных подключений. Из этого можно сделать вывод, что ответ b) при 1000 одновременных подключений - неправильный, поскольку спецификацией не предусмотрено такое количество подключений. Также стоит отметить, что главной целью нагрузочного тестирования является создание определенной нагрузки (большое количество пользователей) для того, чтобы отследить показатели производительности системы. Если рассматривать варианты ответов c) Сервер падает при переходе на страничку регистрации и d) Сервер возвращает неверную информацию при перезагрузке странички регистрации, нельзя однозначно ответить, что эти баги были пропущены именно во время нагрузочного тестирования, ведь не указано, что они были обнаружены именно при увеличении нагрузки на систему. Следовательно, правильный ответ a) При 900-1000 одновременных подключениях сервер падает.

-
[JS] Какой способ перехвата ошибки применим для следующего кода?

function asyncFunc(a, b) {
   return new Promise((resolve, reject) => {
       if (a.name.length < b.name.length) {
           reject();
       } else {
           resolve();
       }
   });
}
 
asyncFunc({ fullName: 'John Mistin' }, { lastName: 'Mistin' });

a) try {} catch(err) {}
b) then().catch()
c) Присвоить результат выполнения в переменную и проверить функцией .isValid()
d) Ничего из перечисленного
e) Присвоить результат выполнения в переменную => преобразовать в Boolean => проверить на true/false

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

Чтобы ответить на этот вопрос, нужно понимать, как работает Promise. Промисы в JS предназначены для обработки асинхронных операций. Объект класса Promise имеет 3 состояния: pending (выполняется), fulfilled (выполнен успешно), rejected (выполнен с ошибкой). Когда мы создаем объект Promise, он имеет состояние pending. В конструктор мы передаем функцию, которая принимает два колбека resolve и reject:

new Promise((resolve, reject) => {...});

resolve - принимает результат выполнения промиса и переводит его в состояние fulfilled (resolve(result)).

reject - принимает ошибку и переводит в состояние rejected (reject(new Error(‘error’))).

Для того, чтобы обработать успешный результат, у промиса есть метод then(), а для перехвата ошибки catch():

promise
   .then(result => {...})
   .catch(error => {...})

Метод then также может принимать вторым параметром функцию, которая вызовется при ошибке, так же, как и catch().

Также важно понимать, что промис всегда возвращает промис, вне зависимости от того, произошла ошибка или нет, т.е.:

try {
   promise.then(() => {
       throw new Error('error');
   }).catch(error => {
       // выполнится
   });
} catch (error) {
   // не выполнится
}

Метод catch тоже возвращает промис. Если из метода промиса вернуть значение, то оно превратится в промис со статусом fulfilled:

const result = promise.then(() => {
   return 2;
});
console.log(result instanceof Promise); // true

Еще у промиса есть метод finally(), который выполняется в любом случае, произошла ошибка или нет.

Кроме этого, у промиса есть ряд статических методов:

Promise.resolve(value) - возвращает промис со статусом fulfilled. Promise.reject(error) - возвращает промис со статусом rejected. Promise.all([]) - возвращает промис после того, как все переданные промисы в массиве выполнятся со статусом fulfilled.

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

Таким образом, ответ a) неправильный, потому что промис не влияет на конструкцию try{} catch(){}. Ответ с) неправильный, потому что у промиса нет метода isValid(). Ответ e) неправильный, потому что промис всегда возвращает промис, и если объект привести к булевому значению, то ответ всегда будет true. И ответ d) неправильный, потому что правильный ответ b), так как ошибку в промисе можно перехватить или методом catch(), или вторым аргументом метода then().

-
[.NET] Будет ли вызван метод Dispose() после выполнения данного кода?

using (StreamReader reader = File.OpenText("file.txt"))
{
    throw new Exception();
}

a) Нет, из-за того, что было сгенерировано исключение
b) Нет, метод Dispose не будет вызван
c) Да, метод будет вызван в любом случае
d) Ошибка компиляции

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

Правильный ответ: c) Да, метод будет вызван в любом случае

Давайте сначала рассмотрим, что такое Dispose метод и к чему он здесь вообще. Итак, Dispose паттерн используется для того, чтобы освободить управляемые ресурсы, такие как сетевое соединение, работа с файловой системой и другие. Все наверняка слышали о таком механизме в .NET, как CLR, работа которого заключается в освобождении ресурсов (очистка памяти), но, к сожалению, он ничего не знает о неуправляемых ресурсах, именно здесь в помощь и приходит Dispose паттерн, а именно реализация IDisposable интерфейса. Этот интерфейс требует реализации public void Dispose() метода, в теле которого и выполняется освобождение всех ресурсов, которые могут использоваться как экземпляр данного класса. В рекомендуемой версии его реализация происходит следующим образом:

public class MyClass : IDisposable
{
    private bool disposed = false;

    // Публичная реализация интерфейса Dispose паттерна.
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    // Скрытая реализация интерфейса Dispose паттерна.
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Освобождение управляемых ресурсов.
            }

            // Освобождение неуправляемых ресурсов.
            disposed = true;
        }
    }
}

Итак, мы разобрались, что такое Dispose паттерн и какова его реализация в .NET, но как он относится к нашей задаче? А так, что, using конструкция - это не более, чем "синтаксический сахар", то есть это конструкция, которая под капотом развернется в следующую:

StreamReader reader = File.OpenText("file.txt");
try
{
    throw new Exception();
}
finally
{
    if (reader != null)
    {
        reader.Dispose();
    }
}

Важно заметить, что using конструкцию можно использовать только для тех классов, которые реализуют IDisposable паттерн, в противном случае компилятор выдаст ошибку еще на этапе компиляции. Но это не про StreamReader класс, потому что он использует неуправляемые ресурсы, и Microsoft позаботились, чтобы реализовать для него и ему подобных, Dispose паттерн. Итак, разобравшись, как будет выглядеть наш кусок кода в конечном итоге, можно быть уверенным, что Dispose метод будет вызван в любом случае, потому что он выполняется в finally. Напомнить себе, как работает try-finally конструкция, можно здесь.

-
[JAVA] Что выведет следующий код?

String s1 = "Hello world!";
String s2 = new String("Hello world!");
String s3 = "Hello world!";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
System.out.println(s1 == s3);
System.out.println(s1.equals(s3));

a) false, true, false, true
b) true, false, true, false
c) true, true, true, true
d) false, true, true, true


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

Сегодняшний вопрос затрагивает один из очень интересных аспектов работы со строками в Java.

Зная, что оператор == для сравнения проверяет равенство ссылок, а метод equals может быть переопределен в любом классе(поскольку любой класс является наследником Object) и используется для проверки двух объектов на равенство, можно предположить, что ответ будет false, true, false, true(a). Но, как и во всех вопросах марафона, здесь есть небольшая уловка. Начнем с того, что зная, как работает equals(сравнивает содержимое строки по значению), можно узнать 2 из 4 элементов последовательности - элементы 1 и 3(начиная с нулевого индекса) будут true, следовательно, ответ должен иметь вид ?, true, ?, true. Это позволяет нам откинуть вариант b. Теперь давайте вспомним задачу про StringBuilder, в которой мы обсуждали иммутабельность строк. Помимо прочих преимуществ, это свойство дает возможность компилятору проводить оптимизацию под названием интернация строк. При этой оптимизации строчные литералы помещаются в специальную область памяти, известную как String Pool. В зависимости от версии Java эта область может находиться в PermGen области памяти, в которой не производится сборка мусора, или располагаться непосредственно на Heap, где сборка мусора производится. Эта оптимизация позволяет сократить потребление памяти путем хранения лишь одной копии строки и увеличить производительность за счет избегания лишних аллокаций. В коде это выглядит так:

    var intern = "Binary Studio Academy";
    var intern2 = "Binary Studio Academy";
    System.out.println(intern == intern2);//true

Соответственно, мы с уверенностью можем сказать, что System.out.println(s1 == s3) будет true, ответ a отпадает.

Однако перед тем, как мы сможем окончательно ответить на этот вопрос, нам необходимо обсудить ещё одну тему: String Literal vs String Object. В примере s1 - строковый литерал, а s2 - использует конструктор String для создания новой строки. Основное различие в том, что s1 - будет интернировано, т.к. это строковый литерал, а s2 - аллоцирует память и создаст новый объект строки на куче. Соответственно, System.out.println(s1 == s2) выведет false.

Собрав кусочки мозаики воедино, мы можем получить последовательность false, true, true, true, что соответствует варианту d, как всегда, поздравляем ответивших правильно.

Сегодня также будет бонусная задачка:

Можно ли, не изменяя уже написанный код, дописать что-то вместо <your code>, чтобы получить true?

    var literal = "BSA";
    var wrapped = new String(literal);
    System.out.println(literal == wrapped<your code>);

В решении этой задачки помогут дополнительные материалы:

Ответ на эту задачку мы также опубликуем в следующем выпуске марафона(как и разбор новой задачи). Stay tuned for more.

-
[QA] Задано бизнес-правило по расчету скидки в интернет-магазине. Какой техникой тест-дизайна лучше воспользоваться для отображения правил? Если клиент постоянный и количество единиц товара в заказе больше 10 -скидка 10%. Если клиент новый, и количество единиц товара в заказе больше 10 - скидка 5%. Если клиент постоянный и количество единиц товара в заказе меньше 10 - скидка 7%

a) Таблица переходов состояний
b) Анализ граничных значенийй
c) Таблица решений

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

В задание предоставлены правила с условиями, при одновременном исполнении которых произойдет определенное действие (клиенту будет предоставлена скидка соответствующего размера). В данном случае целесообразно применить технику тест дизайна, которая лучше всего помогает отобразить взаимодействие условий и действий. Правильным ответом будет с) Таблица решений. Именно эта техника тест-дизайна даст возможность визуализировать все возможные комбинации и поможет определиться с нужными тест-кейсами. Таблица решений, как правило, имеет 4 составляющих: условия, варианты выполнения условий, действия, необходимость действий. Условия - список возможных условий: варианты выполнения условий - комбинации выполнения или невыполнения условий из списка, действия - список возможных действий; необходимость действий - указание необходимости выполнения соответствующего действия для каждой комбинации условий. На рисунке можно увидеть, как легко с помощью таблицы решений мы визуализировали правила, которые указаны в задании.

Таблица решениц

-
[JS] Почему при удалении одного из элементов массива arr1 одновременно изменится массив arr2?

const arr1 = [1, 150, 3.5];
const arr2 = arr1;
arr2.pop();

a) arr1 и arr2 будут ссылаться на один и тот же объект в памяти
b) arr1 и arr2 будут разными массивами, но их методы будут синхронизированы
c) arr2 будет дочерним объектом arr1, поэтому их значения будут изменяться одновременно
d) arr1 и arr2 будут разными массивами, но их значения будут синхронизированы

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

В JS все объекты передаются по ссылке, т.е. переменная хранит адрес на ячейку в памяти, которая указывает на начало объекта. Таким образом, когда мы записываем объект в переменную или передаем его как аргумент в функцию, его копия не создается, и когда мы выполняем операции над ним, то изменяются все переменные, которые ссылаются на этот объект.

Массив в JS тоже представляет собой объект класса Array. Если представить память в виде таблицы ниже, то переменная arr1 будет хранить адрес a1, и когда мы присвоим ее arr2, то и arr2 тоже будет хранить адрес a1.

Адрес a1 a2 a3
Значение 1 150 3.5

Кстати, когда мы сравниваем два массива, то сравниваются тоже адреса, а не их значения, независимо от строгости сравнения.

Поэтому операции над объектами в JS разделяются на изменяемые (mutable) и неизменяемые (immutable). Иммутабельные операции создают копию объекта целиком и после этого выполняют свои действия.

В массиве к иммутабельным операциям относятся: slice(), concat(), filter(), map(), reduce()/reduceRight().

А к мутабельным: push(), pop(), sort(), reverse(), shift(), unshift(), splice(), fill(), copyWithin().

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

Исходя из вышесказанного, единственный правильный ответ a), потому что в JS ничего само по себе не синхронизируется, а наследуются друг от друга классы, а не объекты.

-
[.NET] Что будет выведено в результате выполнения следующего кода?

var str1 = $@"Hello World!";
var str2 = $@"Hello World!";
Console.WriteLine($"{str1 == str2}, {str1.Equals(str2)}, {ReferenceEquals(str1, str2)}");

a) True, False, False
b) True, True, True
c) True, True, False
d) False, True, False

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

Правильный ответ: b) True, True, True

В данном куске кода выполняется сравнение строк тремя различными методами. Давайте рассмотрим, как каждый из них будет вести себя для типа данных string (System.String). Ведь именно такой тип данных получается в неявно типизированных переменных str1 и str2. Обычно ссылочные типы данных сравниваются по адресу в памяти, то есть они равны, если указывают на одну и ту же ячейку памяти, но это не про string, ведь этот тип данных имеет переопределенный оператор ==, который реализует сравнения по значению. Также string имеет переопределенный виртуальный метод Equals, который наследуется от базового типа object и также выполняет сравнение по значению. Хотя эти 2 метода сравнения выглядят идентично, в них все же есть разница. Заключается она в том, что метод Equals является виртуальным методом, унаследованным от родительского класса, а следовательно для этого метода будут работать правила полиморфизма, дальше рассмотрим их подробнее. А переопределения оператор == является статическим методом, который касается только класса string.

Давайте рассмотрим работу класса Equals для сравнения различных типов данных. Итак, если мы сравниваем объект типа string с объектом типа object, который указывает на строку, то они будут сравниваться по значению.

Пример:

string str = "Hello World!";
object obj = "Hello World!";
Console.WriteLine(str.Equals(obj));

Как и ожидалось, результат будет True. Здесь выполняется сравнение по значению, так как переменная obj приведется к типу string, потому что метод Equals вызывается с переменной типа string.

А если сравнивать объект типа object, который указывает на строку с объектом типа string, то они будут сравниваться по ссылке, поскольку будет браться реализация Equals из базового класса object.

Пример:

string str = "Hello World!";
object obj = "Hello World!";
Console.WriteLine(obj.Equals(str));

После выполнения данного кода все равно получается результат True. Как же так случилось? Можно подумать, что строки всё равно сравнивались по значению, но нет. Это происходит из-за такого подхода оптимизации ресурсов программы, как интернирования строк. А именно при создании строки проверяется, существует ли уже строка с таким же значением в пуле строк, если да - то вместо создания нового объекта переменной присваивается адрес в памяти на уже созданный объект из пула строк. Таким образом, при сравнении по ссылке на первый взгляд разных объектов мы узнаем, что на самом деле это один и тот же объект. Поэтому и при выполнении ReferenceEquals для строк с одинаковым значением результат будет true. Отсюда и получим результат выполнения: True True True

-
[JAVA] Что такое Stream в Java?

a) Базовый тип данных для InputStream и OutputStream, который отвечает за работу с файлами
b) Это отдельный поток, в котором могут выполняться некоторые операции
c) Базовый тип данных, который позволяет работать с коллекциями и выполнять с ними различные операции (filtering, mapping, ordering...)
d) В Java нет понятия Stream


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

Этот вопрос не сложный, но покрывает несколько важных тем из Java. Начнем отбрасывать варианты по порядку. a) Базовый тип данных для InputStream и OutputStream, который отвечает за работу с файлами. Помимо того, что Input/Output Stream могут быть использованы не только для ввода-вывода из файла, но и из других источников, например, сетевых сокетов. Однако InputStream и OutputStream - абстрактные классы, у которых общий родительский класс - Object. Поэтому вариант a) - неправлильный.

b) Это отдельный поток, в котором могут выполняться некоторые операции. Для создания и управления потоками используется класс Thread, а не Stream, так что вариант b) отпадает.

c) Базовый тип данных, который позволяет работать с коллекциями и выполнять с ними различные операции (filtering, mapping, ordering...). Этот класс был добавлен в Java 8 для декларативной работы с коллекциями. Декларативный подход позволяет описывать трансформации над коллекциями в намного более лаконичной манере, упрощая разработку и поддержку кода. Также Stream ленив по своей природе - существует разделение на терминальные и промежуточные операции. Промежуточные операции не вызывают моментального исполнения Stream, а порождают новый Stream, не проводя дополнительных итераций, и только при вызове терминальной операции произойдет итерация по коллекции.

Промежуточные операции, в свою очередь, делятся на два типа: stateful и stateless. Такие операции, как map и filter - stateless, им не нужно хранить информацию о ранее обработанных элементах, поэтому такие Streamы могут быть обработаны за 1 итерацию по коллекции. С другой стороны, такие операции, как сортировка, требуют полной итерации по коллекции, поэтому при параллельной обработке может понадобиться несколько итераций для выполнения Stream. Резюмируя этот пункт, можно сказать, что Stream - это действительно базовый тип, который значительно упрощает работу с коллекциями, поэтому правильный ответ - c), поздравляем всех, кто ответил правильно.

Как всегда, оставляем полезные ссылки:

-
[QA] При регистрации нового аккаунта пользователь корректно заполнил все поля и нажал кнопку зарегистрироваться. Поле логин подсветилось красным, и появился текст "логин уже занят". Где сработала логика валидации?

a) На клиенте
b) На сервере

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

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

Валидация на стороне сервера - это проверка, которая возникает на сервере после отправки данных. Серверный код используется для проверки данных перед их сохранением в базе данных.

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

Более детально ознакомиться с проверкой данных формы можно здесь: https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation

-
[JS] Почему выполнение данного кода закончится с ошибкой (выберите наиболее точный ответ)?

class New {
   constructor(someVar) {
       this.name = someVar;
   }
}
const extends = new New();

a) extends - зарезервированное слово, которое нельзя использовать в качестве переменной
b) extends и New - зарезервированные слова, которые нельзя использовать в качестве переменных
c) New - зарезервированное слово, которое нельзя использовать в качестве переменной
d) extends - зарезервированное слово, которое нельзя использовать в качестве переменной, а someVar не передано в качестве аргумента при создании нового объекта

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

В JS существует ряд зарезервированных ключевых слов, которые нельзя использовать в качестве идентификаторов, т.е. в названии переменных, функций, классов. Все управляющие конструкции JS по умолчанию зарезервированы, это означает, что следующий код приведет к ошибке:

function function() {} // SyntaxError: Unexpected token 'function'

При этом есть ряд слов, которые зарезервированы на будущее, например implements, interface, package и т.д. С полным списком вы можете ознакомиться по ссылке: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#keywords

Кроме этого, язык JS по умолчанию использует кодировку UTF-8 и является регистрозависимым. Это значит, что следующий код будет работать без ошибок:

function функция()
function Function()

Таким образом, в ответе a) утверждается, что extends - зарезервированное слово, и так и есть, потому что extends используется при наследовании классов. В ответе b) говорится, что extends и New - зарезервированы, и действительно new зарезервировано, так как используется для инстанцирования классов, но так как JS регистрозависимый, то New не зарезервировано и может использоваться в качестве идентификатора. Соответственно, b) и c) ответы - неправильные. В варианте d) также утверждается, что ошибка из-за того, что не передали someVar в конструктор класса, но если не передать аргумент функции, то он будет иметь значение undefined, но к ошибке не приведет. Итого получается, что единственный правильный вариант - a).

-
[.NET] В чем разница между консольным приложением и библиотекой классов?

a) Библиотека классов компилируется по-другому.
b) Нет никакой разницы.
c) Консольное приложение не имеет метаданных.
d) У консольного приложения есть EntryPoint, а у библиотеки классов - нет.

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

Правильный ответ: d) У консольного приложения есть EntryPoint, а у библиотеки классов - нет.

Ответить на этот вопрос довольно легко, но иногда программисты, которые начинают свой путь программирования не обращают внимания на такие довольно очевидные на первый взгляд вещи. Итак, чтобы ответить на этот вопрос, нужно знать, что мы получим при компиляции нашего проекта. Ответ на этот вопрос - сборка. Сборка - это логическая единица, содержащая скомпилированный код для выполнения .NET платформой и не важно, на чем написана программа, то ли на C #, F # или других .NET совместимых языках, потому что все они на выходе генерируются в MSIL инструкции. Также сборки состоят и из других вспомогательных компонентов, таких как:

  • Манифест, который содержит метаданные сборки. Описывает, как элементы сборки связаны между собой.
  • Метаданные типов.
  • Ресурсы (изображения, символьные строки, объекты и другие)
  • MSIL инструкции

Файл сборки может быть сгенерирован как в .dll, так и в .exe формат. Где .dll файл - это библиотека динамической компоновки, а .exe - исполняемый файл. Иногда сборка может быть разделена и на несколько файлов, но всегда есть один главный файл сборки. Также стоит упомянуть, что одна и та же структура сборки используется, что для исполняемого файла, что для библиотечной сборки, а главное отличие между ними в том, что исполняемый файл содержит точку входа (метод Main), а библиотека - нет. Теперь зная, что такое сборка и какова ее структура, можно с легкостью ответить на поставленный вопрос. А именно, мы теперь знаем, что сборка компилируются одинаково для любых видов проекта и всегда содержит манифест, а отличием является то, что консольная программа имеет точку входа, то есть EntyPoint, а библиотека классов - нет.

-
[JAVA] Какой способ оптимизации даст наилучший результат, если итоговая строка получается очень большой?

String concatenatedString = "";
String str = "";
do {
       concatenatedString += str;
       str = this.getNextStr();
} while (str != null);
​
System.out.println(concatenatedString);

a) Использовать StringBuilder
b) Собрать все строки в HashSet, а затем вызвать String.join("", hashSet)
c) Ни один из вариантов не даст существенного результата
d) Собрать все строки в ArrayList, а затем вызвать String.join("", arrayList)


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

Начнем разбор с отбрасывания очевидных вариантов. Зная, что строки в java мутабельные и при каждой операции += будет создаваться копия строки, можно сказать, что вариант с) - неправильный, этот код можно оптимизировать дальше. Если вам необходимо сконкатенировать большое число строк, никогда не используйте оператор сложения на строках, т.к. асимптотическая сложность такого варианта конкатенирования будет O(n^2).

Также, зная, как работает HashSet, мы можем откинуть вариант b), т.к. этот код выдаст другой результат на строках с повторяющимися элементами. Например, исходный код при входном наборе "a", "b", "a" выведет в консоль "aba", а вариант с HashSet - "ab" Теперь мы можем рассмотреть два оставшихся варианта - StringBuilder и ArrayList + String.join. Если бы строки уже были считаны в ArrayList, то ArrayList + String.join должен быть быстрей, но если строки ещё необходимо считать, то StringBuider должен оказаться быстрее... Однако нельзя сказать точно, не проведя бенчмарк! Поэтому, вооружившись JMH(Java Microbenchmark Harness), напишем несколько простых тестов для проверки производительности StringBuilder и ArrayList в разных сценариях. Всего рассмотрим 3 сценария:

  • строки нужно считать и записать в StringBuilder из массива
  • строки нужно считать в ArrayList и соединить строки, использовав String.join
  • строки уже считаны и можно сразу использовать String.join

Исходный код бенчмарка выглядит так:

package benchmark;import org.openjdk.jmh.annotations.*;import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.TimeUnit;@Fork(value = 1)
@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 2)
@Measurement(iterations = 10)
public class StringBenchmark{@State(Scope.Thread)
   public static class StringBenchmarkState{
       public ArrayList<String> strings = new ArrayList<>();
       private final static int WORDS_COUNT = 5000000;
       private final static String chars = "abcdefghijklmnopqrstuvwxyz";private static String genRandomString(int length, Random rand){
           StringBuilder builder = new StringBuilder();
           for(int i = 0; i < length; i++){
               builder.append(
                       chars.charAt(
                               rand.nextInt(chars.length())
                       )
               );
           }return builder.toString();
       }@Setup()
       public void doSetup(){
           Random random = new Random(24042021);
           for(int i = 0; i < WORDS_COUNT; i++){
               strings.add(genRandomString(random.nextInt(60) + 40, random));
           }
       }
   }
      @Benchmark
   public String benchmarkStringBuilder(StringBenchmarkState state){
       StringBuilder builder = new StringBuilder();
       for(String str : state.strings){
           builder.append(str);
       }return builder.toString();
   }@Benchmark
   public String benchmarkStringJoinLoaded(StringBenchmarkState state){
       return String.join("", state.strings);
   }@Benchmark
   public String benchmarkStringJoin(StringBenchmarkState state){
       ArrayList<String> list = new ArrayList<>();
       for(String str : state.strings){
           list.add(str);
       }
       return String.join("", list);
   }
}

Перед запуском каждого теста мы будем создавать 5 000 000 строк длинной от 40 до 100 символов. Для того, чтобы все бенчмарки работали с исходными данными, при создании генератора псевдослучайных чисел укажем seed. Каждый бенчмарк будет запускаться 10 раз с двумя разогревочными прогонами (для нивеляции JIT компиляции). Выводить результат исполнения будем в миллисекундах. После запуска бенчмарка и ожидания его завершения мы увидим такие результаты:

Benchmark                                  Mode  Cnt     Score     Error  Units
StringBenchmark.benchmarkStringBuilder       ss   10  3585.107 ± 536.668  ms/op
StringBenchmark.benchmarkStringJoin          ss   10  4272.556 ± 589.598  ms/op
StringBenchmark.benchmarkStringJoinLoaded    ss   10  3682.284 ± 211.065  ms/op

Как видим, если сравнивать варианты, когда строки нужно считать, StringBuilder - однозначный победитель со средним результатом в 3585.107 ms/op по сравнению с 4272.556 ms/op у ArrayList + String.join, следовательно правильный ответ - a), как всегда, поздравляем ответивших правильно. Однако, как мы и предпологали, ArrayList + String.join оказались сравнимы по производительности со StringBuilder, когда строки уже считаны. Однако StringBuilder в среднем всё ещё быстрее. Поэтому, если вам нужно оптимизировать код, всегда замеряйте производительность разных решений. Также стоит обратить внимание на то, зачем нам нужен warmup. Для этого посмотрим на ход выполнения бенчмарка benchmarkStringJoinLoaded:

# Benchmark: benchmark.StringBenchmark.benchmarkStringJoinLoaded
# Run progress: 66.67% complete, ETA 00:00:57
# Fork: 1 of 1
# Warmup Iteration   1: 4493.003 ms/op
# Warmup Iteration   2: 3864.348 ms/op
...
StringBenchmark.benchmarkStringJoinLoaded    ss   10  3682.284 ± 211.065  ms/op

Как видим, первая итерация была намного медленней среднего времени выполнения, а вторая - уже куда ближе к нему. Это связано с тем, что при первом выполнении метода происходит JIT компиляция, которая также занимает время. На этом на сегодня всё, stay tuned for more. Как обычно - список полезных материалов:

-
[QA] На какой вопрос пользовательская история (user story) НЕ отвечает

a) Кто?
b) Что?
c) Зачем?
d) Как?

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

Для начала вспомним, что такое пользовательская история (user story). User story - это краткая формулировка, которая описывает, чего хочет достичь пользователь системы. Для создания пользовательских историй в основном используют такой шаблон: as a < type of user >, I want < some feature > so that < some reason >. То есть, это ответы на вопросы: кто? (кто этот пользователь в системе, какова его роль), что? (что именно должно быть сделано), зачем? (какая ценность для пользователя).

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

Следовательно, правильный ответ d) Как?, user story не отвечает только на этот вопрос из всех перечисленных.

Более детально с темой user stories можно ознакомиться по ссылке https://www.youtube.com/watch?v=apOvF9NVguA.

-
[JS] Почему при замене var на let выполнение кода завершится с ошибкой?

var a = 0;
a = b;
var b = 3;

a) Hoisting свойственнен только переменным var
b) Переменные let невозможно перезадать
c) Значение переменной let можно назначить только одновременно с объявлением
d) По всем вышеуказанным причинам

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

Один из самых популярных вопросов на собеседованиях связан со всплытием или hoisting’ом. Поэтому и мы не стоим в стороне от современных трендов, так что во вступительном тесте вы можете встретить подобные вопросы. Ну что ж, давайте разберемся, что это за зверь такой 🦙

Hoisting - это механизм JS, который перемещает объявление переменных и функций вверх области видимости. Т.е. можно обратиться к переменной или функции до ее объявления и не получить ошибку. В примере выше интерпретатор видит код так:

var a;
var b;
a = 0;
a = b;
b = 3;

В стандарте ES6 в игру вступили let и const, и изменили ее правила. С let и const hoisting не работает, т.е. мы обязаны сперва объявить переменную, а потом уже к ней обращаться. Кроме этого, переменные будут доступны только внутри блока, т.е. следующий код c let/const приведет к ошибке:

if (true) {
   var a = 12;
}
console.log(a); // 12

if (true) {
   const a = 12;
}
console.log(a); // ReferenceError: a is not defined

Отличие же let от const только в том, что переменные объявленные с const не могут быть перезаписаны:

const a = 2;
a = 3; // нельзя (TypeError: Assignment to constant variable.)

let b = 2;
b = 3; // можно

Итого, можно уверенно сказать, что ответ a) единственный правильный, потому что переменные, объявленные с let, можно перезадать, значит b) ответ неверный. с) тоже неверный, так как это в const мы можем назначать значение только при объявлении. И соответственно d) неправильный, так как мы доказали некорректность ответов b) и c).

Более детально про hoisting можете ознакомиться по ссылкам:

https://www.digitalocean.com/community/tutorials/understanding-hoisting-in-javascript

https://developer.mozilla.org/en-US/docs/Glossary/Hoisting

-
[.NET] При переопределении оператора == необходимо:

a) Реализовать интерфейс IEquatable
b) Переопределить оператор !=
c) Реализовать интерфейс IComparable
d) Все вышеперечисленное

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

Правильный ответ: b) Переопределить оператор !=

Давайте сначала разберемся, что такое перегрузка операторов. Перегрузка операторов - это такая возможность языка программирования C#, которая позволяет кастомным классам и структурами реализовать свое поведение для некоторых встроенных операторов. Такая перегрузка операторов позволит работать с собственными типами так, как будто они были встроены в платформу (int, double и т.д.).

Среди операторов, которые можно перегрузить, есть такие группы:

  • Унарные операторы: +, -,!, ++, -, true, false
  • Бинарные операторы: +, -, *, /,%, &, |, ^, <<, >>
  • Операторы сравнения: ==,! =, <,>, <=,> =

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

public static return_type operator op(parameter_type operand)
{
    // logic
}

И для бинарных операций:

public static return_type operator op(parameter_type2 operand1, parameter_type2 operand2)
{
    // logic
}

Но чтобы перезагруженные операторы работали должным образом, операторы из группы "операторов сравнения" нужно реализовывать парами, то есть при переопределении оператора меньше < нужно переопредилять оператор больше >, и так же с операторами больше равно - меньше равно и операторами равно - не равно. Если не реализовать оператор из пары, то компилятор выдаст ошибку и не скомпилирует такой код. Отсюда и получаем ответ на наш вопрос, а именно: нужно переопределить оператор! =

Больше о перезагрузке операторов можно прочесть в спецификации языка программирования C#

-
[JAVA] Какой результат исполнения 4-ой строки?

double x, y;
x = 0.0;
y = 0.0;
System.out.print(x/y);

a) Error
b) 0
c) null
d) NaN


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

Данный вопрос затрагивает тему реализации арифметики с плавающей точкой в Java. Зная, что double - примитивный тип, можно откинуть вариант null, поскольку это не валидное значение для примитивных типов. Значит, вариант c) - неправильный. Помимо этого, возвращение null из методов - это отличный способ получить NullPointerException в runtime, поэтому нужно избегать возвращения null, и вместо этого использовать или паттерн NullObject, или, что более предпочтительно, тип Optional<T>, который специально создан для работы со значениями, которых может и не быть.

Логично предположить, что при делении на ноль будет выброшено исключение ArithmeticException, как это происходит при делении целых чисел на 0. Но это не так, так что вариант a) отпадает. Для того, чтобы ответить на вопрос, почему так, нам нужно узнать немного больше о реализации чисел с плавающей точкой в Java.

Согласно документации, The floating-point types are float and double, which are conceptually associated with the single-precision 32-bit and double-precision 64-bit format IEEE 754 values and operations as specified in IEEE Standard for Binary Floating-Point Arithmetic, ANSI/IEEE Standard 754-1985 (IEEE, New York).. При ознакомлением со спецификацией стандарта можно узнать, что в спецификации есть несколько специальных значений, а именно Positive Infinity, Negative Infinity, +0, -0 и NaN(not a number). И действительно, если мы откроем документацию класса Double, то можно заметить такие константы, как POSITIVE_INFINITY, NEGATIVE_INFINITY и NaN. Нам нужно лишь определиться, является ли деление 0 на 0 валидной операцией, которая использует интуицию "0 / на любое число = 0", или же это исключительная ситуация. Тут нам также стоит обратиться к стандарту IEEE 754 и узнать, в каких ситуациях результат операции будет NaN:

  • все математические операции, содержащие NaN в качестве одного из операндов;

  • деление нуля на ноль;

  • деление бесконечности на бесконечность;

  • умножение нуля на бесконечность;

  • сложение бесконечности с бесконечностью противоположного знака;

  • ряд других менее распространенных случаев, про которые вы можете почитать дополнительно; ​ Итак, деление 0/0 вернет NaN, что позволяет нам откинуть вариант b) как неправильный и поздравить всех, кто ответил d). ​ На этом интересные особенности арифметики с плавающей точкой не заканчиваются. Вот вам ещё ряд задачек:

  • 1/0;

  • 1.0/0;

  • 1.0/-0;

  • -0 == 0;

  • 0 > -0;

  • 0 >= -0;

  • Double.POSITIVE_INFINITY / Double.NEGATIVE_INFINITY

  • Double.NEGATIVE_INFINITY / -0 ​ Проверить правильность своих ответов можно, исполнив каждую из этих строк, например, в интерактивном REPL. ​ Также, раз уж речь зашла о числах с плавающей точкой, стоит поговорить о том, как точность вычисления влияет на приложения в реальном мире. И начнем со списка известных инцидентов, связанных с ошибками плавающей точки:

  • ракета Arian 5 https://en.wikipedia.org/wiki/Ariane5#Notablelaunches

  • The Patriot missile failure incident http://www-users.math.umn.edu/~arnold//disasters/patriot.html

  • Sleipner oil rig collapse https://en.wikipedia.org/wiki/Sleipner_A#Collapse

  • Vancouver stock exchange incident https://en.wikipedia.org/wiki/VancouverStockExchange ​ Это далеко не все баги, связанные с ошибками вычислений с плавающей точкой, а лишь самые известные и дорогостоящие. Надеюсь, теперь Вы понимаете, насколько критическими могут быть ошибки, связанные с плавающей точкой. Давайте теперь обсудим, как избежать подобных ошибок:

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

  • Будьте внимательны при округлениях. Старайтесь использовать округление как терминальную(последнюю) операцию при работе с плавающей точкой. Внимательно следите, какой режим округления будет использован(в некоторых случаях вам может понадобится округление по математическим правилам, в некоторых - округление в большую/меньшую сторону).

  • При работе с арифметикой, которая требует максимальной точности, например, финансовые операции, используйте тип decimal(в Java это BigDecimal), который специально создан для подобных задач и зачастую поддерживает несколько режимов округления. Если у вас есть проблемы с поддержкой decimal на уровне языка - можно использовать целые числа при работе с финансовыми вычислениями. Однако тут стоить помнить о максимальном размере числа, которое можно записать в тип целого числа в вашем языке. Также использование целых чисел может быть предпочтительнее, если необходимо поддерживать валюты с разным количеством знаков в экспоненте (счетных денежных единиц). Так, японские иены не имеют аналогов наших копеек (они были изъяты из оборота в 1954 году), поэтому в Японии вы не встретите цен вроде 9.99. С другой стороны, одна кувейтская динара соответствует 1000 филс (аналог наших копеек) и цены вроде 9.999 там вполне реальны.

  • Обращайте внимание на то, какие типы данных для работы с Decimal вам предоставляет база данных. Старайтесь избегать автогенераций миграций из моделей и контролируйте тип данных вручную, особенно, если работаете с финансовыми данными. Большинство БД содержит специальный тип для работы с Decimal, а могут и вовсе поддерживать специальный тип для работы с денежными единицами. В случае, если такого типа нет, можно использовать целые числа или задуматься о выборе другой БД ​ А на этом на сегодня всё. Если хотите больше узнать о представлении чисел с плавающей точкой в стандарте IEEE 754, советую посмотреть видео про fast inverse square root алгоритм из Quake. Как всегда, оставляем полезные ссылки:

  • floating point in Java https://introcs.cs.princeton.edu/java/91float/

  • floating point in Java docs https://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.2.3

  • Optional<T> https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html

  • fast inverse square root https://www.youtube.com/watch?v=p8u_k2LIZyo ​ Stay tuned for more

-
[QA] Согласно требованиям, пользователь получает доступ к контенту только если подтвердит, что ему 18+ лет. Какая техника тест-дизайна поможет выявить баг?

---def is_old_enough(age): return age > 18---

a) Таблица переходов состояний b) Анализ граничных значений
c) Таблица решений
d) Классы эквивалентности

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

Для тестирования проверки возраста наиболее целесообразно использовать такие техники тест-дизайна, как классы эквивалентности и анализ граничных значений (ведь именно на “границах” входящих данных часто встречаются ошибки в системе). Можем выделить такие классы эквивалентности: 0-18; 18-max (нужно уточнить, какое именно максимальное значение принимает поле, если это предусмотрено). Границами этих классов будут 0,18, max (если максимальное значение не установлено, можно проверить, например 100, учитывая, что поле принимает возраст). Также, согласно технике граничных значений, нужно к границам добавить -1 и 1 и проверять, кроме самих границ, еще и полученные значения. Соответственно, во время проверки значения 18 будет обнаружено баг, поскольку в коде прописано строгое неравенство “>”, число 18 не удовлетворяет это условие и доступ к контенту предоставлен не будет, что противоречит требованиям. Следовательно, правильный ответ - b) Анализ граничных значений.

-
[JS] Оберіть варіант, у якому при зверненні до методу helper не виведеться помилка. (itemHandler.addItem(2)):

a)

const itemHandler = {
   addItem: function (arg) {
       let a = this.helper(arg);
   },
   helper: (prop) => {
       return prop;
   }
};

b)

const itemHandler = {
   addItem: (arg) => {
       let a = this.helper(arg);
   },
   helper: (prop) => {
       return prop;
   }
};

c) Обидва правильні

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

Чергове питання з рубрики "захоплюючий" javascript. У цьому питанні перевіряється, чи знаєш ти, що таке контекст функції і як працюють arrow functions. Це питання часто виникає при роботі з React, коли ти намагаєшся передати метод через властивості компонента, і не можеш зрозуміти, чому в методі компонента стан іншого компонента 🤷🏿 Але не хвилюйтеся, зараз ми з цим розберемося 😉

Коли ми оголошуємо метод/функцію через function, то при її виклику буде використовуватися контекст (this), в якому вона викликається. Але варто нам записати функцію в інший об'єкт, this відразу зміниться. Щоб це зрозуміти, давайте проведемо експеримент:

// оголошуємо функцію
function f() {
   return this;
}
 
// записуємо ії в об’єкт
const a = { f: f };
 
// перезаписуємо ії з об’єкта
const b = {};
b.f = a.f;
 
// викликаємо їі з методу об’єкта
const c = {
   f: function () {
       return f();
   }
}
 
// перевіряємо
console.log(f() === globalThis);    // true
console.log(a.f() === a);           // true
console.log(b.f() === b);           // true
console.log(c.f() === globalThis);  // true

У першому варіанті повернеться globalThis - це глобальний контекст (у браузері це window). Іншими словами, якщо функція викликається не як метод об'єкта, то this буде глобальний. Але варто нам записати її в якийсь об'єкт, то this зміниться. Крім того в JS у функції є 3 методи, які дозволяють змінити контекст насильно:

  1. f.apply(context, [...args]) - викличе функцію з контекстом context, значення аргументів функції будуть відповідати елементам масиву args
  2. f.call(context, ...args) - викличе функцію з контекстом context, аргументи (args) передаються послідовно
  3. f.bind(context, ...args) - змінить контекст і поверне нову функцію, без виклику

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

const a = {};
const d = {};
d.f = f.bind(a);
console.log(d.f() === a) // true

Arrow function у свою чергу успадковує контекст з місця свого визначення. Причому, якщо ми визначимо arrow function у інший arrow function, то js буде підніматися вгору, поки не зустріне перший-ліпший контекст 🤯 Давайте подивимося, що буде якщо f визначити через arrow function:

const f = () => this;
 
const a = { f: f }
 
const b = {}
b.f = a.f;
 
const c = {
   f: function () {
       return f();
   }
}
 
console.log(f() === globalThis);    // true
console.log(a.f() === globalThis);  // true
console.log(b.f() === globalThis);  // true
console.log(c.f() === globalThis);  // true

Тобто на відміну від function, контекст у arrow function не змінився, незалежно від того, де ми її викликали. Давайте розглянемо ще один приклад:

const contextA = {
   f1: function () {
       const foo = () => this;
       return foo(); // викликаємо
   },
   f2: function () {
       const bar = () => this;
       return bar; // не викликаємо
   }
}
const contextB = {};
contextB.f1 = contextA.f1;    // (1)
contextB.f2 = contextA.f2();  // (2)
 
console.log(contextA.f1() === contextA);    // true
console.log(contextA.f2()() === contextA);  // true
console.log(contextB.f1() === contextB);    // true
console.log(contextB.f2() === contextA);    // true

Зверніть увагу, що коли ми поміняли контекст методу f1 з contextA на contextB (1), то він автоматично змінився для внутрішньої foo, тому що контекст визначення foo змінився.

А в методі f2 ми визначили функцію bar в контексті contextA та привласнили її contextB (2), при цьому контекст не змінився, тому що місце визначення залишилося в contextA.

Отже, щоб бути впевненим у тому, що контекст функції буде той, який ми очікуємо, незалежно від того, де ми будемо її викликати, ми можемо використовувати або bind(), або arrow function.

Фуух, це було складно, але я думаю, тепер ви готові відповісти на питання 🙂 У варіанті b) ми визначаємо функцію в глобальному контексті, а не в контексті об'єкта, а оскільки в globalThis ми не визначили метод helper(), то буде помилка this.helper is not a function. Значить, варіант b) неправильний, відповідно і відповідь c) - теж. Залишається тільки a), ну а ми й без цього вже знаємо, що якщо метод визначений через function, то контекст буде той, де ми цей метод викличемо, в даному випадку на об'єкті itemHandler.

Щоб краще підготуватися до вступного тесту, рекомендую ознайомитися з функціями за наступними посиланнями:

https://javascript.info/function-expressions
https://javascript.info/arrow-functions-basics
https://javascript.info/javascript-specials
https://javascript.info/advanced-functions

-
[.NET] Який результат виконання програми?

static void Main(string[] args)
{
    Console.Write(GetValue());
}

public static string GetValue()
{
    try
    {
        throw new Exception("oops");
    }
    catch (Exception)
    {
        Console.Write("catch ");
        return "return ";
    }
    finally
    {
        Console.Write("finally ");
    }
}

a) catch return finally
b) catch finally return
c) finally catch return
d) catch return

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

Правильна відповідь: b) catch finally return

Щоб відповісти на це питання, давайте спочатку заглибимось у виключення і як їх обробляти на платформі .NET. Виключення - це помилка, яка генерується при виконанні програми внаслідок непередбачуваної поведінки. Якщо таку помилку не обробити, то наша програма завершить своє виконання. Саме для цього мова C# надає розробникам таку конструкцію, як try-catch-finally блоки. Де try блок - це та ділянка коду, яка на нашу думку є ненадійною і в нас є план дій, як усунути негативні наслідки в разі виникнення непередбачуваної ситуації в ній. Цей план дій описується в catch блоках, при чому їх може бути як декілька, так і взагалі не бути об’явленим. Декілька catch блоків описують для того, аби можна було більш точно обробити помилку, наприклад помилки різних типів (ArgumentException, InvalidOperationException і т.д), або накласти додаткову умову через вираз when, який вказується зразу після catch виразу. І останній блок - це опціональний блок finally, який виконується зразу після обробки виключення у блоці catch, і він може бути вказаним тільки один раз. Зазвичай finally блок застосовується для звільнення ресурсів, таких як закриття файлу.

Давайте спробуємо відтворити порядок дій програми, взявши до уваги все, що було описано вище. Спочатку викликається метод GetValue, результат виконання якого буде виведено в консоль за допомогою Console.Write. При виконанні GetValue метода одразу ж генерується виключення, але оскільки код, що генерує виключення, обернений в try блок, то виконання переходить в catch, де виводиться стрічка “catch ” і повертається “return ”. Але не забуваймо, що в нас також є finally, який виконується зразу після catch блоку. Тому результат повернення зберігається і буде повернуто до місця виклику лише після того, як відпрацює finally, тобто коли виведеться стрічка “finally ”. Склавши всі ці кроки докупи, отримаємо “catch finally return”.

-
[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);
   }
}

-
[QA] Які з перелічених методик тестування відносяться до динамічних?


a) Тестування специфікації b) Читання source code
c) Приймальне тестування
d) Тестування GUI

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

Для початку пригадаємо, що таке динамічне та статичне тестування. Динамічне тестування - це тип тестування, який перевіряє функціональність застосунку під час виконання коду. Фактично це означає перевірку застосунку під час його роботи. Статичне тестування навпаки не передбачає, що програмний код буде запущено. Зазвичай статичне тестування проводиться на первинних стадіях розробки ПО, тобто не передбачає імплементації повного функціоналу. Під визначення статичного тестування підходять відповіді a) Тестування специфікації та b) Читання source code, оскільки тестування цих типів може бути проведено без запуску коду. Тому правильна відповідь c) Приймальне тестування та d) Тестування GUI, оскільки виконати тестування цих типів неможливо без запуску застосунку. Приймальне тестування виконується для перевірки виконання програмним забезпеченням задач, які були поставлені, та відповідності вимогам. Тестування GUI - це перевірка графічного інтерфейсу.

-
[JS] Чи можна замінити вираз 1 виразом 2?

const someBooleanValue = isNaN(+someVariable)
const someBooleanValue = +someVariable == NaN

a) Так, у будь-якому випадку
b) Ні
c) Так, якщо someVariable в обох виразах буде числом

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

У цьому питанні приховані одразу два підводні камені JS: приведення типів та властивості NaN.

Знак + перед змінною приведе значення до числового типу, тобто +someVariable еквівалентно Number(someVariable). Наприклад, якщо привести до числа наступний рядок 34, то ми отримаємо число:

+"34" + 6   //  40

Але якщо додати рядок до змінної, то незважаючи на значення, результатом буде рядок, тобто typeof ('' + someVariable) === 'string'. Наприклад:

"" + "34" + 6   // “346”

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

console.log(+(true) )         // 1
console.log(+('2') )          // 2
console.log(+(2) )            // 2
console.log(+(null) )         // 0
console.log(+({}))            // NaN
console.log(+([]))            // 0
console.log(+(undefined) )    // NaN
console.log(+('a') )          // NaN

Як бачите, у випадках, коли JS не може перетворити значення у число, ми отримуємо NaN (Not a Number). До речі, чи знаєте ви, чому пустий масив дорівнює 0? Пишіть свою відповідь у коментарях 😉

Знаючи це, ми можемо відсіяти відповідь с) , бо someVariable не обов’язково зберігати число. Але щоб відповісти правильно, ми маємо знати головну властивість NaN: NaN не дорівнює жодному зі значень, включаючи NaN, тобто:

console.log(NaN === NaN)  // false

Тобто, вираз 2 не буде коректним, якщо +someVariable поверне NaN.

Для того, щоб перевірити, чи дорівнює змінна NaN, треба використовувати функцію isNaN(someVariable) або ж Number.isNaN(someVariable). Тому вираз 1 є єдиним правильним способом перевірити, чи значення є числом, і це означає, що вони не еквівалентні між собою. Тому правильна відповідь b) Ні.

До речі, якщо в масиві одне з значень буде NaN, то indexOf(NaN) поверне -1, а от includes(NaN) поверне true:

let arr = [2, 4, NaN, 12];
arr.indexOf(NaN);           // -1
arr.includes(NaN);          // true

Більш детально з NaN можете ознайомитись за посиланням.

-
[.NET] Який рядок потрібно розкоментувати для того, щоб отримати Hello World?

public abstract class BaseClass
{
    public virtual void Hello()
    {
    	Console.Write("Hello");
    	//Console.WriteLine(" World!"); // 1
    }
}
 
public class ChildClass : BaseClass
{
    public override void Hello()
    {
    	//this.Hello(); // 2
    	//base.Hello(); // 3
    	Console.WriteLine(" World!");
    }
}
 
class Program
{
    static void Main(string[] args)
    {
    	BaseClass item = new ChildClass();
    	item.Hello();
    	Console.ReadLine();
    }
}

a) 1
b) 2
c) 3
d) Hello World! і так виводиться

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

Правильна відповідь: c) 3

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

Перейдемо до завдання. В першому рядку метода Main створюється екземпляр класу ChildClass, і одразу ж виконується неявне приведення його до базового класу BaseClass, тобто виконується upcasting. Хоча в нас посилання є BaseClass типу, але все одно воно вказує на ту ділянку в пам’яті, де було створено екземпляр класу ChildClass. Тому при виклику функції Hello в другому рядку викликається перевизначена версія методу з класу ChildClass, тобто варіанти 1 і 4 вже відпадають. Якщо розкоментувати рядок 2, то відбудеться вічна рекурсія, яка завершиться помилкою переповненого стеку. Залишається варіант 3, який і є правильним. В цьому випадку спочатку виконається реалізація базового класу, що виводить стрічку “Hello”, і після цього виведеться " World!" з наявної реалізації.

-
[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/Covarianceandcontravariance(computerscience), отже, його можна перевизначити у класах нащадках. Розглянемо щє один приклад, цього разу спробуємо погратись з типами параметрів:

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, тицяй на посилання знизу. Побачимось в Академії ;) ​

-
[QA] Що з наведеного нижче буде правильним показником прогресу тестування?


a) Кількість непомічених дефектів
b) Кількість тестів, які ще не виконані
c) Загальна кількість дефектів у продуктіі
d) Зусилля, необхідні для виправлення всіх дефектів

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

Для того, щоб відповісти на це питання, згадаємо один з семи основних принципів тестування: тестування виявляє присутність дефектів, але не гарантує їх відсутність. Тобто тестування дозволяє знизити кількість дефектів, але якщо деякі з них не були знайдені, то це не означає, що вони відсутні. З цього випливає, що на будь-якому етапі тестування неможливо визначити кількість непомічених дефектів та загальну кількість дефектів у продукті. А отже, це не є показником прогресу тестування. Оскільки під час тестування неможливо знайти всі дефекти, то відповідно і виправити всі - неможливо. Отже, правильна відповідь - b) кількість тестів, які ще не виконані

-
[JS] Який з наведених варіантів є вірним рішенням наступної задачі?

Є масив array, заповнений довільними числами. Необхідно наповнити масив newArray числами з array, які не рівні 0.

let array = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55];

let newArray = [];

a)

for (let i = 0; i < array.length; i++) {
	if (array[i]) {
		newArray.push(array[i]);
	}
}

b)

array.forEach((index, element) => {
	if (element) {
		newArray.push(element);
	}
});

c)

newArray = array.map(element => {
	if(element) {
		return element;
	}
});
  1. Жоден

  2. Всі


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

Якщо вам це питання здається легким і ви вибрали будь який інший варіант окрім варіанту a) - рекомендую розібрати це питання разом з нами. Якщо ви вибрали правильний варіант - вітаю, ви молодець, але розбір все одно буде корисно глянути:)

Отож, одразу почнемо з варіантів відповідей:

а) В даному варіанті для перебору масиву використовується звичайний цикл for. Тобто для кожного з елементів масиву array виконується перевірка if (array[i]), яка являє собою неявне приведення числового типу до логічного. Згадаємо, що усі значення числового типу, окрім 0, при приведенні до логічного типу повернуть true. Тому всі ненульові елементи успішно пройдуть перевірку, і далі для кожного з них виконається newArray.push(array[i]). Метод масиву push додає один або більше елементів в кінець масиву - якраз те, що нам потрібно!

Отже варіант а) є вірним. Але давайте розберемо, чому не підходять інші варіанти.

b) Тут реалізація рішення схожа на варіант а), за винятком того, що для перебору елементів array замість циклу for використовується метод масиву forEach. Метод forEach приймає як параметр функцію, яка буде викликана для кожного елемента масиву один раз. Ця функція буде викликана з трьома параметрами: function callback(currentValue, index, array),

де currentValue - значення поточного елемента масиву,
index - індекс поточного елемента масиву,
array - масив, для якого було викликано forEach.

Ось тут і прихована помилка, яку уважні вже помітили! У варіанті b) за значення елементу масиву береться другий параметр функції, коли насправді ж другий параметр відповідає індексу елемента. В результаті ми отримаємо щось на кшталт: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] Саме тому цей варіант не спрацює коректно і є невірним.

с) В третьому варіанті використовується метод map, який як і forEach, приймає як параметр функцію-callback та викликає її один раз для кожного елемента масиву. При цьому він створює новий масив зі значень, які поверне ця функція-callback. Отже, якщо значення елементу array не дорівнює 0, то він пройде перевірку if (element) і функція поверне значення даного елемента, яке запишеться у масив newArray. Але якщо елемент дорівнює 0, то наш callback поверне undefined і масив newArray буде містити undefined окрім числових значень, а це не задовольняє умову: [undefined, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Тому варіант с) невірний.

Зрозуміло, що останні два варіанти також невірні. Отже, правильна відповідь a).

-
[PHP] Що виведе даний код?

function test(): void {
   $array = [1, 2, 3];
   foreach($array as &$number){
       echo $number . ' ';   
   }
   echo PHP_EOL;
   foreach($array as $number){
       echo $number . ' ';
   }
}
test();

a)

1 2 3
1 2 3

b)

1 2 3
1 2 2

c)

1 2 3
3 3 3

d) Нічого, відбудеться помилка


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

Помилок в коді немає, отже варіант d відпадає. Запустивши програму ми побачимо, що правильна відповідь - b.

1 2 3
1 2 2

Давайте розберемося, чому так відбувається.

В основі такої поведінки лежать дві особливості PHP: область видимості змінних і посилання.

У PHP поділяють дві області видимості змінних: локальну і глобальну. Кожна функція створює свою область видимості. Однак локальну область видимості створюють тільки функції - цикли і оператори розгалуження не створюють свої області видимості, і тому змінні, які оголошуються всередині циклу, доступні і поза ним. Розглянемо наступний приклад:

$array = [1, 2, 3];

foreach($array as $number){
   echo $number . ' ';
}

echo PHP_EOL;
echo $number;

В результаті його виконання буде виведено

1 2 3 
3 

Тепер ми розуміємо, що змінна $number в першому і другому циклі foreach - одна і та ж сама.

Після цього в гру вступає досить екзотична і рідко використовувана можливість PHP - посилання.

Посилання в PHP дозволяють звертатися до однієї і тієї ж змінної під різними іменами, наприклад

$a = 42;
$b = &$a;
$b = 21;

echo $a . ' ' . $b;
//prints 21 21

Тепер, коли у нас є всі шматочки пазла, зберемо все воєдино.

  1. Під час першої ітерації створюється змінна $number, якої ще немає в області видимості, в яку за посиланням записуються елементи з масиву $array, тобто на першій ітерації $number вказує на перший елемент [1, 2, 3], на другій ітерації на другий елемент - [1, 2, 3], на третій елемент на третій ітерації відповідно - [1, 2, 3].

  2. Після того, як цикл закінчився, змінна $number знаходиться в області видимості функції і вказує на останній елемент масиву: [1, 2,3]

  3. Починається виконання другого циклу. Змінна $number вже є в області видимості, тому вона перевикористовується.

  1. Коли інтерпретатор PHP виконує foreach, він присвоює значення змінної $number:

на першій ітерації $number = $array[0], отже масив набуває вигляду [1, 2, 1];
на другий ітерації $number = $array [1], отже масив набуває вигляду [1, 2, 2];
на третій ітерації $ number = $ array [2], отже масив не змінюється [1, 2, 2];

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

Follow up

Що виведе даний код:

$arr = [1, 2, 3];

function test(array $array): void{
   foreach($array as &$number){
       echo $number . ' ';   
   }
   echo PHP_EOL;

   foreach($array as $number){
       echo $number . ' ';
   }
   echo PHP_EOL;
}

test($arr);

foreach($arr as $num){
   echo $num . ' ';
}

Якщо ви відповіли

1 2 3
1 2 2
1 2 2

то ви не вгадали. Правильна відповідь:

1 2 3 
1 2 2 
1 2 3 

Це відбувається через ще одну особливість PHP, яку ми вже розглядали в першому питанні марафону: при зміні масиву всередині функції, масив копіюється (тобто відбувається copy-on-write), і оригінальний масив не змінюється.

Більш докладно прочитати про посилання і області видимості змінних можна тут:

References Explained - Manual

Variable scope - Manual

-
[.NET] Коли присвоюється тип даних змінній, що оголошена за допомогою var?

a) Runtime
b) Interpret time
c) Compile time
d) Dynamic linking time
e) Application Initialization time


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

Відразу ж можемо прибрати зі списку можливих відповідей варіант b), оскільки C# є компільованою мовою, а отже у нього немає етапу інтерпретації.

Також варто згадати, що ключове слово var не означає, що ми можемо змінювати тип змінної в процесі виконання програми (хоч це і співзвучно з "variant"). Натомість, ключове слово var використовується для створення змінної, не визначаючи її тип явно.

Не варто плутати var і dynamic. Об'єкт з типом dynamic може мати будь-яке значення, обходить статичну перевірку типів і вважається, що він підтримує будь-які операції на етапі компіляції. Конкретний тип змінної присвоюється в runtime, в процесі виконання програми.

Dynamic Linking - це процес зв'язування збірок (Dynamic Link Libraries), за допомогою якого різні процеси можуть використовувати одну і ту ж функціональність, не створюючи їх копії. Процес не стосується типів всередині таких збірок. Детальніше про DLL можна дізнатися тут.

Application initialization - це процес запуску програми. До цього моменту всі статичні типи вже визначені, а середовище виконання буде працювати тільки з динамічними типами і рефлексією.

Отож, правильним варіантом є c).

П.С.: в офіційній документації Microsoft в розділі var у другому реченні вказано, що компілятор визначає і присвоює тип, який найбільше підходить :)

-
[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

-
[QA] Була реалізована нова фіча: "Публікація постів у стрічці Facebook". У ході реалізації був задіяний код інтеграції з іншими соцмережами. Виберіть, що можна вважати регресійним тестуванням у даній ситуації.

a) Публікація постів у стрічці Facebook
b) Публікація постів в Twitter
c) Публікація постів в Instagram
d) Коментування поста
e) Зчитування тексту поста
f) Монетизація лайків

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

Для відповіді на це запитання спочатку треба розібратися з тим, що таке регресійне тестування. Регресійний тип тестування пов'язаний зі змінами у програмі, для виявлення нових дефектів в незмінних областях програмного забезпечення. Під це визначення підпадают два варіанти - b) Публікація постів в Twitter та c) в Instagram. Перший варіант не підходить тому, що це новий функціонал.

-
[JS] Необхідно отримати масив зі значеннями об’єкта. Які методи неправильні?

a)

for (let key of someData) {
   someList.push(someData[key]);
}

b)

someList = Object.keys(someData).map(key => {
  return [someData[key]];
});

c)

someList = Object.values(someData);

d)

for (let key in someData) {
  someList.push(someData[key]);
}

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

Давай по порядку розберемо кожну із функцій. Для зразку використовуватимемо об'єкт

const someData = { a: 1, b: 2, c: 3 }

Object.keys - повертає масив із назвами полів (ключами або властивостями) об'єкта. Тобто результатом Object.keys(someData) буде масив ['a', 'b', 'c']

Object.values натомість повертає значення всіх полів. Результатом Object.values(someData) буде масив [1, 2, 3]

for ... in - використовується для обходу в циклі усіх властивостей об'єкта. Для

прикладу наведений нижче код виведе у консолі усі ключі об'єкта та їх значення

for (const key of someData) {
  console.log(`${key} - ${someData[key]}`)	
}


// у консолі
// a - 1 
// b - 2
// c - 3

for ... of - використовується для обходу в циклі усіх елементів ітерабельних об'єктів (масивів, рядків, NodeList, Set, Map та ін.). З об'єктами дана конструкція дасть помилку. Приклад:

const someArray = [1, 2, 3];

for (const item of someArray){
  console.log(item);
}


// у консолі
// 1
// 2
// 3

Тому відразу відкидаємо варіант, який використовує for ... of. Всі решта варіантів дійсно присвоюють someList масив. Проте поглянемо уважно на варіант b): return [someData[key]] - виявляється, тут зайва пара квадратних дужок. В результаті для нашого someData отримаємо двовимірний масив [[1], [2], [3]]. Отож неправильними є відповіді a) та b).

-
[PHP] Яку дату буде містити об'єкт $a після виконання наступного фрагмента коду?

$a = new \DateTimeImmutable('2020-04-25 09:00:00');

$a->modify('next day midnight');

a) 2020-04-25 09:00:00
b) 2020-04-26 00:00:00
c) в PHP немає \DateTimeImmutable
d) рядок модифікації має неправильний формат


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

Відразу хочу обмовитися, що в PHP є клас \DateTimeImmutable. Отже, варіант c точно не правильний. Викреслюємо його.

Варіант d теж є неправильним. Тут використовується Відносний формат дати. Це досить зручний і потужний інструмент. Рекомендую ознайомитись, це однозначно стане вам в пригоді в майбутньому.

Залишилося 2 відповіді a і b. Щоб правильно відповісти, який з варіантів правильний, треба знати, що ж таке імутабельність і чим відрізняється звичайний \DateTime від \DateTimeImmutable. Імутабельним (незмінним, immutable) називається об'єкт, стан якого не може бути змінено після створення. Результатом будь-якої модифікації такого об'єкта завжди буде новий об'єкт, при цьому старий об'єкт не зміниться. Так як об'єкт $a у нас імутабельний, а ми знаємо, що імутабельний об'єкт не може змінити свого стану, то відповідь a) - 2020-04-25 09:00:00 є правильною (до речі це дата початку першого відбіркового етапу, так що не забудь зареєструватись тут).

Тепер, коли ми знаємо, що таке імутабельність і як поводяться іммутабельні об'єкти, давайте розберемося, навіщо ж це необхідно. Насправді це досить велика тема, але ми розглянемо одну з найбільш очевидних переваг імутабельних об'єктів. Уявіть, що у вас є клас, який представляє якийсь відрізок часу:

class DatePeriod
{

    private \DateTime $start;
    private \DateTime $end;

    public function __construct(\DateTime $start, \DateTime $end)
    {
        if ($start >= $end) {
            throw new \InvalidArgumentException('Start date can\'t be greater than end date');
        }

        $this->start = $start;
        $this->end = $end;
    }

    public function getStartDate(): DateTime
    {
        return $this->start;
    }

    public function getEndDate(): DateTime
    {
        return $this->end;
    }

    public function getDuration(): int
    {
        return $this->end->getTimestamp() - $this->start->getTimestamp();
    }
}

Це досить простий клас. У конструкторі ми перевіряємо, що start date у нас не більше, ніж end date, чим гарантуємо валідність створеного об'єкта. Але що ж станеться, якщо хтось викличе наступну конструкцію:

$period = new DatePeriod(
    new \DateTime(‘now’),
    new \DateTime(+10 days’)
);

$period->getStartDate()->modify(+20 days’);

Через те, що об'єкт start date передається за посиланням (насправді об'єкт містить ідентифікатор об'єкта, але для простоти будемо вважати, що він передається за посиланням), в цьому випадку він буде змінений також і всередині об'єкта $period, що автоматично призведе до невалідності об'єкта $period, адже ми припускаємо, що $start ніколи не може бути більше ніж $end. Це так званий side effect. Щоб уникнути подібної ситуації, варто задуматися про використання імутабельних об'єктів.

-
[.NET] Яке твердження щодо структур є правильним?

a) В структурах не можна створювати властивості, лише поля.
b) Структура може мати явний конструктор без параметрів.
c) Структура може бути створена без ключового слова new.
d) Структура може успадковуватись від інших структур.
e) Структура може реалізовувати лише один інтерфейс.


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

Для початку давайте розберемо, що таке структура, і чим вона відрізняється від класу.

Структура - це тип даних, що може містити дані (поля, властивості) і логіку (методи). Але на відміну від класу, який є типом за посиланням і зберігається в купі, структура є типом за значенням, тому зберігається в стеку. Окрім цього, у неї є ще деякі особливості.

Наприклад, ми не можемо явно оголосити конструктор без параметрів. Але у структури вже є неявний конструктор, який ініціалізує всі члени класу значеннями за замовчуванням.

Також структура не може успадковувати інші структури і класи, проте може реалізовувати одночасно декілька інтерфейсів. Сама ж структура за замовчуванням є sealed, що означає, що ми не можемо її успадковувати.

Тому правильним варіантом є c). Структуру дійсно можна створити, не використовуючи ключове слово new, але важливо пам'ятати, що всі члени класу повинні бути доступними і проініціалізованими перед першим використанням екземпляру структури.

Якщо ж ви хочете дізнатися докладніше про інші відмінності структур від класів, радимо подивитися про це в офіційній документації Microsoft

-
[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, але все досить доступно)

-
[QA] Ти документуєш спосіб трансформації даних з існуючої системи для того, щоб вони стали більш сумісними з новою системою. Як називається тип вимог, що ти створюєш?

a) Нефункціональні вимоги
b) Transition requirements
c) Функціональні вимоги
d) Solution requirements

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

На перший погляд, може здатися, вимоги щодо перетворення даних можуть відноситься до вимог для нового рішення (Solution requirements), адже саме вони визначають умови для нього. Але такі вимоги не розкривають технічних деталей. Тому правильною відповіддю буде b) - Transition requirements, вони покривають перетворення даних (можливостей та інші) з таких, що існують в системі, на нові. Більше на тему вимог рекомендую подивитись на каналі Карла Вігерса - https://www.youtube.com/watch?v=u2GD4-7tHqc&list=PLA1dXT4tBFfcRj7WmtSbIMlhKHWWUuktk

-
[JS] Який код потрібно використати, щоб в консоль були виведені результати асинхронної функції asyncFoo()?

const asyncFoo = () => fetch(`https://google.com`);

const foo = async () => {
	let data;

	/* код тут */

	console.log(data);
};

a) data = async asyncFoo();
b) data = () => asyncFoo();
c) data = asyncFoo();
d) data = await asyncFoo();
e) data = asyncFoo;


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

Такі питання зазвичай складні для більшості студентів через використання асинхронних функцій. Асинхронні функції використовуються для операцій, які можуть виконуватися довгий час, і мережевий запит - чудовий приклад такої операції. Варто згадати, що JavaScript однопоточна мова програмування, і, щоб не зупиняти основний потік виконання, асинхронні функції виконуються окремо від решти через Event loop. Такі функції оголошуються з модифікатором async і неявно повертають Promise.

Повернемося до нашого завдання. В нас є асинхронна функція foo, всередині якої нам потрібно отримати дані з функції asyncFoo. Як бачимо, asyncFoo повертає результат виконання методу fetch, який в свою чергу виконує мережевий запит за URL адресою, вказаною першим аргументом, і повертає Promise. Отже, після виконання, функція asyncFoo також поверне Promise.

Тепер, якщо ми просто виконаємо функцію data = asyncFoo(), то в консоль буде виведено Promise в стані очікування - не те, що ми хотіли побачити. Нам потрібно дочекатися виконання Promise-у, щоб отримати дані із запиту. Для цього використовується оператор await, він призупиняє виконання асинхронної функції допоки Promise не буде виконаний або відхилений. Отже правильна відповідь d)

Якщо ви переконані в тому що правильний інший варіант, розглянемо їх також:

a) В цьому варіанті виникне помилка через неправильне використання ідентифікатора async.
b) Такий варіант теж не вірний тому, що в змінній data буде міститися функція, результат котрої поверне Promise.
c) Це не вірний варіант, бо в data буде Promise в стані очікування.
d) Вірний варіант.
e) Цей варіант невірний, адже в data присвоюється сама функція asyncFoo, а не результат її виконання

-
[PHP] Як треба змінити клас A, щоб серіалізований рядок не містив поля 'collection'?

<?php

final class A
{
    private string $name;
    private array $collection = [];

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function addToCollection(\DateTimeInterface $element)
    {
        $this->collection[] = $element;
    }
}

$a = new A('name');
$a->addToCollection(new DateTime());
$a->addToCollection(new DateTime('tomorrow'));

$serialized = serialize($a);

a) Реалізувати магічні методи __sleep() и __wakeup()
b) Реалізувати інтерфейс Serializable
c) Реалізувати ArrayAccess
d) Реалізувати магічні методи __get() та __set()
e) Реалізувати магічний метод __toString()


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

Для того, щоб контролювати, які поля містять серіалізований рядок треба або реалізувати магічний метод __sleep(), або реалізувати інтерфейс Serializable. Слід зауважити, що згідно з документацією, класи, що реалізують цей інтерфейс, більше не підтримують __sleep() і __wakeup(). Метод serialize викликається всякий раз, коли потрібна серіалізація екземпляру класу. Цей метод не викликає __destruct() і не має ніяких побічних дій, окрім тих, які запрограмовані всередині нього. Коли дані десеріалізуються, клас відомий і відповідний метод unserialize() викликається як конструктор замість виклику __construct(). Якщо вам необхідно викликати стандартний конструктор, ви можете це зробити в цьому методі.

Розглянемо приклад реалізації методу __sleep

public function __sleep()
{
    return ['name'];
}

Якщо порівняти результати серіалізації класу Container перед та після реалізації методу __sleep(), ми побачимо:

  • Перед:
O:1:"A":2:{s:7:"Aname";s:6:"name A";s:13:"Acollection";a:2:{i:0;O:8:
"DateTime":3:{s:4:"date";s:26:"2020-04-02 03:11:52.004918";s:13:
"timezone_type";i:3;s:8:"timezone";s:16:"America/New_York";}i:1;O:8:"DateTime":
3:{s:4:"date";s:26:"2020-04-03 00:00:00.000000";s:13:"timezone_type";
i:3;s:8:"timezone";s:16:"America/New_York";}}}
  • Після:
O:1:"A":1:{s:7:"Aname";s:6:"name A";}

Як ми бачимо, рядок більше не містить інформацію про властивість collection.

Того ж самого результату можна досягти імплементуючи інтерфейс Serializable як наведено нижче:

public function serialize()
{
	return serialize([
		'name' => $this->name,
	]);

}

public function unserialize($data) 
{
	$data = unserialize($data);

	$this->name = $data['name'];

}

Отже, як ви вже могли здогадатись варіанти a та b є вірними.

Даний підхід дозволяє передавати поточний стан об‘єкта, не передаваючи при цьому з‘єднання з базою даних, внутрішній кеш та іншу зайву інформацію, тим самим економлячи об’єм повідомлення, яке буде передаватися. Це може стати у нагоді у системах із розподіленими обчисленнями

-
[.NET] Який рядок потрібно розкоментувати для того, щоб отримати Hello World?

public abstract class ParentClass {
    public virtual void Hello() {
        Console.Write("Hello");
    }
}

public class ChildClass: ParentClass {
    public override void Hello() {
        //base.Hello(); // 1
        //this.Hello(); // 2
        Console.WriteLine(" World");
    }
}

static void Main(string[] args) {
    ParentClass item = new ChildClass();
    item.Hello();
    Console.ReadLine();
}

a) 1
b) 2
c) Hello World і так виводиться
d) Неможливо отримати Hello World


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

Для того, щоб дати правильну відповідь, необхідно знати, як працює успадкування в C#, в чому різниця між base і this, а також яку функцію виконує конструкція virtual/override. Давайте розберемо по порядку.

Успадкування - це один із принципів ООП, згідно з яким клас може "переймати" члени іншого, базового класу.

Наприклад:

public class ITCompany
{
	public void CreateSoftware ()
    {
    	Console.WriteLine ( "Software created");
    }
}

public class BinaryStudio : ITCompany
{
}

Тут клас BinaryStudio успадковує функціональність класу ITCompany, у якого є метод CreateSoftware. Отож, тепер ми можемо викликати метод CreateSoftware і у класу BinaryStudio.

public static void Main ()
{
	var binaryStudio = new BinaryStudio ();
	binaryStudio.CreateSoftware ();
}

В консоль виведеться "Software created".

Ключове слово base використовується для доступу до членів базового класу, а this- для доступу до членів поточного класу.

Вже на цьому етапі ми можемо відсіяти варіант b) 2, оскільки після аналізу коду можна зрозуміти, що насправді метод Hello класу ChildClass викликає сам себе, створюючи рекурсію, що призведе до виключення.

І тут ми вже близькі до розв'язки нашого питання. Механізм virtual / override.

Ми можемо позначити метод в базовому класі ключовим словом virtual, для того, щоб дозволити класам-спадкоємцям перевизначати його логіку. При цьому сам метод, що перевизначає логіку, ми позначаємо ключовим словом override.

Також варто звернути увагу, що змінна, в якої ми викликаємо метод Hello, має тип базового класу ParentClass, незважаючи на те, що ми присвоїли об'єкт типу ChildClass.

Тепер давайте розберемо на прикладі коду з питання, чим відрізнятимуться результати з використанням virtual / override і без.

У разі, якщо ми не використовуємо перевизначення логіки, буде викликаний метод Hello у класу ParentClass, оскільки наша змінна має саме цей тип (якби тип змінної був ChildClass, то тоді викликався б метод цього класу).

Якщо ж ми перевизначили логіку, то CLR буде шукати нову реалізацію в класах-спадкоємцях. Тобто, хоч тип змінної це ParentClass, його метод Hello позначений ключовим словом virtual. Тому при виконанні програми буде викликаний метод класу ChildClass. Після цього можемо розкоментувати рядок 1 і за допомогою base викликати метод базового класу. У такий спосіб отримаємо рядок "Hello World".

Отже, правильна відповідь - варіант a) 1.

-
[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

-
[QA] У банківській системі існує наступна модель кредитування: при кредиті в 5000 гривень - кредит надається без відсотків, при наступному рефінансуванні зі збільшенням кредиту на суму до 2500 гривень - під 7%, при наступному рефінансуванні зі збільшенням на суму до 18000 гривень - під 23%, при рефінансуванні зі збільшенням на ще більшу суму - відсоток буде 60%. Визначте, який з наборів даних підходить під один клас еквівалентності.

a) 4000 ₴ ; 6700 ₴ ; 20000 ₴ ;
b) 5000 ₴ ; 6200 ₴; 7501 ₴;
c) 7500 ₴; 18000 ₴; 25500 ₴;
d) 8000 ₴; 20000 ₴; 25500 ₴;

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

За умовами завдання можемо скласти наступні класи еквівалентності:

1й клас: 0 - 5000 під 0%
2й клас: 5001 - 7500 під 7%
3й клас: 7501 - 25500 під 23%
4й клас: 25501 - і все, що вище під 60%

Виходить, що правильною відповіддю буде d), оскільки цей набір даних підпадає під лише один - третій клас.

-
[JS] Які значення будуть в змінних після виконання коду?

let a = [1, 3, 2, 4];
let b = a.sort();

a) a = [1, 3, 2, 4]; b = [1, 2, 3, 4];
b) a = [1, 2, 3, 4]; b = [1, 2, 3, 4];
c) a = [1, 2, 3, 4]; b = 4;
d) a = [1, 2, 3, 4]; b = [1, 3, 2, 4];


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

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

Метод працює наступним чином: за замовчуванням елементи приводяться до рядка та порівнюються значення їх UTF-16 послідовності. Наприклад, при виконанні наступного коду [9, 80].sort() результатом буде масив [80, 9] . В числовій послідовності 9 менше за 80, але числа конвертуються в строки та порівнюється їх значення в Unicode послідовності в котрій 80 менше за 9. В нашому прикладі масив a містить всі значення як одноцифрові числа тому результат співпадає з відсортованим чисельним результатом.

Розберемо наданий код

  1. let a = [1, 3, 2, 4]; ініціалізуємо змінну та присвоюємо їй масив.
  2. let b = a.sort(); ініціалізуєму змінну b та присвоюємо їй значення результату виконання сортування масиву a. Тобто масив a вже також містить відсортовані елементи.

А тепер розглянемо варіанти відповідей

  1. а) - невірна відповідь тому, що метод sort змінює вихідний масив, а не створює новий, тобто змінна a повинна мати значення відсортованого масиву.
  2. b) - вірна, після виконання коду змінні a та b мають значення відсортованого масиву.
  3. c) - невірна тому, що змінна b повинна мати значення відсортованого масиву
  4. d) - теж невірна тому, що метод sort повертає відсортований масив, а не вихідний.

Отже, правильна відповідь b)

-
[PHP] Який буде результат виконання наступного блоку коду?

class Foo
{
    public static function handle(&$bar) 
    {
        if (strlen($bar) > 5) {
            unset($bar);
            $bar = "World!";
            return ;
        }
        
        $bar = "There!";
    }
}

$bar = 'Hello ';
echo $bar;
Foo::handle($bar);
echo $bar;

a) Hello There!
b) Hello World!
c) Hello
d) Hello Hello


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

Отже, спробуємо розглянути варіант відповіді №1, де результатом виконання коду є рядок "Hello There!". Для того, щоб отримати вказаний результат, довжина рядка, змінної $bar, має бути менше або дорівнювати 5 символам. Що, безумовно, не так. Відповідно, 1-й варіант не є вірним.

Другий варіант, на перший погляд, виглядає більш правдоподібним, ніж попередній. Довжина рядка дійсно більше 5 символів. Але уважний кодер обов'язково помітить один нюанс: $bar передається в функцію unset(), яка, згідно з документацією, робить видалення змінної. "Ну і що?!, - вигукне все той же уважний кодер, - адже відразу ж після цього змінна з тим же ім'ям $bar ініціалізується новим значенням!". І виявиться неправий через те, що змінна $bar в метод handle() передавалася за посиланням і була видалена. Виходить, що початкова змінна успішно знищена. А нова змінна з тим же ім'ям вже створена в контексті методу і з нього не повертається. Виходить, що варіант "Hello World!" теж не правильний.

Ну все, 3-й варіант "Hello" вже точно вірний! Перша частина відобразилася, а після цього змінна видалена в методі handle() класу Foo. Значить, відображати більше нічого. А ось і ні! Згідно з офіційною документацією PHP (Manual): "Якщо змінна, яка передається за посиланням, видаляється всередині функції, то буде видалена лише локальна змінна. Змінна в області видимості виклику функції збереже те ж саме значення, що і до виклику unset()."

Таким чином правильним виявляється останній варіант: "Hello Hello".

-
[.NET] (C# 7.0) Оберіть правильний варіант перевірки типу.

a)

var score = 10;

if (score as int d) Write(d);

b)

var score = 10;

if (score is int d) Write(d);

c)

var score = 10;

if (score is d) Write(d);

d) жоден з наведених варіантів не є правильним.


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

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

  • as - використовується для явного приведення в посилальний тип або nullable тип значень. Якщо ж приведення неможливе, повертається null.

Наприклад:

object x = "Binary Studio Academy";
var res = x as string;

Приведення пройде успішно, і res буде дорівнювати рядку "Binary Studio Academy".

object x = 120;
var res = x as string;

Однак, якщо ми замінимо рядок на число, то res буде дорівнювати null через неможливість явного приведення числа в рядок.

У разі, якщо ми хочемо отримати виняток при невдалому приведенні типів, або якщо ми працюємо з типом значень, варто використати cast оператор ().

  • is - використовується для перевірки відповідності типів.

Наприклад:

var a = new Car ();
var b = new Person ();
Console.WriteLine (a is Car);
Console.WriteLine (b is Car);

Починаючи з C# 7.0, завдяки pattern matching ми можемо оголошувати нову змінну для результату прямо в виразі (book is Document doc).

Беручи до уваги все вищезгадане, можна зауважити, що

  • варіант a) нам не підходить, оскільки нам потрібно перевірити тип, а не привести його;
  • варіант c) неправильний, оскільки синтаксис потребує наявність типу з яким порівнюємо, а не змінну.

Тому правильним варіантом є варіант b).

-
[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

-
[QA] Припустимо, що прийшов час релізу, і тобі треба виконати великий набір тест кейсів за дуже короткий час. Від чого б ти відштовхувався, щоб забезпечити якість релізу?

a) Оцінка твого project manager’a / teamlead’a
b) Пріоритет тест кейсів
c) Уподобання клієнтів продукту
d) Пройдеш усі тестові сценарії, навіть якщо треба овертаймити

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

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

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

Отже, правильна відповідь – b) Пріоритет тест кейсів та c) Уподобання клієнтів продукту.

-
[JS] Що буде виведено в консоль під час виконання функції?

function () {
	const numbers = [1, 2, 3];
	numbers.reduce((sum, number) => {
		console.log(sum, number);
		sum += number;
		return sum;
	});
}

a)

0 1  
1 2  
3 3  

b)

1 2  
3 3  

c)

undefined 1  
1 2  
3 3  

d)

1 2  
3 3  
6 undefined  

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

Для того, щоб відповісти на це питання, потрібно знати, як працює метод массиву reduce. Цей метод приймає наступні параметри:

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

Де accumulator - значення, яке ми повертаємо при ітерації, currentValue - поточне значення при ітерації, індекс масиву та початкове значення. В нашому випадку акумулятором буде сума, а поточним значенням при ітерації будуть значення масиву (по черзі 1, 2, 3).

Отже, оскільки початкове значення для функції не задано (початкове значення суми), то при першому проході в консолі ми повинні побачити undefined 1. Далі відповідно 1 2 та 3 3.

Якщо хтось подумав так само, то це не правильна відповідь. Якщо не передати initialValue в метод reduce, то він в якості нього візьме перший елемент масиву array. А отже, ітерації будуть всього дві, починаючи з другого елементу (з першого, якщо рахувати з нуля) і результат консолі виведений всього двічі. Правильна відповідь b).

-
[PHP] Що буде виведено на екран в результаті виконання наступного коду?

class Dog {
   static $whoami = 'dog';
   static $sound = 'barks';

   static function makeSounds() {
       echo self::makeSound() . ', ';
       echo static::makeSound() . PHP_EOL;
   }

   static function makeSound() {
       echo static::$whoami . ' ' . static::$sound;
   }
}

class Puppy extends Dog {
   static $whoami = 'puppy';
   static $sound = 'howls';

   static function makeSound(){
       echo static::$whoami . ' whines';
   }
}

Dog::makeSounds();
Puppy::makeSounds();

a)

dog barks, dog whines  
puppy howls, puppy barks  

b)

dog barks, dog barks  
puppy howls, puppy whines  

c)

dog barks, dog barks  
puppy whines, puppy howls  

d)

dog howls, dog barks  
puppy barks, puppy whines  

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

В даному прикладі використовується «Late Static Bindings» яке означає, що ключове слово static пов'язує метод або властивість (до якого відбувається звернення через static: :) не з тим класом, в якому було визначено використовує його метод, а з тим, в якому цей метод був викликаний під час виконання.

Першим рядком відповіді, як ви вже напевно здогадалися, буде «dog barks, dog barks».

Але ось що менш очевидно, так це другий рядок відповіді - «puppy howls, puppy whines».

Щоб зрозуміти, чому так відбувається, давайте подивимося на метод makeSounds() в класі Dog.

У методі makeSounds() спочатку викликається self::makeSound(). self:: завжди вказує на контекст того класу, в якому відбувається звернення через нього (в даному випадку це клас Dog). Тому self::makeSound() завжди буде призводити до версії методу makeSound() з класу Dog. Це справедливо і в тому випадку, коли відбувається виклик Puppy::makeSounds(), тому що в класі Puppy немає власного методу makeSounds(), і тому викликається метод makeSounds() з класу Dog. В результаті, при виклику Puppy::makeSounds() виконується self::makeSound(), що призводить до появи тексту «puppy howls».

Потім в методі makeSounds() класу Dog викликається static::makeSound (). static:: обробляється інакше, ніж self::. При зверненні через static:: використовується версія методу, яка визначена в контексті класу який викликав цей метод під час виконання (в даному випадку, це клас Puppy). Отже, виклик static::makeSound() призводить до виконання версії методу makeSound() з класу Puppy. Саме з цієї причини при виклику з Puppy::makeSounds() методу static::makeSound() виводиться текст «puppy whines». Отже правильна відповідь - b

-
[.NET] (C # 7.0) Який з наведених варіантів виклику функції TryParse спричинить помилку компіляції?

a) int.TryParse("10", out var _);
b) int.TryParse("10", out _);
c) int.TryParse("10", out count);
d) int.TryParse("10", out int count);

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

Для початку, давайте розглянемо як можна передавати аргументи в методи.

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

Наприклад:

var count = 10;
Calculate(count);
Console.WriteLine(count);

public static void Calculate(int count)
{
	count++;
}

Що б ми не зробили всередині методу Calculate, count все одно буде дорівнювати 10, оскільки всередині програма буде працювати з копією цієї змінної.

  1. За посиланням за допомогою ключового слова ref. Замість копіювання значення, ми передаємо посилання, тобто всі дії всередині методу будуть відображатися на значенні аргументу, який ми передали.

Наприклад:

var count = 10;
Calculate(ref count);
Console.WriteLine(count);

public static void Calculate(ref int count)
{
	count++;
}

У цьому випадку метод Calculate буде працювати з посиланням на нашу змінну count, а не з копією значення, тому результат буде дорівнювати 11.

  1. За посиланням за допомогою ключового слова out. Все так само, як і з ref, за винятком того, що для ref потрібно, щоб змінна була проініціалізована до передачі у метод, а у випадку з out - ми ініціалізуємо її всередині. Також, починаючи з C # 7.0 ми можемо користуватися перевагами pattern matching і оголошувати out змінні прямо у списку аргументів під час виклику методу (наприклад, int.TryParse("10", out int count)). До речі у нас є класна стаття яка описує Pattern Matching вздовж і впоперек.

C# 7.0 також додав можливість використання так званих "порожніх" змінних. Навіщо вони потрібні? У комерційній розробці часто буває ситуація коли доводиться працювати з чужим кодом, але при цьому не можна його змінювати через зворотню сумісність. І буває так, що потрібно щось передати в якості аргументу, однак нам не потрібен результат. У цьому випадку ми можемо використовувати Discards.

Якщо зібрати всі ці знання воєдино, вийде, що варіанти відповідей a) і b) не спричинять помилку компіляції, оскільки ми дійсно можемо використовувати символ _, якщо нам не потрібен результат. Також в цьому випадку компілятор дозволяє нам не ставити тип змінної. А ось варіант c) якраз викличе помилку, тому що ми не вказали тип змінної, яку створюємо.

-
[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 стрімко розвивається, і стежити за оновленням специфікації і додаванням / видаленням можливостей в синтаксисі мови доводиться постійно (з певною періодичністю звичайно). Це потрібно для того, щоб бути в курсі останніх змін в екосистемі мови і мати змогу перейти на нову версію зі "смачними" нововведеннями, які дозволяють спростити життя розробника і заощадити час на написання коду час від часу.

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

-
[QA] Дано систему бронювання і покупки залізничних квитків. Квиток може бути доступним, заброньованим, оплаченим повернутим або використаним. Якою технікою тест-дизайну краще скористатись у даному контексті?

a) Класи еквівалентності
b) Аналіз граничних значень
c) Таблиця/діаграма переходів станів
d) Таблиця рішень

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

В даному питанні слід зосередитися на бізнес процесі. Оскільки нам відомо, що квиток має різні стани, то логічно, що ці стани були викликані якоюсь дією. Ідеальним варіантом відповіді є таблиця переходів станів, яка дозволяє розробити численні комбінації для досягнення станів. Кожен крок у такій таблиці описується чотирма полями - поточний стан, подія / умова, дія і новий стан. Мета цієї техніки полягає в тому, щоб не тільки скласти список "скриптових" сценаріїв поведінки, але також знайти невизначені ситуації в системі.

-
[JS] Що поверне функція?

function someFunction () {
	const numbers = [1, 2, 3];
	if (typeof numbers === 'array') {
		return true;
	}
}

a) null
b) undefined
c) true
d) false


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

Все просто - достатньо відкрити консоль Google Chrome і глянути результат. Або трохи подумати і розібрати функцію по рядкам:

  1. const numbers = [1, 2, 3]; - ініціалізація масиву. Тут все просто і без помилок. Йдемо далі.

  2. if (typeof numbers === 'array') - умова, в якій ми порівнюємо тип масиву. Поверне false, оскільки typeof numbers дорівнює “Object”, що в свою чергу не дорівнює “Array”. Якщо ти не знаєш, чому typeof поверне не “Array”, то радимо глянути на статтю в нашому блозі - в ній один з наших коучів розбирає подібну задачу.

  3. { return true; } - Оскільки умова ніколи не виконається, то цей рядок ми пропускаємо.

  4. І оскільки наша функція явно нічого не повертає, то вона поверне неявно undefined.

    Тобто:

    function () {
    	const numbers = [1, 2, 3];
    	if (typeof numbers === 'array') {
    		return true;
    	}
    	return undefined;
    }

Очевидно, що так функцію ніхто не стане писати - якщо функція перевіряє, чи numbers являє собою масив, то ми б отримали цей массив аргументом функції та явно повертали значення true або false.

    function сheckArray(numbers) {
    	return Array.isArray(numbers);
	}
або  
    function сheckArray(numbers) {
    	return numbers instanceof Array;
    }

Але в якості тестового питання це хороший приклад, щоб перевірити ваші знання.

-
[PHP] Що виведе наведений нижче код?

function isEqual(float $a, float $b) {
  return $a === $b;
}

function getEqualKeyValuesCount(array $array) {
  return count(array_filter($array, 'isEqual', ARRAY_FILTER_USE_BOTH));
}

$array = [];
$step = 0.01;

for ($i = 2.99; $i >= 0; $i -= $step) {
  $array[$i] = $i;
}

echo getEqualKeyValuesCount($array);

a) 0
b) 3
c) 299
d) 300


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

Очевидно, що цикл for буде виконуватись 300 ітерацій від 2.99 до 0 включно. Отже, варіант c одразу відпадає.

Однак, варіант d - 300 також є неправильним. Справа в тому, що ключем масиву в PHP може бути лише ціле число або строка. Якщо використовувати в якості ключа float, то його значення буде попередньо сконвертованно в integer, тобто буде відкинута дрібна частина, як при використанні функції floor. Детальніше: Arrays.

Існує 3 цілих числа від 0 до 2.99: 0, 1, 2. Однак, варіант b також невірний.

Зверніть увагу на функцію isEqual - вона містить грубу помилку, через яку завжди буде повертати false. Таке порівняння float не є коректним, так як цей тип даних має обмежену точність. Щоб переконатися в цьому, спробуємо вивести будь-яке з чисел в масиві $array.

echo $array[1]; // 1
// echo і var_dump не допоможуть виявити помилку.
echo number_format($array[1], 50); // 1.00000000000002042810365310288034379482269287109375
// зовсім інша справа. Це значення явно відрізняється від
echo number_format(1, 50); // 1.00000000000000000000000000000000000000000000000000

Щоб виправити баг, потрібно переписати функцію isEqual таким чином:

function isEqual(float $a, float $b) {
  $epsilon = 0.00001;
  return abs($a - $b) < $epsilon;
};

По стандарту IEEE-754, який реалізовують більшість сучасних мов програмування (в тому числі і PHP), число 1 можливо точно представити в пам'яті. А ось число 0.01, яке зберігається в змінній $step, в двійковій системі представити точно неможливо. Бо воно являє собою нескінченний періодичний дріб 0.00 (00001010001111010111). Тому воно було представлено приблизно, а після того як ми послідовно здійснювали операції з цим числом, помилка накопичувалась.

До речі, через накопичення помилки в цьому прикладі зробити значення $epsilon рівним константі PHP_FLOAT_EPSILON, не допомогло б виправити баг. Вибір кроку або початкового значення циклу дробовим числом - погана ідея.

Отже правильна відповідь a.

Ще один приклад:

function isAbsoluteZero($temperature) {
   return $temperature === -273.15;
};

$step = 0.01;
$temperature = 0;

while (!isAbsoluteZero($temperature)) {
   $temperature -= $step;
}

if ($temperature === -273.15) {
   echo 'Absolute zero!';
} else if ($temperature > -273.15) {
   echo 'Not so cold';
} else {
   echo 'Unreal cold!';
}

Незважаючи на те, що в if-else блоці передбачено виведення одного з трьох рядків для будь-якого значення $temperature, на екран не буде виведено нічого. Програма зависне через нескінченний цикл while. Значення температури ніколи точно не збігається зі значенням -273.15.

-
[.NET] Скільки запитів до бази даних буде виконано в результаті виконання наступного коду?

class Program
    {
        private static readonly StudentsContext database = new StudentsContext();

        private static IQueryable<Student> GetStudents() => database.Students;

        private static IQueryable<Subject> GetSubjects() =>
            from subject in database.Subjects where subject.Name select subject; 

        static void Main(string[] args)
        {
            var count = from s in GetStudents()
                join sbj in GetSubjects() on s.SubjectId equals sbj.SubjectId
                group s.Name by sbj
                into g
                select new
                {
                    Count = g.Sum(s => s)
                }
                .ToList();
        }
    }

a) Жодного
b) Один
c) Два
d) Три

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

Запити в LINQ діляться на два основні типи: ті, які виконуються негайно (Immediate), і ті, що виконуються в ході програми (Deferred / Lazy).

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

Для того, щоб виконати запит який повертає одиничне значення, нам необхідно застосувати один з aggregate (Sum(), Count(), etc) або element (First(), Last()) операторів.

Для того, щоб виконати запит який повертає колекцію, ми можемо викликати ToList(), ToDictionary() або ToArray().

Докладніше про Immediate / Deferred Query Execution можна знайти тут, а повний список операторів - тут.

У нашому випадку запит буде виконаний при виклику методу ToList(), так як всі інші методи (крім Sum) належать до Deferred Execution. Не дивлячись на те, що метод Sum() відноситься до Immediate Execution операторів, запит не буде виконаний одразу, так як після використання методу GroupBy() ми маємо доступ до SQL функцій агрегацій і Sum буде виконана на стороні SQL сервера. Тому правильною відповіддю є варіант b) Один.

-
[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.

-
[QA] Дано поле для введення ім'я користувача, у якого є обмеження: кількість символів повинна бути не менше 1 і не більше 15. Виберіть, які тестові дані перевіряють граничні значення за кількістю.

a) -порожньо-
b) a
c) Ян
d) Іван
e) Іван Петрович
f) Іван Васильович
g) Іван Афанасійович


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

Для виконання цього завдання потрібно згадати про аналіз граничних значень - перевірка поведінки системи на "межі" вхідних даних. У нашому випадку це 1 і 15. У теорії, до меж потрібно додавати 1 і -1, що дасть в результаті 6-ть перевірок, але це є надмірним. Тому, достатньо перевірити невалідні - 0, 16, та валідні - 1, 15. Таким чином правильними відподями будуть варіанти a, b, f, g

-
[JS] Які функції у наведеному фрагменті коду можна назвати чистими (pure function)?

let a1 = 10;
let a2 = 5;
const obj1 = {
	prop1: 2,
	prop2: 4
};

function func1(arg1, arg2) {
	return arg1 + arg2;
}

function func2(arg1, arg2) {
	return arg1 + a2 * arg2;
}

function func3(arg1, arg2) {
	a1 = arg2;
	return arg1 + a1;
}

a) func1
b) func2 и func3
c) func1 и func2
d) жодна

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

Спершу розберемося, що таке “чиста функція”. Чистими функціями називають функції, які відповідають двом умовам:

  • При одних і тих самих аргументах функція завжди повертає одне і те саме значення
  • Не містить побічних ефектів (side effects)

У першій умові йдеться про те, що функція завжди повертатиме однаковий результат, якщо вхідні аргументи також однакові. Наприклад, функція added завжди повертатиме суму вхідних аргументів.

function adder(x, y) {
	return x + y
}

adder(7, 5) //12
adder(7, 5) //12
adder(7, 5) //12

Але якщо використати аргументи ззовні, то результат вже не буде одним і тим самим, навіть якщо аргументи функції одні й ті ж самі:

let y = 5;

function adder(x) {
	return x + y;
}

adder(7) //12
y = 10;
adder(7) //17

Результат функції непередбачуваний, оскільки залежить від змінної y, і такі функції називають не чистими. Чисті функції не повинні покладатись на змінні за межами їх області видимості.

Друга умова стверджує, що чиста функція не повинна ніяк впливати на змінні зовні чи змінювати стан додатку, тобто містити так звані side effects. До побічних ефектів можна віднести роботу з DOM, роботу з файловою системою або HTTP запити.

Наприклад, функція adder хоча і повертає завжди один і той самий результат, та все одно окрім цього змінює стан ззовні (записує дані у файл):

function adder(x) {
	let result = x + y;
	fs.writeFileSync('result.txt', result);
	return result;
}

adder(7, 5) //12
adder(7, 5) //12

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

function adder(x) {
	return x + y;
}

let result = adder(7, 5);
fs.writeFileSync('result.txt', result);

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

var obj = {
	active: false
}

function setActive(obj) {
	obj.active = true;
}

setActive(obj);

Отже, повертаючись до питання:

  • функція func1чиста.
  • функція func2нечиста, оскільки використовує аргумент a2,запозичений ззовні.
  • функція func3також не чиста, оскільки мутує аргумент a1.

Правильна відповідь: а — func1

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

-
[PHP] Що відбудеться зі змінними $a і $b після виконання наведеного коду?

function doSomething(array $array, \DateTime $object)
{
   $object->modify('+5 days');
   $array[] = $object;
}

$a = [];
$b = new \DateTime('2020-01-01');

doSomething($a, $b);

a) $a міститиме об’єкт $b, $b залишиться без змін
b) $a і $b залишаться без змін
c) $aзалишиться без змін, об’єкт $b буде змінено
d) $a буде містити об’єкт $b, об’єкт $b зміниться

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

Як ви напевно знаєте, у РНР змінні об’єктів не містять самого об’єкта як значення. Така змінна містить лише ідентифікатор об’єкта, котрий дозволяє знайти конкретний об’єкт при зверненні до нього. Коли об’єкт передається як аргумент функції, повертається або присвоюється іншій змінній, то ці різні змінні містять копію ідентифікатора, що вказує на один і той самий об’єкт. Таким чином варіанти a і b, вочевидь, неправильні. Більш детальну інформацію про ООП і роботу з об’єктами можна знайти тут — Classes and Objects — Manual

З масивами (у РНР масиви — хеш-таблиці), все відбувається дещо інакше. Річ у тім, що при передачі масиву як аргумента функції або при присвоюванні масиву іншій змінній завжди відбувається копіювання значення Arrays — Manual Це означає, що змінна $array у функції doSomething містить свою локальну копію масиву $a, і змінну $a не буде змінено. Правильний варіант відповіді — c.

Розгляньмо ще такий приклад:

$a = new \DateTime('2020-01-01');
$arrayA = [$a];
$arrayB = $arrayA;
$a->modify('+5 days');

У цьому прикладі $arrayB є копією масиву $arrayA. У свою чергу, $arrayA (як і $arrayB) у якості свого єдиного елемента містить ідентифікатор об’єкта $a. Це означає, що при зміні об’єкта $a фактично оновиться і значення елементів масивів $arrayA і $arrayB.

-
[.NET] Який результат виконання поданого методу?

public string GetString()
{
 var a = "Binary Academy";
 var b = a;
 a.Replace("Binary", "Music");

 return b;
}

a) Exception
b) "Music Academy"
c) "Binary Academy"
d) ""

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

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

Стрічки в .NET незмінні, тому при виконанні операцій над ними методи повертають нову стрічку. У нашому випадку метод Replace поверне нову стрічку “Music Academy”, але результат нікуди не буде присвоєно, а сама змінна а залишиться неторканою через імутабельність. Однак, навіть якби ми присвоїли результат назад в а, створився б новий рядок (а отже, нове посилання), але b при цьому не зміниться, оскільки буде так само зберігати лінк на початковий об’єкт.

Тому правильна відповідь — варіант c. “Binary Academy”

-
[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/.

-
[QA] У специфікації вказано, що форма реєстрації нової соціальної мережі мусить витримувати навантаження у 10 одночасних з'єднань. В продакшені навантаження складало понад 100 одночасних підключень. Який QA процес допоміг би уникнути цієї проблеми?


a) валідація
b) верифікація
c) оцінка ризиків
d) тестування навантаження

Розгорнути правильну відповідь з поясненням
У вимогах чітко вказано, що система повинна витримувати навантаження у 10 паралельних підключень, і перевірка цієї вимоги є верифікацією (перевірка відповідності додатку прописаним вимогам). Можливо, існує думка, що тестування навантаження допомогло б виявити проблему ще до її появи, але таке тестування грунтувалося б на специфікації. Тому про 100 одночасних підключень ніхто б не думав. Тож правильна відповідь — валідація, перевірка відповідності додатку вимогам, що маються на увазі (всі решта, крім прописаних). Для цього необхідно думати поза документацією і розуміти домен продукту, що розробляється.