【问题标题】:Can an overriding method have a different access specifier from that in the base class?覆盖方法可以具有与基类中不同的访问说明符吗?
【发布时间】:2016-06-27 15:25:15
【问题描述】:

在抽象类中,我必须为方法使用哪个访问修饰符, 所以子类可以决定它是否应该是公共的?是否可以在 Java 中“覆盖”修饰符?

public abstract class A {

    ??? void method();
}

public class B extends A {
    @Override
    public void method(){
        // TODO
    }
}

public class C extends B {
    @Override
    private void method(){
        // TODO
    }
}

我知道静态绑定会有问题,如果 有人打电话:

// Will work
A foo = new B()
foo.method();

// Compiler ?
A foo = new C();
foo.method();

但也许还有另一种方式。我怎样才能做到这一点?

【问题讨论】:

  • 您可以将覆盖的 final 但不是私有的。您也可以将其弃用并抛出不受支持的操作异常(番石榴对不可变集合执行此操作)

标签: java abstract modifiers


【解决方案1】:

这是@Override 合同的一部分。

答案是:不可能实现你所拥有的。

访问级别不能比被覆盖的更严格 方法的访问级别。例如:如果超类方法是 声明为public,则子类中的覆盖方法不能 私有或受保护。

这不仅仅是abstract 类的问题,而是所有类和方法的问题。

