不使用 instanceof 的重载方法的动态调度(运行时多态性)

Posted

技术标签:

【中文标题】不使用 instanceof 的重载方法的动态调度(运行时多态性)【英文标题】:Dynamic dispatch (runtime-polymorphism) with overloaded methods without using instanceof 【发布时间】:2018-04-12 18:52:42 【问题描述】:

我想将ArcLine的对象保存在一个ArrayList中,然后得到两者的交集。问题是我如何将ij 转换为其原始类。我知道instanceof 有效,但那将是最肮脏的方法。

public class Intersection 
    public static boolean intersect(ArrayList<Curve> list1, ArrayList<Curve> list2) 
        for (Curve i : list1) 
            for (Curve j : list2) 
                if (i.intersection(j).length > 0) 
                    return true;
            
        
        return false;
    


public abstract class Curve 
    public Point[] intersection(Curve c) 
        return new Point[] ;
    


public class Line extends Curve 
    public Point[] intersection(Line l) 
        // returns intersection Point of this and l
    

    public Point[] intersection(Arc a) 
        // returns intersection Point(s)
    


public class Arc extends Curve 
    public Point[] intersection(Line l) 
        // return intersection Point(s) of this and l
    

    public Point[] intersection(Arc a) 
        // returns intersection Point(s)
    

感谢您的帮助!

【问题讨论】:

我认为使用 instanceof 没有问题。您会在同一个 ArrayList 中有不同的对象类型,还是每个 ArrayList 实例只有一种类型的对象?如果没有,您可以使用通配符。 您确实意识到ArcLine 正在重载父方法而不是真正覆盖它? 你没有覆盖交点(曲线)你只是重载它,注意代码不会像你期望的那样工作 @McTi 跟进问题。 LineArc 是否有 Curve 中不存在的其他方法?也就是说,它们有不同于Curve 的操作还是intersection 唯一的操作? @CKing 是的,他们还有其他方法 【参考方案1】:

有两种方法可以处理这样的用例:


1.实现Multiple dispatch

首先将Curve 设为一个接口,并将intersect 的两个重载版本添加到该接口,从而使它们成为合约的一部分。接下来,让每个子类中的intersection(Curve c) 方法将调用委托给适当的重载表单。 (又名Visitor pattern)

interface class Curve 
    public Point[] intersection(Curve c);

    public Point[] intersection(Line l);

    public Point[] intersection(Arc c);



class Line extends Curve 
    
    public Point[] intersection(Curve c) 
        return c.intersection(this);
    
    
    @Override
    public Point[] intersection(Line l) 
        System.out.println("line interesection with line");
        return new Point[0];
    

    @Override
    public Point[] intersection(Arc c) 
        System.out.println("line intersection with arc");
        return new Point[0];
    



class Arc extends Curve 
    
    public Point[] intersection(Curve c) 
        return c.intersection(this);
    
    @Override
    public Point[] intersection(Line l) 
        System.out.println("arc interesection with line");
        return new Point[0];
    

    @Override
    public Point[] intersection(Arc c) 
        System.out.println("arc interesection with arc");
        return new Point[0];
    

然后您可以在 Intersection 类中调用您的 intersection 方法,而无需任何显式转换:

public class Intersection 
    public static boolean intersect(ArrayList<Curve> list1,
            ArrayList<Curve> list2) 
        for (Curve i : list1) 
            for (Curve j : list2) 
                if (i.intersection(j).length > 0)
                    return true;
            
        
        return false;
    
    
    public static void main(String[] args) 
        Curve line1 = new Line();
        Curve arc1 = new Arc();
        Curve line2 = new Line();
        Curve arc2 = new Arc();
        
        ArrayList<Curve> list1 = new ArrayList<>();
        ArrayList<Curve> list2 = new ArrayList<>();
        list1.add(line1);
        list1.add(arc1);
        list2.add(line2);
        list2.add(arc2);
        
        Intersection.intersect(list1, list2);
    
    

Extras:看看this实现Visitor模式的替代方法。


2。使直线和曲线遵守同一个界面(契约):

如果LineArc 遵循Curve 的接口,您的代码将不再需要intersect 方法的重载版本。如果我们说LineCurve 并且Arc 也是Curve,那么这两个类都应该具有与Curve 相同的接口(接口我的意思是他们支持的操作列表)。如果这些类没有与Curve 相同的接口,这就是问题所在。 Curve 中的方法应该是 LineArc 类需要的唯一方法。

