Brain friendly programming in Java

We have many principles for writing better more maintainable software, but still end up with a tangled mess.

Most of us have heard about the Single Responsibility Principle (SRP), Robert C Martin expressed this principle as “A class should have only one reason to change”.

He later clarified this by stating “Gather together the things that change for the same reasons. Separate those things that change for different reasons”.

We also have the Single Layer of Abstraction Principle (SLAP) which is expressed as “Don’t mix levels of abstractions”.

Dan Terhorst-North presented his Unix Philosophy as “does one thing well”.

The goal is to create more maintainable software, not to fulfill the principle. It’s still possible to create a mess even though we follow all the SOLID principles. So what do these principles mean, and how can I as a developer use them to create better software?

The Science Behind Brain-Friendly Programming Methods

In “The Programmer’s Brain“, Dr. Felienne Hermans talks about how our brain works working with code.  It’s a highly recommended read.

One of the key takeaways for me was the fact that our working memory can only keep track of 3 – 5 chunks of information at the same time. How big a chunk is depends on how much experience we have with the current task.

It can be individual statements, lines, or entire design patterns.

We can make our code easier to understand, to lower the cognitive load of the reader, by limiting the number of chunks of information we have to keep track of.

Ok, so how do we do that then?

Cognitive Load: Minimizing Mental Strain for Optimal Programming Performance

This is when the presentation “Simple Made Easy” from Rich Hickey, comes in.

He talks about how we complect our code.

Complect is an old word that means to braid together, to weave together. By completing our code we make it harder to understand by requiring the reader to keep more chunks of information in memory. We are increasing the cognitive load for the reader.

He suggests that we should separate our code based on the following questions:

  • What needs to be done?
  • Who wants to do it?
  • How should it be done?
  • When should it be done?
  • Where should it be done?
  • Why should it be done?

By mixing these, complecting them, we create code that is harder to understand because it requires the reader to keep more chunks in memory, which creates a higher cognitive load.

By keeping these separate, we get code that is easier to change and reuse, for instance, we can then change how something is done without affecting who wants it done.

Another thing we can do to make our code easier to understand is to take advantage of new features in the language we are using.

We often hear that we should update to the latest version of our language for performance and security reasons. But there are also improvements in the language that can make our code easier to understand.

Let’s take Java as an example.

Leveraging New Java Language Features to Lower the Cognitive Load of the Programmer

Java, being one of the most popular programming languages in the world, is constantly evolving to meet the changing demands of software development. With each iteration, Java introduces new language features that not only enhance the performance and scalability of applications but also aim to make the lives of programmers easier.

I’ll list a few of the new Java language features that, when used, can lower the cognitive load of programmers, allowing us to write more efficient and maintainable code.

Concise Code with Lambda Expressions

Lambda expressions, introduced in Java 8, allow developers to write more concise code by expressing instances of anonymous functions. By using lambda expressions, programmers can reduce boilerplate code and focus on the core logic of their applications. This feature is a good match for handling collections, stream processing, and event handling. For example, instead of writing lengthy loops to iterate over a list, lambda expressions can be used to achieve the same result in a more compact and readable manner.

Improved Null Safety with Optional

Java 8 introduced the Optional class, which helps handle null values more effectively. By using Optional, programmers can avoid NullPointerExceptions and write safer code. Optional encourages developers to handle null values explicitly, leading to more robust and error-free applications. This feature reduces the cognitive load of programmers by providing a clear and standardized way to deal with null values, making the code more predictable and easier to reason about.

Enhanced Pattern Matching

Java 14 introduced pattern matching for instanceof and this has been extended and improved on in newer versions since then. This feature simplifies the code by combining type checking and type casting into a single operation. Programmers can write cleaner and more concise code when working with complex data structures by using pattern matching. Pattern matching reduces the cognitive load of programmers by eliminating the need for if-else blocks and switch statements, leading to more readable and maintainable code.

Simplified Asynchronous Programming with CompletableFuture

Asynchronous programming is essential for building responsive and scalable applications. Java 8 introduced the CompletableFuture class, which makes asynchronous programming simpler by providing a convenient way to work with future values. Programmers can chain asynchronous operations and handle their results more intuitively using CompletableFuture. This feature reduces the cognitive load of developers by abstracting the complexity of asynchronous programming and allowing them to focus on the business logic of their applications.

The evolution of Java language features has brought significant improvements to the way programmers write and maintain code. By leveraging features such as lambda expressions, Optional, pattern matching, and CompletableFuture, developers can lower the cognitive load associated with programming tasks. These features enable programmers to write more concise, robust, and maintainable code, ultimately leading to increased productivity and efficiency in software development. 

As Java continues to evolve, developers can expect more innovations to enhance their programming experience further and make Java an even more powerful and developer-friendly language.

Conclusion

By understanding how the brain processes information, developers can create programming languages and environments that are more intuitive, efficient, and less cognitively demanding for users. This approach can improve the overall user experience, increase productivity, and promote greater accessibility for individuals of all cognitive abilities.

Embracing brain-friendly programming will not only benefit individual programmers and users but has the potential to revolutionize the way we interact with technology and shape the future of computing.

5 Smart Ways to Use java.util.Objects

Objects provide a set of utility methods to manipulate Java objects. In this post, we will delve deep into the world of java.util.Objects and examine five smart ways to use it. From comparison, null-checking to hashing, there is much to learn, so buckle up and let’s dive in!

Introduction

The java.util.Objects class was added in Java 7 to provide utility methods for working with objects. Its purpose is to simplify common tasks such as null-checking and equality testing and to reduce the amount of boilerplate code that developers need to write.

Prior to Java 7, developers had to write their own null-checking code using if statements, which could be error-prone and time-consuming. The java.util.Objects class provides a set of static methods that handle null values in a consistent and reliable way, making it easier for developers to write robust and maintainable code.

Checking for Null objects with requireNonNull

When working with objects in Java, it is important to ensure that they are not null to avoid unexpected NullPointerExceptions. To address this, Objects contains a method, requireNonNull(), which helps check for null objects before they are used. This gives the programmer control of when to check for null objects instead of having the code blow up when the object is used.

public int doSometing(String parameter) {
   // do lots of stuff here
   return parameter.length();
}

If the method doSomething is called with a null value it will throw a NullPointerException, Cannot invoke “String.length()” because “parameter” is null. In this case, we can clearly see the problem in the code but if the method is more complicated it might not be so easy. A good programming practice is to check all parameters for validity before using them. Objects give us not one but three methods to do just that.

public int doSometing(String parameter) {
   Objects.requireNonNull(parameter);
   // do lots of stuff here
   return parameter.length();
}

This will also throw a NullPointerException. The difference, in this case, is that it will be thrown by the requireNonNull call at the start of the method. If we need to provide more information in the exception we can use one of the other two variants of this method.

Objects.requireNonNull(parameter, "Additional information");
Objects.requireNonNull(parameter, () -> "Additional information");

Both of these will use the provided message in the generated exception. The difference is that the second variant will only generate the message if the given object is null.

Providing Default Values with requireNonNullElse

The requireNonNullElse method is a convenient way to provide default values for null objects. It was introduced in Java 9 and helps reduce NullPointeExceptions by providing a value to use when a null is received, avoiding the need for the developer to explicitly check for null before using the object.

It reduces the risk of NullPointerExceptions and helps to make our code more concise and readable. By providing default values for null objects, it simplifies our code and makes it more robust. As such, it is a valuable addition to our programming arsenal and should be used whenever appropriate.

public int doSometing(String parameter) {
   String localParam = Objects.requireNonNullElse(parameter, "");
   // do lots of stuff here
   return localParam.length();
}

In addition to the basic usage described above, the method has other features that make it more flexible. For example, it works with any type of object and can handle complex objects.

To optimize performance, the alternative variant of this method, requireNonNullElseGet, can be utilized when obtaining the default value involves a resource-intensive operation. This method takes a Supplier instead of an object as the second parameter and we can use it to call a method to get the default value. The Supplier will only be called if the provided parameter is null.

public int doSometing(String parameter) {
   String localParam = Objects.requireNonNullElseGet(parameter, this::getDefaultValue);
   // do lots of stuff here
   return localParam.length();
}

private String getDefaultValue() {
   return "";
}

Comparing Objects with Objects.equals()

Objects.equals method was added to the Java language to provide a null-safe comparison of objects, and to address the limitations of the Object.equals() method. It makes it easier and more convenient to compare objects and is a useful addition to the language.

Before Java 7 and the addition of Objects.equals we had to construct something like the following example to make a null-safe comparison between two objects. It contains lots of details and boilerplate code.

private boolean isEqual(Object s1, Object s2) {
   if (s1 == null && s2 == null) {
      return true;
   }

   if (s1 == null || s2 == null) {
      return false;
   }

   return s1.equals(s2);
}

By using Objects.equals we can instead implement this method like the example below. The Objects.equals method will take care of the null checks and will only call the equals method on our object if both s1 and s2 are non-null. Not only will this remove the noise of checking for null, but it will also protect the equals method in our object from being called with a parameter that is null.

private boolean isEqual(Object s1, Object s2) {
   return Objects.equals(s1, s2);
}

Retrieving HashCodes with Objects.hashCode()

In this example, the hashCode() method is implemented using Objects.hashCode() to calculate the hash code based on the id and name fields. By passing the fields as arguments to Objects.hashCode(), it will internally handle null values and produce a consistent hash code.

import java.util.Objects;

public class MyClass {
    private int id;
    private String name;

    // Constructor, getters, and other methods

    @Override
    public int hashCode() {
        return Objects.hashCode(id, name);
    }

    // we have to implement a equals method, but it have been left out for brevity
 
}

It’s important to ensure that the fields used in hashCode() are the same fields considered in the equals() method to maintain the general contract between these two methods.

By implementing hashCode() in this manner, you can generate a hash code that takes into account the relevant fields in your class, simplifying the process and ensuring consistency.

Creating Custom Comparison Strategies with Objects.compare()

You can use the Object.compare() method if you want to compare two instances of an object that doesn’t implement a comparator or if you want to use a custom comparison strategy. It will return 0 if both instances are equal. Otherwise, it will return the result of the passed comparator. This means that it will return 0 if both instances are null. You might still get a NullPointerException, depending on the implementation of the comparator.

Integer a = 42;
Integer b = 7;
Objects.compare(a, b, Integer::compareTo);  // will return 1

By supplying a custom comparator to this method, we can create a custon comparison strategy.

Integer a = 42;
Integer b = 7;
Objects.compare(a, b, this::myCustomComparator);

private int myCustomComparator(Integer val1, Integer val2) {
   int result = 0;
   // implementation of custom comparison
   return result;
}

Conclusion: Embracing the Flexibility of java.util.Objects

In conclusion, java.util.Objects provide a wide range of useful methods that can simplify the coding process. By taking advantage of these methods, developers can write cleaner, more efficient code and reduce the likelihood of runtime errors.

Whether you’re a seasoned Java developer or just getting started with the language, understanding how to use java.util.Objects effectively is an important skill to have. By incorporating these methods into your codebase, you can improve code quality and productivity while reducing the risk of errors and bugs in your application.