Чому виклик статичного методу через екземпляр не є помилкою для компілятора Java?


76

Я впевнений, що всі ви знаєте поведінку, яку я маю на увазі - такий код, як:

Thread thread = new Thread();
int activeCount = thread.activeCount();

провокує попередження компілятора. Чому це не помилка?

РЕДАГУВАТИ:

Щоб бути зрозумілим: питання не має нічого спільного з Threads. Я усвідомлюю, що під час обговорення часто наводяться приклади ниток, оскільки вони дійсно можуть зіпсувати щось із ними. Але насправді проблема в тому, що таке використання - це завжди нісенітниця, і ти не можеш (грамотно) написати такий дзвінок і думати про це. Будь-який приклад виклику методу цього типу буде безглуздим. Ось ще:

String hello = "hello";
String number123AsString = hello.valueOf(123);

Це робить його таким, ніби кожен екземпляр рядка має метод "String valueOf (int i)".


15
Щоб розширити вашу точку зору, надлишковий екземпляр може бути навіть нульовим: String hello = null; hello.valueOf(123);працює!
Тіло

Відповіді:


79

В основному я вважаю, що дизайнери Java допустили помилку, коли розробляли мову, і занадто пізно це виправляти через проблеми сумісності. Так, це може призвести до дуже оманливого коду. Так, вам слід уникати цього. Так, вам слід переконатись, що ваша IDE налаштована так, щоб сприймати це як помилку, IMO. Якщо ви коли-небудь розробляли мову самостійно, майте це на увазі як приклад того, чого слід уникати :)

Щоб відповісти на думку DJClayworth, ось що дозволено в C #:

public class Foo
{
    public static void Bar()
    {
    }
}

public class Abc
{
    public void Test()
    {
        // Static methods in the same class and base classes
        // (and outer classes) are available, with no
        // qualification
        Def();

        // Static methods in other classes are available via
        // the class name
        Foo.Bar();

        Abc abc = new Abc();

        // This would *not* be legal. It being legal has no benefit,
        // and just allows misleading code
        // abc.Def();
    }

    public static void Def()
    {
    }
}

Чому, на мою думку, це вводить в оману? Тому що, якщо я розглядаю код, someVariable.SomeMethod()я очікую, що він використовуватиме значенняsomeVariable . Якщо SomeMethod()є статичним методом, це очікування недійсне; код мене обманює. Як це може бути добре ?

Як не дивно, Java не дозволяє використовувати потенційно неініціалізовану змінну для виклику статичного методу, незважаючи на те, що єдиною інформацією, яку вона буде використовувати, є оголошений тип змінної. Це непослідовний і безпомічний безлад. Навіщо це дозволяти?

EDIT: Це редагування є відповіддю на відповідь Клейтона, який стверджує, що дозволяє успадкування для статичних методів. Це не так. Статичні методи просто не є поліморфними. Ось коротка, але повна програма, яка демонструє:

class Base
{
    static void foo()
    {
        System.out.println("Base.foo()");
    }
}

class Derived extends Base
{
    static void foo()
    {
        System.out.println("Derived.foo()");
    }
}

public class Test
{
    public static void main(String[] args)
    {
        Base b = new Derived();
        b.foo(); // Prints "Base.foo()"
        b = null;
        b.foo(); // Still prints "Base.foo()"
    }
}

Як бачите, значення часу виконання bповністю ігнорується.


Думаю, вам може чогось не вистачити. Дивіться мій пост.
DJClayworth 04.03.09

1
@DJClayworth: Ні. Це дозволено в C #, але someInstanceOfMyClass.doComplexCalculation () не буде.
Джон Скіт,

5
@Jon: Я провів подібний тест і підтвердив ваші висновки. Виходячи з цього, я погоджуюсь, що неправильно для Java дозволяти статичні методи викликати з екземпляра var. Жодних можливостей не отримується (як я спочатку думав), але потенціал плутанини або помилок збільшується.
Клейтон

Значення часу виконання b повністю ігнорується, оскільки ви викликаєте статичний метод, який залежить лише від класу b, а не від його значення. Усі значення типу Base мають однаковий метод. Ви оголосили базу b, тому b є базою, а не похідною. Якби ви оголосили Derived b, він би надрукував "Derived.foo ()".
Matthew

@Matthew: Так, я знаю ... Я не впевнений, що ви намагаєтесь сказати. (Я б особисто не сказав, що "всі значення типу Base мають однаковий метод" - екземпляр взагалі не "має" методу ... але, я думаю, я знаю, що ви маєте на увазі.)
Джон Скіт,

15

Чому це помилка? Екземпляр має доступ до всіх статичних методів. Статичні методи не можуть змінити стан екземпляра (спроба - це помилка компіляції).

Проблема з відомим прикладом, який ви даєте, дуже специфічна для потоків , а не статичних викликів методів. Схоже, ви отримуєте activeCount()для потоку, на який посилається thread, але ви дійсно отримуєте рахунок для виклику потоку. Це логічна помилка, яку ви як програміст робите. У цьому випадку компілятор повинен зробити попередження. Ви повинні прислухатися до попередження та виправити свій код.

EDIT: Я розумію, що синтаксис мови - це те, що дозволяє писати оманливий код, але пам’ятайте, що компілятор та його попередження також є частиною мови. Мова дозволяє зробити щось, що компілятор вважає сумнівним, але воно дає вам попередження, щоб переконатися, що ви знаєте, що це може спричинити проблеми.


9
Це не має значення для інстанції . Єдине, це оголошений тип змінної. Це жахлива, жахлива помилка та помилка з боку дизайнерів мов, IMO.
Джон Скіт,

1
Так, це не має значення для екземпляра, але я не думаю, що виклик статичного методу з екземпляра повинен бути повноцінною помилкою. Це просто дуже поганий стиль. IMHO, клас повинен бути або всіма статичними методами, або статичними методами повинен бути приватний, але я б не хотів, щоб компілятор це застосовував.
Білл Ящірка

1
@Bill: Я не думаю, що це навіть давало попередження в перші дні. Попередження було додано, оскільки воно вводило людей в оману. Яка користь від цього дозволу?
Джон Скіт,

4
@Bill: Поставте це так - якби Java цього не дозволила, і хтось запропонував, що це має дозволити, якою би була ваша реакція? Ви б справді сперечалися на користь цього? Або ви могли б стверджувати (як і я), що це безглузда особливість, яка без потреби дозволяє оманливий код?
Джон Скіт,

2
Я не купую причину "часу компіляції" - очевидно, що ви шукаєте метод за допомогою посилання; скільки часу потім потрібно, щоб перевірити, чи це статично чи ні? Я думаю, що простіше просто вважати це помилкою мовного дизайну.
Джон Скіт

9

Вони більше не можуть зробити помилку через весь код, який вже є.

Я з вами, що це повинна бути помилка. Можливо, повинен бути варіант / профіль для компілятора, щоб оновити деякі попередження до помилок.

Оновлення: Коли вони ввели ключове слово assert у 1.4, яке має подібні потенційні проблеми сумісності зі старим кодом, вони зробили його доступним, лише якщо ви явно встановили вихідний режим на "1.4" . Я припускаю, що це може зробити помилку в новому режимі джерела "Java 7". Але я сумніваюся, що вони зробили б це, враховуючи, що всі клопоти це може спричинити. Як зазначали інші, не потрібно суворо заважати вам писати заплутаний код. І мовні зміни в Java повинні обмежуватися строго необхідними на даний момент.


У компіляторі Eclipse є така опція (indirectStatic, help.eclipse.org/help33/index.jsp?topic=/… )
Пітер Штібрані,

1
Але вони зробили це, коли представили ключове слово assert в Java 1.4. Чому вони не можуть повторити це так само?
Хосам Алі

Ти маєш рацію. Для Assert вам потрібно спеціально встановити рівень компілятора до 1,4, інакше він все одно компілюється без assert. Я припускаю, що 1,5 анотації та дженерики працюють однаково. Отже, можна мати рівень компіляції Java 7, що зробить це помилкою.
Тіло


2

Ймовірно, для того самого логічного, що робить це не помилкою:

public class X
{
    public static void foo()
    {
    }

    public void bar()
    {
        foo(); // no need to do X.foo();
    }
}

2

Справді важливою річчю, з точки зору компілятора, є можливість розпізнавання символів. У випадку статичного методу він повинен знати, в якому класі шукати його, оскільки він не пов'язаний з яким-небудь конкретним об'єктом. Дизайнери Java, очевидно, вирішили, що оскільки вони можуть визначити клас об'єкта, вони можуть також вирішити клас будь-якого статичного методу для цього об'єкта з будь-якого екземпляра об'єкта. Вони вирішили дозволити цьому - можливо, похитнувшись спостереженням @ TofuBeer - щоб надати програмісту певної зручності. Інші дизайнери мов зробили різний вибір. Можливо, я потрапив би до останнього табору, але для мене це не така вже й велика справа. Можливо, я дозволив би використання, про яке згадує @TofuBeer,


2

Це не помилка, оскільки це частина специфікації, але ви, очевидно, запитуєте про обґрунтування, про яке ми всі можемо здогадуватися.

Я припускаю, що джерелом цього є насправді дозволити методу в класі викликати статичний метод у тому ж класі без клопоту. Оскільки виклик x () є законним (навіть без імені власного класу), виклик this.x () також повинен бути законним, а отже, виклик через будь-який об'єкт також став законним.

Це також допомагає спонукати користувачів перетворювати приватні функції на статичні, якщо вони не змінюють стан.

Крім того, компілятори зазвичай намагаються уникати оголошення помилок, коли немає можливості, що це може призвести до прямої помилки. Оскільки статичний метод не змінює стан і не турбується про виклик об'єкта, він не викликає фактичної помилки (просто плутанини), щоб дозволити це. Достатньо попередження.


2
@Uri: Досить просто зробити його доступним у поточному класі, не вказуючи назви класу. C # впорався з цим, а компілятори Java спромоглися визначити різницю (оскільки в такому випадку ви не отримаєте попередження), то чому це слід вказувати саме так?
Джон Скіт,

2

Метою посилання на змінну екземпляра є лише надання типу, який включає статику. Якщо ви подивитесь на байтовий код, що викликає статичний за допомогою instance.staticMethod або EnclosingClass.staticMethod, створюється той самий виклик статичного методу байт-коду. Посилання на екземпляр не відображається.

Відповідь також, чому це все там, ну просто є. Поки ви використовуєте клас. а не через екземпляр ви допоможете уникнути плутанини в майбутньому.


1

Можливо, ви можете змінити його у своїй IDE (у налаштуваннях Eclipse -> Java -> Компілятор -> Помилки / попередження)


0

Тут немає варіанту. У Java (як і в багатьох інших lang.) Ви можете отримати доступ до всіх статичних членів класу через його ім'я класу або об'єкт екземпляра цього класу. Це залежить від вас, вашого випадку та програмного рішення, яке з них вам слід використовувати, що забезпечує більшу читабельність.


0

Це досить стара тема, але все ще актуальна і напрочуд приносить більший вплив у наш час. Як зазначав Джон, це може бути просто помилкою, яку зробили дизайнери Java на самому початку. Але я не уявляю, що це може вплинути на безпеку.

Багато кодерів знають Apache Velocity, гнучкий і потужний механізм шаблонів. Він настільки потужний, що дозволяє подавати шаблон із набором іменованих об'єктів - суворо розглядається як об'єкти з мови програмування (спочатку Java). До цих об’єктів можна отримати доступ із шаблону, як у мові програмування, тому, наприклад, екземпляр Java String може використовуватися з усіма його загальнодоступними полями, властивостями та методами

$input.isEmpty()

де введенням є рядок , проходить безпосередньо через JVM і повертає true або false у вихідні дані синтаксичного аналізатора Velocity). Все йде нормально.

Але в Java всі об'єкти успадковуються від Object, тому наші кінцеві користувачі також можуть помістити це в шаблон

$input.getClass()

отримати екземпляр String Class.

І за допомогою цього посилання вони також можуть викликати статичний метод forName (String) щодо цього

$input.getClass().forName("java.io.FileDescriptor")

використовуйте будь-яке ім'я класу та використовуйте його для будь-якого облікового запису веб-сервера (деформувати, красти вміст БД, перевіряти конфігураційні файли, ...)

Цей подвиг якимось чином (у конкретному контексті) описаний тут: https://github.com/veracode-research/solr-injection#7-cve-2019-17558-rce-via-velocity-template-by-_s00py

Це було б неможливо, якби заборонялося викликати статичні методи з посилання на екземпляр класу.

Я не кажу, що певна програма програмування є кращою за іншу або близько того, але я просто хочу порівняти. Існує порт Apache Velocity для .NET. У C # неможливо викликати статичні методи лише з посилання на екземпляр, що робить експлойт таким чином марним:

$input.GetType().GetType("System.IO.FileStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")

-9

Я просто враховую це:

instanceVar.staticMethod();

щоб бути скороченням для цього:

instanceVar.getClass().staticMethod();

Якщо вам завжди доводилося це робити:

SomeClass.staticMethod();

тоді ви не зможете використовувати успадкування для статичних методів.

Тобто, викликаючи статичний метод через екземпляр, вам не потрібно знати, який конкретний клас є екземпляром під час компіляції, лише те, що він реалізує staticMethod () десь уздовж ланцюжка успадкування.

EDIT: Ця відповідь неправильна. Детальніше див. У коментарях.


1
Добре, це ще одна причина, чому цього не можна дозволяти - адже вас теж вводять в оману. Це діє не так, як ти хочеш. Він буде тільки використовувати заявлений тип «instanceVar» в вашому прикладі. Чорт візьми, значення instanceVar може бути навіть нульовим.
Джон Скіт,

1
@Clayton: Так, але це не викликає getClass (). Я відредагую свою відповідь на прикладі.
Джон Скіт,

3
(І якщо він справді викликав getClass (), він не міг би викликати staticMethod (), оскільки це не метод у класі <T>.)
Джон Скіт,

5
@ Джон: Я збираюся залишити це тут. Я вчуся на помилках стільки, скільки будь-що інше, тому це може допомогти комусь іншому побачити це. Спасибі за вашу допомогу.
Клейтон,

3
@Clayton: Ви можете зробити цю вікі-спільноту, щоб ми могли проголосувати за неї, не караючи вас.
Білл Ящірка
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.