Скрытие имени Java: трудный путь

96

У меня проблема со скрытием имени, которую очень сложно решить. Вот упрощенная версия, объясняющая проблему:

Есть класс: org.A

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

Тогда есть класс net.foo.X

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

А теперь вот проблемный класс, который наследуется от Aи хочет вызватьnet.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
    }
}

Как видите, это невозможно. Я не могу использовать простое имя, Xпотому что оно скрыто унаследованным типом. Я не могу использовать полное имя net.foo.X, потому что netоно скрыто унаследованным полем.

В Bмоей кодовой базе есть только класс ; классы net.foo.Xи org.Aявляются библиотечными, поэтому я не могу их изменить!

Мое единственное решение выглядит так: я мог бы вызвать другой класс, который в свою очередь вызывает X.doSomething(); но этот класс будет существовать только из-за конфликта имен, что кажется очень запутанным! Нет ли раствор , в котором я могу напрямую позвонить X.doSomething()из B.doSomething()?

На языке, который позволяет указывать глобальное пространство имен, например, global::в C # или ::C ++, я мог бы просто использовать префикс netэтого глобального префикса, но Java этого не позволяет.

гексицид
источник
Некоторое быстрое исправление может быть таким: public void help(net.foo.X x) { x.doSomething(); }и позвонить сhelp(null);
Absurd-Mind
@ Absurd-Mind: Ну, вызов статического метода через несуществующий объект кажется еще более беспорядочным, чем мое решение :). Я даже получу предупреждение компилятора. Но правда, это было бы еще одним хакерским решением помимо моего.
gexicide 04
@JamesB: Это вызовет неправильный X! net.foo.Xесть метод, нет org.A.X!
gexicide 04
3
Вы должны унаследовать от A? Наследование может быть очень неприятным, как вы уже выяснили…
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 за отношение чистого кода. Но мне кажется, что в этой ситуации вам следует пойти на компромисс. Просто сделайте это и напишите хороший длинный комментарий о том, почему вам пришлось это сделать (возможно, со ссылкой на этот вопрос).
sampathsris 06

Ответы:

84

Вы можете привести nullк типу, а затем вызвать для него метод (который будет работать, поскольку целевой объект не участвует в вызове статических методов).

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

Это имеет преимущества

  • отсутствие побочных эффектов (проблема с созданием экземпляра net.foo.X),
  • не требуя переименования чего-либо (так что вы можете дать методу Bимя, которое хотите, поэтому a import staticне будет работать в вашем конкретном случае),
  • не требуя введения класса делегата (хотя это может быть хорошей идеей…), и
  • не требует накладных расходов или сложности работы с API отражения.

Обратной стороной является то, что этот код действительно ужасен! Для меня это генерирует предупреждение, и это в целом хорошо. Но поскольку он работает с проблемой, которая в противном случае совершенно непрактична, добавление

@SuppressWarnings("static-access")

в подходящей (минимальной!) точке включения компилятор выключится.

Donal Fellows
источник
1
Почему нет? Вы бы просто поступили правильно и реорганизовали код.
Gimby 04
1
Статический импорт не намного понятнее, чем это решение, и работает не во всех случаях (вы облажались, если doSomethingв вашей иерархии есть метод), так что да, лучшее решение.
Voo
@Voo Я открыто признаю, что на самом деле я пошел и попробовал все варианты, которые другие люди указали в качестве ответов, сначала точно воспроизведя исходную проблему, и я был поражен, насколько сложно было ее решить. Исходный вопрос аккуратно закрывает все другие, более приятные варианты.
Donal Fellows
1
Проголосуйте за творчество, хотя я бы никогда не сделал этого в производственном коде. Я считаю, что косвенное обращение - это ответ на эту проблему (разве это не для всех проблем?).
Ethanfar 06
Спасибо Доналу за это решение. Фактически, это решение выделяет места, где можно использовать «Тип» и нельзя использовать переменную. Таким образом, в «приведении» компилятор выберет «Тип», даже если существует переменная с таким же именем. Для большего количества таких интересных случаев я бы порекомендовал 2 книги "Java Pitfalls": books.google.co.in/books/about/… books.google.co.in/books/about/…
RRM
38

Вероятно, самый простой (не обязательно самый простой) способ справиться с этим - использовать класс делегата:

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

а потом ...

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

Это несколько многословно, но очень гибко - вы можете заставить его вести себя так, как хотите; плюс он работает примерно одинаково как с staticметодами, так и с объектами

Подробнее об объектах делегатов здесь: http://en.wikipedia.org/wiki/Delegation_pattern

blgt
источник
Наверное, самое чистое из имеющихся решений. Я бы, вероятно, использовал это, если бы у меня была эта проблема.
LordOfThePigs 05
37

Вы можете использовать статический импорт:

import static net.foo.X.doSomething;

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

Остерегайтесь этого Bи Aне используйте методы с именемdoSomething

Абсурд-разум
источник
Разве net.foo.X.doSomething имеет доступ только к пакету? Это означает, что вы не можете получить к нему доступ из пакета com.bar
JamesB
2
@JamesB Да, но тогда полный вопрос не будет иметь смысла, поскольку метод никак не будет доступен. Я думаю, что это ошибка при упрощении примера
Absurd-Mind
1
Но исходный вопрос, кажется, активно хотят использовать doSomethingв качестве названия метода в B
Donal Fellows
3
@DonalFellows: Вы правы; это решение не сработает, если мой код останется таким, как я опубликовал. К счастью, я могу переименовать метод. Однако представьте себе случай, когда мой метод переопределяет другой метод, и я не могу его переименовать. В этом случае такой ответ действительно не решит проблему. Но это было бы еще более случайным совпадением, чем моя проблема, так что, возможно, никогда не будет человека, который столкнется с такой проблемой с конфликтом тройных имен :).
gexicide 04
4
Чтобы прояснить для всех остальных: это решение не работает, как только у вас есть doSomethingместо в иерархии наследования (из-за того, как решаются цели вызова методов). Так что Кнут поможет вам, если вы хотите вызвать toStringметод. Решение, предложенное Доналом, - это то, что есть в книге Блоха о Java Puzzlers (я думаю, что не искал его), поэтому мы можем считать его действительно авторитетным ответом :-)
Voo
16

Правильный способ сделать это - статический импорт, но в самом худшем случае вы МОЖЕТЕ создать экземпляр класса, используя отражение, если вы знаете его полное имя.

Java: новый экземпляр класса, не имеющий конструктора по умолчанию

А затем вызовите метод экземпляра.

Или просто вызовите сам метод с отражением: вызов статического метода с использованием отражения

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

Конечно, это явно последние средства.

EpicPandaForce
источник
2
Этот ответ, безусловно, является самым большим перебором до сих пор - пальцы вверх :)
gexicide
3
За этот ответ вы должны получить значок завершителя; вы предоставили ответ тому единственному бедному человеку, которому приходится использовать древнюю версию Java и решать именно эту проблему.
Gimby 04
Я вообще-то не думал о том, что static importфича добавили только в Java 1.5. Я не завидую людям, которым нужно разрабатывать под 1.4 и ниже, мне пришлось однажды и это было ужасно!
EpicPandaForce 04
1
@Gimby: Ну, все еще есть ((net.foo.X)null).doSomething()решение, которое работает в старой java. Но если тип Aдаже содержит внутренний тип net, ТО это единственный ответ, который остается действительным :).
gexicide 04
6

Не совсем ответ, но вы можете создать экземпляр X и вызвать на нем статический метод. Это был бы способ (грязный, я признаю) вызвать ваш метод.

(new net.foo.X()).doSomething();
Майкл Лаффарг
источник
3
Приведение nullк типу и последующий вызов метода для него было бы лучше, поскольку создание объекта может иметь побочные эффекты, которых я не хочу.
gexicide 04
@gexicide Эта идея кастинга null, казалось бы, была одним из наиболее чистых способов сделать это. Должен @SuppressWarnings("static-access")быть без предупреждений…
Donal Fellows
Спасибо за точность null, но кастинг nullвсегда был для меня разбитым сердцем.
Майкл Лаффарг
4

Нет необходимости выполнять приведение или подавлять какие-либо странные предупреждения или создавать какой-либо избыточный экземпляр. Просто уловка, использующая тот факт, что вы можете вызывать статические методы родительского класса через подкласс. (Подобно моему хакерскому решению здесь .)

Просто создайте такой класс

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

(Частный конструктор в этом последнем классе гарантирует, что никто не сможет случайно создать экземпляр этого класса.)

Тогда вы можете позвонить X.doSomething()через него:

    public class B extends A {

        public void doSomething() {
            XX.doSomething();
        }
billc.cn
источник
Интересен еще один подход. Однако класс со статическим методом является последним служебным классом.
gexicide 05
Да, в таком случае нельзя использовать этот трюк. Еще одна причина, почему статический метод - это ошибка языка, и следует использовать объектный подход Scala.
billc.cn 05
Если вы все равно собираетесь создать другой класс, то, вероятно, лучше всего будет использовать класс делегата, как описано в ответе blgt.
LordOfThePigs 05
@LordOfThePigs Тем не менее, это позволяет избежать лишних вызовов методов и будущей нагрузки на рефакторинг. Новый класс в основном служит псевдонимом.
billc.cn 06
2

Что, если вы попытаетесь получить глобальное пространство имен, учитывая, что все файлы находятся в одной папке. ( 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...

         }
     }
Vectoria
источник
Как это работает? AFAIK getGlobal()не является стандартным методом Java для пакетов ... (я думаю, что у пакетов не может быть никаких методов в Java ...)
siegi
1

Это одна из причин, по которой композиция предпочтительнее наследования.

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

Я бы использовал паттерн Стратегия.

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();
    }
}

Теперь вы также развязали свою зависимость, поэтому ваши модульные тесты будет проще писать.

Эрик Мэдсен
источник
Вы забыли добавить implements SomethingStrategyв XSomethingStrategyобъявление класса.
Рикардо Соуза,
@rcdmk Спасибо. Должен быть исправлен сейчас.
Эрик Мэдсен