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 / 3 → 3, 10.0 / 3 → 3.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.