Интерфейсы со статическими полями в java для обмена "константами"

116

Я смотрю на некоторые проекты Java с открытым исходным кодом, чтобы проникнуть в Java, и заметил, что многие из них имеют какой-то интерфейс «констант».

Например, у processing.org есть интерфейс под названием PConstants.java , и большинство других базовых классов реализуют этот интерфейс. Интерфейс пронизан статическими элементами. Есть ли причина для такого подхода или это считается плохой практикой? Почему бы не использовать перечисления там, где это имеет смысл , или статический класс?

Мне кажется странным использовать интерфейс, позволяющий использовать какие-то псевдо «глобальные переменные».

public interface PConstants {

  // LOTS OF static fields...

  static public final int SHINE = 31;

  // emissive (by default kept black)
  static public final int ER = 32;
  static public final int EG = 33;
  static public final int EB = 34;

  // has this vertex been lit yet
  static public final int BEEN_LIT = 35;

  static public final int VERTEX_FIELD_COUNT = 36;


  // renderers known to processing.core

  static final String P2D    = "processing.core.PGraphics2D";
  static final String P3D    = "processing.core.PGraphics3D";
  static final String JAVA2D = "processing.core.PGraphicsJava2D";
  static final String OPENGL = "processing.opengl.PGraphicsOpenGL";
  static final String PDF    = "processing.pdf.PGraphicsPDF";
  static final String DXF    = "processing.dxf.RawDXF";


  // platform IDs for PApplet.platform

  static final int OTHER   = 0;
  static final int WINDOWS = 1;
  static final int MACOSX  = 2;
  static final int LINUX   = 3;

  static final String[] platformNames = {
    "other", "windows", "macosx", "linux"
  };

  // and on and on

}
кицунэ
источник
15
Примечание: static finalнеобязательно, это избыточно для интерфейса.
ThomasW
Также обратите внимание , что platformNamesможет быть public, staticи final, но это, безусловно , не является постоянной. Единственный постоянный массив - это массив нулевой длины.
Vlasec
@ThomasW Я знаю, что этому несколько лет, но мне нужно было указать на ошибку в вашем комментарии. static finalне обязательно является избыточным. Поле класса или интерфейса, содержащее только finalключевое слово, будет создавать отдельные экземпляры этого поля по мере создания вами объектов класса или интерфейса. Использование static finalзаставит каждый объект совместно использовать область памяти для этого поля. Другими словами, если у класса MyClass есть поле final String str = "Hello";, для N экземпляров MyClass в памяти будет N экземпляров поля str. Добавление staticключевого слова приведет только к 1 экземпляру.
Sintrias

Ответы:

160

Обычно это считается плохой практикой. Проблема в том, что константы являются частью общедоступного «интерфейса» (точнее сказать) реализующего класса. Это означает, что реализующий класс публикует все эти значения во внешние классы, даже если они требуются только внутри. Константы распространяются по всему коду. Примером может служить интерфейс SwingConstants в Swing, который реализован десятками классов, которые «реэкспортируют» все свои константы (даже те, которые они не используют) как свои собственные.

Но не верьте мне на слово, Джош Блох также говорит, что это плохо:

Шаблон постоянного интерфейса - это плохое использование интерфейсов. То, что класс использует некоторые константы внутри, является деталью реализации. Реализация постоянного интерфейса приводит к просачиванию этой детали реализации в экспортируемый API класса. Для пользователей класса не имеет значения, что класс реализует постоянный интерфейс. На самом деле, это может даже сбить их с толку. Хуже того, это представляет собой обязательство: если в будущем выпуске класс будет изменен так, что ему больше не нужно будет использовать константы, он все равно должен реализовать интерфейс, чтобы гарантировать двоичную совместимость. Если нефинальный класс реализует постоянный интерфейс, пространство имен всех его подклассов будет загрязнено константами в интерфейсе.

Перечисление может быть лучшим подходом. Или вы можете просто поместить константы как общедоступные статические поля в классе, который не может быть создан. Это позволяет другому классу получать к ним доступ, не загрязняя свой собственный API.

Дэн Дайер
источник
8
Перечисления здесь - отвлекающий маневр - или, по крайней мере, отдельный вопрос. Разумеется, следует использовать перечисления, но они также должны быть скрыты, если они не нужны разработчикам.
DJClayworth
12
BTW: вы можете использовать перечисление без экземпляров в качестве класса, который не может быть создан. ;)
Питер Лоури
5
Но зачем вообще реализовывать эти интерфейсы? Почему бы не использовать их просто как хранилище констант? Если мне нужны какие-то глобальные общие константы, я не вижу "более чистых" способов сделать это.
Shadox
2
@DanDyer да, но интерфейс делает некоторые объявления неявными. Как и публичный статический финал, это просто значение по умолчанию. Зачем возиться с классом? Enum - ну это смотря как. Перечисление должно определять набор возможных значений для сущности, а не набор значений для разных сущностей.
shadox
4
Лично я чувствую, что Джош неправильно отбивает мяч. Если вы не хотите, чтобы ваши константы просачивались - независимо от того, в какой тип объекта вы их поместили - вам необходимо убедиться, что они НЕ являются частью экспортируемого кода. Интерфейс или класс, оба могут быть экспортированы. Таким образом, правильный вопрос заключается не в том, в какой тип объекта я их помещаю, а в том, как мне организовать этот объект. И если константы используются в экспортируемом коде, вы все равно должны убедиться, что они доступны после экспорта. Таким образом, утверждение о «плохой практике», по моему скромному мнению, неверно.
Лоуренс
99

Вместо реализации «интерфейса констант» в Java 1.5+ вы можете использовать статический импорт для импорта констант / статических методов из другого класса / интерфейса:

import static com.kittens.kittenpolisher.KittenConstants.*;

Это позволяет избежать уродства, когда ваши классы реализуют интерфейсы, которые не имеют функциональности.

Что касается практики наличия класса только для хранения констант, я думаю, что иногда это необходимо. Есть определенные константы, которым просто нет естественного места в классе, поэтому лучше иметь их в «нейтральном» месте.

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

Например:

/** Set of constants needed for Kitten Polisher. */
public final class KittenConstants
{
    private KittenConstants() {}

    public static final String KITTEN_SOUND = "meow";
    public static final double KITTEN_CUTENESS_FACTOR = 1;
}
Zarkonnen
источник
Итак, вы объясняете, что из-за статического импорта мы должны использовать классы вместо интерфейсов, чтобы повторить ту же ошибку, что и раньше ?! Это глупо!
gizmo
11
Нет, я совсем не это говорю. Я говорю о двух независимых вещах. 1. Используйте статический импорт вместо злоупотребления наследованием. 2: Если у вас должен быть репозиторий констант, сделайте его последним классом, а не интерфейсом.
Zarkonnen
«Постоянные интерфейсы» никогда не создавались как часть какого-либо наследования. Таким образом, статический импорт предназначен только для синтаксического сахара, а наследование от такого интерфейса - ужасная ошибка. Я знаю, что Sun сделала это, но они также сделали массу других основных ошибок, это не повод имитировать их.
gizmo
3
Одна из проблем с кодом, опубликованным для вопроса, заключается в том, что реализация интерфейса используется только для облегчения доступа к константам. Когда я вижу, что что-то реализует FooInterface, я ожидаю, что это повлияет на его функциональность, а вышеперечисленное нарушает это. Статический импорт решает эту проблему.
Zarkonnen
2
gizmo - я не поклонник статического импорта, но то, что он там делает, избегает необходимости использовать имя класса, например, ConstClass.SOME_CONST. Выполнение статического импорта не добавляет эти члены в класс, который вы Z. не хотите наследовать от интерфейса, он фактически говорит обратное.
mtruesdell
8

Я не претендую на право быть правым, но давайте посмотрим на этот небольшой пример:

public interface CarConstants {

      static final String ENGINE = "mechanical";
      static final String WHEEL  = "round";
      // ...

}

public interface ToyotaCar extends CarConstants //, ICar, ... {
      void produce();
}

public interface FordCar extends CarConstants //, ICar, ... {
      void produce();
}

// and this is implementation #1
public class CamryCar implements ToyotaCar {

      public void produce() {
           System.out.println("the engine is " + ENGINE );
           System.out.println("the wheel is " + WHEEL);
      }
}

// and this is implementation #2
public class MustangCar implements FordCar {

      public void produce() {
           System.out.println("the engine is " + ENGINE );
           System.out.println("the wheel is " + WHEEL);
      }
}

ToyotaCar ничего не знает о FordCar, а FordCar не знает о ToyotaCar. Принцип CarConstants надо менять, но ...

Константы не следует менять, потому что колесо круглое, а egine - механическое, но ... В будущем инженеры-исследователи Toyota изобрели электронный двигатель и плоские колеса! Давайте посмотрим на наш новый интерфейс

public interface InnovativeCarConstants {

          static final String ENGINE = "electronic";
          static final String WHEEL  = "flat";
          // ...
}

и теперь мы можем изменить нашу абстракцию:

public interface ToyotaCar extends CarConstants

в

public interface ToyotaCar extends InnovativeCarConstants 

И теперь, если нам когда-нибудь понадобится изменить основное значение, если ДВИГАТЕЛЬ или КОЛЕСО, мы можем изменить интерфейс ToyotaCar на уровне абстракции, не касаясь реализаций.

Я знаю, это НЕ БЕЗОПАСНО, но я все еще хочу знать, что вы думаете об этом

pleerock
источник
Я хочу узнать вашу идею сейчас, в 2019 году. Для меня поля интерфейса предназначены для совместного использования некоторыми объектами.
Raining
Я написал ответ, связанный с вашей идеей: stackoverflow.com/a/55877115/5290519
Raining
это хороший пример того, как правила PMD очень ограничены. Попытка улучшить код с помощью бюрократии остается тщетной попыткой.
bebbo
6

