Advanced Features of Functions
Functions in Java offer some advanced features which come in quite handy while doing programming. These include functions overloading, defining functions that receive a variable number of arguments and recursive functions. This chapter is devoted to discussing these advanced features. Let us begin with function overloading.
Function Overloading
With the facility of function overloading we can have multiple functions in a class with the same name. For example, if we wish to write functions that return the absolute value of a numeric argument we can consider writing a separate function for each numeric data type one that returns absolute value of an int, another which returns absolute value of a long and yet another which returns absolute value of a double.
Since all these functions basically do the same thing, it seems unnecessary to have three different function names. Java overcomes this situation by allowing the programmer to create three different functions with the same name. These functions are called overloaded functions. The following program illustrates how to implement them:
// Overloading functions to do different but similar jobs
package functionoverloadingproject ;
public class FunctionOverloadingProject
{
public static void main ( String[ ] args )
{
int i = -25, j ;
long l = -100000, m ;
double d = -12.34, e ;
j = abs ( i ) ;
m = abs ( l ) ;
e = abs ( d ) ;
System.out.println ( "j = " + j + " m = " + m + " e = "+ e ) ;
}
static int abs ( int ii )
{
return ( ii > 0 ? ii : ii * -1 ) ;
}
static long abs ( long ll )
{
return ( ll > 0 ? ll : ll * -1 ) ;
}
static double abs ( double dd )
{
return ( dd > 0 ? dd : dd * -1 ) ;
}
}
The output of the program would be:
j = 25 m = 100000 e = 12.34
How does the Java compiler know which of the abs( )s should be called when a call is made? It decides from the type of the argument being passed during the function call. For example, if an int is being passed the integer version of abs( ) gets called, if a double is being passed then the double version of abs( ) would agree.
Overloaded functions must at least differ in the type, number or order of parameters they accept. Just having different return types is not enough to differentiate them.
It’s a bad programming idea to create overloaded functions that perform different types of actions; functions with the same name should have the same general purpose. For example, if we write an abs( ) function that returns the square root of a number, it would be both silly and confusing. We must use overloaded functions judiciously. Their purpose is to provide a common name for several similar but slightly divergent functions. Overusing overloaded functions can make a program unreadable.
Functions with Variable Number of Arguments
The functions that we have used so far used to receive a fixed number of arguments. These include the following functions:
System.out.println ( “a = ” + a ) ;
y = Math.pow ( 2.0, 5.0 ) ;
s = calSum ( a, b, c ) ;
Java also permits functions to receive variable number of arguments.
For example,
System.out.printf ( “%d %d”, a, b ) ;
System.out.printf ( “%d %d %f %f”, a, b, c, d ) ;
Here, in the first call to printf( ) we are passing 3 arguments, whereas, in the second call we are passing 5 arguments.
We can even define functions that can receive a variable number of arguments. The following program illustrates this:
package varargsproject ;
public class VarArgsProject
{
public static void main ( String args[ ] )
{
double d1 = 10.0 ; double d2 = 20.0 ;
double d3 = 30.0 ; double d4 = 40.0 ;
System.out.printf ( "Avg = %f\n", average ( d1, d2 ) ) ;
System.out.printf ( "Avg = %f\n", average ( d1, d2, d3 ) ) ;
System.out.printf ( "Avg = %f\n", average ( d1, d2, d3, d4 ) ) ;
}
static double average ( double... numbers )
{
double total = 0.0 ;
for ( double d : numbers )
total = total + d ;
double avg ;
avg = total / numbers.length ;
return avg ;
}
}
Here we are passing a different number of arguments to average( ) function in each call. To make average( ) capable of receiving such a variable number of arguments, a special syntax is used while defining its parameters.
static double average ( double… numbers )
{
..
}
Here double is followed by ellipsis ( aning that, this function is going to receive a different number of doubles in each call. numbers are treated as an array. So, while finding the average value, we need to iterate through the array elements. To do this, a special for loop syntax has been evolved.
Each time through this for loop, d takes the next value from the numbers array. The number of values present in the array can be obtained using the length property of the array. This aspect of the array would be discussed in detail in Chapter 12.
Sometimes we might be required to define a function that receives fixed arguments as well as variable arguments. While defining such a function we have to ensure that the fixed arguments precede the variable arguments while making the call to such functions.
Recursion
Recursion is a powerful concept in programming where a function calls itself to solve a problem. The key idea is that the problem is broken down into smaller sub-problems that are simpler to solve. In the case of calculating a factorial, recursion is used to express the problem in terms of itself.
Factorial Example Using Recursion
Let’s break down the factorial example you provided in Java.
The factorial of a number nnn (denoted as n!n!n!) is defined as:n!=n×(n−1)!n! = n \times (n-1)!n!=n×(n−1)!
For example, for 4!4!4!:4!=4×3!=4×(3×2!)=4×(3×(2×1!))=4×3×2×1=244! = 4 \times 3! = 4 \times (3 \times 2!) = 4 \times (3 \times (2 \times 1!)) = 4 \times 3 \times 2 \times 1 = 244!=4×3!=4×(3×2!)=4×(3×(2×1!))=4×3×2×1=24
Recursive Function for Factorial
Let’s go over the code:
package recursivefactorialproject;
import java.util.*;
public class Main {
public static void main(String[] args) {
int a, fact;
Scanner scn;
scn = new Scanner(System.in);
System.out.println("Enter any number ");
a = scn.nextInt();
fact = rec(a);
System.out.println("Factorial value = " + fact);
}
static int rec(int x) {
int f;
if (x == 1)
return 1; // Base case: 1! = 1
else
f = x * rec(x - 1); // Recursive case: n! = n * (n-1)!
return f;
}
}
How the Recursive Function Works
- Initial Call:
- When the user inputs a number (say
3
), the main method callsrec(3)
.
- When the user inputs a number (say
- First Call to rec(3):
- rec(3)rec(3)rec(3) checks if xxx equals
1
. Since x=3x = 3x=3, theif
condition is false. So, it computes 3×rec(2)3 \times rec(2)3×rec(2).
- rec(3)rec(3)rec(3) checks if xxx equals
- Second Call to rec(2):
- rec(2)rec(2)rec(2) also checks if xxx equals
1
. Since x=2x = 2x=2, it computes 2×rec(1)2 \times rec(1)2×rec(1).
- rec(2)rec(2)rec(2) also checks if xxx equals
- Third Call to rec(1):
- rec(1)rec(1)rec(1) reaches the base case. Since x=1x = 1x=1, it returns
1
.
- rec(1)rec(1)rec(1) reaches the base case. Since x=1x = 1x=1, it returns
- Returning the Results:
- Now the recursion starts unwinding:
- rec(1)rec(1)rec(1) returns
1
. - rec(2)rec(2)rec(2) computes 2×1=22 \times 1 = 22×1=2 and returns
2
. - rec(3)rec(3)rec(3) computes 3×2=63 \times 2 = 63×2=6 and returns
6
.
- rec(1)rec(1)rec(1) returns
- Now the recursion starts unwinding:
- Final Output:
- The final result is printed as
Factorial value = 6
.
- The final result is printed as
Visualizing the Recursive Calls
Here’s how the recursion unfolds step by step when rec(3)rec(3)rec(3) is called:
rec(3) → 3 * rec(2)
rec(2) → 2 * rec(1)
rec(1) → 1 (base case)
- The recursive calls are stacked like a chain of function calls.
- When the base case rec(1)rec(1)rec(1) returns, the previous calls start returning values until we reach the first call.
Important Concepts:
- Base Case: This is the condition that terminates the recursion. Without a base case, the function would continue calling itself indefinitely, leading to a stack overflow. In this example, the base case is when x=1x = 1x=1.
- Recursive Case: This is the part of the function that reduces the problem size and calls the function again. In the factorial example, the recursive case is f=x×rec(x−1)f = x \times rec(x – 1)f=x×rec(x−1).
Advantages and Limitations:
- Advantages:
- Recursive solutions are often simpler and easier to write, especially for problems like factorial, Fibonacci numbers, and tree traversals.
- Limitations:
- Recursive calls consume memory on the stack, and too many recursive calls can result in a stack overflow.
- Recursion can sometimes be less efficient than iterative solutions for certain problems, due to the overhead of function calls and maintaining the call stack.
Recursion Visualization (in terms of Stack):
Here’s a simple way to visualize the flow of recursion:
rec(3) → 3 * rec(2) → 3 * (2 * rec(1)) → 3 * (2 * 1)
As you can see, each function waits for the result of the next call, and once the base case is reached, the results are combined and returned back through the chain.
Understanding recursion can take some time, but once grasped, it’s an elegant solution to many problems.