2012-03-18 18 views
20

Tengo una colección (o lista o lista de matrices) en la que quiero poner tanto valores de cadena como valores dobles. Decidí hacer una colección de objetos y usar sobrecarga y polimorfismo, pero hice algo mal.Sobrecarga en Java y despacho múltiple

corro una pequeña prueba:

public class OOP { 
    void prova(Object o){ 
     System.out.println("object"); 
    } 

    void prova(Integer i){ 
    System.out.println("integer"); 
    } 

    void prova(String s){ 
     System.out.println("string"); 
    } 

    void test(){ 
     Object o = new String(" "); 
     this.prova(o); // Prints 'object'!!! Why?!?!? 
    } 

    public static void main(String[] args) { 
     OOP oop = new OOP(); 
     oop.test(); // Prints 'object'!!! Why?!?!? 
    } 
} 

En la prueba se parece como el tipo de argumento se decide en tiempo de compilación y no en tiempo de ejecución. ¿Porqué es eso?

Esta pregunta se relaciona con:

Polymorphism vs Overriding vs Overloading
Try to describe polymorphism as easy as you can

EDIT:

Ok el método a ser llamado se decide en tiempo de compilación. ¿Hay alguna solución para evitar el uso del operador instanceof?

+0

Porque así es como funciona la sobrecarga de Java. –

+0

Pero ese objeto es un String en tiempo de ejecución, y hay un método definido para las cadenas. ¿Qué estoy haciendo mal? –

+3

Está asumiendo que la elección de qué sobrecarga llamar se realiza en tiempo de ejecución. No es; se decide en tiempo de compilación, según el tipo estático del argumento. –

Respuesta

17

Esta publicación contiene la respuesta de voo y proporciona detalles sobre/alternativas al enlace tardío.

General Las JVM solo usan único despacho: el tipo de tiempo de ejecución solo se considera para el objeto receptor; para los parámetros del método, se considera el tipo estático. Una implementación eficiente con optimizaciones es bastante fácil usando method tables (que son similares a las tablas virtuales de C++). Puede encontrar detalles, p. en the HotSpot Wiki.

Si desea envío múltiple para sus parámetros, echar un vistazo a

  • groovy. Pero, según mi conocimiento más reciente, tiene una implementación desacostumbrada y lenta de despacho múltiple (ver, por ejemplo, this performance comparison), p. sin almacenamiento en caché
  • clojure, pero eso es bastante diferente a Java.
  • MultiJava, que ofrece despacho múltiple para Java. Además, puede usar
    • this.resend(...) en lugar de super(...) para invocar el método reemplazado más específico del método adjunto;
    • envío de valor (ejemplo de código a continuación).

Si desea palo con Java, puede

  • rediseñar su aplicación moviendo métodos sobrecargados más de una jerarquía de clases de grano más fino. Se proporciona un ejemplo en Josh Bloch's Effective Java, Artículo 41 (Usar sobrecarga juiciosamente);
  • utiliza algunos patrones de diseño, como Estrategia, Visitante, Observador. Con frecuencia, estos pueden resolver los mismos problemas que el envío múltiple (es decir, en esas situaciones, usted tiene soluciones triviales para esos patrones que usan despacho múltiple).

Valor de despacho:

class C { 
    static final int INITIALIZED = 0; 
    static final int RUNNING = 1; 
    static final int STOPPED = 2; 
    void m(int i) { 
    // the default method 
    } 
    void m([email protected]@INITIALIZED i) { 
    // handle the case when we're in the initialized `state' 
    } 
    void m([email protected]@RUNNING i) { 
    // handle the case when we're in the running `state' 
    } 
    void m([email protected]@STOPPED i) { 
    // handle the case when we're in the stopped `state' 
    } 
} 
+0

Multijava es muy interesante, pero la última versión es del año 2006. La Java efectiva de Josh Bloch simplemente dice que tienes que crear muchas clases con un solo método, eso es algo que evitaría. Los patrones son el camino a seguir. –

+0

Oh - interesante y triste que Multijava ya no se mantenga. Supongo que los chicos se están enfocando más en otros proyectos como el Java Modeling Language. Sobre el ejemplo de Effective Java: ¿no puedes transferir eso a casos más generales? P.ej. Si necesita distinguir 3 comportamientos en su ejemplo, use un GeneralDecorator y dos subclases NumberDecorator y TextualDecorator. Entonces solo necesita OOP.prova (GeneralDecorator), que llama a SomeMethod() desde GeneralDecorator, que se envía solo. ¿O lo llamarías un patrón ya (por ejemplo, inversión de control)? – DaveFar

+0

Btw, [aquí hay una publicación en el blog sobre cómo resolver su problema en Scala] (http://cleverlytitled.blogspot.de/2010/01/dynamic-dispatch-in-scala.html), que solo tiene un solo despacho, también. La solución no es posible en Java, ya que usa los rasgos de Scala. – DaveFar

1

esto no es polymoprhism, sólo tiene que haber un método sobrecargado y lo llamó con el parámetro de tipo de objeto

3

Al llamar a un método que se sobrecargado, Java recoge el tipo más restrictivo por el tipo de la variable pasó a la función. No usa el tipo de la instancia real.

1

Todo en Java es un objeto Object/(excepto los tipos primitivos). Almacena cadenas y enteros como objetos, y luego, al llamar al método prove, todavía se los llama objetos. Debería echarle un vistazo a la palabra clave instanceof. Check this link

void prove(Object o){ 
    if (o instanceof String) 
    System.out.println("String"); 
    .... 
} 
10

Lo que queremos es el doble o más en general multiple dispatch, algo que se llevan a la práctica en otros idiomas (Common Lisp viene a la mente)

Es de suponer que la razón principal de Java no lo tiene, se debe se trata de una penalización de rendimiento porque la resolución de sobrecarga debe hacerse en tiempo de ejecución y no en tiempo de compilación. La forma habitual de solucionar esto es el visitor pattern, bastante feo, pero así son las cosas.

+0

Respondo tu respuesta (+1), aunque no estoy seguro de que las técnicas de compilación actuales sean mucho más lentas para envíos múltiples (consulta mi respuesta a continuación). ¿Tiene alguna estadística actual sobre eso? – DaveFar

+0

@DaveBall No he realizado ninguna prueba y ciertamente dudo que sea "mucho más lenta" en la mayoría de las situaciones. Pero ciertamente agrega un nivel adicional de complejidad a la optimización y puedo pensar en situaciones comunes en las que obtendríamos una llamada virtual en lugar de una llamada directa. Muchas clases de Java implementan una interfaz como la única clase, que permite la optimización habitual. Esto se basa en el hecho de que muchas interfaces son implementadas por una sola clase. Contrario a esto, la mayoría de los métodos toman parámetros bastante generales cuando existe más de una subclase aplicable (por ejemplo, todas las clases de colección) – Voo

+0

cont. Así que tenemos algunos gastos generales, básicamente se reduce a algunos en cascada y carga la clase para todos los parámetros (aunque es bastante barato) en tiempo de ejecución. No creo que afecte el caso general, aunque no necesitamos despacho múltiple y en los casos en que lo hacemos evita una clase molesta de errores y realmente aborrezco el patrón de visitante. – Voo

1

vieja pregunta, pero la respuesta no ofrece una solución concreta en Java para resolver el problema de una manera limpia.
De hecho, no es una pregunta fácil pero muy interesante. Aquí está mi contribución.

De acuerdo, el método a llamar se decide en tiempo de compilación. ¿Existe una solución alternativa para evitar el uso del operador instanceof?

Como se dice en la excelente respuesta @DaveFar, Java admite solo el método de envío único.
En este modo de envío, el compilador limita el método para invocar tan pronto como la compilación confiando en los tipos declarados de los parámetros y no en sus tipos de tiempo de ejecución.

Tengo una colección (o lista o lista de arreglo) en el que quiero poner ambos valores de cadena y valores dobles.

Para resolver la respuesta de una manera limpia y utilizar un despacho doble, tenemos que traer la abstracción para los datos manipulados.
¿Por qué?

Aquí un enfoque ingenuo visitante ilustran el problema:

public class DisplayVisitor { 

    void visit(Object o) { 
     System.out.println("object")); 
    } 

    void visit(Integer i) { 
     System.out.println("integer"); 
    } 

    void visit(String s) { 
     System.out.println("string")); 
    } 

} 

Ahora, pregunta: ¿cómo las clases visitadas pueden invocar el método visit()?
El segundo envío de la implementación de despacho doble se basa en el contexto "este" de la clase que acepta ser visitado.
Así que tenemos que tener un método accept() en Integer, String y Object clases para llevar a cabo esta segunda expedición:

public void accept(DisplayVisitor visitor){ 
    visitor.visit(this); 
} 

Pero imposible! Las clases visitadas son clases integradas: String, Integer, Object.
Así que no tenemos forma de agregar este método.
Y, de todos modos, no queremos agregar eso.

Para implementar el envío doble, tenemos que poder modificar las clases que queremos pasar como parámetro en el segundo despacho.
Entonces en lugar de manipular Object y List<Object> como tipo declarado, manipularemos Foo y List<Foo> donde la clase Foo es un contenedor que contiene el valor del usuario.

Aquí es la interfaz Foo:

public interface Foo { 
    void accept(DisplayVisitor v); 
    Object getValue(); 
} 

