Java for C# Programmers

This page contains an overview of the differences between the Java and C# programming languages, from the perspective of a C# programmer who is new to Java. The comparison is not encyclopedic but rather highlights some fundamental points that are potentially troublesome or otherwise remarkable. New features introduced in the major revisions Java SE 5–11 are noted where appropriate.

Since both languages are closely tied to their respective runtimes, I’ll also cover any relevant differences between the Java Virtual Machine (JVM) and the .NET Framework, as well as basic library classes. I don’t cover the library frameworks for networking, serialization, GUI, XML, and so on. The capabilities of both platforms are broadly equivalent in these respects, but the implementations are quite different.

C# 7 and later — After the release of C# 6, Microsoft moved the rapidly evolving language to Github and apparently no longer bothers to write proper specifications. Some of the new features are directly copied from Java (e.g. digit separators) but it’s generally safe to assume that C# 7+ features won’t be available in Java. The rest of this page only covers C# up to version 6.

Further Reading

See Compiling Java Code for a quick primer on how to compile and run Java programs.

Oracle’s JDK 11 Documentation comprises all reference material, including the Java Language and VM Specifications, the Java Platform SE 11 API Specification, and the Java Tutorials. For a thorough performance analysis of Java language and library elements, see Mikhail Vorontsov’s Java Performance Tuning Guide.

The best printed introductions are Horstmann’s Core Java and Bloch’s Effective Java, as well as Horstmann’s Core Java for the Impatient as a quick overview. Please see Java Books for further recommendations. Andrei Rinea’s tutorial series, Beginning Java for .NET Developers, covers selected topics in more detail.

Article Contents

  1. Keywords
  2. Primitives
  3. Arithmetic
  4. Control Flow
  5. Arrays
  6. Strings
  7. Classes
  8. Inheritance
  9. Interfaces
  10. Nested Classes
  11. Lambda Expressions
  12. Enumerations
  13. Value Types
  14. Packages & Modules
  15. Exceptions
  16. Generics
  17. Collections
  18. Annotations
  19. Comments

1. Keywords

Java and C# use a very similar syntax and provide similar sets of keywords, both derived from C/C++. Here I’ll briefly list some noteworthy differences that aren’t mentioned in the following sections.

  • assert is equivalent to C# Debug.Assert calls. The assertion facility required a new language keyword because Java offers no other way to conditionally elide method calls. See Exceptions for details on catching assertion failures. (Java SE 1.4)
  • class is equivalent to C# typeof when used as the “class literal” after a type name (example).
  • final is equivalent to C# const on primitive or string variables holding compile-time constants; C# readonly on other fields; and C# sealed on classes and methods. Java reserves const but does not use it.
  • instanceof is equivalent to C# is for checking an object’s runtime type. There is no equivalent to C# as, so you must always use an explicit cast after a successful type check.
  • native is equivalent to C# extern for declaring external C functions.
  • C# string is missing. You must always use the capitalized library type, String.
  • synchronized is equivalent to C# MethodImplOptions.Synchronized as a method attribute, and to C# lock around a code block within a method.
  • transient is equivalent to C# NonSerializableAttribute for exempting fields from serialization.
  • var declares implicitly typed local variables as in C#. (Java SE 10, but see below)
  • Object... args (three periods) is equivalent to C# params, and also implicitly creates an array from all listed arguments. (Java SE 5)

Java versions prior to SE 10 lacked var but offered type inference for lambda expressions (Java SE 8) and for generic type arguments, including diamond notation for generic constructors. Do not combine var with generic type inference, or the omitted type variables may be inferred as Object! See Stuart W. Marks’ style guidelines for more information.

Missing Keywords

