Hello World & Output
Hello, World
Console.WriteLine("Hello, World!"); class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
} System.out.println is Java's equivalent of Console.WriteLine — it prints a string followed by a newline. Every Java program must have a main method inside a class; there are no top-level statements as in C# 9+.Formatted output
var name = "Java";
var version = 26;
Console.WriteLine($"Hello from {name} {version}!"); class Main {
public static void main(String[] args) {
var name = "Java";
var version = 26;
System.out.printf("Hello from %s %d!%n", name, version);
}
} Java has no string interpolation. Use
System.out.printf with C-style format specifiers (%s, %d, %f), or String.formatted() / String.format() to build a formatted string. Use %n instead of \n for the platform line separator.Building formatted strings
var width = 5.5;
var height = 3.2;
var description = $"Area: {width * height:F2}";
Console.WriteLine(description); class Main {
public static void main(String[] args) {
double width = 5.5;
double height = 3.2;
String description = "Area: %.2f".formatted(width * height);
System.out.println(description);
}
} String.formatted() (Java 15+) is an instance-method shorthand for String.format(). Both produce a string without printing it, corresponding to C#'s interpolated string expression $"..." assigned to a variable.Text blocks / multiline strings
var json = """
{
"name": "Alice",
"age": 30
}
""";
Console.WriteLine(json); class Main {
public static void main(String[] args) {
String json = """
{
"name": "Alice",
"age": 30
}
""";
System.out.println(json);
}
} Java text blocks (Java 15+) use
"""...""" — the same delimiter as C# raw string literals. The common indentation prefix is stripped automatically. Text blocks also support %s-style interpolation via .formatted().Variables & Types
Local type inference with var
var greeting = "Hello";
var count = 42;
var ratio = 3.14;
Console.WriteLine($"{greeting} {count} {ratio}"); class Main {
public static void main(String[] args) {
var greeting = "Hello";
var count = 42;
var ratio = 3.14;
System.out.printf("%s %d %s%n", greeting, count, ratio);
}
} Java has had
var for local variable type inference since Java 10. Unlike C#, Java var can only be used for local variables inside methods — not for fields, parameters, or return types. The type is still statically inferred and checked at compile time.Primitive and reference types
int counter = 0;
long bigNumber = 10_000_000_000L;
double ratio = 3.14;
bool active = true;
char letter = 'A';
Console.WriteLine($"{counter} {bigNumber} {ratio} {active} {letter}"); class Main {
public static void main(String[] args) {
int counter = 0;
long bigNumber = 10_000_000_000L;
double ratio = 3.14;
boolean active = true;
char letter = 'A';
System.out.printf("%d %d %f %b %c%n", counter, bigNumber, ratio, active, letter);
}
} Java uses
boolean (not bool) and has true primitives (int, long, double, char). C#'s bool, int, and double map directly. Primitive types in Java have boxed counterparts (Integer, Long, Double) needed for generics — C# uses the same keyword for both.Constants and final
const double Pi = 3.14159;
const string AppName = "MyApp";
Console.WriteLine($"{AppName}: {Pi}"); class Main {
static final double PI = 3.14159;
static final String APP_NAME = "MyApp";
public static void main(String[] args) {
System.out.printf("%s: %f%n", APP_NAME, PI);
}
} Java uses
final on variables/fields to prevent reassignment, corresponding to C#'s const (compile-time) or readonly (runtime). Class-level constants are also static. Java convention for constant names is UPPER_SNAKE_CASE; C# uses PascalCase.Autoboxing and type erasure
// C# uses the same keyword for both value and reference contexts:
var numbers = new List<int> { 1, 2, 3 };
int first = numbers[0];
Console.WriteLine(first); import java.util.List;
import java.util.ArrayList;
class Main {
public static void main(String[] args) {
// Generics require boxed types, not primitives:
var numbers = new ArrayList<Integer>(List.of(1, 2, 3));
int first = numbers.get(0); // auto-unboxed Integer → int
System.out.println(first);
}
} Java generics require reference types — you must use
Integer, not int, in List<Integer>. Java autoboxes between int ↔ Integer automatically, but this incurs allocation. C#'s generics are reified and work directly with int — no boxing for List<int>.Enums
var direction = Direction.North;
Console.WriteLine(direction);
Console.WriteLine((int)direction);
enum Direction { North, South, East, West } class Main {
enum Direction { NORTH, SOUTH, EAST, WEST }
public static void main(String[] args) {
var direction = Direction.NORTH;
System.out.println(direction);
System.out.println(direction.ordinal());
}
} Java enums are full classes and can have fields, constructors, and methods — more powerful than C#'s integer-backed enums. Java uses
ordinal() for the integer position (zero-indexed). Convention for enum constant names is UPPER_SNAKE_CASE in Java, PascalCase in C#.Strings
String equality (== vs equals)
var greeting1 = "Hello";
var greeting2 = "Hello";
Console.WriteLine(greeting1 == greeting2);
Console.WriteLine(greeting1.Equals(greeting2)); class Main {
public static void main(String[] args) {
String greeting1 = "Hello";
String greeting2 = new String("Hello"); // force a new object
System.out.println(greeting1 == greeting2); // false (different references)
System.out.println(greeting1.equals(greeting2)); // true (same content)
}
} This is Java's most common gotcha for C# developers:
== compares object references, not content. Always use .equals() to compare string values. String literals are interned so "Hello" == "Hello" may be true, but never rely on it.String operations
var text = " Hello, Java! ";
Console.WriteLine(text.Trim());
Console.WriteLine(text.Trim().ToUpper());
Console.WriteLine(text.Trim().Replace("Java", "World"));
Console.WriteLine(text.Trim().StartsWith("Hello")); class Main {
public static void main(String[] args) {
String text = " Hello, Java! ";
System.out.println(text.strip());
System.out.println(text.strip().toUpperCase());
System.out.println(text.strip().replace("Java", "World"));
System.out.println(text.strip().startsWith("Hello"));
}
} Java string methods closely mirror C# with minor name differences:
trim()/strip() (use strip() for Unicode whitespace), toUpperCase()/toLowerCase(), replace(), startsWith(), endsWith(), contains(), substring(), split(). They return new strings — Java strings are immutable.Joining strings
var fruits = new[] { "apple", "banana", "cherry" };
Console.WriteLine(string.Join(", ", fruits)); import java.util.List;
class Main {
public static void main(String[] args) {
var fruits = List.of("apple", "banana", "cherry");
System.out.println(String.join(", ", fruits));
}
} String.join(delimiter, elements) mirrors C#'s string.Join(delimiter, elements) almost exactly. For building strings incrementally, use StringBuilder — appending to a String in a loop creates many intermediate objects.StringBuilder
var builder = new StringBuilder();
builder.Append("Hello");
builder.Append(", ");
builder.Append("World!");
Console.WriteLine(builder.ToString()); class Main {
public static void main(String[] args) {
var builder = new StringBuilder();
builder.append("Hello");
builder.append(", ");
builder.append("World!");
System.out.println(builder.toString());
}
} StringBuilder is available in both languages with almost identical APIs. The Java version uses lowercase method names (append, toString, insert, delete) while C# uses PascalCase.Converting to and from strings
var number = 42;
var text = number.ToString();
Console.WriteLine(text.GetType().Name);
var parsed = int.Parse("123");
Console.WriteLine(parsed + 1); class Main {
public static void main(String[] args) {
int number = 42;
String text = String.valueOf(number);
System.out.println(text.getClass().getSimpleName());
int parsed = Integer.parseInt("123");
System.out.println(parsed + 1);
}
} Java uses
String.valueOf(x) or Integer.toString(n) to convert to string (not a method on the value itself). Parsing uses the boxed type's static method: Integer.parseInt(), Double.parseDouble(). These throw NumberFormatException on invalid input.Numbers & Math
Integer arithmetic
Console.WriteLine(10 + 3);
Console.WriteLine(10 - 3);
Console.WriteLine(10 * 3);
Console.WriteLine(10 / 3);
Console.WriteLine(10 % 3); class Main {
public static void main(String[] args) {
System.out.println(10 + 3);
System.out.println(10 - 3);
System.out.println(10 * 3);
System.out.println(10 / 3);
System.out.println(10 % 3);
}
} Integer arithmetic is identical. Java has no unsigned integer types —
int and long are always signed. For large numbers, Java's BigInteger and BigDecimal correspond to C#'s BigInteger and decimal (though the APIs differ).Math functions
Console.WriteLine(Math.Sqrt(16));
Console.WriteLine(Math.Pow(2, 10));
Console.WriteLine(Math.Abs(-42));
Console.WriteLine(Math.Max(10, 20));
Console.WriteLine(Math.Round(3.567, 2)); class Main {
public static void main(String[] args) {
System.out.println(Math.sqrt(16));
System.out.println(Math.pow(2, 10));
System.out.println(Math.abs(-42));
System.out.println(Math.max(10, 20));
System.out.println(Math.round(3.567 * 100.0) / 100.0);
}
} Java's
Math class mirrors C#'s with lowercase method names. Java Math.round(double) returns a long (rounds to nearest integer); for decimal rounding, multiply, round, and divide. Both languages expose Math.PI and Math.E.Integer overflow behavior
// C# throws OverflowException in checked context:
// checked { int x = int.MaxValue + 1; } // throws
int maxValue = int.MaxValue;
Console.WriteLine(maxValue);
Console.WriteLine(unchecked(maxValue + 1)); // wraps silently class Main {
public static void main(String[] args) {
int maxValue = Integer.MAX_VALUE;
System.out.println(maxValue);
System.out.println(maxValue + 1); // silently wraps
}
} Java integer arithmetic always wraps silently on overflow — there is no
checked context. C# by default also wraps (unchecked is the default), but checked blocks enable overflow detection. Use Math.addExact(a, b) in Java to get an ArithmeticException on overflow.Collections
Lists
var fruits = new List<string> { "apple", "banana", "cherry" };
fruits.Add("date");
Console.WriteLine(fruits.Count);
Console.WriteLine(fruits[0]); import java.util.ArrayList;
class Main {
public static void main(String[] args) {
var fruits = new ArrayList<String>();
fruits.add("apple");
fruits.add("banana");
fruits.add("cherry");
fruits.add("date");
System.out.println(fruits.size());
System.out.println(fruits.get(0));
}
} Java's
ArrayList<T> corresponds to C#'s List<T>. Key API differences: size() not Count, get(i) not [i], add() not Add(), remove() not Remove(). Java also has List.of(...) for immutable lists.Immutable vs mutable collections
// ReadOnlyCollection is an unmodifiable view in the BCL:
var source = new[] { "a", "b", "c" };
var readOnly = Array.AsReadOnly(source);
Console.WriteLine(readOnly.Count);
// Mutable copy:
var mutable = readOnly.ToList();
mutable.Add("d");
Console.WriteLine(mutable.Count); import java.util.ArrayList;
import java.util.List;
class Main {
public static void main(String[] args) {
// Immutable (unmodifiable):
var immutable = List.of("a", "b", "c");
// Mutable:
var mutable = new ArrayList<>(immutable);
mutable.add("d");
System.out.println(mutable.size());
}
} List.of() (Java 9+) creates an immutable list — any mutation attempt throws UnsupportedOperationException. To build a mutable list from it, wrap with new ArrayList<>(list). Java also has Collections.unmodifiableList() to create an unmodifiable view of a mutable list.Arrays
var numbers = new int[] { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers.Length);
Console.WriteLine(numbers[2]);
Array.Sort(numbers);
Console.WriteLine(string.Join(", ", numbers)); import java.util.Arrays;
class Main {
public static void main(String[] args) {
int[] numbers = { 1, 4, 2, 5, 3 };
System.out.println(numbers.length);
System.out.println(numbers[2]);
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers));
}
} Java arrays are fixed-size, just like C#. Key differences:
length (not Length) is a field, not a property; Arrays.sort() and Arrays.toString() are static utility methods in java.util.Arrays. For most use cases, ArrayList is preferred over raw arrays.Sets
var unique = new HashSet<string> { "apple", "banana", "apple" };
Console.WriteLine(unique.Count);
Console.WriteLine(unique.Contains("apple")); import java.util.HashSet;
class Main {
public static void main(String[] args) {
var unique = new HashSet<String>();
unique.add("apple");
unique.add("banana");
unique.add("apple"); // duplicate ignored
System.out.println(unique.size());
System.out.println(unique.contains("apple"));
}
} Java's
HashSet<T> corresponds to C#'s HashSet<T> with minor API differences (size() vs Count, contains() vs Contains()). For an insertion-order-preserving set, use LinkedHashSet; for a sorted set, use TreeSet.Sorting with comparators
var names = new List<string> { "Charlie", "Alice", "Bob" };
names.Sort();
Console.WriteLine(string.Join(", ", names));
names.Sort((first, second) => second.CompareTo(first));
Console.WriteLine(string.Join(", ", names)); import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class Main {
public static void main(String[] args) {
var names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));
names.sort(Comparator.naturalOrder());
System.out.println(String.join(", ", names));
names.sort(Comparator.reverseOrder());
System.out.println(String.join(", ", names));
}
} Java uses
list.sort(comparator) or Collections.sort(list). Comparator.naturalOrder() / Comparator.reverseOrder() correspond to default vs reverse sorting in C#. Comparator.comparing(keyExtractor) is Java's equivalent of LINQ's OrderBy(keySelector).Maps
HashMap basics
var scores = new Dictionary<string, int>
{
{ "Alice", 95 },
{ "Bob", 87 },
};
Console.WriteLine(scores["Alice"]);
Console.WriteLine(scores.Count); import java.util.HashMap;
class Main {
public static void main(String[] args) {
var scores = new HashMap<String, Integer>();
scores.put("Alice", 95);
scores.put("Bob", 87);
System.out.println(scores.get("Alice"));
System.out.println(scores.size());
}
} Java's
HashMap<K,V> corresponds to C#'s Dictionary<K,V>. Key API differences: put(k,v) vs [k]=v, get(k) vs [k], size() vs Count, containsKey(k) vs ContainsKey(k). get() returns null for missing keys — no KeyNotFoundException.Safe map access
var scores = new Dictionary<string, int> { { "Alice", 95 } };
var aliceScore = scores.GetValueOrDefault("Alice", 0);
var bobScore = scores.GetValueOrDefault("Bob", 0);
Console.WriteLine($"{aliceScore}, {bobScore}"); import java.util.HashMap;
import java.util.Map;
class Main {
public static void main(String[] args) {
var scores = new HashMap<>(Map.of("Alice", 95));
int aliceScore = scores.getOrDefault("Alice", 0);
int bobScore = scores.getOrDefault("Bob", 0);
System.out.printf("%d, %d%n", aliceScore, bobScore);
}
} getOrDefault(key, defaultValue) (Java 8+) corresponds to C#'s GetValueOrDefault(key, defaultValue). Alternatively, check containsKey(k) before calling get(k), or use getOrDefault to avoid null returns.Iterating over a map
var capitals = new Dictionary<string, string>
{
{ "France", "Paris" },
{ "Germany", "Berlin" },
};
foreach (var (country, capital) in capitals)
Console.WriteLine($"{country}: {capital}"); import java.util.Map;
class Main {
public static void main(String[] args) {
var capitals = Map.of("France", "Paris", "Germany", "Berlin");
for (var entry : capitals.entrySet()) {
System.out.printf("%s: %s%n", entry.getKey(), entry.getValue());
}
}
} Iterate with
entrySet() for both key and value, keySet() for keys only, or values() for values only. Map.of() (Java 9+) creates an immutable map with concise syntax. Order is not guaranteed for HashMap — use LinkedHashMap for insertion order.Counting with computeIfAbsent
var words = new[] { "apple", "banana", "apple", "cherry", "banana", "apple" };
var counts = new Dictionary<string, int>();
foreach (var word in words)
counts[word] = counts.GetValueOrDefault(word) + 1;
foreach (var (word, count) in counts)
Console.WriteLine($"{word}: {count}"); import java.util.HashMap;
class Main {
public static void main(String[] args) {
String[] words = { "apple", "banana", "apple", "cherry", "banana", "apple" };
var counts = new HashMap<String, Integer>();
for (String word : words) {
counts.merge(word, 1, Integer::sum);
}
counts.forEach((word, count) ->
System.out.printf("%s: %d%n", word, count));
}
} merge(key, value, remappingFunction) (Java 8+) is the idiomatic way to accumulate into a map. If the key is absent, the value is stored directly; otherwise, the function combines the existing and new values. forEach iterates with a lambda that receives each key-value pair.Control Flow
If / else
var temperature = 22;
if (temperature > 30)
Console.WriteLine("Hot");
else if (temperature > 20)
Console.WriteLine("Warm");
else
Console.WriteLine("Cool"); class Main {
public static void main(String[] args) {
int temperature = 22;
if (temperature > 30)
System.out.println("Hot");
else if (temperature > 20)
System.out.println("Warm");
else
System.out.println("Cool");
}
} The
if/else if/else structure is syntactically identical between C# and Java. Both allow omitting braces for single-statement bodies, though most style guides recommend always including them.Switch expressions (Java 14+)
var day = "Monday";
var dayType = day switch
{
"Saturday" or "Sunday" => "Weekend",
_ => "Weekday",
};
Console.WriteLine(dayType); class Main {
public static void main(String[] args) {
String day = "Monday";
String dayType = switch (day) {
case "Saturday", "Sunday" -> "Weekend";
default -> "Weekday";
};
System.out.println(dayType);
}
} Java's switch expression (Java 14+) uses
-> arrows and returns a value, closely matching C#'s switch expression. Multiple values per case are comma-separated. A yield statement returns from a multi-statement case arm. The compiler enforces exhaustiveness for sealed types and enums.Ternary operator
var score = 75;
var grade = score >= 60 ? "Pass" : "Fail";
Console.WriteLine(grade); class Main {
public static void main(String[] args) {
int score = 75;
String grade = score >= 60 ? "Pass" : "Fail";
System.out.println(grade);
}
} The ternary operator
condition ? trueValue : falseValue is identical in both languages. Both also support the switch expression as a more readable alternative for multi-branch expressions.Try-with-resources vs using
// C# using declaration:
// using var reader = new StreamReader("file.txt");
// Auto-disposed at end of scope.
// Or: using (var reader = new StreamReader("file.txt")) { ... }
Console.WriteLine("C# using closes resources automatically"); class Main {
public static void main(String[] args) {
// Java try-with-resources (AutoCloseable):
// try (var reader = new BufferedReader(new FileReader("file.txt"))) {
// System.out.println(reader.readLine());
// }
System.out.println("Java try-with-resources closes resources automatically");
}
} Java's
try (Resource res = ...) (Java 7+) is the equivalent of C#'s using statement. The resource is automatically closed when the try block exits, even on exception. Java requires the resource to implement AutoCloseable, matching C#'s IDisposable.Loops & Streams
For and enhanced for loop
var fruits = new[] { "apple", "banana", "cherry" };
foreach (var fruit in fruits)
Console.WriteLine(fruit); class Main {
public static void main(String[] args) {
String[] fruits = { "apple", "banana", "cherry" };
for (String fruit : fruits) {
System.out.println(fruit);
}
}
} Java's enhanced
for (Type var : collection) corresponds to C#'s foreach (var item in collection). It works on arrays and any Iterable — including all Java collection types.Filter and map (Streams vs LINQ)
var numbers = new[] { 1, 2, 3, 4, 5, 6 };
var evenSquares = numbers
.Where(n => n % 2 == 0)
.Select(n => n * n)
.ToList();
Console.WriteLine(string.Join(", ", evenSquares)); import java.util.Arrays;
import java.util.stream.Collectors;
class Main {
public static void main(String[] args) {
int[] numbers = { 1, 2, 3, 4, 5, 6 };
var evenSquares = Arrays.stream(numbers)
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.boxed()
.collect(Collectors.toList());
System.out.println(String.join(", ", evenSquares.stream()
.map(String::valueOf).collect(Collectors.toList())));
}
} Java Streams are the LINQ equivalent:
filter = Where, map = Select, collect(toList()) = ToList(). Java primitive streams (IntStream, LongStream) avoid boxing; call boxed() to convert to Stream<Integer> when needed.Reduce and aggregate
var numbers = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers.Sum());
Console.WriteLine(numbers.Min());
Console.WriteLine(numbers.Max());
Console.WriteLine(numbers.Average()); import java.util.Arrays;
class Main {
public static void main(String[] args) {
int[] numbers = { 1, 2, 3, 4, 5 };
System.out.println(Arrays.stream(numbers).sum());
System.out.println(Arrays.stream(numbers).min().getAsInt());
System.out.println(Arrays.stream(numbers).max().getAsInt());
System.out.println(Arrays.stream(numbers).average().getAsDouble());
}
} min() and max() on a stream return OptionalInt — call getAsInt() or orElse(defaultValue). LINQ's aggregates throw InvalidOperationException on empty sequences; Java stream aggregates return empty Optional.Grouping (GroupBy vs Collectors.groupingBy)
var words = new[] { "ant", "bear", "ape", "bee", "cat" };
var byLength = words.GroupBy(w => w.Length);
foreach (var group in byLength)
Console.WriteLine($"Length {group.Key}: {string.Join(", ", group)}"); import java.util.Arrays;
import java.util.stream.Collectors;
class Main {
public static void main(String[] args) {
String[] words = { "ant", "bear", "ape", "bee", "cat" };
var byLength = Arrays.stream(words)
.collect(Collectors.groupingBy(String::length));
byLength.forEach((length, group) ->
System.out.printf("Length %d: %s%n", length, String.join(", ", group)));
}
} Collectors.groupingBy(classifier) corresponds to LINQ's GroupBy(keySelector). The result is a Map<Key, List<T>>. Downstream collectors like Collectors.counting() or Collectors.joining() further aggregate each group.Methods
Method basics
static int Add(int first, int second) => first + second;
static string Greet(string name) => $"Hello, {name}!";
Console.WriteLine(Add(3, 4));
Console.WriteLine(Greet("World")); class Main {
static int add(int first, int second) {
return first + second;
}
static String greet(String name) {
return "Hello, " + name + "!";
}
public static void main(String[] args) {
System.out.println(add(3, 4));
System.out.println(greet("World"));
}
} Java methods use
camelCase (same as C#). All methods must be inside a class. Top-level helper methods called from main must be static. Java has no expression-body syntax (=>); every method body uses braces.No optional parameters in Java
static void Greet(string name, string title = "")
{
var prefix = string.IsNullOrEmpty(title) ? "" : title + " ";
Console.WriteLine($"Hello, {prefix}{name}!");
}
Greet("Alice");
Greet("Alice", "Dr."); class Main {
static void greet(String name) {
greet(name, "");
}
static void greet(String name, String title) {
String prefix = title.isEmpty() ? "" : title + " ";
System.out.println("Hello, " + prefix + name + "!");
}
public static void main(String[] args) {
greet("Alice");
greet("Alice", "Dr.");
}
} Java has no default parameter values or named arguments. The idiomatic alternative is method overloading — define multiple overloads, where shorter-argument overloads delegate to the full version. The Builder pattern is the Java equivalent of C#'s many-optional-parameters APIs.
Variable-argument methods
static int Sum(params int[] numbers) => numbers.Sum();
Console.WriteLine(Sum(1, 2, 3, 4)); class Main {
static int sum(int... numbers) {
int total = 0;
for (int number : numbers) total += number;
return total;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3, 4));
}
} Java's varargs (
Type... name) correspond to C#'s params Type[] name. The varargs parameter must be last in the parameter list. Inside the method, it behaves as a regular array. String.format(String format, Object... args) is the classic Java varargs example.No extension methods in Java
// C# extension method (call on "hello" as if it's a method on string):
Console.WriteLine("hello".Shout());
static class StringExtensions
{
public static string Shout(this string text) => text.ToUpper() + "!";
} class StringUtils {
static String shout(String text) {
return text.toUpperCase() + "!";
}
}
class Main {
public static void main(String[] args) {
System.out.println(StringUtils.shout("hello"));
}
} Java has no extension methods. The alternative is a utility class with static methods (similar to how C# code looked before extension methods). Java interfaces with
default methods (Java 8+) can add methods to existing types — a limited form of the same pattern at the interface level.Lambdas & Functional Interfaces
Lambda expressions
Func<int, int> doubler = x => x * 2;
Func<int, int, int> adder = (first, second) => first + second;
Console.WriteLine(doubler(5));
Console.WriteLine(adder(3, 4)); import java.util.function.Function;
import java.util.function.BiFunction;
class Main {
public static void main(String[] args) {
Function<Integer, Integer> doubler = x -> x * 2;
BiFunction<Integer, Integer, Integer> adder = (first, second) -> first + second;
System.out.println(doubler.apply(5));
System.out.println(adder.apply(3, 4));
}
} Java lambdas use
-> (C# uses =>). Java's standard functional interfaces are in java.util.function: Function<T,R> (one arg), BiFunction<T,U,R> (two args), Predicate<T> (boolean), Consumer<T> (void), Supplier<T> (no args). They are invoked with .apply(), .test(), or .accept() — not called directly.Method references
var names = new List<string> { "Charlie", "Alice", "Bob" };
names.ForEach(Console.WriteLine);
var lengths = names.Select(s => s.Length).ToList();
Console.WriteLine(string.Join(", ", lengths)); import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
class Main {
public static void main(String[] args) {
var names = List.of("Charlie", "Alice", "Bob");
names.forEach(System.out::println);
var lengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(String.join(", ", lengths.stream()
.map(String::valueOf).collect(Collectors.toList())));
}
} Java method references (
ClassName::methodName or instance::methodName) are compact lambda alternatives. String::length is equivalent to s -> s.length(). System.out::println corresponds to C#'s Console.WriteLine method group. Both languages support these for single-method functional interfaces.Predicates and function composition
Func<int, bool> isEven = n => n % 2 == 0;
Func<int, bool> isPositive = n => n > 0;
var numbers = new[] { -4, -1, 0, 2, 3, 6 };
var positiveEvens = numbers.Where(n => isEven(n) && isPositive(n));
Console.WriteLine(string.Join(", ", positiveEvens)); import java.util.Arrays;
import java.util.function.IntPredicate;
import java.util.stream.Collectors;
class Main {
public static void main(String[] args) {
IntPredicate isEven = n -> n % 2 == 0;
IntPredicate isPositive = n -> n > 0;
int[] numbers = { -4, -1, 0, 2, 3, 6 };
var positiveEvens = Arrays.stream(numbers)
.filter(isEven.and(isPositive))
.boxed()
.map(String::valueOf)
.collect(Collectors.joining(", "));
System.out.println(positiveEvens);
}
} Java's
Predicate<T> supports .and(), .or(), and .negate() for composition — more explicit than C#'s && and || inside a lambda. Collectors.joining(delimiter) joins stream elements into a string, matching LINQ's string.Join(delimiter, enumerable).Optional (Java) vs nullable reference types (C#)
string? FindUser(int userId)
=> userId == 1 ? "Alice" : null;
var user = FindUser(1);
Console.WriteLine(user?.ToUpper() ?? "Not found");
var missing = FindUser(99);
Console.WriteLine(missing?.ToUpper() ?? "Not found"); import java.util.Optional;
class Main {
static Optional<String> findUser(int userId) {
return userId == 1 ? Optional.of("Alice") : Optional.empty();
}
public static void main(String[] args) {
Optional<String> user = findUser(1);
System.out.println(user.map(String::toUpperCase).orElse("Not found"));
Optional<String> missing = findUser(99);
System.out.println(missing.map(String::toUpperCase).orElse("Not found"));
}
} Java's
Optional<T> is an explicit wrapper for nullable results — the API equivalent of C#'s nullable reference types (T?) and null-conditional operator (?.). Call map() to transform if present, orElse() for a default, and ifPresent() to consume. Java does not have the ?. null-conditional operator.Classes & OOP
Classes and constructors
Person person = new("Alice", 30);
Console.WriteLine(person);
class Person
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age) { Name = name; Age = age; }
public override string ToString() => $"{Name} (age {Age})";
} class Person {
private final String name;
private final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (age " + age + ")";
}
}
class Main {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
System.out.println(person);
}
} Java classes use explicit getters rather than C# properties (
getName() is the Java convention, not Name). final fields set in the constructor cannot be reassigned. @Override is the Java annotation for override (optional but recommended by convention). Java records (Java 16+) handle the immutable data-class pattern more concisely.Inheritance
Animal dog = new Dog("Rex");
Console.WriteLine(dog.Speak());
class Animal
{
public string Name { get; }
public Animal(string name) { Name = name; }
public virtual string Speak() => $"{Name} makes a sound";
}
class Dog : Animal
{
public Dog(string name) : base(name) { }
public override string Speak() => $"{Name} says: Woof!";
} class Animal {
protected final String name;
Animal(String name) { this.name = name; }
String speak() { return name + " makes a sound"; }
}
class Dog extends Animal {
Dog(String name) { super(name); }
@Override
String speak() { return name + " says: Woof!"; }
}
class Main {
public static void main(String[] args) {
Animal dog = new Dog("Rex");
System.out.println(dog.speak());
}
} Java uses
extends (not :), super() to call the parent constructor (not base()), and @Override annotation. All Java methods are virtual by default (unlike C# where virtual must be explicit). Use final on a method to prevent overriding.Access modifiers
var account = new BankAccount();
account.Deposit(100);
Console.WriteLine(account.Balance);
class BankAccount
{
private decimal balance = 0;
public void Deposit(decimal amount) { balance += amount; }
public decimal Balance => balance;
} class BankAccount {
private double balance = 0;
public void deposit(double amount) {
this.balance += amount;
}
public double getBalance() {
return balance;
}
}
class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount();
account.deposit(100);
System.out.println(account.getBalance());
}
} Java's access modifiers are
public, protected, private, and package-private (no keyword). Package-private is Java's version of C#'s internal. Java has no protected internal or private protected combinations. Java has no properties — expose fields via getter/setter methods.Abstract classes
new Circle(5).Describe();
abstract class Shape
{
public abstract double Area();
public void Describe() => Console.WriteLine($"Area: {Area():F2}");
}
class Circle : Shape
{
double radius;
public Circle(double radius) { this.radius = radius; }
public override double Area() => Math.PI * radius * radius;
} abstract class Shape {
abstract double area();
void describe() {
System.out.printf("Area: %.2f%n", area());
}
}
class Circle extends Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
@Override
double area() { return Math.PI * radius * radius; }
}
class Main {
public static void main(String[] args) {
new Circle(5).describe();
}
} Abstract classes are nearly identical:
abstract class in both; abstract on the method (no body). Java uses extends to subclass. The main difference: Java methods are virtual by default, so non-abstract methods in the base class are already overridable without any keyword.Static members
new Counter(); new Counter(); new Counter();
Console.WriteLine(Counter.Count);
class Counter
{
private static int count = 0;
public Counter() { count++; }
public static int Count => count;
} class Counter {
private static int count = 0;
Counter() { count++; }
static int getCount() { return count; }
}
class Main {
public static void main(String[] args) {
new Counter(); new Counter(); new Counter();
System.out.println(Counter.getCount());
}
} Static members work identically in both languages. Java has no static properties — expose static fields via static getter methods. Java does not have static classes (a whole class cannot be
static unless it is a nested class); instead, non-instantiable utility classes use a private constructor.Interfaces & Abstract Classes
Interfaces
IShape square = new Square(4);
Console.WriteLine(square.Describe());
interface IShape
{
double Area();
string Describe() => $"Area: {Area():F2}"; // default method
}
class Square : IShape
{
double side;
public Square(double side) { this.side = side; }
public double Area() => side * side;
} interface Shape {
double area();
default String describe() {
return "Area: %.2f".formatted(area());
}
}
class Square implements Shape {
private final double side;
Square(double side) { this.side = side; }
@Override
public double area() { return side * side; }
}
class Main {
public static void main(String[] args) {
Shape square = new Square(4);
System.out.println(square.describe());
}
} Java uses
implements (not :) to declare interface implementation. Java interfaces support default methods (Java 8+) — the equivalent of C# interface default implementations. A class can implement multiple interfaces in both languages. Java interface members are public abstract by default.Sealed classes (Java 17+)
// C# has no sealed-hierarchy syntax like Java.
// The closest pattern uses an abstract base with sealed subclasses:
// abstract class Shape {}
// sealed class Circle : Shape {}
// sealed class Rectangle : Shape {}
// But the compiler does NOT enforce that all subclasses are listed. sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
class Main {
public static void main(String[] args) {
Shape shape = new Circle(5.0);
String description = switch (shape) {
case Circle c -> "Circle r=" + c.radius();
case Rectangle r -> "Rect " + r.width() + "x" + r.height();
};
System.out.println(description);
}
} Java's
sealed interface/class (Java 17+) restricts which classes can implement/extend it. Combined with pattern matching in switch, the compiler enforces exhaustiveness — you cannot forget a case. C# uses abstract classes with sealed subclasses for a similar effect, but the compiler does not enforce an exhaustive list.Single-method interfaces as lambdas
// C# delegates are roughly equivalent:
Action<string> printer = message => Console.WriteLine(message);
Func<int, bool> isEven = n => n % 2 == 0;
printer("Hello");
Console.WriteLine(isEven(4)); import java.util.function.Consumer;
import java.util.function.Predicate;
class Main {
public static void main(String[] args) {
Consumer<String> printer = message -> System.out.println(message);
Predicate<Integer> isEven = n -> n % 2 == 0;
printer.accept("Hello");
System.out.println(isEven.test(4));
}
} Any interface with exactly one abstract method is a functional interface in Java and can be instantiated with a lambda. The
@FunctionalInterface annotation is optional but documents intent. C#'s delegates (Func, Action, Predicate) are the equivalent concept.Records
Records (immutable data classes)
var point = new Point(3.0, 4.0);
Console.WriteLine(point.X);
Console.WriteLine(point);
record Point(double X, double Y); record Point(double x, double y) {}
class Main {
public static void main(String[] args) {
Point point = new Point(3.0, 4.0);
System.out.println(point.x());
System.out.println(point);
}
} Java records (Java 16+) are immutable data classes with auto-generated constructor, accessors,
equals(), hashCode(), and toString(). They closely match C# records. One difference: Java record accessor methods have no get prefix — point.x() not point.getX().Copying records with modifications
var alice = new Person("Alice", 30);
var olderAlice = alice with { Age = 31 };
Console.WriteLine(olderAlice);
record Person(string Name, int Age); record Person(String name, int age) {
Person withAge(int newAge) {
return new Person(this.name, newAge);
}
}
class Main {
public static void main(String[] args) {
Person alice = new Person("Alice", 30);
Person olderAlice = alice.withAge(31);
System.out.println(olderAlice);
}
} Java records have no built-in
with expression like C#. The idiomatic approach is a hand-written withField() method that copies the record with one field changed. C#'s alice with { Age = 31 } is more concise. Some Java code generator libraries produce these with methods automatically.Record equality
var point1 = new Point(1.0, 2.0);
var point2 = new Point(1.0, 2.0);
Console.WriteLine(point1 == point2);
Console.WriteLine(point1.Equals(point2));
record Point(double X, double Y); record Point(double x, double y) {}
class Main {
public static void main(String[] args) {
Point point1 = new Point(1.0, 2.0);
Point point2 = new Point(1.0, 2.0);
System.out.println(point1 == point2); // true (records support ==)
System.out.println(point1.equals(point2)); // true
}
} Both C# and Java records generate structural equality automatically. For Java records,
== also works correctly (unlike regular classes) because Java generates an equals() based on all record components, and the JVM interns small records. In both languages, records should be preferred for value-semantic data.Error Handling
Try / catch / finally
try
{
var number = int.Parse("abc");
Console.WriteLine(number);
}
catch (FormatException ex)
{
Console.WriteLine($"Parse error: {ex.Message}");
}
finally
{
Console.WriteLine("Always runs");
} class Main {
public static void main(String[] args) {
try {
int number = Integer.parseInt("abc");
System.out.println(number);
} catch (NumberFormatException exception) {
System.out.println("Parse error: " + exception.getMessage());
} finally {
System.out.println("Always runs");
}
}
} The
try/catch/finally structure is syntactically identical. Java's exception hierarchy: Throwable → Error (don't catch) / Exception → RuntimeException (unchecked). In C# all exceptions are unchecked; Java introduces checked exceptions that must be declared.Checked exceptions
// C# has no checked exceptions — all exceptions are unchecked.
// Methods can throw anything without declaring it.
static string ReadConfig(string path) => File.ReadAllText(path); import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
// Must declare IOException in the throws clause:
static String readConfig(String path) throws IOException {
return Files.readString(Path.of(path));
}
public static void main(String[] args) {
try {
System.out.println(readConfig("config.txt"));
} catch (IOException exception) {
System.out.println("Could not read file: " + exception.getMessage());
}
}
} Java's checked exceptions must be declared in the method signature (
throws IOException) or caught in the method body. This is enforced by the compiler — the caller is required to handle or propagate them. C# has no checked exceptions; all exceptions are unchecked, making code less verbose but shifting discipline to documentation.Custom exceptions
try { throw new InsufficientFundsException(50); }
catch (InsufficientFundsException ex) { Console.WriteLine(ex.Message); }
class InsufficientFundsException : Exception
{
public decimal Amount { get; }
public InsufficientFundsException(decimal amount)
: base($"Insufficient funds: need {amount:F2} more") { Amount = amount; }
} class InsufficientFundsException extends RuntimeException {
private final double amount;
InsufficientFundsException(double amount) {
super("Insufficient funds: need %.2f more".formatted(amount));
this.amount = amount;
}
double getAmount() { return amount; }
}
class Main {
public static void main(String[] args) {
try {
throw new InsufficientFundsException(50);
} catch (InsufficientFundsException exception) {
System.out.println(exception.getMessage());
}
}
} Custom exceptions extend
RuntimeException (unchecked) or Exception (checked) — extending RuntimeException is the modern preference as it avoids forcing callers to handle it. Call super(message) to pass the message to the base constructor, matching C#'s base(message).Multi-catch
try
{
throw new InvalidOperationException("test");
}
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
{
Console.WriteLine($"Caught: {ex.Message}");
} class Main {
public static void main(String[] args) {
try {
throw new IllegalStateException("test");
} catch (IllegalArgumentException | IllegalStateException exception) {
System.out.println("Caught: " + exception.getMessage());
}
}
} Java's multi-catch (
catch (TypeA | TypeB e)) corresponds to C#'s catch (TypeA or TypeB e). Both were added to reduce the boilerplate of duplicate catch blocks. The exception variable is effectively final in a multi-catch block in Java.Pattern Matching
Pattern matching for instanceof
object value = "Hello, World!";
if (value is string text)
Console.WriteLine($"String of length {text.Length}"); class Main {
public static void main(String[] args) {
Object value = "Hello, World!";
if (value instanceof String text) {
System.out.println("String of length " + text.length());
}
}
} Java's
instanceof Pattern variable (Java 16+) corresponds to C#'s is Type variable. Both eliminate the explicit cast after the type check. The variable is in scope for the duration of the if block.Switch pattern matching (Java 21+)
object shape = new Circle(5.0);
var description = shape switch
{
Circle circle => $"Circle r={circle.Radius}",
int integer => $"Integer {integer}",
string text => $"String '{text}'",
null => "null",
_ => "other",
};
Console.WriteLine(description);
record Circle(double Radius); class Main {
record Circle(double radius) {}
public static void main(String[] args) {
Object shape = new Circle(5.0);
String description = switch (shape) {
case Circle circle -> "Circle r=" + circle.radius();
case Integer i -> "Integer " + i;
case String text -> "String '" + text + "'";
case null -> "null";
default -> "other";
};
System.out.println(description);
}
} Java switch pattern matching (Java 21+) closely mirrors C#'s switch expression. Both support type patterns, constant patterns, and guard clauses. Java uses
when for guard conditions: case Integer i when i > 0 ->.Guarded patterns (when clause)
int score = 85;
var grade = score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
_ => "F",
};
Console.WriteLine(grade); class Main {
public static void main(String[] args) {
int score = 85;
// Primitive type patterns are preview in Java 26 — use if/else for now:
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);
}
} Java's
when guard clause in switch patterns mirrors C# relational patterns: case int s when s >= 90 -> "A" corresponds to >= 90 => "A". Primitive type patterns (case int s) are still a preview feature in Java 26, so the if/else chain is the currently stable equivalent.Record patterns (Java 21+)
object value = new Point(3.0, 4.0);
if (value is Point(var x, var y))
Console.WriteLine($"Point at ({x}, {y})");
record Point(double X, double Y); record Point(double x, double y) {}
class Main {
public static void main(String[] args) {
Object value = new Point(3.0, 4.0);
if (value instanceof Point(double x, double y)) {
System.out.printf("Point at (%.1f, %.1f)%n", x, y);
}
}
} Java record patterns (Java 21+) destructure a record in an
instanceof check or switch case, binding its components to local variables. The syntax matches C#'s positional patterns for records. Both allow nesting: case Point(Circle c, _) matches a Point whose first component is a Circle.