Пакеты и права доступа

Модификаторы доступа public, private, protected

Как было рассказано ранее, наличие модификатора public перед методом или полем означает, что его можно использовать где угодно, а модификатора private - что его можно использовать только внутри данного класса. Например:

class A {
    private int myValue1;
    public int myValue2;
    ...
    void f() {
        ...
        myValue1 = 1;      	// внутри класса можно использовать любое его поле 
        myValue2 = 2;     
        ...
        A a;
        a.myValue1; 	// так тоже можно		
        ...
    }
}

...

A a;
a.myValue1 = 1;      	// ошибка компиляции - private поле 
a.myValue2 = 2;     	// public поле можно использовать везде

Рассказ о protected. Часть 1.

Если член (поле или метод) класса объявлен с модификатором protected, то он доступен не только внутри самого класса, но и внутри всех классов-наследников:

class Base {
    protected int myValue;
    ...
}

class Derived extends Base {
    ...
    myValue = 1;     
    Derived d = ...;
    d.myValue = 2;   // это поле тоже можно использовать  
    Base b = ...;
    b.myValue = 3;   // ошибка компиляции 
  ...
}

protected - "прозрачное" для наследников private поле (метод). Поэтому в данном примере произойдёт ошибка, причём в независимости от того, каким объектом на самом деле является b (компилятор этого не может знать). Большинство ошибок прав доступа выявляются именно на стадии компиляции.

Этот рассказ был бы правильным, если бы в Java существовали только три модификатора прав доступа - public, private и protected - но это не совсем так.

Пакеты (Packages)

В самом начале мы выяснили, что при создании нового класса мы должны поместить его обязательно в файл с таким же именем, но это не единственная особенность Java, касающаяся организации файлов и папок.

Когда мы до этого писали файл Main.java, мы компилировали его в той же папке, запуская компилятор строчкой javac Main.java. Но если наш файл лежит в каком-то другом каталоге (например dir1/dir2), нам нужно не просто прописать путь в командной строке, но указать это и в самом файле следующим образом:

package dir1.dir2;  // директива package может идти только первой строкой в файле
public class Main {
    ...
}

Для того, чтобы теперь скомпилировать наш Main.java мы вводим javac dir1/dir2/Main.java, при этом создается файл Main.class, лежащий в dir1/dir2. А для того, чтобы запустить наш класс, нужно ввести строку java dir1.dir2.Main (все разделители пути превращаются в точки).

Таким образом, имя пакета, указанного в файле, совпадает с относительным путём каталога, в котором он находится. Пакет содержит только те файлы, которые лежат в самом каталоге. Файлы из подкаталогов не являются файлами корневого пакета.

dir1.dir2.Main называется полным именем класса (fully qualified name). При необходимости в программе можно прописывать полные имена классов:

org.amse.np.list.List l = new org.amse.np.list.List;

Но зачастую удобнее импортировать нужный нам класс или группу классов, чтобы использовать их короткие имена:

package org.amse.np.hw10;        
import org.amse.np.list.List;   // импортируем класс List
import org.amse.np.dllist.*;    // импортируем все классы из пакета dllist (классы подкаталогов не импортируются!)

class Main {
    ...
    List l = new List();      
}

В любой класс всегда импортируются все классы пакета, в котором находится данный класс, и часть стандартной библиотеки Java (как если бы мы каждый раз писали в начале нашего файла строку import java.lang.*;), что позволяет использовать такие стандартные классы и методы как String, System.out.println() и т.п.

Пакеты можно использовать, чтобы группировать классы, но также для разделения пространства имен. Классы с одинаковым коротким именем List можно различать по именам их пакетов. Поэтому для именования пакетов пользуются следующими общепринятыми соглашениями:

Однако package - это не только способ разделения на каталоги, но и важная сущность языка.

Права доступа по умолчанию

Отсутствие явно указанного модификатора прав доступа перед членом класса означает, что этот член будет доступен во всех классах из данного пакета и не доступен вне пакета. Этот уровень доступа также называют package local.

Таким образом, в Java есть всего четыре уровня доступа: public, private, protected и default (package local)

Рассказ о protected. Часть 2

В Java правилом является то, что у метода класса-наследника при перекрытии должны быть права не более ограничительные, чем у метода класса-родителя:

class Base {
    public void f() {
        ...
    }
}
class Derived extends Base {
    private void f() {     // ошибка прав доступа во время компиляции
        ...
    }
}

По ограничительности уровни доступа можно распределить так:

private < protected, default < public

Если верить первой части рассказа о protected, то нельзя сказать, какой уровень доступа - protected или default - более ограничительный. Можно придумать случаи, когда будет доступно поле default и не доступно поле protected и наоборот.

Чтобы не возникало неоднозначностей с правами доступа, например при наследовании, в Java protected-уровень включает ещё и default. То есть protected член доступен всем подклассам, плюс классам внутри пакета.

Иерархия уровней доступа выглядит следующим образом:

private < default < protected < public

Таким образом, при использовании модификатора protected мы позволяем всем классам из данного пакета иметь доступ к нашему полю/методу, что в большинстве случаев является не той ситуацией, к которой мы стремимся.

Вложенные классы

Мы рассмотрим вложенные классы лишь вскользь, как ещё один способ ограничения использования классов.

Существует два основных типа вложенных классов: static классы (nested)(?) и просто классы-члены (inner). Общий синтаксис:

class Outer {
    [static] class Inner {
        ...
    }
    ...
}  

Достаточно часто применяются вложенные private static классы:

class List {
    private static class ListElement {
        ...
    }
}

При компиляции класса с вложенным классом создаются два файла, в данном случае: List.class и List$ListElement.class. При этом полное имя вложенного класса будет выглядеть следующим образом: org.amse.np.list.List.ListElement

class Outer {
    static class Inner {
        int myValue;
	    ...
    }		
    ...
    Inner i = new Inner();  // экземпляр вложенного класса можно создать внутри самого класса... 
}
...
Outer.Inner i = new Outer.Inner();  // ...или снаружи 

Статические вложенные классы ведут себя так же, как обычные классы, но они имеют доступ к другим статическим членам данного класса.

Нестатический вложенный класс привязан не к самому окружающему классу, а к его экземпляру, поэтому помимо всех своих полей он ещё имеет неявное поле this, указывающее на конкретный экземпляр, частью которого является вложенный класс. Такой вложенный класс имеет доступ ко всем полям и методам окружающего его класса.

class Outer {
    class Inner {
        ...
    }	   
    ...
    Inner i = new Inner();  // создается неявное поле, куда записывается Outer.this
}	
  
Outer o = ...;
Inner i = o.new Inner();

Дополнение: модификаторы доступа для внешних классов

По мимо обычных внешних public классов в Java можно также создавать так называемые local классы. Они могут лежать в одном файле с public классом и являются аналогами вложенных классов.

public class A {
    ...
}
class B {
   ...
}

В одном файле может быть только один public класс и сколько угодно local. Файл должен называться так же, как и public класс.