Java.equals(C#)?

Main differences in language features between C# and Java. How competition helps them to be better.
Java.equals(C#)?

So you skipped. You skipped all of the previous parts where I talked about some important differences. But you're allowed to continue reading from this point, just don't butthurt too much, because this part is focusing solely on language features:

Tuples

Let's start with a simple one. In C# you can use (int x, int y) instead of any type anywhere and get over with it. In Java it's different, so I want you to check the example:

public (string name, int age) GetAge()
{
  return (name: "Aboba", age: 54);
}
import javafx.util.Pair;

public Pair<String, Integer> getAge() {
  return new Pair<>("Aboba", 54);
}

Basically, it's the same, but only almost the same!

️Nullability

Just watch this Billion dollar mistake by Tony Hoare - creator of the ALGOL programming language, where he introduced this null value into every type. He thought, at the time, that this was very convenient, but it turned out to be very problematic in the future with NullReferenceException being THE MOST COMMON ERROR IN THE WORLD.

So how to solve it? Well, just split types that can have null values and the ones that can't. In C#, for example, you can enable a special mode and use string? type - it basically means string + null. And you're required to check for null everywhere it is used.

In Java a similar thing is used, but it's more optional. You'll need to annotate a type that you want to make nullable with @Nullable. But it's, honestly, more tedious to write.

️Properties

I've always found it weird how Java does all these shenanigans with its getters and setters when in C# you can just do the:

class Point
{
  public int X { get; set; }
  public int Y { get; set; }
}

So to access one of the coordinates, just use p.X and it will work like a charm! Because property is a function, that behaves like a value. You can also customize all these get and set methods to make them do what you want:

class Point
{
  public int _x;
  public int X 
  { 
    get => _x, 
    private set
    {
      Console.WriteLine("We're setting X, this must be some occasion");
      _x = value; 
    }
  }
  public int Y { get; set; }
}

So it's quite powerful syntactic sugar here!

Dynamic

Let's get this straight. Java doesn't directly support this stuff. But you can overcome this. Continue.

dynamic keyword in C# is useful in situations where the type of an object or variable is not known until runtime. With the dynamic keyword, developers can write code that can adapt to different types at runtime. This provides more flexibility and ease of use.

For example, suppose we have an external data source that provides data in JSON format. We can use dynamic to deserialize the data without knowing the exact structure of the data beforehand. The following code snippet shows how dynamic keyword can be used to deserialize JSON data:

using Newtonsoft.Json;

dynamic data = JsonConvert.DeserializeObject(jsonString);
var (name, age, hobbies) = (data.name, data.age, data.hobbies);

For this feature to be available there's a whole new dynamic runtime involved! But I've heard rumors that C# dynamic keyword is really a bad practice and there's a new thing going out(unfortunately I don't have a link for this).

LINQ n' stuff

This one is my favorite! LINQ (Language Integrated Query) is a feature in C# that provides a consistent way to query data from various sources. It works by using a set of standard query operators to manipulate data in a declarative way.

using System;
using System.Collections.Generic;
using System.Linq;

public static class Extensions
{
  // Custom extension method that returns a sequence of distinct numbers
  public static IEnumerable<int> DistinctNumbers(this IEnumerable<int> source)
    {
      // Use LINQ's Distinct method to get unique numbers
      return source.Distinct();
    }
}

class Program
{
  static void Main(string[] args)
  {
    // Create a list of numbers
    List<int> numbers = new List<int>() { 1, 2, 2, 3, 3, 3 };

    // Use the custom DistinctNumbers method to get unique numbers
    var distinctNumbers = numbers.DistinctNumbers();

    // Use LINQ's Sum method to get the sum of the numbers
    var sum = numbers.Sum();

    // Output the results
    Console.WriteLine("Original numbers: " + string.Join(", ", numbers));
    Console.WriteLine("Distinct numbers: " + string.Join(", ", distinctNumbers));
    Console.WriteLine("Sum: " + sum);
  }
}

Its design is certainly an example of wise design. But the development of this feature was not instant. It was a few years of constant small language improvements before they were all pushed for LINQ to emerge in the end.

Here are the main parts that made up LINQ historically:

Lambdas

Lambda expressions are used to create anonymous functions that can be passed as arguments to methods or stored as variables. In LINQ, lambdas are commonly used to define the predicates and projections that are applied to data sets.

int[] numbers = { 1, 2, 3, 4, 5 };
var filteredNumbers = numbers.Where(n => n % 2 == 0);
Example

Generics

Generics are used extensively in LINQ to create type-safe query operators that can be applied to different types of data.

public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
    foreach (T item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}
Example

IEnumerable

The IEnumerable interface is used as the base type for all LINQ query results. It provides a standard way to iterate over data sets and supports lazy evaluation of query results.

IEnumerable<int> numbers = Enumerable.Range(1, 5);
foreach (int number in numbers)
{
    Console.WriteLine(number);
}
Example

Laziness

LINQ uses lazy evaluation to defer the execution of query operations until the results are actually needed. This can improve performance and reduce memory usage, especially when working with large data sets.

var numbers = Enumerable.Range(1, 10);
var evenNumbers = numbers.Where(n => n % 2 == 0);
var firstFiveEvenNumbers = evenNumbers.Take(5);
foreach (int number in firstFiveEvenNumbers)
{
    Console.WriteLine(number);
}
Example

Yield

The yield keyword is used in LINQ to define iterator methods that can be used to generate sequences of data on the fly. This is useful for implementing query operators that operate on large or infinite data sets.

public static IEnumerable<int> GenerateRandomNumbers(int count)
{
    Random random = new Random();
    for (int i = 0; i < count; i++)
    {
        yield return random.Next();
    }
}
Example

Query syntax

The query syntax in LINQ provides a more natural way to express queries using SQL-like syntax. It is translated into method calls behind the scenes and provides a more readable way to write complex queries.

var numbers = from n in Enumerable.Range(1, 10)
              where n % 2 == 0
              select n;
foreach (int number in numbers)
{
    Console.WriteLine(number);
}
Example

Value types

In C#, both structs and classes are used to define custom data types, but they have some differences. Structs are value types, whereas classes are reference types.

When a struct is assigned to a variable or passed as a parameter, its value is copied to the new location. This means that changes to the variable or parameter do not affect the original value. In contrast, when a class is assigned to a variable or passed as a parameter, only a reference to the object is copied. This means that changes to the variable or parameter affect the original object.

structs are typically used for small data types that have value semantics, while classes are used for larger data types that require reference semantics.

Local functions

They both support this (finally)! See the example in Java:

public void outerMethod() {
  System.out.println("This is the outer method.");
   
  // Nested method
  void innerMethod() {
    System.out.println("This is the inner method.");
  }
   
  // Call the nested method
  innerMethod();
}

This fights the creation of a lot of stupid __do_helper_thing methods.

Partial classes

In C#, a partial class is a class that can be split into multiple files, while still being treated as a single class by the compiler. This feature allows developers to organize a class's implementation across multiple files, making it easier to manage large and complex classes.

To create a partial class, simply add the partial keyword to the class declaration in each file:

public partial class MyClass {
    public void Method1() {
        // implementation
    }
}
File1.cs
public partial class MyClass {
    public void Method2() {
        // implementation
    }
}
File2.cs

In this example, both files define the same class MyClass, but with different methods. When compiled, the two files will be merged into a single class definition with both Method1 and Method2 included:

public partial class MyClass {
    public void Method1() {
        // implementation
    }

    public void Method2() {
        // implementation
    }
}
Resulting code

Partial classes can be especially useful when working with code generated by tools or frameworks, as they allow developers to add their own customizations and extensions to the generated code without modifying the original source files.
Also, this works with partial methods.

Anonymous types

In C#, anonymous types are a way to create a new class with read-only properties on the fly, without explicitly defining a class. Anonymous types are useful for creating objects to store and manipulate data that is not intended to be reused in other parts of the program.

var person = new { Name = "John", Age = 30 };
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");

In Java anonymous classes is something similar yet different. They are a way to create a subclass or an implementation of an interface without explicitly defining a new class. Anonymous classes are useful when you need to create a one-time-use class to provide a specific implementation of a method or an interface.

interface MyInterface {
  void doSomething();
}

public class MyClass {
  public static void main(String[] args) {
    MyInterface obj = new MyInterface() {
      public void doSomething() {
        System.out.println("Doing something...");
      }
    };
    obj.doSomething();
  }
}

Either way, comparing different approaches to similar things in these languages helps us detect the design space limits and create better software, giving more economical output! Good.

Other

There are also parts of languages that weren't covered here:

This was mainly applause on C# side. But actually, it was the subjective reality of a C# dev that has encountered some Java once in a while. I hope this goes better over time as I promise to update the page every now and then when I'll get new input on Java's part.

Have a good time!