4

While reading this question, I remembered a bug in a program I wrote while I was first learning java that took me forever to locate and essentially boiled down to the following behavior:

String s1 = "This was a triumph";
String n1 = null;
System.out.println(s1 + n); // prints "This was a triumphnull"

Some other notable examples (and perplexing counter-examples) of similar behavior:

// "nullThis was a triumph", coercion happens commutatively
System.out.println(n1 + s1);

// as per above question, println explicitly converts null Strings to "null"
System.out.println(n1);

// similar result
System.out.println(String.valueOf(n1));

// NullPointerException (!!); null not silently converted to "null"
// note that this is the kind of thing I expected to occur for the other examples
// when I wrote the buggy code in the first place
System.out.println(n1.toString());

While I suppose I technically understand this behavior, I definitely don't grok it, so my question is:

  1. From a language design standpoint, why does java convert a null String to a "null" String in so many cases?
  2. Why doesn't it do so in the cases in which...it doesn't do so?

EDIT

I appreciate the answers so far, but I just want to clarify that my confusion largely originates from how null Strings are treated so differently in this respect than other null Objects. For example:

Integer i1 = 42;
Integer n2 = null;
// Both produce NullPointerExceptions:
Integer.valueOf(n2);
System.out.println(i1 + n2);

I also want to reemphasize that a NullPointerException is the kind of behavior I expected, which is why I was so confused about the null/"null" String conversion in the first place.

Community
  • 1
  • 1
mintchkin
  • 1,564
  • 2
  • 14
  • 13

2 Answers2

3

Only objects have methods. null is not an object; it has no methods. Any attempt to call methods of null will raise a NullPointerException. This is somewhat similar to what you'd get if you tried to call 3.toString(), though it's a runtime error instead of compile-time.

The examples you've given where null converts to "null" all special-case null references in an attempt to provide a more friendly interface. They can do this because they aren't method calls on null; they can have extra handling like x == null? "null" : x.toString() built in. This is similar to why you can call System.out.println(3) when 3.toString() fails.


The handling of null Integers works by autoboxing, a completely unrelated mechanism to that which handles null in printing and string concatenation.

When an Integer (or one of the other 7 primitive wrapper types) appears in a context that requires a primitive int, the compiler automatically inserts an Integer.valueOf call to perform the conversion. Integer.valueOf has no safe default for nulls; while "null" is the obvious string form of null and is quite useful for debugging output, coercing null Integers to 0 or any other value would be less useful and much more bug-prone.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • `String.valueOf` is the method you speak of in the last paragraph, IIRC. – E_net4 Jul 05 '14 at 23:08
  • @E_net4: I don't know what actually gets called. `Objects.toString` and `String.valueOf` do pretty much the same thing. – user2357112 Jul 05 '14 at 23:09
  • I would argue 1. That it is pretty much the definitive purpose of .toString() to "convert everything to a string", and 2. That conversely, performing the operation Integer(42) + Integer(null) results in a NullPointerException, rather than performing a silent conversion as happens in String addition – mintchkin Jul 05 '14 at 23:09
  • @mintchkin `x.toString()` means "find the object referenced by `x` and call its `toString` method with no arguments". A null reference does not point to any object, so there is no toString method to call. – Patricia Shanahan Jul 05 '14 at 23:13
  • @mintchkin: What silent coercion should it take on the `Integer`? – ChiefTwoPencils Jul 05 '14 at 23:14
  • @ChiefTwoPencils I suppose I'm not sure, in the same way that I wouldn't have been sure what silent coercion should have taken place on a String. – mintchkin Jul 05 '14 at 23:17
  • saying "ints, objects, null, everything" is plainly wrong. null is handled as an exceptional case, primitives are boxed to respective Object counterpart, Object instances use toString(), there ain't no "*everything*" in Java. –  Jul 05 '14 at 23:20
  • @vaxquis: While primitives, objects, and null have separate logic to handle them (including separate overloads for `println` and `valueOf`), I consider it correct to say "everything". The intent is that no matter what you put in there, it's converted to a string. – user2357112 Jul 05 '14 at 23:26
  • yup, but since it's converted using different means, I don't see how sticking it together helps to explain the internals in any way... –  Jul 05 '14 at 23:29
  • Also, your example is flawed... you can easily type cast ((Integer)42).toString(), while ((String)null).toString() will throw an NPEx; thus, these two cases ain't similar in any serious way. –  Jul 05 '14 at 23:33
  • @vaxquis: The casts you're comparing are only superficially similar. (This would've been so much easier to explain before autoboxing.) – user2357112 Jul 05 '14 at 23:37
  • yup, but since boxing is already here, I call shenanigans not on the content of your answer (which is OK), but on the wording (unclear and hand-waving). I'd gladly upvote your answer if both the content and the wording would be clear and helpful. –  Jul 05 '14 at 23:39
  • ""null" is the obvious string form of null" I guess this is really the crux of my inability to parse the logic behind this design. The null type has absolutely no relation with the string "null", and in my mind if you're going to make a type conversion, the Integer 0 is a much more conceptually similar type since it more accurately describes how null is commonly represented in memory. To be honest, if this was truly the logic behind the design decision, then your real answer to my question is "bad language design". – mintchkin Jul 06 '14 at 19:03
  • @mintchkin: `0` might be the representation of `null` in memory, but `"null"` is the representation of `null` in source code. You look at those 4 letters, and you know you're dealing with `null`. For human-readability, you can't really beat that. – user2357112 Jul 06 '14 at 21:56
  • /agree user2357112 - the thing I hated about C nulls the most was the #define NULL 0 macro, allowing the if(pointer) construct (which was good half of the time, and bad for the other half) - I'm so glad they finally introduced nullptr to allow safe, type-safe null pointer use in C++11, it literally (as it's a null literal...) made my day, and I use it almost everywhere; **btw, note that nullptr != 0** ! also, mind me, but NULL is **not** a pointer to 0x0, and it was discussed countless times already - I use 0x0 pointer all of the times in uC programming, it seldom is "null" in those arch's. –  Jul 07 '14 at 12:07
  • @user2357112 Right, but java was historically designed to resemble c++, where a null pointer was literally defined as 0 and had no meaning as a std::string (since it's not a pointer type), so I still struggle to understand the motivation for the language design choice. However, I think most of the possible motivations have been explored already, so I'm just going to accept this answer being the first one that most directly answered my question. – mintchkin Jul 07 '14 at 21:35
2

1. It was a design choice, dictated by the fact that String & StringBuffer class is supported since JDK1.0 (see http://docs.oracle.com/javase/6/docs/api/java/lang/String.html etc), that is since January 21, 1996 - and it supports concatenation using + operator since then. In J2SE 5.0 (from September 30, 2004), also known as JDK 1.5, both StringBuilder (non-thread safe, but faster - + started using it instead of StringBuffer now), autoboxing and generics (with erasure) were added - the whole paradigm shifted from metaprogramming using reflection to template programming, with not-so-obvious casting solutions. (see e.g. https://codegolf.stackexchange.com/questions/28786/write-a-program-that-makes-2-2-5 - Java solution uses Integer cache poisoning & boxing/unboxing to achieve this not-so-surprising result)

String.java

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

Objects.java

public static String toString(Object o) {
    return String.valueOf(o);
}

PrintStream.java

public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
} 

StringBuilder.java

public StringBuilder append(Object obj) {
    return append(String.valueOf(obj));
}

etc. (Java src.zip, JDK v8)

Virtually all Java library methods dealing with printing and toString conversion handle null this way (ie. call String.valueOf(), thus converting it to literal "null" String). Also, note that concat by + also works this way, because it translates to library calls during compilation (calling StringBuilder.append(Object o)).

2. Explicitly calling #toString() on a null Object reference will still cause a NPEx, as trying to call any other method on it.

3. (yeah, I know there was no part 3) for a fun time, try executing, eg.

System.out.println( ((String)null).toString() );

(obviously fails), and then

System.out.println( ((String)null).valueOf((String)null).toString() ); // works?! why!?

spoiler:

static methods.

Community
  • 1
  • 1
  • Thanks for the source code illustration, it's interesting. I suppose I should have expressed that part of my confusion comes from String addition, which doesn't throw a npe, performing so differently from Integer addition, which does. – mintchkin Jul 05 '14 at 23:27
  • @mintchkin: Well, you can edit that into your question, and we can edit our answers with explanations of how `Integer` addition is a consequence of the unboxing logic and how that all works. – user2357112 Jul 05 '14 at 23:28
  • 1
    Historical context is important here: autoboxing (which enables Integer addition) came in Java 5, whereas String addition as syntactic sugar for `StringBuffer` operations has been there since day one. – biziclop Jul 05 '14 at 23:32
  • I'd say that it's about the casting in Java in general - there ain't and won't be any direct casting between primitives (attempts fail at compile), while boxing allows you to, e.g., try to cast boolean->Boolean->Integer->int (fails at runtime) –  Jul 05 '14 at 23:38
  • @biziclop That's actually a very interesting point that at least sheds some light for me on why there's an apparent design inconsistency between null String and Integer objects, if not why null String objects were treated the way they are in the first place – mintchkin Jul 05 '14 at 23:49
  • @mintchkin It's hard to unpick design decisions made two decades ago but I think it was driven by the desire to reduce verbosity. Imagine if you had to null-check everything before printing its value into a log. – biziclop Jul 06 '14 at 16:26
  • @biziclop With respect to the other two answers which I'm sure might be helpful to others, I think your two comments come closest to answering my question, which was why the behavior exists as it does in the first place, especially in the context of divergent behavior for other objects. If you'd like to condense them into an answer, I'll mark it accepted. – mintchkin Jul 06 '14 at 19:14
  • @mintchkin I added a paragraph clarifying the matters you now pointed to, I hope you find the added content satisfactory. –  Jul 07 '14 at 12:11
  • @mintchkin Thanks, but apart from the hard facts of when each feature was introduced, the rest is an educated guess on my behalf. I do agree that this area is very inconsistent, but as a disclaimer I've been against autoboxing since its announcement, which might have clouded my judgement. – biziclop Jul 07 '14 at 15:14