JavaRush /Java блог /Random UA /Проектування Класів та Інтерфейсів (Переклад статті)
fatesha
22 рівень

Проектування Класів та Інтерфейсів (Переклад статті)

Стаття з групи Random UA
Проектування Класів та Інтерфейсів (Переклад статті) - 1

Зміст

  1. Вступ
  2. Інтерфейси
  3. Інтерфейс-маркери
  4. Функціональні інтерфейси, статичні методи та методи за замовчуванням
  5. Абстрактні класи
  6. Незмінні (постійні) класи
  7. Анонімні класи
  8. Видимість
  9. успадкування
  10. Множинне успадкування
  11. Спадкування та композиція
  12. Інкапсуляція
  13. Final класи та методи
  14. Що далі
  15. Завантажити вихідний код

1. ВВЕДЕННЯ

Незалежно від того, яку мову програмування ви використовуєте (і Java тут не виняток), дотримання правильних принципів проектування - є ключовим фактором до написання чистого, зрозумілого та піддається перевірці коду; а також створювати його довгоживучим, що легко підтримує вирішення проблем. У цій частині уроку ми збираємося обговорити фундаментальні будівельні блоки, які надає мову Java, і ввести пару принципів проектування, прагнучи допомогти вам зробити найкращі дизайн-рішень. Точніше, ми збираємося обговорити інтерфейси та інтерфейси з використанням методів за замовчуванням (нова функція Java 8), абстрактні та остаточні (final) класи, незмінні класи, успадкування, композицію та переглянути правила видимості (або доступності), яких ми коротко торкнулися в 1 частині уроку"How to create and destroy objects" .

2. ІНТЕРФЕЙСИ

В об'єктно-орієнтованому програмуванні поняття інтерфейсів формує основи розвитку контрактів (прим. перекладача: "Контракт" в ОВП — набір чітко визначених умов, що регулюють відносини між класом-сервером та його клієнтами). Двома словами, інтерфейси визначають набір методів (контрактів) і кожен клас, який потребує підтримки цього специфічного інтерфейсу, повинен забезпечити реалізацію цих методів: досить проста, але потужна ідея. Багато мов програмування мають інтерфейси у тому чи іншого формі, але Java особливо забезпечує підтримку мови при цьому. Давайте поглянемо на просте інтерфейсне визначення Java.
package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
void performAction();
}
У фрагменті вище, інтерфейс, який ми назвали SimpleInterface, оголошує лише один спосіб з ім'ям performAction. Головні відмінності інтерфейсів стосовно класів — те, що інтерфейси окреслюють, який має бути контакт (оголошують метод), але з забезпечують їх реалізацію. Однак, інтерфейси в Java можуть бути складнішими: вони можуть включати вкладені інтерфейси, класи, підрахунки, анотації та константи. Наприклад:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefinitions {
    String CONSTANT = "CONSTANT";

    enum InnerEnum {
        E1, E2;
    }

    class InnerClass {
    }

    interface InnerInterface {
        void performInnerAction();
    }

    void performAction();
}
У цьому складнішому прикладі є кілька обмежень, які інтерфейси беззастережно накладають щодо вкладених конструкцій та оголошень методу, і до виконання цього примушує компілятор Java. Перш за все, навіть якщо це не було оголошено очевидно, кожне оголошення методу в інтерфейсі є публічним (public) (і може бути лише публічним). Таким чином, наступні оголошення методів еквівалентні:
public void performAction();
void performAction();
Варто згадати, що кожен окремий метод в інтерфейсі явно оголошений абстрактним і навіть ці оголошення методу еквівалентні:
public abstract void performAction();
public void performAction();
void performAction();
Що стосується оголошених полів констант, додатково до того, що вони є публічними , вони також неявно статичні і позначені, як final . Тому такі оголошення також еквівалентні:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
І, нарешті, вкладені класи, інтерфейси чи підрахунки, крім того, що є публічними , також неявно оголошені як static . Наприклад, дані оголошення також еквівалентні:
class InnerClass {
}

static class InnerClass {
}
Стиль, який ви оберете - це ваша особиста перевага, проте знання цих простих властивостей інтерфейсів може врятувати вас від непотрібного введення.

3. Інтерфейс-маркер

Інтерфейс маркери - це особливий вид інтерфейсу, який не має методів або інших вкладених конструкцій. Як це визначає бібліотека Java:
public interface Cloneable {
}
Інтерфейс-маркери - не контракти по суті, але частково корисна техніка, щоб "прикріпити" або "зв'язати" деяку специфічну межу з класом. Наприклад, щодо Cloneable , клас позначений як доступний для клонування, проте, спосіб, яким це можна або слід реалізувати, не є частиною інтерфейсу. Ще один дуже відомий і широко використовуваний приклад інтерфейс-маркера - Serializable:
public interface Serializable {
}
Цей інтерфейс позначає клас, як придатний для перетворення на послідовну форму (серіалізацію) і десеріалізацію (deserialization), і знову, це не вказує спосіб, як це можна або слід реалізовувати. Інтерфейс-маркери займають своє місце в об'єктно-орієнтованому програмуванні, хоча вони не задовольняють головну мету інтерфейсу, щоб бути контрактом. 

4. ФУНКЦІОНАЛЬНІ ІНТЕРФЕЙСИ, МЕТОДИ ЗА УМОВЧЕННЯМ І СТАТИЧНІ МЕТОДИ

З випусків Java 8 інтерфейси отримали нові дуже цікаві можливості: статичні методи, методи за замовчуванням і автоматичне перетворення з лямбд (функціональні інтерфейси). У розділі інтерфейси ми підкреслювали той факт, що інтерфейси в Java можуть тільки оголошувати методи, але не забезпечують їх реалізацію. З методом за промовчанням, все інакше: інтерфейс може відзначити метод ключовим словом default та забезпечити реалізацію для нього. Наприклад:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    void performAction();

    default void performDefaulAction() {
        // Implementation here
    }
}
Будучи на рівні екземпляра, методи за замовчуванням могли бути перевизначені кожною реалізацією інтерфейсу, але тепер інтерфейси можуть включати статичні методи, наприклад: package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}
Можна сказати, що надання реалізації в інтерфейсі завдає поразки цілому задуму контрактного програмування. Але є багато причин, чому ці функції були введені в мову Java і незалежно від того, наскільки вони корисні або збивають з пантелику, вони є там для вас і вашого користування. Функціональні інтерфейси це зовсім інша історія і вони випробувані як дуже корисні доповнення до мови. В основному, функціональний інтерфейс - це інтерфейс лише з одним абстрактним методом, оголошеним у ньому. Runnableінтерфейс із стандартної бібліотеки – це дуже гарний приклад цієї концепції.
@FunctionalInterface
public interface Runnable {
    void run();
}
Компілятор Java по-різному обробляє функціональні інтерфейси і може перетворити лямбда-функцію на реалізацію функціонального інтерфейсу, де це має сенс. Давайте розглянемо наступний опис функції: 
public void runMe( final Runnable r ) {
    r.run();
}
Для виклику цієї функції Java 7 і нижче повинна надаватися реалізація інтерфейсу Runnable(наприклад використовуючи анонімні класи), але Java 8 достатньо передати реалізацію методу run() використовуючи синтаксис лямбди:
runMe( () -> System.out.println( "Run!" ) );
Крім того, анотація @FunctionalInterface (анотації будуть розкриті в деталях у 5 частині підручника) натякає, що компілятор може перевірити, чи містить інтерфейс лише один абстрактний метод, тому будь-які зміни, внесені в інтерфейсі в майбутньому, не порушуватимуть цього припущення.

5. АБСТРАКТНІ КЛАСИ

Ще одна цікава концепція, яку підтримує Java, — поняття абстрактних класів. Абстрактні класи частково схожі на інтерфейси Java 7 і дуже близькі інтерфейсу з методом за замовчуванням Java 8. На відміну від звичайних класів, не можна створювати екземпляри абстрактного класи, але він може бути підкласом (зверніться до розділу "Спадкування" для отримання більш докладної інформації ). Що ще важливіше, абстрактні класи можуть містити абстрактні методи: особливий вид методів без реалізації, як і інтерфейс. Наприклад:
package com.javacodegeeks.advanced.design;

public abstract class SimpleAbstractClass {
    public void performAction() {
        // Implementation here
    }

    public abstract void performAnotherAction();
}
У цьому прикладі клас SimpleAbstractClassоголошений як абстрактний і містить один оголошений абстрактний метод. Абстрактні класи дуже корисні, більшість або навіть деякі частини деталей реалізації можуть спільно використовуватися з багатьма підкласами. Як би там не було, вони, як і раніше, залишають двері прочиненими і дозволяють налаштувати поведінку, властиву кожному з підкласу за допомогою абстрактних методів. Варто згадати, на відміну інтерфейсів, які можуть містити лише громадські оголошення, абстрактні класи можуть використовувати всю силу правил доступності, щоб управляти видимістю абстрактного способу.

6. НЕЗМІННІ КЛАСИ

