Как вы уже догадались, static
- это ключевое слово в Java
.
Но прежде чем продолжить - давайте вспомним вот что.
Для понимания того - что такое static
давайте еще раз быстро проговорим такие вещи как классы и объекты.
В Java
мы оперируем такими понятиями, как Класс
и instance
класса - объект.
Так вот, Класс
- это описание методов и свойств Объекта
, а Объект
- уже инстанс класса, сущность.
Если говорить совсем простым языком, то можно сказать: класс - это как техническое описание прибора, купленного вами в электронном дискаунтере. То, каким он должен быть: материал, форма, список составляющих и т.д.
А объект - уже сам прибор, со своим уникальным набором свойств. Например, у него может быть цвет, материал изготовления, форма, он может состоять также из более мелких узлов(вспомогательных приборов) и т.д
Это все - свойства объекта, они уникальны - для каждого прибора они свои.
Но что, если мы хотим, чтобы некоторое поле или метод принадлежали не объекту, а классу объектов? Чтобы оно было одним для всех объектов без исключения.
Применительно к переменным и методам классов static
именно так и сработает.
Указав модификатор static
у поля или метода класса, вы тем самым говорите: это поле или метод принадлежат именно классу.
И для всех instance
-ов класса это поле или метод будут одни.
Грубо говоря, если вы объявляете поле класса статическим, то вы как бы расшариваете это поле для всех объектов.
Такое поле уже не принадлежит объекту, оно принадлежит именно классу.
И изменение этого поля у одного объекта влечет его изменение у всех объектов.
Из того, что static
члены класса принадлежат именно классу следует еще и то, что
мы не можем получить доступ к нестатическим членам.
public class Counter {
private int count;
public static void printCount() {
System.out.println(count); //compile time error
}
}
И это логично, так как экземпляра объекта класса еще нет,у нас нет и переменной count
.
После этого введения возникает вопрос - а где вообще можно использовать static
?
Ключевое слово static
может быть использовано при объявлении:
- Поля класса
- Метода класса
- Блока кода
- Вложенного класса
Наиболее часто встречаемое применение - это первые два пункта: поля и методы.
Статические методы принадлежат классу, а значит для их использования не нужно создавать экземпляр объекта класса. Чаще всего метод можно и правильно сделать статическим только в случае, если весь контекст для его работы передается в аргументах.
Пример: Math.abs(-20)
, Math.sqrt(144)
.
Для того, чтобы извлечь корень из числа, вам по сути не нужно ничего, кроме самого этого числа. То же самое и с возведением в степень и т.д.
Т.е контекст операции - число. Оно передается в аргументах и все, можно уже работать.
Если же для работы статического метода передаваемых ему аргументов недостаточно - это верный признак того, чтобы крепко задуматься: а должен ли он вообще быть статическим?
Очень часто статические методы группируются в один класс, который называется как-нибудь с приставкой Utils
, так называемый класс утилит.
Хороший пример этого - FileUtils
, который есть в большом количестве библиотек, Apache Commons IO
в их числе.
В идеале такой класс является еще и финальным - final
.
Статические методы применяются также при использовании factory
и builder
паттернов.
Подробнее об этом в Паттерны.
Также часто можно встретить использование статических методов для создания экземпляров объектов в зависимости от каких то условий, добавления промежуточной логики в создание объекта и т.д
Случаев такого использования много, они есть и в стандартной библиотеке, например: String.valueOf(15)
, Integer.valueOf("14")
и т.д
Если есть подобные методы для создания объекта, то лучше воспользоваться именно ими.
Т.е предпочтительней использовать Integer.valueOf("14")
, нежели new Integer(14)
.
Все дело в том, что при использовании статических методов для создания экземпляра объекта вы можете добавить какие-то дополнительные возможности, дополнительную логику.
В частности, статические методы создания
valueOf()
у классов-оболочек примитивов, кроме чисел с плавающей точкой, имеют кеш. По умолчанию данный кеш содержит значения от-128
до127
, если значение попадает в кеш - оно достается в разы быстрее, чем при использовании конструктора, при этом не расходууется лишняя память.Наиболее часто используемые значения также кешируются.
Статическое поле принадлежит классу, для его использования не нужно создавать экземпляр объекта класса. Как уже было сказано, раз оно принадлежит классу, то оно не уникально для каждого экземпляра - оно общее для них. Наверное, я уже порядком надоел с этими напоминаниями, но, если понять это, становится гораздо проще находить области применения.
Статические поля используются многими для создания счетчиков - 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);
// ...
}
// ...
}
Т.е у нас есть некоторый каталог и есть данные для заполнения. Мы можем инициализацию вынести в конструктор, но тогда при каждом создании экземпляра объекта класса будет происходить заполнение каталога, а нам надо, чтобы каталог был заполнен только один раз. Для этого мы и использовали статический блок кода, и он выполнился ровно один раз при иницализации класса.
Подобный подход используется еще и в 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
поля.
Если посмотреть стандартную реализацию 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.
Раз поле или метод принадлежит классу, то и обращаться к нему необходимо через имя класса.
Пусть у нас есть некоторый класс со статическим полем:
class Example {
public static int field = 14;
}
Далее мы хотим обратиться к этому полю, и возникает вопрос: как правильно это сделать? Есть два варианта:
- Обратиться к полю через экземпляр объекта класса
- Обратиться к полю через класс
Так вот, сразу скажу, забудьте про первый вариант. Да, он рабочий, но делать так не стоит. Более того, это глупо! Ведь статические поля принадлежат не экземпляру объекта класса, а самому классу.
Что это значит на практике. Давайте возьмем класс, описанный выше, и ответим на вопрос: что будет, если мы выполним следующий код:
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
переменные, статические поля не сериализуются.
После десериализации новый объект будет содержать его первичное значение