C, PHP, VB, .NET

Дневникът на Филип Петров


* Под-пакети и не-статични вложени член класове

Публикувано на 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. Именно затова казваме, че чрез тяхната функционалност се засилва капсулацията на данни.

И за да усложним още повече нещата ще кажем, че представеният пример е само един от четирите вида вложени класове - нарича се "вложен член клас". Съществуват още "статичен вложен член клас", "локален клас" и "анонимен клас". Ще ги разгледаме поотделно в следваща статия.

 



2 коментара


  1. "Когато един клас не е публичен (т.е. видимост по подразбиране) казахме, че се вижда само и единствено от други класове в текущия клас."

    Тук не трябва ли да е, че се вижда само от класовете в текущия пакет?

Добави коментар

Адресът на електронната поща няма да се публикува


*