Незмінність стає все більш важливою в розробці програмного забезпечення в наш час. Підйом багатоядерних систем викликало багато питань, пов'язаних із спільним використанням даних та паралелізмом. Але одна проблема виразно виникла: невеликий (або навіть відсутність) зміненого стану призводить до кращої розширюваності (масштабованості) і більш простого міркування про систему. На жаль, мова Java не забезпечує гідну підтримку класової незмінності. Однак, користуючись комбінацією технік, можна проектувати класи, які незмінні. Насамперед, всі поля класу мають бути остаточні (позначені як final ). Це добрий початок, але не дає гарантій. 
package com.javacodegeeks.advanced.design;

import java.util.Collection;

public class ImmutableClass {
    private final long id;
    private final String[] arrayOfStrings;
    private final Collection<String> collectionOfString;
}
По-друге, стежте за правильною ініціалізацією: якщо поле є посиланням на колекцію або масив, не призначайте ті поля безпосередньо з аргументів конструктора, разом робіть копії. Це буде гарантувати, що стан колекції або масиву не буде змінено за межами.
public ImmutableClass( final long id, final String[] arrayOfStrings,
        final Collection<String> collectionOfString) {
    this.id = id;
    this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
    this.collectionOfString = new ArrayList<>( collectionOfString );
}
І, нарешті, забезпечення належного доступу (гетери). Для колекцій, незмінний вигляд має бути наданий у вигляді обгортки  Collections.unmodifiableXxx: З масивами єдиний спосіб забезпечити справжню незмінність – це надати копію замість повернення посилання на масив. Це може бути неприйнятним з практичної точки зору, оскільки це дуже залежить від розміру масиву і може покласти величезний тиск на збирача сміття.
public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
Навіть цей маленький приклад дає хорошу ідею, що незмінність ще громадянин першого класу в Java. Все може бути ускладнене, якщо незмінний клас має поле, що посилається на об'єкт іншого класу. Ті класи повинні бути також незмінними, проте немає жодного способу це забезпечити. Є кілька гідних аналізаторів вихідного коду Java, як FindBugs і PMD, які можуть суттєво допомогти, перевіряючи ваш код і вказуючи на загальні недоліки програмування Java. Ці інструменти – великі друзі будь-якого розробника Java.

7. АНОНІМНІ КЛАСИ

У попередній Java 8 era, анонімні класи були єдиним способом забезпечити оперативне визначення класів та негайне створення екземпляра. Метою анонімних класів було зменшити шаблон та забезпечити короткий та легкий шлях представлення класів як запис. Давайте поглянемо на типовий старомодний шлях породити новий потік у Java:
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
}
У цьому прикладі реалізація Runnableinterface надається відразу як анонімний клас. Хоча є деякі обмеження, пов'язані з анонімними класами, основні недоліки їх використання — докладний синтаксис конструкцій, яким зобов'язує Java як мову. Навіть просто анонімний клас, який нічого не робить, вимагає щонайменше 5 ліній коду щоразу під час запису.
new Runnable() {
   @Override
   public void run() {
   }
}
На щастя, з Java 8, лямбдою і функціональними інтерфейсами всі ці стереотипи скоро підуть, нарешті написання коду Java буде виглядати по-справжньому коротко.
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread( () -> { /* Implementation here */ } ).start();
    }
}

8. ВИДИМІСТЬ

Ми вже трохи говорабо про правила видимості та доступності Java в частині 1 підручника. У цій частині ми збираємося повернутися до цієї теми знову, але у контексті створення підкласів. Проектування Класів та Інтерфейсів (Переклад статті) - 2Видимість різних рівнів дозволяє або забороняє класам переглядати інші класи або інтерфейси (наприклад, якщо вони знаходяться в різних пакетах або вкладені один в одного) або підкласів бачити та отримувати доступ до методів, конструкторів та полів їхніх батьків. У наступному розділі, успадкування, ми побачимо це у дії.

9. СПАДЧИНА

Спадкування - одне з ключових понять об'єктно-орієнтованого програмування, що виступає як основа побудови класу зв'язків. У поєднанні з видимістю та правилами доступності, успадкування дозволяє проектувати класи ієрархії, з можливістю розширення та підтримування. На понятійному рівні, успадкування в Java реалізується за допомогою створення підкласів та ключового слова extends, разом із батьківським класом. Підклас успадковує всі публічні та захищені елементи батьківського класу. Крім того, підклас успадковує package-private елементи батьківського класу, якщо обидва (підклас та клас) знаходяться в одному пакеті. При цьому, дуже важливо незалежно від того, що ви намагаєтеся спроектувати, дотримуватися мінімального набору методу, які клас виставляє публічно або для його підкласів. Наприклад, давай ті розглянемо клас Parentта його підклас Child, щоб продемонструвати різницю у рівнях видимості та їх ефекти.
package com.javacodegeeks.advanced.design;

