36

36


Допустим, у нас имеется класс, не реализующий интерфейс Readable, — как заставить его работать с Scanner? Перед вами пример класса, генерирующего ве­щественные числа:



// interfaces/RandomDoubles java import java util *;



public class RandomDoubles {



private static Random rand = new Random(47), public double next О { return rand nextDouble(), } public static void main(String[] args) {



RandomDoubles rd = new RandomDoubles(), for(int i = 0, l < 7. i ++)



System out print(rd next О + " "),



}



} /* Output



0 7271157860730044 0 5309454508634242 0 16020656493302599 0 18847866977771732



0 5166020801268457 0 2678662084200585 0 2613610344283964 *///.-



Мы снова можем воспользоваться схемой адаптера, но на этот раз адапти­руемый класс создается наследованием и реализацией интерфейса Readable. Псевдомножественное наследование, обеспечиваемое ключевым словом inter­face, позволяет создать новый класс, который одновременно является и Random- Doubles, и Readable:



//• interfaces/AdaptedRandomDoubles java // Создание адаптера посредством наследования import java nio *. import java util *,



public class AdaptedRandomDoubles extends RandomDoubles implements Readable { private int count;



public AdaptedRandomDoubles(int count) { this count = count,



}



public int read(CharBuffer cb) { if(count-- == 0)



return -1.



String result = Double toString(nextO) + "



cb.append(result); return result.lengthO;



}



public static void main(String[] args) {



Scanner s = new Scanner(new AdaptedRandomDoubles(7)), while(s hasNextDoubleO)



System.out print(s nextDoubleO + " ");



}



} /* Output-



0.7271157860730044 0.5309454508634242 0 16020656493302599 0 18847866977771732 0.5166020801268457 0.2678662084200585 0.2613610344283964 *///•-



Так как интерфейсы можно добавлять подобным образом только к сущест­вующим классам, это означает, что любой класс может быть адаптирован для метода, получающего интерфейс. В этом заключается преимущество интерфей­сов перед классами.



Поля в интерфейсах



Так как все поля, помещаемые в интерфейсе, автоматически являются статиче­скими (static) и неизменными (final), объявление interface хорошо подходит для создания групп постоянных значений. До выхода Java SE5 только так можно было имитировать перечисляемый тип enum из языков С и С++:



//• interfaces/Months java



// Использование интерфейсов для создания групп констант, package interfaces:



public interface Months { int



JANUARY = 1, FEBRUARY = 2, MARCH = 3, APRIL = 4, MAY « 5. JUNE = 6, JULY = 7, AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10. NOVEMBER = 11, DECEMBER = 12:



} ///-



Отметьте стиль Java — использование только заглавных букв (с разделением слов подчеркиванием) для полей со спецификаторами static и final, которым присваиваются фиксированные значения на месте описания. Поля интерфейса автоматически являются открытыми (public), поэтому нет необходимости явно указывать это.



В Java SE5 появилось гораздо более мощное и удобное ключевое слово enum, поэтому надобность в применении интерфейсов для определения констант от­пала. Впрочем, эта старая идиома еще может встречаться в некоторых старых программах.



Инициализация полей интерфейсов



Поля, определяемые в интерфейсах, не могут быть «пустыми константами», но могут инициализироваться не-константными выражениями. Например:






//• interfaces/RandVals java // Инициализация полей интерфейсов // не-константными выражениями, import java.util.*;



public interface RandVals {



Random RAND = new Random(47); int RANDOM_INT = RAND.nextInt(10); long RAND0M_L0NG = RAND.nextLong() * 10; float RANDOMJLOAT = RAND, next Long () * 10; double RAND0M_D0UBLE = RAND.nextDouble() * 10; } ///.-



Так как поля являются статическими, они инициализируются при первой загрузке класса, которая происходит при первом обращении к любому из полей интерфейса. Простой тест:



//: interfaces/TestRandVals.java import static net.mindview.util.Print.*;



public class TestRandVals {



public static void main(String[] args) { print(RandVals.RANDOMJNT); pri nt(RandVals.RAND0M_L0NG); print(RandVals.RANDOM FLOAT); pri nt(RandVa1s.RANDOM^DOUBLE);



}



} /* Output: 8



-32032247016559954 -8.5939291E18 5.779976127815049 *///:-



