5

Почему в Java нельзя переопределять статические методы?

12

Почему невозможно переопределить статические методы?

Статические методы в языке программирования не могут быть переопределены, так как они привязаны к классу, а не к экземпляру этого класса. Это означает, что при вызове статического метода осуществляется обращение не к конкретному объекту, а к самому классу.

Для иллюстрации этой проблемы, рассмотрим следующий пример на языке Java:

class Parent {
    static void staticMethod() {
        System.out.println("Static method in Parent");
    }

    void instanceMethod() {
        System.out.println("Instance method in Parent");
    }
}

class Child extends Parent {
    static void staticMethod() {
        System.out.println("Static method in Child");
    }

    void instanceMethod() {
        System.out.println("Instance method in Child");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent childAsParent = new Child();
        
        parent.staticMethod(); // Вывод: "Static method in Parent"
        childAsParent.staticMethod(); // Вывод: "Static method in Parent"
        
        parent.instanceMethod(); // Вывод: "Instance method in Parent"
        childAsParent.instanceMethod(); // Вывод: "Instance method in Child"
    }
}

В этом примере статический метод staticMethod в классе Child не переопределяет статический метод staticMethod из класса Parent. При вызове staticMethod у объекта childAsParent, который ссылается на экземпляр класса Child, будет вызван метод из родительского класса Parent, а не из дочернего класса Child.

Таким образом, статические методы не могут быть переопределены в классах-наследниках, потому что они связаны с самими классами, а не с их экземплярами.

5 ответ(ов)

5

Переопределение методов зависит от наличия экземпляра класса. Суть полиморфизма в том, что вы можете создать подкласс, и объекты этих подклассов будут иметь разные реализации одних и тех же методов, определенных в суперклассе (и переопределенных в подклассах). Статический метод не связан ни с каким экземпляром класса, поэтому к этому понятию это не относится.

Дизайну Java придавали значение два основных момента. Во-первых, это была забота о производительности: Smalltalk подвергался критике за медлительность (включая сборку мусора и полиморфные вызовы), и создатели Java хотели избежать этих проблем. Во-вторых, было принято решение, что целевая аудитория Java — разработчики C++. Реализация статических методов таким образом принесла пользу в виде привычности для программистов C++ и также была очень быстрой, так как не требовала ожидания времени выполнения для определения, какой метод следует вызывать.

2

Лично я считаю, что это недостаток в дизайне Java. Да, я понимаю, что нестатические методы связаны с экземпляром, тогда как статические методы привязаны к классу и т.д. Тем не менее, посмотрите на следующий код:

public class RegularEmployee {
    private BigDecimal salary;

    public void setSalary(BigDecimal salary) {
        this.salary = salary;
    }

    public static BigDecimal getBonusMultiplier() {
        return new BigDecimal(".02");
    }

    public BigDecimal calculateBonus() {
        return salary.multiply(getBonusMultiplier());
    }

    /* ... предположительно много другого кода ... */
}

public class SpecialEmployee extends RegularEmployee {
    public static BigDecimal getBonusMultiplier() {
        return new BigDecimal(".03");
    }
}

Этот код не будет работать так, как вы могли бы ожидать. А именно, SpecialEmployee получает 2% бонуса так же, как обычные сотрудники. Но если убрать static, то SpecialEmployee будет получать 3% бонуса.

(Хотя стоит признать, что этот пример не совсем удачен с точки зрения стиля кода, так как в реальной жизни вы, вероятно, захотите хранить множитель бонуса в базе данных, а не жестко закодировать его. Но я не хотел загромождать пример лишним кодом, не относящимся к сути вопроса.)

Списывая это, кажется, что довольно логично сделать getBonusMultiplier статическим. Возможно, вы хотите отобразить множитель бонуса для всех категорий сотрудников, не создавая экземпляры для каждой категории. Какой смысл в поиске таких примеров? Что если мы создаем новую категорию сотрудников и у нас еще нет ни одного сотрудника, которому она назначена? Это вполне логично для статической функции.

