📜 ⬆️ ⬇️

Top 10 Mistakes That Newbies Make to Java

Hello, my name is Alexander Akbashev, I am Lead QA Engineer in the Skyforge project. As well as part-time assistant tully in Technopark on the course "Advanced Java programming." Our course is in the second semester of Technopark, and we get students who have completed courses in C ++ and Python. Therefore, I have long wanted to prepare material on the most common mistakes newbies in Java. Unfortunately, I did not intend to write such an article. Fortunately, our compatriot Mikhail Selivanov wrote this article, but 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 became 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-independent Java virtual machine has formed a new approach to programming. Add to this the smooth learning curve and the slogan “Write once, run everywhere”, which is almost always true. Still, errors still occur, and here I would like to make out the most common ones.

Error one: neglect existing libraries


For Java, a myriad of libraries has been written, but newbies often do not use all this wealth. Before reinventing the wheel, it is better to first study the available developments on the subject of interest. Over the years, many libraries have been brought to perfection by developers, and you can use them for free. Examples include libraries for logback logback and Log4j, Netty and Akka network libraries. And some developments, like Joda-Time, have become the de facto standard among programmers.
')
On this topic 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 provoked an endless loop. The service stopped responding and the user tried to enter the same data again. In the end, all the processor resources of the server allocated for this application were occupied by this infinite loop. And if the author of this naive tool for replacing characters took advantage of one of the well-known libraries, HtmlEscapers or Google Guava , probably this annoying incident would not have happened. Even if there was some hidden error in the library, then surely it would have been discovered and corrected by the developer community before it manifested itself on my project. This is typical of most of the most popular libraries.

Error two: 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, the failure of switch statements is often helpful. But if it 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, the program after Zero will output One, since the command execution flow will go through all the switches until it encounters 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 highlight parts of the code with specific behavior in separate classes. Such errors can be found using static code analyzers, for example, FindBugs or PMD .

Error three: forget to release resources


Every time after the program opens a file or establishes a network connection, it is necessary to release the resources used. The same applies to situations when there were any exceptions when operating with resources. Someone might argue that the FileInputStream has a finalizer calling the close () method for garbage collection. But we cannot know exactly when the build 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 will not have to worry about the release of resources, this will happen automatically after the execution of the operator.

Error four: memory leaks


In Java, automatic memory management is used, allowing you not to engage in manual allocation and release. But this does not mean that developers can not be at all interested in how applications use memory. Alas, but still there may be problems. As long as the program holds references to objects that are no longer needed, the memory is not released. Thus, it can be called a memory leak. The reasons are different, and the most frequent of them is just the presence of a large number of references to objects. After all, while there is a link, the garbage collector cannot remove 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 is no longer needed, then the link has not gone away. Such static fields are considered roots for the garbage collector and are not collected by him.

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 example:

 final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); final Deque<BigDecimal> 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(); } 

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

If you want to learn more about memory leaks, you can study the article devoted to this .

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

Fifth error: excessive amount of garbage




This happens when the program creates a large number of objects that are used for a very short time. At the same time, the garbage collector continuously removes unnecessary objects from the memory, which leads to a severe drop in performance. 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 it is necessary to use a variable 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 variant it takes a lot of time to execute code, then in the second one the performance is already much higher.

Error six: use without the need for null pointers


Try to avoid using null. For example, returning empty arrays or collections is better than methods than null, since this will prevent NullPointerException from occurring. Below is an example of a method that handles a collection obtained from another method:

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

If getAccountIds () returns null when the person has no account , a NullPointerException will occur. To avoid this, you need to check for null. 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 various ways. For example, use the Optional class, which can be either an empty object or a wrapper (wrap) for any value:

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

Java 8 uses a more concise approach:

 Optional<String> 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.

Error Seven: 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 understand the reasons. Do not ignore such events. If necessary, drop them again to view the error message, or deposit it. As a last resort, 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 indicate the insignificance of the exception using a message in a variable:

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

Translator's note: Practice does not get tired of proving that there are no unimportant exceptions. If you want to ignore the exception, then you need to add some additional checks to either not cause an exception in principle, or to ignore the exception super-pointwise. Otherwise, you expect long hours of debugging in search of an error that was so easy to write to the log. You also need to remember that creating an exception is not a free operation. At a minimum, you need to collect a callstack, and for this you need to pause at a safepoint. And it all takes time ...

Error Eight: ConcurrentModificationException


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

 List<IHat> 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 executing this code, a ConcurrentModificationException will pop up because the code modifies the collection during iteration. The same exception will arise if one of several threads working with one list tries to modify the collection while other threads iterate it. Simultaneous modification of the collection is a frequent occurrence with multi-threading, but in this case, you need to use appropriate tools, such as synchronization lock (synchronization lock), 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 cycle.

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

 List<IHat> 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 in which you do not need to create a new collection:

 Iterator<IHat> 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 (list iterator). Iterators that implement the ListIterator interface support both delete operations, and additions and assignments. ListIterator implements the Iterator interface, so our example will look almost the same as the Iterator deletion method . The difference lies in the type of header iterator and its retrieval using the listIterator () method. The following fragment demonstrates how you can replace each earflap with a sombrero using the ListIterator.remove and ListIterator.add methods :

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

With the ListIterator, calls to delete and add methods can be replaced by one call:

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

Using the flow methods presented in Java 8, you can transform the collection into a stream, and then filter it out according to some criteria. Here is an example of how a streaming API can help in filtering caps without a ConcurrentModificationException :

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

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

You can do it another way - use the List.removeIf method, introduced in Java 8. This is the shortest option:

 hats.removeIf(IHat::hasEarFlaps); 

And that's all. At the internal level, this method is enabled by Iterator.remove .

Use specialized collections

If at the very beginning we decided to use CopyOnWriteArrayList instead of ArrayList , then there would be no problems at all, because CopyOnWriteArrayList uses modifying methods (assignment, addition and deletion) that do not change the base array of the collection. Instead, a new, modified version is created. This allows you to simultaneously iterate and modify the original version of the collection 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 that are 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 the iteration of the stream. Avoid this. The following is an example of a thread abuse:

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

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

Error Nine: breach of contract


It happens that for the correct operation of the code from the standard library or from some vendor you need to follow certain rules. For example, the hashCode and equals contract guarantees the operation of a collection of collections from the Java collection framework, as well as other classes that use the hashCode and equals methods. Failure to comply with the contract does not always lead to exceptions or interruption of compilation. Everything is somewhat more complicated, sometimes it can affect the operation of the application so that you do not notice anything suspicious. Erroneous code can get into production and lead to unpleasant consequences. For example, cause UI to be buggy, incorrect data reports, poor performance, data loss, etc. Fortunately, this rarely happens. The same aforementioned contract hashCode and equals are used in collections based on hashing and comparing objects like HashMap and HashSet . Simply put, a contract contains two conditions:

Violation of the first rule leads to problems when trying to extract objects from a 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 overridden hashCode and equals methods. But the contract was still broken, because hashCode returns random values ​​for the same object each time. Most likely, the boat named Enterprise will not be found in the hash array, despite the fact that it was previously added:

 public static void main(String[] args) { Set<Boat> 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. Here is what is said about its functionality in the official Java documentation:
The main contract finalize is that it is invoked then and if, when the virtual machine determines that there are no longer any reasons why this object should be available to any 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 again making the object available to other threads. 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 an I / O transaction to break the connection before the object is permanently deleted.

You should not use the finalize method to release resources like file handlers, because it is not known when it can be called. This can occur while the garbage collector is running. As a result, the duration of his work will be unpredictably delayed.

Error tenth: using raw types instead of parameterized ones


According to the Java specification, the raw type is either a non-parameterized or non-static member of class R , which is not inherited from the superclass or superinterface R. Before the appearance of generic types in Java, there was no alternative to raw types. Generalized programming has been supported since version 1.5, and this was a very important step in the development of the language. However, for the sake of compatibility, it was not possible to get rid of such a disadvantage as the potential for breaking the class system.

 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 in the form of a raw ArrayList. Since its type is not specified, we can add any object to it. But in the last line elements are thrown into int, doubled and output. This code will be compiled without errors, but if you run it, a runtime exception will pop up, because we are trying to write the string variable to a numeric one. Obviously, if we hide the necessary information from the type system, it will not prevent us from writing erroneous code. Therefore, try to determine the types of objects that are going to be stored in the collection:

 List<Integer> 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 in the line in which the collection is defined:

 List<Integer> listOfNumbers = new ArrayList<>(); 

This option is not compiled because we are trying to add a string variable to the collection that can store only numeric ones. The compiler will generate an error and point to the line in which 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 the appearance of a runtime exception due to contradictions in the type system will be minimized.

Conclusion


Many aspects of software development on the Java platform are simplified, thanks to the division into a complex Java Virtual Machine and the language itself. However, wide possibilities, like automatic memory management or decent OOP tools, do not exclude the likelihood of problems. The tips here are universal: practice regularly, study the library, read the documentation. And do not forget about static code analyzers, they can indicate the existing bugs and suggest what you should pay attention to.

Source: https://habr.com/ru/post/251365/


All Articles