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)); // вызов метода с оберткой

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

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

  • Доступы к методам и свойствам только на расширение (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() { ... };
}

Enum

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

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

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();
}
 Comments
Comment plugin failed to load
Loading comment plugin