Paper Plugin Guide
File mode0

Java basics

Making decisions: if, else and switch

Run different code depending on what is true right now.

Beginner45 min read

Plugins are full of questions: Is this a player? Do they have permission? Which block did they break? On this page you will learn how Java answers those questions with if, else and switch, and you will use them to build a small plugin with health warnings, rank prefixes, game mode rules and a launch pad.

Why plugins are full of decisions

So far, every program you have written runs every line, top to bottom, every time. Real programs need to choose. A plugin almost never does the same thing for everyone: admins get a red prefix, players without permission get a "no" message, and a pressure plate only launches you if it is the right kind of plate.

Here is a decision you will write in some form in almost every plugin you make:

A typical plugin decision
1if (player.hasPermission("myplugin.fly")) {2    player.setAllowFlight(true);3} else {4    player.sendRichMessage("<red>You are not allowed to fly.");5}
  1. Asks a yes-or-no question: does this player have the myplugin.fly permission?
  2. Runs only when the answer is yes.
  3. Everything in the else part runs only when the answer is no.

Read it out loud and it almost sounds like English: "if the player has permission, let them fly, otherwise tell them no." That is the whole idea of this page. The rest is learning the exact shapes Java accepts, and the mistakes that trip everyone up.

Your first if

An if statement runs a block of code only when a condition is true. Press the green run arrow to try it, then change 4.0 to 15.0 and run it again (in live mode).

FirstIf.javaRuns on Java 25
1void main() {2    double health = 4.0;3 4    if (health < 6.0) {5        IO.println("Your health is low. Eat something!");6    }7 8    IO.println("Health check finished.");9}
  1. A health value like a player's. In Minecraft, 20.0 is full health (10 hearts), so 4.0 is just 2 hearts.
  2. The keyword if, then a condition in round brackets, then an opening curly brace. The condition health < 6.0 is either true or false.
  3. This line is inside the braces, so it only runs when the condition is true.
  4. This line is after the closing brace, outside the if, so it always runs.

The condition must be a boolean: a value that is either true or false. Comparisons such as health < 6.0, coins >= price and level == 30 all produce a boolean, and so do methods like player.hasPermission(...) and player.isFlying(). You met these comparison operators in Operators and math.

How if and else choose a path The program reaches an if. It checks a condition. If the condition is true the if block runs, otherwise the else block runs. Then both paths join and the code continues. Code runs top to bottom until it reaches the if player.getHealth() < 6 ? true false The if block runs sendWarning(); The else block runs sendCheer(); Code after the if both paths meet here
An if statement is a fork in the road: when the condition is true, Java runs the block; when it is false, Java skips it. Either way, it continues after the closing brace.

The most common if mistake: = instead of ==

One equals sign stores a value (coins = 50 puts 50 into coins). Two equals signs compare values (coins == 50 asks "is coins 50?"). Mix them up and Java refuses to compile:

AssignInsteadOfCompare.javaRuns on Java 25
1void main() {2    int coins = 50;3 4    if (coins = 50) {5        IO.println("Exactly fifty coins!");6    }7}
  1. Wrong: this tries to store 50 instead of asking a question. The result is a number, not true or false, so Java complains.

The error says "int cannot be converted to boolean": Java expected a yes-or-no answer in the brackets and got a number. Whenever you see that message on an if line, look for a missing =.

if and else: two paths

Often you want to do one thing when the condition is true and a different thing when it is false. Add else and a second block:

IfElse.javaRuns on Java 25
1void main() {2    int coins = 35;3    int price = 50;4 5    if (coins >= price) {6        coins = coins - price;7        IO.println("You bought a diamond sword!");8        IO.println("Coins left: " + coins);9    } else {10        int missing = price - coins;11        IO.println("You need " + missing + " more coins.");12    }13}
  1. Does the player have enough coins? >= means "greater than or equal to", so exactly 50 coins is enough.
  2. Only runs when the purchase goes through. A block can hold as many lines as you like.
  3. Exactly one of the two blocks runs, never both and never neither.
  4. A variable created inside a block only exists inside that block. You will learn more about this in Methods.

This is the shape of every shop, every permission check and every "are you sure?" in a plugin: one path for yes, one for no.

else if: more than two choices

Health is not just "low" or "fine". You might want four or five levels. Chain extra checks with else if:

HealthLevels.javaRuns on Java 25
1void main() {2    double health = 7.5;3 4    if (health <= 0) {5        IO.println("You are dead.");6    } else if (health < 4) {7        IO.println("CRITICAL: find a bed or a golden apple!");8    } else if (health < 10) {9        IO.println("Hurt: be careful.");10    } else if (health < 20) {11        IO.println("Scratched: you will heal soon.");12    } else {13        IO.println("Full health.");14    }15}
  1. The first question. If it is true, Java runs this block and skips everything else in the chain.
  2. Only asked when every question above it was false. So when Java gets here, it already knows health is above 0.
  3. With health 7.5, this is the first true question, so "Hurt" is printed.
  4. The final else has no condition. It catches everything that did not match above. It is optional.
health is 7.5 Java asks each question from the top. The first true one wins. health <= 0 ? false "You are dead." health < 4 ? false "CRITICAL: ..." health < 10 ? true "Hurt: be careful." runs health < 20 ? skipped "Scratched: ..." else skipped "Full health."
Java walks down an else if chain from the top and stops at the first true condition. Only that one block runs; the rest are skipped, even if they would also be true.

The important rule: only the first true branch runs. Health 2.0 is less than 4, but it is also less than 10 and less than 20. Java does not care: it stops at the first match. That is why the order of the checks matters so much. Watch what happens when the order is wrong:

WrongOrder.javaRuns on Java 25
1void main() {2    double health = 2.0;3 4    if (health < 10) {5        IO.println("Hurt: be careful.");6    } else if (health < 4) {7        IO.println("CRITICAL: find a bed or a golden apple!");8    } else {9        IO.println("Healthy.");10    }11}
  1. Health 2.0 is less than 10, so this branch wins immediately...
  2. ...and this "critical" branch can never run, because anything below 4 was already caught by the line above.

In a plugin: a low-health warning

Time to use an else if chain in real plugin code. This listener runs every time something takes damage. If it is a player, it works out how much health they will have left and shows a warning in the action bar (the small text line above the hotbar):

HealthWarningListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-conditions-demosrcmainjavacomexamplejavaconditionsdemoHealthWarningListener.java

The package com.example.javaconditionsdemo is the folder path com/example/javaconditionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • java-conditions-demo/
    • src/main/
      • java/com/example/javaconditionsdemo/Package com.example.javaconditionsdemo
        • ConditionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • GameModeListener.javaListener: reacts to events
        • HealthWarningListener.javayou are hereListener: reacts to events
        • LaunchPadListener.javaListener: reacts to events
        • RankJoinListener.javaListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.javaconditionsdemo;2 3import net.kyori.adventure.text.Component;4import net.kyori.adventure.text.format.NamedTextColor;5import org.bukkit.entity.Player;6import org.bukkit.event.EventHandler;7import org.bukkit.event.Listener;8import org.bukkit.event.entity.EntityDamageEvent;9 10public final class HealthWarningListener implements Listener {11 12    @EventHandler13    public void onDamage(EntityDamageEvent event) {14        if (!(event.getEntity() instanceof Player player)) {15            return;16        }17        double healthAfter = player.getHealth() - event.getFinalDamage();18        if (healthAfter <= 0) {19            return;20        }21 22        if (healthAfter < 4) {23            player.sendActionBar(Component.text("CRITICAL: find a bed or a golden apple!", NamedTextColor.DARK_RED));24        } else if (healthAfter < 10) {25            player.sendActionBar(Component.text("Hurt: be careful.", NamedTextColor.GOLD));26        } else {27            player.sendActionBar(Component.text("Just a scratch.", NamedTextColor.YELLOW));28        }29    }30}
  1. An event handler: Paper calls this method every time any entity (a player, a cow, a zombie) is about to take damage. See Events and listeners.
  2. Damage can hit any entity, but we only care about players. This line says "if the entity is not a player, stop here". The section on instanceof below explains exactly how it works.
  3. The player's health now, minus the damage they are about to take. The event fires before the damage is applied, so we do the subtraction ourselves.
  4. If this hit is going to kill them, a warning is pointless, so the handler stops with return.
  5. The same ladder as the runnable example: most serious first.
  6. Shows the text in this player's action bar. Component.text makes a piece of colored text; Text and colors covers it.

When a zombie leaves you with 3 health, this appears above your hotbar:

CRITICAL: find a bed or a golden apple!

Combining conditions

Real questions often have several parts: "is the player an operator, or do they have the build permission and are they outside spawn?" Java joins conditions with three logical operators:

OperatorRead it asTrue whenExample
&&andboth sides are truelevel >= 10 && level < 20
||orat least one side is trueisOperator || isVip
!notthe value is false!player.isFlying()
CombinedConditions.javaRuns on Java 25
1void main() {2    boolean isOperator = false;3    boolean hasBuildPermission = true;4    boolean isInSpawn = true;5    int level = 12;6 7    if (isOperator || (hasBuildPermission && !isInSpawn)) {8        IO.println("You may build here.");9    } else {10        IO.println("You cannot build here.");11    }12 13    if (level >= 10 && level < 20) {14        IO.println("You can enter the iron arena.");15    }16 17    if (!isOperator) {18        IO.println("Operator tools are hidden.");19    }20}
  1. Operators may always build. Everyone else needs the permission and must be outside spawn. The round brackets group the "and" part, just like brackets in math.
  2. A range check: between 10 and 19. You cannot write 10 <= level < 20 in Java; you must spell out both halves.
  3. The ! flips true to false and false to true. Read it as "if not operator".

Java is lazy in a useful way. With &&, if the left side is false, Java does not even look at the right side, because the answer is already false. With ||, if the left side is true, it skips the right side. This is called short-circuiting, and plugins rely on it to avoid crashes. You will see it in the launch pad below: block == null || block.getType() != ... only asks for the block's type when there really is a block.

Comparing text: equals, not ==

Text in Java is a String, and Strings must be compared with .equals(...), never with ==. The == operator checks whether two variables point at the very same object in memory, which is usually not what you mean; two Strings with the same letters can be different objects. Working with text explains this in detail. This program reads a rank you type:

CompareStrings.javaRuns on Java 25
1void main() {2    String typed = IO.readln("Type a rank (for example Admin): ");3    String rank = typed.strip();4 5    if (rank.equals("admin")) {6        IO.println("equals: exact match, so Admin with a capital A does not count.");7    }8 9    if (rank.equalsIgnoreCase("admin")) {10        IO.println("equalsIgnoreCase: Welcome, administrator!");11    } else if (rank.equalsIgnoreCase("vip")) {12        IO.println("equalsIgnoreCase: Thanks for supporting the server!");13    } else if (rank.isEmpty()) {14        IO.println("You did not type anything.");15    } else {16        IO.println("Unknown rank: " + rank);17    }18}
  1. Reads a line you type. In file mode the recorded run typed Admin.
  2. Removes spaces at the start and end, so " admin " still matches.
  3. An exact match: capital letters count, so Admin is not equal to admin.
  4. Ignores capital letters. This is what you usually want for anything a player types.
  5. True when the String has no characters at all.

In plugins you compare Strings most often when reading what a player typed after a command, for example /shop buy versus /shop sell:

Checking a command argument
1if (args[0].equalsIgnoreCase("buy")) {2    openBuyMenu(player);3} else if (args[0].equalsIgnoreCase("sell")) {4    openSellMenu(player);5}
  1. The first word the player typed after the command name. Commands, part 1 shows where args comes from.
  2. Methods you would write yourself. You will learn to write methods like these in Methods.

In a plugin: rank prefixes on join

This listener picks a prefix for the join message with an else if chain over permissions. Notice the order: Admin is checked first.

RankJoinListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-conditions-demosrcmainjavacomexamplejavaconditionsdemoRankJoinListener.java

The package com.example.javaconditionsdemo is the folder path com/example/javaconditionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • java-conditions-demo/
    • src/main/
      • java/com/example/javaconditionsdemo/Package com.example.javaconditionsdemo
        • ConditionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • GameModeListener.javaListener: reacts to events
        • HealthWarningListener.javaListener: reacts to events
        • LaunchPadListener.javaListener: reacts to events
        • RankJoinListener.javayou are hereListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

14@EventHandler15public void onJoin(PlayerJoinEvent event) {16    Player player = event.getPlayer();17 18    String prefix;19    if (player.hasPermission("conditionsdemo.rank.admin")) {20        prefix = "<red>[Admin]</red>";21    } else if (player.hasPermission("conditionsdemo.rank.vip")) {22        prefix = "<gold>[VIP]</gold>";23    } else {24        prefix = "<gray>[Member]</gray>";25    }26    event.joinMessage(MINI_MESSAGE.deserialize(prefix + " <white><name></white> <gray>joined the game",27        Placeholder.unparsed("name", player.getName())));28 29    String greeting = player.hasPlayedBefore() ? "Welcome back" : "Welcome for the first time";30    player.sendRichMessage("<green><greeting>, <name>!",31        Placeholder.unparsed("greeting", greeting),32        Placeholder.unparsed("name", player.getName()));33}
  1. Declares the variable without a value yet. Every branch below gives it one, and Java checks that no path leaves it empty.
  2. Server operators have this permission by default (see plugin.yml below), so they get the Admin prefix.
  3. Only checked when the player is not an admin. A server owner grants it with a permissions plugin such as LuckPerms.
  4. The fallback for everyone else.
  5. Replaces the yellow "joined the game" line that everyone sees. The prefix is glued to the front with +.
  6. A one-line choice using the ternary operator, explained further down.

With an operator called Steve and a regular player called Alex, chat looks like this:

[Admin] Steve joined the game
[Member] Alex joined the game
Welcome back, Alex!

Nested ifs vs guard clauses

An if block can contain another if. That is called nesting, and it is fine in small doses. But plugin code often has to check three or four things before doing any real work, and nesting each check inside the last one creates a staircase of braces that is hard to read. Compare these two programs. They do exactly the same thing:

NestedChecks.javaRuns on Java 25
1void main() {2    boolean isPlayer = true;3    boolean hasPermission = true;4    int homesSet = 3;5    int maxHomes = 3;6 7    if (isPlayer) {8        if (hasPermission) {9            if (homesSet < maxHomes) {10                IO.println("Home saved!");11            } else {12                IO.println("You already have " + maxHomes + " homes.");13            }14        } else {15            IO.println("You do not have permission.");16        }17    } else {18        IO.println("Only players can set homes.");19    }20}
  1. Level one.
  2. Level two, inside level one.
  3. Level three. The real work, saving the home, is buried in the middle.
  4. This message belongs to the very first if, but it sits at the bottom, far away from it.
GuardClauses.javaRuns on Java 25
1void main() {2    boolean isPlayer = true;3    boolean hasPermission = true;4    int homesSet = 3;5    int maxHomes = 3;6 7    if (!isPlayer) {8        IO.println("Only players can set homes.");9        return;10    }11    if (!hasPermission) {12        IO.println("You do not have permission.");13        return;14    }15    if (homesSet >= maxHomes) {16        IO.println("You already have " + maxHomes + " homes.");17        return;18    }19 20    IO.println("Home saved!");21}
  1. Check for a problem. If there is one, say so...
  2. ...and stop. return leaves the method right away, so nothing below it runs.
  3. Each check is flipped: we test for the bad case, so the good case falls through.
  4. The real work sits at the bottom, with no nesting at all. If Java gets here, every check passed.

The second style uses guard clauses: short ifs at the start that return early when there is nothing to do. Plugin developers use them everywhere, because event handlers are full of "this is not my business" checks.

Nested ifs The real work hides at the deepest level. if (isPlayer) { if (hasPermission) { if (homes < max) { save the home } else ... Each else sits far from the if it belongs to. Guard clauses Check each problem, stop early, then do the work. Not a player? return No permission? return Too many homes? return save the home Flat, and each message sits next to its check.
Nesting pushes the real work deeper with every check. Guard clauses deal with each problem and stop, leaving the main work flat at the bottom.

In a plugin: a launch pad

This listener turns light weighted pressure plates (the gold ones) into launch pads. It has three "not my business" checks before it does anything, so it is written with guard clauses:

LaunchPadListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-conditions-demosrcmainjavacomexamplejavaconditionsdemoLaunchPadListener.java

The package com.example.javaconditionsdemo is the folder path com/example/javaconditionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • java-conditions-demo/
    • src/main/
      • java/com/example/javaconditionsdemo/Package com.example.javaconditionsdemo
        • ConditionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • GameModeListener.javaListener: reacts to events
        • HealthWarningListener.javaListener: reacts to events
        • LaunchPadListener.javayou are hereListener: reacts to events
        • RankJoinListener.javaListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

16@EventHandler17public void onStep(PlayerInteractEvent event) {18    if (event.getAction() != Action.PHYSICAL) {19        return;20    }21    Block block = event.getClickedBlock();22    if (block == null || block.getType() != Material.LIGHT_WEIGHTED_PRESSURE_PLATE) {23        return;24    }25    Player player = event.getPlayer();26    if (!player.hasPermission("conditionsdemo.launchpad")) {27        return;28    }29 30    Vector launch = player.getLocation().getDirection().multiply(1.5).setY(1.0);31    player.setVelocity(launch);32    player.sendActionBar(Component.text("Whoosh!", NamedTextColor.AQUA));33}
  1. PlayerInteractEvent fires for left clicks, right clicks and for stepping on things like pressure plates.
  2. Guard 1: we only care about stepping on something (a "physical" action), not clicking. != means "is not equal to".
  3. The block involved. It can be null (nothing), for example when a player clicks the air.
  4. Guard 2, with short-circuiting: if block is null, Java stops here and never calls getType() on nothing, which would crash.
  5. Guard 3: players without the permission walk over the plate normally.
  6. The direction the player is looking, made 1.5 times stronger, with an upward push of 1.0. A Vector is a direction plus a strength.
  7. Throws the player in that direction.
Whoosh!

switch: choosing by value

When you compare one value against a list of exact options ("is the mode survival, creative, adventure or spectator?"), an else if chain gets repetitive. A switch does the same job more neatly. You will see two styles online. First the classic switch statement:

SwitchStatement.javaRuns on Java 25
1void main() {2    String mode = "creative";3 4    switch (mode) {5        case "survival":6            IO.println("Survive, mine and craft.");7            break;8        case "creative":9            IO.println("Unlimited blocks and flying.");10            break;11        case "adventure":12            IO.println("Look, but do not break.");13            break;14        default:15            IO.println("Unknown game mode: " + mode);16    }17}
  1. Look at the value of mode and jump to the matching case.
  2. A label: the code after it runs when mode equals "creative". Switch compares Strings with equals for you, so this is safe.
  3. Leaves the switch. Forget it, and Java keeps running into the next case.
  4. Runs when no case matched, like the final else.

That break is a famous trap. Without it, Java "falls through" into the next case and runs that code too. This is called fall-through:

MissingBreak.javaRuns on Java 25
1void main() {2    String mode = "survival";3 4    switch (mode) {5        case "survival":6            IO.println("Survive, mine and craft.");7        case "creative":8            IO.println("Unlimited blocks and flying.");9            break;10        default:11            IO.println("Unknown game mode: " + mode);12    }13}
  1. No break after this line...
  2. ...so Java keeps going and prints the creative text too, even though the mode is survival.

Modern switch expressions

Modern Java (and modern Paper code) mostly uses the arrow style instead. It has no fall-through, needs no break, and can hand back a value directly. That makes it a switch expression:

SwitchExpression.javaRuns on Java 25
1void main() {2    String mode = "spectator";3 4    String description = switch (mode) {5        case "survival" -> "Survive, mine and craft.";6        case "creative" -> "Unlimited blocks and flying.";7        case "adventure" -> "Look, but do not break.";8        case "spectator" -> "Fly through walls and watch.";9        default -> "Unknown game mode: " + mode;10    };11    IO.println(description);12 13    int lives = switch (mode) {14        case "hardcore" -> 1;15        case "survival", "adventure" -> 3;16        default -> {17            IO.println("Nobody can die in " + mode + " mode.");18            yield 0;19        }20    };21    IO.println("Lives: " + lives);22}
  1. The whole switch produces a value, which goes straight into description.
  2. Arrow style: the value after -> is the result for this case. Only one case ever runs, so there is nothing to fall through.
  3. A switch expression must cover every possible value. For Strings that means you need a default.
  4. A switch expression is part of a statement, so it ends with a semicolon after the closing brace.
  5. Several values can share one case, separated by commas.
  6. When a case needs more than one line, give it a block in braces...
  7. ...and use yield to hand back the result from inside the block.

In a plugin: switch on a game mode

Paper describes many fixed lists of things with an enum: a type that has a fixed set of named values. GameMode is one: its only values are SURVIVAL, CREATIVE, ADVENTURE and SPECTATOR. Switching on an enum is one of the most common uses of switch in plugins. (Enums get their own chapter: Enums, records and modern Java.)

GameModeListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-conditions-demosrcmainjavacomexamplejavaconditionsdemoGameModeListener.java

The package com.example.javaconditionsdemo is the folder path com/example/javaconditionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • java-conditions-demo/
    • src/main/
      • java/com/example/javaconditionsdemo/Package com.example.javaconditionsdemo
        • ConditionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • GameModeListener.javayou are hereListener: reacts to events
        • HealthWarningListener.javaListener: reacts to events
        • LaunchPadListener.javaListener: reacts to events
        • RankJoinListener.javaListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

13@EventHandler14public void onGameModeChange(PlayerGameModeChangeEvent event) {15    Player player = event.getPlayer();16    GameMode newMode = event.getNewGameMode();17    boolean inNether = player.getWorld().getEnvironment() == World.Environment.NETHER;18 19    if (newMode == GameMode.CREATIVE && inNether && !player.hasPermission("conditionsdemo.creative.nether")) {20        event.setCancelled(true);21        player.sendRichMessage("<red>Creative mode is not allowed in the Nether.");22        return;23    }24 25    String description = switch (newMode) {26        case SURVIVAL -> "Survive, mine and craft.";27        case CREATIVE -> "Unlimited blocks and flying.";28        case ADVENTURE -> "Look, but do not break.";29        case SPECTATOR -> "Fly through walls and watch.";30    };31    player.sendRichMessage("<gray>Game mode changed. <yellow><description>",32        Placeholder.unparsed("description", description));33}
  1. Fires just before a player's game mode changes, for example after /gamemode creative. It can be canceled.
  2. Saving a condition in a well-named boolean variable makes the if below read like a sentence.
  3. Three conditions joined with &&: switching to creative, while in the Nether, without the special permission. All three must be true. Enum values are compared with ==, which is correct for enums.
  4. Stops the change. The player stays in their old game mode.
  5. Inside the switch you write just SURVIVAL, not GameMode.SURVIVAL; Java knows which enum it is.
  6. All four game modes are covered, so no default is needed. If Minecraft ever added a fifth mode, the compiler would point at this switch and tell you to handle it.
Game mode changed. Unlimited blocks and flying.
Creative mode is not allowed in the Nether.

The ternary operator: small choices

Sometimes you only need to choose between two values, like "coin" or "coins". A full if and else for that feels heavy. The ternary operator does it in one line: condition ? valueIfTrue : valueIfFalse.

PingColors.javaRuns on Java 25
1void main() {2    int ping = 145;3 4    String color;5    if (ping < 80) {6        color = "green";7    } else if (ping < 200) {8        color = "yellow";9    } else {10        color = "red";11    }12    IO.println("Ping " + ping + " ms is shown in " + color);13 14    String quality = ping < 100 ? "good" : "laggy";15    IO.println("Your connection is " + quality + ".");16 17    int coins = 1;18    IO.println("You have " + coins + (coins == 1 ? " coin" : " coins"));19}
  1. A three-way choice is clearest as an else if chain. Ping is how long, in milliseconds, a message takes to reach the player.
  2. Read it as: "is ping below 100? If so, "good", otherwise "laggy"".
  3. A classic use: picking the singular or plural word. The brackets around the ternary keep it separate from the + signs.

In plugins, the ternary operator shows up for small things like messages. The join listener above uses it to greet new and returning players differently:

RankJoinListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-conditions-demosrcmainjavacomexamplejavaconditionsdemoRankJoinListener.java

The package com.example.javaconditionsdemo is the folder path com/example/javaconditionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • java-conditions-demo/
    • src/main/
      • java/com/example/javaconditionsdemo/Package com.example.javaconditionsdemo
        • ConditionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • GameModeListener.javaListener: reacts to events
        • HealthWarningListener.javaListener: reacts to events
        • LaunchPadListener.javaListener: reacts to events
        • RankJoinListener.javayou are hereListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

29String greeting = player.hasPlayedBefore() ? "Welcome back" : "Welcome for the first time";
  1. True when the server already has saved data for this player.

Another one you might write for a /fly status message:

Showing an on/off state
1String state = player.isFlying() ? "<green>on" : "<red>off";2player.sendRichMessage("<gray>Flight: " + state);

Pattern matching with instanceof (a preview)

In plugins you constantly need to ask "what kind of thing is this?" Who ran this command: a player, or the server console? What took damage: a player, or a zombie? The instanceof keyword answers that question, and modern Java lets you name the result in the same step. This is called pattern matching:

Who ran the command?
1if (sender instanceof Player player) {2    player.setAllowFlight(true);3} else {4    sender.sendRichMessage("<red>Only players can fly.");5}
  1. Two jobs in one: checks whether sender is a player, and if so, gives you a variable called player of type Player to use inside the block.
  2. Only players can fly, so this method only exists on Player. That is why we needed the player variable.

The health warning listener used the guard clause version of the same idea: if (!(event.getEntity() instanceof Player player)) { return; }. Read it as "if the entity is not a player, stop". The extra round brackets are needed so that ! flips the whole instanceof check. After that line, Java knows the entity is a player, and player can be used for the rest of the method. You will understand fully why this works in Inheritance and interfaces; for now, learn it as a pattern.

The whole plugin

All four listeners live in one small plugin. The main class registers them:

ConditionsDemoPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-conditions-demosrcmainjavacomexamplejavaconditionsdemoConditionsDemoPlugin.java

The package com.example.javaconditionsdemo is the folder path com/example/javaconditionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • java-conditions-demo/
    • src/main/
      • java/com/example/javaconditionsdemo/Package com.example.javaconditionsdemo
        • ConditionsDemoPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • GameModeListener.javaListener: reacts to events
        • HealthWarningListener.javaListener: reacts to events
        • LaunchPadListener.javaListener: reacts to events
        • RankJoinListener.javaListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.javaconditionsdemo;2 3import org.bukkit.plugin.PluginManager;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class ConditionsDemoPlugin extends JavaPlugin {7 8    @Override9    public void onEnable() {10        PluginManager pluginManager = getServer().getPluginManager();11        pluginManager.registerEvents(new HealthWarningListener(), this);12        pluginManager.registerEvents(new RankJoinListener(), this);13        pluginManager.registerEvents(new GameModeListener(), this);14        pluginManager.registerEvents(new LaunchPadListener(), this);15        getLogger().info("Health warnings, rank greetings, game mode rules and launch pads are ready.");16    }17}
  1. Paper calls this when the plugin starts. It is the place to register listeners.
  2. One line per listener. Forget one, and that feature silently does nothing.

And plugin.yml declares the permissions the code checks, with sensible defaults:

plugin.ymlCompiles on Paper 26.3Compile Lab
Where this file livesjava-conditions-demosrcmainresourcesplugin.yml

Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.

  • java-conditions-demo/
    • src/main/
      • java/com/example/javaconditionsdemo/Package com.example.javaconditionsdemo
        • ConditionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • GameModeListener.javaListener: reacts to events
        • HealthWarningListener.javaListener: reacts to events
        • LaunchPadListener.javaListener: reacts to events
        • RankJoinListener.javaListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlyou are hereTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1name: ConditionsDemo2version: '1.0.0'3main: com.example.javaconditionsdemo.ConditionsDemoPlugin4api-version: '26.3'5description: Health warnings, rank greetings, game mode rules and launch pads, all built from if, else and switch.6permissions:7  conditionsdemo.rank.admin:8    description: Shows the Admin prefix when joining.9    default: op10  conditionsdemo.rank.vip:11    description: Shows the VIP prefix when joining.12    default: false13  conditionsdemo.creative.nether:14    description: Allows creative mode in the Nether.15    default: false16  conditionsdemo.launchpad:17    description: Lets a player use launch pads.18    default: true
  1. Operators have this permission automatically.
  2. Nobody has it until a server owner grants it, not even operators.
  3. Everyone has it unless a server owner takes it away.
  • java-conditions-demo/
    • src/
      • main/
        • java/
          • com/example/javaconditionsdemo/
            • ConditionsDemoPlugin.javaMain class: registers the four listeners
            • HealthWarningListener.javaelse if chain: action bar warnings when hurt
            • RankJoinListener.javaPermission checks for prefixes, ternary greeting
            • GameModeListener.javaCombined conditions and a switch over GameMode
            • LaunchPadListener.javaGuard clauses: gold pressure plates launch players
        • resources/
          • plugin.ymlName, main class and the four permissions
Server console
[14:02:11 INFO]: [ConditionsDemo] Enabling ConditionsDemo v1.0.0[14:02:11 INFO]: [ConditionsDemo] Health warnings, rank greetings, game mode rules and launch pads are ready.

Download the whole plugin as a Gradle project, run it with Run Paper Server, and try each feature: take fall damage, rejoin, place a gold pressure plate and run across it, then go to the Nether and try /gamemode creative. You can also open it in the Compile Lab.

Mistakes to avoid

  • Using = instead of ==. Java stops you with "cannot be converted to boolean".
  • Comparing Strings with ==. Use equals or equalsIgnoreCase. The == version often seems to work in small tests and then fails with real player input.
  • Checking in the wrong order. In an else if chain, the first true branch wins. Put the most specific check first.
  • Forgetting break in a classic switch. Or avoid the problem completely with the arrow style.
  • Leaving out the braces. Without braces, only the very next line belongs to the if. The second line below always runs, even though the indentation suggests otherwise:
Missing bracesHas a mistake
1if (health < 4)2    IO.println("Critical!");3    IO.println("Drink a potion!");
  • A semicolon right after the condition. if (health < 4); ends the if right there, with an empty body, so the block below always runs. Java does not warn you. Always write { straight after the closing bracket.
Try it

Titles for kills

Many PvP servers give players a title based on how many players they have defeated. Finish this program so it picks the right title for any number of kills:

KillsTitle
0Peaceful
1 to 9Fighter
10 to 49Warrior
50 to 99Champion
100 or moreLegend
KillTitles.java (starter)Runs on Java 25
1void main() {2    int kills = 27;3    String title = "Unknown";4 5    IO.println("Kills: " + kills);6    IO.println("Title: " + title);7}

Test it with 0, 5, 27, 50, 99 and 250 kills.

Hint 1

Use an else if chain and set title in every branch. Start with the special case, 0.

Hint 2

Because Java stops at the first true branch, you do not need kills >= 10 && kills < 50. By the time Java reaches kills < 50, it already knows kills is at least 10.

Show the solution

The chain goes from the smallest number up, so each branch only has to check its upper limit. A negative number of kills should never happen, but checking for it costs one line and makes bad data obvious.

KillTitlesSolution.javaRuns on Java 25
1void main() {2    int kills = 27;3    String title;4 5    if (kills < 0) {6        title = "Invalid";7    } else if (kills == 0) {8        title = "Peaceful";9    } else if (kills < 10) {10        title = "Fighter";11    } else if (kills < 50) {12        title = "Warrior";13    } else if (kills < 100) {14        title = "Champion";15    } else {16        title = "Legend";17    }18 19    IO.println("Kills: " + kills);20    IO.println("Title: " + title);21}
  1. No starting value: every branch sets one, and the compiler checks that.
  2. Only reached when kills is above 0, so this means 1 to 9.
  3. 100 or more.
Try it

Put the titles into a plugin

Now make it real. Write a listener that, when a player joins, tells them how many players they have defeated and what their title is, with a different color for each title. Players can see this:

Player kills: 27. Your title: Warrior
Hint 1

Start from the PlayerJoinEvent listener in Events and listeners.

Hint 2

Minecraft already counts player kills as a statistic: int kills = player.getStatistic(Statistic.PLAYER_KILLS);

Hint 3

Put the color tag into the title text, for example title = "<gold>Warrior";, then add it to the end of the message with +.

Show the solution

The else if chain is the same as in the Java program. Only the source of the number changed: instead of a fixed value, the player's real statistic. This is how most plugin logic is born: work it out in plain Java first, then plug in values from Paper.

KillTitleListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-conditions-exercisesrcmainjavacomexamplejavaconditionsexerciseKillTitleListener.java

The package com.example.javaconditionsexercise is the folder path com/example/javaconditionsexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • java-conditions-exercise/
    • src/main/
      • java/com/example/javaconditionsexercise/Package com.example.javaconditionsexercise
        • ConditionsExercisePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • KillTitleListener.javayou are hereListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.javaconditionsexercise;2 3import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;4import org.bukkit.Statistic;5import org.bukkit.entity.Player;6import org.bukkit.event.EventHandler;7import org.bukkit.event.Listener;8import org.bukkit.event.player.PlayerJoinEvent;9 10public final class KillTitleListener implements Listener {11 12    @EventHandler13    public void onJoin(PlayerJoinEvent event) {14        Player player = event.getPlayer();15        int kills = player.getStatistic(Statistic.PLAYER_KILLS);16 17        String title;18        if (kills == 0) {19            title = "<green>Peaceful";20        } else if (kills < 10) {21            title = "<yellow>Fighter";22        } else if (kills < 50) {23            title = "<gold>Warrior";24        } else if (kills < 100) {25            title = "<red>Champion";26        } else {27            title = "<dark_purple>Legend";28        }29 30        player.sendRichMessage("<gray>Player kills: <white><kills></white>. Your title: " + title,31            Placeholder.unparsed("kills", String.valueOf(kills)));32    }33}
  1. The same statistic you can see in the game's Statistics screen, saved by the server for every player.
  2. Each title carries its own MiniMessage color tag.
  3. The title, with its color, is glued onto the end of the message.
  4. Placeholders take text, so the number is turned into a String first.

Remember to register the listener in your main class's onEnable. The finished exercise plugin is ready to download.

Recap

  • if (condition) { ... } runs a block only when the condition is true. Add else for the other path.
  • In an else if chain only the first true branch runs, so put the most specific check first.
  • Join conditions with && (and), || (or) and ! (not). Short-circuiting makes x == null || ... safe.
  • Compare Strings with equals or equalsIgnoreCase; compare numbers and enum values with ==.
  • Guard clauses check for problems first and return early. Event handlers are full of them.
  • Arrow-style switch expressions pick a value by matching cases, with no fall-through. Switches on enums like GameMode can cover every value without a default.
  • condition ? a : b chooses between two values in one line.
  • if (sender instanceof Player player) checks the type and gives you a ready-to-use variable.

Quick quiz

  1. Health is 3. The chain checks health < 10 first, then else if (health < 4). What happens?

  2. A player types /shop BUY. Which check matches it?

  3. Why is if (block == null || block.getType() != Material.STONE) safe when block is null?

  4. What is the main reason plugin developers use guard clauses in event handlers?

  5. Why does this switch expression compile without a default? String name = switch (mode) { case SURVIVAL -> "S"; case CREATIVE -> "C"; case ADVENTURE -> "A"; case SPECTATOR -> "Sp"; }; (mode is a GameMode)

Next steps

  • Loops: do something for every online player, every slot or every second of a countdown.
  • Methods: put your decisions into named, reusable pieces like titleFor(kills).
  • Practice Arena: plugin challenges that need exactly these checks, with real tests.