0

I have the following class, which is used for controlling some debugging and beta testing options in various places in my Android app. It just contains some flags and some logic to (de)serialize it to/from JSON.

public class DevConfiguration {
    public boolean dontSendSMS;


    public static String toJsonString(DevConfiguration devConfiguration) {
        JSONObject json = new JSONObject();

        if( devConfiguration != null ) {
            try {
                json.put("dontSendSMS", devConfiguration.dontSendSMS);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        return json.toString();
    }


    public static DevConfiguration fromJsonString(String jsonString) {
        if( jsonString.isEmpty() )
            return null;

        DevConfiguration result = new DevConfiguration();

        try {
            JSONObject jsonObj = new JSONObject(jsonString);
            result.dontSendSMS = jsonObj.optBoolean("dontSendSMS", false);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        return result;
    }

}

Now, in one of my services I receive a serialized DevConfiguration object in an Intent, and might later pass it on to another service:

serviceIntent.putExtra("dev_conf", DevConfiguration.toJsonString(mDevConfiguration));

I choose to make the toJsonString() method static, so that I don't risk invoking it on a null instance. However, it's still possible to make a mistake somewhere and invoking the static method on an instance - potentially a null instance!

mDevConfiguration.toJsonString(mDevConfiguration);

There is a Lint warning in Android Studio, but still it's a potential NullPointerException bug waiting to happen. I thought it might be possible to hide it by defining a similar private method but with a different signature

/** Hide instance implementation **/
private String toJsonString(Object o){ return ""; }

but of course calling it with a DevConfiguration parameter will invoke the static method anyway, and the IDE doesn't give any more warnings than before either.

Is there any way to "hide" the static method from instance variables?

EDIT

Comments make it clear that invoking a static method on a null instance is perfectly legal. However, the question is not "How do I prevent a NullPointerException when invoking a static method on a null instance?", but the more general "How can I prevent invoking a static method on an instance of my class?".

In other words - is there any way to prevent the compiler from compiling if one accidentally tries to invoke a static method on an instance?

Magnus
  • 17,157
  • 19
  • 104
  • 189
  • Inner class should help you. – Nicholas K Sep 26 '18 at 18:25
  • 2
    Did you actually try to test your assumptions? I don't get a NPE: https://ideone.com/u2UPPd – Johannes Kuhn Sep 26 '18 at 18:29
  • @JohannesKuhn Doh! You're right, it doesn't throw an NPE, and that should be the accepted answer. In my defence, I found the linked question to be weirdly phrased, and probably that's why it didn't show up when searching or entering my question title. Didn't find anything when doing a quick Googling either. – Magnus Sep 26 '18 at 18:46
  • I did a search for `static method on null` and this was the first result. – Johannes Kuhn Sep 26 '18 at 19:06
  • @JohannesKuhn I did a search for `prevent static method on instance` since I was interested in that general situation rather than the edge case of a null instance. This very question is the first relevant one that applies to Java. – Magnus Sep 26 '18 at 19:26

2 Answers2

3

Calling a static method on a variable with null value will not raise NullPointerException. Following code will print 42 even though variable i is null.

public class Test {
    public static void main(String... args) {
        Integer i = null;
        System.out.println(i.parseInt("42"));
    }
}

When calling static methods by variable, what really matters is the declared type of the variable and not the referenced type of its value. This is related to the fact that static methods in java are not polymorphic.


„How can I prevent invoking a static method on an instance of my class?"

Calling static methods by variable is just a regular language feature defined in the Java spec. I’d be surprised if there were any method to suppress it in general.

If I had to do it for a selected class, I would probably migrate static methods to a separate „companion” utility (as described in another answer).

But having such static (factory) methods in your class is a perfectly fine idiom (see for example: Joshua Bloch, „Effective Java”, Item 1: Consider static factory methods instead of constructors). I wouldn’t easily give up on it.

user3078523
  • 1,520
  • 18
  • 27
  • This is a clear, informative and useful answer! It's just not the answer to this specific question. See my edit at the bottom of the question. – Magnus Sep 26 '18 at 19:34
  • @BadCash, I thought that maybe knowing that there is no risk of NPE when calling static method on an instance, you wouldn’t necessarily try to prevent it. I will update my answer. – user3078523 Sep 26 '18 at 19:59
0

I see a few ways you could do this:

  1. Use a Utils class:

    public class Utils {
        public static String toJsonString(DevConfiguration devConfiguration) {
            JSONObject json = new JSONObject();
    
            if( devConfiguration != null ) {
                try {
                    json.put("dontSendSMS", devConfiguration.dontSendSMS);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
    
            return json.toString();
        }
    
    
        public static DevConfiguration fromJsonString(String jsonString) {
            if( jsonString.isEmpty() )
                return null;
    
            DevConfiguration result = new DevConfiguration();
    
            try {
                JSONObject jsonObj = new JSONObject(jsonString);
                result.dontSendSMS = jsonObj.optBoolean("dontSendSMS", false);
            } catch (JSONException e) {
                e.printStackTrace();
            }
    
            return result;
        }
    }
    

    Now you can just makes calls to Utils.method() and avoid confusion.

  2. Use Kotlin

    Kotlin actually makes it really hard (if not impossible) to call a static method on a dynamic receiver. It won't show in the method suggestions, and will underline in red if you type it manually. It might not even compile, although I haven't gotten that far.

    Kotlin also has built-in null protection: instance?.method(). The ? means method() just won't execute if instance is null.

  3. Just don't call a static method on a dynamic receiver. If you do it by accident, go back and fix it. You shouldn't be relying on Java to work around your syntax errors for you.

Finally, why even do this? I highly doubt mDevConfiguration is ever null, unless you initialize it in a really weird spot. If it is, you may want to look at reorganizing your code. Because, again, you shouldn't be relying on Java to work around your syntax errors for you. Also, if it is null, it won't throw an NPE, at least in Java, since it doesn't need a dynamic receiver to run (this is probably different in Kotlin).

It's up to you to make code that works as it should, and implement the proper null checks, error handling, etc. If you miss something, it's no big deal; that's why you test your code and fix the crashes and bugs you catch before you release it. Anything you don't catch will be reported by the Google Play Console (if you publish there) or Firebase (if you implement that) or your users.

Sorry if the above sounds harsh, but I'm really having trouble seeing why you'd want to do this instead of just checking your code.

If you really want to keep this structure, at least make the constructor for DevConfiguration private:

public class DevConfiguration {
    //...

    private DevConfiguration() {}

    //...
}

That way, only the static methods inside it can create an instance.

TheWanderer
  • 16,775
  • 6
  • 49
  • 63
  • As mentioned, the configuration arrives in an Intent but is not crucial for the app to function since it's only used in beta and debug builds. Also, having a singleton or static `DevConfiguration`is an idea I considered, but I want different components to be able to run different DevConfiguration's simultaneously. And I'd rather avoid any possible NPE due to coding mistakes if at all possible, since mistakes will be made and bugs will arise no matter what. Turns out it's a non-issue as well. But thanks for taking the time anyway. – Magnus Sep 26 '18 at 18:53
  • Making the constructor private doesn't make it a singleton. `fromJsonString()` will still create a new instance every time it's called. – TheWanderer Sep 26 '18 at 18:54
  • Sorry, I was confusing your answer with another one (now deleted) who suggested making it a static class. But either way, at some point or another I am actually creating instances of DevConfiguration by using `new` and not from a JSON string. – Magnus Sep 26 '18 at 18:57
  • That's fine. I'm going to say again, though, that if you make a mistake in code, it's best to fix it. It's impossible to avoid making a mistake most of the time. – TheWanderer Sep 26 '18 at 18:59
  • I don't disagree with what you're saying. But I'd argue that what I set out to achieve in the question is analogous to defining a private constructor just so one doesn't accidentally create an instance. I was thinking there might be a way to catch the mistake in the IDE by having a "private" instance method and public class method, instead it turns out that the Java compiler is the one that's allowing mistakes here - even calling a static method on a null instance is totally fine! – Magnus Sep 26 '18 at 19:07
  • Yep. Be careful though, and don't rely on that behavior. Like I said in my answer, I believe Kotlin isn't so forgiving, because of its null-protection features. Kotlin prevents your issue altogether, though, by not allowing you to call static methods or retrieve static fields on a dynamic receiver. – TheWanderer Sep 26 '18 at 19:10