user7148508
user7148508

Reputation: 47

Java creating instance getter easily and quickly

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

Answers (5)

Mureinik
Mureinik

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

Maarten Folkers
Maarten Folkers

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

Lajos Arpad
Lajos Arpad

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

davidxxx
davidxxx

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

Oliver Charlesworth
Oliver Charlesworth

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:

  1. Code generation (e.g. Immutables or Lombok). These are good, but require extra tooling and build steps.

  2. 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

Related Questions