public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";

    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;

    // No one can see it
    private class PrivateClass {
    }

    // Only visible to subclasses
    protected interface ProtectedInterface {
    }

    // Everyone can call it
    public void publicAction() {
    }

    // Only subclass can call it
    protected void protectedAction() {
    }

    // No one can call it
    private void privateAction() {
    }

    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
package com.javacodegeeks.advanced.design;

// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }

    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }

    public void childAction() {
        this.protectedField = "value";
    }
}
Спадкування – дуже велика тема сама по собі, з великою кількістю тонких деталей, характерних для Java. Однак, є кілька правил, які легко дотримуватися, і які можуть дуже допомогти зберегти стислість в класовій ієрархії. У Java кожен підклас може перевизначати будь-які успадковані методи його батька, якщо він не був оголошений як остаточний (final). Однак, немає спеціального синтаксису або ключового слова, щоб позначити метод як перевизначений, що може призвести до плутанини. Ось чому була введена інструкція @Override : щоразу, коли ваша мета - перевизначити успадкований спосіб, будь ласка, використовуйте інструкцію @Override, щоб стисло позначити це. Інша дилема, з якою Java розробники постійно стикаються в проектування - це побудова класів ієрархії (з конкретними або абстрактними класами) порівняно з реалізацією інтерфейсів. Настійно рекомендуємо надавати перевагу інтерфейсам по відношенню до класів або абстрактних класів, де це можливо. Інтерфейси легші, їх простіше тестувати та підтримувати, плюс до всього, вони мінімізують побічні ефекти змін реалізації. Багато просунуті техніки програмування, такі як створення проксі (proxy) класів у стандартній бібліотеці Java, покладаються на інтерфейси.

10. МНОЖНЕ СПАДЧИНА

На відміну від С++ та деяких інших мов, Java не підтримує множинне успадкування: Java кожен клас може мати тільки одного прямого батька (з класом Objectу вершині ієрархії). Проте, клас може реалізовувати кілька інтерфейсів, і, таким чином, стікування (stacking) інтерфейсів - єдиний спосіб досягти (або імітувати) множинне успадкування Java.
package com.javacodegeeks.advanced.design;

