Pull to refresh

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

Reading time 8 min
Views 56K

Предисловие



Хочу продолжить делиться приобретенными знаниями и своими впечатлениями от подготовки к экзамену. Огромное спасибо всем тем, кто дал рекомендации к нулевой части этой серии! Сегодня я поговорю еще немножко о модификаторах доступа и их взаимоотношениях с наследованием и пакетами, рассмотрю 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, арифметика), а также о неявных классах и потоках.
Tags:
Hubs:
+31
Comments 30
Comments Comments 30

Articles