Five common mistakes Java programmers make

  • 2020-04-01 04:04:12
  • OfStack

The following is a text description of each error combined with a detailed explanation of the code to show you, the specific content is as follows:


1. Overuse of Null

It is a best practice to avoid overusing null values. For example, it is better to have methods return an empty array or collection instead of a null value, because this prevents the program from throwing nullpointerexceptions. The following code snippet gets a collection from another method:


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


When a person does not have an account, getAccountIds() returns a null value and the program throws a NullPointerException. So you need to add a null check to solve this problem. If you replace the returned null value with an empty list, then NullPointerException will not occur. Also, because we no longer need to empty check the variable accountId, the code will be more concise.

When you want to avoid null values, different scenarios may take different approaches. One way to do this is to use the Optional type, which can be either an empty object or a package of values.
 


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

In fact, Java8 offers a much cleaner approach:
 


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

Java has supported Optional types since the Java8 release, but it is well known in the world of functional programming. Prior to this, it had been used in early versions of Google Guava for Java.

2. Ignore exceptions

We often ignore anomalies. However, for beginners and experienced Java programmers, the best practice is still to work with them. Exceptions are usually thrown with a purpose, so in most cases you need to record the event that caused the exception. Don't take this too lightly. If necessary, you can rethrow it, display the error message to the user in a dialog box, or log the error message. At the very least, you should explain why you didn't handle the exception in order to let other developers know what happened.
 


selfie = person.shootASelfie(); 
try { 
 selfie.show();
} catch (NullPointerException e) {
 // Maybe, invisible man. Who cares, anyway?
}

An easy way to emphasize that an exception is not important is to use this information as the variable name of the exception, like this:
 


try { selfie.delete(); } catch (NullPointerException unimportant) {  }
 
3. Concurrent modification exception

This exception occurs when the collection object is modified without using the methods provided by the iterator object to update the contents of the collection. For example, here is a hats list and you want to delete all the values containing the ear flaps:
 


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);
 }
}

If you run this code, will be throw ConcurrentModificationException, because the code in the traversal of the collection at the same time to be modified. The same exception may occur when multiple processes work on the same list, and while one process iterates through the list, another process tries to modify the contents of the list.

Concurrent changes to the contents of a collection are common in multiple threads, so you need to use the methods commonly used in concurrent programming, such as synchronous locking, special collections for concurrent changes, and so on. There is a slight difference between single-threaded and multi-threaded situations in which Java solves this problem.

Collect objects and delete them in another loop

The immediate solution is to put hats with ear flaps into a list and then remove it with another loop. However, this requires an additional collection to hold the hats that will be removed.
 


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 method is simpler and does not require the creation of additional collections:
 


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

Methods using ListIterator

List iterator is a good choice when the collection you want to modify implements the List interface. Iterator, which implements the ListIterator interface, supports not only delete operations, but also add and set operations. The ListIterator interface implements the Iterator interface, so this example looks a lot like the remove method of Iterator. The only difference is the type of hat iterator and how we get iterator -- using the listIterator() method. The following snippet shows how to use   The listiterator. remove and listiterator. add methods replace the hat with ear flaps with sombreros.


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

Using ListIterator, the call to remove and add methods can be replaced by the call to just one set method:
 


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
 }
}

Use the stream method in Java 8

In Java8, developers can convert a collection to a stream and filter the stream based on some criteria. This example describes how stream API filtration hats and avoid ConcurrentModificationException. The hats = hats. Stream (). The filter ((hat - > ! Hat. HasEarFlaps ()))


 .collect(Collectors.toCollection(ArrayList::new));
The Collectors. ToCollection method will create a new ArrayList that will hold the filtered hats values. If the filter condition filters out a large number of entries, a large ArrayList will be generated. Therefore, it should be used with caution.

Use the list.removeif method in Java 8

You can use another more concise method in Java 8 -- the removeIf method:
 


 hats.removeIf(IHat::hasEarFlaps);

At the bottom, it USES iterator.remove to do this.

Use special collections

If you decide to use CopyOnWriteArrayList instead of ArrayList in the first place, you won't have a problem. Because   CopyOnWriteArrayList provides methods to modify (for example, set, add, remove), instead of changing the original array of collections, it creates a new modified version. This allows changes to be made while traversing the collection of the original version, thus not throwing   ConcurrentModificationException anomalies. The disadvantage of this collection is also obvious - a new collection is generated for each change.

