Reputation: 47
I am storing classes like House
, Car
, Clothes
, FacialFeatures
; all parts of a Person
in a HashMap
:
public class Person {
private HashMap<String, PersonalItem> map = new HashMap<String, PersonalItem>();
public void init(){
map.put("house", new House());
map.put("car", new Car());
map.put("clothes", new Clothes());
//etc...
}
public PersonalItem getPersonalItem(String name){
return map.get(name);
}
}
external usage:
public static void main(String[] args){
Person person = getPerson(args);
//if i want to use car.setColor(blue); i have to do:
((Car) person.getPersonalItem("car")).setColor(blue);
//or if i want to use house.setExterior(wooden); i have to do:
((House) person.getPersonalItem("house")).setExterior(wooden);
}
How can I make it so if I were to use the following code:
person.getPersonalItem("house").setExterior(wooden);
It would work and getPersonalItem
would return Object instanceof House
if the input is "house." I don't want to have to write out a getter every time:
public House getPersonalItemHouse(){
return (House) map.get("house");
}
Is there an alternative?
Upvotes: 1
Views: 461
Reputation: 311863
One (dirty?) trick you could use is to use the Class
object as the key of the map instead of an arbitrary name and downcast it directly in the getter:
public class Person {
private Map<Class<? extends PersonalItem>, PersonalItem> map = new HashMap<>();
public void init() {
map.put(House.class, new House());
map.put(Car.class, new Car());
map.put(Clothes.class, new Clothes());
//etc...
}
public <T extends PersonalItem> T getPersonalItem(Class<T> cls) {
return (T) map.get(cls);
}
You still have a cast there, but it's limited to a single place, and you don't have to know about it when using the Person
class:
person.getPersonalItem(House.class).setExterior(wooden);
The drawback of this approach is that a person can't have two personal items of the same type.
Upvotes: 4
Reputation: 101
The solution using generics, and the class object as key of the map seems best to me. However, if a class e.g. House has not been added to the map:
person.getPersonalItem(House.class).setExterior(wooden);
Can be programmed with code completion in the IDE, but will give a null pointer at run time. Previously somebody gave the solution:
public <T extends PersonalItem> T getPersonalItem(String name){
return (T) map.get(name);
}
which indeed only covers the code duplication part of the question, and suffers from the same dangers. However, if you have to code:
House house = (House)person.getPersonalItem("house");
it's way more clear that you have to ascertain that the "house" key is actually present in the map.
Upvotes: 1
Reputation: 76804
I would use a file or database table where possible keywords are stored, linked to classes, like
Person: house, car, clothes; House: owner, garage
and so on if it is a file and if it is a database table, then a table of
properties(id, className, propertyName)
could help. Whatever the input is, you could write a code which would generate classes, like PersonDefinition
where getters and setters are defined (naturally, you can handle types of properties as well, I have ommitted this for the sake of simplicity)
and then make sure you inherit Person
from PersonDefinition
and whenever you build your project, a pre-build event should generate the PersonDefinition
class
, so you will have all the setters and getters you need.
Upvotes: 1
Reputation: 131456
What you try to do is not safe concerning the type safety.
Performing casts that may happen at runtime to avoid multiply methods is not necessary a good thing.
In your case I wonder if accessing the values from the Map makes really sense.
If the Map defines the state of the Person class, use rather fields and getters for each one.
If you have many of them and don't want to write/pollute your class, you can use Lombok that generate getters (and other boiler plate code if needed) for you at compile time.
As alternative approach to Mureinik, you could use a generic in the getPersonalItem()
method to infer the type from the client side of the invocation. In this way, you can have multiple entry in the map which the objects have the same type.
@SuppressWarnings("unchecked")
public <T extends PersonalItem> T getPersonalItem(String name, Class<T> clazz){
return (T) map.get(name);
}
And invoke it :
person.getPersonalItem("myHouse", House.class).setExterior(stone);
person.getPersonalItem("mySecondHouse", House.class).setExterior(wooden);
Upvotes: 1
Reputation: 272667
It sounds like you're using the string + map approach to avoid explicit getters (and setters). But you've thrown away all the compile-time type information. So there isn't much help that the compiler can give you here. Indeed, the approaches in the other answers won't protect you against accidentally associating a type of Car
with "house"
.
If verbosity is the concern, there are alternatives that allow you to retain compile-time types:
Code generation (e.g. Immutables or Lombok). These are good, but require extra tooling and build steps.
Use Kotlin, specifically data classes. They're about as succinct as you can get, and Kotlin has very good interop with Java.
I'd strongly advocate for #2 - I've experienced high benefit and low friction with this approach (i.e. defining types in Kotlin, and then consuming them from a Java codebase).
Upvotes: 1