Saturday, June 5, 2021

Java Tutorial: Methods

Chapters

Java Methods

Method(or method definition) is a block element in java that groups a collection of statements to perform a specific task. Java offers a myriad of pre-defined methods some of them are the print(),println(),etc. Although, we can't always rely on pre-defined methods, thus we need to create our own method to suit our needs.
General Form:
access-modifier return-type method-name(parameters)
{//code to execute}

Example:

public class SampleClass{

  public void displayMessage(String txt1){
    System.out.println("The message is: " + txt1);
  }
  
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   sc.displayMessage("I like programming!");
 }
}

Result:
The message is: I like programming!
Method definition must be a member of a class or interface. Method definition can only be nested in class or interface block. Now let's examine the form of java's method definition.

access-modifier: This part specifies the method's accessibility to other classes. There are different types of access modifiers and we're gonna stick with the "public" modifier for now. "public" modifier grants access to all classes that are part of our project.

return-type: Methods can return a value based on the type of this "return-type" once the program encounters the "return" keyword. In the example above the return type is void. void return type means that the method doesn't return any value.We will learn more about return types later.

method-name name of the method. Refer to this link:Java Naming Convention to folow naming convention.

parameters: These are variables that hold the imported values from the outside part of the method call. You can put multiple parameters in the parentheses by using comma to separate them.

Method Call

Methods have two forms: Method Definition and Method Call. We know method definition, let's discuss method call.
General Form: class-reference.method-name(arguments);

class-reference: Reference of a class that owns a method. In the method example above, we call displayMessage() method in the SampleClass class like this:
sc.displayMessage("I like programming!");
"sc" is the variable name that holds the reference of the instance of SampleClass. If a class is static and we wanna call one of its methods, we call it like this:
MyStaticClass.displayMessage("Message");
There are multiple ways to call a method like System.out.println() but for now, let's stick with the class instance.

method-name: name of the method that is defined in the method definition. In the method example above, I defined a method and named it "displayMessage". Then, I used its name as part of the method call in order to specifically call that method.

arguments: values that we wanna include when the program starts processing the method call. Number,type and order of arguments must match the number,type and order of parameters. In the method example above, displayMessage() has one parameter which is a String type. When I called displayMessage() I only put one argument which is also a String type.

Methods can be called in method definition block, constructor block, etc. We can't directly call methods in class or interface blocks.
Example 

public class SampleClass{

 public void myMethod(){
   System.out.println("My Method");
 }
 
 //error
 myMethod();
}

Return type and The return Keyword

Methods can return any value of any type. There's one special kind of return type and that is the "void" keyword. void is a special return type that returns nothing. Normally, method definition requires the return keyword.
In the example in chapter 1, we didn't put any return keyword in the method definition that's because we use void as the return type. Method definition with a "return-type" except for void needs to use the return keyword.

Though, we can still use the return keyword even a method definition has void return type.
Example:

public class SampleClass{

  public void displayMessage(String txt1){
    if(txt1.equals("bad")){
       System.out.println("Message can't be displayed.");
       return;
    }
    System.out.println("The message is: " + txt1);
  }
  
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   sc.displayMessage("bad");
   System.out.println("Done");
 }
}

Result:
Message can't be displayed.
Done
Remove the return keyword and the result is going to be:
Message can't be displayed.
The message is: bad
Done

return keyword makes the program to exit out of the method call and return the execution to the method caller. Lets review the example above. In most java application, program execution starts at the main method, once the main method encounters the method call, main method will give the execution to the method call.

Then, the execution encounters the return keyword in the method call, thus the execution exits out of the method call and return to the main method which is the method where displayMessage() is called. Then, the program will continue to execute codes next to the method call in the main method.

Now, let's try a different return type
Example:

public class SampleClass{

  public String removeSpaces(String txt1){
    return txt1.replace(" ","");
  }
  
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   String noSpaceTxt = sc.removeSpaces("I do not need space");
   System.out.println(noSpaceTxt);
 }
}

