Чи покращення ефективності використання кінцевого ключового слова на Java?


347

У Java ми бачимо багато місць, де finalможна використовувати ключове слово, але його використання є рідкісним.

Наприклад:

String str = "abc";
System.out.println(str);

У вищенаведеному випадку це strможе бути, finalале це, як правило, припинено.

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

Чи справді використання кінцевого ключового слова в будь-якому або в усіх цих випадках справді покращує ефективність? Якщо так, то як? Будь ласка, поясніть. Якщо правильне використання finalдійсно має значення для продуктивності, які звички повинен розвивати програміст Java, щоб найкраще використовувати ключове слово?


Я не думаю, що товариш, диспетчеризація методів (кешування сайтів викликів і ...) - проблема в динамічних мовах, а не в мовах статичного типу
Jahan

Якщо я запускаю свій інструмент PMD (плагін для затемнення), який використовується для ознайомлення, він пропонує внести зміни для змінної у випадку, як показано вище. Але я не зрозумів її концепції. Дійсно, вистава вражає так сильно ??
Abhishek Jain

5
Я подумав, що це типове екзаменаційне питання. Я пам'ятаю , що остаточне дійсно впливають на продуктивність, IIRC кінцеві класи можуть бути оптимізовані з допомогою JRE в деякому роді , тому що вони не можуть бути підкласи.
Kawu

Я насправді це перевірив. На всіх віртуальних машинах я перевірив, використання остаточного на локальних змінних зробив поліпшити продуктивність (трохи, але тим не менш може бути фактором в методах корисності). Вихідний код у моїй відповіді нижче.
іржа

1
"передчасна оптимізація - корінь усього зла". Просто дозвольте компілятору зробити свою роботу. Напишіть читабельний та добре коментований код. Це завжди найкращий вибір!
кайзер

Відповіді:


285

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

(Звичайно, це припущення, що ви використовуєте HotSpot - але це, безумовно, найпоширеніший JVM, так що ...)

На мій погляд, ви повинні використовувати finalна основі чіткого дизайну та читабельності, а не з міркувань продуктивності. Якщо ви хочете змінити що-небудь з міркувань продуктивності, вам слід виконати відповідні вимірювання, перш ніж вигинати найясніший код з форми - таким чином ви зможете вирішити, чи варто досягти будь-якої додаткової продуктивності, ніж поганої читабельності / дизайну. (На мій досвід, цього майже ніколи не варто; YMMV.)

EDIT: Як уже згадувалося підсумкові поля, варто довести, що вони в будь-якому разі є хорошою ідеєю з точки зору чіткого дизайну. Вони також змінюють гарантовану поведінку з точки зору видимості перехресних потоків: після завершення конструктора будь-яке остаточне поле гарантовано буде видно в інших потоках негайно. Це, мабуть, найпоширеніше finalв моєму досвіді, хоча, як прихильник принципу "дизайн спадщини або заборони його" Джоша Блоха, я, мабуть, повинен finalчастіше використовувати для занять ...


1
@Abhishek: Про що зокрема? Найважливіший момент - останній - що ви майже напевно не повинні про це турбуватися.
Джон Скіт

9
@Abishek: finalзазвичай рекомендується, оскільки це полегшує розуміння коду та допомагає знаходити помилки (оскільки це робить наміри програмістів явними). PMD, напевно, рекомендує використовувати finalчерез ці проблеми стилю, а не з міркувань продуктивності.
sleske

3
@Abhishek: Багато чого, ймовірно, буде специфічним для JVM, і може покладатися на дуже тонкі аспекти контексту. Наприклад, я вважаю, що сервер HotSpot JVM все ще дозволить вбудовувати віртуальні методи при переопределенні в одному класі з швидкою перевіркою типу, де це необхідно. Але деталі важко визначити і цілком можуть змінюватися між випущеними.
Джон Скіт

5
Тут я хотів би процитувати Effective Java, 2 - е видання, пункт 15, Мінімізація мінливість: Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more secure.. Крім того , An immutable object can be in exactly one state, the state in which it was created.проти Mutable objects, on the other hand, can have arbitrarily complex state spaces.. З мого особистого досвіду використання ключового слова finalповинно підкреслити наміри розробника схилятися до незмінності, а не до "оптимізації" коду. Я закликаю вас прочитати цю главу, захоплююче!
Луї Ф.

2
З інших відповідей видно, що використання finalключового слова для змінних може зменшити кількість байтових кодів, що може вплинути на продуктивність.
Жульєн Кронегг

86

Коротка відповідь: не хвилюйтеся з цього приводу!

Довга відповідь:

Говорячи про кінцеві локальні змінні, майте на увазі, що використання ключового слова finalдопоможе компілятору статично оптимізувати код , що в кінцевому підсумку може призвести до більш швидкого коду. Наприклад, кінцеві рядки a + bв наведеному нижче прикладі з'єднуються статично (під час компіляції).

public class FinalTest {

    public static final int N_ITERATIONS = 1000000;

    public static String testFinal() {
        final String a = "a";
        final String b = "b";
        return a + b;
    }

    public static String testNonFinal() {
        String a = "a";
        String b = "b";
        return a + b;
    }

    public static void main(String[] args) {
        long tStart, tElapsed;

        tStart = System.currentTimeMillis();
        for (int i = 0; i < N_ITERATIONS; i++)
            testFinal();
        tElapsed = System.currentTimeMillis() - tStart;
        System.out.println("Method with finals took " + tElapsed + " ms");

        tStart = System.currentTimeMillis();
        for (int i = 0; i < N_ITERATIONS; i++)
            testNonFinal();
        tElapsed = System.currentTimeMillis() - tStart;
        System.out.println("Method without finals took " + tElapsed + " ms");

    }

}

Результат?

Method with finals took 5 ms
Method without finals took 273 ms

Тестовано на Java Hotspot VM 1.7.0_45-b18.

Отже, наскільки фактично покращено ефективність роботи? Я не наважуюся сказати. У більшості випадків, ймовірно, граничні (~ 270 наносекунд у цьому синтетичному тесті, оскільки конкатенація рядків взагалі уникається - рідкісний випадок), але у високооптимізованому коді корисності це може бути фактором. У будь-якому випадку відповідь на початкове запитання - так, це може покращити ефективність, але в кращому випадку .

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


2
Я трохи переписав ваш код, щоб перевірити обидва випадки 100 разів. Врешті-решт середній час фіналу становив 0 мс та 9 мс для нефіналу. Збільшення кількості ітерацій до 10М встановлює середнє значення 0 мс та 75 мс. Однак найкращий пробіг для нефіналу склав 0 мс. Можливо, це результати виявлення VM просто викидають чи щось? Я не знаю, але незалежно від використання фіналу суттєво зміниться.
Casper Færgemand

5
Помилковий тест. Попередні тести зігріють JVM і принесуть користь пізнішим випробуванням. Перестановіть свої тести і подивіться, що станеться. Вам потрібно запустити кожен тест у власному екземплярі JVM.
Стів Куо

16
Жоден тест не хибний, розминка була врахована. Другий тест - РІЗНИЙ, не швидший. Без розминки другий тест був би НАДІЛЬНО.
rustyx

6
У testFinal () весь час повертався один і той же об'єкт, з пулу рядків, тому що відновлення кінцевих рядків і об'єднання літеральних рядків оцінюється під час компіляції. testNonFinal () щоразу повертає новий об'єкт, що пояснює різницю у швидкості.
anber

4
Що змушує вас вважати сценарій нереальним? Stringконкатенація - набагато дорожча операція, ніж додавання Integers. Зробити це статично (якщо можливо) ефективніше, ось що показує тест.
іржа

62

ТАК це може. Ось приклад, коли фінал може підвищити продуктивність:

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

врахуйте наступне:

public class ConditionalCompile {

  private final static boolean doSomething= false;

    if (doSomething) {
       // do first part. 
    }

    if (doSomething) {
     // do second part. 
    }

    if (doSomething) {     
      // do third part. 
    }

    if (doSomething) {
    // do finalization part. 
    }
}

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

public class ConditionalCompile {

  private final static boolean doSomething= false;

    if (false){
       // do first part. 
    }

    if (false){
     // do second part. 
    }

    if (false){
      // do third part. 
    }

    if (false){
    // do finalization part. 

    }
}

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

public class ConditionalCompile {


  private final static boolean doSomething= false;

  public static void someMethodBetter( ) {

    // do first part. 

    // do second part. 

    // do third part. 

    // do finalization part. 

  }
}

таким чином зменшуючи зайві коди чи будь-які непотрібні умовні перевірки.

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

public class Test {
    public static final void main(String[] args) {
        boolean x = false;
        if (x) {
            System.out.println("x");
        }
        final boolean y = false;
        if (y) {
            System.out.println("y");
        }
        if (false) {
            System.out.println("z");
        }
    }
}

Під час компіляції цього коду з Java 8 та декомпіляції з нього javap -c Test.classми отримуємо:

public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static final void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: ifeq          14
       6: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #22                 // String x
      11: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      14: iconst_0
      15: istore_2
      16: return
}

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


1
@ ŁukaszLech Я дізнався про це з книги Oreilly: Hardcore Java, в їх главі щодо остаточного ключового слова.
mel3kings

15
Це говорить про оптимізацію під час компіляції, означає, що розробник знає значення VALUE остаточної булевої змінної під час компіляції, який сенс написання, якщо блоки в першу чергу, то для цього сценарію, коли IF-УМОВИ не потрібні та не роблять якийсь сенс? На мою думку, навіть якщо це підвищує продуктивність, це в першу чергу помилковий код, і його можна оптимізувати самим розробником, а не перекладати відповідальність на компілятор, і питання головним чином має намір задати питання про підвищення продуктивності у звичайних кодах, де використовується остаточний, який має програмний сенс.
Bhavesh Agarwal

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

37

За твердженням IBM - це не стосується класів чи методів.

http://www.ibm.com/developerworks/java/library/j-jtp04223.html


1
... і за даними IBM, це робиться для полів : ibm.com/developerworks/java/library/j-jtp1029/… - а також рекламується як найкраща практика.
Філцен

2
Стаття "04223" - з 2003 року. Зараз майже сімнадцять років. Це було ... Java 1.4?
dmatej

16

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

Для довідки це було протестовано на javacверсії 8, 9і 10.

Припустимо, цей метод:

public static int test() {
    /* final */ Object left = new Object();
    Object right = new Object();

    return left.hashCode() + right.hashCode();
}

Збірка цього коду , як це, видає точний же байт - код , як і при finalбув мати місце ( final Object left = new Object();).

Але цей:

public static int test() {
    /* final */ int left = 11;
    int right = 12;
    return left + right;
}

Виробляє:

   0: bipush        11
   2: istore_0
   3: bipush        12
   5: istore_1
   6: iload_0
   7: iload_1
   8: iadd
   9: ireturn

Залишаючи finalбути присутнім виробляє:

   0: bipush        12
   2: istore_1
   3: bipush        11
   5: iload_1
   6: iadd
   7: ireturn

Код досить зрозумілий, якщо є константа часу компіляції, він буде завантажений безпосередньо в стек операндів (він не буде зберігатися в масив локальних змінних, як це робиться в попередньому прикладі bipush 12; istore_0; iload_0) - що має сенс оскільки ніхто не може його змінити.

З іншого боку, чому у другому випадку компілятор не виробляє, istore_0 ... iload_0це не за межами мене, це не так, що слот 0використовується будь-яким чином (він може зменшити масив змінних таким чином, але, можливо, у мене відсутні деякі деталі внутрішніх справ, не можу скажи точно)

Я був здивований, побачивши таку оптимізацію, враховуючи, як javacце роблять маленькі . Як нам завжди користуватися final? Я навіть не збираюся писати JMHтест (який я хотів спочатку), я впевнений, що різниця в порядку ns(якщо можливо взагалі бути захопленим). Єдине місце, в чому це може бути проблемою, це те, коли метод не може бути вписаний через його розмір (а декларування finalзменшить цей розмір на кілька байт).

Є ще два питання final, які потрібно вирішити. По-перше, коли метод finalJITточки зору), такий метод є мономорфним - і це найулюбленіші з них JVM.

Потім є finalзмінні екземпляри (які повинні бути встановлені в кожному конструкторі); вони важливі, оскільки вони гарантують коректно опубліковані посилання, про що тут трохи доторкнулися, а також точно вказані JLS.


