Заметки о модификаторах доступа, аннотациях
Модификаторы доступа
Модификатор - это ключевое слово языка, которое может каким-либо образом изменить смысл некоторого определения (например, класса или метода). Среди всех модификаторов отдельно выделяют группу модификаторов доступа:
- private члены класса доступны только внутри класса
- package-private или default (по умолчанию) члены класса видны внутри пакета
- protected члены класса доступны внутри пакета и в классах-наследниках
- public члены класса доступны всем
Во время наследования возможно изменение модификаторов доступа в сторону БОЛЬШЕЙ видимости.
Модификатор доступа у конструкторов, методов и полей может быть любой, а вот с классами и их блоками не так все просто. Класс может быть только либо public, либо default, причем в одном файле может находиться только один public класс. У блока может быть только один модификатор – default.
Модификаторы static, abstract и final
Static
- Применяется к внутренним классам, методам, переменным и логическим блокам
- Статические переменные инициализируются во время загрузки класса
- Статические переменные едины для всех объектов класса (одинаковая ссылка)
- Статические методы имеют доступ только к статическим переменным
- К статическим методам и переменным можно обращаться через имя класса
- Статические блоки выполняются во время загрузки класса
- Не static методы не могут быть переопределены как static
- Локальные переменные не могут быть объявлены как static
- Абстрактные методы не могут быть static
- Static поля не сериализуются (только при реализации интерфейса Serializable)
- Только static переменные класса могут быть переданы в конструктор с параметрами, вызывающийся через слово super(//параметр//) или this(//параметр//)
Abstract
- Применяется только для методов и классов
- У абстрактных методов нет тела метода
- Является противоположностью final: final класс не может наследоваться, abstract класс обязан наследоваться
- Класс должен быть объявлен как abstract если:
- он содержит хотя бы один абстрактный метод
- он не предоставляет реализацию наследуемых абстрактных методов
- он не предоставляет реализацию методов интерфейса, реализацию которого он объявил
необходимо запретить создание экземпляров класса
Final
- Поля не могут быть изменены, методы переопределены
- Классы нельзя наследовать
- Этот модификатор применяется только к классам, методам и переменным (также и к локальным переменным)
- Аргументы методов, обозначенные как final, предназначены только для чтения, при попытке изменения будет ошибка компиляции
- Переменные final не инициализируются по умолчанию, им необходимо явно присвоить значение при объявлении или в конструкторе, иначе – ошибка компиляции
- Если final переменная содержит ссылку на объект, объект может быть изменен, но переменная всегда будет ссылаться на тот же самый объект
- Также это справедливо и для массивов, потому что массивы являются объектами, – массив может быть изменен, а переменная всегда будет ссылаться на тот же самый массив
- Если класс объявлен final и abstract (взаимоисключающие понятия), произойдет ошибка компиляции
- Так как final класс не может наследоваться, его методы никогда не могут быть переопределены
Конструктор не может быть static, abstract или final
Модификаторы strictfp, transient, volatile, synchronized, native
Strictfp
- Применяется для методов и классов
- Обеспечивает выполнение операций над числами типа float и double (с плавающей запятой) по стандарту IEEE 754
Transient
- Применяется только для переменных уровня класса (локальные переменные не могут быть объявлены как transient)
- Transient-переменные могут не быть final или static.
- Transient-переменные не сериализуются
Volatile
- Используется только с переменными
- Может использоваться со static переменными
- Не используется с final переменными - Значение переменной, объявленной как volatile, измененное одним потоком, асинхронно меняется и для других потоков
- Применяется в многопоточных приложениях
Synchronized
- Применяется только к методам или частям методов
- Используется для контроля доступа к важным частями кода в многопоточных программах
Native
- Используется только для методов
- Обозначает, что метод написан на другом языке программирования
- Классы в Java используют много native методов для повышения производительности и доступа к аппаратным средствам
- Можно предавать/возвращать Java объекты из native методов
- Сигнатура метода должна заканчиваться “;”, фигурные скобки вызовут ошибку компиляции
Особенности в интерфейсах
- Методы всегда public и abstract, даже если это не объявлено
- Методы не могут быть static, final, strictfp, native, private, protected
- Переменные только public static final, даже если это не объявлено
- Переменные не могут быть strictfp, native, private, protected
- Может только наследовать (extends) другой интерфейс, но не реализовывать интерфейс или класс (implements).
Модификаторы классов (class modifiers)
В контексте класса используются модификаторы abstract, final, static, strictfp. О модификаторе strictfp знать не критично. Если интересно - читайте о нем в конце статьи. Модификатор abstract, примененный к классу, говорит о том, что класс является (или считается) незаконченным, а задание "завершить" класс возлагается на наследников. Попытка инстанциировать такой класс приведет к ошибке компиляции, например:
public abstract class Expression {
Expression e = new Expression();
}
В результате компиляции этого кода получим ошибку: Expression is abstract; cannot be instantiated.
Обратите внимание, у абстрактного класса не обязательно должны быть абстрактные методы.
Модификатор final у класса говорит о том, что от него нельзя наследоваться.
public final class Example {
class Subclass extends Example { }
}
Попытка компиляции кода приведет к ошибке: cannot inherit from final Example.
Очевидно, класс нельзя объявить одновременно final и abstract.
Вложенные классы в Java могут быть объявлены как static, например:
public class Outer {
static class Inner { }
}
В этом случае класс называется статическим вложенным классом и имеет доступ к статическим полям и методам обрамляющего класса.
Модификаторы методов (method modifiers)
С методами вариантов немного больше. Методы в Java могут быть объявлены как abstract, final, static, strictfp, native, synchronized. Метод с модификатором abstract может быть объявлен как метод-член (member method) в пределах абстрактного класса (или интерфейса). В этом случае тело метода отсутствует, а реализация может быть предоставлена в классах-наследниках. Если же метод объявлен как абстрактный в конкретном классе, то получим ошибку компиляции. Метод, объявленный с модификатором final не может быть переопределен в наследниках.
- Метод с модификатором static относится к классу в целом, а не к его экземплярам, то есть в него не передается объект this. Такой метод может быть вызван используя имя класса.
- Модификатор native перед объявлением метода указывает, что он явялется специфическим для операционной системы. Как и у абстрактного метода, у него тоже нет тела, а реализация находится в скомпилированном виде в файлах JVM. Примеры объявления таких методов можно найти,например, в классе java.io.FileInputStream:
private native void open(String name) throws FileNotFoundException;
private native int readBytes(byte b[], int off, int len) throws IOException;
- Наконец, модификатор synchronized у метода говорит о том, что перед его выполнением должен быть захвачен монитор объекта (для нестатического метода), либо монитор, связанный с классом (для статического метода).
Вот небольшой пример кода, который демонстрирует описание синхронизированных методов. Захват монитора так же осуществляется с помощью ключевого слова synchronized.
public class Synch {
public synchronized void a() { }
public static synchronized void b() { }
final static Synch s = new Synch();
public static void main(String[] args) {
synchronized (s) {
// lock on object
}
synchronized (Synch.class) {
// lock on class
}
}
}
Модификаторы полей (field modifiers)
Перейдем к полям классов. Они могут быть описаны с такими модификаторами как static, final, transient, volatile.
- Если поле класса объявлено как static, то будет существовать ровно одно значение этого поля, не зависимо от того, сколько экземпляров класса будет создано, даже если не будет создано ни одного экземпляра. Такие статические поля еще называют переменными уровня класса (class variable). Поле с модификатором final не может поменять своего значения после инициализации. Это касается и статических, и нестатических полей (member fields).
public class Sample {
final static int constant = 5;
public static void main(String[] args) {
Sample.constant = 0;
}
}
Компиляция этого кода приведет к ошибке: cannot assign a value to final variable constant.
- Для указания того, что во время сериализации объекта некоторое поле нужно игнорировать, используется модификатор transient. Обычно такие поля не являются частью внутреннего состояния объекта, либо хранят промежуточные значения.
- С модификатором volatile все немного посложнее. Попытаюсь объяснить попроще. В Java потоки могут хранить значения некоторых переменных в некотором своем локальном контексте. Если один поток изменит значение такого поля, то другой поток может об этом не узнать (так как хранит копию). Для полей с модификатором volatile есть гарантия, что все потоки всегда будут видеть актуальное значение такой переменной.
Модификаторы, связанные с интерфейсами
К интерфейсам можно применить те же модификаторы, что и к классам, естественно кроме final. Интерфейс является abstract по-умолчанию. В случае со вложенным интерфейсом ключевое слово static можно не указывать - он в любом случае будет статическим. Методы интерфейсов по-умлочанию являются public abstract, поэтому к ним не применимы модификаторы final, static и native. Синхронизированными они тоже быть не могут, так как интерфейс нельзя инстанциировать. Поля интерфейсов по-умолчанию являются public static final, а значит должны быть проинициализированы.
Ну и default - последняя разработка в области модификаторов :). Метод интерфейса, помеченный как default, предоставляет реализацию этого метода по-умолчанию. Например:
public interface Test {
void method1();
default void method2() { }
}
Этот код скомпилируется без ошибок на Java версии 1.8.
Другие контексты использования модификаторов
Есть еще 2 варианта, где могут использоваться модификаторы. Первый вариант - это локальные переменные, которые могут быть объявлены как final. В этом случае значение переменной нельзя будет изменить в в пределах метода после инициализации Второй вариант - это статический блок инициализации, который выполняется при загрузке класса:
public class Test {
static int i;
static {
i = 5;
}
}
Ну и наконец, перейдем к модификатору strictfp. Модификатор strictfp для класса и интерфейса указывает на то, что все методы класса/интерфейса будут strictfp. Ну а если метод описан как strictfp (явно либо неявно), то JVM гарантирует, что результаты вычисления выражений с double и float в пределах метода будут одинаковыми на всех платформах. Данный модификатор был введен в версии Java 1.2. Но сейчас все современные компиляторы сами проставляют этот модификатор, так что по поводу переносимости ваших программ можете быть спокойны.
Аннотации
Аннотации представляют собой некие метаданные, которые могут добавлятся в исходный код программы и семантически не влияют на нее, но могут использоваться в процессе анализа кода, компиляции и даже во время выполнения. Вот основные варианты использования аннтоаций: предоставлять необходимую информацию для компилятора; предоставлять метаданные различным инструментам для генерации кода, конфигураций и т.д.; использоваться в коде во время выполнения програмного кода (reflection). Аннотации могут быть применены, например, к декларациям классов, полей, методов, ну и конечно же аннотаций :). Для описания новой аннотации используется ключевое слово @interface. Вот банальный пример аннотации:
public @interface Description {
String title();
int version() default 1;
String text();
}
И пример ее использования:
@Description(title="title", version=2, text="text")
public class Clazz { /* */ }
Сразу хочу обратить ваше внимание - в качестве типов у элементов аннотации могут использоваться только примитивные типы, перечисления и класс String. Если у аннотации нет элементов, ее называют маркером (marker annotation type). В этом случае при использовании аннотации круглые скобки можно не писать.
В случае, когда аннтоация указывается для другой аннотации, первую называют мета-аннотацией (meta-annotation type). Достаточно часто вам придется сталкиваться с мета-аннтоацией Retention. Она показывает, как долго необходимо хранить аннтоацию и инициализируется одним из трех значений:
- RetentionPolicy.SOURCE - аннотация используется на этапе компиляции и должна отбрасываться компилятором;
- RetentionPolicy.CLASS - аннтоация будет записана в class-файл компилятором, но не должна быть доступна во время выполнения (runtime);
- RetentionPolicy.RUNTIME - аннотация будет записана в class-файл и доступна во время выполнения через reflection.
Тут есть еще одна вещь, на которую хочу обратить ваше внимание: по умолчанию у всех аннотаций стоит RetentionPolicy.CLASS. Это мне кажется недодумкой. В исходниках JDK очень часто используется эта policy, но вот в разработке нужна именно RetentionPolicy.RUNTIME. К сожалению, ничего уже не поменяется из-за обратной совместимости.
Примеры использования аннотаций
Предположим, нам нужно ограничить доступ к некоторым функциям веб-приложения для разных пользователей. Иными словами необходимо реализовать права (permissions). Для этого можно добавить следующее перечисление в класс пользователя:
public class User {
public static enum Permission {
USER_MANAGEMENT, CONTENT_MANAGEMENT
}
private List<Permission> permissions;
public List<Permission> getPermissions() {
return new ArrayList<Permission>(permissions);
}
// ...
}
Создадим аннтоацию, которую затем будем использовать для проверки прав:
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionRequired {
User.Permission value();
}
Теперь предположим у нас есть некоторое действие, право на выполнение которого мы хотим ограничить, например, UserDeleteAction. Мы добавляем аннтоацию на это действие следующим образом:
@PermissionRequired(User.Permission.USER_MANAGEMENT)
public class UserDeleteAction {
public void invoke(User user) { /* */ }
}
Теперь используя reflection можно принимать решение, разрешать или не разрешать выполнение определенного действия:
User user = ...;
Class<?> actionClass = ...;
PermissionRequired permissionRequired =
actionClass.getAnnotation(PermissionRequired.class);
if (permissionRequired != null)
if (user != null && user.getPermissions().contains(permissionRequired.value()))
// выполнить действие