В Java
мы оперируем такими понятиями, как Класс
и instance
класса - объект.
Класс
- это описание методов и свойств Объекта
, а Объект
- это уже инстанс класса, сущность.
Можно сказать, что класс - это как техническое описание прибора, купленного вами в электронном дискаунтере. То, каким он должен быть: материал, форма, список составляющих и т.д.
Объект же - это сам прибор, со своим уникальным набором свойств.
Например, у него может быть цвет, материал изготовления, форма, он может состоять также из более мелких узлов(вспомогательных приборов) и т.д
Все это является свойствами объекта, они уникальны и для каждого прибора они свои.
Работая с объектом мы используем эти свойства, изменяем их и реагируем на поведение объекта.
Для примера можно взять обычный массив. Массив, являясь объектом, может сохранять(добавлять) элемент, удалять и т.д. Это его поведение. В то же время, у массива есть еще и длина - это уже свойство объекта. Добавляя или удаляя элементы вы изменяете и длину массива.
При этом, все эти изменения затрагивают только тот массив, с которым мы работаем сейчас и никак не влияют на другие.
Однако иногда возникают ситуации, когда необходимо, чтобы некоторое поле или метод мы могли использовать либо без создания объекта, либо чтобы это свойство или метод было общим для всех объектов.
В качестве примера, рассмотрим метод, который печатает в консоль число, которое передается аргументом в этот метод:
public class Printer {
void print(int n) {
System.out.println(n);
}
}
Если мы напишем такой код, то для каждого использования этого метода будет необходим объект класса Printer
.
При этом объект по сути то и не нужен, так как все что необходимо для работы передается в аргументах методу, т.е контекст выполняемой операции полностью доступен из аргументов метода и результат не влияет на состояние объекта.
Т.е такой метод не связан с объектом класса, он связан только с классом, в котором объявлен.
А раз он не связан с объектом, то и объект, для использования этого метода, не нужен. Нужен только класс, в котором объявлен этот метод.
И вот тут как раз в дело вступает модификатор static
.
Указав модификатор static
у поля или метода класса, вы тем самым говорите: это поле или метод принадлежат именно классу.
Если поле или метод принадлежит классу, то для всех объектов класса это поле или метод будет одно, так как оно уже не принадлежит объекту. При этом изменение этого поля у одного объекта влечет его изменение у всех объектов.
Грубо говоря, если вы объявляете поле класса статическим, то вы как бы расшариваете это поле для всех объектов.
Представьте себе документ, который редактирует несколько пользователей. Если один из пользователй изменяет какое-то слово или абзац, то документ меняется у всех пользователей.
Из того, что static
-члены принадлежат именно классу следует еще и то, что из статических методов нельзя получить доступ к нестатическим членам класса, т.е к тому, что принадлежит объекту.
public class Counter {
private int count;
public static void printCount() {
System.out.println(count); //compile time error
}
}
Это довольно логично, так как экземпляра объекта класса может еще не быть, поэтому доступа до переменной count
нет.
Вообще говоря,
static
крайне плохо ложится наООП
парадигму и в идеале надо стараться свести использованиеstatic
методов и изменяемых переменных к минимуму.
У вас должен возникнуть вопрос: "А где вообще можно использовать static
?"
Ключевое слово static
может быть использовано при объявлении:
- Поля класса
- Метода класса
- Блока кода
- Вложенного класса
Наиболее часто встречаемое применение - это первые два пункта: поля и методы.
Статические методы принадлежат классу, а значит, для их использования не нужно создавать экземпляр объекта класса.
Чаще всего можно задуматься о том, чтобы сделать метод статическим только в том случае, если весь контекст для его работы передается в аргументах.
Пример: Math.abs(-20)
, Math.sqrt(144)
.
Для того, чтобы извлечь корень из числа, вам не нужно ничего, кроме самого числа. То же самое и с возведением в степень.
Контекст операции в данном случае - это число. Оно передается в аргументах и поэтому все, что нам нужно для работы уже доступно, мы никак не влияем на состояние объекта и нигде его не сохраняем.
Если же для работы статического метода передаваемых ему аргументов недостаточно, то это верный признак того, что стоит крепко задуматься: "А должен ли он вообще быть статическим?".
Очень часто статические методы, принадлежащие к одной области работы, группируются в один класс, образуя так называемые Utility
-классы.
Такие классы обычно имеют в своем имени слова типа Utils
, тем самым намекая на то, что это класс для утилит.
Это классы без состояния, stateless
, имеющие только статические методы.
Хороший пример такого подхода - FileUtils
, который есть в большом количестве библиотек, Apache Commons IO
в их числе.
Область работы такого класса - это работа с файлами, если заглянуть внутрь мы обнаружим методы, наподобие boolean exists(File file)
, void writeToFile(List<String> data)
и т.д.
В идеале такой класс является еще и финальным - final
, так как не к чему позволять таким классам возможность участвовать в наследовании.
Статические методы применяются также при использовании factory
и builder
паттернов.
Подробнее об этом в Паттерны.
Статические методы часто используются еще и для создания экземпляров объектов, в зависимости от каких-то условий, для добавления промежуточной логики в создание объекта и т.д
Например: String.valueOf(15)
, Integer.valueOf("14")
.
Т.е это некоторые factory
-методы, которые помогают нам создавать объекты.
Обычно внутри таких методов добавляется еще какая-то логика или оптимизация.
В частности, статические методы создания
valueOf()
у классов-оболочек примитивов, кроме чисел с плавающей точкой, имеют кеш. По умолчанию данный кеш содержит значения от-128
до127
, если значение попадает в кеш - оно достается в разы быстрее, чем при использовании конструктора, при этом не расходууется лишняя память.Наиболее часто используемые значения также кешируются.
Поэтому, если есть возможность, то лучше использовать такие factory
-методы.
Например, предпочтительней использовать Integer.valueOf("14")
, вместо new Integer(14)
.
Так как статическое поле принадлежит классу, то оно не является уникальным для каждого экземпляра - оно общее для всех экземпляров.
Это значит, что статические поля, например, можно использовать для создания примитивных счетчиков(counter
-ов).
Представим для примера, что мы хотим посчитать количество созданных экземпляров класса:
class Person {
private static int count = 0;
Person() {
count++;
}
public static int getCount() {
return count;
}
}
//some code
public static void main(String[] args) {
new Person();
new Person();
new Person();
new Person();
System.out.println(Person.getCount());
}
Выведет нам 4
.
Также статическое поле используется внутри Singleton
-паттерна.
Подробнее о Singleton.
Но в основном статические поля используются для создания констант:
final class FileUtils {
public static final char SEPARATOR = ';';
public static final int BATCH_SIZE = ';';
}
Тут, я думаю, все понятно - константа и в Африке константа.
Про константы и их оформление можно прочесть вот тут.
Инициализация статического блока кода выполняется на этапе загрузки класса, грубо говоря, в момент первого к нему обращения. Благодаря этому статические блоки кода используются тогда, когда необходимо выполнить какую-то логику еще до создания экземпляра объекта.
С небольшой оговоркой можно считать, что это конструктор для всего класса.
Как уже было сказано выше, статический блок выполняется на этапе загрузки класса и выполняется только один раз.
Выглядит синтаксис статического блока вот так:
static {
// Static block code
}
Статических блоков может быть несколько, их выполнение будет происходить в порядке объявления.
Статические блоки можно использовать для инициализации статических полей, например:
public class Car {
static Map<String, Set<Car>> catalog = new HashMap<String, Set<Car>>();
static {
catalog.put("model105", new HashSet<Car>());
catalog.put("model125", new HashSet<Car>());
catalog.put("model140", new HashSet<Car>());
catalog.put("model201", new HashSet<Car>());
}
public Car (String model) {
catalog.get(model).add(this);
// ...
}
// ...
}
Т.е у нас есть некоторый каталог и есть данные для заполнения.
Мы можем инициализацию вынести в конструктор, но тогда при каждом создании экземпляра объекта класса будет происходить заполнение каталога, а нам надо, чтобы каталог был заполнен только один раз - так как он принадлежит классу, он общий для всех объектов.
Для этого мы и использовали статический блок кода, и он выполнился ровно один раз при иницализации класса, тем самым мы один раз заполнили наш каталог и далее можем его использовать.
Подобный подход используется еще и при работе с базами данных в Java
, например, в JDBC, когда надо загрузить драйвер для работы с конкретной БД
.
Так вот, загрузку драйвера как раз делают в статическом блоке:
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
Подробнее о порядке выполнения кода при инициализации класса: Порядок инициализации полей класса в Java.
Однако помните! Статичный блок НЕ может пробросить перехваченные исключения, но может выбросить не перехваченные. В случае возникновения исключения в статическом блоке кода выбросится
ExceptionInInitializerError
.
В Java
можно объявить класс внутри другого класса.
Такой вложенный класс называется nested
-классом.
Вложенные классы делятся на статические и нестатические.
Нестатические вложенные классы называют еще внутренними классами - inner
-классами.
Класс, внутри которого объявлен другой класс, назовем обрамляющим или outer
-классом.
Для иллюстрации вышесказанного:
publuc class OuterClass {
static NestedClass {
}
class InnerClass {
}
}
Понятно, что nested
класссы принадлежат outer
классу, в то время как inner
классы принадлежат уже экземпляру объекта класса.
Зачем же вообще нужны nested
классы?
Например, вы пишите реализацию связного списка.
Вам нужен класс Node
, в котором вы будете хранить значение и ссылки на предыдущий и следующий элемент списка.
Этот класс будет использоваться только внутри вашей реализации, нигде больше он не нужен.
Логично, что исходя из этого делать отдельный public
класс излишне.
Объявив же в такой ситуации вложенный класс, вы добьетесь сразу нескольких преимуществ:
-
Логически сгруппируете классы, которые используются в одном месте
Если класс полезен только для одного стороннего класса, то логично ввести его в этот класс и поддерживать вместе.
-
Это способ привести код к более читабельному и поддерживаемому виду.
Достигается это тем, что вы можете объявить вложенный класс ближе к месту использования и скрыть его от использования в других классах.
-
Это увеличивает инкапсуляцию.
Если вложенный класс используется только внутри стороннего класса, логично скрыть его и использовать лишь внутри. Аналогично с тем, как мы объявляем
private
поля.
Если посмотреть стандартную реализацию java.util.LinkedHashMap
в Java 8
, то там именно так и сделано:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
// some code and implementation.
}
Подробнее о вложенных классах: Вложенные классы в Java.
Если коротко, то раз поле или метод принадлежит классу, то и обращаться к нему необходимо через имя класса.
Если нужны подробности, то давайте посмотрим на следующий класс, содержащий статическое поле:
public class Example {
public static int field = 14;
}
Мы хотим обратиться к этому полю, и возникает вопрос: как правильно это сделать?
Есть два рабочих варианта:
- Обратиться к полю через экземпляр объекта класса.
- Обратиться к полю через класс.
public class ExampleTest {
public static void main(String[] args) {
System.out.println(new Example().field); // способ 1
System.out.println(Example.field); // способ 2
}
}
Так вот, сразу скажу: забудьте про первый вариант. Да, он рабочий, но делать так не стоит. Более того, это глупо!
Вспомните, что статические поля принадлежат не экземпляру объекта класса, а самому классу. Так зачем вам объект?
Для закрепления приведу следующий пример:
Возьмем класс, описанный выше, и ответим на вопрос: что будет, если мы выполним следующий код:
Example ex = null;
System.out.println(ex.field);
Если вы ответили, что будет исключение NullPointerException
, то прочтите еще раз статью.
Мы не увидим NullPointerException
, так как вместо ex
в месте обращения к статической переменной field
умный компилятор увидит ошибку глупого программиста и подставит Example
- имя класса.
Ведь мы обращаемся к полю, которое принадлежит классу! А значит обращение будет не к объекту экземпляра, и код, который выполнится будет равнозначен выполнению:
System.out.println(Example.field);
Что корректно и без ошибок выведет нам 14
.
Т.е даже если вы будете обращаться к полю или методу, которое является статическим, через объекты экземпляров, компилятор все равно будет вас исправлять.
Запомните следующее: обращение к статическим методам и полям осуществляется только через имя класса.
Еще один интересный момент состоит в том, что статический метод нельзя переопределить.
Т.е при работе с статическими методами override
невозможен.
Если вы объявите один и тот же метод в классе-наследнике и в родительском классе, т.е. метод с таким же именем и сигнатурой, вы лишь спрячете
метод родительского класса, но не переопределите.
Такое скрытие называется hiding
.
При обращении к статическому методу, который объявлен как в родительском, так и в классе-наследнике, во время компиляции всегда будет вызван метод исходя из типа ссылки, по которой идет обращение к такому полю или методу.
public class HidingExample {
public static void main(String[] args) {
Parent p = new Child();
Child ch = new Child();
p.test(1);
ch.test(2);
}
}
class Parent {
public static void test(int k) {
System.out.println("Static Parent " + k);
}
}
class Child extends Parent {
public static void test(int k) {
System.out.println("Static Child " + k);
}
}
Результатом будет:
Static Parent 1
Static Child 2
В первом вызове тип ссылки p
был Parent
, во втором - Child
.
Соответственно, в первом случае метод test
вызвался у класса Parent
, а во втором у Child
.
Данный пример непрозрачно намекает, что использовать статические методы при наследовании не стоит.
При таком подходе вы легко можете допустить ошибку и не заметить ее, так как все примеры прекрасно компилируется.
Поэтому, если вы не хотите потерять друзей, сон и семью - избегайте подобного.
- Статические методы и поля не потокобезопасны.
- Статическим может быть только вложенный класс, но не класс верхнего уровня.
- Во время сериализации, также как и
transient
переменные, статические поля не сериализуются.
После десериализации новый объект будет содержать его первичное значение.
Модификатор static
применяется тогда, когда необходимо сделать так, чтобы поле, метод и т.д принадлежали классу
.
Статические члены класса напрямую принадлежат классу, а не его экземпляру.
Ключевое слово static
может быть использовано при объявлении:
- Поля класса
- Метода класса
- Блока кода
- Вложенного класса
Чаще всего оно применяется для первых двух случаев.
Для ответа на вопрос, должен ли быть метод статическим, надо понять: доступен ли контекст выполняемой операции полностью из аргументов метода?
Если ответ 'да', то это весомый аргумент для использования static
. Так как в таком случае, вам не нужен объект класса - вы не используете его состояние, поведение и т.д.
Использовать статические методы при наследовании не стоит из-за hiding
.
В целом, static
использовать стоит аккуратно, помимо случаев, когда вы создаете константу или factory-метод
.