Я компілював код за допомогою Java 8 (JDK 1.8.0_162) з параметрами налагодження ( javac -g FinalTest.java), декомпілював код за допомогою javap -c FinalTest.classта не отримав однакових результатів (з final int left=12, я отримав bipush 11; istore_0; bipush 12; istore_1; bipush 11; iload_1; iadd; ireturn). Так, так, згенерований байт-код залежить від безлічі факторів, і важко сказати, чи finalвпливає чи ні впливає на продуктивність. Але оскільки байт-код відрізняється, може існувати певна різниця в продуктивності.
Жульєн Кронегг


13

Ви справді запитуєте про два (принаймні) різні випадки:

  1. final для локальних змінних
  2. final для методів / занять

Джон Скіт вже відповів 2). Про 1):

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

final може змінити поле для захищених / публічних полів класу; там компілятору дуже складно дізнатись, чи встановлюється поле не один раз, як це могло статися з іншого класу (який, можливо, навіть не був завантажений). Але навіть тоді JVM може використовувати техніку, яку описує Джон (оптимістично оптимізуйте, поверніть, якщо завантажений клас, який змінює поле).

Підсумовуючи це, я не бачу жодної причини, чому це повинно сприяти продуктивності. Тож такий тип мікрооптимізації навряд чи допоможе. Ви можете спробувати тестування, щоб переконатися, але я сумніваюся, що це матиме значення.

Редагувати:

Насправді, згідно з відповіддю Тимо Весткемпера, в деяких випадках final можна покращити ефективність для класових полів. Я стою виправлений.


Я не думаю, що компілятор може правильно перевірити кількість присвоєння локальної змінної: а що робити, якщо тоді ще й інші структури з численними призначеннями?
gyorgyabraham

1
@gyabraham: Компілятор вже перевіряє ці випадки, якщо ви оголосили локальну змінну як final, щоб переконатися, що ви не призначите її двічі. Наскільки я бачу, та сама логіка перевірки може бути (і, ймовірно, використовується) для перевірки, чи може бути змінною final.
sleske

Остаточна локальна змінна не виражається в байт-коді, тому JVM навіть не знає, що вона була остаточною
Стів Куо

1
@SteveKuo: Навіть якщо він не виражений у байт-коді, це може допомогти javacоптимізувати його. Але це лише спекуляція.
sleske

1
Компілятор міг з'ясувати, чи була призначена локальна змінна рівно один раз, але на практиці це не відбувається (крім перевірки помилок). З іншого боку, якщо finalзмінна має примітивний тип або тип Stringі одразу призначається константа часу компіляції, як у прикладі запитання, компілятор повинен її вбудувати, оскільки змінна є постійною часом компіляції за специфікацією. Але для більшості випадків використання код може виглядати по-різному, але все одно не має значення, чи константа є вкладеною чи зчитується з локальної змінної.
Холгер

6

Примітка: Не є експертом з Java

Якщо я правильно пам’ятаю свою Java, було б дуже мало способів покращити продуктивність за допомогою остаточного ключового слова. Я завжди знав, що він існує для "хорошого коду" - дизайну та читабельності.


1

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


1

Власне, під час тестування деякого коду, пов'язаного з OpenGL, я виявив, що використання остаточного модифікатора в приватному полі може погіршити продуктивність. Ось початок перевіреного мною класу:

public class ShaderInput {

    private /* final */ float[] input;
    private /* final */ int[] strides;


    public ShaderInput()
    {
        this.input = new float[10];
        this.strides = new int[] { 0, 4, 8 };
    }


    public ShaderInput x(int stride, float val)
    {
        input[strides[stride] + 0] = val;
        return this;
    }

