Java 11 Developer Certification - Java Methods

December 10, 2020

What we are covering in this lesson

  1. What is a Java method
  2. Structure of a method declaration

    • Method Modifier
    • Method Header
    • Method Body
  3. Valid Method Declarations
  4. Invalid Method Declarations
  5. Code Examples
  6. Understanding return types
  7. Contructors

    • Private Constructor
    • final class
    • final method

What is a Java method

A Java method can accept zero to many statements, which define some behaviour and which can be reused. A method can accept zero to many parameters, including a variable number of parameters, and can optionally return an element back. An important thing to remember is that Java always passes parameters by value and not by reference. Although it can be very confusing because the value itself can be a reference.

Structure of a method declaration

The following is from Oracle’s Java 11 specification describing the structure of a method declaration:

{MethodModifier} MethodHeader MethodBody

Method Modifier

These are similar to Field Modifiers.

  • There are access modifiers

    • The default modifier is package - no modifier in other words
    • Other modifiers are public, private and protected
  • There are non-access modifiers

    • abstract - declaring a method abstract means it’s not implemented and it has no body. A non-abstract method is called a concrete method.
    • static - a static method, also called class method, can be called using the class name.
    • final - declares that the method definition is complete and no subclass can override it or hide it.
    • strictfp - this is used to restrict floating point calculations to ensure portability across platforms.
    • synchronized - creates a thread-safe method
    • native - A method that is native is implemented in a platform-dependent code, usually written in a different programming language like C
  • Modifier rules are as below

    • A keyword can only appear once as a modifier for a method declaration
    • Only one of the access modifiers - private, protected, public - is allowed for a single method
    • If we want to use the abstract modifier, the method can only use public or protected modifier or no access modifier (default) and must be defined in an abstract class
    • native and strictfp are not modifier that can be used in the same declaration

Method Header

The method header consists of

  • The result of a method declaration either declares the type of value that the method returns (the return type), or uses the keyword void to indicate that the method does not return a value.
  • The name of the method.
  • The formal parameters of the method or constructor, if any, are specified by lists of comma-separated parameter specifiers.

    • Each parameter specifier consists of a type, optionally preceded by the final modifier and an identifier that specifies the name of the parameter
    • If a method or constructor has no formal parameters, and no receiver parameter, then an empty pair of parentheses appears in the declaration of the method or constructor.
    • A formal parameter of a method or constructor may be a varargs parameter, indicated by an ellipsis followed by the type.
    • At most, one varargs parameter is permitted, it has to be in the very last position.
  • A method can optionally declare a throws clause to denote any checked exception classes.

At a minimum, a valid method declaration would look like void method()

Method Body

Before discussing method body structure, we need to look at something called method signature.

  • A method signature uniquely identifies a method
  • Two methods cannot have the same signature in a single class
  • The signature contains

    • Method name
    • Ordered list of paramter types
  • The signature DOES NOT consist of

    • modifiers
    • return type
    • parameter names
    • throws clause

Now lets look at the method body. A method body is either a block of code that implements the method or simply a semi-colon indicating the lack of an implementation (abstract method). A concrete method (method having a valid implementation) requires brackets, curly braces.

As mentioned before, Java always passes parameters by value, not by reference. The pass by value (or call by value) means anything passed into a function or method call is unchanged in the caller’s scope when the method returns. Call by reference (pass by reference) is where a method receives an implicit reference to the variable to be used as a parameter and not a copy of its value. In this case, any modification made to the parameter would be visible to the calling code as well.

Call by reference can be imitated in languages that use call by value by making use of references. This is similar to call by sharing (passing an object which can then be mutated).

Java is said to have call by sharing which means that string objects, which are immutable, and primitive data types will not have their values changed (a copy is made and passed to methods) when the method returns to the calling scope. However, if we are passing an object reference of any kind, a change to the underlying object which the reference refers to, will be reflected when the method returns.

This is still considered pass by value, because the reference that the method receives in the form of a parameter, is a copy of the reference in the calling scope. So, the reference won’t change when returning from the method, however, the data on the referenced object may change if it’s been changed in the method.

Valid Method Declaration Examples

Valid Declarations Notes
void method() method with no parameters and returning no result (void)
void method(String a) method with one parameter of type String, returning no result (void)
void method(Object… o) method accepts varargs parameters, meaning we can pass 0 to many Objects in a comma delimited list to it
Object method() method with no parameters and returning an Object
String method(final int a) method with 1 parameter of type int and returning a String value
String method(final int a, String b) method with 2 parameters, one of type final int and one of String, and returning a String value

Invalid Method Declaration Examples

Invalid Declarations Reason for being invalid
void method(String a,b) Each parameter must have its own type
void method(Object… o, String a) varags must be last parameter in the method parameter list
void method(Object… o1, Object… o2) only 1 varargs is permitted
void method(final final int a) duplicate final keyword cannot be used
void method(Object a, String a) duplicate parameter name cannot be used

Code Examples

Lets look at some of the code examples to understand method and constructors properly.

package maxCode.online.Methods;

public class TestPassByValue {
	public static void main(String[] args) {
		// Set up some test data
		// Primitive data variables
		int a = 1;
		int b = 2;

		// String variables
		String aString = new String("123");
		String bString = new String("456");

		// Object variables using StringBuilder
		StringBuilder abc = new StringBuilder("abc");
		StringBuilder def = new StringBuilder("def");

		System.out.println("-------- Before method call --------");

		// Print out values and hashcodes prior to method call
		System.out.println("abc.hashCode() = " + abc.hashCode() + ", value = " + abc);
		System.out.println("def.hashCode() = " + def.hashCode() + ", value = " + def);
		System.out.println("a = " + a);
		System.out.println("b = " + b);
		System.out.println("aString.hashCode() = " + aString.hashCode() + ", value = " + aString);
		System.out.println("bString.hashCode() = " + bString.hashCode() + ", value = " + bString);

		// Execute method on each type of data
		changeValue(a, b);
		changeValue(aString, bString);
		changeValue(abc, def);

		System.out.println("-------- After method call --------");

		// Print out values and hashcodes after the method call
		System.out.println("abc.hashCode() = " + abc.hashCode() + ", value = " + abc);
		System.out.println("def.hashCode() = " + def.hashCode() + ", value = " + def);
		System.out.println("a = " + a);
		System.out.println("b = " + b);
		System.out.println("aString.hashCode() = " + aString.hashCode() + ", value = " + aString);
		System.out.println("bString.hashCode() = " + bString.hashCode() + ", value = " + bString);
	}

	public static void changeValue(Object o1, Object o2) {
		// This code changes the values of the parameters passed to it.
		switch (o1.getClass().getName()) {
		case ("java.lang.Integer"):
			o1 = 10;
			o2 = 20;
			break;
		case ("java.lang.String"):
			o1 = "aaaa";
			o2 = "bbbb";
			break;
		case ("java.lang.StringBuilder"):
			o1 = ((StringBuilder) o1).append("xyz");
			o2 = ((StringBuilder) o2).append("zzz");
			break;
		}
	}
}

Output

-------- Before method call --------
abc.hashCode() = 705927765, value = abc
def.hashCode() = 366712642, value = def
a = 1
b = 2
aString.hashCode() = 48690, value = 123
bString.hashCode() = 51669, value = 456
-------- After method call --------
abc.hashCode() = 705927765, value = abcxyz
def.hashCode() = 366712642, value = defzzz
a = 1
b = 2
aString.hashCode() = 48690, value = 123
bString.hashCode() = 51669, value = 456

As we can see above, the hashCode of the variables remain the same before and after the method calls. Important this to note here is that the hashCodes of the String and StringBuilder objects didn’t change. However, the actual values of the StringBuilder objects did change.

Understanding return types

The return type of the method is present in the method header. Methods which do not return anything must contain a void return type, except for constructors. Below are few of the valid examples of return types.

Valid return Examples Notes
Number getNumber() { return Integer.valueOf(1); } We can return a subtype of the declared return type (Integer is a subtype of Number)
Number getNumber() { return null; } We can return null for a reference type return type
long getNumber() { int myInt = 5; return myInt; } We can return narrower primitive datatype if the return type is a primitive type (long here)
long getNumber() { return Long.valueOf(5L); } We can return a primitive datatype wrapper if the return type is a primitive
long getNumber() { return Integer.valueOf(10); } We can return a wrapper of a primitive to a primitive that is the same or wider
void getNumber() { return; } we can have a return statement, returning no reference or primitive in case of void method

Now that we have looked at the valid examples, below are few of the invalid examples for the return types and we should refrain from using any of these.

Invalid return Examples Notes
Integer getNumber() { Number n = Integer.valueOf(10); return n; } We cannot return a supertype of the defined return type
void getNumber() { return null; } We cannot return null when return type if void
long getNumber() { return null; } We cannot return null if return type is primitive
int getNumber() { return 10L; } We cannot return a primitive larger than the return type

Constructors

A constructor is a special method used in the creation of an object that is an instance of a class.

  • The constructor method name must be the same as the class name.
  • The constructor method does not have any return type, not even void.
  • The return statement cannot be used in a constructor.
  • The constructor method is required, but its not compulsory to declare one.

    • If you declare zero constructors in a class, Java will create an implicit noargs constructor with the same access modifier as the class and a call to super, which executes the parent’s constructor.
    • If you declare one or more constructors, Java will not create an implicit no arguments constructor for you, even if you do not self declare a noargs constructor.

So lets verify these using a simple java code.

package maxCode.online.Methods;

class ZeroConstructorClass {
	// A class with no constructors defined
}

// Create a sub class
class SingleConstructorClass extends ZeroConstructorClass {
	private String name;

	// Constructor has one parameter
	SingleConstructorClass(String name) {
		this.name = name;
	}

	public String getName() {
		return this.name;
	}
}

public class ConstructorExample {
	public static void main(String[] args) {
		//SingleConstructorClass consClass = new SingleConstructorClass();	//Error - The constructor SingleConstructorClass() is undefined
		// Instantiate the object with the one parameter constructor
		SingleConstructorClass consClass = new SingleConstructorClass("Max");
		System.out.println(consClass.getName());
	}
}

Output:

Max

So our code has 3 classes, a child class with no constructors, a parent class with one parameterized constructor, and a main class which instantiates and prints the value.

ZeroConstructorClass doesnt have any constructors but Java adds a no-arg constructor by default.

If we manually add a constructor in the parent class, the child class constructor calls it by default. We can also manually call it using the super() keyword and pass parameters as per the parent constructor defined.

But one important thing to remember is that once we add a parameterized constructor to any class, Java will not add the default zero arg constructor, and the code can result into a compilation error.

So if I change the code like below, it will result in a compilation error

package maxCode.online.Methods;

class ZeroConstructorClass {
	ZeroConstructorClass(String arg1) {
		System.out.println("Parent Constructor arg = : " + arg1);
	}
}

// Create a sub class
class SingleConstructorClass extends ZeroConstructorClass {
	private String name;

	// Constructor has one parameter
	SingleConstructorClass(String name) {	//This line will throw error as default constructor is not added by Java for parent class
		this.name = name;
	}

	public String getName() {
		return this.name;
	}
}

public class ConstructorExample {
	public static void main(String[] args) {
		//SingleConstructorClass consClass = new SingleConstructorClass();	//Error - The constructor SingleConstructorClass() is undefined
		// Instantiate the object with the one parameter constructor
		SingleConstructorClass consClass = new SingleConstructorClass("Max");
		System.out.println(consClass.getName());
	}
}

To resolve the compilation issue, we can either call the parent constructor using super(arg1) and passing a parameter to it, or we can also create a no arg constructor in the parent class.

So just to summarize, we must always remember these things about constructor

  • If we don’t define one, we will get one with no arguments and an implicit call to super().
  • If we do declare one, and the class is a child of another class, the implicit call to super() is still there even if we do not explicitly call super() ourself.

Private Constructor

Now we will look at a code example where we will create a class with no public or package constructor, but only a private constructor, also knows as singleton class.

package maxCode.online.Methods;

class OnlyClass {
	private OnlyClass() {
		// private constructor, the ONLY constructor
	}

	private int classVariable;

	// instances can only be created within current class
	// or a nested class
	private static final OnlyClass instance = new OnlyClass();

	// You can expose the only instance via a static method
	public static OnlyClass getInstance() {
		return instance;
	}

	// Or you can make static methods which call methods on the
	// protected instance
	public static void doSomething() {
		instance.doSomethingRestricted();
	}

	private void doSomethingRestricted() {
		this.classVariable++;
		System.out.println("Only one instance of this class exists or will ever exist");
	}
}

public class RestrictedObjects {
	public static void main(String[] args) {
		OnlyClass.doSomething();
		//OnlyClass o = new OnlyClass();	//The constructor OnlyClass() is not visible
	}
}

//Implicit super constructor OnlyClass() is not visible for default constructor. Must define an explicit constructor
//class AnotherClass extends OnlyClass { }

Output:

Only one instance of this class exists or will ever exist

We have created a class containing only one private constructor. It contains a public static method which can be called using the class name. At any moment, there will exist only one instance of this class. So we cannot also create any instance of OnlyClass else we will get an error stating The constructor OnlyClass() is not visible

An important thing to remember is that we cannot extend a class that has only a private constructor except as a nested class. The lines commented at the end would throw error for the same reason.

Now we will look at how a class with a private constructor different from a final class.

final class

Have a look at the code below where we create a final class FinalEntity which has a class variable, a constructor, and a void method.

The main class instantiates a couple of objects of this FinalEntity, and then calls the method for each instance. So we can create instances of the class eventhough it is marked final!

Another difference lies in how other class can extend a final class. So a final class cannot be extended further by any other class. Hence we should use a final class only in such cases where we do not want the class to be extended any further.

package maxCode.online.Methods;

//Example of a final class
final class FinalEntity {
	private String name;

	FinalEntity(String name) {
		this.name = name;
	}

	void doSomething() {
		System.out.println("doing something for " + this.name);
	}
}

public class FinalClassExample {
	public static void main(String[] args) {
		// You can create as many instances of a final class as you wish
		FinalEntity f1 = new FinalEntity("a");
		FinalEntity f2 = new FinalEntity("b");
		f1.doSomething();
		f2.doSomething();
	}
}

Output:

doing something for a
doing something for b

This was all about final class. We can also define a method as final, which would mean that the method cannot be overriden by any other class. So now, lets have a look at final method examples.

final method

package maxCode.online.Methods;

//Create an abstract class with abstract methods
abstract class WillHaveManySubTypes {
	public abstract void doXYourWay();

	public abstract void doYYourWay();

	public abstract String toString();

	// Here we create a final method in an abstract class
	final void doZOnlyOneWay() {
		System.out.println("Z can only be done this way");
	}
}

public class FinalMethodExample extends WillHaveManySubTypes {
	private boolean flag = false;

	public static void main(String[] args) {

		// We instantiate an object and test all the methods
		FinalMethodExample e = new FinalMethodExample();
		e.doXYourWay();
		e.doYYourWay();
		e.doZOnlyOneWay();
		System.out.println(e);
	}

	// Override and implement the abstract class's abstract methods
	public void doXYourWay() {
		System.out.println("Implemented X my way");
	}

	public void doYYourWay() {
		System.out.println("Implemented Y my way");
	}

	//Cannot override the final method from WillHaveManySubTypes
	/*public void doZOnlyOneWay() {
		System.out.println("Implement Z my way");
	}*/

	public String toString() {
		// return super.toString();
		return "Not implemented yet";
	}
}

Output

Implemented X my way
Implemented Y my way
Z can only be done this way
Not implemented yet

We have an abstract class with three abstract methods and a final method which is not extensible. Any subclass cannot override this method, and has to execute the same code. Then we have the main class which extends the abstract class and implements the two methods.

Note that if we try to override the final method, it will return a compile time error starting Cannot override the final method from WillHaveManySubTypes.

One more important thing to remember is that we MUST implement all the abstract methods in the main class, otherwise we will get a compiler error - The type FinalMethodExample must implement the inherited abstract method WillHaveManySubTypes.toString().

And since we have overriden the toString() method, we can see the output displayed as Not implemented yet when printing any object.

If we want to use the java.lang provided toString() now, we cannot use it even by calling super(). The compiler will complain Cannot directly invoke the abstract method toString() for the type WillHaveManySubTypes since we have defined toString to be an abstract method.

super() executes the parent class method and the parent class in our case is an abstract class with an abstract method toString() and hence the error.

Thats all for this section, we will look at some more topics related to methods in detail in the next section.