Core Java

How to write methods efficiently

This article is part of our Academy Course titled Advanced Java.

This course is designed to help you make the most effective use of Java. It discusses advanced topics, including object creation, concurrency, serialization, reflection and many more. It will guide you through your journey to Java mastery! Check it out here!

1. Introduction

In this section of the tutorial we are going to spend some time discussing different aspects related to designing and implementing methods in Java. As we have seen in previous parts of the tutorial, it is very easy to write methods in Java, however there are many things which could make your methods more readable and efficient.

2. Method signatures

As we already know very well, Java is an object-oriented language. As such, every method in Java belongs to some class instance (or a class itself in case of static methods), has visibility (or accessibility) rules, may be declared abstract or final, and so on. However, arguably the most important part of the method is its signature: the return type and arguments, plus the list of checked exceptions which method implementation may throw (but this part is used less and less often nowadays). Here is a small example to start with:

public static void main( String[] args ) {
    // Some implementation here
} 

The method main accepts an array of strings as an only argument args and returns nothing. It would be very nice to keep all methods as simple as main is. But in reallity, a method signature may become unreadable. Let us take a look at the following example:

public void setTitleVisible( int lenght, String title, boolean visible ) {
    // Some implementation here
} 

The first thing to notice here is that by convention, the method names in Java are written in camel case, for example: setTitleVisible. The name is well chosen and tries to describe what the method is supposed to do.

Secondly, the name of each argument says (or at least hints) about its purpose. It is very important to find the right, explanatory names for the method arguments, instead of int i, String s, boolean f (in very rare cases it makes sense however).

Thirdly, the method takes only three arguments. Although Java has a much higher limit for allowed number of arguments, it is highly recommended to keep this number below 6. Beyond this point method signature becomes hard to understand.

Since the Java 5 release, the methods can have variable list of arguments of the same type (called varargs) using a special syntax, for example:

public void find( String ... elements ) {
    // Some implementation here
} 

Internally, the Java compiler converts varargs into an array of the respective type and that is how varargs can be accessed by the method implementation.

Interestingly, Java also allows to declare the varargs argument using generic type parameter. However, because the type of the argument is not known, the Java compiler wants to be sure that the varargs are used responsibly and advice the method to be final and annotated with the @SafeVarargs annotation (more details about annotations are covered in the part 5 of the tutorial, How and when to use Enums and Annotations). For example:

@SafeVarargs
final public< T > void find( T ... elements ) {
    // Some implementation here
} 

The other way around is by using the @SuppressWarnings annotation, for example:

@SuppressWarnings( "unchecked" )
public< T > void findSuppressed( T ... elements ) {
    // Some implementation here
} 

The next example demonstrates the usage of the checked exceptions as part of the method signature. In recent years the checked exception has proved not to be as useful as they intended to be, causing more boilerplate code to be written than problems solved.

public void write( File file ) throws IOException {
    // Some implementation here
} 

Last but not least, it is generally advised (but rarely used) to mark the method arguments as final. It helps to get rid of bad code practice when method arguments are reassigned with different values. Also, such method arguments could be used by anonymous classes (more details about anonymous classes are covered in part 3 of the tutorial, How to design Classes and Interfaces), though Java 8 eased a bit this constraint by introducing effectively final variables.

3. Method body

Every method has its own implementation and purpose to exist. However, there are a couple of general guidelines which really help writing clean and understandable methods.

Probably the most important one is the single responsibility principle: try to implement the methods in such a way, that every single method does just one thing and does it well. Following this principle may blow up the number of class methods, so it is important to find the right balance.

Another important thing while coding and designing is to keep method implementations short (often just by following single responsibility principle you will get it for free). Short methods are easy to reason about, plus they usually fit into one screen so they could be understood by the reader of your code much faster.

The last (but not least) advice is related to using return statements. If a method returns some value, try to minimize the number of places where the return statement is being called (some people go even further and recommend to use just single return statement in all cases). More return statements method has, much harder it becomes to follow its logical flows and modify (or refactore) the implementation.

4. Method overloading

The technique of methods overloading is often used to provide the specialized version of the method for different argument types or combinations. Although the method name stays the same, the compiler picks the right alternative depending on the actual argument values at the invocation point (the best example of overloading is constructors in Java: the name is always the same but the set of arguments is different) or raises a compiler error if none found. For example:

public String numberToString( Long number ) {
    return Long.toString( number );
}

public String numberToString( BigDecimal number ) {
    return number.toString();
} 

Method overloading is somewhat close to generics (more details about generics are covered in part 4 of the tutorial, How and when to use Generics), however it is used in cases where the generic approach does not work well and each (or most) generic type arguments require their own specialized implementations. Nevertheless, combining both generics and overloading could be very powerful, but often not possible in Java, because of type erasure (for more details please refer to part 4 of the tutorial, How and when to use Generics). Let us take a look on this example:

public< T extends Number > String numberToString( T number ) {
    return number.toString();
}

public String numberToString( BigDecimal number ) {
    return number.toPlainString();
} 

Although this code snippet could be written without using generics, it is not importan for our demonstration purposes. The interesting part is that the method numberToString is overloaded with a specialized implementation for BigDecimal and a generic version is provided for all other numbers.

5. Method overriding

We have talked a lot about method overriding in part 3 of the tutorial, How to design Classes and Interfaces. In this section, when we already know about method overloading, we are going to show off why using @Override annotation is so important. Our example will demonstrate the subtle difference between method overriding and overloading in the simple class hierarchy.

public class Parent {
    public Object toObject( Number number ) {
        return number.toString();
    }
} 

The class Parent has just one method toObject. Let us subclass this class and try to come up with the method version to convert numbers to strings (instead of raw objects).

public class Child extends Parent {
    @Override
    public String toObject( Number number ) {
        return number.toString();
    }
} 

Nevertheless the signature of the toObject method in the Child class is a little bit different (please see Covariant method return types for more details), it does override the one from the superclass and Java compiler has no complaints about that. Now, let us add another method to the Child class.

public class Child extends Parent {
    public String toObject( Double number ) {
        return number.toString();
    }
} 

Again, there is just a subtle difference in the method signature (Double instead of Number) but in this case it is an overloaded version of the method, it does not override the parent one. That is when the help from Java compiler and @Override annotations pay off: annotating the method from last example with @Override raises the compiler error.

6. Inlining

Inlining is an optimization performed by the Java JIT (just-in-time) compiler in order to eliminate a particular method call and replace it directly with method implementation. The heuristics JIT compiler uses are depending on both how often a method is being invoked and also on how large it is. Methods that are too large cannot be inlined effectively. Inlining may provide significant performance improvements to your code and is yet another benefit of keeping methods short as we already discussed in the section Method body.

7. Recursion

Recursion in Java is a technique where a method calls itself while performing calculations. For example, let us take a look on the following example which sums the numbers of an array:

public int sum( int[] numbers ) {
    if( numbers.length == 0 ) {
        return 0;
    } if( numbers.length == 1 ) {
        return numbers[ 0 ];
    } else {
        return numbers[ 0 ] + sum( Arrays.copyOfRange( numbers, 1, numbers.length ) );
    }
} 

It is a very ineffective implementation, however it demonstrates recursion well enough. There is one well-known issue with recursive methods: depending how deep the call chain is, they can blow up the stack and cause a StackOverflowError exception. But things are not as bad as they sound because there is a technique which could eliminate stack overflows called tail call optimization. This could be applied if the method is tail-recursive (tail-recursive methods are methods in which all recursive calls are tail calls). For example, let us rewrite the previous algorithm in a tail-recursive manner:

public int sum( int initial, int[] numbers ) {
    if( numbers.length == 0 ) {
        return initial;
    } if( numbers.length == 1 ) {
        return initial + numbers[ 0 ];
    } else {
        return sum( initial + numbers[ 0 ],
            Arrays.copyOfRange( numbers, 1, numbers.length ) );
    }
} 

Unfortunately, at the moment the Java compiler (as well as JVM JIT compiler) does not support tail call optimization but still it is a very useful technique to know about and to consider whenever you are writing the recursive algorithms in Java.


 

8. Method References

Java 8 made a huge step forward by introducing functional concepts into the Java language. The foundation of that is treating the methods as data, a concept which was not supported by the language before (however, since Java 7, the JVM and the Java standard library have already some features to make it possible). Thankfully, with method references, it is now possible.

Type of the referenceExample
Reference to a static methodSomeClass::staticMethodName
Reference to an instance method of a particular objectsomeInstance::instanceMethodName
Reference to an instance method of an arbitrary object of a particular typeSomeType::methodName
Reference to a constructorSomeClass::new

Table 1

Let us take a look on a quick example on how methods could be passed around as arguments to other methods.

public class MethodReference {
    public static void println( String s ) {
        System.out.println( s );
    }

    public static void main( String[] args ) {
        final Collection< String > strings = Arrays.asList( "s1", "s2", "s3" );
        strings.stream().forEach( MethodReference::println );
    }
} 

The last line of the main method uses the reference to println method to print each element from the collection of strings to the console and it is passed as an argument to another method, forEach.

9. Immutability

Immutability is taking a lot of attention these days and Java is not an exception. It is well-known that immutability is hard in Java but this does not mean it should be ignored.

