问题 如何在Java中实现抽象静态方法?


关于包含静态抽象Java方法的不可能性存在许多问题。关于此的解决方法(设计缺陷/设计强度)也有很多。但我找不到任何关于我即将陈述的具体问题。

在我看来,制作Java的人和很多使用它的人并没有像我和其他许多人那样思考静态方法 - 作为类函数或属于类的方法而不是任何对象。那么还有其他一些实现类函数的方法吗?

这是我的例子:在数学中,a  是一组对象,可以使用某些操作*以某种合理的方式相互组合 - 例如,正实数在正常乘法下形成一个组(X * ÿ = X × ÿ),并且整数组形成一个组,其中'乘法'操作是加法( * ñ =  + ñ)。

在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());
  }
}

但是,团队还有另外一个重要的属性:每个团队都有一个 身份元素  - 一个元素 Ë 这样的 X * Ë = 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中是不兼容的。那么语言中是否有一些可用于执行此操作的相关概念?


10600
2018-03-17 15:44


起源

static 方法不是继承的...... - Luiggi Mendoza
它背后的想法是什么? static 意味着它不依赖于特定的实例 - 而是依赖于静态类型,和 abstract 意味着您希望调用动态类型。这是自相矛盾的。如果你发现自己需要一些如此古怪的东西 - 这是明确的 代码味道。 - amit
你有操作的对象吗?看起来这将是身份所属的地方,作为操作实例的一部分。不需要静电。 - Nathan Hughes
@LuiggiMendoza我确实读过它,但这不正确。 static 方法是像任何其他方法一样继承的。 stackoverflow.com/questions/10291949/... - Tristan
@ajb我想要一种方式来打电话 getIdentity 当你在编译时不知道实际的类。 - John Gowers


答案:


基本上你要求的是能够在编译时强制一个类定义一个具有特定签名的给定静态方法。

你不能用Java真正做到这一点,但问题是:你真的需要吗?

所以,假设您采用当前实现静态的选项 getIdentity() 在每个子类中。考虑到在你之前你实际上不需要这种方法 使用 它,当然,如果你试图使用它,但它没有定义,你  得到编译器错误提醒您定义它。

如果您定义它但签名不是“正确”,并且您尝试使用它而不是您定义它,那么您也将遇到编译器错误(关于使用无效参数调用它,或返回类型问题等)。 )。

因为你不能通过基类型调用子类静态方法,所以你是 总是 必须明确地打电话给他们,例如 GInteger.getIdentity()。而且,如果你试着打电话,编译器就会抱怨 GInteger.getIdentity() 什么时候 getIdentity() 没有定义,或者如果你使用不正确,你基本上获得编译时检查。当然,您唯一缺少的是能够强制定义静态方法,即使您从未在代码中使用过静态方法。

所以你已经非常接近了。

你的例子就是一个很好的例子 什么 你想要,但我会挑战你想出一个例子,其中有一个关于缺少静态函数的编译时警告是必要的;我唯一能想到的就是如果你正在创建一个供别人使用的库,你想确保你不要忘记实现一个特定的静态函数 - 但是对所有子类进行适当的单元测试也可以在编译期间捕获它(你无法测试 getIdentity() 如果它不存在)。

注意:查看您的新问题评论:如果您要求能力 呼叫 给出一个静态方法 Class<?>,你不能,本身(没有反射) - 但你仍然可以得到你想要的功能,如中所述 Giovanni Botta的回答;您将牺牲运行时检查的编译时检查,但能够使用标识编写通用算法。所以,这实际上取决于你的最终目标。


4
2018-03-17 15:58



我想我想出了一个很棒的场景 static abstract 会非常有用。事实上,我目前正在努力解决问题 static abstract 会让事情变得简单得多。这是解释它的问题: stackoverflow.com/questions/33326553/... - ryvantage
基本上,我有四个数据库表,可以用一个超类对象表示,但数据库表有不同的表名/列。 tablename实际上是一个属性 类而不是 例。因此,如果超类型可以强制子类型定义超类可以在静态上下文中使用的静态变量,那将是非常棒的。一个静态的抽象变量。 - ryvantage
难道你不能用非静态方法论证吗?看来你所建议的是首先忽略接口/抽象类的目的,这并没有真正帮助。 - Sambo


数学组只有一个特征操作,但Java类可以有任意数量的操作。因此这两个概念不匹配。

我可以想象像Java类这样的东西 Group 由a组成 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
2018-03-17 16:00





你的推理可能会有一些误解。你看到一个数学“组”就像你定义它(如果我能记得很清楚);但它的要素并不是因为它们属于这一群体。我的意思是整数(或实数)是一个独立的实体,也属于组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
2018-03-17 16:01



我同意。建模必须在组级别完成,而不是在组元素级别完成。这将解决许多问题,并为更合适的面向对象模型做出贡献。 - Giovanni Botta


没有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 */ }

您可以使用a轻松实现这些方法 类到实例映射 或者是自己选择的家庭解决方案。关键是你要保留一个静态的身份元素图,每个元素一个 GroupElement 实现。

这里需要一个约定:每个子类 GroupElement 必须静态声明自己的标识元素,例如,

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

在里面 identity 你可以抛出一个方法 RuntimeException 如果身份从未注册过。这不会给你静态检查,但至少运行时检查你的 GroupElement 类。

替代方案更加冗长,需要您实例化 GroupElement 仅通过工厂的类,它还将负责返回标识元素(和其他类似的对象/函数):

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

这是一种通常在企业应用程序中使用的模式,当工厂通过应用程序中的某个依赖注入框架(Guice,Spring)注入时,它可能过于冗长,难以维护并且可能对您来说过度杀伤。

编辑:在阅读了其他一些答案之后,我同意您应该在组级别而不是组元素级别进行建模,因为元素类型可以在不同的组之间共享。尽管如此,上述答案提供了一种强制执行您描述的行为的一般模式。

编辑2:通过上面的“编码惯例”,我的意思是有一个静态方法 getIdentity 在每个子类中 GroupElement,正如一些人所说。这种方法的缺点是不允许针对该组编写通用算法。再次,最好的解决方案是第一次编辑中提到的那个。


2
2018-03-17 16:01



这基本上与OP提出的问题基本相同,只是形式不同。在OP想要强制定义特定静态方法的情况下,该解决方案仍然需要执行特定的静态初始化步骤,并且该步骤也不是强制执行的。无论如何,必须完成编译器不强制执行的某些静态操作。在OP的特定情况下,此解决方案的另一个问题是在运行时之前不会捕获错误。 - Jason C
正如我在答案的开头指出的那样,任何解决方案都会在Java中产生这种缺点。无法静态强制执行此行为,因此必须通过运行时检查强制执行此行为。 - Giovanni Botta
你可以得到 关 但是编译时检查。考虑: GInteger.getIdentity() 如果身份(在编译时将失败)getIdentity())没有定义。 - Jason C
这只是一个惯例,并且不允许您在组上编写通用算法,而我的解决方案确实如此。但是,正如我在答案的最后提到的那样,最好的解决方案是完全改变方法并对组进行建模而不是对其元素进行建模。 - Giovanni Botta
当我说组的通用算法时,这就是我的意思:为了获得身份,您不需要事先知道类型,因此您可以为任何组编写通用算法。 - Giovanni Botta


如果你需要能够在编译时生成一个不知道类的标识,那么第一个问题是,你怎么知道在运行时你想要什么类?如果该类基于其他一些对象,那么我认为最干净的方法是在超类中定义一个方法,这意味着“获取一个类与其他一些对象相同的标识”。

public GroupElement getIdentitySameClass();

必须在每个子类中重写。覆盖可能不会使用该对象;该对象仅用于选择正确的对象 getIdentity 多态调用。最有可能的是,你也想要一个静态的 getIdentity 在每个类中(但我不知道编译器强制写一个),所以子类中的代码可能看起来像

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

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

另一方面,如果你需要的课程来自a Class<T> 对象,我认为你需要使用反射开始 getMethod。或者看看Giovanni的答案,我认为这个答案更好。


1
2018-03-17 16:11





我们都同意,如果你想实现组,你将需要一个组接口和类。

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(); 
}

1
2018-03-17 15:58





“方法getIdentity不依赖于对象的任何实例,因此应该声明为static”

实际上,如果它不依赖于任何实例,它只能返回一些常量值,它不必是静态的。

仅仅因为静态方法不依赖于实例,并不意味着您应该始终将它用于此类情况。


0



这符合我的条件(2)吗?我绝对打算参加 getIdentity 返回常量值的方法,但如果尝试从静态上下文调用该方法,则会出现错误。 - John Gowers
我认为关键是你可能想要在没有任何实例的情况下获得身份。 - Giovanni Botta