1

According to Why define a Java object using interface (e.g. Map) rather than implementation (HashMap), when creating objects, I should declare the most abstract type, eg : Map hashMap=new HashMap(). However, I don't think it is a good idea, because it seems suffers from the same problem as primitive obsession. Let's consider the following case about primitive obsession:

Case 1:

public class Case{
    public static void methodForAgeOnly(int age){
    }
    public static int methodThatReturnsTemperature(){
        int temperature=someTemperatureOutside();
        return temperature;
    }
    public static void main(String[] args){
        int age=30;
        int temperature=20;

        age=temperature; //problem 1
        methodForAgeOnly(temperature); //problem 2
        age=methodThatReturnsTemperature(); //problem 3
    }
}

Clearly, we don't want problems 1-3 to occur (swapping age and temperature), which is a problem that may occur with only primitives. So we could enclose age and temperature into a class :

Case 2:

public class Case{
    public static void methodForAgeOnly(Age age){
    }
    public static Temperature methodThatReturnsTemperature(){
        Temperature temperature=someTemperatureOutside();
        return temperature;
    }
    public static void main(String[] args){
        Age age=30;
        Temperature temperature=20;

        age=temperature; //problem 1
        methodForAgeOnly(temperature); //problem 2
        age=methodThatReturnsTemperature(); //problem 3
    }
}

so that the code won't compile if we swap age and temperature.

Now, consider case 3, which declares the most abstract type:

public class Case{
    public static void methodForUsernameMapOnly(Map<String,String> usernameMap){
    }
    public static Map<String,String> methodThatReturnsSessionIdMap(){
        Map<String,String> sessionIdMap=someSessionIdMapOutside();
        return sessionIdMap;
    }
    public static void main(String[] args){
        Map<String,String> usernameMap=new HashMap<String,String>();
        Map<String,String> sessionIdMap=new TreeMap<String,String>();

        usernameMap=sessionIdMap; //problem 1
        methodForUsernameMapOnly(sessionIdMap); //problem 2
        usernameMap=methodThatReturnsSessionIdMap(); //problem 3
    }
}

which allows us to swap usernameMap and sessionIdMap. Doesn't it suffer from the same problem as case 1? So I think declaring the exactly using type instead of the abstract type is a better habit:

Case 4:

public class Case{
    public static void methodForUsernameMapOnly(HashMap<String,String> usernameMap){
    }
    public static TreeMap<String,String> methodThatReturnsSessionIdMap(){
        TreeMap<String,String> sessionIdMap=someSessionIdMapOutside();
        return sessionIdMap;
    }
    public static void main(String[] args){
        HashMap<String,String> usernameMap=new HashMap<String,String>();
        TreeMap<String,String> sessionIdMap=new TreeMap<String,String>();

        usernameMap=sessionIdMap; //problem 1
        methodForUsernameMapOnly(sessionIdMap); //problem 2
        usernameMap=methodThatReturnsSessionIdMap(); //problem 3
    }
}

which prevents us from mixing usernameMap with sessionIdMap by compile error. While I agree declaring the most abstract type allows us to change less code when we want to replace HashMap with TreeMap, it reduces type safety, which I think type safety is more important than easy to change because the change may never happen. Is the argument above true?

Note: I think

HashMap<String,String> usernameMap=new HashMap<String,String>(); 

is still programming to interface because according to https://softwareengineering.stackexchange.com/a/402035/432039, HashMap seems already an interface, just it is not the most abstract interface.

3
  • MultiMap extends from TreeMap ... so not a solution either. This problem has very little to do with primitive obsession. It's rather about Interface segregation.
    – Laiv
    Commented Jul 5, 2023 at 10:11
  • You only have the problem in 4 because you use primitive types (String) all over. If you had custom types for user name and session id just as you have for age and temperature in 2, you wouldn't have the issue.
    – JacquesB
    Commented Jul 5, 2023 at 15:26
  • The problems you listed aren't why primitive obsession is bad. It's bad because it makes your code less expressive, less clear, more complicated and more susceptible to rewrites. It's a bit of a catch 22, because if you don't quite yet understand why and how, you can't really make effective use of abstraction - and you'll still end up with confusing code that's prone to rewrites. And I realize that's not really an answer that provides enough insight, but it would take at least a book chapter to explain, and then perhaps years of experience to truly understand. Commented Jul 5, 2023 at 22:12

2 Answers 2

5

Yes but no.

Your example is only better because you have picked the specific case where your code uses two different implementations of Map; if you had a third use somewhere public static HashMap<String, String> methodThatReturnsPermissionsIdMap() or something you'd be back in the state where you weren't compile-time safe.

The problem is that you have primitive obsession in your maps. If your "username map" was Map<Username, Fullname> then you couldn't confuse that with Map<SessionId, WhateverItIsThatSessionIdMapsTo>.

0

No.

Using Age instead of int for age makes sense because you want to have extra stuff on your age object that can be useful eg.

public class Age
{
   TimeSpan AgeAsTimeSpan() ...
   Validate() ...   //cant be negative
   DateTime DoB() ...
}

If you just use int then these functions get repeated all over your codebase.

However, in the case of your methodForUsernameMapOnly the equivalent would be to use a custom UsernameMap class, not a HashMap. This class would contain some extra functions specific to a list of usernames.

If you have those extra functions and want to use them, sure go ahead and create that class.

However (again), if your method only requires that you pass it key value pair list, and doesn't use any of the username specific functions, as is usually the case. Then it just requires a Map and should be defined as such. It doesn't care how you have implemented your Map because it only uses the methods exposed on the Interface.

I think a more common approach would be to have a Username class and simply require a Map<string, Username> where the key is the userId. Its uncommon to have special aggregate functions unique to the type of objects in a collection.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.