Everyone makes mistakes, not just beginners, but even professionals. This article goes over a dozen common mistakes that Java newbies and newcomers make and how to avoid them. Have you or your colleagues made any of these common Java mistakes early in your career?
Everyone makes mistakes, not only learners or beginners but professionals. As a programming course, the CodeGym team often collects mistakes of newbies to improve our auto validator. This time we decided to interview experienced programmers about mistakes in Java they made closer to their careers start or noticed them among their young colleagues.
We collected their answers and compiled this list of dozen popular mistakes Java beginners make. The order of errors is random and does not carry any special meaning.
An abstract class lets you create functionality that subclasses can implement or override. In an interface, you just define functionality, but not implement it. Although a class can only extend one abstract class, it can use multiple interfaces.
The choice between interface and abstract class depends on many factors.
Let’s look at the example below. Here we have incorrect construction of the object hierarchy. In particular, a misunderstanding of when to apply the interface, and when should be an abstract class.
public interface BaseEntity {
long getId();
void setId(long id);
}
public interface NamedEntity extends BaseEntity {
String getName();
void setName(String name);
}
public class User implements NamedEntity {
private long id;
private String name;
private String avatarUrl;
@Override
public long getId() {
return id;
}
@Override
public void setId(long id) {
this.id = id;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
}
public class Group implements NamedEntity {
private long id;
private String name;
private String description;
@Override
public long getId() {
return id;
}
@Override
public void setId(long id) {
this.id = id;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
public class Comment implements BaseEntity {
private long id;
private String content;
@Override
public long getId() {
return id;
}
@Override
public void setId(long id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
Here the beginner uses an interface, in spite of the fact that class is much more appropriate for this task.
The reason is that you can keep duplicated code into a class. If it is necessary, you can add Interfaces on top of parent classes. So here is the right decision (using abstract classes instead of interfaces):
public abstract class BaseEntity {
private long id;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
}
public abstract class NamedEntity extends BaseEntity {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class User extends NamedEntity {
private String avatarUrl;
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
}
public class Group extends NamedEntity {
private String description;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
public class Comment extends BaseEntity {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
Newbies often forget about the order that constructors are called when objects are created.
The rule is simple: constructors are called in inheritance order. When you think about the logic, it becomes clear that executing constructors in order of inheritance makes some sense.
The superclass knows nothing about its subclasses.
Thus, any initialization must be done in the superclass completely independently of any initialization performed by the subclass. Therefore, this should be done first.
public class Animal {
public Animal() {
System.out.println("Animal constructor has worked off");
}
}
public class Cat extends Animal {
public Cat() {
System.out.println("Cat constructor has worked off!");
}
public static void main(String[] args) {
Cat cat = new Cat();
}
}
The Output is:
Animal constructor has worked off
Cat constructor has worked off!
As you can see, when creating child elements, the base class constructors are implicitly called first.
In addition, it is important to remember to call the correct parent constructor, otherwise, the parent default constructor will be called, as in the following example:
public abstract class AbstractToken {
private final Collection<Object> authorities;
public AbstractToken(Collection<Object> authorities) {
if (authorities == null) {
this.authorities = Collections.emptyList();
return;
}
for (Object a : authorities) {
if (a == null) {
throw new IllegalArgumentException("Authorities collection cannot contain any null elements");
}
}
ArrayList<Object> temp = new ArrayList<>(authorities.size());
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);
}
public AbstractToken() {
this.authorities = Collections.emptyList();
}
}
public class MyToken extends AbstractToken {
private int userId;
public MyToken(User user, Object... authority) {
this.userId = user.getId();
}
}
public class User {
private int id;
public int getId() {
return id;
}
The constructor of the parent class is not explicitly called in the constructor of MyToken. So the constructor of AbstractToken with no parameters will be called. This constructor is missing the required object initialization part. Don’t forget to call a particular constructor you really need:
public class MyToken extends AbstractToken {
private int userId;
public MyToken(User user, Object... authority) {
super(Arrays.asList(authority));
this.userId = user.getId();
}
}
Overriding and Overloading are two very important concepts in Java. They could be really confusing for Java novice programmers.
Simply put, overriding allows you to take a method of the parent class and write its own implementation of this method in each inherited class.
The new implementation will “replace” the parent in the child class.
Here is an example. We have an Animal class with the voice() method.
public class Animal {
public void voice() {
System.out.println("Speak!");
}
}
Let’s say we need to override the behavior of the method in the derived classes. For example, we will implement 4 inheritor classes, which will have their own implementation of the voice method.
public class Bear extends Animal {
@Override
public void voice() {
System.out.println("Grrr!");
}
}
public class Cat extends Animal {
@Override
public void voice() {
System.out.println("Meow!");
}
}
public class Dog extends Animal {
@Override
public void voice() {
System.out.println("Bow-wow!");
}
}
public class Snake extends Animal {
@Override
public void voice() {
System.out.println("Hiss-hiss!");
}
}
Now let’s check how it works.
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
Animal animal3 = new Bear();
Animal animal4 = new Snake();
animal1.voice();
animal2.voice();
animal3.voice();
animal4.voice();
}
}
The output is:
Bow-wow!
Meow!
Grrr!
Hiss-hiss!
In addition to overriding, in the program we can use methods with the same name, but with different types and/or a number of parameters. This mechanism is called method overloading.
public class Program{
public static void main(String[] args) {
System.out.println(sum(2, 3)); // 5
System.out.println(sum(4.5, 3.2)); // 7.7
System.out.println(sum(4, 3, 7)); // 14
}
static int sum(int x, int y){
return x + y;
}
static double sum(double x, double y){
return x + y;
}
static int sum(int x, int y, int z){
return ukx + y + z;
}
}
Three options or three overloads of the sum() method are defined here, but when it is called, depending on the type and number of parameters passed, the system will choose the version that is most suitable.
There is another option where a newbie can make a mistake when overriding. You can simply remember in which case which method is called:
public class MyParent {
public void testPrint() {
System.out.println("parent");
}
}
public class MyChild extends MyParent {
@Override
public void testPrint() {
System.out.println("child");
}
public static void main(String[] args) {
MyParent o1 = new MyParent();
MyParent o2 = new MyChild();
MyChild o3 = new MyChild();
// MyChild o4 = (MyChild) new MyParent(); // ClassCastException
o1.testPrint();
o2.testPrint();
o3.testPrint();
}
}
The output is:
parent
child
child
Very often novice programmers don’t know how to work with exceptions correctly. To begin with, they simply ignore them, especially those that have moved from other programming languages. However, exceptions are thrown for some reason, so don’t ignore them.
There are also cases of careless handling of exceptions. For example, a newbie writes code, and their IDE starts underlining it in red and explains that certain exceptions may be thrown during its execution.
In this case, an inexperienced programmer often prefers to wrap all the code in a try-catch and do nothing in catch block:
public static void main(String[] args) {
try {
String urlString = new Scanner(System.in).nextLine();
URL url = new URL(urlString);
String content = new Scanner(url.openStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next();
System.out.println(content);
} catch (IOException ignored) {
}
}
The problem is that in this case, we will not know what kind of exception happened, for what reason, and whether it happened at all.
There are different options for how to fix the situation. For example:
public static void main(String[] args) {
try {
String urlString = new Scanner(System.in).nextLine();
URL url = new URL(urlString);
String content = new Scanner(url.openStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next();
System.out.println(content);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
String urlString = new Scanner(System.in).nextLine();
URL url = new URL(urlString);
String content = new Scanner(url.openStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next();
System.out.println(content);
}
public static void main(String[] args) {
URL url = getUrl();
String content = getContent(url, 3);
System.out.println(content);
}
private static String getContent(URL url, int attempts) {
while (true) {
try {
return new Scanner(url.openStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next();
} catch (IOException e) {
if (attempts == 0) {
throw new RuntimeException(e);
}
attempts--;
}
}
}
private static URL getUrl() {
while (true) {
String urlString = new Scanner(System.in).nextLine();
try {
return new URL(urlString);
} catch (MalformedURLException e) {
System.out.println("URL is incorrect. Please try again.");
}
}
}
Beginners are often confused about choosing the right collection. Sometimes they choose this or that collection by mistake because they do not deeply understand data structure.
The wrong choice can affect the effectiveness of your program. On the other hand, if the collection is selected correctly, your program will look simpler and more logical, and the solution will be more efficient.
To find out which type of collection is right for your task, find out the characteristics and behavior of each one, as well as the differences between them.
You need to be clear about the pros and cons of each specific implementation (ArrayList vs LinkedList, treeMap vs HashMap, and so on).
I recommend the first steps:
A huge number of libraries have been written for Java, but beginners often do not notice all these gems.
Don’t try to reinvent the wheel, first learn the existing developments on the issue of interest. Many libraries have been perfected by developers over the years, and you can use them for free. For example, Google Guava, or Log4j.
Let’s have an example. This is what the code looks like without using libraries:
private static Map<String, Integer> getFrequencyMap(Set<String> words, List<String> wordsList) {
Map<String, Integer> result = new HashMap<>();
for (String word : words) {
int count = 0;
for (String s : wordsList) {
if (word.equals(s)) {
count++;
}
}
result.put(word, count);
}
return result;
}
Here we use Collections.frequency() library:
private static Map<String, Integer> getFrequencyMap(Set<String> words, List<String> wordsList) {
Map<String, Integer> result = new HashMap<>();
for (String word : words) {
result.put(word, Collections.frequency(wordsList, word));
}
return result;
}
Of course, here we’ve got just a short example, so the complexity of the code has changed only slightly. However, the readability of the code has significantly increased.
In large projects with many classes, using built-in and third-party libraries can significantly speed up development, improve readability, and testability. So don’t forget to explore Java libraries.
Very often, novice programmers “test” their code incorrectly. For example, using System.out.println(), substituting and printing different values to the console.
Seriously, from the very first steps, you should learn how to use the excellent JUnit library and write tests for your programs. Moreover, you will definitely need it in your work. Unit testing your own code is a good practice for developers.
Every time your program opens a file or sets a network connection, you need to free up the resources it uses. It is also true for cases of exceptions when working with resources.
Of course, FileInputStream has a finalizer that calls the close() method to collect garbage. However, you can’t be sure about the beginning of the build cycle. Thus, there is a risk that the input stream may consume resources indefinitely.
The problem for beginners is that not freeing resources does not lead to compile-time or run-time errors. So it’s easy to forget about it. Let’s give an example.
private void populateConfigs(Path propertiesPath) throws IOException {
assert propertiesPath != null;
DirectoryStream<Path> directoryStream = Files.newDirectoryStream(propertiesPath, "*.properties");
for (Path entry : directoryStream) {
Properties properties = new Properties();
properties.load(Files.newBufferedReader(entry));
validateAndSave(properties);
}
}
The program works, but we’ll better do it the right way. Let’s close the resource as follows:
private void populateConfigs(Path propertiesPath) throws IOException {
assert propertiesPath != null;
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(propertiesPath, "*.properties")) {
for (Path entry : directoryStream) {
Properties properties = new Properties();
properties.load(Files.newBufferedReader(entry));
validateAndSave(properties);
}
}
}
The Object class is the parent class for all Java objects. This class has methods equal() and hashCode ().
The equals () method, as its name suggests, is used to simply check if two objects are equal. hashCode() method that allows you to get a unique integer number for a given object.
Often newbies don’t feel like these methods need to be overridden for their objects.
The default implementation of the equals() method simply checks the two objects by reference to see if they are equivalent.
For example, you need to compare two points on the coordinate plane, let’s try to override the equals method:
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
Now, without overriding the equals() method for the Point class, let’s try to compare the points. To do this, let’s create a PointDemo class and in it, there are three points, two according to the logic of Cartesian coordinates — equal (with the same abscissa and ordinate) and the third, which differs from them.
public class PointDemo {
public static void main(String[] args) {
Point point1 = new Point(2, 3);
Point point2 = new Point(2, 3);
Point point3 = new Point(2, 5);
if (point1.equals(point2))
System.out.println("1 and 2 are equal");
else
System.out.println("1 and 2 are not equal");
if (point1.equals(point3)) System.out.println("1 and 3 are equal");
else System.out.println("1 and 3 are not equal");
}
}
The output is:
1 and 2 are not equal
1 and 3 are not equal
More often, a practicing beginner forgets not to override equals(), but to override it correctly. For example, they forget that the object can be null or that it needs to be checked for equivalence to itself. Let’s write the correct equals() method to compare two points on the plane:
public boolean equals(Object obj) {
if (obj == null) return false; // checking if the passed object is null
if (obj == this) return true; //checking if the passed object is equal to itself
if (obj.getClass() == this.getClass()) { // checking if the passed object has the same result of the getClass () method as the current one, on which the equals method was called
Point point = (Point) obj; // now we can definitely convert the passed object to type Point
if (point.x == this.x && point.y == this.y) // if the coordinates match, then return true, otherwise false
return true;
}
return false;
}
}
Now if we run the main() method of the PointDemo() class we get the following result:
1 and 2 are equal
1 and 3 are not equal
This newbie mistake leads to a NullPointerEception that is thrown when they try to use an uninitialized object. Don’t be confused about declaring an object variable, this does not mean that it is initialized.
So if you write something like:
private static String name;
public static void main(String[] args) {
System.out.println(name.length());
}
you’ll get an exception when you try to call the length method because the name field is null. You should always initialize them before working with variables.
You need to be careful to remember that Wrapper classes such as Integer or Boolean are reference data types and the type of variable can be null. In this case, it is better to avoid operations in which the null doesn’t work well.
Often newbies work with uninitialized variables of type Integer or Boolean somewhere as with int or bool, which can cause NullPointerException errors.
For example, if we have any Boolean s, which is Null by default, and they try to call it in some if (s), we will get an error.
Also, autoboxing of variables of primitive types requires an exact match of the type of the original primitive — the type of the “wrapper class”. Attempting to autopack a variable of type byte into Short without first explicitly casting byte-> short will cause a compilation error.
Here is an example:
public static class Man {
private String name;
private Integer age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public static void main(String[] args) {
Man man = new Man();
System.out.println(man.getAge());
}
}
In this example, when we run the main method, we will receive a NullPointerException error. This happens because we return int instead of Integer in the getAge getter. As we said before, int can’t be null and auto-unboxing occurs, in such cases, you need to be careful and pay attention that the field is not null or the getAge method returns Integer.
Experts said that the most common mistake among developers, in general, is dealing with asynchronous code (concurrency, threads, etc.).
If in a real project it becomes necessary to work with code asynchronously, you don’t need to use low-level multithreaded programming. It could be beneficial for learning issues or some kind of experimentation.
However, in a real project, this leads to unnecessary complexity and many potential errors. Therefore, when working with multithreading, it is better to use classes from the java.util.concurrent package or other ready-made third-party libraries (Guava, etc.)
Here is an example of the code:
public class MyTask implements Runnable {
private int monitoringPeriod;
private boolean active;
@Override
public void run() {
// do work
}
public int getMonitoringPeriod() {
return monitoringPeriod;
}
public void setMonitoringPeriod(int monitoringPeriod) {
this.monitoringPeriod = monitoringPeriod;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
public class Solution {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(8);
Collection<MyTask> tasks = generateTasks();
for (MyTask task : tasks) {
if (task.isActive()) {
scheduler.scheduleWithFixedDelay(task, 0, task.getMonitoringPeriod(), TimeUnit.SECONDS);
}
}
}
private static Collection<MyTask> generateTasks() {
Collection<MyTask> result = new HashSet<>();
// tasks generation
return result;
}
}
If you try to do the same, but manually start the threads, you might end up with hard-to-read and difficult-to-maintain code. It’s also about the invention of the wheel.
We tried to describe the most popular mistakes made by Java newbies, according to experts, experienced programmers. Many examples are intended in the article so that it is convenient for beginners to get rid of these errors as soon as possible.
In fact, these are not really errors. These are certain stages of the practical use of the language, through which most of the beginning Java programmers go. Errors of the very first stage (for example, incorrect placement of curly braces or semicolons we will omit) and start with those that Java Trainee and Java Junior often do. Sure, it is advisable not to prolong this stage of “popular mistakes” for a long time. This is exactly what we wish you for in your Java developer way.
Previously published at https://jaxenter.com/java-mistakes-174395.html