Java Basics: Классы

Классы

Именование

Правилами языка, именование ограничено следующим образом:

  • Только буквы и цифры, “_” и “$”;
  • Первым символом не может быть цифра;
  • Не может быть ключевое слово (включая var);
  • Не может состоять только из “_“.

Модификаторы доступа

private - закрытый доступ;
default - не указывается. Доступ будет ограничен только для классов в рамках пакета;
protected - default + доступен для дочерних классов;
public - публичный класс.

Модификатор strictfp используется для обозначения ограничения точности вычислений с float и double по стандарту IEEE, для обеспечения переносимости (портативности) приложения. Используется для научных целей - когда требуется высокая точность вычислений на любой архитектуре или ОС. В современной JVM указывается по умолчанию для всех методов.

Модификатор protected

Данный модификатор очень коварен и работает не всегда очевидно.
Главное, что для него следует запомнить - доступ предоставляется только в рамках тех классов и сущностей, которые наследуются или являются классами, объявившими метод.

Для внешних пакетов следует рассматривать protected как private (т.е. даже для наследников родительский метод виден не будет если сущность, к которой обращаются, является родительской):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package x;

public class A {
protected void print() {}
}

package y;

import x.A;

public class B extends A {
public void test() {
B b1 = new B();
b1.print();

A b2 = new B();
b2.print(); // ошибка, т.к. b2 рассматривается как объект класса A и фактически print для него private

A a1 = new A();
a1.print(); // ошибка, т.к. a1 - класса A, а print для него с модификатором доступа private
}
}

Пример выше актуален только если класс B будет в ином пакете, нежели A. Если в том же пакете, все print будут видны.

static

Доступ к static переменной есть даже у null объекта - Java обращается не к переменной объекта, а к классу:

1
2
3
Integer x = 1;
x = null;
int max = x.MAX_VALUE;

Перегрузка метода

Метод считается перегруженным, если есть другой метод с отличной сигнатурой, но с тем же именем.

Перегрузке может быть подвержено все что угодно: список аргументов, результат, список исключений, область видимости. Ограничением, правда, служит результат (return), который отличается от оригинала, но при этом, не отличается список атрибутов:

1
2
public void meth(int a) {}
public String meth(int a) {} // ошибка

И даже при изменении на static:

1
public static String meth(int a) {} // ошибка

В случае передачи varargs компилятор так же найдет ошибку перегрузки:

1
2
3
public void meth(int[] x);
public void meth(int ... x); // ошибка
public String meth(int ... x); // ошибка

varargs компилируется как массив соответствующего типа. Т.е. в первом случае будет дубликат метода, а во втором - ошибка перегрузки (отличный return).

В случае наличия двух методов с классом-оберткой и примитивом - Java пытается использовать примитив если он был передан:

1
2
3
4
5
public int meth(int x);
public int meth(Integer x);

meth(1); // вызов метода с примитивом
meth(Integer.valueOf(1)); // вызов метода с оберткой

Выбор метода

Для выбора метода для исполнения, Java руководствуется принципом ближайшего (наиболее родственного) типа:

1
2
3
4
5
6
7
8
9
10
public void method(long arg) {}  // 1
public void method(String arg) {} // 2
public void method(Object arg) {} // 3
public void method(Number arg) {} // 4
public void method(Integer arg) {} // 5
public void method(double arg) {} // 6

method(1); // 1
method(Short.valueOf((short) 1)); // 4
method(1f); // 6

Java будет действовать следующим образом:

  1. Поиск метода, у которого схожий примитивный тип;
  2. Поиск метода, с примитивным старшим типом (char -> short -> int -> long; float -> double);
  3. Autoboxing;
  4. Поиск ближайшего по иерархии типа.

Для массивов autoboxing и unboxing не допускаются. varargs рассматриваются компилятором как массивы. Поэтому при наличии метода с сигнатурой varargs и массива такого же типа, компилятор выдаст ошибку.

Constructor

Все final (но не static) переменные должны быть инициализированы по выходу из вызываемого конструктора. Компилятор проверяет это условие и выдает ошибку если оно не удовлетворено. Инициализация может быть как в конструкторе, так и в блоке сущности:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyObject {
private final String name;
private final int age;

{
name = "simplest";
}

public MyObject() {
age = 1;
}

public MyObject(int age) {
this.age = age;
}
}

Наследование

Основные правила наследования:

  • Доступы к методам и свойствам только на расширение (private -> protected, protected -> public);
  • Типизация только на детализацию типов (CharSequence -> String, Number -> Float, IOException -> FileNotFoundException).

Наследование с generic сложнее - хотя Java на этапе компиляции отбрасывает типизацию generic и добавляет ее в виде преобразования типов в runtime - в определении методов, для @Override необходимо указывать родительский тип generic. Т.е. нельзя сделать так:

1
2
3
4
5
6
7
class A {
public void x(List<CharSequence> x) {}
}

class B extends A {
public void x(List<String> x) {} // ошибка компиляции
}

При этом, получается что на этапе компиляции это 2 одинаковых метода с аргументом List, что не должно выдавать ошибку если бы отсутствовал generic. Так, например:

1
2
public void x(CharSequence x) {}
public void x(String x) {}

В данном случае, сигнатура будет рассматриваться как перегрузка метода.

Для generic допускается уточнение аргумента, т.е.:

1
2
public void x(List<String> x) {}
public void x(ArrayList<String> x) {}

Допускается, но методы будут рассматриваться как перегруженные.

На возвращаемые типы действуют аналогичные правила: уточнение типа допускается, а уточнение generic - нет:

1
2
3
4
public List<CharSequence> x() {}
public ArrayList<String> x() {} // ошибка компиляции
public ArrayList<CharSequence> x() {}
public List<String> x() {} // ошибка компиляции

Private наследование

Поскольку методы private видны только в рамках класса, наследование таких методов запрещено - они просто не видны. При этом, создание дочерними классами таких же методов разрешено (даже с той же сигнатурой) - это будут абсолютно независимые методы!

Static наследование

Для static существует псевдо-наследование, при котором вызов метода допускает вызов статического метода с тем же именем и сигнатурой:

1
2
3
4
5
6
7
class A {
public static void x() {}
}

class B extends A {
public static void x() {}
}

При вызове метода с четким указанием класса:

1
2
3
4
5
A.x();
B.x();

B b = new B();
b.x(); // вызов B.x()

Но если у класса B убрать определение static метода x, то:

1
b.x();  // вызов A.x()

Final наследование

Ключевое слово final запрещает @Override для методов, в т.ч. и static:

1
2
3
4
5
6
7
8
9
class A {
public static final void x() {}
}
class B extends A {
public static void x() {} // ошибка компиляции
}
class C extends A {
public static final void x() {}
}

Наследование и вызов static

В коде может быть неочевидная уловка:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class X {
public static int getX() {
return 3;
}
public void print() {
System.our.println(getX());
}
}

class Y extends X {
public static int getX() {
return 5;
}
}

new Y().print(); // будет выведено 3

Необходимо следить за маркером static и источником вызова. Для static - то что его вызывает (какой тип), тот метод и будет вызван!

Конвертация между типами

Компилятор проверяет возможность конвертации и в случае, если никакой связи между классами нет (т.е. не обнаружено наследование), то конвертация выдаст ошибку на этапе компиляции:

1
2
3
4
5
6
7
class X {}
class Y {}

X x = new X();
Y y = (Y) x; // ошибка компиляции

Y okY = (Y) (Object) x; // ошибка времени исполнения, ClassCastException

При этом, прямого наследования в классовой иерархии нет - это и влечет ошибку. Однако использование промежуточных типов решает проблему (как в примере выше).

1
2
3
4
5
6
7
8
9
10
11
12
interface Z {}
class X implements Z {}
class Y implements Z {}

X x = new X();
Y y = (Y) x; // ошибка компиляции

Z z = new X();
Y y = (Y) z; // runtime ошибка (ClassCastException)

Z xz = x;
Y yz = (Y) xz; // runtime ошибка (ClassCastException)

Inner классы

  • может быть public, protected, package-private, private;
  • может быть наследуемым или сам расширять другие типы;
  • может быть abstract или final;
  • имеет доступ ко всем методам и свойствам объекта, в класс которых вписан (в том числе, и к private).

Создание может быть выполнено и вне класса:

1
My.InnerClass inner = new My.InnerClass();

При этом, класс InnerClass должен быть объявлен как static.
Так же допускается создание с использованием объекта:

1
2
My my = new My();
My.InnerClass inner = new my.InnerClass();

При этом, классу достаточно быть видимым из места, где выполняется его создание. В данном случае, подобная запись равноценна созданию сущности в методе в рамках класса My и обращение к этому методу.

Вложенный класс и родительский, могут содержать переменную с одинаковым именем. В этом случае доступ к ним можно осуществить так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class A{
private int x = 1;

public class B {
private int x = 2;

public class C {
private int x = 3;

public void exec() {
out.println(this.x); // 3
out.println(this.B.x); // 2
out.println(this.A.x); // 1
out.println(this.A.x + this.B.x + x); // 6
}
}
}
}

Для инициализации сущности потребуется объект родительского класса.

Static-inner класс

Позволяет создавать сущности вложенных классов без родительских. Поддерживает импорт посредством static:

1
import static my.pack.My.InnerClass;

С последующим обращением к классу по имени - InnerClass.

Local классы

  • не имеют модификаторов;
  • не могут содержать static полей и методов, кроме static final;
  • доступны все поля и методы родительского класса;
  • доступно использование внешних переменных (в т.ч. так же локальных), но только если они имею модификатор final или являются effective final:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
private int x =10;

public void meth() {
final int a = 1;
int b = 2;
int c = 3;

class B {
public void action() {
out.println(a);
out.println(b);
out.println(c); // ошибка компиляции: c не является final или effective final
A.this.x = 20;
}
}

B obj1 = new B();
class C extends B {}
C obj2 = new C();
c = -3;
}
}

Anonymous классы

Почти то же самое, что и local классы, но не имеют имени и используются как разовое решение:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
protected interface I {
public void go();
}

public void main() {
I face = new I() {
@Override
public void go() {...}
};
face.go();
}
}

И local и anonymous объекты могут использоваться для возврата из метода (return). Хотя, если класс не будет виден родительскому классу для метода, типом возврата может стать только Object.

1
2
3
4
5
6
7
8
9
10
class A {
public B meth() { // ошибка, класс B неизвестен. Можно заменить на Object
class B {
public void action() {
}
}
B objB = new B();
return objB;
}
}

Anonymous классы могут быть реализацией интерфейса, но только одного. Плюсом таких классов является использование в любом месте кода, без необходимости дополнительных определений.

И для local и для anonymous справедливо то, что они могут создаваться и вне метода, например, как часть определения переменной класса:

1
2
3
public class X {
Serializable serial = new Serializable() { ... };
}

Interface

Наличие модификатора abstract не является ошибкой как для методов, так и для определения интерфейса:

1
2
3
public abstract interface MyInterface {
public abstract void method();
}

Наличие public у метода является необязательным - все не реализованные методы у интерфейса должны быть реализованы классами и быть публичными.
Для класса, реализующего интерфейс допускается реализация методов с возвращением типа данных, расширяющего тот что указан у метода в интерфейсе.

Скрытые дополнения компилятора

  • добавляется модификатор abstract для интерфейса;
  • добавляется public static final для переменных в интерфейсе;
  • на не реализованные методы в интерфейсе - добавляется public;
  • методы abstract, default и static в интерфейсе без private автоматически становятся public.

Реализация нескольких интерфейсов

Допускается реализация нескольких интерфейсов с методами, имеющими одинаковую сигнатуру. Но если сигнатура различается, а имена одинаковы компилятор выдаст ошибку.

Abstact class vs interface

  1. Можно применить несколько интерфейсов к классу. Допускается только 1 наследование от абстрактного класса;
  2. У методов абстрактного класса можно менять модификаторы доступа. У интерфейсов только public;
  3. У интерфейсов не может быть instance переменных.

Default методы интерфейсов

Они могут быть переопределены реализующими интерфейс классами. При этом, модификатор доступа будут public - так же как у метода в интерфейсе.
default метод не может быть abstract, final или static.

Если класс реализует несколько интерфейсов с одинаковой сигнатурой default методов компилятор заставит переопределить его:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Front {
public default int pictureSize() {
return 100;
}
}

interface Back {
public default int pictureSize() {
return 1;
}
}

class Picture implements Front, Back {
public int pictureSize() {
return 0;
}
}

Метод класса так же может вызвать одну из реализаций:

1
2
3
4
5
class Drawing implements Front, Back {
public int pictureSize() {
return Front.super.pictureSize();
}
}

Static методы интерфейсов

  1. Должны иметь модификатор static и иметь тело метода;
  2. static методы получают модификатор доступа public, если иной не указан;
  3. Не могут быть abstract или final;
  4. При вызове должно быть задействовано имя интерфейса. Даже если вызывается из наследника.

Private методы интерфейсов

  1. Должны иметь тело;
  2. private static могут быть вызваны только в рамках интерфейса, его объявившего;
  3. private метод может быть вызван только другим private или default методом в рамках интерфейса, его объявившего.

Enum

Все конструкторы объектов должны быть private (либо не указаны вовсе). В случае отсутствия модификатора доступа он принудительно ограничится private. Модификаторы доступа public и protected запрещены для enum.

Инициализация enum выполняется только в случае обращения к ним и только 1 раз и создает его только 1 раз на протяжении всего жизненного цикла enum. Причем, инициализация проводится сразу для всех значений. А не только для того, к которому обратились:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum Test {
ONE("1"), TWO("2");

private final String value;

Test(String value) {
this.value = value;
System.out.println("init enum: " +value);
}
}

out.println("----------1");
int targetTest = Test.ONE.ordinal();
out.println("----------2");
int ignoreTest = Test.TWO.ordinal();
out.println("----------3");

Вывод:

1
2
3
4
5
----------1
init enum: 1
init enum: 2
----------2
----------3

Допускается регистрация абстрактных методов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
enum Test {
ONE("1") {
@Override
public void method() {...}
},
TWO("2") {
@Override
public void method() {...}
},
WITH_OVERRIDE("override") {
@Override
public void method() {...}
@Override
public void doCall(String value) {
...
}
};

private final String value;

Test(String value) {
this.value = value;
System.out.println("init enum: " + value);
}

protected void doCall(String value) { ... }

public abstract void method();
}

enum может так же реализовывать интерфейс. Правила такие же как и для класса - он должен реализовать все абстрактные методы всех интерфейсов. Но в случае с enum данное требование распространяется не на тип enum, а на его значения:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface IFace {
String asString();
}

enum ENum implements IFace {
ONE {
@Override
String asString() { return "one" }
},
TWO {
@Override
String asString() { return "two"; }
@Override
int asInt() { return 2; }
},
THREE {
@Override
String asString() { return "three"; }
@Override
int asInt() { return 3; }
};

public int asInt() {
return 1;
}
}

Sealed классы

Новый модификатор для классов, появившийся в Java 14 Preview. Он позволяет ограничивать доступ к наследованию класса другими. Перечень классов, которые могут использовать sealed класс в качестве родительского должен быть перечислен после ключевого слова permits:

1
2
3
4
5
6
public sealed class Parent permits Son, Daughter { ... }
public final class Son extends Parent { ... }
public sealed Daughter extends Parent permits Lisa, Mary { }
public non-sealed class Lisa extends Daughter { ... }
public non-sealed class Mary extends Daughter { ... }
public non-sealed class Grandmother extends Parent { ... } // compile error, not allowed

Ограничения

  1. Те классы, на которые sealed класс выдал разрешение на наследование должны существовать и должны наследоваться от sealed класса:
1
2
public sealed class Parent permits Child { ... }
public class Child { ... }

Ошибка будет заключаться в том что sealed класс объявил Child своим наследником, у самого класса Child не указано что он расширяет Parent. Так же ошибка была бы в случае если бы класс Child отсутствовал;
2. Каждый класс, который наследуется(!) от sealed класса может иметь только один из 3-х модификаторов: final, sealed и non-sealed.

  • final - действует так же как и для “обычного” класса - не может наследоваться другим классом;
  • sealed - равно как и родительский sealed класс, ограничивает список допустимых наследников;
  • non-sealed - список допустимых наследников не будет ограничен, а класс становится наследуемым другими без каких-либо ограничений;
  1. Наследники sealed классов не всегда должны быть указаны в permits списке. Те подклассы, которые находятся в том же java файле или являются вложенным (nested) классом, не обязательно должны быть перечислены в permits списке sealed класса;
  2. Наследники sealed класса должны находиться в том же пакете или модуле, что и sealed классы.

Sealed интерфейсы

Правила такие же как и для классов (конечно, кроме final). Допускается формирование интерфейсов, расширяющих sealed интерфейсы:

1
2
3
public sealed interface SealedFace permits IFace { ... }
public sealed interface IFace extends SealedFace permits Implementation { ... }
public non-sealed interface Implementation extends IFace { ... }

Sealed и switch

При использовании switch, sealed классы приобретают еще 1 возможность:

1
2
3
4
5
6
7
8
public static sealed class Parent permits Child1, Child2 {}
public static non-sealed class Child1 extends Parent {}
public static non-sealed class Child2 extends Parent {}

String v = switch (p) {
case Child1 c1 -> "child1";
case Child2 c2 -> "child2";
};

Если Parent не будет sealed, то он потребовал бы наличие default условия с соответствующей обработкой. Но в случае с sealed компилятор точно знает какие классы могут ему встретиться и не станет требовать default условия!

Record

Тип данных, который представляет собой immutable структуру с данными, позволяющую существенно сократить объем кода.

1
record Person (String name, Integer age) {}

Этой записи достаточно для создания типа, аналогичного final классу, с 2 final полями, 2 getter методами, а так же с методами toString, hashCode и equals.

При этом, тип данных имеет ряд существенных ограничений и особенностей:

  1. Его конструктор по умолчанию требует добавления всех представленных переменных;
  2. setter’ы отсутствуют;
  3. Сам тип record создает immutable объект, но это не обозначает что внутри содержатся immutable объекты. Для достижения настоящего immutable состояния придется создавать копии объектов путем переписывания getter’ов для переменных;
  4. record может вообще не содержать ни одной переменной;
  5. От record нельзя наследоваться, но при этом сам record может реализовывать интерфейсы. Поскольку record это финальный тип (final), компилятор потребует реализации всех методов, которые представлены во всех интерфейсах, которые реализует record;
  6. instance переменные добавлять нельзя. Все переменные описываются в определении record.

Дополнительные конструкторы record

Допускается определение дополнительных конструкторов:

1
2
3
4
5
6
7
8
9
10
11
12
13
record Person (String name, Integer age) {
public Person (String name, Integer age) {
this.name = name;
this.age = age;
}
public Person(char[] name, double age) {
this(new String(name), calculateAge(age));
}

public static int calculateAge(double age) {
return (int) age;
}
}

В случае объявления дополнительного конструктора компилятор потребует сделать обращение к конструктору по умолчанию (посредством this(...)). Переопределение конструктора по умолчанию не требуется, но допускается.

Компактные конструкторы

В связи с потенциально большим количеством переменных в record, была добавлена возможность проверки и трансформации лишь некоторых членов record посредством укороченного конструктора:

1
2
3
4
5
6
public record Person(String name, Integer age) {
public Person {
if(age < 18) throw new IllegalArgumentException("Person should be an adult!");
name = name + " - ok";
}
}

При этом установка переменной через this будет рассматриваться как повторное определение значения для final переменной и спровоцирует ошибку компиляции:

1
2
3
4
5
public record Person(String name, Integer age) {
public Person {
this.name = name + " - ok"; // compilation error
}
}

Лямбда и функциональные интерфейсы

Использование 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
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Fun {
public boolean test();

String toString();

int hashCode();

boolean equals(Object obj);
}

Сигнатура базовых методов toString, hashCode, equals является критической - отход от нее будет обозначать реализацию нового метода. При этом, компилятор все еще не потребует реализовывать toString, hashCode, equals в классах, реализующих функциональный интерфейс. По умолчанию они будут сгенерированы компилятором, но по желанию их можно переопределить.

 Comments
Comment plugin failed to load
Loading comment plugin