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
- Keywords
- Primitives
- Arithmetic
- Control Flow
- Arrays
- Strings
- Classes
- Inheritance
- Interfaces
- Nested Classes
- Lambda Expressions
- Enumerations
- Value Types
- Packages & Modules
- Exceptions
- Generics
- Collections
- Annotations
- 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 reservesconst
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 andyield 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 includenative
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 (
?.
) andnameof
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:
- 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.
- 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.
Dependency — requires
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.
Visibility — exports
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.