Core Java. Лекция 3

Classes. Interfaces. Records. Enums

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

thethreewords

Everything is a class

  • Any code is a method of some class

  • Any data is stored in fields of some class

  • Any data type (except primitives, but including arrays) are inherited from the Object class

Classes are located in packages

  • edu.phystech.foo

  • edu.phystech.foo.bar

  • Each .java file starts with a package declaration:
    package edu.phystech.hello;

  • In the package directory can be package-info.java, containing no classes, but only JavaDoc above the package keyword.

  • <Package Name>.<classname> specifies the full identifier of any class available in your source code or from libraries (for example, edu.phystech.hello.App)

  • Nested packages are different packages in Java (package-private classes of one package will not be visible in another)

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

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.

  • It is not forbidden to initialize the field at the place of its definition:
    int a = 42 or even int a = getValue().

this field

{ ...

  int value;

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

  registerMe(Registrator r) {
    // a link to oneself is needed
    r.register(this);
  }
}

Objects are passed by reference!

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

Birth, life and death of an object

new Employee("Bob")
life1

Reference assignment

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

Reference assignment

Employee junior = hacker;
life3

Reference loss

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

Garbage collection

life5

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 double bonus;
  . . .
  public void setBonus(double 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 keyword:

    • at the class level prohibits class inheritance

    • at the method level prohibit method inheritance

  • Why?

    • "Template method pattern"

    • 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
        //no others may inherit from Pet
        Cat, Dog, Fish {
}

public final Cat {...}

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

public non-sealed Fish {...}

Important example: sealed interfaces and `record`s

We don’t know what interface or record is yet, but just remember:

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

  • The constructor must exist.

  • 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.

Overriding constructors when inheriting classes

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

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

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

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

Overloading constructors

  • 'this(…​) ` call

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

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

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

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() {
           //there is an access to the external class's state!
           System.out.println(field);
           //prints 42
      }
   }
   void initInner(){
     // initialization of the nested class inside the outer class
     new Inner();
   }
}

//initialization of a nested outside the outer class
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 {
      //nested class field hides external class field
      int field = 18;
      public void show() {
           System.out.println(field);
           //prints 18
      }
   }
}

Nested classes

class Outer {
   int field = 42;
   class Inner {
      //nested class field hides external class field
      int field = 18;
      public void show() {
           System.out.println(Outer.this.field);
           //prints 42!
      }
   }
}

Local nested classes

class Outer {
   void outerMethod() {
      //final (or effectively final) here matters!
      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() {
      //they do not capture the external state
      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();
//unlike non-static 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) {
     //here we have access to all external fields
     //and effectively final-variables
  }
});

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

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