Почему Java не позволяет переопределять статические методы? - PullRequest
497 голосов
/ 08 февраля 2010

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

Если возможно, пожалуйста, используйте пример.

Ответы [ 22 ]

469 голосов
/ 08 февраля 2010

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

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

174 голосов
/ 08 февраля 2010

Лично я считаю, что это недостаток в дизайне 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());
    }

    /* ... presumably lots of other code ... */
}

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

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

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

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

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

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

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

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

Обновление

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

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

38 голосов
/ 10 октября 2012

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

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

Файл Base.java:

package sp.trial;
public class Base {
  static void printValue() {
    System.out.println("  Called static Base method.");
  }
  void nonStatPrintValue() {
    System.out.println("  Called non-static Base method.");
  }
  void nonLocalIndirectStatMethod() {
    System.out.println("  Non-static calls overridden(?) static:");
    System.out.print("  ");
    this.printValue();
  }
}

Файл Child.java:

package sp.trial;
public class Child extends Base {
  static void printValue() {
    System.out.println("  Called static Child method.");
  }
  void nonStatPrintValue() {
    System.out.println("  Called non-static Child method.");
  }
  void localIndirectStatMethod() {
    System.out.println("  Non-static calls own static:");
    System.out.print("  ");
    printValue();
  }
  public static void main(String[] args) {
    System.out.println("Object: static type Base; runtime type Child:");
    Base base = new Child();
    base.printValue();
    base.nonStatPrintValue();
    System.out.println("Object: static type Child; runtime type Child:");
    Child child = new Child();
    child.printValue();
    child.nonStatPrintValue();
    System.out.println("Class: Child static call:");
    Child.printValue();
    System.out.println("Class: Base static call:");
    Base.printValue();
    System.out.println("Object: static/runtime type Child -- call static from non-static method of Child:");
    child.localIndirectStatMethod();
    System.out.println("Object: static/runtime type Child -- call static from non-static method of Base:");
    child.nonLocalIndirectStatMethod();
  }
}

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

Object: static type Base; runtime type Child.
  Called static Base method.
  Called non-static Child method.
Object: static type Child; runtime type Child.
  Called static Child method.
  Called non-static Child method.
Class: Child static call.
  Called static Child method.
Class: Base static call.
  Called static Base method.
Object: static/runtime type Child -- call static from non-static method of Child.
  Non-static calls own static.
    Called static Child method.
Object: static/runtime type Child -- call static from non-static method of Base.
  Non-static calls overridden(?) static.
    Called static Base method.

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

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

и последний кейс:

"При вызове статического метода из метода объекта класса выбран статический метод, доступный из самого класса, а не из класса, определяющего тип времени выполнения объекта . "

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

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

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

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

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

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

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

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

На данный момент в 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 как квалификаторов для static методов класса , Тогда нам всем придется привыкнуть к тому факту, что методы private static и public final не переопределяются и поэтому могут быть безопасно разрешены во время компиляции и «безопасны» для чтения как локальные ссылки.

22 голосов
/ 20 февраля 2013

На самом деле мы были не правы.
Несмотря на то, что Java не позволяет вам переопределять статические методы по умолчанию, если вы тщательно изучите документацию по классам классов и методов в 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 {
            // System.out.println(this.getClass().getDeclaredMethod(
            // "getBonusMultiplier").toString());
            try {
                return salary.multiply((BigDecimal) this.getClass()
                    .getDeclaredMethod("getBonusMultiplier").invoke(this));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        return null;
    }
    // ... presumably lots of other code ...
}

final class SpecialEmployee extends RegularEmployee {

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

public class StaticTestCoolMain {

