Home > Enterprise >  get a getter method from field name to avoid if-else
get a getter method from field name to avoid if-else

Time:08-28

I have this code, which obviously doesn't look nice - it seems all the if-else can somehow be avoided.

if(sortBy.equals("firstName"))
 personList.sort(Comparator.comparing(Person::getFirstName));
else if(sortBy.equals("lastName")) 
 personList.sort(Comparator.comparing(Person::getLastName));
else if(sortBy.equals("age")) 
 personList.sort(Comparator.comparing(Person::getAge));
else if(sortBy.equals("city")) 
 personList.sort(Comparator.comparing(Person::getCity));
else if(sortBy.equals("state")) 
 personList.sort(Comparator.comparing(Person::getState));
else if(sortBy.equals("zipCode")) 
 personList.sort(Comparator.comparing(Person::getZipCode));

the function takes sortBy, which is the name of one of the attributes of a Person, and applies a sorting to a personList based on that field. How can I avoid the if-else and write a better looking, possibily one line code?

Currently I have found that I can use a HashMap to create a mapping between a field name and a corresponding comparator.

map.put("age", Comparator.comparing(Person::getAge));
map.put("firstName", Comparator.comparing(Person::getFirstName))
...

And use personList.sort(map.get(sortBy)).

But still felt like it can further be improved without an extra step, to the point where it follows the open-closed principle, and adding a new field to Person would not need us to modify the code. I'm looking for something like

personList.sort(Comparator.comparing(Person::getterOfField(sortBy)))

CodePudding user response:

As keshlam suggested, I think using the reflection API is the best fitting answer to your question, but keep in mind that using it in production code is generally discouraged.

Note: if you add a new Person-attribute which isn't itself Comparable, you'll have to resort to a custom Comparator anyway. With that in mind, you might want to keep the Map<String, Comparator<?>> solution you already have.

CodePudding user response:

In general java doesn't want you to work with it this way1; it is not a structurally typed language, and unlike e.g. javascript or python, objects aren't "hashmaps of strings to thingies".

Also, your request more fundamentally doesn't add up: You can't just go from "field name" to "sort on that": What if the field's type isn't inherently sortable (is not a subtype of Comparator<Self>?)

What if there is a column in whatever view we're talking about / config file that is 'generated'? Imagine you have a field LocalDate birthDate; but you have a column 'birth month'2. You can sort on birth month, no problem. However, given that it's a 'generated value' (not backed directly by a field, instead, derived from a calculation based on field(s)), you can't just sort on this. You can't even sort on the backing field (as that would sort by birth year first, not what you want), nor does 'backing field' make sense; what if the virtual column is based on multiple fields?

It is certainly possible that currently you aren't imagining either virtual columns or fields whose type isn't self-sortable and that therefore you want to deposit a rule that for this class, you close the door on these two notions until a pretty major refactor, but it goes to show perhaps why "java does not work that way" is in fact somewhat 'good' (closely meshes with real life concerns), and why your example isn't as boilerplatey as you may have initially thought: No, it is not, in fact, inevitable. Specifically, you seem to want:

  • There is an exact 1-to-1 match between 'column sort keys' and field names.
  • The strategy to deliver on the request to sort on a given column sort key is always the same: Take the column sort key. Find the field (it has the same name); now find its getter. Create a comparator based on comparing get calls; this getter returns a type that has a natural sorting order guaranteed.

Which are 2 non-obvious preconditions that seem to have gotten a bit lost. At any rate, a statement like:

if(sortBy.equals("firstName"))
 personList.sort(Comparator.comparing(Person::getFirstName));

encodes these 2 non-obvious properties, and trivially, therefore means it is also possible to add virtual columns as well as sort keys that work differently (for example, sorts on birth month, or, sorts on some explicit comparator you write for this purpose. Or even sorts case insensitively; strings by default do not do that, you'd have to sort by String.CASE_INSENSITIVE_COMPARATOR instead.

It strikes me as a rather badly written app if a change request comes in with: "Hey, could you make the sort option that sorts on patient name be case insensitive?" and you go: "Hooo boy that'll be a personweek of refactoring work!", no?

But, if you insist, you have 2 broad options:

Reflection

Reflection lets you write code that programatically gets a list of field names, method names, and can also be used to programatically call them. You can fetch a list of method names and filter out everything except:

  • instance methods
  • with no arguments
  • whose name starts with get

And do a simple-ish get-prefix-to-sort-key conversion (basically, .substring(3) to lop off the get, then lowercase the first character, though note that the rules for getter to field name get contradictory if the first 'word' of the field is a single letter, such as getXAxis, where half of the beanspec documents say the field name is definitely XAxis, as xAxis would have become getxAxis, and the other half say it is ambiguous and could mean the field name is XAxis or xAxis).

It looks something like this:

// intentionally raw type!
Map comparators = new HashMap();
for (Method m : Person.class.getMethods()) {
  if (Modifiers.isStatic(m.getModifiers()) continue;
  if (m.getParameterCount() != 0) continue;
  String n = m.getName();
  if (!n.startsWith("get") || n.length() < 4) continue;
  n = Character.toLowerCase(n.charAt(3))   n.substring(4);
  comparators.put(n, (a, b) -> ((Comparator) a).compareTo(b));
}
MyClass.COMPARATORS = (Map<String, Comparator<?>>) Collections.unmodifiableMap(comparators);

Note how this causes a boatload of errors because you just chucked type checking out the window - there is no actual way to ensure that any given getter type actually is an appropriate Comparable. The warnings are correct and you have to ignore them, no fixing that, if you go by this route.

Annotation Processors

This is a lot more complicated: You can stick annotations on a method, and then have an annotation processor that sees these and generates a source file that does what you want. This is more flexible and more 'compile time checked', in that you can e.g. check that things are of an appropriate type, or add support for mentioning a class in the annotation that is an implementation of Comparable<T>, T being compatible with the type of the field you so annotate. You can also annotate methods themselves (e.g. a public Month getBirthMonth() method). I suggest you search the web for an annotation processor tutorial, it'd be a bit much to stuff an example in an SO answer. Expect to spend a few days learning and writing it, it won't be trivial.

[1] This is a largely objective statement. Falsifiable elements: There are no field-based 'lambda accessors'; no foo::fieldName support. Java does not support structural typing and there is no way to refer to things in the language by name alone, only by fully qualified name (you can let the compiler infer things, but the compiler always translates what you write to a fully "named" (package name, type name that the thing you are referring to is in, and finally the name of the method or field) and then sticks that in the class file).

[2] At least in the Netherlands it is somewhat common to split patient populations up by birth month (as a convenient way to split a population into 12 roughly equally sized, mostly arbitrary chunks) e.g. for inviting them in for a checkup or a flu shot or whatnot.

  • Related