Но это не работает.

Да, я понимаю, что могу придумать множество способов переписать указанный код, чтобы он работал. Моя точка зрения не в том, что это создает неразрешимую проблему, а в том, что это создает ловушку для неопытного программиста, поскольку язык не ведет себя так, как я считаю, что разумный человек ожидал бы.

Возможно, если бы я попытался написать компилятор для языка ООП, я быстро осознал бы, почему реализовать возможность переопределения статических функций было бы сложно или невозможно.

Или, возможно, есть какие-то хорошие причины, почему Java так себя ведет. Может кто-то указать на преимущества такого поведения, на какую-то категорию задач, которые упрощаются благодаря этому? Я имею в виду, не просто указывайте на спецификацию языка Java и говорите "видите, здесь задокументировано, как это работает". Я это знаю. Но есть ли разумная причина, почему это ДОЛЖНО работать именно так? (Кроме очевидного "сделать это правильно было слишком сложно"...)

Обновление

@VicKirk: Если вы имеете в виду, что это "плохой дизайн", поскольку не соответствует тому, как Java обрабатывает статические методы, мой ответ: "Ну, да, конечно." Как я уже сказал в своем исходном сообщении, это не работает. Но если вы подразумеваете, что это плохой дизайн в том смысле, что с языком, в котором это работало бы, было бы что-то фундаментально неправильно, т.е. где статические методы можно было бы переопределять так же, как виртуальные функции, и что это каким-то образом ввело бы неоднозначность или было бы невозможно реализовать эффективно, я отвечаю: "Почему? Что не так с этой концепцией?"

Я считаю, что пример, который я привел, является вполне естественным желанием. У меня есть класс, который имеет функцию, не зависящую от любых данных экземпляра, и которую я, вполне разумно, мог бы захотеть вызвать независимо от экземпляра, а также вызвать из метода экземпляра. Почему это не должно работать? За эти годы я сталкивался с этой ситуацией довольно много раз. На практике я обхожу это, делая функцию виртуальной, а затем создаю статический метод, единственная цель которого — быть статическим методом, который передает вызов на виртуальный метод с фиктивным экземпляром. Это кажется довольно запутанным способом добиться поставленной цели.

0

Краткий ответ: это вполне возможно, но в Java так не делается.

Вот код, который иллюстрирует текущую ситуацию в Java:

Файл Base.java:

package sp.trial;
public class Base {
  static void printValue() {
    System.out.println("  Вызван статический метод Base.");
  }
  void nonStatPrintValue() {
    System.out.println("  Вызван нестатический метод Base.");
  }
  void nonLocalIndirectStatMethod() {
    System.out.println("  Нестатический метод вызывает переопределённый(?) статический метод:");
    System.out.print("  ");
    this.printValue();
  }
}

Файл Child.java:

package sp.trial;
public class Child extends Base {
  static void printValue() {
    System.out.println("  Вызван статический метод Child.");
  }
  void nonStatPrintValue() {
    System.out.println("  Вызван нестатический метод Child.");
  }
  void localIndirectStatMethod() {
    System.out.println("  Нестатический метод вызывает собственный статический метод:");
    System.out.print("  ");
    printValue();
  }
  public static void main(String[] args) {
    System.out.println("Объект: статический тип Base; динамический тип Child:");
    Base base = new Child();
    base.printValue();
    base.nonStatPrintValue();
    System.out.println("Объект: статический тип Child; динамический тип Child:");
    Child child = new Child();
    child.printValue();
    child.nonStatPrintValue();
    System.out.println("Класс: вызов статического метода Child:");
    Child.printValue();
    System.out.println("Класс: вызов статического метода Base:");
    Base.printValue();
    System.out.println("Объект: статический/динамический тип Child -- вызов статического из нестатического метода Child:");
    child.localIndirectStatMethod();
    System.out.println("Объект: статический/динамический тип Child -- вызов статического из нестатического метода Base:");
    child.nonLocalIndirectStatMethod();
  }
}

Если вы запустите этот код (я делал это на Mac, из Eclipse, используя Java 1.6), вы получите следующий вывод:

Объект: статический тип Base; динамический тип Child.
  Вызван статический метод Base.
  Вызван нестатический метод Child.
Объект: статический тип Child; динамический тип Child.
  Вызван статический метод Child.
  Вызван нестатический метод Child.
Класс: вызов статического метода Child.
  Вызван статический метод Child.
Класс: вызов статического метода Base.
  Вызван статический метод Base.
Объект: статический/динамический тип Child -- вызов статического из нестатического метода Child.
  Нестатический метод вызывает собственный статический.
    Вызван статический метод Child.
Объект: статический/динамический тип Child -- вызов статического из нестатического метода Base.
  Нестатический метод вызывает переопределённый(?) статический.
    Вызван статический метод Base.

Здесь единственные случаи, которые могут вызвать удивление (и о которых и идет речь в вопросе), выглядят так:

  • Первый случай: "Динамический тип не используется для определения, какие статические методы будут вызваны, даже когда они вызываются с помощью экземпляра объекта obj.staticMethod()."
  • Последний случай: "Когда статический метод вызывается из метода объекта класса, выбирается статический метод, доступный из самого класса, а не из класса, определяющего динамический тип объекта."

Вызов с помощью экземпляра объекта

Статический вызов разрешается во время компиляции, тогда как вызов нестатического метода разрешается во время выполнения. Обратите внимание, что хотя статические методы унаследованы (от родительского класса), они не переопределяются (дочерним классом). Это может быть неожиданностью, если вы ожидали иного.

Вызов из метода объекта

Методы объекта разрешаются с использованием динамического типа, но статические методы (класса) разрешаются с использованием типа времени компиляции (объявленного типа).

Изменение правил

Чтобы изменить эти правила, так чтобы последний вызов в примере вызывал Child.printValue(), статические вызовы должны были бы получать тип во время выполнения, а не разрешаться компилятором во время компиляции с использованием объявленного класса объекта (или контекста). Тогда статические вызовы могли бы использовать (динамическую) иерархию типов для разрешения вызова, так же как это делается сейчас с вызовами методов объектов.

Это можно было бы легко реализовать (если бы мы изменили Java 😮) и это вовсе не выглядит неразумным, однако, это повлекло бы за собой интересные соображения.

Основное соображение заключается в том, что нам нужно определить, какие статические вызовы методов должны это делать.

В данный момент Java имеет эту "особенность" в языке, при которой вызовы obj.staticMethod() заменяются на вызовы ObjectClass.staticMethod() (обычно с предупреждением). [Примечание: ObjectClass — это тип времени компиляции для obj]. Эти вызовы были бы хорошими кандидатами для переопределения таким образом, принимая во внимание динамический тип obj.

Если бы мы это сделали, это сделало бы тела методов труднее для восприятия: статические вызовы в родительском классе могли бы потенциально быть динамически "перенаправлены". Чтобы избежать этого, нам пришлось бы вызывать статический метод с указанием имени класса — что сделало бы вызовы более очевидными и разрешаемыми с учетом иерархии типов на время компиляции (как это происходит сейчас).

Другие способы вызова статического метода более сложны: this.staticMethod() должен означать то же самое, что и obj.staticMethod(), принимая динамический тип this. Однако это может вызвать проблемы с существующими программами, которые вызывают (якобы локальные) статические методы без обозначения (что можно считать эквивалентом this.method()).

А что насчет необозначенных вызовов staticMethod()? Я предлагаю, чтобы они делали то же самое, что и сегодня, и использовали локальный контекст класса для определения, что делать. В противном случае возникнет большая путаница. Конечно, это означает, что method() будет означать this.method(), если method был нестатическим методом, и ThisClass.method() если method был статическим. Это еще один источник путаницы.

Другие соображения

