2. Types, equality and operators
Full examples: lessons/l02 — after mvn compile, run one with java -cp target/classes lessons.l02.Numbers; CI checks every output below.
Two kinds of types, and no struct
Section titled “Two kinds of types, and no struct”| C# | Java | |
|---|---|---|
| Built-in value types | int, long, double, bool, char, decimal… |
8 primitive types: byte short int long float double boolean char |
| User-defined value types | struct, record struct |
none (see Project Valhalla) |
| Everything else | reference types | reference types |
| Unsigned integers | byte, ushort, uint, ulong |
none |
| Decimal arithmetic | decimal |
the BigDecimal class |
| Primitive in an object context | boxing to object |
boxing to a wrapper class: Integer, Long, Boolean… |
In C#, int is System.Int32, a struct with methods. In Java, int is a primitive with no methods, and Integer is a separate class. The compiler converts between them automatically (autoboxing), which is convenient until it isn’t.
Numbers
Section titled “Numbers”int max = Integer.MAX_VALUE;System.out.println(max + 1);
try { System.out.println(Math.addExact(max, 1));} catch (ArithmeticException e) { System.out.println("ArithmeticException: " + e.getMessage());}
byte b = (byte) 200;System.out.println(b);System.out.println(Byte.toUnsignedInt(b));
int allOnes = -1;System.out.println(Integer.toUnsignedString(allOnes));System.out.println(Integer.divideUnsigned(allOnes, 2));-2147483648ArithmeticException: integer overflow-5620042949672952147483647- Overflow wraps silently, exactly like C# in its default
uncheckedcontext. Java has nocheckedkeyword:Math.addExact,multiplyExactand friends throw instead. byteis signed (−128 to 127). Reading bytes from a file or a network buffer is where this bites:(byte) 200is-56, andByte.toUnsignedIntgives you the 200 back. In C#,byteis unsigned andsbyteis the signed one.- No
uintorulong, butIntegerandLonghave static methods that interpret the same bits as unsigned.
The C# side, run by the course’s CI:
-2147483648OverflowException: Arithmetic operation resulted in an overflow.200-56Division and char arithmetic behave the same in both languages: integer division by zero throws (ArithmeticException: / by zero vs DivideByZeroException), floating-point division gives Infinity, and 'a' + 1 is the int 98.
== compares references for objects
Section titled “== compares references for objects”This is the one to remember. For primitives, == compares values. For objects — including Integer and String — it compares references, and Java has no operator overloading to change that.
Integer a = 127;Integer b = 127;Integer c = 128;Integer d = 128;System.out.println(a == b);System.out.println(c == d);System.out.println(c.equals(d));
String literal = "hello";String sameLiteral = "hello";String built = new StringBuilder("hel").append("lo").toString();System.out.println(literal == sameLiteral);System.out.println(literal == built);System.out.println(literal.equals(built));truefalsetruetruefalsetruea == bistrueonly because autoboxing goes throughInteger.valueOf, which caches the values −128 to 127. From 128 on, each boxing creates a new object. Code that comparesIntegers with==passes every test with small IDs and fails in production.- Two identical string literals are the same object (literals are interned), but a string built at run time is not.
- Use
equalsfor objects, always.Objects.equals(x, y)also handlesnull.
In C#, string overloads == to compare contents, so literal == built is True. Boxed values compared as object behave like Java’s, but you rarely write that in C#:
object boxedA = 127, boxedB = 127;Console.WriteLine(boxedA == boxedB); // False: reference comparison on object
string literal = "hello";string built = new System.Text.StringBuilder("hel").Append("lo").ToString();Console.WriteLine(literal == built); // True: string overloads ==Boxing also brings null into arithmetic. Unboxing a null Integer throws, and since Java 14 the message says exactly which variable was null (helpful NullPointerExceptions):
Integer missing = null;int value = missing;NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "missing" is nullvar and final
Section titled “var and final”var name = "Ada"; // inferred as Stringfinal var year = 1843; // cannot be reassigned; C# has no equivalent for localsvar works like C#’s, but only for local variables with an initializer:
class VarField { var count = 0;}VarField.java:2: error: 'var' is not allowed here var count = 0; ^1 errorfinal on a local or a field means “assigned exactly once” — C#’s readonly for fields, and something C# has no keyword for on locals. There is no const: a constant is a static final field.
class ReassignFinal { void run() { final int limit = 10; limit = 20; }}ReassignFinal.java:4: error: cannot assign a value to final variable limit limit = 20; ^1 errorConversions are as strict as in C#
Section titled “Conversions are as strict as in C#”Narrowing needs a cast, int is not a boolean, and a local must be definitely assigned before use. The rules match C#’s almost one for one:
class LossyConversion { void run() { double price = 3.5; int rounded = price; }}LossyConversion.java:4: error: incompatible types: possible lossy conversion from double to int int rounded = price; ^1 errorclass Unassigned { int run(boolean flag) { int result; if (flag) { result = 1; } return result; }}Unassigned.java:7: error: variable result might not have been initialized return result; ^1 errorStrings: formatting and text blocks
Section titled “Strings: formatting and text blocks”Java has no string interpolation. String templates were previewed in Java 21 and 22, then withdrawn. You format with String.formatted (or String.format), using printf-style specifiers:
System.out.println("%s published her notes in %d.".formatted(name, year));Ada published her notes in 1843.Text blocks are Java’s multi-line strings, close to C# raw string literals. The closing """ sets the indentation to strip:
String json = """ { "name": "%s", "year": %d } """.formatted(name, year);System.out.print(json);{ "name": "Ada", "year": 1843}switch expressions
Section titled “switch expressions”Arrow-form switch is an expression, like C#’s switch expression, and never falls through. A block that computes the value ends with yield:
var size = Size.MEDIUM;int price = switch (size) { case SMALL -> 3; case MEDIUM -> 4; case LARGE -> { int base = 4; yield base + 1; }};A switch over an enum that lists every constant needs no default. Over an int it does, and Java makes it an error where C# only warns (CS8509):
class SwitchNotExhaustive { String describe(int code) { return switch (code) { case 200 -> "OK"; case 404 -> "Not Found"; }; }}SwitchNotExhaustive.java:3: error: the switch expression does not cover all possible input values return switch (code) { ^1 errorLesson 8 takes switch further, with patterns and sealed types.
Key takeaways
Section titled “Key takeaways”- Java has eight primitives and no user-defined value types; everything else is a reference.
==on objects compares references: useequals. TheIntegercache makes==look right for small numbers.- Overflow is silent;
Math.*Exactis thecheckedequivalent.byteis signed and there are no unsigned types. varis for locals only;finalmeans assigned once; constants arestatic final.- No interpolation:
formatted, text blocks, andswitchexpressions with->andyield.
Exercises
Section titled “Exercises”- Without running it, predict the output. Then explain how to fix the method.
static boolean sameId(Integer left, Integer right) { return left == right;}// sameId(42, 42) → ?// sameId(1000, 1000) → ?Solution
true, then false: 42 is inside the Integer cache, so both arguments box to the same object; 1000 is not, so they are two objects. Compare values with left.equals(right), or Objects.equals(left, right) if either may be null. If null is not a valid ID, the best fix is to declare the parameters as int.
- A C# method reads a length as a
uintfrom a binary header. Write the Java equivalent ofuint length = BitConverter.ToUInt32(bytes, 0);for a little-endianbyte[], returning a value that can hold everyuint.
Solution
static long readUInt32LittleEndian(byte[] bytes) { return java.nio.ByteBuffer.wrap(bytes, 0, 4) .order(java.nio.ByteOrder.LITTLE_ENDIAN) .getInt() & 0xFFFFFFFFL;}getInt() returns a signed int; masking with 0xFFFFFFFFL widens it to a long without sign extension (Integer.toUnsignedLong does the same). ByteBuffer is big-endian by default, unlike BitConverter on x86, hence the explicit order.
- Rewrite this C# method in Java, keeping it an expression:
static string Classify(int status) => status switch{ >= 200 and < 300 => "success", 404 => "not found", _ => "other",};Solution
static String classify(int status) { return switch (status) { case 404 -> "not found"; default -> status >= 200 && status < 300 ? "success" : "other"; };}Java 25 has no relational patterns like >= 200 and < 300 on primitives (primitive patterns are still in preview), so the range check moves into the default branch — or the whole method becomes an if chain.