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–8 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.
- Control Flow
- Nested Classes
- Lambda Expressions
- Value Types
On the web, Oracle’s reference documentation comprises the Java Language and VM Specifications, the Java Platform SE 8 API Specification, the Java Platform Overview with developer guides, 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 & Cornell’s Core Java and Bloch’s Effective Java. Please see Java Books and .NET Books for further recommendations on both platforms. Andrei Rinea’s tutorial series, Beginning Java for .NET Developers, covers selected topics in more detail. Please also read Oracle Java on Windows to ensure you’re using the Server VM rather than the obsolete Client VM.
Java SE 8 — See Everything about Java 8 for examples of the most important changes, and Baeldung’s Java 8 for an exhaustive list of planned, implemented, and dropped features. Horstmann’s Core Java (8) for the Impatient is an excellent printed overview. Lastly, Brian Goetz’s Evolving Java is a one-hour presentation focused on the new lambda expressions.
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.
assertis equivalent to C#
Debug.Assertcalls. The assertion facility required a new language keyword because Java offers no other way to conditionally elide method calls. (Java SE 1.4)
classis equivalent to C#
typeofwhen used as the “class literal” after a type name (example).
finalis equivalent to C#
conston primitive or string variables holding compile-time constants; C#
readonlyon other fields; and C#
sealedon classes and methods. Java reserves
constbut does not use it.
instanceofis equivalent to C#
isfor 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.
nativeis equivalent to C#
externfor declaring external C functions.
stringis missing. You must always use the capitalized library type,
synchronizedis equivalent to C#
MethodImplOptions.Synchronizedas a method attribute, and to C#
lockaround a code block within a method.
transientis equivalent to C#
NonSerializableAttributefor exempting fields from serialization.
Object... args(three periods) is equivalent to C#
params, and also implicitly creates an array from all listed arguments. (Java SE 5)
Java entirely lacks the following C# keywords and functionality:
#if/#define/#pragmaand conditional compilation. Workarounds include dynamic class loading and external preprocessors or other build tools.
#regionblocks have no equivalent, but Java IDEs of course allow syntactic code folding.
async/awaitfor asynchronous callbacks and
yield break/returnfor enumerators. You must write the required state machines by hand.
dynamicfor dynamic typing at runtime. Java SE 7 provides the invokedynamic JVM instruction but does not expose it in Java.
eventand 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/unsafeand pointer operations. Workarounds include
nativemethods and platform-specific libraries.
get/setfor field-like properties. Use traditional method syntax with corresponding prefixes, as standardized by the JavaBeans pattern.
operator/explicit/implicitand operator overloading, including custom indexers and conversion operators. See special notes on string operators.
partialclasses and methods. Each class and non-abstract method is fully defined in one file.
ref/outand 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.
varfor implicit typing (currently planned). Java does feature type inference for lambda expressions and for generic type arguments, including diamond notation for generic constructors. (Java SE 7/8)
- 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 (
nameofoperator, 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 standard library 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.
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.
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.
decimalis 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#,
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 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 adds 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
goto but does not define it. However, statement labels do exist, and in a strange twist
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#
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
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 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.
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.
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
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.
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.
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.
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.
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. 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
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
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.
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. Currently planned for Java SE 10, 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.
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.
Java packages are largely equivalent to C# namespaces, with some important differences. Note that Java 9 will introduce a new module system with better control over visibility and dependencies. Mark Reinhold describes it in State of the Module System, and Dustin Marx has summarized the highlights.
Storage — 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.
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.
import is equivalent to C#
using for namespace imports, but always references 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.
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.
There is no dedicated keyword for the default visibility level, so it’s implied if neither
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 has no concept of “friend” access (
InternalsVisibleTo attribute) that provides elevated visibility to specific other packages or classes.
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
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 (
return) classify as “abrupt completion” in the same sense as
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 here for examples and further information. Enable the compiler option
-Xlint:finally to check for this pitfall.
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 for Java 10 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
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 — Java does not allow static fields of any generic type. After type erasure, only a single field typed
Object would remain for all type instantiations of the generic class. Non-generic static fields are shared among all type instantiations. This is the polar opposite of C# which allocates new storage for all static fields, even non-generic ones, for each type instantiation of a generic class.
Wildcards — Any generic type parameter that is never referred to may be specified as
?, a so-called wildcard. Wildcards allow generic parameters in non-generic methods, and they can also be bounded with
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.
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 publicly exposed 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.
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#
overridewhich 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.
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.