    // more stuff ...

І це метод, який я використовував для тестування продуктивності різних альтернатив, серед яких клас ShaderInput:

public static void test4()
{
    int arraySize = 10;
    float[] fb = new float[arraySize];
    for (int i = 0; i < arraySize; i++) {
        fb[i] = random.nextFloat();
    }
    int times = 1000000000;
    for (int i = 0; i < 10; ++i) {
        floatVectorTest(times, fb);
        arrayCopyTest(times, fb);
        shaderInputTest(times, fb);
        directFloatArrayTest(times, fb);
        System.out.println();
        System.gc();
    }
}

Після третьої ітерації, коли VM підігрівся, я послідовно отримував ці цифри без остаточного ключового слова:

Simple array copy took   : 02.64
System.arrayCopy took    : 03.20
ShaderInput took         : 00.77
Unsafe float array took  : 05.47

З кінцевим ключовим словом:

Simple array copy took   : 02.66
System.arrayCopy took    : 03.20
ShaderInput took         : 02.59
Unsafe float array took  : 06.24

Зверніть увагу на цифри для тесту ShaderInput.

Не важливо, чи я роблю поля публічними, чи приватними.

До речі, є ще кілька приголомшливих речей. Клас ShaderInput перевершує всі інші варіанти, навіть із кінцевим ключовим словом. Це чудовий b / c, в основному це клас, що обертає плаваючий масив, а інші тести безпосередньо маніпулюють масивом. Треба розібратися в цьому. Може мати щось спільне з вільним інтерфейсом ShaderInput.

Також System.arrayCopy, мабуть, дещо повільніше для малих масивів, ніж просто копіювання елементів з одного масиву в інший у циклі for. І використання sun.misc.Unsafe (як і прямий java.nio.FloatBuffer, не показаний тут) виконує безвідмовно.


1
Ви забули зробити параметри остаточними. <pre> <code> public ShaderInput x (final int stride, final float val) {input [strides [stride] + 0] = val; повернути це; } </code> </pre> З мого досвіду, виконання будь-якої змінної чи фіналу поля дійсно може підвищити продуктивність.
Anticro

1
О, а також зробіть інші остаточними: <pre> <code> final int arraySize = 10; final float [] fb = new float [arraySize]; for (int i = 0; i <arraySize; i ++) {fb [i] = random.nextFloat (); } кінцеві int часи = 1000000000; for (int i = 0; i <10; ++ i) {floatVectorTest (раз, fb); arrayCopyTest (раз, fb); shaderInputTest (раз, fb); directFloatArrayTest (раз, fb); System.out.println (); System.gc (); } </code> </pre>
Anticro

1

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

Дедалі краща практика - робити змінними остаточними, де це можливо. Я хотів би, щоб Java за замовчуванням зробила "змінні" фіналом і мала ключове слово "Mutable", щоб дозволити зміни. Незмінні класи призводять до набагато кращого потокового коду, і лише огляд класу з "остаточним" перед кожним членом швидко покаже його непорушний.

Інший випадок - я перетворював багато коду для використання анотацій @ NonNull / @ Nullable (Ви можете сказати, що параметр методу не повинен бути нульовим, тоді IDE може попереджати вас про кожне місце, де ви переходите змінну, яка не позначена @ NonNull - вся справа поширюється до смішної міри). Набагато простіше довести змінну або параметр члена не може бути нульовим, коли він позначений остаточним, оскільки ви знаєте, що його не призначають більше ніде.

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

Остаточне поняття для методів чи занять - це ще одна концепція, оскільки вона забороняє дуже коректну форму повторного використання та насправді не дуже розказує читачеві. Найкраще використовувати, мабуть, те, як вони зробили String та інші внутрішні типи остаточними, так що ви могли покладатися на послідовну поведінку скрізь - Це запобігло багато помилок (хоча буває, що я хотів би ЛЮБИТЬ розширити рядок .... ой можливості)


0

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

'final' - це твердження, що ви маєте намір змінну не змінювати (тобто змінна не змінюватиметься!). Потім компілятор може допомогти вам, поскаржившись, якщо ви порушите власне обмеження.

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

(Я використовую заключний для змінних членів)


-3

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


2
Що за дурниці це? Чи є у вас джерела, чому ви вважаєте, що це так?
J. Doe

-4

final Ключове слово може використовуватися п’ятьма способами на Java.

  1. Клас є заключним
  2. Довідкова змінна є остаточною
  3. Локальна змінна є остаточною
  4. Метод є остаточним

Клас є остаточним: клас є остаточним - це означає, що ми не можемо поширюватися або успадкування означає успадкування не можливе.

Аналогічно - Об'єкт є остаточним: деякий час ми не змінювали внутрішній стан об'єкта, тому в такому випадку ми можемо вказати, що об'єкт є final object.object final означає, що не є змінною також final.

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


2
Під "об'єктом є остаточним" ви маєте на увазі "об'єкт незмінний".
gyorgyabraham

6
Ви праві, але не відповідаєте на питання. ОП не запитували, що finalозначає, але чи finalвпливати на результативність використання.
Honza Zidek
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.