为什么Java中静态方法不能是抽象的?

666
问题在于为什么Java中不能定义抽象静态方法?例如:
abstract class foo {
    abstract void bar( ); // <-- this is ok
    abstract static void bar2(); //<-- this isn't why?
}

14
少数原因:静态方法必须具有主体,即使它们是抽象类的一部分,因为不需要创建类的实例来访问其静态方法。另一种思考方式是,如果我们暂时假设允许这样做,那么问题在于静态方法调用不提供任何运行时类型信息(RTTI),请记住不需要创建实例,因此它们无法重定向到它们特定的重写实现,从而允许抽象静态根本毫无意义。换句话说,它不能提供任何多态性好处,因此不被允许。 - sactiw
这个问题是大约15年前提出的。难道没有一种方法可以在预编译时使用(popper)注解来强烈表示一个抽象类中的静态方法应该被重写吗? - undefined
30个回答

599
因为"抽象"意味着:"没有实现功能",而"静态"意味着:"即使您没有对象实例也有功能"。这是一个逻辑矛盾。

416
更简洁的回答是“糟糕的语言设计。”“Static”应该意味着“属于类”,因为这正是直觉上的使用方式,正如这个问题所展示的那样。请参见Python中的“classmethod”。 - Alexander Ljungberg
14
@Tomalak,我很抱歉,我的表述不够清晰。当然,静态方法“属于类”。但是,它只是存在于相同的命名空间中。一个静态方法并不是类对象本身的方法:它不使用“this”作为类对象,并且在继承链中没有正确参与。如果它真正是一个类方法,那么“abstract static”的写法就非常合理了。它将成为类对象本身的一个方法,子类对象必须实现该方法。当然,尽管我对这种语言有所抱怨,但你的回答是正确的。 - Alexander Ljungberg
794
这不是逻辑矛盾,而是语言上的缺陷,许多其他语言支持这个概念。“抽象”意味着“在子类中实现”, “静态”的意思是“在类上执行而不是类实例上执行”。没有逻辑矛盾。 - Eric Grange
12
@Eric: 而且,你所说的并不适用于 "abstract static":一个在子类中实现的函数 X 不能同时在类上执行 - 只能在子类上执行。这时它就不再是抽象的了。 - Tomalak
88
@Tomakak:你的逻辑是循环的。static并不意味着“非空”的,那只是Java不允许静态方法是抽象的结果。它的意思是“可在类上调用”(它应该意味着“仅可在类上调用”,但这是另一个问题)。如果Java支持abstract static方法,我期望它意味着1)该方法必须由子类实现,2)该方法是子类的类方法。一些方法作为实例方法就没有意义。不幸的是,在创建抽象基类(或接口)时,Java不允许您指定这一点。 - Michael Carman
显示剩余22条评论

386

语言设计不佳。直接调用静态抽象方法比创建实例仅仅为了使用该抽象方法更有效。特别是在将抽象类作为枚举无法扩展的解决方法时,这一点尤其正确,这也是另一个糟糕的设计示例。希望他们在下一个版本中解决这些限制。


33
Java充满了奇怪的限制,闭包只能访问final变量就是其中之一。而且还有很多其他限制,几乎是无穷无尽的。Java程序员的工作就是了解这些限制及其解决方法。为了在业余时间玩得更开心,我们需要使用比Java更好的东西。但我不会担心这个,因为这正是实现进步的种子。 - ceving
22
我认为你所提到的“不好的语言设计”实际上更像是“保护性语言设计”,其目的是限制程序员由于不必要的语言特性而违反OO原则。 - ethanfar
30
“抽象静态”这个概念是否违反了面向对象原则? - Trevor
10
@threed,完全不是这样,但当然有人说“static”这个概念本身就已经违反了某些规定... - Pacerier
8
静态需求的存在清楚地表明,“面向对象原则”并不像通常所声称的那样全面。 - Ben
显示剩余2条评论

153

你无法覆盖静态方法,因此将其声明为抽象方法是没有意义的。此外,在抽象类中声明一个静态方法将使该方法属于该类,而不是覆盖该方法的类,所以无法使用。


27
很遗憾,Java中的静态方法无法被覆盖。 - Michel
12
@Michel: 有什么意义呢?如果你想要基于实例的行为,使用实例方法即可。 - Ran Biron
9
这个回答是不正确的。抽象类中的静态方法可以正常工作并且经常被使用。只是该类本身的静态方法可能不是抽象的。@Michel 覆盖静态方法是没有意义的。没有实例,运行时如何知道要调用哪个方法? - erickson
72
即使没有实例,类层次结构仍然完整 - 静态方法的继承可以像实例方法的继承一样工作。 Smalltalk 就是这样做的,并且非常有用。 - Jared
9
@matiasg 这并不是什么新鲜事。抽象类一直允许拥有静态的、非抽象的方法。 - Matt Ball
显示剩余3条评论

100
abstract注解应用于方法时,表示该方法必须在子类中被覆盖。
在Java中,static成员(方法或字段)不能被子类重写(在其他面向对象语言中不一定如此,例如SmallTalk)。static成员可以被隐藏,但这与重写根本不同。
由于静态成员不能在子类中被重写,因此无法对它们应用abstract注解。
另外,其他编程语言也支持静态继承,就像实例继承一样。从语法角度来看,这些语言通常需要在声明中包含类名。例如,在Java中,假设您正在编写ClassA的代码,以下语句等效(如果methodA()是静态方法,并且没有具有相同签名的实例方法):
ClassA.methodA();

methodA();
在SmallTalk中,类名是必需的,因此语法为(请注意,SmallTalk不使用“.”来分隔“主语”和“谓语”,而是将其用作语句终止符):
ClassA methodA.

由于类名总是必需的,因此可以通过遍历类层次结构来确定方法的正确“版本”。值得一提的是,我偶尔会想念静态继承,并且在最初接触Java时缺乏静态继承而受到了伤害。此外,SmallTalk是鸭子类型的(因此不支持契约式编程)。因此,它没有类成员的抽象修饰符。


2
“静态成员不能被子类覆盖”是错误的。至少在Java6中是可能的。不确定从什么时候开始出现这种情况。 - Steven De Groote
23
静态成员确实无法被子类覆盖。如果子类具有与超类中静态方法相同签名的静态方法,则它不会覆盖它,而是隐藏它。http://docs.oracle.com/javase/tutorial/java/IandI/override.html 不同之处在于多态性仅适用于被覆盖,而不适用于隐藏方法。 - John29
3
谢谢您的澄清,除了名称上的区别之外,它在使用上似乎相似。 - Steven De Groote
2
@Steven De Groote 是的,用法类似,但行为不同。这就是为什么没有静态抽象方法的原因 - 如果它们不支持多态性,那么静态抽象方法有什么意义呢? - John29
5
当在超类中调用该方法时,差异就会变得明显。假设Super.foo调用了Super.bar。如果子类实现了Subclass.bar并调用foo,则foo仍将调用Super.bar,而不是Subclass.bar。因此,你实际上有两个完全不同且不相关的方法都被称为"bar"。在任何有用意义上,这都不是重写。 - Doradus
我认为这是一个限制(在2023年)。我需要一个 Operation 抽象类和几个操作子类,每个子类都有静态的 newReq()newRsp(),但是我无法在 Operation 类中声明抽象静态方法 newReq()newRsp() - zipper

14

我也问过同样的问题,这是为什么:

因为抽象类表示不会提供实现并允许子类提供实现。

因此,子类必须重写超类的方法,

规则1 - 静态方法不能被重写

因为静态成员和方法是编译时元素,所以允许静态方法的重载(编译时多态)而不是重写(运行时多态)

所以它们不能是抽象的。

没有像abstract static这样的东西 <--- 在Java世界中不被允许


5
“Java不允许静态方法被覆盖是因为静态成员和方法是编译时元素”这种说法不正确。使用abstract static可以进行静态类型检查,详见https://dev59.com/_3RC5IYBdhLWcg3wOeSB#rDAPoYgBc1ULPQZFFA4V 。Java不允许静态方法被覆盖的真正原因是因为Java本身不支持静态方法被覆盖。 - Pacerier
重载与多态无关。除了在某种程度上Java和JavaScript都带有"Java"这个前缀之外,重载和覆盖没有任何共同点。一个方法的名称并不是识别它的唯一标志,而是它的签名。因此,foo(String)foo(Integer)并不相同,仅此而已。 - Captain Man
@CaptainMan 过载通常被称为“参数多态”,因为根据参数的类型,会调用不同的方法,这就是多态。 - Davor

11

这是一种糟糕的语言设计,而且没有理由不能实现。

事实上,在这里有一种模式或方法,可以在Java中模仿它,至少可以修改自己的实现:

public static abstract class Request {                 

        // Static method
        public static void doSomething() {
                get().doSomethingImpl();
        }
        
        // Abstract method
        abstract void doSomethingImpl();

        /////////////////////////////////////////////
        private static Request SINGLETON;
        private static Request get() {
            if ( SINGLETON == null ) {
                // If set(request) is never called prior,
                // it will use a default implementation. 
                return SINGLETON = new RequestImplementationDefault();
            }
            return SINGLETON;
        }
        public static Request set(Request instance){
            return SINGLETON = instance;
        }
        /////////////////////////////////////////////
}

两种实现:

/////////////////////////////////////////////////////

public static final class RequestImplementationDefault extends Request {
        @Override void doSomethingImpl() {
                System.out.println("I am doing something AAA");
        }
}

/////////////////////////////////////////////////////

public static final class RequestImplementaionTest extends Request {
        @Override void doSomethingImpl() {
                System.out.println("I am doing something BBB");
        }
}

/////////////////////////////////////////////////////

可以按如下方式使用:

Request.set(new RequestImplementationDefault());

// Or

Request.set(new RequestImplementationTest());

// Later in the application you might use

Request.doSomething();

这将使你能够以静态方式调用方法,但仍能够更改实现,例如在测试环境中。

理论上,您也可以在ThreadLocal上执行此操作,并且能够针对每个线程上下文设置实例,而不是全局设置,如此处所示,然后就可以执行Request.withRequest(anotherRequestImpl, () -> { ... })或类似操作。

现实世界通常不需要使用ThreadLocal方法,通常只需能够全局地更改测试环境的实现即可。

请注意,这仅是一种模式,旨在通过稍微复杂的实现来解决通常无法修改的静态代码的问题,以便保留静态方法提供的直接、简单和干净的调用方法,并能够在需要时切换实现。


9
我不明白你在哪里提供了一个被问题要求的 抽象静态 方法示例,并且你用粗体字写着 可以在Java中完成。这完全是误导。 - Blip
2
它本身并不允许您将其定义为抽象静态,但是您可以通过使用此模式/技巧实现类似的结果,从而可以更改s静态方法的实现。这几乎是误导性的。它是可行的,但使用不同的语义。 - mjs
这是一个扩展抽象类的示例,然后将静态方法放在子类中。这不是任何一种方式可以在Java中完成的示例。 - Scuba Steve
3
@ScubaSteve,首先你的结论是错误的。其次,它可以达到相同的结果。这意味着类的静态访问可以被另一种实现方式改变。这不是说我将静态关键字抽象化的答案,而是使用这个模式,您仍然可以使用静态方法并改变它们的实现方式。但它只在全局范围内起作用,这是一个负面影响,但对于测试/生产/开发环境,这对我们来说已经足够了。 - mjs

6
根据定义,静态方法不需要了解“this”的情况。因此,它不能是虚拟方法(根据通过“this”可用的动态子类信息进行重载);相反,静态方法重载仅基于在编译时可用的信息(这意味着:一旦引用超类的静态方法,您调用的是超类方法,而不是子类方法)。
基于这个原因,抽象静态方法将是相当无用的,因为您永远不会用其引用替换某些已定义的内容。

5
  • 抽象方法只定义在超类中以便可以在子类中被覆盖。然而,静态方法不能被覆盖。因此,在一个抽象的静态方法中编译时就会出错。

    现在的问题是为什么静态方法不能被覆盖呢?

  • 这是因为静态方法属于特定的类而不是它的实例。如果你尝试覆盖一个静态方法,你将不会得到任何编译或运行时错误,但编译器会隐藏超类的静态方法。


2

假设有两个类,ParentChild。其中 Parent 是抽象类。声明如下:

abstract class Parent {
    abstract void run();
}

class Child extends Parent {
    void run() {}
}

这意味着任何一个Parent的实例必须指定如何执行run()方法。
然而,假设现在Parent不是abstract
class Parent {
    static void run() {}
}

这意味着Parent.run()将执行静态方法。
抽象方法的定义是“声明但未实现的方法”,这意味着它本身不返回任何内容。
静态方法的定义是“对于调用它的实例,无论参数如何,都返回相同的值”。
抽象方法的返回值会随着实例的变化而改变。静态方法则不会。静态抽象方法基本上是一个返回值恒定但不返回任何内容的方法。这是一个逻辑矛盾。
此外,真的没有什么理由使用静态抽象方法。

2
我发现已经有很多答案了,但是我没有看到实际的解决方案。当然,这是一个真正的问题,在Java中没有排除这种语法的好理由。由于原始问题缺乏可能需要这种语法的上下文,我提供上下文和解决方案:
假设你有一堆相同的类中的静态方法。这些方法调用一个特定于类的静态方法:
class C1 {
    static void doWork() {
        ...
        for (int k: list)
            doMoreWork(k);
        ...
    }
    private static void doMoreWork(int k) {
        // code specific to class C1
    }
}
class C2 {
    static void doWork() {
        ...
        for (int k: list)
            doMoreWork(k);
        ...
    }
    private static void doMoreWork(int k) {
        // code specific to class C2
    }
}

C1C2中的doWork()方法是相同的。可能会有很多这样的类:C3C4等。如果允许使用static abstract,则可以通过执行以下操作来消除重复代码:

abstract class C {
    static void doWork() {
        ...
        for (int k: list)
            doMoreWork(k);
        ...
    }

    static abstract void doMoreWork(int k);
}

class C1 extends C {
    private static void doMoreWork(int k) {
        // code for class C1
    }
}

class C2 extends C {
    private static void doMoreWork(int k) {
        // code for class C2
    }
}

但是这段代码无法编译,因为不允许使用 static abstract 的组合。 然而,可以通过使用 static class 结构来绕过此问题,因为它是被允许的:

abstract class C {
    void doWork() {
        ...
        for (int k: list)
            doMoreWork(k);
        ...
    }
    abstract void doMoreWork(int k);
}
class C1 {
    private static final C c = new  C(){  
        @Override void doMoreWork(int k) {
            System.out.println("code for C1");
        }
    };
    public static void doWork() {
        c.doWork();
    }
}
class C2 {
    private static final C c = new C() {
        @Override void doMoreWork(int k) {
            System.out.println("code for C2");
        }
    };
    public static void doWork() {
        c.doWork();
    }
}

通过这个解决方案,唯一重复的代码是:

    public static void doWork() {
        c.doWork();
    }

2
由于在您的最终解决方案中,抽象类C没有任何静态方法,那么为什么不让C1和C2扩展它并覆盖doMoreWork()方法,并让任何其他类创建其实例并调用所需的方法。基本上,您正在执行相同的操作,即使用匿名类扩展类C,然后在C1和C2中使用其静态实例以允许从静态方法中访问,但这根本不是必需的。 - sactiw
1
我不理解你在这里提供的上下文。在你最终的解决方案中,你可以调用C1.doWork()或者C2.doWork(),但是不能调用C.doWork()。此外,在你提供的例子中,假设它被允许运行,那么C类将怎样找到doMoreWork()的实现呢?最后,我要说你所提供的上下文代码是一个糟糕的设计。为什么?因为你创建了一个独立的函数来处理唯一的代码,而不是为共同的代码创建一个函数,再在C类中实现一个静态函数。这样更容易! - Blip

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接