PONYλM2Modula-2

Visual Basic.CodeCompared.To/Java

An interactive executable cheatsheet comparing Visual Basic and Java

Visual Basic (.NET 10) Java 25
Output & Running
Hello, World
Two platforms with the same shape of ceremony, arranged slightly differently.
Option Strict On Imports System Module HelloWorld Sub Main() Console.WriteLine("Hello, World!") End Sub End Module
class Main { public static void main(String[] args) { System.out.println("Hello, World!"); } }
Java keeps the wrapper Visual Basic keeps: a type containing an entry point. Module becomes class, Sub Main() becomes public static void main(String[] args) — that exact signature, always. Console.WriteLine becomes System.out.println. Blocks close with a brace rather than End, and every statement ends with a semicolon. Of every target on this anchor, Java asks for the most boilerplate around the smallest program.
Formatted output
There is no interpolated string here — this is the feature a Visual Basic programmer misses most.
Option Strict On Imports System Module FormatDemo Sub Main() Dim name As String = "Ada" Dim score As Integer = 42 Dim ratio As Double = 0.8756 Console.WriteLine($"Hello, {name}! Score: {score}") Console.WriteLine($"Padded: {score:D5}, rounded: {ratio:F2}") End Sub End Module
class Main { public static void main(String[] args) { String name = "Ada"; int score = 42; double ratio = 0.8756; System.out.println("Hello, " + name + "! Score: " + score); System.out.printf("Padded: %05d, rounded: %.2f%n", score, ratio); System.out.println(String.format("%s scored %d", name, score)); } }
Java has no $"..." equivalent. The three options are concatenation with +, String.format, and printf, all using C-style placeholders: %s, %d, %.2f, %05d. %n is the newline, which is why printf needs one and println does not. Java 21's string templates were withdrawn before standardisation, so this is the state of things and is unlikely to change soon.
Syntax Fundamentals
Blocks: braces instead of End
The C-family shape: parentheses around the condition, braces around the block, semicolons on statements.
Option Strict On Imports System Module BlockDemo Sub Main() Dim temperature As Integer = 30 If temperature > 25 Then Console.WriteLine("Warm") ElseIf temperature > 10 Then Console.WriteLine("Mild") Else Console.WriteLine("Cold") End If End Sub End Module
class Main { public static void main(String[] args) { int temperature = 30; if (temperature > 25) { System.out.println("Warm"); } else if (temperature > 10) { System.out.println("Mild"); } else { System.out.println("Cold"); } } }
Every End becomes a closing brace, Then disappears, and ElseIf becomes two words. A single-statement block may drop its braces, and the convention in most Java codebases is to keep them anyway. Note that && and || replace AndAlso and OrElse, and a condition must be a boolean — there is no truthiness, so if (count) does not compile.
Case sensitivity and naming
The usual adjustment, with a naming convention that differs from .NET in one visible way.
Option Strict On Imports System Module CaseDemo Sub Main() Dim customerName As String = "Grace" Console.WriteLine(customerName) Console.WriteLine(CustomerName) End Sub End Module
class Main { public static void main(String[] args) { String customerName = "Grace"; String CustomerName = "Hopper"; System.out.println(customerName); System.out.println(CustomerName); } }
Identifiers are case-sensitive. The convention is camelCase for variables and methods — not PascalCase — with PascalCase reserved for classes and interfaces and UPPER_SNAKE_CASE for constants. So person.Describe() becomes person.describe(), and this is the fastest way to spot code written by someone arriving from .NET. Interfaces take no I prefix either.
Comments
Three forms, and the documentation one is the direct counterpart of the XML comment.
Option Strict On Imports System Module CommentDemo ''' <summary>Doubles a number.</summary> Function Twice(value As Integer) As Integer ' A line comment Return value * 2 End Function Sub Main() Console.WriteLine(Twice(21)) End Sub End Module
class Main { /** * Doubles a number. * @param value the number to double */ static int twice(int value) { // A line comment /* and a block comment */ return value * 2; } public static void main(String[] args) { System.out.println(twice(21)); } }
' becomes //, plus a block form /* ... */ Visual Basic never had. Javadoc, /** ... */, is what '''<summary> becomes: the same idea with @param, @return and @throws tags instead of XML elements, read by the IDE and by the javadoc tool.
Variables & Types
Declaring a variable
The declaration turns inside out, and one of these types has no Java equivalent as a primitive.
Option Strict On Option Infer On Imports System Module DeclarationDemo Sub Main() Dim count As Integer = 10 Dim price As Decimal = 19.99D Dim label As String = "widget" Dim inferred = 42 Const MaximumRetries As Integer = 3 Console.WriteLine($"{count} {price} {label} {inferred} {MaximumRetries}") End Sub End Module
import java.math.BigDecimal; class Main { static final int MAXIMUM_RETRIES = 3; public static void main(String[] args) { int count = 10; BigDecimal price = new BigDecimal("19.99"); String label = "widget"; var inferred = 42; System.out.println(count + " " + price + " " + label + " " + inferred + " " + MAXIMUM_RETRIES); } }
Read Dim count As Integer right to left and you have int count. Option Infer becomes var, available for locals only. Const becomes static final, and a local constant is just final. The one that needs care: Java has no Decimal. BigDecimal is the money type, it is a class rather than a primitive, it has no literal syntax, and it must be constructed from a stringnew BigDecimal(19.99) has already lost the precision you wanted.
Primitives and their boxes
Java draws a line .NET does not: a primitive is not an object, and generics only accept objects.
Option Strict On Imports System Imports System.Collections.Generic Module TypeDemo Sub Main() Dim whole As Integer = 2 Dim big As Long = 3L Dim rough As Single = 4.5F Dim precise As Double = 6.7 Dim letter As Char = "A"c Dim flag As Boolean = True ' A List of Integer works directly Dim numbers As New List(Of Integer) From {whole} Console.WriteLine($"{whole} {big} {rough} {precise} {letter} {flag} {numbers.Count}") End Sub End Module
import java.util.List; class Main { public static void main(String[] args) { int whole = 2; long big = 3L; float rough = 4.5f; double precise = 6.7; char letter = 'A'; boolean flag = true; // A generic collection needs the BOXED type List<Integer> numbers = List.of(whole); System.out.println(whole + " " + big + " " + rough + " " + precise + " " + letter + " " + flag + " " + numbers.size()); } }
Integerint, Longlong, Singlefloat, Doubledouble, Booleanboolean, Charchar with single quotes. In .NET an Integer is an object when it needs to be. In Java the primitive int and the class Integer are different types, and List<int> does not compile — it must be List<Integer>. The conversion happens automatically (autoboxing), which is convenient until it is not; the gotchas section covers the case where it bites.
Nothing becomes null — and Optional
Both languages have a null; Java adds a wrapper that makes absence visible in the signature.
Option Strict On Imports System Module NullDemo Function FindName(id As Integer) As String If id = 1 Then Return "Ada" Return Nothing End Function Sub Main() Dim found As String = FindName(1) If found IsNot Nothing Then Console.WriteLine(found) Dim missing As String = FindName(2) Console.WriteLine(If(missing, "(not found)")) Dim absentNumber As Integer? = Nothing Console.WriteLine(absentNumber.GetValueOrDefault(-1)) End Sub End Module
import java.util.Optional; class Main { static Optional<String> findName(int id) { return id == 1 ? Optional.of("Ada") : Optional.empty(); } public static void main(String[] args) { findName(1).ifPresent(System.out::println); System.out.println(findName(2).orElse("(not found)")); Integer absentNumber = null; System.out.println(absentNumber == null ? -1 : absentNumber); } }
Nothing becomes null, with two differences. A Java primitive cannot be null — int x = null; does not compile — so nullability means using the boxed type, which is Java's version of Integer?. And modern Java prefers Optional<T> as a return type: the caller can see absence is possible and orElse supplies the fallback that the two-argument If() supplies. Optional is for return values, not fields or parameters — that is the community convention.
Converting between types
The conversion functions have counterparts, and the TryParse pattern does not.
Option Strict On Imports System Module ConversionDemo Sub Main() Dim text As String = "123" Dim parsed As Integer = CInt(text) Dim asText As String = CStr(parsed * 2) Dim widened As Double = parsed Dim value As Integer If Integer.TryParse("12x", value) Then Console.WriteLine(value) Else Console.WriteLine("not a number") End If Console.WriteLine($"{parsed} {asText} {widened}") End Sub End Module
class Main { public static void main(String[] args) { String text = "123"; int parsed = Integer.parseInt(text); String asText = String.valueOf(parsed * 2); double widened = parsed; try { System.out.println(Integer.parseInt("12x")); } catch (NumberFormatException error) { System.out.println("not a number"); } System.out.println(parsed + " " + asText + " " + widened); } }
CIntInteger.parseInt, CDblDouble.parseDouble, CStrString.valueOf. Widening is implicit as it is in Visual Basic; narrowing needs a cast, (int) someDouble, which is DirectCast. There is no TryParse — Java has no output parameters — so a conversion that might fail is wrapped in try/catch (NumberFormatException). That is the idiomatic answer, not a workaround.
Operators
== compares references for objects
The most consequential single-character difference on the page, and it fails silently.
Option Strict On Imports System Module EqualityDemo Sub Main() Dim left As String = "hello" Dim right As New String("hello".ToCharArray()) ' = on strings compares VALUE Console.WriteLine(left = right) Console.WriteLine(left Is right) Console.WriteLine(1 = 1) End Sub End Module
class Main { public static void main(String[] args) { String left = "hello"; String right = new String("hello".toCharArray()); // == on objects compares REFERENCES System.out.println(left == right); System.out.println(left.equals(right)); System.out.println(1 == 1); } }
For primitives, == compares values as you expect. For objects — including String== compares references, so two strings with identical contents can be unequal. Visual Basic's = on strings compares value, so a mechanically translated comparison changes meaning without any warning. Always use .equals(), and Objects.equals(a, b) when either side may be null. Java's == is Visual Basic's Is; Visual Basic's = is Java's .equals.
Arithmetic and integer division
The same trap C# sets, for the same reason.
Option Strict On Imports System Module ArithmeticDemo Sub Main() Dim quotient As Integer = 17 \ 5 Dim exact As Double = 17 / 5 Dim remainder As Integer = 17 Mod 5 Dim squared As Double = 7 ^ 2 Console.WriteLine($"{quotient} {exact} {remainder} {squared}") End Sub End Module
class Main { public static void main(String[] args) { int quotient = 17 / 5; double exact = 17 / 5.0; int remainder = 17 % 5; double squared = Math.pow(7, 2); System.out.println(quotient + " " + exact + " " + remainder + " " + squared); } }
There is no \: 17 / 5 between two ints truncates to 3, so a Visual Basic / copied straight across silently starts truncating. Make an operand a double to get the fractional answer. Mod becomes %, taking the sign of the left operand as Visual Basic does. There is no exponent operator: ^ is bitwise exclusive-or, so 7 ^ 2 quietly evaluates to 5 — use Math.pow.
If() becomes ?: — and there is no ??
The three-argument form translates directly; the two-argument one has no operator.
Option Strict On Imports System Module ConditionalDemo Sub Main() Dim score As Integer = 72 Dim grade As String = If(score >= 60, "pass", "fail") Dim supplied As String = Nothing Dim label As String = If(supplied, "(unnamed)") Console.WriteLine($"{grade} / {label}") End Sub End Module
import java.util.Objects; class Main { public static void main(String[] args) { int score = 72; String grade = score >= 60 ? "pass" : "fail"; String supplied = null; String label = Objects.requireNonNullElse(supplied, "(unnamed)"); String alsoLabel = supplied != null ? supplied : "(unnamed)"; System.out.println(grade + " / " + label + " / " + alsoLabel); } }
If(condition, a, b) becomes condition ? a : b. If(value, fallback) has no operator equivalent — Java has no ?? and no ?.. The nearest things are Objects.requireNonNullElse, an explicit ternary, or Optional.ofNullable(x).orElse(y). This is why null-heavy Java code is noticeably wordier than the equivalent C#, and why Optional is preferred at API boundaries.
Strings
Common string operations
Nearly a rename exercise, with two details worth pausing on.
Option Strict On Imports System Module StringDemo Sub Main() Dim text As String = " Visual Basic " Console.WriteLine($"[{text.Trim()}]") Console.WriteLine(text.Trim().ToUpper()) Console.WriteLine(text.Contains("Basic")) Console.WriteLine(text.Trim().Replace(" ", "-")) Console.WriteLine(text.Trim().Substring(0, 6)) Console.WriteLine(text.Trim().Length) End Sub End Module
class Main { public static void main(String[] args) { String text = " Visual Basic "; System.out.println("[" + text.strip() + "]"); System.out.println(text.trim().toUpperCase()); System.out.println(text.contains("Basic")); System.out.println(text.trim().replace(" ", "-")); System.out.println(text.trim().substring(0, 6)); System.out.println(text.trim().length()); } }
Trimtrim, ToUppertoUpperCase, Containscontains, Replacereplace — all camelCase. Two details: substring(start, end) takes an end index where .NET's Substring(start, length) takes a length, so any call with a non-zero start needs recomputing. And strip() is the Unicode-aware trim added in Java 11; trim() only removes characters below U+0020 and is kept for compatibility.
Quoted and multi-line strings
The backslash escapes here, and there is no verbatim prefix — only a text block.
Option Strict On Imports System Module QuotingDemo Sub Main() Dim quoted As String = "She said ""hello""." Dim path As String = "C:\reports\summary.txt" Dim block As String = "line one" & Environment.NewLine & "line two" Console.WriteLine(quoted) Console.WriteLine(path) Console.WriteLine(block) End Sub End Module
class Main { public static void main(String[] args) { String quoted = "She said \"hello\"."; String path = "C:\\reports\\summary.txt"; String block = """ line one line two"""; System.out.println(quoted); System.out.println(path); System.out.println(block); } }
Java treats \ as an escape, so a doubled quote becomes \" and a Windows path needs every backslash doubled. There is no @"...". What Java does have is the text block, """, which spans lines and strips the common indentation relative to the closing delimiter — the same idea as C#'s raw string literal, and the right home for JSON or SQL. Text blocks still process escapes.
Building a string
The same class with the same purpose, spelled in camelCase.
Option Strict On Imports System Imports System.Text Module BuilderDemo Sub Main() Dim builder As New StringBuilder() For number As Integer = 1 To 5 builder.Append(number) If number < 5 Then builder.Append(", ") Next Console.WriteLine(builder.ToString()) Console.WriteLine(String.Join(" | ", New String() {"a", "b", "c"})) End Sub End Module
class Main { public static void main(String[] args) { StringBuilder builder = new StringBuilder(); for (int number = 1; number <= 5; number++) { builder.append(number); if (number < 5) builder.append(", "); } System.out.println(builder.toString()); System.out.println(String.join(" | ", "a", "b", "c")); } }
Both languages have immutable strings, so both have a builder. StringBuilder keeps its name; Append becomes append. String.Join(separator, items) becomes String.join(separator, items) with the arguments in the same order, unlike Python and Ruby where the separator does the joining. Note that new is required to construct anything — Java has no As New shorthand.
Collections
Lists
The same growable list, with an interface-and-implementation split .NET does not insist on.
Option Strict On Imports System Imports System.Collections.Generic Module ListDemo Sub Main() Dim fruits As New List(Of String) From {"apple", "banana"} fruits.Add("cherry") fruits.Insert(0, "apricot") fruits.Remove("banana") Console.WriteLine(fruits.Count) Console.WriteLine(fruits(0)) Console.WriteLine(String.Join(", ", fruits)) End Sub End Module
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<String> fruits = new ArrayList<>(List.of("apple", "banana")); fruits.add("cherry"); fruits.add(0, "apricot"); fruits.remove("banana"); System.out.println(fruits.size()); System.out.println(fruits.get(0)); System.out.println(String.join(", ", fruits)); } }
List(Of String) becomes List<String> — angle brackets rather than (Of ...). The convention is to declare the interface and construct the implementation: List<String> x = new ArrayList<>(), where the empty <> infers the type argument. Countsize(), fruits(0)fruits.get(0) — there is no indexer syntax for anything but arrays. Careful with remove: on a List<Integer>, remove(2) removes the item at index 2, while remove(Integer.valueOf(2)) removes the value 2.
Dictionaries become maps
The same structure with different method names — and a choice of implementation that decides the iteration order.
Option Strict On Imports System Imports System.Collections.Generic Module MapDemo Sub Main() Dim ages As New Dictionary(Of String, Integer) From { {"Ada", 36}, {"Grace", 45} } ages("Alan") = 41 For Each entry As KeyValuePair(Of String, Integer) In ages Console.WriteLine($"{entry.Key} is {entry.Value}") Next Console.WriteLine(ages.ContainsKey("Ada")) Dim found As Integer ages.TryGetValue("Nobody", found) Console.WriteLine(found) End Sub End Module
import java.util.LinkedHashMap; import java.util.Map; class Main { public static void main(String[] args) { Map<String, Integer> ages = new LinkedHashMap<>(); ages.put("Ada", 36); ages.put("Grace", 45); ages.put("Alan", 41); for (Map.Entry<String, Integer> entry : ages.entrySet()) { System.out.println(entry.getKey() + " is " + entry.getValue()); } System.out.println(ages.containsKey("Ada")); System.out.println(ages.getOrDefault("Nobody", 0)); ages.merge("Ada", 1, Integer::sum); System.out.println(ages.get("Ada")); } }
Dictionary(Of K, V) becomes Map<K, V>. ages("Alan") = 41 becomes put, reading becomes get, ContainsKey becomes containsKey, and TryGetValue becomes getOrDefault — no output parameter. The implementation matters: HashMap has no defined iteration order, while LinkedHashMap preserves insertion order and TreeMap sorts by key. A .NET Dictionary happens to preserve insertion order in practice, so code that relied on that needs LinkedHashMap here.
Arrays
The brackets change, and so does what the number inside them means.
Option Strict On Imports System Module ArrayDemo Sub Main() Dim names() As String = {"Ada", "Grace", "Alan"} ' The number is the UPPER BOUND Dim scores(4) As Integer scores(0) = 10 Console.WriteLine(names.Length) Console.WriteLine(scores.Length) Console.WriteLine(names(1)) End Sub End Module
import java.util.Arrays; class Main { public static void main(String[] args) { String[] names = { "Ada", "Grace", "Alan" }; // The number is the LENGTH int[] scores = new int[5]; scores[0] = 10; System.out.println(names.length); System.out.println(scores.length); System.out.println(names[1]); System.out.println(Arrays.toString(names)); } }
Dim scores(4) declares indices 0 through 4 — five slots, because the number is the upper bound — while new int[5] names the count. They happen to agree here; they will not when the number is not one less. UBound becomes length - 1, and note length is a field on an array but a method on a String. Printing an array gives you a useless identity hash, so Arrays.toString is what you actually want.
Control Flow
Select Case becomes switch
Modern Java has a switch that is an expression, and it removes the fall-through hazard entirely.
Option Strict On Imports System Module SelectDemo Function Describe(code As Integer) As String Select Case code Case 1 Return "one" Case 2, 3 Return "two or three" Case Else Return "something else" End Select End Function Sub Main() Console.WriteLine(Describe(1)) Console.WriteLine(Describe(3)) Console.WriteLine(Describe(9)) End Sub End Module
class Main { static String describe(int code) { return switch (code) { case 1 -> "one"; case 2, 3 -> "two or three"; default -> "something else"; }; } public static void main(String[] args) { System.out.println(describe(1)); System.out.println(describe(3)); System.out.println(describe(9)); } }
The arrow formcase 1 -> "one"; — has no fall-through and needs no break, so it behaves exactly like Select Case. Several values share an arm with a comma, matching Case 2, 3. Used as an expression it must produce a value on every path, and the compiler checks that. The older colon form still exists and still falls through silently; there is no reason to write it in new code.
Pattern matching
The test-then-cast pair collapses into one, and a guard can be attached.
Option Strict On Imports System Module PatternDemo Function Describe(value As Object) As String If TypeOf value Is Integer Then Dim number = CInt(value) Return If(number < 0, "negative", "number " & number.ToString()) End If If TypeOf value Is String Then Return "text of " & CStr(value).Length.ToString() End If Return "unknown" End Function Sub Main() Console.WriteLine(Describe(-4)) Console.WriteLine(Describe(7)) Console.WriteLine(Describe("hello")) End Sub End Module
class Main { static String describe(Object value) { return switch (value) { case Integer number when number < 0 -> "negative"; case Integer number -> "number " + number; case String text -> "text of " + text.length(); default -> "unknown"; }; } public static void main(String[] args) { System.out.println(describe(-4)); System.out.println(describe(7)); System.out.println(describe("hello")); } }
TypeOf value Is Integer becomes value instanceof Integer number, which tests and binds in one step — and inside a switch it becomes a type pattern, case Integer number ->. when adds a guard, spelled the same way C# spells it. Record patterns go further and destructure fields directly. All of this arrived between Java 16 and 21 and is what makes modern Java read very differently from the Java of ten years ago.
Loops
For ... Next
A declarative range becomes three explicit clauses.
Option Strict On Imports System Module ForDemo Sub Main() For index As Integer = 1 To 5 Console.Write(index & " ") Next Console.WriteLine() For countdown As Integer = 10 To 0 Step -2 Console.Write(countdown & " ") Next Console.WriteLine() End Sub End Module
class Main { public static void main(String[] args) { for (int index = 1; index <= 5; index++) { System.out.print(index + " "); } System.out.println(); for (int countdown = 10; countdown >= 0; countdown -= 2) { System.out.print(countdown + " "); } System.out.println(); } }
The clauses are "initializer; condition; step". You state the ending condition, so the inclusive To 5 becomes <= 5 — the classic off-by-one, and worth checking every time. Step -2 becomes the step clause. The loop variable is scoped to the loop, as index As Integer is. Console.Write becomes System.out.print.
For Each
The enhanced for loop, and the reminder that Java has no built-in way to get the index alongside the value.
Option Strict On Imports System Imports System.Collections.Generic Module ForEachDemo Sub Main() Dim words As New List(Of String) From {"alpha", "beta", "gamma"} For Each word As String In words Console.WriteLine(word.ToUpper()) Next For index As Integer = 0 To words.Count - 1 Console.WriteLine($"{index}: {words(index)}") Next End Sub End Module
import java.util.List; class Main { public static void main(String[] args) { List<String> words = List.of("alpha", "beta", "gamma"); for (String word : words) { System.out.println(word.toUpperCase()); } for (int index = 0; index < words.size(); index++) { System.out.println(index + ": " + words.get(index)); } } }
For Each word As String In words becomes for (String word : words) — the colon is read as "in". It works on anything implementing Iterable and on arrays. When you also need the position there is no enumerate and no entries(): the counted loop shown is the ordinary answer, and it is what most Java code does.
While, Do and leaving early
Both loop forms carry over, an Until has to be inverted, and Java can name the loop it is leaving.
Option Strict On Imports System Module WhileDemo Sub Main() Dim remaining As Integer = 3 While remaining > 0 remaining -= 1 End While Console.WriteLine(remaining) Dim attempt As Integer = 0 Do attempt += 1 Loop Until attempt >= 2 Console.WriteLine(attempt) For number As Integer = 1 To 10 If number Mod 2 = 0 Then Continue For If number > 7 Then Exit For Console.Write(number & " ") Next Console.WriteLine() End Sub End Module
class Main { public static void main(String[] args) { int remaining = 3; while (remaining > 0) { remaining -= 1; } System.out.println(remaining); int attempt = 0; do { attempt += 1; } while (attempt < 2); System.out.println(attempt); outer: for (int number = 1; number <= 10; number++) { if (number % 2 == 0) continue; if (number > 7) break outer; System.out.print(number + " "); } System.out.println(); } }
While ... End While becomes while (...) { }; the Do ... Loop family becomes do { } while (...), and since there is no Until, Loop Until attempt >= 2 flips to while (attempt < 2). Exit For becomes break, Continue For becomes continue. Java adds labels, so break outer leaves a specific enclosing loop — the one thing here Visual Basic and C# both lack.
Methods
Sub and Function both become methods
The Sub/Function split disappears; the return type moves to the front.
Option Strict On Imports System Module MethodDemo Sub Announce(message As String) Console.WriteLine($"** {message} **") End Sub Function Add(left As Integer, right As Integer) As Integer Return left + right End Function Sub Main() Announce("starting") Console.WriteLine(Add(2, 3)) End Sub End Module
class Main { static void announce(String message) { System.out.println("** " + message + " **"); } static int add(int left, int right) { return left + right; } public static void main(String[] args) { announce("starting"); System.out.println(add(2, 3)); } }
Sub becomes void and Function ... As Integer becomes int. Everything must live inside a class — there are no free functions and no Module, so a bag of helpers becomes a class of static methods. Unlike Visual Basic's Module, whose members are callable unqualified, a static method in another class needs the class name: MathHelpers.add(2, 3).
Optional arguments become overloads
Java has neither optional parameters nor named arguments, and the workaround is visible in the line count.
Option Strict On Imports System Module ArgumentDemo Function Greet(name As String, Optional greeting As String = "Hello", Optional punctuation As String = "!") As String Return $"{greeting}, {name}{punctuation}" End Function Sub Main() Console.WriteLine(Greet("Ada")) Console.WriteLine(Greet("Grace", "Welcome")) Console.WriteLine(Greet("Alan", punctuation:=".")) End Sub End Module
class Main { static String greet(String name) { return greet(name, "Hello", "!"); } static String greet(String name, String greeting) { return greet(name, greeting, "!"); } static String greet(String name, String greeting, String punctuation) { return greeting + ", " + name + punctuation; } public static void main(String[] args) { System.out.println(greet("Ada")); System.out.println(greet("Grace", "Welcome")); System.out.println(greet("Alan", "Hello", ".")); } }
There are no default parameter values and no named arguments. The idiom is a chain of overloads, each delegating to the fullest one — which is why Java APIs have so many methods with the same name. Skipping a middle argument is impossible; you pass the default explicitly, as the third call does. When a method has more than three or four parameters the community answer is a builder object rather than more overloads.
ParamArray becomes varargs
A direct translation with the same rules on both sides.
Option Strict On Imports System Module VarargDemo Function SumAll(ParamArray values() As Integer) As Integer Dim total As Integer = 0 For Each value As Integer In values total += value Next Return total End Function Sub Main() Console.WriteLine(SumAll(1, 2, 3)) Console.WriteLine(SumAll()) Console.WriteLine(SumAll(New Integer() {4, 5})) End Sub End Module
class Main { static int sumAll(int... values) { int total = 0; for (int value : values) { total += value; } return total; } public static void main(String[] args) { System.out.println(sumAll(1, 2, 3)); System.out.println(sumAll()); System.out.println(sumAll(new int[] { 4, 5 })); } }
ParamArray values() As Integer becomes int... values — three dots after the type. It must be the last parameter, there can be only one, and the caller may pass loose arguments or a ready-made array. Inside the method it simply is an array.
There is no ByRef
Java is strictly pass-by-value, and what that means depends on whether the value is a reference.
Option Strict On Imports System Imports System.Collections.Generic Module ByRefDemo Sub Twice(ByRef value As Integer) value *= 2 End Sub Sub AddItem(items As List(Of Integer)) items.Add(99) End Sub Sub Main() Dim number As Integer = 21 Twice(number) Console.WriteLine(number) Dim numbers As New List(Of Integer) From {1} AddItem(numbers) Console.WriteLine(numbers.Count) End Sub End Module
import java.util.ArrayList; import java.util.List; class Main { static int twice(int value) { value *= 2; // the caller sees nothing return value; } static void addItem(List<Integer> items) { items.add(99); // mutates the caller's list } public static void main(String[] args) { int number = 21; twice(number); System.out.println(number); number = twice(number); System.out.println(number); List<Integer> numbers = new ArrayList<>(List.of(1)); addItem(numbers); System.out.println(numbers.size()); } }
There is no ByRef, no ref, no out — every argument is copied. For a primitive that means the caller sees nothing. For an object the reference is copied, so reassigning the parameter changes nothing but calling a mutating method changes the caller's object. The idiomatic answer is to return a new value; when a method genuinely must produce two things, return a record or an array rather than reaching for a wrapper class.
Classes & Interfaces
A class and its constructor
The constructor takes the class's name, and the fields are declared with the modifiers you already use.
Option Strict On Imports System Public Class Person Private ReadOnly _name As String Private ReadOnly _age As Integer Public Sub New(name As String, age As Integer) _name = name _age = age End Sub Public Function Describe() As String Return $"{_name}, age {_age}" End Function End Class Module ClassDemo Sub Main() Dim person As New Person("Ada", 36) Console.WriteLine(person.Describe()) End Sub End Module
class Person { private final String name; private final int age; Person(String name, int age) { this.name = name; this.age = age; } String describe() { return name + ", age " + age; } } class Main { public static void main(String[] args) { Person person = new Person("Ada", 36); System.out.println(person.describe()); } }
Public Sub New becomes a method named after the class with no return type. ReadOnly becomes final. Me becomes this, needed here to distinguish the field from the parameter of the same name — a very common Java idiom. Access modifiers are the same words in lower case, and a member with none is package-private, a visibility level .NET has no exact equivalent for (Friend is per-assembly, this is per-package).
There are no properties
The construct Visual Basic invented and C# adopted never reached Java, and its absence shapes how Java code reads.
Option Strict On Imports System Public Class Temperature Private _celsius As Double Public Property Celsius As Double Get Return _celsius End Get Set(value As Double) _celsius = Math.Max(value, -273.15) End Set End Property Public ReadOnly Property Fahrenheit As Double Get Return _celsius * 9.0 / 5.0 + 32 End Get End Property End Class Module PropertyDemo Sub Main() Dim reading As New Temperature() reading.Celsius = 100.0 Console.WriteLine(reading.Fahrenheit) reading.Celsius = -500 Console.WriteLine(reading.Celsius) End Sub End Module
class Temperature { private double celsius; double getCelsius() { return celsius; } void setCelsius(double value) { celsius = Math.max(value, -273.15); } double getFahrenheit() { return celsius * 9.0 / 5.0 + 32; } } class Main { public static void main(String[] args) { Temperature reading = new Temperature(); reading.setCelsius(100.0); System.out.println(reading.getFahrenheit()); reading.setCelsius(-500); System.out.println(reading.getCelsius()); } }
Java has no property syntax. The convention is a pair of methods named getX and setX — the JavaBeans convention, which every framework and IDE assumes. So reading.Celsius = 100 becomes reading.setCelsius(100), and there is no way to make a field access run code later without changing every caller. That is why Java codebases wrap fields in getters from the start, where C# and Visual Basic can promote a field to a property invisibly.
Records
The whole anchor column is what one line of Java writes for you.
Option Strict On Imports System Public Class Money Public ReadOnly Property Amount As Decimal Public ReadOnly Property Currency As String Public Sub New(amount As Decimal, currency As String) Me.Amount = amount Me.Currency = currency End Sub Public Overrides Function Equals(other As Object) As Boolean Dim candidate = TryCast(other, Money) Return candidate IsNot Nothing AndAlso candidate.Amount = Amount AndAlso candidate.Currency = Currency End Function Public Overrides Function GetHashCode() As Integer Return HashCode.Combine(Amount, Currency) End Function Public Overrides Function ToString() As String Return $"Money[amount={Amount}, currency={Currency}]" End Function End Class Module RecordDemo Sub Main() Dim price As New Money(9.99D, "USD") Dim same As New Money(9.99D, "USD") Console.WriteLine(price) Console.WriteLine(price.Equals(same)) End Sub End Module
import java.math.BigDecimal; record Money(BigDecimal amount, String currency) {} class Main { public static void main(String[] args) { Money price = new Money(new BigDecimal("9.99"), "USD"); Money same = new Money(new BigDecimal("9.99"), "USD"); System.out.println(price); System.out.println(price.equals(same)); System.out.println(price.amount()); } }
A record declares the fields, the constructor, equals, hashCode and a readable toString. Records are immutable, and the accessors are named after the components — price.amount(), not getAmount(), which is the one place Java breaks its own getter convention. There is no with expression as C# records have, so a modified copy means constructing a new one by hand. Records also work as patterns in a switch, which is where they earn most of their keep.
Interfaces
Matching is by signature rather than by declaration — and an interface may carry working code.
Option Strict On Imports System Imports System.Collections.Generic Public Interface IGreeter Function Greet(name As String) As String End Interface Public Class Formal Implements IGreeter ' The member may be named anything Public Function SayHello(name As String) As String Implements IGreeter.Greet Return $"Good day, {name}." End Function End Class Module InterfaceDemo Sub Main() Dim greeters As New List(Of IGreeter) From {New Formal()} For Each greeter As IGreeter In greeters Console.WriteLine(greeter.Greet("Ada")) Next End Sub End Module
import java.util.List; interface Greeter { String greet(String name); default String greetTwice(String name) { return greet(name) + " " + greet(name); } } class Formal implements Greeter { @Override public String greet(String name) { return "Good day, " + name + "."; } } class Main { public static void main(String[] args) { for (Greeter greeter : List.of(new Formal())) { System.out.println(greeter.greet("Ada")); System.out.println(greeter.greetTwice("Ada")); } } }
Implements IGreeter becomes implements Greeter on the class, and the method must have the same name as the interface member; Visual Basic's per-member Implements IGreeter.Greet, which lets you rename, has no counterpart. @Override is optional but always worth writing — it makes a misspelling a compile error. The addition is the default method, which carries a real body, so an interface can supply shared behaviour the way a Ruby mixin does. Interface names take no I prefix.
Inheritance
Four Visual Basic keywords collapse into three Java ones, and one of them is missing.
Option Strict On Imports System Public MustInherit Class Shape Public MustOverride Function Area() As Double Public Function Report() As String Return $"{Me.GetType().Name}: {Area():F2}" End Function End Class Public NotInheritable Class Circle Inherits Shape Private ReadOnly _radius As Double Public Sub New(radius As Double) _radius = radius End Sub Public Overrides Function Area() As Double Return Math.PI * _radius * _radius End Function End Class Module InheritanceDemo Sub Main() Dim shape As Shape = New Circle(2.0) Console.WriteLine(shape.Report()) End Sub End Module
abstract class Shape { abstract double area(); String report() { return String.format("%s: %.2f", getClass().getSimpleName(), area()); } } final class Circle extends Shape { private final double radius; Circle(double radius) { this.radius = radius; } @Override double area() { return Math.PI * radius * radius; } } class Main { public static void main(String[] args) { Shape shape = new Circle(2.0); System.out.println(shape.report()); } }
Inherits becomes extends, MustInherit and MustOverride both become abstract, NotInheritable becomes final, and MyBase becomes super. The missing one is Overridable: every non-final method is overridable in Java, which is the opposite default from .NET, so sealing a method means marking it final rather than leaving a keyword off. A class may extend one class and implement any number of interfaces.
Lambdas & Streams
Lambdas
The arrow is familiar; what is new is that each shape of function has its own interface and its own method name.
Option Strict On Imports System Module LambdaDemo Sub Main() Dim twice As Func(Of Integer, Integer) = Function(value) value * 2 Dim shout As Action(Of String) = Sub(message) Console.WriteLine(message.ToUpper()) Console.WriteLine(twice(21)) shout("done") End Sub End Module
import java.util.function.Consumer; import java.util.function.Function; class Main { public static void main(String[] args) { Function<Integer, Integer> twice = value -> value * 2; Consumer<String> shout = message -> System.out.println(message.toUpperCase()); System.out.println(twice.apply(21)); shout.accept("done"); } }
Function(value) value * 2 becomes value -> value * 2. But there is no single Func/Action pair: Java uses functional interfaces, and which one you need depends on the shape — Function<T,R> (called with apply), Consumer<T> (accept), Supplier<T> (get), Predicate<T> (test), BiFunction, and primitive-specialised versions such as IntFunction to avoid boxing. A method reference, String::toUpperCase, is Java's AddressOf.
LINQ becomes streams
Every LINQ operator has a counterpart, with one ceremony at each end of the chain.
Option Strict On Imports System Imports System.Linq Module LinqDemo Sub Main() Dim numbers() As Integer = {5, 3, 9, 1, 7, 2} Dim result = numbers. Where(Function(number) number > 2). Select(Function(number) number * 10). ToList() Console.WriteLine(String.Join(", ", result)) Console.WriteLine(numbers.Sum()) Console.WriteLine(numbers.Any(Function(number) number > 8)) Console.WriteLine(numbers.OrderBy(Function(number) number).First()) End Sub End Module
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(5, 3, 9, 1, 7, 2); List<Integer> result = numbers.stream() .filter(number -> number > 2) .map(number -> number * 10) .collect(Collectors.toList()); System.out.println(result); System.out.println(numbers.stream().mapToInt(Integer::intValue).sum()); System.out.println(numbers.stream().anyMatch(number -> number > 8)); System.out.println(numbers.stream().sorted().findFirst().orElseThrow()); } }
Wherefilter, Selectmap, AnyanyMatch, AllallMatch, FirstfindFirst, OrderBysorted, Aggregatereduce, GroupByCollectors.groupingBy. Two differences: you must open the chain with .stream() and close it with a collect (or toList() in Java 16+), and a stream is single-use — iterating one twice throws. Sum needs mapToInt because Stream<Integer> has no sum; the primitive streams do.
Error Handling
Try/Catch
Nearly a straight translation — the reordering inside the catch is the only change.
Option Strict On Imports System Module TryDemo Sub Main() Try Dim value As Integer = Integer.Parse("not a number") Console.WriteLine(value) Catch error_ As FormatException Console.WriteLine($"Bad format: {error_.Message}") Catch error_ As Exception Console.WriteLine($"Something else: {error_.Message}") Finally Console.WriteLine("always runs") End Try End Sub End Module
class Main { public static void main(String[] args) { try { int value = Integer.parseInt("not a number"); System.out.println(value); } catch (NumberFormatException error) { System.out.println("Bad format: " + error.getMessage()); } catch (Exception error) { System.out.println("Something else: " + error.getMessage()); } finally { System.out.println("always runs"); } } }
Catch name As TypeName becomes catch (TypeName name). Finally becomes finally. Order still matters, most specific first. error_.Message becomes error.getMessage(), since Java has no properties. Java also allows catch (A | B error) to handle two types in one block, which .NET has no equivalent for. There is no exception filter — no When — so a conditional catch becomes an if and a rethrow.
Checked exceptions
The one idea in Java error handling that .NET has nothing like, and the one that shapes the most code.
Option Strict On Imports System Module CheckedDemo ' Nothing in the signature says this can fail Function Risky(value As Integer) As Integer If value < 0 Then Throw New ArgumentException("negative") Return value * 2 End Function Sub Main() ' The compiler does not require a Try here Console.WriteLine(Risky(21)) End Sub End Module
import java.io.IOException; class Main { // "throws" is part of the signature and the compiler enforces it static int risky(int value) throws IOException { if (value < 0) throw new IOException("negative"); return value * 2; } static int unchecked(int value) { if (value < 0) throw new IllegalArgumentException("negative"); return value * 2; } public static void main(String[] args) throws IOException { System.out.println(risky(21)); System.out.println(unchecked(21)); } }
Java divides exceptions in two. A checked exception must be declared with throws and every caller must either catch it or declare it too — the compiler refuses otherwise. An unchecked one (anything extending RuntimeException) behaves like every .NET exception: invisible in the signature, catchable or not as you please. Checked exceptions are why so much Java code has throws IOException travelling up through it, and why some codebases wrap everything in unchecked exceptions on principle.
Using becomes try-with-resources
The same guarantee, spelled as a clause on the try.
Option Strict On Imports System Imports System.IO Module UsingDemo Sub Main() Dim contents As String = "" Using writer As New StringWriter() writer.WriteLine("first line") writer.WriteLine("second line") contents = writer.ToString() End Using Console.WriteLine(contents.Trim()) End Sub End Module
import java.io.StringWriter; class Main { public static void main(String[] args) throws Exception { String contents; try (StringWriter writer = new StringWriter()) { writer.write("first line\n"); writer.write("second line\n"); contents = writer.toString(); } System.out.println(contents.strip()); } }
Using ... End Using becomes try (resource) { ... }, and it closes the resource whether the block ends normally or by exception. Java's AutoCloseable is IDisposable, and close() is Dispose(). Several resources go in one clause separated by semicolons, closed in reverse order. Note that close() may itself be declared to throw, which is why the method carries throws Exception.
⚠ Gotchas for Visual Basic Programmers
⚠ == on strings compares references
Worth its own row because it is the difference most likely to compile, pass a quick test, and then fail on real data.
Option Strict On Imports System Module StringEqualityGotcha Sub Main() Dim typed As String = "hello" Dim built As String = "hel" & "lo" Dim runtime As String = New String("hello".ToCharArray()) Console.WriteLine(typed = built) Console.WriteLine(typed = runtime) End Sub End Module
class Main { public static void main(String[] args) { String typed = "hello"; String built = "hel" + "lo"; String runtime = new String("hello".toCharArray()); System.out.println(typed == built); // true — both interned System.out.println(typed == runtime); // FALSE — different objects System.out.println(typed.equals(runtime)); } }
Visual Basic's = on strings compares value. Java's == compares identity. Compile-time constants are interned, so typed == built is true and the bug hides — until a string arrives from a file, a database or user input, when it becomes false. Always equals, and Objects.equals(a, b) when either might be null. This is the same trap C# would set except that C# overloads == for String to compare value; Java does not.
⚠ Boxed integers compare by reference too
The same mistake as the previous row, with a threshold that makes it look intermittent.
Option Strict On Imports System Module BoxingGotcha Sub Main() Dim small As Object = 100 Dim alsoSmall As Object = 100 Dim large As Object = 1000 Dim alsoLarge As Object = 1000 ' Value equality either way Console.WriteLine(small.Equals(alsoSmall)) Console.WriteLine(large.Equals(alsoLarge)) End Sub End Module
class Main { public static void main(String[] args) { Integer small = 100; Integer alsoSmall = 100; Integer large = 1000; Integer alsoLarge = 1000; System.out.println(small == alsoSmall); // true — cached System.out.println(large == alsoLarge); // FALSE — not cached System.out.println(large.equals(alsoLarge)); // true } }
Java caches boxed Integer objects for values from −128 to 127, so == on two boxed 100s is true and on two boxed 1000s is false. Code tested with small numbers passes and then fails in production. The rule is the same as for strings — use equals, or better, keep the values as primitive int where you can. Autoboxing is also where an unexpected NullPointerException comes from: unboxing a null Integer into an int throws.
⚠ Generic types are erased
A .NET generic is real at runtime. A Java generic is not, and that has consequences you meet quickly.
Option Strict On Imports System Imports System.Collections.Generic Module ErasureGotcha Sub Main() Dim numbers As New List(Of Integer) From {1, 2, 3} ' The type argument is real at runtime Console.WriteLine(numbers.GetType().GetGenericArguments()(0).Name) Console.WriteLine(TypeOf CObj(numbers) Is List(Of Integer)) End Sub End Module
import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(1, 2, 3); // Nothing at runtime knows this was a List of Integer System.out.println(numbers.getClass().getSimpleName()); System.out.println(numbers instanceof List<?>); // System.out.println(numbers instanceof List<Integer>); // will not compile } }
Java implements generics by erasure: List<Integer> and List<String> are the same class at runtime and the type argument is gone. So you cannot ask what a list holds, cannot write instanceof List<Integer>, cannot create new T[], and cannot overload two methods that differ only in type argument. .NET reifies generics, so all of those work there. The practical consequence is that a Java API needing the type at runtime asks you to pass a Class<T> alongside it.
⚠ There is no Decimal
Money still works — but it stops being arithmetic and becomes method calls.
Option Strict On Imports System Module DecimalGotcha Sub Main() Dim price As Decimal = 0.1D Dim tax As Decimal = 0.2D Console.WriteLine(price + tax) Console.WriteLine((price + tax) = 0.3D) End Sub End Module
import java.math.BigDecimal; class Main { public static void main(String[] args) { double loose = 0.1 + 0.2; System.out.println(loose); System.out.println(loose == 0.3); BigDecimal price = new BigDecimal("0.1"); BigDecimal tax = new BigDecimal("0.2"); BigDecimal total = price.add(tax); System.out.println(total); System.out.println(total.compareTo(new BigDecimal("0.3")) == 0); } }
There is no Decimal primitive. BigDecimal is exact and is the right type for money, with three costs: it must be built from a string (new BigDecimal(0.1) has already lost the value), it has no operators so + becomes .add() and * becomes .multiply(), and equals compares scale as well as value — so 0.10 does not equal 0.1 and you must use compareTo(...) == 0. Every one of those catches people once.
⚠ No My namespace, and a different everything
This is a whole different platform, not a different syntax — the standard library shares almost no names with .NET.
Option Strict On Imports System Module PlatformGotcha Sub Main() Console.WriteLine(Environment.MachineName.Length > 0) Console.WriteLine(IsNumeric("42")) Console.WriteLine(Now.Year > 2000) End Sub End Module
import java.net.InetAddress; import java.time.LocalDate; class Main { public static void main(String[] args) throws Exception { System.out.println(InetAddress.getLocalHost().getHostName().length() > 0); System.out.println("42".matches("\\d+")); System.out.println(LocalDate.now().getYear() > 2000); } }
Nothing carries over by name. My.Computer.Name becomes InetAddress.getLocalHost().getHostName(), My.Computer.FileSystem becomes java.nio.file.Files and Path, IsNumeric becomes a regular expression or a try around parseInt, Now becomes LocalDate.now() or Instant.now(). NuGet becomes Maven or Gradle, the .vbproj becomes a pom.xml or build.gradle, and MsgBox and the WinForms designer have no equivalent — a desktop Java application means Swing or JavaFX, and most Java work is server-side.