Конечно, поля не являются частью интерфейса. Данные хранятся в статиче­ской области памяти, отведенной для данного интерфейса.



Вложенные интерфейсы



Интерфейсы могут вкладываться в классы и в другие интерфейсы
1
. При этом обнаруживается несколько весьма интересных особенностей:



//: interfaces/nesting/Nestinglnterfaces.java package interfaces.nesting;



class A {



interface В {



void f();



}



public class BImp implements В { public void f() {}



private class BImp2 implements В {продолжение &public void f() {}



}



public interface С { void f();



}



class CImp implements С { public void f() {}



}



private class CImp2 implements С { public void f() {}



}



private interface D { void f();



}



private class DImp implements D { public void f() {}



}



public class DImp2 implements D { public void f() {}



}



public D getDO { return new DImp2(); }



private D dRef;



public void receiveD(D d) { dRef = d; dRef.fO:



}



interface E {



interface G {



void f();



}



// Избыточное объявление public: public interface H { void f();



}



void g();



// He может быть private внутри интерфейса: //! private interface I {}



public class Nestinglnterfaces {



public class BImp implements А.В { public void f() {}



}



class CImp implements А С { public void f() {}



}



// Private-интерфейс не может быть реализован нигде, // кроме как внутри класса, где он был определен: //! class DImp implements A.D { //! public void f() {} //! }



class EImp implements E { public void g() {}



}



class EGImp implements E.G { public void f() {}






}



class EImp2 implements E { public void g() {} class EG implements E.G { public void f() {}



}



}



public static void main(String[] args) { A a = new ), // Нет доступа к A.D. //! A D ad = a getDO. // He возвращает ничего, кроме A.D: //! A DImp2 di2 = a getDO. // Член интерфейса недоступен //' a getDO f().



// Только другой класс А может использовать getDO А а2 = new А(). а2 receiveD(a getDO).



}



} /// ~



Синтаксис вложения интерфейса в класс достаточно очевиден. Вложенные интерфейсы, как и обычные, могут иметь «пакетную» или открытую (public) видимость.



Любопытная подробность: интерфейсы могут быть объявлены закрытыми (private), как видно на примере A.D (используется тот же синтаксис описания, что и для вложенных классов). Для чего нужен закрытый вложенный интер­фейс? Может показаться, что такой интерфейс реализуется только в виде за­крытого (private) вложенного класса, подобного DImp, но A.DImp2 показывает, что он также может иметь форму открытого (public) класса. Тем не менее класс A.DImp2 «замкнут» сам на себя. Факт реализации private-интерфейса не может упоминаться в программе, поэтому реализация такого интерфейса — просто способ принудительного определения методов этого интерфейса без добавле­ния информации о дополнительном типе (то есть восходящее преобразование становится невозможным).



Метод getD() усугубляет сложности, связанные с private-интерфейсом, — это открытый (public) метод, возвращающий ссылку на закрытый (private) интер­фейс. Что можно сделать с возвращаемым значением этого метода? В методе main() мы видим несколько попыток использовать это возвращаемое значение, и все они оказались неудачными. Заставить метод работать можно только од­ним способом — передать возвращаемое значение некоторому объекту, которо­му разрешено его использование (в нашем случае это еще один объект А, у кото­рого имеется необходимый метод receiveD()).



Интерфейс Е показывает, что интерфейсы могут быть вложены друг в друга. Впрочем, правила для интерфейсов — в особенности то, что все элементы ин­терфейса должны быть открытыми (public), — здесь строго соблюдаются, поэто­му интерфейс, вложенный внутрь другого интерфейса, автоматически объявля­ется открытым и его нельзя сделать закрытым (private).



Пример Nestinglnterfaces демонстрирует разнообразные способы реализации вложенных интерфейсов. Особо стоит отметить тот факт, что при реализации интерфейса вы не обязаны реализовывать вложенные в него интерфейсы. Так­же закрытые (private) интерфейсы нельзя реализовать за пределами классов, в которых они описываются.



Интерфейсы и фабрики



Предполагается, что интерфейс предоставляет своего рода «шлюз» к несколь­ким альтернативным реализациям. Типичным способом получения объектов, соответствующих интерфейсу, является паттерн«фабрика».Вместо того, чтобы вызывать конструктор напрямую, вы вызываете метод объекта-фабрики, кото­рый предоставляет реализацию интерфейса — в этом случае программный код теоретически отделяется от реализации интерфейса, благодаря чему становится возможной совершенно прозрачная замена реализации. Следующий пример де­монстрирует типичную структуру фабрики:



//: interfaces/Factories.java



import static net.mindview.util.Print.*;



interface Service { void methodic); void method2();



}



interface ServiceFactory { Service getServiceO;



}



class Implementationl implements Service {



ImplementationlO {} // Доступ в пределах пакета public void methodic) {print("Implementationl methodl");}



public void method2() {print("Implementationl method2");} } • . .



class ImplementationlFactory implements ServiceFactory { public Service getServiceO {



return new ImplementationlO;



}



}



class Implementation2 implements Service {



Implementation2() {} // Доступ в пределах пакета publ/ic void methodlО {print("Implementation2 methodl");} public void method2() {print("Implementation2 method2");}



}



class Implementation2Factory implements ServiceFactory { public Service getService.O {



return new Implementation2();



}



}



public class Factories {



public static void serviceConsumer(ServiceFactory fact) { Service s = fact.getServiceO;






s methodic). s.method2();



}



public static void main(String[] args) {



serviceConsumer(new ImplementationlFactoryO); // Реализации полностью взаимозаменяемы serviceConsumec(new Implementation2FactoryO);



}



} /* Output. Implementation! methodl Implementationl method2 Implementation2 methodl Implementation2 method2 *///:-



Без применения фабрики вам пришлось бы где-то указать точный тип созда­ваемого объекта Service, чтобы он мог вызвать подходящий конструктор.



Но зачем вводить лишний уровень абстракции? Данный паттерн часто при­меняется при создании библиотек. Допустим, вы создаете систему для игр, ко­торая позволяла бы играть в шашки и шахматы на одной доске:



//: interfaces/Games.java



// Игровая библиотека с использованием фабрики



import static net.mindview.util.Print.*:



interface Game { boolean moveO; } interface GameFactory { Game getGameO; }



class Checkers implements Game { private int moves = 0: private static final int MOVES = 3: public boolean motfeO {



print("Checkers move " + moves): return ++moves != MOVES;



}



}



class CheckersFactory implements GameFactory {



public Game getGameO { return new CheckersO; }



}



class Chess implements Game { private int moves = 0; private static final int MOVES = 4; public boolean moveO {



print("Chess move " + moves); return ++moves != MOVES;



}



}



class ChessFactory implements GameFactory {



public Game getGameO { return new ChessO; }



}



public class Games {



public static void playGame(GameFactory factory) {



Game s = factory.getGameO;продолжение &while(s.moveO)



}



public static void main(String[] args). { playGame(new CheckersFactoryO); playGame(new ChessFactoryO);



}



} /* Output: Checkers move 0 Checkers move 1 Checkers move 2 Chess move 0 Chess move 1 Chess move 2 Chess move 3 *///:-



Если класс Games представляет сложный блок кода, такое решение позволит повторно использовать его для разных типов игр.



В следующей главе будет представлен более элегантный способ реализации фабрик на базе анонимных внутренних классов.



Резюме



После первого знакомства интерфейсы выглядят так хорошо, что может пока­заться, будто им всегда следует отдавать предпочтение перед реальными клас­сами. Конечно, в любой ситуации, когда вы создаете класс, вместо него можно создать интерфейс и фабрику.



Многие программисты поддались этому искушению. Тем не менее любая аб­стракция должна быть мотивирована реальной потребностью. Основное назна­чение интерфейсов — возможность переработки реализации в случае необходи­мости, а не введение лишнего уровня абстракции вместе с дополнительными сложностями. Дополнительные сложности могут оказаться довольно сущест­венными. А представьте, что кто-то будет вынужден разбираться в вашем коде и в конечном итоге поймет, что интерфейсы были добавлены «на всякий слу­чай», без веских причин — в таких ситуациях все проектирование, которое вы­полнялось данным разработчиком, начинает выглядеть довольно сомнительно.