The 10 most common mistakes new to Java make

Original author: Mikhail Selivanov
  • Transfer
  • Tutorial
Hello, my name is Alexander Akbashev, I am Lead QA Engineer in the Skyforge project. And also part-time assistant tully in the Technopark on the course "Advanced Java Programming". Our course is in the second semester of the Technopark, and we receive students who take courses in C ++ and Python. Therefore, I have long wanted to prepare material devoted to the most common mistakes of beginners in Java. Unfortunately, I did not intend to write such an article. Fortunately, such an article was written by our compatriot - Mikhail Selivanov, however, in English. Below is a translation of this article with a few comments. For all comments related to the translation, please write in private messages.



Initially, the Java language was created for interactive television , but over time it began to be used wherever possible. Its developers were guided by the principles of object-oriented programming, abandoning the excessive complexity inherent in the same C and C ++. Platform independence of the Java virtual machine formed at one time a new approach to programming. Add to this a smooth learning curve and the slogan “Write once, run everywhere”, which almost always corresponds to the truth. But still, errors still occur, and here I would like to make out the most common of them.

Mistake One: Neglecting Existing Libraries


A myriad of libraries have been written for Java, but beginners often do not use all this wealth. Before reinventing the wheel, it is better to first study the existing developments on the issue of interest. Many libraries have been perfected by developers for years, and you can use them for free. Examples include logback and log4j libraries, Netty and Akka network libraries. And some developments, like Joda-Time, have become the de facto standard among programmers.

On this subject I want to talk about my experience working on one of the projects. The part of the code that was responsible for escaping HTML characters was written by me from scratch. For several years everything worked without failures. But once a user request triggered an endless loop. The service stopped responding, and the user tried to enter the same data again. In the end, all server processor resources allocated for this application were taken up by this endless loop. And if the author of this naive tool for replacing characters used one of the well-known libraries, HtmlEscapers or Google Guavaprobably this annoying incident did not happen. Even if there was some hidden error in the library, it would surely have been discovered and corrected by the developer community before it would have appeared on my project. This is typical of most of the most popular libraries.

Second error: do not use the break keyword in the Switch-Case construct


Such errors can be very confusing. It happens that they are not even detected, and the code gets into production. On the one hand, failing to execute switch statements is often useful. But if this was not originally intended, the absence of the break keyword can lead to disastrous results. If we omit break in case 0 in the example below , then the program will display One after Zero, since the flow of command execution will go through all the switches until it encounters a break .

public static void switchCasePrimer() {
    	int caseIndex = 0;
    	switch (caseIndex) {
        	case 0:
            	System.out.println("Zero");
        	case 1:
            	System.out.println("One");
            	break;
        	case 2:
            	System.out.println("Two");
            	break;
        	default:
            	System.out.println("Default");
    	}
}

Most often, it is advisable to use polymorphism to isolate parts of the code with specific behavior into separate classes. And you can look for such errors with the help of static code analyzers, for example, FindBugs or PMD .

Third mistake: forget to free resources


Each time after the program opens a file or establishes a network connection, you need to free up the resources used. The same applies to situations when there are any exceptions when operating with resources. Some might argue that FileInputStream has a finalizer that calls the close () method to collect garbage. But we cannot know exactly when the assembly cycle will start, so there is a risk that the input stream may take resources for an indefinite period of time. Especially for such cases, Java 7 has a very useful and neat try-with-resources statement :

private static void printFileJava7() throws IOException {
    try(FileInputStream input = new FileInputStream("file.txt")) {
        int data = input.read();
        while(data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }
}

This operator can be used with any objects related to the AutoClosable interface . Then you do not have to worry about freeing resources, this will happen automatically after the statement is executed.

Fourth error: memory leak


Java uses automatic memory management, which eliminates the need for manual allocation and deallocation. But this does not mean at all that developers may not even be interested in how applications use memory. Alas, problems can still arise here. As long as the program holds references to objects that are no longer needed, memory is not freed. Thus, it can be called a memory leak. The reasons are different, and the most common of them is just the presence of a large number of references to objects. After all, as long as there is a link, the garbage collector cannot delete this object from the heap. For example, you described a class with a static field containing a collection of objects, and a link was created. If you forgot to reset this field after the collection became unnecessary, then the link has not gone away.

Another common cause of leaks is the presence of circular references. In this case, the collector simply cannot decide whether more objects are needed that cross-reference each other. Leaks can also occur on the stack when using the JNI (Java Native Interface). For instance:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);
scheduledExecutorService.scheduleAtFixedRate(() -> {
	BigDecimal number = numbers.peekLast();
   	if (number != null && number.remainder(divisor).byteValue() == 0) {
     	System.out.println("Number: " + number);
		System.out.println("Deque size: " + numbers.size());
	}
}, 10, 10, TimeUnit.MILLISECONDS);
	scheduledExecutorService.scheduleAtFixedRate(() -> {
		numbers.add(new BigDecimal(System.currentTimeMillis()));
	}, 10, 10, TimeUnit.MILLISECONDS);
try {
	scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
	e.printStackTrace();
}

Two tasks are created here. One of them takes the last number from the two-way numbers queue and displays its value and the size of the queue if the number is a multiple of 51. The second task puts the number in the queue. Both tasks have a fixed schedule, iterations occur at intervals of 10 milliseconds. If you run this code, then the size of the queue will increase indefinitely. In the end, this will cause the queue to fill up all available heap memory. To prevent this, but still preserve the semantics of the code, you can use another method to extract numbers from the queue: pollLast . It returns an element and removes it from the queue, while peekLast only returns.

If you want to know more about memory leaks, you can study the article dedicated to this .

Translator's note: in fact, in Java, the problem of circular references has been solved, because modern garbage collection algorithms take into account reachability of links from root nodes. If objects containing links to each other are not reachable from the root, they will be considered garbage. Read about garbage collection algorithms in Java Platform Performance: Strategies and Tactics .

Fifth error: excessive amounts of garbage




This happens when a program creates a large number of objects that have been used for a very short time. At the same time, the garbage collector non-stop removes unnecessary objects from memory, which leads to a strong performance drop. A simple example:

String oneMillionHello = "";
for (int i = 0; i < 1000000; i++) {
    oneMillionHello = oneMillionHello + "Hello!";
}
System.out.println(oneMillionHello.substring(0, 6));

In Java, string variables are immutable. Here, at each iteration, a new variable is created, and for addressing you need to use a mutable StringBuilder :

StringBuilder oneMillionHelloSB = new StringBuilder();
    for (int i = 0; i < 1000000; i++) {
        oneMillionHelloSB.append("Hello!");
    }
System.out.println(oneMillionHelloSB.toString().substring(0, 6));

If in the first version it takes a lot of time to execute the code, then in the second one the performance is much higher.

Sixth error: use without the need for null pointers


Try to avoid using null. For example, it is better to return empty arrays or collections by methods than null, as this will prevent a NullPointerException from occurring . The following is an example of a method that processes a collection obtained from another method:

List accountIds = person.getAccountIds();
for (String accountId : accountIds) {
    processAccount(accountId);
}

If getAccountIds () returns null when person does not have an account , then a NullPointerException will be thrown . To prevent this from happening, you must do a null check. And if an empty list is returned instead of null, then the problem with NullPointerException does not occur. In addition, code without null checks is cleaner.

In different situations, you can avoid using null in different ways. For example, use the Optional class , which can be either an empty object or a wrap for some value:

Optional optionalString = Optional.ofNullable(nullableString);
if(optionalString.isPresent()) {
    System.out.println(optionalString.get());
}

Java 8 uses a more concise approach:

Optional optionalString = Optional.ofNullable(nullableString);
optionalString.ifPresent(System.out::println);

Optional appeared in the eighth version of Java, but in functional programming it was used long before that. For example, in Google Guava for earlier versions of Java.

Seventh error: ignoring exceptions


Often, novice developers do not handle exceptions. However, do not neglect this work. Exceptions are thrown for a reason, and in most cases you need to deal with the reasons. Do not ignore such events. If necessary, drop them again to see the error message, or sign up. In extreme cases, you need to at least justify for other developers the reason why you did not understand the exception.

selfie = person.shootASelfie();
try {
    selfie.show();
} catch (NullPointerException e) {
    // Может, человек-невидимка. Да какая разница?
}

It is best to designate a minor exception with a message in a variable:

try { selfie.delete(); } catch (NullPointerException unimportant) {  }

Translator's note: Practice never tires of proving that there are no unimportant exceptions. If you want to ignore the exception, then you need to add some additional checks so as not to either raise an exception in principle, or to ignore the exception very precisely. Otherwise, you will have long hours of debug in search of an error that was so easy to write to the log. You also need to remember that throwing an exception is not a free operation. At a minimum, you need to collect a call stack, and for this you need to pause on safepoint. And it all takes time ...

Eighth error: ConcurrentModificationException


This exception occurs when the collection is modified during iteration by any methods, except for the iterator itself. For example, we have a list of hats, and we want to remove all earflaps from it:

List hats = new ArrayList<>();
hats.add(new Ushanka()); // that one has ear flaps
hats.add(new Fedora());
hats.add(new Sombrero());
for (IHat hat : hats) {
    if (hat.hasEarFlaps()) {
        hats.remove(hat);
    }
}



When this code is executed, a ConcurrentModificationException will be thrown , because the code modifies the collection during iteration. The same exception will occur if one of several threads working with the same list tries to modify the collection while other threads iterate over it. Modifying a collection at the same time is a frequent occurrence during multithreading, but in this case you need to use appropriate tools, such as synchronization locks, special collections adapted for simultaneous modification, etc.

In the case of one thread, this problem is solved a little differently.

Collect objects and delete them in another loop

The decision to collect earflaps and remove them during the next cycle immediately comes to mind. But then you have to create a new collection for storing caps prepared for removal.

List hatsToRemove = new LinkedList<>();
for (IHat hat : hats) {
    if (hat.hasEarFlaps()) {
        hatsToRemove.add(hat);
    }
}
for (IHat hat : hatsToRemove) {
    hats.remove(hat);
}

Use the Iterator.remove method.

This is a more concise way where you do not need to create a new collection:

Iterator hatIterator = hats.iterator();
while (hatIterator.hasNext()) {
    IHat hat = hatIterator.next();
    if (hat.hasEarFlaps()) {
        hatIterator.remove();
    }
}

Use ListIterator methods

When a modified collection implements the List interface , it is advisable to use a list iterator. Iterators that implement the ListIterator interface support both delete and add and assign operations. ListIterator implements the Iterator interface , so our example will look almost the same as the Iterator removal method . The difference lies in the type of the header iterator and its getting using the listIterator () method . The following snippet demonstrates how to replace each earflaps with a sombrero using the methods ListIterator.remove and ListIterator.add:

IHat sombrero = new Sombrero();
ListIterator hatIterator = hats.listIterator();
while (hatIterator.hasNext()) {
    IHat hat = hatIterator.next();
    if (hat.hasEarFlaps()) {
        hatIterator.remove();
        hatIterator.add(sombrero);
    }
}

Using ListIterator, calls to delete and add methods can be replaced with a single call:

IHat sombrero = new Sombrero();
ListIterator hatIterator = hats.listIterator();
while (hatIterator.hasNext()) {
    IHat hat = hatIterator.next();
    if (hat.hasEarFlaps()) {
        hatIterator.set(sombrero); // set instead of remove and add
    }
}

Using the stream methods introduced in Java 8, you can transform a collection into a stream, and then filter it according to some criteria. Here is an example of how a threading API can help in filtering headers without throwing a ConcurrentModificationException :

hats = hats.stream().filter((hat -> !hat.hasEarFlaps()))
        .collect(Collectors.toCollection(ArrayList::new));

The Collectors.toCollection method creates a new ArrayList with filtered headers. If a large number of objects satisfies the criteria, then this can be a problem, since the ArrayList is quite large. So use this method with caution.

You can do something else - use the List.removeIf method introduced in Java 8. This is the shortest option:

hats.removeIf(IHat::hasEarFlaps);

And that’s it. Internally, this method is invoked by Iterator.remove .

Use specialized collections

If at the very beginning we decided to use CopyOnWriteArrayList instead of ArrayList , then there would be no problem at all, because CopyOnWriteArrayList uses methods of modification (assignment, addition and deletion) that do not change the base array (backing array) of the collection. Instead, a new, modified version is created. Because of this, you can iterate and modify the original version of the collection at the same time without fear of getting a ConcurrentModificationException . The disadvantage of this method is obvious - you have to generate a new collection for each modification.

There are collections configured for different cases, for example, CopyOnWriteSet and ConcurrentHashMap .

Another possible error related to ConcurrentModificationException is to create a stream from the collection, and then modify the backing collection during iteration of the stream. Avoid this. The following is an example of improper handling of a stream:

List filteredHats = hats.stream().peek(hat -> {
    if (hat.hasEarFlaps()) {
        hats.remove(hat);
    }
}).collect(Collectors.toCollection(ArrayList::new));

The peek method collects all the elements and applies a specific action to each. In this case, it is trying to remove an item from the base list, which is not correct. Try to use other methods described above.

Mistake nine: breach of contract


It happens that for the correct operation of the code from the standard library or from some vendor, certain rules must be observed. For example, the hashCode and equals contract guarantees the work of a collection of collections from the Java collection framework, as well as other classes using the hashCode and equals methods. Non-compliance with a contract does not always result in exceptions or interruption of compilation. Here, everything is somewhat more complicated, sometimes it can affect the application so that you do not notice anything suspicious. The wrong code can get into production and lead to unpleasant consequences. For example, cause UI buggy, incorrect data reports, poor performance, data loss, etc. Fortunately, this rarely happens. The same hashCode and equals contract mentioned above is used in collections based on hashing and comparing objects like HashMap and HashSet . Simply put, a contract contains two conditions:
  • If two objects are equivalent, then their codes must also be equivalent.
  • Even if two objects have the same hash codes, they may not be equivalent.

Violation of the first rule leads to problems when trying to retrieve objects from hashmap.

public static class Boat {
    private String name;
    Boat(String name) {
        this.name = name;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Boat boat = (Boat) o;
        return !(name != null ? !name.equals(boat.name) : boat.name != null);
    }
    @Override
    public int hashCode() {
        return (int) (Math.random() * 5000);
    }
}

As you can see, the Boat class contains the overridden hashCode and equals methods . But the contract was still broken, because hashCode every time returns random values ​​for the same object. Most likely, a boat called Enterprise will never be found in the hash array, despite the fact that it was previously added:

public static void main(String[] args) {
    Set boats = new HashSet<>();
    boats.add(new Boat("Enterprise"));
    System.out.printf("We have a boat named 'Enterprise' : %b\n", boats.contains(new Boat("Enterprise")));
}

Another example relates to the finalize method . This is what its functionality says in the official Java documentation:
The main finalize contract is that it is called then and if, when the virtual machine determines that there are no more reasons why this object should be available to some thread (not yet dead). An exception may be the result of the completion of some other object or class that is ready to be completed. The finalize method can perform any action, including making the object accessible to other threads again. But usually finalize is used for cleanup actions before the object is permanently deleted. For example, this method for an object that is an input / output connection can explicitly perform I / O transactions to break the connection before the object is permanently deleted.

You do not need to use the finalize method to free resources like file handlers, because it is not known when it can be called. This can happen while the garbage collector is running. As a result, its duration will unpredictably drag on.

Mistake # 10: using raw types instead of parameterized ones


Согласно спецификации Java, сырой тип является либо непараметризованным, либо нестатическим членом класса R, который не унаследован от суперкласса или суперинтерфейса R. До появления в Java обобщённых типов, альтернатив сырым типам не существовало. Обобщённое программирование стало поддерживаться с версии 1.5, и это стало очень важным шагом в развитии языка. Однако ради совместимости не удалось избавиться от такого недостатка, как потенциальная возможность нарушения системы классов.

List listOfNumbers = new ArrayList();
listOfNumbers.add(10);
listOfNumbers.add("Twenty");
listOfNumbers.forEach(n -> System.out.println((int) n * 2));

Here the list of numbers is presented as a raw ArrayList. Since its type is not specified, we can add any object to it. But in the last line in int elements are thrown, doubled and displayed. This code compiles without errors, but if you run it, a runtime exception will pop up because we are trying to write a string variable to a numeric one. Obviously, if we hide the necessary information from the type system, it will not save us from writing erroneous code. Therefore, try to determine the types of objects that you intend to store in the collection:

List listOfNumbers = new ArrayList<>();
listOfNumbers.add(10);
listOfNumbers.add("Twenty");
listOfNumbers.forEach(n -> System.out.println((int) n * 2));

This example differs from the original version by the line in which the collection is specified:

List listOfNumbers = new ArrayList<>();

This option does not compile, because we are trying to add a string variable to a collection that can only store numeric ones. The compiler will throw an error and point to the line where we are trying to add the string Twenty to the list. So always try to parameterize generic types. In this case, the compiler will be able to check everything, and the chances of a runtime exception due to inconsistencies in the type system will be minimized.

Conclusion


Many aspects of developing software on the Java platform are simplified due to the separation of the complex Java Virtual Machine and the language itself. However, features like automatic memory management or decent OOP tools do not rule out the possibility of problems. The tips here are universal: practice regularly, study libraries, read documentation. And do not forget about static code analyzers, they can point out existing bugs and suggest what you should pay attention to.

Also popular now: