Как реализовать абстрактные статические методы в 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());
  }
}

Однако существует еще одно важное свойство, которое имеет группа: каждая группа имеет элемент identity - элемент 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, который:

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

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

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

Математическая группа имеет только одну характеристическую операцию, однако класс 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

В ваших рассуждениях может возникнуть некоторая недоразумение. Вы видите математическую "Группу", как вы ее определяете (если я хорошо помню); но его элементы не характеризуются тем фактом, что они принадлежат к этой группе. Я имею в виду, что целое число (или реальное) является автономным объектом, который также относится к группе 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
}

Все это идея; Я на самом деле не касался математики в течение долгого времени, поэтому я мог бы быть неправым.

Удачи!

Ответ 4

Не существует 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 */ }

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

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

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

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

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

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

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

РЕДАКТИРОВАТЬ: прочитав некоторые другие ответы, я согласен с тем, что вы должны моделировать на уровне группы, а не на уровне элементов группы, поскольку типы элементов могут быть разделены между разными группами. Тем не менее, приведенные ответы дают общую схему для обеспечения поведения, которое вы описываете.

РЕДАКТИРОВАТЬ 2. Посредством "соглашения о кодировании" выше я имею в виду статический метод getIdentity в каждом подклассе GroupElement, как упоминалось некоторыми. Этот подход имеет нижнюю сторону, не позволяющую создавать общие алгоритмы против группы. Еще раз, лучшим решением для этого является тот, который упоминается в первом редактировании.

Ответ 5

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

public GroupElement getIdentitySameClass();

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

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

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

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

Ответ 6

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

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

Мы реализуем группы как синглтоны, чтобы мы могли статически ставить 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(); 
}

Ответ 7

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

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

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