Классы
Именование
Правилами языка, именование ограничено следующим образом:
- Только буквы и цифры, “_” и “$”;
- Первым символом не может быть цифра;
- Не может быть ключевое слово (включая
var); - Не может состоять только из “_“.
Модификаторы доступа
private - закрытый доступ;default - не указывается. Доступ будет ограничен только для классов в рамках пакета;protected - default + доступен для дочерних классов;public - публичный класс.
Модификатор strictfp используется для обозначения ограничения точности вычислений с float и double по стандарту IEEE, для обеспечения переносимости (портативности) приложения. Используется для научных целей - когда требуется высокая точность вычислений на любой архитектуре или ОС. В современной JVM указывается по умолчанию для всех методов.
Модификатор protected
Данный модификатор очень коварен и работает не всегда очевидно.
Главное, что для него следует запомнить - доступ предоставляется только в рамках тех классов и сущностей, которые наследуются или являются классами, объявившими метод.
Для внешних пакетов следует рассматривать protected как private (т.е. даже для наследников родительский метод виден не будет если сущность, к которой обращаются, является родительской):
1 | package x; |
Пример выше актуален только если класс B будет в ином пакете, нежели A. Если в том же пакете, все print будут видны.
static
Доступ к static переменной есть даже у null объекта - Java обращается не к переменной объекта, а к классу:
1 | Integer x = 1; |
Перегрузка метода
Метод считается перегруженным, если есть другой метод с отличной сигнатурой, но с тем же именем.
Перегрузке может быть подвержено все что угодно: список аргументов, результат, список исключений, область видимости. Ограничением, правда, служит результат (return), который отличается от оригинала, но при этом, не отличается список атрибутов:
1 | public void meth(int a) {} |
И даже при изменении на static:
1 | public static String meth(int a) {} // ошибка |
В случае передачи varargs компилятор так же найдет ошибку перегрузки:
1 | public void meth(int[] x); |
varargs компилируется как массив соответствующего типа. Т.е. в первом случае будет дубликат метода, а во втором - ошибка перегрузки (отличный return).
В случае наличия двух методов с классом-оберткой и примитивом - Java пытается использовать примитив если он был передан:
1 | public int meth(int x); |
Выбор метода
Для выбора метода для исполнения, Java руководствуется принципом ближайшего (наиболее родственного) типа:
1 | public void method(long arg) {} // 1 |
Java будет действовать следующим образом:
- Поиск метода, у которого схожий примитивный тип;
- Поиск метода, с примитивным старшим типом (
char->short->int->long;float->double); - Autoboxing;
- Поиск ближайшего по иерархии типа.
Для массивов autoboxing и unboxing не допускаются. varargs рассматриваются компилятором как массивы. Поэтому при наличии метода с сигнатурой varargs и массива такого же типа, компилятор выдаст ошибку.
Constructor
Все final (но не static) переменные должны быть инициализированы по выходу из вызываемого конструктора. Компилятор проверяет это условие и выдает ошибку если оно не удовлетворено. Инициализация может быть как в конструкторе, так и в блоке сущности:
1 | class MyObject { |
Наследование
Основные правила наследования:
- Доступы к методам и свойствам только на расширение (
private->protected,protected->public); - Типизация только на детализацию типов (
CharSequence->String,Number->Float,IOException->FileNotFoundException).
Наследование с generic сложнее - хотя Java на этапе компиляции отбрасывает типизацию generic и добавляет ее в виде преобразования типов в runtime - в определении методов, для @Override необходимо указывать родительский тип generic. Т.е. нельзя сделать так:
1 | class A { |
При этом, получается что на этапе компиляции это 2 одинаковых метода с аргументом List, что не должно выдавать ошибку если бы отсутствовал generic. Так, например:
1 | public void x(CharSequence x) {} |
В данном случае, сигнатура будет рассматриваться как перегрузка метода.
Для generic допускается уточнение аргумента, т.е.:
1 | public void x(List<String> x) {} |
Допускается, но методы будут рассматриваться как перегруженные.
На возвращаемые типы действуют аналогичные правила: уточнение типа допускается, а уточнение generic - нет:
1 | public List<CharSequence> x() {} |
Private наследование
Поскольку методы private видны только в рамках класса, наследование таких методов запрещено - они просто не видны. При этом, создание дочерними классами таких же методов разрешено (даже с той же сигнатурой) - это будут абсолютно независимые методы!
Static наследование
Для static существует псевдо-наследование, при котором вызов метода допускает вызов статического метода с тем же именем и сигнатурой:
1 | class A { |
При вызове метода с четким указанием класса:
1 | A.x(); |
Но если у класса B убрать определение static метода x, то:
1 | b.x(); // вызов A.x() |
Final наследование
Ключевое слово final запрещает @Override для методов, в т.ч. и static:
1 | class A { |
Наследование и вызов static
В коде может быть неочевидная уловка:
1 | class X { |
Необходимо следить за маркером static и источником вызова. Для static - то что его вызывает (какой тип), тот метод и будет вызван!
Конвертация между типами
Компилятор проверяет возможность конвертации и в случае, если никакой связи между классами нет (т.е. не обнаружено наследование), то конвертация выдаст ошибку на этапе компиляции:
1 | class X {} |
При этом, прямого наследования в классовой иерархии нет - это и влечет ошибку. Однако использование промежуточных типов решает проблему (как в примере выше).
1 | interface Z {} |
Inner классы
- может быть
public,protected,package-private,private; - может быть наследуемым или сам расширять другие типы;
- может быть
abstractилиfinal; - имеет доступ ко всем методам и свойствам объекта, в класс которых вписан (в том числе, и к
private).
Создание может быть выполнено и вне класса:
1 | My.InnerClass inner = new My.InnerClass(); |
При этом, класс InnerClass должен быть объявлен как static.
Так же допускается создание с использованием объекта:
1 | My my = new My(); |
При этом, классу достаточно быть видимым из места, где выполняется его создание. В данном случае, подобная запись равноценна созданию сущности в методе в рамках класса My и обращение к этому методу.
Вложенный класс и родительский, могут содержать переменную с одинаковым именем. В этом случае доступ к ним можно осуществить так:
1 | public class A{ |
Для инициализации сущности потребуется объект родительского класса.
Static-inner класс
Позволяет создавать сущности вложенных классов без родительских. Поддерживает импорт посредством static:
1 | import static my.pack.My.InnerClass; |
С последующим обращением к классу по имени - InnerClass.
Local классы
- не имеют модификаторов;
- не могут содержать
staticполей и методов, кромеstatic final; - доступны все поля и методы родительского класса;
- доступно использование внешних переменных (в т.ч. так же локальных), но только если они имею модификатор
finalили являются effective final:
1 | class A { |
Anonymous классы
Почти то же самое, что и local классы, но не имеют имени и используются как разовое решение:
1 | class A { |
И local и anonymous объекты могут использоваться для возврата из метода (return). Хотя, если класс не будет виден родительскому классу для метода, типом возврата может стать только Object.
1 | class A { |
Anonymous классы могут быть реализацией интерфейса, но только одного. Плюсом таких классов является использование в любом месте кода, без необходимости дополнительных определений.
И для local и для anonymous справедливо то, что они могут создаваться и вне метода, например, как часть определения переменной класса:
1 | public class X { |
Interface
Наличие модификатора abstract не является ошибкой как для методов, так и для определения интерфейса:
1 | public abstract interface MyInterface { |
Наличие public у метода является необязательным - все не реализованные методы у интерфейса должны быть реализованы классами и быть публичными.
Для класса, реализующего интерфейс допускается реализация методов с возвращением типа данных, расширяющего тот что указан у метода в интерфейсе.
Скрытые дополнения компилятора
- добавляется модификатор
abstractдля интерфейса; - добавляется
public static finalдля переменных в интерфейсе; - на не реализованные методы в интерфейсе - добавляется
public; - методы
abstract, default иstaticв интерфейсе безprivateавтоматически становятсяpublic.
Реализация нескольких интерфейсов
Допускается реализация нескольких интерфейсов с методами, имеющими одинаковую сигнатуру. Но если сигнатура различается, а имена одинаковы компилятор выдаст ошибку.
Abstact class vs interface
- Можно применить несколько интерфейсов к классу. Допускается только 1 наследование от абстрактного класса;
- У методов абстрактного класса можно менять модификаторы доступа. У интерфейсов только
public; - У интерфейсов не может быть instance переменных.
Default методы интерфейсов
Они могут быть переопределены реализующими интерфейс классами. При этом, модификатор доступа будут public - так же как у метода в интерфейсе.
default метод не может быть abstract, final или static.
Если класс реализует несколько интерфейсов с одинаковой сигнатурой default методов компилятор заставит переопределить его:
1 | interface Front { |
Метод класса так же может вызвать одну из реализаций:
1 | class Drawing implements Front, Back { |
Static методы интерфейсов
- Должны иметь модификатор
staticи иметь тело метода; staticметоды получают модификатор доступаpublic, если иной не указан;- Не могут быть
abstractилиfinal; - При вызове должно быть задействовано имя интерфейса. Даже если вызывается из наследника.
Private методы интерфейсов
- Должны иметь тело;
private staticмогут быть вызваны только в рамках интерфейса, его объявившего;privateметод может быть вызван только другимprivateили default методом в рамках интерфейса, его объявившего.
Enum
Все конструкторы объектов должны быть private (либо не указаны вовсе). В случае отсутствия модификатора доступа он принудительно ограничится private. Модификаторы доступа public и protected запрещены для enum.
Инициализация enum выполняется только в случае обращения к ним и только 1 раз и создает его только 1 раз на протяжении всего жизненного цикла enum. Причем, инициализация проводится сразу для всех значений. А не только для того, к которому обратились:
1 | enum Test { |
Вывод:
1 | ----------1 |
Допускается регистрация абстрактных методов:
1 | enum Test { |
enum может так же реализовывать интерфейс. Правила такие же как и для класса - он должен реализовать все абстрактные методы всех интерфейсов. Но в случае с enum данное требование распространяется не на тип enum, а на его значения:
1 | interface IFace { |
Sealed классы
Новый модификатор для классов, появившийся в Java 14 Preview. Он позволяет ограничивать доступ к наследованию класса другими. Перечень классов, которые могут использовать sealed класс в качестве родительского должен быть перечислен после ключевого слова permits:
1 | public sealed class Parent permits Son, Daughter { ... } |
Ограничения
- Те классы, на которые
sealedкласс выдал разрешение на наследование должны существовать и должны наследоваться отsealedкласса:
1 | public sealed class Parent permits Child { ... } |
Ошибка будет заключаться в том что sealed класс объявил Child своим наследником, у самого класса Child не указано что он расширяет Parent. Так же ошибка была бы в случае если бы класс Child отсутствовал;
2. Каждый класс, который наследуется(!) от sealed класса может иметь только один из 3-х модификаторов: final, sealed и non-sealed.
final- действует так же как и для “обычного” класса - не может наследоваться другим классом;sealed- равно как и родительский sealed класс, ограничивает список допустимых наследников;non-sealed- список допустимых наследников не будет ограничен, а класс становится наследуемым другими без каких-либо ограничений;
- Наследники
sealedклассов не всегда должны быть указаны вpermitsсписке. Те подклассы, которые находятся в том же java файле или являются вложенным (nested) классом, не обязательно должны быть перечислены вpermitsспискеsealedкласса; - Наследники
sealedкласса должны находиться в том же пакете или модуле, что иsealedклассы.
Sealed интерфейсы
Правила такие же как и для классов (конечно, кроме final). Допускается формирование интерфейсов, расширяющих sealed интерфейсы:
1 | public sealed interface SealedFace permits IFace { ... } |
Sealed и switch
При использовании switch, sealed классы приобретают еще 1 возможность:
1 | public static sealed class Parent permits Child1, Child2 {} |
Если Parent не будет sealed, то он потребовал бы наличие default условия с соответствующей обработкой. Но в случае с sealed компилятор точно знает какие классы могут ему встретиться и не станет требовать default условия!
Record
Тип данных, который представляет собой immutable структуру с данными, позволяющую существенно сократить объем кода.
1 | record Person (String name, Integer age) {} |
Этой записи достаточно для создания типа, аналогичного final классу, с 2 final полями, 2 getter методами, а так же с методами toString, hashCode и equals.
При этом, тип данных имеет ряд существенных ограничений и особенностей:
- Его конструктор по умолчанию требует добавления всех представленных переменных;
- setter’ы отсутствуют;
- Сам тип
recordсоздает immutable объект, но это не обозначает что внутри содержатся immutable объекты. Для достижения настоящего immutable состояния придется создавать копии объектов путем переписывания getter’ов для переменных; recordможет вообще не содержать ни одной переменной;- От
recordнельзя наследоваться, но при этом самrecordможет реализовывать интерфейсы. Посколькуrecordэто финальный тип (final), компилятор потребует реализации всех методов, которые представлены во всех интерфейсах, которые реализуетrecord; - instance переменные добавлять нельзя. Все переменные описываются в определении
record.
Дополнительные конструкторы record
Допускается определение дополнительных конструкторов:
1 | record Person (String name, Integer age) { |
В случае объявления дополнительного конструктора компилятор потребует сделать обращение к конструктору по умолчанию (посредством this(...)). Переопределение конструктора по умолчанию не требуется, но допускается.
Компактные конструкторы
В связи с потенциально большим количеством переменных в record, была добавлена возможность проверки и трансформации лишь некоторых членов record посредством укороченного конструктора:
1 | public record Person(String name, Integer age) { |
При этом установка переменной через this будет рассматриваться как повторное определение значения для final переменной и спровоцирует ошибку компиляции:
1 | public record Person(String name, Integer age) { |
Лямбда и функциональные интерфейсы
Использование var совместно с лямбдой не допускается без принудительной передачи типа с generic:
1 | var func = (String x ) -> x.indexOf("1"); |
Компилятор не пропустит данную строку, поскольку var и лямбда представляют довольно абстрактный контекст. При этом, принудительная передача типа исправит ситуацию:
1 | var func = (Function<String, Integer>) (String x ) -> x.indexOf("1"); |
Функциональные интерфейсы
Аннотация @FunctionalInterface служит в большей степени как маркировочная. При ее наличии компилятор проверит наличие не более 1 метода. Фактически, функциональным интерфейсом можно считать типы, которые не помечены аннотацией.
Методы от Object не учитываются в подсчете методов функционального интерфейса - они считаются базовыми для всех видов объектов в Java (а функциональный интерфейс это тоже Java объект). Иными словами, подобная запись допустима:
1 |
|
Сигнатура базовых методов toString, hashCode, equals является критической - отход от нее будет обозначать реализацию нового метода. При этом, компилятор все еще не потребует реализовывать toString, hashCode, equals в классах, реализующих функциональный интерфейс. По умолчанию они будут сгенерированы компилятором, но по желанию их можно переопределить.