Базы данныхИнтернетКомпьютерыОперационные системыПрограммированиеСетиСвязьРазное
Поиск по сайту:
Подпишись на рассылку:

Назад в раздел

Java. Объектно-ориентированное программирование с интерфейсами

div.main {margin-left: 20pt; margin-right: 20pt}Java. Объектно-ориентированное программирование с интерфейсами
 

Неважно, как оно работает.
Важно, как оно выглядит!

Введение
 

"В узком смысле слова Java - это объектно-ориентированный язык, напоминающий C++, но более простой для освоения и использования. В более широком смысле Java - это целая технология программирования, изначально рассчитанная на интеграцию с Web-сервисом. ... Java-среда должна быть как можно более мобильной, в идеале полностью независимой от платформы."
Java как центр архипелага. Александр Таранов, Владимир Цишевский

Приведенная цитата типична для статей, посвященных Java. Все сказанное в ней - правда. Но не вся.

Java - это не только "С++ без указателей" и не только "С++ для интернета". Java - это объектно-ориентированный язык нового поколения.

Осознание сего факта потребовало от меня пересмотра стереотипов, сложившихся во время программирования на С++. Этот процесс я планирую отобразить в серии статей "ООП на Java".

В данной статье рассматривается одно из отличительных свойств Java как языка объектно-ориентированного программирования - концепция интерфейса. На мой взгляд, концепция интерфейса - одно из важнейших нововведений языка, до конца пока не осознанное. Интерфейсы ждут своих исследователей.

Предполагаемые очередные темы - множественное наследование, динамические типы, обобщение и классификация, отображение на РСУБД.

Статья в большей степени ориентирована на философию Java, чем на практические рекомендации по программированию. Предполагается, что читатель знаком с ООП на С++ или на Java.

 

Хороший обзор Java (особенно для знакомых с C++) можно найти в статье Java как центр архипелага. Александр Таранов, Владимир Цишевский infocity.kiev.ua, раздел Программирование - Java

Лучшая книга по Java Брюс Эккель. Философия Java. Оригинал можно скачать по адресу: http://www.mindview.net/.

Первый вопрос, возникший у меня при знакомстве с Java, - Зачем нужны интерфейсы? Казалось бы вполне достаточно обычных и абстрактных классов в стиле C++. Для маскировки отсутствия множественного наследования? Смешно.

После некоторого количества размышлений и особенно после неоднократного прочтения упомянутой книги Брюса Эккеля вопрос трансформировался: Зачем в Java нужны классы?

Привычные варианты ответа: класс вводит новый тип, класс обобщает .... 1. Декларация типа
 

Суть вещи тогда поймешь,
когда правильно назовешь ее.

Новый тип вводится спецификацией интерфейса.

В C++ класс неявно определяет интерфейс. И в силу этого одновременно объявляет тип. При этом единственный интерфейс связывается с единственной реализацией. Множественное наследование и абстрактные классы в C++ - это прежде всего попытка обойти жесткую детерминированность.

В Java подобного ограничения нет. Любой интерфейс (тип) может иметь много реализаций. Любой класс может реализовывать много интерфейсов.

В качестве примера попробуем объявить собственный тип "число". Для краткости ограничимся операциями сложения и умножения.

/* INumber.java ------------------------------------------------------ (8< * * Декларация типа INumber и фрагмент программы, использующий этот тип. * * -------------------------------------------------------------------- */ interface INumber { public void setValue(String s); public INumber add(INumber n); public INumber mul(INumber n); public String toString(); } class CalcNumber { void calculation(INumber n1, INumber n2, INumber n3) { INumber xx; xx = n2; // Если закомментировать предыдущую строку, то компилятор выдаст ошибку: // variable xx might not have been initialized xx.setValue("5.3"); System.out.println("xx="+xx.toString()); n1.setValue("21"); n2.setValue("37.6"); System.out.println("n1="+n1.toString()); System.out.println("n2="+n2.toString()); System.out.println("n3="+n3.toString()); System.out.println("(n1+n2)*n3=" + n1.add(n2).mul(n3).toString()); n1.setValue("21"); System.out.println("(n2+n1)*n3=" + n2.add(n1).mul(n3).toString()); n2.setValue("37.6"); System.out.println("n1*(n2+n3)=" + n1.mul(n2.add(n3)).toString()); n1.setValue("21"); n2.setValue("37.6"); System.out.println("n3*(n1+n2)=" + n3.mul(n1.add(n2)).toString()); } } // (8<

Из приведенного примера видно: Интерфейс позволяет объявить тип. В приведенном примере объявляются переменные и параметры типа INumber, описываются действия над ними. Компиляция выполняется без ошибок. Реализация типа передается через объект. Объекты n1, n2 и n3 передаются через параметры. Тем самым компилятор информируется, что объекты проинициализированы где-то за пределами данного модуля. Этого достаточно. Классы пока не нужны. Инициализировать объект в приведенном модуле мы не можем, т.к. для этого необходимо иметь реализацию.

Разрыв между декларацией типа данных и его реализацией не является чем-то новым. Например, в спецификации С оговорено, что реализация базовых типов зависит от платформы. А реализация плавающих чисел даже на одной платформе всегда зависела от наличия сопроцессора.

Добавим два варианта реализации.

/* DblNumber.java ---------------------------------------------------- (8< * * Реализация типа INumber через double. * * -------------------------------------------------------------------- */ class DblNumber implements INumber { double d; public DblNumber(double ip) { d = ip; } public void setValue(String s) { d = (new Double(s)).doubleValue(); } public INumber add(INumber n) { d += (new Double(n.toString())).doubleValue(); return this; } public INumber mul(INumber n) { d *= (new Double(n.toString())).doubleValue(); return this; } public String toString() { return (new Double(d)).toString(); } } // (8<

/* IntNumber.java ---------------------------------------------------- (8< * * Реализация типа INumber через int. * * -------------------------------------------------------------------- */ class IntNumber implements INumber { int i; public IntNumber(int v) { i = v; } public void setValue(String s) { String sw=s; int l = sw.indexOf('.'); if (l > 0) sw = sw.substring(0, l); i = (new Integer(sw)).intValue(); } public INumber add(INumber n) { String sw = n.toString(); int l = sw.indexOf('.'); if (l > 0) sw = sw.substring(0, l); i += (new Integer(sw)).intValue(); return this; } public INumber mul(INumber n) { String sw = n.toString(); int l = sw.indexOf('.'); if (l > 0) sw = sw.substring(0, l); i *= (new Integer(sw)).intValue(); return this; } public String toString() { return (new Integer(i)).toString(); } } // (8<

Проверим результат:

/* TestNumber.java --------------------------------------------------- (8< * * Тестирование типа INumber. * * -------------------------------------------------------------------- */ public class TestNumber { public static void main(String[] args) { INumber i1 = new IntNumber(22); INumber i2 = new DblNumber(11.2); INumber i3 = new DblNumber(3.4); CalcNumber cn = new CalcNumber(); cn.calculation(i1, i2, i3); } } // (8<

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

xx=5.3 n1=21 n2=37.6 n3=3.4 (n1+n2)*n3=174 (n2+n1)*n3=199.24 n1*(n2+n3)=861 n3*(n1+n2)=197.2

Обратите внимание: реализация передается через объект. Класс нужен для порождения объекта, несущего реализацию. Но не обязательно, как увидим позднее.

Интересно отметить, что результат операции над INumber зависит от последовательности использования переменных. Эффект возникает потому, что в спецификации типа мы опустили важные для чисел свойства: точность и диапазон допустимых значений. В результате они неявно берутся из базового типа, использованного при реализации. В данном случае достаточно добавить метод
setFormat(maxValue, minValue, decimal). 2. Реализация типа
 

- Нужно ли знать формулу аспирина, чтобы вылечить головную боль?
- Нет! Достаточно иметь деньги в кармане.

В предыдущем примере мы видели, что реализация передается через объект. Следовательно, в объекте упакована вся необходимая информация по реализации интерфейса. Если поведение определяется интерфейсом, а реализация упакована в объекте, то зачем нужен класс? - Классы нужны для наследования реализации и повторного использования кода. Если повторное использование не требуется, то и класс не нужен.

В следующем примере есть только один класс - для запуска приложения. Собственно логика приложения реализована без использования классов!

/* TestAnimal.java --------------------------------------------------- (8< * * Образец бесклассовой реализации * * -------------------------------------------------------------------- */ import java.util.ArrayList; interface Animal { void giveSignals(); void goHome(); String getTitle(); String getNick(); } interface Command { void exeCommand(Animal an); } interface Ranch { void add(Animal an); void visitAll(Command cmd); } public class TestAnimal { public static void main(String[] args) { Ranch myRanch = new Ranch() { private ArrayList ranchAnimals = new ArrayList(); public void add(Animal a) { ranchAnimals.add(a); } public void visitAll(Command cmd) { for(int i = 0; i < ranchAnimals.size(); i++) cmd.exeCommand((Animal)ranchAnimals.get(i)); } }; // end of new Ranch() // add animals myRanch.add(new Animal() //dog { public void giveSignals() { System.out.println("Гав-гав"); } public void goHome() { System.out.println("Бежит в будку"); } public String getTitle() { return new String("собака"); } public String getNick() { return new String("Блэк"); } }); // end of add new Animal dog myRanch.add(new Animal() // sheep { public void giveSignals() { System.out.println("Бе-е"); } public void goHome() { System.out.println("Идет в загон"); } public String getTitle() { return new String("овца"); } public String getNick() { return new String(""); } }); // end of add new Animal sheep myRanch.add(new Animal() // another sheep { public void giveSignals() { System.out.println("Бе-е"); } public void goHome() { System.out.println("Идет в загон"); } public String getTitle() { return new String("овца"); } public String getNick() { return new String(""); } }); // end of add new Animal another sheep // gives signals System.out.println("n<<<<<<< Все подали голос >>>>>>>>>n"); myRanch.visitAll(new Command() { public void exeCommand(Animal a) { System.out.print(a.getTitle()+" "+a.getNick() + " говорит: "); a.giveSignals(); } }); // go to Home System.out.println("n<<<<<<< Все домой! >>>>>>>>>n"); myRanch.visitAll(new Command() { public void exeCommand(Animal a) { System.out.print(a.getTitle()+" "+a.getNick() + " идет домой: "); a.goHome(); } }); } } // (8<

Использование класса Sheep позволило бы сократить текст программы. Никаких других преимуществ введение этого класса не дает. Для остальных объектов определение соответствующих классов не дает ничего.

Результат выполнения программы:

<<<<<<< Все подали голос >>>>>>>>> собака Блэк говорит: Гав-гав овца говорит: Бе-е овца говорит: Бе-е <<<<<<< Все домой! >>>>>>>>> собака Блэк идет домой: Бежит в будку овца идет домой: Идет в загон овца идет домой: Идет в загон

Кто-нибудь скажет, что в приведенном примере использованы анонимные классы и будет прав.

Но что такое анонимный класс? В спецификации Java сказано: декларация анонимного класса автоматически извлекается компилятором из выражения создания экземпляра класса. Т.е. авторы языка воспользовались принципом чайника и привели задачу создания "самоопределенного" объекта к уже решенной. Другими словами, обычно сначала декларируется класс, а затем порождается его экземпляр. С анонимным классом все наоборот - сначала описывается экземпляр, а потом под него подгоняется класс. Реинжиниринг называется. :)

Можно сказать, что анонимный класс нужен для того, чтобы узаконить существование созданного объекта.

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

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

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

Назвался груздем - полезай в кузов.

Наследование типа и полиморфизм обеспечиваются наследованием интерфейса и ничем иным.

Простой пример:

/* TestShips.java ---------------------------------------------------- (8< * * Наследование интерфейсов и полиморфизм * * -------------------------------------------------------------------- */ import java.util.ArrayList; interface Ship { void runTo(String s); } interface WarShip extends Ship { void bombard(); } interface Transport extends Ship { void loadTroops(int n); void landTroops(); } public class TestShips { public static void main(String[] args) { ArrayList ships = new ArrayList(); for(int i = 0; i < 3; i++) ships.add(new Transport() { private int troopers; public void runTo(String s) { System.out.println("Транспорт направляется в "+s+"."); } public void loadTroops(int n) { troopers = n; } public void landTroops() { System.out.println((new Integer(troopers)).toString()+" отрядов десантировано."); } } ); for(int i = 0; i < 2; i++) ships.add(new WarShip() { public void runTo(String s) { System.out.println("Корабль направляется в "+s+"."); } public void bombard() { System.out.println("Корабль бомбардирует цель."); } } ); for(int i = 0; i < 3; i++) ((Transport)ships.get(i)).loadTroops(i+5); for(int i = 0; i < ships.size(); i++) ((Ship)ships.get(i)).runTo("Вражий Порт"); for(int i = 0; i < 3; i++) ((Transport)ships.get(i)).landTroops(); for(int i = 3; i < ships.size(); i++) ((WarShip)ships.get(i)).bombard(); // Run-time error: java.lang.ClassCastException // ((Transport)ships.get(4)).landTroops(); // Run-time error: java.lang.ClassCastException // ((WarShip)ships.get(1)).bombard(); // Compile-time error: cannot resolve symbol // ((Ship)ships.get(1)).landTroops(); // ((Ship)ships.get(4)).bombard(); } } // (8<

Результат выполнения программы:

Транспорт направляется в Вражий Порт. Транспорт направляется в Вражий Порт. Транспорт направляется в Вражий Порт. Корабль направляется в Вражий Порт. Корабль направляется в Вражий Порт. 5 отрядов десантировано. 6 отрядов десантировано. 7 отрядов десантировано. Корабль бомбардирует цель. Корабль бомбардирует цель.

Концепция интерфейсов добавляет полиморфизму второе измерение: Иерархический полиморфизм в стиле C++, основанный на приведении к базовому типу классов и /или интерфейсов (см. TestShips); Полиморфизм экземпляров, основанный на разных реализациях одного и того же интерфейса (см. INumber).

Наследование имеет два аспекта: "быть похожим (внешне) на" - наследование типа, поведения; "быть устроенным как" - наследование реализации.

Наследование реализации не означает наследование типа! В практике это не встречается, потому что и в С++ и в Java невозможно наследование реализации без наследования интерфейса. В C++ интерфейс и класс неотделимы друг от друга. В Java интерфейс от класса отделить можно, но класс от интерфейса - нельзя.

В С++ и в Java совокупность общедоступных (public) методов неявно образует интерфейс данного класса. В силу этого наследование класса автоматически означает как наследование реализации, так и наследование интерфейса (типа). Очевидно, что наследование структуры данных и программного кода не определяет тип потомка. Например, абстрактные методы являются частью интерфейса и не являются частью реализации. Если бы можно было исключить их из наследования, то мы получили бы наследование реализации без сохранения типа.

Обратите внимание, что в DblNumber и IntNumber наследования реализации нет. Поэтому иерархия классов не используется. 4. Обобщение

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

Возможно повторное использование: исходного кода (атрибуты); исполняемого кода (методы).

Классы обеспечивают два измерения повторного использования: классификация - экземпляр (объект) использует реализацию класса; обобщение - классы наследуют реализацию родительских классов.

Таким образом, истинное предназначение класса - упаковка повторно используемого кода в соответствии с принципами объектно-ориентированной технологии.  

Copyright (c) 2001 Alexandre Moskovskikh. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".
Вкратце: Вы можете свободно читать, копировать, распространять, продавать и т.д. данную статью целиком без изменений (включая текст GFDL). В остальных случаях смотрите оригинал лицензии.
GNU Free Documentation License



  • Главная
  • Новости
  • Новинки
  • Скрипты
  • Форум
  • Ссылки
  • О сайте




  • Emanual.ru – это сайт, посвящённый всем значимым событиям в IT-индустрии: новейшие разработки, уникальные методы и горячие новости! Тонны информации, полезной как для обычных пользователей, так и для самых продвинутых программистов! Интересные обсуждения на актуальные темы и огромная аудитория, которая может быть интересна широкому кругу рекламодателей. У нас вы узнаете всё о компьютерах, базах данных, операционных системах, сетях, инфраструктурах, связях и программированию на популярных языках!
     Copyright © 2001-2024
    Реклама на сайте