V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
cpstar
V2EX  ›  Java

咨询一个继承、重载、父类、多态的方法调用问题

  •  
  •   cpstar · 2022-01-30 17:02:18 +08:00 · 3510 次点击
    这是一个创建于 788 天前的主题,其中的信息可能已经有所发展或是发生改变。

    遇到一个问题,需要外部调用父类的方法,怎么搞?

    class Father {
    	public void name() {
    		System.out.println("father");
    	}
    }
    
    class Son extends Father {
    	public void name() {
    		System.out.println("son");
    	}
    }
    

    如果 new Son()的话,

    Son a = new Son();
    Father b = new Son();
    

    甭管 a 还是 b ,调用 name()都会显示 son 。

    有没有什么办法,使得 b 调用 name()得到 father ?

    BTW ,Son 可能还有其他方法需要 b 来调用。 再 BTW ,不限于使用反射来搞定。

    第 1 条附言  ·  2022-01-30 22:53:17 +08:00

    好吧,我认为这个已经无解了。

    然后描述一下场景。 类B继承自类A,重写了A的m方法,然后B的m方法内部写了super.m()来调用A的m(),这都没有问题。A和B都是库文件,原则上不能动。

    但是现在来了一个类C,一定程度上在模拟B的其他方法,其中就调用了m(),但是因为B的m除了super.m()还加料了,而在C的处理过程中,不希望加料,也就是能够调用到A的m()——对了A是abstract抽象类不能实例,m也不是static。C可以随便动。

    大抵就是:

    abstract class A {
    	public void m() {...}
    }
    class B extends B {
    	public void m() {
    		super.m();
    		do sth. else;
    	}
    	public void s() {
    		m();
    		do x;
    		do y;
    	}
    }
    class C {
    	public void x() { // similar to B.s()
    		B b = new B();
    		B.m();
    		undo sth. else;
    		do x;
    		do y;
    	}
    }
    

    于是我现在的做法就是在C上先经过B的加料,再专门减料。算是贴了一个大补丁凑合怼过去了。

    39 条回复    2022-02-09 13:21:49 +08:00
    golangLover
        1
    golangLover  
       2022-01-30 17:46:24 +08:00 via Android
    在 son 里面写 super.name()
    cpstar
        2
    cpstar  
    OP
       2022-01-30 18:13:08 +08:00
    @golangLover 必须是 b 调用,外部调用,不是内部
    Building
        3
    Building  
       2022-01-30 18:35:19 +08:00
    这是一个类调用同一个方法返回不同结果了啊,无解的,调用同一个方法返回的东西一定是一样的。
    littlewing
        4
    littlewing  
       2022-01-30 18:36:27 +08:00
    貌似没办法
    换 C++ 吧
    golangLover
        5
    golangLover  
       2022-01-30 18:48:56 +08:00 via Android
    @cpstar 那不会了。为什么提供多一个不同名的方法不行呢?
    Jooooooooo
        6
    Jooooooooo  
       2022-01-30 18:49:30 +08:00
    感觉是 xy 问题啊, 要不细说说究竟是什么场景需要这样.
    ccde8259
        7
    ccde8259  
       2022-01-30 19:19:21 +08:00 via iPhone
    首先得让 Father 去实现一个具有 name 方法的 interface ,然后是反射重写对象头把 Son 实例的 Class ptr 指向 Father 。
    lsry
        8
    lsry  
       2022-01-30 19:30:37 +08:00
    如果是想让 father 调用自己的方法(以变量定义时候的类型,而不是实际类型),可以将方法前面加 static 。
    Leviathann
        9
    Leviathann  
       2022-01-30 19:59:55 +08:00
    子类型是新的类型啊
    又不是 javascrpit 存了父类型的原型
    SoloCompany
        10
    SoloCompany  
       2022-01-30 20:38:18 +08:00
    简单来说是不可能的, 你必须修改 A (父类) 或 B (子类) 提供一个非多态的 alias 才能调用到
    SoloCompany
        11
    SoloCompany  
       2022-01-30 20:41:13 +08:00
    再详细一些, 反射只能 hack field / method 的不可见或不可访问的问题而无法 hack method 因多态而 hidden 的问题, 真实的 hack 只能把想调用的方法重新写一次, 可以利用反射越过无法访问的 filed / method
    bigbyto
        12
    bigbyto  
       2022-01-30 20:42:26 +08:00 via iPhone
    invokedymanic 可以实现,不过尽可能不要这样做。
    crayygy
        13
    crayygy  
       2022-01-30 20:45:29 +08:00
    外部有什么必须要调用父类方法的理由吗?一般涉及到这类很 tricky 的问题的时候从需求侧开始思考问题比较合适,也许有更合适的其他方法
    251
        14
    251  
       2022-01-30 21:20:12 +08:00
    既然重写了 name(),就说明 son.name()能完全代替 father.name()的功能,所以不需要调 father.name().如果不能完全替代,说明 father.nameson.name() 功能不一样,那就不要重写了。C++ 应该可以,java 不知道。
    251
        15
    251  
       2022-01-30 22:04:17 +08:00
    刚才又想了一下,肯定行。但这个问题有点无聊了,a 跟 b 是同一个对象,同一个对象调用签名一样的方法,jvm 默认调第二方法,但你又想掉第一个?好比 两个都叫翠花,你就叫一声翠花,鬼知道你想默认调用那个翠花?你可能想要的是想通过声明的方式不同以改变调用不用的方法,那你自己把字节码编译好,用 classloader 的 defineclass 加载,肯定可以。
    pptom
        16
    pptom  
       2022-01-30 22:22:36 +08:00
    这就是多态啊,只有运行时才确定对象是哪个类的实例。不理解这个问题的意义
    EvanLuo42
        17
    EvanLuo42  
       2022-01-30 22:23:58 +08:00 via iPhone
    所以为什么要 b 调 a ,这样做用继承又有什么用。你 b 单方面的调用一个子类,从我的观点来看违背了 oop 的思想。
    Jwyt
        18
    Jwyt  
       2022-01-30 22:38:31 +08:00
    单看代码你直接 new Father() 不就完了
    cpstar
        19
    cpstar  
    OP
       2022-01-30 22:56:57 +08:00
    @littlewing 4# 提问题之前搜了一下,说到了 C++可以 upcast 到父类,就能用父类的方法覆盖子类。然后就想起来当初学习 JAVA 时,就说 C++不纯粹面向对象,这就是其中一个问题,做不到这个层面的多态。
    cpstar
        20
    cpstar  
    OP
       2022-01-30 23:05:26 +08:00
    @251 15# 我感觉也应该行,而且我还知道 jvm 上,类加载之后,除了子类加载到内存中并且具有方法指针入口,父类也会加载到内存里,只不过多态调用的时候,实例中的方法入口指针指向的是子类的(因为实例是子类的),所以我在问题里提到了反射(反射没有系统学过,不太清楚怎么用)。

    按照 @SoloCompany 11#,我能理解反射能够做到的,也仅是“类”这个层面的,再结合 @bigbyto 12#提到的 invokedynamic ,这个应该是需要渗透到 jvm 层面操控“实例”来搞定了(比如修改实例的方法入口指针),某种程度上应该就是 reflect jvm 。擦,怎么感觉有点像 matrix 了,neo 要苏醒,囧 rz 。。。
    251
        21
    251  
       2022-01-30 23:41:04 +08:00   ❤️ 1
    行肯定行,要改字节码,你愿意折腾可以看看(我没看) http://rohandhapodkar.blogspot.com/2012/03/call-grand-parent-method-in-java.html 。说一下我的思路:先编译字节码->反编译字节码->改字节码->编译成字节码
    ipwx
        22
    ipwx  
       2022-01-31 00:04:33 +08:00
    @cpstar 根本不用 upcast ,C++ 直接用随便用。

    https://godbolt.org/z/fbPv4srsv
    bigbyto
        23
    bigbyto  
       2022-01-31 00:23:20 +08:00 via iPhone   ❤️ 1
    不是很麻烦,mybatis 有类似的调用接口,贴一段代码你参考。不过尽量别这么做就是了。

    public static void main(String[] args) throws Throwable {
    Father f = new Son();
    MethodType mt = MethodType.methodType(void.class);
    Field impl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
    impl.setAccessible(true);
    MethodHandle mh = ((MethodHandles.Lookup) impl.get(null)).findSpecial(Father.class,"name",mt,Son.class);
    mh.invoke(f);
    }
    bigbyto
        24
    bigbyto  
       2022-01-31 00:29:18 +08:00 via iPhone
    这问题麻烦的点在于 jvm 方法调用都是使用 invokevirtual 指令,这条指令的分派逻辑是固定不变的。MethodHandle 提供了一些动态特性,可以使用 invokedynamic 来执行动态分派逻辑。
    clf
        25
    clf  
       2022-01-31 02:58:44 +08:00
    emmm ,所以为啥会有这样的需求???

    其实是可以的,只是你别当作是私有方法即可,直接静态方法,通过对象去调用静态方法 IDE 虽然会 warning ,但可以无视(既然你有这样诡异的需求)

    参考如下:

    public class Father {
    public static void name(){
    System.out.println("father");
    }
    }

    public class Son extends Father {
    public static void name(){
    System.out.println("son");
    }
    }

    测试:
    Father father = new Son();
    Son son = new Son();
    father.name();
    son.name();

    输出:
    father
    son
    clf
        26
    clf  
       2022-01-31 03:05:48 +08:00
    在调用 name 的时候本质上是调用了 Father 和 Son 的静态方法。

    如果你想要的是一个根据自身属性输出不同结果,而 Son 有一种算法,Father 有另外一种算法,而且所涉及的内部属性父类子类均有,那么建议变成:
    public class Father {
    public static String demo(Father f){
    //这里是父类计算方法
    }
    }

    public class Son extends Father {
    public static String demo(Father f){
    //这里是子类计算方法
    }
    }

    测试:
    Father father = new Son();
    Son son = new Son();
    father.demo(father);
    son.demo(son);
    xuanbg
        27
    xuanbg  
       2022-01-31 08:36:50 +08:00
    继承就是这样被玩坏的。我一向只把基类当作子类的抽象,绝不搞什么乱七八糟的多态。
    cpstar
        28
    cpstar  
    OP
       2022-01-31 09:04:09 +08:00
    @clf 看一下追加,A 、B ( Father 、Son )是库类,不能轻易动的,所以只能在实例上和 C 上做文章。

    @xuanbg 面向对象三大特性,封装、继承、多态
    iseki
        29
    iseki  
       2022-01-31 15:06:32 +08:00 via Android
    看了下你的附言,正确的做法大概是直接集成类 A ,自己实现一部分功能。出这种问题要么是你对这俩类的使用方法不恰当,要么就是这俩类本身封装的不好,才会让你需要做这么奇怪的事。
    C++下方法可以是虚的也可以不是,Java 都是虚的就没正常办法了…
    aliveyang
        30
    aliveyang  
       2022-01-31 16:45:17 +08:00
    无解,子与父分明是两个对象
    ahhui
        31
    ahhui  
       2022-01-31 17:27:10 +08:00
    C 类不可以用 Wapper 设计模式吗?不谈继承,C 类有 A,B 两个类共用的公开方法,在 C 类里保存一个 B 类实例,所有的 C 类公开方法调用 B 类的公开方法返回,唯独 C.Name 的时候,自己来个实现不使用 B 的方法。如果是需要 C.Name 来自 A.Name ,那么一个 C 类实例就维护 A 类和 B 类两个实例化的对象不就行了吗?当然这个时候,C 类既可以和 AB 类完全没有继承关系,也可以让 C 类从 A 或者 B 继承下来。如果从 A 或者 B 继承,那么你就只需要单独维护另外一个类的实例即可(有点绕,就是假设 C 从 B 继承,实例化的时候创建一个 A 的对象自己引用,调用 C.Name 的时候不从 supper 访问,直接访问 A.Name 返回。同理,如果 C 从 A 继承,那就维护一个 B 的对象引用即可)。
    ychost
        32
    ychost  
       2022-01-31 17:53:33 +08:00
    C# 可以,Java 是 [静态多分派] ,而 C# 是 [动态单分派]
    ychost
        33
    ychost  
       2022-01-31 17:53:44 +08:00
    @ychost 动态多分派
    MrKrabs
        34
    MrKrabs  
       2022-02-01 01:05:14 +08:00
    为什么不直接 new 一个 Father 呢
    bololobo
        35
    bololobo  
       2022-02-03 12:09:27 +08:00
    31 楼正解
    bololobo
        36
    bololobo  
       2022-02-03 12:11:08 +08:00
    @bololobo 这种需求用继承来实现不太合适
    1194129822
        37
    1194129822  
       2022-02-06 11:05:53 +08:00
    @bigbyto 你这种方法是错的,Java 类模型,子类或者类外无法访问祖类被重写方法。你这方法是 java7 某个版本的 bug ,在《深入理解 Java 虚拟机》里面也有你这例子,但是 R 大后来去问 JVM 相关开发者,确认这是 bug ,之后就修复了。修改字节码 invokevirtual 变成 invokespecial 也只是访问父类方法,也无法访问祖类方法。
    ikas
        38
    ikas  
       2022-02-08 13:49:29 +08:00
    java 是简化了这种继承的调用,你基本可以无脑的调用,而不是注意到底是谁实现 /重写的

    如果你用 c#或者其他一些语言,你就要分清
    Joker123456789
        39
    Joker123456789  
       2022-02-09 13:21:49 +08:00   ❤️ 1
    这样就好了:Father b = new Father();

    其他都是歪门邪道。

    你都 new Son 了,却希望他是 Father , 自己好好想想这个想法对吗?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1366 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 23:34 · PVG 07:34 · LAX 16:34 · JFK 19:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.