有几种策略可以消除子类具有父类中不存在的方法的需要:

如果子类与超类相比需要额外的输入,请通过构造函数提供这些输入,而不是创建对这些输入进行操作的单独方法。 如果子类需要超类不支持的其他行为,请通过组合(阅读策略模式)支持此行为,而不是添加方法来支持其他行为。

一旦您不再需要在子类中使用超类中不存在的专用方法,您的代码就会自动消除对instanceof 或类型检查的需要。这与Liskov substitution principle 一致。


【讨论】:

【参考方案2】:

由于每个子类都必须知道其他子类(例如,Arc 必须知道 Line 类才能实现 ArcLine 交集),所以有使用instanceof 没有错。

在每个子类中,您可以重写基类的 public Point[] intersection(Curve c) 方法并将实现分派给重载方法之一。

例如:

public class Arc extends Curve     
    @Override
    public Point[] intersection(Curve c) 
        if (c instanceof Line)
            return instersection ((Line) c);
        else if (c instanceof Arc)
            return intersection ((Arc) c);
        else
            return an empty array or null, or throw some exception
    

    public Point[] intersection(Line l) 
        // return intersection Point(s) of this and l
    

    public Point[] intersection(Arc a) 
        // returns intersection Point(s)
    

这样您就不必更改 public static boolean intersect(ArrayList&lt;Curve&gt; list1, ArrayList&lt;Curve&gt; list2) 方法中的任何内容。

【讨论】:

对不起,我在看到你的想法之前就发布了我的想法 :) @McTi 如果我错了,请纠正我,但你说,“我知道 instanceof 有效,但那将是最肮脏的方法。” 暗示你正在寻找解决方案不使用instanceof?我给了你两个很好的选择,以避免在我的回答中使用instanceof。我挠头想知道这是怎么接受的答案? (此答案的作者无意冒犯。我试图从 OPs 帖子中寻求不使用 instanceof 的解决方案,然后接受使用的答案) @Eran 我看到这个解决方案有两个问题 1)intersection(Curve c) 方法中的代码需要在所有子类中重复,这很好但是 2)这很容易成为维护的噩梦如果必须引入更多子类(例如 Elipse、Parabola 等),则需要向所有子类添加新的 else if 条件和 instanceof 检查。即使除了 Line 或 Curve 不需要支持任何东西,重复 instanceof 检查即使是一个额外的子类也似乎是一种代码味道。【参考方案3】:

另一种方法是使用Class 类的isAssignableFrom 方法。下面是一个例子:

Exception e = new Exception();
RuntimeException rte = new RuntimeException();
System.out.println(e.getClass().isAssignableFrom(RuntimeException.class));
System.out.println(rte.getClass().isAssignableFrom(Exception.class));
System.out.println(rte.getClass().isAssignableFrom(RuntimeException.class));

Here'sisAssignableFrom 方法的 javadoc,它是这么写的:

确定此 Class 对象表示的类或接口是否 要么是相同的,要么是超类或超接口, 由指定的 Class 参数表示的类或接口。它 如果是,则返回 true;否则返回false。如果这个 Class 对象 表示原始类型,如果指定,此方法返回 true Class 参数就是这个 Class 对象;否则返回 假的。

【讨论】:

【参考方案4】:

首先考虑:您是否需要将ijCurve 转换为ArcLine

例如,请看这里:

What's the need to use Upcasting in java?

如果您确定确实需要向上转换,不幸的是没有神奇的彩蛋 - 您无法避免使用 instanceof 来决定要向上转换的类。

你可以将责任委托给另一个班级,但基本上你无法避免。

对不起!

【讨论】:

【参考方案5】:

好的,所以我发现的一个解决方案是在Curve 中使用抽象方法,在子类中使用if-else 链。但是我对这个解决方案并不满意。

public abstract class Curve 
    public abstract Point[] intersection(Curve c);


public class Line extends Curve 
    public Point[] intersection(Curve c) 
        if (c instanceof Line) 
            return this.intersection((Line) c);
         else if (c instanceof Arc) 
            return this.intersection((Arc) c);
        
    

    private Point[] intersection(Line l) 
        // returns intersection Point of this and l
    

    private Point[] intersection(Arc a) 
        // returns intersection Point(s)
    

【讨论】:

【参考方案6】:

Curve 更改为接口。保持ArrayList&lt;Curve&gt; 不变,而是将intersection 方法提取到一个单独的类,并让它在Curves 上运行。

您需要在此处使用instanceof 检查,但由于使用继承,您的设计会更简洁。

public interface Curve 
...


public class Line extends Curve 
...


public class Arc extends Curve 
...


public class IntersectionUtility 
    public static boolean intersects(ArrayList<Curve> list1, ArrayList<Curve> list2) 
        for (Curve i : list1) 
            for (Curve j : list2) 
                if (i.intersection(j).length > 0) 
                    return true;
            
        
        return false;
    

   public Point[] intersection(Curve a, Curve b) 
      if (a.instanceof(Line.class)) 
        if (b.instanceof(Line.class)) 
           return findIntersection((Line) a, (Line) b); // two Lines
         else 
           return findIntersection((Line) a, (Arc) b); // a Line and an Arc
        
       else 
        if (b.instanceof(Line.class)) 
           return findIntersection((Line) b, (Arc) a); // a Line and an Arc
         else 
           return findIntersection((Arc) a, (Arc) b); // two Arcs
        
      
   

    public Point[] findIntersection(Line a, Line b) 
        // returns intersection Point of two Lines
    

    public Point[] findIntersection(Arc a, Arc b) 
        // returns intersection Point(s) of two Arcs
    

    public Point[] findIntersection(Line a, Arc b) 
        // returns intersection Point(s) of an Line and an Arc
    

【讨论】:

不确定如何编译?没有带Curve 参数的findIntersection【参考方案7】:

如果您不想使用instanceof,那么替代方法是使用组合来获取类型。以下方法不会使用instanceof,只会使用首选Class.cast 操作:

public static class Intersection 

        public static boolean intersect(ArrayList<Curve> list1, ArrayList<Curve> list2)             
            for (Curve i : list1)                 
                Optional<Line> il = i.get(Line.class);
                Optional<Arc> ia = i.get(Arc.class);

                for (Curve j : list2) 
                    Optional<Line> jl = j.get(Line.class);
                    Optional<Arc> ja = j.get(Arc.class);

                    Point[] intersection = null;

                    if ( il.isPresent() )
                        if ( jl.isPresent() ) intersection = il.get().intersection( jl.get() );
                        else if ( ja.isPresent() ) intersection = il.get().intersection( ja.get() );
                    else if ( ia.isPresent() )
                        if ( jl.isPresent() ) intersection = ia.get().intersection( jl.get() );
                        else if ( ja.isPresent() ) intersection = ia.get().intersection( ja.get() );
                        

                    if ( intersection != null && intersection.length > 0 ) return true;
                
            
            return false;
        
    

    public static abstract class Curve 

        public abstract <T extends Curve> Optional<T> get(Class<T> clazz);

    

    public static class Line extends Curve 

        public <T extends Curve> Optional<T> get(Class<T> clazz)
            return clazz.equals(Line.class) ? Optional.of( clazz.cast(this) ) : Optional.empty();
        

        public Point[] intersection(Line l) 
            return new Point[] ;
        

        public Point[] intersection(Arc a) 
            return new Point[] ;
        
    

    public static class Arc extends Curve 

        public <T extends Curve> Optional<T> get(Class<T> clazz)
            return clazz.equals(Arc.class) ? Optional.of( clazz.cast(this) ) : Optional.empty();
        

        public Point[] intersection(Line l) 
            return new Point[] ;
        

        public Point[] intersection(Arc a) 
            return new Point[] ;
        
     

【讨论】:

以上是关于不使用 instanceof 的重载方法的动态调度(运行时多态性)的主要内容,如果未能解决你的问题,请参考以下文章

使用instanceof操作符判断对象类型及方法的重载

Java面向对象(OOP)--面向对象三大特性之一:多态(多态方法绑定instanceof和类型转换(重写重载和多态的关系))

避免 Ruby 中的方法重载

多方法和多调度之间有啥区别吗?

day14 多态与抽象

为啥 ruby​​ 不支持方法重载?