В Java есть много ненависти к этому шаблону. Однако интерфейс статических констант иногда имеет значение. Вам необходимо в основном выполнить следующие условия:

  1. Концепции являются частью открытого интерфейса нескольких классов.

  2. Их ценности могут измениться в будущих выпусках.

  3. Очень важно, чтобы все реализации использовали одни и те же значения.

Например, предположим, что вы пишете расширение для гипотетического языка запросов. В этом расширении вы собираетесь расширить синтаксис языка некоторыми новыми операциями, которые поддерживаются индексом. Например, у вас будет R-Tree, поддерживающее геопространственные запросы.

Итак, вы пишете публичный интерфейс со статической константой:

public interface SyntaxExtensions {
     // query type
     String NEAR_TO_QUERY = "nearTo";

     // params for query
     String POINT = "coordinate";
     String DISTANCE_KM = "distanceInKm";
}

Позже новый разработчик думает, что ему нужно создать лучший индекс, поэтому он приходит и создает реализацию R *. Реализуя этот интерфейс в своем новом дереве, он гарантирует, что разные индексы будут иметь идентичный синтаксис на языке запросов. Более того, если позже вы решите, что имя «nearTo» сбивает с толку, вы можете изменить его на «insideDistanceInKm» и знать, что новый синтаксис будет соблюдаться всеми реализациями вашего индекса.

PS: Вдохновение для этого примера взято из пространственного кода Neo4j.

phil_20686
источник
5

Учитывая преимущество ретроспективного взгляда, мы видим, что Java во многих отношениях не работает. Одним из основных недостатков Java является ограничение интерфейсов абстрактными методами и статическими конечными полями. Новые, более сложные объектно-ориентированные языки, такие как Scala, подразделяют интерфейсы на черты, которые могут (и обычно включают) конкретные методы, которые могут иметь нулевую арность (константы!). Для ознакомления с описанием черт как единиц составного поведения см. Http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf . Краткое описание того, как черты в Scala сравниваются с интерфейсами в Java, см. Http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5, В контексте обучения объектно-ориентированному проектированию упрощенные правила, такие как утверждение, что интерфейсы никогда не должны включать статические поля, являются глупыми. Многие черты естественно включают константы, и эти константы соответственно являются частью общедоступного «интерфейса», поддерживаемого чертой. При написании кода Java не существует чистого, элегантного способа представления признаков, но использование статических полей final в интерфейсах часто является частью хорошего обходного пути.

Корки Картрайт
источник
12
Ужасно пафосный и ныне устаревший.
Esko
1
Замечательная способность проникновения в суть (+1), хотя, возможно, слишком критичная в отношении Java.
Питер - Восстановить Монику
0

Согласно спецификации JVM, поля и методы в интерфейсе могут иметь только Public, Static, Final и Abstract. Ссылка из Inside Java VM

По умолчанию все методы в интерфейсе абстрактны, даже если вы не упомянули об этом явно.

Интерфейсы предназначены только для описания. Он не может содержать никаких реализаций. Поэтому, чтобы избежать реализации классов для изменения спецификации, она делается окончательной. Поскольку интерфейс не может быть создан, они становятся статическими для доступа к полю с использованием имени интерфейса.

om39a
источник
0

У меня недостаточно репутации, чтобы дать комментарий Плеероку, поэтому я должен создать ответ. Я сожалею об этом, но он приложил немало усилий, и я хотел бы ему ответить.

Плеерок, вы создали идеальный пример, чтобы показать, почему эти константы должны быть независимыми от интерфейсов и от наследования. Для клиента приложения не важно, есть ли техническая разница между этими реализациями автомобилей. Для клиента они такие же, просто автомобили. Итак, клиент хочет взглянуть на них с этой точки зрения, которая представляет собой интерфейс, подобный I_Somecar. На протяжении всего приложения клиент будет использовать только одну точку зрения, а не разные для каждой марки автомобиля.

Если клиент хочет сравнить автомобили перед покупкой, у него может быть такой метод:

public List<Decision> compareCars(List<I_Somecar> pCars);

Интерфейс - это договор о поведении, который показывает разные объекты с одной точки зрения. То, как вы его спроектируете, у каждой марки автомобилей будет своя наследственная линия. Хотя на самом деле это совершенно правильно, потому что автомобили могут быть настолько разными, что это может быть похоже на сравнение совершенно разных типов объектов, в конце концов, есть выбор между разными автомобилями. И это перспектива интерфейса, которым должны поделиться все бренды. Выбор констант не должен делать это невозможным. Пожалуйста, обратите внимание на ответ Зарконнена.

Лук Бергман
источник
-1

Это появилось еще до того, как появилась Java 1.5 и она не принесла нам перечислений. До этого не было хорошего способа определить набор констант или ограниченных значений.

Это все еще используется, большую часть времени либо для обратной совместимости, либо из-за количества рефакторинга, необходимого для избавления от многих проектов.

штуковина
источник
2
До Java 5 можно было использовать шаблон перечисления с безопасным типом (см. Java.sun.com/developer/Books/shiftintojava/page1.html ).
Дэн Дайер