it-roy-ru.com

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

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

Мне кажется, что люди, которые создали Java, и довольно много людей, которые ее используют, не думают о статических методах так, как я и многие другие - как функции класса или методы, принадлежащие классу. а не к какому-либо объекту. Так есть ли другой способ реализации функции класса? 

Вот мой пример: в математике группа - это набор объектов, которые могут быть составлены друг с другом с помощью некоторой операции * некоторым разумным способом - например, положительные действительные числа образуют группу при нормальном умножении (x * y = x × y), а набор целых чисел образует группу, в которой операция умножения является сложением (m * n = m + n). 

Естественным способом моделирования этого в Java является определение интерфейса (или абстрактного класса) для групп:

public interface GroupElement
{
  /**
  /* Composes with a new group element.
  /* @param elementToComposeWith - the new group element to compose with.
  /* @return The composition of the two elements.
   */
  public GroupElement compose(GroupElement elementToComposeWith)
}

Мы можем реализовать этот интерфейс для двух приведенных выше примеров: 

public class PosReal implements GroupElement
{
  private double value;

  // getter and setter for this field

  public PosReal(double value)
  {
    setValue(value);
  }

  @Override
  public PosReal compose(PosReal multiplier)
  {
    return new PosReal(value * multiplier.getValue());
  }
}

а также

public class GInteger implements GroupElement
{
  private int value;

  // getter and setter for this field

  public GInteger(double value)
  {
    setValue(value);
  }

  @Override
  public GInteger compose(GInteger addend)
  {
    return new GInteger(value + addend.getValue());
  }
}

Тем не менее, есть еще одно важное свойство, которым обладает группа: каждая группа имеет элемент идентификации - элемент e, такой что x * e = x для все x в группе. Например, элемент тождественности для положительных вещественных чисел при умножении равен 1, а элемент тождественности для целых чисел при сложении - 0. В этом случае имеет смысл иметь метод для каждого реализующего класса, подобный следующему: 

public PosReal getIdentity()
{
  return new PosReal(1);
}

public GInteger getIdentity()
{
  return new GInteger(0);
}

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

Есть ли способ реализовать этот метод getIdentity, который: 

  1. Обеспечивает согласованность всех реализаций GroupElement, поэтому каждая реализация GroupElement принудительно включает функцию getIdentity.
  2. Ведет себя статически; то есть мы можем получить элемент идентификации для данной реализации GroupElement, не создавая экземпляр объекта для этой реализации. 

Условие (1) по существу говорит «абстрактно», а условие (2) говорит «статично», и я знаю, что static и abstract несовместимы в Java. Так есть ли в языке некоторые связанные понятия, которые можно использовать для этого?

13
John Gowers

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

Вы не можете сделать это на Java, но вопрос в том, действительно ли вам это нужно?

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

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

Поскольку вы не можете вызывать статические методы с подклассами через базовый тип, вам всегда нужно явно вызывать их всегда, например, GInteger.getIdentity(). А поскольку компилятор уже будет жаловаться, если вы попытаетесь вызвать GInteger.getIdentity(), когда getIdentity() не определена, или если вы используете его неправильно, вы по существу получаете проверку во время компиляции. Конечно, единственное, чего вам не хватает, - это способности обеспечить определение статического метода, даже если вы никогда не используете его в своем коде.

Так что то, что у вас уже есть, довольно близко.

Ваш пример - хороший пример, который объясняет что вы хотите, но я бы предложил вам привести пример, когда во время компиляции необходимо предупредить об отсутствующей статической функции; Единственное, о чем я могу подумать, так это то, что если вы создаете библиотеку для использования другими, и вы хотите убедиться, что вы не забыли реализовать определенную статическую функцию - но провести надлежащее модульное тестирование всех ваших подклассов. может поймать это и во время компиляции (вы не можете протестировать функцию getIdentity(), если она отсутствует).

Примечание: глядя на ваш новый комментарий к вопросу: если вы запрашиваете возможность вызывать статический метод с заданным Class<?>, вы не можете, по сути (без размышления), - но вы все равно можете получить желаемую функциональность, как описано в ответ Джованни Ботта ; вы пожертвуете проверками во время компиляции для проверок во время выполнения, но получите возможность писать универсальные алгоритмы, используя идентичность. Итак, это действительно зависит от вашей конечной цели.

5
Jason C

Математическая группа имеет только одну характеристическую операцию, однако класс Java может иметь любое количество операций. Поэтому эти два понятия не совпадают.

Я могу представить что-то вроде Java-класса Group, состоящего из Set элементов и конкретной операции, которая сама по себе будет интерфейсом. Что-то вроде

public interface Operation<E> {
   public E apply(E left, E right);
}

С этим вы можете построить свою группу:

public abstract class Group<E, O extends Operation<E>> {
    public abstract E getIdentityElement();
}

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

3
Ray

В ваших рассуждениях могут быть некоторые недоразумения. Вы видите математическую «Группу», как вы ее определяете (если я хорошо помню); но его элементы не характеризуются тем, что они принадлежат к этой группе. Я имею в виду, что целое число (или действительное число) - это отдельная сущность, которая также принадлежит группе XXX (среди других ее свойств).

Итак, в контексте программирования я бы отделил определение (class) формы группы от определения ее членов, возможно, используя обобщенные формы:

interface Group<T> {
    T getIdentity();
    T compose(T, T);
}

Еще более аналитическое определение будет:

/** T1: left operand type, T2: right ..., R: result type */
interface BinaryOperator<T1, T2, R> {
    R operate(T1 a, T2 b);
}

/** BinaryOperator<T,T,T> is a function TxT -> T */
interface Group<T, BinaryOperator<T,T,T>> {
    void setOperator(BinaryOperator<T,T,T> op);
    BinaryOperator<T,T,T> getOperator();
    T getIdentity();
    T compose(T, T); // uses the operator
}

Все это идея; На самом деле я давно не касался математики, поэтому могу ошибаться.

Повеселись!

3
Nikos Paraskevopoulos

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

Типичный способ, которым это делается в Java, состоит в том, чтобы ваш интерфейс GroupElement объявлял два статических метода, таких как этот:

public static <T extends GroupElement> 
  T identity(Class<T> type){ /* implementation omitted */ }

static <T extends GroupElement> 
  void registerIdentity(Class<T> type, T identity){ /* implementation omitted */ }

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

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

public class SomeGroupElement implements GroupElement{
  static{
    GroupElement.registerIdentity(SomeGroupElement.class, 
      /* create the identity here */);
  }
}

В методе identity вы можете выбросить RuntimeException, если личность никогда не была зарегистрирована. Это не даст вам статическую проверку, но, по крайней мере, проверку во время выполнения для ваших классов GroupElement.

Альтернатива этому немного более многословна и требует от вас создания экземпляров ваших классов GroupElement только через фабрику, которая также позаботится о возвращении элемента identity (и других подобных объектов/функций):

public interface GroupElementFactory<T extends GroupElement>{
  T instance();
  T identity();
}

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

EDIT: после прочтения некоторых других ответов я согласен с тем, что вам следует моделировать на уровне группы, а не на уровне элементов группы, поскольку типы элементов могут совместно использоваться различными группами. Тем не менее, приведенные выше ответы обеспечивают общий шаблон для обеспечения поведения, которое вы описываете.

EDIT 2: Под «соглашением о кодировании» я подразумеваю наличие статического метода getIdentity в каждом подклассе GroupElement, как упоминалось некоторыми. У этого подхода есть и обратная сторона: нельзя использовать общие алгоритмы для группы. Еще раз, лучшее решение для этого - то, которое упомянуто в первом редактировании.

2
Giovanni Botta

Если вам нужна возможность генерировать идентичность, когда класс неизвестен во время компиляции, первый вопрос: как узнать во время выполнения, какой класс вы хотите? Если класс основан на каком-то другом объекте, то я думаю, что самый простой способ - определить метод в суперклассе, который означает «получить идентификатор, класс которого совпадает с» некоторого другого объекта.

public GroupElement getIdentitySameClass();

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

public static GInteger getIdentity() { ... whatever }

@Override
public GInteger getIdentitySameClass() { return getIdentity(); }

С другой стороны, если нужный вам класс происходит от объекта Class<T>, я думаю, вам нужно использовать отражение, начинающееся с getMethod . Или посмотрите ответ Джованни, который я считаю лучше.

1
ajb

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

public interface Group<MyGroupElement extends GroupElement>{
    public MyGroupElement getIdentity()
}

Мы реализуем группы как singletons , поэтому мы можем получить статический доступ к getIdentity через instance.

public class GIntegerGroup implements Group<GInteger>{

    // singleton stuff
    public final static instance = new GIntgerGroup();
    private GIntgerGroup(){};

    public GInteger getIdentity(){
        return new GInteger(0);
    }
}

public class PosRealGroup implements Group<PosReal>{

    // singleton stuff
    public final static instance = new PosRealGroup();
    private PosRealGroup(){}        

    public PosReal getIdentity(){
        return new PosReal(1);
    }
}

если нам также понадобится получить удостоверение от элемента группы, я обновлю ваш интерфейс GroupElement:

public Group<GroupElement> getGroup();

и GInteger с:

public GIntegerGroup getGroup(){
    return GIntegerGroup.getInstance(); 
}

и PosReal с:

public PosRealGroup getGroup(){
    return PosRealGroup.getInstance(); 
}
1
Colin

«метод getIdentity не зависит от какого-либо экземпляра объекта и поэтому должен быть объявлен как статический»

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

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

0
Leo