    static public 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

;)

17 голосов
/ 08 февраля 2010

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

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

EDIT

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

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

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

ob2 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. Я не знаю, почему они позволили этот ярлык.

13 голосов
/ 20 сентября 2012

Переопределение метода стало возможным благодаря динамической диспетчеризации , что означает, что объявленный тип объекта определяет не его поведение, а тип времени выполнения:

Animal lassie = new Dog();
lassie.speak(); // outputs "woof!"
Animal kermit = new Frog();
kermit.speak(); // outputs "ribbit!"

Даже если lassie и kermit объявлены как объекты типа Animal, их поведение (метод .speak()) варьируется, поскольку динамическая диспетчеризация только bind вызов метода .speak() реализации во время выполнения, а не во время компиляции.

Теперь вот где ключевое слово static начинает иметь смысл: слово «статический» является антонимом для «динамического». Поэтому причина, по которой вы не можете переопределить статические методы, заключается в том, что нет динамической диспетчеризации статических элементов - , потому что static буквально означает «не динамический». Если они отправляются динамически (и, следовательно, могут быть переопределены), ключевое слово static просто не будет иметь смысла.

9 голосов
/ 29 октября 2017

Да. Практически Java допускает переопределение статического метода, и теоретически Нет, если вы переопределяете статический метод в Java, тогда он будет скомпилирован и работать гладко, но потеряет полиморфизм, который является базовым свойством Java. Вы будете читать везде, что вы не можете попробовать самостоятельно скомпилировать и запустить. вы получите ответ. например Если у вас есть Class Animal и статический метод eat (), и вы переопределяете этот статический метод в его подклассе, то давайте называть его Dog. Тогда, когда бы вы ни назначали объект Dog для ссылки на животных и вызывали eat () в соответствии с Java, должна быть вызвана функция eat () собаки, но в статических переопределенных животных будет вызываться функция eat ().

class Animal {
    public static void eat() {
        System.out.println("Animal Eating");
    }
}

class Dog extends Animal{
    public static void eat() {
        System.out.println("Dog Eating");
    }
}

class Test {
    public static void main(String args[]) {
       Animal obj= new Dog();//Dog object in animal
       obj.eat(); //should call dog's eat but it didn't
    }
}


Output Animal Eating

Согласно принципу полиморфизма Java, результат должен быть Dog Eating.
Но результат был другим, потому что для поддержки полиморфизма Java использует Late Binding, что означает, что методы вызываются только во время выполнения, но не в случае статических методов. В статических методах компилятор вызывает методы во время компиляции, а не во время выполнения, поэтому мы получаем методы в соответствии со ссылкой, а не со ссылкой на объект, содержащий ссылку, поэтому вы можете сказать, что практически он поддерживает статическое переопределение, но теоретически это не так. «т.

5 голосов
/ 14 января 2013

Ну ... ответ НЕТ, если вы думаете с точки зрения того, как переопределенный метод должен вести себя в Java. Но вы не получите никакой ошибки компилятора, если попытаетесь переопределить статический метод. Это означает, что если вы попытаетесь переопределить, Java не остановит вас; но вы, конечно, не получите тот же эффект, что и для нестатических методов. Переопределение в Java просто означает, что конкретный метод будет вызываться на основе типа времени выполнения объекта, а не типа его времени компиляции (что имеет место с переопределенными статическими методами). Хорошо ... есть предположения, почему они ведут себя странно? Поскольку они являются методами класса и, следовательно, доступ к ним всегда разрешается во время компиляции только с использованием информации о типе времени компиляции. Доступ к ним с использованием ссылок на объекты - это просто дополнительная свобода, предоставляемая разработчиками Java, и мы, конечно, не должны думать о том, чтобы прекращать эту практику только тогда, когда они ограничивают ее:

Пример : давайте попробуем посмотреть, что произойдет, если мы попробуем переопределить статический метод: -

class SuperClass {
// ......
public static void staticMethod() {
    System.out.println("SuperClass: inside staticMethod");
}
// ......
}

public class SubClass extends SuperClass {
// ......
// overriding the static method
public static void staticMethod() {
    System.out.println("SubClass: inside staticMethod");
}

// ......
public static void main(String[] args) {
    // ......
    SuperClass superClassWithSuperCons = new SuperClass();
    SuperClass superClassWithSubCons = new SubClass();
    SubClass subClassWithSubCons = new SubClass();

    superClassWithSuperCons.staticMethod();
    superClassWithSubCons.staticMethod();
    subClassWithSubCons.staticMethod();
    // ...
}
}

выход : -
SuperClass: inside staticMethod
SuperClass: inside staticMethod
SubClass: inside staticMethod

Обратите внимание на вторую строку вывода. Если бы staticMethod был переопределен, эта строка должна быть идентична третьей строке, так как мы вызываем функцию staticMethod () для объекта Runtime Type как «SubClass», а не как «SuperClass». Это подтверждает, что статические методы всегда разрешаются с использованием только их информации о типе времени компиляции.

5 голосов
/ 04 марта 2010

В Java (и во многих языках ООП, но я не могу говорить за всех; у некоторых вообще нет статического) все методы имеют фиксированную сигнатуру - параметры и типы. В виртуальном методе подразумевается первый параметр: ссылка на сам объект и при вызове изнутри объекта компилятор автоматически добавляет this.

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

Из-за этой разницы между ними; виртуальные методы всегда имеют ссылку на объект контекста (т. е. this), поэтому в кучи можно ссылаться на все, что принадлежит этому экземпляру объекта. Но со статическими методами, поскольку не передается ссылка, этот метод не может получить доступ к переменным и методам объекта, поскольку контекст не известен.

Если вы хотите, чтобы Java изменила определение так, чтобы контекст объекта передавался для каждого метода, статического или виртуального, то в сущности вы бы имели только виртуальные методы.

Как кто-то спросил в комментарии к оператору - какова ваша причина и цель, почему вы хотите эту функцию?

Я не знаю много Ruby, так как это было упомянуто OP, я провел некоторое исследование. Я вижу, что в классах Ruby действительно особый вид объектов, и можно создавать (даже динамически) новые методы. Классы являются полными объектами классов в Ruby, их нет в Java. Это то, что вы должны будете принять при работе с Java (или C #). Это не динамические языки, хотя C # добавляет некоторые формы динамических. На самом деле, в Ruby, насколько я мог найти, нет «статических» методов - в этом случае это методы объекта-синглтона. Затем вы можете переопределить этот синглтон новым классом, и методы в предыдущем объекте класса будут вызывать методы, определенные в новом классе (правильно?). Поэтому, если вы вызываете метод в контексте исходного класса, он все равно будет выполнять только исходную статику, но вызов метода в производном классе вызовет методы из родительского или подкласса. Интересно, и я вижу некоторую ценность в этом. Требуется другой шаблон мышления.

Поскольку вы работаете в Java, вам нужно будет приспособиться к такому способу действий. Почему они это сделали? Ну, возможно, чтобы улучшить производительность в то время на основе технологии и понимания, которое было доступно. Компьютерные языки постоянно развиваются. Вернитесь достаточно далеко, и ООП не будет. В будущем появятся другие новые идеи.

РЕДАКТИРОВАТЬ : еще один комментарий. Теперь, когда я вижу различия и, будучи самим разработчиком Java / C #, я могу понять, почему ответы, которые вы получаете от разработчиков Java, могут сбивать с толку, если вы работаете с таким языком, как Ruby. Методы Java static не совпадают с методами Ruby class. Разработчикам Java будет трудно понять это, так же как и тем, кто работает в основном с таким языком, как Ruby / Smalltalk. Я могу видеть, как это могло бы сильно сбить с толку тем фактом, что Java также использует «метод класса» как еще один способ говорить о статических методах, но этот же термин по-разному используется в Ruby. В Java нет методов класса Ruby (извините); В Ruby отсутствуют статические методы в стиле Java, которые на самом деле являются просто старыми функциями процедурного стиля, как показано в C.

Кстати - спасибо за вопрос! Сегодня я узнал что-то новое о методах класса (стиль Ruby).

4 голосов
/ 20 февраля 2013

Мне нравится и двойной комментарий Джея (https://stackoverflow.com/a/2223803/1517187).
Я согласен, что это плохой дизайн Java.
Многие другие языки поддерживают переопределение статических методов, как мы видели в предыдущих комментариях. Я чувствую, что Джей также пришел на Яву из Delphi, как я.
Delphi (Object Pascal) был первым языком, реализующим ООП.
Очевидно, что многие люди имели опыт работы с этим языком, поскольку в прошлом он был единственным языком для написания коммерческих продуктов с графическим интерфейсом. И - да, мы могли бы в Delphi переопределить статические методы. На самом деле, статические методы в Delphi называются «методами класса», в то время как Delphi имела другое понятие «статические методы Delphi», которые были методами с ранним связыванием. Чтобы переопределить методы, которые вам пришлось использовать с поздним связыванием, объявите «виртуальную» директиву. Так что это было очень удобно и интуитивно понятно, и я ожидал этого на Java.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...