In Java, immutability is all about changing internal state. As an example, let us take a look on the JavaBeans specification (http://docs.oracle.com/javase/tutorial/javabeans/). It states very clearly that setters may modify the state of the containing object and that is what every Java developer expects.

However, the alternative approach would be not to modify the state, but return a new one every time. It is not as scary as it sounds and the new Java 8 Date/Time API (developed under JSR 310: Date and Time API umbrella) is a great example of that. Let us take a look on the following code snippet:

final LocalDateTime now = LocalDateTime.now();
final LocalDateTime tomorrow = now.plusHours( 24 );

final LocalDateTime midnight = now
    .withHour( 0 )
    .withMinute( 0 )
    .withSecond( 0 )
    .withNano( 0 ); 

Every call to the LocalDateTime instance which needs to modify its state returns the new LocalDateTime instance and keeps the original one unchanged. It is a big shift in API design paradigm comparing to old Calendar and Date ones (which mildly speaking were not very pleasant to use and caused a lot of headaches).

10. Method Documentation

In Java, specifically if you are developing some kind of library or framework, all public methods should be documented using the Javadoc tool (http://www.oracle.com/technetwork/articles/java/index-jsp-135444.html). Strictly speaking, nothing enforces you to do that, but good documentation helps other developers to understand what a particular method is doing, what arguments it requires, which assumptions or constraints its implementation has, what types of exceptions and when could be raised and what the return value (if any) could be (plus many more things).

Let us take a look on the following example:

/**
 * The method parses the string argument as a signed decimal integer.
 * The characters in the string must all be decimal digits, except
 * that the first character may be a minus sign {@code '-'} or plus
 * sign {@code '+'}.
 *
 * <p>An exception of type {@code NumberFormatException} is thrown if
 * string is {@code null} or has length of zero.
 *
 * <p>Examples:
 * <blockquote><pre>
 * parse( "0" ) returns 0
 * parse( "+42") returns 42
 * parse( "-2" ) returns -2
 * parse( "string" ) throws a NumberFormatException
 * </pre></blockquote>
 *
 * @param str a {@code String} containing the {@code int} representation to be parsed
 * @return the integer value represented by the string
 * @exception NumberFormatException if the string does not contain a valid integer value
 */
public int parse( String str ) throws NumberFormatException {
    return Integer.parseInt( str );
} 

It is quite a verbose documentation for such a simple method as parse, is but it shows up a couple of useful capabilities the Javadoc tool provides, including references to other classes, sample snippets and advanced formatting. Here is how this method documentation is reflected by Eclipse, one of the popular Java IDEs.

6.Javadoc.Eclipse

Just by looking on the image above, any Java developer from junior to senior level can understand the purpose of the method and the proper way to use it.

11. Method Parameters and Return Values

Documenting your methods is a great thing, but unfortunately it does not prevent the use cases when a method is being called using incorrect or unexpected argument values. Because of that, as a rule of thumb all public methods should validate their arguments and should never believe that they are going to be specified with the correct values all the time (the pattern better known as sanity checks).

Returning back to our example from the previous section, the method parse should perform the validation of its only argument before doing anything with it:

public int parse( String str ) throws NumberFormatException {
    if( str == null ) {
        throw new IllegalArgumentException( "String should not be null" );
    }

    return Integer.parseInt( str );
} 

Java has another option to perform validation and sanity checks using the assert statements. However, those could be turned off at runtime and may not be executed. It is preferred to always perform such checks and raise the relevant exceptions.

Even having the methods documented and validating their arguments, there are still couple of notes to mention related to the values they could return. Before Java 8, the simplest way for a method to say “I have no value to return at this time” was just by returning null. That is why Java is so infamous for NullPointerException exceptions. Java 8 tries to address this issue with introduction of Optional < T > class. Let us take a look on this example:

public< T > Optional< T > find( String id ) {
    // Some implementation here
} 

Optional < T > provides a lot of useful methods and completely eliminates the need for the method to return null and pollute your code with null checks everywhere. The only exception probably is collections. Whenever method returns the collection, it is always better to return the empty one instead of null (and even Optional < T >), for example:

public&lt; T &gt; Collection&lt; T &gt; find( String id ) {
return Collections.emptyList();
} 

12. Methods as API entry points

Even if you are just a developer building applications within your organization or a contributor to one of the popular Java framework or library, the design decisions you are taking play a very important role in a way how your code is going to be used.

While the API design guidelines are worth of several books, this part of the tutorial touches many of them (as methods become the API entry points) so a quick summary would be very helpful:

13. What’s next

This part of the tutorial was talking a bit less about Java as a language, but more about how to use Java as a language effectively, in particular by writing readable, clean, documented and effective methods. In the next section we are going to continue with the same basic idea and discuss general programming guidelines which are intended to help you to be better Java developer.

14. Download the Source Code

This was a lesson on How to write methods efficiently. You may download the source code here: advanced-java-part-6

Andrey Redko

Andriy is a well-grounded software developer with more then 12 years of practical experience using Java/EE, C#/.NET, C++, Groovy, Ruby, functional programming (Scala), databases (MySQL, PostgreSQL, Oracle) and NoSQL solutions (MongoDB, Redis).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button