В 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.
Вътрешни класове в методи и обхвати
Това които видяхте до тук може да служи за компас при използването на вътрешни класове. Изобщо кодът който ще пишете включвайки вътрешни класове ще бъде с “прости” вътрешни класове които са малки и лесни за разбиране. Обаче дизайнът на вътрешните класове е много завършен и ще иза множество други неща, по-скрити, пътища, които може да използвате, ако искате: вътрешни класове могат да бъдат създадени в методи и даже в произволен обхват. Има две причини да се прави това:
-
Както се показа преди, при реализацията на някакъв интерхейс за да може да се създаде и върне манипулатор.
-
Когато решавате сложен проблем и искате да създадете клас за да си помогнете, но не искате той да е достъпен за публиката.
В следващите примери предишният код ще се промени за да използва:
-
Клас дефиниран вътре в метод
-
Клас дефиниран в обхват вътре в метод
-
Анонимен клас реализиращ interface
-
Анонимен клас разширяващ клас който има конструктор не по подразбиране
-
Анонимен клас който изпълнява инициализация на полета
-
Анонимен клас който изпълнява конструиране чрез инициализация на екземпляра (анонимните вътрешни класове не могат да имат конструктори)
Това ще стане в пакета 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 вътрешен клас значи:
-
Не е необходимо да има създаден обект от обгръщащия клас за да се създаде обект от static вътрешен клас.
-
Не може да се ползва обгръщащия обект извътре на 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 framework.
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.
Тук е мястото, където вътрешните класове влизат в играта. Те позволяват две неща:
-
Да се изрази цялата реализация на рамката в единствен клас, капсулирайки с това всичко, което е уникално за реализацията. Вътрешните класове се използват за да се изразят многото различни видове action( ) необходими да се реши проблема. В добавка следващия пример използва private вътрешни класове така че реализацията е напълно скрита и може да се променя безнаказано.
-
Чрез въътрешните класове се избягва реализацията да стане тромава, понеже лесно има достъп до всеки член на външния клас. Без тази възможност кодът може да стане толкова неприятен, че да се видите принудени да търсите друг начин.
Да вземем конкретна рамка проектирана да работи с оранжерия.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 ще видите как елегантно те се използват за описание на графичен интерфейс. Като свършите въпросната секция ще бъдете напълно убедени.
Сподели с приятели: |