JavaRush /Java Blog /Random EN /How to Write Methods Efficiently
pena
Level 41
Москва

How to Write Methods Efficiently

Published in the Random EN group
The original article is located at: http://www.javacodegeeks.com/2015/09/how-to-write-methods-efficiently.html#download tutorial Posted by: Andrey Redko in Core Java (Java Core) 18 September 2015 This post is part of our Academy's Advanced Java course. This course is designed to help you use Java more efficiently. It discusses more advanced topics like object creation, parallelization, serialization, reflection, and more. This knowledge will guide you on your journey to the heights of Java mastery.
Course Contents
1. Introduction 2. Method signature 3. Method body 4. Method overloading 5. Method overriding 6. Inlining 7. Recursion 8. Method references 9. Immutability 10. Method documentation 11. Method parameters and return values ​​12. Method as entry point to appendix 13. What's next 14. Downloading the source code
1. Introduction
In this section of the tutorial, we are going to spend some time discussing the different aspects involved in designing and implementing methods in Java. In the previous part of the tutorial, you saw that writing methods in Java is very easy, but there are many things that can make your methods more readable and efficient.
2. Method signatures
As you already know, Java is an object-oriented language. Essentially, every Java method belongs to some part of the class (or the class itself in the case of a static method). It has visibility (or accessibility) rules, can be declared abstract or final, and so on. However, perhaps the most important part of a method is its signature: the return type and arguments, plus a list of implementation exceptions to check for each method that can be thrown (but this part was not used often, and even less often these days). Let's start with a small example. 1 public static void main( String[] args ) { 2 // Some implementation here 3 } The main method only takes an array of strings as args and returns nothing. It could be very nice to keep all methods as simple as main. But in reality, the method signature can become unreadable. Let's take a look at the following example: 1 public void setTitleVisible( int lenght, String title, boolean visible ) { 2 // Some implementation here 3 } The first thing you notice here is that conventions are originally used in Java method names, such as setTitleVisible. The name is well chosen and tries to describe what the method is supposed to do. Second, the argument names tell (or at least hint at) their purpose. It is very important to find correct, meaningful names for method arguments, instead of int i, String s, boolean f (in very rare cases, however, this makes sense). Third, the method has only three arguments. Although Java has a much larger limit on the number of arguments allowed, it is highly recommended not to exceed 6 arguments. Going beyond this limit makes the signature hard to understand. Since Java 5 was released, methods can have a different list of arguments of the same type (called varargs - variable arguments) and use a special syntax, 1 public void find( String … elements ) { 2 // Some implementation here 3 } Internally, the Java compiler converts the variable arguments to an array of the appropriate types, and thus the variable arguments can be accepted to implement the method. Interestingly, Java also allows varargs to be declared using generic type parameters. However, because the type of the argument is not known, the Java compiler wants to be sure that varargs are being used correctly and advises final methods to be annotated with @SafeVarargs annotated (for more information, see Part 5 of the tutorial, How and when to use Enums and Annotations (how and when to use Enums and Annotations). use Enumerations and Comments).For example: 1 @SafeVarargs 2 final public< T > void find( T ... elements ) { 3 // Some implementation here 4 } The other closest way is to use @SuppressWarnings comments, for example 1 @SuppressWarnings( "unchecked" ) 2 public< T > void findSuppressed( T ... elements ) { 3 // Some implementation here 4 } The following example demonstrates the use of exception checking as part of the method signature. In the recent past, exception checking has proven not to be as useful as it was supposed to be, with the result that boilerplate code has been used more for writing than for solving problems. 1 public void write( File file ) throws IOException { 2 // Some implementation here 3 } Last but not least, it is generally recommended (but rarely used) to mark method arguments as final. This will help get rid of the practice of writing bad code when method arguments are intended for different values. In addition, such method arguments can be used by anonymous classes (more on anonymous classes is covered in Part 3 of the tutorial, How to design Classes and Interfaces), although Java 8 eased this limitation a little by introducing efficient final variables. .
3. Method body
Each method has its own implementation and purpose of existence. However, there are a couple of general guidelines that really help to write clear and understandable methods. Probably the most important principle is the Single Responsibility Principle: one should try to implement a method in such a way that each single method does one thing, and does it well. Following this principle can bloat the number of class methods, and it is important to find the right balance. Another important thing in the coding and design process is to keep the implemented methods short. For short methods, it's easy to understand the reason why they're made, plus they usually fit on the screen, and thus can be understood very quickly by the reader of your code. Last (but not least) advice is related to the use of return statements.
4. Method overload
The technique of method overloading is often used to provide specialization of method versions for different types of arguments or combinations of them. Although the method name is the same, the computer chooses the correct alternative by delving into the current argument values ​​at the point of invocation (the best example of overloading is Java constructors: the name is always the same, but the arguments are different) or raises a compiler error if no such method variant is found. For example: 1 public String numberToString( Long number ) { 2 return Long.toString( number ); 3 } 4 5 public String numberToString( BigDecimal number ) { 6 return number.toString(); 7 } Method overloading is somewhat close to generics (more information on generics can be found in part 4 of the How and when to use Generics tutorial), however overloading is used in the case where the generic approach does not work well and every or most argument types that are generic require their own specialized implementations. However, combining both generics and overloading can be very performant, but this is often not possible in Java because the type is erased (more info in Part 4 of the How and when to use Generics tutorial). Let's take a look at an example: 1 public< T extends Number > String numberToString( T number ) { 2 return number.toString(); 3 } 4 5 public String numberToString( BigDecimal number ) { 6 return number.toPlainString(); 7 } Although this piece of code could have been written without the use of generics, this is irrelevant for our demonstration purposes. Interestingly, the numberToString method is overloaded with a special implementation of BigDecimal and the generic version is for all other numbers.
5. Method overriding
We 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 why using the @Override annotation is so important. Our The example will demonstrate the subtle difference between method overriding and method overloading in a simple class hierarchy.The 1 public class Parent { 2 public Object toObject( Number number ) { 3 return number.toString(); 4 } 5 } parent class has only one toObject method.Let's create a subclass of this class and try to come up with a version of the method to convert numbers to strings (instead of raw objects). 1 public class Child extends Parent { 2 @Override 3 public String toObject( Number number ) { 4 return number.toString(); 5 } 6 } However, the signature of the toObject method in the child class is slightly different (see Covariant method return types for more details), and this makes overriding it from the superclass into your class without the Java compiler throwing any errors and warnings. Now, let's add one more method to the child class. 1 public class Child extends Parent { 2 public String toObject( Double number ) { 3 return number.toString(); 4 } 5 } Again, there is only a slight difference in the method signature (Double instead of Number), but the fact that this is an overloaded version of the method in this case does not override the parent method overrides. That is, when a hint from the Java compiler and @Override annotations overlap: the annotated method from the last example with @Override will cause a compiler error.
6. Embedding
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 the method implementation. The compiler's use of JIT heuristics depends on two things - how often the method is currently called, and how large it is. Methods that are too large cannot be efficiently inlined. Inlining can provide a significant performance boost to your code and the advantage of keeping methods short, as we discussed in the Method body section.
7. Recursion
Recursion in Java is a technique where a method calls itself to perform a calculation. For example, let's take a look at the following example, which sums the number of an array: 1 public int sum( int[] numbers ) { 2 if( numbers.length == 0 ) { 3 return 0; 4 } if( numbers.length == 1 ) { 5 return numbers[ 0 ]; 6 } else { 7 return numbers[ 0 ] + sum( Arrays.copyOfRange( numbers, 1, numbers.length ) ); 8 } 9 } This is a very inefficient implementation, but it demonstrates recursion quite well. There is one well-known problem with recursive methods: depending on how deep the call chain goes, they can overflow the stack and throw a StackOverflowError exception. But not everything is as bad as it seems, because there is a technique that can eliminate stack overflow called tail call optimization (call tail optimization). It can be applied if the method is tail recursive (tail recursive methods are methods in which all recursive calls are tail calls). For example, let's rewrite the previous algorithm using tail recursion: 01 public int sum( int initial, int[] numbers ) { 02 if( numbers.length == 0 ) { 03 return initial; 04 } if( numbers.length == 1 ) { 05 return initial + numbers[ 0 ]; 06 } else { 07 return sum( initial + numbers[ 0 ], 08 Arrays.copyOfRange( numbers, 1, numbers.length ) ); 09 } 10 } Unfortunately, the Java compiler (as well as the JVM JIT compiler) does not currently support tail call optimization, but it is still a very useful technique and should be known and taken into account when you are writing recursive algorithms in Java.
8. Method Links
Java 8 takes a huge step forward by introducing functional concepts into the Java language. The foundation that treats methods as data is a notion that was not supported in the language before (however, since Java 7 was released, the JVM and the Java standard library already had some work to do to make this possible). Luckily, with method references, this is now possible. A static method reference: SomeClass::staticMethodName A concrete object instance method reference: someInstance::instanceMethodName An arbitrary object instance method reference of a specific type: SomeType::methodName A constructor reference: SomeClass::new Let's take a look at a small example of how methods can be used as arguments to other methods. 01 public class MethodReference { 02 public static void println( String s ) { 03 System.out.println( s ); 04 } 05 06 public static void main( String[] args ) { 07 final Collection< String > strings = Arrays.asList( "s1", "s2", "s3" ); 08 strings.stream().forEach( MethodReference::println ); 09 } 10 } On the last line, the main method uses a reference to the println method to print each element from the collection of strings to the console, it is passed as an argument to another method, forEach.
9. Immutability
Immutability gets a lot of attention these days, and Java is no exception. It's well known that immutability is hard to achieve in Java, but that doesn't mean it should be ignored. In Java, immutability is all knowledge about changing internal state. As an example, let's take a look at the JavaBeans specification (http://docs.oracle.com/javase/tutorial/javabeans/). It says, very clearly, that setters can change the state of an object, anything before that containing it, and that's what every Java developer expects. However, an alternative approach would be to not change the state, but return a new object (new) each time. It's not as scary as it sounds, and the new Java 8 Date/Time API (developed under the JSR 310: Date and Time API front) is a great example of this. Let's take a look at the following code snippet: 1 final LocalDateTime now = LocalDateTime.now(); 2 final LocalDateTime tomorrow = now.plusHours( 24 ); 3 4 final LocalDateTime midnight = now 5 .withHour( 0 ) 6 .withMinute( 0 ) 7 .withSecond( 0 ) 8 .withNano( 0 ); Each call to a LocalDateTime object that needs to change its state returns a new LocalDateTime instance, and keeps the original unchanged. This is a big shift in the API design paradigm compared to the old Calendar and Date (which, to put it mildly, were not very pleasant to use and caused a lot of headaches).
10. Documentation of the method
In Java, in particular, if you are developing some kind of library or framework, all public methods must be documented using the Javadoc tool (http://www.oracle.com/technetwork/articles/java/index-jsp-135444.html ). Strictly speaking, nothing forces you to do this, but good documentation helps other developers understand what a particular method does, what arguments it requires, what are the assumptions or limitations of its implementation, what types of exceptions it throws and when they occur, what the return value might be. (if any), as well as many other things. Let's take a look at the following example: 01 /** 02 * The method parses the string argument as a signed decimal integer. 03 * The characters in the string must all be decimal digits, except 04 * that the first character may be a minus sign {@code '-'} or plus 05 * sign {@code '+'}. 06 * 07 *

An exception of type {@code NumberFormatException} is thrown if 08 * string is {@code null} or has length of zero. 09 * 10 *

Examples: 11 *

12	 * parse( "0" ) returns 0
13	 * parse( "+42") returns 42
14	 * parse( "-2" ) returns -2
15	 * parse( "string" ) throws a NumberFormatException
16	 * 
17 * 18 * @param str a {@code String} containing the {@code int} representation to be parsed 19 * @return the integer value represented by the string 20 * @exception NumberFormatException if the string does not contain a valid integer value 21 */ 22 public int parse( String str ) throws NumberFormatException { 23 return Integer.parseInt( str ); 24 }
This is rather verbose documentation for a simple method like parse, but it does show a couple of useful features provided by the Javadoc tool, including references to other classes, sample snippets, and advanced formatting. This is how this method documentation is reflected in Eclipse, one of the popular Java IDEs. Just by looking at the image above, any Java developer from junior to senior level can understand the purpose of the method and use it appropriately.
11. Method parameters and return values
Documenting your methods is a great thing, but unfortunately it doesn't prevent cases where a method is called using incorrect or unexpected argument values. Because of this, as a rule, all public methods must validate their arguments and never have to be sure that the correct values ​​will be specified all the time when called (a pattern better known as sanity checks (sanitary check)). Returning to our example from the previous section, the parse method must perform a check on its single argument before doing anything with it: 1 public int parse( String str ) throws NumberFormatException { 2 if( str == null ) { 3 throw new IllegalArgumentException( "String should not be null" ); 4 } 5 6 return Integer.parseInt( str ); 7 } Java has another option for performing sanity checks using assert statements. However, those that may have been disabled at run time may not be executed. Preferably, always perform such checks and throw appropriate exceptions. Even having documented methods and checking their arguments, I want to make a couple more comments related to return values. Before Java 8 came out, the easiest way to say that a method is currently irrelevant to return was to simply return null. This is why Java is so bad at getting a NullPointerException. Java 8 tries to address this issue with the introduction of the Optional<T> class. Let's take a look at this example: 1 public< T > Optional< T > find( String id ) { 2 // Some implementation here 3 } Optional < T > provides many useful methods, and completely eliminates the need to return null in a method and pollute your code with null checks everywhere. The only exception is probably collections. Whenever a method returns a collection, it's always better to return null instead of null (and even Optional<T>), like this: 1 public< T > Collection< T > find( String id ) { 2 return Collections.emptyList(); 3 }
12. Method as an entry point to the application
Whether you're just a developer writing applications in your organization, or a contributor to one of the most popular Java frameworks or libraries, the design decisions you make play a very important role in how your code will be used. While API design best practices are worth several books, this part of the tutorial covers many of them (how methods become the entry point to APIs), so a quick overview is very helpful: • Use meaningful names for methods and their arguments (Method signatures). ) Try to keep the number of arguments to less than 6 (Method signatures section) • Keep your methods short and readable (Method body and Inlining section) • Always document public methods, including preconditions and examples,
13. What's next
This part of the tutorial talks a little less about Java as a language, but more about how to use the Java language effectively, in particular writing methods that are readable, clean, documented, and efficient. In the next section, we'll continue the same basic idea and discuss general programming principles that are meant to help you become a better Java developer.
14. Download source code
This was a lesson about how to write methods efficiently. You can download the source code here:
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION