Ocultación de nombres en Java: el camino difícil


96

Tengo un problema con la ocultación de nombres que es extremadamente difícil de resolver. Aquí hay una versión simplificada que explica el problema:

Hay una clase: org.A

package org;
public class A{
     public class X{...}
     ...
     protected int net;
}

Entonces hay una clase net.foo.X

package net.foo;
public class X{
     public static void doSomething();
}

Y ahora, aquí está la clase problemática que hereda Ay quiere llamarnet.foo.X.doSomething()

package com.bar;
class B extends A {

    public void doSomething(){
        net.foo.X.doSomething(); // doesn't work; package net is hidden by inherited field
        X.doSomething(); // doesn't work; type net.foo.X is hidden by inherited X
    }
}

Como ve, esto no es posible. No puedo usar el nombre simple Xporque está oculto por un tipo heredado. No puedo usar el nombre completo net.foo.Xporque netestá oculto por un campo heredado.

Solo la clase Bestá en mi base de código; las clases net.foo.Xy org.Ason clases de biblioteca, ¡así que no puedo alterarlas!

Mi única solución se ve así: podría llamar a otra clase que a su vez llama X.doSomething(); pero esta clase solo existiría debido al choque de nombres, ¡que parece muy complicado! ¿No hay solución en la que puedo llamar directamente X.doSomething()desde B.doSomething()?

En un lenguaje que permite especificar el espacio de nombres global, por ejemplo, global::en C # o ::en C ++, podría simplemente prefijar neteste prefijo global, pero Java no lo permite.


Una solución rápida podría ser esta: public void help(net.foo.X x) { x.doSomething(); }y llamar conhelp(null);
Absurd-Mind

@ Absurd-Mind: Bueno, llamar a un método estático a través de un objeto inexistente parece incluso más complicado que mi solución :). Incluso recibiré una advertencia del compilador. Pero es cierto, esta sería otra solución hacky además de la mía.
gexicida

@JamesB: ¡Esto instanciaría la X incorrecta! net.foo.Xtiene el método, no org.A.X!
gexicida

3
¿Se tiene que heredar de A? La herencia puede ser tan desagradable, como has descubierto ...
Donal Fellows

2
I could call another class that in turn calls X.doSomething(); but this class would only exist because of the name clash, which seems very messy+1 por actitud de código limpio. Pero para mí, parece que esta es una situación en la que debería hacer una compensación. Simplemente haga esto y envíe un comentario largo y agradable sobre por qué tuvo que hacerlo (probablemente con un enlace a esta pregunta).
sampathsris

Respuestas:


84

Puede convertir un nullal tipo y luego invocar el método en eso (lo que funcionará, ya que el objeto de destino no está involucrado en la invocación de métodos estáticos).

((net.foo.X) null).doSomething();

Esto tiene los beneficios de

  • estar libre de efectos secundarios (un problema con la creación de instancias net.foo.X),
  • no requiere cambiar el nombre de nada (por lo que puede dar el método con Bel nombre que desea que tenga; es por eso import staticque a no funcionará en su caso exacto),
  • no requiere la introducción de la clase de delegado (aunque eso podría ser una buena idea ...), y
  • no requiere la sobrecarga o la complejidad de trabajar con la API de reflexión.

¡La desventaja es que este código es realmente horrible! Para mí, genera una advertencia y eso es algo bueno en general. Pero dado que está solucionando un problema que de otro modo no sería práctico, agregar un

@SuppressWarnings("static-access")

en un punto de inclusión apropiado (¡mínimo!) cerrará el compilador.


1
Por qué no? Simplemente haría lo correcto y refactorizaría el código.
Gimby

1
Las importaciones estáticas no son mucho más claras que esta solución y no funcionan en todos los casos (estás jodido si hay un doSomethingmétodo en alguna parte de tu jerarquía), así que sí, la mejor solución.
Voo

@Voo Admito libremente que en realidad fui y probé todas las opciones que otras personas habían enumerado como respuestas, habiendo reproducido primero exactamente cuál era el problema original, y me sorprendió lo difícil que era solucionarlo. La pregunta original cierra perfectamente todas las demás opciones más agradables.
Donal Fellows

1
Vote por la creatividad, sin embargo, nunca haría esto en el código de producción. Creo que la indirección es la respuesta a este problema (¿no es para todos los problemas?).
ethanfar

Gracias Donal por esta solución. En realidad, esta solución resalta los lugares donde se puede usar un "Tipo" y no se puede usar una variable. Entonces, en una "conversión", el compilador elegiría el "Tipo" incluso si hay una variable con el mismo nombre. Para más casos tan interesantes, recomendaría 2 libros "Java Pitfalls": books.google.co.in/books/about/… books.google.co.in/books/about/…
RRM

38

Probablemente la forma más simple (no necesariamente la más fácil) de administrar esto sería con una clase delegada:

import net.foo.X;
class C {
    static void doSomething() {
         X.doSomething();
    }
}

y entonces ...

class B extends A {
    void doX(){
        C.doSomething();
    }
}

Esto es algo detallado, pero muy flexible: puede hacer que se comporte de la forma que desee; además, funciona de la misma manera tanto con staticmétodos como con objetos instanciados

Más sobre objetos delegados aquí: http://en.wikipedia.org/wiki/Delegation_pattern


Probablemente la solución más limpia proporcionada. Probablemente lo usaría si tuviera ese problema.
LordOfThePigs

37

Puede utilizar una importación estática:

import static net.foo.X.doSomething;

class B extends A {
    void doX(){
        doSomething();
    }
}

Tenga cuidado con eso By Ano contenga métodos con nombredoSomething


¿Net.foo.X.doSomething solo tiene acceso al paquete? Lo que significa que no puede acceder a él desde el paquete com.bar
JamesB

2
@JamesB Sí, pero la pregunta completa no tendría sentido, ya que el método no sería accesible de ninguna manera. Creo que esto es un error al simplificar el ejemplo
Absurd-Mind

1
Pero la pregunta original parece querer usar activamente doSomethingcomo el nombre del método en B
Donal Fellows

3
@DonalFellows: Tienes razón; esta solución no funciona si mi código tuviera que permanecer como lo publiqué. Afortunadamente, puedo cambiar el nombre del método. Sin embargo, piense en un caso en el que mi método anula otro método, entonces no puedo cambiarle el nombre. En este caso, esta respuesta no resolvería el problema. Pero esto sería aún más coincidente de lo que ya es mi problema, por lo que tal vez nunca haya ningún humano que se encuentre con tal problema con el triple choque de nombres :).
gexicida

4
Para dejarlo en claro para todos los demás: esta solución no funciona tan pronto como tenga doSomethingalgún lugar en la jerarquía de herencia (debido a cómo se resuelven los destinos de llamadas a métodos). Entonces Knuth te ayudará si quieres llamar a un toStringmétodo. La solución propuesta por Donal es la que está en el libro Java Puzzlers de Bloch (creo que no lo he buscado), por lo que podemos considerarla la respuesta autorizada realmente :-)
Voo

16

La forma correcta de hacer las cosas sería la importación estática, pero en el peor de los casos, PODRÍA construir una instancia de la clase usando la reflexión si conoce su nombre completo.

Java: newInstance de clase que no tiene un constructor predeterminado

Y luego invoca el método en la instancia.

O simplemente invoque el método en sí con reflexión: invocando un método estático usando reflexión

Class<?> clazz = Class.forName("net.foo.X");
Method method = clazz.getMethod("doSomething");
Object o = method.invoke(null);

Por supuesto, estos son obviamente los últimos recursos.


2
Esta respuesta es, con mucho, la mayor exageración hasta ahora - pulgar hacia arriba :)
gexicide

3
Debería obtener una insignia de finalista para esta respuesta; proporcionaste la respuesta para esa pobre persona singular que tiene que usar una versión antigua de Java y resolver este problema exacto.
Gimby

De hecho, no pensé en el hecho de que la static importfunción se agregó solo en Java 1.5. No envidio a las personas que necesitan desarrollar para 1.4 o menos, tuve que hacerlo una vez y fue terrible.
EpicPandaForce

1
@Gimby: Bueno, todavía existe la ((net.foo.X)null).doSomething()solución que funciona en el antiguo java. Pero si el tipo Aincluso contiene un tipo interno net, ENTONCES esta es la única respuesta que sigue siendo válida :).
gexicida

6

No es realmente LA respuesta, pero puede crear una instancia de X y llamar al método estático en ella. Esa sería una forma (sucia, lo admito) de llamar a tu método.

(new net.foo.X()).doSomething();

3
Enviar nullal tipo y luego llamar al método sería mejor, ya que crear un objeto puede tener efectos secundarios que no deseo.
gexicida

@gexicide Esa idea de casting nullparece ser una de las formas más limpias de hacerlo. Necesita @SuppressWarnings("static-access")estar libre de advertencias ...
Donal Fellows

Gracias por la precisión null, pero el casting nullsiempre me ha roto el corazón.
Michael Laffargue

4

No es necesario realizar ningún lanzamiento o suprimir advertencias extrañas o crear una instancia redundante. Solo un truco usando el hecho de que puede llamar a métodos estáticos de la clase principal a través de la subclase. (Similar a mi solución pirateada aquí ).

Solo crea una clase como esta

public final class XX extends X {
    private XX(){
    }
}

(El constructor privado en esta clase final se asegura de que nadie pueda crear accidentalmente una instancia de esta clase).

Entonces puedes llamar a X.doSomething()través de él:

    public class B extends A {

        public void doSomething() {
            XX.doSomething();
        }

Interesante, otro enfoque más. Sin embargo, la clase con el método estático es una clase de utilidad final.
gexicida

Sí, no puedo usar este truco en ese caso. Otra razón más por la que el método estático es un lenguaje falla y se debe tomar el enfoque de objetos de Scala.
billc.cn

Si va a crear otra clase de todos modos, entonces usar una clase delegada como se describe en blgt answer es probablemente lo mejor.
LordOfThePigs

@LordOfThePigs Sin embargo, esto evita la llamada al método adicional y la carga de refactorización futura. La nueva clase básicamente sirve como alias.
billc.cn

2

¿Qué sucede si intenta obtener el espacio de nombres global dado que todos los archivos están en la misma carpeta? ( http://www.beanshell.org/javadoc/bsh/class-use/NameSpace.html )

    package com.bar;
      class B extends A {

       public void doSomething(){
         com.bar.getGlobal().net.foo.X.doSomething(); // drill down from the top...

         }
     }

¿Como funciona esto? AFAIK getGlobal()no es un método estándar de Java para paquetes ... (creo que los paquetes no pueden tener ningún método en Java ...)
siegi

1

Ésta es una de las razones por las que la composición es preferible a la herencia.

package com.bar;
import java.util.concurrent.Callable;
public class C implements Callable<org.A>
{
    private class B extends org.A{
    public void doSomething(){
        C.this.doSomething();
    }
    }

    private void doSomething(){
    net.foo.X.doSomething();
    }

    public org.A call(){
    return new B();
    }
}

0

Usaría el patrón de estrategia.

public interface SomethingStrategy {

   void doSomething();
}

public class XSomethingStrategy implements SomethingStrategy {

    import net.foo.X;

    @Override
    void doSomething(){
        X.doSomething();
    }
}

class B extends A {

    private final SomethingStrategy strategy;

    public B(final SomethingStrategy strategy){
       this.strategy = strategy;
    }

    public void doSomething(){

        strategy.doSomething();
    }
}

Ahora también ha desacoplado su dependencia, por lo que sus pruebas unitarias serán más fáciles de escribir.


Ha olvidado agregar un implements SomethingStrategyen la XSomethingStrategydeclaración de clase.
Ricardo Souza

@rcdmk Gracias. Debería arreglarse ahora.
Erik Madsen
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.