Java entirely lacks the following C# keywords and functionality:

  • #if/#define/#pragma and conditional compilation. Workarounds include dynamic class loading and external preprocessors or other build tools.
  • #region blocks have no equivalent, but Java IDEs of course allow syntactic code folding.
  • async/await for asynchronous callbacks and yield break/return for enumerators. You must write the required state machines by hand.
  • dynamic for dynamic typing at runtime. Java SE 7 provides the invokedynamic JVM instruction but does not expose it in Java.
  • event and implicit code generation for events. You must write the entire event infrastructure by hand, as demonstrated in From C# To Java: Events (but see below).
  • fixed/sizeof/stackalloc/unsafe and pointer operations. Workarounds include native methods and platform-specific libraries.
  • get/set for field-like properties. Use traditional method syntax with corresponding prefixes, as standardized by the JavaBeans pattern.
  • operator/explicit/implicit and operator overloading, including custom indexers and conversion operators. See special notes on string operators.
  • partial classes and methods. Each class and non-abstract method is fully defined in one file.
  • ref/out and call-by-reference. The only way to pass method parameters by reference is to wrap them in another object, for example a one-element array.
  • Named and optional method parameters. Again, you must use object wrappers to set parameters by name or to provide default parameter values.
  • Object initializers that set members by name, and index initializers that set elements by index. You can use the rather ugly double brace initialization idiom to simulate them.
  • Any of these new features in C# 6: null-conditional (?.) and nameof operator, expression-bodied functions, exception filters (catch…when), and string interpolation ($).

As an alternative to writing your own event infrastructure, consider using the “observable” objects defined in the JavaFX package javafx.beans and its sub-packages. Such objects let you attach listeners that are notified on value changes – often the intended use of events. (Note: JavaFX has become a separate download as of Java SE 11 and is now available here.)

Java has no equivalent to LINQ keywords. As of Java SE 8, lambda expressions and stream libraries replicate the method-based incarnation of LINQ to Objects, complete with lazy and/or parallel evaluation. Third-party libraries for LINQ-like queries in Java include iciql, Jinq, jOOQ, and QueryDSL.

2. Primitives

Java and C# have equivalent sets of primitive types (int etc.), with the following exceptions:

  • Java only has signed numeric types, including byte. All unsigned variants are missing. Java SE 8 added unsigned operations as library methods.
  • C# decimal is similar to the Java class BigDecimal which has no corresponding primitive.
  • All Java primitives have equivalent library classes. However, those are not synonyms as in C# but rather boxed versions. This is because Java does not support value types in its class hierarchy.
  • For the same reason, Java has no nullable variants of primitives. Simply use the equivalent library classes – they are all reference types and therefore nullable.
  • In addition to decimal, hexadecimal, and octal literals for numeric values, Java SE 7 added binary literals and underscore literals.

Note the distinction between primitive-equivalent classes in C# and Java. In C#, int and System.Integer are synonyms: both represent an unboxed primitive value type. You need an (Object) cast to explicitly wrap a reference around such values.

But in Java, only int is an unboxed primitive value whereas java.lang.Integer represents the strongly-typed boxed version! C# programmers must take care not to use Java primitives and the corresponding class names interchangeably. See autoboxing for more details. (Java SE 5)

Java automatically unboxes primitive values from their strongly-typed wrapper objects as needed within the context of assignments, mathematical operations, or method invocations. Explicit casts to primitive types are only required when unboxing from a more general class (Number, Object).

3. Arithmetic

Java lacks C# checked/unchecked to toggle overflow checking for arithmetic expressions. Instead, integral overflow silently truncates bits as in C/C++, and floating-point overflow produces negative or positive infinity. Floating-point 0/0 produces NaN (not a number). Only integral division by zero throws an ArithmeticException. Java SE 8 added various …Exact methods for arithmetic operations and type conversions to java.lang.Math that always throw an exception on overflow.

Java provides the modifier strictfp that can be applied to classes, interfaces, and methods. It forces all intermediate results of floating-point arithmetic to be truncated to IEEE 754 sizes, for reproducible results across platforms. The library class java.lang.StrictMath also defines a set of standard functions with portable behavior, based on fdlibm.

4. Control Flow

Java reserves goto but does not define it. However, statement labels do exist, and in a strange twist break and continue were enhanced to accept them. You can only jump out of local blocks, but break operates on any block – not just loops. This makes Java break almost the full equivalent of C# goto.

Java switch operates on (boxed or unboxed) primitives and enums, and since Java SE 7 also on strings. You don’t need to qualify enum case values with the enum type as in C#. Java allows fall-through from one case to the next, just like C/C++. Use the compiler option -Xlint:fallthrough to warn against missing break statements. There is no equivalent to C# goto targeting a case label.

5. Arrays

As in .NET, Java arrays are specialized reference types with automatic index checking. Unlike .NET, Java supports only one array dimension directly. Multi-dimensional arrays are simulated by nesting one-dimensional arrays. However, when all dimensions are specified during initialization, all required nested arrays are allocated implicitly. For example, new int[3][6] allocates both the outer array and all three nested arrays of six integers, without the need for repetitive new statements. Any number of rightmost dimensions can be left unspecified to later manually create a ragged array.

Much array-related functionality is provided by the helper class Arrays: comparison, conversion, copying, filling, hash code generation, partitioning, sorting & searching, and output as a human-readable string which the instance method Array.toString notably doesn’t do. Java SE 8 added several parallel… methods that perform multithreaded operations if possible.

6. Strings

As in C#, Java strings are immutable sequences of UTF-16 code units which each fit into one 16-bit char primitive. For strings containing any 32-bit Unicode code points (i.e. real-world characters) that require surrogate pairs of two UTF-16 code units, you cannot use the ordinary char indexer methods. Instead, use various helper methods for code point indexing which Java defines directly on the String class.

Java SE 9 introduced a compact representation for strings containing only ISO-8859-1 (Latin-1) characters. Such strings use 8 rather than 16 bits per character. This is an automatic and purely internal optimization that does not affect public APIs.

Operators — Unlike C#, Java does not special-case the == operator for strings, so this will only test for reference equality. Use String.equals or String.equalsIgnoreCase to test for content equality.

Java does special-case the + operator for string concatenation (JLS §15.18.1). Annoyingly, this performs JavaScript-like automatic conversion of all types to String. As soon as one String operand is found, any existing intermediate sum to the left and any remaining individual operands to the right are converted to String and concatenated as such. As in C#, piecemal string concatenation can also be inefficient. Use the dedicated StringBuilder class for better performance.

7. Classes

Java lacks C# static classes. To create a class that only contains static utility methods, use the old-fashioned approach of defining a private default constructor. Java does feature a static class modifier, but only for nested classes and with very different semantics.

Class Objects contains some simple but useful helper methods for objects of any type, including hash code generation for multiple objects and various null-safe operations.

Construction — Constructor chaining uses this(…) as in C# and super(…) for base classes, but these calls appear as the first line in the constructor body rather than before the opening brace.

Java lacks static constructors but offers anonymous initializer blocks in both static and instance variants. Multiple initializer blocks are acceptable, and will be executed in the order in which they appear before any constructor runs. Static initializer blocks are executed when the class is first loaded.

Destruction — Java supports finalizers that run before the garbage collector destroys an object, but these are rather sensibly called finalize instead of C#’s misleading C++ destructor syntax. Finalizer behavior is complex and problematic, so you should generally prefer try/finally cleanup.

Java provides not only weak references like C# that may be collected at any time, but also soft references that are only collected in response to memory pressure.

8. Inheritance

Base classes are called superclasses and accordingly referenced with the keyword super rather than C# base. When declaring derived classes, Java extends (for superclasses) and implements (for interfaces) specify the same inheritance relationships for which C# uses simple colons.

C# virtual is missing entirely because all Java methods are virtual unless explicitly declared final. There is little performance penalty because the JVM Hotspot optimizer, unlike the rather stupid .NET CLR optimizer, can dynamically inline virtual methods when no override is detected at runtime.

Java SE 5 added covariant return types to support its type-erasing generics. Covariance is achieved through compiler-generated bridge methods, so watch out for versioning issues with subclasses.

9. Interfaces

Like C#, Java supports single class inheritance and multiple interface inheritance. Interface names are not prefixed with I as in .NET, nor in any other way distinguished from class names.

Java does not support C# extension methods to externally attach implementation to an interface (or class), but it does allow implementation within an interface. Java interfaces may contain constants, i.e. public static final fields. The field values may be complex expressions which are evaluated when the interface is first loaded.

Java SE 8 added static and default methods on interfaces, and Java SE 9 private methods as well. Default methods are used in the absence of a normal class implementation. This obviates the need for abstract default implementing classes, and also allows extending interfaces without breaking existing clients.

Java does not support C# explicit interface implementation to hide interface-mandated methods from public view. This is probably a good thing since the access semantics for explicit interface implementations are notoriously error-prone when multiple classes are involved.

10. Nested Classes

Use the static modifier to define nested classes that behave the same way as in C#. Nested classes without that modifier are Java’s special inner classes. These may also appear locally within a method, either with a dedicated class name or anonymously.

Inner Classes — Non-static nested classes are inner classes which carry an implicit reference to the outer class instance that created them, similar to the implicit this reference of instance methods. You can also use outerObj.new InnerClass() to associate a specific outer class instance. The inner class can access all private fields and methods of its outer class, optionally using the prefix OuterClass.this for disambiguation. The static modifier on a nested class prevents this implicit association.

