Language comparisons are most useful when they expose different design choices, not when they try to declare a winner. Java has several features that make certain modeling and one-off implementation tasks concise; C# either solves those tasks differently or leaves more ceremony to the developer.
This article examines four of those differences: class-based enums, covariant returns, functional interfaces, and anonymous interface implementations. It was written in September 2022, so treat the language-support details as a snapshot of that date and follow the linked proposals for later changes.
1. Class-Based Enums
Unlike Java, enums in C# and C++ are named numeric constants. That is no secret. What are enums in Java? In essence, they are syntactic sugar over a class. Let us write one that represents the token types recognized by a lexical analyzer:
enum TokenType {
IDENTIFIER,
NUMBER,
ASSIGN;
}
Because an enum is also a class, we can add a constructor, methods, fields, and even an interface implementation. Let us give each enum constant the ability to produce a regular expression:
interface ToPattern {
Pattern getPattern();
}
enum TokenType implements ToPattern {
IDENTIFIER("[a-zA-Z][a-zA-Z0-9]*"),
NUMBER("[0-9]+"),
ASSIGN("[=]");
private final String pattern;
private TokenType(String pattern) {
this.pattern = pattern;
}
@Override
public Pattern getPattern() {
return Pattern.compile(pattern);
}
}
How can we do something similar in C#? There are two options:
- Attributes and extension methods with reflection—but they cannot implement interfaces:
[AttributeUsage(AttributeTargets.Field)]
internal class PatternAttribute : Attribute
{
public string Pattern { get; }
public PatternAttribute(string pattern) =>
Pattern = pattern;
}
public enum TokenType
{
[Pattern("[a-zA-Z][a-zA-Z0-9]*")]
Identifier,
[Pattern("[0-9]+")]
Number,
[Pattern("[=]")]
Assign
}
public static class TokenTypeExtensions
{
public static Regex GetRegex(this TokenType tokenType) =>
new(typeof(TokenType)
.GetField(tokenType.ToString())!
.GetCustomAttribute<PatternAttribute>()!
.Pattern);
}
- Classes with public static constants:
interface IHasRegex
{
Regex Regex { get; }
}
class TokenType : IHasRegex
{
public static readonly TokenType Identifier =
new("[a-zA-Z][a-zA-Z0-9]*");
public static readonly TokenType Number =
new("[0-9]+");
public static readonly TokenType Assign =
new("[=]");
private readonly string _pattern;
private TokenType(string pattern) =>
_pattern = pattern;
public Regex Regex => new(_pattern);
}
That raises a question:
Why do I need C# enums if I can implement them the way Java does internally?
The question is especially relevant given the new capabilities added to the switch keyword in recent language versions.
2. Full Support for Covariant Return Types
Since C# 9, the language has supported covariant method return types. Previously, you might have written code like this:
abstract record Fruit;
record Apple : Fruit;
record Orange : Fruit;
abstract class FruitFactory<TFruit>
where TFruit : Fruit
{
public abstract TFruit Create();
}
class AppleFactory : FruitFactory<Apple>
{
public override Apple Create() => new();
}
class OrangeFactory : FruitFactory<Orange>
{
public override Orange Create() => new();
}
Now you can omit the extra constructs:
abstract class FruitFactory
{
public abstract Fruit Create();
}
class AppleFactory : FruitFactory
{
public override Apple Create() => new();
}
class OrangeFactory : FruitFactory
{
public override Orange Create() => new();
}
Java has supported this for almost its entire history, and the feature applies more broadly. It also works when implementing and extending interfaces. Suppose I define a structure that can be copied along with its data. I need to indicate that it is copyable, which is what the Cloneable contract does. By default, clone returns Object. To avoid littering the code with casts, I can declare that clone returns the type being copied:
class Tree<T> implements Cloneable {
private final Node<T> root;
public Tree(Node<T> root) {
this.root = root;
}
@Override
public Tree<T> clone() throws CloneNotSupportedException {
super.clone();
return new Tree<>(root.clone());
}
}
class Node<T> implements Iterable<Node<T>>, Cloneable {
private final T data;
private final List<Node<T>> children;
public Node(T data) {
this.data = data;
children = new ArrayList<>();
}
private void push(Node<T> node) {
children.add(node);
}
@Override
public Iterator<Node<T>> iterator() {
return new ArrayList<>(children).iterator();
}
@Override
public Node<T> clone() throws CloneNotSupportedException {
super.clone();
var node = new Node<>(data);
for (var child : this) {
node.push(child.clone());
}
return node;
}
}
You cannot do the same in C#. It produces an error:
Method 'Clone' cannot implement method from interface 'System.ICloneable'. Return type should be 'object'.
class Foo : ICloneable
{
public Foo Clone()
{
throw new NotImplementedException();
}
}
Why interfaces still lack covariant return types is an open question, even in the language specification discussion.
3. Functional Interfaces
Java has the concept of a functional interface. A functional interface is an interface with a single abstract method. Its key feature is that instances can be initialized with lambda expressions—since Java 8:
@FunctionalInterface
interface IntegerBinaryExpression {
int evaluate(int a, int b);
}
// ...
IntegerBinaryExpression add = (a, b) -> a + b;
System.out.println(add.evaluate(3, 5)); // 8
It is not hard to see why this works if you inspect what the IDE suggests as a replacement for the value assigned to the IntegerBinaryExpression variable named add:
IntelliJ IDEA
Accepting the suggested replacement gives us:
IntegerBinaryExpression add = Integer::sum;
Together with the double-colon syntax (::), this points to one conclusion: functional interfaces are Java's mechanism for implementing callbacks. C# has delegates, so the need for similar syntactic sugar is debatable. It does look convenient, however, especially for interfaces instantiated only once in a project.
4. Anonymous Interface Implementations
Perhaps the previous example belongs in this section because it demonstrates a special case of another Java feature I find excellent: anonymous interface implementations.
Consider a contract with at least two methods:
interface Pair<F, S> {
F first();
S second();
}
If we start typing new Pair to create an instance of this interface, the IDE does not report that abstract types cannot be instantiated. Instead, it offers to implement the methods:
var myPair = new Pair<String, Integer>() {
@Override
public String first() {
return "first";
}
@Override
public Integer second() {
return 2;
}
};
You can do the same with classes, whether abstract or not:
class Book {
public void read() {
// ...
}
}
// ...
var myBook = new Book() {
@Override
public void read() {
super.read();
}
};
This feature opens up new options when you want to avoid bloating the project structure and create contract implementations on the fly, or when you need to encapsulate a specific contract usage scenario. I would welcome it in C#; the CLR has everything it needs to support it. There is even a feature request in the Roslyn repository.
What C# Developers Can Learn from Java
None of these features should be copied into C# merely because Java has them. Delegates already cover much of the functional-interface use case, while richer enums and anonymous implementations would trade convenience for new language and tooling complexity.
The useful question for a C# codebase is narrower: when the language lacks a construct, does the local workaround remain explicit, navigable, and easy to debug? If it does not, understanding how another language models the same problem can lead to a better API even when C# never adopts the syntax.
Related C# Language Guides
Follow StepOne on GitHub for open-source C#/.NET projects, compiler experiments, production-focused examples, and new releases.

Top comments (0)