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.
Porque así es como funciona la sobrecarga de Java. –
Pero ese objeto es un String en tiempo de ejecución, y hay un método definido para las cadenas. ¿Qué estoy haciendo mal? –
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. –