diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava.sln b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava.sln new file mode 100644 index 000000000..9a0fda14a --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpVsJava", "CSharpVsJava\CSharpVsJava.csproj", "{D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|x64.Build.0 = Debug|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|x86.Build.0 = Debug|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|Any CPU.Build.0 = Release|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|x64.ActiveCfg = Release|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|x64.Build.0 = Release|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|x86.ActiveCfg = Release|Any CPU + {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/CSharpVsJava.csproj b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/CSharpVsJava.csproj new file mode 100644 index 000000000..ed9781c22 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/CSharpVsJava.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ConcurrencyDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ConcurrencyDemo.cs new file mode 100644 index 000000000..7743a66ce --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ConcurrencyDemo.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace CSharpVsJava; + +public static class ConcurrencyDemo +{ + public static async Task RunAsync() + { + var stopwatch = Stopwatch.StartNew(); + + var stock = await Task.WhenAll(Enumerable.Range(1, 10_000).Select(GetStockAsync)); + + Console.WriteLine($"{stock.Length:N0} stock checks in {stopwatch.ElapsedMilliseconds:N0} ms"); + } + + private static async Task GetStockAsync(int productId) + { + await Task.Delay(TimeSpan.FromSeconds(1)); // stands in for a database or HTTP call + return productId % 7; + } +} diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/EverydayDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/EverydayDemo.cs new file mode 100644 index 000000000..650a49889 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/EverydayDemo.cs @@ -0,0 +1,23 @@ +namespace CSharpVsJava; + +public record Money(decimal Amount) +{ + public static Money operator +(Money left, Money right) => new(left.Amount + right.Amount); +} + +public class Product +{ + public required string Name { get; init; } + public required Money Price { get; set; } +} + +public static class EverydayDemo +{ + public static void Run() + { + var lamp = new Product { Name = "Desk lamp", Price = new Money(30m) }; + lamp.Price += new Money(5m); + + Console.WriteLine($"{lamp.Name} costs {lamp.Price.Amount}"); + } +} diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/GenericsDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/GenericsDemo.cs new file mode 100644 index 000000000..5148bc197 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/GenericsDemo.cs @@ -0,0 +1,16 @@ +namespace CSharpVsJava; + +public static class GenericsDemo +{ + public static void Run() + { + var quantities = new List { 3, 1, 2 }; + var customers = new List { "Ann", "Bob" }; + + Console.WriteLine(quantities.GetType()); + Console.WriteLine(quantities.GetType() == customers.GetType()); + Console.WriteLine(Describe(quantities)); + } + + private static string Describe(List items) => $"{items.Count} items of type {typeof(T).Name}"; +} diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/LinqDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/LinqDemo.cs new file mode 100644 index 000000000..eafb1612d --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/LinqDemo.cs @@ -0,0 +1,24 @@ +namespace CSharpVsJava; + +public record Order(string Customer, int Quantity); + +public static class LinqDemo +{ + public static void Run() + { + var orders = new List + { + new("Ann", 3), new("Bob", 1), new("Ann", 4), new("Cid", 5), new("Bob", 2) + }; + + var totals = orders + .GroupBy(order => order.Customer) + .Select(group => new { Customer = group.Key, Items = group.Sum(order => order.Quantity) }) + .OrderByDescending(total => total.Items); + + foreach (var total in totals) + { + Console.WriteLine($"{total.Customer}: {total.Items}"); + } + } +} diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/MemoryDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/MemoryDemo.cs new file mode 100644 index 000000000..cefd63bc9 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/MemoryDemo.cs @@ -0,0 +1,19 @@ +namespace CSharpVsJava; + +public static class MemoryDemo +{ + public static void Run() + { + const int count = 1_000_000; + var before = GC.GetAllocatedBytesForCurrentThread(); + + var quantities = new List(count); + for (var i = 0; i < count; i++) + { + quantities.Add(i); + } + + var allocated = GC.GetAllocatedBytesForCurrentThread() - before; + Console.WriteLine($"List with {quantities.Count:N0} items: {allocated / 1_000_000.0:F1} MB allocated"); + } +} diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/NullDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/NullDemo.cs new file mode 100644 index 000000000..dc38beed3 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/NullDemo.cs @@ -0,0 +1,18 @@ +namespace CSharpVsJava; + +public static class NullDemo +{ + public static void RunUnchecked() + { + string? email = FindEmail(customerId: 42); + Console.WriteLine(email.Length); + } + + public static void Run() + { + string? email = FindEmail(customerId: 42); + Console.WriteLine(email?.Length ?? 0); + } + + private static string? FindEmail(int customerId) => customerId == 1 ? "ann@example.com" : null; +} diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/Program.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/Program.cs new file mode 100644 index 000000000..cb112236a --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/Program.cs @@ -0,0 +1,16 @@ +using CSharpVsJava; + +switch (args.FirstOrDefault()) +{ + case "generics": GenericsDemo.Run(); break; + case "memory": MemoryDemo.Run(); break; + case "concurrency": await ConcurrencyDemo.RunAsync(); break; + case "linq": LinqDemo.Run(); break; + case "shapes": ShapesDemo.Run(); break; + case "nulls": NullDemo.Run(); break; + case "nulls-unchecked": NullDemo.RunUnchecked(); break; + case "everyday": EverydayDemo.Run(); break; + default: + Console.WriteLine("Pass one of: generics, memory, concurrency, linq, shapes, nulls, nulls-unchecked, everyday"); + break; +} diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ShapesDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ShapesDemo.cs new file mode 100644 index 000000000..867b2853a --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ShapesDemo.cs @@ -0,0 +1,27 @@ +namespace CSharpVsJava; + +public abstract record Shape; +public record Circle(double Radius) : Shape; +public record Square(double Side) : Shape; + +public static class ShapesDemo +{ + public static void Run() + { + Shape[] shapes = [new Circle(1), new Square(2)]; + foreach (var shape in shapes) + { + Console.WriteLine($"{shape}: {Area(shape):F2}"); + } + + var bigger = new Circle(1) with { Radius = 3 }; + Console.WriteLine(bigger); + } + + private static double Area(Shape shape) => shape switch + { + Circle(var radius) => Math.PI * radius * radius, + Square(var side) => side * side, + _ => throw new ArgumentOutOfRangeException(nameof(shape)) + }; +} diff --git a/csharp-intermediate-topics/CSharpVsJava/README.md b/csharp-intermediate-topics/CSharpVsJava/README.md new file mode 100644 index 000000000..cad0f4f31 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/README.md @@ -0,0 +1,42 @@ +# C# vs Java: Which One Should You Pick? + +Sample code for the Code Maze article [C# vs Java: Which One Should You Pick?](https://code-maze.com/csharp-vs-java/). + +Each difference the article shows is one small program in both languages: + +| Demo | C# (console app) | Java (single-file program) | +|---|---|---| +| Generics at runtime | `GenericsDemo.cs` | `java/Generics.java` | +| Memory for a million numbers | `MemoryDemo.cs` | `java/Memory.java` | +| 10,000 one-second waits | `ConcurrencyDemo.cs` | `java/Concurrency.java` | +| Grouping query | `LinqDemo.cs` | `java/Orders.java` | +| Records and pattern matching | `ShapesDemo.cs` | `java/Shapes.java` | +| Null safety | `NullDemo.cs` | `java/Nulls.java` | +| Properties, operators, checked exceptions | `EverydayDemo.cs` | `java/Everyday.java` | + +## Running the C# demos + +You need the .NET 10 SDK. From this folder, pass the name of one demo: + +``` +dotnet run --project CSharpVsJava -- generics +``` + +The names are `generics`, `memory`, `concurrency`, `linq`, `shapes`, `nulls`, `nulls-unchecked` and `everyday`. + +The build prints one warning on purpose, CS8602 in `NullDemo.RunUnchecked()`, and `nulls-unchecked` ends in a `NullReferenceException`. That is the bug the article asks the compiler to catch. + +## Running the Java programs + +You need JDK 25 or later. Each file runs on its own, with no Maven or Gradle project: + +``` +cd java +java Generics.java +``` + +`Nulls.java` ends in a `NullPointerException` (exit code 1) on purpose, and `Everyday.java` expects no `orders.csv` in the folder it runs from. + +## A note on CI + +The repository's CI builds this folder through `CSharpVsJava.sln`, so it compiles the C# project only. It does not compile or run the files in `java/`. They are here so the reader can run both sides, and we ran them with Eclipse Temurin 25 when we wrote the article. diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Concurrency.java b/csharp-intermediate-topics/CSharpVsJava/java/Concurrency.java new file mode 100644 index 000000000..17351f3be --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/java/Concurrency.java @@ -0,0 +1,20 @@ +void main() throws Exception { + long start = System.nanoTime(); + + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + var checks = IntStream.rangeClosed(1, 10_000) + .mapToObj(id -> executor.submit(() -> getStock(id))) + .toList(); + for (var check : checks) { + check.get(); + } + } + + long elapsed = Duration.ofNanos(System.nanoTime() - start).toMillis(); + IO.println("%,d stock checks in %,d ms".formatted(10_000, elapsed)); +} + +int getStock(int productId) throws InterruptedException { + Thread.sleep(Duration.ofSeconds(1)); // stands in for a database or HTTP call + return productId % 7; +} diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Everyday.java b/csharp-intermediate-topics/CSharpVsJava/java/Everyday.java new file mode 100644 index 000000000..143fb3e7c --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/java/Everyday.java @@ -0,0 +1,31 @@ +record Money(BigDecimal amount) { + Money add(Money other) { + return new Money(amount.add(other.amount)); + } +} + +class Product { + private final String name; + private Money price; + + Product(String name, Money price) { + this.name = name; + this.price = price; + } + + String getName() { return name; } + Money getPrice() { return price; } + void setPrice(Money price) { this.price = price; } +} + +void main() { + var lamp = new Product("Desk lamp", new Money(new BigDecimal("30"))); + lamp.setPrice(lamp.getPrice().add(new Money(new BigDecimal("5")))); + IO.println(lamp.getName() + " costs " + lamp.getPrice().amount()); + + try { + IO.println(Files.readString(Path.of("orders.csv"))); + } catch (IOException e) { + IO.println("Could not read " + e.getMessage()); + } +} diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Generics.java b/csharp-intermediate-topics/CSharpVsJava/java/Generics.java new file mode 100644 index 000000000..5943bf80f --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/java/Generics.java @@ -0,0 +1,12 @@ +void main() { + var quantities = new ArrayList(List.of(3, 1, 2)); + var customers = new ArrayList(List.of("Ann", "Bob")); + + IO.println(quantities.getClass()); + IO.println(quantities.getClass() == customers.getClass()); + IO.println(describe(quantities, Integer.class)); +} + + String describe(List items, Class type) { + return items.size() + " items of type " + type.getSimpleName(); +} diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Memory.java b/csharp-intermediate-topics/CSharpVsJava/java/Memory.java new file mode 100644 index 000000000..66dfef3c6 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/java/Memory.java @@ -0,0 +1,17 @@ +import com.sun.management.ThreadMXBean; +import java.lang.management.ManagementFactory; + +void main() { + final int count = 1_000_000; + var threads = (ThreadMXBean) ManagementFactory.getThreadMXBean(); + long before = threads.getCurrentThreadAllocatedBytes(); + + var quantities = new ArrayList(count); + for (int i = 0; i < count; i++) { + quantities.add(i); + } + + long allocated = threads.getCurrentThreadAllocatedBytes() - before; + IO.println("ArrayList with %,d items: %.1f MB allocated" + .formatted(quantities.size(), allocated / 1_000_000.0)); +} diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Nulls.java b/csharp-intermediate-topics/CSharpVsJava/java/Nulls.java new file mode 100644 index 000000000..13ebc9fd9 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/java/Nulls.java @@ -0,0 +1,14 @@ +void main() { + IO.println(findEmail(42).map(String::length).orElse(0)); + + String email = findEmailOrNull(42); + IO.println(email.length()); +} + +Optional findEmail(int customerId) { + return customerId == 1 ? Optional.of("ann@example.com") : Optional.empty(); +} + +String findEmailOrNull(int customerId) { + return customerId == 1 ? "ann@example.com" : null; +} diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Orders.java b/csharp-intermediate-topics/CSharpVsJava/java/Orders.java new file mode 100644 index 000000000..195b3efe6 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/java/Orders.java @@ -0,0 +1,13 @@ +record Order(String customer, int quantity) {} + +void main() { + var orders = List.of( + new Order("Ann", 3), new Order("Bob", 1), new Order("Ann", 4), + new Order("Cid", 5), new Order("Bob", 2)); + + orders.stream() + .collect(Collectors.groupingBy(Order::customer, Collectors.summingInt(Order::quantity))) + .entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .forEach(total -> IO.println(total.getKey() + ": " + total.getValue())); +} diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Shapes.java b/csharp-intermediate-topics/CSharpVsJava/java/Shapes.java new file mode 100644 index 000000000..4c5008a93 --- /dev/null +++ b/csharp-intermediate-topics/CSharpVsJava/java/Shapes.java @@ -0,0 +1,17 @@ +sealed interface Shape permits Circle, Square {} +record Circle(double radius) implements Shape {} +record Square(double side) implements Shape {} + +void main() { + List shapes = List.of(new Circle(1), new Square(2)); + for (var shape : shapes) { + IO.println("%s: %.2f".formatted(shape, area(shape))); + } +} + +double area(Shape shape) { + return switch (shape) { + case Circle(double radius) -> Math.PI * radius * radius; + case Square(double side) -> side * side; + }; +}