Последнее изменение: 22 сентября 2007г.

Четыре важных "p"

В Java, как и в любом уважающем себя объектно-ориентированном языке, есть такое явление как модификаторы доступа. Ко всему – полям, методам, классам. Их четыре, и все начинаются на "p" – private, package, protected и public. Именно этой четверке "p" и посвящена данная статья.

Мы поговорим вот о чем:

Сразу хочу сделать замечание. Модификатора package в виде ключевого слова в Java нет. А данный уровень доступа обозначается просто отсутствием остальных трех модификаторов.

Итак, мы начинаем.

Модификаторы – описание

Для начала определимся, о чем пойдет разговор. А именно – какие уровни доступа вообще существуют и что они означают.

  • private – "моё и только моё". К полям, методам и классам, объявленным private, имеет доступ только класс, в котором они объявлены. Для обозначения этого уровня используется ключевое слово private.

  • package – "моё и соседское". К полям, методам и классам, объявленным package, имеет доступ не только класс, в котором они объявлены, но и все классы, находящиеся в том же самом пакете. О том, что представляет собой пакет с точки зрения контроля доступа, речь пойдет ниже. Ключевого слова для обозначения этого уровня доступа, как я уже говорил, нет. Достаточно не указать любой другой.

  • protected – "моё и всех наследников". К полям, методам и классам, объявленным protected, имеет доступ класс, в котором они объявлены, все классы, находящиеся в том же самом пакете и все классы, унаследованные от того, где сделано объявление. Для обозначения уровня доступа используется ключевое слово protected.

  • public – "всем-всем-всем". К полям, методам и классам, объявленным public доступ имеет кто угодно. С этим уровнем надо обращаться с осторожностью, особенно это касается полей класса – очень легко потерять контроль над тем, кто и где модифицирует переменные. Для обозначения этого уровня доступа используется ключевое слово public.

Ничего сложного? Почти. Тонкости есть и тут. О них – дальше. А сейчас – пара слов о том, что такое...

Пакет с точки зрения контроля доступа

Для чего вообще вводить такое понятие как пакет? В статье Ликбез я говорил о пакете как о средстве структурирования кода. На самом деле – это несколько больше. Достаточно задаться очень простым вопросом – а как происходит разделение на пакеты?

Как правило, в пакет объединены классы, тесно связаные друг с другом логически. Возьмем, например, пакет java.awt.event. Что в нем находится? Интерфейсы обработчиков событий и классы, описывающие сами события. Или, скажем, java.lang.reflect – тут можно найти все, что связано с работой с метаданными классов.

То есть по сути пакет представляет собой некую структурную единицу в разработке. Причем связь между пакетами, как правило, односторонняя, во всяком случае когда пакеты находятся на разных уровнях иерархии, как, например, java.awt и java.awt.event (если связь двунаправленная – возможно, стоит пересмотреть разделение). В любом случае, стоит стремиться к тому, чтобы пакеты были слабосвязными, в этом случае их проще как разрабатывать, так и тестировать.

А раз мы имеем структурную единицу – логично предположить, что внутри ее существуют какие-то связи. Совершенно неважные, если речь идет об использовании этой единицы, ибо она представляет собой черный ящик с известным API. И связи эти желательно не светить наружу ни при каких обстоятельствах и никому – просто потому, что человек, не знакомый досконально со структурой пакета, может по незнанию дернуть не за ту ниточку. Что самым плачевным образом скажется на работе всего пакета.

Вот для обеспечения возможности организовывать такие связи и был введен уровень доступа package.

Пример

Пусть у меня есть фабрика, создающая компоненты. Один из вариантов реализации создает компоненты по текстовому описанию. У компонента может быть множество свойств, потому на каждый компонент желательно написать отдельный класс, который будет выставлять компоненту все нужные свойства. Классический вариант шаблона Builder.

Раскрывать детали реализации нам совсем не надо. Поэтому правильным будет следующее – реализацию фабрики сделать public, а все builder-ы – с уровнем доступа package. Тогда фабрика может ими успешно манипулировать, но никто другой этого сделать не сможет. О том, как запретить добавлять в пакет чужие классы, я писал вот тут.

Понимание того, когда нужно использовать уровень доступа package, приходит со временем, вместе с пониманием того, что такое пакет. Однако...

Общее правило применения модификаторов доступа таково – чем больше ограничен доступ, тем лучше.

Не только не стоит, но зачастую и опасно применять, скажем, protected там, где хватило бы и private. Чем больший доступ вы даете к полю, методу, конструктору, классу – тем больше вероятность того, что с ним сделают что-нибудь не так.

Вот мы и добрались до интересного. А именно –

Кому и что можно

Тут я хотел бы остановиться на двух моментах, с пониманием которых чаще всего бывают проблемы.

Момент первый. Пусть у нас есть следующий класс:

package ru.skipy.test.access;

public class AccessTest {

    private int privateField = 0;

    public void testAccess(AccessTest anotherObject){
        anotherObject.privateField = 1;
        System.out.println("anotherObject.getPrivateField()="+anotherObject.getPrivateField() );
        System.out.println("anotherObject.privateField="+anotherObject.privateField );
    }

    private int getPrivateField(){
        return privateField;
    }
}

Вопрос: скомпилируется ли он? Иначе говоря – разрешен ли в методе testAccess доступ к private-методам и полям другого экземпляра того же класса?

Да, разрешен. И к методам, и к полям. Класс скомпилируется. Так что "моё и только моё", как я это формулировал выше, не ограничивает доступ экземпляром класса, а распространяется и на другие экземпляры того же класса. О чем знают не все. Более того, это распространяется и на статические методы – т.е. если в статический метод класса A передать экземпляр класса A, то внутри этого метода разрешен доступ ко всем private-членам переданного экземпляра.

Момент второй. Более сложный. Пусть у нас есть два класса:

package ru.skipy.test.access;

public class AccessTest {

    protected int protectedField = 0;

    protected int getProtectedField(){
        return protectedField;
    }
}
package ru.skipy.test.access.sub;

import ru.skipy.test.access.AccessTest;

public class ChildAccessTest extends AccessTest {

    public void testAccess(AccessTest anotherObject) {
        anotherObject.protectedField = 3;
        System.out.println("anotherObject.getProtectedField()="+anotherObject.getProtectedField() );
        System.out.println("anotherObject.protectedField="+anotherObject.protectedField );
    }
}

Обратите внимание на пакеты. Унаследованный класс находится не в том же самом пакете, что и родительский. Вопрос. Скомпилируется ли класс ChildAccessTest? Иначе говоря – разрешен ли в методе testAccess дочернего класса доступ к protected-полям и методам экземпляра родительского класса?

Ответ: нет, не разрешен! И класс не скомпилируется! А вот если мы вместо AccessTest в качестве типа аргумента укажем ChildAccessTest или любой класс, унаследованный от ChildAccessTest – тогда все будет в порядке. При том, что поля и методы-то на самом деле определены в родительском классе. Так что "моё и всех наследников", как я описал protected-доступ выше, совершенно не означает, что доступ к члену класса имеют любые наследники. К полю или методу собственного экземпляра – да. К полю или методу чужого экземпляра – только если чужой экземпляр является экземпляром того же класса наследника (или унаследованного в свою очередь от него). А вот к полю или методу чужого экземпляра и при этом родительского класса – нет.

У меня есть некоторые соображения, почему сделано именно так. Я не знаю, насколько они верны, но, во всяком случае, они кажутся логичными. Если кто-нибудь даст ссылки на документы, где это описано явно – буду признателен.

Дело в том, что когда мы получаем в качестве параметра экземпляр AccessTest, мы не знаем, каков действительно тип этого параметра. Может статься так, что это какой-то подкласс AccessTest (для определенности OneToFiveClass), который накладывает ограничения на значения того поля, которое мы хотим использовать. Скажем, оно должно быть от 1 до 5. Если бы мы имели доступ к этому полю – мы могли бы тоже унаследоваться от AccessTest, а потом передать ему экземпляр OneToFiveClass и поменять значение поля, обойдя все ограничения. Дыра невероятного размера.

Ну а поскольку дыры в Java не поощряются – эту возможность закрыли. И об этом тоже знают далеко не все.

Полное описание того, кому и что можно, есть, разумеется, в спецификации языка. Вот тут: http://java.sun.com/docs/books/jls/third_edition/html/names.html#6.6. Там есть и примеры.

Отдельно я хотел бы затронуть такой вопрос – а какие вообще модификаторы у кого бывают? У членов класса – полей, методов и конструкторов, – любые. А вот сами классы могу быть public или package. Исключение составляют внутренние классы, у них может быть любой модификатор доступа. И в этом плане внутренние классы ничем не отличаются от обычных членов класса – если они private, то могут использоваться только внутри того класса, где объявлены, если protected – внутри класса, где они объявлены и во всех его наследниках.

Переходим к следующему вопросу.

Зачем нужны private-конструкторы

Этот вопрос я увидел в логах сервера. Вернее, его задавали Google, а уже со страницы поиска пришли на этот сайт. Всплывает этот вопрос регулярно, потому я решил о нем тоже поговорить. Вернее, не только о private- конструкторе, но и о protected. С остальными двумя проблем с пониманием вроде не возникает.

Для начала – что такое вообще конструктор как явление? Это способ инициализации экземпляра создаваемого объекта. Несколько конструкторов – несколько способов. Некоторые из этих способов мы можем дать использовать всем, а некоторые можем оставить только себе.

protected-конструктор может использоваться в двух случаях. Первый – когда он вызывается в качестве конструктора родительского класса, явно или неявно, в обычных или анонимных классах (имеется в виду вызов super(...) или new MyClass(...){ ... }). И второй – он может вызываться в простой конструкции new ..., но только внутри пакета, где определен класс.

private-конструктор – средство создания классов, зарезервированное для нас самих. Например, это нужно при реализации шаблона Singleton – чтобы экземпляры класса могли создавать только мы и никто другой.

Пример

Представим себе массив байтов, который должен быть заполнен значениями из определенного набора. Логично обернуть его в класс и осуществлять контроль в методе set(index,value). Т.е. имеем класс:

package ru.skipy.test.access;

public class ByteArrayWrapper{

    private byte[] data;

    public ByteArrayWrapper(int size){
        data = new byte[size];
    }

    public byte get(int index){
        return data[index];
    }

    public byte set(int index, byte value){
        // performing checks here
        data[index] = value;
    }
}

Для простоты контроль входных параметров опущен – он сейчас не имеет значения.

А вот теперь представьте, что нам надо часть этого массива получить в виде точно такого же объекта. Причем этот новый массив должен быть синхронизирован со старым. Вот тут нам и пригодится private-конструктор:

package ru.skipy.test.access;

public class ByteArrayWrapper{

    private byte[] data;
    private int offset;
    private int length;


    public ByteArrayWrapper(int size){
        data = new byte[size];
        offset = 0;
        length = size;
    }

    private ByteArrayWrapper(byte[] data, int startIndex, int endIndex){
        this.data = data;
        this.offset = startIndex;
        this.length = endIndex – startIndex;
    }

    public byte get(int index){
        return data[index + offset];
    }

    public byte set(int index, byte value){
        // performing checks here
        data[index + offset] = value;
    }

    public ByteArrayWrapper getRange(int startIndex, int endIndex){
        return new ByteArrayWrapper(data, startIndex, endIndex);
    }
}

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

Думаю, что суть теперь ясна. Существует множество случаев, когда нам надо создать объект определенным образом и не дать этого же сделать тем, кто будет использовать наш класс. Именно тогда и пригодится private-конструктор.

Ну и последняя тема, которой я хотел бы коснуться –

private и внутренние классы

Для начала создадим вот такой простой пример. Собственно, вместе с build-файлом он есть вот тут – только скачать и распаковать.

package ru.skipy.test.access;

public class PrivateInnerTest {

    private Inner inner;

    public PrivateInnerTest(){
        inner = new Inner();
    }

    public void test(){
        System.out.println("Field: "+inner.field);
        System.out.println("Method: "+inner.getField());
    }

    private class Inner{

        private int field;

        private Inner(){
            field = 1;
        }

        private int getField(){
            return field;
        }
    }

    public static void main(String[] args) {
        PrivateInnerTest pit = new PrivateInnerTest();
        pit.test();
    }
}

Пример компилируется и работает. В нем мы видим два класса: PrivateInnerTest и Inner. Однако сколько классов на самом деле создается? В build-файле есть цель list, которая выводит содержимое директории, в которой после компиляции оказываются файлы классов. Выполняем команду ant list – и... видим следующее:

  196 PrivateInnerTest$1.class
  775 PrivateInnerTest$Inner.class
1 102 PrivateInnerTest.class

Т.е. в довесок к нашим PrivateInnerTest и PrivateInnerTest$Inner создан еще один – PrivateInnerTest$1. Вопрос. Зачем?

В свое время поиск ответа привел меня к очень любопытным открытиям. Пора браться за дизассемблер1.

Для начала посмотрим на внутренние классы, имеющиеся у класса PrivateInnerTest (в дереве они находятся в разделе Attributes):

inner classes

Как мы и видели, внутренних классов два. Обратите внимание на access flags. Для PrivateInnerTest$Inner это private, как мы и сделали. А вот у PrivateInnerTest$1 модификаторы – static synthetic. Что означает, во-первых, что этот класс синтетический, т.е. сгенерированный компилятором, во-вторых, что у него уровень доступа package, ибо модификаторы уровня доступа отсутствуют!

Теперь посмотрим на класс PrivateInnerTest$Inner. Тут есть на что посмотреть! Я специально свел вместе информацию о методах:

PrivateInnerTest$Inner methods

Прежде всего обратите внимание на количество методов. В коде их всего два – конструктор и getField. А в байткоде их... пять! Два первых – наши, три остальных – синтетические. Что ж, будем разбираться.

Первый метод – <init> – это наш private-конструктор. Единственный параметр – экземпляр внешнего класса PrivateInnerTest (напоминаю, что в конструктор внутреннего класса всегда добавляется один параметр – экземпляр внешнего класса; этот параметр заполняется автоматически).

Второй метод – getField – тоже наш. Модификатор доступа private, параметров нет, возвращаемое значение – int (об этом говорит сигнатура – ()I)

А вот дальше начинается самое интересное.

Третий метод – <init>еще один конструктор! Но! На этот раз, во-первых, синтетический, во-вторых – с уровнем доступа package! И обратите внимание на сигнатуру. Два параметра – один уже знакомый нам экземпляр внешнего класса, а второй... экземпляр PrivateInnerTest$1! Вот он и вылез – тот самый синтетический класс!

Четвертый и пятый методы – близнецы братья. access$100 и access$200. Внешне отличаются только названием. Оба они синтетические, оба статические, оба принимают в качестве параметра экземпляр PrivateInnerTest, оба возвращают int. И у обоих – уровень доступа package.

Возникает резонный вопрос: что делают эти синтетические конструктор и методы? Ниже приведен их байт-код:

//<init>

aload_0
aload_1
invokespecial #3 <ru/skipy/test/access/PrivateInnerTest$Inner.<init>>
return
//access$100

aload_0
getfield #2 <ru/skipy/test/access/PrivateInnerTest$Inner.field>
ireturn
//access$200

aload_0
invokevirtual #1 <ru/skipy/test/access/PrivateInnerTest$Inner.getField>
ireturn

Расшифровываю. #1, #2, #3 – это элементы пула констант класса. Фактически, идентификаторы нашего метода getField, нашего поля field и нашего конструктора соответственно. То есть смысл этих синтетических package-методов сводится к тому, чтобы вызвать соответствующий private-метод, конструктор, или вернуть значение private-поля переданного экземпляра PrivateInnerTest$Inner (мы уже выяснили, что внутри статических методов доступ к private-полям переданного экземпляра разрешен).

Чем дальше, тем интереснее. Надеюсь, вы уже не удивитесь, когда я скажу, что в классе PrivateInnerTest реально вызываются именно синтетические методы?

Вот фрагмент конструктора PrivateInnerTest – создание экземпляра PrivateInnerTest$Inner:

new #2 <ru/skipy/test/access/PrivateInnerTest$Inner>
dup
aload_0
aconst_null
invokespecial #3 <ru/skipy/test/access/PrivateInnerTest$Inner.<init>>
putfield #4 <ru/skipy/test/access/PrivateInnerTest.inner>

Т.е. – выделили память под класс ru/skipy/test/access/PrivateInnerTest$Inner (#2 в пуле констант), вызвали его конструктор (#3 в пуле констант ссылается на синтетический конструктор, это видно в дизассемблере), причем в качестве параметров передали конструктору this и константу null (!), после чего инициализировали переменную inner (#4 в пуле констант).

Точно так же используются синтетические методы access$100 и access$200 (фрагменты кода метода test):

aload_0
getfield #4 <ru/skipy/test/access/PrivateInnerTest.inner>
invokestatic #10 <ru/skipy/test/access/PrivateInnerTest$Inner.access$100>
...
aload_0
getfield #4 <ru/skipy/test/access/PrivateInnerTest.inner>
invokestatic #15 <ru/skipy/test/access/PrivateInnerTest$Inner.access$200>

Таким образом, мы приходим к интересному заключению: доступ к private-членам внутренних классов осуществляется не напрямую, а через синтетические package-методы. Более того, обратный доступ – из внутреннего класса к private-членам внешнего – осуществляется точно так же! Если добавить в класс PrivateInnerTest private-поле и обратиться к нему, например, из getField – уже в PrivateInnerTest будет сгенерирован синтетический package-метод, и именно он будет вызываться для получения значения private-поля внешнего класса.

Красивая картинка, правда?

Разъяснил ситуацию хороший специалист по JVM товарищ Maxim Kizub, за что ему огромное спасибо.

Дело в том, что внутренние классы – вещь несколько искусственная, введенная на уровне языка. Для виртуальной же машины классы PrivateInnerTest и PrivateInnerTest$Inner никак не связаны. Они имеют ссылки друг на друга и находятся в одном пакете. А следовательно – взаимный доступ у них именно уровня пакета, а к private-членам друг друга у них доступа естественным образом нет. А согласно языку – есть. Вот для устранения этого противоречия между языком и JVM и генерируются синтетические package-методы.

Да, а как же класс PrivateInnerTest$1, с которого все началось? Он-то зачем?

А вот зачем. Если метод можно сгенерировать с произвольным именем, то конструктор – нет. Его имя предопределено – <init>. И различать конструкторы можно исключительно по параметрам. Вот для этого различия в синтетический конструктор добавлен еще один параметр – синтетический класс. Этот класс не имеет методов – даже конструктора, убедитесь сами в дизассемблере! – и создать его нельзя. В качестве этого параметра всегда передается null, так что создавать его JVM и не пытается. А загрузить может, ибо соответствующий байткод есть.

Вот к чему может привести случайный взгляд, брошенный в директорию со скомпилированными классами. Будьте бдительны!

* * *

Думаю, на этом с модификаторами доступа можно закончить. Надеюсь, вопросов стало меньше, а ответов больше. Хотя как знать! Может и наоборот – чем больше узнаёшь, тем больше появляется вопросов. В любом случае – всем спасибо!



1. JClassLib компании ej-technologies, очень удобный визуальный дизассемблер. Что немаловажно – бесплатный (лицензия GPL). Взять можно вот тут: http://www.ej-technologies.com/products/jclasslib/overview.html.