Думаю, что тот, кто ввел понятие интерфейса, возможно и не подозревал, какое фантастическое по своим возможностям сотворил явление. Хотя это только мои догадки. В любом случае понятие интерфейса раздвинуло возможности ООП весьма сильно.
Так что же такое интерфейс ? По сути — это описание голой функциональности без каких либо привязок к особенностям класса. Если выражаться немного образно, то классы получили возможность иметь профессии — отправитель почты, управляющий транзакциями, распределитель страниц, контроллер и т.д. Я сейчас пытаюсь обрушить на вас грандиозность этой идеи и понимаю, что пока не получается. Просто поверьте на слово — это здорово. С помощью интерфейсов отношения между объектами становятся более гибкими, что позволяет строить архитектуру приложений из еще более независимых блоков.
Если опять вернуться к аналогии профессии — по сути вас не волнует пол, цвет глаз, возраст и рост человека, который работает водителем, электриком или программистом. Вам важно, что он умеет делать эту работу и умеет делать ее хорошо (в какой-то степени). Что еще важно отметить — как человек может обладать несколькими профессиями, так и класс может реализовывать несколько интерфейсов. Если вернуться к теме наследования, то как известно класс может наследоваться ТОЛЬКО ОТ ОДНОГО класса. А вот интерфейсов у него может быть достаточно много.
Перейдем от слов к делу — вернемся к нашему (широко известному в узких кругах) классу Robot 🙂
В прошлый раз мы научили его двигаться и запоминать свой маршрут для отображения на форме. При его создании мы немного забежали вперед — я ввел класс, о котором мы еще некоторое время не будем говорить — ArrayList. Пока мы не будем его обсуждать — просто еще раз отмечу, что это класс позволяет вам работать с динамическими списками объектов — добавлять, удалять, просматривать, перебирать. Расширим возможности нашего робота — наделим его способностью сообщать кому-нибудь о том, что он начал двигаться вперед и остановился. Возможно, что не такая уж и бесполезная вещь — например при наблюдении за марсоходом.
Заострим свое внимание на словах «сообщать кому-нибудь». Это очень тонкий момент — роботу ведь действительно неважно, кому сообщать. Вот она, точка применения интерфейса — нам неважно кто будет слушать — нам важно, чтобы этот кто-то или что-то умело слушать наше сообщение, соблюдало определенный контракт. Пришло время посмотреть, как определяется интерфейс.
1 2 3 4 5 6 7 8 9 10 11 12 |
package edu.javacourse.robot; /** * Интерфейс слушателя событий от робота */ public interface RobotListener { // Метод будет вызываться в момент начала движения public void startMove(double x, double y); // Метод будет вызываться в момент окончания движения public void endMove(double x, double y); } |
Как видите, описание интерфейса достаточно несложный процесс — гораздо сложнее понять, когда он действительно нужен. Рассмотрим его несколько подробнее.
Во-первых, для описания интерфейса надо использовать слово interface. Во-вторых, методы не содержат тела — совсем. Это просто запрещено правилами. Создается только описание — доступность, возвращаемый тип и входные параметры. После этого ставится точка с запятой.
У вас может возникнуть вопрос — а зачем мы передаем координаты x и y в методы интерфейса ? Вполне резонный вопрос, но и вполне резонный ответ — робот же должен сообщить где он стартовал и где остановился.
Настало время модифицировать код робота для того, чтобы он, во-первых, мог зарегистрировать «слушателя», а во-вторых он должен с ним уметь работать. Смотрим код.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
package edu.javacourse.robot; import java.util.ArrayList; public class Robot { private double x = 0; private double y = 0; protected double course = 0; private ArrayList<RobotLine> lines = new ArrayList<RobotLine>(); // Ссылка на слушателя событий от робота // Обратите внимание, что это ссылка на ИНТЕРФЕЙС private RobotListener listener; public Robot(double x, double y) { this.x = x; this.y = y; } // Метод для установки реального слушателя. public void setListener(RobotListener listener) { this.listener = listener; } public void forward(int distance) { // Вызываем слушателя (если он установлен) в начале if(listener !=null) { listener.startMove(x, y); } // Запоминаем координаты робота перед перемещением final double xOld = x; final double yOld = y; // Меняем координаты x += distance * Math.cos(course / 180 * Math.PI); y += distance * Math.sin(course / 180 * Math.PI); // Вызываем слушателя (если он установлен) после остановки if(listener !=null) { listener.endMove(x, y); } // Запоминаем координаты пройденного пути в списке // Класс List позволяет добавить объект и хранить его lines.add(new RobotLine(xOld, yOld, x, y)); } public double getX() { return x; } public double getY() { return y; } public double getCourse() { return course; } public void setCourse(double course) { this.course = course; } public ArrayList<RobotLine> getLines() { return lines; } } |
В нашем коде есть три момента, на которые надо обратить внимание. Первое — это объявление ссылки на слушателя.
private RobotListener listener;
Да-да, это то самое решение для отношений между объектами, которое мы обсуждали в разделе Отношения между классами. И опять никакого чуда не произошло — мы должны сказать роботу, кому он должен посылать сообщения. Второе — для установки слушателя нам потребуется метод
1 2 3 |
public void setListener(RobotListener listener) { this.listener = listener; } |
И наконец третье — в методе forward робот вызывает слушателя. Обратите внимание — перед вызовом мы делаем проверку на NULL — если слушатель не установлен, то мы можем получить неприятную ошибку NullPointer — указатель пустой. Такие ошибки делают и достаточно опытные программисты — так что будьте внимательны. Очень коварная ошибка. Хотя достаточно простая при обнаружении.
Наш робот готов посылать события и наша задача теперь создать этого самого слушателя. И теперь ВНИМАНИЕ — вы не можете создавать объект типа интерфейс. Очень похоже на абстрактный класс, но если в абстрактном классе хоть какой-то код может присутствовать, то в интерфейсе его вообще нет. Если опять вернуться к аналогии профессии, то наличие врача в клинике означает присутствие человека, который обладает профессией врача. С интерфейсами дело обстоит точно также — мы должны создать класс, который реализует (имплементирует — implements) нужный интерфейс. И опять же по аналогии с профессией — класс может реализовать более одного интерфейса. Сделаем простого слушателя:
1 2 3 4 5 6 |
package edu.javacourse.robot; // Наш класс реализует интерфейс robotListener public class SimpleRobotListener implements RobotListener { } |
Как видите, форма записи достаточно несложная — если вы хотите сказать, что класс реализует интерфейс вы пишите слово implements и после него указываете нужный интерфейс. Если надо реализовать несколько интерфейсов, то они пишутся через запятую — например так
1 |
public MyClass implements Interface1, Interface2, Interface3 |
Если вы создаете класс, который реализует интерфейс, вы обязаны иметь в этом классе методы с точно такими же описаниями, что и в интерфейсе. Т.е. сейчас наш класс при компиляции будет выдавать ошибку. Сделаем простую реализацию.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package edu.javacourse.robot; // Наш класс реализует интерфейс robotListener public class SimpleRobotListener implements RobotListener { @Override public void startMove(double x, double y) { System.out.println("Робот начал движение, координаты:" + x + "," + y); } @Override public void endMove(double x, double y) { System.out.println("Робот закончил движение, координаты:" + x + "," + y); } } |
Надеюсь, вы помните, что значит аннотация @Override — мы это уже обсуждали. Чтобы не бегать по ссылкам — это специальное обозначение того, что метод переопределен.
Слушатель готов — осталось только подключить его к нашему роботу и запустить программу. Подключение делается через вызов метода setListener. Можем это сделать в классе RobotManager.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package edu.javacourse.robot; import edu.javacourse.robot.ui.RobotFrame; import javax.swing.JFrame; public class RobotManager { public static void main(String[] args) { // Количество сторон многоугольника final int COUNT = 4; // Длина стороны final int SIDE = 100; Robot robot = new Robot(200, 50); // Установка слушателя для робота SimpleRobotListener srl = new SimpleRobotListener(); robot.setListener(srl); // Создаем замкнутую фигуру с количеством углов COUNT for (int i = 0; i < COUNT; i++) { robot.forward(SIDE); robot.setCourse(robot.getCourse() + 360 / COUNT); } // Создаем форму для отрисовки пути нашего робота RobotFrame rf = new RobotFrame(robot); rf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); rf.setVisible(true); } } |
В коде мы создаем экземпляр объекта SimpleRobotListener, который реализует интерфейс RobotListener и класс Robot без проблем позволяет установить себе слушателя. Отметим важную мысль — классу Robot совершенно неважно, какой класс реализует нужный интерфейс.
Если быть еще более точным, то при создании SimpleRobotListener можно (даже нужно) писать так:
1 |
RobotListener srl = new SimpleRobotListener(); |
Можно вспомнить про полиморфизм — класс умеет быть слушателем робота (профессия у него такая). И мы работаем с экземпляром класса SimpleRobotListener как с профессией RobotListener. Такое абстрагирование весьма удобно при проектировании — вы это увидите. Вы не привязываетесь к конкретному классы — вы привязываетесь исключительно к «профессии».
Теперь при запуске нашей программы в консоль вывода будет виден текст, который должен выводить наш слушатель.
Исходный код программы можно скачать здесь — Robot5
Свойства и константы
Т.к. интерфейс является исключительно описанием «что делать», но никогда не содержит «как делать» (как вы уже видели, методы не содержат реализацию), то интерфейс не может включать свойства — их просто негде вызывать. Из этого правила есть одно исключение — интерфейс может иметь константы. Что-то вроде этого
1 2 3 4 |
public interface SimpleInterface { public static final String NAME = "simple name"; } |
К полю NAME можно обращаться как к константе. Думаю, что здесь все достаточно очевидно и понятно — в интерфейсе можно описать константы. Что бывает востребовано.
Двигаем квадрат
Рассмотрим пример, который позволит нам создать интерактивное графическое приложение, в котором мы будем использовать интерфейсы. Я очень тепло отношусь к примерам, которые позволяют наглядно посмотреть работу программы. И графические приложения являются крайне благодарным материалом. Итак, наша задача — создать приложение, которое в помощью кнопок будет передвигать по экрану квадрат.
На форме будет две кнопки — UP и DOWN, которые позволят двигать квадрат соответственно вверх и вниз. Само приложение не сложное — здесь важно увидеть применение интерфейсов.
Кнопки на самом деле очень похожи по своей идее на нашего робота — при нажатии на них они способны рассылать события тем объектам, которые у них зарегистрированы как слушатели. Причем кнопкам позволяется иметь много слушателей одновременно — целый список.
Приведем код формы, которая содержит три компонента — две кнопки для управления и панель, которая умеет рисовать квадрат с определенными координатами и, что важно отметить сразу, умеет «слушать» события от кнопок. Умение слушать достигается очень просто — наша панель реализует интерфейс ActionListener (этот интерфейс описан в библиотеке Swing). Причем компонент умеет слушать события от обеих кнопок — он зарегистрирован в качестве слушателя у обеих. Итак, смотрим код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package edu.javacourse.move; import java.awt.BorderLayout; import javax.swing.JButton; import javax.swing.JFrame; public class MoveSquareFrame extends JFrame { public static final String UP = "UP"; public static final String DOWN = "DOWN"; public MoveSquareFrame() { SquareComponent sc = new SquareComponent(); // Кладем компонент для рисования квадрата add(sc); // Создаем кнопку для посылки команды движения вверх JButton btnUp = new JButton(UP); // Устанавливаем ей идентификатор, по которому сможем узнать эту кнопку btnUp.setActionCommand(UP); // Устанавливаем ей слушатель - компонент для рисования квадрата btnUp.addActionListener(sc); // Кладем кнопку на самый верх формы - на север add(btnUp, BorderLayout.NORTH); // Создаем кнопку для посылки команды движения вниз JButton btnDown = new JButton(DOWN); // Устанавливаем ей идентификатор, по которому сможем узнать эту кнопку btnDown.setActionCommand(DOWN); // Устанавливаем ей слушатель - компонент для рисования квадрата btnDown.addActionListener(sc); // Кладем кнопку на самый низ формы - на юг add(btnDown, BorderLayout.SOUTH); // Устанавливаем координаты setBounds(100, 100, 400, 400); } } |
Если просто аккуратно прочитать код, то видно, что сначала мы создаем панель, а потом создаем кнопки. При создании кнопки мы даем ее заголовок (прямо в конструкторе), потом устанавливаем ей название команды (для того, чтобы панель могла различать, кто ее позвал — кнопка UP или DOWN). Вызывая метод кнопки addActionListener мы регистрируем нашу панель в качестве слушателя. И в самом конце устанавливаем нашу кнопку либо вверх, либо вниз. Мы уже касались вопроса о layout в разделе Полиморфизм — так вот по умолчанию форма использует BorderLayout — здесь компоненты распределяются по сторонам света — север, юг, запад, восток и в центре. Если не указывать направление, то компонент располагается в центре — дальше догадаетесь.
Теперь посмотрим код нашей панели — он не должен показаться вам очень сложным — метод прорисовки мы уже использовали неоднократно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package edu.javacourse.move; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JComponent; // Наш класс умеет получать события от кнопки т.к. реализует интерфейс ActionListener public class SquareComponent extends JComponent implements ActionListener { // Определяем константу для размера квадрата private static final int SQUARE_SIZE = 30; // Определяем константу для шага private static final int STEP = 10; // Определяем поля для хранения текущих координат квадрата private int x = 0; private int y = 0; @Override public void actionPerformed(ActionEvent e) { // Входной параметр содержит ссылку на того, кто послал сообщение. // Получает объект с помощью вызова getSource() // С помощью слова instanceof мы можем проверить, что объект принадлежит // классу JButton (или его потомку) if (e.getSource() instanceof JButton) { // Приводим объект к типу JButton JButton btn = (JButton) e.getSource(); // Сравниваем команду со строкой UP if (MoveSquareFrame.UP.equals(btn.getActionCommand())) { // Вверх двигаемся уменьшением координаты Y y -= STEP; } // Сравниваем команду со строкой DOWN if (MoveSquareFrame.DOWN.equals(btn.getActionCommand())) { // Вниз двигаемся увеличением координаты Y y += STEP; } // Перерисовываем компонент для обновления экрана repaint(); } } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.drawRect(x, y, SQUARE_SIZE, SQUARE_SIZE); } } |
Новинкой для нас будет метод actionPerformed. Это метод, который описан в интерфейсе ActionListener. И кнопке совершенно безразлично, какой именно класс ее слушает — это может быть другой компонент, класс для записи файлов, для отсылки почты и еще море всяких других классов. Важен просто контракт — «я умею слушать кнопку». Это позволяет в разы повысить гибкость при проектировании. Возвращаясь к методу — он принимает в качестве параметра специальный класс/объект, который содержит интересную информацию об источнике события — в данном случае о кнопке. В нашем случае нам очень интересен параметр, который мы устанавливали — getActionCommand/setActionCommand. Именно он нам скажет какая кнопка нажата. Еще раз обратите внимание на приятную возможность слушать события от обеих кнопок.
Ну и наконец код для запуска нашей формы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package edu.javacourse.move; import javax.swing.JFrame; public class MoveSquare { public static void main(String[] args) { // Создаем графическое окно MoveSquareFrame msf = new MoveSquareFrame(); // Задаем правидо, по которому приложение завершиться при // закрытии этой формы msf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Делаем окно видимым msf.setVisible(true); } } |
Предлагаю вам расширить пример — добавить туда кнопки LEFT и RIGHT и двигать квадрат в стороны. Также вы можете сделать проверку, чтобы квадрат «не убежал» за пределы экрана.
Исходный код примера находится здесь — MoveSquare
Интерфейсы играют очень важную роль в большом количестве технологий, шаблонов проектирования. Мы рассмотрели только основные идеи и формы записи. Дополнительная информация будет появляться по мере продвижения по курсу.
И теперь нас ждет следующая статья: Расширенное описание классов.