Java Features

Su Suriya Ravichandran Updated 10 Nov 2026
12 min read ·Lesson 2 of 3

Java's design was shaped by a specific set of goals: run the same program on any machine, make large programs safer to write, and keep code readable over many years of maintenance. Each feature below exists to serve one of those goals. Understanding the reasoning behind each one explains many of the rules you will meet later in the language.

Platform Independence

Java source code compiles to bytecode, not to native machine instructions. Bytecode is executed by the Java Virtual Machine (JVM), and a JVM exists for every major operating system and processor architecture.

          Program.java
               |
             javac
               |
          Program.class  (bytecode, identical on every platform)
         /     |      \
   JVM on   JVM on   JVM on
   Windows  Linux    macOS

The practical consequence: you compile once and ship the same .class or .jar files to every platform. A build server on Linux can produce artifacts that run unchanged on a developer's Mac and a customer's Windows server.

Compare this with C or C++, where a program must be compiled separately for each target platform, and the resulting binaries are not interchangeable.

What platform independence does not cover

The bytecode is portable, but your program can still make platform-specific assumptions. File path separators (0 on Windows, / elsewhere), line endings, default character encodings, and operating-system commands differ between platforms. Java provides abstractions for these (File.separator, System.lineSeparator()`, explicit charsets), and using them is what keeps a program truly portable.

Object-Oriented Design

Java is organized around classes and objects. A class is a blueprint describing data (fields) and behavior (methods). An object is a concrete instance built from that blueprint.

public class BankAccount {
    private String owner;
    private double balance;

    public BankAccount(String owner, double initialDeposit) {
        this.owner = owner;
        this.balance = initialDeposit;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit must be positive");
        }
        balance += amount;
    }

    public double getBalance() {
        return balance;
    }
}
BankAccount account = new BankAccount("Priya", 500.0);
account.deposit(250.0);
System.out.println(account.getBalance()); // 750.0

Here BankAccount bundles the data (owner, balance) together with the only operations allowed on that data. The private keyword prevents outside code from writing account.balance = -1000;. Every change must go through deposit, which validates the input. This bundling of data with the rules that protect it is encapsulation, and it is the foundation of why object-oriented code scales to large teams: each class is responsible for keeping its own state valid.

Java supports the four classic object-oriented principles directly in its syntax:

PrincipleWhat it meansJava mechanism
EncapsulationHide internal state; expose controlled operationsprivate fields, public methods
InheritanceBuild a new class from an existing oneextends keyword
PolymorphismOne interface, many implementationsMethod overriding, interfaces
AbstractionDefine what without specifying howabstract classes, interface

Unlike C++, Java does not support multiple inheritance of classes. A class can extend exactly one parent. It can, however, implement any number of interfaces. This restriction was deliberate: multiple class inheritance introduces ambiguity (the "diamond problem") that complicates both the language and the programs written in it.

Java is not purely object-oriented. Primitive types such as int, double, and boolean are not objects, and static methods belong to a class rather than to an instance. These exceptions exist for performance: wrapping every integer in an object would make arithmetic enormously slower.

Static and Strong Typing

Every variable in Java has a declared type that the compiler knows before the program runs.

int count = 10;
String name = "Java";
count = "ten";   // compile error: incompatible types

The third line never reaches runtime. The compiler rejects it because a String cannot be assigned to an int. This is static typing: types are checked at compile time.

Java is also strongly typed: it does not silently convert between unrelated types. In JavaScript, "5" + 1 produces "51" and "5" - 1 produces 4. In Java, mixing types in ways that could lose information requires an explicit cast, and operations between unrelated types are errors.

double price = 9.99;
int whole = price;          // compile error: possible lossy conversion
int whole = (int) price;    // allowed: explicit cast, result is 9

The explicit cast tells both the compiler and the next developer reading the code that the loss of the fractional part is intentional.

Why this matters in practice

Static typing is more verbose, but it allows tools to understand your code completely. An IDE can rename a method across a million-line codebase with certainty, show every caller of a method, and flag a type mismatch the moment you type it. Refactoring a large dynamically-typed codebase requires extensive tests to catch what the Java compiler catches for free.

Local variable type inference

Since Java 10, the var keyword lets the compiler infer a local variable's type from its initializer:

var count = 10;                       // inferred as int
var names = new ArrayList<String>();  // inferred as ArrayList<String>

This is still static typing. count is an int forever; count = "ten" is still an error. var only removes the need to write the type twice when it is obvious from the right-hand side. It works only for local variables with an initializer, not for fields, method parameters, or return types.

Automatic Memory Management

In C and C++, the programmer allocates memory and must remember to free it. Forgetting to free causes memory leaks; freeing too early causes crashes and security vulnerabilities.

Java removes this responsibility. Objects are created with new, and the JVM's garbage collector (GC) automatically reclaims memory from objects that are no longer reachable from any live variable.

public void process() {
    StringBuilder temp = new StringBuilder();
    temp.append("working...");
    // temp goes out of scope here; the object becomes unreachable
    // and the garbage collector will reclaim it at some point
}

There is no delete or free. The programmer never calculates object sizes, never frees memory, and cannot accidentally use memory that has already been released.

The trade-off is that you do not control when memory is reclaimed. Garbage collection runs on its own schedule. For most applications this is irrelevant. For latency-sensitive systems, the JVM offers several GC implementations (G1, ZGC, Shenandoah) with different pause characteristics, selectable via command-line flags.

Garbage collection does not eliminate every kind of leak. If you keep a reference to an object you no longer need, for example by adding objects to a static list and never removing them, the GC cannot reclaim them because they are still reachable. These "logical leaks" are the main memory problem in Java programs.

No Explicit Pointers

C and C++ expose raw memory addresses as pointers. You can perform arithmetic on them, cast them to unrelated types, and read or write arbitrary memory. This power is the source of buffer overflows, use-after-free bugs, and most memory-corruption vulnerabilities.

Java has references instead of pointers. A reference identifies an object, but you cannot see its numeric address, cannot add to it, cannot cast it to point at a different kind of object, and cannot use it to reach memory outside the object it refers to.

int[] numbers = new int[5];
numbers[10] = 1;   // throws ArrayIndexOutOfBoundsException at runtime

In C, the equivalent write would silently corrupt whatever happened to live in memory after the array. In Java, every array access is bounds-checked, and an out-of-range index produces a clear exception instead of undefined behavior.

Built-In Security Model

The JVM verifies bytecode before executing it. The bytecode verifier checks that the code does not forge references, does not access private fields of other classes, does not overflow the stack, and does not jump to invalid instructions. This happens even for code you compiled yourself, which means a corrupted or maliciously modified .class file is rejected before it runs.

Combined with automatic bounds checking, the absence of pointer arithmetic, and the type system, this makes entire categories of vulnerabilities (buffer overflows, format-string attacks, dangling pointers) impossible in pure Java code. Security bugs in Java applications still happen, but they are almost always logic errors (injection, broken authentication, misconfiguration) rather than memory corruption.

Robustness Through Exceptions

Java requires many errors to be handled explicitly through exceptions. When something goes wrong, an exception object is thrown, and the normal flow stops until some code catches it.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class ReadConfig {
    public static void main(String[] args) {
        try {
            String content = Files.readString(Path.of("config.txt"));
            System.out.println(content);
        } catch (IOException e) {
            System.out.println("Could not read config: " + e.getMessage());
        }
    }
}

Files.readString declares that it may throw IOException. The compiler refuses to compile a call to it unless the calling code either catches that exception or declares that it may throw it onward. This is a checked exception, and it forces the programmer to make a conscious decision about failure instead of ignoring it.

Not all exceptions are checked. Programming errors such as dividing by zero (ArithmeticException) or calling a method on null (NullPointerException) are unchecked and may be thrown anywhere. The distinction is covered in depth in the exceptions section of this tutorial.

Built-In Multithreading

Java supported threads from its first version, long before most mainstream languages. A thread is an independent path of execution within a program. Multiple threads can run simultaneously on multiple CPU cores.

public class ParallelWork {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("Running in: " + Thread.currentThread().getName());
        });
        worker.start();
        worker.join();
        System.out.println("Running in: " + Thread.currentThread().getName());
    }
}

Output:

Running in: Thread-0
Running in: main

new Thread(...) creates a thread whose work is given as a lambda expression. start() begins executing it concurrently. join() makes the main thread wait until the worker finishes.

The standard library includes synchronized blocks, locks, thread pools (ExecutorService), concurrent collections, atomic variables, and, since Java 21, virtual threads, which are extremely lightweight threads managed by the JVM that allow millions of concurrent tasks. This built-in support is a major reason Java dominates server-side development, where handling thousands of simultaneous requests is the normal workload.

High Performance Through JIT Compilation

A common misconception is that Java is slow because it runs on a virtual machine. This was partly true in the 1990s and is not true today.

The JVM includes a Just-In-Time (JIT) compiler. When the program starts, bytecode is interpreted. The JVM monitors which methods and loops execute most often. Those hot spots are compiled to native machine code, optimized using information gathered at runtime, and from then on run at native speed. Because the JIT sees how the code actually behaves (which branches are taken, which types actually appear), it can sometimes produce code faster than an ahead-of-time compiler could.

The cost is startup time and warm-up: a Java program is slowest in its first seconds. For long-running servers this is irrelevant. For short command-line tools it is noticeable, which is why GraalVM native-image compilation (ahead-of-time compilation of Java to a native binary) exists for that use case.

Rich Standard Library

The JDK ships with a very large class library covering almost every common programming need without third-party dependencies:

AreaPackageExamples
Core types and utilitiesjava.langString, Math, Thread, Integer
Collectionsjava.utilArrayList, HashMap, HashSet, Optional
Functional programmingjava.util.function, java.util.streamFunction, Predicate, Stream
File and stream I/Ojava.io, java.nio.fileFiles, Path, BufferedReader
Date and timejava.timeLocalDate, Duration, ZonedDateTime
Networkingjava.net, java.net.httpHttpClient, URI, Socket
Concurrencyjava.util.concurrentExecutorService, ConcurrentHashMap, CompletableFuture
Database accessjava.sqlConnection, PreparedStatement
Text formatting and regexjava.text, java.util.regexNumberFormat, Pattern

java.lang is imported automatically into every file; everything else requires an import statement.

Backward Compatibility

Java places unusual emphasis on not breaking existing code. A .class file compiled with Java 8 in 2014 runs on a Java 25 JVM today. Methods are deprecated for years, with warnings, before removal, and removals are rare.

This is a direct reason for Java's dominance in long-lived enterprise systems. A company can upgrade its JVM for security patches and performance improvements without rewriting applications, and libraries written a decade ago continue to work.

The cost is that some older APIs remain in the library even though better alternatives exist. java.util.Date is still present although java.time replaced it in Java 8; Vector and Hashtable still exist although ArrayList and HashMap are preferred. This tutorial notes such cases and teaches the modern alternatives.

Distributed and Network-Ready

Java was designed when the internet was becoming mainstream, and networking is part of the core library rather than an add-on. Opening a TCP socket, making an HTTP request, or serializing an object to send across a network can all be done with standard classes.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class FetchExample {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/get"))
                .build();
        HttpResponse<String> response =
                client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode());
    }
}

This sends an HTTP GET request and prints the status code (200 on success) using only classes bundled with the JDK. HttpClient has been part of the standard library since Java 11.

Modern Language Evolution

Since adopting six-month releases in 2017, Java has added a steady stream of features that reduce verbosity while keeping static typing:

FeatureSinceWhat it does
var for local variables10Type inference for locals
Switch expressions14switch that returns a value, with -> arms
Text blocks15Multi-line string literals with """
Records16Compact immutable data classes
Pattern matching for instanceof16Test and bind in one step
Sealed classes17Restrict which classes may extend a type
Pattern matching for switch21Match on type and structure
Virtual threads21Lightweight threads for massive concurrency

A record, for example, replaces dozens of lines of boilerplate with one:

public record Point(int x, int y) { }

This single line declares a class with two final fields, a constructor, accessor methods x() and y(), and correct equals, hashCode, and toString implementations. Each of these modern features is covered in its own lesson later in this tutorial.

Multi-Language Platform

The JVM runs more than Java. Because any language that compiles to bytecode can run on it, the JVM hosts Kotlin, Scala, Groovy, Clojure, and others. Code written in these languages can call Java libraries directly, and Java code can call them back.

This means learning Java also means learning the platform underneath Kotlin (Android's recommended language) and Scala (used in Apache Spark). The standard library, the build tools, the concurrency model, and the deployment story are shared across all of them.

0 Comments

Reviewed before they appear

No comments yet.

Java
Ask about this post
AI Ask about this post

Ask questions about Java Features and get answers drawn from it.

Signed-in readers only.