Книга е още в много ранна фаза на написване



страница41/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   37   38   39   40   41   42   43   44   ...   73

Вътрешни класове


В Java 1.1 е възможно да се сложи дефиниция на клас в друга дефиниция на клас. Това се нарича вътрешен клас. Вътрешният клас е полезна черта, понеже поз­волява да се групират класовете които логически са обединени и да се упра­вля­ва видимостта на единия в другия. Важно е да се разбере обаче, че въ­треш­ни­те класове доста се отличават от композицията.

Често когато учите за тях нуждата от вътрешните класове не е непосредсвено оче­видна. В края на тази секция, след като се опише целия синтаксис и се­ман­ти­ка­та, ще намерите пример който изяснява ползите от вътрешните класове.

Създавате вътрешен клас точно както очаквате да се прави: чрез слагане на де­фи­ниция на клас в класа, с който работите в момента: (Виж стр. 89 ако имате труд­ности с пускането на тази програма.)

//: c07:parcel1:Parcel1.java

// Creating inner classes

package c07.parcel1;


public class Parcel1 {

class Contents {

private int i = 11;

public int value() { return i; }

}

class Destination {



private String label;

Destination(String whereTo) {

label = whereTo;

}

String readLabel() { return label; }



}

// Using inner classes looks just like

// using any other class, within Parcel1:

public void ship(String dest) {

Contents c = new Contents();

Destination d = new Destination(dest);

}

public static void main(String[] args) {



Parcel1 p = new Parcel1();

p.ship("Tanzania");

}

} ///:~


Вътрешните класове, когато се използват вътре в ship( ), точно както кои да са дру­ги класове. Единствената разлика на практика е че имената са вместени в Parcel1. Ще видите много по-нататък, че това не е единствената разлика.

По-типично е външният клас да има метод, който връща манипулатор към въ­треш­ния клас, както тук:

//: c07:parcel2:Parcel2.java

// Returning a handle to an inner class

package c07.parcel2;
public class Parcel2 {

class Contents {

private int i = 11;

public int value() { return i; }

}

class Destination {



private String label;

Destination(String whereTo) {

label = whereTo;

}

String readLabel() { return label; }



}

public Destination to(String s) {

return new Destination(s);

}

public Contents cont() {



return new Contents();

}

public void ship(String dest) {



Contents c = cont();

Destination d = to(dest);

}

public static void main(String[] args) {



Parcel2 p = new Parcel2();

p.ship("Tanzania");

Parcel2 q = new Parcel2();

// Defining handles to inner classes:

Parcel2.Contents c = q.cont();

Parcel2.Destination d = q.to("Borneo");

}

} ///:~


Ако искате да направите обект от вътрешен клас където и да е освен в не-static метод на външния клас трябва да специфицирате името на метода като OuterClassName.InnerClassName, както се вижда в main( ).

Вътрешни класове и ъпкастинг


До тук вътрешните класове не изглеждат много драматично. Най-после, ако ис­ка­те скриване, Java вече има перфектен механизъм за скриване – само оставяте класа да бъде “приятелски” (видим само вътре в пакета) без да правите въ­тре­шен клас.

Обаче вътрешните класове се изявяват ако започнете ъпкастинг към базови кла­со­ве, в частност към interface. (Ефектът от произвеждане на интерфейсов ма­ни­пу­латор от обекта който го използва е в основата си същото като ъпкастинг към ба­зов клас.) Така е защото вътрешният клас после може да бъде напълно не­ви­дим и недостъпен за който и да било, което е удобно за скриване на реализацията. Всичко което вземате обратно е манипулатор към базов клас или interface и е възможно даже да не можете да намерите точния тип, както е по­ка­за­но тук:

//: c07:parcel3:Parcel3.java

// Returning a handle to an inner class

package c07.parcel3;
abstract class Contents {

abstract public int value();

}
interface Destination {

String readLabel();

}
public class Parcel3 {

private class PContents extends Contents {

private int i = 11;

public int value() { return i; }

}

protected class PDestination



implements Destination {

private String label;

private PDestination(String whereTo) {

label = whereTo;

}

public String readLabel() { return label; }



}

public Destination dest(String s) {

return new PDestination(s);

}

public Contents cont() {



return new PContents();

}

}


class Test {

public static void main(String[] args) {

Parcel3 p = new Parcel3();

Contents c = p.cont();

Destination d = p.dest("Tanzania");

// Illegal -- can't access private class:

//! Parcel3.PContents c = p.new PContents();

}

} ///:~



Сега Contents и Destination представят интерфеййсите достъпни за клиент-про­гра­миста. (interface, запомнете, автоматично прави всички свои членове public.) За удобство всичките са в един файл, но обикновено Contents и Destination биха били и двете public в техни собствени файлове.

Нещо ново е добавено в Parcel3: вътрешният клас PContents е private така че ни­кой освен Parcel3 няма достъп до него. PDestination е protected, така че ни­кой освен Parcel3, класовете в Parcel3 пакета (тъй като protected също дава па­ке­тен достъп; тоест, protected е също “приятелски”), и наследниците на Parcel3 ня­ма достъп до PDestination. Това значи че клиент-програмистът има огра­ни­че­но знание и достъп до тези членове. Фактически не можете даже да на­пра­ви­те даункаст към private вътрешен клас (или protected вътрешен клас ако не сте на­следник), понеже нямате достъп до имет, както може да видите в class Test. Та­ка private вътрешния клас дава начин да се избегнат всякакви зависимости от ти­па и напълно да се скрие реализацията. В добавка разширяването на interface е безполезно от гледна точка на приложния програмист, понеже той не може да по­лучи достъп до никакъв друг метод освен тези които са част от публичния interface клас. Това също дава възможност на Java компилатора да произведе по-ефективен код.

Нормалните (невътрешни) класове не могат да бъдат направени private или protected – само public или “приятелски.”

Забележете че Contents не е необходимо да е abstract клас. Бихте могли също да използвате и обикновен клас, но най-типичната начална точка за такъв ди­зайн е interface.


Вътрешни класове в методи и обхвати


Това които видяхте до тук може да служи за компас при използването на въ­треш­ни класове. Изобщо кодът който ще пишете включвайки вътрешни кла­со­ве ще бъде с “прости” вътрешни класове които са малки и лесни за разбиране. Оба­че дизайнът на вътрешните класове е много завършен и ще иза множество дру­ги неща, по-скрити, пътища, които може да използвате, ако искате: въ­треш­ни класове могат да бъдат създадени в методи и даже в произволен обхват. Има две причини да се прави това:

  1. Както се показа преди, при реализацията на някакъв интерхейс за да може да се създаде и върне манипулатор.

  2. Когато решавате сложен проблем и искате да създадете клас за да си помогнете, но не искате той да е достъпен за публиката.

В следващите примери предишният код ще се промени за да използва:

  1. Клас дефиниран вътре в метод

  2. Клас дефиниран в обхват вътре в метод

  3. Анонимен клас реализиращ interface

  4. Анонимен клас разширяващ клас който има конструктор не по подразбиране

  5. Анонимен клас който изпълнява инициализация на полета

  6. Анонимен клас който изпълнява конструиране чрез инициализация на ек­зем­пляра (анонимните вътрешни класове не могат да имат конструктори)

Това ще стане в пакета innerscopes. Първо общите интерфейси от предишния код ще се дефинират в техни собствени файлове за да се използват във всички при­мери:

//: c07:innerscopes:Destination.java

package c07.innerscopes;
interface Destination {

String readLabel();

} ///:~

Беше изтъкнато че Contents би могъл да бъде abstract клас, така че тука той ще бъ­де в по-натурална форма, като interface:



//: c07:innerscopes:Contents.java

package c07.innerscopes;


interface Contents {

int value();

} ///:~

Въпреки че е обикновен клас с реализация, Wrapping също се използва като общ “интерфейс” за извлечените от него класове:



//: c07:innerscopes:Wrapping.java

package c07.innerscopes;


public class Wrapping {

private int i;

public Wrapping(int x) { i = x; }

public int value() { return i; }

} ///:~

Ще забележите по-горе че Wrapping има конструктор който изисква аргумент, за да станат малко по-интересни нещата.



Първият пример показва създаването на целия клас в обхвата на метод (вместо в обхвата на друг клас):

//: c07:innerscopes:Parcel4.java

// Nesting a class within a method

package c07.innerscopes;


public class Parcel4 {

public Destination dest(String s) {

class PDestination

implements Destination {

private String label;

private PDestination(String whereTo) {

label = whereTo;

}

public String readLabel() { return label; }



}

return new PDestination(s);

}

public static void main(String[] args) {



Parcel4 p = new Parcel4();

Destination d = p.dest("Tanzania");

}

} ///:~


Класът PDestination е част от dest( ) а не от Parcel4. (Също забележете че бих­те могли да използвате PDestination за вътрешен клас на всеки клас от същата под­директория без конфликт на имената.) Затова PDestination не може да бъде до­стъпен отвън на dest( ). Забележете ъпкастинга който става в оператора за връ­щане – нищо не излиза от dest( ) освен манипулатор на базовия клас Destination. Разбира се фактът че името на класа PDestination е сложено вътре в dest( ) не значи че PDestination не е валиден обект когато dest( ) завърши.

Следващия пример позволява как може да вложите вътрешен клас в произво­лен обхват:

//: c07:innerscopes:Parcel5.java

// Nesting a class within a scope

package c07.innerscopes;
public class Parcel5 {

private void internalTracking(boolean b) {

if(b) {

class TrackingSlip {



private String id;

TrackingSlip(String s) {

id = s;

}

String getSlip() { return id; }



}

TrackingSlip ts = new TrackingSlip("slip");

String s = ts.getSlip();

}

// Can't use it here! Out of scope:



//! TrackingSlip ts = new TrackingSlip("x");

}

public void track() { internalTracking(true); }



public static void main(String[] args) {

Parcel5 p = new Parcel5();

p.track();

}

} ///:~



Класът TrackingSlip е вместен вътре в обхвата на if оператор. Това не значи че кла­сът е третиран в зависимост от условие – той се компилира заедно с всичко дру­го. Обаче не е достъпен извън обхвата където е дефиниран. Във всичко дру­го е като всеки обикновен клас.

Следващият пример изглежда малко странно:

//: c07:innerscopes:Parcel6.java

// A method that returns an anonymous inner class

package c07.innerscopes;
public class Parcel6 {

public Contents cont() {

return new Contents() {

private int i = 11;

public int value() { return i; }

}; // Semicolon required in this case

}

public static void main(String[] args) {



Parcel6 p = new Parcel6();

Contents c = p.cont();

}

} ///:~


Методът cont( ) комбинира създаването на връщана стойност с дефиниция на клас която представя върнатата стойност! В добавка класът е анонимен – няма име. За да станат нещата малко по-зле, изглежда като че сме започнали да съз­да­ваме Contents обект:

return new Contents()

но тогава, преди да стигнете до точката със запетая, вие казвате, “Но чакай, мис­ля че влизам в дефиниция на клас”:

return new Contents() {

private int i = 11;

public int value() { return i; }

};

Този странен синтаксис значи “създай обект от анонимен клас който е наследен от Contents.” Манипулаторът върнат от new е автоматично ъпкастнат към Contents манипулатор. Синтаксисът за анонимен вътрешен клас е съкратено за­пис­ване на:



class MyContents extends Contents {

private int i = 11;

public int value() { return i; }

}

return new MyContents();



В анонимния вътрешен клас Contents е създаден чрез използване на кон­струк­тор по подразбиране. Следния пример показва какво да се прави ако базовият клас иска аргумент:

//: c07:innerscopes:Parcel7.java

// An anonymous inner class that calls the

// base-class constructor

package c07.innerscopes;
public class Parcel7 {

public Wrapping wrap(int x) {

// Base constructor call:

return new Wrapping(x) {

public int value() {

return super.value() * 47;

}

}; // Semicolon required



}

public static void main(String[] args) {

Parcel7 p = new Parcel7();

Wrapping w = p.wrap(10);

}

} ///:~


Тоест, проста давате подходящ аргумент на конструктора на базовия клас, тука x-ът даден на new Wrapping(x). Анонимният клас не може да има конструктор, където нормално се вика super( ).

И в двата предишни примера точка-запетаята не означава края на тялото на кла­са (както прави в C++). Вместо това означава края на израза, който съдържа въ­трешния клас. Това е идентично с използването на точка-запетаята навсякъде дру­гаде.

Какво ще стане ако се опитате да инициализирате нещо в обект от анонимен въ­тре­шен клас? Поради анонимността няма име, с което да се нарече кон­струк­то­ра, така че няма конструктор. Можете, обаче, да изпълните инициализация в точ­ката на определяне на полетата:

//: c07:innerscopes:Parcel8.java

// An anonymous inner class that performs

// initialization. A briefer version

// of Parcel5.java.

package c07.innerscopes;


public class Parcel8 {

// Argument must be final to use inside

// anonymous inner class:

public Destination dest(final String dest) {

return new Destination() {

private String label = dest;

public String readLabel() { return label; }

};

}



public static void main(String[] args) {

Parcel8 p = new Parcel8();

Destination d = p.dest("Tanzania");

}

} ///:~



Ако при дефинирането на анонимен вътрешен клас пожелаете да използвате обект който е дефиниран извън анонимния вътрешен клас компилаторът из­иск­ва той да бъде final. Това е защото аргументът на dest( ) е final. Ако забравите ще получите грешка при компилация.

Доколкото просто присвоявате поле, горният подход е точен. Ами ако трябва да изпълните неща, които се правят в конструктори? В Java 1.1 с инициа­ли­за­ция на екземпляр можете, фактически, да зъдъдете конструктор за анонимен въ­трешен клас:

//: c07:innerscopes:Parcel9.java

// Using "instance initialization" to perform

// construction on an anonymous inner class

package c07.innerscopes;


public class Parcel9 {

public Destination

dest(final String dest, final float price) {

return new Destination() {

private int cost;

// Instance initialization for each object:

{

cost = Math.round(price);



if(cost > 100)

System.out.println("Over budget!");

}

private String label = dest;



public String readLabel() { return label; }

};

}



public static void main(String[] args) {

Parcel9 p = new Parcel9();

Destination d = p.dest("Tanzania", 101.395F);

}

} ///:~



Вътре в инициализатора на екземпляра може да видите код, който не може да бъ­де изпълнен при инициализацията на полета (т.е. if оператора). Така че ефек­тив­но инициализатора на екземпляра е конструктора на аноничния вътрешен клас. Разбира се, той е ограничен; не може да претоварвате инициализаторите на екземпляра така че имате един конструктор от този вид.

Връзката към външния клас


До тук сякаш вътрешните класове са схема за скриване на имена и организация на код, която е полезна, но не неизбежна. Обаче има и друго нещо. Когато съз­да­вате вътрешен клас обектите от него имат връзка към външния клас който ги съз­дава, а така те имат достъп до полетата на външния клас – без никакви спе­циал­ни квалификации. В добавка вътрешните класове имат право на достъп до всич­ки елементи на външния клас.2 Следващия пример демонстрира това:

//: c07:Sequence.java

// Holds a sequence of Objects
interface Selector {

boolean end();

Object current();

void next();

}
public class Sequence {

private Object[] o;

private int next = 0;

public Sequence(int size) {

o = new Object[size];

}

public void add(Object x) {



if(next < o.length) {

o[next] = x;

next++;

}

}



private class SSelector implements Selector {

int i = 0;

public boolean end() {

return i == o.length;

}

public Object current() {



return o[i];

}

public void next() {



if(i < o.length) i++;

}

}



public Selector getSelector() {

return new SSelector();

}

public static void main(String[] args) {



Sequence s = new Sequence(10);

for(int i = 0; i < 10; i++)

s.add(Integer.toString(i));

Selector sl = s.getSelector();

while(!sl.end()) {

System.out.println((String)sl.current());

sl.next();

}

}



} ///:~

Sequence е просто масив с фиксирана дължина от Object с клас който го об­гръ­ща. Викате add( ) за добавите нов Object в края на последователността (ако има мя­сто). За да се намери всеки обект в Sequence има интерфейс наречен Selector, кой­то позволява да видите дали сте на end( ) (края-б.пр.), да видите current( ) (те­кущия-б.пр.) Object и да отидете на следващия next( ) Object в Sequence (по­сле­дователността). Понеже Selector е interface, много други обекти може да при­ложат interface по техни си начини и много методи може да вземат interface ка­то аргумент, с оглед да се осигури родов код.

Тук SSelector е частен клас който дава функционалността на Selector. В main( ), мо­же да се види създаването на Sequence, следвано от събиране на определен брой String обекти. Тогава се прави Selector с извикване на getSelector( ) и това се използва за да се движим през Sequence и избираме всеки елемент.

В началото SSelector изглежда като друг вътрешен клас. Разгледайте го обаче от­близо. Забележете че всеки от методите end( ), current( ) и next( ) се отнася към o, който манипулатор не е част от SSelector, а е private поле в обгръщащия клас. Обаче вътрешният метод има достъп до полетата на външния клас като че са негови. Излиза че това е много удобно, както личи в горния пример.

Така вътрешния клас има достъп до членовете на обгръщащия го клас. Как ста­ва това? Вътрешният клас трябва да помни връзка към породилия го външин клас. Тогава като споменете член на обгръщащия клас този (скрит) указател се из­ползва за достъп до члена. За щастие компилаторът върши всичките по­д­роб­но­сти заради вас, но може сега да разберете, че вътрешен клас може да се съз­да­де само асоцииран с обект от обгръщащия клас. Процесът на конструирането изиск­ва инициализация на манипулатор към обгръщащия клас и компилаторът ще се оплаква, ако няма достъп до него. Повечето пъти всичко това става без на­месата на програмиста.


static вътрешни класове


За да се разбере значението на static когато се приложи към вътрешен клас тряб­ва да се припомни че вътрешният клас неявно разполага с указател към об­гръ­щащия го клас. Това не е така, обаче, когато вътрешният клас е static. static въ­трешен клас значи:

  1. Не е необходимо да има създаден обект от обгръщащия клас за да се създаде обект от static вътрешен клас.

  2. Не може да се ползва обгръщащия обект извътре на static вътрешен клас.

Има ограничения: static членовете могат да бъдат само във обгръщащия клас, така че вътрешният клас не може да имя static данни или static вътрешни кла­со­ве.

Ако не е необходимо да създадете обект от външния клас за да създадете обект от вътрешния клас, може всичко да направите static. За да направите това, тряб­ва също и вътрешните класове да са static:

//: c07:parcel10:Parcel10.java

// Static inner classes

package c07.parcel10;
abstract class Contents {

abstract public int value();

}
interface Destination {

String readLabel();

}
public class Parcel10 {

private static class PContents

extends Contents {

private int i = 11;

public int value() { return i; }

}

protected static class PDestination



implements Destination {

private String label;

private PDestination(String whereTo) {

label = whereTo;

}

public String readLabel() { return label; }



}

public static Destination dest(String s) {

return new PDestination(s);

}

public static Contents cont() {



return new PContents();

}

public static void main(String[] args) {



Contents c = cont();

Destination d = dest("Tanzania");

}

} ///:~


В main( ) не е необходим обект от Parcel10 наместо това използвате нормалния син­таксис за избор на static член за да извикате методите, които връщат ма­ни­пу­латори към Contents и Destination.

Нормално не може да слагате никакъв (изпълним-б.пр.) код в interface, но static въ­трешен клас може да бъде част от interface. Тъй като класът е static това не на­рушава правилата за интерфейсите – static вътрешния клас само се слага в про­странството на имената на интерфейса:

//: c07:IInterface.java

// Static inner classes inside interfaces


interface IInterface {

static class Inner {

int i, j, k;

public Inner() {}

void f() {}

}

} ///:~



По-рано в книгата съветвах да се слага main( ) във всеки клас с цел тестване на кла­са. Един недостатък на това е допълнителният код, който трябва да се влачи. Ако това е проблем, може да използвате static вътрешен клас да съдържа вашия код:

//: c07:TestBed.java

// Putting test code in a static inner class
class TestBed {

TestBed() {}

void f() { System.out.println("f()"); }

public static class Tester {

public static void main(String[] args) {

TestBed t = new TestBed();

t.f();

}

}



} ///:~

Това генерира отделен клас наречен TestBed$Tester (за стартиране на програмата пишете java TestBed$Tester). Може да използвате този клас за тест­ва­не, но не е необходимо да го включвате в крайната версия.


Обръщения към обект от външния клас


Ако е необходимо да произведете манипулатор към външния обект пишете името на външния клас следвано от точка и this. Например в класа Sequence.Sselector всеки може да направи и запомни манипулатор към вън­ш­ния клас Sequence като се напише Sequence.this. Това което се получава е ав­то­ма­тично с точния тип. (Всичко е известно и проверено по време на компилация, та­ка че няма допълнителни разходи по време на изпълнение.)

Понякога е нужно да се каже на обекти да създадат обекти от някой от техните въ­трешни класове. За да се направи това е необходимо да се даде манипулатор към този външен клас на израза с new, подобно на това:

//: c07:parcel11:Parcel11.java

// Creating inner classes

package c07.parcel11;
public class Parcel11 {

class Contents {

private int i = 11;

public int value() { return i; }

}

class Destination {



private String label;

Destination(String whereTo) {

label = whereTo;

}

String readLabel() { return label; }



}

public static void main(String[] args) {

Parcel11 p = new Parcel11();

// Must use instance of outer class

// to create an instances of the inner class:

Parcel11.Contents c = p.new Contents();

Parcel11.Destination d =

p.new Destination("Tanzania");

}

} ///:~


За да се създаде обект от вътрешния клас направо не се следва същата форма да се обръщаме към името на външния клас Parcel11 както може да се очаква, а вме­сто това се използва обект от външния клас за да се направи обект от въ­треш­ния клас:

Parcel11.Contents c = p.new Contents();

И така, не е възможно да се създаде обект от вътрешен клас докато не се съз­да­де обект от външния клас. Това е защото вътрешният клас е твърде свързан с външ­ния клас, който го е създал. Обаче ако направите static вътрешен клас не е необ­ходимо да има указател към външен обект.

Наследяване от вътрешни класове


Понеже конструкторът на вътрешният клас трява да използва манипулатор към външ­ния клас, нещата са малко по-усложнени ако наследявате от вътрешен клас. Проблемът е в “тайния” манипулатор към обгръщащия обект който тряб­ва да бъде инициализиран и вече в извлечения клас не остава манипулатор към вън­шен клас. Решението е да се използва синтаксис който да прави връзката яв­на:

//: c07:InheritInner.java

// Inheriting an inner class
class WithInner {

class Inner {}

}
public class InheritInner

extends WithInner.Inner {

//! InheritInner() {} // Won't compile

InheritInner(WithInner wi) {

wi.super();

}

public static void main(String[] args) {



WithInner wi = new WithInner();

InheritInner ii = new InheritInner(wi);

}

} ///:~


Може да се види, че InheritInner разширява само вътрешния клас, не и външ­ния. Но когато се дойде до изпълнението на конструктор, този по подразбиране не става и не може просто да дадете манипулатор на външния. Освен това тряб­ва да се използва синтаксисът:

enclosingClassHandle.super();

вътре в конструктора. Това дава необходимия манипулатор и тогава про­гра­ма­та се компилира.

Могат ли вътрешни класове да се подтискат?


Какво става, ако създадете вътрешен клас, наследите от вънщния клас и пре­де­фи­нирате вътрешния клас? Тоест, възможно ли е да се подтисне вътрешен клас? То­ва изглежда като мощна концепция, но “подтискането” на вътрешен клас като­чели е друг метод на външния клас не прави нищо:

//: c07:BigEgg.java

// An inner class cannot be overriden

// like a method


class Egg {

protected class Yolk {

public Yolk() {

System.out.println("Egg.Yolk()");

}

}

private Yolk y;



public Egg() {

System.out.println("New Egg()");

y = new Yolk();

}

}


public class BigEgg extends Egg {

public class Yolk {

public Yolk() {

System.out.println("BigEgg.Yolk()");

}

}

public static void main(String[] args) {



new BigEgg();

}

} ///:~



Конструкторът по подразбиране се прави автоматично от компилатора и вика кон­структора на базовия клас. Може да се помисли че след като BigEgg се съз­да­ва “подтиснатата” версия на Yolk би се използвала, но това не е така. Изходът е:

New Egg()

Egg.Yolk()

Този пример показва, че не става никаква допълнителна магия, ако наследите външ­ния клас. Обаче е възможно явно да наследите от вътрешния клас:

//: c07:BigEgg2.java

// Proper inheritance of an inner class


class Egg2 {

protected class Yolk {

public Yolk() {

System.out.println("Egg2.Yolk()");

}

public void f() {



System.out.println("Egg2.Yolk.f()");

}

}



private Yolk y = new Yolk();

public Egg2() {

System.out.println("New Egg2()");

}

public void insertYolk(Yolk yy) { y = yy; }



public void g() { y.f(); }

}
public class BigEgg2 extends Egg2 {

public class Yolk extends Egg2.Yolk {

public Yolk() {

System.out.println("BigEgg2.Yolk()");

}

public void f() {



System.out.println("BigEgg2.Yolk.f()");

}

}



public BigEgg2() { insertYolk(new Yolk()); }

public static void main(String[] args) {

Egg2 e2 = new BigEgg2();

e2.g();


}

} ///:~


Сега BiggEgg2.Yolk явно extends Egg2.Yolk и подтиска методите му. Методът insertYolk( ) позволява BigEgg2 да направи ъпкаст на един от неговите Yolk обек­ти към y манипулатора в Egg2, така че когато g( ) извика y.f( ) под­тис­на­та­та версия на f( ) се използва. Изходът е:

Egg2.Yolk()

New Egg2()

Egg2.Yolk()

BigEgg2.Yolk()

BigEgg2.Yolk.f()

Второто извикване на Egg2.Yolk( ) е извикването в конструктора на базовия клас на BigEgg2.Yolk конструктора. Може да видите, че подтиснатата версия на f( ) се използва когато се вика g( ).

Идентификатори на вътрешния клас


Тъй като всеки клас произвежда .class файл който съдържа всичката ин­фор­ма­ция как да се създаде обект от този тип (тази информация произвежда мета-клас наречен Class обект), може да познаете че вътрешните класове трябва съ­що да създават .class файлове да съдържат информация за техните Class обек­ти. Имената на тези файлове/класове имат строга формула: името на об­хва­ща­щия клас, следвано от ‘$’, следван от името на вътрешния клас. Например .class фай­ловете създадени от InheritInner.java включват:

InheritInner.class

WithInner$Inner.class

WithInner.class

Ако вътрешните класове са анонимни компилаторът започва просто да ге­не­ри­ра числа за техни идентификатори. Ако вътрешни класове са вместени във въ­треш­ни класове, техните имена са просто добавени след ‘$’ и иден­ти­фи­ка­тор(ите) на външни класове).

Въпреки че тази схема на генерация на имена е проста и праволинейна, тя също е добра и успява в много ситуации.3 Понеже това е стандартна схема за имена в Java, генерираните файлове са автоматично независими от платформата. (Забележете че Java компилаторът променя вътрешните класове по всички на­чи­ни, щото те да работят.)


Защо вътрешни класове: рамки на управлението


До тук видяхте много синтаксис и семантика, обясняващи как работят въ­треш­ни­те класове, но това не отговаря на въпроса защо съществуват те. Защо Sun си съз­даде толкова безпокойства вмъквайки толкова фундаментална черта на ези­ка в Java 1.1? Отговорът е нещо, което аз ще споменавам като control frame­work.

Application framework е клас или множество от класове, проектиран(о) да се ре­ша­ва определен проблем. За да се приложи приложната рамка се наследяват един или няколко класа и се подтискат колкото е нужно на брой методи. Кодът кой­то се пише за подтиснатите методи променя поведението с цел да се реши точ­но необходимия (по-друг от първоначалния - б.пр.) проблем. Рамката за управ­ление е частен случай на приложна рамка, проектирана с цел да се реагира на събития; система, която в поведението си се ръководи главно от събития се на­рича задвижвана от събития система . Един от най-важните проблеми в по­требителското програмиране е графичният потребителски интерфейс (GUI), кой­то почти изцяло е задвижван от събития. Както ще видите в глава 13, Swing би­блиотеката на Java е управляваща рамка, която елегантно решава GUI про­бле­ма използвайки вътрешни класове.

За да видим как вътрешните класове позволяват лесно създаване и използване на управляващи рамки да вземем една, чиято задача е да изпълнява нещо, ко­га­то се случи събитието “готовност.” Макар че “готовност” би могло да значи мно­го неща, в нашия случай то се определя по часовника. Ова което се по­лу­ча­ва е управляваща рамка, която не е определено какво управлява (и би могла да управ­лява всичко-б.пр.). Първо, има интерфейс който описва всяко възможно управ­ляващо събитие. Той е abstract клас наместо истински interface понеже по­ведението по подразбиране е управление по часовник, така че част от реа­ли­за­цията може да бъде включена тук:

//: c07:controller:Event.java

// The common methods for any control event

package c07.controller;
abstract public class Event {

private long evtTime;

public Event(long eventTime) {

evtTime = eventTime;

}

public boolean ready() {



return System.currentTimeMillis() >= evtTime;

}

abstract public void action();



abstract public String description();

} ///:~


Конструкторът просто прихваща времето когато искате да стартира Event, до­ка­то ready( ) казва когато стане време да се пуска. Разбира се, ready( ) би могло да бъде подтиснато в извлечен клас за да може действието на Event да се ос­но­ве на нещо друго, различно от времето.

action( ) е методът, който се вика когато Event е ready( ) и description( ) дава текс­това информация за Event.

Следващия файл съдържа фактическата управляваща рамка, която управлява и пус­ка събитията. Първият клас фактически е “помощен” чиято задача е да съ­дър­жа Event обекти. Би могъл да се замести с която и да е подходяща колекция и в глава 8 ще откриете такива, които правят трика без писането на този до­пъл­ни­телен код:

//: c07:controller:Controller.java

// Along with Event, the generic

// framework for all control systems:

package c07.controller;


// This is just a way to hold Event objects.

class EventSet {

private Event[] events = new Event[100];

private int index = 0;

private int next = 0;

public void add(Event e) {

if(index >= events.length)

return; // (In real life, throw exception)

events[index++] = e;

}

public Event getNext() {



boolean looped = false;

int start = next;

do {

next = (next + 1) % events.length;



// See if it has looped to the beginning:

if(start == next) looped = true;

// If it loops past start, the list

// is empty:

if((next == (start + 1) % events.length)

&& looped)

return null;

} while(events[next] == null);

return events[next];

}

public void removeCurrent() {



events[next] = null;

}

}


public class Controller {

private EventSet es = new EventSet();

public void addEvent(Event c) { es.add(c); }

public void run() {

Event e;

while((e = es.getNext()) != null) {

if(e.ready()) {

e.action();

System.out.println(e.description());

es.removeCurrent();

}

}

}



} ///:~

EventSet съдържа до 100 Events. (В “реална” колекция от глава 8 нямаше да се гла­воболите с максималната дължина, понеже те си я сменят сами). index се из­полз­ва за пазене сведения за следващото свободно място, next се използва при тър­сене на следващия Event в списъка за да се види какво ще се прави по-на­татък. Това е важно през време на извикването на getNext( ), понеже Event обек­тите се махат от листа (чрез removeCurrent( )) след като са стартирани, та­ка че getNext( ) ще намери дупки в списъка като се движи през него.

Забележете че removeCurrent( ) не просто слага някакъв флаг за да отбележи че обек­тите са използвани. Вместо това слага манипулатор да бъде null. Това е важ­но, понеже ако боклучарят види че обектът се използва той няма да го по­чи­сти. Ако очаквате вашите манипулатори да станат излишни (както тука), до­бре е да ги приравните на null когато вече не трябват за да дадете възможност на боклучаря да ги чисти.



Controller е мястото, където става истинската работа. Там се използва EventSet за владеене на неговите Event обекти, а addEvent( ) позволява да се добавят съ­би­тия в този списък. Но важен метод е run( ). Този метод цикли през EventSet, тър­сейки Event обект който е ready( ) да стартира. За всеки за който намери ready( ) вика action( ) метода, извежда description( ) и после маха Event от спи­съ­ка.

Забележете че до този момент не се знае какво точно прави Event. И това е гвоз­деят на програмата; как се “отделят нещата които се променят от тези, кои­то остават така.” Или, използвайки моя термин, “векторът на промяната” са раз­личните действия свързани с различните Event обекти, а различните дей­ствия се изразяват чрез създаване на различни подкласове на Event.

Тук е мястото, където вътрешните класове влизат в играта. Те позволяват две неща:


  1. Да се изрази цялата реализация на рамката в единствен клас, капсулирайки с това всичко, което е уникално за реализацията. Вътрешните класове се из­полз­ват за да се изразят многото различни видове action( ) необходими да се реши проблема. В добавка следващия пример използва private вътрешни кла­сове така че реализацията е напълно скрита и може да се променя без­на­ка­зано.

  2. Чрез въътрешните класове се избягва реализацията да стане тромава, по­не­же лесно има достъп до всеки член на външния клас. Без тази възможност ко­дът може да стане толкова неприятен, че да се видите принудени да тър­си­те друг начин.

Да вземем конкретна рамка проектирана да работи с оранжерия.4 Всяка дей­ност е напълно различна: светлини, вода, включване и изключване на тер­мо­ста­ти, зумери и рестартиране на системата. Но управляващата рамка е про­ек­ти­ра­на лесно да изолира този тъйй различен код. За всеки тип дейност се наследява нов Event вътрешен клас и се пише управляващ код в action( ).

