Java i C++
Jedną z przyczyn szybkiej akceptacji Javy jest z pewnością jej podobieństwo do C++. Ma to niebagatelne znaczenie, ponieważ większość współczesnych programistów zna C++ dość dobrze, a potwierdzeniem tego może być mało znana wiadomość, że opracowane w USA Informatyczne Testy Kwalifikacyjne dla uczniów szkół średnich (oryg. Advanced Placement Tests) zakładają znajomość C++ (chociaż nikogo zapewne nie zdziwi, jeśli w najbliższej przyszłości zostaną zastąpione testami z Javy).
Z podstawowego C++ zapożyczono do Javy większość składni. Zrezygnowano jedynie z dyrektyw, struktur, unii, wskaźników, operatorów definiowanych i wielodziedziczenia (oryg. multiple inheritance).
Jako rekompensatę zapewniono przenośność programów między dowolnymi platformami sprzętowymi i systemowymi, zdalne wywoływanie metod (funkcji składowych klas), wbudowano w język mechanizmy współbieżności i dynamicznego zarządzania pamięcią oraz uniemożliwiono wyrządzanie szkód przez aplety pochodzące z niepewnych źródeł.
Zwłaszcza ta ostatnia cecha (której poświęcono ulubione przez większość "prawdziwych" programistów wskaźniki) zadecyduje zapewne o przyszłości Javy jako języka Internetu. Istnieje bowiem gwarancja, że nikt nie poniesie uszczerbku, jeśli z ciekawości lub z potrzeby skorzysta z dowolnego apletu, który znajdzie w Internecie, w tym z takiego, który został napisany przez złośliwego programistę.
Dzięki wprowadzonym uproszczeniom, nie występują w Javie takie znane z C++ deklaracje jak na przykład
void (*signal(int, void (*)(int)))(int);
które nawet wprawnym programistom sprawiają od czasu do czasu trudności.
Na skutek dołączenia do języka obszernych bibliotek, programy napisane w Javie są dość krótkie i dużo bardziej przejrzyste niż analogiczne programy napisane w C++. Dzięki temu uruchamianie i testowanie programów staje się łatwiejsze, a kod źródłowy ma cechy samodokumentujące (oryg. self-documenting).
Wszystkie to razem powoduje, że popularność Javy stale rośnie i coraz to nowe rzesze użytkowników przekonują się do tego języka. Szczególnie widoczne jest to w znanych środowiskach akademickich, gdzie z planów dydaktycznych są na korzyść Javy eliminowane takie popularne języki programowania jak Visual Basic, Delphi i Turbo Pascal.
Java a język C++
U podstaw zrozumienia Javy leży pojęcie odnośnika (zmiennej do identyfikowania innych zmiennych) oraz odniesienia (danej identyfikującej zmienną, przypisywanej odnośnikowi).
Ponieważ odnośniki istnieją w C++, można je przypomnieć odwołując się do następującego programu (którego zresztą, jako jednego z nielicznych, nie da się zapisać w Javie).
#include <iostream.h>
int main(void)
{
int one = 10, two = 20;
int &max(int &, int &);
cout << max(one, two) << endl; // 20
return 0;
}
int &max(int &refOne, int &refTwo)
{
return refOne > refTwo ? refOne : refTwo;
}
W chwili wywołania funkcji max odnośnik refOne jest inicjowany odniesieniem do zmiennej one, a odnośnik refTwo jest inicjowany odniesieniem do zmiennej two.
Rezultatem funkcji max jest odnośnik typu "int &" zainicjowany odniesieniem do większego z argumentów.
Klasy
Podobnie jak w C++, klasa jest opisem rodziny obiektów. Struktura obiektów jest określona przez pola klasy, a operacje na obiektach są określone przez jej konstruktory i metody.
Poza polami, konstruktorami i metodami klasa może zawierać także zmienne i funkcje statyczne. Takie składniki klasy (oryg. members) nie są związane z poszczególnymi obiektami, ale należą do całej klasy.
Istotna z punktu widzenia ochrony informacji dostępność (oryg. acessibility) pól, metod, zmiennych i funkcji jest w Javie określana indywidualnie, a nie tak jak w C++, w ramach sekcji. Jeśli dostępności pewnych składników klasy nie określi się jawnie, to będą one dostępne w pakiecie klas. Stanowi to odmianę deklaracji zaprzyjaźnienia (oryg. friend) znanej z C++.
public
class Complex { // publiczna klasa
protected double re; // chronione pole
private double im; // prywatne pole
public Complex
(double re, double im) // publiczny konstruktor
{
this.re = re;
this.im = im;
}
double abs() // przyjazna metoda
{
return Math.sqrt(re * re + im * im);
}
}
Ponieważ w Javie nie ma list inicjacyjnych, więc inicjowanie pól obiektu odbywa się w ciele konstruktora.
Zmienne
Deklaracje zmiennych typów predefiniowanych (w tym "char", "int", "long", "boolean") mają w Javie taką samą interpretację jak w ANSI C++.
Zmienne typu "char" są 16-bitowe, dzięki czemu umożliwiają reprezentowanie wszystkich znaków Unikodu (oryg. Unicode).
W szczególności
int var = 12
jest deklaracją zmiennej var typu "int" zainicjowanej daną o wartości 12, a
char chr = 'ś'
jest deklaracją zmiennej chr zainicjowaną kodem litery ś.
Natomiast deklaracje, w których występuje typ definiowany są interpretowane inaczej niż w C++.
Na przykład
class Point {
// ...
}
Point point;
Zadeklarowano odnośnik point do zmiennych klasy Point, a nie obiekt point klasy Point.
Obiekty
Ponieważ w Javie nie ma struktur ani unii, więc każdy jej obiekt jest egzemplarzem pewnej klasy (oryg. class instance). Z klasą są związane jej zmienne i funkcje, a w obiektach są zawarte zmienne, konstruktory i metody.
Właściwości zmiennych klasy, zarówno tych które są wspólne jej wszystkim obiektom, jak i tych, które wchodzą w skład poszczególnych obiektów, są określone przez deklaracje pól klasy (oryg. class field).
Uwaga: Ze względu na efektywność implementacji, kod metod klasy nie jest powielany i znajduje się fizycznie (ale nie logicznie!) poza obiektami klasy.
W następującej definicji klasy sklasyfikowano jej składniki.
class Fixed {
static int count = 0; // zmienna
static int getCount() // funkcja
{
return count;
}
int value; // pole
Fixed(int val) // konstruktor
{
value = val;
count++;
}
int getValue() // metoda
{
return value;
}
}
Fabrykowanie obiektów
W celu utworzenia obiektu należy użyć wyrażenia fabrykującego (oryg. factory expression) o postaci
new TypObiektowy(Arg, Arg, ... , Arg)
Jego rezultatem jest odnośnik zawierający odniesienie do właśnie sfabrykowanej zmiennej (w Javie słowo kluczowe new nie jest operatorem!).
public void paint(Graphics gDC)
{
Point point;
point = new Point(10, 20);
gDC.drawLine(0, 0, point.x, point.y);
}
Odniesienie do obiektu sfabrykowanego podczas opracowania wyrażenia
new Point(10, 10)
przypisano odnośnikowi point.
Uwaga: W odróżnieniu od C++, w Javie nie można za pomocą operacji new, tworzyć zmiennych skalarnych nie-obiektowych. A zatem nie istnieją wyrażenia fabrykujące takie jak na przykład
new int
albo
new Point
Odnośniki a wskaźniki
Reakcją każdego kto dowiaduje się, że w Javie nie ma wskaźników jest pytanie:
A jak programuje się listowe struktury danych?
Bo przecież w C++ bez wskaźników nie ma na to sposobu.
Okazuje się jednak, że odnośniki można w Javie nie tylko inicjować, ale że można im także przypisywać odniesienia. To już rozwiązuje problem.
W szczególności następujący program w C++
include <iostream.h>
struct Item {
Item *next;
int value;
Item(int i) : value(i)
{
}
};
int main(void)
{
Item *head = 0;
for(int i = 0; i < 10 ; i++) {
Item *newItem = new Item(i);
newItem->next = head;
head = newItem;
}
// ...
return 0;
}
przybiera w Javie postać
class Item {
Item next;
int value;
Item(int i)
{
value = i;
}
}
public
class Main {
public static void main(String args[])
{
Item head = null;
for(int i = 0; i < 10 ; i++) {
Item newItem = new Item(i);
newItem.next = head;
head = newItem;
}
// ...
}
}
A zatem brak wskaźników w Javie nie stanowi żadnego ograniczenia w możliwościach programowania dynamicznych struktur danych.
Między bajki można także włożyć opowieści o tym dla jakich to wzniosłych celów poświęcono wskaźniki. W istocie bezpieczeństwo Javy i wykluczenie możliwości programowania w niej wirusów, nie wynika z pozbycia się wskaźników, ale z wyeliminowania konwersji wskaźnikowych oraz z rygorystycznej kontroli ładowania i interpretowania B-kodu.
Procedury
Procedurami są konstruktory, funkcje i metody klasy (w Javie nie ma funkcji i zmiennych globalnych!). Identycznie jak w C++, każda procedura znajduje się w zakresie (oryg. scope) jej klas macierzystych, a zatem z ciała procedury są dostępne wszystkie składniki klasy, w tym również te, których deklaracje występują poniżej definicji procedury.
Na przykład
class Master {
int dx()
{
return dx;
}
int dx = 0;
// ...
}
Zasługuje na uwagę, że w Javie wolno definiować pole i metodę o takim samym identyfikatorze.
Zmienne lokalne
Zakresem i jednocześnie zasięgiem deklaracji zmiennej lokalnej (w tym parametru) procedury jest cały blok w którym wystąpiła deklaracja (począwszy od punktu tuż za deklaratorem).
Zasięgiem deklaracji zmiennych sterujących instrukcji for jest tylko ciało tej instrukcji.
void Sub(int x, int y)
{
for(int i = 0; false ; );
for(int i = 1; false ; ); // dobrze (w ANSI C++ błąd!)
int v = i; // błąd (nieznany inicjator)
int j = 2;
for(int j = 2; false ; ); // błąd (ponowna deklaracja)
int y; // błąd (ponowna deklaracja)
int z = 10;
int v = 20;
{
int v = 30; // błąd (ponowna deklaracja)
int u = 40;
}
{
int u = 50; // dobrze!
int z = 60; // błąd (ponowna deklaracja)
}
}
Odnośnik this
W ciele konstruktora i metody jest dostępny odnośnik this identyfikujący obiekt na rzecz którego wywołano konstruktor albo metodę.
Pierwszą (i tylko pierwszą!) instrukcją konstruktora może być instrukcja
this(Arg, Arg, ... , Arg);
albo
super(Arg, Arg, ... , Arg);
W pierwszej z nich jest wywoływany konstruktor danej klasy, a w drugiej konstruktor jej nadklasy (klasy bazowej). Jeśli w ciele konstruktora nie wystąpi żadna z tych instrukcji, to domniema się, że jego pierwszą instrukcją jest
super();
Dziedziczenie
Dziedziczenie wyraża się za pomocą słowa kluczowego extends.
Ponieważ istnieje tylko jedna klasa pierwotna (jest nią Object), więc w Javie hierarchia klas jest drzewem, a nie lasem (grafem acyklicznym) jak w C++.
class MyObject extends Object {
// ...
}
Polimorfizm
Każda metoda Javy jest domyślnie wirtualna (oryg. virtual). Każde wywołanie metody, która nie jest prywatna jest polimorficzne, tj.
Do wykonania jest wybierana metoda identyfikowana przez odniesienie przypisane odnośnikowi na rzecz którego odbywa się wywołanie.
Uwaga: Podczas kompilowania programu typ odnośnika służy tylko do upewnienia się, że w jego klasie (albo w interfejsie) występuje definicja wywoływanej metody. Podczas wykonywania programu typ odnośnika nie jest już brany pod uwagę.
class Horse {
// ...
void draw(Graphics gDC)
{
// ... wykreśl konia
}
static void fun(Graphics gDC, Horse horse)
{
horse.draw(gDC);
}
}
class Zebra extends Horse {
// ...
void draw(Graphics gDC)
{
// ... wykreśl zebrę
}
}
Wywołanie
horse.draw(gDC)
jest polimorficzne.
Jeśli parametr horse identyfikuje obiekt klasy Zebra, na przykład po wywołaniu funkcji fun z procedury paint
public void paint(Graphics gDC)
{
fun(gDC, new Zebra("Stripes", 8));
}
to w funkcji fun zostanie wywołana metoda draw klasy Zebra, mimo iż horse jest odnośnikiem do obiektów klasy Horse.
Implementowanie
Mimo iż każda klasa może mieć co najwyżej jedną nadklasę (w Javie nie ma wielodziedziczenia!), to jednak może implementować dowolnie wiele interfejsów.
Uwaga: Interfejs jest odmianą klasy abstrakcyjnej, która zawiera tylko deklaracje metod i definicje zmiennych.
Jeśli klasa implementuje interfejs, a nie ma być klasą abstrakcyjną, to musi dostarczyć definicje wszystkich metod zadeklarowanych w interfejsie.
Uwaga: Implementowanie interfejsu stosuje się najczęściej wówczas, gdy zestaw klas ma bardzo odległego, albo niedostępnego przodka, ale gdy musi być wyposażony we wspólną cechę, wyrażaną takimi słowami jak: skalowalna, przemieszczalna, przeliczalna, wykonywalna, itp.
class Shape {
// ...
}
interface Drawable {
// ...
void draw(Graphics gDC);
}
class DrawableShape extends Shape implements Drawable {
// ...
DrawableShape()
{
}
public void draw(Graphics gDC)
{
// ...
}
}
Wywoływanie
Każdemu odnośnikowi typu interfejsowego można przypisać odniesienie do obiektu klasy implementującej ten interfejs. Na rzecz takiego odnośnika można wówczas wywołać dowolną metodę tej klasy. Wywołanie takie jest wówczas polimorficzne.
Na przykład
public void paint(Graphics gDC)
{
Drawable item = new DrawableShape();
item.draw(gDC);
}
Wywołanie
item.draw(gDC);
jest polimorficzne.
Mimo iż odnośnik item jest klasy Drawable, następuje wywołanie metody draw klasy DrawableShape.
Tablice
Deklaracja tablicy w Javie jest deklaracją odnośnika do tablicy. Sama tablica musi być utworzona za pomocą wyrażenia fabrykującego.
Uwaga: Każda tablica jest obiektem klasy pochodnej klasy Object i implementuje interfejs Cloneable. Obiekt tablicowy jest wyposażony w publiczne pole length określające liczbę elementów tablicy.
Elementy predefiniowane
W celu utworzenia tablicy o elementach typu predefiniowanego należy użyć wyrażenia fabrykującego
new Typ [Rozmiar]
Jego rezultatem jest anonimowy odnośnik zainicjowany odniesieniem do właśnie sfabrykowanej tablicy.
Na przykład, wykonanie instrukcji
int arr[]; // deklaracja odnośnika
arr = new int [3]; // utworzenie tablicy
for(int i = 0; i < arr.length ; i++)
arr[i] = 0;
powoduje utworzenie i zainicjowanie (liczbą 0) wszystkich elementów tablicy identyfikowanej przez odnośnik arr.
Elementy obiektowe
W celu utworzenia tablicy o elementach typu obiektowego należy użyć wyrażenia fabrykującego
new Typ [Rozmiar]
Jego rezultatem jest odnośnik zainicjowany odniesieniem do wektora odnośników do elementów właśnie sfabrykowanej tablicy.
Na przykład, wykonanie instrukcji
String arr[]; // deklaracja odnośnika
arr = new String [3]; // utworzenie wektora odnośników
for(int i = 0; i < arr.length ; i++)
arr[i] = new String(); // utworzenie elementu podstawowego
powoduje utworzenie i zainicjowanie (pustym łańcuchem) wszystkich elementów podstawowych tablicy identyfikowanej przez odnośnik arr.
Wyjątki
Jeśli wykonanie instrukcji może spowodować powstanie sytuacji wyjątkowej nie wywodzącej się od RuntimeException i Error, to taka instrukcja musi być ujęta w blok instrukcji try, albo nagłówek procedury obejmującej tę instrukcję musi zawierać frazę throws wyszczególniającą klasę wyjątku.
W pierwszym przypadku we frazie catch określa się co należy uczynić w razie powstania sytuacji wyjątkowej, a w drugim pozostawia się taką decyzję procedurze wywołującej.
Na przykład, podczas wykonywania procedury
void setChar(char arr[], int pos, FileInputStream src)
{
int chr = src.read(); // IOException
arr[pos] = chr; // IndexOutOfBoundsException
}
mogą powstać dwie sytuacje wyjątkowe: IOException związana z nieudaną operacją wejścia oraz IndexOutOfBoundsException (klasy pochodnej od RuntimeException) związana z niewłaściwie dobranym indeksem tablicy.
Pierwsza z nich wymaga ujęcia instrukcji
int chr = src.read(); // IOException
w blok instrukcji try, na przykład
void setChar(char arr[], int pos, FileInputStream src)
{
try {
int chr = src.read(); // IOException
}
catch(IOException e) {
// ... // reakcja na sytuację wyjątkowa
}
arr[pos] = chr; // IndexOutOfBoundsException
}
albo użycia frazy throws wyszczególniającej klasę IOException
void setChar(char arr[], int pos, FileInputStream src)
throws IOException
{
int chr = src.read(); // IOException
arr[pos] = chr; // IndexOutOfBoundsException
}
natomiast druga nie wymaga takich zabiegów.
Uwaga: Jeśli użyto frazy throws, to instrukcja wywołująca procedurę setChar musi być ujęta w blok instrukcji try, albo procedura zawierająca taką instrukcję musi zawierać frazę throws wyszczególniającą klasę IOException.