public class MultipleInterfaces implements Runnable, AutoCloseable {
    @Override
    public void run() {
        // Some implementation here
    }

    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
Реалізація кількох інтерфейсів насправді досить потужна, але часто необхідність знову і знову використовувати реалізацію призводить до глибокої класової ієрархії як спосіб подолати відсутність підтримки множинного успадкування Java. 
public class A implements Runnable {
    @Override
    public void run() {
        // Some implementation here
    }
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
І так далі. Нещодавній випуск Java 8 вирішує проблему з використанням методів за замовчуванням. Через методи за замовчуванням, інтерфейси фактичні стали надавати не тільки контракт, а й реалізацію. Отже, класи, які реалізують ці інтерфейси, також автоматично успадкують ці реалізовані методи. Наприклад:
package com.javacodegeeks.advanced.design;

public interface DefaultMethods extends Runnable, AutoCloseable {
    @Override
    default void run() {
        // Some implementation here
    }

    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}

// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
Майте на увазі, що множинне спадкування дуже потужний, але й у той же час небезпечний інструмент. Добре відому проблему «Ромб смерті» часто називають основним дефектом реалізації множинного наслідування, тому розробники змушені проектувати класи ієрархії дуже ретельно. На жаль, інтерфейси Java 8 з методами за замовчуванням також стають жертвою цих дефектів.
interface A {
    default void performAction() {
    }
}

interface B extends A {
    @Override
    default void performAction() {
    }
}

interface C extends A {
    @Override
    default void performAction() {
    }
}
Наприклад, наступний фрагмент коду не вдасться скомпілювати:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
На даний момент, справедливо сказати, що Java як мова завжди намагався уникнути кутових випадків об'єктно-орієнтованого програмування, але, оскільки мова розвивається, деякі з тих випадків стали раптово з'являється. 

11. СПАДЧИНА І КОМПОЗИЦІЯ

На щастя, успадкування не єдиний шлях спроектувати ваш клас. Іншою альтернативою, яка, на думку багатьох розробників, є набагато кращою, ніж спадкування, є композиція. Ідея дуже проста: замість створення ієрархії класів їх потрібно компонувати з інших класів. Давайте подивимося на цей приклад:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
Клас Vehicleскладається з двигуна (engine) та коліс (плюс багато інших частин, які залишені осторонь для простоти). Однак, можна сказати, що клас Vehicleтак само є машиною (engine), тому може бути спроектована з використанням успадкування. 
public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}
Яке рішення проектування буде правильним? Загальні основні рекомендації відомі як IS-A (є) і HAS-A (містить) принципи. IS-A - це зв'язок успадкування: підклас також задовольняє класову специфікацію батьківського класу та різновид батьківського класу (Прим. перекладача: У книзі Heading in Java принцип IS-A описаний так: "Коли один клас успадковує інший, ми говоримо, що дочірній клас ( підклас) розширює батьківський Якщо ви хочете дізнатися, чи розширює одна сутність іншу, проведіть перевірку на відповідність - IS-A (є).») Отже, HAS-A - це зв'язок композиції: клас володіє (або містить) об'єкт, який В більшості випадків, принцип HAS-A працює краще, ніж IS-A з ряду причин: 
  • Проектування гнучкіше;
  • Модель стабільніша, оскільки зміни не поширюються через класову ієрархію;
  • Клас та його композиція слабо пов'язані порівняно з композицією, яка щільно пов'язує батька та його підклас.
  • Логічний хід думки в класі простіше, тому що всі його залежності включені в ньому ж, в одному місці. 
Як би там не було, успадкування має своє місце, вирішує низку існуючих проблем проектування у різний спосіб, так що не слід ним нехтувати. Будь ласка, утримуйте ці дві альтернативи в голові під час проектування вашої об'єктно-орієнтованої моделі.

12. ІНКАПСУЛЯЦІЯ.

Поняття інкапсуляції в об'єктно-орієнтованому програмуванні полягає у прихованні всіх деталей реалізації (як режим роботи, внутрішні методи тощо) від зовнішнього світу. Переваги від інкапсуляції у зручності супроводу та легкості змін. Внутрішня реалізація класу ховається, робота з даними класу відбувається виключно через публічні методи класу (реальна проблема, якщо ви розробляєте бібліотеку чи фреймфорки (структури), використану багатьма людьми). Інкапсуляція в Java досягається за допомогою правил видимості та доступності. У Java вважається, що найкраще ніколи не виставляти поля безпосередньо, тільки за допомогою гетерів (getters) і сетерів (setters) (якщо поля не позначені як остаточні (final)). Наприклад:
package com.javacodegeeks.advanced.design;

public class Encapsulation {
    private final String email;
    private String address;

    public Encapsulation( final String email ) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getEmail() {
        return email;
    }
}
Цей приклад нагадує те, що називається JavaBeans у мові Java: стандартні класи Java, написані відповідно до набору угод, одна з яких дає доступ до полів лише за допомогою гетер та сеттер методів. Як ми вже підкреслювали у розділі спадкування, будь ласка, завжди дотримуйтесь мінімального контракту публічності у класі, використовуючи принципи інкапсуляції. Все, що не повинно бути публічним – має стати приватним (або protected/package private, залежить від проблеми, яку ви вирішуєте). У довгостроковій перспективі це окупиться, даючи вам свободу в проектуванні без внесення критичних змін (або, принаймні, мінімізують їх). 

13. КІНЦЕВІ КЛАСИ І МЕТОДИ

У Java є спосіб запобігти можливості класу стати підкласом від іншого класу: інший клас має бути оголошений як остаточний (final). 
package com.javacodegeeks.advanced.design;

public final class FinalClass {
}
Це ключове слово final в оголошення методу запобігає можливість перевизначення методу в підкласах. 
package com.javacodegeeks.advanced.design;

public class FinalMethod {
    public final void performAction() {
    }
}
Немає загальних правил, щоб вирішити повинен клас чи методи бути остаточними чи ні. Остаточні класи і методи обмежують розширюваність і дуже складно думати наперед, чи повинен клас бути успадкованим, або повинен або не повинен бути перевизначений в майбутньому. Це особливо важливо для розробників бібліотеки, оскільки рішення проектування подібні до цього могли б суттєво обмежити застосовність бібліотеки. Стандартна бібліотека Java має кілька прикладів остаточних класів, причому найбільш відомий - це клас String. На ранній стадії було прийнято це рішення, щоб запобігти будь-яким спробам розробників з'явитися з власним, «найкращим» рішенням реалізації string. 

14. ЩО ДАЛІ

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

15. СКАЧАТИ ПОХІДНИЙ КІД

Завантажити вихідний ви можете тут - advanced-java-part-3 Джерело: How to design Classes an
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