Както е типично за приложна рамка GreenhouseControls е наследен от Controller:

//: c07:controller:GreenhouseControls.java

// This produces a specific application of the

// control system, all in a single class. Inner

// classes allow you to encapsulate different

// functionality for each type of event.

package c07.controller;


public class GreenhouseControls

extends Controller {

private boolean light = false;

private boolean water = false;

private String thermostat = "Day";

private class LightOn extends Event {

public LightOn(long eventTime) {

super(eventTime);

}

public void action() {



// Put hardware control code here to

// physically turn on the light.

light = true;

}

public String description() {



return "Light is on";

}

}



private class LightOff extends Event {

public LightOff(long eventTime) {

super(eventTime);

}

public void action() {



// Put hardware control code here to

// physically turn off the light.

light = false;

}

public String description() {



return "Light is off";

}

}



private class WaterOn extends Event {

public WaterOn(long eventTime) {

super(eventTime);

}

public void action() {



// Put hardware control code here

water = true;

}

public String description() {



return "Greenhouse water is on";

}

}



private class WaterOff extends Event {

public WaterOff(long eventTime) {

super(eventTime);

}

public void action() {



// Put hardware control code here

water = false;

}

public String description() {



return "Greenhouse water is off";

}

}



private class ThermostatNight extends Event {

public ThermostatNight(long eventTime) {

super(eventTime);

}

public void action() {



// Put hardware control code here

thermostat = "Night";

}

public String description() {



return "Thermostat on night setting";

}

}



private class ThermostatDay extends Event {

public ThermostatDay(long eventTime) {

super(eventTime);

}

public void action() {



// Put hardware control code here

thermostat = "Day";

}

public String description() {



return "Thermostat on day setting";

}

}



// An example of an action() that inserts a

// new one of itself into the event list:

private int rings;

private class Bell extends Event {

public Bell(long eventTime) {

super(eventTime);

}

public void action() {



// Ring bell every 2 seconds, rings times:

System.out.println("Bing!");

if(--rings > 0)

addEvent(new Bell(

System.currentTimeMillis() + 2000));

}

public String description() {



return "Ring bell";

}

}



private class Restart extends Event {

public Restart(long eventTime) {

super(eventTime);

}

public void action() {



long tm = System.currentTimeMillis();

// Instead of hard-wiring, you could parse

// configuration information from a text

// file here:

rings = 5;

addEvent(new ThermostatNight(tm));

addEvent(new LightOn(tm + 1000));

addEvent(new LightOff(tm + 2000));

addEvent(new WaterOn(tm + 3000));

addEvent(new WaterOff(tm + 8000));

addEvent(new Bell(tm + 9000));

addEvent(new ThermostatDay(tm + 10000));

// Can even add a Restart object!

addEvent(new Restart(tm + 20000));

}

public String description() {



return "Restarting system";

}

}



public static void main(String[] args) {

GreenhouseControls gc =

new GreenhouseControls();

long tm = System.currentTimeMillis();

gc.addEvent(gc.new Restart(tm));

gc.run();

}

} ///:~


Забележете че light, water, thermostat и rings всичките принадлежат на външ­ния клас GreenhouseControls, а вътрешните класове нямат проблеми с достъпа до тези полета. Също, повечето от action( ) методите също включват някакво управ­ление на хардуер, което най-вероятно ще въвлече изпълнението на не-Java код.

Повечето от Event класовете изглеждат подобни, но Bell и Restart са спе­ци­ал­ни. Bell бие (включва зумер, камбана или нещо такова - б.пр.) и ако не е било до­статъчно, добавя още един Bell обект към списъка на събитията, така че ще бие пак по-късно. Забележете как вътрешните класове почти изглеждат като мно­жествено наследяване: Bell има методите на Event и също има всичките ме­то­ди на външния клас GreenhouseControls.



Restart е отговорен за стартирането на системата, затова слага всички необ­хо­ди­ми събития. Разбира се, по-гъвкав начин да се реализиратова е да се избегне твър­дото кодиране и те да се четат от файл. (Едно упражнение в глава 10 иска да модифицирате този код за да се постигне това.) Тъй като Restart( ) е просто друг Event обект може просто да се добави Restart обект в Restart.action( ) та­ка че системата редовно да се рестартира. И всичко каквото трябва да се на­пра­ви в main( ) е да се създаде GreenhouseControls обект и да се добави Restart обект за пускането му.

Този пример трябва да издигне много в очите ви значението на вътрешните обек­ти, особено като са използвани в управляваща рамка. Обаче в последната част на глава 13 ще видите как елегантно те се използват за описание на гра­фи­чен интерфейс. Като свършите въпросната секция ще бъдете напълно убедени.





Сподели с приятели:
1   ...   37   38   39   40   41   42   43   44   ...   73




©obuch.info 2024
отнасят до администрацията

    Начална страница