Local Classes — Inner classes may appear as local classes within methods. In addition to all private members of the implicitly associated outer class instance, local classes also have access to all local variables that are in scope within the declaring method, so long as they are effectively final.

Anonymous Classes — Local classes may be declared as single-use instances, with an initializer expression that specifies a superclass or interface. The compiler internally generates a class with a hidden name that extends the superclass or implements the interface. This practice is known as anonymous classes.

Until Java SE 8, anonymous classes served as Java’s equivalent to lambda expressions, although more powerful since they may contain most ordinary class members. Constructors are disallowed – use initializer blocks instead. (This feature can be abused for the double brace initialization idiom.)

Java’s version of functional programming ultimately relies on anonymous classes. Consequently, interfaces that define only a single method are called functional interfaces, and anonymous classes that implement them are called function objects.

11. Lambda Expressions

Java SE 8 added lambda expressions as an alternative to function objects, syntactically more concise and with a faster internal implementation. The syntax is identical to C#, except with -> instead of => as the function arrow. Argument types are inferred if absent.

Java predefines basic functional interfaces in java.util.function and elsewhere. Unfortunately, due to Java’s type-erasing generics and its lack of value types, the predefined types are both uglier and less comprehensive than .NET’s delegate library. Edwin Dalorzo explains the details, and also warns about possible conflicts with checked exceptions.

Since lambda expressions are semantically equivalent to anonymous classes, they are implicitly typed as whatever interface the caller requires, e.g. Comparator<T>. You can use functional interface types to store lambda expressions in variables, just as with C# delegate types. Moreover, lambda expressions can access any local variables in outer scopes that are effectively final which somewhat surprisingly includes for-each loop variables.

Method References — Instead of defining a lambda expression where a function object is expected, you can also supply a method reference to any existing static or instance method. Use a double colon (::) to separate class or instance name from method name. This syntax can also reference superclass methods as super::method and constructors as ClassName::new. Passing typed array constructors such as int[]::new to generic methods allows creating arrays of any desired type.

While very convenient, method references to instance methods are evaluated somewhat differently from equivalent lambda expressions which can lead to surprising behavior. See Java Method Reference Evaluation for examples.

12. Enumerations

Java SE 5 introduced type-safe enums as an alternative to loose integer constants. Unlike C# enum types, Java enums are full-fledged reference types. Each enumerated constant represents one named instance of the type. Users cannot create any new instances aside from the enumerated ones, ensuring that the stated list of constants is final. This unique implementation has two important consequences:

  1. Java enum variables can be null and default to null. This means you don’t have to define a separate “no value” constant, but you must perform null checking if you do require a valid enum value.
  2. Java enum types support arbitrary fields, methods, and constructors. This lets you associate arbitrary data and functionality with each enum value, without requiring external helper classes. (Each enum value is an anonymous subclass instance in this case.)

Two specialized collections, EnumMap and EnumSet, provide high-performance subsets of enum values with or without associated data. Use EnumSet when you would use a C# enum with the [Flags] attribute. The internal implementation is in fact identical, namely a bit vector.

13. Value Types

One significant defect of Java is the lack of user-defined value types. When released in an undecided future version, Project Valhalla should offer .NET-style value types with generics support – see the proposals State of the Values and Minimal Value Types for more details. Right now, the only value types offered by Java are its primitives which live completely outside the class hierarchy. This section briefly describes the impact on semantics, performance, and generics.

Semantics — Value types have two important semantic properties: they cannot be null (i.e. have “no value”), and their entire contents are copied on each assignment, making all copies independent of each other in terms of future mutations. The first property is currently impossible to achieve for user-defined types in Java, and can only be approximated by frequent null checking.

Surprisingly, the second property doesn’t matter because value types should be immutable anyway, as Microsoft discovered the hard way. Value types in .NET are mutable by default, and that caused no end of obscure bugs due to implicit copying operations. Now the standard recommendation is to make all value types immutable, and that’s also true for value-like Java classes such as BigDecimal. But once an object is immutable the theoretical effects of mutation are irrelevant.

Performance — Value types store their contents directly on the stack or embedded within other objects, without a reference or other metadata. This means they require far less memory, assuming the contents aren’t much bigger than the metadata. Moreover, the garbage collector’s workload is reduced, and no dereferencing step is needed to access the contents.

