Core Java. Lecture #3

Classes. Interfaces. Object class and its standard methods

Ivan Ponomarev, Synthesized.io/MIPT

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

  • If the superclass does not have a constructor without arguments, the first call should be 'super(…​) `.

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

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

Overloading constructors

  • 'this(…​) ` call

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?

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

// it can't be instantiated!
public interface Prism
{
   // it's a constant!
   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.

instanceof operator

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

x instanceof A // false if x == null
c1 instanceof C2 // true or false
i1 instanceof C2 // true or false
c2 instanceof C1 // always returns true
c3 instanceof C2 // won't compile

Type casting (up to Java 14)

Person p = . . .;
if (p instanceof Student) {
    //if you do not protect with instanceof, ClassCastException is possible
    Student s = (Student) p;
    . . .
}

Pattern Matching for instanceof (Java 14+, JEP305)

Person p = . . .;
if (p instanceof Student s) {
   //the Student s variable is visible here
    . . .
} else {
   //the Student s variable is NOT visible here
}

//Will compile
if (obj instanceof String s && s.length() > 5) {
   . . .
   s.contains(..)
}

//Won't compile
if (obj instanceof String s || s.length() > 5) {...}

Less and less redundant code

//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) {
    //Won't comile if we forgot some of the Expr implementations!
    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());
  };
}

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: local records, enums and interfaces

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

Object: the Cosmic Superclass

  • Any class in Java is a descendant of 'Object'

  • You don’t need to write class Employee extends Object

  • Important methods are defined in this class

    • equals and hashCode

    • toString

equals() and hashCode()

  • boolean equals(Object other) returns true iff the internal states coincide

  • int hashCode() returns an integer value that must match for objects with the same internal state

  • This is needed for hash tables (and probably is a leaky abstraction)

equals formal contract

  1. Reflexivity:
    \(\forall x \ne \mathrm{null} (x.equals(x))\)

  2. Symmetry:
    \(\forall x \ne \mathrm{null} \, \forall y \ne \mathrm{null} (x.equals(y) \iff y.equals(x))\)

  3. Transitivity:
    \(\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. Consistency: if compared objects didn’t change, each equals invocation must return the same value.

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

hashCode formal contract

  1. Consistency: if compared objects didn’t change, each hashCode() invocation must return the same value (but not necessary the same between different runs of the application)

  2. Relation to equals:
    \(\forall x \forall y (x.equals(y) \Rightarrow x.hashCode() = y.hashCode())\)

  3. While+ \(x.hashCode() = y.hashCode() \Rightarrow x.equals(y)\)
    is not obligatory, but it’s desirable for most of the cases.

Conclusions

  1. equals and hashCode should be overridden together and consistently to fulfil the contract \(x.equals(y) \Rightarrow x.hashCode() = y.hashCode()\)

  2. It is difficult to properly implement equals and hashCode, but, fortunately, you do not need to do it yourself.

  3. For testing there is a special library EqualsVerifier.

  4. To generate equals and hashCode you can either use the capabilities of the IDE or Lombok library.

Code generation of 'equals' and 'hashCode'

generateequals

Code generation of 'equals' and 'hashCode'

public class Person {
    private String name;
    private int age;
    @Override
    public boolean equals(Object o) {
       // NEVER EVER write this yourself!
        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);
    }
}

Or if we have Lombok

import lombok.EqualsAndHashCode;

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

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

Or if we have Lombok

import lombok.ToString;

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

Full boilerplaite of the class

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

  /*Oh wait! We need:
   * constructor
   * getX() and getY()
   * equals and hashCode
   * toString
   * 40 lines of code for nothing!
   */

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

Or if we have Lombok

import lombok.Data;

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

Or if we have Java 14+

record Point(int x, int y) {
  public double distance(Point other) {
    ...
  }
}
  • private final fields

  • constructor

  • access using x() and y()

  • equals, hashCode and toString

  • one can’t inherit a record from a class or a record, but can implement interfaces