JavaRush /Java блог /Random /Кофе-брейк #266. Метод по умолчанию в Java 8: вопросы и о...

Кофе-брейк #266. Метод по умолчанию в Java 8: вопросы и ответы. Принцип подстановки Лисков: разбор практического примера

Статья из группы Random

Метод по умолчанию в Java 8: вопросы и ответы

Источник: Medium В этой публикации рассмотрены распространенные вопросы о методе по умолчанию (default method) в Java, которые могут встретиться разработчику на собеседовании. Кофе-брейк #266. Метод по умолчанию в Java 8: вопросы и ответы. Принцип подстановки Лисков: разбор практического примера - 1

1. Что такое метод по умолчанию?

Метод по умолчанию (default method) — это метод, представленный в Java 8, который можно объявить внутри интерфейса. До Java 8 все методы, объявленные внутри интерфейса, были только public abstract (общедоступными абстрактными).

//до Java 8 разрешен только public abstract
interface Level1
{
    default void method1() //invalid
    {
      System.out.println("method-1");
    }
  public abstract void method2(); //valid
}

// код после выхода Java 8
interface Level1
{
 
  default void method1() //valid
  {
    System.out.println("method-1");
  }

  public abstract void method2(); //valid
}

2. Почему возникла необходимость в абстрактном методе?

До Java 8, если мы хотели представить новый метод внутри интерфейса, то все классы реализации должны были предоставить тело (body, реализацию) вновь представленного метода, даже если у них нет требований к вновь представленному методу.

interface Level1
{
  void method1();// по умолчанию это public abstract

}

class Class1 implements Level1
{
    public void method1()
    {
      System.out.println("method-1");
    }
}

class Class2 implements Level1
{
    public void method1()
    {
      System.out.println("method-x");
    }
}
Теперь предположим, что в новой версии появился новый метод method2, который требуется только внутри Class1. Проблема здесь в том, что если мы хотим выполнить компиляцию, то для Class2 необходимо обеспечить ту же самую реализацию, даже если она требуется только для Class1, а это просто ужасно.

interface Level1
{
  void method1();// по умолчанию это public abstract
  void method2();
}

class Class1 implements Level1
{
    public void method1()
    {
      System.out.println("method-1");
    }
    public void method2()
    {
      System.out.println("method-2");
    }
}

class Class2 implements Level1
{
    public void method1()
    {
      System.out.println("method-x");
    }
    public void method2(){} // ненужный код 
}
Для решения этой проблемы в Java ввели метод по умолчанию (default). Он предоставляет реализацию по умолчанию для любого класса. То есть, позволяет избегать необходимости изменять все классы, которые реализуют этот интерфейс. В классе, который реализует интерфейс с методами по умолчанию, вы можете их переопределить.

interface Level1
{
  public abstract void method1();
  default void method2(){}; //метод по умолчанию
}

class Class1 implements Level1
{
    public void method1()
    {
      System.out.println("method-1");
    }
  @Override
  public void method2()
    {
      System.out.println("method-2");
    }
}

class Class2 implements Level1
{
    public void method1()
    {
      System.out.println("method-x");
    }
  // Нет реализации method2
}

3. Ключевое слово default, используемое внутри интерфейса, совпадает с модификатором доступа default?

Нет, они разные. Ключевое слово default, используемое внутри интерфейса, сообщает: “код внутри фигурных скобок {} метода по умолчанию является реализацией по умолчанию”. Но модификатор доступа по умолчанию делает область действия класса, конструктора, переменной, метода или элемента данных package-private. То есть, к ней можно получить доступ внутри пакета. Если модификатор доступа не используется, компилятор считает это значением по умолчанию (default).

4. Какие ключевые слова разрешены внутри интерфейса в Java 8?

Разрешены: public, static, default и abstract.

5. Это единственные ключевые слова, разрешенное внутри интерфейса?

До Java 8 public, static, default и abstract были единственными разрешенными ключевыми словами, но начиная с Java 9 нам разрешено использовать ключевые слова/методы private.

6. Как добиться множественного наследования при использовании метода по умолчанию внутри интерфейса?

Когда мы используем метод по умолчанию внутри интерфейса, чтобы добиться множественного наследования, необходимо переопределить метод по умолчанию.

interface Level1
{
  default void method1()
  {
    System.out.println("Method-1");
  }
}

interface Level2
{
  default void method1()
  {
    System.out.println("Method-2");
  }
}

class Class1 implements Level1, Level2
{
  public static void main(String[] args)
  {
    System.out.println("Hi from main method");
  }
}
Приведенный выше код приведет к ошибке компиляции. Чтобы решить эту проблему, нам нужно переопределить method1 внутри Class1. Исправленный код будет выглядеть вот так:

class Class1 implements Level1, Level2
{

 @Override
 public void method1() {
  Level1.super.method1();
 }

 public static void main(String[] args)
   {
     System.out.println("Hi from main method");
   }
}

Принцип подстановки Лисков: разбор практического примера

Источник: Medium Содержание этой статьи посвящено одному из важнейших принципов SOLID — принципу подстановки (замещения) Лисков. Разработчик должен иметь возможность заменять объекты базового класса объектами дочерних классов, и это не должно изменять поведение/характеристики программы. Мы можем придерживаться этого принципа, используя наследование, расширяя класс или реализуя интерфейс. Мы можем считать методы, определенных в супертипе, как определение контракта. Ожидается, что каждый подтип будет придерживаться этого контракта. Если подкласс не соблюдает контракт суперкласса, он нарушает принцип подстановки Лисков (Liskov Substitution Principle, LSP).

Как метод подкласса может нарушить контракт метода суперкласса?

  1. Возврат объекта, несовместимого с объектом, возвращаемым методом суперкласса.
  2. Создание нового исключения, которое не было создано методом суперкласса.
  3. Изменение семантики или введение побочных эффектов, не являющихся частью контракта суперкласса.

Чем полезен принцип подстановки Лисков?

Принцип подстановки Лисков можно считать проверкой или способом определить, не слишком ли вы преждевременно обобщили концепцию и создали суперкласс там, где в этом нет необходимости. Вот базовый пример, используемый для описания этого принципа в действии. Ниже вы увидите код программы, которая создает экземпляр класса прямоугольника (rectangle), а также класса квадрата (square). Класс Square наследуется от класса Rectangle, потому что некоторые утверждают, что квадрат можно считать типом прямоугольника. В этой настройке класс Square расширяет Rectangle и пытается обеспечить его свойства квадрата, переопределяя методы setWidth и setHeight. Это нарушает LSP, поскольку Square больше не ведет себя так же, как Rectangle.

class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int area() {
        return width * height;
    }
}



class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; 
    }

    @Override
    public void setHeight(int height) {
        this.height = height;
        this.width = height; 
    }
}
Далее вы можете увидеть, как это способно вызвать проблему:

public class Main {

    public static void main(String[] args) {

 
        Rectangle rect = new Rectangle();
        processRectangle(rect); 
        
        Rectangle sq = new Square();
        processRectangle(sq);
    }
    
    void processRectangle(Rectangle r) {
        r.setWidth(5);
        r.setHeight(10);
        System.out.println("Area: " + r.area());
    }
}
Это было бы приемлемо для Rectangle, но привело бы к нежелательным результатам при использовании метода processRectangle() для square. Решением данной проблемы будет следующим: вместо того, чтобы наследовать square от Rectangle, нужно создать интерфейс с именем shape (форма) и реализовать его как в Rectangle, так и в square, как показано ниже.

interface Shape {
    int area();
}

class Rectangle implements Shape {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public int area() {
        return width * height;
    }
}

class Square implements Shape {
    private int side;

    public void setSide(int side) {
        this.side = side;
    }

    @Override
    public int area() {
        return side * side;
    }
}
Здесь у нас есть интерфейс shape с функцией area(). Теперь и прямоугольник, и квадрат могут сохранять свое поведение, не нарушая принцип Лисков. Теперь с помощью интерфейса Shape мы можем единообразно работать с экземплярами Rectangle и Square без неожиданных или нежелательных эффектов.

public class Main {

    public static void main(String[] args) {

        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(5);
        rectangle.setHeight(10);


        processShape(rectangle); 
        // выводит на печать "Area: 50"
        
        Square square = new Square();
        square.setSide(5);


        processShape(square); 
        // выводит на печать "Area: 25"    


    }

    void processShape(Shape shape) {
        System.out.println("Area: " + shape.area());
    }
}
Теперь метод processShape() работает с любой фигурой, реализующей интерфейс shape. Это соответствует LSP, поскольку позволяет производить замену без изменения ожидаемого поведения.
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