Every core concept explained in plain language, with a compilable example and its actual output — from your first class to streams and generics.
01
Introduction & Syntax
Java is statically typed and compiled, and every executable program needs a public class whose name matches the file, plus a main method as the entry point. Curly braces define blocks, and every statement ends with a semicolon — both required, not optional whitespace like Python.
The file MyClass.java must contain a class actually named MyClass.
public static void main(String[] args) is where execution starts.
Comments: // for a single line, /* ... */ for a block.
Main.java
// Every Java program needs a class with a main method
publicclass Main {
publicstaticvoidmain(String[] args) {
System.out.println("Hello, CodeNFacts!");
if (true) {
System.out.println("This runs inside the if-block");
}
}
}
output
Hello, CodeNFacts!
This runs inside the if-block
02
Variables & Data Types
Every variable in Java has a fixed type declared up front, and that type can never change. Primitives (int, double, boolean, char) store raw values directly; String and everything else are reference types that store a pointer to an object.
Common primitives: int, long, double, float, boolean, char.
String is technically an object, not a primitive, even though it's used constantly.
Declaring a variable without a value (int x;) leaves it unusable until assigned.
Main.java
publicclass Main {
publicstaticvoidmain(String[] args) {
int age = 25; // whole numbers
double price = 9.99; // decimals
String name = "Ada"; // text
boolean isActive = true; // true or false
char grade = 'A'; // a single character
System.out.println(age);
System.out.println(price);
System.out.println(name + " scored an " + grade);
System.out.println(isActive);
}
}
output
25
9.99
Ada scored an A
true
03
Operators
Java's arithmetic operators behave differently depending on the types involved — dividing two ints truncates toward zero, so you have to explicitly cast to double if you want a fractional result. Comparison and logical operators work the same way you'd expect from most C-family languages.
int / int always produces an int; cast one side to double for real division.
% is the remainder operator, not just for floats.
&& and || short-circuit, just like Python's and/or.
System.out.println((double) a / b); // 3.3333333333333335
System.out.println(a > b && b > 0); // true
}
}
output
13
7
30
3
1
3.3333333333333335
true
04
Strings
A Java String is immutable — every method that looks like it modifies a string actually returns a brand-new one. That's why you always assign the result back (s = s.toUpperCase()) instead of expecting the original variable to change in place.
+= and + concatenate strings; mixing types auto-converts to String.
Common methods: .toUpperCase(), .substring(), .replace(), .split(), .charAt().
Use .equals() to compare string content — == compares object identity instead.
An array is a fixed-size, ordered block of values of the same type. Once created, its length can't grow or shrink — for a resizable collection, you reach for ArrayList instead (covered next).
Declared with a type and square brackets: int[] scores.
arr.length is a field, not a method — no parentheses.
The enhanced for-loop (for (int x : arr)) reads every element without an index.
Main.java
publicclass Main {
publicstaticvoidmain(String[] args) {
int[] scores = {90, 85, 77};
System.out.println(scores[0]); // 90
scores[1] = 88;
System.out.println(scores.length); // 3
for (int score : scores) {
System.out.println(score);
}
}
}
output
90
3
90
88
77
06
ArrayList (Collections)
ArrayList is Java's resizable, general-purpose list — part of the Collections framework. Unlike arrays, it grows automatically as you add elements, and it only works with object types, so primitives get auto-boxed (int becomes Integer).
Declared through the List interface: List<String> items = new ArrayList<>().
.add(), .remove(), .get(index), and .set(index, value) are the core operations.
The diamond operator <> lets Java infer the generic type on the right side.
HashMap stores key-value pairs for near-instant lookup by key, the same role Python's dict plays. Regular HashMap doesn't guarantee any order — LinkedHashMap is the variant to reach for when insertion order actually matters.
.get(key) returns null if the key is missing; .getOrDefault() avoids that surprise.
Iterate with .entrySet() to get both keys and values in one pass.
Keys need a proper .equals()/.hashCode() pair — Strings and boxed numbers already have one.
Main.java
import java.util.LinkedHashMap;
import java.util.Map;
publicclass Main {
publicstaticvoidmain(String[] args) {
Map<String, Integer> ages = new LinkedHashMap<>();
if / else if / else routes execution down different branches, evaluated top to bottom until one condition is true. The ternary operator (condition ? a : b) packs a simple if/else into a single expression when you just need to pick a value.
Conditions must evaluate to a boolean — unlike some languages, 0 and null aren't automatically falsy.
switch is a common alternative to a long elif-style chain for a single variable.
The ternary operator is an expression, so it can sit directly inside an assignment.
Main.java
publicclass Main {
publicstaticvoidmain(String[] args) {
int score = 82;
String grade;
if (score >= 90) {
grade = "A";
} elseif (score >= 80) {
grade = "B";
} elseif (score >= 70) {
grade = "C";
} else {
grade = "F";
}
System.out.println(grade); // B
String label = (score % 2 == 0) ? "even" : "odd";
System.out.println(label); // even
}
}
output
B
even
09
Loops
The classic for loop gives full control over the counter, condition, and increment in one line; the enhanced for loop reads through a collection without needing an index at all. while and break/continue behave the same way they do in most languages.
for (init; condition; update) is the traditional counted loop.
for (Type item : collection) is the enhanced loop for reading, not indexing.
break exits the loop entirely; continue skips to the next iteration.
Main.java
publicclass Main {
publicstaticvoidmain(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println("count: " + i);
}
String[] fruits = {"apple", "banana", "cherry"};
for (int i = 0; i < fruits.length; i++) {
System.out.println(i + " " + fruits[i]);
}
int n = 0;
while (n < 5) {
if (n == 3) break;
System.out.println("n is " + n);
n++;
}
}
}
output
count: 0
count: 1
count: 2
0 apple
1 banana
2 cherry
n is 0
n is 1
n is 2
10
Methods
A method is Java's version of a function — always attached to a class, always declared with an explicit return type (or void for none). static methods belong to the class itself and can be called without creating an object first.
The return type comes right before the method name: static int total(...).
Varargs (int... numbers) let a method accept any number of arguments as an array.
A method declared void must not return a value.
Main.java
publicclass Main {
staticStringgreet(String name) {
return"Hello, " + name + "!";
}
staticinttotal(int... numbers) {
int sum = 0;
for (int n : numbers) sum += n;
return sum;
}
publicstaticvoidmain(String[] args) {
System.out.println(greet("Ada")); // Hello, Ada!
System.out.println(total(1, 2, 3, 4)); // 10
}
}
output
Hello, Ada!
10
11
Classes & Objects
A class defines the fields (data) and methods (behavior) that every object created from it will have. new allocates an actual object in memory and hands you back a reference to it — that reference is what a variable of the class type actually holds.
Fields declared in a class become instance variables — each object gets its own copy.
this refers to the current instance inside a non-static method.
A class can be as simple as data + methods, with no inheritance involved at all.
Main.java
publicclass Main {
staticclass Dog {
String name;
String breed;
Dog(String name, String breed) {
this.name = name;
this.breed = breed;
}
Stringbark() {
return name + " says woof!";
}
}
publicstaticvoidmain(String[] args) {
Dog rex = newDog("Rex", "Labrador");
System.out.println(rex.bark()); // Rex says woof!
System.out.println(rex.name + " is a " + rex.breed);
}
}
output
Rex says woof!
Rex is a Labrador
12
Constructors
A constructor runs automatically when an object is created with new, and its job is to set up the object's initial state. A class can have several constructors with different parameter lists — this(...) lets one constructor delegate to another instead of repeating setup logic.
A constructor shares its name with the class and has no return type, not even void.
If you write zero constructors, Java gives you a free no-argument one — but only until you write your own.
@Override marks a method that's intentionally replacing a parent's version.
Main.java
publicclass Main {
staticclass Point {
int x, y;
Point() {
this(0, 0); // delegates to the constructor below
}
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
publicStringtoString() {
return"(" + x + ", " + y + ")";
}
}
publicstaticvoidmain(String[] args) {
Point origin = newPoint();
Point p = newPoint(3, 4);
System.out.println(origin); // (0, 0)
System.out.println(p); // (3, 4)
}
}
output
(0, 0)
(3, 4)
13
Inheritance
extends lets one class reuse and build on another's fields and methods. The subclass can override any inherited method to change its behavior, and super(...) reaches back into the parent's constructor to initialize the parts it's responsible for.
Java only supports single inheritance for classes — one direct parent, no more.
instanceof checks whether an object is (or descends from) a given type.
A method call on a parent-typed variable still runs the subclass's overridden version.
An interface defines a contract — a set of methods any implementing class must provide — without dictating how. It's how Java gets around single inheritance: a class can implement several interfaces even though it can only extend one class.
implements is used for interfaces, extends is used for classes.
A class implementing an interface must define every method the interface declares.
An abstract class can mix fully implemented methods with ones subclasses must fill in.
Polymorphism means the same method call can behave differently depending on the actual object it's called on, decided at runtime. Storing different subclasses in a single array typed as the parent class is what makes this pattern so useful — one loop, many behaviors.
This is called 'dynamic dispatch': Java looks at the object's real type, not the variable's declared type.
It's the core mechanic behind writing code that works with any Shape, Animal, etc.
Overloading (same name, different parameters) is a separate concept from overriding.
Main.java
publicclass Main {
staticclass Animal {
Stringspeak() { return"..."; }
}
staticclass Dog extends Animal {
@OverrideStringspeak() { return"Woof"; }
}
staticclass Cat extends Animal {
@OverrideStringspeak() { return"Meow"; }
}
publicstaticvoidmain(String[] args) {
Animal[] animals = { newDog(), newCat() };
for (Animal a : animals) {
System.out.println(a.speak()); // each runs its own override
}
}
}
output
Woof
Meow
16
Exception Handling
try/catch lets a program recover from a runtime error instead of crashing. Java distinguishes checked exceptions (which a method must declare with throws) from unchecked ones like ArithmeticException, which can be thrown anywhere without warning.
catch blocks match by exception type — catch the most specific type you can handle.
finally always runs, whether an exception was thrown or not — great for cleanup.
throw lets you raise your own exception; throws declares one a method might pass along.
Main.java
publicclass Main {
static Integer divide(int a, int b) {
try {
int result = a / b;
System.out.println("Division succeeded");
return result;
} catch (ArithmeticException e) {
System.out.println("Can't divide by zero!");
returnnull;
} finally {
System.out.println("Done attempting division");
}
}
publicstaticvoidmain(String[] args) {
System.out.println(divide(10, 2));
System.out.println(divide(10, 0));
}
}
output
Division succeeded
Done attempting division
5
Can't divide by zero!
Done attempting division
null
17
File I/O
Java's file APIs are more explicit than Python's — try-with-resources ensures a file handle gets closed automatically once the block finishes, even if an exception is thrown partway through. The newer java.nio.file API (Files, Paths) is usually the more convenient choice for simple reads and writes.
try (Resource r = ...) { } automatically closes r when the block ends.
Most file operations declare throws IOException — Java forces you to acknowledge failure is possible.
Files.readAllLines() is a quick way to pull a whole text file into a List<String>.
writer.write("File I/O is verbose but explicit\n");
}
// Reading it back
Files.readAllLines(Paths.get("notes.txt"))
.forEach(System.out::println);
}
}
output
Learning Java with CodeNFacts
File I/O is verbose but explicit
18
Packages & Imports
A package groups related classes under a namespace, mirrored by folder structure on disk, which keeps large codebases organized and avoids class name collisions. import brings a class from another package into scope so you can reference it by its short name.
The package declaration, if present, must be the very first line in the file.
java.lang (String, Math, System, ...) is imported automatically — everything else needs an explicit import.
A wildcard import (import com.codenfacts.utils.*) pulls in every public class in that package.
PackageExample.java
// File 1: com/codenfacts/utils/MathHelper.java
package com.codenfacts.utils;
publicclass MathHelper {
publicstaticintsquare(int n) {
return n * n;
}
}
// File 2: Main.java, in a different package
import com.codenfacts.utils.MathHelper;
publicclass Main {
publicstaticvoidmain(String[] args) {
System.out.println(MathHelper.square(6)); // 36
}
}
output
36
19
Generics
Generics let a class or method work with any type while still catching type mismatches at compile time, instead of at runtime like raw Object-based code would. Box<String> and Box<Integer> share one implementation but are checked as distinct, type-safe versions of it.
The type parameter (commonly T) acts as a placeholder filled in when the class is used.
Generics only work with reference types — use Integer instead of int, for example.
A generic method declares its own type parameter before the return type: static <T> T method(...).
Main.java
publicclass Main {
staticclass Box<T> {
private T content;
voidput(T item) { this.content = item; }
T get() { return content; }
}
static <T> T firstElement(T[] items) {
return items[0];
}
publicstaticvoidmain(String[] args) {
Box<String> box = new Box<>();
box.put("hello");
System.out.println(box.get()); // hello
Integer[] nums = {1, 2, 3};
System.out.println(firstElement(nums)); // 1
}
}
output
hello
1
20
Streams & Lambda Expressions
A lambda (n -> n * n) is a compact, unnamed function, most often passed straight into a Stream operation. Streams let you chain filter, map, and collect operations to transform a collection declaratively, instead of writing a manual loop with a mutable accumulator.
A stream doesn't run anything until a terminal operation (like .collect() or .sum()) is called.
Method references (Integer::intValue) are shorthand for a lambda that just calls one method.
Streams don't modify the original collection — they produce a new result.
Main.java
import java.util.List;
import java.util.stream.Collectors;
publicclass Main {
publicstaticvoidmain(String[] args) {
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenSquares = nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(evenSquares); // [4, 16, 36]
int total = nums.stream().mapToInt(Integer::intValue).sum();
System.out.println(total); // 21
}
}
output
[4, 16, 36]
21
Ready to go deeper?
Ask the AI tutor any Java question and get a step-by-step walkthrough, live.