Как стать автором
Обновить

Комментарии 76

Лучше алгоритмы на доске спрашивайте. По этим вопросам можно взять человека который потратил пару дней на подготовку к ним, и не взять всех остальных.

В вопросах в основном мелочи которые при необходимости гуглятся за 15 минут, но наизусть не помнятся если совсем недавно именно с ними не работал.
НЛО прилетело и опубликовало эту надпись здесь
Вы fizzbuzz или определение простое число или нет (любым способом) на доске не напишите? Не Гугл же.

Это гораздо лучше чем спрашивать «Generics: Ковариантность/контравариантность.» большая часть разработчиков слов то таких не знает. Ибо не надо. А если понадобится то быстрогугл сразу решает пробелму.
Проблему непонимания ковариантности гугл не решает. Насчет «не надо» — ну в общем да, не особо часто.
Определение из Вики:
Ковариантностью называется сохранение иерархии наследования исходных типов в производных типах в том же порядке. Так, если класс Cat наследуется от класса Animal, то естественно полагать, что перечисление IEnumerable<Cat> будет потомком перечисления IEnumerable<Animal>. Действительно, «список из пяти кошек» — это частный случай «списка из пяти животных». В таком случае говорят, что тип (в данном случае обобщённый интерфейс) IEnumerable<T> ковариантен своему параметру-типу 

Контравариантностью называется обращение иерархии исходных типов на противоположную в производных типах. Так, если класс String наследуется от класса Object, а делегат Action<T> определён как метод, принимающий объект типа T, то Action<Object>  наследуется от делегата Action<String>, а не наоборот. Действительно, если «все строки — объекты», то «всякий метод, оперирующий произвольными объектами, может выполнить операцию над строкой», но не наоборот. В таком случае говорят, что тип (в данном случае обобщённый делегат) Action<T> контравариантен своему параметру-типу T.


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

И там все в списке примерно такое. «Назвать методы Object» equals и hashCode я назову сразу. toString немного подумав. Остальное только по документации или исходникам JDK. Ибо оно не используется практически никогда.
>Чтобы найти прочитать и понять 5 минут достаточно.
Не совсем согласен. Во-первых, увы не всем достаточно, а главное, оно может и появляется, какое-то понимание, но очень краткосрочное.

>И через неделю-месяц это определение забудется за ненадобностью.
Ну я собственно этоже самое и имел в виду, когда говорил, что гугление не решает проблему понимания этой темы. Потому что настоящее понимание позволяет такое же определение написать/вывести, исходя по большей части из «здравого смысла».
Потому что настоящее понимание позволяет такое же определение написать/вывести, исходя по большей части из «здравого смысла».

Представим себе собеседование. Судя по формулировкам на нем прямо так и спросят. А расскажите о Ковариантность/контравариантность в дженериках Джавы. Тут любой кто не помнит определение будет в тупике и больше чем «Эээ. А что эти слова значат?» Выдавить из себя не сможет. В итоге собеседование завалено, хотя человек просто не знает определение и слов. Которые не используются в работе.

И опять таки остальное ровно такое же.
«Почему хранить пароль предпочтительнее в char[]/byte[], а не в String? (**)»
Вот прямо так ведь и спросят. Я честно затруднюсь ответить. На мой вкус хуже. Потому что граблей на которые можно наступить заметно больше, а плюсов нет.

И даже после их «правильного» ответа
Строка в виде литерала сразу раскрывает пароль, плюс она всегда хранится в string-пуле
byte[]/char[] возможно сбросить после использования, и удалить все ссылки на него

