1. Accessing
non-static members from static methods (mostly from the main method)
Novice
programmers often make this mistake as they don’t fully understand the
differences between static and non-static stuffs in Java. And interestingly,
this mistake mostly happens in the static method main()where one attempts to access
an instance variable or method. Consider the following code snippet:
1
2
3
4
5
6
7
|
public class StaticExample1
{
public int number; //
instance variable
public static void main(String[]
args) {
number
= 30; // compile error
}
}
|
Java
compiler will issue this error:
1
|
error: non-static
variable number cannot be referenced from a static context
|
Here, number is an instance variable
which means that we can only access it via an object of its class, as shown in
the following correction:
1
2
3
4
5
6
7
8
|
public class StaticExample1
{
public int number; //
instance variable
public static void main(String[]
args) {
StaticExample1 obj = new StaticExample1();
obj.number = 30;
}
}
|
Another
solution is making the variable become static, but it depends on the program’s
purpose. For example, it’s fine for the following program to access a constant
(static final field):
1
2
3
4
5
6
7
8
|
public class StaticExample2
{
public static final int BUFFER_SIZE
= 4096; // a constant
public static void main(String[]
args) {
byte[] data = new byte[BUFFER_SIZE];
}
}
|
The
same applies for an instance method being accessed by a static method. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class StaticExample3
{
// instance method:
public int sum(int a, int b)
{
return a
+ b;
}
public static void main(String[]
args) {
int
a = 399;
int
b = 256;
int
c = sum(a,
b); // compile error
}
}
|
And
here’s a solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class StaticExample3
{
// instance method:
public int sum(int a, int b)
{
return a
+ b;
}
public static void main(String[]
args) {
int
a = 399;
int
b = 256;
StaticExample3 obj = new StaticExample3();
int
c = obj.sum(a, b);
}
}
|
So to
avoid this mistake, remember the following rules:
·
Instance variables and
methods must be accessed via object reference. They cannot be accessed from
static context.
·
Static variables and
methods can be accessed from both static and non-static context.
To
fully understand the differences between static and non-static stuffs, consult
the tutorial: Differences
between static and non-static stuffs in Java
2. Missing closing curly braces
This
is also a common mistake which one writes an opening curly brace (the { ) but forget to put a
closing one (the } ). A closing curly brace is
often missed due to improper indentation of the code, or due to putting the
open brace without closing it immediately. Can you spot the missing curly brace
in following code snippet?
1
2
3
4
5
6
7
8
9
10
|
Thread stopper
= new Thread(new Runnable() {
public void run()
{
try
{
Thread.sleep(RECORD_TIME);
} catch (InterruptedException
ex) {
ex.printStackTrace();
}
recorder.finish();
});
|
Yes,
you can see that there is a missing closing brace for the method run() although the code looks
seemingly fine due to the last two lines are indented improperly. Here’s the
correction:
1
2
3
4
5
6
7
8
9
10
|
Thread stopper
= new Thread(new Runnable() {
public void run()
{
try
{
Thread.sleep(RECORD_TIME);
} catch (InterruptedException
ex) {
ex.printStackTrace();
}
recorder.finish();
}
});
|
Although
this mistake is detected quickly by the compiler and instantly by modern IDEs,
programmers still often make it. So to avoid this mistake:
·
Always write the
closing brace immediately after the opening one, and then put the code inside.
·
Indent/format the code
properly.
3. Missing break in switch case construct
Novice
programmers often misses a break in the switch case construct which does not cause compile error but makes
the code runs wrongly. It’s because the switch case construct in Java has a
feature called ‘fall through’ in which the code execution continues to the next
case if there is no break in the current case. Let’s look at the following
example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
Scanner scanner
= new Scanner(System.in);
int option
= scanner.nextInt();
switch (option)
{
case
1:
System.out.println("1 ->
New");
break;
case
2:
System.out.println("2 ->
Edit");
break;
case
3:
System.out.println("3 -> Delete");
break;
case
4:
System.out.println("4 ->
View");
break;
case
5:
System.out.println("5 ->
Quit");
default:
System.out.println("Unknown
command");
}
|
Can
you spot the missing break in
this code? Let run the code and enter 5 as input and you will get the following
output:
1
2
|
5 -> Quit
Unknown command
|
Oops!
It’s because there is a missing break in the 5th case, so the execution falls through to
the default case. Here’s the
correction:
1
2
3
4
5
|
case 5:
System.out.println("5 ->
Quit");
break;
default:
System.out.println("Unknown
command");
|
This
mistake is a little bit difficult to detect because it doesn’t cause compile
error. We only realize the problem when the program runs not as expected.
However,
sometimes we do need to remove the breaks, as shown in the following method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public static int getDaysOfMonth(int month, int year)
{
switch (month)
{
case
1:
case
3:
case
5:
case
7:
case
8:
case
10:
case
12:
return 31;
case
4:
case
6:
case
9:
case
11:
return 30;
case
2:
return (year
% 4 == 0) ? 29 : 28;
default:
throw new IllegalArgumentException();
}
}
|
As
you can see, this method returns number of days for a given month and year, and
the fall-through feature of the switch case is a great help.
Read
this article to
fully understand about the switch case construct in Java.
4. Confusing assignment with comparison (= and ==)
The = is an assignment operator
whereas the == is a relational
operator, and sometimes one mistakenly uses = instead of ==. Take a look at
this code snippet:
1
2
3
4
5
6
7
8
|
Scanner scanner
= new Scanner(System.in);
int number
= scanner.nextInt();
if (number
= 1) {
System.out.println("Choose
1");
} else {
System.out.println("Choose
other");
}
|
Here, number = 1 is an assignment not a
comparison, thus the compiler detects this mistake easily. Correction:
1
|
if (number
== 1) {
|
But
watch out for boolean assignment which the
compiler cannot detect, as shown in the following example:
1
2
3
4
5
|
public void openFile(String
path, boolean readOnly) {
if
(readOnly
= true) {
// this is always true
}
}
|
The if (readOnly = true) seems to be a comparison but
it is an assignment and the variable is of type boolean so the code compiles
normally. The problem is, code inside the if block always get executed regardless of the
value of the argument. Correction:
1
|
if (readOnly
== true) {
|
Consult
this article to
understand more about operators in Java.
5. Confusing object comparison (== instead of .equals)
The == operator compares two objects
physically (their memory addresses) whereas the equals() method compares two
objects semantically (their information). And in most of the cases, we should
compare two objects meaningfully using the equals() method. Novice programmers often make a
mistake by comparing two String objects
like this:
1
2
3
4
5
6
7
|
public void getMeat(String
type) {
if
(type
== "beef") {
System.out.print("Choose
beef");
} else if (type
== "pork") {
System.out.print("Choose
pork");
}
}
|
This
seems to work correctly with the following call:
1
|
getMeat("pork");
|
This
works because Java caches String literals
in String constant pool so that the == works here. But this won’t work:
1
2
|
String meatType
= new String("pork");
obj.getMeat(meatType);
|
Why?
Because meatType is now a
different String object so the ==
operator returns false. The rule is always using
the equals() method to compare two
objects, unless you have reason to compare two objects directly.
Thus
here’s the correction for the above method:
1
2
3
4
5
6
7
|
public void getMeat(String
type) {
if
(type.equals("beef"))
{
System.out.print("Choose
beef");
} else if (type.equals("pork"))
{
System.out.print("Choose
pork");
}
}
|
Also
pay attention when comparing two Integer wrapper objects. Consider the
following code:
1
2
3
4
|
Integer i1
= 100;
Integer i2
= 100;
System.out.println(i1
== i2);
|
This
will print true, but the following code
prints false:
1
2
3
4
|
Integer i1
= 500;
Integer i2
= 500;
System.out.println(i1
== i2);
|
Why?
It’s because Java caches integer numbers ranging from -128 to 127. So remember
the rule of using equals() method to compare
objects.
Refer
to this
article to
understand more about the equals() method.
6. Confusing about 0-based or 1-based index
Like
many other programming languages, array index in Java are 0-based, which means
the first element in the array starts at the index 0, the second at 1, and so
on. However, sometimes we mistakenly treat the first element at index 1. For
example:
1
2
|
String[] fruits =
{"Apple", "Banana", "Carrot", "Grape"};
String firstFruit =
fruits[1]; // should be: fruits[0]
|
The
problem is even worse if the array has only one element, then an ArrayIndexOutOfBoundsException will occur. For
example:
1
2
|
int[] numbers =
{256};
int firstNumber
= numbers[1]; // cause ArrayIndexOutOfBoundsException
|
And
it’s worth paying attention to some methods whose index parameters are both
0-based and 1-based. Let’s take the method substring(beginIndex, endIndex) of the String class for instance:
the beginIndex is 0-based, but
the endIndex is 1-based.
1
2
3
|
String title
= "JavaProgramming";
String subTitle =
title.substring(0, 4);
System.out.println(subTitle);
|
This
gives “Java” as the result.
Likewise,
the method subList()of List collections has fromIndex starts from 0 and toIndex starts from 1. For
example:
1
2
3
4
|
List<String>
names =
Arrays.asList("Andy", "Bob", "Carol", "David", "Eric");
List<String>
subNames = names.subList(0, 3);
System.out.println(subNames);
|
This
gives the result: [Andy,
Bob, Carol]
So
you can infer the rule for such methods: begin index starts from 0 and end
index starts from 1.
Also
be aware of the Calendar class which returns
date on 1-based but returns month in 0-based. For example:
1
2
3
4
5
6
7
8
9
|
Calendar calendar =
Calendar.getInstance();
// current date is
November, 2nd 2016
calendar.setTimeInMillis(System.currentTimeMillis());
int date
= calendar.get(Calendar.DATE);
int month
= calendar.get(Calendar.MONTH);
System.out.printf("date
= %d, month = %d", date, month);
|
This
prints: date
= 2, month = 10
So
bear in mind these rules to avoid this beginner mistake.
7. Using less restrictive access modifiers
This
is another very common mistake of all beginners: using access modifiers without
taking care of its visibility. The Java language provides 4 access modifiers
for protecting members of a class:
public > protected
> default > private
These
access modifiers are sorted from the least restrictive (public) to the most restrictive (private). However, novice
programmers do not fully understand this and often use the public and default access
modifiers which make the code less secured.
Consider
the following class:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Person
{
public String
name;
int
age;
public Person(String
name, int age) {
if
(name == null ||
age < 1 || age > 100) {
throw new IllegalArgumentException("Invalid
name or age");
}
this.name = name;
this.age = age;
}
}
|
It
seems to be fine as the constructor validates the name and age to make sure they are
properly set. For example, this instantiation looks fine:
1
|
Person p1 = new Person("Alex", 30);
|
And
this instantiation causes an exception to be thrown, as expected by the logic
in the constructor:
1
|
Person p2 = new Person(null, 0);
|
But
the problem is, the member variables name and age are not protected (name can be accessed
everywhere and age can be accessed in
the same package), one can
easily bypass the check logic in the constructor by writing code like this:
1
2
3
|
Person p1 = new Person("Alex", 30);
p1.name
= null; // valid
p1.age =
-1; // valid
|
So to
protect member variables from unwanted changes, we need to use more restrictive
access modifier such as private (cannot be modified outside the class)
or protected (can be modified only by subclasses or classes in the
same package). Thus the following is the correction for the Person class:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Person
{
private String
name;
private int age;
public Person(String
name, int age) {
if
(name == null ||
age < 1 || age > 100) {
throw new IllegalArgumentException("Invalid
name or age");
}
this.name = name;
this.age = age;
}
}
|
So
remember this rule: always use the most restrictive access modifiers if possible.
Consult this article to fully understand
about access modifiers in Java.
8. Forgetting to free resources
This
is also a common mistake which one easily forgets to close resources after use,
such as network connections, database connections, file streams, etc. This
mistake may lead to resources leaked or memory occupied by no longer used
objects.
Let’s
look at this example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static void copyFile(File
sourceFile, File destFile) {
FileChannel sourceChannel
= null;
FileChannel destChannel
= null;
try
{
sourceChannel = new FileInputStream(sourceFile).getChannel();
destChannel = new FileOutputStream(destFile).getChannel();
sourceChannel.transferTo(0,
sourceChannel.size(), destChannel);
} catch (IOException
ex) {
ex.printStackTrace();
}
}
|
As
you can see, this code misses the statements to close the sourceChannel and destChannel after using them. A
solution to this is using the try-with-resources structure available
since Java 7, which automatically closes the resources. For example, the above
code can be re-written like the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static void copyFile(File
sourceFile, File destFile) {
try
(
FileChannel sourceChannel
= new FileInputStream(sourceFile).getChannel();
FileChannel destChannel = new FileOutputStream(destFile).getChannel();
) {
sourceChannel.transferTo(0,
sourceChannel.size(), destChannel);
} catch (IOException
ex) {
ex.printStackTrace();
}
}
|
Therefore,
to never forget freeing resources, use the try-with-resources structure on the
resources implement the AutoCloseable interface.
9. Ignoring Exceptions
This
is another common mistake of beginners who are too lazy to write code for handling exceptions. It seems to be of no harms
if the code runs fine without exceptions. However, in case the exceptions
occurred, the code can fail silently which adds difficulty in finding the
problem.
Let’s
take a look at the following program:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Sum
{
public static void main(String[]
args) {
int
a = 0;
int
b = 0;
try
{
a = Integer.parseInt(args[0]);
b = Integer.parseInt(args[1]);
} catch (NumberFormatException
ex) {
}
int
sum = a + b;
System.out.println("Sum =
" + sum);
}
}
|
As
you can see, this program calculates the sum of two numbers passed via
command-line arguments. Note that the catch block is left empty. If we try to
run this program by the following command line:
1
|
java Sum 123 456y
|
It
will fail silently:
1
|
Sum = 123
|
It’s
because the second argument 456y causes a NumberFormatException to be thrown, but
there’s no handling code in the catch block so the program continues with
incorrect result.
So to
avoid such potential problem, always handle exceptions at least by printing the
stack trace to inform the error when it happens:
1
2
3
4
5
6
7
|
try {
a = Integer.parseInt(args[0]);
b = Integer.parseInt(args[1]);
} catch (NumberFormatException
ex) {
ex.printStackTrace();
}
|
Having
said that, don’t be lazy to ignore exceptions, as writing a simple a print
stack trace statement takes only few seconds and it can save you hours of
debugging later if the problem occurs.
10. Modifying a collection while iterating it
Sooner
or later, anyone who starts programming with Java makes this common mistake:
attempt to modify (remove) elements in a collection while iterating over it.
Consider the following example:
1
2
3
4
5
6
7
8
|
List<String>
fixedList = Arrays.asList("Apple", "Banana", "Carrot", "Grape");
List<String>
listFruit = new ArrayList<>(fixedList);
for (String
fruit : listFruit) {
if
(fruit.contains("e"))
{
listFruit.remove(fruit);
}
}
|
This
code attempts to remove String elements containing “e” letter but it
causes ConcurrentModificationException at runtime, even in a
single-threaded program. Why? There are two main reasons that Java prohibits
this kind of operation:
·
The list’s size is changed
dynamically when you remove an element. But it also causes the iteration
behaves unpredictable as the content of the list gets changed.
·
Imagine in a
multi-threaded program, while one thread is accessing this list and another
thread is removing elements from this list, the result will be unpredictable.
So is
there any solution?
Yes,
there’s a few.
First,
we can build a list contains elements that needs to be removed, then iterate on
this list to remove elements in the original list. For example:
1
2
3
4
5
6
7
8
9
10
|
List<String>
fruitToRemove = new ArrayList<>();
for (String
fruit : listFruit) {
if
(fruit.contains("e"))
{
fruitToRemove.add(fruit);
}
}
for (String
fruit : fruitToRemove) {
listFruit.remove(fruit);
}
|
Second,
a more compact solution is using an iterator which is designed for modifying a
collection while iterating its elements. The following example works well:
1
2
3
4
5
6
7
8
9
|
Iterator<String>
iterator = listFruit.iterator();
while (iterator.hasNext())
{
String next = iterator.next();
if
(next.contains("e"))
{
iterator.remove();
}
}
|
Third,
Java 8 makes thing much simpler with the Streams API. For example, the following code produces same
result as the above code:
1
2
3
|
List<String>
result = listFruit.stream()
.filter(fruit ->
!fruit.contains("e"))
.collect(Collectors.toList());
|
That’s
the 10 common mistakes in Java programming which every novice programmer makes
in his or her life. Note that this is not a top 10 because we don’t have
reliable statistics data.
We
hope you find this article helpful and we are welcome for suggestions for other
common mistakes in Java.
No comments:
Post a Comment