Вступление
Модификаторы - это ключевые слова, которые позволяют нам точно настроить доступ к нашему классу и его членам, их область действия и поведение в определенных ситуациях. Например, мы можем контролировать, какие классы / объекты могут получить доступ к определенным членам нашего класса, может ли класс быть унаследованным или нет, можем ли мы переопределить метод позже, должны ли мы переопределить метод позже и т. Д.
Ключевые слова-модификаторы записываются перед типом и именем
переменной / метода / класса (возвращаемого значения), например
private int myVar
или public String toString()
.
Модификаторы в Java делятся на две группы - доступ и отсутствие доступа :
- Доступ:
public
,private
,protected
. - Без доступа: статический ,
окончательный ,
абстрактный ,
синхронизированный ,
изменчивый ,
временный и
native
.
native
не рассматривается более подробно ниже, поскольку это простое
ключевое слово, которое отмечает метод, который будет реализован на
других языках, а не на Java. Он работает вместе с Java Native Interface
(JNI). Он используется, когда мы хотим писать критические для
производительности разделы кода на языках, более ориентированных на
производительность (например, C).
Хотите узнать больше о модификаторах доступа , а не о запрете доступа? Если да, ознакомьтесь с нашей статьей Модификаторы доступа в Java .
Модификаторы без доступа
Эти типы модификаторов используются для управления множеством вещей, таких как возможности наследования, независимо от того, имеют ли все объекты нашего класса одно и то же значение члена или имеют свои собственные значения этих членов, может ли метод быть переопределен в подклассе и т. Д.
Краткий обзор этих модификаторов можно найти в следующей таблице:
Название модификатора Обзор
статический Член принадлежит классу, а не объектам этого класса.
окончательный Значения переменных не могут быть изменены после назначения, методы не могут быть переопределены, классы не могут быть унаследованы.
абстрактный Если применяется к методу - должен быть реализован в подклассе, если применяется к классу - содержит абстрактные методы
синхронизированный Управляет доступом потока к блоку / методу.
летучий Значение переменной всегда считывается из основной памяти, а не из памяти конкретного потока.
преходящий Член пропускается при сериализации объекта.
Статический модификатор
static
делает член класса независимым от любого объекта этого класса.
Здесь следует помнить о нескольких особенностях:
- Переменные, объявленные как
static
являются общими для всех объектов класса (поскольку в данном случае переменная по существу принадлежит самому классу), то есть объекты не имеют своих собственных значений для этой переменной, вместо этого все они имеют одно общее значение. - Переменные и методы, объявленные как
static
могут быть доступны через имя класса (вместо обычной ссылки на объект, напримерMyClass.staticMethod()
илиMyClass.staticVariable
), и к ним можно получить доступ без создания экземпляра класса . static
методы могут использовать толькоstatic
переменные и вызывать другиеstatic
методы и никоим образом не могут ссылаться наthis
илиsuper
(экземпляр объекта может даже не существовать, когда мы вызываемstatic
метод, поэтомуthis
не имеет смысла).
Примечание: Очень важно отметить , что static
переменные и методы
не могут получить доступ к ОТСУТСТВИЮ static
(например) переменных и
методы. С другой стороны, static
переменные и методы могут
обращаться к static
переменным и методам.
Это логично, поскольку static
члены существуют даже без объекта этого
класса, тогда как члены экземпляра существуют только после создания
экземпляра класса.
Статические переменные
Для переменных мы используем static
если хотим, чтобы переменная была
общей / общей для всех объектов.
Давайте посмотрим, как static
переменные ведут себя иначе, чем обычные
переменные экземпляра:
class StaticExample {
public static int staticInt = 0;
public int normalInt = 0;
// We'll use this example to show how we can keep track of how many objects
// of our class were created, by changing the shared staticInt variable
public StaticExample() {
staticInt++;
normalInt++;
}
}
// No instances of StaticExample have been created yet
System.out.println(StaticExample.staticInt); // Prints: 0
// System.out.println(StaticExample.normalInt); // this won't work, obviously
// Let's create two instances of StaticExample
StaticExample object1 = new StaticExample();
// We can refer to static variables via an object reference as well,
// however this is not common practice, we usually access them via class name
// to make it obvious that a variable/method is static
System.out.println(object1.staticInt); // Prints: 1
System.out.println(object1.normalInt); // Prints: 1
StaticExample object2 = new StaticExample();
System.out.println(object2.staticInt); // Prints: 2
System.out.println(object2.normalInt); // Prints: 1
// We can see that increasing object2's staticInt
// increases it for object1 (and all current or future objects of that class)
object1.staticInt = 10;
object1.normalInt = 10;
System.out.println(object2.staticInt); // Prints: 10
System.out.println(object2.normalInt); // Prints: 1 (object2 retained its own value for normalInt as it depends on the class itself)
Статические методы
Наиболее распространенным примером использования static
является метод
main()
, он объявлен как static
потому что его нужно вызывать до
того, как появятся какие-либо объекты. Другим распространенным примером
является Math
поскольку мы используем методы этого класса, не создавая
сначала его экземпляр (например, Math.abs()
).
Хороший способ подумать о static
методах: «Имеет ли смысл использовать
этот метод без предварительного создания объекта этого класса?»
(например, вам не нужно создавать экземпляр Math
, чтобы вычислить
абсолютное значение числа).
Статические методы могут использоваться для доступа и изменения static
членов класса. Хотя они обычно используются для управления параметрами
метода или вычисления чего-либо и возврата значения.
Эти методы называются служебными методами:
static int average(int num1, int num2) {
return (num1+num2)/2;
}
Этот служебный метод можно использовать, например, для вычисления среднего двух чисел.
Как упоминалось выше, Math
часто используется для вызова static
методов. Если мы посмотрим на исходный код, мы можем заметить, что он в
основном предлагает служебные методы:
public static int abs(int i) {
return (i < 0) ? -i : i;
}
public static int min(int a, int b) {
return (a < b) ? a : b;
}
public static int max(int a, int b) {
return (a > b) ? a : b;
}
Статические блоки
Также есть static
блок. static
блок выполняется только один раз,
когда класс создается впервые (или static
член, даже если класс не
создан) и перед остальной частью кода.
Давайте добавим static
блок в наш класс StaticExample
class StaticExample() {
...
static {
System.out.println("Static block");
}
...
}
StaticExample object1 = new StaticExample(); // "Static block" is printed
StaticExample object2 = new StaticExample(); // Nothing is printed
Независимо от их положения в классе static
блоки инициализируются
перед любыми другими нестатическими блоками, включая конструкторы:
class StaticExample() {
public StaticExample() {
System.out.println("Hello from the constructor!");
}
static {
System.out.println("Hello from a static block!");
}
}
Создание экземпляра этого класса приведет к выводу:
Hello from a static block!
Hello from the constructor!
Если присутствует несколько static
блоков, они будут выполняться в
соответствующем порядке:
public class StaticExample {
static {
System.out.println("Hello from the static block! 1");
}
public StaticExample() {
System.out.println("Hello from the constructor!");
}
static {
System.out.println("Hello from the static block! 2");
}
}
Создание экземпляра этого класса приведет к выводу:
Hello from the static block! 1
Hello from the static block! 2
Hello from the constructor!
Статический импорт
Как уже упоминалось, лучше вызывать static
члены с префиксом имени
класса, а не имени экземпляра. Кроме того, в некоторых случаях мы
никогда не создаем экземпляр класса со static
методами, такими как
Math
, который предлагает множество служебных методов, касающихся
математики.
При этом, если мы static
члены класса, мы можем импортировать
отдельные члены или все из них, используя static import
. Это
позволяет нам пропустить префикс их вызовов с именем класса:
package packageOne;
public class ClassOne {
static public int i;
static public int j;
static public void hello() {
System.out.println("Hello World!");
}
}
package packageTwo;
static import packageOne.ClassOne.i;
public class ClassTwo {
public ClassTwo() {
i = 20;
}
}
Или, если мы хотим импортировать все static
члены ClassOne
, мы
могли бы сделать это так:
package packageTwo;
import static packageOne.ClassOne.*;
public class ClassTwo {
public ClassTwo() {
i = 20;
j = 10;
}
}
То же касается и методов:
package packageTwo;
import static packageOne.ClassOne.*;
public class ClassTwo {
public ClassTwo() {
hello();
}
}
Запуск этого приведет к выводу:
Hello World!
Это может показаться не таким важным, но это помогает, когда мы вызываем
много static
членов класса:
public int someFormula(int num1, int num2, int num3) {
return Math.ceil(Math.max(Math.abs(num1), Math.abs(num2))+Math.max(Math.abs(num2), Math.abs(num3)))/(Math.min(Math.abs(num1), Math.abs(num2))+Math.min(Math.abs(num2), Math.abs(num3)));
}
// Versus...
import static java.lang.Math.*;
public int someFormula(int num1, int num2, int num3) {
return ceil(max(abs(num1), abs(num2))+max(abs(num2), abs(num3)))/(min(abs(num1), abs(num2))+min(abs(num2), abs(num3)));
}
Последний модификатор
Ключевое слово final
может иметь одно из трех значений:
- для определения именованных констант (переменных, значения которых не могут измениться после инициализации)
- чтобы предотвратить переопределение метода
- чтобы предотвратить наследование класса
Именованные константы
Добавление final
модификатора к объявлению переменной делает эту
переменную неизменной после ее инициализации.
final
модификатор часто используется вместе со static
модификатором,
если мы определяем константы. Если мы применим только static
к
переменной, ее все равно можно будет легко изменить. С этим также
связано соглашение об именах:
static final double GRAVITATIONAL_ACCELERATION = 9.81;
Такие переменные часто включаются в служебные классы, такие как Math
,
вместе с многочисленными служебными методами.
Хотя в некоторых случаях они также гарантируют свои собственные классы,
такие как Constants.java
:
public static final float LEARNING_RATE = 0.3f;
public static final float MOMENTUM = 0.6f;
public static final int ITERATIONS = 10000;
Примечание : при использовании final
с переменными ссылки на
объект будьте осторожны с ожидаемым типом поведения. Учтите следующее:
class MyClass {
int a;
int b;
public MyClass() {
a = 2;
b = 3;
}
}
final MyClass object1 = new MyClass();
MyClass object2 = new MyClass();
object1
переменная object1 действительно является final
и ее
значение не может измениться, но что это значит для ссылочных переменных
в любом случае? Это означает, что object1
больше не может изменить
объект, на который он указывает, но мы можем изменить сам объект. Это
то, что часто сбивает с толку:
// object1 = object2; // Illegal!
object1.a = 5; // Perfectly fine
Параметры метода также можно объявить final
. Это используется, чтобы
убедиться, что наш метод не изменяет параметр, который он получает при
вызове.
Локальные переменные также могут быть объявлены final
. Это
используется для того, чтобы переменная получила значение только один
раз.
Предотвращение переопределения
Если вы укажете final
при определении метода, любой будущий подкласс
не сможет его переопределить.
class FinalExample {
final void printSomething() {
System.out.println("Something");
}
}
class ExtendsFinalExample extends FinalExample {
// This would cause a compile-time error
//void printSomething() {
// System.out.println("Some other thing");
//}
// However, you are perfectly free to overload this method
void printSomething(String something) {
System.out.println(something);
}
}
Небольшой бонус объявления действительно final методов как final
-
небольшое повышение производительности всякий раз, когда мы вызываем
этот метод. Обычно Java разрешает вызовы методов динамически во время
выполнения, но с методами, объявленными final
, Java может разрешить
вызов к нему во время компиляции, или, если метод действительно мал, он
может просто встроить вызовы этого метода, поскольку он «знает», что это
не будет отменено. Это устраняет накладные расходы, связанные с вызовом
метода.
Предотвращение наследования
Это использование final
довольно прямолинейно, класс, определенный с
final
не может быть унаследован. Это, конечно же, неявно объявляет все
методы этого класса final (они не могут быть переопределены, если класс
не может быть унаследован с самого начала).
final class FinalExample {...}
Абстрактный модификатор
abstract
используется для определения методов, которые позже будут
реализованы в подклассе. Чаще всего он используется для предположить ,
что некоторые функции должны быть реализованы в подклассе, или (по
некоторым причинам) не может быть реализована в суперкласса. Если класс
содержит abstract
метод, он также должен быть объявлен abstract
.
Примечание : вы не можете создать объект abstract
класса. Для
этого вам необходимо предоставить реализацию для всех abstract
методов.
Примером может служить простой класс под названием Employee
который
инкапсулирует данные и методы для сотрудника. Предположим, что не все
сотрудники получают одинаковую оплату, некоторые типы сотрудников
получают почасовую оплату, а некоторые получают фиксированную заработную
плату.
abstract class Employee {
int totalHours; // In a month
int perHour; // Payment per hour
int fixedRate; // Fixed monthly rate
...
abstract int salary();
...
}
class Contractor extends Employee {
...
// Must override salary if we wish to create an object of this class
int salary() {
return totalHours*perHour;
}
...
}
class FullTimeEmployee extends Employee {
...
int salary() {
return fixedRate;
}
...
}
class Intern extends Employee {
...
int salary() {
return 0;
}
...
}
Если подкласс не обеспечивает реализацию всех abstract
методов в
суперклассе, он также должен быть объявлен как abstract
, и объект
этого класса не может быть создан.
Примечание : abstract
широко используется с полиморфизмом,
например, мы бы сказали
ArrayList<Employee> employees = new ArrayList();
, и добавьте к нему
объекты Contractor
, FullTimeEmployee
и Intern
Несмотря на то, что
мы не можем создать объект Employee
, мы все равно можем использовать
его как ссылочный тип переменной.
Синхронизированный модификатор
Когда двум или более потокам необходимо использовать один и тот же ресурс, нам нужно каким-то образом убедиться, что только один из них имеет доступ к нему одновременно, т.е. нам нужно синхронизировать их.
Этого можно достичь несколькими способами, и один простой и понятный
способ (хотя и с несколько ограниченным использованием) - использовать
ключевое слово synchronized
Прежде чем вы узнаете, как использовать это ключевое слово, важно понять
концепцию монитора. Каждый объект в Java имеет свой собственный неявный
монитор, связанный с ним. Монитор - это «взаимоисключающая» блокировка,
означающая, что только один поток может «владеть» монитором
одновременно. Когда поток входит в монитор, никакой другой поток не
может войти в него, пока не завершится первый поток. Это то, что делает
synchronized
Потоки выходят за рамки этой статьи, поэтому я сосредоточусь только на
синтаксисе synchronized
.
Мы можем синхронизировать доступ к методам и блокам кода. Синхронизация блоков кода работает путем предоставления экземпляра объекта, доступ к которому мы хотим синхронизировать, и кода, который мы хотим выполнить, связанного с этим объектом.
class SynchronizedExample {
...
SomeClass object = new SomeClass();
....
synchronized(object) {
// Code that processes objects
// only one thread at a time
}
// A synchronized method
synchronized void doSomething() {
...
}
...
}
Летучий модификатор
volatile
сообщает Java, что переменная может быть неожиданно изменена
какой-либо другой частью программы (например, в многопоточном
программировании), и поэтому значение этой переменной всегда считывается
из основной памяти (а не из кеша ЦП), и что каждое изменение volatile
переменная хранится в основной памяти (а не в кэше ЦП). Имея это в виду,
volatile
следует использовать только при необходимости, поскольку
чтение / запись в память каждый раз дороже, чем с кешем ЦП, и только
чтение / запись в память только при необходимости.
Проще говоря, когда поток считывает значение volatile
переменной,
гарантируется, что он прочитает последнее записанное значение. По сути,
volatile
переменная делает то же самое, что и synchronized
методы /
блоки, мы просто не можем объявить переменную synchronized
.
Переходный модификатор
Когда переменная объявлена как transient
, это означает, что ее
значение не сохраняется, когда объект сохраняется в памяти.
transient int a;
означает, что когда мы записываем объект в память,
содержимое «a» не будет включено. Например, он используется, чтобы
убедиться, что мы не храним личную / конфиденциальную информацию в
файле.
Когда мы пытаемся прочитать объект, содержащий transient
переменные,
все transient
переменных будут установлены на null
(или значения по
умолчанию для примитивных типов), независимо от того, какими они были,
когда мы записали объект в файл. Другой пример использования - это когда
значение переменной должно быть получено на основе других данных
(например, чей-то текущий возраст) и не является частью постоянного
состояния объекта.
Примечание : что-то очень интересное происходит, когда мы используем
transient
и final
моменты вместе. Если у нас есть transient final
переменная, которая оценивается как постоянное выражение (строки или
примитивные типы), JVM всегда будет сериализовать ее, игнорируя любой
потенциальный модификатор transient
Когда transient final
используется со ссылочными переменными, мы получаем ожидаемое поведение
transient
умолчанию.
Заключение
Модификаторы - это ключевые слова, которые позволяют нам точно настроить доступ к нашему классу и его членам, их область действия и поведение в определенных ситуациях. Они обеспечивают фундаментальные качества для наших классов и их членов. Каждый разработчик должен быть с ними внимательно ознакомлен, чтобы использовать их наилучшим образом.
Например, осознание того, что protected
контроль доступа можно легко
обойти, или transient final
модификатор final, когда дело доходит до
константных выражений.