Result:
Idonotneedspace
In the example above, the return keyword has a value next to it which is a method that returns a String value. replace is a method in the String class. The first argument is the character to be replaced and the second argument is the character that replaces the character in the first argument.

So, let's breakdown the example above. First, The execution starts at the main() method then SampleClass is instantiated, then the program assigns a value to noSpaceText but the value that is to be assigned is a value that is to be returned by removeSpaces() method.

So, the main() method gives the execution to removeSpaces() then the execution encounters the return keyword and ready to return to the main() but before the execution returns to main() it needs to bring a value and that value is the value that is to be returned by the replace() method.

So, removeSpaces() gives the execution to replace().Once the execution is done with replace()(or it encounters the return keyword) it returns back to removeSpaces() with the value it needed and once the execution is done with removeSpaces() it returns back to the main() with the value it needed and then that value is assigned to noSpaceTxt then noSpaceTxt passes a copy of its value to println() and println() displays the value on the command prompt(or terminal).

As you can see, program execution took a long journey just to print a message on the command prompt(or terminal). I didn't breakdown its full journey in the println() and replace() method.

If the example above confuses you, let me give you an easier one.
Example:

public class SampleClass{

  public String goodOrBad(String txt1){
    String result = null;
    
    switch(txt1){
    
     case "good":
     result = "The word is good.";
     break;
    
     case "bad":
     result = "The word is bad.";
     break;
     
     default:
     result = "No Match!";
     break;
    
    }
    
    return result;
  }
  
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   System.out.println(sc.goodOrBad("good"));
 }
}

Result:
The word is good.
In the above example, I used the return keyword once. Don't use the return keyword too much 'cause using the return keyword too much may lead us to a bad code design.
Example:

public String goodOrBad(String txt1){
    
    switch(txt1){
    
     case "good":
     return "The word is good.";
    
     case "bad":
     return "The word is bad.";
     
     default:
     return "No Match!";
    
    }
  }
This syntax is legal but it's a bad code design 'cause of too much use of return keyword. In complex method definition, too much use of return keyword can make your method definition unreadable. If possible, only use one return keyword as an exit point.

When placing return keyword in an if statement, we still need to put another return keyword in the method block.
Example:

public String checkStr(String txt1){
  
  if(txt1.equals("String"))
    return txt1;
    
  return null;
}
Try removing the return outside if statement and you will encounter a compile-time error. The error happens because if the if statement fails then the return inside that if statement won't be executed and now the execution is stuck in the method call because it can't find an exit point. Let's try changing the return type to void.
Example:

public void checkStr(String txt1){
  
  if(txt1.equals("String")){
    System.out.println(txt1);
    return;
  }
    
}
This code doesn't throw any compile-time error because the return type is void. Unlilke other return types, the compiler doesn't expect a return keyword from method definition with void return type.

When placing return keyword in if-else statement both statements should have an exit point.
Example:

public String checkStr(String txt1){
  
  if(txt1.equals("String"))
    return txt1;
  else return "Not String";
}
For if-else-if, it's better to use one return type.
public String checkStr(String txt1){
  String result = null;
  
  if(txt1.equals("small"))
    result = "small string";
  else if(txt1.equals("medium"))
    result = "medium string";
  else if(txt1.equals("large"))
    result = "large string";
  
  return result;
}
We can't put codes next to return keyword. If we do, we will encounter an "unreachable" error
public void myMethod(){
  return;
  System.out.println("myMethod");
}
This code will throw "unreachable" error. Java can't execute codes next to return keyword. When the execution encounters the return keyword, it will exit the method immediately. Thus, the codes next to return keyword are "unreachable".

Parameterless Method

Methods can have no parameters
public void noParam(){
 System.out.println("This method doesn't have parameters!");
}

Method Overloading

There are times that we want to have two methods that share the same name but have similar or different function. We can do that in java by using method overloading. Method overloading is part of OOPs(Object-Oriented Programming System) concept. Method overloading resides in static polymorphism which is part of OOPs concept and does static binding. Binding is a way of connecting a method call to a method body. Overloaded methods must not be identical.
Example:

public class SampleClass{
  
  public int findMax(int[] n){
    int max = 0;
    for(int num : n)
      if(num > max)
        max = num;
    
    return max;
  }
  
  public int findMax(int n1, int n2){
    
    if(n1 > n2)
       return n1;
    else return n2; //n1 < n2 or n1 == n2
  }
  
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   int[] numbers = {3,1,22,2,6,15,10};
   
   String str = "Find the maximum value"+
                " of these numbers: ";
                
   for(int i = 0; i < numbers.length;i++){
     if(i != numbers.length-1)
     
       str = str + numbers[i] + ", ";
     else str = str + numbers[i];
   }
   
   System.out.println(str+"\n"+"Max value: "+
                      sc.findMax(numbers));
   System.out.println();
   System.out.println("Find the maximum value"+
                      " between 10 and 4 "+
                      "\nMax Value: "+
                      sc.findMax(10,4));
 }
}

Result
Find the maximum value of these numbers: 3, 1, 22, 2, 6, 15, 10
Max value: 22

Find the maximum between 10 and 4
Max value: 10
In the example above, we have two overloaded methods: findMax(int[] n), findMax(int n1,int n2)
These overloaded methods are not identical because they have different number of parameters. Let's try overloaded methods with different return types.
public class SampleClass{
  
 public int add(int n1, int n2){
  return n1 + n2;
 }
 
 public void add(int n1, int n2){
  System.out.println("Add "+n1+" to "+n2+" = "+(n1+n2));
 }
  
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   System.out.println("Adding 3 to 5 = "+
                      sc.add(3,5));
   sc.add(5,5);
 }
}
This code will have a compile time error. They might have different return types but their parameters have the same count and type and Java considers these methods identical. Let's try overloaded methods with the same amount of parameters but the type of their parameters are different.
public class SampleClass{
  
 public void add(int n1, int n2){
  System.out.println("Add int: "+n1+" + "+n2+" = "+(n1+n2));
 }
 
 public void add(long n1, long n2){
  System.out.println("Add long: "+n1+" + "+n2+" = "+(n1+n2));
 }
  
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   System.out.println("int addtion");
   sc.add(8,3);
   System.out.println("long addtion");
   sc.add(5,5);
 }
}

Result
int addition
Add int: 8 + 3 = 11
long addition
Add int: 5 + 5 = 10
This code will be compiled successfully. Although, the result looks incorrect. The add() method with integer parameters has been called two times instead of calling the add() method with long type parameters.

This happens because when we write a number in our code, for example "5", that number is an integer literal by default. Thus, add() with integer parameters is always called. This also happens to floating-point numbers. When we write "5.5" in our code then java considers that number as double by default. To solve this problem above, add "L" suffix in the second method call. Adding the suffix "L" means that the number is long type.
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   System.out.println("int addtion");
   sc.add(8,3);
   System.out.println("long addtion");
   sc.add(5L,5L);
 }
 
Result
int addition
Add int: 8 + 3 = 11
long addition
Add long: 5 + 5 = 10
Just like return type, access modifiers don't matter in method overloading.
 void add(int n1, int n2){
  System.out.println("Add int: "+n1+" + "+n2+" = "+(n1+n2));
 }
 
 public void add(long n1, long n2){
  System.out.println("Add long: "+n1+" + "+n2+" = "+(n1+n2));
 }
The code above will be compiled fine. The first add() has default modifier. default modifier grants access to all classes that are part of a package where the class or class member that has default modifier resides. The second add() has public modifier.

Let's try overloading static and non-static methods.
 public static void add(int n1, int n2){
  System.out.println("Add int: "+n1+" + "+n2+" = "+(n1+n2));
 }
 
 public void add(long n1, long n2){
  System.out.println("Add long: "+n1+" + "+n2+" = "+(n1+n2));
 }
This code will be compiled without errors. Just like return type and access modifiers, static keyword doesn't matter in method overloading. If you wanna know more then check out these links: are static and non static overloads each other and 8.4.9. Overloading

If an overloaded method has a primitive parameter and another overloaded method has wrapper class, the method with primitive parameter is going to be called.
public class SampleClass{
  
  void display(int i){
    System.out.println(i + " is int.");
  }
  
  void display(Integer i){
    System.out.println(i + " is Integer.");
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    sc.display(10);
  }
}
If an overloaded method has a parameter and another overloaded method has Variable Arguments parameter, the method with the parameter that is not varargs is going to be called.
public class SampleClass{

  void display(int i){
    System.out.println(i + " is int.");
  }
  
  void display(Integer... i){
    for(Integer integer : i)
       System.out.println(integer + " is Integer.");
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    sc.display(10);
  }
}

Method Overriding

Method Overriding is similar to method overloading but method overriding works with class inheritance. Understanding of inheritance is required to understand this topic. Method overriding is part of OOPs(Object-Oriented Programming System) concept.It resides in dynamic polymorphism which is part of OOPs concept and does dynamic binding. Binding is a way of connecting a method call to a method body. Overriding method(method in sub class) must have close similarities to Overriden method(method in super class).
Example

public class SampleClass extends SuperClass{
  
  @Override
  public void createMessage(){
    System.out.println("Message from SampleClass class");
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    SuperClass supC = new SuperClass();
    sc.createMessage();
    supC.createMessage();
  }
}

class SuperClass{

  public void createMessage(){
    System.out.println("Message from SuperClass class");
  }
}

Result
Message from SampleClass class
Message from SuperClass class
As you can see, each class has different implementation of createMessage(). Use @Override annotation to specify that the method overrides the method in the super class. Using annotations improves the readability of your code.

If we want to call a member(visible to sub class) of super class in sub class then we can use the super keyword.
Example

public class SampleClass extends SuperClass{
  
  @Override
  public void createMessage(){
    System.out.println("Message from SampleClass class");
    super.createMessage();
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    sc.createMessage();
  }
}

class SuperClass{

  public void createMessage(){
    System.out.println("Message from SuperClass class");
  }
}

Result
Message from SampleClass class
Message from SuperClass class
Here are some rules for method overriding

1.) If an overriden method has parameters then the overriding method must match the the number,type and order of overriden method parameters. Parameter names in overriding method can be different from parameter names in overriden method.

2.) Unlike in method overloading, access modifier does matter in method overriding. The access modifier of overriding method must be equal or less restrictive than the overriden method.
Example

public class SampleClass extends SuperClass{
  
  //Overriding Method
  @Override
  void createMessage(){
    System.out.println("Message from SampleClass class");
    super.createMessage();
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    sc.createMessage();
  }
}

class SuperClass{
  
  //Overriden Method
  public void createMessage(){
    System.out.println("Message from SuperClass class");
  }
}
This code will throw a compile-time error because the overriding method has access modifier that is more restrictive than the overriden method. default access modifier is more restrictive than public access modifier.

3.) we cannot override methods with static, private or final keyword. Those methods belong to their respective classes. static methods belong to the class itself and can't be overriden because the compiler implements static binding to static methods; private methods belong to the class instance and can't be inherited by sub classes; and final methods can't be overriden.

4.) Overriding method can only throw unchecked exceptions if overriden method doesn't throw any exception.
public class SampleClass{

  public static void main(String[]args){
  }
}

class Message{

  void displayMessage(){
    System.out.println("This is my Message!");
  }
  
  void sendMessage(String message){
    System.out.println("Message: " + message);
    System.out.println("Message sent!");
  }
}

class Messenger extends Message{
  
  //no error
  @Override
  public void displayMessage(){
    System.out.println("Message In Messenger");
  }
  
  //error
  /*
  @Override
  public void sendMessage(String message) throws Exception{
    System.out.println("Send Message: " + message);
    System.out.println("Message Sent!");
  }
  */
  
}
5.) If the overriden method throws an exception then, the overriding method can only throw the same exception or related(sub class) exceptions.
public class SampleClass{

  public static void main(String[]args){
  }
}

class Message{

  void displayMessage() throws Exception{
    System.out.println("This is my Message!");
  }
  
  void sendMessage(String message) throws RuntimeException{
    System.out.println("Message: " + message);
    System.out.println("Message sent!");
  }
}

class Messenger extends Message{
  
  //no error because RuntimeException is a sub class of
  //Exception
  @Override
  public void displayMessage() throws RuntimeException{
    System.out.println("Message In Messenger");
  }
  
  //error because Exception is not a RuntimeException and
  //not a sub class of RuntimeException
  /*
  @Override
  public void sendMessage(String message) throws Exception{
    System.out.println("Send Message: " + message);
    System.out.println("Message Sent!");
  }
  */
  
}
6.) Overriding methods, with the same name and parameter-list but with different return types in one class, throws a compile time error that is related to method overloading.
public class SampleClass{

  public static void main(String[]args){
  }
}

abstract class Message{

  abstract void message();
}

abstract class Greetings extends Message{

  String message(){
    return "Greetings";
  }
}

class Messenger extends Greetings{
  
  //error: method message() is already
  //defined...
  @Override
  public void message(){}
  @Override
  public String message(){ return ""; }
}
If we add a parameter in one of the message() methods then the code above will compile successfully.
public class SampleClass{

  public static void main(String[]args){
  }
}

abstract class Message{

  abstract void message();
}

abstract class Greetings extends Message{

  //add one parameter to make the two message()
  //not identical
  String message(String message){
    return "Greetings! " + message;
  }
}

class Messenger extends Greetings{
  
  //error: method message() is already
  //defined...
  @Override
  public void message(){}
  @Override
  public String message(String message){
    return "Greetings From Messenger! " + message;
  }
}
Covariant Return Type

You need to have an understanding of inheritance to understand this topic. We can change the return type of the overriding method as long as that return type is a subtype of the overriden return type. Prior to java5, overriding method must have the same return type as the overriden method. Thus, their return types are invariant. However, since java5, overriding methods can change their return type. Though, this only works with non-primitive types.
public class SampleClass{

  public static void main(String[]args){
  
    Case penCase = new Case();
  	penCase.getPen().displayName();
  	Metal metalcase = new Metal();
  	metalcase.getPen().displayName();
  }
}

class Pen{
  private String name = "Pen";
  
  public void displayName(){
    System.out.println(name);
  }
  
}
class Ballpoint extends Pen{
  private String name = "Ballpoint Pen";
  
  public void displayName(){
    System.out.println(name);
  }
  
}

class Case{

  public Pen getPen(){
    return new Pen();
  }
}

class Metal extends Case{

  @Override
  public Ballpoint getPen(){
   return new Ballpoint();
  }
}
The code above will be compiled fine. Ballpoint class is a subclass of Pen class. Therefore, Ballpoint class is a subtype of Pen class. Now, invert the overriding method in metal and the overriden method in Case:
class Case{
  
  public Ballpoint getPen(){
   return new Ballpoint();
  }
}

class Metal extends Case{

  @Override
  public Pen getPen(){
    return new Pen();
  }
}
This code throws a compile-time error regarding incompatibility because Pen class is the parent class of Ballpoint. Therefore, Pen class is the supertype of Ballpoint. Covariant return type only works with overriding method that has a return type that is a subtype.

Abstract Method

Abstract methods are similar to method definition but they don't have method body. You will see abstract methods in abstract class and interface. If a class extends or implements an abstract class or interface with abstract methods then the class is required to override and define those abstract methods.
General Form: access-modifier abstract return-type method-name(parameters);

Example
public class SampleClass extends AbstractClass{

  //Override createMessage()
  @Override
  public void createMessage(){
    System.out.println("Message from SampleClass");
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    sc.createMessage();
  }
}

abstract class AbstractClass{
public abstract void createMessage();
}
This code will be compiled without errors. try removing the createMessage() in SampleClass and you will encounter a compile-time error.

Java pass-by-value

When we assign a value to a variable, we're doing pass-by-value. Let's say a variable is an address and is pointing at a value.
int a = 5;
the "a" variable here is an address and that address is pointing at value "5". Now, let's try two variables.
int a = 5;
int b = 10;
int a = b;

Let's focus on int a = b;.When we print "a" on cmd the result is 10. When we do this kind of assignment, we're passing a copy of the value of b to a. Pretty easy right? Now, let's try passing values to a method call.
public class SampleClass{
  
  public void changeVal(int a){
    a += 5;
    System.out.println("\"a\" value in changeVal: " + a);
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    int a = 5;
    System.out.println("\"a\" value before changeVal: " + a);
    sc.changeVal(a);
    System.out.println("\"a\" value after changeVal: " + a);
  }
}

Result
"a" value before changeVal: 5
"a" value in changeVal: 10
"a" value after changeVal: 5
As you can see, the value of int a is still 5 after the program completed changeVal() execution. When we put "a" in changeVal() java only assigned the value of variable "a" of main() to "a" variable of changeVal(). Now, let's try objects.
public class SampleClass{
  
  public void changeVal(StringBuilder a){
    a.append("cd");
    System.out.println("\"a\" value in changeVal: " 
                       + a.toString());
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    StringBuilder a = new StringBuilder("ab");
    System.out.println("\"a\" value before changeVal: "
                       + a.toString());
    sc.changeVal(a);
    System.out.println("\"a\" value after changeVal: "
                       + a.toString());
  }
}

Result
"a" value before changeVal: ab
"a" value in changeVal: abcd
"a" value after changeVal: abcd
Now, "a" value has changed after the program completed changeVal() execution. With this result, you might consider that java is not pass-by-value at all. Let me explain the code above first.

Take a look at this code: StringBuilder a = new StringBuilder("ab");
When we assign an object to a variable, java assigns the address of the object, in other words, the reference of the object to the variable. Now, we have two adresses, the address of the variable and that address is pointing at the address of the object and that address is pointing at the real object in the heap.

When we pass the value of "a" in changeVal(), we're passing the reference of the object in the "a" variable in changeVal(). Now, the variable "a" in main() and the variable "a" in changeVal() are now pointing at the same object reference. Thus, any changes in "a" variable in changeVal() will be reflected to "a" variable in main().

Now, let's tweak the example above.
public class SampleClass{
  
  public void changeVal(StringBuilder a){
    a = new StringBuilder("abcd");
    a.append("ef");
    System.out.println("\"a\" value in changeVal: "
                       + a.toString());
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    StringBuilder a = new StringBuilder("ab");
    System.out.println("\"a\" value before changeVal: "
                       + a.toString());
    sc.changeVal(a);
    System.out.println("\"a\" value after changeVal: "
                       + a.toString());
  }
}

Result
"a" value before changeVal: ab
"a" value in changeVal: abcdef
"a" value after changeVal: ab
The value of "a" in main didn't change after the program completed changeVal() execution. Let's examine the code. So, we put "a" of main() in changeVal(). Now, "a" in main and "a" in changeVal() point at the same object reference. Now, take a look at this code
a = new StringBuilder("abcd");
This code created a new StringBuilder with "abcd" String and assigned the reference to "a" in changeVal(). Thus, "a" in main and "a" in changeVal are not pointing at the same object reference anymore. Thus, Any changes in "a" in changeVal() won't be reflected to "a" in main().

Arrays are considered as objects. So, when we pass an array in a method, we're passing the array's reference. Thus, we can modify arrays that are passed in methods.
public class SampleClass{
  
  public void changeVal(String[] a){
    a[0] = "String0";
    a = new String[]{"StringA"};
    System.out.println("\"a\" value in changeVal: " + a[0]);
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    String[] a = new String[]{"MyString"};
    System.out.println("\"a\" value before changeVal: " + a[0]);
    sc.changeVal(a);
    System.out.println("\"a\" value after changeVal: " + a[0]);
  }
}

Result
"a" value before changeVal: MyString
"a" value in changeVal: StringA
"a" value after changeVal: String0
Let's test other object types like String and some wrapper classes.
public class SampleClass{
  
  public void changeVal(String a){
    a += "cd";
    System.out.println("\"a\" value in changeVal: " + a);
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    String a = "ab";
    
    System.out.println("\"a\" value before changeVal: " + a);
    sc.changeVal(a);
    System.out.println("\"a\" value after changeVal: " + a);
  }
}

Result
"a" value before changeVal: ab
"a" value in changeVal: abcd
"a" value after changeVal: ab
Take a look at this code: a += "cd";
this code is equivalent to this: a = new String(a + "cd");
Note: When talking about memory storage, the expression "cd" and new String(a + "cd") store string objects in different memory sections. Refer to my "Java Tutorial: Exploring The String Class" blogpost.

Now, Let's try wrapper class. Wrapper classes are classified as value-based classes.
public class SampleClass{
  
  public void changeVal(Integer a){
    a += 4;
    System.out.println("\"a\" value in changeVal: " + a);
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    Integer a = 3;
    
    System.out.println("\"a\" value before changeVal: " + a);
    sc.changeVal(a);
    System.out.println("\"a\" value after changeVal: " + a);
  }
}

Result
"a" value before changeVal: 3
"a" value in changeVal: 7
"a" value after changeVal: 3
Take a look at this code: a += 4;
this code is equivalent to this: a = Integer.valueOf(a + 4);

Actually, there are two types of value passing mechanism, if you will. These are: pass-by-value and pass-by-reference. Java is often specified as pass-by-value and pass-by-reference. Go to this article: Is Java “pass-by-reference” or “pass-by-value”? to clear the confusion between pass-by-value and pass-by-reference.

Method Chaining

This topic requires you to have a basic understanding of class and class instance.
When we wanna call multiple methods in one line we can use method chaining. Before that, let's call methods like we usually do.
public class SampleClass{
 private int num;
 
 public void setNum(int n){
   num = n;
   System.out.println("num is set to " + num);
 }
 
 public void add(int n){
   int result = num + n;
   System.out.println(num + " + " + n + " = " + result);
   num = result;
 }
 
 public void square(){
  int result = num * num;
  System.out.println(num + "^2" + " = " + result);
  num = result;
 }
 
 public int getNum(){ return num; }
 
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   sc.add(3);
   sc.square();
   sc.add(2);
   sc.add(4);
   sc.square();
   
   System.out.println("num value is: " + sc.getNum());
 }
 
}
As you can see, calling multiple methods is pretty lengthy. Now, let's try method chaining.
public class SampleClass{
 private int num;
 
 public SampleClass setNum(int n){
   num = n;
   System.out.println("num is set to " + num);
   return this;
 }
 
 public SampleClass add(int n){
   int result = num + n;
   System.out.println(num + " + " + n + " = " + result);
   num = result;
   return this;
 }
 
 public SampleClass square(){
  int result = num * num;
  System.out.println(num + "^2" + " = " + result);
  num = result;
  return this;
 }
 
 public int getNum(){ return num; }
 
 public static void main(String[]args){
   SampleClass sc = new SampleClass();
   int num = sc.add(3).square().add(2).add(4).square().getNum();
   System.out.println("num value is: " + num);
 }
 
}
As you can see, I only write "sc" once. Before I explain the code above let me introduce the this keyword. this keyword refers to a class instance in which it's used. return this; means we're returning the reference of an instance class. In the example above, this keyword returns the instance of SampleClass() that has been assigned to sc.

First we called sc.add(3), sc held a class instance of SampleClass. Then, add() returned the class instance that was assigned to sc. Now, the program called square(). This time, the return value of previous call to add(3) was used to call square(). The program called add(2) again, this time the return value of previous call to square() was used to call add(2) and so on.

Then, the last method in the chain which was getNum() returned an int value and since there were no more methods to call the return value of getNum() was assigned to num. Try removing getNum() and you will encounter an error.

Variable Arguments(Varargs)

Varargs was introduced in JDK5 and its usage is to simplify the parameter structure of a method. Varargs can take indefinite amount of arguments when a method that use varargs is called. Prior to JDK5, we can use an array type as a parameter to send a collection of data to a method or we overload a method to accept varying argument list. We can still utilize those techniques or we can use varargs.
public class SampleClass{
  
  //varargs
  void displayNames(String... names){
  
    for(int i = 0; i < names.length; i++)
      System.out.println(names[i]);
  }
  
  public static void main(String[]args){
  
    SampleClass sc = new SampleClass();
    System.out.println("name set #1");
    sc.displayNames("Dave","Crow","Ben","Wolf");
    System.out.println();
    System.out.println("name set #2");
    sc.displayNames("Owen","Sid");
    
  }
}
Take a look at this syntax: void displayNames(String... names)
The ... indicates that this parameter will take a variable number of arguments. The data type before ... indicates the type of arguments that this varargs parameter accepts. names is the parameter name.

As you can see in the displayNames() method body, varargs parameter is treated as an array. We can get the varargs parameter length by using the length field. You might wonder: "What if we put an array as an argument?". Well, let's try.
public class SampleClass{
  
  //varargs
  void displayNames(String... names){
  
    for(int i = 0; i < names.length; i++)
      System.out.println(names[i]);
  }
  
  public static void main(String[]args){
  
    SampleClass sc = new SampleClass();
    System.out.println("name set #1");
    String[] names = {"Dave","Crow","Ben","Wolf"};
    sc.displayNames(names);
    System.out.println();
    System.out.println("name set #2");
    sc.displayNames(new String[]{"Owen","Sid"});
    
  }
}
As you can see, an array as argument in a varargs parameter is acceptable because varargs is treated like an array.

We can also write a varargs parameter that takes arrays as an argument and it will be treated as a two-dimensional array.
public class SampleClass{
  
  //varargs
  void displayNames(String[]... names){
  
    for(int i = 0; i < names.length; i++)
      for(int j = 0; j < names[i].length;j++)
          System.out.println(names[i][j]);
  }
  
  public static void main(String[]args){
  
    SampleClass sc = new SampleClass();
    System.out.println("name set #1");
    sc.displayNames(new String[]{"Owen","Sid"},
                    new String[]{"Kay","Clover"});
    
  }
}
Before we use varargs, we need to follow some rules.
1.) When adding a varargs parameter on a parameter list with multiple parameters, varargs parameter must be the last parameter entry on the list.
public class SampleClass{
  
  //error: varargs parameter must be the last parameter
  /*
  void displayNames(String... names,String setName,int setNo){
    
    System.out.println("Set Name: " + setName);
    System.out.println("Set Number: " + setNo);
    for(int i = 0; i < names.length; i++)
      System.out.println(names[i]);
  }
  */
  
  void displayNames(String setName,int setNo,String... names){
    
    System.out.println("Set Name: " + setName);
    System.out.println("Set Number: " + setNo);
    for(int i = 0; i < names.length; i++)
      System.out.println(names[i]);
  }
  
  public static void main(String[]args){
  
    SampleClass sc = new SampleClass();
    sc.displayNames("C Set",1,"Civ","Crow","Cromwell","Crux");
    System.out.println();
    sc.displayNames("S Set",2,"Steve","Sid");
    
  }
}
2.) two or more varargs parameters in a parameter list are not allowed.
public class SampleClass{
  
  //compile-time error
  void displayNames(int... numbers,String... names){
  }
  public static void main(String[]args){}
}
When we call a method with varargs as its last parameter, we can omit the last argument in the method call and java won't complain.
public class SampleClass{

  public static void main(String[] args){
    SampleClass sc = new SampleClass();
    sc.displayNames("C Set",1);
  }
  
  void displayNames(String setName,int setNo,String... names){
    
    System.out.println("Set Name: " + setName);
    System.out.println("Set Number: " + setNo);
    System.out.println("names length: " + names.length);
    for(int i = 0; i < names.length; i++)
      System.out.println(names[i]);
  }
}

No comments:

Post a Comment