CodeNFacts
CodeHub
Home

All Categories


Sign In
Java · 20 core topics

Learn Java from the ground up

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
public class Main {
public static void main(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
public class Main {
public static void main(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.
Main.java
public class Main {
public static void main(String[] args) {
int a = 10, b = 3;
 
System.out.println(a + b); // 13
System.out.println(a - b); // 7
System.out.println(a * b); // 30
System.out.println(a / b); // 3 (int division truncates)
System.out.println(a % b); // 1
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.
Main.java
public class Main {
public static void main(String[] args) {
String name = "codenfacts";
 
System.out.println(name.toUpperCase()); // CODENFACTS
System.out.println(name.charAt(0)); // c
System.out.println(name.substring(0, 4)); // code
System.out.println(name.replace("code", "learn"));
 
int age = 5;
System.out.println(name + " is " + age + " years old");
 
String[] words = "java is fun".split(" ");
System.out.println(String.join("-", words)); // java-is-fun
}
}
output
CODENFACTS
c
code
learnnfacts
codenfacts is 5 years old
java-is-fun
05

Arrays

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
public class Main {
public static void main(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.
Main.java
import java.util.ArrayList;
import java.util.List;
 
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("cherry");
 
fruits.set(1, "blueberry");
System.out.println(fruits); // [apple, blueberry, cherry]
 
fruits.remove("cherry");
System.out.println(fruits.size()); // 2
System.out.println(fruits.contains("apple")); // true
}
}
output
[apple, blueberry, cherry]
2
true
07

HashMap

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;
 
public class Main {
public static void main(String[] args) {
Map<String, Integer> ages = new LinkedHashMap<>();
ages.put("Ada", 30);
ages.put("Bo", 25);
 
System.out.println(ages.get("Ada")); // 30
System.out.println(ages.getOrDefault("Cy", -1)); // -1
 
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
output
30
-1
Ada: 30
Bo: 25
08

Conditionals

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
public class Main {
public static void main(String[] args) {
int score = 82;
String grade;
 
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else if (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
public class Main {
public static void main(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
public class Main {
static String greet(String name) {
return "Hello, " + name + "!";
}
 
static int total(int... numbers) {
int sum = 0;
for (int n : numbers) sum += n;
return sum;
}
 
public static void main(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
public class Main {
static class Dog {
String name;
String breed;
 
Dog(String name, String breed) {
this.name = name;
this.breed = breed;
}
 
String bark() {
return name + " says woof!";
}
}
 
public static void main(String[] args) {
Dog rex = new Dog("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
public class Main {
static class 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
public String toString() {
return "(" + x + ", " + y + ")";
}
}
 
public static void main(String[] args) {
Point origin = new Point();
Point p = new Point(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.
Main.java
public class Main {
static class Animal {
String name;
Animal(String name) { this.name = name; }
String speak() { return name + " makes a sound"; }
}
 
static class Cat extends Animal {
Cat(String name) { super(name); }
 
@Override
String speak() { return name + " says meow"; }
}
 
public static void main(String[] args) {
Animal whiskers = new Cat("Whiskers");
System.out.println(whiskers.speak()); // Whiskers says meow
System.out.println(whiskers instanceof Animal); // true
}
}
output
Whiskers says meow
true
14

Interfaces & Abstract Classes

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.
Main.java
public class Main {
interface Shape {
double area();
}
 
static class Circle implements Shape {
double radius;
Circle(double radius) { this.radius = radius; }
 
@Override
public double area() { return Math.PI * radius * radius; }
}
 
public static void main(String[] args) {
Shape shape = new Circle(2);
System.out.printf("Area: %.2f%n", shape.area()); // Area: 12.57
}
}
output
Area: 12.57
15

Polymorphism

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
public class Main {
static class Animal {
String speak() { return "..."; }
}
static class Dog extends Animal {
@Override String speak() { return "Woof"; }
}
static class Cat extends Animal {
@Override String speak() { return "Meow"; }
}
 
public static void main(String[] args) {
Animal[] animals = { new Dog(), new Cat() };
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
public class 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!");
return null;
} finally {
System.out.println("Done attempting division");
}
}
 
public static void main(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>.
Main.java
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
 
public class Main {
public static void main(String[] args) throws IOException {
// Writing to a file
try (FileWriter writer = new FileWriter("notes.txt")) {
writer.write("Learning Java with CodeNFacts\n");
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;
 
public class MathHelper {
public static int square(int n) {
return n * n;
}
}
 
// File 2: Main.java, in a different package
import com.codenfacts.utils.MathHelper;
 
public class Main {
public static void main(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
public class Main {
static class Box<T> {
private T content;
 
void put(T item) { this.content = item; }
T get() { return content; }
}
 
static <T> T firstElement(T[] items) {
return items[0];
}
 
public static void main(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;
 
public class Main {
public static void main(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.