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

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

-
[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) кількість тестів, які ще не виконані