Lably
Skip to content

Comparison of C Sharp and Java

  • 55 min read
  • 12 chapters

Related on Lably

Through Java (programming language), Library (computing) and Object-oriented programming

C# and Java are similar languages that are typed statically, strongly, and manifestly. Both are object-oriented, and designed with semi-interpretation or runtime just-in-time compilation, and both are curly brace languages, like C and C++.

Types

Unified type system

Both languages are statically typed with class-based object orientation. In Java the primitive types are special in that they are not object-oriented and they could not have been defined using the language itself. They also do not share a common ancestor with reference types. The Java reference types all derive from a common root type. C# has a unified type system in which all types (besides unsafe pointers) ultimately derive from a common root type. Consequently, all types implement the methods of this root type, and extension methods defined for the object type apply to all types, even primitive int literals and delegates. This allows C#, unlike Java, to support objects with encapsulation that are not reference types.

In Java, compound types are synonymous with reference types; methods cannot be defined for a type unless it is also a class reference type. In C# the concepts of encapsulation and methods have been decoupled from the reference requirement so that a type can support methods and encapsulation without being a reference type. Only reference types support virtual methods and specialization, however.

Both languages support many built-in types that are copied and passed by value rather than by reference. Java calls these types primitive types, while they are called simple types in C#. The primitive/simple types typically have native support from the underlying processor architecture.

The C# simple types implement several interfaces and consequently offer many methods directly on instances of the types, even on the literals. The C# type names are also merely aliases for Common Language Runtime (CLR) types. The C# System.Int64 type is exactly the same type as the long type; the only difference is that the former is the canonical .NET name, while the latter is a C# alias for it.

Java does not offer methods directly on primitive types. Instead, methods that operate on primitive values are offered through companion primitive wrapper classes. A fixed set of such wrapper classes exist, each of which wraps one of the fixed set of primitive types. As an example, the Java java.lang.Long type is a reference type that wraps the primitive long type. They are not the same type, however.

Data types

Numeric types

Signed integers

Both Java and C# support signed integers with bit widths of 8, 16, 32 and 64 bits. They use the same name/aliases for the types, except for the 8-bit integer that is called a byte in Java and a sbyte (signed byte) in C#.

Unsigned integers

C# supports unsigned in addition to the signed integer types. The unsigned types are byte, ushort, uint and ulong for 8, 16, 32 and 64 bit widths, respectively. Unsigned arithmetic operating on the types are supported as well. For example, adding two unsigned integers (uints) still yields a uint as a result; not a long or signed integer.

Java does not feature unsigned integer types. In particular, Java lacks a primitive type for an unsigned byte. Instead, Java's byte type is sign extended, which is a common source of bugs and confusion.

Unsigned integers were left out of Java deliberately because James Gosling believed that programmers would not understand how unsigned arithmetic works.

In programming language design, one of the standard problems is that the language grows so complex that nobody can understand it. One of the little experiments I tried was asking people about the rules for unsigned arithmetic in C. It turns out nobody understands how unsigned arithmetic in C works. There are a few obvious things that people understand, but many people don't understand it.

Java versions 8 and 9 added some limited built-in unsigned integer operations, but they are only exposed as static methods on the primitive wrapper classes; they operate on signed primitive integer types, treating them as if they were unsigned.

High-precision decimal numbers

C# has a type and literal notation for high-precision (28 decimal digits) decimal arithmetic that is appropriate for financial and monetary calculations. Contrary to the float and double data types, decimal fractional numbers such as 0.1 can be represented exactly in the decimal representation. In the float and double representations, such numbers often have non-terminating binary expansions, making those representations more prone to round-off errors.

While Java lacks such a built-in type, the Java library does feature an arbitrary precision decimal type. This is not considered a language type and it does not support the usual arithmetic operators; rather it is a reference type that must be manipulated using the type methods. See more about arbitrary-size/precision numbers below.

Advanced numeric types

Both languages offer library-defined arbitrary-precision arithmetic types for arbitrary-size integers and decimal point calculations.

Only Java has a data type for arbitrary precision decimal point calculations. Only C# has a type for working with complex numbers.

In both languages, the number of operations that can be performed on the advanced numeric types is limited compared to the built-in IEEE 754 floating point types. For instance, none of the arbitrary-size types support square root or logarithms.

C# allows library-defined types to be integrated with existing types and operators by using custom implicit/explicit conversions and operator overloading. See example in section Integration of library-defined types

Characters

Both languages feature a native char (character) datatype as a simple type. Although the char type can be used with bit-wise operators, this is performed by promoting the char value to an integer value before the operation. Thus, the result of a bitwise operation is a numeric type, not a character, in both languages.

Built-in compound data types

Both languages treat strings as (immutable) objects of reference type. In both languages, the type contains several methods to manipulate strings, parse, format, etc. In both languages regular expressions are considered an external feature and are implemented in separate classes.

Both languages' libraries define classes for working with dates, times, time zones, and calendars in different cultures. Java provides java.util.Date, a mutable reference type with millisecond precision, and (since Java 8) the java.time package (including classes such as java.time.LocalDate, java.time.LocalTime, and java.time.LocalDateTime for date-only, time-only, and date-and-time values), a set of immutable reference types with nanosecond precision. In contrast, the C# System.DateTime is an immutable struct value type for date-and-time information with 100-nanosecond precision; the .NET 6 API also added System.DateOnly and System.TimeOnly, similar structures for date-only or time-only operations. C# also defines a System.TimeSpan type for working with time periods; Java 8 provides the java.time.Duration class for the same purpose. Both languages support date and time arithmetic according to different cultures and time zones.

User-defined value type (struct)

C# allows creating user-defined value types, using the struct keyword. Unlike classes and like the standard primitives, such value types are passed and assigned by value rather than by reference. They can also be part of an object (either as a field or boxed), or stored in an array without the memory indirection that normally exists for class types.

Because value types have no notion of a null value and can be used in arrays without initialization, they always come with an implicit default constructor that essentially fills the struct memory space with zeroes. Further constructors can be defined only with one or more arguments. Value types do not have virtual method tables, and because of that (and the fixed memory footprint), they are implicitly sealed. However, value types can (and frequently do) implement interfaces. For example, the built-in integer types implement several interfaces.

Apart from the built-in primitive types, Java does not include the concept of value types.

Enumerations

Both languages define enumerations, but they are implemented in fundamentally different ways. As such, enumerations are one area where tools designed to automatically translate code between the two languages (such as Java to C# converters) fail.

C# has implemented enumerations in a manner similar to C, that is as wrappers around the bit-flags implemented in primitive integral types (int, byte, short, etc.). This has performance benefits and improves interaction with C/C++ compiled code, but provides fewer features and can lead to bugs if low-level value types are directly cast to an enumeration type, as is allowed in the C# language. Therefore, it is seen as syntactic sugar. In contrast, Java implements enumerations as full featured collection of instances, requiring more memory and not aiding interaction with C/C++ code, but providing added features in reflection and intrinsic behavior. The implementation in each language is described in the table below.

In both C# and Java, programmers can use enumerations in a switch statement without conversion to a string or primitive integer type. However, C# disallows implicit fall-through unless the case statement does not contain any code, as it is a common cause of hard-to-find bugs. Fall-through must be explicitly declared using a goto statement.

Delegates, method references

C# implements object-oriented method pointers in the form of delegates. A delegate is a special type that can capture a reference to a method. This reference can then be stored in a delegate-type variable or passed to a method through a delegate parameter for later invocation. C# delegates support covariance and contravariance, and can hold a reference to any signature-compatible static method, instance method, anonymous method or lambda expression.

Delegates should not be confused with closures and inline functions. The concepts are related because a reference to a closure/inline function must be captured in a delegate reference to be useful at all. But a delegate does not always reference an inline function; it can also reference existing static or instance methods. Delegates form the basis of C# events, but should not be confused with those either.

Delegates were deliberately left out of Java because they were considered unnecessary and detrimental to the language, and because of potential performance issues. Instead, alternative mechanisms are used. The wrapper pattern, which resembles the delegates of C# in that it allows the client to access one or more client-defined methods through a known interface, is one such mechanism. Another is the use of adapter objects using inner classes, which the designers of Java argued are a better solution than bound method references.

See also example C# delegates and equivalent Java constructs.

Lifted (nullable) types

C# allows value/primitive/simple types to be "lifted" to allow the special null value in addition to the type's native values. A type is lifted by adding a ? suffix to the type name; this is equivalent to using the System.Nullable<T> generic type, where T is the type to be lifted. Conversions are implicitly defined to convert between values of the base and the lifted type. The lifted type can be compared against null or it can be tested for HasValue. Also, lifted operators are implicitly and automatically defined based on their non-lifted base, where – with the exception of some Boolean operators – a null argument will propagate to the result.

Java does not support type lifting as a concept, but all of the built-in primitive types have corresponding wrapper types, which do support the null value by virtue of being reference types (classes).

According to the Java spec, any attempt to dereference the null reference must result in an exception being thrown at run-time, specifically a java.lang.NullPointerException. (It would not make sense to dereference it otherwise, because, by definition, it points to no object in memory.) This also applies when attempting to unbox a variable of a wrapper type, which evaluates to null: the program will throw an exception, because there is no object to be unboxed – and thus no boxed value to take part in the subsequent computation.

The following example illustrates the different behavior. In C#, the lifted*operator propagates the null value of the operand; in Java, unboxing the null reference throws an exception.

Not all C# lifted operators have been defined to propagate null unconditionally, if one of the operands is null. Specifically, the Boolean operators have been lifted to support ternary logic thus keeping impedance with SQL.

The Java Boolean operators do not support ternary logic, nor is it implemented in the base class library.

Late-bound (dynamic) type

C# features a late bound dynamic type that supports no-reflection dynamic invocation, interoperability with dynamic languages, and ad-hoc binding to (for example) document object models. The dynamic type resolves member access dynamically at runtime as opposed to statically/virtual at compile time. The member lookup mechanism is extensible with traditional reflection as a fall-back mechanism.

There are several use cases for the dynamic type in C#:

  • Less verbose use of reflection: By casting an instance to the dynamic type, members such as properties, methods, events etc. can be directly invoked on the instance without using the reflection API directly.
  • Interoperability with dynamic languages: The dynamic type comes with a hub-and-spoke support for implementing dynamically typed objects and common runtime infrastructure for efficient member lookup.
  • Creating dynamic abstractions on the fly: For instance, a dynamic object could provide simpler access to document object models such as XML or XHTML documents.

Java does not support a late-bound type. The use cases for C# dynamic type have different corresponding constructs in Java:

  • For dynamic late-bound by-name invocation of preexisting types, reflection should be used.
  • For interoperability with dynamic languages, some form of interoperability API specific to that language must be used. The Java virtual machine platform does have multiple dynamic languages implemented on it, but there is no common standard for how to pass objects between languages. Usually this involves some form of reflection or reflection-like API. As an example of how to use JavaFX objects from Java.
  • To create and interact with objects entirely at runtime, e.g., interaction with a document object model abstraction, a specific abstraction API must be used.

See also example #Interoperability with dynamic languages.

Pointers

Java precludes pointers and pointer-arithmetic within the Java runtime environment. The Java language designers reasoned that pointers are one of the main features that enable programmers to put bugs in their code and chose not to support them. Java does not allow for directly passing and receiving objects/structures to/from the underlying operating system and thus does not need to model objects/structures to such a specific memory layout, layouts that frequently would involve pointers. Java's communication with the underlying operating system is instead based upon Java Native Interface (JNI) where communication with/adaptation to an underlying operating system is handled through an external glue layer. Java allows manipulation of pointers through an internal sun.misc.Unsafe class, but this is an internal API and has been slated for eventual removal.

While C# does allow use of pointers and corresponding pointer arithmetic, the C# language designers had the same concerns that pointers could potentially be used to bypass the strict rules for object access. Thus, C# by default also precludes pointers. However, because pointers are needed when calling many native functions, pointers are allowed in an explicit unsafe mode. Code blocks or methods that use the pointers must be marked with the unsafe keyword to be able to use pointers, and the compiler requires the /unsafe switch to allow compiling such code. Assemblies that are compiled using the /unsafe switch are marked as such and may only execute if explicitly trusted. This allows using pointers and pointer arithmetic to directly pass and receive objects to/from the operating system or other native APIs using the native memory layout for those objects while also isolating such potentially unsafe code in specifically trusted assemblies.

Reference types

In both languages references are a central concept. All instances of classes are by reference.

While not directly evident in the language syntax per se, both languages support the concept of weak references. An instance that is only referenced by weak references is eligible for garbage collection just as if there were no references at all. In both languages this feature is exposed through the associated libraries, even though it is really a core runtime feature.

Along with weak references, Java has soft references. They are much like weak references, but the Java virtual machine (JVM) will not deallocate softly-referenced objects until the memory is needed.

Arrays and collections

Arrays and collections are concepts featured by both languages.

The syntax used to declare and access arrays is identical, except that C# has added syntax for declaring and manipulating multidimensional arrays.

Multidimensional arrays can in some cases increase performance because of increased locality (as there is one pointer dereference instead of one for every dimension of the array, as it is the case for jagged arrays). However, since all array element access in a multidimensional array requires multiplication/shift between the two or more dimensions, this is an advantage only in very random access scenarios.

Another difference is that the entire multidimensional array can be allocated with a single application of operator new, while jagged arrays require loops and allocations for every dimension. However, Java provides a syntactic construct for allocating a jagged array with regular lengths; the loops and multiple allocations are then performed by the virtual machine and need not be explicit at the source level.

Both languages feature an extensive set of collection types that includes various ordered and unordered types of lists, maps/dictionaries, sets, etc.

Tuples

C# provides two different methods to create tuple types (also known as product types). The first is via the System.Tuple classes, which are immutable reference types provided by the framework API (starting with .NET Framework 4.0) to create generic tuple types. This first method has since been effectively superseded by the second, the System.ValueTuple structs, which are mutable value types provided by the framework API (starting with .NET Framework 4.7).

While the two methods seem superficially similar, they have multiple notable differences. The System.ValueTuple types are value types, so they have a more compact memory footprint; also, the System.ValueTuple types expose their contents as mutable fields, compared to the immutable properties of the System.Tuple classes. Finally, since C# version 7.0, the language has native syntactical support for the construction, deconstruction, and manipulation of tuples as System.ValueTuple instances; this also provides for the arbitrary renaming of the tuples' constituent fields (as opposed to System.Tuple, where the fields are always named Item1, Item2, etc.).

Java does not provide tuple types as part of its language or standard API; numerous third-party libraries exist which can provide tuple types, but they all are necessarily similar to the C# System.Tuple classes. In comparison to C# System.ValueTuple types and their associated syntax, they are more unwieldy to use (requiring the explicit use of constructors or static factory methods to create them, requiring individual member access to deconstruct them, and having fixed names for their elements).

Sign in to keep reading

11 more chapters are free with a Lably account.

More in Computing and communication