为什么我们不应该在Java中使用protected static

139

6
只要一个静态保护字段是“final”,就没有任何问题。跨类共享可变静态字段肯定会引起担忧。多个类更新静态字段不太可靠或易于理解,特别是由于存在任何受保护的字段或方法意味着该类旨在被其他包中的类扩展,可能是不受包含受保护字段类的作者控制的类。 - VGR
6
@VGR,“final”并不意味着字段是不可变的。您始终可以修改由“final”引用变量引用的“object”。 - Zeeshan
@VGR 我不同意。你创建静态变量的唯一原因是为了通过继承仅从另一个包中访问它,并且访问单个字段不应该是继承的原因。在我看来,这是一个有缺陷的设计,如果你采用这种方式,你应该重新考虑你的应用程序结构。这只是我的观点。 - Dioxin
1
@LoneRider 你说得对。我当时想的是不可变的,但是final并不能保证这一点。 - VGR
我也是从同一个问题来到这里的。 - Raj Rajeshwar Singh Rathore
8个回答

95

这更多是一个风格问题,而不是直接的问题。它表明您没有正确地思考类中正在发生的事情。

想一想 static 是什么意思:

这个变量存在于类级别,它不为每个实例单独存在,并且在扩展我的类中它没有独立的存在

再想一想 protected 是什么意思:

这个变量可以被这个类和同一包内的类以及继承我的类看到。

这两个含义不完全互斥,但非常接近。

我唯一能想到需要同时使用这两个关键字的情况是:如果您有一个旨在被扩展的抽象类,那么扩展类就可以使用原始类中定义的常量来修改其行为。然而,这种排列方式最终可能会变得非常混乱,并表示类设计上的弱点。

在大多数情况下,最好将常量定义为公共的,因为这样做会让所有东西都变得更加清晰,并允许子类更灵活地进行扩展。除此之外,在许多情况下,组合比继承更可取,而抽象类则强制实现继承。

为了看到一个如何破坏事物的例子,并且说明变量没有独立存在的含义,请尝试以下示例代码:

public class Program {
    public static void main (String[] args) throws java.lang.Exception {
        System.out.println(new Test2().getTest());
        Test.test = "changed";
        System.out.println(new Test2().getTest());
    }
}

abstract class Test {
    protected static String test = "test";
}

class Test2 extends Test {
    public String getTest() {
        return test;
    }
}

您将看到结果:

test
changed

你可以在此处进行尝试:https://ideone.com/KM8u8O

Test2类能够访问Test的静态成员test而无需限定名称,但它不会继承或拥有自己的副本。它正在查看内存中完全相同的对象。


5
你们过于关注继承了。这种情况通常出现在某人想要在不将其设置为公共的情况下获得包访问权限(例如单元测试)时。 - spudone
3
通常情况下,单元测试文件会被放在与被测试代码相同的包中。为了让它们可以访问被测试代码,可以使用默认(包)访问级别。受保护的访问级别可以让子类也能访问,但这对于单元测试来说并不是必要或相关的。 - Tim B
1
@Kamafeather Protected 的意思是子类可以看到你,同一包中的任何类也可以看到你。所以是的,Program 在同一个包中,因此可以看到受保护的成员。 - Tim B
3
the extending class could then modify the behavior” 的意思是“派生类可以修改行为”。这将违反里氏替换原则。子类不应该修改其超类型的行为。这属于“正方形不是矩形”的论点:调整超类(Rectangle)以调整宽度/高度以确保相等性,对于Square将会产生不希望的结果,如果你用其子类型的实例替换每个超类型。 - Dioxin
1
“我唯一能想到可以同时使用这两种方法的情况是,如果您有一个旨在被扩展的抽象类,扩展类可以使用原始代码中定义的常量来修改其行为。”这句话意思有点隐晦,但确实存在于其中。代码示例也表达了此声明。 - Dioxin
显示剩余6条评论

34

之所以不被赞同是因为它是自相矛盾的。

将变量设为protected意味着它将在包内使用或在子类中继承使用。

将变量设为static将其作为类成员,消除了继承的意图。这只留下了在包内使用的意图,而我们有package-private来实现这一点(没有修饰符)。

我唯一能找到这个有用的情况是,如果你正在声明一个应该用于启动应用程序的类(就像JavaFX的Application#launch),并且只希望能够从子类启动。如果这样做,请确保该方法也是final以防止隐藏。但这不是“常规”,可能是为了防止通过添加新的应用程序启动方式来增加复杂性而实现的。

要查看每个修饰符的访问级别,请参见:Java教程-控制对类成员的访问


5
我不明白为什么使用“static”就能消除继承的意图。因为即使是在另一个包中,我的子类仍然需要将超类字段设置为“protected”才能访问,即使它是“static”的。而“包私有”也无法帮助。 - Aobo Yang
@AoboYang 你说得对,这就是为什么有些人使用 protected static。但这是一种代码异味,因此有了“run”部分。访问修饰符和继承是两个不同的主题。是的,如果它是“包私有的”,你将无法从超类访问静态成员。但你不应该依赖于继承来引用static字段;这是一个设计不良的迹象。你会注意到尝试覆盖static方法没有结果,这清楚地表明继承不是基于类的。如果需要在类或包之外进行访问,则应该是public - Dioxin
3
我有一个类,一开始里面有一些private static的工具函数,但我认为有些人可能希望从我的类中改进或自定义这些工具函数,同时这些工具函数也可以为他们提供便利。使用public可能不合适,因为工具方法并不是给我的类实例的用户使用的。你能帮我想出一个好的设计来代替protected吗?谢谢。 - Aobo Yang
在那个链接中,它说“对于特定成员使用最严格的访问级别。除非你有充分的理由不这样做,否则请使用私有。”,这与这个问题相矛盾,你有什么想法? - Cecilia

16

我并没有看到这件事情被不赞成的特定理由。总会有替代方法来实现相同的行为,而这将取决于实际的架构是否这些替代方法比受保护的静态方法更"好"。但是一个使用受保护的静态方法合理的例子是:

(经过编辑分成单独的软件包,以使protected的使用更清晰)

package a;
import java.util.List;

public abstract class BaseClass
{
    public Integer compute(List<Integer> list)
    {
        return computeDefaultA(list)+computeDefaultB(list);
    }

    protected static Integer computeDefaultA(List<Integer> list)
    {
        return 12;
    }
    protected static Integer computeDefaultB(List<Integer> list)
    {
        return 34;
    }
}

由此衍生出:

package a.b;

import java.util.List;

import a.BaseClass;

abstract class ExtendingClassA extends BaseClass
{
    @Override
    public Integer compute(List<Integer> list)
    {
        return computeDefaultA(list)+computeOwnB(list);
    }

    private static Integer computeOwnB(List<Integer> list)
    {
        return 56;
    }
}

另一个派生类:

package a.b;

import java.util.List;

import a.BaseClass;

abstract class ExtendingClassB extends BaseClass
{
    @Override
    public Integer compute(List<Integer> list)
    {
        return computeOwnA(list)+computeDefaultB(list);
    }

    private static Integer computeOwnA(List<Integer> list)
    {
        return 78;
    }
}

在这里,可以使用protected static修饰符:

  • 这些方法可以是static的,因为它们不依赖于实例变量。它们并不打算直接用作多态方法,而是作为“实用程序”方法提供默认实现,是更复杂计算的一部分,并且作为实际实现的“构建块”。
  • 这些方法不应该是public,因为它们是实现细节。它们也不能是private,因为它们应该由扩展类调用。它们也不能具有“默认”可见性,因为那样它们将无法被其他包中的扩展类访问。

(编辑:有人可能认为原始评论只涉及字段,而不是方法 - 但是,那时它太笼统了)


3
@Thomas 当然,在这种情况下,也是可能的。一般来说:这当然在某种程度上是主观的,但我的经验法则是:当一个方法不打算用于多态,并且它可以是静态的时候,我会将其定义为静态的。与使用final不同的是,这不仅清楚地表明该方法不打算被重写,还额外提示读者该方法不使用实例变量。因此,简而言之:没有理由使其静态。 - Marco13
1
当一个方法不打算被多态使用,并且它可以是静态的时候,我会将其设置为静态。但是当你开始使用模拟框架进行单元测试时,这会给你带来麻烦。但这引导我们进入了另一个话题... - barfuin
@Eugene 那个评论有点让我恼火。可以在不使用任何修饰符的情况下实现包可见性,而 protected 的意思是 “继承类(即使在不同的包中)” … - Marco13
@Marco13 你说得对,我应该把这个写得更好一些。我指的是类似于 @VisibleForTesting 这样的东西... - Eugene
protected是一种访问修饰符,它涉及类/方法/变量的访问权限,而static则涉及继承。静态的另一个属性是它可以在不实例化对象的情况下被使用,这非常方便。如果方法是protected但不是static,则无法在不实例化对象的情况下使用。我在阅读代码时会看到用例并且不会被误导。将其与变量上的final结合使用。 - TomasMolina
显示剩余2条评论

7

静态成员不会被继承,受保护的成员只对子类可见(当然也包括包含类),因此一个 protected staticstatic 具有相同的可见性,这表明程序员存在误解。


1
请问您能提供第一条声明的来源吗?在一个快速测试中,一个受保护的静态整型被继承并在子类中成功地复用。 - hiergiltdiestfu
受保护的静态具有与包私有静态相同的可见性,而不是私有静态。此外,如果您正在使用受保护和静态,则最好仅删除访问修饰符以使其成为包私有(如果您的意图是使其在包内可访问)。 - Dioxin
2
嗯...不对。包私有和protected不同。如果只使用static,则该字段仅在同一包中的子类可见。 - Aaron Digulla
2
@AaronDigulla protected static 允许在包内或子类中访问。将变量设置为静态会消除子类化的意图,只留下在包内访问的意图。 - Dioxin
@VinceEmigh:抱歉,我在和Bohemian说话。应该表达得更清楚些。 - Aaron Digulla
@VinceEmigh:我认为将其设置为静态会意味着包外的代码必须包含自己的派生类才能访问变量。从安全角度来看,如果一个类允许外部派生,“protected static”将等同于“public static”。另一方面,“protected static”会传达这样的暗示,即与该类无关的外部代码不应该访问该变量,即使它不能完全防止这样做。 - supercat

5
实际上,protected static 并没有本质上的问题。如果你真的想要一个可在包和所有子类中可见的静态变量或方法,那么就使用 protected static
有些人通常会避免使用 protected,原因各不相同,有些人认为非 final 的 static 变量应该尽可能避免(我个人在某种程度上同意后者),所以我猜对于同时属于这两个群体的人来说,protectedstatic 的组合看起来可能会很“糟糕”。

3

使用protected可以使变量在子类中使用。当在具体类的上下文中访问相同的静态变量时,定义受保护的静态变量是没有逻辑的。然而,在静态方式中访问超类静态变量时,编译器会发出警告。


1

大多数人已经回答了:

  • protected的意思是 - '包私有 + 对子类可见 - 属性/行为被继承'
  • static的意思是 - '与实例相反 - 它是一个类属性/行为,即它不会被继承'

因此,它们略微矛盾和不兼容。

然而,最近我遇到了一种使用情况,其中将这两个结合起来可能是有意义的。想象一下,您想创建一个抽象类,该类是不可变类型的父类,并且具有许多子类型共有的属性。为了正确实现不可变性并保持可读性,可能会决定使用建造者模式。

package X;
public abstract class AbstractType {
    protected Object field1;
    protected Object field2;
    ...
    protected Object fieldN;

    protected static abstract class BaseBuilder<T extends BaseBuilder<T>> {
        private Object field1; // = some default value here
        private Object field2; // = some default value here
        ...
        private Object fieldN; // = some default value here

        public T field1(Object value) { this.field1 = value; return self();}
        public T field2(Object value) { this.field2 = value; return self();}
        ...
        public T fieldN(Object value) { this.fieldN = value; return self();}
        protected abstract T self(); // should always return this;
        public abstract AbstractType build();
    }

    private AbstractType(BaseBuilder<?> b) {
        this.field1 = b.field1;
        this.field2 = b.field2;
        ...
        this.fieldN = b.fieldN;
    }
}

因为我想要一个非抽象的AbstactType子类型,它实现了自己的非抽象构建器并且位于package X之外,能够访问和重用BaseBuilder,所以使用了protected static
package Y;
public MyType1 extends AbstractType {
    private Object filedN1;

    public static class Builder extends AbstractType.BaseBuilder<Builder> {
        private Object fieldN1; // = some default value here

        public Builder fieldN1(Object value) { this.fieldN1 = value; return self();}
        @Override protected Builder self() { return this; }
        @Override public MyType build() { return new MyType(this); }
    }

    private MyType(Builder b) {
        super(b);
        this.fieldN1 = b.fieldN1;
    }
}

当然,我们可以将BaseBuilder设为公共的,但这又会带来另一个矛盾的声明:
  • 我们有一个不可实例化的类(抽象类)
  • 我们为其提供了一个公共的构建器
因此,在使用protected staticpublic的抽象类构建器两种情况下,我们都结合了矛盾的声明。这是个人偏好的问题。
然而,我仍然更喜欢public的抽象类构建器,因为在面向对象设计和编程的世界中,对我来说protected static感觉更加不自然!

0

protected static 没有任何问题。很多人忽略的一点是,你可能想为静态方法编写测试用例,但在正常情况下不想公开此方法。我已经注意到,这对于编写实用类中静态方法的测试非常有用。


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