Java · lesson 8 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Functions (Methods) in Java
Master Java methods — declarations, static methods, pass-by-value, varargs, recursion, lambda expressions, method references and functional interfaces.
By SkillByExample · Updated
Methods — The Building Blocks of Behaviour
A method is a named block of code that performs a specific task. Methods let you write logic once and reuse it, making programs easier to read, test, and maintain. Every runnable example here is a single file run with the JDK 21 source launcher (java File.java).
// Basic method anatomy
returnType methodName(parameterType paramName, ...) {
// body
return value; // required if returnType is not void
}
public class Calculator {
// void — returns nothing
public void printResult(int result) {
System.out.println("Result: " + result);
}
// returns an int
public int add(int a, int b) {
return a + b;
}
// returns a double
public double divide(double numerator, double denominator) {
if (denominator == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
return numerator / denominator;
}
public static void main(String[] args) {
Calculator calc = new Calculator();
int sum = calc.add(10, 25);
calc.printResult(sum);
System.out.println(calc.divide(10.0, 4.0));
}
}
$ java Calculator.java
Result: 35
2.5
printResult is void, so it does its work by printing; add and divide return values the
caller can store or print. divide uses double parameters, which is why 10.0 / 4.0 gives
2.5 rather than the 2 that int division would.
Access Modifiers and Static Methods
| Modifier | Visible to |
|---|---|
public | Everyone |
protected | Same package + subclasses |
private | Same class only |
| (none) | Same package only |
public class MathUtilsDemo {
public static void main(String[] args) {
System.out.println(MathUtils.square(7));
System.out.println(MathUtils.max(12, 8));
System.out.println(MathUtils.parity(6));
}
}
class MathUtils {
// static — belongs to the class, not an instance
// call as MathUtils.square(5), not new MathUtils().square(5)
public static int square(int n) {
return n * n;
}
public static int max(int a, int b) {
return a > b ? a : b;
}
// private helper — internal use only
private static boolean isEven(int n) {
return n % 2 == 0;
}
public static String parity(int n) {
return isEven(n) ? "even" : "odd";
}
}
$ java MathUtilsDemo.java
49
12
even
MathUtils.isEven is private, so only code inside MathUtils can call it; MathUtilsDemo
uses the public parity method instead. The static methods are called on the class name — no
new MathUtils() anywhere.
Parameters — Pass-by-Value
Java is always pass-by-value. For primitives, the method gets a copy. For objects, the method gets a copy of the reference (it can mutate the object’s state, but cannot reassign the original variable).
public class PassByValueDemo {
// primitives — original is unchanged
static void doubleIt(int x) {
x = x * 2;
System.out.println("Inside: " + x);
}
// object reference — can mutate the object
static void addItem(java.util.List<String> list, String item) {
list.add(item); // modifies the actual list
}
public static void main(String[] args) {
int n = 10;
doubleIt(n);
System.out.println("After: " + n);
var names = new java.util.ArrayList<String>();
addItem(names, "Alice");
System.out.println(names);
}
}
$ java PassByValueDemo.java
Inside: 20
After: 10
[Alice]
n stays 10 after doubleIt(n) because x = x * 2 only reassigns the method’s local copy —
but addItem mutates the actual ArrayList object through its copied reference, which is why
names really does end up with "Alice" in it. Same pass-by-value rule, two different-looking
outcomes.
Varargs — Variable Argument Count
public class Stats {
// varargs — caller can pass any number of ints
public static int sum(int... numbers) {
int total = 0;
for (int n : numbers) total += n;
return total;
}
public static double average(double... values) {
if (values.length == 0) return 0.0;
double total = 0;
for (double v : values) total += v;
return total / values.length;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3));
System.out.println(sum(10, 20, 30, 40));
System.out.println(average(4.0, 7.5, 2.5));
}
}
$ java Stats.java
6
100
4.666666666666667
Inside the method, int... numbers is an ordinary int[], so the same sum accepts three or
four arguments. A varargs parameter must come last in the parameter list.
Recursion
A method that calls itself. Always needs a base case to stop.
public class Recursion {
// factorial: n! = n * (n-1)! base case: 0! = 1
public static long factorial(int n) {
if (n <= 0) return 1; // base case
return n * factorial(n - 1); // recursive case
}
// Fibonacci with memoization (avoid exponential blowup)
private static java.util.Map<Integer, Long> memo = new java.util.HashMap<>();
public static long fib(int n) {
if (n <= 1) return n;
if (memo.containsKey(n)) return memo.get(n);
long result = fib(n - 1) + fib(n - 2);
memo.put(n, result);
return result;
}
public static void main(String[] args) {
System.out.println(factorial(10));
System.out.println(fib(50));
}
}
$ java Recursion.java
3628800
12586269025
fib(50) without memoization would make roughly 40 billion recursive calls — the memo map
is what turns an exponential-time function into a linear one, caching each fib(n) the first
time it’s computed.
Functional Interfaces
A functional interface has exactly one abstract method. Java’s java.util.function package provides the most common ones:
import java.util.function.*;
public class FunctionalDemo {
public static void main(String[] args) {
// Predicate<T> — takes T, returns boolean
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Hi"));
System.out.println(isLong.test("Hello World"));
// Function<T, R> — takes T, returns R
Function<String, Integer> wordCount = s -> s.split("\\s+").length;
System.out.println(wordCount.apply("one two three"));
// Consumer<T> — takes T, returns void
Consumer<String> print = s -> System.out.println(">> " + s);
print.accept("Hello");
// Supplier<T> — takes nothing, returns T
Supplier<Double> random = Math::random;
System.out.println("random: " + random.get() + " (varies)");
// BiFunction<T, U, R> — takes two args, returns R
BiFunction<Integer, Integer, Integer> power = (base, exp) -> {
int result = 1;
for (int i = 0; i < exp; i++) result *= base;
return result;
};
System.out.println(power.apply(2, 10));
// Composing functions
Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus3 = x -> x + 3;
Function<Integer, Integer> times2Plus3 = times2.andThen(plus3);
System.out.println(times2Plus3.apply(5));
}
}
$ java FunctionalDemo.java
false
true
3
>> Hello
random: 0.8164651954186748 (varies)
1024
13
The random: line prints a different value every run — Math::random is a method reference to
a random source, unlike everything else on this page. times2.andThen(plus3) applies
left to right: 5 * 2 = 10, then 10 + 3 = 13, not (5 + 3) * 2.
Lambda Expressions
Lambdas are compact anonymous functions assignable to any functional interface:
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class LambdaDemo {
@FunctionalInterface
interface Transformer<T> {
T transform(T input);
}
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob", "Dave"));
// Lambda — concise
names.sort((a, b) -> a.compareTo(b));
// Method reference — even shorter
names.sort(String::compareTo);
System.out.println(names);
// Custom functional interface
Transformer<String> shout = s -> s.toUpperCase() + "!";
System.out.println(shout.transform("hello"));
// Predicate composition
Predicate<Integer> positive = n -> n > 0;
Predicate<Integer> even = n -> n % 2 == 0;
Predicate<Integer> positiveAndEven = positive.and(even);
List.of(-2, 0, 3, 4, 7, 8).stream()
.filter(positiveAndEven)
.forEach(System.out::println);
}
}
$ java LambdaDemo.java
[Alice, Bob, Charlie, Dave]
HELLO!
4
8
-2 and 0 fail positive; 3 and 7 fail even; only 4 and 8 satisfy both predicates
positiveAndEven chains together with and().
Method References
A cleaner syntax for lambdas that simply delegate to an existing method:
| Form | Syntax | Lambda equivalent |
|---|---|---|
| Static method | ClassName::staticMethod | x -> ClassName.staticMethod(x) |
| Instance on specific object | instance::method | x -> instance.method(x) |
| Instance on arbitrary object | ClassName::instanceMethod | (obj, x) -> obj.method(x) |
| Constructor | ClassName::new | args -> new ClassName(args) |
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class MethodRefDemo {
static int doubleIt(int n) { return n * 2; }
record Person(String name, int age) {}
public static void main(String[] args) {
// 1. Static method reference
Function<Integer, Integer> dbl = MethodRefDemo::doubleIt;
System.out.println(dbl.apply(7));
// 2. Instance method on a particular instance
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;
System.out.println(greet.apply("World"));
// 3. Instance method on an arbitrary instance of the type
Function<String, String> upper = String::toUpperCase;
System.out.println(upper.apply("java"));
// 4. Constructor reference
BiFunction<String, Integer, Person> makePerson = Person::new;
Person p = makePerson.apply("Alice", 30);
System.out.println(p);
// Real-world: print a list
List<String> words = List.of("foo", "bar", "baz");
words.forEach(System.out::println);
// Collect to uppercase list
List<String> upperWords = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperWords);
}
}
$ java MethodRefDemo.java
14
Hello, World
JAVA
Person[name=Alice, age=30]
foo
bar
baz
[FOO, BAR, BAZ]
Person[name=Alice, age=30] is a record’s auto-generated toString() — records get it, plus
equals()/hashCode(), for free from the two-field declaration alone.
Writing Good Methods
Keep each method to one job, name it for what it does, and group long parameter lists into a record. This small checkout follows all three:
import java.util.List;
public class Checkout {
record OrderRequest(String customer, List<Double> prices, double discountPercent) {}
static void validate(OrderRequest r) {
if (r.prices().isEmpty()) throw new IllegalArgumentException("empty order for " + r.customer());
}
static double subtotal(List<Double> prices) {
return prices.stream().mapToDouble(Double::doubleValue).sum();
}
static double applyDiscount(double amount, double percent) {
return amount * (1 - percent / 100);
}
static String placeOrder(OrderRequest r) {
validate(r);
double total = applyDiscount(subtotal(r.prices()), r.discountPercent());
return String.format("%s pays %.2f", r.customer(), total);
}
public static void main(String[] args) {
System.out.println(placeOrder(new OrderRequest("Alice", List.of(19.99, 5.01), 10)));
System.out.println(placeOrder(new OrderRequest("Bob", List.of(), 0)));
}
}
$ java Checkout.java
Alice pays 22.50
Exception in thread "main" java.lang.IllegalArgumentException: empty order for Bob
at Checkout.validate(Checkout.java:7)
at Checkout.placeOrder(Checkout.java:19)
at Checkout.main(Checkout.java:26)
placeOrder reads as a summary of the steps, and each helper can be tested on its own. Bob’s
empty order fails in validate, before any total is computed. The
OrderRequest record replaces three loose parameters, so a caller can’t swap the customer and
the discount by accident, and adding a field later doesn’t change every method signature.
Practice
- Write a varargs method
maxOf(int... nums)that returns the largest argument, and call it with five numbers and with one.
Solution
public class MaxOf {
static int maxOf(int... nums) {
int max = nums[0];
for (int n : nums) if (n > max) max = n;
return max;
}
public static void main(String[] args) {
System.out.println(maxOf(3, 7, 2, 9, 4));
System.out.println(maxOf(42));
System.out.println(maxOf());
}
}
$ java MaxOf.java
9
42
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at MaxOf.maxOf(MaxOf.java:3)
at MaxOf.main(MaxOf.java:11)
Inside the method, varargs is just an int[]. Calling it with no arguments is legal and passes an
empty array, so nums[0] fails; the signature maxOf(int first, int... rest) makes “at least one”
a compile-time rule.
- Write a recursive
gcd(a, b)using the Euclidean algorithm:gcd(a, 0) = a, otherwisegcd(a, b) = gcd(b, a % b).
Solution
public class Gcd {
static int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
public static void main(String[] args) {
System.out.println(gcd(48, 18));
System.out.println(gcd(17, 5));
}
}
$ java Gcd.java
6
1
One base case (b == 0) and one recursive case that always moves toward it, since a % b is
smaller than b.
- Combine two
Predicate<Integer>s withor()and print the result for -3, -2, 4, 5 and 7.
Solution
import java.util.List;
import java.util.function.Predicate;
public class OrPredicate {
public static void main(String[] args) {
Predicate<Integer> negative = n -> n < 0;
Predicate<Integer> even = n -> n % 2 == 0;
Predicate<Integer> negativeOrEven = negative.or(even);
List.of(-3, -2, 4, 5, 7).forEach(n -> System.out.println(n + ": " + negativeOrEven.test(n)));
}
}
$ java OrPredicate.java
-3: true
-2: true
4: true
5: false
7: false
or() needs only one side to be true: -3 passes negative, 4 passes even, -2 passes both.
- Sort
["kiwi", "apple", "fig"]by length withComparator.comparingand a method reference, then by length descending.
Solution
import java.util.*;
public class ByLength {
public static void main(String[] args) {
List<String> words = new ArrayList<>(List.of("kiwi", "apple", "fig"));
words.sort(Comparator.comparing(String::length));
System.out.println(words);
words.sort(Comparator.comparing(String::length).reversed());
System.out.println(words);
}
}
$ java ByLength.java
[fig, kiwi, apple]
[apple, kiwi, fig]
String::length is the “instance method on an arbitrary object” form: Comparator.comparing calls
it on each element being compared.
- Count how many times
fibactually computes a value (not served frommemo) when you callfib(50)twice.
Solution
import java.util.HashMap;
import java.util.Map;
public class FibCount {
static Map<Integer, Long> memo = new HashMap<>();
static int computed = 0;
static long fib(int n) {
if (n <= 1) return n;
if (memo.containsKey(n)) return memo.get(n);
computed++;
long result = fib(n - 1) + fib(n - 2);
memo.put(n, result);
return result;
}
public static void main(String[] args) {
System.out.println(fib(50) + " after " + computed + " computations");
System.out.println(fib(50) + " after " + computed + " computations");
}
}
$ java FibCount.java
12586269025 after 49 computations
12586269025 after 49 computations
Each of fib(2)…fib(50) is computed once; the static memo survives between calls, so the
second call does no new work.