Vergleich mit Java


... [ Seminar WS 2002/03 ] ... [ C# ] ... [ Überblick Spracheigenschaften ] ... [ Vergleich mit Java ] ... [ Fazit ]

Vergleich mit Java

"Microsoft has spoken. - Is anyone really listening?" [Sun Microsystems(8)]


Gemeinsamkeiten

C# und Java haben viele Gemeinsamkeiten, teilweise sind sich die Sprachen derart ähnlich, dass man schon ein zweites Mal auf den Programmcode schauen muss, um die Sprachen unterscheiden zu können. Jedoch sind unter C# zusätzliche Konzepte hinzu gekommen und so kommt es zu durchaus wesentlichen Unterschieden zwischen den Sprachen. Ich will hier zunächst die Gemeinsamkeiten auflisten, um später die Unterschiede aufzeigen zu können.


Properties und Indexer

Properties sind bekannt aus Sprachen wie VisualBasic und Delphi. Sie geben die Möglichkeit, auf interne Daten und Code einer Klasse zuzugreifen. In Java wird dies durch Getter- und Setter-Methoden realisiert. Da diese Methoden oft angewandt werden stellt C# die Properties als zusätzliches Konzept zur Verfügung, was speziell bei RAD-Tools (Rapid Application Development) zu Vorteilen bei der Benutzung führt.

Java
Bsp.setCounter( Bsp.getCounter() + 1);
 
Aufruf get/set - Listing 4.1

C#
Bsp.Counter++;
 
Aufruf Property - Listing 4.2

das Beispiel in C# ist intuitiver und offensichtlich besser lesbar - das spiegelt sich auch in zugehöriger nachfolgender Deklaration wieder:

Java
public int getCounter() {
    return counter;
}
public void setSize(int value) {
size = value;
}
 

Deklaration Getter/Setter- Listing 4.3


C#
public int Counter {
    get {
        return counter;
    }
    set { 
size = value; }

}
 
Deklaration Property- Listing 4.4

Besonders bei read/write-Properties ist C# etwas eleganter als Java, da die get- und set-Methoden unmittelbar logisch zueinander gehören und dies so nun auch optisch deutlich wird. Soll ein Property nur lesbar bzw. schreibbar sein so lässt man einfach den set- bzw. get-Teil weg. Muss man beispielsweise den Namen eines Properties ändern so geschieht dies in C# nur an einer Stelle - in Java können die get- und set-Methoden theoretisch weit voneinander entfernt auftauchen. Genauso sieht dies auch bei Kommentaren aus, die bei C# so nur an einer Stelle auftauchen müssen.
Als einziges Problem könnte hier genannt werde, dass man als Nutzer der Klasse nicht unterscheiden kann, ob es sich bei einem Wert um ein Feld oder ein Property handelt. Allerdings sollte es generell selten vorkommen, dass Felder als "public" deklariert werden.

Indexer sind ein Konzept in C# um Objekte wie Arrays zu behandeln, ähnlich wie Properties nutzen auch sie get/set um Werte zu lesen bzw. zu schreiben.
Ein Beispiel:

C#
using System;

public class Hoersaal
{

int[] anzPlaetze = new int[5];
    //Indexer
    public int this[int index] {
        get {
            return anzPlaetze[index - 1];
        }
        set {
            if ((index >= 1) && (index <=5))
                anzPlaetze[index - 1] = value;
        }
    }
}
//Main
public class MyTest
{ static void Main(){ Hoersaal HS = new Hoersaal(); HS[1] = 75;
Console.WriteLine("Plätze HS1: " + HS[1]);
} }
 
Hoersaal als Indexer - Listing 4.5


Events und Delegates

Event-Handling ist prinzipiell ein Prozess, bei dem ein Objekt ein anderes Objekt informiert, dass ein Ereignis eingetreten ist. C# bietet eine direkte Unterstützung von Events durch das Konzept der Delegates. Ein Delegate kann man sich am ehesten als einen erweiterten Funktionszeiger vorstellen, wie diese z.B. in C, C++ und Delphi (Closure) vorkommen. Mit 'event' deklariert der Entwickler innerhalb einer Klasse ein Datenfeld zur Aufnahme eines Delegate, also ein Funktionsobjekt. Mit dem Operator += kann nun einem Exemplar dieser Klasse ein Funktionsobjekt zugeordnet und mit dem Operator -= wieder entfernt werden. Das Prinzip ist im wesentlichen ein Registrieren und Entfernen einer Callbak-Funktion einschließlich dessen Aufrufkontext.

Ein Beispiel mit einem Wecker als Event-Auslöser:

C#
using System;

//Die Signatur des Delegates
public delegate void WeckerEventHandler(int uhrzeit);

//Klasse, die den Event auslöst
public class Wecker
{

//Ereignis-Objekt
public event WeckerEventHandler Klingeln;
int weckzeit;
int uhrzeit;

//Konstruktor legt Weckzeit fest
public Wecker(int weckzeit) {
this.weckzeit = weckzeit;
}

//Uhrzeit-Property
public int Uhrzeit {
get {
return uhrzeit;
}
set {
uhrzeit = value;
if (uhrzeit == weckzeit) {
Klingeln(uhrzeit); //Event auslösen!
}
}
}

}

//Die Klasse, die den Event auslöst
public class Nachtruhe
{

// Konstante in Sekunden
private const int GUTE_ZEIT_ZUM_AUFSTEHEN = 36000;

public Nachtruhe(Wecker wecker) {
//Callback-Methode anmelden
wecker.Klingeln += new WeckerEventHandler(reagiereAufKlingeln);
}
//Callback-Methode
private void reagiereAufKlingeln(int uhrzeit) {
if (uhrzeit < GUTE_ZEIT_ZUM_AUFSTEHEN) {
Console.WriteLine("Ausmachen und weiterschlafen...");
} else {
Console.WriteLine("Aufstehen und frühstücken...");
}
}
}

//Test-Klasse
public class Test
{

public static void Main() {
Wecker wecker = new Wecker(25200);
Nachtruhe nachtruhe = new Nachtruhe(wecker);
wecker.Uhrzeit = 25200;
}

}
 
Event-Handling mit Delegates am Beispiel eines Weckers - Listing 4.6

Man sieht hier, dass der event-auslösenden Klasse (Wecker) die event-behandelnden Klassen nicht bekannt sein müssen. Die Operatoren += bzw. -= in Bezug auf eine Event-Methode (i.Bsp. Klingeln) erlauben es, beliebig viele Event-Handler anzumelden bzw. zu entfernen also eine Abarbeitungskette aufzubauen. Das Einführen der Delegates durch Microsoft führte zu einer Auseinandersetzung über Sinn und Zweck dieses Prinzips, im Besonderen zwischen Sun und Microsoft. Sun beschreibt in einem veröffentlichten Whitepaper [Sun(9)] Delegates als unnötiges Konzept, das zudem der Objektorientierung wiederspricht. Microsoft reagierte und veröffentlichte seinerseits einen Artikel über Delegates in dem es klarstellt, dass deren Verwendung zu besser strukturiertem und so besser lesbarem Code führt. [Microsoft(10)]



Enums

Aufzählungstypen (Enums) sind Teil vieler gängiger Sprachen, wie Pascal, C, C++ und nun auch von C#. Java hat dieses Konzept nicht eingeführt, weil man Aufzählungstypen durch Klassen nachbilden kann. Die Philosophie bei Sun scheint zu sein, auf die Einführung neuer Sprachkonstrukte zu verzichten, wenn man das Ziel auch mit Klassen erreichen kann. Ob dies sinnvoll ist liegt wohl im Geschmack des Betrachters, denn die Sprachkonstrukte die einen spezifischen Fall beschreiben, können diesen u.U. präziser abbilden und somit in einem gewissen Rahmen das Programmieren erleichten.

C#
//Deklaration
public enum Ampelfarbe {gruen, gelb, rot};
//Benutzung
Ampelfarbe ampel = Ampelfarbe.gruen;
 
Aufzählungstyp in C# - Listing 4.7

in Java wird der gleiche Sachverhalt folgendermaßen deklariert:

Java
//Deklaration
public class Ampelfarbe
{
public final static int GRUEN = 1;
public final static int GELB = 2;
public final static int ROT = 3;
}

//Benutzung
int ampel = Ampelfarbe.GRUEN;
 
"Aufzählungstyp" in Java - Listing 4.8

Der Typ der "ampel" ist im C#-Fall als "Ampelfarbe" gekennzeichnet - in Java ist die ein ganz "normaler" int-Datentyp. Auch wenn intern der Datentyp bei beiden gleich ist, so ist das C#-Beispiel eine bessere Abstraktion des Problems und einfacher zu lesen. Im C#-Beispiel würde ein Ausgabe durch "System.Console.WriteLine(ampel)" wirklich "gruen" ausgeben und nicht den internen int-Wert.
In C# ist ein Wert aus einem enum auch nur über den Wertbezeichner anzusprechen - ein Aufruf über Ordinalzahl ist nur durch explizite Typumwandlung möglich (Ausnahme ist der Ordinalwert 0, der i.A. den Standardwert meint). Als Basistyp für Enums ist neben dem Standard 'int' auch eine Reihe anderer Typen möglich (byte, sbyte, short, ...), dieser muss nach dem enum-Bezeichner explizit angegeben werden.

C#
//Deklaration
public enum Ampelfarbe : byte 
{
gruen = 0, gelb = 5,
rot = 5
};
 
Beispiel mit Angabe des Ordinalwerts - Listing 4.9

In diesem Beispiel kann man auch sehen, dass es möglich ist mehreren Aufzählungswerten den gleichen Ordinalwert zuzuweisen. Bei Aufruf über den Ordinalwert wird dann der zuerst Gelistete benutzt. Enums sind ebenso wie die später vorgestellten Structs Werttypen.


Collections und der Ausdruck foreach

In C# wurde ein foreach-Ausdruck eingeführt, der auf Objekten arbeitet, die Collections genannt werden. Collections sind Objekte, die folgendes Interface implementiert haben:

C#
interface IEnumerable 
{
IEnumerator GetEnumerator();
}
 
Interface IEnumerable - Listing 4.10

C#
interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
 
Interface IEnumerator - Listing 4.11

Im .NET Framework sind diese Klassen im Namensraum "System.Collections" zu finden. So ist zum Beispiel ein Array eine Collection über das man mit dem foreach-Ausdruck iterieren kann.

C#
string[] namen = {"Andreas", "Michael", "Uwe" };

//Iterieren über ein Collection (hier Array)
foreach (string s in namen)
Console.WriteLine(s)
 
foreach mit String-Array - Listing 4.12

Structs

Neben Klassen gibt es in C# Strukturen, die durch das Schlüsselwort 'struct' eingeleitet werden. Structs sind in Sprachen wie Delphi und Visual Basic als zusammengesetzte Typen oder Records bekannt. Auch in C gibt es Structs. Die Möglichkeiten der Structs in C# sind jedoch umfangreicher. In C# sind diese mehr mit Klassen zu vergleichen, denn sie können auch Code (Methoden) und sogar einen Konstruktor enthalten. Trotz vieler Ähnlichkeiten gibt es aber wesentliche Unterschiede. Die Datentypen werden als Werttypen und nicht wie Klassen, Arrays oder Strings als Referenztypen angelegt. Somit werden Structs auf dem Stack erzeugt, im Gegensatz zu Klassen, die immer auf dem Heap angelegt werden. Sie eignen sich für Daten, die nicht zu groß sind, die in großer Zahl vorkommen und für die eine eigene Klasse zuviel Aufwand wäre. Ein weiterer Unterschied zu Klassen ist, dass Structs keinerlei Vererbung erlauben also weder beerbt werden noch vererben können. Mit Boxing- und Unboxing bietet C# einen wirksamen Mechanismus zur Umwandlung von Wert- und Referenztypen und wieder zurück. Microsoft bezeichnet dies als "unified type system". Die Wrapper-Klassen, die in Java nötig sind werden hier quasi vom Compiler automatisch erzeugt.

C# erlaubt mit dem Konzept der Structs den Satz der primitiven Typen zu erweitern. Es ist in der Tat so, dass in C# alle primitiven Typen als Structs angelegt sind, so ist "int" ein Alias für den Struct 'System.Int32'.


Polymorphismus

Mit virtuellen Methoden wird es objektorientierten Sprachen ermöglicht Polymorphismus auszudrücken. Als Vorgabe werden Methodenaufrufe im Gegensatz zu Java in C# früh gebunden (early-binding). Ist eine späte Bindung (late-binding) erfordert muss der Entwickler eine Methode ausdrücklich als 'virtual' kennzeichnen. In Java sind grundsätzlich alle Methoden erstmal virtuell, d.h. Methoden abgeleiteter Klassen mit gleicher Signatur können die Methoden aus der Basisklasse überschreiben. Wie erwähnt muss in C# ähnlich wie in C++ und Delphi eine Methode in der Basisklasse als 'virtual' und die überschreibende Methode mit 'override' (nicht in C++) deklariert werden.

C#
using System;

class A {
public virtual void printName() {
Console.WriteLine("A");
}
}

class B : A {
public override void printName() {
Console.WriteLine("B");
}
}

class C : B {
public new void printName() {
Console.WriteLine("C");
}
}

public class MyTest {

static void Main(){
C c = new C();
c.printName(); //-->C
((B)c).printName(); //-->B
((A)c).printName(); //-->B
}
}
 
Polymorphismus in C# - Listing 4.13

Wird versucht eine nicht-virtuelle Methode zu überschreiben resultiert dies in einem Compile-Time-Error, es sei denn die überschreibende Methode wurde mit dem Schlüsselwort 'new' deklariert. In diesem Fall entsteht eine neue unabhängige Methode, die die Mehtode aus der Basisklasse verdeckt. Vergleicht man diese Schreibweise mit der aus Java dann ist im Quellcode besser zu erkennen welche Methoden überschrieben werden. Ein Vorteil für nicht-virtuelle Methoden ist die höhere Ausführgeschwindigkeit, da nicht zur Laufzeit überprüft wird zu welcher Methode gebunden werden muss. Wobei diese Möglichkeit der nicht-virtuellen Methoden auch in Java problemlos erreicht werden kann. Im Prinzip ist es also so, dass nur der Default bei C# und Java anders gesetzt ist.


Interfaces

Zwischen den Interfaces in Java und C# gibt es nur einen kleinen Unterschied. Die Nutzung ist in C# etwas flexibler, denn besitzt eine Klasse zwei oder mehr Interfaces muss in Java darauf geachtet werden, dass die jeweiligen Interfaces keine gleichen Methoden deklariert haben. In C# wird das Problem gelöst, indem man bei der Implementierung der Methode den Bezeichner des Interfaces voranstellt.

C#
public interface I {
void myFunc();
}

public interface J {
void myFunc();
}

public class A: I, J {
{
void I.myFunc() {
}

void J.myFunc() {
}
}
 
Interfaces und Implementierung - Listing 4.14

Parameterübergabe

In C# gibt es im Vergleich zu Java erweiterte Möglichkeiten zur Parameterübergabe. Ohne Angabe erfolgt die Übergabe als Wert. Nachfolgend sind die möglichen Parameter-Schlüsselworte erläutert. Microsoft hat sich an die Vorgabe der UML (Unified Modeling Language) gehalten, bei der die Parameterübergabe in den Formen 'in', 'out' und 'inout' erfolgen kann [Object Management Group(11)]. 'in' entspricht in C# der reinen Wertübergabe, 'inout' heißt in C# 'ref' und 'out' ist identisch benannt.

'ref':
Bei C# ist es im Vergleich zu Java möglich Refernzparameter festzulegen. Der Parameter muss als Referenzparameter sowohl beim Deklarieren als auch beim Übergeben als ein solcher gekennzeichnet werden (anders als z.B. in C++). Dies geschieht mit dem Schlüsselwort 'ref'. Ein Parameter der so gekennzeichnet ist muss zwingend vor der Übergabe initialisiert sein.

'out':
Bei Parametern, die als 'out' deklariert werden diese als nicht initialisiert angenommen - die aufgerufene Methode muss dafür sorgen, dass die übergebene Variable einen Wert zugewiesen bekommt.

'params':
Dieser Modifier kann nur in Verbindung mit dem letzten Parameter auftreten. So können beliebig viele Parameter eines bestimmten Typs übergeben werden.


Zeigerarithmetik

In C# ist es möglich direkt mit Zeigern zu arbeiten. Hierzu müssen diese Bereiche mit dem Schlüsselwort 'unsafe' deklariert werden. Der Bezeichner 'unsafe' macht bereits deutlich, dass diese Möglichkeit nur mit Vorsicht verwendet und möglichst vermieden werden sollte. Nicht umsonst hat Sun diese Möglichkeit in Java bewusst unterbunden.

Durch die Zeigerarithmetik wird es prinzipiell auch ermöglicht z.B. Gerätetreiber in C# zu schreiben oder mit COM-Objekten zu kommunizieren. Bei Benutzung von Zeigern und unsafe-Bereichen ist darauf zu achten, dass hier keine Garbage-Collection durchgeführt wird, man sich also selbst um die Freigabe von Ressourcen kümmern muss.


Der Switch-Ausdruck

Der Switch-Ausdruck in C# funktioniert im wesentlichen analog zu dem in Java, jedoch kann dieser in C# zusätzlich mit Enums und Strings umgehen. Ein beliebter Fehler in Sprachen wie C, C++ und Java ist das unbeabsichtigte Weglassen der 'break'-Anweisung in einem case-Block, wodurch oftmals ein unerwünschtes Programmverhalten entsteht. Um dies zu vermeiden müssen 'case'-Blöcke in C# nun zwingend mit 'break' abgeschlossen werden, worauf man ggf. durch einen Compile-Fehler aufmerksam gemacht wird.

C#
string str = "aaa";
switch (str) {


    case "aaa" : Console.WriteLine('aaa');
    break; 

    case "bbb": Console.WriteLine('bbb');
    break;

    default: Console.WriteLine(str);
    break;
}
 
Case-Verteiler mit String - Listing 4.15

 


... [ Seminar WS 2002/03 ] ... [ C# ] ... [ Überblick Spracheigenschaften ] ... [ Vergleich mit Java ] ... [ Fazit ]