Если мы изменим это поведение (и сделаем статические вызовы потенциально динамически нелокальными), мы, вероятно, будем хотеть пересмотреть значение final, private и protected как квалификаторов для статических методов класса. Мы все должны будем привыкнуть к тому, что private static и public final методы не переопределяются, и их можно безопасно разрешать на этапе компиляции, и они "безопасны" для чтения как локальные ссылки.

0

На самом деле, мы ошибались.

Несмотря на то, что Java по умолчанию не позволяет переопределять статические методы, если внимательно изучить документацию классов Class и Method в Java, можно найти способ эмулировать переопределение статических методов, используя следующий обходной путь:

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;

class RegularEmployee {

    private BigDecimal salary = BigDecimal.ONE;

    public void setSalary(BigDecimal salary) {
        this.salary = salary;
    }

    public static BigDecimal getBonusMultiplier() {
        return new BigDecimal(".02");
    }

    public BigDecimal calculateBonus() {
        return salary.multiply(this.getBonusMultiplier());
    }

    public BigDecimal calculateOverridenBonus() {
        try {
            return salary.multiply((BigDecimal) this.getClass()
                    .getDeclaredMethod("getBonusMultiplier").invoke(this));
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
            e.printStackTrace();
        }
        return null;
    }
    // ... предполагается, что есть много другого кода ...
}

final class SpecialEmployee extends RegularEmployee {

    public static BigDecimal getBonusMultiplier() {
        return new BigDecimal(".03");
    }
}

public class StaticTestCoolMain {

    public static void main(String[] args) {
        RegularEmployee Alan = new RegularEmployee();
        System.out.println(Alan.calculateBonus());
        System.out.println(Alan.calculateOverridenBonus());
        SpecialEmployee Bob = new SpecialEmployee();
        System.out.println(Bob.calculateBonus());
        System.out.println(Bob.calculateOverridenBonus());
    }
}

Результат выполнения программы:

0.02
0.02
0.02
0.03

Вот что мы и пытались достичь 😃

Даже если мы создадим третью переменную Carl как RegularEmployee и присвоим ей экземпляр SpecialEmployee, мы все равно получим вызов метода RegularEmployee в первом случае и вызов метода SpecialEmployee во втором:

RegularEmployee Carl = new SpecialEmployee();

System.out.println(Carl.calculateBonus());
System.out.println(Carl.calculateOverridenBonus());

Просто взгляните на консоль вывода:

0.02
0.03

😉

0

Статические методы в Java рассматриваются как глобальные и не привязаны к экземпляру объекта.

Концептуально было бы возможно вызывать статические методы из объектов классов (как это делается в языках, таких как Smalltalk), но в Java это не так.

ПРАВКА

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

class MyClass { ... }
class MySubClass extends MyClass { ... }

MyClass obj1 = new MyClass();
MySubClass obj2 = new MySubClass();

obj2 instanceof MyClass --> true

Class clazz1 = obj1.getClass();
Class clazz2 = obj2.getClass();

clazz2 instanceof clazz1 --> false

Вы можете отражаться над классами, но на этом всё. Вы не вызываете статический метод, используя clazz1.staticMethod(), а делаете это через MyClass.staticMethod(). Статический метод не привязан к объекту, поэтому не существует понятия this или super в статическом методе. Статический метод – это глобальная функция; следовательно, нет и полиморфизма, и, как следствие, переопределение метода не имеет смысла.

Однако это могло бы быть возможно, если бы MyClass был объектом во время выполнения, на котором вы вызывали бы метод, как в Smalltalk (или, может быть, в JRuby, как предположил один из комментаторов, но я не знаком с JRuby).

О, да… ещё один момент. Вы можете вызывать статический метод через объект obj1.staticMethod(), но это на самом деле синтаксический сахар для MyClass.staticMethod(), и этого следует избегать. Это обычно вызывает предупреждение в современных IDE. Не понимаю, почему они когда-либо разрешили этот сокращенный синтаксис.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь