Core Java. Лекция 3

Classes. Interfaces. Records. Enums

Иван Пономарёв, КУРС/МФТИ

thethreewords

Everything is a class

  • Любой код — метод некоторого класса

  • Любые данные хранятся в полях некоторого класса

  • Любые типы данных (исключая примитивные, но включая массивы) — наследники класса Object

  • Как мы помним, классы находятся в пакетах.

Class structure: variables, constructors, and methods

class ClassName
{
   field1
   field2
   . . .
   constructor1
   constructor2
   . . .
   method1
   method2
   . . .
}

Define a class

package org.megacompany.staff;
class Employee {
  // instance fields
  private String name;
  private int salary;
  private LocalDate hireDay;
  // constructor
  public Employee(String n, int s, int year, int month, int day) {
    name = n; salary = s;
    hireDay = LocalDate.of(year, month, day);
  }
  // a method
  public String getName() {
    return name; }
  // more methods
  . . .
}

Create and use instances of the class

//If necessary, we can import a class from another package
import org.megacompany.staff.Employee;

//somewhere in the body of the method
. . .
Employee hacker = new Employee("Harry Hacker", 50000, 1989, 10, 1);
Employee tester = new Employee("Tommy Tester", 40000, 1990, 3, 15);

hacker.getName(); //returns "Harry Hacker"

Field initialization

  • Unlike local variables, fields can miss explicit initialization.

  • In this case, primitive types are set to a default value (0, false), and fields with references are set to null.

  • Проинициализировать поле по месту его определения не возбраняется:
    int a = 42 или даже int a = getValue().

this field

{ ...

  int value;

  setValue(int value) {
    // the field is hidden by an argument
    this.value = value;
  }

  registerMe(Registrator r) {
    //нужна ссылка на себя
    r.register(this);
  }
}

Birth, life and death of an object

@AllArgsConstructor
class Employee { int id; String name;}

new Employee(42, "Bob")
Diagram

Присвоение ссылки локальной переменной

Employee hacker = new Employee(42, "Bob");
Diagram

Присвоение ссылки локальной переменной

Employee junior = hacker;
Diagram

Reference loss

hacker = null;
junior = new Employee(43, "Charlie");
Diagram

Garbage collection

Diagram

Передача аргументов: ссылка на объект, значение примитивного типа

public class Employee {
  int age = 18;

  public static void main(String[] args) {
    Employee e = new Employee();
    int a = 1;
    foo(e, a);
    System.out.printf("%d - %d", e.age, a);
    //prints 42 - 1
  }

  static void foo(Employee e, int a) {
    //e passed by reference, a passed by value
    e.age = 42;
    a = 5;
  }
}
Diagram

Value-типы (Java 28+ preview):
неизменяемые значения, копируемые напрямую

private static LocalDate
    addMonth(LocalDate date) {
  date = date.plusMonths(1);
  return date;
}

public static void main(String... args) {
  LocalDate date = LocalDate.of(2026, 9, 15);
  addMonth(date);
}

NB: "аллокция на стеке" — это умозрительная картина. Оптимизатор волен делать что угодно, если семантика позволяет, как для обычных, так и для value-объектов: value даёт больше возможностей для оптимизации.

Diagram

Scopes

Scope

Visibility

private

class only

package-private

package only (default)

protected

class, package, and descendant classes

public

everywhere

Source code files and classes

  • In a .java file, there can be only one public class, named the same as the .java file ('public class Foo' in the 'Foo.java' file).

  • There can be as many package-private classes as you like, but this is rather a bad practice.

Inheritance

employeemanager
public class Manager extends Employee {
  private int bonus;
  . . .
  public void setBonus(int bonus) {
    this.bonus = bonus;
  }
}

Inheritance

// construct a Manager object
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);

for (Employee e : staff)
  System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());

Inheritance: Single parent class

columnclasses

Reference type and object type

employeemanagerex
  Executive ex = new Executive (...);
  // members declared in Manager, Employee, and Executive are available for ex
  Manager m = ex;
  // members declared in Employee and Manager are available for m
  Employee e = m;
 // members declared in Employee only are available for m

Overriding methods

