Method Declaration

Declares a method, (possibly with a body), so that it can be referenced via a method reference, called, and overridden.

A method is a named group of statements that can be referred to and executed via a method invocation statement. A method declaration introduces a method to the program. The group of statements that are executed are called the method's implementation. A method declaration includes the method's name (and formally, it's signature), which can be used to refer to the method elsewhere, though it is not always necessary for a declaration to include the implementation.

Syntax

A method declaration consists of:

1 modifier-list return-type name ( parameter-list ) array-dims throws throw-list body
2 modifier-list < type-parameter-list > annotation-list return-type name ( parameter-list ) array-dims throws throw-list body

where...

modifier-list is a possibly empty set of keywords and annotations that can contain:
  • one of the three access modifiers: public, protected, and private,
  • up to one of each of the following keywords: final, static, strictfp, synchronized, abstract, native, and default,
  • any number of annotations that are applicable to the method (see below for details).
intermixed and in any order.
return-type is a type denoting what type of value, if any, the method returns. The type may have its own annotations. If void is specified as the method's return type, the method returns no value.
name is an identifier that names the method.
parameter-list is a comma-separated list of parameters, the first of which may be a receiver parameter and the last of which may be a variable-arity (var-args) parameter.
parameter is made up of three components, in order:
  1. A possibly empty set of parameter modifiers, in any order. The modifier set can contain the keyword final, as well as annotations that are applicable to parameters.
  2. A parameter type that determines the type of the variable. It may have its own annotations and may be followed by a ... token if it is the last parameter in the parameter list, to indicate that it is a variable-arity argument. The ... may be annotated, as well (e.g. int @SomeAnnotation ...).
  3. A variable name.
array-dims is any number of consecutive pairs of [] strings, optionally separated (e.g. [][] []) and/or split (e.g. [][][ ]) by whitespace. Each [] pair may be annotated with an annotation that targets type use. (Such annotations target the respective dimension of the array that the [] represents.)
throw-list A list of exception types denoting what the method is permitted to throw.
body is either a semicolon ; or a block statement.
type-parameter-list is a comma-separated list of type parameters. Each type parameter is an identifier. The identifier may be followed by the keyword extends and then a set of types to establish an upper bound. The set of types upper-bounding the parameter must be delimitted by the & token, and all but the first must be interface types.

such that...

Syntax Elements

1 A normal method declaration.

2 A generic method declaration, with type parameters.

Components

A method can be declared directly within a class or interface, or within an enum's body declaration (after the constant list). Method declarations can override methods inherited from a parent type.

Annotations

Annotations can be placed in two distinct places in a method declaration, either in the modifier-list, intermixed with keyword modifiers, or, if type parameters are included, after the type-parameter-list and immediately before the return type.

Annotations included in the method declaration's modifier list will apply to:

If the annotation targets both type-uses and methods, the annotation applies to both the return type and the method. If the annotation does not target either, a compile error occurs.

If the method declares type parameters and the annotation is placed after the type-parameter-list, the annotation must target type-uses and the annotation applies only to the return type.

Keyword Modifiers

Keyword modifiers are any of public, protected, private, static, final, synchronized, strictfp, default, abstract, and native.

Access Modifier Keywords

Access modifiers affect the accessibility of a method, which determines from where the method may be invoked (via a method invocation statement), referenced (via a method reference), and overridden. Without any access modifier keywords, a method's accessibility is package-protected, meaning that it can be accessed by code within the same package as where it is declared. If the method has the access modifier:

A method can have at most one access modifier keyword, otherwise a compile error occurs.

Other Keywords

final The final modifier, when used on a method, disables the ability for any subclasses to override the method. Despite having no effect, it may be used on private methods. It cannot be used on declarations directly in an interface, nor on any method declared abstract or native.
static The static modifier causes a method to be a class method rather than an instance method. Resultingly, the method may not refer to this, super, or any other instance methods or instance fields without qualifying such references with an explicit instance. This is in contrast to instance methods, which, in their bodies, may access instance-specific members without explicit qualification. Instance methods additionally may refer to this and super.
strictfp The strictfp modifier causes all floating point computations written in the body of a so modified method to consistently operate on the normal float/double value set (depending on expression type) across implementations. See strictfp for details.
synchronized A synchronized method synchronizes on:
  • this, if the method is an instance method, or
  • the Class object of the class of which it is a member, if the method is static

before it begins executing. The lock is released after the method completes.

abstract Abstract method declarations cannot provide a block statement body. abstract methods cannot be static, final, native, private, synchronized, nor strictfp. Any type containing an abstract method must also, itself, be abstract (interfaces are always inherently abstract). abstract types cannot be instantiated directly; only concrete sub-types can be instantiated directly.

Any non-abstract sub-type that inherits an abstract method from its immediate parent must override the abstract method and provide an implementation:

native The native modifier is used to declare a method whose implementation is provided through some other language, typically C or C++. Such methods' implementations are connected to the program through the Java Native Interface.

The native modifier cannot be used with strictfp or abstract, although it can be used with other modifiers.

default The default modifier allows a method declared in an interface to include an implementation. default methods are not abstract. A method declaration containing default must include a block as its body.

Return Type

A method's return type determines the type of the method invocation that invokes it. Specifically, any method invocation (e.g. doSomething()) will be an expression whose type is the same as the return type of the method it invokes. For example, the following method's return type is int:

int getRandomNumber() {	return (int) (Math.random() * 10);	}// Method declaration

so the expression that invokes it (getRandomNumber()) is an int expression, that can be assigned to an int variable:

int result = getRandomNumber();

If a method has a return type other than void, it is required to execute at least 1 return statement at the end of every possible path of normal execution that does not throw a value.

With a finally block, a method may execute multiple return statements. In such a case, the last abrupt exit from execution of the method takes precedence, i.e., the final return or throw that is executed is what the method completes with:
int test() {
	try {
		return 5;
	} finally {
		return 10;
	}
}
A call to test() returns 10, even though the return 5 does in fact get executed.
If a path of execution does not end, (e.g., if you have an infinite while or for loop), then that path is not required to return a value:
int neverEndingMethod() {
	while(true);
	
	// No return statement required.
	// In fact, writing a return statement here is a compile time error, since the return statement would be unreachable.
}

Parameter List

A method's parameters are a list of inputs that must be provided upon a call to the method. The variables declared in the parameter list are accessible throughout the body of the method and are given values when the method is called.

Overloads

A method may be overloaded if there are two or more method declarations with its same name, but different signatures. A method's signature includes the ordered list of types of its parameters, and methods with different ordered lists of parameter types are considered different when resolving overloads to execute a method invocation statement. The presence of a receiver parameter has no effect on overload resolution, since method invocation statements cannot provide an argument for that parameter explicitly.

Overload resolution happens statically, so the type of the expression used as an argument in the method invocation is what determines the method to be called.

Receiver Parameter

The receiver parameter is a parameter whose type is that of the class the instance method belongs to and whose parameter name is the keyword this. It may be annotated as any other parameter, but it otherwise has no effect on the code; it does not change the method's signature or affect overloading or invoking. The this keyword is innately accessible in instance methods without needing to be declared.

Var-args Parameter

The final parameter in a parameter list can optionally be a variable-arity parameter, by succeeding its type with the ... token. Such a parameter allows any number of arguments, of the parameter's type, to be provided in a method invocation statement as the final arguments in the statement's argument list. An array is automatically created from the arguments provided, and in the body of the method, the varargs parameter is useable in the exact same way as an array.

Type Parameters

Generic type parameters declared in the type-parameter-list can be referenced throughout the remainder of the method, including in the method's parameter-list, body, and even the method's throw-list. Any type parameters declared in the type-parameter-list may also be referenced anywhere else within the type-parameter-list. This allows for recursive type parameters.

Type parameters in the parameter-list

When used as a type in the parameter-list, a type parameter's upper bound is used for overload resolution.

   <T> void test(T input) {	}
// void test(Object input) {	} // Compiler error, generic method test(T) conflicts with this method.

This conflict can be avoided in cases where it is possible to upper-bound the parameter:

   <T extends Dog> void test(T input) {	}
   void test(Object input) {	} // Does not conflict; test(T) cannot be called with ANY object.

However, the upper bound, if provided explicitly, can still conflict with other methods:

   <T extends Dog> void test(T input) {	}
// void test(Dog input) {	} // Conflicts with test(T), since T's upper bound is Dog.

Type parameters in the throw-list

Type parameters may also be used in the throw-list to declare a method that throws a generic exception. This is only possible if the type parameter being used is bounded so that it is guaranteeably a type that can be thrown (Throwable or a subclass thereof).

Type parameters in the body

Type parameters can be used as any other type within the body of the method they are declared in.

array-dims Component

The array-dims is a legacy syntactical element that allows the pairs of brackets ([]), used to declare a dimension of an array type for the return type of the method, to be placed after the parameter list of a method declaration. It can consist of any number of pairs of brackets. The array dims specified in this location are applied to the return type as if appended to the return type.

throws Clause

The throws clause declares what exceptions a method can throw. Any checked exceptions that are thrown in the method body under any path of execution must be assignable to at least one of the types listed in the throws clause, otherwise, the code will not compile. The same exception may be listed in the throw-list more than once. Additionally, RuntimeExceptions and Errors, (both of which are the root unchecked types), and their subtypes, do not need to be listed, but may be.

The rules governing required types in the throw-list apply only to checked exceptions that the method may abruptly terminate due to the throwing of. Because of this, if a checked exception is thrown and caught within the method's body, the exception does not need to be listed in the throws clause.

Examples

static Method Restrictions

Example of inability to reference instance properties from a static method body:

class Dog {
	static Dog someDogInstance = new Dog();

	int age; // Instance field
	static void grow() {
		// age++; // Not allowed; no Dog instance to increment the age of.
		
		Dog x = new Dog(); // Make a Dog instance.
		x.age++;
		
		someDogInstance.age++;
	}
	
	void growThisDog() {
		age++; // Instance methods are called with an instance, and that instance is used implicitly here in the method body.
		this.age++; // Equivalent to age++
		
		someDogInstance.age++; // Instance methods can still access static data directly, but not vice versa.
	}
}

abstract Methods

An expression whose type is an abstract type can still be used to invoke abstract methods in that abstract type, though the implementation that gets executed depends on the concrete type of the value the expression evaluates to:

abstract class Animal {
	abstract void makeSound();
}

class Dog extends Animal {
	void makeSound() {
		System.out.println("Woof");
	}
}

class Cat extends Animal {
	void makeSound() {
		System.out.println("Meow");
	}
}

class Test {
	void test() {
		Animal a = Math.random() < .5 ? new Dog() : new Cat();
		a.makeSound(); // May print "Meow" or "Woof"
	}
}

abstract methods must be implemented by any concrete subclasses that inherit them:

abstract class A {
	abstract void test();
}

class Sub extends A {

	// Sub must implement test since Sub is not an abstract class, test is abstract, and test was inherited by Sub from A.
	@Override void test() {
		System.out.println("Sub");
	}
}

class Test {
	static A getNewA() {
		return new Sub();
	}

	void test() {
		A obj = getNewA();
		obj.test();
	}
}

Method Invocations & Return Type

Example of how return type affects method invocation statements:

public class Test {
	static int x() {
		System.out.println("x");
		return 5;
	}
	
	static void y() {
		System.out.println("y");
	}
	
	public static void main(String[] args) {
		int a = x();
		// int b = y(); // Not allowed; y does not return anything.
		// String c = x(); // Not allowed; x does not return a String.
		
		System.out.println(a); // Prints 5.
	}
}

Example of return type imposing return requirements on method body:

int test() {
	if (Math.rand() > 0.5) {
		return 4;
	}

	// Function may reach this point without returning a value; this is not allowed.
}

Overload Resolution

Method parameters determine which method gets invoked by a method call:

void test(int x) {
	System.out.println("Test was called with the number: " + x);
}

void test(String x) {
	System.out.println("Test was called with: " + x);
}

void runTest() {
	test("abc");
}

The output of invoking runTest() is:

Test was called with: abc

Changing the type, but not the value, of an expression used as an argument for a method, can cause a different overload to be invoked:

void test(String x) {
	System.out.println("STRING VERSION CALLED");
}
void test(Object x) {
	System.out.println("OBJECT VERSION CALLED");
}

void runTest() {
	test((Object) "abc"); // Provide a String, but casted to type "Object."
}

In the above example, test(Object) will be invoked with a String as its argument, leading to the following output:

OBJECT VERSION CALLED

Varargs Parameter

Any number of arguments can be provided in place of a varargs parameter:

void test(String... args) {
	System.out.println("TEST was called with: " + args.length + " argument(s).");
}

void runTest() {
	test(); // Provide no arguments.
	test("a", "b", "c"); // Provide three arguments.
	test("a"); // Provide one argument.
}

A call to runTest() prints:

TEST was called with: 0 argument(s).
TEST was called with: 3 argument(s).
TEST was called with: 1 argument(s).

A var-args parameter can also be unambiguously provided an array argument:

test(new String[] {"a", "b", "c"});

Such method call prints:

TEST was called with: 3 argument(s).

Type Parameters and throws Clause

Use of a type parameter in a throws clause:

<T extends Exception> void doSomethingThenThrow(T exception) throws T {
	if (Math.random() < 0.5)
		throw exception;
}

This can allow callers to have to handle checked exceptions only when calling the method with checked exceptions:

doSomethingThenThrow(new RuntimeException()); // This invocation effectively declares "throws RuntimeException", so no need to wrap in try-catch.
try {
	doSomethingThenThrow(new Exception()); // This invocation effectively declares "throws Exception"
} catch (Exception e) {

}

array-dims Usage

Methods declared with array-dims

// Two total array dims, both after parameter list.
int x()[][] {
	return new int[1][1];
}

// Three total array dims: two in return type, one after parameter-list.
int[][] x()[] {
	return new int[1][1][1];
}

throwing Without throws Clause

A throws clause is only necessary if the method may terminate with a checked exception:

void riskyMethod() throws Exception {
	if (Math.random() < .1)
		throw new Exception("Throwing an exception!");
}

void test() {
	try {
		riskyMethod();
	} catch (Exception e) {
		// Ignore the exception!
	}
}

The above example compiles since the body of the method test() does not have any code deemed to potentially complete abruptly due to a checked exception. If the try-catch were not used to wrap the call to riskyMethod(), Exception or its supertype, Throwable, would need to be listed in the throw-list.

Notes

  1. The final modifier never has an effect on private methods, since such instance methods are not dynamically bound, and since such static methods cannot be overridden anyway.
  2. Type parameters can be used to throw a checked exception in an unchecked fashion by allowing the type of the expression provided to a throw statement to be an unchecked exception while the value itself remains a checked exception. This can be done by casting a checked exception to a generic type parameter, and then throwing the result:

    public static @SuppressWarnings("unchecked") <E extends Throwable> void throwUnchecked(Throwable t) throws E {
    	throw (E) t;
    }

    Any call to the method that does not explicitly provide type arguments will infer E to be RuntimeException, however, the exception provided to the method will still be thrown, even if it is a Throwable instance.

    The cast to E does not fail, since E is upper-bounded by Throwable and the argument to the method is any Throwable instance.

  3. The compiler prefers method overloads that are closer to the type of the expression being used to invoke a method. A call can be ambiguous if two or more overloads can plausibly be called by a single method invocation statement. There are many cases where this can happen, most often with a var-args overload or a null argument, but sometimes with overloads whose problematic parameters are sibling types in a type hierarchy:

    void varargs(int... x) {	}
    void varargs(Integer... x) {	}

    Calling varargs(1) will fail. The caller is required to provide an array explicitly to pick which method is desired, such as through: varargs(new int[] {1}).

    Another example, involving a class hierarchy:

    // Wrapper class
    public class AmbiguityTest {
    	interface X {	}
    	interface Y {	}
    	class Z implements X, Y {	}
    	
    	static void m(X x) {	}
    	static void m(Y y) {	}
    	
    	public static void main(String[] args) {
    		// m(new Z()); // Ambiguous method call; expression of type Z is equidistant from both X and Y. (X & Y are the types of the parameters of the two method overloads of m.)
    	}
    }

    Such can be resolved using a cast, e.g. m((X) new Z()) will invoke m(X). If a method overload is introduced to explicitly accept a Z parameter, such overload will be used instead, since the method invocation has an argument of type Z.

    The null literal can often lead to ambiguity, because it can directly exist as an expression of any reference type:

    // Wrapper class
    public class AmbiguityTest {
    	interface X {	}
    	interface Y {	}
    	class Z implements X, Y {	}
    	
    	static void m(X x) {	}
    	static void m(Y y) {	}
    	
    	public static void main(String[] args) {
    		// m(null); // Ambiguous method call with null literal.
    	}
    }

    A cast can clarify the type of the expression null in the method call and resolve the ambiguity in this case as well. Adding a more specific overload in the class hierarchy will cause the method invocation to prefer that as well:

    // Wrapper class
    public class AmbiguityTest {
    	interface X {	}
    	interface Y {	}
    	class Z implements X, Y {	}
    	
    	static void m(X x) {	}
    	static void m(Y y) {	}
    	static void m(Z z) {	}
    	
    	public static void main(String[] args) {
    		m(null); // Calls m(Z)
    	}
    }

    However, if there is no overload that is a subtype of other overloads, the ambiguity will persist:

    // Wrapper class
    public class AmbiguityTest {
    	static void m(String x) {	}
    	static void m(Number y) {	}
    	
    	public static void main(String[] args) {
    		// m(null); // Ambiguous
    	}
    }