Ruby.CodeCompared.To/Java

An interactive executable cheatsheet for Rubyists learning Java

Ruby 4.0 Java 25
Syntax Basics
Hello, World
puts "Hello, World!"
class Main { public static void main(String[] args) { System.out.println("Hello, World!"); } }
Every Java program begins with a class β€” there is no top-level code as in Ruby. The entry point is always a method called main with the exact signature public static void main(String[] args). Unlike Ruby's puts, Java uses System.out.println(). On Compiler Explorer the class must not be declared public because the filename is fixed to example.java.
Comments
# Single-line comment =begin Multi-line comment (rarely used in practice) =end name = "Alice" # inline comment puts name
class Main { public static void main(String[] args) { // Single-line comment /* Multi-line comment β€” commonly used in Java */ /** Javadoc comment β€” generates HTML documentation */ String name = "Alice"; // inline comment System.out.println(name); } }
Java has three comment styles: // for single-line, /* */ for multi-line, and /** */ for Javadoc. Javadoc comments document classes and methods and are processed by the javadoc tool to generate HTML API documentation β€” an important part of Java's ecosystem. Ruby's =begin/=end multi-line syntax is rarely used in practice; Java's /* */ is idiomatic.
Class Structure
# Ruby: code runs at the top level β€” no class required greeting = "Hello from Ruby" puts greeting puts greeting.upcase
class Greeter { private String message; Greeter(String message) { this.message = message; } String shout() { return message.toUpperCase(); } } class Main { public static void main(String[] args) { Greeter greeter = new Greeter("Hello from Java"); System.out.println(greeter.shout()); } }
Java requires every piece of code to live inside a class β€” there is no top-level code. Multiple non-public classes may appear in a single file. The this keyword is the Java equivalent of Ruby's self: it refers to the current instance and disambiguates between a parameter and a field of the same name. Constructor names match the class name exactly (unlike Ruby's initialize).
Variables & Types
Explicit Type Declarations
name = "Alice" age = 30 height = 1.68 active = true puts "#{name}, #{age}, #{height}, #{active}"
class Main { public static void main(String[] args) { String name = "Alice"; int age = 30; double height = 1.68; boolean active = true; System.out.printf("%s, %d, %.2f, %b%n", name, age, height, active); } }
Java requires every variable to be declared with an explicit type. Where Ruby infers the type from the assigned value at runtime, Java must be told at compile time whether a value is a String, int, double, boolean, and so on. The type annotation serves as live documentation β€” a Java reader knows exactly what kind of data each variable holds without running the code.
Type Inference with var
# Ruby always infers types β€” nothing to enable numbers = [1, 2, 3] message = "count: #{numbers.size}" puts message
import java.util.List; class Main { public static void main(String[] args) { var numbers = List.of(1, 2, 3); // inferred as List<Integer> var message = "count: " + numbers.size(); System.out.println(message); } }
The var keyword (Java 10+) tells the compiler to infer the variable's type from the right-hand side β€” the type is still fixed at compile time. This is syntactic sugar that reduces verbosity without sacrificing type safety. var is only allowed for local variables, not for method parameters, return types, or fields. Ruby's type inference operates at runtime; Java's var operates at compile time.
Primitive Types
# Ruby: all values are objects, including numbers puts 42.class # Integer puts 3.14.class # Float puts true.class # TrueClass puts "a".class # String
class Main { public static void main(String[] args) { int count = 42; long bigNum = 9_876_543_210L; double ratio = 3.14; float fraction = 2.5f; boolean enabled = true; char grade = 'A'; System.out.println(count); System.out.println(bigNum); System.out.println(ratio); System.out.println(enabled); System.out.println(grade); } }
Java has eight primitive types that are NOT objects: byte, short, int, long, float, double, boolean, and char. In Ruby, even 42 is an Integer object with methods. Java primitives are stored directly on the stack for performance, but cannot be used as generic type parameters (List<int> is illegal) or stored in collections β€” their wrapper types (Integer, Double, etc.) must be used instead.
Autoboxing
numbers = [1, 2, 3] numbers.push(42) total = numbers.sum puts total
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(); numbers.add(1); // int autoboxed to Integer numbers.add(2); numbers.add(42); int total = numbers.stream().mapToInt(Integer::intValue).sum(); System.out.println(total); } }
Java collections cannot hold primitive types β€” List<int> is illegal. Each primitive has an object wrapper: int ↔ Integer, double ↔ Double, boolean ↔ Boolean. Java automatically boxes primitives to wrappers when storing in collections (autoboxing) and unboxes them back when retrieved. A hidden danger: if a wrapper object is null and gets unboxed automatically, a NullPointerException is thrown.
Constants with final
MAX_CONNECTIONS = 100 PI = 3.14159 puts MAX_CONNECTIONS puts PI # MAX_CONNECTIONS = 200 # only a warning, not enforced
class Main { static final int MAX_CONNECTIONS = 100; static final double PI = 3.14159; public static void main(String[] args) { System.out.println(MAX_CONNECTIONS); System.out.println(PI); // MAX_CONNECTIONS = 200; // compile error β€” final cannot be reassigned } }
The final keyword creates a variable that cannot be reassigned after initialization β€” enforced at compile time. For class-level constants, static final is the standard pattern, analogous to Ruby's constants. Unlike Ruby, where reassigning a constant produces only a warning at runtime, Java's final is an absolute guarantee: any attempt to reassign is a compilation error that prevents the program from building at all.
null and Optional
name = nil puts name.nil? # true puts name.to_s # "" (safe β€” returns empty string) puts name&.upcase # nil (safe navigation operator)
import java.util.Optional; class Main { public static void main(String[] args) { String name = null; // Unsafe β€” throws NullPointerException at runtime: // System.out.println(name.toUpperCase()); // Safe with null check: if (name != null) System.out.println(name.toUpperCase()); // Safe with Optional: Optional<String> optName = Optional.ofNullable(name); System.out.println(optName.map(String::toUpperCase).orElse("(none)")); } }
null in Java is the source of a large class of bugs β€” a NullPointerException at runtime is one of the most common Java errors. Unlike Ruby's nil, which is a proper object with methods like nil? and to_s, Java's null has no methods at all: calling any method on null throws immediately. Optional<T> (Java 8+) is the idiomatic solution for values that may be absent β€” it forces the caller to acknowledge the empty case, analogous to Ruby's safe navigation operator &..
Type Casting & Conversion
puts "42".to_i + 8 # 50 puts 3.to_f / 2 # 1.5 puts 42.to_s + " things" # "42 things"
class Main { public static void main(String[] args) { // Widening: smaller type to larger β€” automatic, no cast needed int count = 42; double ratio = count; // int β†’ double, safe System.out.println(ratio); // 42.0 // Narrowing: larger type to smaller β€” explicit cast, may lose data double precise = 9.99; int truncated = (int) precise; // fractional part dropped System.out.println(truncated); // 9 // String ↔ number conversion String text = Integer.toString(42); int parsed = Integer.parseInt("100"); System.out.println(text + " and " + parsed); } }
Java distinguishes widening conversions (safe, automatic) from narrowing conversions (may lose data, requires an explicit cast with the target type in parentheses). Converting between strings and numbers requires the wrapper class methods: Integer.parseInt(), Double.parseDouble(), and Integer.toString(). There is no Ruby-style .to_i or .to_s method on primitives β€” the conversion is always explicit.
Strings
String Concatenation
first = "Hello" last = "World" puts first + ", " + last + "!" puts "#{first}, #{last}!" puts [first, last].join(", ")
class Main { public static void main(String[] args) { String first = "Hello"; String last = "World"; System.out.println(first + ", " + last + "!"); System.out.println(String.join(", ", first, last)); System.out.println(first.concat(", ").concat(last)); } }
Java does not have string interpolation. The + operator concatenates strings and automatically converts non-string operands to strings via their toString() method. String.join() is a clean alternative for combining a list of values with a separator. Concatenating with + inside a loop is inefficient (creates a new object every iteration) β€” use StringBuilder for building strings incrementally.
String Formatting
name = "Alice" score = 98.6 puts "Name: #{name}, Score: #{score}" puts format("%-10s %.1f%%", name, score)
class Main { public static void main(String[] args) { String name = "Alice"; double score = 98.6; System.out.printf("%-10s %.1f%%%n", name, score); String line = "Name: %s, Score: %.1f".formatted(name, score); System.out.println(line); } }
String.format() and the newer .formatted() instance method (Java 15+) use the same format specifiers as C's printf: %s for strings, %d for integers, %.2f for floats with 2 decimal places. System.out.printf() prints without a trailing newline β€” use %n as a platform-appropriate line separator. The .formatted() method on string literals is the most idiomatic modern Java approach and directly parallels Ruby's format().
Text Blocks (Multi-line Strings)
json = <<~JSON { "name": "Alice", "age": 30 } JSON puts json
class Main { public static void main(String[] args) { String json = """ { "name": "Alice", "age": 30 } """; System.out.println(json.strip()); } }
Text blocks (Java 15+) use triple-quote delimiters and automatically strip common leading whitespace, producing a clean multi-line string. They are the Java equivalent of Ruby's squiggly heredoc <<~. The closing """ determines the indentation baseline β€” any whitespace to its left is stripped from all lines. Text blocks are particularly useful for embedding JSON, SQL, HTML, or XML without escaping every double-quote.
Common String Methods
word = " Hello, World! " puts word.strip puts word.strip.length puts word.include?("World") puts word.upcase.strip puts word.strip.gsub("World", "Java")
class Main { public static void main(String[] args) { String word = " Hello, World! "; System.out.println(word.strip()); System.out.println(word.strip().length()); System.out.println(word.contains("World")); System.out.println(word.toUpperCase().strip()); System.out.println(word.strip().replace("World", "Java")); } }
Java strings are immutable objects β€” every operation returns a new String instance rather than modifying the original, exactly like Ruby's frozen strings. Method names differ slightly from Ruby: strip() (Ruby: strip), contains() (Ruby: include?), replace() (Ruby: gsub for literal replacement), toUpperCase() (Ruby: upcase). Note that Java method calls always require parentheses, even when there are no arguments.
String Equality β€” equals vs ==
greeting1 = "hello" greeting2 = "hello" puts greeting1 == greeting2 # true β€” always value equality puts greeting1.equal?(greeting2) # implementation-defined puts greeting1.eql?(greeting2) # true
class Main { public static void main(String[] args) { String greeting1 = new String("hello"); String greeting2 = new String("hello"); System.out.println(greeting1 == greeting2); // false β€” reference equality! System.out.println(greeting1.equals(greeting2)); // true β€” value equality System.out.println(greeting1.equalsIgnoreCase("HELLO")); // true } }
This is one of the most common Java gotchas: == on objects compares memory addresses (references), not values. Two different String objects with identical content return false for ==. Always use .equals() to compare string content. String literals from the same intern pool may coincidentally return true for ==, which masks the bug β€” using new String(...) exposes it clearly. Ruby's == on strings always means value equality.
StringBuilder for Mutable Strings
parts = [] (1..5).each { |number| parts << "item#{number}" } puts parts.join(", ")
class Main { public static void main(String[] args) { StringBuilder builder = new StringBuilder(); for (int index = 1; index <= 5; index++) { if (index > 1) builder.append(", "); builder.append("item").append(index); } System.out.println(builder.toString()); } }
Because Java strings are immutable, concatenating with + inside a loop creates a new object on every iteration β€” O(nΒ²) in memory allocations. StringBuilder maintains a mutable internal buffer and produces the final string only when .toString() is called, making it O(n). For thread-safe contexts, use StringBuffer instead. Ruby's array-join pattern (array.map { ... }.join(", ")) is clean and idiomatic; in Java, StringBuilder or a stream Collectors.joining() are the equivalents.
Collections
Arrays
scores = [95, 87, 92, 78, 85] puts scores[0] puts scores.length puts scores.sort.reverse.inspect
import java.util.Arrays; class Main { public static void main(String[] args) { int[] scores = {95, 87, 92, 78, 85}; System.out.println(scores[0]); System.out.println(scores.length); Arrays.sort(scores); System.out.println(Arrays.toString(scores)); } }
Java arrays have a fixed size set at creation time β€” they cannot grow or shrink like Ruby arrays. The length property (no parentheses β€” it is a field, not a method) returns the element count. Printing an array with System.out.println(scores) gives a cryptic reference like [I@1b6d3586 β€” Arrays.toString() is required for a readable representation. For most purposes, ArrayList<T> is preferred over raw arrays in modern Java.
ArrayList (Dynamic Array)
fruits = ["apple", "banana", "cherry"] fruits.push("date") fruits.delete("banana") puts fruits.length fruits.each { |fruit| puts fruit }
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<String> fruits = new ArrayList<>(List.of("apple", "banana", "cherry")); fruits.add("date"); fruits.remove("banana"); System.out.println(fruits.size()); for (String fruit : fruits) { System.out.println(fruit); } } }
ArrayList<T> is the Java equivalent of a Ruby array β€” dynamically sized and ordered. The type parameter <String> enforces that only strings can be added, caught at compile time. In modern Java, List<T> is the preferred interface type for variables and parameters; ArrayList<T> is the most common concrete implementation. Note size() (a method call, with parentheses) vs Ruby's length or size. Wrapping List.of() in new ArrayList<>() makes the list mutable.
HashMap (Hash)
scores = { alice: 95, bob: 87, carol: 92 } scores[:dave] = 78 puts scores[:alice] puts scores.key?(:dave) scores.each { |name, score| puts "#{name}: #{score}" }
import java.util.HashMap; import java.util.Map; import java.util.TreeMap; class Main { public static void main(String[] args) { Map<String, Integer> scores = new HashMap<>(); scores.put("alice", 95); scores.put("bob", 87); scores.put("carol", 92); scores.put("dave", 78); System.out.println(scores.get("alice")); System.out.println(scores.containsKey("dave")); new TreeMap<>(scores).forEach((name, score) -> System.out.println(name + ": " + score)); } }
HashMap<K,V> is the Java equivalent of a Ruby hash. Pairs are added with .put(), retrieved with .get(), and tested with .containsKey(). Unlike Ruby hashes (which preserve insertion order since Ruby 1.9), HashMap does not guarantee any ordering β€” use LinkedHashMap for insertion-order preservation or TreeMap for sorted keys. forEach with a lambda is the idiomatic way to iterate key-value pairs.
HashSet (Set)
require 'set' languages = Set.new(["ruby", "java", "python"]) languages.add("go") puts languages.include?("java") puts languages.include?("perl") puts languages.size
import java.util.HashSet; import java.util.Set; class Main { public static void main(String[] args) { Set<String> languages = new HashSet<>(Set.of("ruby", "java", "python")); languages.add("go"); System.out.println(languages.contains("java")); System.out.println(languages.contains("perl")); System.out.println(languages.size()); } }
HashSet<T> stores unique values with no guaranteed ordering β€” the Java equivalent of Ruby's Set. Adding a duplicate silently does nothing. The contains() method tests membership in O(1) average time. For ordered unique values, use TreeSet<T> (sorted by natural order) or LinkedHashSet<T> (insertion order). Set.of() (Java 9+) creates an immutable set literal, analogous to Ruby's Set[...].
Immutable Collections
colors = ["red", "green", "blue"].freeze puts colors.frozen? puts colors.inspect # colors.push("yellow") # raises FrozenError
import java.util.List; import java.util.Map; import java.util.Set; class Main { public static void main(String[] args) { List<String> colors = List.of("red", "green", "blue"); Map<String, Integer> codes = Map.of("red", 1, "green", 2, "blue", 3); Set<Integer> primes = Set.of(2, 3, 5, 7, 11); System.out.println(colors); System.out.println(codes.get("green")); System.out.println(primes.contains(7)); // colors.add("yellow"); // throws UnsupportedOperationException } }
List.of(), Map.of(), and Set.of() (Java 9+) create immutable collections that throw UnsupportedOperationException on any mutation attempt. They are null-hostile β€” no null elements or keys allowed. These are the Java equivalent of .freeze in Ruby. Prefer these factory methods for collection literals that do not need to grow β€” they communicate intent clearly and prevent accidental mutation.
Sorting Collections
words = ["banana", "apple", "cherry", "date"] puts words.sort.inspect puts words.sort_by(&:length).inspect puts words.sort { |a, b| b <=> a }.inspect
import java.util.ArrayList; import java.util.Comparator; import java.util.List; class Main { public static void main(String[] args) { List<String> words = new ArrayList<>(List.of("banana", "apple", "cherry", "date")); words.sort(Comparator.naturalOrder()); System.out.println(words); words.sort(Comparator.comparingInt(String::length)); System.out.println(words); words.sort(Comparator.reverseOrder()); System.out.println(words); } }
list.sort(comparator) sorts in place using a Comparator for custom ordering. Comparator.comparingInt() extracts an integer sort key β€” equivalent to Ruby's sort_by. Comparator.reverseOrder() reverses natural ordering. Comparators can be chained with .thenComparing() for multi-key sorting, analogous to sort_by { [_1.length, _1] } in Ruby.
Control Flow
if / else if / else
temperature = 22 if temperature > 30 puts "hot" elsif temperature > 20 puts "comfortable" elsif temperature > 10 puts "cool" else puts "cold" end
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("comfortable"); } else if (temperature > 10) { System.out.println("cool"); } else { System.out.println("cold"); } } }
Java's if/else if/else works identically to Ruby's if/elsif/else, with two syntactic differences: conditions must be enclosed in parentheses, and bodies must be enclosed in curly braces (or a single statement, though braces are always recommended to prevent subtle bugs). Java uses else if (two words) where Ruby uses elsif (one word, no space).
Switch Expressions
day = "Monday" type = case day when "Saturday", "Sunday" then "weekend" when "Friday" then "almost weekend" else "weekday" end puts type
class Main { public static void main(String[] args) { String day = "Monday"; String type = switch (day) { case "Saturday", "Sunday" -> "weekend"; case "Friday" -> "almost weekend"; default -> "weekday"; }; System.out.println(type); } }
The switch expression (Java 14+) is a significant improvement over the old switch statement. The arrow -> syntax eliminates fall-through, allows multiple case labels per arm, and makes the whole construct an expression that returns a value β€” directly analogous to Ruby's case/when. Older Java code uses switch statements with break on every arm; the expression form is always preferable in modern Java.
C-style for Loop
5.times { |index| puts "index: #{index}" }
class Main { public static void main(String[] args) { for (int index = 0; index < 5; index++) { System.out.println("index: " + index); } } }
Java's for loop is C-style: initializer, condition, and increment expression all appear in the header. Ruby rarely uses index-based loops β€” the idiomatic style is .times, .each, or .map. In Java, the C-style loop is common when an explicit index is needed; for iterating over a collection, use the enhanced for loop instead.
Enhanced for Loop
fruits = ["apple", "banana", "cherry"] fruits.each { |fruit| puts fruit }
import java.util.List; class Main { public static void main(String[] args) { List<String> fruits = List.of("apple", "banana", "cherry"); for (String fruit : fruits) { System.out.println(fruit); } } }
The enhanced for loop (for-each loop) iterates over any Iterable β€” arrays, lists, sets, map key/value sets, and so on. It is the closest Java equivalent to Ruby's .each. The colon : reads as "in": for (String fruit : fruits) means "for each fruit in fruits". The loop does not provide an index; use the C-style for loop or IntStream.range() when you need the index.
while Loop
counter = 1 while counter <= 5 puts counter counter += 1 end
class Main { public static void main(String[] args) { int counter = 1; while (counter <= 5) { System.out.println(counter); counter++; } } }
Java's while loop is syntactically identical to Ruby's except the condition requires parentheses and the body requires curly braces. Java also has a do/while loop (uncommon in practice) that guarantees the body executes at least once. Unlike Ruby, Java has no until loop or loop keyword β€” while (true) with a break is the standard idiom for an infinite loop.
Methods
Static Methods
def square(number) = number * number def cube(number) = number ** 3 puts square(5) puts cube(3)
class Main { static int square(int number) { return number * number; } static int cube(int number) { return number * number * number; } public static void main(String[] args) { System.out.println(square(5)); System.out.println(cube(3)); } }
In Java there are no standalone functions β€” every method must belong to a class. static methods belong to the class itself rather than to an instance, making them callable without creating an object (analogous to Ruby module methods). Unlike Ruby, Java methods require explicit return type declarations in the signature, and return is always written explicitly β€” there is no implicit return of the last expression.
Method Overloading
def greet(name, greeting: "Hello") "#{greeting}, #{name}!" end puts greet("Alice") puts greet("Bob", greeting: "Hi")
class Main { static String greet(String name) { return "Hello, " + name + "!"; } static String greet(String name, String greeting) { return greeting + ", " + name + "!"; } public static void main(String[] args) { System.out.println(greet("Alice")); System.out.println(greet("Bob", "Hi")); } }
Method overloading allows multiple methods with the same name but different parameter signatures. Java resolves the correct version at compile time based on argument types and count. Ruby achieves similar flexibility with optional and keyword arguments β€” Java's approach is more explicit and type-safe. Overloading is pervasive in Java's standard library: System.out.println(int), println(String), and println(double) are all separate method implementations.
Varargs (Variadic Arguments)
def sum(*numbers) numbers.sum end puts sum(1, 2, 3) puts sum(10, 20) puts sum(1, 2, 3, 4, 5)
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)); System.out.println(sum(10, 20)); System.out.println(sum(1, 2, 3, 4, 5)); } }
Varargs (Type... name) allow a method to accept any number of arguments of a given type. Inside the method, the parameter behaves as an array. Java's int... numbers is the direct equivalent of Ruby's splat operator *numbers. A varargs parameter must be the last parameter in the signature, and a method may have at most one.
Method References
words = ["hello", "world", "ruby"] puts words.map(&:upcase).inspect puts words.map(&:length).inspect
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<String> words = List.of("hello", "world", "java"); List<String> upper = words.stream().map(String::toUpperCase).collect(Collectors.toList()); List<Integer> lengths = words.stream().map(String::length).collect(Collectors.toList()); System.out.println(upper); System.out.println(lengths); } }
Method references (ClassName::methodName) are shorthand for a lambda that simply calls a single method. String::toUpperCase is equivalent to the lambda word -> word.toUpperCase(). They are the Java equivalent of Ruby's &:method_name shorthand (Symbol#to_proc). Method references can refer to instance methods on the argument, static methods, and constructors (ClassName::new).
Streams & Lambdas
Lambda Expressions
multiply = ->(a, b) { a * b } puts multiply.call(3, 4) square = ->(n) { n ** 2 } puts [1, 2, 3, 4, 5].map(&square).inspect
import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; class Main { public static void main(String[] args) { BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; System.out.println(multiply.apply(3, 4)); Function<Integer, Integer> square = number -> number * number; List<Integer> squares = List.of(1, 2, 3, 4, 5).stream() .map(square) .collect(Collectors.toList()); System.out.println(squares); } }
Java lambdas are anonymous function objects implementing a functional interface. The type must be declared explicitly β€” Function<Integer,Integer> for a function taking and returning an Integer, BiFunction<A,B,R> for one with two inputs. The function is invoked with .apply(), not .call() as in Ruby. Ruby's lambdas are more flexible; Java's must conform to exactly one of the types in java.util.function.
Stream filter (select)
numbers = (1..10).to_a puts numbers.select { |n| n.even? }.inspect
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> evens = numbers.stream() .filter(number -> number % 2 == 0) .collect(Collectors.toList()); System.out.println(evens); } }
The Stream API (Java 8+) provides functional-style operations on sequences. stream() converts the collection to a Stream, filter() keeps elements matching the predicate (equivalent to Ruby's select), and collect(Collectors.toList()) converts the result back to a list. Streams are lazy β€” intermediate operations do not execute until a terminal operation (collect, forEach, findFirst, etc.) is called.
Stream map (transform)
words = ["hello", "world", "ruby"] puts words.map(&:length).inspect puts words.map(&:upcase).inspect
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<String> words = List.of("hello", "world", "java"); List<Integer> lengths = words.stream() .map(String::length) .collect(Collectors.toList()); System.out.println(lengths); List<String> upper = words.stream() .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println(upper); } }
map() on a stream transforms each element, producing a new stream with the same number of elements β€” exactly Ruby's Enumerable#map. The resulting stream's type changes to match the mapping function's return type. Use mapToInt(), mapToDouble(), or mapToLong() to get a primitive stream, which unlocks direct .sum(), .average(), and .max() operations without collecting first.
Stream reduce (inject)
numbers = [1, 2, 3, 4, 5] puts numbers.sum puts numbers.reduce(:+) puts numbers.reduce(10, :+) puts numbers.reduce { |total, n| total * n }
import java.util.List; import java.util.Optional; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(1, 2, 3, 4, 5); int sum = numbers.stream().mapToInt(Integer::intValue).sum(); System.out.println(sum); // 15 int withStart = numbers.stream().reduce(10, Integer::sum); System.out.println(withStart); // 25 Optional<Integer> product = numbers.stream().reduce((total, n) -> total * n); System.out.println(product.get()); // 120 } }
reduce() on a stream is equivalent to Ruby's Enumerable#reduce (also called inject). The two-argument form takes an identity value and a combining function; the single-argument form returns an Optional<T> because an empty stream has no meaningful value. For numeric sums, mapToInt().sum() is more readable and more efficient. The Optional return type forces the caller to acknowledge the empty-stream case.
Collectors β€” groupingBy, joining
words = ["apple", "banana", "cherry", "avocado"] grouped = words.group_by { |word| word[0] } puts grouped.sort.to_h.inspect
import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<String> words = List.of("apple", "banana", "cherry", "avocado"); Map<Character, List<String>> grouped = words.stream() .collect(Collectors.groupingBy(word -> word.charAt(0))); new TreeMap<>(grouped).forEach((letter, group) -> System.out.println(letter + ": " + group)); String joined = words.stream().collect(Collectors.joining(", ")); System.out.println(joined); } }
Collectors.groupingBy() is the Java equivalent of Ruby's group_by β€” it partitions stream elements into a Map keyed by the classifier function. Collectors.joining() concatenates strings with a separator, analogous to Ruby's Array#join. Other useful collectors include Collectors.counting() (count per group) and Collectors.toMap() (arbitrary key/value extraction). Wrapping in new TreeMap<>() sorts the output by key.
Stream Chaining
words = ["apple", "fig", "banana", "kiwi", "cherry", "date"] result = words .select { |word| word.length > 3 } .map(&:upcase) .sort .first(3) puts result.inspect
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<String> words = List.of("apple", "fig", "banana", "kiwi", "cherry", "date"); List<String> result = words.stream() .filter(word -> word.length() > 3) .map(String::toUpperCase) .sorted() .limit(3) .collect(Collectors.toList()); System.out.println(result); } }
Streams are designed for chaining β€” each intermediate operation returns a new Stream so multiple transformations compose in a single pipeline that executes only when the terminal operation is reached. Java's .sorted() corresponds to Ruby's .sort, .limit(n) to .first(n), and .filter() to .select(). The pipeline style reads top-to-bottom, mirroring Ruby's method chaining idiom.
Classes & OOP
Class Definition
class Animal def initialize(name, sound) @name = name @sound = sound end def speak = "#{@name} says #{@sound}" end dog = Animal.new("Rex", "woof") puts dog.speak
class Animal { String name; String sound; Animal(String name, String sound) { this.name = name; this.sound = sound; } String speak() { return name + " says " + sound; } } class Main { public static void main(String[] args) { Animal dog = new Animal("Rex", "woof"); System.out.println(dog.speak()); } }
Java and Ruby classes share the same structure: fields hold state, constructors initialize instances, and methods define behavior. The key differences are explicit type declarations on every field and parameter, and the constructor name matching the class name rather than Ruby's initialize. this in Java is equivalent to Ruby's self β€” it disambiguates between a parameter and a field when they share a name.
Encapsulation β€” Getters & Setters
class Person attr_reader :name attr_accessor :age def initialize(name, age) @name = name @age = age end end person = Person.new("Alice", 30) puts person.name person.age = 31 puts person.age
class Person { private String name; private int age; Person(String name, int age) { this.name = name; this.age = age; } String getName() { return name; } int getAge() { return age; } void setAge(int age) { this.age = age; } } class Main { public static void main(String[] args) { Person person = new Person("Alice", 30); System.out.println(person.getName()); person.setAge(31); System.out.println(person.getAge()); } }
Java conventionally makes fields private and exposes them through getter and setter methods (getName(), setAge()) β€” the JavaBeans naming convention. This is the Java equivalent of Ruby's attr_reader and attr_accessor. Many frameworks (Spring, Hibernate, Jackson) rely on this convention for automatic configuration. Modern Java prefers records for immutable data, which generate accessors automatically without getters/setters.
Inheritance
class Shape def initialize(color) = @color = color def describe = "A #{@color} #{shape_name}" def shape_name = "shape" end class Circle < Shape def initialize(color, radius) super(color) @radius = radius end def area = Math::PI * @radius ** 2 def shape_name = "circle" end circle = Circle.new("red", 5) puts circle.describe puts circle.area.round(2)
class Shape { String color; Shape(String color) { this.color = color; } String describe() { return "A " + color + " " + shapeName(); } String shapeName() { return "shape"; } } class Circle extends Shape { double radius; Circle(String color, double radius) { super(color); this.radius = radius; } double area() { return Math.PI * radius * radius; } @Override String shapeName() { return "circle"; } } class Main { public static void main(String[] args) { Circle circle = new Circle("red", 5); System.out.println(circle.describe()); System.out.printf("%.2f%n", circle.area()); } }
Java uses extends for inheritance (Ruby uses <). super() in a constructor calls the parent constructor, exactly like Ruby's super. The @Override annotation is not required but is strongly recommended β€” it causes a compile error if the annotated method does not actually override anything, catching typos silently. Java supports single inheritance only; additional behavior is composed through interfaces.
Abstract Classes
class Vehicle def fuel_type raise NotImplementedError, "#{self.class} must implement fuel_type" end def describe = "Runs on #{fuel_type}" end class Car < Vehicle def fuel_type = "gasoline" end puts Car.new.describe
abstract class Vehicle { abstract String fuelType(); String describe() { return "Runs on " + fuelType(); } } class Car extends Vehicle { @Override String fuelType() { return "gasoline"; } } class Main { public static void main(String[] args) { Car car = new Car(); System.out.println(car.describe()); // new Vehicle(); // compile error β€” cannot instantiate abstract class } }
Abstract classes in Java cannot be instantiated β€” they exist only to be subclassed. Abstract methods declare a contract that all concrete subclasses must fulfill, enforced at compile time. Ruby lacks abstract classes; the convention is to raise NotImplementedError at runtime, which is a weaker guarantee. In Java, forgetting to implement an abstract method is a compilation error. Abstract classes may contain concrete methods (with implementations) alongside abstract ones.
Static Fields & Methods
class Counter @@count = 0 def initialize @@count += 1 end def self.count = @@count end Counter.new Counter.new Counter.new puts Counter.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 fields and methods belong to the class itself, not to any instance β€” shared across all instances. This is the Java equivalent of Ruby's class variables (@@var) and class methods (def self.method). Static members are accessed via the class name (Counter.getCount()), not via an instance. The main method itself is static β€” it can be called by the JVM before any instance is created.
Records
Record Definition
Point = Data.define(:x, :y) point = Point.new(x: 3, y: 4) puts point.x puts point.y puts point
record Point(int x, int y) {} class Main { public static void main(String[] args) { Point point = new Point(3, 4); System.out.println(point.x()); System.out.println(point.y()); System.out.println(point); // Point[x=3, y=4] } }
Records (Java 16+) are a concise syntax for immutable data classes. A record declaration automatically generates a constructor, accessor methods named after the components (x() and y(), not getX()), equals(), hashCode(), and toString(). Records are the Java equivalent of Ruby's Data.define() or a frozen Struct. All components are implicitly final β€” records are inherently immutable and thread-safe.
Custom Methods in Records
Point = Data.define(:x, :y) do def distance_from_origin Math.sqrt(x**2 + y**2) end end point = Point.new(x: 3, y: 4) puts point.distance_from_origin
record Point(int x, int y) { double distanceFromOrigin() { return Math.sqrt(x * x + y * y); } Point translate(int dx, int dy) { return new Point(x + dx, y + dy); } } class Main { public static void main(String[] args) { Point point = new Point(3, 4); Point moved = point.translate(1, 1); System.out.printf("%.2f%n", point.distanceFromOrigin()); System.out.println(moved); } }
Custom methods can be added inside the record body and have full access to the record's components. Because components are final, methods that "modify" a record must return a new instance β€” the translate method above returns a new Point rather than mutating the existing one. This immutable-update pattern is the Java equivalent of Ruby's Data#with.
Interfaces
Interface Definition & Implementation
module Printable def print_info puts "Info: #{info}" end end class Document include Printable def initialize(title) = @title = title def info = @title end Document.new("My Report").print_info
interface Printable { String info(); default void printInfo() { System.out.println("Info: " + info()); } } class Document implements Printable { private String title; Document(String title) { this.title = title; } @Override public String info() { return title; } } class Main { public static void main(String[] args) { new Document("My Report").printInfo(); } }
Java interfaces define contracts β€” method signatures (and optional default implementations) that implementing classes must provide. A class declares implements InterfaceName and must implement all abstract methods. Unlike Ruby modules, interfaces cannot hold instance state. Default methods (Java 8+) allow an interface to provide a method body β€” the closest Java equivalent to Ruby module methods that call abstract methods defined by the including class.
Multiple Interface Implementation
module Flyable def fly = "#{self.class} is flying" end module Swimmable def swim = "#{self.class} is swimming" end class Duck include Flyable include Swimmable end duck = Duck.new puts duck.fly puts duck.swim
interface Flyable { default String fly() { return getClass().getSimpleName() + " is flying"; } } interface Swimmable { default String swim() { return getClass().getSimpleName() + " is swimming"; } } class Duck implements Flyable, Swimmable {} class Main { public static void main(String[] args) { Duck duck = new Duck(); System.out.println(duck.fly()); System.out.println(duck.swim()); } }
A Java class may implement multiple interfaces, separated by commas β€” this is how Java achieves the mixin-like composition that Ruby does with include. If two implemented interfaces have conflicting default methods with the same signature, the class must explicitly override the method to resolve the ambiguity. Java allows only single inheritance of classes (extends) but unlimited interface implementation (implements).
Functional Interfaces
is_even = ->(n) { n.even? } puts [1, 2, 3, 4, 5].select(&is_even).inspect puts is_even.call(4)
import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; class Main { public static void main(String[] args) { Predicate<Integer> isEven = number -> number % 2 == 0; List<Integer> numbers = List.of(1, 2, 3, 4, 5); List<Integer> evens = numbers.stream().filter(isEven).collect(Collectors.toList()); System.out.println(evens); System.out.println(isEven.test(4)); } }
A functional interface has exactly one abstract method and can be satisfied by a lambda. Java's java.util.function package provides the standard set: Predicate<T> (boolean test via .test()), Function<T,R> (transform via .apply()), Consumer<T> (side effect, no return), Supplier<T> (provide value), BiFunction<T,U,R> (two inputs). Ruby's blocks, procs, and lambdas are first-class without this ceremony β€” Java's functional interfaces are the scaffolding that makes lambdas fit into the type system.
Pattern Matching
instanceof Pattern Matching
def describe(value) case value when Integer then "integer: #{value}" when String then "string of length #{value.length}" when Array then "array with #{value.size} elements" else "unknown" end end puts describe(42) puts describe("hello") puts describe([1, 2, 3])
class Main { static String describe(Object value) { if (value instanceof Integer count) { return "integer: " + count; } else if (value instanceof String text) { return "string of length " + text.length(); } else if (value instanceof int[] numbers) { return "array with " + numbers.length + " elements"; } return "unknown"; } public static void main(String[] args) { System.out.println(describe(42)); System.out.println(describe("hello")); System.out.println(describe(new int[]{1, 2, 3})); } }
The instanceof pattern (Java 16+) tests the type and binds the cast result to a new variable in one step. Before Java 16, you had to write if (value instanceof String) { String text = (String) value; ... } β€” three steps instead of one. This is the Java equivalent of Ruby's case/when with class matching. The bound variable (text, count) is only in scope within the branch where the pattern matched.
Switch Pattern Matching
def classify(value) case value in Integer => n if n > 0 then "positive int: #{n}" in Integer => n if n < 0 then "negative int: #{n}" in Integer then "zero" in Float => decimal then "double: #{decimal}" in String => text then "string: #{text}" else "unknown" end end puts classify(42) puts classify(-5) puts classify(0) puts classify(3.14)
class Main { static String classify(Object value) { return switch (value) { case Integer number when number > 0 -> "positive int: " + number; case Integer number when number < 0 -> "negative int: " + number; case Integer number -> "zero"; case Double decimal -> "double: " + decimal; case String text -> "string: " + text; default -> "unknown"; }; } public static void main(String[] args) { System.out.println(classify(42)); System.out.println(classify(-5)); System.out.println(classify(0)); System.out.println(classify(3.14)); } }
Switch pattern matching (Java 21+) combines type testing and value extraction directly in a switch expression. The when keyword adds a guard condition, equivalent to Ruby's case/in with if. Patterns are tested top to bottom and the first match wins. Applied to sealed class hierarchies, the compiler can verify all cases are covered without a default clause β€” analogous to exhaustive matching in Rust or Kotlin.
Sealed Classes
# Ruby: no sealed classes β€” any class can inherit from any other class Shape; end class Circle < Shape; end class Rectangle < Shape; end # No compile-time guarantee that only these two exist
sealed interface Shape permits Circle, Rectangle {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} class Main { static double area(Shape shape) { return switch (shape) { case Circle circle -> Math.PI * circle.radius() * circle.radius(); case Rectangle rect -> rect.width() * rect.height(); // No default needed β€” compiler knows all permitted subtypes }; } public static void main(String[] args) { System.out.printf("Circle area: %.2f%n", area(new Circle(5))); System.out.printf("Rectangle area: %.2f%n", area(new Rectangle(4, 6))); } }
Sealed classes (Java 17+) restrict which classes may extend or implement them β€” the permits clause names every allowed subtype. This enables exhaustive pattern matching: if Shape can only be Circle or Rectangle, the compiler knows the switch handles all cases and no default is needed. This is Java's approach to algebraic data types, a concept familiar from Rust, Kotlin, and Haskell. Ruby has no equivalent β€” duck typing makes such restrictions impossible to express in the type system.
Error Handling
try / catch / finally
def divide(numerator, denominator) raise ArgumentError, "cannot divide by zero" if denominator == 0 numerator.to_f / denominator rescue ArgumentError => error puts "Error: #{error.message}" nil ensure puts "division attempted" end puts divide(10, 2) puts divide(10, 0).inspect
class Main { static Double divide(double numerator, double denominator) { try { if (denominator == 0) throw new ArithmeticException("cannot divide by zero"); return numerator / denominator; } catch (ArithmeticException error) { System.out.println("Error: " + error.getMessage()); return null; } finally { System.out.println("division attempted"); } } public static void main(String[] args) { System.out.println(divide(10, 2)); System.out.println(divide(10, 0)); } }
Java's try/catch/finally is structurally identical to Ruby's begin/rescue/ensure. The finally block always runs regardless of whether an exception was thrown β€” it is Java's ensure. Multiple catch blocks handle different exception types; Java 7+ allows catching multiple types in one block with catch (IOException | SQLException error). Unlike Ruby, Java requires an explicit try block around the code that might throw.
Checked Exceptions
# Ruby: all exceptions are unchecked β€” handling is always optional def parse_number(text) Integer(text) rescue ArgumentError => error puts "Parse failed: #{error.message}" 0 end puts parse_number("42") puts parse_number("oops")
class Main { // throws declares the checked exception β€” callers must handle or re-declare it static int parseNumber(String text) throws NumberFormatException { return Integer.parseInt(text); } public static void main(String[] args) { try { System.out.println(parseNumber("42")); System.out.println(parseNumber("oops")); } catch (NumberFormatException error) { System.out.println("Parse failed: " + error.getMessage()); } } }
Java has two exception categories: checked (subclasses of Exception that are not RuntimeException) and unchecked (subclasses of RuntimeException). Checked exceptions must either be caught or declared in the method signature with throws β€” failing to handle them is a compile error. This forces callers to acknowledge potential failures at API boundaries. Ruby has no equivalent β€” all exceptions are unchecked. The throws clause serves as compile-time-enforced documentation of what can go wrong.
Custom Exceptions
class InsufficientFundsError < StandardError def initialize(amount, balance) super("Cannot withdraw #{amount}: balance is #{balance}") end end def withdraw(balance, amount) raise InsufficientFundsError.new(amount, balance) if amount > balance balance - amount end puts withdraw(100, 30) begin withdraw(100, 150) rescue InsufficientFundsError => error puts error.message end
class InsufficientFundsException extends RuntimeException { InsufficientFundsException(double amount, double balance) { super(String.format("Cannot withdraw %.2f: balance is %.2f", amount, balance)); } } class Main { static double withdraw(double balance, double amount) { if (amount > balance) throw new InsufficientFundsException(amount, balance); return balance - amount; } public static void main(String[] args) { System.out.println(withdraw(100, 30)); try { withdraw(100, 150); } catch (InsufficientFundsException error) { System.out.println(error.getMessage()); } } }
Custom exceptions extend either Exception (checked) or RuntimeException (unchecked). Extending RuntimeException is common for application-level errors that callers are not always expected to catch. The constructor calls super(message) to set the message retrievable via getMessage(), directly paralleling Ruby's StandardError. By convention, exception class names end with Exception.
try-with-resources
# Ruby: block form automatically closes the resource require 'stringio' buffer = StringIO.new buffer.write("hello from Ruby ") buffer.write("second line ") puts buffer.string.strip
import java.io.PrintWriter; import java.io.StringWriter; class Main { public static void main(String[] args) throws Exception { StringWriter buffer = new StringWriter(); try (PrintWriter writer = new PrintWriter(buffer)) { writer.println("hello from try-with-resources"); writer.println("second line"); } // writer closed automatically here, even if an exception was thrown System.out.println(buffer.toString().strip()); } }
Try-with-resources (Java 7+) automatically closes any AutoCloseable resource at the end of the block, even if an exception is thrown. It is the Java equivalent of Ruby's block form (File.open(...) { |file| ... }) or an ensure block with explicit close(). Any class implementing AutoCloseable works: file streams, database connections, network sockets. Without try-with-resources, forgetting to close a resource is one of the most common Java bugs.
Generics
Generic Classes
# Ruby: duck typing β€” no type parameter needed class Box def initialize(content) = @content = content def content = @content def to_s = "Box(#{@content})" end puts Box.new(42) puts Box.new("hello")
class Box<T> { private T content; Box(T content) { this.content = content; } T content() { return content; } @Override public String toString() { return "Box(" + content + ")"; } } class Main { public static void main(String[] args) { Box<Integer> numberBox = new Box<>(42); Box<String> textBox = new Box<>("hello"); System.out.println(numberBox); System.out.println(textBox); System.out.println(numberBox.content() + 8); // type-safe: guaranteed Integer } }
Generics let a class work with different types while preserving compile-time type safety. Box<T> is a blueprint where T is a type parameter filled in at use time. The key benefit over Ruby's duck typing: numberBox.content() is guaranteed at compile time to return an Integer, so numberBox.content() + 8 needs no cast and cannot fail at runtime with a type error. Ruby's equivalent works at runtime without type parameters because every object is always an object.
Generic Methods
def first_or_default(collection, default_value = nil) collection.first || default_value end puts first_or_default([1, 2, 3]) puts first_or_default([], "empty").inspect puts first_or_default(["a", "b"], "x")
import java.util.List; class Main { static <T> T firstOrDefault(List<T> collection, T defaultValue) { return collection.isEmpty() ? defaultValue : collection.get(0); } public static void main(String[] args) { System.out.println(firstOrDefault(List.of(1, 2, 3), 0)); System.out.println(firstOrDefault(List.of(), "empty")); System.out.println(firstOrDefault(List.of("a", "b"), "x")); } }
Generic methods declare their type parameter before the return type: <T> T methodName(...). The compiler infers T from the argument types at the call site β€” firstOrDefault(List.of(1, 2, 3), 0) infers T = Integer without any explicit annotation. Generic methods are the Java mechanism for writing reusable, type-safe utility functions, equivalent to Ruby's duck-typed methods but with compile-time guarantees instead of runtime ones.