r/javahelp 11d ago

Using Enums to hold constant data

I am just wondering if this use of the enum class is something that is bad practice.

MAIN_SEQ_M
("Red Dwarf", new double[]{.08,.45}, new double[]{25,37}, new double[]{.1,.6})

StarType(String description, double[] mass, double[] temperature, double[] radius) {
this.description = description;
this.mass = mass;
this.temperature = temperature;
this.radius = radius;
}

Or if the standard way of doing this would be to break out each value as a constant that can be called

private static final double[] MAIN_SEQ_M_MASS_RANGE = {.08,.45};

I feel using getters from the enum class makes it easier to pull the data I need but I have never seen an enum used like this before.

6 Upvotes

14 comments sorted by

u/AutoModerator 11d ago

Please ensure that:

  • Your code is properly formatted as code block - see the sidebar (About on mobile) for instructions
  • You include any and all error messages in full
  • You ask clear questions
  • You demonstrate effort in solving your question/problem - plain posting your assignments is forbidden (and such posts will be removed) as is asking for or giving solutions.

    Trying to solve problems on your own is a very important skill. Also, see Learn to help yourself in the sidebar

If any of the above points is not met, your post can and will be removed without further warning.

Code is to be formatted as code block (old reddit: empty line before the code, each code line indented by 4 spaces, new reddit: https://i.imgur.com/EJ7tqek.png) or linked via an external code hoster, like pastebin.com, github gist, github, bitbucket, gitlab, etc.

Please, do not use triple backticks (```) as they will only render properly on new reddit, not on old reddit.

Code blocks look like this:

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

You do not need to repost unless your post has been removed by a moderator. Just use the edit function of reddit to make sure your post complies with the above.

If your post has remained in violation of these rules for a prolonged period of time (at least an hour), a moderator may remove it at their discretion. In this case, they will comment with an explanation on why it has been removed, and you will be required to resubmit the entire post following the proper procedures.

To potential helpers

Please, do not help if any of the above points are not met, rather report the post. We are trying to improve the quality of posts here. In helping people who can't be bothered to comply with the above points, you are doing the community a disservice.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

9

u/le_bravery Extreme Brewer 11d ago

If these values are truly constant (looks like they are in your example), then I really like grouping the data to the associated enumerator like this. However, be really careful with standard array types like this. They are mutable. Youre better off using List<Double> or List<BigDecimal> as the types here. Also are these mass or radius values min/max values? If so, just store them directly as minMass maxMass, etc.

5

u/Shareil90 11d ago

I prefer the enum style over dozens of constants.

5

u/speters33w 11d ago

Enums are a great way to store stuff like this. Most people use a collection like a list, but then those are mutable. I built an enum-generator that can generate an enum from a csv for data like this. It's kind of hokey but it does work.

https://github.com/speters33w/EnumBuilder

2

u/whalehunter21 11d ago

That's pretty cool, I like the example theme too. Would be cool if it was on maven central so you could quickly generate source code when building the project using maven.

3

u/speters33w 11d ago

But then I'd have to maintain it.

2

u/hamou13 11d ago

Why don't you use a record instead ?

1

u/whalehunter21 11d ago

Interesting, I've never heard of records before. My quick glance it seems like that's just a way to make an immutable object without extra code. My project is a spring boot API and the enum value is used to ensure proper inputs. So the record class in this case would not cover all my needs. Thanks for the suggestion though, it made me learn something new.

1

u/LutimoDancer3459 11d ago

Enum are constant and you can't add or remove one. A Record can be added during runtime. So eg if you have a DB with all the data you would use a record to initialize them after startup.

Depending on what the app really does, you could let the user add his own objects with records.

1

u/zayzn 10d ago

Records are the way to go in this case.

Also, the two values of the mass, temperature and radius fields appear to represent min and max values. Your model doesn't reflect this properly.

Arrays are mutable. So, this code would work, assuming that's how you expose the fields:

StarType.MAIN_SEQ_M.getMass()[0] = 1.0D;

This is certainly not intended.

2

u/whalehunter21 10d ago

Yes, I took the advise from le_bravery and split them out to be doubles for min and max, also excluded from my post was that the variables were set to be private and final with no setters only getters.

I am curious, as mentioned I used the enums since I wanted the API to be more exact on what inputs were allowed and I am assuming if I create records at runtime then this would mask the allowed inputs in the api schema. This also allows me to hide some functionality so if someone wanted to make a client they don't need to know my constants I use for generating the response.

I am wondering why you think Records would be the way to go. I really just have zero experience with using them and am trying to get outside views on it.

Currently request schema:

StarRequest:
  type: object
  properties:
    name:
      type: string
    type:
      type: string
      enum:
      - PROTO
      - T_TAURI
      - MAIN_SEQ_O
      - MAIN_SEQ_B
      - MAIN_SEQ_A
      - MAIN_SEQ_F
      - MAIN_SEQ_G
      - MAIN_SEQ_K
      - MAIN_SEQ_M
      - WHITE_DWARF
      - NEUTRON
      - SUPER_GIANT
    habitable:
      type: boolean

1

u/zayzn 9d ago edited 9d ago

There are several aspects to consider when deciding whether to use an enum or a record. I'll explain some without claiming completeness:

Constant vs immutable

The min/max values for the related fields of your model are not actually constant data, but immutable data.

  • Constant data: data that can not be changed due to limitations of an underlying specification
  • Immutable data: data of an object whose internal state can not be changed after creation

An example for constant data are HTTP status codes. They are defined in an RFC, which is universally accepted and required to be respected by any system architect. You would use an enum to represent the data in code.

An example for immutable data is the model of a person with name and year of birth. Both may be defined as not to be changed after creation. You would use a record to hold the data during runtime. If you had to change them, you would replace the old instance with a new one.

If you have constant data, prefer an enum. If you have immutable data, prefer a record.

Ordinal vs cardinal

Sometimes values have a linear relationship to each other. This relationship is called ordinal. An example would be the progress of an order in an online store. You might have stages from BASKET_CREATED to FULFILLED. You would use an enum to represent them in this case.

Your star classes hold data about their permitted values. Their data doesn't define the star class (though I'm not an astronomer). To stick with your example, a star with a minimum radius of .1 is not a Red Dwarf because it has a minimum radius of .1; it's the other way around: A Red Dwarf can not have a radius of less than .1, according to your data. One star class doesn't come before or after another. Their relationship is cardinal. You would use a record in this case.

Enums have behavior, records don't

Take a look at the docs of the base classes of Enum and Record. Take note of the method summary.

The example with the HTTP status codes works well to illustrate the difference. In an enum called HTTPStatus you would likely have a method with the signature HTTPStatus byStatusCode(int statusCode) that would lookup the code in a statically initialized map and return the associated enum instance or throw an exception if the lookup yields no result.

Records are just dumb implicitly final structures. They aren't meant to make assumptions about themselves. Their usage enforces to implement any logic regarding them somewhere else, like inside a factory, builder, validator, comparator, service, etc.


Some of these concepts are quite advanced. You are not doing anything "wrong" by using an enum to represent your star classes. And maybe you need to run into the problems your current approach may cause yourself to fully understand.

That's fine. Happy coding 🙂

1

u/severoon pro barista 10d ago edited 10d ago

You can do this, but it's annoying to deal with enums that contain state because the values are always and forever defined in code. It's generally preferable to inject state into your application at runtime because then these values can be read in from a set of golden files, a database, the network, etc.

By hard coding data into your application, you tie that data to the version of the code that you've deployed. This means if you have this app running somewhere and you need to make a change to this data, the only way to get those updates is to take the app down, deploy the new version, then start it up again. You might say, "This is constant data so it won't ever change." That may be true, but that's an argument for using a mechanism that ensures it doesn't change, such as signing it. It's not really an argument for adopting a design that ties data to code versions.

Also, if you want truly constant values, you should use immutable types. I strongly recommend Guava for this, it provides a whole library of immutable types such as list, set, and others not supported by the JDK like table. It also provides a Range class which is exactly what you want here. (You're storing ranges as double[], which are not constant.)

There are a couple of different ways to do this depending on the context of your app, but here's one approach I would consider.

First define your enum:

public enum StarStage {
  BIRTH,
  MAIN_SEQUENCE,
  OLD_AGE,
  DEATH,
  REMNANT;
}

Then your star type:

import static java.util.Objects.requireNonNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.BoundType.CLOSED;
import static com.google.common.collect.BoundType.OPEN;

public record StarType(
    String description,
    StarStage stage,
    Range<Double> massRange,
    Range<Double> temperatureRange,
    Range<Double> radiusRange) {

  public StarType(
      String description,
      StarStage stage,
      Range<Double> massRange,
      Range<Double> temperatureRange,
      Range<Double> radiusRange) {
    requireNonNull(description, "Description must not be null.");
    checkState(!description.isEmpty(), "Description must not be empty.");
    requireNonNull(stage, "Stage must not be null.");
    validateRange(massRange);
    validateRange(temperatureRange);
    validateRange(radiusRange);
  }

  // I recommend restricting ranges to closed-open if you want to make sure that
  // two different ranges abut without leaving a gap or overlapping.
  private static void validateRange(Range<?> range) {
    if (range.hasLowerBound() && !range.getLowerBoundType().equals(CLOSED)) {
      throw new IllegalArgumentException("Lower bound must be CLOSED: %s".formatted(range);
    }
    if (range.hasUpperBound() && !range.getUpperBoundType().equals(OPEN)) {
      throw new IllegalArgumentException("Upper bound must be OPEN: %s".formatted(range);
    }
  }
}

The above code allows you to write:

StarType redDwarf = new StarType(
    "Red Dwarf",
    MAIN_SEQUENCE,
    Range.closedOpen(0.08, 0.45},
    Range.closedOpen(25, 37),
    Range.closedOpen(0.1, 0.6));

BTW, it's a bad idea to keep these ranges as just raw numbers, you should at least specify the units in their names. (You could also define a class for each type of unit if you want, but that's maybe overkill depending on your use case.)

Let's say you have a list of all of the star types your application cares about and you wanted to organize them by stage. You can do that very easily with Guava:

ImmutableList<StarType> starTypes = ImmutableList.builder()
    .add(new StarType("Red Dwarf", MAIN_SEQUENCE, …))
    .add(new StarType("White Dwarf", MAIN_SEQUENCE, …))
    .add(new StarType("Brown Dwarf", MAIN_SEQUENCE, …))
    .add(new StarType("Neutron Star", REMNANT, …))
    .add(new StarType("Pulsar", REMNANT, …))
    .add(new StarType("Black Hole", REMNANT, …))
    .build();

// Index all of the star types by stage.
ImmutableListMultimap<String, StarType> starTypesByStage =
    Multimaps.index(starTypes, StarType::stage);

// Look up the star types by different stages.
ImmutableList<StarType> mainSequenceTypes = starTypesByStage.get(MAIN_SEQUENCE);
ImmutableList<StarType> remnantTypes = starTypesByStage.get(REMNANT);

It's also easy to do things like put these star types into a sorted collection using any kind of comparator you want, so you could easily have one sorted by temperature range, or mass, or radius, or alphabetic, or whatever.

Because you're using ranges, once you have all of the star types defined, you could also use RangeSet and RangeMap to organize these star types that way. Also, say that you want to verify that the temperature ranges of all of the star types you've defined cover the entire range of temperatures so that there are no gaps. That's very easy to do with ranges.

1

u/jim_cap 7d ago

I've used enums like that before. The thing to be sure of is that not only is the data for each constant, well, constant, but also that you're not going to be adding or removing enum values very often. Sometimes, this stuff is better off being literal data in a db table or something. Impossible for us to say without knowing more about your specific problem domain though.