PONY λ M2 Modula-2

Java.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Java and Ruby

Java 26 Ruby 4.0
Output & Comments
Hello World
class Main { public static void main(String[] args) { System.out.println("Hello, Ruby!"); } }
puts "Hello, Ruby!"
Ruby programs need no class wrapper, no main method, and no access modifiers. puts prints a value followed by a newline — the exact equivalent of System.out.println. Ruby scripts run top-to-bottom, with any code at the top level executed immediately.
puts vs print
class Main { public static void main(String[] args) { System.out.println("line one"); System.out.print("no newline"); System.out.print(" here\n"); } }
puts "line one" print "no newline" print " here\n"
puts appends a newline after each argument; print does not. Ruby also has p, which calls inspect on its argument — useful for debugging because it shows the type: p "hi" prints "hi" (with quotes), while puts "hi" prints hi.
p — inspect output for debugging
class Main { public static void main(String[] args) { String greeting = "hello"; int[] numbers = {1, 2, 3}; System.out.println(greeting.getClass().getSimpleName() + ": " + greeting); System.out.println(java.util.Arrays.toString(numbers)); } }
greeting = "hello" numbers = [1, 2, 3] p greeting p numbers
p calls .inspect on each argument, which shows the type and full structure. p "hello" prints "hello" (with quotes); p [1, 2, 3] prints [1, 2, 3]; p nil prints nil. Unlike Java where debug printing requires manual type extraction, p works correctly for any object.
Comments
class Main { // Single-line comment /* * Multi-line comment */ public static void main(String[] args) { int count = 0; // inline comment System.out.println(count); } }
# Single-line comment =begin Multi-line comment =end count = 0 # inline comment puts count
Ruby uses # for single-line comments — shorter than Java's //. The =begin / =end block comment exists but is rarely used in practice; most Ruby code uses consecutive # lines for multi-line comments. The =begin must start at the very beginning of the line.
Variables & Types
No type declarations
class Main { public static void main(String[] args) { int count = 42; String greeting = "Hello"; double price = 9.99; boolean active = true; System.out.println(count); System.out.println(greeting); System.out.println(price); System.out.println(active); } }
count = 42 greeting = "Hello" price = 9.99 active = true puts count puts greeting puts price puts active
Ruby variables are created on first assignment — no type annotation, no var. The interpreter determines the type at runtime. There is no boolean type; the classes are TrueClass and FalseClass, and the values are the singleton objects true and false.
nil instead of null
class Main { public static void main(String[] args) { String value = null; System.out.println(value == null); System.out.println(value); // value.length(); // → NullPointerException } }
value = nil puts value.nil? puts value.inspect puts value.to_s.length # nil.to_s is "", no exception
Ruby's nil is an actual object of class NilClass. Unlike Java's null, nil responds to methods: nil.to_s returns "", nil.to_a returns [], and nil.to_i returns 0. Calling an undefined method on nil raises NoMethodError, not a NullPointerException.
Dynamic typing
class Main { public static void main(String[] args) { // var locks the type at declaration var count = 42; // count = "can't reassign to String"; // compile error Object flexible = 42; flexible = "now a String"; flexible = 3.14; System.out.println(flexible); System.out.println(flexible.getClass().getSimpleName()); } }
value = 42 value = "now a String" value = 3.14 puts value puts value.class
In Ruby, any variable can hold any value at any time — there is no static type constraint, even with var. The .class method returns the actual runtime type. This is fundamentally different from Java's type system, where even var locks the inferred type at declaration.
Constants
class Main { static final int MAX_RETRIES = 3; static final String API_URL = "https://example.com"; public static void main(String[] args) { System.out.println(MAX_RETRIES); System.out.println(API_URL); } }
MAX_RETRIES = 3 API_URL = "https://example.com" puts MAX_RETRIES puts API_URL
Ruby constants start with an uppercase letter; SCREAMING_SNAKE_CASE is the convention. Unlike Java's final, Ruby constants are not truly immutable — reassigning one produces a warning but not an error. For enforced immutability, call .freeze: MAX_RETRIES = 3.freeze.
Type checking
class Main { public static void main(String[] args) { Object value = "hello"; System.out.println(value instanceof String); System.out.println(value instanceof Number); System.out.println(value.getClass().getSimpleName()); } }
value = "hello" puts value.is_a?(String) puts value.is_a?(Numeric) puts value.class
is_a? (or its alias kind_of?) tests whether an object's class matches or inherits from the given class — equivalent to Java's instanceof. .class returns the exact runtime class. Ruby also has respond_to?(:method_name) for duck-typing checks, which is often more idiomatic than checking the class.
Strings
String interpolation
class Main { public static void main(String[] args) { String name = "Alice"; int age = 30; String message = "Hello, " + name + "! You are " + age + " years old."; System.out.println(message); } }
name = "Alice" age = 30 message = "Hello, #{name}! You are #{age} years old." puts message
Ruby uses #{expression} inside double-quoted strings for interpolation — equivalent to Java's string concatenation or the Java 21 String Templates preview feature. Any expression works inside #{}, including method calls and arithmetic. Single-quoted strings do not interpolate: '#{name}' prints literally #{name}.
Single vs double quoted strings
class Main { public static void main(String[] args) { String withEscape = "tab:\there\nnewline"; String literal = "no extra escapes"; System.out.println(withEscape); System.out.println(literal); } }
with_escape = "tab:\there\nnewline" literal = 'no \n escape sequences or #{interpolation} here' puts with_escape puts literal
Ruby has two common string delimiters: double quotes process escape sequences (\n, \t) and interpolation (#{}); single quotes treat everything literally except \\ and \'. Use single quotes when the string needs neither — it signals intent to readers and avoids accidental interpolation.
Multiline strings (heredoc)
class Main { public static void main(String[] args) { String message = """ Dear Alice, Welcome to the team! Regards, HR """; System.out.print(message.strip()); System.out.println(); } }
message = <<~LETTER Dear Alice, Welcome to the team! Regards, HR LETTER puts message.chomp
Ruby's <<~HEREDOC (squiggly heredoc, available since Ruby 2.3) strips leading whitespace, matching Java's text block behavior. The terminator identifier is arbitrary. chomp removes the trailing newline. Unlike Java text blocks, Ruby heredocs support interpolation by default — use <<~'HEREDOC' (single-quoted terminator) to disable interpolation.
Common string methods
class Main { public static void main(String[] args) { String greeting = " Hello, World! "; System.out.println(greeting.strip()); System.out.println(greeting.strip().toUpperCase()); System.out.println(greeting.strip().toLowerCase()); System.out.println(greeting.strip().length()); System.out.println(greeting.strip().replace("World", "Ruby")); } }
greeting = " Hello, World! " puts greeting.strip puts greeting.strip.upcase puts greeting.strip.downcase puts greeting.strip.length puts greeting.strip.gsub("World", "Ruby")
Ruby string method names differ from Java's: strip is the same, but length (or size) replaces length(), upcase/downcase replace toUpperCase()/toLowerCase(), and gsub (global substitute) replaces replace. Methods chain naturally because Ruby strings are objects with a rich API.
String formatting
class Main { public static void main(String[] args) { String formatted = String.format("%-10s: %6.2f", "Price", 9.99); System.out.println(formatted); System.out.printf("Count: %d%n", 42); } }
formatted = "%-10s: %6.2f" % ["Price", 9.99] puts formatted puts "Count: %d" % 42
Ruby uses the % operator for printf-style formatting — a more concise syntax than Java's String.format. The format specifiers are the same as C's printf. Ruby also has sprintf and format as method aliases for the same operation. For most cases, string interpolation (#{}) is preferred over format strings.
Frozen strings (Ruby 4.0)
class Main { public static void main(String[] args) { // Java: String is always immutable String base = "Hello"; String extended = base + ", World!"; // creates a new String System.out.println(base); // unchanged System.out.println(extended); } }
# Ruby 4.0: string literals are frozen by default base = "Hello" puts base.frozen? # true begin base << ", World!" # attempting to mutate a frozen string rescue FrozenError => error puts error.message end mutable = base.dup # dup returns an unfrozen copy mutable << ", World!" puts mutable
In Ruby 4.0, all string literals are frozen by default, matching Java's long-standing String immutability. Attempting to mutate a frozen string (e.g. base << " world") raises FrozenError. Use dup or +"" to get a mutable copy when mutation is required. The +@ unary operator also returns a mutable version: +base.
Symbols
What is a symbol
class Main { public static void main(String[] args) { // Java has no direct equivalent — closest is String.intern() String status = "active".intern(); System.out.println(status); System.out.println(status == "active".intern()); // same object System.out.println(status.getClass().getSimpleName()); } }
status = :active puts status puts status.class puts status == :active puts :active.object_id == :active.object_id # always the same object
Ruby symbols are immutable, interned identifiers written with a colon prefix (:name). Unlike strings, every occurrence of the same symbol literal is the same object — :active.object_id == :active.object_id is always true. The closest Java analog is String.intern(), but symbols are a distinct type used for identifiers: hash keys, method names, and configuration options.
Symbol / string conversion
class Main { public static void main(String[] args) { // Java uses String constants where Ruby uses symbols String role = "user_role"; System.out.println(role.toUpperCase()); System.out.println(role.length()); } }
role = :user_role puts role.to_s puts role.to_s.upcase puts role.to_s.length puts "admin_role".to_sym.class
to_s converts a symbol to a string; to_sym converts a string to a symbol. Symbols respond to fewer methods than strings — they lack mutation methods since symbols are immutable. When you need to manipulate the text (split, substitute, format), convert to a string first.
Symbols as hash keys
import java.util.Map; class Main { public static void main(String[] args) { var person = Map.of("name", "Alice", "age", 30); System.out.println(person.get("name")); System.out.println(person.get("age")); } }
person = { name: "Alice", age: 30 } puts person[:name] puts person[:age]
The { key: value } syntax is Ruby's shorthand for symbol keys — { name: "Alice" } is equivalent to { :name => "Alice" }. Accessing hash values uses bracket notation with the symbol: person[:name]. String keys and symbol keys are distinct: { "name" => "Alice" }[:name] returns nil.
Numbers
Integer — one type, arbitrary size
class Main { public static void main(String[] args) { int smallNumber = 42; long largeNumber = 100_000_000_000L; System.out.println(smallNumber); System.out.println(largeNumber); System.out.println(Long.valueOf(largeNumber).getClass().getSimpleName()); } }
small_number = 42 large_number = 100_000_000_000 puts small_number puts large_number puts small_number.class puts large_number.class
Ruby has a single Integer class that automatically handles arbitrarily large integers — no int/long/BigInteger split. Underscores in numeric literals are for readability (like Java's 100_000) and are ignored by the interpreter. Ruby integers are full objects: 42.class, 42.even?, 42.times { } all work.
Integer iteration methods
class Main { public static void main(String[] args) { for (int count = 0; count < 3; count++) { System.out.println("tick " + count); } for (int number = 1; number <= 5; number++) { System.out.println(number); } } }
3.times { |count| puts "tick #{count}" } 1.upto(5) { |number| puts number }
Ruby integers have iteration methods built in: times counts from 0 to n–1, upto counts up to a limit (inclusive), and downto counts down. These replace Java's for (int i = 0; i < n; i++) loops for simple counting. The block argument receives the current value.
Float and integer division
class Main { public static void main(String[] args) { System.out.printf("%.4f%n", 10.0 / 3.0); System.out.println(10 / 3); // integer division System.out.println(10 % 3); // remainder } }
puts (10.0 / 3.0).round(4) puts 10 / 3 # integer division puts 10 % 3 # remainder
Ruby integer division truncates toward negative infinity (like Java). A float operand makes the result a float: 10 / 33, 10.0 / 33.3333.... The round, ceil, and floor methods are built into Float and Integer, so no Math class is needed for basic numeric operations.
Numeric predicate methods
class Main { public static void main(String[] args) { int number = 42; System.out.println(number % 2 == 0); System.out.println(number > 0); System.out.println(Math.abs(number)); System.out.println((int) Math.pow(2, 10)); } }
number = 42 puts number.even? puts number.positive? puts number.abs puts 2 ** 10
Ruby integers have predicate methods like even?, odd?, positive?, negative?, and zero? — replacing Java's arithmetic comparisons. The ** operator handles exponentiation (Java's Math.pow). Methods ending in ? conventionally return true or false.
Arrays
Creating arrays
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { var numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5)); var fruits = new ArrayList<>(List.of("apple", "banana", "cherry")); System.out.println(numbers); System.out.println(fruits.size()); } }
numbers = [1, 2, 3, 4, 5] fruits = ["apple", "banana", "cherry"] puts numbers.inspect puts fruits.length
Ruby arrays are created with bracket literals — no new ArrayList(), no generic type parameter, no imports. Arrays are dynamic and heterogeneous by default: [1, "two", :three, nil] is valid. The %w[apple banana cherry] shorthand creates an array of strings without quotation marks.
Accessing and slicing
import java.util.List; class Main { public static void main(String[] args) { var colors = List.of("red", "green", "blue", "yellow", "purple"); System.out.println(colors.get(0)); System.out.println(colors.get(colors.size() - 1)); System.out.println(colors.subList(1, 3)); } }
colors = ["red", "green", "blue", "yellow", "purple"] puts colors[0] puts colors[-1] # negative index: from the end puts colors[1, 2].inspect # start index, length puts colors[1..3].inspect # inclusive range slice
Ruby arrays support negative indices: colors[-1] is the last element, colors[-2] the second-to-last. Slicing accepts either array[start, length] or a range: array[1..3] (inclusive). These replace subList and eliminate the common Java off-by-one error where subList(from, to) excludes the last index.
push, pop, shift, unshift
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { var items = new ArrayList<>(List.of("b", "c")); items.add("d"); // push — add to end items.add(0, "a"); // unshift — add to front System.out.println(items.get(items.size() - 1)); items.remove(items.size() - 1); // pop System.out.println(items.get(0)); items.remove(0); // shift System.out.println(items); } }
items = ["b", "c"] items.push("d") # also: items << "d" items.unshift("a") puts items.last items.pop puts items.first items.shift puts items.inspect
push and pop operate on the end of the array (stack); unshift and shift operate on the beginning (queue). The << operator is an alias for push: items << "d". These replace Java's verbose add()/remove(size-1)/add(0, x)/remove(0) on ArrayList.
map, select, reject
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { var numbers = List.of(1, 2, 3, 4, 5, 6); var doubled = numbers.stream().map(n -> n * 2).collect(Collectors.toList()); var evens = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList()); var odds = numbers.stream().filter(n -> n % 2 != 0).collect(Collectors.toList()); System.out.println(doubled); System.out.println(evens); System.out.println(odds); } }
numbers = [1, 2, 3, 4, 5, 6] doubled = numbers.map { |number| number * 2 } evens = numbers.select { |number| number.even? } odds = numbers.reject { |number| number.even? } puts doubled.inspect puts evens.inspect puts odds.inspect
Ruby's map (alias: collect) transforms each element; select (alias: filter) keeps elements matching the block; reject keeps elements that do NOT match. These come from the Enumerable module, available on any class that implements each. No stream() call, no collect(Collectors.toList()) — the result is always an array.
reduce / inject
import java.util.List; class Main { public static void main(String[] args) { var numbers = List.of(1, 2, 3, 4, 5); int total = numbers.stream().reduce(0, Integer::sum); int product = numbers.stream().reduce(1, (acc, n) -> acc * n); System.out.println(total); System.out.println(product); } }
numbers = [1, 2, 3, 4, 5] total = numbers.reduce(0) { |accumulator, number| accumulator + number } product = numbers.inject(:*) # symbol shorthand for the * method puts total puts product
reduce (alias: inject) folds an array into a single value. The first argument is the initial accumulator; the block receives the accumulator and each element in turn. Ruby's shorthand inject(:+) uses the method name as a symbol — equivalent to stream().reduce(Integer::sum) in Java.
sort, uniq, flatten, compact
import java.util.ArrayList; import java.util.Collections; import java.util.List; class Main { public static void main(String[] args) { var numbers = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6)); Collections.sort(numbers); System.out.println(numbers); System.out.println(numbers.stream().distinct().sorted().toList()); } }
numbers = [3, 1, 4, 1, 5, 9, 2, 6] puts numbers.sort.inspect puts numbers.uniq.inspect nested = [[1, 2], [3, [4, 5]]] puts nested.flatten.inspect with_nils = [1, nil, 2, nil, 3] puts with_nils.compact.inspect
sort returns a new sorted array; sort! sorts in place (mutating methods conventionally end in !). uniq removes duplicates (like Java's distinct()). flatten recursively expands nested arrays — Java has no direct equivalent. compact removes nil values — like Java's stream filter for null.
Hashes
Creating hashes
import java.util.Map; class Main { public static void main(String[] args) { var person = Map.of("name", "Alice", "age", 30, "city", "Paris"); System.out.println(person.get("name")); System.out.println(person.size()); } }
person = { name: "Alice", age: 30, city: "Paris" } puts person[:name] puts person.size
Ruby hashes are created with curly-brace literals. The { key: value } syntax uses symbol keys (the most common style); { "key" => value } uses string keys or any other object as a key. Hashes maintain insertion order (since Ruby 1.9), equivalent to Java's LinkedHashMap. No imports, no type parameters.
Accessing and setting values
import java.util.HashMap; class Main { public static void main(String[] args) { var config = new HashMap<String, Object>(); config.put("timeout", 30); config.put("retries", 3); System.out.println(config.get("timeout")); System.out.println(config.getOrDefault("missing", "default")); config.put("timeout", 60); System.out.println(config.get("timeout")); } }
config = {} config[:timeout] = 30 config[:retries] = 3 puts config[:timeout] puts config.fetch(:missing, "default") config[:timeout] = 60 puts config[:timeout]
Hash values are set with bracket assignment: hash[:key] = value. Accessing a missing key returns nil (not an exception). fetch is the strict version: fetch(:key) raises KeyError if the key is absent; fetch(:key, default) returns the default instead. Use fetch when absence means a bug.
Iterating hashes
import java.util.Map; class Main { public static void main(String[] args) { var scores = Map.of("Alice", 95, "Bob", 87, "Carol", 91); scores.forEach((name, score) -> System.out.println(name + ": " + score)); System.out.println(scores.keySet().stream().toList()); System.out.println(scores.values().stream().toList()); } }
scores = { alice: 95, bob: 87, carol: 91 } scores.each { |name, score| puts "#{name}: #{score}" } puts scores.keys.inspect puts scores.values.inspect
each on a hash yields two-element arrays that Ruby automatically destructures: |key, value|. keys and values return plain arrays. Hashes also support the full Enumerable API — map, select, reject, any?, all? — with no need for entrySet().stream().
Hash with a default value
import java.util.HashMap; class Main { public static void main(String[] args) { var word_count = new HashMap<String, Integer>(); String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"}; for (String word : words) { word_count.merge(word, 1, Integer::sum); } System.out.println(word_count); } }
word_count = Hash.new(0) words = ["apple", "banana", "apple", "cherry", "banana", "apple"] words.each { |word| word_count[word] += 1 } puts word_count.inspect
Hash.new(default) sets the value returned for any missing key. word_count["missing"] returns 0 instead of nil, making += 1 safe on first access. For a computed default (e.g. an empty array per key), use a block: Hash.new { |hash, key| hash[key] = [] }.
Transforming hashes
import java.util.Map; import java.util.stream.Collectors; class Main { public static void main(String[] args) { var prices = Map.of("apple", 1.50, "banana", 0.75, "cherry", 2.00); var discounted = prices.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> Math.round(entry.getValue() * 0.9 * 100.0) / 100.0)); System.out.println(discounted); } }
prices = { apple: 1.50, banana: 0.75, cherry: 2.00 } discounted = prices.transform_values { |price| (price * 0.9).round(2) } puts discounted.inspect
transform_values returns a new hash with the same keys and transformed values — equivalent to Java's verbose Collectors.toMap identity-on-keys pattern. transform_keys does the same for keys. Both replace the entrySet().stream().collect(...) boilerplate. merge combines two hashes: defaults.merge(overrides).
Ranges
Range creation
import java.util.stream.IntStream; class Main { public static void main(String[] args) { // Java has no range literal — IntStream is the closest equivalent System.out.println(IntStream.rangeClosed(1, 10).boxed().toList()); System.out.println(IntStream.range(0, 5).boxed().toList()); System.out.println(IntStream.rangeClosed(1, 10).min().getAsInt()); System.out.println(IntStream.rangeClosed(1, 10).max().getAsInt()); } }
inclusive = (1..10) # includes 10 exclusive = (0...5) # excludes 5 puts inclusive.to_a.inspect puts exclusive.to_a.inspect puts inclusive.include?(5) puts inclusive.min puts inclusive.max
1..10 is an inclusive range (includes 10); 0...5 is exclusive (excludes 5). Ranges are first-class objects — call include?, min, max, each, and to_a on them directly. Java has no range literal; the closest is IntStream.rangeClosed, which requires boxing and lacks the concise expression form.
Iterating over ranges
class Main { public static void main(String[] args) { for (int number = 1; number <= 5; number++) { System.out.println(number * number); } for (char letter = 'a'; letter <= 'e'; letter++) { System.out.print(letter + " "); } System.out.println(); } }
(1..5).each { |number| puts number * number } ("a".."e").each { |letter| print "#{letter} " } puts
Ranges support each and the full Enumerable suite: map, select, reduce, first, etc. String ranges iterate over characters using Ruby's lexicographic successor. Ranges of dates and custom objects (any type with succ defined) can also be iterated. This is more expressive than Java's for loop, which works only with numbers.
Ranges in case / when
class Main { public static void main(String[] args) { int score = 78; String grade; if (score >= 90) grade = "A"; else if (score >= 80) grade = "B"; else if (score >= 70) grade = "C"; else if (score >= 60) grade = "D"; else grade = "F"; System.out.println(grade); } }
score = 78 grade = case score when 90..100 then "A" when 80..89 then "B" when 70..79 then "C" when 60..69 then "D" else "F" end puts grade
Ranges work naturally in Ruby's case/when expressions — each when calls === on the range, which checks membership. This replaces Java's cascading if/else if for numeric range checks and is far cleaner than Java's switch, which cannot match ranges. case in Ruby is an expression and returns the matched value.
Control Flow
if / elsif / else
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 if (temperature > 10) { System.out.println("cool"); } else { System.out.println("cold"); } } }
temperature = 22 if temperature > 30 puts "hot" elsif temperature > 20 puts "warm" elsif temperature > 10 puts "cool" else puts "cold" end
Ruby's keyword is elsif (no second "e"), not else if or elif. No parentheses are required around the condition, and no curly braces around the body — end closes the block. if in Ruby is an expression and returns the value of the matched branch, so you can write label = if condition then "yes" else "no" end.
unless
class Main { public static void main(String[] args) { boolean authenticated = false; if (!authenticated) { System.out.println("Please log in"); } } }
authenticated = false unless authenticated puts "Please log in" end
unless is the negated form of if — it executes its body when the condition is falsy. It is equivalent to if !condition but reads more naturally in English for negative guards. Avoid using unless with an else clause (it becomes hard to follow) and avoid unless with a complex negated condition.
case / when — no fallthrough
class Main { public static void main(String[] args) { String direction = "north"; switch (direction) { case "north" -> System.out.println("Going up"); case "south" -> System.out.println("Going down"); case "east", "west" -> System.out.println("Going sideways"); default -> System.out.println("Unknown direction"); } } }
direction = "north" case direction when "north" puts "Going up" when "south" puts "Going down" when "east", "west" puts "Going sideways" else puts "Unknown direction" end
Ruby's case/when has no fallthrough — each when is independent. Multiple values in a single when are comma-separated. when uses === (case equality) for matching, which means it works with classes (when String), ranges (when 1..10), and regular expressions (when /pattern/), not just literal values.
Postfix if / unless and ternary
class Main { public static void main(String[] args) { int count = 5; String label = count == 1 ? "item" : "items"; System.out.println(count + " " + label); boolean debug = true; if (debug) System.out.println("debug mode on"); } }
count = 5 label = count == 1 ? "item" : "items" puts "#{count} #{label}" debug = true puts "debug mode on" if debug
Ruby has the same ternary operator as Java: condition ? value_if_true : value_if_false. Postfix if and unless are idiomatic for single-line guards: expression if condition. The postfix form reads naturally — "do this if that" — and is preferred over a full if ... end block for simple guards.
while / until
class Main { public static void main(String[] args) { int counter = 0; while (counter < 5) { System.out.println(counter); counter++; } } }
counter = 0 while counter < 5 puts counter counter += 1 end
while loops run while the condition is true; until loops run while it is false (while !condition). Ruby has no do...while — use loop do ... break if condition end for a guaranteed first execution. In practice, Ruby code rarely uses while: each, times, and upto cover most iteration.
loop, break, next
class Main { public static void main(String[] args) { int position = 0; while (true) { position++; if (position % 2 == 0) continue; if (position > 10) break; System.out.println(position); } } }
position = 0 loop do position += 1 next if position.even? break if position > 10 puts position end
loop do ... end is Ruby's infinite loop — equivalent to while (true). break exits the loop; next skips to the next iteration (Java's continue). Both work inside blocks as well: next inside each skips to the next element, and break stops the entire iteration.
Methods
def and implicit return
class Main { static int square(int number) { return number * number; } static String greet(String name) { return "Hello, " + name + "!"; } public static void main(String[] args) { System.out.println(square(7)); System.out.println(greet("Alice")); } }
def square(number) number * number end def greet(name) "Hello, #{name}!" end puts square(7) puts greet("Alice")
Ruby methods return the value of their last expression — no return keyword is needed (though it can be used for early return). There is no return type declaration. Methods defined at the top level become private methods of Object. The method body is indented with two spaces; end closes it.
Default parameters
class Main { static String greet(String name, String greeting) { return greeting + ", " + name + "!"; } static String greet(String name) { return greet(name, "Hello"); } public static void main(String[] args) { System.out.println(greet("Alice")); System.out.println(greet("Bob", "Hi")); } }
def greet(name, greeting = "Hello") "#{greeting}, #{name}!" end puts greet("Alice") puts greet("Bob", "Hi")
Ruby supports default parameter values directly in the method signature — no overloading is required. Java requires separate overloaded methods to simulate defaults. Default values are evaluated at call time (not definition time), so def timestamp(value = Time.now) gives a fresh timestamp each call.
Keyword arguments
class Main { static void createUser(String name, int age, String city) { System.out.println(name + ", age " + age + ", from " + city); } public static void main(String[] args) { createUser("Alice", 30, "Paris"); // which arg is which? } }
def create_user(name:, age:, city: "Unknown") puts "#{name}, age #{age}, from #{city}" end create_user(name: "Alice", age: 30, city: "Paris") create_user(age: 25, name: "Bob") # order doesn't matter
Ruby keyword arguments are declared with a colon after the name: name:. At the call site the argument names are visible: create_user(name: "Alice", age: 30). Keyword arguments with defaults are optional; those without are required. Java has no direct equivalent — the self-documenting call site is a significant readability advantage.
Splat (*args) and double splat (**kwargs)
class Main { static void printAll(String... words) { for (String word : words) { System.out.println(word); } } public static void main(String[] args) { printAll("hello", "world", "!"); } }
def print_all(*words) words.each { |word| puts word } end def describe(**attributes) attributes.each { |key, value| puts "#{key}: #{value}" } end print_all("hello", "world", "!") describe(name: "Alice", age: 30)
*args collects positional arguments into an array — like Java's varargs. **kwargs collects keyword arguments into a hash — Java has no direct equivalent. The splat can also expand at the call site: print_all(*words_array) spreads an array into positional arguments.
One-liner method syntax (Ruby 3.0+)
class Main { static int doubleIt(int number) { return number * 2; } static String shout(String message) { return message.toUpperCase() + "!"; } public static void main(String[] args) { System.out.println(doubleIt(5)); System.out.println(shout("hello")); } }
def double_it(number) = number * 2 def shout(message) = message.upcase + "!" puts double_it(5) puts shout("hello")
Ruby 3.0 introduced endless (one-liner) method syntax: def name(params) = expression. It is equivalent to a normal method with an implicit return but reads like a mathematical definition. Use it for simple, single-expression methods where the body is obvious. For multi-line logic, use the traditional def ... end form.
Blocks & Iterators
What is a block
import java.util.List; class Main { public static void main(String[] args) { var names = List.of("Alice", "Bob", "Carol"); names.forEach(name -> System.out.println("Hello, " + name + "!")); } }
names = ["Alice", "Bob", "Carol"] names.each do |name| puts "Hello, #{name}!" end
A Ruby block is an anonymous chunk of code passed to a method. The do |params| ... end form is used for multi-line blocks; the { |params| ... } form is used for single-line blocks. Blocks are similar to Java's lambdas but are passed implicitly — they are not a parameter in the method signature, yet the method can call them with yield.
Chaining blocks
import java.util.List; class Main { public static void main(String[] args) { var numbers = List.of(1, 2, 3, 4, 5); numbers.stream() .filter(n -> n % 2 == 0) .map(n -> n * n) .forEach(System.out::println); } }
numbers = [1, 2, 3, 4, 5] numbers.select { |number| number.even? } .map { |number| number * number } .each { |number| puts number }
Each chained method returns a new array; the next block operates on that result. The convention is: use { } for one-liners that fit on a single line, and do ... end for multi-line blocks. Method chaining in Ruby requires no stream() call — arrays respond to Enumerable methods directly.
yield — accepting a block
import java.util.function.IntConsumer; class Main { static void repeat(int times, IntConsumer action) { for (int count = 0; count < times; count++) { action.accept(count); } } public static void main(String[] args) { repeat(3, count -> System.out.println("Step " + count)); } }
def repeat(times) times.times { |count| yield count } end repeat(3) { |count| puts "Step #{count}" }
yield invokes the block that was passed to the method. Any method can accept a block — no special parameter declaration is required. block_given? checks whether a block was provided. This is the mechanism behind all of Ruby's iterators: each, map, and select all call yield internally.
all?, any?, count, find
import java.util.List; class Main { public static void main(String[] args) { var numbers = List.of(2, 4, 6, 7, 8); System.out.println(numbers.stream().allMatch(n -> n % 2 == 0)); System.out.println(numbers.stream().anyMatch(n -> n > 5)); System.out.println(numbers.stream().filter(n -> n > 4).count()); System.out.println(numbers.stream().filter(n -> n > 5).findFirst().get()); } }
numbers = [2, 4, 6, 7, 8] puts numbers.all? { |number| number.even? } puts numbers.any? { |number| number > 5 } puts numbers.count { |number| number > 4 } puts numbers.find { |number| number > 5 }
Ruby's Enumerable provides all?, any?, none?, count, find, min, max, sum, and first — all operating via a block. No stream() call, no Optional wrapper, no .get(). find returns nil on an empty collection rather than throwing.
each_with_index / each_with_object
import java.util.List; class Main { public static void main(String[] args) { var fruits = List.of("apple", "banana", "cherry"); for (int index = 0; index < fruits.size(); index++) { System.out.println((index + 1) + ". " + fruits.get(index)); } } }
fruits = ["apple", "banana", "cherry"] fruits.each_with_index do |fruit, index| puts "#{index + 1}. #{fruit}" end
each_with_index yields each element and its zero-based index — replacing Java's counter-for loop. each_with_object(accumulator) yields each element and a shared accumulator, useful for building results: words.each_with_object({}) { |word, hash| hash[word] = word.length }.
Procs & Lambdas
Proc — block as an object
import java.util.function.IntUnaryOperator; class Main { public static void main(String[] args) { IntUnaryOperator doubler = n -> n * 2; System.out.println(doubler.applyAsInt(5)); System.out.println(doubler.applyAsInt(10)); } }
doubler = proc { |number| number * 2 } puts doubler.call(5) puts doubler.call(10) puts doubler.(15) # alternative call syntax
A Proc is a block turned into an object — it can be stored, passed, and called. proc { |x| ... } is shorthand for Proc.new { ... }. Procs are Ruby's equivalent of Java's functional interfaces (Function, Consumer, Supplier). Unlike lambdas, procs do not check argument count and treat return as returning from the enclosing method.
Lambda syntax
import java.util.function.UnaryOperator; import java.util.function.BiFunction; class Main { public static void main(String[] args) { UnaryOperator<String> shout = message -> message.toUpperCase() + "!"; BiFunction<Integer, Integer, Integer> add = (first, second) -> first + second; System.out.println(shout.apply("hello")); System.out.println(add.apply(3, 4)); } }
shout = ->(message) { message.upcase + "!" } add = ->(first, second) { first + second } puts shout.call("hello") puts add.call(3, 4) puts add.(3, 4) # alternative call syntax
The ->(params) { body } syntax creates a lambda. Lambdas are stricter than Procs: they enforce argument count (raising ArgumentError on mismatch) and treat return as returning from the lambda itself, not the enclosing method. Prefer lambdas when the behavior should resemble a function with a defined contract.
Method objects and &method
import java.util.List; class Main { static boolean isPalindrome(String word) { return word.equals(new StringBuilder(word).reverse().toString()); } public static void main(String[] args) { var words = List.of("level", "hello", "radar", "world", "civic"); words.stream().filter(Main::isPalindrome).forEach(System.out::println); } }
def palindrome?(word) word == word.reverse end words = ["level", "hello", "radar", "world", "civic"] puts words.select(&method(:palindrome?)).inspect words.each(&method(:puts))
method(:name) retrieves a method as an object; the & prefix converts it to a block — equivalent to Java's method reference (ClassName::methodName). Symbol-to-proc also works: words.map(&:upcase) is equivalent to words.map { |word| word.upcase }. This is the idiomatic Ruby shorthand for single-method transformations.
Proc vs lambda differences
// Java lambdas always enforce arity (the compiler catches mismatches) import java.util.function.BiFunction; class Main { public static void main(String[] args) { BiFunction<String, String, String> combine = (first, second) -> first + ", " + second; System.out.println(combine.apply("a", "b")); } }
flexible_proc = proc { |first, second| "#{first.inspect}, #{second.inspect}" } strict_lambda = lambda { |first, second| first + second } puts flexible_proc.call("a") # second is nil — no error puts strict_lambda.lambda? begin strict_lambda.call("a") # ArgumentError: wrong number of arguments rescue ArgumentError => error puts error.message end
Ruby has two closures: Proc (loose) and lambda (strict). Key differences: lambdas enforce argument count, raising ArgumentError on mismatch; Procs ignore extra arguments and fill missing ones with nil. Also, return in a Proc returns from the enclosing method; return in a lambda returns only from the lambda. lambda? distinguishes them at runtime.
Classes & OOP
Class definition and initialize
class BankAccount { private final String owner; private double balance; BankAccount(String owner, double balance) { this.owner = owner; this.balance = balance; } @Override public String toString() { return owner + ": $" + balance; } } class Main { public static void main(String[] args) { var account = new BankAccount("Alice", 1000.0); System.out.println(account); } }
class BankAccount def initialize(owner, balance) @owner = owner @balance = balance end def to_s "#{@owner}: $#{@balance}" end end account = BankAccount.new("Alice", 1000.0) puts account
Ruby classes use initialize instead of a constructor named after the class. Instance variables start with @. BankAccount.new allocates the object and calls initialize. to_s is Ruby's toString(); defining it makes puts account print nicely. There is no new keyword as a language construct — new is just a class method.
attr_accessor / attr_reader / attr_writer
class Person { private String name; private final int age; Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } // read-only — no setter } class Main { public static void main(String[] args) { var person = new Person("Alice", 30); person.setName("Alicia"); System.out.println(person.getName() + ", " + person.getAge()); } }
class Person attr_accessor :name # generates getter + setter attr_reader :age # generates getter only def initialize(name, age) @name = name @age = age end end person = Person.new("Alice", 30) person.name = "Alicia" puts "#{person.name}, #{person.age}"
attr_accessor generates both a getter and a setter for an instance variable. attr_reader generates only a getter (read-only); attr_writer only a setter. This replaces Java's boilerplate getter/setter methods. Setters use the name= naming convention — person.name = "Alicia" calls the name= method.
Class methods and class variables
class Counter { private static int count = 0; private final int id; Counter() { this.id = ++count; } static int getCount() { return count; } int getId() { return id; } } class Main { public static void main(String[] args) { var first = new Counter(); var second = new Counter(); System.out.println(Counter.getCount()); System.out.println(first.getId()); } }
class Counter @@count = 0 def initialize @@count += 1 @id = @@count end def self.count @@count end def id @id end end first = Counter.new second = Counter.new puts Counter.count puts first.id
Class methods are defined with self.method_name — the equivalent of Java's static methods. Class variables start with @@ and are shared across all instances (like Java's static fields). In practice, Ruby often uses class-level instance variables (@count on the class object itself) instead of @@ to avoid inheritance edge cases.
Inheritance and super
class Animal { protected 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 + " barks"; } } class Main { public static void main(String[] args) { Animal animal = new Dog("Rex"); System.out.println(animal.speak()); System.out.println(animal instanceof Animal); } }
class Animal def initialize(name) @name = name end def speak "#{@name} makes a sound" end end class Dog < Animal def speak "#{@name} barks" end end animal = Dog.new("Rex") puts animal.speak puts animal.is_a?(Animal)
Ruby uses < for inheritance: class Dog < Animal. super calls the parent's method of the same name, passing through all arguments by default. Ruby supports only single inheritance (no multiple base classes), but modules fill the role of Java's interfaces. There is no @Override annotation — overriding methods are simply redefined.
Open classes — adding methods to existing classes
// Java: cannot add methods to existing classes // Closest: static utility methods or Kotlin extension functions class Main { static boolean isPalindrome(String text) { return text.equals(new StringBuilder(text).reverse().toString()); } public static void main(String[] args) { System.out.println(isPalindrome("racecar")); System.out.println(isPalindrome("hello")); } }
class String def palindrome? self == self.reverse end end puts "racecar".palindrome? puts "hello".palindrome? puts 42.class
Ruby classes are always "open" — you can add methods to any class at any time, including built-in classes like String, Integer, and Array. This is called monkey patching and enables natural extension of the standard library. Java's closed class model prevents this. Used with care, open classes make code read naturally; used carelessly, they can introduce surprising behavior.
Method visibility
class Person { private final String name; private final int age; Person(String name, int age) { this.name = name; this.age = age; } public String introduce() { return "Hi, I'm " + name + " and I'm " + formatAge() + "."; } private String formatAge() { return age + " years old"; } } class Main { public static void main(String[] args) { var person = new Person("Alice", 30); System.out.println(person.introduce()); } }
class Person def initialize(name, age) @name = name @age = age end def introduce "Hi, I'm #{@name} and I'm #{format_age}." end private def format_age "#{@age} years old" end end person = Person.new("Alice", 30) puts person.introduce # person.format_age # → NoMethodError: private method
In Ruby, public, protected, and private are keywords that act as mode switches — all method definitions that follow become that visibility. Unlike Java's per-method modifiers, they set a section for the rest of the class body. private methods cannot be called with an explicit receiver — even self.format_age inside the class raises NoMethodError.
Modules & Mixins
Module as namespace
class Main { // Java uses packages; nested static classes simulate namespacing static class Geometry { static class Circle { static double area(double radius) { return Math.PI * radius * radius; } } } public static void main(String[] args) { System.out.printf("%.4f%n", Geometry.Circle.area(5.0)); } }
module Geometry class Circle def self.area(radius) Math::PI * radius ** 2 end end end puts Geometry::Circle.area(5.0).round(4)
Ruby modules serve as namespaces — grouping related classes and constants under a name, equivalent to Java's packages. The :: operator accesses constants and classes inside a module. Unlike Java's package system, Ruby modules can be reopened and extended at any time. Modules also serve a second role as mixins (see next example).
include — module as mixin
interface Greetable { String getName(); default String greet() { return "Hello, I am " + getName(); } } class Person implements Greetable { private final String name; Person(String name) { this.name = name; } @Override public String getName() { return name; } } class Main { public static void main(String[] args) { System.out.println(new Person("Alice").greet()); } }
module Greetable def greet "Hello, I am #{name}" end end class Person include Greetable attr_reader :name def initialize(name) @name = name end end puts Person.new("Alice").greet
include ModuleName mixes in all the module's methods as instance methods — equivalent to Java's interface with default methods, but more powerful because the module can access the including class's state via the class's own methods. Ruby supports including multiple modules (no limit), while Java classes can extend only one class. extend mixes module methods in as class methods.
Comparable mixin
import java.util.List; class Temperature implements Comparable<Temperature> { private final double degrees; Temperature(double degrees) { this.degrees = degrees; } double getDegrees() { return degrees; } @Override public int compareTo(Temperature other) { return Double.compare(this.degrees, other.degrees); } } class Main { public static void main(String[] args) { var temps = List.of( new Temperature(30.0), new Temperature(20.0), new Temperature(25.0)); System.out.println(temps.stream().min(Temperature::compareTo).get().getDegrees()); } }
class Temperature include Comparable attr_reader :degrees def initialize(degrees) @degrees = degrees end def <=>(other) degrees <=> other.degrees end end temperatures = [Temperature.new(30.0), Temperature.new(20.0), Temperature.new(25.0)] puts temperatures.min.degrees puts temperatures.sort.map(&:degrees).inspect puts Temperature.new(25.0).between?(Temperature.new(20.0), Temperature.new(30.0))
Including Comparable and defining <=> (the spaceship operator) grants <, <=, >, >=, between?, and clamp for free — equivalent to implementing Comparable in Java. The <=> operator returns -1, 0, or 1, matching Java's compareTo contract.
Enumerable mixin in a custom class
import java.util.Arrays; import java.util.Iterator; import java.util.List; class NumberSet implements Iterable<Integer> { private final List<Integer> items; NumberSet(Integer... numbers) { items = Arrays.asList(numbers); } @Override public Iterator<Integer> iterator() { return items.iterator(); } } class Main { public static void main(String[] args) { var set = new NumberSet(3, 1, 4, 1, 5); for (int number : set) System.out.println(number * 2); } }
class NumberSet include Enumerable def initialize(*numbers) @numbers = numbers end def each(&block) @numbers.each(&block) end end set = NumberSet.new(3, 1, 4, 1, 5) puts set.map { |number| number * 2 }.inspect puts set.select { |number| number > 2 }.inspect puts set.min puts set.sort.inspect
Including Enumerable and defining each grants the full suite: map, select, reject, reduce, min, max, sort, find, group_by, count, first, to_a, and more — over 50 methods for free. Java's Iterable gives only the raw iterator; Streams must be called explicitly for higher-order methods.
Error Handling
begin / rescue / ensure
class Main { public static void main(String[] args) { try { int result = Integer.parseInt("not a number"); System.out.println(result); } catch (NumberFormatException error) { System.out.println("Caught: " + error.getMessage()); } finally { System.out.println("Always runs"); } } }
begin result = Integer("not a number") puts result rescue ArgumentError => error puts "Caught: #{error.message}" ensure puts "Always runs" end
begin/rescue/ensure map directly to Java's try/catch/finally. rescue catches the exception and binds it with => error. ensure always runs, even if no exception is raised. Unlike Java, all Ruby exceptions are unchecked — you never declare which exceptions a method raises.
raise
class Main { static void validateAge(int age) { if (age < 0) throw new IllegalArgumentException("Age cannot be negative: " + age); if (age > 150) throw new IllegalArgumentException("Age too large: " + age); } public static void main(String[] args) { try { validateAge(-5); } catch (IllegalArgumentException error) { System.out.println(error.getMessage()); } } }
def validate_age(age) raise ArgumentError, "Age cannot be negative: #{age}" if age < 0 raise ArgumentError, "Age too large: #{age}" if age > 150 end begin validate_age(-5) rescue ArgumentError => error puts error.message end
raise is Ruby's equivalent of throw. The most common form is raise ExceptionClass, "message". Bare raise inside a rescue re-raises the current exception. Ruby's ArgumentError is the closest equivalent to Java's IllegalArgumentException. There are no checked exceptions — no method signature declarations for raised exceptions.
Custom exceptions
class InsufficientFundsException extends RuntimeException { private final double shortfall; InsufficientFundsException(double shortfall) { super("Need $" + shortfall + " more"); this.shortfall = shortfall; } double getShortfall() { return shortfall; } } class Main { public static void main(String[] args) { try { throw new InsufficientFundsException(25.50); } catch (InsufficientFundsException error) { System.out.println(error.getMessage() + ", shortfall: " + error.getShortfall()); } } }
class InsufficientFundsError < StandardError attr_reader :shortfall def initialize(shortfall) super("Need $#{shortfall} more") @shortfall = shortfall end end begin raise InsufficientFundsError.new(25.50) rescue InsufficientFundsError => error puts "#{error.message}, shortfall: #{error.shortfall}" end
Custom Ruby exceptions inherit from StandardError (not Exception). Inheriting from StandardError is the convention — bare rescue catches StandardError and its descendants. Custom fields are added as instance variables with attr_reader, replacing Java's verbose field + getter pattern. There is no RuntimeException distinction — all Ruby exceptions are unchecked.
Multiple rescue clauses
class Main { public static void main(String[] args) { try { int result = 10 / Integer.parseInt("0"); System.out.println(result); } catch (NumberFormatException error) { System.out.println("Not a number: " + error.getMessage()); } catch (ArithmeticException error) { System.out.println("Math error: " + error.getMessage()); } catch (Exception error) { System.out.println("Other error: " + error.getMessage()); } } }
begin result = 10 / Integer("0") puts result rescue ArgumentError => error puts "Not a number: #{error.message}" rescue ZeroDivisionError => error puts "Math error: #{error.message}" rescue => error puts "Other error: #{error.message}" end
Multiple rescue clauses catch different exception types, matched in order from top to bottom. Bare rescue => error (with no class specified) catches StandardError and all its descendants — the catch-all. A single rescue can handle multiple types: rescue ArgumentError, TypeError => error.
retry
class Main { static int unstable(int[] attempt) { attempt[0]++; if (attempt[0] < 3) throw new RuntimeException("Temporary failure"); return 42; } public static void main(String[] args) { int[] attempt = {0}; int maxAttempts = 3; while (true) { try { System.out.println("Success on attempt " + attempt[0]); System.out.println(unstable(attempt)); break; } catch (RuntimeException error) { System.out.println("Attempt " + attempt[0] + " failed, retrying..."); if (attempt[0] >= maxAttempts) throw error; } } } }
attempts = 0 begin attempts += 1 raise RuntimeError, "Temporary failure" if attempts < 3 puts "Success on attempt #{attempts}" rescue RuntimeError => error puts "Attempt #{attempts} failed, retrying..." retry if attempts < 3 raise end
retry inside a rescue clause re-runs the entire begin block from the top. This is the idiomatic pattern for transient-failure retries (network calls, rate limits, database timeouts). Java requires a manual loop with a counter; Ruby's retry is declarative and cleaner. Always include a retry limit to prevent infinite loops — bare raise re-raises the original exception after the limit.
Pattern Matching
Array pattern matching
class Main { public static void main(String[] args) { // Java 21+ pattern matching in switch Object value = 42; String description = switch (value) { case Integer number when number > 100 -> "large number: " + number; case Integer number -> "number: " + number; case String text -> "text: " + text; default -> "other"; }; System.out.println(description); } }
point = [1, 2] case point in [Integer => x, Integer => y] puts "Point at #{x}, #{y}" end response = [200, "OK"] case response in [200, body] puts "Success: #{body}" in [404, _] puts "Not found" in [status, message] puts "Error #{status}: #{message}" end
Ruby's case/in (pattern matching, stable since Ruby 3.0) matches arrays by structure. [Integer => x, Integer => y] checks that both elements are integers and binds them to x and y. _ is a wildcard that matches any value without binding. This is more expressive than Java's switch patterns, which match on types but not on structural position.
Hash pattern matching
import java.util.Map; class Main { // Java has no map/hash pattern matching — extract fields, then switch public static void main(String[] args) { var user = Map.of("name", "Alice", "role", "admin", "active", true); String role = (String) user.get("role"); boolean active = (Boolean) user.get("active"); String name = (String) user.get("name"); String message = switch (role) { case "admin" -> active ? name + " is an active admin" : name + " is inactive"; default -> active ? "regular user" : name + " is inactive"; }; System.out.println(message); } }
user = { name: "Alice", role: "admin", active: true } case user in { role: "admin", name: String => name, active: true } puts "#{name} is an active admin" in { active: false, name: String => name } puts "#{name} is inactive" else puts "regular user" end
Hash patterns match by key presence and value: { role: "admin" } succeeds if the hash has a :role key with value "admin". Extra keys are ignored. String => name checks the type and binds the value. Hash patterns are ideal for processing API responses and configuration objects.
Pattern guards
class Main { public static void main(String[] args) { Object value = 42; String description = switch (value) { case Integer number when number > 0 -> "positive: " + number; case Integer number when number < 0 -> "negative: " + number; case Integer number -> "zero"; case String text when text.isEmpty() -> "empty string"; default -> "other"; }; System.out.println(description); } }
value = 42 case value in Integer => number if number > 0 puts "positive: #{number}" in Integer => number if number < 0 puts "negative: #{number}" in Integer puts "zero" in String => text if text.empty? puts "empty string" end
Guards are if conditions appended to a pattern: in Integer => number if number > 0. The pattern must match AND the guard must be true for the branch to execute. This is equivalent to Java 21's guarded patterns (case Integer number when number > 0). Guards can use any Ruby expression, including method calls on the bound variables.
Deconstruct — custom class pattern matching
// Java: records deconstruct automatically record Point(int x, int y) {} class Main { public static void main(String[] args) { Object obj = new Point(3, 4); if (obj instanceof Point(int px, int py)) { System.out.println("x=" + px + ", y=" + py); } } }
class Point attr_reader :x, :y def initialize(x, y) @x = x @y = y end def deconstruct [x, y] end def deconstruct_keys(keys) { x: x, y: y } end end point = Point.new(3, 4) case point in [x, y] puts "Array pattern: #{x}, #{y}" end case point in { x: Integer => px, y: Integer => py } puts "Hash pattern: x=#{px}, y=#{py}" end
Define deconstruct (for array patterns) and deconstruct_keys (for hash patterns) to make custom classes work with case/in. This is similar to Java's automatic record deconstruction, but extensible to any class. Java records generate deconstruction for free; Ruby requires explicit methods, but the approach works on any class including existing library classes.