There are other collections for different scenarios, such as CopyOnWriteSet and ConcurrentHashMap.

Another error that might occur when you concurrently modify a collection is to create a stream from a collection and modify the collection on the back end as you walk through the stream. A general rule for streams is to avoid modifying the collection on the back end when querying the stream. The following example shows how to properly handle a stream:
 


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

The peek method collects all the elements and performs a given action on each one. In this case, the action is an attempt to delete data from an underlying list, which is obviously wrong. To avoid this, try some of the methods described above.

4. The default

Sometimes, code provided by a standard library or a third party must adhere to common dependency guidelines for better collaboration. For example, the common convention of hashCode and equals must be followed to ensure that a set of collection classes and other classes using hashCode and equals methods in the Java collections framework work. Non-compliance does not cause exceptions or break code compilation; It is insidious because it can change application behavior at any time without warning of danger.

Bad code can sneak into a production environment and cause a lot of bad effects. This includes poor UI experience, bad data reporting, poor application performance, data loss, or more. Fortunately, these catastrophic mistakes don't happen very often. The hashCode and equals conventions have been mentioned before, and the scenario in which they occur is that collections depend on hashing or comparing objects, like HashMap and HashSet. In short, this convention has two principles:

If two objects are equal, then the hash code must be equal.
If two objects have the same hash code, they may or may not be equal.
Breaking the first rule of the convention will cause an error when you try to retrieve data from a hashmap. The second rule means that objects with the same hash code are not necessarily equal.

Here are the consequences of breaking the first rule:


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 overrides the equals and hashCode methods. However, it breaks the convention because hashCode returns a random value for the same object every time it is called. The following code probably won't find a boat named Enterprise in the hashset, despite the fact that we added this type of boat ahead of time:
 


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' : %bn", boats.contains(new Boat("Enterprise")));
}

Another example of a convention is the finalize method. Here is a reference to its functional description in the official Java documentation:

The general convention of finalize is that when the JavaTM virtual machine determines that no thread can access a specified object in any way, the method is called, and thereafter the object can only be treated as a result of an action when some other (intended to terminate) object or class terminates. The finalize method has several capabilities, including making this object available again to other threads; However, the primary purpose of a finalize is to perform a cleanup operation before an object is irrevocably discarded. For example, the finalize method representing the input/output connection object performs an explicit I/O transaction to interrupt the connection before the object is permanently discarded.

You can decide to use the finalize method to free up resources in, for example, a file handler, but this usage is bad. Because it is called during garbage collection, and the GC time is uncertain, the time at which finalize will be called is not guaranteed.

5. Use primitive types instead of parameterized ones

According to the Java documentation, primitive types are either non-parameterized or non-static members of class R (and also non-inherited R parent classes or parent interfaces). Before Java generics were introduced, there were no alternative types to the original types. Java has supported generic programming since version 1.5, which is undoubtedly an important feature improvement. However, for reasons of backward compatibility, there is a trap that can corrupt the entire type system. Consider the following:
 


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

This is a list of Numbers defined as the original ArrayList. Since it does not specify a type parameter, you can add any object to it. But the last line maps the element it contains to an int and multiplys it by 2, printing the doubled data to standard output.

This code compiles without errors, but throws a runtime error once it is run, because there is an attempt to map the character type to an int. Obviously, if the necessary information is hidden, the type system will not be able to help write secure code.

To solve this problem, you need to specify a specific type for the object stored in the collection:


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

listOfNumbers.add(10); 
listOfNumbers.add("Twenty");

listOfNumbers.forEach(n -> System.out.println((int) n * 2)); 

The only difference from the previous code is the line that defines the collection:


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

The modified code compilation is impossible to pass, because you are trying to add a string to a collection that only expects to store integers. The compiler will display an error message pointing to the line that tried to add Twenty characters to the list. Parameterized generic types are a good idea. This way, the compiler can check all possible types, and the chance of run-time exceptions due to type inconsistencies is greatly reduced.

The main summary of the above five Java programmers often make mistakes, I hope you can enjoy.


Related articles: