Перейти до вмісту

Освоюємо Java/Інтерфейси

Матеріал з Вікіпідручника

Інтерфейси (interfaces) в мові програмування Java – це посилальний тип даних (reference data type), що подібний до класу, що переважно містить лише константи, сигнатуру методу та вкладені типи. До Java 8 інтерфейси не могли містити реалізацію методів. У Java 8 з'явилася можливість надавати реалізацію методу по замовчуванню з використанням службового слова default, оголошувати статичні методи. А з Java 9 в інтерфейсах стало можливим використовувати приватні методи.

Визначення та реалізація інтерфейсу[ред.]

Інтерфейс визначається наступним чином:

public interface MyInterface <T> {
    public String MYCONST="It is constant"; // константа
    public int method(T o); // нереалізований метод
}

<T> - це частина механізму Generics(Узагальнень), що дозволяє ввести додаткову перевірку типу при роботі, в даному випадку вказується, що інтерфейс буде працювати з будь-яким типом. При створенні об'єктної змінної можна уточнити тип (наприклад, MyInterface<String> myintvar). Generics були введено у випуск Java 5. Таким чином усуваються деякі можливі помилки, пов'язані із типами даних і зокрема з приведенням типів(детальніші у відповідному розділі). Інтерфейси можна створювати і старим способом, без використання узагальнень:

public interface MyInterface {
    public String MYCONST="It is constant" // константа
    public int method1(Object o); // нереалізований метод1
    public int method2(String str); // нереалізований метод2
}

Поля у інтерфейсі є константами, по суті в звичайному класі вони б оголошувались би як

public static final тип НАЗВА_КОНСТАНТИ;

В інтерфейсі дані модифікатори можна не вказувати. Згідно офіційних рекомендацій Oracle не рекомендується писати зайві модифікатори до членів інтерфейсу і згідно тих же рекомендацій константи в Java пишуться з великої літери.

Методи інтерфейсу мають єдиний тип доступу - public, тому в інтерфейсі можна описувати методи без модифікатора доступу. Проте при їх реалізації модифікатор public необхідно вказати.

Деякі автори книг не рекомендують використовувати інтерфейси для збереження констант.

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

public class Car extends SimpleCar implements Moveable, Comparable{
    ........
}

Не можна створити об'єкт інтерфейсу:

х = new ComparaЬle( ... ); //так не можна!

Проте ми можемо використати змінну з типом інтерфейсу присвоївши їй об'єкт класу, що реалізовує даний інтерфейс:

Comparable х=new Car();

Оператор instanceof можна використати для перевірки того, чи клас реалізовує певний інтерфейс:

 if(anObject instanceof Comparable){...}

Можна оголошувати нові інтерфейси, що розширюють існуючі, з використанням ключового слова extends.

Переваги інтерфейсів[ред.]

Перевагою інтерфейсів над абстрактними класами є те, що можна реалізувати кілька інтерфейсів в одному класі. Розглядаючи абстрактні класи, було сказано, що вони зручні при груповій роботі, коли частину методів робить один програміст, а іншу частину інший. Крім того можна ієрархію класів де різні реалізації одного абстрактного класу можуть виконувати схожі, проте в дечому відмінні задачі. В інтерфейсах дана ідея ще більше розширюється. Так, якщо один програміст розробляє клас(назвемо умовно FirstClass), який використовує інший клас(UseableClass), що повинен розробити інший програміст, то першому не потрібно чекати, коли другий програміст зробить необхідний йому UseableClass. Вони можуть узгодити, що клас повинен мати певні методи, описати інтерфейс для даного класу (наприклад, interface Useable) і далі працювати кожен над своїм класом паралельно. Один пише клас UseableClass реалізовуючи(implements) логіку методів інтерфейсу Useable. Iнший пише FirstClass так нібито клас UseableClass вже реалізовано. Єдине, що їм потрібне – це узгоджений між ними інтерфейс, і, зокрема, сигнатура майбутніх методів. Як і у випадку з абстрактними класами в пригоді стає поліморфізм: змінна інтерфейсного типу може певним чином замінити ще не реалізований клас.

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

В Java існує ряд широковживаних інтерфейсів. Щоб об’єкти можна було порівняти між собою, необхідно реалізувати інтерфейс Comparable і його метод compareTo(). По суті призначенням інтерфейсів є змусити програміста, що розробляє певний клас, реалізувати методи, щоб об’єкт правильно функціонував.

В Java SE 8 додана можливість додавати в інтерфейси статичні методи, та методи з реалізацією по замовчуванню з використанням ключового слова default перед ними.

Реалізовуємо електронний словник[ред.]

UML діаграма класів

Розглянемо інтерфейси більш детальніше на прикладі. Для цього розглянемо чорнову реалізацію наступної задачі: двом програмістам необхідно реалізувати мініатюрний словник. Один реалізовуватиме клас WordTranslation, який працюватиме з користувачем, інший реалізовуватиме клас Dictionary, що безпосередньо працюватиме із словами і реалізовуватиме інтерфейс Dict. Після розробки обох класів буде створено третій клас mainClass, який зв’яже реалізацію інтерфейсу із класом, що її використовуватиме. Узагальнена UML діаграма, того, що потрібно зробити зображено на рисунку.

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

Інтерфейс Dict[ред.]

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

package ua.osvjava;

public interface Dict {
    /**
     *Пошук перекладу
     * @param word слово для якого необхідно знайти переклад
     * @return  переклад слова, або ж рядок "переклад не знайдено"
     */
    String findTranslation(String word);
    
    /**
     *Додавання слова в словник
     * @param word  слово
     * @param translalion  переклад слова
     * @return  true, якщо слово додане успішно
     */
    boolean addWord(String word, String translalion);
    
    /**
     *Видаляє із словника слово разом із його перекладом
     * @param word  слово для видалення
     * @return  true, якщо слово успішно видалене
     */
    boolean delWord(String word);
}

Реалізація інтерфейсу Dict[ред.]

Інтерфейс реалізовуватиме клас Dictionary. Для спрощення розуміння прикладу, реалізовано лише метод findTranslation. Усі інші методи нічого не роблять:

package ua.osvjava;

public class Dictionary implements Dict {
    private String[] ukWord={"слово", "читати", "сонце", "робити", "йти"};
    private String[] enWord={"word", "read", "sun", "do", "go"};  
    public Dictionary() {
        
    }

    @Override
    public String findTranslation(String str) {
        for (int i = 0; i < ukWord.length; i++) {
            if (ukWord[i].compareToIgnoreCase(str)==0) return enWord[i];
            if (enWord[i].compareToIgnoreCase(str)==0) return ukWord[i];
        }

        return ">>ПЕРЕКЛАД НЕ ЗНАЙДЕНО!!!<<";
    }

    @Override
    public boolean addWord(String string, String string2) {
        // ....
        // деякі дії по додаванню слова
        return false;
    }

    @Override
    public boolean delWord(String string) {
        // деякі дії по видаленню слова
        //....
        return false;
    }
}

Як бачимо в класі присутні два масиви, які представляють собою зв’язку слово-переклад. Метод findTranslation при зверненні приймає слово і послідовно перебирає два масиви із словами в пошуку співпадіння. Якщо слово знайдено в масиві з українськими словами, то повертається англійський відповідник і навпаки. Якщо слово не знайдено, то повертається рядок тексту ">>ПЕРЕКЛАД НЕ ЗНАЙДЕНО!!!<<";

Насправді в Java існує колекція класів, які призначені для зберігання об’єктів різної природи у вигляді масивів, в тому числі і у вигляді пар ключ-значення (пакет java.util). В цих класах вже реалізовано ряд корисних методів для роботи з цими наборами об’єктів. Зокрема, переважно вони містить ряд методів для пошуку по ключовому слову, видалення об'єкту, вставки об'єктів, визначення розміру. Котрийсь із цих класів можна було б використати для нашої задачі, проте нашим основним завданням є розібратися з масивами.

Ви можете, згодом, спробувати самостійно реалізувати своєрідний електронний словник з використанням класу Dictionary, або іншого з колекції класів, або ж самому реалізувати повноцінний клас для збереження слів та роботи з ними.

Клас WordTranslation[ред.]

Що ж повертаємось до наших баранів, тобто основної задачі. Реалізуємо клас WordTranslation, який буде використовувати клас Dictionary і реалізує спілкування з користувачем через текстову консоль.

Таким чином маємо наступну реалізацію:

package ua.osvjava;

import java.util.Scanner;

public class WordTranslator {
    private Dict dict;

    public WordTranslator(Dict dict) {
        dict = dict;

        Scanner in = new Scanner(System.in);
        String word;
        do {
            System.out.print("Введіть слово для перекладу (ext - вихід): ");
            word = in.next();
            System.out.println(" - "+dict.findTranslation(word));
        } while (!word.equalsIgnoreCase("ext"));
    }

    public String getTranslation(String word) {
        return null;
    }
}

Як бачимо клас доволі простий, через конструктор отримується посилання на об’єкт, клас якого реалізовує інтерфейс Dict. Для збереження даного посилання використовується інтерфейсна змінна типу Dict. Після чого запускається цикл опитування користувача через текстову консоль. Користувач вводить слово, об’єкт класу WordTranslator викликає відповідний метод нашого словника для пошуку слова. Якщо б нам не була необхідна певна універсальність, то ми могли б це записати наступними інструкціями:

String word=do
Dict dict=new Dictionary();
dict.findTranslation(word)

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

Клас WordTranslator повинен одержувати об’єкт через змінну з типом інтерфейсу Dict, яка вже при виконанні програми повинна буде ініціалізовуватись посиланням на конкретний екземпляр класу, що реалізовує Dict. Передача посилання на конкретну реалізацію інтерфейсу здійснюється через конструктор класу. Таким чином він матиме змогу працювати з будь-якою реалізацією даного інтерфейсу. Щоправда існує серйозне обмеження, якщо використовується змінна інтерфейсного типу, то можливе застосування лише методів оголошених в інтерфейсі (згадаймо про поліморфізм об’єктів – батько може заміняти в певних випадках дочірній клас, проте можливе застосування лише методів, які оголошені в даному класі. При цьому, якщо метод не статичний, то відбувається заміщення методів батьківського класу, методами дочірнього класу). Тобто якщо реалізації інтерфейсу Dict міститимуть певні додаткові методи, вони не будуть доступні через інтерфейсну змінну.

Клас MainClass[ред.]

Тепер же у нас є усі доступні класи, щоб їх застосовувати. Лишилось написати клас із методом main, який пов’яже усе докупи і запустить наше прикладення.

package ua.osvjava;

public class MainClass {
    WordTranslator transl;
    Dictionary dictEnUk;

    MainClass() {
        //створюємо словник
        dictEnUk = new Dictionary();
        //запускаємо перекладач слів з текстовим інтерфейсом
        transl = new WordTranslator(dictEnUk);

    }

    public static void main(String[] args) {
        new mainClass();
    }
}

Як бачимо клас простий. Створюється об’єкт типу Dictionary, а далі об’єкту типу WordTranslator передається об’єктне посилання на створений словник.

Результат роботи:

Введіть слово для перекладу (ext - вихід): read
 - читати
Введіть слово для перекладу (ext - вихід): робити
 - do
Введіть слово для перекладу (ext - вихід): translator
 - >>ПЕРЕКЛАД НЕ ЗНАЙДЕНО!!!<<
Введіть слово для перекладу (ext - вихід): ext
 - >>ПЕРЕКЛАД НЕ ЗНАЙДЕНО!!!<<

Остаточну структуру класів та їхню взаємодію зображено на UML діаграмі.

UML діаграма класів електронного словника

Як видно з діаграми, класи WordTranslator та Dictionary між собою не пов’язані напряму, а отримують зв’язок через інтерфейс Dict. Можна скільки завгодно реалізувати версій класів на основі Dict. При цьому клас WordTranslator це не зачепить. Все це можна було б реалізувати через абстрактний клас, проте при використанні інтерфейсів клас Dictionary може запросто бути включеним в певну ієрархію класів, тобто розширювати (extends) інший клас і одночасно реалізовувати наш інтерфейс.

Інтерфейс Comparable[ред.]

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

Інтерфейс Comparable в пакеті java.lang визначений наступним чином:

package java.lang;
import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

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

Порівняти чисельні змінні взагалі немає проблем. Так що такого в цьому інтерфейсі? Суть в тому, що інколи об’єкти необхідно порівняти по певним полям. Наприклад, відсортувати об’єкт Person чи Manager по імені або ж по прізвищу особи/менеджера, або ж по зарплаті і т.п. Тож, щоб це було можливо необхідно подбати про метод compareTo у класі об’єкти якого будуть порівнюватись. По замовчуванню метод compareTo порівнює об’єкти по хеш-коду об’єкта, який залежить від об’єктного посилання, а не від полів об’єкта.

Навіщо саме compareTo і навіщо реалізовувати інтерфейс Comparable? Чому не реалізувати метод з іменем comparePerson. Це звичайно ж працюватиме, проте в результаті втрачаємо ряд функціональних можливостей реалізованих у великій кількості класів Java, які можуть полегшити роботу над вашим власним прикладенням. Тобто існують уже наперед розроблені своєрідні "універсальні" класи, що орієнтовані на роботу з об’єктами типу Comparable. Правильніше казати, що їхні методи створені таким чином, щоб викликати метод compareTo. І в ряді випадків вони можуть бути доволі корисні. Наприклад, існує клас java.util.Arrays, в якому реалізовано метод sort, що дозволяє відсортувати масив будь-якого об’єктного типу. Тобто нам непотрібно реалізовувати вже власне сортування, достатньо на його вхід подати наш об'єктний масив.

Наприклад так:

        Person[] persons= new Person[3];

        persons[0]=new Person("Назаренко","Іван"); 
        persons[1]=new Person("Козаченко","Петро"); 
        persons[2]=new Person("Перебийніс","Василь"); 

        Arrays.sort (persons);
        
        for(Person p:persons){
            System.out.println (p);
        }

Щоб мати змогу зробити щось подібне (відсортувати об’єкти по прізвищу і далі вивести відсортований список) необхідно, щоб клас Person реалізував інтерфейс Comparable і метод сompareTo. Метод сompareTo повертає від’ємне значення, вказаний об’єкт менший, 0 – якщо об'єкти рівні, і додатну величину, якщо об'єкт більший. Результат повернення буде врахований методом sort класу Arrays, при сортуванні об’єктів згідно реалізованого в compareTo правила сортування. Переважно сортується по зростанню чи спаданню, в залежності від того, як ви реалізуєте порівняння об’єктів.

Тож маємо наступний клас Person.

package ua.osvjava.comp;

public class Person implements Comparable<Person>{

    private String firstName;
    private String surName;

    public Person(String sn, String fn) {
        this.surName = sn;
        this.firstName = fn;
    }

    public void setFirstName(String fn) {
        this.firstName = fn;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setSurName(String sn) {
        this.surName = sn;
    }

    public String getSurName() {
        return surName;
    }

    public String toString() {
        return getSurName() + " " + getFirstName();
    }

    @Override
    public int compareTo(Person o) {
        return getSurName().compareToIgnoreCase(o.getSurName());
    }
}

Клас простий, має два поля і методи доступу до даних полів. В ньому реалізовано метод compareTo, шляхом порівняння прізвищ у двох екземплярах класу Person. Один екземпляр - це поточний об'єкт, інший об'єкт Person передається по об'єктному посиланні. Таким чином поточний об'єкт порівнюється з іншими і по результату повернення, судиться чи даний об'єкт більший, менший або ж рівний іншому. В нашому випадку, в майбутньому, результат порівняння, буде врахований у класі Arrays у його алгоритмі сортування (або ж може бути використано у будь-якому іншому класі, який викликатиме метод compareTo для порівняння екземплярів класу Person. Це може бути виклик на зразок такого person1.compareTo(person2)).

В нашій реалізації методу compareTo, для порівняння прізвищ використано метод compareToIgnoreCase - це метод класу String, що порівнює два рядки символів (в даному випадку порівнюються прізвища).

Також заміщено метод toString, що дозволить нам виводити прізвище та ім’я при використанні інструкції:

System.out.println (p);

де p – об’єктна змінна, що посилається на екземпляр класу Person.


Ось клас, в якому використовується екземпляри класу Person і здійснюється вивід осіб в відсортованому порядку:

package ua.osvjava.comp;

import java.util.Arrays;


public class usingPerson {
    usingPerson() {
        Person[] persons= new Person[3];

        persons[2]=new Person("Перебийніс","Василь"); 
        persons[0]=new Person("Козаченко","Петро"); 
        persons[1]=new Person("Назаренко","Іван");

        Arrays.sort (persons);
        
        for(Person p:persons){
            System.out.println (p);
        }
    
    }
    public static void main(String[] args) {
        new usingPerson();
    }
}

Результат:

Козаченко Петро
Назаренко Іван
Перебийніс Василь 

Як бачимо реалізувавши Comparable, ми змогли посортувати об’єкти масиву persons за прізвищем. При цьому нам не прийшлось реалізовувати повноцінний алгоритм сортування. Було використано готову його реалізацію у класі Arrays. Щоб прізвища сортувалися у зворотному порядку, всього лиш достатньо змінити знак результату, що повертається:

return -getSurName().compareToIgnoreCase(o.getSurName());

Інтерфейс Comparable застосовується в стандартних класах мови Javа практично на кожному кроці.

Існують також так звані tagged interfaces (інтерфейси маркери), тіло яких повністю пусте (Cloneable, Serializable). Наприклад, щоб можна було об’єкт клонувати (зробити копію стану об’єкту), необхідно щоб клас, на основі якого створено об’єкт, вказував, що він реалізує інтерфейс Cloneable. Даний інтерфейс без методів. Якщо в класі вказано, що він реалізовує даний метод, то стає доступним використання методу clone() класу Object. В класі Object реалізована перевірка чи клас для клонування є екземпляром Cloneable.

if (obj instanceof Cloneable)....

Таке використання інтерфейсів в даний час не рекомендується. Метод clone() класу Object, проводить копіювання об’єкту неефективно і його усе рівно потрібно замістити у класі, об’єкт якого копіюється, або що проводить копіювання певного об’єкту.

Вирішення конфліктів з методами по замовчуванню[ред.]

Може виникнути декілька конфліктів при використання методів по замовчуванню в інтерфейсах.

1. Новий клас розширює клас з реалізацією методу getName(), також клас реалізовує інтерфейс, який, в свою чергу, також надає власну реалізацію методу getName(), або ж зазначає, що потрібно реалізувати метод getName(). В такому випадку буде вибрана реалізація надана суперкласом, а не та, яку надає інтерфейс.

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

class Truck implements Car, Vehicle {
   public String getName(){
       return Car.super.getName(); //безпосередньо вказуєм, яку реалізацію методу використовувати
   }
}

Вкладені, внутрішні класи · Винятки