Java · lesson 42 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
11Also part of: JUnit
Gradle — Modern Java Build Tool
Learn Gradle, the fast and flexible Java build tool: Kotlin DSL build scripts, the wrapper, dependencies, tests, custom tasks and multi-project builds.
By SkillByExample · Updated
Gradle builds Java projects from a script written in Kotlin (or Groovy) instead of XML. It compiles,
tests and packages your code, downloads dependencies, and skips any task whose inputs haven’t
changed. Every command below was run for real with Gradle 8.14.3 on JDK 21 in an empty
directory; timings in the BUILD SUCCESSFUL in … lines vary from run to run.
A minimal project
Gradle uses the same layout as Maven: src/main/java and src/test/java. Two build files sit at
the root. settings.gradle.kts names the project:
// File: settings.gradle.kts
rootProject.name = "my-app"
build.gradle.kts applies plugins and declares dependencies. The application plugin adds a
run task; it also brings in the java plugin.
// File: build.gradle.kts
plugins {
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.4.8-jre")
}
application {
mainClass = "com.example.App"
}
// File: src/main/java/com/example/App.java
package com.example;
import com.google.common.base.Joiner;
import java.util.Arrays;
public class App {
static String greet(String... names) {
return "Hello, " + Joiner.on(" and ").skipNulls().join(Arrays.asList(names)) + "!";
}
public static void main(String[] args) {
System.out.println(greet("Alice", null, "Bob"));
}
}
The wrapper, then the first run
Generate the wrapper once with an installed gradle; from then on everyone uses ./gradlew, which
downloads the pinned Gradle version on first use.
$ gradle wrapper --gradle-version 8.14.3 -q
$ ls
build.gradle.kts
gradle
gradlew
gradlew.bat
settings.gradle.kts
src
$ ./gradlew run --console=plain
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :run
Hello, Alice and Bob!
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
run depended on compileJava, so Gradle compiled first, resolving Guava from Maven Central
(this machine already had the Gradle distribution and Guava cached, so nothing was downloaded; on a fresh
machine the first ./gradlew also prints a download progress bar). skipNulls() dropped the null name. Commit gradlew, gradlew.bat and the
gradle/ directory so CI and teammates get exactly this Gradle version.
Incremental builds: UP-TO-DATE
Run the same task again without changing anything:
$ ./gradlew run --console=plain
> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :run
Hello, Alice and Bob!
BUILD SUCCESSFUL in 847ms
2 actionable tasks: 1 executed, 1 up-to-date
compileJava was skipped because its inputs (sources, classpath, compiler settings) were
unchanged. run always executes because it has no declared outputs to compare.
Tests with JUnit 5
Add test dependencies and tell the test task to use the JUnit Platform. The junit-bom
platform pins matching versions for the JUnit artifacts.
// File: build.gradle.kts
plugins {
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.4.8-jre")
testImplementation(platform("org.junit:junit-bom:5.12.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
application {
mainClass = "com.example.App"
}
tasks.test {
useJUnitPlatform()
testLogging { events("passed", "failed") }
}
// File: src/test/java/com/example/AppTest.java
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class AppTest {
@Test void skipsNulls() { assertEquals("Hello, Alice and Bob!", App.greet("Alice", null, "Bob")); }
@Test void singleName() { assertEquals("Hello Alice!", App.greet("Alice")); }
}
$ ./gradlew test --console=plain
> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test FAILED
AppTest > singleName() FAILED
org.opentest4j.AssertionFailedError at AppTest.java:9
AppTest > skipsNulls() PASSED
2 tests completed, 1 failed
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///tmp/demo/build/reports/tests/test/index.html
* Try:
> Run with --scan to get full insights.
BUILD FAILED in 1s
3 actionable tasks: 2 executed, 1 up-to-date
The second test expected a missing comma. A failing test fails the whole build, and Gradle writes
an HTML report under build/reports/tests/. Fix the expectation and run again:
// File: src/test/java/com/example/AppTest.java
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class AppTest {
@Test void skipsNulls() { assertEquals("Hello, Alice and Bob!", App.greet("Alice", null, "Bob")); }
@Test void singleName() { assertEquals("Hello, Alice!", App.greet("Alice")); }
}
$ ./gradlew test --console=plain
> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
AppTest > singleName() PASSED
AppTest > skipsNulls() PASSED
BUILD SUCCESSFUL in 1s
3 actionable tasks: 2 executed, 1 up-to-date
Only the changed test class was recompiled; compileJava stayed UP-TO-DATE. The per-test
PASSED/FAILED lines come from the testLogging block. ./gradlew build runs test too, so
a red test also stops packaging.
Dependency configurations
Where a dependency goes decides which classpath it lands on.
| Configuration | Compile | Runtime | Use for |
|---|---|---|---|
implementation | yes | yes | normal dependencies (default choice) |
api (needs java-library) | yes, and exported | yes | types that appear in your library’s public API |
compileOnly | yes | no | annotation-only libraries, provided APIs |
runtimeOnly | no | yes | JDBC drivers, logging backends |
testImplementation | tests | tests | JUnit, Mockito |
dependencies shows what was actually resolved, including transitive dependencies:
$ ./gradlew dependencies --configuration runtimeClasspath -q
------------------------------------------------------------
Root project 'my-app'
------------------------------------------------------------
runtimeClasspath - Runtime classpath of source set 'main'.
\--- com.google.guava:guava:33.4.8-jre
+--- com.google.guava:failureaccess:1.0.3
+--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
+--- org.jspecify:jspecify:1.0.0
+--- com.google.errorprone:error_prone_annotations:2.36.0
\--- com.google.j2objc:j2objc-annotations:3.0.0
A web-based, searchable dependency report is available by adding the --scan option.
One line in build.gradle.kts brought in six artifacts. When two versions of a library meet,
this tree shows which one won (marked ->).
Custom tasks
Tasks are code. register creates one lazily, doLast holds the action, and dependsOn orders
tasks.
// File: build.gradle.kts
plugins {
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.4.8-jre")
}
application {
mainClass = "com.example.App"
}
tasks.register("hello") {
group = "custom"
description = "Prints a greeting"
doLast { println("Hello from Gradle ${gradle.gradleVersion}") }
}
tasks.register<Zip>("zipSources") {
dependsOn("hello")
from("src/main/java")
archiveFileName = "sources.zip"
destinationDirectory = layout.buildDirectory.dir("dist")
}
$ ./gradlew zipSources --console=plain
> Task :hello
Hello from Gradle 8.14.3
> Task :zipSources
BUILD SUCCESSFUL in 947ms
2 actionable tasks: 2 executed
$ ./gradlew zipSources --console=plain
> Task :hello
Hello from Gradle 8.14.3
> Task :zipSources UP-TO-DATE
BUILD SUCCESSFUL in 861ms
2 actionable tasks: 1 executed, 1 up-to-date
$ ./gradlew tasks --group custom -q
------------------------------------------------------------
Tasks runnable from root project 'my-app'
------------------------------------------------------------
Custom tasks
------------
hello - Prints a greeting
To see all tasks and more detail, run gradlew tasks --all
To see more detail about a task, run gradlew help --task <task>
zipSources is a typed Zip task, so Gradle knows its inputs and output and can skip it the
second time. hello has no declared outputs, so it runs every time. --group custom lists only that group;
plain ./gradlew tasks hides ungrouped tasks such as zipSources unless you add --all.
Multi-project builds
A settings file can include several subprojects; one can depend on another with project(":name").
// File: multi/settings.gradle.kts
rootProject.name = "shop"
include("core", "web")
// File: multi/core/build.gradle.kts
plugins { `java-library` }
// File: multi/core/src/main/java/shop/core/Prices.java
package shop.core;
public class Prices {
public static String format(long cents) { return String.format("$%d.%02d", cents / 100, cents % 100); }
}
// File: multi/web/build.gradle.kts
plugins { application }
dependencies { implementation(project(":core")) }
application { mainClass = "shop.web.Main" }
// File: multi/web/src/main/java/shop/web/Main.java
package shop.web;
import shop.core.Prices;
public class Main {
public static void main(String[] args) { System.out.println("Total: " + Prices.format(2599)); }
}
$ ./gradlew -p multi :web:run --console=plain
> Task :core:compileJava
> Task :core:processResources NO-SOURCE
> Task :core:classes
> Task :core:jar
> Task :web:compileJava
> Task :web:processResources NO-SOURCE
> Task :web:classes
> Task :web:run
Total: $25.99
BUILD SUCCESSFUL in 1s
4 actionable tasks: 4 executed
Asking for :web:run made Gradle build core first and put its jar on web’s classpath. Editing
only web later recompiles only web. -p multi points the wrapper at the build in the multi
directory.
Common commands
| Command | Does |
|---|---|
./gradlew build | compile, test, and assemble jars |
./gradlew test | run tests |
./gradlew run | run the application (application plugin) |
./gradlew clean | delete build/ |
./gradlew build -x test | build but skip the test task |
./gradlew tasks | list tasks |
./gradlew dependencies | show resolved dependency trees |
Practice
- Add a
runtimeOnlydependency onorg.slf4j:slf4j-simple:2.0.17and confirm withdependenciesthat it is on the runtime classpath but not the compile classpath.
Solution
// File: build.gradle.kts
plugins {
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.4.8-jre")
runtimeOnly("org.slf4j:slf4j-simple:2.0.17")
}
application {
mainClass = "com.example.App"
}
$ ./gradlew dependencies --configuration compileClasspath -q | grep slf4j
$ ./gradlew dependencies --configuration runtimeClasspath -q | grep slf4j
\--- org.slf4j:slf4j-simple:2.0.17
\--- org.slf4j:slf4j-api:2.0.17
The first grep finds nothing: code can’t import anything from a runtimeOnly dependency, but
the jar is there when the program runs — the right scope for a logging backend or JDBC driver.
- Pass command-line arguments through
./gradlew runand print them frommain.
Solution
// File: src/main/java/com/example/App.java
package com.example;
import com.google.common.base.Joiner;
import java.util.Arrays;
public class App {
static String greet(String... names) {
return "Hello, " + Joiner.on(" and ").skipNulls().join(Arrays.asList(names)) + "!";
}
public static void main(String[] args) {
System.out.println(args.length == 0 ? greet("World") : greet(args));
}
}
$ ./gradlew run -q --args="Ada Grace"
Hello, Ada and Grace!
--args is split on spaces and passed to main; quote a single argument containing spaces
inside it (--args="'Ada Lovelace'").
- Restore the JUnit build file from the tests section, run
./gradlew buildtwice, and explain which tasks run the second time.
Solution
// File: build.gradle.kts
plugins {
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.4.8-jre")
testImplementation(platform("org.junit:junit-bom:5.12.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
application {
mainClass = "com.example.App"
}
tasks.test {
useJUnitPlatform()
}
$ ./gradlew build --console=plain -q
$ ./gradlew build --console=plain
> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :jar UP-TO-DATE
> Task :startScripts UP-TO-DATE
> Task :distTar UP-TO-DATE
> Task :distZip UP-TO-DATE
> Task :assemble UP-TO-DATE
> Task :compileTestJava UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test UP-TO-DATE
> Task :check UP-TO-DATE
> Task :build UP-TO-DATE
BUILD SUCCESSFUL in 867ms
7 actionable tasks: 7 up-to-date
Nothing changed, so every task — including test — is skipped. build also produced
build/distributions/*.zip and .tar because the application plugin adds distribution tasks.
- Add a
shouttask that depends onhelloand prints in upper case, then run onlyshout.
Solution
// File: build.gradle.kts
plugins {
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.4.8-jre")
}
application {
mainClass = "com.example.App"
}
tasks.register("hello") {
doLast { println("Hello from Gradle") }
}
tasks.register("shout") {
dependsOn("hello")
doLast { println("HELLO FROM GRADLE!") }
}
$ ./gradlew shout -q
Hello from Gradle
HELLO FROM GRADLE!
dependsOn makes hello run first even though only shout was requested.