Core Java. Лекция 3

Классы. Интерфейсы. Класс Object и его стандартные методы

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

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) родительский класс имеет конструктор без аргументов — то неявным образом у класса появляется публичный конструктор без аргументов по умолчанию.

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

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

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

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

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

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

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

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

  • Вызов this(…​)

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 оказался плохой идеей

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

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) сколько угодно интерфейсов — множественное наследование.

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

Оператор instanceof

instanceof
C1 c1; C2 c2; C3 c3; I1 i1;

x instanceof A //  false, если x == null
c1 instanceof С2 // true или false
i1 instanceof C2 // true или false
c2 instanceof C1 // всегда возвращает true
с3 instanceof C2 // не скомпилируется

Приведение типов (до Java 14)

Person p = . . .;
if (p instanceof Student) {
    //если не защитить instanceof, возможен ClassCastException
    Student s = (Student) p;
    . . .
}

Pattern Matching for instanceof (Java 14+, JEP305)

Person p = . . .;
if (p instanceof Student s) {
   //здесь доступна переменная Student s
    . . .
} else {
   //здесь недоступна переменная Student s
}

//Скомпилируется
if (obj instanceof String s && s.length() > 5) {
   . . .
   s.contains(..)
}

//НЕ скомпилируется
if (obj instanceof String s || s.length() > 5) {...}

Все меньше избыточного кода

//Before Java 10
if (obj instanceof Number) {
  Number n = (Number) obj;
  System.out.println(n.longValue());
}

//Java 10+
if (obj instanceof Number) {
  var n = (Number) obj;
  System.out.println(n.longValue());
}

//Java 14+
if (obj instanceof Number n) {
  System.out.println(n.longValue());
}

Pattern matching for switch (JEP406, preview feature in Java 17)

public int calculate(Expr expr) {
  return switch (expr) {
    //Не скомпилируется, если мы забудем что-то из реализаций Expr!
    case Literal l -> l.value();
    case Divide d -> calculate(d.left()) / calculate(d.right());
    case Multiply m -> calculate(m.left()) * calculate(m.right());
    case Add a -> calculate(a.left()) + calculate(a.right());
    case Subtract s -> calculate(s.left()) - calculate(s.right());
  };
}

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

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-переменным
  }
});

Object: the Cosmic Superclass

  • Любой класс в Java является наследником Object

  • Писать class Employee extends Object не надо

  • В этом классе определены важные методы

    • equals и hashCode

    • toString

equals() и hashCode()

  • boolean equals(Object other) возвращает true т. и т. т., когда внутреннее состояние совпадает

  • int hashCode() возвращает целое значение, которое обязано совпадать для объектов с одинаковым внутренним состоянием

  • Это нужно для хеш-таблиц (и, пожалуй, является протекшей абстракцией)

Формальный контракт equals

  1. Рефлексивность:
    \(\forall x \ne \mathrm{null} (x.equals(x))\)

  2. Симметричность:
    \(\forall x \ne \mathrm{null} \, \forall y \ne \mathrm{null} (x.equals(y) \iff y.equals(x))\)

  3. Транзитивность:
    \(\forall x \ne \mathrm{null} \, \forall y \ne \mathrm{null} \, \forall z \ne \mathrm{null} (x.equals(y) \& y.equals(z) \Rightarrow x.equals(z))\)

  4. Консистентность: если сравниваемые объекты не изменялись, повторный вызов equals должен возвращать одно и то же значение.

  5. \(\forall x \ne \mathrm{null} (x.equals(\mathrm{null}) = \mathrm{false})\)

Формальный контракт hashCode

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

  2. Связь с equals:
    \(\forall x \forall y (x.equals(y) \Rightarrow x.hashCode() = y.hashCode())\)

  3. Хотя
    \(x.hashCode() = y.hashCode() \Rightarrow x.equals(y)\)
    и не обязательно, но желательно для большинства случаев.

Выводы

  1. Переопределять equals и hashCode нужно только вместе и согласованно, чтобы выполнить контракт \(x.equals(y) \Rightarrow x.hashCode() = y.hashCode()\)

  2. Грамотно реализовать equals и hashCode трудно, но, к счастью, самостоятельно это делать не нужно.

  3. Для тестирования есть специальная библиотка EqualsVerifier.

  4. Для генерации equals и hashCode можно использовать возможности IDE или библиотеки Lombok.

Генерация equals и hashCode

generateequals

Генерация equals и hashCode

public class Person {
    private String name;
    private int age;
    @Override
    public boolean equals(Object o) {
       // никогда, НИКОГДА не пишите это сами
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

Или, если у нас Lombok

import lombok.EqualsAndHashCode;

@EqualsAndHashCode
public class Person {
    private  int age;
    private  String name;
}

Переопределение toString

public class Person {
    private String name;
    private int age;
    . . .
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
. . .
Person person = . . .
System.out.println(person);

Или, если у нас Lombok

import lombok.ToString;

@ToString
public class Person {
    private  int age;
    private  String name;
}

Полный «обвес» класса

public class Point {
  private final int x;
  private final int y;

  /*Подождите! Нам нужны:
   * конструктор
   * getX() и getY()
   * equals и hashCode
   * toString
   * 40 строчек кода из ничего!
   */

  public double distance(Point other) {
    ...
  }
}

Или, если у нас Lombok

import lombok.Data;

@Data
public class Point {
  private final int x;
  private final int y;
  public double distance(Point other) {
    ...
  }
}

Или, если у нас Java 14+

record Point(int x, int y) {
  public double distance(Point other) {
    ...
  }
}
  • private final поля

  • конструктор

  • доступ через x() и y()

  • equals, hashCode и toString

  • невозможно наследование от класса и от рекорда, но возможна реализация интерфейсов