getValue() devuelve el valor para el usuario.
Especifica Object como tipo de retorno, pero Java admite retornos de covarianza (desde la versión 1.5), por lo que podríamos definir un tipo más específico para cada subclase para evitar downcasts.

ObjectFoo

public class ObjectFoo implements Foo { 

    private Object value; 

    public ObjectFoo(Object value) { 
     this.value = value; 
    } 

    @Override 
    public void accept(DisplayVisitor v) { 
     v.visit(this); 
    } 

    @Override 
    public Object getValue() { 
     return value; 
    } 

} 

StringFoo

public class StringFoo implements Foo { 

    private String value; 

    public StringFoo(String string) { 
     this.value = string; 
    } 

    @Override 
    public void accept(DisplayVisitor v) { 
     v.visit(this); 
    } 

    @Override 
    public String getValue() { 
     return value; 
    } 

} 

IntegerFoo

public class IntegerFoo implements Foo { 

    private Integer value; 

    public IntegerFoo(Integer integer) { 
     this.value = integer; 
    } 

    @Override 
    public void accept(DisplayVisitor v) { 
     v.visit(this); 
    } 

    @Override 
    public Integer getValue() { 
     return value; 
    } 

} 

Aquí está el DisplayVisitor clase visitar Foo subclases:

public class DisplayVisitor { 

    void visit(ObjectFoo f) { 
     System.out.println("object=" + f.getValue()); 
    } 

    void visit(IntegerFoo f) { 
     System.out.println("integer=" + f.getValue()); 
    } 

    void visit(StringFoo f) { 
     System.out.println("string=" + f.getValue()); 
    } 

} 

Y aquí es un código de ejemplo para probar la aplicación:

public class OOP { 

    void test() { 

     List<Foo> foos = Arrays.asList(new StringFoo("a String"), 
             new StringFoo("another String"), 
             new IntegerFoo(1), 
             new ObjectFoo(new AtomicInteger(100))); 

     DisplayVisitor visitor = new DisplayVisitor(); 
     for (Foo foo : foos) { 
      foo.accept(visitor); 
     } 

    } 

    public static void main(String[] args) { 
     OOP oop = new OOP(); 
     oop.test(); 
    } 
} 

Salida:

cadena = una cadena

cadena = otra cadena

número entero = 1

objeto = 100


Mejorar la aplicación

La implementación real requiere la introducción de una clase contenedora específico para cada buit-en tipo que quiero envolver Como se discutió, no tenemos la opción de operar un despacho doble.
Pero tenga en cuenta que el código se repite en Foo subclases podría evitarse:

private Integer value; // or String or Object 

@Override 
public Object getValue() { 
    return value; 
} 

de hecho podríamos introducir una clase genérica abstracta que contiene el valor del usuario y proporciona un descriptor de acceso a:

public abstract class Foo<T> { 

    private T value; 

    public Foo(T value) { 
     this.value = value; 
    } 

    public abstract void accept(DisplayVisitor v); 

    public T getValue() { 
     return value; 
    } 

} 

ahora Foo sublasses son más ligeros que declarar:

public class IntegerFoo extends Foo<Integer> { 

    public IntegerFoo(Integer integer) { 
     super(integer); 
    } 

    @Override 
    public void accept(DisplayVisitor v) { 
     v.visit(this); 
    } 

} 

public class StringFoo extends Foo<String> { 

    public StringFoo(String string) { 
     super(string); 
    } 

    @Override 
    public void accept(DisplayVisitor v) { 
     v.visit(this); 
    } 

} 

public class ObjectFoo extends Foo<Object> { 

    public ObjectFoo(Object value) { 
     super(value); 
    } 

    @Override 
    public void accept(DisplayVisitor v) { 
     v.visit(this); 
    } 

} 

Y el método test() se debe modificar para declarar un tipo de comodín (?) para el tipo Foo en la declaración List<Foo>.

void test() { 

    List<Foo<?>> foos = Arrays.asList(new StringFoo("a String object"), 
             new StringFoo("anoter String object"), 
             new IntegerFoo(1), 
             new ObjectFoo(new AtomicInteger(100))); 

    DisplayVisitor visitor = new DisplayVisitor(); 
    for (Foo<?> foo : foos) { 
     foo.accept(visitor); 
    } 

} 

De hecho, si realmente se necesita, podríamos simplificar más Foo subclases mediante la introducción de la generación de código java.

La declaración de esta subclase:

public class StringFoo extends Foo<String> { 

    public StringFoo(String string) { 
     super(string); 
    } 

    @Override 
    public void accept(DisplayVisitor v) { 
     v.visit(this); 
    } 

} 

podía tan simple como la que se declara una clase y la adición de una anotación en:

@Foo(String.class) 
public class StringFoo { } 

Dónde Foo es una anotación personalizada procesado en tiempo de compilación.