【讨论】:

    【解决方案2】:

    在覆盖方法时,您只能将修饰符更改为更宽的,反之则不行。例如,此代码将是有效的:

    public abstract class A {
    
        protected void method();
    }
    
    public class B extends A {
        @Override
        public void method() { }
    }
    

    但是,如果您尝试缩小可见性,则会收到编译时错误:

    public abstract class A {
        protected void method();
    }
    
    public class B extends A {
        @Override
        private void method() {}
    }
    

    对于你的情况,我建议让C 不实现A,因为A 的抽象暗示有一个非私有的method()

    public class C {
        private void method(){
          //TODO
        }
    }
    

    另一种选择是在 C 中实现 method() 并抛出 RuntimeException:

    public class C extends A {
    
        @Override
        public void method(){
            throw new UnsupportedOperationException("C doesn't support callbacks to method()");
        }
    }
    

    【讨论】:

    • 如果我在 A protected 和 C protected 中创建方法,它也可以工作,并且无法从包或子类外部访问?
    • 是的,但他希望它是私密的。
    【解决方案3】:

    放宽限制是可以的,但不能让它变得更严格:

    public abstract class A {
        protected void method();
    }
    
    public class B extends A {
        @Override
        public void method(){    // OK
        }
    }
    
    public class C extends A {
        @Override
        private void method(){    // not allowed
        }
    }
    

    使原始方法private 也不起作用,因为这种方法在子类中不可见,因此无法被覆盖。

    我建议使用interfaces 来选择性地公开或隐藏该方法:

    public interface WithMethod {
        // other methods
        void method();
    }
    
    public interface WithoutMethod {
        // other methods
        // no 'method()'
    }
    
    public abstract class A {
        protected void method();
    }
    
    public class B extends A implements WithMethod {
        @Override
        public void method(){
          //TODO
        }
    }
    
    public class C extends B implements WithoutMethod {
        // no 'method()'
    }
    

    ...那么只能通过接口处理实例。

    【讨论】:

    • 我不应该只使用接口并删除抽象类吗?
    • 在您的示例代码中,C 类的实例是否仍然拥有来自B 类的method()?由于C extends B。为类C 添加implements WithoutMethod 无法从类C删除 method()。正确的?所以“只通过接口处理实例。”只是让你假装C没有method()
    • 嗯...一个有趣的想法,但是...这样你就可以让WithMethod扩展WithoutMethod。然后C 类将隐式实现WithoutMethod。不过,B 类也一样,这可能是也可能不是 OP 一开始想要的。
    • @Tersosauros 你是对的,C 在这种情况下仍然有方法,但是如果你只将它作为WithoutMethod 接口暴露给用户,用户将无法访问该方法。在较小的项目中,将整个类层次结构(ABC)设为包私有并且仅公开接口可以确保用户不会针对实际类编写代码。在更大的项目中,您可以将 API 和实现分成两个模块 (JAR),并且仅在编译时提供 API。
    • @Tersosauros:如果要扩展现有类,则必须提供其所有方法,否则将违反 LSP。
    【解决方案4】:

    理论:

    你有确定的修饰符顺序:

    public <- protected <- default-access X<- private
    

    当您覆盖该方法时,您可以增加,但不能减少修饰符级别。例如,

    public -> []
    protected -> [public]
    default-access -> [public, default-access]
    private -> []
    

    实践:

    在您的情况下,您不能将??? 变成某个修饰符,因为private最低的 修饰符,而private 类成员不会被继承。

    【讨论】:

    • 这是不正确的,尽管这是一个普遍的神话。事实上,正确的顺序是public -&gt; protected -&gt; default-access -&gt; private。您实际上可以从同一包中的其他类访问其他包中的子类的受保护成员,而 default-access 仅提供前者。参考JLS 8.4.8.3
    【解决方案5】:

    出于非常充分的理由,您所要求的内容是不可能的。

    Liskov substitution principle 基本上说:一个类 S 是另一个类 T 的子类,只有这样,当你可以用一些“S 对象”替换任何出现的“T 对象”时 - 没有注意到。

    如果您允许 S 将公共方法简化为私有方法,那么您就不能再这样做了。因为突然之间,可以在使用一些 T ... 时调用的方法不再可以在 S 上调用。

    长话短说:继承不是天上掉下来的东西。它是作为程序员负责的类的属性。换句话说:继承不仅仅意味着在你的源代码中写下“class S extends T”!

    【讨论】:

    • LSP 要求基类方法存在并且在所有派生类中都有定义的函数。它并不要求函数在所有派生类中都有用。例如,派生类的契约规定某些虚拟基类属性将始终为该派生类的所有实例返回“false”是合法的。在这种情况下,在派生类中隐藏该成员可能是完全合理的,因为没有理由在引用已知上调用它来标识派生类对象。
    • 你自己说的:方法必须存在于派生类中。将其私有化与从外部角度完全删除完全一样。当然,你可以在 S 允许的范围内使用“S”;但这就是“透明交换”的全部意义所在;您正在以 T 的形式访问 S。因此我不确定您要说什么。
    • 如果某个类实例的引用的接收者可能想要使用它的“Wizzle()”方法(如果有的话),或者执行其他一些可以达到相同效果但更少的方法序列有效地,并且一些衍生品将能够“Wizzle()”而另一些则不能,让基类包含“CanWizzle()”属性和“Wizzle()”方法可能会有所帮助,合同约定如果“CanWizzle()”返回 true,则“Wizzle()”方法将返回有用的信息,如果“CanWizzle()”返回 false,则可能不会。不能“Wizzle()”的派生类...
    • ...没有理由允许在已知标识该派生类实例的引用上调用“Wizzle()”方法(因为这样的调用不可能工作) .
    • 也许是这样。但是这样的讨论并没有这样笼统的说。然后我会问:如果你的一些东西可以吹口哨;而其他人则不能;那么拥有一个通用基类的目的是什么?
    【解决方案6】:

    由于多态性,这是不可能的。考虑以下。您在类A 中有一个方法,它带有一些不是private 的访问修饰符。为什么不是私人的?因为如果它是私有的,那么任何其他类都无法知道它的存在。所以它必须是其他东西,并且必须可以从某处访问其他东西。

    现在让我们假设您将C 类的实例传递给某处。但是您事先将其上传到A,因此您最终会在某处拥有此代码:

    void somewhereMethod(A instance) {
        instance.method(); // Ouch! Calling a private method on class C.
    }
    

    一个很好的例子是Qt中的QSaveFile。与 Java 不同,C++ 实际上允许降低访问权限。所以他们就这么做了,禁止close() 方法。他们最终得到的是一个QIODevice 子类,它不再是真正的QIODevice。如果您将指向QSaveFile 的指针传递给某个接受QIODevice* 的方法,他们仍然可以调用close(),因为它在QIODevice 中是公开的。他们通过让QSaveFile::close()(这是私人的)调用abort()来“修复”这个问题,所以如果你这样做,你的程序会立即崩溃。不是一个很好的“解决方案”,但没有更好的“解决方案”。这只是糟糕的OO设计的一个例子。这就是 Java 不允许这样做的原因。

    编辑

    不是我错过了你的课程是抽象的,而是我也错过了B extends C,而不是A。这样你想要做的事情是完全不可能的。如果 B 中的方法是public,那么它在所有子类中也是公共的。你唯一能做的就是记录它不应该被称为并且可能覆盖它来抛出UnsupportedOperationException。但这会导致与QSaveFile 相同的问题。请记住,您班级的用户可能甚至不知道它是 C 的实例,因此他们甚至没有机会阅读其文档。

    总的来说,从面向对象的角度来看,这只是一个非常糟糕的主意。也许你应该问另一个问题,关于你试图用这个层次结构解决的确切问题,也许你会得到一些关于如何正确解决问题的好建议。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-12-06
      • 2013-11-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多