Core Java. Лекция 3

Classes. Interfaces. Records. Enums

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

thethreewords

Всё есть класс

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

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

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

Классы помещаются в пакеты

  • edu.phystech.foo

  • edu.phystech.foo.bar

  • Каждый .java-файл начинается с объявления пакета:
    package edu.phystech.hello;

  • В корне пакета может быть package-info.java, не содержащий классы, а только JavaDoc над ключевым словом package.

  • <Имя пакета>.<имя класса> задаёт полный идентификатор любого класса, доступного в исходном коде или через библиотеки (например, edu.phystech.hello.App)

  • Вложенные пакеты — это разные пакеты с точки зрения Java (package-private классы одного пакета не будут видны в другом)

Структура класса: переменные, конструкторы и методы

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

Определяем класс

package org.megacompany.staff;
class Employee {
  // instance fields
  private String name;
  private double salary;
  private LocalDate hireDay;
  // constructor
  public Employee(String n, double 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
  . . .
}

Создаём и используем экземпляры класса

//При необходимости, импортируем
import org.megacompany.staff.Employee;

//где-то в теле метода
. . .
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"

Про инициализацию полей

  • В отличие от локальных переменных, поля можно не инициализировать явно.

  • В этом случае примитивные типы получают значение по умолчанию (0, false), а поля со ссылками — значение null.

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

Поле this

{ ...

  int value;

  setValue(int value) {
    //поле перекрыто аргументом
    this.value = value;
  }

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

Объект передаётся по ссылке!

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;
    }
}

Рождение, жизнь и смерть объекта

new Employee("Bob")
life1

Присвоение ссылки

Employee hacker = new Employee("Bob");
life2

Присвоение ссылки

Employee junior = hacker;
life3

Потеря ссылки

hacker = null;
junior = new Employee("Charlie");
life4

Сборка мусора

life5

Области видимости

Область видимости

Кому доступно

private

только классу

package-private

только пакету (по умолчанию)

protected

классу, пакету, и классам-наследникам

public

всем

Файлы с исходным кодом и классы

  • В одном .java файле может быть один публичный класс, названный так же, как и .java-файл (public class Foo в файле Foo.java).

  • Может быть сколько угодно package-private-классов, но это скорее плохая практика.

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

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

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

// 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());

​Наследование: единственный родительский класс​

columnclasses

Тип ссылки и тип объекта

employeemanagerex
  Executive ex = new Executive (...);
  //для ex доступны члены, объявленные в Manager, Employee и Executive
  Manager m = ex;
  //для m доступны члены, объявленные в Employee и Manager
  Employee e = m;
  //для e доступны члены, объявленные только в Employee

Переопределение (overriding) методов

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

class Manager extends Employee {
  private int bonus;
  @Override //не обязательная, но крайне желательная штука
  public int getTotalPayout() {
    return getSalary() + bonus;
  }
}

Использование super

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

В отличие от this, super не указывает ни на какой объект (и его нельзя никуда передать). Это лишь указание компилитору вызвать метод суперкласса.

Ковариантность возвращаемых типов

overriding
  • Возвращаемый тип может быть того же типа или субтипа

  • Типы аргументов обязаны совпадать

final-классы и методы

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

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

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

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

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

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

sealed-типы (Java 15+)

Наследоваться можно, но только тем, кому разрешено:

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

public final Cat {...}

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

public non-sealed Fish {...}

Важный пример: sealed-интерфейсы и record-ы

Пока не знаем ни что такое 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) методов

Сигнатура метода определяется его названием и типами аргументов:

//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)
. . .

Статические поля и методы

Данные, общие для всех экземпляров класса:

class Employee {
  /*WARNING: данный пример
  не работает при многопоточном исполнении*/
  private static int nextId = 1;
  private int id;
  . . .
  public void setId() {
    id = nextId;
    nextId++;
  }
}

Статические константы

Выделяем память один раз

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


. . .

Math.PI // возвращает 3.14...

Статические методы

Статическим методам доступны только статические переменные и вызовы других статических методов

class Employee {

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

. . .
Employee.nextId() //имя класса вместо объекта

psvm

Теперь мы понимаем: метод main доступен всем и не требует инстанцирования объекта:

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

Конструктор

public class Person {
    //public-конструктор без аргументов
    public Person() {
       ....
    }

    //package-private конструктор с аргументом
    Person(String  name) {
        ....
    }
}

​Конструкторы​

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

  • Если мы 1) явно не написали конструктор, 2) родительский класс имеет конструктор без аргументов — то неявным образом у класса появляется публичный конструктор без аргументов по умолчанию.

  • Если мы явно написали хотя бы один конструктор, конструктор по умолчанию исчезает.

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

  • Конструктор не обязан быть публичным.

Переопределение конструкторов при наследовании классов

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

  • После Java25: пишем как угодно, главное в конструкторе субдкласса не брать значения не проинстанцированных полей суперкласса.

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

class Employee extends Person{
    Employee(String name) {
        super(name);
        ...
    }
}

Перегрузка конструкторов

  • Вызов this(…​)

  • Java 25+: свобода в плане того, где он может быть вызван.

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

    Person(){
        this("unknown");
    }
}

Секции инициализации

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

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

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

А как же деструктор?

  • А его нет!

  • Даже не пытайтесь переопределять finalize

  • Почему метод finalize оказался плохой идеей

Вложенные классы

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();

Вложенные классы

Каждый экземпляр Inner имеет неявную ссылку на Outer.

inner

Вложенные классы

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

Вложенные классы

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

Локальные вложенные классы

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 {};
   }
}

Вложенные статические классы

По сути, ничем не отличаются от просто классов:

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();

Анонимные классы

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();
    }
}

Использование анонимных классов

  • Чаще всего — как реализация абстрактных классов и интерфейсов «по месту»

  • Анонимный класс — вложенный класс, поэтому до появления лямбд и ссылок на методы это был единственный способ организовать коллбэк

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

Абстрактные классы и методы

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

Реализация абстрактного класса

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;
  }
}

Правила

  • Класс, в котором хотя бы один из методов не реализован, должен быть помечен как abstract

  • Нереализованные методы в классе возникают двумя способами:

    • явно объявлены как abstract

    • унаследованы из абстрактных классов или интерфейсов и не переопределены.

  • Абстрактные классы нельзя инстанцировать через new.

    • new Person("John Doe"); — ошибка компиляции: 'Person is abstract, cannot be instantiated'.

Интерфейсы

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

   //это --- публичные абстрактные методы!
   double getArea();
   double getHeight();

   //этот метод может вызывать другие и читать final-поля
   default double getVolume() {
      return getArea() * getHeight();
   }
}

Реализация интерфейса

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;
    }
}

Если какой-то из методов интерфейса не будет переопределён, класс нужно пометить как abstract.

Чем интерфейсы отличаются от абстрактных классов?

  • Нет внутреннего состояния и конструкторов

  • Можно наследоваться (extends) только от одного класса, но реализовывать (implements) сколько угодно интерфейсов — множественное наследование.

  • Поэтому как абстракция, интерфейс предпочтительнее.

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

  • 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 (что это, мы узнаем вскоре).

//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)));

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

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

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

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