Oracle’s Server VM is quite adept at optimizing small objects that C# would implement as value types, so there’s no big difference in computational performance. However, the extra metadata inevitably bloats large collections of small objects. You need complex wrapper classes to work around this problem, see e.g. Compact Off-Heap Structures/Tuples In Java.

Generics — As outlined in Project Valhalla: Goals, the fact that primitives are not classes means they cannot appear as generic type arguments. You must use equivalent class wrappers instead (e.g. Integer for int), resulting in expensive boxing operations. The only way to avoid this is hard-coding variants of generic classes that are specialized for primitive type arguments. The Java library is littered with such specializations. There is currently no better solution for this, until Project Valhalla delivers value types that integrate primitives into the class hierarchy.

14. Packages & Modules

Java packages are largely equivalent to C# namespaces, with some important differences. As of Java SE 9, modules provide additional features for dependency checking and access control.

Storage Format

The Java class loader expects a directory structure that replicates the declared package structure. Fortunately, the Java compiler can automatically create that structure (-d .). Moreover, each source file can only contain one public class and must have the name of that class, including exact capitalization.

These restrictions have an unexpected benefit: the Java compiler has an integrated “mini-make” facility. Since the locations and names of all object files are exactly prescribed, the compiler can check automatically which files need updating and recompiles only those.

For distribution, the entire directory structure of compiled Java class files along with metadata and any required resources is usually placed in a Java archive (JAR). Technically, this is simply an ordinary ZIP file. The extension .jar is the same for both executables and libraries; the former are marked internally as having a main class.

Unlike .NET assemblies, JAR files are completely optional and have no semantic significance. All access control is achieved through package and (to a greater degree) module declarations. In this regard, Java packages and modules combine the functionality of .NET namespaces and assemblies.

Packages

Java packages are the basic way to organize classes. They are not quite expressive enough for large projects, such as the JDK itself, which led to the development of a new module system for Java SE 9. Non-modular packages continue to be supported, however, and should be sufficient for smaller applications.

Declaration — A Java package statement is equivalent to a C# namespace block, but implicitly applies to the entire source file. This means you cannot mix packages in a single source file, but it does remove one pointless level of indentation compared to the C# format.

Java import is equivalent to C# using for namespace imports, but always references individual classes. Use .* to import all classes in a package. The form import static is equivalent to using static (C# 6) and allows using static class members without qualification (Java SE 5). There is no class name aliasing, however.

Storage — The directory holding a package’s source code may contain an optional file package-info.java that is only used for documentation. In a non-modular application, directory trees for the same package can occur multiple times in different sub-projects. The contents of all visible occurrences are merged.

Visibility — The default visibility for classes, methods, and fields is package-internal. This is roughly equivalent to C# internal but refers to declared packages (C# namespaces), not to physical deployment units (JAR files or .NET assemblies). Outside code can therefore gain access to all default-visible objects in a deployment unit, simply by declaring itself part of the same package. You must explicitly seal your JAR files if you wish to prevent this, or else use modules (Java SE 9).

There is no dedicated keyword for the default visibility level, so it’s implied if neither public, private, nor protected is present. C# programmers must take special care to mark private fields as private to avoid this default! Moreover, Java protected is equivalent to C# internal protected, i.e. visible to derived classes and to all classes in the same package. You cannot restrict visibility to subclasses only.

Finally, Java packages have no concept of “friend” access (InternalsVisibleTo attribute) that provides elevated visibility to specific other packages or classes. Package members that should be visible to any other package must be public or protected.

Modules

Java SE 9 introduced modules that combine any number of packages with explicit dependency and visibility declarations. Technically all code runs in modules now, but for backward compatibility any non-modular code is treated like a module that depends on all present modules and exports all its packages.

Oracle does not currently provide concise documentation for modules. You can dig through Mark Reinhold’s link-studded announcement, consult chapter 7 of the Java Language Specification, or buy Cay Horstmann’s Core Java 9 for the Impatient. The following overview is non-exhaustive.

Declaration & Storage — Each module corresponds to one single directory with the (arbitrary) module name, containing the file module-info.java and subdirectory trees for all contained packages. The packages are declared as usual. All module declarations reside in module-info.java, using special Java keywords valid only there.

Dependencyrequires declares any modules required by the current module. Optionally, transitive makes a required module an implicit requirement of anyone using the current module. Non-required modules are unavailable to the current module, even if they are present on the module path.

Visibilityexports declares any packages exported for use, and opens declares any packages open to external reflection. Optionally, exports/opens can restrict visibility to a given list of named modules. Any packages not made visible are inaccessible to other modules. Thus, public members of non-exported packages are equivalent to C# internal members.

While modules may have the same name as packages, all module names and all visible package names within an application must be unique. It is therefore not possible to augment a package declared in another module, fixing a strange loophole of Java packages.

15. Exceptions

Java is somewhat infamous for its checked exceptions, i.e. exception types that must be specified in a throws clause if a method throws but does not catch them. The value of checked exceptions has long been debated, on the grounds of programmer psychology (compiler errors are silenced by swallowing exceptions, which is worse than not handling them) and component interaction.

For example, meaningless throws clauses may proliferate through worst-case scenarios, or else the exceptions are handled at inappropriate locations to stop that proliferation. Anders Hejlsberg famously rejected checked exceptions when designing C#. Some programmers simply avoid them altogether by wrapping checked exceptions in unchecked ones, although Oracle is not fond of that practice.

Conceptually, however, checked exceptions are quite simple: all exceptions are checked unless derived from Error (serious internal errors) or RuntimeException (typically programming errors). The usual suspects are I/O errors that are expected during normal operation and must be handled accordingly.

Otherwise, Java exception handling is very similar to C#. Java SE 7 added multiple exception types in one catch block, and a try version that replicates C# using. The try-with-resources statement relies on the (Auto)Closeable interface, just like C# using relies on IDisposable. Java SE 9 allows try-with-resources to use effectively final variables, too.

Assertion Errors — Java’s base class for all runtime errors is not Exception as in .NET but rather Throwable from which both Exception and Error derive. Unfortunately Java’s AssertionError, thrown by assert failures, is an Error rather than an Exception. So if you wish to handle assertion errors along with exceptions, for example on a background thread, you must catch Throwable rather than Exception. See Catching Java Assertion Errors for details and links.

Jumps and finally — As in C#, exceptions thrown in finally clauses discard previously thrown exceptions in the associated try blocks. Unlike C#, which forbids jumping out of finally, simply returning from a Java finally clause also discards all previous exceptions! The reason for this surprising behavior is that all jump statements (break, continue, return) classify as “abrupt completion” in the same sense as throw. The finally clause’s abrupt completion discards the try block’s previous abrupt completion.

An even weirder consequence, although less likely to occur in practice, is that jumping out of finally overrides ordinary return statements in the associated try block. See Jumping out of Java finally for examples and further information. Enable the compiler option -Xlint:finally to check for this pitfall.

16. Generics

Java SE 5 introduced generics two years before Microsoft added them to .NET 2. While both versions look similar in source code, the underlying implementations are quite different. To ensure maximum backward compatibility, Sun opted for type erasure that eliminates type variables at runtime and replaces all generic types with non-generic equivalents. This does allow seamless interoperation with legacy code, including precompiled byte code, but at a huge cost to new development.

C# generics are simple, efficient, and nearly foolproof. Java generics resemble C++ templates in their tendency to generate incomprehensible compiler errors, yet don’t even support unboxed primitives as type arguments! If you want an efficient resizable integer collection in Java, you cannot use any implementation of List<T> etc. because that would force wasteful boxing on all elements.

Instead, you must define your own non-generic collection that hard-codes int as the element type, just as in the bad old days of plain C or .NET 1. (Of course, you could also use one of several third-party libraries.) Primitives in generics are planned as part of Project Valhalla – see Value Types above and Ivan St. Ivanov’s article series Primitives in Generics (part 2, part 3).

Rather than attempting to explain the complex differences between Java and C# generics, I point you to the sources cited above, and to Angelika Langer’s extremely comprehensive Java Generics FAQ. In the rest of this section I’ll cover just a few noteworthy points.

Construction — Java lacks the C# new constraint, but nonetheless allows instantiation of generic type arguments with the class literal trick. Supply the class literal Class<T> c for the desired type argument T to a method, then within the method use c.newInstance() to create a new instance of type T.

As of Java 8, you can also use method references to constructors which were introduced along with lambda expressions and are described in that section.

Static Fields — Static fields are shared among all type instantiations of a generic class. This is a consequence of type erasure which collapses all different instantiations into a single runtime class. C# does the opposite and allocates new storage for all static fields of each generic type instantiation.

Java disallows static fields and methods from using any generic type variables. Type erasure would produce a single field or method using Object (or some more specific non-generic type) on the shared runtime class. Due to type erasure, only instance fields and methods can be type-safe for distinct type arguments from different type instantiations.

Type Bounds — Optional bounds for generic type variables (JLS §4.4) are similar to C# but with a different syntax. Bounds consist of one principal type (class or interface) and zero ore more interface bounds, appended with &. For example, <T extends C & I> is equivalent to C# <T> where T: C, I. This ensures that the actual type of T is some subtype of C that also implements interface I which C itself does not implement. Interestingly, Java also allows interface bounds in cast expressions (JLS §15.16).

Void — Just as you cannot specify primitives as generic type arguments, you also cannot specify the keyword void. Use the Void class for any type parameter of a generic interface that your implementing class does not use.

Wildcards — Any generic type parameter that is never referred to may be simply specified as ?, a so-called wildcard. Wildcards can also be bounded with extends or super. This allows co- and contravariance like C# in/out but is not limited to interfaces. To reference a wildcarded type argument, capture it with a separate method declaring a named type parameter.

There’s one neat trick related to wildcards. If a container holds some generic element type with a wildcard, e.g. the collection returned by TableView<S>.getColumns, then you can put instances with different concrete types for the wildcard in the same container. This is not possible in C# where different concrete type arguments produce incompatible classes.

17. Collections

The Java Collections Framework (tutorial) is much better designed than its .NET equivalent. Collection variables are usually typed from a rich hierarchy of interfaces. These are nearly as powerful as their implementing classes, so the latter are only used privately for instantiation. Hence, most collection algorithms work on any framework-conforming collection with appropriate semantics. This includes a variety of composable wrapper methods, such as dynamic subranges and read-only views.

Some combinations of interface methods and concrete implementations may perform poorly, e.g. indexing a linked list. Java prefers exposing a possibly slow operation to not exposing the operation at all, which is generally the case with .NET’s more restrictive collection interfaces.

Iterators — Java allows mutating a collection while iterating over its elements, but only through the current iterator. Java also features a specialized ListIterator that can return its element index. .NET allows neither mutation nor index retrieval when using a collection iterator.

Java SE 5 added a for-each loop equivalent to the C# foreach statement, but without a dedicated keyword. This loop does not expose the mutation and index retrieval facilities of Java collection iterators. As in C#, for-each loops over arrays are special-cased to avoid creating iterator objects.

Streams — Java SE 8 added streams & pipelines that chain method calls for cumulative operations, like the method-based version of C# LINQ. Streams can be created from regular collections, but also from generator functions or external files. Pipelines fetch new elements only as needed, and can process them either sequentially or in parallel. Lambda expressions are used to customize pipeline operations. Finally, a terminal operation converts the result into another regular Java object or collection.

18. Annotations

Java SE 5 introduced annotations which are roughly equivalent to .NET attributes. Annotations tag program elements with arbitrary metadata, for later extraction by library methods or programming tools. Aside from syntactic differences, here are some noteworthy points for C# developers:

  • Annotations cannot change the semantics of the annotated program element. In particular, they cannot completely suppress method calls, like [Conditional("DEBUG")] on .NET assertions.
  • @FunctionalInterface verifies that an interface contains only a single method, and so can be implemented by lambda expressions or method references.
  • @Override replaces C# override which is inexplicably missing from the Java language.
  • @SuppressWarnings and the specific form @SafeVarargs are equivalent to C# #pragma warning. They are often needed in conjunction with Java’s type-erasing generics.

Java SE 8 allows annotating type usage in addition to type declarations. You need external tools to benefit from such annotations, though.

19. Comments

Like C#, Java defines a standard format for code comments on classes and methods that can be extracted as formatted HTML pages. Unlike C#, the Javadoc processor that ships with the JDK directly performs output formatting, so you don’t need an external formatter such as NDoc or Sandcastle.

The capabilities are similar, although Javadoc lacks a compiler-checked way to reference parameters within comment text. The syntax is quite different and much more concise, as Javadoc mostly relies on implicit formatting and compact @ tags. HTML tags are used only where no appropriate @ tag exists.

If you need to convert large amounts of C# XML comments to Javadoc format you should check out my Comment Converter that does most of the mechanical translation for you.

Summaries — By default, the first sentence of a Javadoc comment is automatically treated as its summary. Java SE 10 introduced the tag {@summary … } to explicitly define the summary, equivalent to the <summary> element of C# XML comments.