Java · lesson 10 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
11Methods in Java
Learn Java methods by running them: parameters, return values, records for multiple results, pass-by-value, scope, varargs and recursion.
By SkillByExample · Updated
A method is a named, reusable block of code: it takes parameters, runs, and optionally
returns a value. Every example is a single file run with the JDK 21 source launcher
(java File.java).
Anatomy of a method
public class Anatomy {
// modifiers return name parameters
public static double average(int a, int b) {
double result = (a + b) / 2.0;
return result;
}
public static void main(String[] args) {
double avg = average(3, 8);
System.out.println("average = " + avg);
System.out.println(average(10, 10));
}
}
$ java Anatomy.java
average = 5.5
10.0
The call average(3, 8) copies 3 and 8 into the parameters a and b, runs the body, and
the return value replaces the call expression. Dividing by 2.0 rather than 2 forces
floating-point division — (3 + 8) / 2 would give 5.
void methods and returning early
A void method returns nothing; a bare return still exits it early. Methods with a return type
can return from several places.
public class Early {
static void printPositive(int n) {
if (n <= 0) {
System.out.println("skipped " + n);
return; // leave now
}
System.out.println("value " + n);
}
static String grade(int score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
return "C";
}
public static void main(String[] args) {
printPositive(5);
printPositive(-2);
System.out.println(grade(95) + grade(85) + grade(40));
}
}
$ java Early.java
value 5
skipped -2
ABC
The compiler checks that every path of a non-void method ends in a return (or a throw). Delete
the last return "C"; and grade stops compiling with “missing return statement”.
Returning several values with a record
A method has one return type, so bundle related results in a record.
public class MinMaxDemo {
record MinMax(int min, int max) {}
static MinMax minMax(int[] values) {
int min = values[0], max = values[0];
for (int v : values) {
if (v < min) min = v;
if (v > max) max = v;
}
return new MinMax(min, max);
}
public static void main(String[] args) {
MinMax r = minMax(new int[] {4, -2, 9, 7});
System.out.println(r);
System.out.println("range = " + (r.max() - r.min()));
}
}
$ java MinMaxDemo.java
MinMax[min=-2, max=9]
range = 11
The record gives each value a name and type, and its generated toString makes the result easy to
print while debugging.
Pass-by-value: primitives and objects
Java always copies the argument. For an object, what gets copied is the reference.
public class PassByValue {
static void tryToDouble(int x) {
x = x * 2; // changes the copy only
}
static void appendBang(StringBuilder sb) {
sb.append("!"); // follows the reference to the shared object
sb = new StringBuilder("new"); // repoints the copy; caller unaffected
}
public static void main(String[] args) {
int n = 5;
tryToDouble(n);
System.out.println("n = " + n);
StringBuilder msg = new StringBuilder("hi");
appendBang(msg);
System.out.println("msg = " + msg);
}
}
$ java PassByValue.java
n = 5
msg = hi!
n is untouched because tryToDouble changed its own copy. msg changed because the method used
its copy of the reference to modify the one shared StringBuilder — but the reassignment to "new"
was invisible to main.
Scope: variables live inside their braces
A variable exists from its declaration to the end of the block that contains it.
public class Scope {
static int counter = 0; // class field: visible in every method
static void increment() {
int step = 1; // local to increment()
counter += step;
}
public static void main(String[] args) {
increment();
increment();
for (int i = 0; i < 2; i++) {
int doubled = i * 2; // exists only inside the loop body
}
System.out.println(counter);
System.out.println(step);
}
}
$ java Scope.java
Scope.java:16: error: cannot find symbol
System.out.println(step);
^
symbol: variable step
location: class Scope
1 error
error: compilation failed
step belongs to increment’s body, so main cannot see it — and the same would be true of i
or doubled after the loop. Remove the last line and the program prints 2.
Varargs: any number of arguments
int... nums accepts zero or more ints and hands them to the method as an int[].
public class Varargs {
static int sum(int... nums) {
int total = 0;
for (int n : nums) total += n;
return total;
}
static String join(String sep, String... parts) {
return String.join(sep, parts);
}
public static void main(String[] args) {
System.out.println(sum());
System.out.println(sum(1, 2, 3));
System.out.println(sum(new int[] {10, 20}));
System.out.println(join("-", "a", "b", "c"));
}
}
$ java Varargs.java
0
6
30
a-b-c
A varargs parameter must come last, and a method can have only one. You can also pass an existing array directly, as the third call shows.
Recursion
A recursive method calls itself on a smaller input until it reaches a base case.
public class Recursion {
static long factorial(int n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // smaller problem
}
static int fib(int n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
public static void main(String[] args) {
System.out.println("5! = " + factorial(5));
System.out.println("20! = " + factorial(20));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) sb.append(fib(i)).append(' ');
System.out.println(sb.toString().trim());
}
}
$ java Recursion.java
5! = 120
20! = 2432902008176640000
0 1 1 2 3 5 8 13 21 34
factorial(5) waits on factorial(4), which waits on factorial(3), down to the base case; the
results multiply on the way back up. 20! still fits in a long; 21! would overflow silently.
Static vs instance methods
A static method belongs to the class. An instance method runs on an object and can use its fields.
public class Counter {
private int count;
void increment() { count++; } // instance: uses this.count
int getCount() { return count; }
static int add(int a, int b) { // static: no object needed
return a + b;
}
public static void main(String[] args) {
System.out.println(Counter.add(3, 4));
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment();
c1.increment();
c2.increment();
System.out.println(c1.getCount() + " " + c2.getCount());
}
}
$ java Counter.java
7
2 1
Each Counter object has its own count, so c1 and c2 differ. add touches no field, so it is
called on the class. Calling increment() directly from static main would not compile — there
is no this there.
Practice
- Write
greet(String name)and an overloadgreet(String name, String title)so thatgreet("Nguyen", "Dr.")returns"Hello, Dr. Nguyen!", with the one-argument version delegating to the two-argument one.
Solution
public class Greet {
static String greet(String name) {
return greet(name, "");
}
static String greet(String name, String title) {
String who = title.isEmpty() ? name : title + " " + name;
return "Hello, " + who + "!";
}
public static void main(String[] args) {
System.out.println(greet("Nguyen", "Dr."));
System.out.println(greet("Sam"));
}
}
$ java Greet.java
Hello, Dr. Nguyen!
Hello, Sam!
Java picks the overload from the number of arguments, and only one version holds the formatting logic.
- Write a varargs method
maxOf(int first, int... rest)that returns the largest argument. Why is the separatefirstparameter useful?
Solution
public class MaxOf {
static int maxOf(int first, int... rest) {
int max = first;
for (int n : rest) 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(-5));
}
}
$ java MaxOf.java
9
-5
Requiring first means maxOf() with no arguments does not compile, so there is never an empty
array with no maximum.
- Write
divide(int a, int b)returning a record with both quotient and remainder, and print the result for17and5.
Solution
public class Divide {
record DivResult(int quotient, int remainder) {}
static DivResult divide(int a, int b) {
return new DivResult(a / b, a % b);
}
public static void main(String[] args) {
DivResult r = divide(17, 5);
System.out.println(r.quotient() + " remainder " + r.remainder());
System.out.println(r);
}
}
$ java Divide.java
3 remainder 2
DivResult[quotient=3, remainder=2]
Same shape as MinMax: one call, two named values back.
tryToDoublein the pass-by-value example has no effect on the caller. Rewrite it so the caller ends up with10, without changing how Java passes arguments.
Solution
public class DoubleIt {
static int doubleIt(int x) {
return x * 2;
}
public static void main(String[] args) {
int value = 5;
value = doubleIt(value);
System.out.println(value);
}
}
$ java DoubleIt.java
10
The method still sees only a copy; the change is that it returns the new value and the caller assigns it to its own variable.
- Write a recursive
power(int base, int exp)and print2^0,2^10and3^4.
Solution
public class Power {
static long power(int base, int exp) {
if (exp == 0) return 1;
return base * power(base, exp - 1);
}
public static void main(String[] args) {
System.out.println(power(2, 0));
System.out.println(power(2, 10));
System.out.println(power(3, 4));
}
}
$ java Power.java
1
1024
81
exp == 0 is the base case, and each call reduces exp by one, so the recursion always reaches it.