Ключовата дума interface придвижва концепцията за абстрактност една стъпка напред. Може да си я мислим като “чисто” абстрактен клас. Тя позволява на създателя да определи формата на класа: имената на методите, аргументните списъци и връщаните стойности, но не и телата на методите. Един interface може също да съдържа даннови членове от примитивни типове, но те са имплицитно static и final. interface дава само форма, не реализация.
interface казва: “Така ще изглеждат класовете реализиращи този конкретен интерфейс.” Така всеки код който използва конкретен interface знае кои методи могат да се викат с него interface и това е всичко. Така че interface се използва да се установи “протокол” между класове. (Някои ООП езици използват думата protocol за същата цел.)
За да се създаде interface, използвайте ключовата дума interface вместо ключовата дума class. Както за клас може да добавите ключовата дума public преди ключовата дума interface (но само ако този interface е дефиниран във файл със същото име) или да не го добавите, оставяйки “приятелски” статус.
За да се направи клас да отговаря на конкретен interface (или група от interface-и) използвайте ключовата дума implements. Вие казвате “interface-ът е за как изглежда а ето как работи.” Отделно от това има голямо сходство с наследяването. Диаграмата за примера с инструментите показва това:
…
Веднъж като реализирате interface, тази реализация става обикновен клас който може да бъде разширяван по обичайния начин.
Може да изберете явно да декларирате методите в interface като public. Но те са public и ако не го направите. Така че когато implement-ирате един interface, методите от interface трябва да са дефинирани като public. Иначе ще станат по подразбиране “приятелски” и по този начин ще ограничите достъпността им при наследяване, което не е позволено от Java компилатора.
Може да видите това в променена версия на примера с Instrument. Забележете че всеки метод в interface е само декларация, което е единственото нещо, позволявано от компилатора тук. В добавка никой от методите в Instrument5 не е деклариран като public, но все пак те са автоматично public:
//: c07:Music5.java
// Interfaces
import java.util.*;
interface Instrument5 {
// Compile-time constant:
int i = 5; // static & final
// Cannot have method definitions:
void play(); // Automatically public
String what();
void adjust();
}
class Wind5 implements Instrument5 {
public void play() {
System.out.println("Wind5.play()");
}
public String what() { return "Wind5"; }
public void adjust() {}
}
class Percussion5 implements Instrument5 {
public void play() {
System.out.println("Percussion5.play()");
}
public String what() { return "Percussion5"; }
public void adjust() {}
}
class Stringed5 implements Instrument5 {
public void play() {
System.out.println("Stringed5.play()");
}
public String what() { return "Stringed5"; }
public void adjust() {}
}
class Brass5 extends Wind5 {
public void play() {
System.out.println("Brass5.play()");
}
public void adjust() {
System.out.println("Brass5.adjust()");
}
}
class Woodwind5 extends Wind5 {
public void play() {
System.out.println("Woodwind5.play()");
}
public String what() { return "Woodwind5"; }
}
public class Music5 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument5 i) {
// ...
i.play();
}
static void tuneAll(Instrument5[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
Instrument5[] orchestra = new Instrument5[5];
int i = 0;
// Upcasting during addition to the array:
orchestra[i++] = new Wind5();
orchestra[i++] = new Percussion5();
orchestra[i++] = new Stringed5();
orchestra[i++] = new Brass5();
orchestra[i++] = new Woodwind5();
tuneAll(orchestra);
}
} ///:~
Останалата част работи по стария начин. Няма значение дали провите ъпкаст към “обикновен” клас с име Instrument5, abstract-ен клас наречен Instrument5, или към interface наречен Instrument5. Поведението е същото. Фактически може да видите в метода tune( ) че няма никакво свидетелство че Instrument5 е “обикновен” клас, abstract-ен клас или interface. Това е целта: Всеки подход дава на програмиста различен начин за управление на създаването и използването на обекти.
“Многократно наследяване” в Java
interface не е просто “по-чиста” форма на abstract-ен клас. Той има по-важно предназначение от това. Понеже interface въобще няма реализация – тоест няма памет асоциирана с interface – няма и нищо, което да пречи много interface-и да бъдат комбинирани. Това е ценно понеже по някой път е нужно да може да се каже “x е a и b и c.” В C++ комбинирането на много класове се нарича многократно наследяване и носи малко ограничаващ действията багаж понеже всеки клас си има реализация. В Java може да се направи същото, но само един от класовете може да има реализация, така че проблемът от C++ не се появява в Java когато се комбинират много интерфейси:
…
В извлечен клас не сте заставени да имате базов клас който е или abstract или “конкретен” (такъв който няма abstract-ни методи). Ако наследите от не-interface, може да наследите само от един. Всички останали базови елементи трябва да бъдат interface-и. Слагате всички имена за наследяване след ключовата дума implements и ги разделяте със запетаи. Може да имате колкото искате interface-и и всеки става независим тип, към който може да се прави кастинг. Следния пример показва конкретен клас комбиниран с няколко interface-а за да се получи нов клас:
//: c07:Adventure.java
// Multiple interfaces
import java.util.*;
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
class ActionCharacter {
public void fight() {}
}
class Hero extends ActionCharacter
implements CanFight, CanSwim, CanFly {
public void swim() {}
public void fly() {}
}
public class Adventure {
static void t(CanFight x) { x.fight(); }
static void u(CanSwim x) { x.swim(); }
static void v(CanFly x) { x.fly(); }
static void w(ActionCharacter x) { x.fight(); }
public static void main(String[] args) {
Hero i = new Hero();
t(i); // Treat it as a CanFight
u(i); // Treat it as a CanSwim
v(i); // Treat it as a CanFly
w(i); // Treat it as an ActionCharacter
}
} ///:~
Може да се види че Hero комбинира конкретния клас ActionCharacter с интерфейсите CanFight, CanSwim и CanFly. Като се комбинира конкретен клас с интерфейси по този начин, конкретният клас трябва да е пръв, после интерфейсите. (Иначе компилаторът дава грешка.)
Забележете, че сигнатурата на fight( ) е същата в interface CanFight и класа ActionCharacter и че fight( ) не е дефиниран в Hero. Правилото за interface е че може да наследявате от него (както ще видите след малко), но тогава получавате друг interface. Ако искате да създадете обект от нов тип той трябва да бъде клас с всички необходими дефиниции. Въпреки че Hero не дава експлицитна дефиниция за fight( ), тя идва от ActionCharacter автоматично така че е възможно създаването на обекти от Hero.
В клас Adventure може да видите четири метода които взимат за аргументи различни интерфейси и конкретния клас. Когато се създаде обект от Hero той може да се подаде на всеки от тези методи, което значи че ще се направи ъпкаст към всеки от интерфейсите interface по ред. Поради начина по който интерфейсите са проектирани в Java това работи без засечки и без никакво усилие от страна на програмиста.
Помнете че централната причина за съществуването на интерфейсите бе посочена в горния пример: да може да се прави ъпкаст към повече от един базов тип. Обаче втората причина да се използват интерфейси е същата като за abstract базови класове: да се предотврати възможността клиент-програмистът да направи обекти от този клас и да се посочи, че това е само интерфейс. Това довежда до въпроса: interface ли ще използвате или abstract-ен клас? interface дава ползите от abstract класа и ползите от interface, токо че ако е възможно да декларирате вашия клас без никакви методи и член-променливи винаги ще предпочитате interface-и пред abstract-ни класове. Фактически ако знаете че нещо ще става базов клас първо ще се опитате да го направите като interface, а само ако се наложи да имате дефиниции на методи и/или член-променливи ще го преработите в abstract клас.
Разширяване на интерфейс с наследяване
Лесно може да се добавят декларации на методи в interface чрез наследяване и също може да се комбинират няколко interfaces в нов interface с наследяване. В двата случая се получава нов interface, както в следния пример:
//: c07:HorrorShow.java
// Extending an interface with inheritance
interface Monster {
void menace();
}
interface DangerousMonster extends Monster {
void destroy();
}
interface Lethal {
void kill();
}
class DragonZilla implements DangerousMonster {
public void menace() {}
public void destroy() {}
}
interface Vampire
extends DangerousMonster, Lethal {
void drinkBlood();
}
class HorrorShow {
static void u(Monster b) { b.menace(); }
static void v(DangerousMonster d) {
d.menace();
d.destroy();
}
public static void main(String[] args) {
DragonZilla if2 = new DragonZilla();
u(if2);
v(if2);
}
} ///:~
DangerousMonster е просто разширение на Monster каета дявя нов interface. Това е реализирано в DragonZilla.
Синтаксисът използван във Vampire работи само когато се наследяват интерфейси. Нормално се използва extends със само един клас, но тъй като interface може да се направи от много други интерфейси, extends може да се отнася за няколко други интерфейси от които се прави нов interface. Както може да се види interface имената просто са разделени със запетаи.
Групиране на константи
Понеже всички полета в interface са автоматично static и final, interface-ът е удобен инструмент за задаване на групи константи, много подобно на enum в C или C++. Например:
//: c07:Months.java
// Using interfaces to create groups of constants
package c07;
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 примитивите, които имат статични инициализатори – тоест, за константите по време на компилация.
Полетата в един interface са автоматично public, така че не е необходимо специално да се указва това.
Сега може да използвате константите извън пакета им импортирайки c07.* или c07.Months както бихте импортирали всеки друг пакет и споменавайки стойностите с изрази като Months.JANUARY. Разбира се, получавате int така че нямате допълнителната сигурност на типа от C++ enum, но тази (често използвана) техника сигурно е подобрение спрямо твърдото записване на числа във вашите програми. (Което често се нарича “магически числа” и дава много труден за поддръжка код.)
Ако искате допълнителна сигурност за типа, може да направите класа така:1
//: c07:Month2.java
// A more robust enumeration system
package c07;
public final class Month2 {
private String name;
private Month2(String nm) { name = nm; }
public String toString() { return name; }
public final static Month2
JAN = new Month2("January"),
FEB = new Month2("February"),
MAR = new Month2("March"),
APR = new Month2("April"),
MAY = new Month2("May"),
JUN = new Month2("June"),
JUL = new Month2("July"),
AUG = new Month2("August"),
SEP = new Month2("September"),
OCT = new Month2("October"),
NOV = new Month2("November"),
DEC = new Month2("December");
public final static Month2[] month = {
JAN, JAN, FEB, MAR, APR, MAY, JUN,
JUL, AUG, SEP, OCT, NOV, DEC
};
public static void main(String[] args) {
Month2 m = Month2.JAN;
System.out.println(m);
m = Month2.month[12];
System.out.println(m);
System.out.println(m == Month2.DEC);
System.out.println(m.equals(Month2.DEC));
}
} ///:~
Класът е наречен Month2 понеже вече има Month в стандартната библиотека на Java. Той е final клас с private конструктор така че никой не може да наследява от него или да прави обекти. Единствените екземпляри са final static създадените в самия клас: JAN, FEB, MAR и т.н. Тези обекти са също използвани в масива month, което позволява да избирате месеците по номер вместо по име. (Забележете допълнителния JAN в масива за да се даде отместване единици, така че December да е 12-ти месец.) В main( ) може да видите сигурността на типа: m е Month2 обект така че може да се присвои само на Month2. В предишния пример Months.java имаше само int стойности, така че на int променливата която щеше да представя месеците можеха да се дадат произволни стойности, което не бе много сигурно.
Този подход също позволява да се използва == или equals( ) взаимозаменяемо, както е показано в края на main( ).
Инициализиране на полета в интерфейсите
Полетата декларирани в интерфейси са автоматично static и final. Те не могат да бъдат “blank finals,” но могат да се инициализират с неконстантни изрази. Например:
//: c07:RandVals.java
// Initializing interface fields with
// non-constant initializers
import java.util.*;
public interface RandVals {
int rint = (int)(Math.random() * 10);
long rlong = (long)(Math.random() * 10);
float rfloat = (float)(Math.random() * 10);
double rdouble = Math.random() * 10;
} ///:~
Понеже полетата са static, те се инициализират когато класът се натоварва за пръв път, при първия достъп до кое да е поле. Ето прост тест:
//: c07:TestRandVals.java
public class TestRandVals {
public static void main(String[] args) {
System.out.println(RandVals.rint);
System.out.println(RandVals.rlong);
System.out.println(RandVals.rfloat);
System.out.println(RandVals.rdouble);
}
} ///:~
Полетата, разбира се, не са част от интерфейса, а са запомнени в статичната памет отделена за този интерфейс.
Сподели с приятели: |