* Под-пакети и не-статични вложени член класове
Публикувано на 14 октомври 2009 в раздел ПИК3 Java.
В статията класове и пакети на Java показахме примери за създаване на един пакет и няколко класа вътре в него. Също така по-късно показахме как е възможно едни класове да наследяват други. Що се отнася до пакетите - беше споменато, но не докрай обяснено, че те следват йерархичната структура на директории и под-директории. Сега ще обясним всичко по-подробно, но първо ще се спрем върху няколко основни неща свързани с организацията при изпълнението на програми.
Всяка виртуална машина на Java идва с определен набор (всъщност доста голям) от готови пакети с класове. Такива пакети са java.io (съдържат класове работещи с входно-изходни устройства), java.net (класове свързани с мрежови потоци) и т.н. Забелязвате веднага, че "системните" за Java класове започват винаги с думата "java" последвана от точка и конкретизиране на името на друг пакет (io, net, ...). Тук вече имаме наличие на йерархия - системния пакет java има свой под-пакет io, който пък притежава клас FileInputStream. За да достигнем до този клас ние указваме пълният път до него - java.io.FileInputStream.
Когато пишем наша собствена програма ние обикновено я слагаме извън системната директория на JDK, например в "c:\myprogram". Нека за пример в тази директория сме сложили един клас HelloWorld.class, който показва тривиалното съобщение на екрана. Ако изпълните този клас от текущата директория, програмата ще стартира:
Файл HelloWorld.java намиращ се в c:\myprogram:
package myprogram; public class HelloWorld{ public static void main(String[] args){ System.out.println("Hello World"); } }
Поредица от команди и тяхното изпълнение:
C:\> cd myprogram C:\myprogram> dir 14.10.2009 a. 13:52 <:DIR>: . 14.10.2009 a. 13:52 <:DIR>: .. 14.10.2009 a. 13:44 133 HelloWorld.java 1 File(s) 133 bytes 2 Dir(s) 213 201 666 048 bytes free C:\myprogram> javac HelloWorld.java C:\myprogram> cd .. C:\> java myprogram.HelloWorld Hello World C:\>
С програмата javac ние компилирахме файл HelloWorld.java и създадохме байткод HelloWorld.class. Чрез програмата java ние заредихме main метода на този клас. Виждате, че достъпваме класа чрез пътя до него - "myprogram.HelloWorld".
Ето какво ще стане обаче ако се намираме в друга директория, различна от c:\:
C:\> cd temp C:\temp> java myprogram.HelloWorld Exception in thread "main" java.lang.NoClassDefFoundError: myprogram/HelloWorld Caused by: java.lang.ClassNotFoundException: myprogram.HelloWorld at java.net.URLClassLoader$1.run(Unknown Source) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at java.lang.ClassLoader.loadClassInternal(Unknown Source) Could not find the main class: myprogram.HelloWorld. Program will exit. C:\temp>
Явно е, че виртуалната машина не може да намери нашият клас. Логично е да се досетим защо се получава така - в директория temp, в която се намираме, няма поддиректория myprogram и няма файл HelloWorld.class. Веднага можете да си направите аналогия със стандартните програми с разширение ".exe" - можете да стартирате изпълнимия файл от неговата собствена директория.
Когато говорим за пакети обаче аналогията с изпълнимите .exe файлове не е достатъчна. Освен, че изпълняваме класове от някой пакет като програми (т.е. класове с main метод), ние често използваме класовете и като библиотеки. Става въпрос за абсолютно същия принцип, по който вие създавахте обект java.util.Scanner или още повече - правихте import "java.util.*" и вмъквахте множество от класове от един пакет. Естествено е, че бихме желали да можем да правим същото и с наши класове и пакети и не желаем да сме ограничени да работим само и единствено в една главна директория. Например може да пожелаем да вмъкнем клас SayHello от директория c:\myprogram2 в нашия клас HelloWorld от директория c:\myprogram.
За щастие java ни дава такава функционалност чрез environment variable "CLASSPATH". По подразбиране в тази променлива е записана само ".", т.е. "текущата директория". Именно затова ние можехме да извикаме myprogram.HelloWorld от c:\ - само от такава "текуща директория" може да се "види" този пакет и класа в него. Ето един пример как можем да вмъкнем един пакет от "чужда" директория в нашата програма:
Файл SayHello.java в директория c:\myprogram2:
package myprogram2; public class SayHello{ String message; public SayHello(String message){ this.message = message; } public void speak(){ System.out.println(this.message); } }
Файл HelloWorld.java в директория c:\myprogram:
package myprogram; import myprogram2.SayHello; public class HelloWorld{ public static void main(String[] args){ SayHello msg = new SayHello("Hola amigos"); msg.speak(); } }
Ето какво би се случило ако се опитаме не да изпълним, а дори само да компилираме програмата HelloWorld.java:
C:\>cd c:\myprogram2 C:\myprogram2>javac SayHello.java C:\myprogram2>cd c:\myprogram C:\myprogram>javac HelloWorld.java HelloWorld.java:2: package myprogram2 does not exist import myprogram2.SayHello; ^ HelloWorld.java:5: cannot find symbol symbol : class SayHello location: class myprogram.HelloWorld SayHello msg = new SayHello("Hola amigos"); ^ HelloWorld.java:5: cannot find symbol symbol : class SayHello location: class myprogram.HelloWorld SayHello msg = new SayHello("Hola amigos"); ^ 3 errors C:\myprogram>
Случи се точно това, което предположихме - пакет myprogram2 не е видим от програмата в пакет myprogram. За да се справим с този проблем трябва да добавим базовата директория за пакета myprogram2 (в случая това е c:\, защото именно тя "вижда" директорията/пакета myprogram2) в променливата CLASSPATH:
C:\myprogram>set CLASSPATH=.;C:\; C:\myprogram>javac HelloWorld.java C:\myprogram>cd .. C:\>java myprogram.HelloWorld Hola amigos C:\>
Сега вече е време да поговорим и за "под-пакети". Те не са нищо по-различно от под-директории в директориите на някой пакет. Ето един пример:
Файл SayHello.java в директория C:\myprogram2\myprogram2sub:
package myprogram2.myprogram2sub; public class SayHello{ String message; public SayHello(String message){ this.message = message; } public void speak(){ System.out.println("Subpackage says: "+this.message); } }
Файл SayHello.java в директория c:\myprogram2:
package myprogram2; public class SayHello{ String message; public SayHello(String message){ this.message = message; } public void speak(){ System.out.println(this.message); } }
Файл HelloWorld.java в c:\myprogram:
package myprogram; import myprogram2.*; public class HelloWorld{ public static void main(String[] args){ SayHello msg = new SayHello("Hola amigos"); msg.speak(); myprogram2.myprogram2sub.SayHello msg2 = new myprogram2.myprogram2sub.SayHello("abc"); msg2.speak(); } }
Изпълнение:
C:\>cd myprogram2 C:\myprogram2>javac SayHello.java C:\myprogram2>cd myprogram2sub C:\myprogram2\myprogram2sub>javac SayHello.java C:\myprogram2\myprogram2sub>cd c:\myprogram C:\myprogram>javac HelloWorld.java C:\myprogram>cd .. C:\>java myprogram.HelloWorld Hola amigos Subpackage says: abc C:\>
Виждате, че при първото създаване на обект "SayHello msg =..." се създаде обекта от клас myprogram2.SayHello - това е така, защото ние вмъкнахме всички класове от пакет myprogram2 чрез "import myprogram2.*". Така компилаторът не се "обърква" от факта, че имаме два класа с едно и също име. Вторият клас обаче сме длъжни да го достъпваме чрез пълният път myprogram2.myprogram2sub.SayHello. В противен случай ще се получи грешка и компилаторът няма да знае кой от двата класа да "избере". Ето един пример - модифицираме файла HelloWorld.java в:
package myprogram; import myprogram2.*; import myprogram2.myprogram2sub.*; public class HelloWorld{ public static void main(String[] args){ SayHello msg = new SayHello("Hola amigos"); msg.speak(); SayHello msg2 = new SayHello("abc"); msg2.speak(); } }
При компилация ще се получи грешка:
C:\myprogram> javac HelloWorld.java HelloWorld.java:6: reference to SayHello is ambiguous, both class myprogram2.myprogram2sub.SayHello in myprogram2.myprogram2sub and class myprogram2.SayHello in myprogram2 match SayHello msg = new SayHello("Hola amigos"); ...
За изход от такива ситуации е нужно винаги да достъпваме класовете чрез пълният път до техните пакети. Принципно за да сме сигурни, че няма да попадаме в такива ситуации е нужно да спазваме следните две правила:
- Давайте уникални имена на пакетите, които пишете;
- Давайте уникални имена на класовете, които пишете .
Йерархията на пакети и под-пакети всъщност много наподобява тази на класове и "вложени класове". В езика Java ни е дадена възможност да правим следните конструкции:
class ВъншенКлас { ... class ВложенКлас { ... } }
В предишната статия споменахме, че има два вида класове - публични и стандартни. Публичните класове можеха да се виждат от всички класове, включително от класове в "чужди" пакети (в горните примери направихме именно това - клас SayHello беше публичен в пакет myprogram2 и по този начин клас HelloWorld от пакет myprogram го виждаше). Когато един клас не е публичен (т.е. видимост по подразбиране) казахме, че се вижда само и единствено от други класове в текущия пакет.
В случая на вложените класове имаме на първо място логическо групиране на класовете:
public class HelloWorld{ public static void main(String[] args){ // Създаваме инстанция на външен клас: MessagesClass m = new MessagesClass("Hello World", 3); m.speak(); // Създаваме инстанция на вложен клас на клас m: MessagesClass.SpeakerClass s = m.new SpeakerClass("Hola"); s.speak(); } } class MessagesClass{ String msg; int times; public MessagesClass(String msg, int times){ this.msg=msg; this.times = times; } public void speak(){ SpeakerClass s = new SpeakerClass(this.msg); for(int i=0; i<this.times; i++){ s.speak(); } } class SpeakerClass{ String msg; public SpeakerClass(String msg){ this.msg=msg; } public void speak(){ System.out.println(this.msg); } } }
В този пример на вложения клас не бяхме сложили модификатор за достъп, т.е. по подразбиране той е видим от текущия пакет и само от него могат да се правят инстанции. Капсулация на данни се получава тогава, когато поставим модификатор за достъп private:
public class HelloWorld{ public static void main(String[] args){ // Създаваме инстанция на външен клас: MessagesClass m = new MessagesClass("Hello World", 3); m.speak(); // Вече не е възможно да правим инстанция: //MessagesClass.SpeakerClass s = m.new SpeakerClass("Hola"); //s.speak(); } } class MessagesClass{ String msg; int times; public MessagesClass(String msg, int times){ this.msg=msg; this.times = times; } public void speak(){ SpeakerClass s = new SpeakerClass(this.msg); for(int i=0; i<this.times; i++){ s.speak(); } } private class SpeakerClass{ String msg; public SpeakerClass(String msg){ this.msg=msg; } public void speak(){ System.out.println(this.msg); } } }
Така вложеният клас се вижда само и единствено от неговия външен клас. Това е и най-честият случай - НЕ правим инстанции на вложени класове, а ги използваме само вътре в техните външни класове. Поради тази причина често се казва, че такива вложени класове са "помагащи класове". Впрочем не е проблем да имаме вложени абстрактни класове, както и наследяване на вложени класове вътре във външен клас.
Виждате, че вложените класове могат да бъдат освен public и default (както външните) така и private и protected. Именно затова казваме, че чрез тяхната функционалност се засилва капсулацията на данни.
И за да усложним още повече нещата ще кажем, че представеният пример е само един от четирите вида вложени класове - нарича се "вложен член клас". Съществуват още "статичен вложен член клас", "локален клас" и "анонимен клас". Ще ги разгледаме поотделно в следваща статия.
"Когато един клас не е публичен (т.е. видимост по подразбиране) казахме, че се вижда само и единствено от други класове в текущия клас."
Тук не трябва ли да е, че се вижда само от класовете в текущия пакет?
Така е, промених го.