This is a review of the second one - I think the first one is perfectly acceptable, the second one I've got issues.
Strings
There's a string in there that is printed out at the end. Ok. However, whenever the integer is divisible by 5, the string is being changed with the instruction: str += "Buzz";
This constructs a new String object each time through that if block which is completely inappropriate for a String. If it is to be mutable, make it a StringBuilder instead.
I know people will go "but this will only hit it 20 times - are 20 new objects that big a deal?" And the answer is "no, 20 new objets aren't a big deal - but this is indicative of someone who will put it in a loop that gets executed a few thousand times and wonder where the memory is going" (I've fixed that code before).
String comparison
Setting aside the +=
for the String a bit, the final if statement in that block:
if(str.equals("")){
str = i;
}
We're doing a test of a string against a String with .equals("")
. Because of String interning, this ""
is exactly the same object as the ""
at the start of the loop.
This means that the test could be done as:
if(str == ""){
str = i;
}
I'll make the disclaimer now that while this is correct and would be faster, static analysis tools will complain about it and people looking at it will think its a bug. You probably shouldn't do it that way... And its going to do that test anyways in the first test of .equals(Object anObject) in the String class.
The grumble that I have there is more one of that if the String is not ""
, then there is a lot more tests that need to be done before it returns back to false.
A test of:
if(str.length() == 0){
str = i;
}
or
if(str.isEmpty()){
str = i;
}
is a better, more succinct test that says exactly what it means without going through all the hoopla of the code in .equals()
.
There seems to be some confusion about my stance on String tests.
if(str == ""){
str = i;
}
I said that that was correct... and it is in that for this code it will give the correct answer all of the time. It is not good code. It will cause static analysis tools to raise flags about it - for good reason. It will cause your co-workers to look at it and think you've got a bug there - for good reason. It is not good code.
Doing a .equals("")
in Java is excessive. One of the static analysis inspections that IntelliJ has under "Performance Issues" is String.equals("")
Reports .equals() being called to compare a String with an empty string. It is normally more performant to test a String for emptiness by comparing its .length()
to zero instead.
Thus, the options that should be considered for this are instead:
if(str.length() == 0){
str = i;
}
or
if(str.isEmpty()){
str = i;
}
(I'd go for the isEmpty()
over length() == 0
though at the end of the day its the same).
So, why is this an issue? Lets look at the code for .equals(Object anObject) (the code is slightly different in 6-b14 than 7u40-b43 through 8-b132 though it is essentially the same).
1012 public boolean equals(Object anObject) {
1013 if (this == anObject) {
1014 return true;
1015 }
1016 if (anObject instanceof String) {
1017 String anotherString = (String)anObject;
1018 int n = count;
1019 if (n == anotherString.count) {
1020 char v1[] = value;
1021 char v2[] = anotherString.value;
1022 int i = offset;
1023 int j = anotherString.offset;
1024 while (n-- != 0) {
1025 if (v1[i++] != v2[j++])
1026 return false;
1027 }
1028 return true;
1029 }
1030 }
1031 return false;
1032 }
If they are the same object (this will be true for the case where ""
falls through because neither i%3
nor i%5
is the case, it will kick out right at the front and we're done. However, if the String is "Fizz"
or "Buzz"
or "FizzBuzz"
it will then check to make sure its an instance of the String then test the count (a private final value set up in the constructor).
The count
for ""
is 0, while the other strings are either 4 or 8, and so then we will fall through to the return on 1031 with a false.
So why the big deal?
Well, the code for isEmpty()
is:
670 public boolean isEmpty() {
671 return count == 0;
672 }
Really short and to the point. The shortness of the method call matters. One of those -XX
calls that no one ever touches (nor should they without knowing what they are doing) is: -XX:MaxInlineSize=#
(Java HotSpot VM Options). The default for this is 35 bytes of byte code.
If you have a small method, HotSpot will inline the method. No stack frame, no method lookup after HotSpot decides its going to do something. Its just smack right there in the code.
if(str.isEmpty()){
str = i;
}
becomes something that looks more like
if(str.count == 0){
str = i;
}
This is why people say that lots of small methods in Java is a good thing - because the compiler will inline them.
And so, this the best thing to do in that it shows exactly what you care about in the code for the person reading, doesn't get bogged down with calls to big methods that do many other things that you don't care about in this case.
if(str.isEmpty()){
Is the string empty? ... and we're done. The intent is clear and the code is simpler.
If, one was to have previously switched to a StringBuilder for this instead, there is no isEmpty()
call, but there is a length()
call that returns the count that should be used (source in AbstractStringBuilder 6-14).
What about the byte code for the StringBuilder.append vs +=
with a String? The +=
is much easier to read, what is the deal with that?
Lets take some quick code...
public void testMethod() {
String foo = "";
StringBuilder bar = new StringBuilder("");
String qux = "";
for(int i = 0; i < 100; i++) {
foo += Integer.toString(i); // 38
bar.append(i); // 39
qux = qux.concat(Integer.toString(i)); // 40
}
}
and look at the byte code generated.
L6
LINENUMBER 38 L6
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 4
INVOKESTATIC java/lang/Integer.toString (I)Ljava/lang/String;
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 1
L7
LINENUMBER 39 L7
ALOAD 2
ILOAD 4
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
POP
L8
LINENUMBER 40 L8
ALOAD 3
ILOAD 4
INVOKESTATIC java/lang/Integer.toString (I)Ljava/lang/String;
INVOKEVIRTUAL java/lang/String.concat (Ljava/lang/String;)Ljava/lang/String;
ASTORE 3
What does all of that mean?
Well, line 38 (the +=
line):
- creates a new StringBuilder
- invokes append on it with
foo
- invokes an Integer.toString to convert the integer to a String
- invokes append on the string builder with the String from the previous step
- converts the StringBuilder back into a String
- stores that in foo
Note that the StringBuilder that was created can now be disposed of (along with the String that was created to allow the +=
to work because String += String is defined but not String += int.
On the other hand, line 39 does:
- invoke StringBuilder.append with an int.
And as another option String.concat.
- invokes an Integer.toString to convert the integer to a String
- invoke String.concat (creates a new String object)
- store that new string in qux
StringBuilder.append with an int is rather straight forward and optimized for putting an integer into an an array. There's a call to Integer.getChars
in there that is the same as the code for Integer.toString.
The key to this is that there is no additional String being created (and discarded) for the Integer, nor a StringBuilder being created (and discarded) for the +=
when you use the StringBuilder in the first place.
Now, I'm moderately surprised (I knew it was the case, but still I'm moderately surprised) that the code for a single right hand side doesn't just do a String.concat(String) call which would probably be better (you've still got a bunch of Strings being created though its still less). However, the optimization would take additional work because if there was another +=
or a series of String foo = bar + qux + baz
somewhere in that scope it would probably use the same StringBuilder as initially allocated.
Still, the point stands - if you are going to mutate a String, start off with a mutable string class and work with that. There are several ways to write more preformat code than +=
and given how Strings are used having a +
inside a loop is setting yourself up for a disaster of unnecessary String objects floating around. String.concat
can be good in certain cases when you are doing a single right hand side and not doing it repeatedly (the FizzBuzz code would be candidate for that), but a StringBuilder is still a better choice.