class Employee {
  private int salary;
  public int getSalary() {
    return salary;
  }
  public int getTotalPayout(){
    return getSalary();
  }
}

class Manager extends Employee {
  private int bonus;
  @Override //this is not mandatory, but highly desirable thing
  public int getTotalPayout() {
    return getSalary() + bonus;
  }
}

Using super

class Manager extends Employee {
  private int bonus;
  @Override
  public int getTotalPayout() {
    return super.getTotalPayout() + bonus;
  }
}

Unlike this, super does not point to any object (and cannot be passed as an argument). It just tells the compiler to call the superclass method.

Covariance of return types

overriding
  • The return type can be of the same type or subtype

  • Argument types must match

final classes and methods

  • Ключевое слово final:

    • на уровне класса запрещает наследование класса

    • на уровне метода запрещает наследование метода

  • Зачем это нужно?

    • Паттерн "Шаблонный метод"

    • J. Bloch: 'Design and document for inheritance, or else prohibit it'

sealed types (Java 15+)

A sealed class be subclassed, but only by those who explicitly allowed to:

public sealed class Pet
        permits
        //никакие другие не могут наследоваться от него
        Cat, Dog, Fish {
}

public final Cat {...}

public sealed Dog permits Hound, Terrier, Toy {...}

public non-sealed Fish {...}

Important example: sealed interfaces and `record`s

Пока не знаем ни что такое interface, ни что такое record, но просто запомним:

public sealed interface Expr
        permits Add, Subtract, Multiply, Divide, Literal {}

//implicitly final!
public record Add(Expr left, Expr right) implements Expr {}
public record Subtract(Expr left, Expr right) implements Expr {}
public record Multiply(Expr left, Expr right) implements Expr {}
public record Divide(Expr left, Expr right) implements Expr {}
public record Literal(int value) implements Expr {}

Overloading methods

Method signature is defined by its name and argument types:

//org.junit.jupiter.api.Assertions
void assertEquals(short expected, short actual)
void assertEquals(short expected, short actual, String message)
void assertEquals(short expected, short actual, Supplier<String> messageSupplier)
void assertEquals(byte expected, byte actual)
void assertEquals(byte expected, byte actual, String message)
void assertEquals(byte expected, byte actual, Supplier<String> messageSupplier)
void assertEquals(int expected, int actual)
void assertEquals(int expected, int actual, String message)
void assertEquals(int expected, int actual, Supplier<String> messageSupplier)
. . .

Static fields and methods

Data common to all instances of the class:

class Employee {
  /*WARNING: This example
  does not work in multi-threaded environment*/
  private static int nextId = 1;
  private int id;
  . . .
  public void setId() {
    id = nextId;
    nextId++;
  }
}

Static constants

Allocate memory once

public class Math {
   . . .
   public static final double PI = 3.14159265358979323846;
   . . .
}

. . .

Math.PI // returns 3.14...

Static methods

Only static variables and other static methods are available to static methods

class Employee {

  private static int nextId = 1;
  private int id;
  . . .
  public static int getNextId() {
    return nextId; // returns static field
  }
}

. . .
Employee.nextId() //class name instead of object

psvm

Now we understand: the main method is available everywhere and does not require instantiation of the object:

public class App {
  public static void main(String... args) {
     System.out.println("Hello, world!");
  }
}

Constructor

public class Person {
    //public constructor without arguments
    public Person() {
       ....
    }

    //package-private constructor with argument
    Person(String name) {
        ....
    }
}

Constructors

  • Конструктор обязан быть у каждого класса.

  • If we don’t 1) explicitly write a constructor, 2) the parent class has a constructor without arguments — then implicitly the class will have a public constructor with no default arguments.

  • If we explicitly wrote at least one constructor, the default constructor disappears.

  • If the parent class does not have a constructor without arguments, the default constructor is not created.

  • Constructor does not have to be public.

Вызов конструктора суперкласса

  • Если у суперкласса нет доступного конструктора без параметров, цепочка конструкторов подкласса должна явно вызвать super(…​).

  • До Java 25: super(…​) должен быть первым оператором конструктора.

  • Java 25+: перед super(…​) можно выполнять код, который не использует создаваемый объект — например, проверяет и преобразовывает аргументы.

class Person {
    Person(String  name){
        ...    }
}
class Employee extends Person{
    Employee(String name) {
        if (name.isBlank()) //Java 25+
            throw new IllegalArgumentException("name is blank");
        super(name);
        ...    }
}

Overloading constructors

  • this(…​): вызов другого конструктора того же класса.

  • До Java 25: должен быть первым оператором.

  • Java 25+: перед ним можно выполнять код, который не использует создаваемый объект — например, проверяет и преобразовывает аргументы.

public class Person {
    Person(String  name){
        ...
    }

    Person() {
      String name = System.getProperty(
                    "user.name", "unknown"); // Java 25+
      this(name);
    }
}

Initialization sections

class Employee {
  private static int nextId;
  private int id;

  // static initialization block
  static {
    nextId = ThreadLocalRandom.current().nextInt(10000);
  }

  // object initialization block
  {
    id = nextId;
    nextId++;
  }
}

What about the destructor?

  • There are no destructors!

  • Don’t even try to use 'finalize'

  • Why the finalize method was a bad idea?

Nested classes

class Outer {
   int field = 42;
   class Inner {
      public void show() {
           //есть доступ к состоянию внешнего класса!
           System.out.println(field);
           //печатает 42
      }
   }
   void initInner(){
     //инициализация вложенного класса внутри
     new Inner();
   }
}

//инициализация вложенного класса извне
Outer.Inner in = new Outer().new Inner();

Nested classes

Each instance of Inner has an implicit reference to Outer.

inner

Nested classes

class Outer {
   int field = 42;
   class Inner {
      //поле вложенного класса перекрывает поле внешнего класса
      int field = 18;
      public void show() {
           System.out.println(field);
           //печатает 18
      }
   }
}

Nested classes

class Outer {
   int field = 42;
   class Inner {
      //поле вложенного класса перекрывает поле внешнего класса
      int field = 18;
      public void show() {
           System.out.println(Outer.this.field);
           //печатает 42!
      }
   }
}

Local nested classes

class Outer {
   void outerMethod() {
      //final (или effectively final) тут существенно
      final int x = 98;
      System.out.println("inside outerMethod");
      class Inner {
         void innerMethod() {
            System.out.println("x = " + x);
         }
      }
      Inner y = new Inner();
      y.innerMethod();
   }
}

Java 15+: локальные record-ы, enum-ы и интерфейсы

class Outer {
   void outerMethod() {
      //они не захватывают внешнее состояние
      record Foo (int a, int b) {};
      enum Bar {A, B};
      interface Baz {};

      //NB:
      //static not allowed here!
      static class X {};
   }
}

Nested static classes

In fact, they are no different from just classes:

class Outer {
   private static void outerMethod() {
     System.out.println("inside outerMethod");
   }
   static class Inner {
     public static void main(String[] args) {
        System.out.println("inside inner class Method");
        outerMethod();
     }
   }
}
. . .
Outer.Inner x = new Outer.Inner();
// в отличие от не статического: new Outer().new Inner();

Anonymous classes

class Demo {
    void show() {
        System.out.println("superclass");
    }
}
class Flavor1Demo {
    public static void main(String[] args){
        Demo d = new Demo() {
            void show() {
                super.show();
                System.out.println("subclass");
            }
        };
        d.show();
    }
}

Using Anonymous Classes

  • Most often - as an implementation of abstract classes and interfaces "in place"

  • An anonymous class is a nested class, so before the introduction of lambdas and method references, this was the only way to organize a callback

. . .
button.onMouseClick(new EventListener(){
  void onClick(Event e) {
     //здесь у нас доступ ко всем внешним полям
     //и effectively final-переменным
  }
});

Abstract classes and methods

abstractsample
public abstract class Person
{
  public Person(String name) {
    this.name = name;
  }
  public String getName() {
    return name;
  }
  public abstract String getDescription();
}

Abstract class implementation

public class Student extends Person
{
  private String major;
  public Student(String name, String major) {
    super(name);
    this.major = major;
  }
  @Override
  public String getDescription() {
    return "a student majoring in " + major;
  }
}

Rules

  • A class in which at least one of the methods is not implemented must be marked as abstract

  • Unimplemented methods in a class occur in two ways:

    • explicitly declared as abstract

    • Inherited from abstract classes or interfaces and not overridden.

  • Abstract classes cannot be instantiated through new operator.

    • new Person('John Doe'); — compilation error: 'Person is abstract, cannot be instantiated'.

Interfaces

//его нельзя инстанцировать!
public interface Prism
{
   //это --- final-переменная!
   double PI = 3.14;

   //these are public abstract methods!
   double getArea();
   double getHeight();

   //this method can call other methods and read constants
   default double getVolume() {
      return getArea() * getHeight();
   }
}

Interface implementation

public class Parallelepiped implements Prism {
    private double a;
    private double b;
    private double h;
    @Override
    public double getArea() {
        return a * b;
    }

    @Override
    public double getHeight() {
        return h;
    }
}

If any of the interface methods are not overridden, the class should be marked as abstract.

How are interfaces different from abstract classes?

  • No internal state and constructors

  • You can inherit (via extends) from only one class, but implement (via implements) as many interfaces as you want — multiple inheritance.

  • Therefore, as an abstraction, the interface is preferred.

Специальные виды типов

  • Enumeration Classes

  • Records

  • Annotation Interfaces (о них речь пойдёт значительно позже)

Enumeration Classes (Java 5+)

public enum Size
  { SMALL, MEDIUM, LARGE, EXTRA_LARGE };

. . .

Size s = Size.MEDIUM;

for (Size s: Size.values()) . . .

switch (s) {
  case SMALL: . . .
  case LARGE: . . .
}

Поля, методы и конструкторы для перечислений

public enum Size
{
   SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
   private final String abbreviation;
   private Size(String abbreviation) {
     this.abbreviation = abbreviation;
   }
   public String getAbbreviation() {
     return abbreviation;
   }
}

. . .

Size s = . . .
s.getAbbreviation(); // вернёт S, M, L или XL

Enums: интерфейсы и абстрактные методы

interface Rule { boolean canGo();  }
enum TrafficLight implements Rule {
    RED {
        @Override public boolean canGo() { return false; }
        @Override int durationSeconds()  { return 55; }
    },
    YELLOW {
        @Override public boolean canGo() { return false; }
        @Override int durationSeconds()  { return 5; }
    },
    GREEN {
        @Override public boolean canGo() { return true; }
        @Override int durationSeconds()  { return 45; }
    };
    // Abstract method implemented individually by each constant
    abstract int durationSeconds();
}

Records (Java 16+)

  • Иммутабельные объекты (один раз создав, нельзя менять состояние)

  • Компактный синтаксис

  • Наследование запрещено, но можно реализовывать интерфейс

  • Автоматическая поддержка equals/hashCode (что это, мы узнаем вскоре)

  • Любой record может быть value-типом (value record).

//Defines a record with two int fields
public record Point(int x, int y) { }

Records

public record Point(int x, int y) {
    public double distance(Point other) {
        int dx = x - other.x;
        int dy = y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

Point p1 = new Point(1, 2);
//Get value via implicit accessor method
System.out.println(p1.x());
System.out.println(p1.distance(new Point(3, 4)));

Value-классы (Java28+ preview feature)

  • value-объект не имеет identity: два экземпляра с одинаковым состоянием неразличимы.

  • Все поля экземпляра неявно final; JVM может хранить объект без дополнительной косвенности.

  • value можно объявить обычный класс или record.

  • Конкретный value-класс неявно final; наследование возможно через abstract value class.

abstract value class Quantity {
  int value; //implicitly final
  Quantity(int value) {
    this.value = value;
  }
}
value class Distance //implicitly final
    extends Quantity {
  Distance(int value) {
    super(value);
  }
}

value record Point(int x, int y) {}

Базовые принципы проектирования классов

  • Минимизируйте область видимости (private всё, что только можно)

  • Минимизируйте мутабельность (final на всём, что только можно)

  • Документируйте точки расширения через наследование, или запрещайте наследование (final, sealed)