Learn Java

Lesson 57Phase 6: Functional and modern Java

Annotations, docs, stack traces, and the IDE

Reading and using annotations, understanding a stack trace, and working effectively in a real editor.

By Updated 55 min

  • @Override
  • @Deprecated
  • @SuppressWarnings
  • @FunctionalInterface
  • reading docs
  • stack traces

Up to now we have written code that the computer runs. This lesson is about the layer around that code: small notes you leave for the compiler, the documentation you read instead of guessing, the crash report Java hands you when something goes wrong, and the editor that ties it all together. None of this is new Java syntax to memorize. It is the craft of working like an engineer.

By the end of this lesson
  • Read and use the three annotations worth knowing now: @Override, @Deprecated, @SuppressWarnings.
  • Find a method's parameters, return type, and exceptions in the Java documentation.
  • Read a stack trace top to bottom and point at the exact line that is your bug.
  • Use auto-complete, go-to-definition, and a debugger, and see how they relate to the Atelier visualizer.
From before
You already know overriding a method, exceptions, and how a method declares its parameters and return type from methods. That is everything we lean on here. We add no new control flow and no new types, just better ways to read and steer the code you can already write.

Annotations: notes you leave on your code#

An annotation is a small label, written with an @ sign, that you attach to a piece of code: a method, a field, a class. It does not change what the code doesat runtime on its own. It is metadata, which means “data about the code”. Some annotations are read by the compiler to check your work. Some are read by tools. Some are just documentation for the next human.

You have actually seen one already without us naming it. When you override a method, it is good practice to write @Override on the line above. Here is what it looks like in context.

Override.java
read along
1public class Main {
2 public static void main(String[] args) {
3 Animal a = new Dog();
4 a.speak();
5 }
6}
7
8class Animal {
9 void speak() {
10 System.out.println("...");
11 }
12}
13
14class Dog extends Animal {
15 @Override
16 void speak() {
17 System.out.println("Woof");
18 }
19}
Output
Woof

Read @Overrideas a promise to the compiler: “I intend for this method to replace one from the parent class.” The compiler then checks that the promise is true. If a method with that exact name and parameters really does exist in Animal, all is well. If it does not, the compiler stops you with an error instead of letting a silent bug through.

Key idea

@Overridedoes not make overriding happen. Overriding already happens by matching the method's name and parameters. The annotation just asks the compiler to confirm you really did what you meant to.

Why @Override earns its keep
Suppose you meant to override speak() but typed speek() by accident, or gave it a wrong parameter. Without @Override, Java happily treats it as a brand new method, your real override never runs, and you waste an afternoon. With @Override, the compiler says “this does not override anything” immediately. Put it on every method you intend as an override.

Three more worth knowing

@Deprecated marks something as old and best avoided. The code still works, but you are warning future readers: there is a newer, better way, so do not reach for this in new code. When you call something marked @Deprecated, the compiler nudges you with a warning. You see this a lot in the real Java library, where a method from an old version stays around so it does not break existing programs, but is gently labelled “not this one anymore”.

Deprecated.java
read along
1public class Main {
2 public static void main(String[] args) {
3 Thermostat t = new Thermostat();
4
5 // The compiler warns here: oldReading() is deprecated.
6 System.out.println(t.oldReading());
7
8 // Preferred replacement, no warning.
9 System.out.println(t.reading());
10 }
11}
12
13class Thermostat {
14 @Deprecated
15 int oldReading() {
16 return 13;
17 }
18
19 String reading() {
20 return "13 C";
21 }
22}
Output
13
13 C

@SuppressWarningsgoes the other way. It tells the compiler “I know you want to warn me about this, and I have a good reason, so stay quiet here.” You name the specific warning you are silencing, for example @SuppressWarnings("deprecation") to silence the nudge about calling a deprecated method on purpose. Use it sparingly and locally. A warning is the compiler trying to help; silence it only when you have genuinely understood it and decided it does not apply.

@FunctionalInterface goes on an interface that is meant to have exactly one abstract method, the kind a lambda can stand in for, as in enums and lambdas. Like @Override, it asks the compiler to check: add a second abstract method by mistake and the interface stops compiling, with a message saying it is not a functional interface, instead of every lambda that uses it breaking somewhere else.

Annotation
What it means
@Override
“I intend to override a parent method. Compiler, please check.”
@Deprecated
“This still works but is outdated. Prefer the newer way.”
@SuppressWarnings
“I understand this warning and choose to silence it here.”
@FunctionalInterface
“This interface has one abstract method, for lambdas. Compiler, keep it that way.”
You will see annotations everywhere later
Real Java frameworks lean heavily on annotations to describe intent without extra code. That is a topic for after this course. Here we only want the four above to feel familiar, so that when you read other people's code you recognize @ labels as notes, not magic.

Reading the documentation instead of guessing#

The Java standard library is enormous. Nobody memorizes it. The skill that separates a confident programmer from a stuck one is not knowing every method, it is knowing how to look one up quickly and read what it says. That reference is called the Javadoc: official, generated documentation for every class and method in the library.

When you look up a method, four things are worth finding every time. Read them in this order and a method that looked mysterious becomes a small contract you can trust.

  1. 1The signature: the method's name and the types it takes. For example, String substring(int beginIndex, int endIndex). This tells you what to pass and in what order.
  2. 2The parameters: what each argument means. Here, beginIndex is where the slice starts and endIndex is one past where it ends. The docs spell out the subtle cases so you do not have to discover them by crashing.
  3. 3The return: what you get back, and its type. substring returns a new String; it does not change the original. The docs say so explicitly.
  4. 4The exceptions it can throw, sometimes labelled “Throws”. substring throws an IndexOutOfBoundsException if you ask for a range outside the string. Knowing this ahead of time is how you write the guard before the crash, not after.

Here is the documented behavior put to work. We trust the docs, then confirm them by running the code and reading the output.

Docs.java
1public class Main {
2 public static void main(String[] args) {
3 String word = "atelier";
4
5 // From the docs: substring(begin, end) takes chars
6 // from begin up to but NOT including end.
7 String part = word.substring(0, 4);
8
9 System.out.println(part);
10 System.out.println(part.length());
11 System.out.println(word);
12 }
13}
Prints
›atel
›4
›atelier

The output is atel, then 4, then atelier. Every line matches what the docs promised: the slice runs from index 0 up to but not including 4, its length is 4, and the original word is untouched because substring returns a new string. We did not have to guess any of that. We read the contract and the code obeyed it.

Look it up, do not guess
When you are unsure what a method does, where its index starts, or whether it changes the original, do not run ten experiments to reverse-engineer it. Open the docs and read the four things above. A guess that happens to work today is a bug waiting for a different input tomorrow. Reading the contract is faster and it is correct.

Reading a stack trace#

When a Java program hits an error it cannot handle, it stops and prints a stack trace: a report of what went wrong and where. Beginners often glance at the wall of text, feel a flash of panic, and scroll away. That is the wrong instinct. A stack trace is the single most useful gift the program can give you. It tells you almost exactly where the bug is, if you read it in the right order.

Read it in two passes.

  1. 1The top line tells you what happened: the exception type, then a colon, then a message. NullPointerException means you used something that was null as if it were a real object. The type and message are your headline.
  2. 2The lines below, each starting with at, are the call chain, newest first. The topmost at line is exactly where the program was when it crashed. Each line under it is the method that called the one above. You read down the chain until you find a line in your own code, and that is almost always your bug.

Run this program and read the trace it prints. Read the headline, then find the first line that points into code we wrote.

Crash.java
1public class Main {
2 public static void main(String[] args) {
3 Greeter g = new Greeter();
4 System.out.println(g.greet(null)); // line 4
5 }
6}
7
8class Greeter {
9 String greet(String name) {
10 int n = name.length(); // line 10: name is null here
11 return "Hello, " + name + " (" + n + ")";
12 }
13}
Prints
!Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
! at Greeter.greet(Main.java:10)
! at Main.main(Main.java:4)
Stops on line 10

NullPointerException: Cannot invoke "String.length()" because "name" is null

A variable held null, so there was no object to use.

Walk the trace. The headline says NullPointerException and even tells us the cause: name was null when we called length() on it. The first at line, at Greeter.greet(Main.java:10), points at line 10, inside greet: the line int n = name.length();. That is the crash site. The next line, Main.java:4, is who called greet, and there we see the culprit: we passed null in. The trace handed us both the symptom and its origin in three lines.

Key idea

Top line: what went wrong. First at line: where it broke. Walk down to the first line in your own files: that is the bug to fix.

The crash line is not always the wrong line
Line 10 is where the program died, but the mistake was made on line 4, where null was passed in. A stack trace shows you the crash site at the top and the path that led there below it. Read down the chain. The real fix is often a few frames lower than where the smoke is.

Now step up to the same kind of error

The visualizer prints the same trace Java does, and it also stops on the failing line and marks it, with the memory view showing what every variable held. Run the snippet below. It uses an object that was never built, so calling a method on it fails the same way a NullPointerException does. Watch where the run stops and which line is marked.

LiveNull.java
1public class Main {
2 public static void main(String[] args) {
3 Account a = new Account();
4 a.balance = 100;
5
6 Account b = null; // never built
7 b.deposit(50); // using a null object: this is the crash
8
9 System.out.println(a.balance);
10 }
11}
12
13class Account {
14 int balance;
15
16 void deposit(int amount) {
17 balance = balance + amount;
18 }
19}
Prints
!Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Account.deposit(int)" because "b" is null
! at Main.main(Main.java:7)
Stops on line 7

NullPointerException: Cannot invoke "Account.deposit(int)" because "b" is null

A variable held null, so there was no object to use.

The run stops at b.deposit(50), and the console shows Java's report: Cannot invoke "Account.deposit(int)" because "b" is null, at Main.main(Main.java:7). b was set to null and never pointed at a real Account, so there is no object to deposit into, and the memory view shows b holding null. The headline is “you used a null as if it were an object”, and the marked line is where you did it. Reading either one, your job is identical: find the null, then walk back to where it should have been a real object.

Working in a real editor#

An IDE, an Integrated Development Environment, is an editor built for one language with tools woven in: it understands your code as you type, not just the letters but the meaning. The popular ones for Java are IntelliJ IDEA, Eclipse, and VS Code with a Java extension. You do not need to master one today. You do need to know the four moves that make you fast, because each one removes a kind of guessing.

Auto-complete

Type a variable, then a dot, and the editor lists every method and field available on it. Instead of remembering whether a string's method is toUpper() or toUpperCase(), you type word. and pick from the list. Auto-complete is the docs brought into the exact spot you are typing. It also quietly teaches you what a type can do.

Go-to-definition

Hold the right key and click a method or class name, and the editor jumps to where it is defined. When you see g.greet(null) and wonder what greet actually does, you do not search the project by hand. You jump straight to its source. This turns a large unfamiliar codebase from a maze into something you can navigate.

The debugger

This is the big one, and it should feel familiar, because it is exactly what the Atelier visualizer has been doing for you all along. A debugger lets you pause a running program and look inside it. The vocabulary is small.

  • A breakpoint is a marker you place on a line. The program runs normally until it reaches that line, then pauses so you can look around.
  • Step over runs the current line and stops on the next one, treating any method call as a single step.
  • Step into goes inside a method call so you can watch it run line by line, rather than skipping past it.
  • Inspect variables shows you the current value of every variable in view while you are paused, so you can confirm what the program actually holds, not what you assumed.
Key idea

A debugger is a slow-motion, pausable view of your program with every variable on display. It turns “I think it does this” into “I can see it does this”.

You already know how to debug
Every time you pressed Run on a trace snippet and stepped through it watching the values change, you were using a debugger. The step buttons are step over and step into. The state panel is inspect variables. The marked line is the breakpoint you are paused on. A real IDE debugger is the same idea pointed at your own running programs, with the difference that you choose where to pause.

Try it

Answer each one in your head or on paper first, then open the answer.

1
You write a method tostring() in a subclass meaning to override toString() from the parent, and you add @Override above it. What happens, and why is that good?
2
You are about to call word.indexOf("e") and you are not sure what it returns when the letter is not found. What is the right first move, and what are the four things to read?
3
Given this trace, on which file and line did the program crash, and what kind of error was it? java.lang.NullPointerException ... at Greeter.greet(Greeter.java:7) at Main.main(Main.java:4)
4
Match each debugger move to plain words: breakpoint, step over, step into, inspect variables.

Practice

Work through these on your own. Each one builds on this page.

1
Take any class with overriding you wrote in an earlier lesson and add @Override to each method you intended as an override. Then deliberately misspell one method name and confirm the compiler complains. Restore it afterwards. Write one sentence on what the annotation bought you.
2
Look up String.replace in the Java docs. In your own words, write down its signature, what its two parameters mean, what it returns, and whether it changes the original string. Then write a tiny program that confirms your reading.
3
Run the live null demo in this lesson. Then change the code so it does not crash, by building a real Account for b before depositing. Write two or three sentences explaining, using the words from the stack-trace section, what was wrong before and which line the report pointed at.
4
Write down, from memory, the four IDE moves from this lesson and one sentence each on what kind of guessing each one removes. Then note which Atelier visualizer feature already does the same job as the debugger.

Spotted a mistake, or a part that did not make sense? Report it by email