я все еще затрудняюсь. И скорее всего буду спорить. Если у зломышленника есть доступ к памяти процесса в котором пароли проверяются, то уже спасать пароли поздно. Пароли точно утекли. Исходим из этого.
Защита «Строка в виде литерала сразу раскрывает пароль, а переменная password четко видимая в любом отладчике видимо не раскрывает?» как-то по детски выглядит.
>Которые не используются в работе.
Вы никогда List что-ли не пишете? А как только вы его написали, и ежели у вашего MySuperObject есть наследники, вы должны понимать, что у вас имеет место, ко/контра либо инвариантность. А если не понимаете — вероятность накосячить весьма велика. Названий можете не знать, не вопрос. Но реально не понимать разницу, хотя бы на интуитивном уровне...? А аббревиатуру LSP вы тоже никогда не видели что-ли? Я если что не конкретно про кого-то лично, пусть будет абстрактный разработчик в вакууме.

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

>И опять таки остальное ровно такое же.
Ну, про остальное в целом — скорее согласен.
Вы никогда List что-ли не пишете? А как только вы его написали, и ежели у вашего MySuperObject есть наследники, вы должны понимать, что у вас имеет место, ко/контра либо инвариантность. А если не понимаете — вероятность накосячить весьма велика. Названий можете не знать, не вопрос. Но реально не понимать разницу, хотя бы на интуитивном уровне...?

Особенность джавы в том что это работает в одну сторону. А мы о Джаве говорим. И соответвенно эти определения получаются чисто академическими. Так что, что как и кого наследует в шаблонах расскажу без проблем. А вот про вариативности извините, не смогу. Они были в лучше случае на каком-то курсе и успешно забылись за ненадобностью. Соответвенно вопрос на собеседовании в котором будут они звучать вызовет замешательство и непонимание.

А аббревиатуру LSP вы тоже никогда не видели что-ли?

Мне даже вики не помогла.
Значения LSP:

    LSP — англ. label switch path — виртуальный канал, туннель, путь в протоколе MPLS.
    LSP — англ. layered service provider — технология ОС Windows.
    LSP — англ. lightest supersymmetric particle — легчайшая суперсимметричная частица.
    LSP — англ. Liskov substitution principle — принцип подстановки Барбары Лисков.
    LSP — белорусский музыкант и одноимённый белорусский музыкальный коллектив.

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

LSP — это принцип подстановки Лисков. Ну то есть, я опять же вполне допускаю вариант, что не зная ничего этого вы можете успешно много лет работать. Но только до какого-то предела — примерно пока вам не потребуется написать свою «коллекцию».

Если по русски "Наследники в Override функциях не должны делать ничего неожиданного." Я даже больше сказать могу. Весь код должен работать самым ожидаемым способом. Не надо форматировать диск в функции getValue() оверрайдит она что-то или нет не важно.


Это действительно такая странная и редкая вещь вещь что ее надо знать и обсуждать отдельно? Вроде обычный здравый смысл.

То что вы говорите — это совсем не про ко/контравариантность.

Это про написание нормального кода. Высказанное обычным языком. Без терминов которые не используются в реальности. Ну и максимально близко к теме.


Начали с одного из предлагаемых вопросов. Где предполагалось что пришедший на собеседование знает что означает слово ковариантность и выдаст определение из учебника. И моего возражения что это очень странный вопрос для собеседования.

Я вот слова эти знаю, даже знаю примерно какую ситуацию они описывают, но вечно путаю какое из них что значит ибо они черт побери почти одинаково написаны/звучат. А на практике я их встречал только в документации да книгах, в разговорах с коллегами ни разу не слышал.

Используете String.intern()? Спасибо, до свидания.

Не, ну почему же сразу? Но вот вопрос к автору по этому поводу у меня тоже имеется — а нафига спрашивать вот такое? Вот вы прям без String.intern() ну никак, вообще невозможно разрабатывать в вашей компании?

Если что, я видел замечание, что автор не предлагает задавать все вопросы из списка, но причина их появления в списке все-же осталась пока до конца не ясной.
Не, ну почему же сразу?

Потому что это создаёт проблемы на ровном месте, а что хорошего получаешь взамен — непонятно.

Я поэтому и хотел бы задать вопрос автору — нафига? А точнее, переформулировал бы подход к подобным вопросам вообще. Не «Что такое String.intern()?», как у автора, а акцентировать на то, для чего это применяется.

>а что хорошего получаешь взамен — непонятно.
И вот уже в зависимости от ответа на этот вопрос — обсуждать дальше технические тонкости. Например, почему вам непонятно, что можно получить потенциальный прирост производительности (в определенных условиях, разумеется)? А дальше уже обсуждаем, готовы ли вы купить этот прирост взамен на потенциальные проблемы (и расспросить, какие именно).

Ну т.е., любой из вопросов этого списка, на первый взгляд, как раз для меня выглядит лишь поводом, чтобы поговорить. А чтоб поговорить — сойдет любая в общем тема — но все-таки, лучше что-то более широко используемое.
Например, почему вам непонятно, что можно получить потенциальный прирост производительности (в определенных условиях, разумеется)?

Скажу честно, я не очень понимаю, как можно получить прирост в производительности с помощью String.intern(), а вот, как получить с его помощью уменьшение производительности в общем-то понятно.


С помощью String.intern() можно сэкономить память, но если вопрос производительности вас хоть как-то волнует, лучше экономить память другими методами.


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

Поддерживаю

Ну чисто в теории сравнение указателей дешевле чем сравнение строк. Прирост производительности получить можно. Но в проде так не надо делать, естесвенно.
Ну чисто в теории сравнение указателей дешевле чем сравнение строк.

Это конечно правда.


Прирост производительности получить можно.

Да? Каким образом?


Но в проде так не надо делать, естесвенно.

Если прирост в производительности действительно будет, почему не воспользоваться этим приёмом на проде?

Давайте натяну сову на глобус.
Есть у нас стейт машина. Большая и сложная. У которой состояние это строка. И внутри море функций проверяющих текущее состояние и если оно нужное что-то делающих. Замена .equals на == ускорит работу.


Потому что всегда есть более хороший способ сделать тоже самое.

Замена .equals на == ускорит работу.

Если вы будете где-то постоянно прогонять строки через String.intern(), то не ускорит. А если не будете, то при чём тут String.intern() ?

Так один раз на входе в эту стейт машину. И один раз при загрузке.
И дальше много-много раз ==.

Так один раз на входе в эту стейт машину. И один раз при загрузке.

Получается 2 раза за всё время работы программы? Или я что-то не понял?


Если я всё понял правильно, то String.intern() тут не нужен, нужен справочник внутри стейт машины.

При загруке intern на все варианты делаем. Для каждой входной строки один раз и далее == много раз.

Вы просили пример где быстрее будет. Я натянул сову на глобус, раз так надо. Естесвенно есть варианты сделать тоже самое лучше.
Вы просили пример где быстрее будет. Я натянул сову на глобус, раз так надо. Естественно есть варианты сделать тоже самое лучше.

Чтобы в описанном вами случае применить String.intern() нужно во-первых знать о том, что такое существует, а во-вторых зачем-то решить применить именно его, несмотря на то, что первая мысль тут — применить словарь.


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


Вот есть у нас объект, который нужно собрать, а потом больше он меняться не будет. Очевидное и плохое решение тут — сделать ему сеттеры. Неочевидное и хорошее — применить паттерн билдер.


Я не могу придумать ситуации, где плохое решение со String.intern() напрашивается само по себе.

Мне кажется собирать стейтмашину на строках — плохая идея. Чем вам Enum не угодили?
>Но в проде так не надо делать, естесвенно.
Почему не надо? Я видел утверждения, что этого добра полно как в коде runtime, так и в продуктах Apache. Это не гарантия конечно, но и считать всех авторов дураками заранее тоже не приходится.

Учитывая того как работает String.intern() авторам Jackson были предоставлены доказательства ( Jackson Issue #332 ) того, что не надо использовать этот подход для экономии памяти, вместо этого лучше использовать свой дедупликатор (который контроллируемо может экономить память), но автор оказался упёртым, даже не смотря на предоставление прод профиля.


Читать https://shipilev.net/jvm/anatomy-quarks/10-string-intern/

Ну, в принципе логично. Хотя ваш подход к интервью я все равно не понимаю. У любого механизма есть свои ограничения, свои достоинства и недостатки. Если человек их изложит — мы возможно получим представление о том, как он думает. Даже если в конкретном частном случае он ошибается.

Знать о вещи, которая вредная, можно конечно, но спрашивать такое на собеседовании — это полная дичь.


Если человек не знает её, значит поставить человека в неловкое положение, на вопрос потратили время (минимально 5 минут из обычно часа собеседования), у кандидата появится неприятное ощущение того, что его валят — и главный вопрос — зачем? Реальная практическая ценность близка к нулю.


Как по мне, кто начинает разговор о intern как правило сами не знают о чём оно.

Ну если вы посмотрите комменты — то вопрос «зачем» был первым, который я тут задал. На мой взгляд, спрашивать надо вообще не фичи языка/фреймворка и т.п., а то, как человек решает задачи — т.е. задача должна быть исходной точкой, а способ решения — вторичен (и если уж человек предложит intern как способ, и сможет это как-то обосновать — мы почти наверное многое узнаем о его уровне).
В некоторых случаях экономия памяти — это уже практически прямое повышение производительности.

>>Прирост производительности получить можно.
>Да? Каким образом?
Сравнение длинных строк — это операция намного более дорогая, чем сравнение указателей. Ну т.е. в зависимости от средней длины можно получить очень даже ого-го.

Ну а в целом — хороший же вопрос, если к нему подойти вот с такой стороны, разве нет?
В некоторых случаях экономия памяти — это уже практически прямое повышение производительности.

Да, но только если действия, которые предприняты для экономии памяти не убивают производительность.


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

Небольшая поправочка. Сравнение длинных строк одинаковой длины — операция намного более дорогая, чем сравнение указателей. Если строки разной длины, то выигрыш уже не такой большой.


Ну т.е. в зависимости от средней длины можно получить очень даже ого-го.

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


Ну а в целом — хороший же вопрос, если к нему подойти вот с такой стороны, разве нет?

В целом, да, хороший, единственное что обычно получается обсудить не сам String.intern(), а то, как он мог бы работать и какие преимущества мог бы дать.

Вы точно понимаете, что знаете как устроен String.intern() внутри ?

А почему вы решили, что нет? Если что, про память — это было совсем о другом.

volatile не только устанавливает отношение happens-before, это ключевое слово ещё и делает чтение и запись переменных атомарными. Необходимости использовать для этого атомики нет.


И сделать ключом Enum можно, даже если используется обыкновенный HashMap, работат будет только не очень быстро

Нет не делает, i++ волатайл не являются атомарной операцией в отличии от AtomicInteger с его методом incrementAndGet

Нет не делает

Нет, делает ))


i++ волатайл не являются атомарной операцией

I++ это не одна операция. Это сначала чтение переменной, а потом запись в переменную. volatile делает атомарными чтение и запись, но не их комбинации.

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

Извините, но чтение или запись и так являются атомарными операциями и без волатайл.

Только для переменных размером четыре байта и меньше. Для long и double гарантии атомарности чтения и записи нет.


Волатайл нам дает слабую синхронизацию, но не атомарность.

У volatile двойная семантика. Это ключевое слово создаёт отношение happens-before между записью и чтением и делает запись и чтение атомарными.

Вы правы насчет примитивных double и long, но это скорее исключение. Все остальные операции чтения и записи атомарны и без волатайл.
Еще раз, happens-before это не про атомарность, это про видимость значения. Волатайл обеспечивает видимость, но не атомарность.

Вы правы насчет примитивных double и long, но это скорее исключение. Все остальные операции чтения и записи атомарны и без волатайл.

Мне сложно оспорить ваше утверждение, особенно если учесть, что вы практически процитировали мой комментарий, на который отвечаете ))


Еще раз, happens-before это не про атомарность, это про видимость значения.

Не совсем. Отношение happens-before даёт гарантии, что изменения в памяти, сделанные до события, будут видны после события, так что речь идёт не о видимости одного значения, а о видимости значений всех переменных, даже если на них нет модификатора volatile.


Волатайл обеспечивает видимость, но не атомарность.

volatile создаёт отношение happens-before между записью и чтением и делает запись и чтение атомарными. Такие дела.

Мне кажется мы с вами говорим о разных вещах, если отбросить примитивы long double, можете мне объяснить что по по вашему значит не атомарное чтение или запись например int или String?

Мне кажется мы с вами говорим о разных вещах

Я говорю о том, что делает volatile. Вы, мне кажется, тоже.


если отбросить примитивы long double

Не надо отбрасывать long и double )), это неотъемлемая часть джавы


можете мне объяснить что по по вашему значит не атомарное чтение или запись например int или String?

Я могу объяснить, что значило бы неаторманое чтение int или String, но, как вы неоднократно повторили, большого смысла в этом нет, так как оно атомарно по спецификации.

операции чтения и записи атомарны и без волатайл.

Чтение без волатайл, читает значение из кэш, если оно там есть, которое может отличаться от значения в основной памяти. Такое чтение безопасно, пока с данным куском памяти работает один поток, потому что, когда он сделает запись, обновится так же и значение в кэше. Но если есть 2 потока и которые имеют разный кэш, когда один поток поменяет значение, в кэше второго останется старое.
Волатайл говорит о том, что значение всегда нужно читать из основной памяти, а не из кэш. Поэтому чтение с волатайл будет медленней чем без, но при этом есть гарантия, что другой поток получит корректное значение. Запись всегда идет в основную память.
По крайней мере так все было лет 10 назад, возможно с тех пор что то изменилось.

Спасибо, но как это противоречит моему определению атомарности?

Нашел хорошую цитату «под атомарностью операции зачастую подразумевают видимость ее результата всем участникам системы, к которой это относится (в данном случае — потокам)».

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

volatile применяет все барьеры памяти перед записью или чтением для обеспечения полной когерентности кэша процессоров. Но для рядовых джавистов протокол MESI не объясняют.

volatile применяет все барьеры памяти перед записью или чтением для обеспечения полной когерентности кэша процессоров.

А ещё он обеспечивает атомарность записи и чтения ))

Это да.

Только атомарность операций типа i++ уже не обеспечивается, так как там отдельно чтение и отдельно запись и уже между ними может кто-то вклиниться.

А в классах типа AtomicInteger наблюдаются бесконечные циклы.

 public 
final int getAndSet(int newValue) {
        for (;;) {   <-=====================*
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }


Я как-то эксперментировал, чтобы посчитать, — сколько итераций этот цикл может проходить, если два потока лезут к одной переменной. В самых худших случаях получалось аж по 200 раз. Это еще с учетом того, что сам CAS(asm cmpxchg) занимает намного больше тактов процессора, чем простые операции работы с памятью.

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

Да, легко. Но для разумного использования многопоточности в первую очередь надо знать теорию, а потом уже выяснять про барьеры памяти и всё такое. Из низкоуровневых штук, наверное нужно знать только про такие явления, как попадание конкуретно изменяемых данных в одну линию кеша.


А чтобы умно использовать, — надо знать барьеры памяти.

Вот, кстати, вы привели хороший пример. Знание о борьерах памяти в описаной вами ситуации не очень сильно помогает. Сильно помогает знаение о том, что такое оптимистическая блокировка и пессимистическая блокировка и когда какую использовать.


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


Теория рулит, короче, хотя и про барьеры памяти узнать тоже не помешает.

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

Караул! Комментарии воруют! :)

Но для рядовых джавистов протокол MESI не объясняют.

И правильно делают. Это низкоуровневые детали реализации, которые могут поменяться.

Лучше тогда сразу от многопоточности уйти в многопроцессность, как сделал google chrome. Там намного меньше проблем.
а теперь представьте, что этот список вопросов и ожидаемый правильный ответ какой-нибудь сеньор-помидор дал HR-у, и он/она будет эти вопросы задавать и фильтровать кандидатов на первоначальном скрининге…

В статье уже есть пометка :)


Это также не руководство к действию — не надо закидывать бедных кандидатов всеми вопросами из списка.
Значит не надо идти в такую контору. Если профессиональные вопросы задаёт не профессионал, то и профессиональность ответов он не сможет оценить.
Мало того, что надо оценивать правильность ответов, но надо понимать в чём конкретно была ошибка. Возможно собеседник термином ошибся, и описал полный ответ на другой вопрос.
Тут в комментариях все набросились на volatile, а в статье требуют апдейта гораздо более базовые вещи.

  1. Пожалуйста, пожалуйста, пожалуйста замените Random на ThreadLocalRandom. Класс ThreadLocalRandom существует начиная с Java 7. Он в 3-4 раза быстрее, потокобезопасный, более удобный. О его абсолютном преимуществе перед Random написано в книге «Effective Java». Использовать Random сейчас — то же самое, что использовать какой-нибудь Vector вместо ArrayList.
  2. Для чего нужен LinkedHashMap? — почему-то не увидел в ответе «для реализации LRU cache», хотя это один из основных сценариев использования, о чём даже сказано в его Javadoc
  3. Секция про ввод-вывод крайне куцая и устаревшая. Вообще ничего нет про java.nio, что странно в 2020м. Хотя бы включите вопрос «какие знаете способы прочитать текстовый файл
Офтопик немного, но какая же хорошая презентация! Забрал в закладки.

Мне кажется, что тут должна быть ссылка на все.
Помоему вопросы это только на экзамене годяться чтоб проверить учил или не учил последние пару дней, а подбору работника они никак не помогут.
Всегда давал тестовое задание на простой кейс, где базу нужно создать со связями сохранить туда данные из реквеста и потом достать то что нужно.
По итогам всегда видно подумал человек об удобстве тестирования или нет, перформанс, ну и как сами таблицы выглядят и в спеке есть небольшие детали которые показывают на сколько кандидат хорошо умеет спеку читать, а не делает только то что хочет видеть.
Хороший сениор обычно делает всё по максимуму хорошо, ленивый что-то упустит, а джуниоры сделают как умеют особо не парясь.
Назвать методы Object

Это что-то навроде проверки, знает ли студент, на экзамен по какому предмету он пришёл?

А в ответе на вопрос про Thread states не должно быть еще TIMED_WAITING?

Сразу бросилось в глаза отсутствие Object.clone()

Вы часто используете Object.clone()?

Нет, но и finalize() не использую, а его вы добавили.
+
PECS — Producer-Extends-Consumer-Super, метод отдаёт ковариантный тип, принимает контравариантный (прим. автора — последнее интуитивно не очень понятно)


Интуитивно можно понять контрвариантность как это обращение иерархии наследования по отношению к действию над иерархией, например, все операции, которые можно выполнить над object можно выполнить и над string
устройство атомиков и конкурентных коллекций вы оцениваете в 2 звёзды, а способы запуска потока в 3? серьезно?
НЛО прилетело и опубликовало эту надпись здесь
volatile. happens-before. (**) Для double/long типов есть проблема атомарности, она решается через атомики

Как раз для volatile это утверждение неверно. JLS гарантирует атомарность чтения/записи volatile long/double.
docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7

Не знаю что тут можно собирать 2 года. Полгода и то много. Я джаву учу 7 месяцев (до этого ранее знать что есть такой язык, может быть имел представления о переменных, циклах, но не более того). Так что тут всё просто. Тут необходимый, но недостаточный уровень знаний. Как то так. Пара вопросов для меня не были известны. А так, конечно, спасибо, но это явно не достаточно для полноценного разработчика.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации