C#
versus Java |
Combien de fois n'a t-on pas entendu les questions ou réflexions suivantes : "mais qu'est-ce que ça fait de plus que Java, ce C# ? " ou encore "C# ? Ah bah oui, c'est le Java propriétaire de Microsoft pour contrer Sun" et combien de fois n'avez vous pas eu envie d'expliquer ou de convaincre à ces personnes, au demeurant de bonne foi, que C# méritait plus d'estime car il possédait de multiples facettes et qualités souvent trop longues à énumérer ou à résumer au détour d'un forum de discussions. D'ailleurs, dans la majeure partie des cas, ce dédain face à un nouveau langage, qui plus est développé par Microsoft (quel sacrilège ;-)), n'est qu'un moyen de dissimuler une certaine forme d'ignorance.
Cet article intitulé "C# versus Java" se propose de vous faire connaître C# à travers un ensemble de caractéristiques communes ou différentes de Java (Exceptions, Héritages, ...) avec un regard serein et objectif. Devant la multitude de sujets proposés, nous avons essayé de regrouper les fonctionnalités qui présentaient des similitudes, celles qui différaient de part leur implémentation et enfin, celles qui avaient un caractère novateur.
La plupart des exemples ont été testés avec Microsoft .NET Framework beta 2 et Java 2 Standard Edition.
L'auteur de l'article original est Dare Obasanjo qui a eu la gentillesse de nous autoriser à enrichir et à traduire ses écrits. Certaines parties ont été totalement revisitées par nos soins pour plus de clarté et de simplicité mais en aucun cas le sens et la teneur de l'article original n'ont été modifiés. Nous espérons qu'après avoir lu ces quelques pages vous serez plus à même de comprendre ce qu'est C#, mais aussi d'analyser ou de mieux juger par vous même ses multiples caractéristiques et fonctionnalités.
Index Rapide
Comme Java, C# possède une
super classe, mère de tous les objets : System.Object.
La classe
équivalent s'appelle java.lang.object. Les méthodes
présentent dans ces deux classes sont très similaires (ex: toString()) excepté
wait(), notify() et les autres méthodes liées à la synchronisation.
NOTE: En C#, une classe de type objet peut soit être écrite sous la forme "object" en minuscule ou "Object". En fait, à la compilation ces deux types sont remplacés par System.Object.
Il y a énormément de similitudes entre les deux langages, presque tous les mots-clés Java ont un équivalent en C# à part quelques exceptions telles que transient, throws et strictfp. La table ci-dessous vous illustre les mots-clés présents des deux cotés
mot-clé C#
|
mot-clé Java
|
mot-clé C#
|
mot-clé Java
|
mot-clé C#
|
mot-clé Java
|
mot-clé
C# |
mot-clé
Java |
abstract |
abstract |
explicit |
N/A |
object |
N/A |
this |
this |
as |
N/A |
extern |
native |
operator |
N/A |
throw |
throw |
base |
super |
finally |
finally |
out |
N/A |
true |
true |
bool |
boolean |
fixed |
N/A |
override |
N/A |
try |
try |
break |
break |
float |
float |
params |
N/A |
typeof |
N/A |
byte |
N/A |
for |
for |
private |
private |
uint |
N/A |
case |
case |
foreach |
N/A |
protected |
N/A |
ulong |
N/A |
catch |
catch |
get |
N/A |
public |
public |
unchecked |
N/A |
char |
char |
goto |
goto1 |
readonly |
N/A |
unsafe |
N/A |
checked |
N/A |
if |
if |
ref |
N/A |
ushort |
N/A |
class |
class |
implicit |
N/A |
return |
return |
using |
import |
const |
const1 |
in |
N/A |
sbyte |
byte |
value |
N/A |
continue |
continue |
int |
int |
sealed |
final |
virtual |
N/A |
decimal |
N/A |
interface |
interface |
set |
N/A |
void |
void |
default |
default |
internal |
protected |
short |
short |
while |
while |
delegate |
N/A |
is |
instanceof |
sizeof |
N/A |
: |
extends |
do |
do |
lock |
synchronized |
stackalloc |
N/A |
: |
implements |
double |
double |
long |
long |
static |
static |
N/A |
strictfp |
else |
else |
namespace |
package |
string |
N/A |
N/A |
throws |
enum |
N/A |
new |
new |
struct |
N/A |
N/A |
transient |
event |
N/A |
null |
null |
switch |
switch |
N/A |
volatile |
De la même manière que Java est compilé en byte-code et s'exécute dans un environnement d'exécution managé (Machine Virtuelle JVM), C# est compilé en MSIL s'exécutant dans la CRL (Common Langage Runtime). Les deux plate-formes supportent la compilation Just In Time (JIT). Toutefois, il existe une légère différence entre les deux plate-formes, les compilateurs Java permettent de désactiver totalement le JIT en fonctionnant uniquement en mode interprété alors que les compilateurs .NET en général intègre nativement le JIT. Enfin, il existe des deux cotés la possibilité de pré-compiler en code natif le source. .
Tous les objets Java sont créés sur le tas en utilisant le mot-clé new. Il en va de même pour la plupart des objets C# n'étant pas des types de valeurs (ValueType).
Le ramasse miettes en C# implémente un algorithme bien connu appelé Mark and Compact garbage collection algorithm.
Dans les langages tels que C ou C++, la dimension de chaque sous tableau doit être identique dans les tableaux à plusieurs dimensions. En Java et C#, les tableaux n'ont pas à se plier à cette contrainte car ils peuvent être créés comme des tableaux à une dimension référençant d'autres tableaux. Ils sont appelés "Jagged Arrays". Pour cette raison, les tailles initiales des lignes et colonnes de ce type de tableaux peuvent-être différentes. C'est ce que montre le code suivant :
int
[][]myArray = new int[2][];
myArray[0]
= new int[3];
myArray[1]
= new int[9];
Le code précédent est valide pour Java et C#.
Comme en Java et contrairement à C++, les méthodes en C# doivent être intégrées dans une classe.
C#, comme Java, supporte le concept d'interface qui est assimilé à une classe abstraite pure. De la même façon, C# et Java autorisent l'héritage multiple d'interface et simple d'implémentation.
C# possède une classe System.String qui est équivalente à java.lang.String. Les deux classes sont "immuables", c'est à dire qu'une fois les objets créés, il n'est pas possible de modifier leur valeur. Ceci dans le but de protéger le mécanisme d'encapsulation ô combien fondamental.
C# Code
string
csString = "Apple Jack";
csString.ToLower();
/* Does not modify string, instead returns lower case
copy of string */
Java Code
String
jString = "Grapes";
jString.toLowerCase();
/* Does not modify string, instead returns lower case
copy of string */
Pour créer une chaîne de caractères autorisant la modification avec la même référence, il est conseillé d'utiliser les classes System.Text.StringBuilder pour C# et java.lang.StringBuffer pour Java.
NOTE:
En C#, la classe chaîne peut être écrite sous la forme string
ou String.
Les deux langages proposent des mécanismes consistant à interdire toute extension d'une classe. Soit par souci d'optimisation, soit par souci de sécurité. Ainsi, il est interdit de les dériver pour redéfinir des méthodes ou simplement réutiliser l'implémentation. En C#, vous utilisez le mot-clé sealed et en Java le mot-clé final.
C# Code
sealed
class Student
{
string fname;
string lname;
int
uid;
void attendClass()
{}
}
Java Code
final
class
Student
{
String fname;
String lname;
int
uid;
void attendClass()
{}
}
Les exceptions en C# et Java partagent énormément de caractéristiques. Les deux langages supportent l'utilisation de l'ordre try pour indiquer qu'un bloc est susceptible de lever une exception et catch pour capturer l'exception en question. De plus, finally est implémenté de la même manière pour spécifier qu'une région de code doit, dans tous les cas être exécutée (exception ou pas). Cela permet de libérer des ressources proprement. Les deux langages proposent une hiérarchie de classes d'Exceptions dérivant d'une super classe : System.Exception pour C# et java.lang.Exception pour Java. Aussi, il est possible de chaîner la levée ou la capture d'exception (throw dans un catch) de part et d'autre. Cela permet, lors de la levée d'une exception, de retourner à l'appelant un type d'exception correspondant à son contexte et à sa couche d'architecture. Par exemple, une ligne non trouvée dans une table se traduira par une SQLException que le développeur prendra soin de renvoyer à l'interface graphique sous la forme d'un ObjectNotFoundException.
NOTE: Cependant, il existe une différence fondamentale entre C# et Java. Le mot-clé throws n'existe pas en C# car vous n'êtes pas contraint de spécifier dans la signature d'une méthode le fait qu'elle est susceptible de lever une exception. Il n'y a, contrairement à Java, aucune vérification de faite à l'exécution.
C# Code
using
System;
using
System.IO;
class
MyException: Exception
{
public
MyException(string message): base(message){ }
public
MyException(string message, Exception
innerException):
base(message, innerException){
}
}
public
class ExceptionTest
{
static
void DoStuff()
{
throw new FileNotFoundException();
}
public
static int
Main()
{
try
{
try
{
DoStuff();
return 0; //won't get
to execute
}
catch(IOException
ioe)
{ /* parent of FileNotFoundException */
throw new MyException("MyException occured", ioe);
/* rethrow new exception with inner exception
specified */
}
}
finally
{
Console.WriteLine("***Finally block executes even though MyException not
caught***");
}
}//Main(string[])
} // ExceptionTest
Java Code
class MyException
extends Exception{
public MyException(String message){ super(message); }
public MyException(String message, Exception
innerException){ super(message, innerException); }
}
public class ExceptionTest {
static
void doStuff(){
throw new
ArithmeticException();
}
public static void main(String[]
args) throws Exception{
try{
try{
doStuff();
return;
//won't get to execute
}catch(RuntimeException re){ /* parent of ArithmeticException */
throw new MyException("MyException occured", re); /* rethrow new exception with cause specified
*/
}
}finally{
System.out.println("***Finally block executes even though MyException not
caught***");
}
}//main(string[])
} // ExceptionTest
Les variables d'instance et les variables statiques peuvent être initialisées dès leur déclaration, et ce, dans les deux langages. Si la variable membre est une variable d'instance, alors l'initialisation sera effectuée juste avant l'appel du constructeur. Les membres statiques, eux, sont initialisés dès le chargement de la classe par le Runtime. Cela intervient en règle générale avant l'appel du constructeur. Il est aussi possible de définir de part et d'autre des blocs statiques qui seront exécutés au chargement. Il sont appelés plus communément constructeurs statiques.
C# Code
using
System;
class
StaticInitTest
{
string
instMember = InitInstance();
string
staMember = InitStatic();
StaticInitTest()
{
Console.WriteLine("In instance constructor");
}
static
StaticInitTest()
{
Console.WriteLine("In static constructor");
}
static
String InitInstance()
{
Console.WriteLine("Initializing instance
variable");
return "instance";
}
static
String InitStatic()
{
Console.WriteLine("Initializing static variable");
return "static";
}
static
void DoStuff()
{
Console.WriteLine("Invoking static DoStuff()
method");
}
public
static void
Main(string[] args)
{
Console.WriteLine("Beginning main()");
StaticInitTest.DoStuff();
StaticInitTest sti = new
StaticInitTest();
Console.WriteLine("Completed main()");
}
}
Java Code
class
StaticInitTest
{
String instMember = initInstance();
String staMember = initStatic();
StaticInitTest()
{
System.out.println("In instance constructor");
}
static
{
System.out.println("In static constructor");
}
static
String initInstance()
{
System.out.println("Initializing instance
variable");
return
"instance";
}
static
String initStatic()
{
System.out.println("Initializing static variable");
return
"static";
}
static
void doStuff()
{
System.out.println("Invoking static DoStuff()
method");
}
public
static void
main(String[] args)
{
System.out.println("Beginning main()");
StaticInitTest.doStuff();
StaticInitTest sti = new
StaticInitTest();
System.out.println("Completed main()");
}
}
Exécution des deux exemples :
In static constructor
Beginning main()
Invoking static DoStuff() method
Initializing instance variable
Initializing static variable
In instance constructor
Completed main()
Le point d'entrée dans les deux langages est la méthode Main(). Il existe une différence mineure concernant le nom de la méthode qui commence par un "M" majuscule et les paramètres utilisés. Ainsi, le main de C# est surchargé et retourne un code de status alors que celui de Java impose une signature bien précise et retourne void.
C# Code
using
System;
class
A
{
public
static void
Main(String[] args)
{
Console.WriteLine("Hello World");
}
}
Java Code
class
B
{
public
static void
main(String[] args)
{
System.out.println("Hello World");
}
}
Il est généralement recommandé d'insérer dans chaque classe une méthode Main() permettant tester la classe de manière unitaire indépendamment de la méthode Main() générale.
Ainsi, il est possible d'avoir deux classes, A et B, contenant toutes deux une méthode Main(). En Java, pour spécifier la méthode Main appelée, il suffit de spécifier le nom de la classe en ligne de commande ou à travers un outil. En C#, le problème est légèrement différent car le processus de compilation génère un exécutable pouvant contenir plusieurs Main(). C'est pourquoi, un paramètre de compilation précisera la méthode par défaut qui sera appelée lors de l'exécution (/main). L'utilisation de ces techniques associées à une compilation conditionnelle via les directives de pré-processing vous procure une grande souplesse dans les tests.
Java Exemple
C:\CodeSample> javac A.java B.java
C:\CodeSample> java A
Hello World from class A
C:\CodeSample> java B
Hello World from class B
C# Exemple
C:\CodeSample> csc /main:A /out:example.exe A.cs B.cs
C:\CodeSample> example.exe
Hello World from class A
C:\CodeSample> csc /main:B /out:example.exe A.cs B.cs
C:\CodeSample> example.exe
Hello World from class B
Ainsi, en Java, contrairement à C#, il n'est pas nécessaire de recompiler les classes pour changer le point d'entrée du programme. D'un autre coté, en C#, les directives de compilation permettent de produire du code paramétrable pour la phase de test ou de release.
C#
utilise la syntaxe du C++ pour l'héritage. Le même mot-clé s'utilise pour
l'héritage d'implémentation et d'interface contrairement à Java qui spécifie
extends
et
implements
.
C# Code
using
System;
class
B:A, IComparable
{
int CompareTo(){}
public
static void
Main(String[] args)
{
Console.WriteLine("Hello World");
}
}
Java Code
class
B extends A implements Comparable
{
int
compareTo(){}
public
static void
main(String[] args)
{
System.out.println("Hello World");
}
}
Les développeurs Java pourront toujours argumenter que cela rend les sources difficilement lisibles car le mot-clé indique si la classe mère est une interface ou pas. Dans la pratique, ce n'est pas vraiment un problème dans la mesure où Microsoft définit clairement que les noms d'interfaces doivent commencer par IMyInterface (tel que ICloneable).
En
C#, l'opérateur is
est totalement analogue au mot-clé instanceof
de Java. Les deux
bouts de code suivant sont équivalent :
C# Code
if(x
is MyClass)
MyClass
mc = (MyClass) x;
Java Code
if(x
instanceof MyClass)
MyClass
mc = (MyClass) x;
Un
Namespace C# est une manière de regrouper des classes par domaine et s'utilise
de manière similaire par rapport au mot-clé package
en Java. Les
développeurs C++ noteront que la syntaxe des namespaces en C# est très proche de
C++. Toutefois, en Java, la structure arborescente des packages dicte la
structure des sources sous-jacentes, ce qui n'est pas le cas de C#.
Examples :
C# Code
namespace
com.carnage4life
{
public
class MyClass
{
int x;
void doStuff(){}
}
}
Java Code
package
com.carnage4life;
public
class MyClass
{
int x;
void
doStuff(){}
}
Enfin, la syntaxe C# autorise la multiplicité des déclarations de namespaces au sein d'un même fichier.
C# Code
using
System;
namespace
Company
{
public class MyClass
{ /*
Company.MyClass */
int x;
void doStuff(){}
}
namespace Carnage4life
{
public class MyOtherClass
{ /* Company.Carnage4life.MyOtherClass */
int y;
void
doOtherStuff(){}
public static void
Main(string[] args)
{
Console.WriteLine("Hey, I can nest namespaces");
}
}// class
MyOtherClass
}// namespace
Carnage4life
}// namespace Company
La syntaxe et la sémantique des constructeurs en C# est identique à celle de Java. C# propose aussi le concept de destructeur dans le même esprit que C++ (avec un ~) à ceci près que la sémantique est strictement la même qu'un finaliseur (Finalize) Java. Bien que les finaliseurs soient intégrés au langage, il est recommandé de s'en servir avec précaution car il n'existe aucun contrôle quant à l'ordre dans lequel ils sont appelés. De plus, les objets possédant des finaliseurs ralentissent le traitement du Ramasse miettes qui est contraint de les mémoriser plus longtemps dans le mécanisme de libération.
NOTE: En C#, les destructeurs (finalizers) appellent automatiquement les finaliseurs de leur classe mère contrairement à Java qui impose un appel explicite (super.finalise()).
C# Code
using
System;
public
class MyClass
{
static
int num_created = 0;
int i =
0;
MyClass()
{
i = ++num_created;
Console.WriteLine("Created object #" + i);
}
~MyClass()
{
Console.WriteLine("Object #" + i + " is being finalized");
}
public
static void
Main(string[] args)
{
for(int i=0; i <
10000; i++)
new MyClass();
}
}
Java Code
public
class MyClass
{
static
int num_created = 0;
int i =
0;
MyClass()
{
i = ++num_created;
System.out.println("Created object #" + i);
}
public
void finalize()
{
System.out.println("Object #" + i + " is being finalized");
}
public
static void
main(String[] args)
{
for(int
i=0; i < 10000; i++)
new MyClass();
}
}
En
Java, il est possible de spécifier explicitement la synchronisation d'un bloc
afin d'éviter que deux threads modifient simultanément la même section critique.
C# fournit un mot-clé lock
qui est sémantiquement
l'équivalent du synchronized
en Java.
C# Code
public
void WithdrawAmount(int num)
{
lock(this)
{
if(num
< this.amount)
this.amount
-= num;
}
}
Java Code
public
void withdrawAmount(int num)
{
synchronized(this)
{
if(num
< this.amount)
this.amount
-= num;
}
}
C#
et Java supportent tous les deux les méthodes synchronisées. Lorsqu'une méthode
synchronisée est appelée, le premier thread prend la main sur le moniteur
d'objets et bloque l'accès aux autres threads le temps de l'exécution de la
méthode. En Java, les méthodes synchronisées sont précédées du mot-clé
synchronized. En C#, il existe plusieurs manières d'implémenter la
synchronisation de méthodes : par Attributs de Runtime (MethodImplOptions.Synchronized
)
ou par utilisation du mot-clé Interlocked.
C# Code
using
System;
using
System.Runtime.CompilerServices;
public
class BankAccount
{
[MethodImpl(MethodImplOptions.Synchronized)]
public
void WithdrawAmount(int num)
{
if(num < this.amount)
this.amount - num;
}
}//BankAccount
Java Code
public
class BankAccount
{
public
synchronized void withdrawAmount(int num)
{
if(num < this.amount)
this.amount - num;
}
}//BankAccount
La
table ci-dessous vous synthétise les différents mot-clés permettant de modifier
la visibilité et l'accès à une classe dans le but de protéger l'encapsulation.
Les fans de C++ déçus lorsque Sun a modifié la portée de l'instruction
protected
seront
heureux de noter que C# garde la même sémantique que C++. Cela signifie donc
qu'un membre "protected" ne peut être accédé que par d'autres méthodes membres
situées dans la même classe ou dans une classe dérivée mais n'est en aucun cas
visible de l'extérieur (même package ou non).
Le
mot-clé internal
signifie que la fonction membre ne peut être accédée que par d'autres classes
situées dans la même assembly. Associée à protected
, la visibilité est
étendue aux classes dérivées.
C# access modifier |
Java access modifier |
private |
private |
public |
public |
internal |
protected |
protected |
N/A |
internal protected |
N/A |
NOTE:
La visibilité par défaut d'un champ ou d'une méthode en C# est private
alors qu'en Java elle
est protected.
La capacité à découvrir les méthodes et champs dans une classe et à invoquer dynamiquement des méthodes à l'exécution est appelé la reflection. Cette caractéristique existe aussi bien en Java qu'en C# à la différence près que la reflection s'effectue au niveau d'une assembly en C# et au niveau d'une classe en Java.
C# Code
using
System;
using
System.Xml;
using
System.Reflection;
using
System.IO;
class
ReflectionSample
{
public
static void
Main( string[] args)
{
Assembly assembly=null;
Type type=null;
XmlDocument doc=null;
try
{
// Load the requested assembly and get the
requested type
assembly =
Assembly.LoadFrom("C:\\WINNT\\Microsoft.NET\\Framework\\v1.0.2914\\System.XML.dll");
type = assembly.GetType("System.Xml.XmlDocument", true);
//Unfortunately one cannot dynamically
instantiate types via the Type object in C#.
doc =
Activator.CreateInstance("System.Xml","System.Xml.XmlDocument").Unwrap()
as XmlDocument;
if(doc != null)
Console.WriteLine(doc.GetType() + " was created at
runtime");
else
Console.WriteLine("Could not dynamically create object at
runtime");
}
catch(FileNotFoundException)
{
Console.WriteLine("Could not load Assembly:
system.xml.dll");
return;
}
catch(TypeLoadException)
{
Console.WriteLine("Could not load Type: System.Xml.XmlDocument from
assembly: system.xml.dll");
return;
}
catch(MissingMethodException)
{
Console.WriteLine("Cannot find default constructor of " +
type);
}
catch(MemberAccessException)
{
Console.WriteLine("Could not create new XmlDocument
instance");
}
// Get the methods from the
type
MethodInfo[] methods = type.GetMethods();
//print the method signatures and
parameters
for(int
i=0; i < methods.Length; i++)
{
Console.WriteLine ("{0}", methods[i]);
ParameterInfo[] parameters = methods[i].GetParameters();
for(int
j=0; j < parameters.Length; j++)
{
Console.WriteLine (" Parameter: {0} {1}", parameters[j].ParameterType,
parameters[j].Name);
}
}//for (int i...)
}
}
Java Code
import
java.lang.reflect.*;
import
org.w3c.dom.*;
import
javax.xml.parsers.*;
class
ReflectionTest
{
public
static void
main(String[] args)
{
Class c=null;
Document d;
try
{
c =
DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument().getClass();
d = (Document)
c.newInstance();
System.out.println(d + " was created at
runtime from its Class object");
}
catch(ParserConfigurationException
pce)
{
System.out.println("No document builder
exists that can satisfy the requested configuration");
}
catch(InstantiationException
ie)
{
System.out.println("Could not create new
Document instance");
}
catch(IllegalAccessException
iae)
{
System.out.println("Cannot access
default constructor of " + c);
}
// Get the methods from the
class
Method[] methods = c.getMethods();
//print the method signatures and
parameters
for (int i = 0; i < methods.length; i++)
{
System.out.println(
methods[i]);
Class[] parameters =
methods[i].getParameterTypes();
for (int j = 0; j < parameters.length; j++)
{
System.out.println("Parameters: " +
parameters[j].getName());
}
}
}
}
A
la vue du code précédent, vous aurez remarqué que la granularité de la
reflection est plus grande en C# qu'en Java. Quelque fois, vous avez besoin
d'obtenir les méta-données d'une classe spécifique encapsulée sous la forme d'un
objet. Cet objet est du type java.lang.Class
en Java et
System.Type
en C#.
Ainsi, pour récupérer une instance de cette classe, vous utilisez getClass() en
Java et GetType() en C#.
C# Code
Type t = typeof(ArrayList);
Java Code
Class c = java.util.Arraylist.class; /* Must append ".class" to fullname of class */
Pour
déclarer des constantes en Java, vous devez spécifier le mot-clé final
. Les variables "finales"
peuvent être initialisées soit à la compilation, soit à l'exécution. Une fois la
variable initialisée, elle devient immuable (non modifiable). Lorsque cette
variable est une référence, la référence ne pourra pointer que sur un seul objet
durant sa durée de vie. Pour déclarer des constantes en C#, le mot-clé
const
est utilisé
pour la compilation et readonly
à l'exécution. const
a toujours été demandé
par la communauté de développeurs Java mais n'a jamais vraiment eu d'écho de la
part de Sun. Gageons que la prochaine version du JDK intègrera ce mot-clé au
langage car, à l'heure actuelle, le développeur est contraint de jongler avec le
clonage d'objets et le masquage par interface afin d'éviter de briser
l'encapsulation.
Contrairement à C++, il n'est possible ni en Java ni en C# de spécifier une classe immuable (équivalent du const MyClass* p).
C# Code
using
System;
public
class ConstantTest
{
/* Compile time constants */
const
int i1 = 10; //implicitly a static variable
// code below won't compile because of 'static'
keyword
// public
static const int i2 = 20;
/* run time
constants */
public
static readonly uint l1
= (uint) DateTime.Now.Ticks;
/* object reference as constant */
readonly
Object o = new Object();
/* uninitialized readonly variable */
readonly
float f;
ConstantTest()
{
// unitialized readonly variable must be
initialized in constructor
f = 17.21f;
}
}
Java Code
import java.util.*;
public
class ConstantTest
{
/* Compile
time constants */
final int i1 = 10; //instance variable
static
final int i2 =
20; //class variable
/* run time
constants */
public
static final
long l1 = new
Date().getTime();
/* object
reference as constant */
final
Vector v = new
Vector();
/*
uninitialized final */
final
float f;
ConstantTest()
{
// unitialized final variable must be
initialized in constructor
f = 17.21f;
}
}
NOTE
: Le langage Java supporte aussi les paramètres de méthodes précédés du mot-clé
final
. Cela permet
aux classes internes déclarées dans la méthode d'accéder aux paramètres en
question. Cette fonctionnalité n'existe pas en C# .
Pour
chaque type primitif en Java, il existe un correspondant en C# avec le même nom,
excepté byte
. Le
byte
en Java est signé et est ainsi proche du
type sbyte
de C# (et non byte
). De plus, C# possède des
versions non signées pour la plupart des types : ulong, uint, ushort
et
byte.
La seule
différence majeure provient du type decimal
qui n'effectue aucun
arrondi au prix d'un surplus de place et d'une rapidité de traitement
moindre.
Ci-dessous, plusieurs manières d'implémenter des valeurs réelles
en C#.
C# Code
decimal
dec = 100.44m; //m is the suffix used to specify
decimal numbers
double dbl = 1.44e2d; //e is used to specify exponential notation while d is the
suffix used for doubles
Java possède deux façons de déclararer un tableau : la première, créé pour la compatibilité avec C et C++ et la deuxième plus simple et plus claire. C# utilise, quant à lui, la deuxième façon.
C# Code
int[]
iArray = new int[100]; //valid, iArray
is an object of type int[]
float
fArray[] = new float[100]; //ERROR: Won't
compile
Java Code
int[]
iArray = new int[100]; //valid, iArray
is an object of type int[]
float
fArray[] = new float[100]; //valid, but
isn't clear that fArray is an object of type float[]
C# et Java appelle implicitement les constructeurs père en cas de création d'un objet. Les deux langages proposent un moyen d'appeler explicitement le constructeur père en lui passant des paramètres spécifiques. De plus, C# et Java assurent que l'appel des constructeurs se fait dans un ordre bien précis assurant ainsi l'impossibilité d'accéder à des variables non encore initialisées. Enfin les deux langages intègrent la surcharge de constructeur et l'appel inter-constructeur afin de réutiliser du code existant. Ce procédé est aussi appelé "chaînage de constructeurs".
C# Code
using
System;
class
MyException: Exception
{
private
int Id;
public
MyException(string message): this(message, null,
100){ }
public
MyException(string message, Exception
innerException):
this(message, innerException, 100){
}
public
MyException(string message, Exception
innerException, int id):
base(message,
innerException)
{
this.Id = id;
}
}
Java Code
class
MyException extends
Exception
{
private
int Id;
public MyException(String message)
{
this(message, null, 100);
}
public
MyException(String message, Exception innerException)
{
this(message, innerException, 100);
}
public
MyException( String message,Exception innerException, int id)
{
super(message, innerException);
this.Id = id;
}
}
En Java et en C# il est possible d'imbriquer des déclarations de classes. En Java, il existe deux types de classes imbriquées : les classes non statiques connues sous le nom de classe internes, et les classes statiques. Une classe Java interne peut être considérée comme une relation 1,1 avec sa classe englobante et ne peut posséder de méthodes statiques.
C# possède l'équivalent des classes statiques Java imbriquées mais n'intègre rien d'équivalent aux classes internes de Java. Le code suivant nous montre un exemple de classe imbriquée en C# et Java.
C# Code
public
class Car
{
private
Engine engine;
private
class Engine
{
string make;
}
}
Java Code
public
class Car
{
private
Engine engine;
private
static class
Engine
{
String make;
}
}
NOTE : En Java, une classe imbriquée peut être déclarée dans n'importe quels types de bloc, ce qui n'est pas le cas de C#. La possibilité de créer des classes imbriquées à l'intérieur de méthodes peut sembler inutile mais combinée aux classes anonymes, cela peut conduire à concevoir de puissantes applications basées sur les Design Pattern.
Les
Threads en Java sont créés par dérivation de la classe java.lang.Thread
et en
re-définissant la méthode run() ou par implémentation directe de l'interface the
java.lang.Runnable
.
Vous avez le choix entre l'héritage (historique) et la délégation (plus
sûre).
En
C#, la création de Threads se fait à l'aide de l'opération new System.Threading.Thread
en
passant en paramètre un délégué (System.Threading.ThreadStart
)
qui n'est autre qu'un pointeur vers la méthode destinée à être exécutée.
Contrairement à Java qui impose comme point d'exécution d'un Tread la fonction
run()
, il est
possible en C# de spécifier n'importe quelle fonction
En
Java, toutes les classes héritent des méthodes wait()
, notify()
and notifyAll()
situées dans
java.lang.Object.
Les méthodes équivalentes en C# sont Wait(), Pulse() and
PulseAll() se trouvant dans la classe System.Threading.Monitor.
L'example
ci-dessous nous déroule le scénario dans lequel des threads sont lancés dans un
ordre bien défini et doivent être traités dans le même ordre. Comme l'exécution
des threads est par nature non-déterministe, ceux qui arriveront après les
autres devront attendre leur tour à l'aide de la méthode wait().
C# Code
using
System;
using
System.Threading;
using
System.Collections;
public
class WorkerThread
{
private
int idNumber;
private
static int
num_threads_made = 1;
private
ThreadSample owner;
public
WorkerThread(ThreadSample owner)
{
idNumber = num_threads_made;
num_threads_made++;
this.owner =
owner;
}/* WorkerThread()
*/
//sleeps for a
random amount of time to simulate working on a task
public void PerformTask()
{
Random r = new Random((int) DateTime.Now.Ticks);
int timeout = (int) r.Next() % 1000;
if(timeout < 0)
timeout *= -1;
//Console.WriteLine(idNumber +
":A");
try
{
Thread.Sleep(timeout);
}
catch (ThreadInterruptedException
e)
{
Console.WriteLine("Thread #" + idNumber + "
interrupted");
}
//Console.WriteLine(idNumber +
":B");
owner.workCompleted(this);
}/*
performTask() */
public
int getIDNumber() {return idNumber;}
}
// WorkerThread
public
class ThreadSample
{
private
static Mutex m = new Mutex();
private
ArrayList threadOrderList = new ArrayList();
private
int NextInLine()
{
return (int) threadOrderList[0];
}
private
void RemoveNextInLine()
{
threadOrderList.RemoveAt(0);
//all threads have shown up
if(threadOrderList.Count ==
0)
Environment.Exit(0);
}
public
void workCompleted(WorkerThread
worker)
{
try
{
lock(this)
{
while(worker.getIDNumber() !=
NextInLine())
{
try
{
//wait for some other thread to finish
working
Console.WriteLine ("Thread #" + worker.getIDNumber() +
"
is waiting for Thread #" +
NextInLine() + " to show up.");
Monitor.Wait(this,
Timeout.Infinite);
}
catch (ThreadInterruptedException e)
{}
}//while
Console.WriteLine("Thread #" + worker.getIDNumber() + " is home
free");
//remove this ID number from the list of
threads yet to be seen
RemoveNextInLine();
//tell the other threads to
resume
Monitor.PulseAll(this);
}
}
catch(SynchronizationLockException){Console.WriteLine("SynchronizationLockException
occurred");}
}
public
static void
Main(String[] args)
{
ThreadSample ts = new ThreadSample();
/* Launch 25 threads */
for(int
i=1; i <= 25; i++)
{
WorkerThread wt = new WorkerThread(ts);
ts.threadOrderList.Add(i);
Thread t = new Thread(new
ThreadStart(wt.PerformTask));
t.Start();
}
Thread.Sleep(3600000); //wait for it all to
end
}/*
main(String[]) */
}//ThreadSample
Java Code
import
java.util.*;
class
WorkerThread extends
Thread
{
private
Integer idNumber;
private static int num_threads_made = 1;
private ThreadSample owner;
public WorkerThread(ThreadSample
owner)
{
super("Thread
#" + num_threads_made);
idNumber = new
Integer(num_threads_made);
num_threads_made++;
this.owner =
owner;
start(); //calls run and starts the
thread.
}/* WorkerThread() */
//sleeps for a random amount of time to simulate working on
a task
public void
run()
{
Random
r = new Random(System.currentTimeMillis());
int timeout = r.nextInt() % 1000;
if(timeout
< 0)
timeout *= -1 ;
try{
Thread.sleep(timeout);
} catch (InterruptedException e){
System.out.println("Thread #" + idNumber + "
interrupted");
}
owner.workCompleted(this);
}/* run() */
public Integer
getIDNumber() {return
idNumber;}
}
// WorkerThread
public
class ThreadSample{
private
Vector threadOrderList = new Vector();
private Integer
nextInLine(){
return (Integer)
threadOrderList.firstElement();
}
private void
removeNextInLine(){
threadOrderList.removeElementAt(0);
//all threads have shown up
if(threadOrderList.isEmpty())
System.exit(0);
}
public
synchronized void workCompleted(WorkerThread
worker){
while(worker.getIDNumber().equals(nextInLine())==false)
{
try {
//wait for some other thread to finish
working
System.out.println (Thread.currentThread().getName() + " is waiting for
Thread #" +
nextInLine() + " to show up.");
wait();
} catch (InterruptedException e)
{}
}//while
System.out.println("Thread #" + worker.getIDNumber() + " is home
free");
//remove this ID number from the list of
threads yet to be seen
removeNextInLine();
//tell the other threads to
resume
notifyAll();
}
public static void main(String[]
args) throws
InterruptedException
{
ThreadSample ts = new ThreadSample();
/* Launch 25 threads */
for(int
i=1; i <= 25; i++)
{
new WorkerThread(ts);
ts.threadOrderList.add(new Integer(i));
}
Thread.sleep(3600000); //wait for it all to
end
}/* main(String[]) */
}//ThreadSample
Le
mot-clé volatile
permet en Java d'interdire aux compilateurs de réaliser certaines optimisations
de code telles que le déplacement des variables du tas vers la pile. Ce genre
d'optimisation, dans la plupart des cas n'a aucun effet nocif sur le déroulement
de 99% de vos applications, mais dans certaines circonstances, cela peut mener à
bien des casses-têtes difficles à résoudre.
Il
existe des différences fondamentales dans la sémantique de l'ordre volatile en
C# et Java qui sont illustrées dans l'exemple ci-dessous tiré de l'article
suivant : The "Double-Checked Locking is Broken"
Declaration
C# Code
/*
Used to lazily instantiate a singleton class */
/*
WORKS AS EXPECTED
*/
class
Foo
{
private
volatile Helper helper = null;
public
Helper getHelper()
{
if (helper == null)
{
lock(this)
{
if (helper == null)
helper = new
Helper();
}
}
return helper;
}
}
Java Code
/*
Used to lazily instantiate a singleton class */
/*
BROKEN UNDER CURRENT SEMANTICS FOR VOLATILE */
class
Foo
{
private
volatile Helper helper = null;
public
Helper getHelper()
{
if (helper == null)
{
synchronized(this)
{
if
(helper == null)
helper = new
Helper();
}
}
return helper;
}
}
Bien que le code précédent paraît identique excepté l'utilisation de synchronized en Java et du lock en C#, il n'est pas garantie que la version Java fonctionne sur toutes les machines virtuelles. A l'heure actuelle, le modèle mémoire de Java n'interdit pas le ré-agencement dans l'ordre des affectations de variables volatile ou pas, et il ainsi possible qu'un objet soit créé avant que la référence helper ne pointe vers le nouvel objet. Dans ce cas, deux objets sont créées et une référence vers un objet incomplet peut être retourné. En C#, la sémantique de l'ordre volatile prend en compte ce genre de problème et l'ordre des opérations de lecture et écriture ne peut être modifiée avant une affectation de variable volatile. De plus, en C#, annoter une variable volatile interdit au compilateur JIT de placer des variables dans des registres et assure qu'elles seront stockées en mémoire (tas).
Pour plus d'informations sur ce sujet bien connu des développeurs Java, n'hésitez pas à parcourir l'article de Javaworld sur le thème "Double Checked locking mecanism" : Double-checked locking: Clever, but broken.
La surcharge d'opérateurs autorise la redéfinition de la sémantique d'un opérateur (+,=,...) dans un contexte particulier. Ainsi, l'égalité de deux chaînes de caractères se résume à comparer leur valeur. Par défaut, le langage vous fournira une égalité de référence, difficilement applicable dans ce cas précis.
Cette caractéristique du langage a toujours été source de nombreuses discussions acharnées de la part des développeurs car son abus peut conduire à divers problèmes. Par exemple, l'utilisation de la surcharge des opérateurs "++" et "--" dans le cas d'une connexion ou déconnexion d'une socket réseau peut rendre un programme source difficilement lisible. D'un autre coté, la surcharge de l'opérateur [] pour renvoyer une copie d'un tableau membre est une alternative intéressante pour garantir l'encapsulation.
La surcharge d'opérateurs tend à être utile lorsqu'elle est utilisée de manière intuitive en accord avec le type des classes manipulées. Ainsi, il est judicieux de s'en servir pour les collections ([]), le calcul matriciel (+ et -), les calculs de nombres complexes ou la comparaison de types spécifiques induisant la modification de l'égalité sémantique (chaînes, classes monnaie, ...).
Le code ci-dessous vous illustre différentes utilisations de la surcharge d'opérateurs en C#.
NOTE:
Contrairement à C++, C# n'autorise pas la surcharge des opérateurs suivants ;
new
, ( )
, ||
, &&
, =
.
C# Code
using
System;
class
OverloadedNumber
{
private
int value;
public
OverloadedNumber(int value)
{
this.value = value;
}
public
override string ToString()
{
return value.ToString();
}
public
static OverloadedNumber operator -(OverloadedNumber
number)
{
return new OverloadedNumber(-number.value);
}
public
static OverloadedNumber operator +(OverloadedNumber number1, OverloadedNumber
number2)
{
return new OverloadedNumber(number1.value + number2.value);
}
public
static OverloadedNumber operator ++(OverloadedNumber
number)
{
return new OverloadedNumber(number.value + 1);
}
}
public
class OperatorOverloadingTest
{
public
static void
Main(string[] args)
{
OverloadedNumber number1 = new
OverloadedNumber(12);
OverloadedNumber number2 = new
OverloadedNumber(125);
Console.WriteLine("Increment: {0}", ++number1);
Console.WriteLine("Addition: {0}", number1 +
number2);
}
}
//
OperatorOverloadingTest
Il
existe deux différences majeures entre l'instruction switch
de C# et Java. C#
autorise l'utilisation de chaînes de caractères et interdit le "Fall-through",
c'est à dire la possibilité pour un label donné d'exécuter plusieurs lignes
lorsque l'instruction "break" est absente. Cela n'est possible que lorsque le
label en question ne propose aucune instruction (cf exemple ci-dessous). Les
"Fall-through" ont été explicitement désactivés en C# car ils sont source
d'erreurs (bugs difficiles à identifier).
C# Code
switch(foo)
{
case
"A":
Console.WriteLine("A
seen");
break;
case
"B":
case
"C":
Console.WriteLine("B
or C seen");
break;
/* ERROR: Won't compile due to fall-through at case "D"
*/
case
"D":
Console.WriteLine("D
seen");
case
"E":
Console.WriteLine("E
seen");
break;
}
Les Assemblies en C# ont de nombreux points communs avec les fichiers archives JAR de Java. Une Assembly est une unité de déploiement primaire contenant un ou plusieurs Namespaces (ou packages).
Tout comme Java, les Assemblies contiennent du code MSIL (byte-code), un ensemble de méta-données et des ressources requises par les classes. N'hésitez pas à consulter l'article sur la structure des Assemblies dans ce même site pour plus d'informations.
De plus, plusieurs traitements tels que la gestion de la sécurité, le déploiement ou le versionning sont réalisés au niveau de l'Assembly. En Java, ces configurations s'effectuent au niveau du fichier ZIP ou JAR ou directement sur l'ensemble des classes non compactées. Alors que les compilateurs C# génèrent des Assemblies sous la forme de fichiers .DLL ou .EXE, les exécutables Java sont des archives (JAR ou ZIP).
Un certain nombre de langages de programmation populaires contiennent des Frameworks de gestion de Collections consistant à proposer une implémentation de structures de données pré-définies (Tableaux, Listes chainées, Dictionnaires, ...). L'avantage de ces Frameworks est d'éviter au développeur la tâche consistant à coder tous les algorithmes de parcours, tri et création de collections.
Dans
C#, les collections sont contenues dans le namespace System.Collections.
Ce
namespace contient l'ensemble des interfaces et classes représentant des types
de données abstraits tels que IList, IEnumerable, IDictionary, ICollection et
CollectionBase. Ces classes permettent de manipuler des structures de données
indépendamment de leur implémentation. De plus, le namespace System.Collections
propose des
implémentations concrètes du type ArrayList, Queue, SortedList et Stack.
L'avantage de C# par rapport à Java concernant les collections réside dans la
manière dont est gérée la synchronisation. Ainsi, en C# il suffit d'utiliser un
wrapper synchronisé alors qu'en Java certaines classes sont synchronisées
(ArrayList, ...) et d'autres ne le sont pas (Vector, Hashtable, ..).
Les
collections en Java contiennent des classes, interfaces et classes abstraites
dans le package java.util.
S'il existe
énormément de similitudes avec C#, nous pouvons considérer que le package Java
demeure légèrement plus complet que son homologue en proposant des
caractéristiques supplémentaires. Ainsi, vous trouverez en Java des classes
"ensemblistes" non ordonnées représentées sous l'appellation "set" avec
les TreeSet, HashSet, ...
Contrairement
à Java, C# contient l'instruction goto
qui peut être utilisée
pour réaliser un saut direct vers un label dans un programme.
C# Code
using
System;
using
System.Net.Sockets;
class
GotoSample
{
public
static void
Main(string[] args)
{
int num_tries = 0;
retry:
try
{
num_tries++;
Console.WriteLine("Attempting to connect to network. Number of tries =" +
num_tries);
//Attempt to connect to a network times out
//or some some other network connection error
that
//can be recovered
from
throw new SocketException();
}
catch(SocketException)
{
if(num_tries <
5)
goto retry;
}
}/*
Main(string[]) */
}//GotoSample
Un des concepts les plus important de la programmation orientée objet est le sacro-saint Polymorphisme. Le Polymorphisme vous aide à interagir avec les membres d'une hiérarchie de classes en utilisant le type le plus générique. Concrètement, cela signifie que vous devez posséder des méthodes dans la classe de base qui seront ensuite redéfinies dans les classes filles. Par exemple, prenons le cas de la classe MoyenDeLocomotion contenant la fonction démarrer(), elle-même redéfinie ou implémentée dans la sous-classe Voiture. Le client utilisant le type générique MoyenDeLocomotion appellera donc la méthode démarrer() sans se préoccuper du type concret manipulé. La méthode démarrer() dans cet exemple est appelée méthode virtuelle.
En
Java, toutes les méthodes sont par défaut virtuelles alors qu'en C#, comme en
C++, le développeur doit explicitement l'indiquer. C'est pourquoi, C# propose un
mot-clé n'ayant pas d'équivalent en Java : virtual.
Ici encore, cette
caractéristique a toujours été source de moult discussions entre pour et contre.
Néanmoins, il faut noter que la nature virtuelle des méthodes constitue un frein
aux optimisations de la machine virtuelle devant prévoir l'extensibilité de la
classe en question. Une autre raison dans le choix de C# pourrait s'expliquer
par le fait que les méthodes virtuelles peuvent être accidentellement redéfinies
par le développeur dans une sous-classe. Dans ce cas, l'appel à la méthode de la
classe mère sera masqué par la méthode fille et pourrait engendrer des résultats
inexplicables.
Enfin,
Java et C# proposent respectivement les mots-clé final
et sealed
permettant d'interdire
toute redéfinition d'une méthode ou dérivation d'une classe.
Les exemples ci-dessous vous illustrent l'utilisation des méthodes virtuelles en C# et Java.
C# Code
using
System;
public
class Parent
{
public
void DoStuff(string str)
{
Console.WriteLine("In Parent.DoStuff: " + str);
}
}
public
class Child: Parent
{
public
void DoStuff(int n)
{
Console.WriteLine("In Child.DoStuff: " + n);
}
public
void DoStuff(string str)
{
Console.WriteLine("In Child.DoStuff: " + str);
}
}
public
class VirtualTest
{
public
static void
Main(string[] args)
{
Child ch = new Child();
ch.DoStuff(100);
ch.DoStuff("Test");
((Parent) ch).DoStuff("Second Test");
}
}//VirtualTest
OUTPUT
:
In Child.DoStuff: 100
In Child.DoStuff: Test
In Parent.DoStuff: Second Test
Java Code
class
Parent
{
public
void DoStuff(String str)
{
System.out.println("In Parent.DoStuff: "
+ str);
}
}
class
Child extends Parent
{
public
void DoStuff(int n)
{
System.out.println("In Child.DoStuff: " + n);
}
public
void DoStuff(String str)
{
System.out.println("In Child.DoStuff: " + str);
}
}
public
class VirtualTest
{
public
static void
main(String[] args)
{
Child ch = new Child();
ch.DoStuff(100);
ch.DoStuff("Test");
((Parent) ch).DoStuff("Second Test");
}
}//VirtualTest
OUTPUT
:
In Child.DoStuff: 100
In Child.DoStuff: Test
In Child.DoStuff: Second Test
Le
comportement de l'exemple précédent en Java peut être reproduit en C# en
marquant la méthode DoStuff(string) dans la classe mère virtual
et en spécifiant dans
la méthode de la classe fille DoStuff(string) le mot-clé override
.
C# Code
using
System;
public
class Parent
{
public
virtual void
DoStuff(string str)
{
Console.WriteLine("In Parent.DoStuff: " + str);
}
}
public
class Child: Parent
{
public
void DoStuff(int n)
{
Console.WriteLine("In Child.DoStuff: " + n);
}
public
override void
DoStuff(string str)
{
Console.WriteLine("In Child.DoStuff: " + str);
}
}
public
class VirtualTest
{
public
static void
Main(string[] args)
{
Child ch = new Child();
ch.DoStuff(100);
ch.DoStuff("Test");
((Parent) ch).DoStuff("Second Test");
}
}//VirtualTest
OUTPUT
:
In Child.DoStuff: 100
In Child.DoStuff: Test
In Child.DoStuff: Second Test
L'exemple précédent peut encore être modifié pour reproduire le résultat initial en modifiant la signature de la méthode DoStuff(string) dans la classe fille ainsi :
public
new void DoStuff(string str)
indique que même si la méthode DoStuff de la classe mère est virtuelle, la classe fille peut implémenter "sa" propre version de la méthode sans redéfinir la précédente (elle peut être appelée ainsi : parent.DoStuff())
Les deux langages supportent les classes d'entrées/sorties. Les exemples ci-dessous réalisent une copie du contenu d'un fichier appelé "input.txt" vers un autre fichier "output.txt".
C# Code
using
System;
using
System.IO;
public
class FileIOTest
{
public
static void
Main(string[] args)
{
FileStream inputFile = new FileStream("input.txt",
FileMode.Open);
FileStream outputFile = new
FileStream("output.txt", FileMode.Open);
StreamReader sr
= new
StreamReader(inputFile);
StreamWriter sw
= new
StreamWriter(outputFile);
String str;
while((str = sr.ReadLine())!= null)
sw.Write(str);
sr.Close();
sw.Close();
}
}//FileIOTest
Java Code
import
java.io.*;
public
class FileIO{
public
static void
main(String[] args) throws IOException
{
File inputFile = new File("input.txt");
File outputFile = new
File("output.txt");
FileReader in = new FileReader(inputFile);
BufferedReader br = new
BufferedReader(in);
FileWriter out =
new
FileWriter(outputFile);
BufferedWriter bw = new
BufferedWriter(out);
String str;
while((str = br.readLine())!= null)
bw.write(str);
br.close();
bw.close();
}
}//FileIOTest
La persistance d'objets, connue aussi sous le terme Serialisation est la caractéristique consistant à lire et à écrire l'état d'un objet (la valeur de ses attributs) dans un flux quelconque (fichier texte, socket réseau, ...). La Sérialisation est utile lorsqu'il s'agit de sauvegarder l'état d'un graphe d'objets entre plusieurs invocations de méthodes. Vous êtes en mesure de reconstruire entièrement l'état de votre application suite à un crash ou tout autre arrêt inopiné. De plus, la Sérialisation vous permet de faire transiter des objets à travers le réseau dans le but de transférer des objets entre plusieurs applications. Ce mécanisme est fortement utilisé dans les architectures distribuées avec RMI,CORBA ou encore .NET Remoting.
Les
objets sérialisables en C# sont marqués par l'attribut de Runtime [Serializable].
L'attribut
[Serializable]
indique que les membres de la classe ne
peuvent pas être sérialisés. Ces membres sont en règle générale des variables
temporaires ou des champs n'ayant aucune signification lorsqu'elles sont
persistantes (Thread, ...). C# fournit deux formats de sérialisation : XML et un
format binaire propriétaire.
Vous avez la possibilité d'implémenter votre
propre format de sérialisation par implémentation de l'interface ISerializable
.
En
Java, les objets sérialisables sont ceux qui implémentent l'interface
Serializable
alors que les champs non sérialisables
sont marqués avec le mot-clé transient
. Par défaut, Java
supporte la sérialisation d'objets sous un format binaire propriétaire mais
fournit, comme en C#, un mécanisme
permettant soit d'étendre le format initial (readObject
et writeObject
), soit de ré-écrire
entièrement le processus de sérialisation (interface Externalizable
)
private
void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException;
private void writeObject(java.io.ObjectOutputStream stream) throws IOException
C# Code
using
System;
using
System.IO;
using
System.Reflection;
using
System.Runtime.Serialization;
using
System.Runtime.Serialization.Formatters.Binary;
using
System.Runtime.Serialization.Formatters.Soap;
[Serializable]
class
SerializeTest
{
[NonSerialized]
private
int x;
private int y;
public
SerializeTest(int a, int b)
{
x = a;
y = b;
}
public
override String
ToString()
{
return "{x=" + x + ", y=" + y + "}";
}
public
static void
Main(String[] args)
{
SerializeTest st = new SerializeTest(66,
61);
Console.WriteLine("Before Binary Write := " + st);
Console.WriteLine("\n Writing SerializeTest object to
disk");
Stream output =
File.Create("serialized.bin");
BinaryFormatter bwrite = new
BinaryFormatter();
bwrite.Serialize(output, st);
output.Close();
Console.WriteLine("\n Reading SerializeTest object from
disk\n");
Stream input =
File.OpenRead("serialized.bin");
BinaryFormatter bread = new
BinaryFormatter();
SerializeTest fromdisk = (SerializeTest)bread.Deserialize(input);
input.Close();
/* x will be 0 because it won't be read from
disk since non-serialized */
Console.WriteLine("After Binary Read := " +
fromdisk);
st = new SerializeTest(19, 99);
Console.WriteLine("\n\nBefore SOAP(XML) Serialization := " +
st);
Console.WriteLine("\n Writing SerializeTest object to
disk");
output =
File.Create("serialized.xml");
SoapFormatter swrite = new
SoapFormatter();
swrite.Serialize(output, st);
output.Close();
Console.WriteLine("\n Reading SerializeTest object from
disk\n");
input =
File.OpenRead("serialized.xml");
SoapFormatter sread = new
SoapFormatter();
fromdisk = (SerializeTest)sread.Deserialize(input);
input.Close();
/* x will be 0 because it won't be read from
disk since non-serialized */
Console.WriteLine("After SOAP(XML) Serialization := " +
fromdisk);
Console.WriteLine("\n\nPrinting XML Representation of
Object");
XmlDocument doc =
new XmlDocument();
doc.Load("serialized.xml");
Console.WriteLine(doc.OuterXml);
}
}
Java Code
import java.io.*;
class
SerializeTest implements
Serializable{
transient int x;
private int y;
public
SerializeTest(int a, int b){
x = a;
y = b;
}
public String
toString(){
return "{x=" + x + ",
y=" + y + "}";
}
public static void main(String[]
args) throws Exception{
SerializeTest st = new SerializeTest(66,
61);
System.out.println("Before Write := " +
st);
System.out.println("\n Writing SerializeTest object to
disk");
FileOutputStream out = new
FileOutputStream("serialized.txt");
ObjectOutputStream so = new
ObjectOutputStream(out);
so.writeObject(st);
so.flush();
System.out.println("\n Reading
SerializeTest object from disk\n");
FileInputStream in = new
FileInputStream("serialized.txt");
ObjectInputStream si = new ObjectInputStream(in);
SerializeTest fromdisk =
(SerializeTest)si.readObject();
/* x will be 0 because it won't be read from
disk since transient */
System.out.println("After Read := " +
fromdisk);
}
}
Java et C# proposent tous les deux un mécanisme de génération de documentation à partir des sources. Il suffit de respecter une certaine forme d'écriture des commentaires, lesquels seront traités (parsing) par un outil afin de produire une documentation sous la forme de fichiers texte. La langage Java a été le premier à intégrer cette technique et C# s'en est inspiré.
Javadoc est l'outil utilisé pour extraire à partir des sources l'ensemble de la documentation. Il génère du code HTML à partir des commentaires du programme. Le meilleur exemple d'utilisation de Javadoc est la documentation du JDK.
Les différentes méta-données pouvant être spécifiées sont les suivantes :
o La description de la méthode
o Les exceptions levées par la méthode.
o Les paramètres acceptés
o Les paramètres de retour.
o Les méthodes associées et attributs membre.
o Une indication sur la caractère obsolète de la méthode (deprecated).
o La version de l'API dans laquelle elle a été créée.
L'information sur le caractère obsolète d'une méthode est aussi utilisée par le compilateur pour générer des messages d'avertissement ou d'erreurs.
Javadoc fournit automatiquement les informations suivantes :
o Les API héritées
o La liste des classe dérivées
o Un classement hiérarchique, alphabétique de vos classes
o Un index contenant la liste des méthodes et variables utilisées
Java Code
/**
* Calculates the square of a number.
* @param num the number to calculate.
* @return the square of the number.
* @exception NumberTooBigException this
occurs if the square of the number
* is too big to be stored in an int.
*/
public
static int
square(int num) throws NumberTooBigException{}
C#
utilise XML comme format pour la génération de documentation. Les fichiers
contiennent les méta-données spécifiées par l'utilisateur ainsi que des
informations supplémentaires générées automatiquement. Il existe très peu de
différences entre les types de méta-données C# et Java à quelques exceptions
près (@author
,
@version
, ou
@deprecated
, ...).
L'avantage indéniable de la génération de documentation en XML réside dans
l'indépendance du format de stockage par rapport à la présentation. Ainsi, il
suffit d'associer une feuille de style XSLT au document pour pouvoir générer du
HTML, des fichiers texte ou encore du PDF. Microsoft fournit par défaut une
feuille de style (HTML).
C# Code
///<summary>Calculates
the square of a number.</summary>
///<param
name="num">The
number to calculate.</param>
///<return>The
square of the number. </return>
///<exception>NumberTooBigException
- this occurs if the square of the number is too big to be stored in an int.
</exception>
public
static int
square(int num){}
Il est possible dans les deux langages d'inclure plusieurs déclarations de classes dans le même fichier avec plusieurs différences notables. Ainsi, en Java il ne peut y avoir qu'une seule classe de visibilité publique par fichier et elle doit posséder absolument le même nom que le fichier en question. Ces contraintes n'existent pas en C#, vous pouvez définir plusieurs classes publiques et leur donner un nom quelconque sans rapport avec le fichier.
L'utilisation
d'une bibliothèque dans une application est une opération à deux étapes. La
première étape consiste à référencer la bibliothèque dans le programme source en
utilisant les directives using
pour C# et import
pour Java. La deuxième
étape consiste à indiquer au compilateur le chemin des dites bibliothèques. En
Java, cette opération se fait à l'aide de la variable d'environnement
CLASSPATH
ou d'une
option du compilateur : -classpath
. En C#, seule
l'option du compilateur /r
est utilisée.
Le
modèle de programmation évènementielle consiste à mettre en place un mécanisme
permettant de notifier des objets abonnés à un évènement particulier. Ce modèle
est souvent associé au design pattern observer/observable et s'utilise en
général dans la conception d'interfaces graphiques (IHM). Java et C# proposent
tous les deux des mécanismes supportant les évènements mais avec une
implémentation totalement différente. Ainsi, en Java, java.util.EventObject
est la
classe mère de tous les évènements et possède un ensemble de méthodes permettant
d'identifier la source de l'évènement. Ensuite, un abonné appelé
Listener, identifié à l'aide d'interfaces existantes, devra implémenter
une méthode dite de callback. Vous remarquerez que Java traite la gestion
d'évènements avec un typage fort. Vous trouverez donc dans les APIs
MouseListener, ActionListener, KeyListener, .. contenant toutes des méthodes de
callback en relation avec la sémantique de l'évènement (keyPressed,
actionPerformed, ...)
C# utilise
les délégués pour fournir un mécanisme explicite permettant de gérer
l'abonnement/notification. Les délégués sont concrètement des pointeurs de
fonction et un évènement est généralement un type dérivé de System.EventArgs
. Ainsi,
lorsqu'un évènement apparaît, il suffit de passer au gestionnaire abonné la
source et l'évènement en question de la manière suivante : new YourEventArgs(inits).
L'émetteur possède une méthode protected
précédée en général
de "On" (e.g OnClick, OnClose, OnInit, etc...) invoquée lorsqu'un évènement
spécifique apparaît. Cette méthode redirige ensuite l'appel au délégué
spécifique en passant l'objet EventArgs préalablement initialisé. Les méthodes
"OnXX" sont protected pour permettre à une classe dérivée d'y faire appel.
L'abonné est une méthode (contrairement à Java où c'est une classe) acceptant
les mêmes arguments et paramètres de retour que le délégué. En règle générale,
les délégués possèdent deux arguments : Object
pour l'objet source et
EventArgs
pour le
contenu de l'évènement, et void
comme paramètre de
retour.
En
C#, le mot-clé event
spécifie un type
particulier de délégué : ceux chargés de traiter des évènements. Pendant la
phase de compilation, le compilateur surcharge les opérateurs += et -= afin de
simplifier l'opération d'enregistrement ou de suppression d'un callback. En
Java, il vous faut passer par des méthodes prévues à cet effet : addActionListener()
et
RemoveActionListener().
L'exemple ci-dessous vous illustre une classe générant 20 nombres aléatoires et produisant un évènement à chaque fois qu'un nombre pair est généré.
C# Code
using
System;
class
EvenNumberEvent: EventArgs
{
/* HACK:
fields are typically private, but making this internal so
it
* can be accessed from other classes. In
practice should use properties.
*/
internal
int number;
public
EvenNumberEvent(int number):base()
{
this.number =
number;
}
}
class
Publisher
{
public
delegate void
EvenNumberSeenHandler(object sender, EventArgs
e);
public
event EvenNumberSeenHandler EvenNumHandler;
protected void
OnEvenNumberSeen(int num)
{
if(EvenNumHandler!= null)
EvenNumHandler(this, new EvenNumberEvent(num));
}
//generates 20
random numbers between 1 and 20 then causes and
//event to
occur if the current number is even.
public
void RunNumbers()
{
Random r = new Random((int) DateTime.Now.Ticks);
for(int
i=0; i < 20; i++)
{
int current = (int) r.Next(20);
Console.WriteLine("Current number is:" + current);
//check if number is even and if so initiate
callback call
if((current % 2) ==
0)
OnEvenNumberSeen(current);
}//for
}
}//Publisher
public
class EventTest
{
//callback
function that will be called when even number is
seen
public
static void
EventHandler(object sender, EventArgs
e)
{
Console.WriteLine("\t\tEven Number Seen:" +
((EvenNumberEvent)e).number);
}
public
static void
Main(string[] args)
{
Publisher pub = new Publisher();
//register the callback/subscriber
pub.EvenNumHandler += new
Publisher.EvenNumberSeenHandler(EventHandler);
pub.RunNumbers();
//unregister the callback/subscriber
pub.EvenNumHandler -= new
Publisher.EvenNumberSeenHandler(EventHandler);
}
}
Java Code
import
java.util.*;
class
EvenNumberEvent extends EventObject
{
public
int number;
public
EvenNumberEvent(Object source, int number)
{
super(source);
this.number =
number;
}
}
interface
EvenNumberSeenListener
{
void
evenNumberSeen(EvenNumberEvent ene);
}
class
Publisher
{
Vector subscribers = new Vector();
private
void OnEvenNumberSeen(int num)
{
for(int
i=0, size = subscribers.size(); i < size; i++)
((EvenNumberSeenListener)subscribers.get(i)).evenNumberSeen(new EvenNumberEvent(this, num));
}
public
void
addEvenNumberEventListener(EvenNumberSeenListener ensl)
{
subscribers.add(ensl);
}
public
void
removeEvenNumberEventListener(EvenNumberSeenListener ensl)
{
subscribers.remove(ensl);
}
//generates 20
random numbers between 1 and 20 then causes and
//event to
occur if the current number is even.
public
void RunNumbers()
{
Random r = new
Random(System.currentTimeMillis());
for(int
i=0; i < 20; i++)
{
int current = (int) r.nextInt() % 20;
System.out.println("Current number is:"
+ current);
//check if number is even and if so initiate
callback call
if((current % 2) ==
0)
OnEvenNumberSeen(current);
}//for
}
}//Publisher
public
class EventTest implements
EvenNumberSeenListener
{
//callback
function that will be called when even number is seen
public
void evenNumberSeen(EvenNumberEvent
e)
{
System.out.println("\t\tEven Number Seen:" +
((EvenNumberEvent)e).number);
}
public
static void
main(String[] args)
{
EventTest et = new
EventTest();
Publisher pub = new Publisher();
//register the callback/subscriber
pub.addEvenNumberEventListener(et);
pub.RunNumbers();
//unregister the callback/subscriber
pub.removeEvenNumberEventListener(et);
}
}
L'interopérabilité inter-langages est la capacité à intégrer plusieurs applications écrites dans différents langages. Il existe plusieurs manières de ré-utiliser du code C/C++ en Java. Tout d'abord, JNI (Java Native Interface) a été créé dans ce but. Les classes natives (non Java) appelées doivent respecter un certain modèle de développement utilisant des types bien spécifiques propres à l'API JNI. Pour créer une telle application il vous faut respecter les étapes suivantes :
1.
Créer un programme Java contenant une méthode identifiée
par le mot-clé native
2.
Charger la bibliothèque correspondante au code C/C++ à
l'aide de la méthode loadLibrary()
3. Compiler la classe contenant la déclaration des méthodes natives
4.
Utiliser le générateur de stubs javah
avec l'option -jni
afin de générer les ".h"
nécessaires aux invocations
5. Coder l'implémentation de la méthode native (généralement du C ou du C++).
6. Compiler les fichiers ".h" et les fichiers source dans une bibliothèque partagée (.dll,.so, ...)
Cette méthode assure que le code source Java est totalement indépendant de la bibliothèque native utilisée. Ainsi, tout portage vers d'autres plate-formes s'en trouve simplifié.
Java propose aussi la possibilité d'intéragir avec un système distribué via un ORB (Object Request Broker) tel que Corba ou Rmi. Le middleware est responsable d'assurer la partie communication et marshalling des paramètres entre un objet Java et un objet C++. Sans chercher aussi loin, les WebServices avec Soap sont aussi une solution à ce style de problème. Bien entendu, ces techniques supposent de mettre en place une mécanique assez lourde et parfois inutile lorsque les deux applications n'ont aucune vocation à être distribuées.
Avec une interopérabilité binaire, les objets sont en mesure d'interagir entre eux de manière plus intégré. Ainsi, il est possible d'hériter de l'interface ou de la classe d'un objet écrit dans un autre langage ou de partager les même types. Les compilateurs et autres débogueurs se basent uniquement sur le byte-code généré qui est le même pour tous les langages. C'est l'approche de .NET.
La CLR se charge de l'environnement d'exécution et la CLS assure l'homogénéité du code binaire exécuté (MSIL) à travers un ensemble de types unifiés (CTS). Il existe à l'heure actuelle plus d'une dizaine de compilateurs proposant la génération de code binaire .NET.
Un autre aspect de l'interopérabilité inter-langage supporté par C# est l'interaction avec les objets COM. Le Framework propose des mécanismes permettant d'appeler simplement un objet COM à partir de code C#. Il suffit d'utiliser des outils fournis par le Framework et permettant de générer des classes .NET Proxy prenant en charge le passage entre les deux mondes (tlbimp et tlbexp).
Les
programmes C# peuvent aussi appeler une fonction située dans n'importe quelle
DLL en utilisant le mot-clé
extern
associé à l'attribut DllImport
. L'avantage de cette
technique est que la méthode de la DLL en question n'a pas besoin d'être
nécessairement écrite pour C# et l'inconvénient réside dans le caractère non
portable de cette technique puisque l'invocation est fortement liée au format
binaire des DLL Win32 spécifiques à Microsoft. Aussi, c'est un autre moyen
simple de ré-utiliser du code "non managé" sans passer par des wrappers.
Indéniablement, C# propose plus d'alternatives que Java concernant l'intégration et l'interopérabilité avec les applications développées dans d'autres langages. D'un autre coté, Java insiste sur le caractère portable de son architecture et rejette totalement toute instruction liée à une plate-forme spécifique. Mais, il apparaît de plus en plus que le cloisonnement de la plate-forme Java autour d'un seul langage est une stratégie de moins en moins payante car la communauté de développeurs ne semble pas encore prête à accepter le monopole d'un et seul langage. Techniquement, il est tout à fait envisageable d'intégrer la compilation de code C#, VB ou Eiffel en byte-code Java, mais Sun le veut-il vraiment ?
Pour
fournir un contrôle total sur la libération des ressources utilisées par les
objets, C# propose l'interface System.IDisposable
contenant la
méthode Dispose()
pouvant être appelée directement par un utilisateur lorsqu'il décide de libérer
une ressource. L'utilisateur peut
ainsi spécifier de manière explicite qu'il désire libérer des objets qui ne sont
plus utilisés. Cette technique est à différencier de la Finalisation proposée
par Java et C# qui n'assure pas une libération "déterministe" des objets.
Concrètement, le fait d'appeler la méthode Dispose()
invoque la méthode
GC.SuppressFinalisation(this)
qui supprime l'objet de la file de finalisation. Ainsi, l'objet peut-être libéré
sans avoir à attendre que le Thread de finalisation se lance.
Il
existe une autre manière plus simple de libérer explicitement des objets en
utilisant l'instruction using
tel que décrit dans
l'exemple suivant.
Si
une classe est "disposable", il est préférable de faire usage de la méthode
Dispose()
une et
une seule fois. Ainsi, de multiples appels successifs à Dispose()
n'auront aucun effet.
Vous pouvez implémenter un mécanisme basé sur des flags permettant de contrôler
le fait que la classe ait été déjà libérée. L'exemple ci-dessous nous illustre
un programme dans lequel une classe référence un fichier ouvert jusqu'à ce que
le méthode Dispose() soit appelée.
C# Code
using
System;
using
System.IO;
public
class MyClass : IDisposable
{
bool
disposed = false;
FileStream f;
StreamWriter sw;
private
String name;
private
int numShowNameCalls = 0;
MyClass(string name)
{
f = new FileStream("logfile.txt",
FileMode.OpenOrCreate);
sw = new
StreamWriter(f);
this.name = name;
Console.WriteLine("Created " + name);
}
~MyClass()
{
Dispose(false);
}
public
void Dispose()
{
if(!disposed)
{
Dispose(true);
}
}
private
void Dispose(bool disposing)
{
lock(this)
{ /* prevents multiple threads from disposing
simultaneously */
/* disposing variable is used to indicate if
this method was called from a
* Dispose() call or during
finalization. Since finalization order is not
* deterministic, the
StreamWriter may be finalized before this object in
* which case, calling
Close() on it would be inappropriate so we try to
* avoid that.
*/
if(disposing)
{
Console.WriteLine("Finalizing " + name);
sw.Close(); /* close file since object is done
with */
GC.SuppressFinalize(this);
disposed = true;
}
}
}
public
string ShowName()
{
if(disposed)
throw new
ObjectDisposedException("MyClass");
numShowNameCalls++;
sw.Write("ShowName() Call #" + numShowNameCalls.ToString() + "\n");
return "I am " + name;
}
public
static void
Main(string[] args)
{
using (MyClass mc = new MyClass("A MyClass
Object"))
{
for(int i = 0; i
< 10; i++)
{
Console.WriteLine(mc.ShowName());
} //for
}/* runtime calls Dispose on MyClass object
once "using" code block is exited, even if exception thrown */
}//Main
}
Ce
mécanisme est très proche des destructeurs de C++ et permet de ne pas se soucier
de la gestion des allocations mémoire.
La libération déterministe d'objet
est une avancée significative dans une prise en charge plus fine des Ramasse
miettes.
NOTE : Attention, l'appel à la méthode Dispose()
ne demande pas
directement au Ramasse miettes de libérer l'objet mais accélère ce processus en
outre-passant l'étape de finalisation. .
Les Délégués sont un mécanisme permettant d'implémenter des fonctions de callback. Ils sont très proche des pointeurs de fonction en C ou C++ et peuvent s'avérer très utiles dans certaines conditions. Par exemple pour déléguer les opérations de tri de collections ou de transformation de listes d'objets. Une autre utilisation de cette technique est la déclaration d'évènements sur le modèle émetteur/récepteur
L'exemple ci-dessous illustre le mécanisme de création et d'utilisation des délégués.
C# Code
using
System;
//delegate
base
public
class HasDelegates
{
// delegate
declaration, similar to a function pointer
declaration
public
delegate bool
CallbackFunction(string a, int b);
//method that
uses the delegate
public
bool execCallback(CallbackFunction doCallback,
string x, int
y)
{
Console.WriteLine("Executing Callback
function...");
if (doCallback == null)
throw ArgumentException("Callback can't
be null!");
return doCallback(x, y);
}
}
public
class FunctionDelegates
{
public
static bool
FunctionFoo(string a, int b)
{
Console.WriteLine("Foo: {0} {1}", a, b);
return true;
}
}
public
class DelegateTest
{
public
static void
Main(string[] args)
{
HasDelegates MyDel = new
HasDelegates();
//create delegate
HasDelegates.CallbackFunction myCallback =
new
HasDelegates.CallbackFunction(FunctionDelegates.FunctionFoo);
//pass delegate to delegate function
MyDel.execCallback(myCallback, "Twenty", 20);
}
} // DelegateTest
L'exemple suivant illustre l'utilisation d'un délégué statique :
C# Code
using
System;
//delegate
base
public
class HasDelegates
{
// delegate
declaration, similar to a function pointer
declaration
public
delegate bool
CallbackFunction(string a, int b);
//method that
uses the delegate
public
bool execCallback(CallbackFunction doCallback,
string x, int
y)
{
Console.WriteLine("Executing Callback
function...");
return doCallback(x, y);
}
}
public
class FunctionDelegates
{
public
static readonly HasDelegates.CallbackFunction
BarFuncCallback =
new
HasDelegates.CallbackFunction(FunctionBar);
public
static bool
FunctionBar(string a, int b)
{
Console.WriteLine("Bar: {0} {1}", b, a);
return true;
}
}
public
class DelegateTest
{
public
static void
Main(string[] args)
{
HasDelegates MyDel = new
HasDelegates();
// with static delegate, no need to know how
to create delegate
MyDel.execCallback(FunctionDelegates.BarFuncCallback, "Thirty Three",
33);
}
} // DelegateTest
Les énumérations sont utilisées pour grouper certaines constantes de même catégorie. Ainsi, la constante "Couleur" prendra l'énumération de valeurs suivantes : ("rouge", "vert","bleu", ..). Le manque de type d'énumération dans Java contraint à utiliser des valeurs entières ne garantissant pas la sécurité des types. Une autre technique pour assurer le respect des types consiste à créer une classe différente pour chaque valeur, ce qui tend à multiplier inutilement le nombre de type alors qu'en C# le compilateur prend en charge automatiquement cette tâche.
Vous
trouverez ci-dessous une comparaison d'un code C# et Java utilisant la classe
javax.swing.border.TitledBorder
redéfinie afin d'illustrer les bénéfices des énumérations C# par rapport à
Java.
NOTE : En C#, les énumérations supporttent la méthode ToString(), ainsi elles peuvent renvoyer leur valeur sous la forme de chaîne de caractères (par exemple "Left") et pas uniquement sous la forme d'entier. Il existe aussi une méthode statique Parse() permettant de convertir une chaîne de caractères en énumération.
C# Code
/*
declared somewhere visible */
public
enum Justification{ Left, Right, Center,
Leading, Default_Justification};
/**
* Sets the title-justification of the
titled border.
*/
public
void setTitleJustification(Justification
titleJustification)
{
//check
if justification is a valid value
if
(Enum.IsDefined(typeof(Justification),
titleJustification)
{
/* JUSTIFICATION CODE GOES HERE */
}
}
Java Code
/**
* Sets the title-justification of the
titled border.
*/
public
void setTitleJustification(int titleJustification)
{
//
check if the justification is valid value
if(titleJustification
!= LEADING && titleJustification != CENTER &&
titleJustification
!= LEFT &&
titleJustification != RIGHT
&&
titleJustification
!= DEFAULT_JUSTIFICATION)
{
titleJustification
= DEFAULT_JUSTIFICATION;
}
/*
JUSTIFICATION CODE GOES HERE */
}
En Java et C#, les données sur le tas (heap) sont libérées par le ramasse miettes lorsqu'elles ne sont plus utilisées, alors que les données situées sur la pile ne rentre pas sous la responsabilité du ramasse miettes et sont instantanément supprimées au retour d'un appel de méthode. De plus, les allocations sur la pile sont beaucoup plus rapide que celles dans le tas. De plus, les problèmes liés à la fragmentation de la mémoire n'existent pas dans la pile.
C'est pourquoi, pour éviter d'utiliser inutilement le tas (heap), C# propose de stocker directement certains objets dans la pile. Ces objets sont appelés types de valeur (ValueType). D'ailleurs, en C#, l'ensemble des types primitifs sont des ValueType stockés sous la forme de structures (cf. Int32). Contrairement aux classes, les ValueType sont toujours passées par valeur et ne sont jamais libérées par le ramasse miettes. De plus, les tableaux de ValueType contiennent des valeurs et non des références d'objets, ce qui permet d'économiser de la mémoire et du temps.
Pour
déclarer un type de valeur, il suffit de d'utiliser le mot-clé struct
au lieu de class
. Pour instancier un type
de valeur, le mot-clé new
doit être utilisé tout
comme une classe normale. Les structures et les constructeurs se comportent
différemment par rapport aux classes. Dans le cas d'une classe, la création
d'une instance doit toujours précéder l'utilisation de l'objet. Si le mot clé
new est omis, la création n'a pas lieu et la référence est nulle.
Tous
les types struct héritent implicitement de la classe object
. Une déclaration de
struct peut spécifier une liste d'interfaces implémentées, mais il n'est pas
possible qu'elle spécifie une classe de base. C'est pourquoi les structs ne sont
jamais abstraits et sont toujours implicitement finales (sealed
). Comme l'héritage n'est
pas géré dans les structs, la visibilité par défaut déclarée est protected
ou internal protected
. et les
membres de fonctions ne peuvent être abstract
ou virtual
.
C# Code
using
System;
struct Point
{
public
int x;
public
int y;
public
Point( int x, int y)
{
this.x = x;
this.y =
y;
}
public
override string ToString()
{
return String.Format("({0}, {1})", x,
y);
}
public
static void
Main(string[] args)
{
Point start = new Point(5, 9);
Console.WriteLine("Start: " + start);
/* The line below wouldn't compile if Point
was a class */
Point end = new Point();
Console.WriteLine("End: " + end);
}
} // Point
Le boxing (ou emboîtage) est une des fonctionnalités les plus intéressantes de C#. Le boxing est la capacité du langage à traiter des objets comme des types de valeur de manière implicite. L'équivalent en Java sont les classes Wrappers (Integer, Float, ...) permettant d'encapsuler un type primitif dans une classe afin de le traiter comme un objet (passage par référence, ...). La différence entre Java et C# se situe plus au niveau de l'implémentation que de la sémantique même du mécanisme. L'exemple ci-dessous nous illustre le fonctionnement du boxing de manière implicite et explicite.
C# Code
using
System;
using
System.Collections;
struct
Point
{
//member
fields
private int x;
private
int y;
public
Point (int x, int y)
{
this.x = x;
this.y = y;
}
public
override string ToString()
{
return String.Format("({0}, {1})", x,
y);
}
}//Point
class
Test
{
public
static void
PrintString(object o)
{
Console.WriteLine(o);
}
public
static void
Main(string[] args)
{
Point p = new Point(10, 15);
ArrayList list = new ArrayList();
int z = 100;
PrintString(p); //p boxed to object when
passed to PrintString
PrintString(z); //z boxed to object when
passed to PrintString
// integers and float boxed when stored in
collection
// therefore no need for Java-like wrapper
classes
list.Add(1);
list.Add(13.12);
list.Add(z);
for(int
i =0; i < list.Count; i++)
PrintString(list[i]);
}
}
Cet
opérateur ne possède pas d'équivalent en Java mais se rapproche du
fonctionnement du dynamic_cast
de C++. Son fonctionnement consiste à exécuter une conversion
explicite d'une référence vers un type donné et en cas d'échec renvoie la valeur
null
. Cela permet
en une seule opération d'effectuer la conversion et l'assignation.
C# Code
MyClass
mc = o as MyClass;
if(mc
!= null) //check if cast successful
mc.doStuff();
NOTE
: L'opérateur as
s'utilise aussi pour convertir des types de valeur.
La
boucle foreach
est
une instruction très populaire dans le monde des langages de scripts tels que
Perl, PHP, Tcl/Tk. C'est un moyen efficace et peu verbeux d'effectuer une
itération sur une collection d'objets implémentant l'interface System.Collections.IEnumerable.
C# Code
string[]
greek_alphabet = {"alpha", "beta", "gamma", "delta",
"epsilon"};
foreach(string str in
greek_alphabet)
Console.WriteLine(str
+ " is a letter of the greek alphabet");
Les
propriétés sont un moyen d'accéder aux propriétés membre d'une classe en
respectant les règles d'encapsulation. Dans le monde Java, les accesseurs
(getteurs et setteurs) sont utilisés à ces fins. L'avantage des propriétés en C#
est de permettre à l'utilisateur d'accéder aux attributs d'un objet de la même
manière que s'il effectuait directement l'opération object.attribut
alors qu'en
réalité il appelle une méthode de manière totalement transparente.
Il est aussi possible de créer des propriétés en lecture seule (read-only), écriture seule ou lecture/écriture si les accesseurs associés sont implémentés ou pas.
L'exemple ci-dessous nous illustre les différents types de propriétés :
C# Code
using
System;
public
class User
{
public
User(string name)
{
this.name = name;
}
private
string name;
//read-only
property for name member variable
public
string Name
{
get
{
return name;
}
}
private
static int
minimum_age = 13;
//read-write
property for class member, minimum_age
public
static int
MinimumAge
{
get
{
return minimum_age;
}
set
{
if(value > 0 && value < 100)
minimum_age = value;
else
Console.WriteLine("{0} is an invalid age, so minimum age remains at {1}",
value, minimum_age);
}
}
public
static void
Main(string[] args)
{
User newuser = new User("Bob Hope");
User.MinimumAge = -5; /* prints error to
screen since value invalid */
User.MinimumAge = 18;
//newuser.Name = "Kevin Nash"; Causes compiler
error since Name property is read-only
Console.WriteLine("Minimum
Age: " + User.MinimumAge);
Console.WriteLine("Name: {0}", newuser.Name);
}
} // User
L'inconvénient des propriétés C# est qu'elles peuvent mener à des situations pour le moins bizarre lorsqu'une exception se produit. Imaginez une contrainte sur une propriété définissant le fait qu'une valeur ne doit pas dépasser un certain seuil. En cas de débordement, une exception va être levée à l'intérieur de l'accesseur et le client devra traiter cette erreur alors qu'il réalise simplement une affectation. C'est ce qui est représenté dans l'exemple ci-dessous avec la classe Clock :
C# Code
try
{
myClock.Hours = 28; /* setter
throws exception because 28 is an invalid hour value
*/
myClock.Minutes
= 15;
myClock.Seconds
= 39;
}
catch(InvalidTimeValueException
itve)
{
/*
figure out which field was invalid and report error */
}
Pour éviter ce genre de situation, il est préférable dans la mesure du possible de traiter les exceptions dans le corps des accesseurs et si ce n'est pas possible, indiquez clairement dans la documentation que cette méthode est susceptible de lever une exception que l'utilisateur devra catcher.
Les Attributs de Runtime fournissent un moyen élégant d'insérer des annotations (i.e méta-données) dans un module, une classe, une méthode, un paramètre ou une variable membre. Les attributs sont très populaires dans un certain nombre de langages de programmation tels que le noyau Linux (si si) ou les objets COM (Component Object Model) . Ces attributs sont un moyen de fournir des informations au runtime dans le but d'exécuter des tâches additionnelles ou d'étendre les caractéristiques d'un type donné.
Les quelques exemples ci-dessous vous illustrent les différents types attributs pouvant être rencontrés.
[MethodImpl(MethodImplOptions.Synchronized)]
:
Similaire au mot-clé synchronized de Java.
[Serializable]
: Similaire à
l'implémentation de l'interface java.io.Serializable
de
Java.
C# Code
//declaration of bit field enumeration
[Flags]
enum ProgrammingLanguages{
C = 1,
Lisp = 2,
Basic = 4,
All = C | Lisp | Basic
}
aProgrammer.KnownLanguages = ProgrammingLanguages.Lisp; //set known languages ="Lisp"
aProgrammer.KnownLanguages |= ProgrammingLanguages.C; //set known languages ="Lisp C"
aProgrammer.KnownLanguages &= ~ProgrammingLanguages.Lisp; //set known languages ="C"
if((aProgrammer.KnownLanguages & ProgrammingLanguages.C) > 0){ //if programmer knows C
//.. do something
}
[WebMethod]
: utilisé en
combinaison avec ASP.NET permet de spécifier qu'une méthode est un web service.
Il est possible d'accéder aux attributs d'un module, d'une classe ou d'une méthode par l'intermédiaire des APIs de reflection. C'est une fonctionnalité très utile pour vérifier à l'exécution si une classe supporte un type d'attribut spécifique ou pour extraire des valeurs données.
Les
développeurs peuvent créer leur propres attributs de Runtime en sous-classant
System.Attribute
.
L'exemple suivant nous indique comment implémenter son propre attribut
fournissant des informations sur l'auteur des sources et comment récupérer les
valeurs correspondantes à l'exécution.
C# Code
using
System;
using
System.Reflection;
[AttributeUsage(AttributeTargets.Class)]
public
class AuthorInfoAttribute:
System.Attribute
{
string
author;
string
email;
string
version;
public
AuthorInfoAttribute(string author, string email)
{
this.author = author;
this.email = email;
}
public
string Version
{
get
{
return version;
}
set
{
version = value;
}
}
public
string Email
{
get
{
return email;
}
}
public
string Author
{
get
{
return author;
}
}
}
[AuthorInfo("Dare Obasanjo",
"kpako@yahoo.com", Version="1.0")]
class
HelloWorld
{
}
class
AttributeTest
{
public
static void
Main(string[] args)
{
/* Get Type object of HelloWorld class
*/
Type t = typeof(HelloWorld);
Console.WriteLine("Author Information for " + t);
Console.WriteLine("=================================");
foreach(AuthorInfoAttribute att in t.GetCustomAttributes(typeof(AuthorInfoAttribute), false))
{
Console.WriteLine("Author: " + att.Author);
Console.WriteLine("Email:
" + att.Email);
Console.WriteLine("Version: " + att.Version);
}//foreach
}//Main
}
Les
indexeurs ont une syntaxe spécifique permettant de surcharger l'opérateur []
d'une classe. Ils sont utiles lorsqu'une classe est un conteneur pour d'autres
types d'objets. Les indexeurs supportent divers types comme index : entiers,
chaines de caractère, ... Il est aussi possible de créer des indexeurs
supportant les tableaux multi-dimensionels. Enfin, les indexeurs peuvent être
surchargés. En Java, les indexeurs sont implémentés à travers des accesseurs.
Ainsi, en Java on écrirait : myList.getElement(3)
et en C# :
myList[3]
C# Code
using
System;
using
System.Collections;
public
class IndexerTest: IEnumerable, IEnumerator
{
private
Hashtable list;
public
IndexerTest ()
{
index = -1;
list = new Hashtable();
}
//indexer that
indexes by number
public
object this[int
column]
{
get
{
return
list[column];
}
set
{
list[column] = value;
}
}
/* indexer
that indexes by name */
public
object this[string
name]
{
get
{
return this[ConvertToInt(name)];
}
set
{
this[ConvertToInt(name)] = value;
}
}
/* Convert
strings to integer equivalents */
private
int ConvertToInt(string value)
{
string loVal = value.ToLower();
switch(loVal)
{
case "zero": return 0;
case "one": return 1;
case "two": return 2;
case "three": return
3;
case "four": return 4;
case "five": return 5;
default:
return 0;
}
return 0;
}
/**
* Needed to implement IEnumerable
interface.
*/
public
IEnumerator GetEnumerator(){ return
(IEnumerator) this; }
/**
* Needed for IEnumerator.
*/
private
int index;
/**
* Needed for IEnumerator.
*/
public
bool MoveNext()
{
index++;
if(index >=
list.Count)
return false;
else
return true;
}
/**
* Needed for IEnumerator.
*/
public
void Reset()
{
index = -1;
}
/**
* Needed for IEnumerator.
*/
public
object Current
{
get
{
return
list[index];
}
}
public
static void
Main(string[] args)
{
IndexerTest it = new IndexerTest();
it[0] = "A";
it[1] = "B";
it[2] = "C";
it[3] = "D";
it[4] = "E";
Console.WriteLine("Integer Indexing: it[0] = " + it[0]);
Console.WriteLine("String
Indexing: it[\"Three\"] = " + it["Three"]);
Console.WriteLine("Printing entire contents of object via enumerating
through indexer :");
foreach( string str in
it)
{
Console.WriteLine(str);
}
}
} // IndexerTest
C#
intègre un pré-processeur proposant un sous-ensemble limité des fonctionnalités
fournies par les pré-processeurs C/C++. Par exemple, la directive #include
n'existe pas. Les seules directives présentes sont liées aux opérations
d'affectations d'identificateurs permettant de réaliser de la compilation
conditionnelle avec #define
,#undef
et #if,#elif.
Les directives
#error
et
#warning
provoquent
l'affichage de messages d'erreurs ou de warning pendant la phase de compilation.
Enfin, #line
permet
de spécifier la ligne correspondante au fichier source afin de reporter le
numéro de ligne en cas d'erreur.
C# Code
#define
DEBUG /* #define must be first token in file */
using
System;
class
PreprocessorTest
{
public
static void
Main(string[] args)
{
#if
DEBUG
Console.WriteLine("DEBUG Mode := On");
#else
Console.WriteLine("DEBUG Mode := Off");
#endif
}
}
Le
mot-clé using
peut
être utilisé pour assigner à un nom donné un type correspondant. Le mécanisme
est assez similaire aux typedef en C ou C++. Son comportement a été détourné en
C# pour permettre une meilleure lisibilité du code source, mais aussi pour
résoudre les problèmes de conflits entre les noms des namespaces.
C# Code
using
Terminal = System.Console;
class
Test
{
public
static void
Main(string[] args)
{
Terminal.WriteLine("Terminal.WriteLine is equivalent to
System.Console.Writeline");
}
}
Le
package Reflection.Emit
contient un ensemble de classes pouvant être utilisées afin de
générer des instructions .NET à l'exécution. Ces instructions sont ensuite
compilées en mémoire et peuvent être stockées physiquement sur disque sous la
forme d'une assembly. Ce mécanisme existe dans Java mais n'est pas proposé en
standard dans les APIs. Les moteurs de Servlets/JSP l'utilise pour générer le
source d'une servlet et pour la charger en mémoire. Les conteneurs EJB s'en
servent pour générer l'implémentation des classes techniques (EjbObject) ou le
code de persistance des Entity. Malheureusement, ces classes se trouvent dans
les packages sun.tools.*
(tools.jar),
ce qui limite fortement leur utilisation.
Bien
que l'ensemble du langage C# soit très proche de Java et que les pointeurs ne
soient pas gérés de manière intégré, leur utilisation n'est pas proscrite dans
le cadre de blocs "unsafe
" ou non protégé. Il faut
garder à l'esprit que tout code s'exécutant dans ce type de bloc ne bénéficie
pas de tous les services du Framework (vérification de types, ...). Cela
signifie que le programme doit faire une confiance aveugle dans ce genre de
code. Leur utilisation est appropriée lorsqu'il est nécessaire de faire appel à
une zone de mémoire spécifique, par exemple dans le cas de drivers utilisant des
adresses d'entrées/sorties particulières.
De
plus, le garbage collector pouvant à tout moment ré-allouer des variables
"managés", il est absolument nécessaire de "sticker" ou figer ces variables le
temps du traitement par le bloc unsafe. Cette opération est réalisée à l'aide du
mot-clé "fixed
"
C# Code
using
System;
class
UnsafeTest
{
public
static unsafe
void Swap(int*
a, int*b)
{
int temp = *a;
*a = *b;
*b = temp;
}
public
static unsafe
void Sort(int*
array, int size)
{
for(int
i= 0; i < size - 1; i++)
for(int
j = i + 1; j < size; j++)
if(array[i] >
array[j])
Swap(&array[i], &array[j]);
}
public
static unsafe
void Main(string[] args)
{
int[] array = {9, 1, 3, 6, 11, 99, 37,
17, 0, 12};
Console.WriteLine("Unsorted Array:");
foreach(int x in
array)
Console.Write(x +
" ");
fixed(
int* iptr = array )
{ // must use
fixed to get address of array
Sort(iptr, array.Length);
}//fixed
Console.WriteLine("\nSorted Array:");
foreach(int x in
array)
Console.Write(x +
" ");
}
}
En Java, les arguments sont passés par valeur, cela signifie que lorsqu'une méthode est appelée, les arguments sont copiés sur la pile (copie des valeurs primitives mais aussi des références). En C#, comme en C++ et d'une certaine manière en C, il est possible de spécifier qu'une liste d'arguments doit être passée par référence et non par valeur. Ce type de caractéristique est particulièrement intéressante lorsqu'une méthode doit retourner une liste d'objets modifiés. Ainsi, en Java il est impossible de swaper (intervertir) deux nombres entiers sans passer par leur type Wrapper.
En
C#, les mot-clés pour spécifier qu'un paramètre doit être passé par valeur ou
par référence sont ref
et out
. Bien entendu, tout
paramètre passé avec ref
doit avoir été au préalable initialisé, dans le cas où la méthode
se charge de créer l'objet, le mot-clé out
doit être précisé.
Java Code
class
PassByRefTest
{
public
static void
changeMe(String s)
{
s = "Changed";
}
public
static void
swap(int x, int y)
{
int z = x;
x =
y;
y = z;
}
public
static void
main(String[] args)
{
int a = 5, b = 10;
String s = "Unchanged";
swap(a, b);
changeMe(s);
System.out.println("a := " + a + ", b :=
" + b + ", s = " + s);
}
}
OUTPUT
a := 5, b := 10, s = Unchanged
C# Code
using
System;
class
PassByRefTest
{
public
static void
ChangeMe(out string s)
{
s = "Changed";
}
public
static void
Swap(ref int
x, ref int
y)
{
int z = x;
x =
y;
y = z;
}
public
static void
Main(string[] args)
{
int a = 5, b = 10;
string s;
Swap(ref a, ref b);
ChangeMe(out s);
Console.WriteLine("a := " + a + ", b := " + b + ", s = " +
s);
}
}
OUTPUT
a := 10, b := 5, s = Changed
En
C et C++, il est possible de spécifier qu'une fonction prend un nombre variable
de paramètres, cette fonctionnalité est fortement utilisée dans le cadre des
fonctions printf() et scanf(). En C#, le fonctionnement est le même excepté
qu'il faut spécifier le mot-clé param
pour indiquer que le
nombre de paramètres est variable.
C# Code
using
System;
class
ParamsTest
{
public
static void
PrintInts(string title, params int[]
args)
{
Console.WriteLine(title + ":");
foreach(int num in
args)
Console.WriteLine(num);
}
public
static void
Main(string[] args)
{
PrintInts("First Ten Numbers in Fibonacci Sequence", 0, 1, 1, 2, 3, 5, 8,
13, 21, 34);
}
}
C#
fournit un moyen élégant pour résoudre le problème d'échappement des caractères
spéciaux dans une chaîne. Ainsi, les backslashes, tabulations, quotes et autres
newlines peuvent être intégrés à la chaîne sans avoir à utiliser une séquence
d'échappement particulière. La seule contrainte est de préfixer la déclaration
de la chaîne avec le symbole @
.
C# Code
using
System;
class
VerbatimTest
{
public
static void
Main()
{
//verbatim string
string filename = @"C:\My Documents\My Files\File.html";
Console.WriteLine("Filename 1: " + filename);
//regular string
string filename2 = "C:\\My Documents\\My Files\\File.html";
Console.WriteLine("Filename 2: " + filename2);
string snl_celebrity_jeopardy_skit =
@"
Darrell Hammond
(Sean Connery) : I'll take ""Swords"" for $400
Will Farrell (Alex Trebek) : That's S-Words, Mr
Connery.
";
Console.WriteLine(snl_celebrity_jeopardy_skit);
}
}
C#
fournit une option pour explicitement détecter ou ignorer les débordements dans
le cas de conversion vers un type plus faible. Le débordement lève une exception
du type System.OverFlowException
et
comme la détection met en place des mécanismes pouvant affecter les
performances, il est possible de l'activer ou de la désactiver en spécifiant les
mots-clés checked
et unchecked
au début de
blocs.
C# Code
using
System;
class
CheckedTest
{
public
static void
Main()
{
int num = 5000;
/* OVERFLOW I */
byte a = (byte) num; /* overflow
detected only if /checked compiler option on */
/* OVERFLOW II */
checked
{
byte b = (byte) num; /* overflow
ALWAYS detected */
}
/* OVERFLOW III
*/
unchecked
{
byte c = (byte) num; /* overflow
NEVER detected */
}
}//Main
}
Quelquefois, lorsqu'il est nécessaire d'implémenter une interface, il se peut que vous soyez confronté à des problèmes de conflits de noms. Par exemple, imaginons la classe FileRepresentation qui implémente les interfaces IWindow et IFileHandler. Les deux interfaces possèdent une méthode Close() qui dans le cas de l'interface IWindow permet de refermer une fenêtre et dans le cas de IFileHandler permet de refermer un fichier. En Java, il n'existe aucune solution à ce genre de problème à part écrire deux méthodes possédant chacune des noms différents. C# propose une alternative intéressante au développeur en lui permettant d'implémenter explicitement les deux interfaces dans la classe FileRepresentation. Ainsi, lorsque le client veut fermer un fichier, il spécifie l'interface utilisée de la manière suivante : (IFileHandler) obj.Close() et ou (IWindow) obj.Close().
NOTE : Les méthodes de ces interfaces sont privées et ne peuvent être appelées sans passer par une conversion explicite
C# Code
using
System;
interface
IVehicle
{
//identify
vehicle by model, make, year
void
IdentifySelf();
}
interface
IRobot
{
//identify
robot by name
void
IdentifySelf();
}
class
TransformingRobot : IRobot, IVehicle
{
string
model;
string
make;
short
year;
string
name;
TransformingRobot(String name, String
model, String make, short
year)
{
this.name = name;
this.model = model;
this.make = make;
this.year = year;
}
void
IRobot.IdentifySelf()
{
Console.WriteLine("My name is " + this.name);
}
void
IVehicle.IdentifySelf()
{
Console.WriteLine("Model:" + this.model
+ " Make:" + this.make + " Year:" + this.year);
}
public
static void
Main()
{
TransformingRobot tr = new
TransformingRobot("SedanBot", "Toyota", "Corolla", 2001);
// tr.IdentifySelf(); ERROR
IVehicle v =
(IVehicle) tr;
IRobot
r = (IRobot) tr;
v.IdentifySelf();
r.IdentifySelf();
}
}
OUTPUT
Model:Toyota Make:Corolla Year:2001
My name is SedanBot
Une notion importante qui a contribuée à la réussite du langage Java est sa capacité à s'exécuter sur n'importe quelle plate-forme sans avoir à ré-écrire le code. Sun officiellement supporte Linux, Windows, Solaris et d'autres éditeurs tiers assurent le portage de la machine virtuelle sur OS/2, AIX et MacOS.
A l'heure actuelle, la plate-forme .NET n'est vraiment utilisable que sur les systèmes Windows. Plusieurs efforts de portage ont été entrepris à travers le monde. Citons dans ce domaine, les projets Mono (http:///www.go-mono.org), Halcyon (conversion .NET vers Java) ou encore Microsoft avec le portage de .NET sur FreeBSD (nom de code : rotor).
Les
extensions Java sont un moyen d'enrichir les APIs internes du JDK dans le but de
fournir une implémentation spécifique de certains packages système. Cela
signifie que l'extension est intégrée au sein des classes java.lang, java.util, java.net,
etc, et peut-être
utilisée par l'ensemble des applications utilisant Java.
Pour faire un parallèle avec C#, c'est comme si l'extension proposée s'intégrait dans le namespace System contenu dans l'assembly System.dll.
Une des fonctions les plus intéressantes de Java est la possibilité de pouvoir redéfinir le comportement du chargeur de classes associé à une procédure d'invocation distante. Le chargement dynamique de classe permet en Java de télécharger une classe à partir de n'importe quelle source (HTTP, Fichier, FTP, JMS, ...) et d'introduire ainsi de nouveaux types dans la machine cible. Il est donc possible d'étendre à loisir les fonctionnalités d'un système sans avoir à redéployer l'application toute entière.
Java Code
public
class MyRMIServer extends
UnicastRemoteObject
implements SomeInterface
{
public MyRMIServer() throws RemoteException{
super();}
public String obtainName(IStockTicker
ticker){
String
stock_ticker = ticker.getTicker();
if(stock_ticker.equalsIgnoreCase("MSFT"))
return "Microsoft Corporation";
else if(stock_ticker.equalsIgnoreCase("SUNW"))
return "Sun Microsystems";
else
return "Unknown Stock Ticker";
}/* obtainName(IStockTicker) */
}
La
méthode distante obtainName() dans
la classe ci-dessus accepte des types qui implémentent l'interface StockTicker.
Le client doit juste se contenter de passer un objet du type de l'interface
attendue. Par exemple, le type CAC40Stock qui n'existe pas sur le serveur
peut-être téléchargé à partir d'une source équelconque et s'utilise comme s'il
avait toujours existé sur le serveur, et cela sans arrêter ou relancer
l'application.
En C#, l'API .NET Remoting fournit un mécanisme similaire permettant de télécharger à distance du code à partir de n'importe quelle machine à partir du moment où le client publie l'URL de l'assembly.
En Java, il est possible de déclarer des constantes dans les interfaces qui seront disponibles aux classes dérivées. Cette technique est souvent utilisée dans Java pour pallier à l'absence de mécanisme intégré de gestion d'énumérations.
En C#, cette caractéristique n'existe pas et ne constitue pas réellement un problème dans la mesure où ce langage fournit déjà un mécanisme d'énumération (enums)
Une classe anonyme est une classe instanciée à l'initialisation du type et ne possédant pas de nom. Les classes anonymes sont souvent utilisées lorsqu'il est nécessaire d'avoir une et une seule instance d'un type donné existant dans l'application. En Java, leur utilisation est souvent cantonnée à la gestion des évènements, les classes callback sont la plupart du temps anonyme.
L'exemple qui suit illustre l'implémentation du Design Pattern State (automate à états finis) en utilisant des classes anonymes
Java Code
/*
An instance of this class represents the current state of a ClientView GUI.
*/
public
abstract class
ClientState{
//
This instance of the class is used to signify that the user is not logged
in.
//
The only thing a user can do in this state is login and exit.
public static
ClientState NOT_LOGGED_IN = new ClientState()
{
public void
setMenuState(ClientView cv) {
cv.setMenuEnabledState(false); /* disable all menus */
cv.menuLogin.setEnabled(true);
cv.menuExit.setEnabled(true);
//can't type
cv.textArea.setEnabled(false);
}
public String toString(){
return "ClientState: NOT_LOGGED_IN";
}
};
//
This instance of the class is used to signify that the user is logged
in
//
but has not yet created a document to work with. The user cannot type or save
//
anything in this mode.
public static
ClientState NO_OPEN_DOCUMENT = new
ClientState() {
public void
setMenuState(ClientView cv) {
cv.setMenuEnabledState(false); /* disable all menus */
cv.menuLogin.setEnabled(true);
cv.menuExit.setEnabled(true);
cv.menuOpenFile.setEnabled(true);
cv.menuNewFile.setEnabled(true);
//can't type
cv.textArea.setEnabled(false);
}
public String toString(){
return "ClientState: NO_OPEN_DOCUMENT";
}
};
// This instance of the class is used to signify that the
user is editting a file.
//
In this mode the user can use any functionality he/she sees fit.
public static
ClientState EDITTING_DOCUMENT = new
ClientState() {
public void
setMenuState(ClientView cv) {
cv.setMenuEnabledState(true); /*
enable all menus */
cv.textArea.setEnabled(true);
}
public String toString(){
return "ClientState:EDITTING_DOCUMENT";
}
};
//
Default constructor private to stop people from directly creating instances
//
of the class.
private ClientState() {;}
//
This disables various elements of the ClientView's menu dependent on
which
//
ClientState object this is.
public abstract
void setMenuState(ClientView
cv);
}
// ClientState
Below
is an example of the code that would utilize the above ClientState class.
bool
loginUser(String username, String passwd) {
//check if already logged in
if(myGUI.state == ClientState.NOT_LOGGED_IN)
return true;
//enable parts of the GUI if the user
authenticates
if(userAuthenticated(username,
passwd)){
myGUI.state = ClientState.NO_OPEN_DOCUMENT;
myGUI.state.setMenuState(myView);
return true;
}
return false;
}/* loginUser(String, String)
*/
Le
version des Assemblies est une des fonctionnalités les plus intéressantes de C#
et de .NET en général. Ainsi, vous avez la possibilité d'exécuter dans un même
domaine d'application plusieurs versions différentes d'une même classe. Cela est
possible du fait que les programmes C# référencent statiquement des
bibliothèques de classes à l'aide de leur nom mais aussi de leur version. Vous
ne risquez donc pas d'avoir des problèmes de cohérence entre différentes
versions de DLL s'exécutant sur la même machine. Les développeurs Java
connaissent bien les âfres du déploiement d'applications Web (JSP et servlets)
référençant de multiples versions de parseur XML (xerces) ou de libraries
ant.jar. Cela est dû au fait que les ClassLoaders Java fonctionnent sur le
principe de premier chargé premier servi. Même si le modèle Java du chargement
dynamique de classes est, il faut l'avouer, plus riche que celui de C#, il n'en
demeure pas moins que cette fonctionnalité manque cruellement dans le JDK.
La plupart des développeurs, spécialement ceux possédant un background C ou C++ apprécieront à leur juste valeur les caractéristiques de surcharge d'opérateurs, pointeurs, délégués et autre libération de mémoire déterministe faisant de C# un langage véritablement riche. Aussi, les développeurs Java désirant se former à ce nouveau langage seront agréablement surpris par le fait que certaines caractéristiques manquantes à Java sont supportées par C#, à savoir le Boxing/Unboxing, les types de valeurs, le mot-clé const, les délégués ou autre héritage explicite d'interface. D'un autre coté, le manque de support d'exception typées, l'absence de classes anonymes, la portabilité multi-plateforme ou le fait que la classe ne soit pas la plus petite unité de déploiement (c'est l'assembly) font que le choix entre C# et Java n'est plus aussi clair.
Ainsi, la vrai question est plutôt : lequel de ces deux langages pourra évoluer avec le temps pour s'adapter à de nouvelles situations lorsqu'elles apparaîtront ? Sun a joué un rôle remarquable avec Java en modifiant la syntaxe lorsque le besoin se faisait sentir (modèle d'évènements 1.0, swing, assert, ...). D'un autre coté, certaines lacunes du langage commence à peser sérieusement et aucune solution n'est réellement proposée :
§ L'absence de mécanisme de versionning de classes : possibilité d'avoir plusieurs versions différentes de classes chargées en même temps en mémoire.
§ Les mots-clé Const, Delegate, ReadOnly ...
§ Le manque de mécanisme intégré permettant d'étendre les fonctionnalités de base d'une classe au niveau des sources (cf Attributs de runtime)
§ Les performances de Swing, ...
Il faut bien l'avouer, C# avec les attributs de Runtime et ces multiples fonctionnalités s'avère techniquement mieux armé pour le combat. Mais, l'histoire nous a démontré à maintes reprises que les langages les plus riches n'étaient pas forcément ceux qui réussissaient le mieux ...
Je
remercie sincèrement Dare Obasanjo de nous avoir permis de modifier et d'adapter
son article en Français.
Merci aussi à toutes les personnes ayant passé des
heures entières à relire et corriger ces centaines de lignes. (Sami Jaber)
© 2002 mailto:kpako@yahoo.com