从通用方法中调用重载方法的问题

4

我是一名有用的助手,可以为您进行翻译。

我遇到了有趣的事情(在Java和C#中都适用)。

Java代码:

public class TestStuff {

    public static void main(String[] args) {
        Printer p = new PrinterImpl();
        p.genericPrint(new B());    
    }

}

class PrinterImpl implements Printer {

    void print(A a) {
        System.out.println("a");
    }

    void print(B b) {
        System.out.println("b");
    }

    @Override
    public <T extends A> void genericPrint(T b) {
        print(b);
    }

}

interface Printer {
    public <T extends A> void genericPrint(T a);
}

class A {

}

class B extends A{

}

C# 代码:

namespace TestStuff
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var printer = new Printer();
            printer.GenericPrint(new B());
        }

    }

    public class Printer
    {

        public void Print(A a)
        {
            Console.WriteLine("a");
        }

        public void Print(B b)
        {
            Console.WriteLine("b");
        }

        public void GenericPrint<T>(T a) where T : A
        {
            Print(a);
        }

    }

    public class B : A
    {

    }

    public class A
    {

    }

}

当我写出这样的代码时,我期望在两种情况下都会输出"b",但是,如你所见,输出的却是"a"。
我已经阅读了C#语言规范,它说明重载方法是在编译时被选择的。这就解释了为什么会出现这种情况。
然而,我没有时间去检查Java语言规范。
请问是否有人能够给出更详细的解释,以及为什么会出现这种情况?我该如何实现我想要的效果?
提前感谢!

1
你的问题实际上不需要涉及泛型。定义一个接受Object参数的方法,并重载它以接受一个String参数的方法。然后声明一个类型为Object的变量并将其设置为一个String。当传递该变量时,查看调用哪个重载方法 - 它是相同的行为。 - Paul Bellora
忽必烈,这不是关于这个例子的。=) - Sergey Savenko
明白了,我注意到你想解决一个具体的问题,但是问题的描述让人感觉你对这个行为很感兴趣。只是想指出如何减少这种情况。 - Paul Bellora
3个回答

5

关键是要理解在Java中泛型只在编译时可用。这只是编译器在编译时使用的语法糖,但在生成类文件时会被丢弃。

因此,以下代码:

   public <T extends A> void genericPrint(T b) {
      print(b);
   }

被编译成:

   public void genericPrint(A b) {
      print(b);
   }

由于print的参数是类型A,因此重载版本print(A a)是被解析的。建议在A类的实例上使用多态调用或访问者模式来回调到PrinterImpl以满足您的用例。

类似这样:

interface Visitor {
   void visit(A a);

   void visit(B b);
}

class PrinterImpl implements Printer, Visitor {

   void print(A a) {
      System.out.println("a");
   }

   void print(B b) {
      System.out.println("b");
   }

   public <T extends A> void genericPrint(T b) {
      b.accept(this);
   }

   public void visit(A a) {
      print(a);
   }

   public void visit(B b) {
      print(b);
   }
}

interface Printer {
   public <T extends A> void genericPrint(T a);
}

class A {
   public void accept(Visitor v) {
      v.visit(this);
   }
}

class B extends A {
   public void accept(Visitor v) {
      v.visit(this);
   }
}

哇,这真的很酷。有趣的模式 =)之前没有见过 - Sergey Savenko
@Dead_ok 在维基百科上添加了访问者模式的链接。 - shams

1

确实,重载方法在编译时被选择,这对Java也是正确的(动态方法调度)。 然而,泛型的工作方式略有不同。您的方法GenericPrinter只能处理类型A或其派生类。这是该方法的限制。假设在您的GenricPrinter类中调用了一个在A中定义的方法。

public class A
{
    void DoSomethingA()
    {
    }    
}
.
.
.
public void GenericPrint<T>(T a) where T : A
{
    //constraint makes sure this is always valid
    a.DoSomethingA();
    Print(a);
}

因此,这个约束条件将确保只有 A 或其子类包含上述方法才会被允许。尽管您传递了 A 的子类的实例,但由于约束条件,GenericPrinter 将把子类视为 A。只需删除约束部分(T:A),B 就会按预期打印。


2
如果他移除了那个限制,就不会有适当的打印方法。 - Matthias
如果移除约束,a 就只是一个基本类型为 object 的对象。 - Matthias
Matthias是正确的,不会找到适当的方法。但是,如果您提供Print(Object o),它将始终被调用。 - Sergey Savenko

0

在您的GenericPrint方法中没有对a类型进行运行时检查。您使用where T:A部分强制执行的唯一事项是可以调用Print

顺便说一下,除了通用方法之外:如果您想打印a,尽管它是B实例,则必须将该变量声明为A obj = new B()


同意,这就是我之前所说的。但是如何实现所需的行为呢? - Sergey Savenko
但我不明白为什么你需要那个GenericPrint方法?多态性应该足够了。 - Matthias
重点是我有一个接口,它定义了一个方法,该方法将特定接口的实现作为参数。我想要的是根据实际传递的类型来改变行为。而且我不想使用instanceOf() =)。我知道这是一种不好的做法,但它可以帮助我节省一些资源,因为方法实现可以针对实现接口的特定类型更加高效。 - Sergey Savenko
唯一的另一个解决方案是调整对这个方法的调用,并转换为具体的实现。然后针对这些实现提供不同的方法。 - Matthias
这显然会有所帮助,但我正在尝试避免使用instanceOf()。 - Sergey Savenko
1
@MatthiasKoch,你可以使用访问者模式来避免在调用时进行强制转换 :) - shams

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