Подготовка к экзамену Oracle Certified Professional Java Programmer — Часть 1

    Предисловие



    Хочу продолжить делиться приобретенными знаниями и своими впечатлениями от подготовки к экзамену. Огромное спасибо всем тем, кто дал рекомендации к нулевой части этой серии! Сегодня я поговорю еще немножко о модификаторах доступа и их взаимоотношениях с наследованием и пакетами, рассмотрю varargs и перечисления, а также массивы и способы их инициализации. Я надеюсь, что хабражители снова откликнутся и дополнят то, о чем я забыл упомянуть или попросту не знал.

    Продолжаем готовиться к экзамену под катом.



    Содержание для всей серии


    1. Идентификаторы, правила именования, модификаторы для классов и интерфейсов
    2. Модификаторы для методов и полей, vararg, enum и массивы


    Методы, поля, локальные переменные и их модификаторы



    Как я уже говорил, в Java существуют четыре модификатора доступа: public, private, protected и отсутствие модификатора (он же модификатор по умолчанию). К невложенным классам и интерфейсам применимы только два из них: public и модификатор по умолчанию. К методам и полям класса применим весь набор.

    1. Если метод или поле имеют модификатор public, то они потенциально доступны всей вселенной.
    2. Если метод или поле имеют модификатор доступа private, то они доступны только в рамках класса. Такие члены класса не наследуются, поэтому их невозомжно заместить в подклассах. Помните об этом.
    3. Если метод или поле имеют модификатор доступа по умолчанию, то они доступны только в рамках пакета.
    4. Если метод или поле имеют модификатор доступа protected, то они, прежде всего, доступны самому классу и его наследникам. Кроме того, доступ к этим членам класса могут получить их собратья по пакету.


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

    Хочу также обратить внимание на некоторые особенности, которые возникают при использовании доступа по умолчанию и модификатора protected. Рассмотрим следующуий пример. Пусть имеется базовый класс, объявленный в пакете test. Этот класс обладает двумя полями. Первое объявлено с доступом по умолчанию, второе — protected.

    package org.kimrgrey.scjp.test;
     
    public class BaseClass {
            int defaultValue;
            protected int protectedValue;
           
            public BaseClass() {
                    this.defaultValue = 1;
                    this.protectedValue = 1;
            }
    }
    


    Если объявить в этом пакете класс SamePackageAccess, который не будет наследоваься от BaseClass, то он все равно получит доступ и к полю defaultValue, и к полю protectedValue. Об этой особенности модификатора protected стоит помнить: члены класса, объявленные как protected, в рамках пакета доступны как через наследование, так и через ссылку. Пример:

    package org.kimrgrey.scjp.test;
     
    public class SamePackageAccess {
            public SamePackageAccess() {
                    BaseClass a = new BaseClass();
                    a.defaultValue = 2;
                    a.protectedValue = 2;
            }
    }
    


    В случае с наследованием в этом пакете доступ по-прежнему сохраняется к обоим полям, причем как по ссылке, так и через наследование.

    package org.kimrgrey.scjp.test;
     
    public class SamePackageSubclass extends BaseClass {
            public SamePackageSubclass() {
                    this.defaultValue = 3;
                    this.protectedValue = 3;
                    BaseClass a = new BaseClass();
                    a.defaultValue = 3;
                    a.protectedValue = 3;
            }
     
    }
    


    Теперь давайте посмотрим, что будет, если мы выйдем за пределы пакета. Первое, что случится — мы потеряем доступ к полю, объявленному без явного указания модификатора доступа. Его не будут видеть абсолютно все классы вне родного пакета, в том числе и прямые наследники BaseClass. Поле же с модификатором protected будет доступно через наследование всем своим подклассам. Однако даже наследник не сможет его использовать через ссылку. Кроме того, будучи однажды унаследованным классом вне пакета, поле становится закрытым для любых классов, за исключением дальнейших наследников.

    package org.kimrgrey.scjp.main;
     
    import org.kimrgrey.scjp.test.BaseClass;
     
    public class OtherPackageSubclass extends BaseClass {
            public OtherPackageSubclass() {
                    this.defaultValue = 10; // Line 8: не получим доступ, потому что другой пакет
                    this.protectedValue = 10;
                   
                    BaseClass a = new BaseClass();
                    a.protectedValue = 10; // Line 12: по ссылке не могут обращаться даже наследники BaseClass
            }
    }
    


    В этом примере содержится также еще одна важная деталь. Предположим, вас спрашивают, что же случиться если скомпилировать приведенный выше код? И дают следующие варианты ответа:
    1. Код будет успешно скомпилирован
    2. Возникнет ошибка компиляции на строке с номером 8
    3. Возникнет ошибка компиляции на строке с номером 12

    В этом случае, если явно не указано обратное, выбрать нужно все правильные варианты ответов: 2 и 3, — а не останавливаться на первом подходящем. Внимательно читайте все ответы и проверяйте их на корректность. Именно этот подход работает лучше всего. Прочитав и поняв суть вопроса, проверяйте и анализируйте именно ответы, а не код, приведенный в формулировке.

    Среди модификаторов, связанных с наследованием, следует также рассмотреть final. На методы final действует также, как на классы: запрещает их переопределение наследниками. При этом расширять сам класс, в котром находится final метод, по-прежнему можно.

    Разрешается применять модификатор final к полям, аргументам методов и локальным переменным. В случае примитивных типов будет запрещено любое изменение значения переменной, кромее ее инициализации. Тут следует помнить, что моментом инициализации локальных переменных считается первое присваивание им значения в рамках метода. До этого переменную использовать нельзя: получите ошибку при компиляции. Помеченное final поле также придется явным образом инициализировать. Это можно сделать либо непосредственно при объявлении, в инициализационном блоке, либо в конструкторе того класса, в котором оно объявлено. Оставлять инициализацию final полей на совести наследников не разрешается. В случае ссылок модификатор final запретит переприсваивать ссылку. Сам объект, на который ссылка указывает, все еще можно изменять: вызывать изменяющие его состояния методы, присваивать полям новое значение и так далее.

    Важно помнить, что к локальным переменным неприменимы никакие модификаторы, кроме final. Поэтому, если вы видите в объявлении локальной переменной что-то вроде private int a, то можно смело говорить, что это не скомпилируется. А что же с полями?
    1. К полям, как я уже говорил, применимы все четыре уровня доступа.
    2. Поле может быть помечено как final.
    3. Поле может быть помечено как transient.
    4. Поле может быть помечено как static.
    5. Поле может быть помечено как volatile.
    6. Поле не может быть помечено как abstract.
    7. Поле не может быть помечено как synchronized.
    8. Поле не может быть помечено как strictfp.
    9. Поле не может быть помечено как native.

    Некоторые из модификаторов, упомянутых выше, я раньше не описывал. Постараюсь рассмотреть их позже, в соответсвующих темах (transient будет рассмотрен в рамках сериализации, а synchronized и volatile — в многопоточности).

    Методы с переменным количеством аргументов


    1. Когда вы указываете параметр vararg, то базовым типом может быть любой тип: примитивный или нет.
    2. Чтобы объявить такой параметр, вы пишите тип, потом три точки, пробел, затем имя массива, который будет использоваться в рамках метода: void f (int... a). Можно также разделить тип, три точки и идентифкаторы пробелами, так: void f(int ... a). Внимательно следите за точками. Авторы экзамена любят переносить их за идентификатор. Такой подход не работает.
    3. В метод могут передаваться другие параметры, но в этом случае параметр vararg должен быть последним: void f (double x, int... a)
    4. В методе может быть один и только один vararg параметр.

    Для наглядности приведу хороший пример вопроса по этой теме. Выберите такое объявление метода doSomething(), чтобы приведенный ниже код был удачно скомпилирован?

    package org.kimrgrey.scjp.main;
     
    public class Application {
           
            public static void main(String[] args) {
                    doSomething(1);
                    doSomething(1, 2);
            }
     
    }
    


    1. static void doSomething(int... values) {}
    2. static void doSomething(int[] values) {}
    3. static void doSomething(int x, int... values) {}


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

    package org.kimrgrey.scjp.main;
     
    public class Application {
           
            private static void f (int... a) {
                    for (int i = 0; i < a.length; ++i) {
                            System.out.println(a[i]);
                    }
            }
           
            public static void main(String[] args) {
                    f(new int[] {1, 2 ,3});
            }
     
    }
    


    Все чудесно соберется и отработает. Формального объяснения этому я не знаю, но предполагаю, что это связано с тем, что vararg-параметр является всего лишь синтаксическим сахаром и воспринимается компилятором как ссылка на массив, поэтому никаких проблем не возникает.

    Перечисления


    1. У перечислений могут быть конструкторы.
    2. У перечислений могут быть поля.
    3. У перечислений могут быть методы.
    4. Если перечисление объявляется вне класса, оно может получить только два уровня доступа: public или по умолчанию.
    5. У перечислений есть статический метод values(), который возвращает массив, содержащий все возможные значения перечисления, причем строго в том порядке, в котором они были объявлены.

    Для каждого из значений в рамках перечисления вы можете объявить свое собственное «тело» — его специфическое описание. При этом специфичные для значения версии методов перегружают вариант, который используется для всего перечисления в целом. Это позволяет менять поведение членов перечисления в зависимости от нужд приложения. Пример:

    package org.kimrgrey.scjp.main;
     
    import static java.lang.System.*;
     
    enum Currency {
            UNKNOWN,
            USD {
                    public String getStringCode() {
                            return "USD";
                    }
                   
                    public int getSomethingElse() {
                            return 10;
                    }
            },
            UAH {
                    public String getStringCode() {
                            return "UAH";
                    }
            },
            RUR {
                    public String getStringCode() {
                            return "RUR";
                    }
            };
           
            public String getStringCode() {
                    return "";
            }
    }
     
    public class Application {
           
            private static void f (int... a) {
                    for (int i = 0; i < a.length; ++i) {
                            out.println(a[i]);
                    }
            }
           
           
            public static void main(String[] args) {
                    out.println(Currency.USD.getStringCode());
                    // out.println(Currency.USD.getSomethingElse());
            }
     
    }
    



    В результате выполения этого кода в стандартный поток вывода будет помещена строка «USD». Обратите внимание на метод getSomethingElse(). Он объявлен для значения USD, однако не упоминается для всего перечисления. Не смотря на то, что в объявлении стоит public, никто из вне доступ к этому методу получить не сможет. Если строку за номером 44 раскомментировать, то код даже не скомпилируется.

    Немного о массивах



    В Java допустимы два варианта объявления массивов. Квадратные скобки могут быть размещены после имени типа, так: int[] a, — или после идентификатора, так: int a[]. Важно понимать, что оба способа абсолютно равноправны с точки зрения синтаксиса, хотя первый из них и является рекомендуемым. Таким образом, String[] s[] — это ни что иное, как двумерный массив строк. Скомпилируется без вопросов.

    При объявлении массива нельзя указать его размер, так как память выделяется только в момент создания массива: int[] a = new int [4]. Поэтому код int a[4] вызовет ошибку компиляции. В случае с массивом ссылок на объекты важно помнить, что при создании массива сами объекты не создаются. К примеру, код Thread threads = new Thread [20] создаст массив из двадцати null'ов, никаких конструкторов вызываться не будет.

    При построении многомерных массивов о них нужно думать, как о массивах, каждый элемент которых ссылается снова на массив. Абстрактные конструкции вроде матриц и кубов упрощают программирование, но могут усложнить сдачу экзамена. К примеру, конструкция int [][]a = new int [10][] вполне допустима и создаст двумерный массив, элементы которого могут быть проинициализированы позже: a[0] = new int [100], — причем совсем не обязательно массивами равной длины: a[1] = new int [200].

    Для того, чтобы проинициализировать массив быстро (не элемент за элементом), можно применять синтаксис вроде этого: int[] x ={1, 2, 3}. В фигурных скобках могут стоять не только константы, но и переменные и даже выражения. Можно также создавать анонимные массивы, что часто используется когда нужно передать строго определенный массив в функцию: f(new int [] {2, 4, 8}). Если вы видите на экзамене такую конструкцию, то обязательно присмотритесь внимательнее. Есть вероятность, что будет написано что-то вроде этого: f(new int[3] {2, 4, 8}). Такой код не будет скомпилирован, так как размер анонимного массива вычисляется исходя из его объявления и не должен указываться явным образом.

    На этом я закончу на сегодня. В ближайшее время обязательно поговорим об особенностях некоторых важных операций в Java (присваивание, сравнение, instanceOf, арифметика), а также о неявных классах и потоках.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 30
    • +3
      По-моему, всё описанное в этой статье обязан знать каждый Java-программист. Иначе он не Java-программист.
      • 0
        А вот, к примеру, то, что нельзя создавать массивы параметризованных типов или как приводятся массивы ссылочных типов — более специфические знания. Хотелось бы узнать побольше о подобных тонкостях.
        • 0
          Таким образом, я не Java программист :) Ну, а если серьезно, то я не согласен. К примеру, я пришел к Java из C++. Поэтому просто не задавался целью изучить нюансы языка. При этом вполне успешно зарабатываю себе на жизнь программированием на Java. А сертификация — хороший повод вернуться к основам.
        • +1
          Объявления String[] s[], t; и String[][] s, t; неравноправны. Поскольку в первом случае t — одномерный массив, а во втором — двумерный. Иными словами скобки справа от идентификатора относятся к нему и только к нему.
          • +1
            Простите, не понял вас. И в том, и вдругом случае объявляется самый что ни на есть двумерный массив.
            1.  
            2. public class Application {
            3.         public static void main(String[] args) {
            4.                 String[] s[] = new String [5][5];
            5.                 s[0][0] = new String("Hello");
            6.                 s[0][1] = new String ("World");
            7.                 System.out.println(s[0][1]);
            8.                 System.out.println(s[0][0]);
            9.         }
            10. };
            11.  

            • 0
              Ну попробуйте тогда скомпилировать следующий код:
              int[] s[], t;
              t = new int[1];
              t[0] = new int[1];
              • 0
                Пропустил пару скобок, не тот, а этот код:
                int[] s[], t;
                t = new int[1][];
                t[0] = new int[1];
                или этот:
                int[] s[], t;
                t = new int[1][1];
                • 0
                  Простите мое невежество конечно, но разве объявление нескольких переменных в одной строке не является дурным тоном, как раз по причине ненужного усложнения для восприятия и как следствие увеличения вероятности ошибок по невнимательности? (данное предложение очевидно является примером такого кода :) )
                  • 0
                    За такие объявления, что в моем примере, действительно нужно наказывать. Но не вижу ничего страшного таком коде — аналог структуры из C (конечно если это внутренний класс, или хотя бы не открытый):
                    class Coords { int x, y; }
          • +1
            Вы слишком категорично написали, что члены класса, объявленные как private не наследуются. Доступа к ним из подкласса действительно нет, но фактически в подклассе они есть. Или я запутался в терминологии (что значит наследуется / не наследуется), или одно из двух.
            Вот, для примера
            public class TestClass 
            {
            	private int var;
            	
            	public TestClass(int var)
            	{
            		this.var = var;
            	}
            	
            	public void printVar()
            	{
            		System.out.println("var = " + var);
            	}
            }
            
            public class TestNewClass extends TestClass
            {
            	private float newVar; 
            	
            	public TestNewClass(int var)
            	{
            		super(var);
            		this.newVar = var * 2.0f;
            	}
            	
            	public void printNewVar()
            	{
            		System.out.println("newVar = " + newVar);
            	}
            }
            
            public static void main(String[] args)
            {
            	TestNewClass tnc = new TestNewClass(8);
            	tnc.printVar();
            	tnc.printNewVar();
            }
            


            исправно выводит
            var = 8
            newVar = 16.0
            • 0
              Вы использовали в выражении формальный параметр конструктора, а не поле суперкласса. Для проверки утверждения вам стоило бы использовать super.var, но это работать не будет т.к. поле приватное.
              • 0
                По всей видимости автор имел в виду, что приватные методы не наследуются. И это действительно так.
                • 0
                  Под наследованием поля я имею в виду явное использование кода суперкласса в потомках. Как вам уже правильно заметили, esin, ваш код использует не поле суперкласса, а параметр, имеющий такое же имя. Кстати, хороший пример на внимательность. Код, получивший модификатор private, может быть выполнен только в рамках того же класса, где он написан. Ни потомки, ни коллеги по пакету его использовать не смогут.
                  • 0
                    Если я правильно помню, то java наследование преобразует в композицию. Таким образом инициация приватных полей родителя выполняется, но потомок его просто не видит.
                    • 0
                      Если вызвать конструктор родителя, то будет инициализация радителя. Нет — нет.
                      • 0
                        А конструктор родителя разве не вызывается при создании экземпляра потомка?
                        • 0
                          Вызывается конструктор по умолчанию. Поля родителя инициализируются или им, или значениями по умолчанию.

                          Можно вызвать super(..) для вызова кастомного конструктора. Только конструктор по умолчанию вызван не будет, а будет вместо него вызван кастомный.
                          • 0
                            Собственно напрашивается вопрос, как тогда можно создать экземпляр потомка, не проинициализировав предка. Не представляю как это возможно.
                    • 0
                      Тонкостей в модификаторах доступа не так уж и много. Те что вы перечислили — весьма очевидны, за исключением поведения protected-членов. Кстати, и this и super — ССЫЛКИ, на текущий класс и на суперкласс соответствено. И только по ним доступны защищённые поля, будь эти ссылки указаны явно или нет.
                      • 0
                        super ссылкой не является, мы можем его использовать для вызова конструктора суперкласса или для доступа к полю/методу, но мы не сможем присвоить super переменной, в отличии от this.

                        Object a = this; //Хорошо
                        Object b = super; //Ошибка компиляции
                      • 0
                        Под наследованием подразумевается также и возможность переопределения (overriding). Если метод не наследуется — следовательно его нельзя переопределить. Поля же нельзя преопределить, будь они хоть public. И на этот нюанс, насколько мне известно, в том экзамене есть ловушки.
                        • 0
                          Ньюанс в том, что поля в наследниках могут быть «скрыты». И на этом действительно можно погореть, потому как в реальной жизни вряд ли хватит ума создавать неприватные переменные, а уж тем более скрывать их в наследниках путем создания переменных с такими же именами.))
                        • 0
                          Да, действительно, я неправ. Оказалось надо было просто обратиться к официальной документации Java.

                          A subclass does not inherit the private members of its parent class. However, if the superclass has public or protected methods for accessing its private fields, these can also be used by the subclass.
                    • 0
                      Было бы интересно узнать, как создать в Java неизменяемый массив. Насколько я знаю, это невозможно, в отличие от коллекций.
                      • 0
                        Огромное спасибо за статью, увидел пару моментов, которых не знал :)
                        Не могли бы Вы чуть подробнее пояснить фразу «Кроме того, будучи однажды унаследованным классом вне пакета, поле становится закрытым для любых классов, за исключением дальнейших наследников.» Как — то я её совсем не понял. Ерунда какая-то. Класс не может знать, унаследован ли он кем-то и уж тем более в зависимости от этого менять модификатор доступа к полю на новый, ещё неведомый науке. Вы явно говорите про что — то другое и я не могу понять про что :)
                        • 0
                          Не за что :) Для этого и пишу. Смотрите, пусть у вас имеется базовый класс в пакете test.

                          1.  
                          2. package org.kimrgrey.scjp.test;
                          3.  
                          4. public class BaseClass {
                          5.         protected int protectedValue;
                          6.        
                          7.         public BaseClass() {
                          8.                 this.protectedValue = 1;
                          9.         }
                          10. }
                          11.  


                          Как я говорил, поле protectedValue будет доступно всем членам пакета, даже если это не наследник.
                          Теперь вы создаете наследника в соседнем пакете — main.

                          1.  
                          2. package org.kimrgrey.scjp.main;
                          3.  
                          4. import org.kimrgrey.scjp.test.BaseClass;
                          5.  
                          6. public class OtherPackageSubclass extends BaseClass {
                          7.         public OtherPackageSubclass() {
                          8.                 this.protectedValue = 2;
                          9.         }
                          10. }
                          11.  


                          Наследник к защищенному полю доступ все еще имеет. Его соседи по пакету — нет. То есть поведение модификатора protected после наследования слегка изменилось. Это больше не доступ в том же пакете + наследники. Это я и имел в виду.

                          При этом есть одна деталь. Классы из пакета test не смогут получить доступ к protected полю по ссылке. Вот такой код отлично работает:

                          1.  
                          2. package org.kimrgrey.scjp.test;
                          3.  
                          4. import org.kimrgrey.scjp.main.OtherPackageSubclass;
                          5.  
                          6. public class SamePackageAccess {
                          7.         public SamePackageAccess() {
                          8.                 OtherPackageSubclass a = new OtherPackageSubclass();
                          9.                 System.out.println(a.protectedValue);
                          10.         }
                          11. }
                          12.  


                          • 0
                            >> Классы из пакета test не смогут получить доступ к protected полю по ссылке

                            Опечатка. Классы из пакета test смогут получить доступ к protected полю по ссылке.
                            • 0
                              Спасибо, теперь понял что Вы имели ввиду, хотя изначальная формулировка простите просто жуть :)
                              А так — то всё логично и понятно если понимать, что protectedValue относится к классу BaseClass и то, что наследник имеет право пользования не делает его хозяином грубо говоря. При наследовании ничего не меняется, поле не становится «закрытым для любых классов». Кто его видел — продолжают видеть, кто не видел, тот продолжает не видеть. Разница только для самого наследника. Потому что это поле по прежнему принадлежит BaseClass, а не наследнику. Можно представить как композицию, в которой ключевое слово extends даёт инкапсулируещему объекту больше прав видеть определённые члены инкапсулируемого :)
                        • 0
                          Поправьте:
                          a.protectedValue = 10; // Line 10: по ссылке не могут обращаться даже наследники BaseClass
                          на
                          a.protectedValue = 10; // Line 12: по ссылке не могут обращаться даже наследники BaseClass
                          • 0
                            Спасибо, поправил. Хотя лучше все таки, на мой взгляд, замечания по опечаткам в ЛС.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.