Java-Entwickler die einen Job als Webentwickler annehmen, werden früher oder später mit der Programmiersprache JavaScript konfrontiert. Obwohl die Namen der beiden Sprachen sehr ähnlich klingen und auch deren Syntax recht ähnlich ist, handelt es sich dabei um zwei grundverschiedene Programmiersprachen.
TypeScript für Java-Entwickler
Für Webentwickler mit Java-Hintergrund stellt TypeScript eine attraktive Alternative für die Entwicklung von JavaScript-Anwendungen dar. Mit dieser Transpilersprache lässt sich JavaScript-Quellcode sehr gut strukturieren und effektiv verwalten.
- Navigation
- 1. Kontrollstrukturen
- 2. Klassen & Co.
- 3. Statische Typisierung
- 4. Visibility Modifier
- 5. Static Modifier
- 6. Konstanten
- 7. Enumerations
- 8. Generics
- 9. Modularisierung
- 10. Exception Handling
- Fazit
In meinem letzten Artikel meiner Blogserie über TypeScript stelle ich alle wichtigen Features und Sprachkonstrukte dieser Sprache anhand der entsprechenden Pendants aus der Java-Welt vor und zeige die jeweiligen Gemeinsamkeiten und Unterschiede beider Sprachen auf.
[smartblock id=6354]
1. Kontrollstrukturen
Die Standard-Kontrollstrukturen If-Else
, Switch
, For
und Foreach
existieren in TypeScript unverändert – auch deren Syntax ist identisch mit der von Java.
1.1. If-Else Konstrukt
Beim If-Else
-Konstrukt gibt es keinerlei Besonderheiten zu beachten; diese Kontrollstruktur funktioniert in beiden Sprachen gleich.
boolean primalCheck = ( 10 > 5 ); boolean secondaryCheck = ( 10 < 5 ); if ( primalCheck ) { // passed } else if ( secondaryCheck ) { // not passed } else { // not passed }
let primalCheck:boolean = ( 10 > 5 ); let secondaryCheck:boolean = ( 10 < 5 ); if ( primalCheck ) { // passed } else if ( secondaryCheck ) { // not passed } else { // not passed }
1.2. Switch-Anweisung
Unter Java ist die Switch
-Anweisung auf Werte der Typen Integer und String sowie auf Enumerations-Konstanten beschränkt. TypeScript ist hier weniger restriktiv und erlaubt den Switch auf Variablen mit Werten beliebiger Typen.
Einen kleinen Unterschied in der Syntax gibt es beim Verwenden von Enumerations-Konstanten: Hier muss unter Java der Klassenname vor der Konstanten ausdrücklich weggelassen werden, wohingegen er in TypeScript angegeben werden muss.
MyDirection direction = this.getDirection(); switch ( direction ) { case LEFT: { this.moveLeft(); break; } case RIGHT: default: { this.moveRight(); break; } }
let direction:MyDirection = this.getDirection(); switch ( direction ) { case MyCharacterDirection.LEFT: { this.moveLeft(); break; } case MyCharacterDirection.RIGHT: default: { this.moveRight(); break; } }
1.3. For-Schleife
Die Syntax der For
-Schleife ist ebenfalls identisch in beiden Sprachen. Lediglich bei der Definition der Iterator-Variablen unterscheidet sich deren Syntax.
MyGameObject[] gameObjects = this.getGameObjects(); for ( int i = 0; i < gameObjects.length; ++i ) { gameObjects[ i ].render(); }
let gameObjects:Array<MyGameObject> = this.getGameObjects(); for ( let i:number = 0; i < gameObjects.length; ++i ) { gameObjects[ i ].render(); }
1.4. Foreach-Schleife
TypeScript bietet eine einfach zu benutzende Foreach
-Schleife, mit der komfortabel über alle Elemente einer Collection iteriert werden kann.
Die Syntax divergiert in beiden Sprachen wieder nur geringfügig: Bei TypeScript ist die Besonderheit, dass das Schlüsselwort let
für die Iterator-Variable zwar angegeben werden muss, eine explizite Typenangabe dazu allerdings nicht erlaubt ist.
Sofern das Ausgabeformat des Compilers JavaScript ES5 oder niedriger ist, musss die Compileroption downlevelIteration mit dem Wert true angegeben werden damit die Foreach-Schleife verwendet werden kann.
MyGameObject[] gameObjects = this.getGameObjects(); for ( MyGameObject gameObject : gameObjects ) { gameObject.render(); }
let gameObjects:Array<MyGameObject> = this.getGameObjects(); for ( let gameObject of gameObjects ) { gameObject.render(); }
2. Klassen, Schnittstellen, Abstraktion und Vererbung
2.1. Klassen und Vererbung
Die Syntax für Klassen ist ebenfalls identisch in beiden Sprachen. Das gilt auch für das Ableiten von Klassen sowie für das Implementieren von Schnittstellen.
class MyPlayer extends MyGameObject implements MyShape, MyCollidable { ... }
class MyPlayer extends MyGameObject implements MyShape, MyCollidable { ... }
2.2. Konstruktoren
Bei der Definition des Konstruktors unterscheidet sich die Syntax beider Sprachen. Im Gegensatz zu Java können Konstruktoren in TypeScript nicht überlagert werden.
class MyPlayer extends MyGameObject { public MyPlayer() { this( "Christopher" ); } public MyPlayer( String name ) { super( name ); } }
class MyPlayer extends MyGameObject { public constructor( name:string ) { super( name ); } }
2.3. Schnittstellen
Auch Schnittstellen funktionieren genauso wie in Java. Mittels des Schlüsselworts interface
eingeleitete Schnittstellen definieren nicht-statische Felder und Methoden, die auf eine Klasse, welche die Schnittstelle implementiert, verfügbar gemacht werden.
Im Gegensatz zu TypeScript können unter Java Schnittstellen auch Konstanten definieren und Felder mit einem Wert vorbelegen. Außerdem müssen unter TypeScript alle durch eine Schnittstelle implementierten Felder explizit überschrieben werden.
Da es sich bei Interfaces um reine Entitäten des TypeScript-Compilers handelt, möchte ich darauf hinweisen, dass diese Information zur Laufzeit nicht mehr verfügbar ist. Die Verwendung des instaceof Operators sollte daher nur auf den Einsatz eigener Klassen beschränkt werden.
interface MyCircle extends MyShape { public static final int SHAPE_ID = 7; public int diameter = 0; public getDiameter() : number; }
interface MyCircle extends MyShape { public diameter:number; public getDiameter() : number; }
2.4. Abstrakte Klassen
Abstrakte Klassen existieren auch in TypeScript und stehen der Funktionsweise unter Java in nichts nach. Beide Sprachen leiten abstrakte Klassen mit dem Schlüsselwort abstract
ein.
Abstrakte Klassen können auch unter TypeScript nicht direkt instanziiert werden. Daher können sie – neben allen gewöhnlichen statischen und nicht-statischen Methoden – auch abstrakte Methoden definieren, die ebenfalls mit dem Schlüsselwort abstract
definiert werden und keinen Methodenkörper besitzen. Dieser wird von konkreten Klassen, die diese abstrakten Klassen erweitern, ausformuliert.
abstract class MyGameObject { protected abstract MyShape getShape(); }
abstract class MyGameObject { protected abstract getShape() : MyShape; }
2.5. Finale Klassen und Methoden
Das Schlüsselwort final
kennzeichnet in Java eine Klasse, die nicht mehr abgeleitet werden darf. Auch Methoden, die von abgeleiteten Klassen nicht überschrieben werden sollen, können diesen Modifier verwenden.
Leider existiert zu diesem Zeitpunkt unter TypeScript noch kein Äquivalent für das final
-Schlüsselwort; Vorschläge dafür befinden sich aber in Bearbeitung. Somit muss auf dieses Feature in TypeScript vorerst verzichtet werden.
final class MyEnemy extends MyGameObject { protected final MyShape getShape() { return this.shape; } }
class MyEnemy extends MyGameObject { protected getShape() : MyShape { return this.shape; } }
3. Statische Typisierung
TypeScript ermöglicht eine statische Typisierung aller Variablen, Parameter und Rückgabewerte. Auf Verletzungen des Typsystems reagiert der TypeScript-Compiler somit analog zum Java-Compiler mit konkreten Fehlern und Warnungen.
In TypeScript wird der Typ der Variable mit einem Doppelpunkt eingeleitet und hinter deren Namen angegeben.
String name = "Christopher";
let name:string = "Christopher";
3.1. Typisierung lokaler Variablen
Lokale Variablen können mit dem Schlüsselwort let
oder var
eingeleitet werden. Hierbei ist die Variante mit let
zu bevorzugen, da die Variable in diesem Fall nur innerhalb des aktuellen Scopes verwendet werden kann, beispielsweise nur innerhalb eines foreach
-Körpers. Somit steht diese Variante dem Standardverhalten lokaler Variablen aus Java in nichts nach.
Wie in JavaScript auch ist es in TypeScript guter Stil, lokale Variablen die sich nicht verändern mit dem Schlüsselwort const anstatt let einzuleiten.
MyGameObject[] gameObjects = this.getGameObjects(); for ( int i = 0; i < gameObjects.length; ++i ) { gameObjects[ i ].render(); } // i is not available here!
let gameObjects:Array<MyGameObject> = this.getGameObjects(); for ( let i:number = 0; i < gameObjects.length; ++i ) { gameObjects[ i ].render(); } // i is not available here!
3.2. Typisierung von Klassenvariablen
Sowohl statische als auch nicht-statische Klassenvariablen können unter TypeScript typisiert werden. Die Syntax für die Typisierung ist hier die selbe wie für lokale Variablen.
class MyLevel { public static MyLevel currentLevel = null; public int width = 0; public int height = 0; public Vector<MyGameObject> gameObjects = null; ... }
class MyLevel { public static currentLevel :MyLevel = null; public width :number = 0.0; public height :number = 0.0; public gameObjects :Array<MyGameObject> = null; ... }
3.3. Typisierung von Rückgabewerten
Für jede Funktion sowie für statische und nicht-statische Klassenmethoden kann unter TypeScript der Rückgabewert explizit angegeben werden. Dieser wird auch hier durch einen Doppelpunkt eingeleitet und ist zwischen der Signatur und dem Körper der Funktion zu definieren.
TypeScript unterstützt wie Java das Schlüsselwort void
, mit dem ausgedrückt werden kann, dass eine Methode über keinen Rückgabewert verfügt.
public void setName( String name ) { this.name = name; }
public setName( name:string ) : void { this.name = name; }
3.4. Casting
Das Casten von Variablen ist in TypeScript mittels spitzer Klammern möglich und funktioniert genauso wie in Java. Hiermit kann beispielsweise ein Objekt einer Superklasse in ein Objekt einer abgeleiteten Klasse gecastet werden.
MyGameObject gameObject = this.getCurrentGameObject(); if ( gameObject instanceof MyPlayer ) { MyPlayer player = (MyPlayer)gameObject; player.jump(); }
let gameObject:my.MyGameObject = this.getCurrentGameObject(); if ( gameObject instanceof my.MyPlayer ) { let player:MyPlayer = <MyPlayer>gameObject; player.jump(); }
4. Visibility Modifier
Auch unter TypeScript existieren die drei Visibility-Modifier public
, protected
und private
. Die Verwendung ist identisch mit denen aus der Java-Welt, wobei nur beim protected
-Modifier ein Unterschied existiert: Da es unter TypeScript keine Pakete gibt, gewährt dieser Modifier lediglich abgeleiteten Klassen Zugriff.
Diese Visibility Information liegt im kompilierten JavaScript-Code nicht mehr vor. Daher existieren auch unter TypeScript Code-Konventionen, die private oder protected Member einer Klasse mit einem Unterstrich zu präfixen.
abstract class MyGameObject { private String name = null; protected MyGameObject( String name ) { this.name = name; } public String getName() { return this.name; } }
abstract class MyGameObject { private name:string = null; protected MyGameObject( name:string ) { this.name = name; } public getName() : String { return this.name; } }
5. Static Modifier
Auch in TypeScript können Methoden und Felder einer Klasse sowohl statisch als auch nicht-statisch definiert werden. Das Verhalten steht dem von Java in nichts nach.
abstract class MyGameObject { private static MyGameObject[] allGameObjects = new MyGameObject[] {}; public static MyGameObject[] getAllGameObjects() { return MyGameObject.allGameObjects; } }
abstract class MyGameObject { private static allGameObjects:Array<MyGameObject> = []; public static getAllGameObjects() : Array<MyGameObject> { return MyGameObject.allGameObjects; } }
6. Konstanten
Lokale Konstanten können in TypeScript mit dem Schlüsselwort const
eingeleitet werden. Für Konstanten die innerhalb eines Klassenkörpers definiert werden, muss das Schlüsselwort readonly
verwendet werden.
public static final int CANVAS_WIDTH = 800; public static final int CANVAS_HEIGHT = 600;
public static readonly CANVAS_WIDTH :number = 800; public static readonly CANVAS_HEIGHT :number = 600;
7. Enumerations
Enumerations stellen unter Java eine sehr mächtige und pragmatische Möglichkeit zur Strukturierung unseres Quellcodes dar, da sie die volle Funktionalität einer Klasse – inklusive Feldern, Methoden und Konstruktoren bieten – und dabei die Anzahl sowie Beschaffenheit der Instanzen exakt durch die Definition der Enumerations-Konstanten festlegen.
Auch unter TypeScript stellen Enumerations eine typisierte Alternative zur Verwendung von primitiven Datentypen dar. Leider sind sie hier aber nicht so umfangreich wie in Java.
Unter TypeScript werden Enumerations-Konstanten standardmäßig durch einen Integer-Wert repräsentiert, können aber auch mit anderen Werten, wie beispielsweise Strings, belegt werden. Die Definition von Feldern, Methoden oder Konstruktoren ist hier aber nicht möglich.
Enumerations können auch mit dem Schlüsselwort const eingeleitet werden. In diesem Fall werden im generierten JavaScript-Code alle Konstanten vom Compiler inline mit dem entsprechenden Wert ersetzt und die Existenz der Enumeration selbst aufgelöst.
enum MyDirection { LEFT, RIGHT ; } enum MyImage { PLAYER_WALK_LEFT( "player/walk_left.png" ), PLAYER_WALK_RIGHT( "player/walk_right.png" ), ; private String path = null; private MyImage( String path ) { this.path = path; } public String getPath() { return this.path; } }
enum MyDirection { LEFT, RIGHT ; } enum MyImage { PLAYER_WALK_LEFT = "player/walk_left.png", PLAYER_WALK_RIGHT = "player/walk_right.png", }
Weitere Teile der Serie
· Der große TypeScript-Workshop
· TypeScript-Tooling mit npm und Webpack
8. Generics
Generics existieren auch unter TypeScript und stehen dem aus Java gewohnten Umfang in nichts nach. Es handelt sich dabei um parametrisierbare Typen, die bei der Instanziierung der Klasse dynamisch gesetzt werden können. Als einfaches Beispiel kann ein Array angesehen werden, das nur Elemente eines festgelegten Datentyps beinhalten darf.
class MyCollection<K> { private Vector<K> storage = null; public MyCollection() { this.storage = new Vector<K>(); } public void addEntry( K k ) { this.storage.add( k ); } } MyCollection<String> myNames = new MyCollection<String>(); myNames.addEntry( "Christopher" );
class MyCollection<K> { private storage:Array<K> = null; public constructor() { this.storage = new Array<K>(); } public addEntry( k:K ) : void { this.storage.push( k ); } } let myNames:MyCollection<string> = new MyCollection<string>(); myNames.addEntry( "Christopher" );
9. Modularisierung
Das Definieren von Namensräumen ist in Java von Anfang an nativ implementiert. Mittels den Anweisungen package
und import
kann jede Kompilationseinheit einen weltweit eindeutigen Paketnamen definieren und zudem genau spezifizieren, welche Klassen außerhalb des eigenen Pakets innerhalb der Einheit benötigt werden.
Unter TypeScript funktioniert die Modularisierung leider nicht so unkompliziert. Best Practice ist hier, auf die Verwendung der Schlüsselwörter namespace
und module
zu verzichten und stattdessen einfach alle zu einem Modul gehörenden Dateien innerhalb einer einzelnen Datei zu exportieren. Diese einzelne Exportdatei kann dann als eigener Namensraum in eine Kompilationseinheit importiert werden.
// src/de/christopherstock/mygame/MyLWJGLKeys.java package de.christopherstock.mygame; import de.christopherstock.mygame.io.*; import java.awt.geom.*; import org.lwjgl.input.*; public class MyLWJGLKeys extends Keyboard { public static final void checkKeys() { boolean keyHoldStrafeLeft = Keyboard.isKeyDown( Keyboard.KEY_A ); ... } }
// src/my.ts export * from './game/MyGame'; export * from './game/MyLevel'; ...
// src/game/MyGame.ts import * as Matter from 'matter-js'; import * as my from '../my'; export class MyGame { public init() { my.MyDebug.init.log( "Initing game engine" ); let engine:Matter.Engine = Matter.Engine.create(); ... } }
10. Exception-Handling
In Java können in einem Try-Catch
-Konstrukt mehrere Catch-Blöcke definiert werden, sodass das Auftreten verschiedener Exceptions unterschiedlich behandelt werden kann. In TypeScript hingegen kann lediglich ein einzelner Catch-Block definiert werden, bei dem die hier definierte Variable zudem nicht typisiert werden darf. Trotzdem ist es möglich, auf unterschiedliche Subklassen der Error-Klasse unterschiedlich zu reagieren.
Zudem unterstützt TypeScript das Schlüsselwort finally
, mit dem eine Codesektion definiert werden kann, die in jedem Fall im Anschluss an die Ausführung des Catch-Blocks durchlaufen wird, selbst wenn der Catch-Block mit einer weiteren Exception aussteigt.
try { throw new RuntimeException( "Intentional RuntimeException" ); } catch ( RuntimeException re ) { // passed } catch ( Exception e ) { // not passed } finally { // always passed after catch block }
try { throw RangeError( "Intentional RangeError" ); } catch ( e ) { // passed if ( e.name == "RangeError" ) { // passed } } finally { // always passed after catch block }
Fazit
Anhand der gezeigten Codebeispiele ist deutlich erkennbar, dass die beiden Sprachen Java und TypeScript sehr viele Gemeinsamkeiten haben. TypeScript bietet hervorragende Möglichkeiten zur Erstellung und Verwaltung von gut strukturiertem Quellcode, dessen Codequalität der von Java-Anwendungen in nichts nachsteht.
Für Java-Entwickler die sich mit der Sprache JavaScript auseinandersetzen stellt TypeScript daher eine attraktive Alternative für den Umstieg zu JavaScript dar.
Ich würde mich sehr freuen, wenn ich mit meinem Artikel einen schnellen Einstieg oder Umstieg in die Programmiersprache TypeScript geben konnte. In diesem Beispielprojekt kommen die meisten der hier gezeigten TypeScript-Techniken zur Anwendung.
Wie immer bin ich für Feedback unter christopher.stock@mayflower.de erreichbar und dankbar.
Schreibe einen Kommentar