Skip to the content.

3.1 Εισαγωγή στον Αντικειμενοστραφή Προγραμματισμό

© Γιάννης Κωστάρας


Δ >

Μαθησιακοί στόχοι

Σε αυτήν την ενότητα θα μάθουμε:

Εισαγωγή

Τα προγράμματα που γράψαμε τις προηγούμενες εβδομάδες ονομάζονται διαδικαστικά. Χάρις στο JShell μάθαμε τη σύνταξη και τις βασικές εντολές και τύπους δεδομένων της Java. Επικεντρωθήκαμε στο συντακτικό της γλώσσας και πώς να λύνουμε αλγορίθμους χωρίς να μπερδευόμαστε με άλλες λεπτομέρειες της γλώσσας.

Ο διαδικαστικός (procedural) προγραμματισμός είναι καλός για μικρά προγράμματα και προβλήματα αλλά όχι τόσο για μεγάλα προγράμματα. Γι’ αυτό το σκοπό έχει αναπτυχθεί ο αντικειμενοστραφής προγραμματισμός (Object Oriented Programming). Αν και το JShell είναι ένα θαυμάσιο εργαλείο για την εκμάθηση της γλώσσας, για να αναπτύξετε προγράμματα θα πρέπει να εντρυφήσετε στον αντικειμενοστραφή προγραμματισμό.

Καθώς ο κόσμος μας αποτελείται από αντικείμενα, αναπαριστώντας τα με κάποιον τρόπο στους Η/Υ έφερε μια νέα επανάσταση στην ανάπτυξη εφαρμογών. Οι εφαρμογές που βασίζονται στα αντικείμενα είναι πιο εύκολες στην κατανόηση και στη συντήρηση.

Η Java ήταν από την αρχή της δημιουργίας της μια αμιγώς αντικειμενοστραφής γλώσσα προγραμματισμού (από την έκδοση 8 και μετά έχει και στοιχεία συναρτησιακής, functional, γλώσσας αλλά αυτό δεν θα μας απασχολήσει σ’ αυτά τα μαθήματα).

Είδαμε ήδη στις τελευταίες ενότητες της προηγούμενης εβδομάδας την πρώτη μας κλάση (class), ότι οι κλάσεις αποτελούνται από μεθόδους (methods) και ότι η πιο σημαντική μέθοδος ενός προγράμματος είναι η main(). Στα μαθήματα αυτής και της επόμενης εβδομάδας θα επικεντρωθούμε στα αντικειμενοστραφή χαρακτηριστικά της γλώσσας.

Κλάσεις και Αντικείμενα

Η Java διαθέτει δυο κατηγορίες τύπων δεδομένων:

Μια κλάση (class) είναι όπως ένα αρχιτεκτονικό σχέδιο, ένα πρότυπο (καλούπι), μια αφαιρετική εικόνα της πραγματικότητας που περιγράφει τις ιδιότητες (γνωρίσματα) ενός αντικειμένου καθώς και τις ενέργειες που μπορούν να εκτελεστούν σ’ αυτές τις ιδιότητες. Κάθε κλάση περιγράφει μία μοναδική οντότητα με ξεχωριστές ιδιότητες (γνωρίσματα). Οι κλάσεις οργανώνονται σε δενδρικές δομές μοναδικού γονικού κόμβου (κληρονομικότητα).

Π.χ. το αυτοκίνητο αποτελεί μια κλάση, καθώς είναι μια αφαίρεση της πραγματικότητας. Κάθε αυτοκίνητο διαθέτει κάποια γνωρίσματα, π.χ. χρώμα, τελική ταχύτητα, κυβισμό, κ.ά. Επίσης, κάθε αυτοκίνητο μπορεί να κινείται, να επιταχύνει και να επιβραδύνει, να στρίβει κλπ. Αυτά αποτελούν τις ενέργειες της κλάσης.

Από μία κλάση μπορούν να δημιουργηθούν πολλά στιγμιότυπα (αντικείμενα). Π.χ. μπορούμε από τη κλάση Αυτοκίνητο να έχουμε απτά αντικείμενα, π.χ. ένα Fiat 500, ένα Peugeot 208, ένα Audi A3. Όλα τα αντικείμενα μιας κλάσης έχουν τα ίδια γνωρίσματα, αλλά οι τιμές των γνωρισμάτων διαφέρουν.

Κάθε κλάση περιγράφει μία μοναδική οντότητα με ξεχωριστές ιδιότητες. Κάθε αντικείμενο μπορεί να:

Π.χ. αν θεωρήσουμε ένα αντικείμενο της κλάσης Οδηγός, αυτό μπορεί να επιταχύνει ή/και να επιβραδύνει το στιγμιότυπο αυτοκινήτου το οποίο οδηγεί.

Άλλο παράδειγμα είναι η μοντελοποίηση μιας εταιρίας. Μια εταιρία αποτελείται από υπαλλήλους. Οπότε χρειαζόμαστε μια κλάση Employee η οποία να διαθέτει π.χ. τα εξής γνωρίσματα:

κλπ.

και π.χ. τις εξής μεθόδους:

κλπ.

Χαρακτηριστικά των αντικειμένων

Ο αντικειμενοστραφής προγραμματισμός εισήχθηκε για να βοηθήσει τους προγραμματιστές να δημιουργούν κλάσεις και αντικείμενα ως μια αφαίρεση της πραγματικότητας και έτσι να μπορούν να σχεδιάζουν πιο εύκολα και κατανοητά τα προγράμματά τους.

Η αφαιρετικότητα (abstraction) είναι επιλεκτική άγνοια! Είναι επιλογή του τι είναι σημαντικό και τι όχι. Δίνει έμφαση και εξάρτηση στα σημαντικά ενώ αγνοεί τα ΜΗ σημαντικά. Επιτυγχάνεται με τη χρήση ενθυλάκωσης (encapsulation) όπως θα δούμε παρακάτω. Σε μια καλή αντικειμενοστραφή σχεδίαση, κάθε αντικείμενο έχει συγκεκριμένο ρόλο και ευθύνες.

Τα αντικείμενα στον αντικειμενοστραφή προγραμματισμό έχουν τρία βασικά χαρακτηριστικά:

Η Java διαθέτει τους εξής τύπους δεδομένων για την αναπαράσταση κλάσεων, τους οποίους και θα μελετήσουμε στη συνέχεια:

Εγγραφές (records)

Η πιο απλή μορφή αναπαράστασης κλάσεων είναι η εγγραφή (record). Τα records εμφανίστηκαν σχετικά πρόσφατα στη γλώσσα, στην έκδοση 14.

Ας υποθέσουμε ότι θέλουμε να φτιάξουμε ένα πρόγραμμα ζωγραφικής στο οποίο ο χρήστης θα μπορεί να σχεδιάζει πάνω σε έναν καμβά, κάτι π.χ. σαν το πρόγραμμα ζωγραφικής των Windows. Θέλουμε να μοντελοποιήσουμε το σημείο. Ένα σημείο ορίζεται στο δισδιάστατο επίπεδο με δυο συντεταγμένες x και y. Ας δούμε πώς μπορούμε να γράψουμε τον τύπο δεδομένων Σημείο στην Java.

jshell> record Point(int x, int y) { 
   ...> }
|  created record Point

Το παραπάνω είναι το “καλούπι” ενός σημείου. Από αυτό μπορούμε να δημιουργήσουμε όσα σημεία, δηλ. αντικείμενα της Point, θέλουμε, ως εξής:

jshell> Point p = new Point (0, 0);
   ...> int x = p.x();
p ==> Point[x=0, y=0]
x ==> 0
    
jshell> p
p ==> Point[x=0, y=0]

Για να δημιουργήσουμε ένα νέο σημείο χρησιμοποιούμε την γνώριμη από τις κλάσεις String και array δεσμευμένη λέξη new. Μέσα στις παρενθέσεις περνάμε τις συντεταγμένες του σημείου. Αυτές αποθηκεύονται στις μεταβλητές x και y. Οι μεταβλητές αυτές ονομάζονται γνωρίσματα ή ιδιότητες (attributes) της κλάσης.

Για να προσπελάσουμε την τιμή ενός γνωρίσματος, χρησιμοποιούμε τη μέθοδο που ονομάζεται όπως το γνώρισμα και παρενθέσεις, π.χ. p.x(). Αυτή επιστρέφει την τιμή του γνωρίσματος x. Αντίστοιχα υπάρχει και η p.y() που επιστρέφει την τιμή του γνωρίσματος y.

Αλλά πώς δημιουργείται το αντικείμενο; Όταν καλούμε την new, αυτή καλεί μια ειδική μέθοδο της κλάσης η οποία ονομάζεται μέθοδος κατασκευής ή constructor. Στην περίπτωση της Point αυτή λαμβάνει δυο ορίσματα int x, int y. Θα μιλήσουμε για μεθόδους κατασκευής όταν μιλήσουμε για τις κλάσεις.

Πώς θα μπορούσαμε να ορίσουμε μια ευθεία γραμμή; Μια ευθεία γραμμή αποτελείται από δυο σημεία:

jshell> record Line(Point p1, Point p2) {}
|  created record Line

jshell> Line line = new Line(p, new Point(10, 10));
line ==> Line[p1=Point[x=0, y=0], p2=Point[x=10, y=10]]

jshell> line.p2()
$8 ==> Point[x=10, y=10]

jshell> line
line ==> Line[p1=Point[x=0, y=0], p2=Point[x=10, y=10]]

Δημιουργήσαμε μια νέα γραμμή (ένα αντικείμενο τύπου Line) δίνοντάς της δυο σημεία, ένα το p που δημιουργήσαμε προηγουμένως, και ένα νέο σημείο καλώντας την μέθοδο κατασκευής της Point και περνώντας ως παραμέτρους x=10, y=10.

Παρόμοια μπορούμε να δημιουργήσουμε και τις εγγραφές Circle, Rectangle:

jshell> record Circle(Point center, int radius) {}
|  created record Circle

jshell> record Rectangle(Point upperLeft, Point lowerRight) {}
|  created record Rectangle

Ένας κύκλος ορίζεται από το κέντρο και την ακτίνα του. Ένα ορθογώνιο ορίζεται από δυο σημεία, το πάνω αριστερό και το κάτω δεξί.

Μέσα στα άγκιστρα μπορούμε να ορίσουμε και δικές μας μεθόδους, π.χ. παρακάτω ορίζουμε μια μέθοδο που μας επιστρέφει την περιφέρεια του κύκλου:

jshell> record Circle(Point center, int radius) {
   ...>     double circumference() {
   ...>         return 2*Math.PI*radius;
   ...>     }
   ...> }
|  replaced record Circle

jshell> Circle circle = new Circle(p, 5);
circle ==> Circle[center=Point[x=0, y=0], radius=5]

jshell> circle.circumference()
$14 ==> 31.41592653589793
    
jshell> /types
|    record Point
|    record Line
|    record Rectangle
|    record Circle

Σαν άσκηση τροποποιήστε την εγγραφή Rectangle προσθέτοντάς της μια μέθοδο που να υπολογίζει το εμβαδό του ορθογώνιου παραλληλογράμμου.

Έστω ότι θέλουμε να δημιουργήσουμε μια μέθοδο που να μπορεί να αλλάζει την τιμή της ακτίνας του κύκλου αφού ένα αντικείμενο κύκλου έχει δημιουργηθεί:

record Circle(Point center, int radius) {
   double circumference() {
       return 2*Math.PI*radius;
   }
   void setRadius(int r) {
       radius = r;
   }
}
|  Error:
|  cannot assign a value to final variable radius
|         radius = r;
|         ^----^
|    update replaced variable circle which cannot be referenced until class Circle is declared

Προς έκπληξή μας δεν μας αφήνει (μας λέει ότι η radius είναι final, δηλ. σταθερά). Η εγγραφή είναι μια αμετάβλητη κλάση (immutable class), δηλ. δεν επιτρέπει ν’ αλλάξουν οι τιμές των γνωρισμάτων της μετά τη δημιουργία των αντικειμένων της.

Επίσης παρατηρήστε ότι δεν μπορούμε να προσπελάσουμε απευθείας τις τιμές των γνωρισμάτων, έξω από την εγγραφή, όπως π.χ. από το περιβάλλον JShell:

jshell> circle.radius
|  Error:
|  radius has private access in Circle
|  circle.radius
|  ^-----------^

jshell> circle.radius = 10;
|  Error:
|  radius has private access in Circle
|  circle.radius = 10;
|  ^-----------^

Η μέθοδος όμως circumference() μπορεί να προσπελάσει την radius, όπως είδαμε πιο πάνω.

Τα γνωρίσματα μιας εγγραφής είναι προσπελάσιμα από τις μεθόδους της εγγραφής, όχι όμως και εξωτερικά από την εγγραφή. Π.χ. πιο πάνω προσπαθήσαμε να προσπελάσουμε το γνώρισμα radius της Circle από το περιβάλλον του JShell, και η Point δεν μας το επέτρεψε. Ούτε μας άφησε να τροποποιήσουμε την τιμή του γνωρίσματος, αφού όπως είπαμε οι εγγραφές είναι αμετάβλητες.

Αυτό το γνώρισμα του αντικειμενοστραφούς προγραμματισμού λέγεται ενθυλάκωση (encapsulation), όπως θα δούμε σε επόμενο μάθημα, και διαφέρει σε σχέση με τον διαδικαστικό προγραμματισμό, όπου μεταβλητές και μέθοδοι είναι ανεξάρτητες. Στον αντικειμενοστραφή προγραμματισμό, οι μεταβλητές ανήκουν σε μια κλάση και μόνο αντικείμενα αυτής της κλάσης έχουν πρόσβαση στις μεταβλητές.

Κλάσεις (Class)

Η εγγραφή (record) εισήχθηκε στην Java από την έκδοση 14 και μετά. Από την αρχή της δημιουργίας της, η γλώσσα υποστηρίζει τις κλάσεις. Τα πάντα είναι κλάσεις στη Java. Ας δούμε πώς θα γράφαμε την εγγραφή Point στις εκδόσεις της Java πριν την 14:

final class Point {
	private final int x, y;
	
	public Point() {
		x=0;
		y=0;
	}
	
	public Point(int x, int y) {
		this.x = x;
		this.y = y;
	}
	
	public int getX() {
		return x;
	}
	
	public int getY() {
		return y;
	}
    
    public String toString() {
        return "Point[" + "x=" + x + ", y=" + y + "]";
    }      
}

Όπως βλέπετε, η εγγραφή μας γλυτώνει από πολύ πληκτρολόγηση! Κατ’ αρχήν βλέπουμε πώς ορίζονται τα γνωρίσματα μέσα σε μια κλάση. Τα γνωρίσματα x, y ορίζονται ως private final. Όπως έχουμε μάθει, η δεσμευμένη λέξη final δηλώνει ότι πρόκειται για σταθερές, δηλ. ότι η τιμή τους δεν μπορεί ν’ αλλάξει. Αλλά πώς είναι δυνατόν να ορίσουμε δυο σταθερές χωρίς να τους δώσουμε αρχική τιμή; Αυτό είναι κάτι που επιτρέπεται στις κλάσεις και θα δούμε στη συνέχεια γιατί. Τη δεσμευμένη λέξη private την βλέπουμε για πρώτη φορά και είναι η αντίθετη της public. Όπως ίσως θυμάστε από προηγούμενο μάθημα, το public δηλώνει ότι το γνώρισμα είναι προσβάσιμο από οποιαδήποτε άλλη κλάση και από την ΕΜ (JVM). Το private δηλώνει ότι είναι προσβάσιμο μόνο από αντικείμενα της ίδιας κλάσης.

Οι μέθοδοι getX() και getY() είναι ίδιες με τις x() και y() της εγγραφής. Απλά παλαιότερα, η σύμβαση ήταν ότι μέθοδοι προσπέλασης των γνωρισμάτων θα πρέπει να ονομάζονται ως getXXX() όπου xxx το γνώρισμα που προσπελάζουν. Παρατηρούμε ότι αυτές οι μέθοδοι είναι public.

Αλλά τι είναι οι Point() και Point(int x, int y); Καλά το μαντέψατε! Είναι οι μέθοδοι κατασκευής (constructors). Καλούνται όταν καλείται η new. Παρατηρήστε πώς διαφέρουν από τις κανονικές μεθόδους επειδή έχουν το ίδιο όνομα με το όνομα της κλάσης και δεν έχουν τύπο επιστροφής. Η πρώτη μέθοδος κατασκευής καλείται όταν δημιουργείτε ένα στιγμιότυπο (instance) ή αντικείμενο (object) της κλάσης Point χωρίς να περάσετε ορίσματα:

jshell> Point p = new Point()
p ==> Point[x=0, y=0]

ενώ η δεύτερη όταν περάσετε:

jshell> Point p = new Point(10, 10)
p ==> Point[x=10, y=10]

Σημείωση! Μην μπερδευτείτε, οι μέθοδοι κατασκευής δεν έχουν καμία σχέση με τις αναδρομικές μεθόδους (recursive methods).

Γι’ αυτό επιτρέπεται οι σταθερές x, y να μην αρχικοποιούνται κατά τη δήλωσή τους, επειδή αρχικοποιούνται μέσα στις μεθόδους κατασκευής, χωρίς αυτό να σημαίνει ότι δεν μπορείτε να τις αρχικοποιήσετε κατά τη δήλωσή τους αντί για μέσα στην μέθοδο κατασκευής.

Αλλά τι είναι αυτό το this; Το this αναφέρεται στο αντικείμενο που δημιουργείται από την κλάση. Επειδή δεν γνωρίζουμε πώς θα το ονομάσει ο προγραμματιστής, μέσα στην κλάση αναφερόμαστε σ’ αυτό με τη δεσμευμένη λέξη this. Π.χ. για το παράδειγμα της Point το this μπορεί να αναφέρεται στο p ή στο p1, δηλ. σε οποιοδήποτε αντικείμενο της κλάσης. Οπότε this.x σημαίνει το γνώρισμα x του αντικειμένου. Αλλά γιατί δεν αναφερόμαστε στο γνώρισμα απλά και μόνο με το όνομά του;

Ας δούμε λίγο καλύτερα το σώμα του δεύτερου κατασκευαστή:

	public Point(int x, int y) {
		this.x = x;
		this.y = y;
	}

Αν γράφαμε:

	public Point(int x, int y) {
		x = x;
		y = y;
	}

τότε ο μεταγλωττιστής δεν μπορεί να καταλάβει αν αναφερόμαστε στο γνώρισμα x ή στην παράμετρο x. Μάλιστα αυτό είναι ένα πολύ κοινό λάθος που γίνεται από τους προγραμματιστές κι ενώ νομίζουν ότι έχουν αλλάξει την τιμή του γνωρίσματος στην ουσία εκχωρούν στην παράμετρο x την τιμή της παραμέτρου x.

Ας το δούμε στην πράξη.

jshell> final class Point {
	private final int x, y;
	
	public Point() {
		x=0;
		y=0;
	}
	
	public Point(int x, int y) {
		x = x;
		y = y;
	}
	
	public int getX() {
		return x;
	}
	
	public int getY() {
		return y;
	}
    
    public String toString() {
        return "Point[" + "x=" + x + ", y=" + y + "]";
    }      
}
|  Error:
|  variable x might not have been initialized
|  	public Point(int x, int y) {
|      

Στο συγκεκριμένο παράδειγμα ο μεταγλωττιστής μας προειδοποιεί ότι τα γνωρίσματα x και y δεν έχουν αρχικοποιηθεί επειδή είναι final και θα ‘πρεπε να αρχικοποιηθούν στον κατασκευαστή. Αλλά ας πάμε ένα βήμα πιο πέρα. Ας μην τις ορίσουμε final.

jshell> final class Point {
	private int x, y;
	
	public Point() {
		x=0;
		y=0;
	}
	
	public Point(int x, int y) {
		x = x;
		y = y;
	}
	
	public int getX() {
		return x;
	}
	
	public int getY() {
		return y;
	}
    
    public String toString() {
        return "Point[" + "x=" + x + ", y=" + y + "]";
    }      
}
|  created class Point

jshell> Point p = new Point(10,20);
p ==> Point[x=0, y=0]

Όπως βλέπετε τα γνωρίσματα x, y δεν αρχικοποιήθηκαν από τις τιμές των ορισμάτων που περάσαμε στον κατασκευαστή. Πολύ προσοχή λοιπόν και καλό είναι να μην ονομάζουμε τις παραμέτρους όπως και τα γνωρίσματα.

Η μέθοδος toString() επιστρέφει μια φιλική προς τον χρήστη αναπαράσταση του αντικειμένου. Αν την είχαμε αφαιρέσει τότε:

jshell> Point p = new Point()
p ==> Point@497470ed

Αυτό που συμβαίνει στο παρασκήνιο είναι ότι η ΕΜ της Java διαθέτει την ανάλογη μνήμη που χρειάζεται για τη δημιουργία του αντικειμένου, αναθέτει μια μοναδική ταυτότητα στο αντικείμενό μας (Point@497470ed) και εκτελεί τον κώδικα της μεθόδου κατασκευής (constructor).

Τέλος, θα μιλήσουμε για την final στον ορισμό της κλάσης σε επόμενα μαθήματα.

Σαν άσκηση γράψτε την εγγραφή Circle που ορίσαμε πιο πάνω σαν κλάση.

Μπορούν να υπάρχουν πολλοί κατασκευαστές με το ίδιο όνομα (overloading) αλλά όχι με την ίδια υπογραφή, δηλ. θα πρέπει να περιέχουν διαφορετικό αριθμό ή/και τύπο παραμέτρων. Ένας κατασκευαστής χωρίς παραμέτρους ονομάζεται εξ’ ορισμού κατασκευαστής (default constructor). Σε περίπτωση που δεν δηλωθεί κατασκευαστής, δημιουργείται αυτόματα ο default (no-args) constructor.

Σημαντική σημείωση: Αν δηλώσουμε έστω μια μέθοδο κατασκευής σε μια κλάση, τότε δεν δημιουργείται αυτόματα default constructor.

Έτσι π.χ. αν ορίσουμε την κλάση Point χωρίς μεθόδους κατασκευαστή (απαιτείται τώρα ν’ αρχικοποιήσουμε τα γνωρίσματά της):

jshell> final class Point {
   ...>     private final int x = 0, y = 0;
   ...>     
   ...>     public int getX() {
   ...>         return x;
   ...>     }
   ...>     
   ...>     public int getY() {
   ...>         return y;
   ...>     }
   ...>     
   ...>     public String toString() {
   ...>         return "Point[" + "x=" + x + ", y=" + y + "]";
   ...>     }      
   ...> }
|  created class Point

jshell> Point p = new Point()
p ==> Point[x=0, y=0]

jshell> Point p = new Point(10, 10)
|  Error:
|  constructor Point in class Point cannot be applied to given types;
|    required: no arguments
|    found:    int,int
|    reason: actual and formal argument lists differ in length
|  Point p = new Point(10, 10);
|            ^---------------^

Βλέπουμε ότι η Java έχει εισάγει κρυφά έναν default constructor χωρίς παραμέτρους. Αυτό επειδή δεν έχουμε δηλώσει άλλους κατασκευαστές. Αν όμως δηλώσουμε τουλάχιστο μια μέθοδο κατασκευής:

jshell> final class Point {
   ...>     private final int x, y;
   ...>     
   ...>     public Point(int x, int y) {
   ...>         this.x = x;
   ...>         this.y = y;
   ...>     }
   ...>     
   ...>     public int getX() {
   ...>         return x;
   ...>     }
   ...>     
   ...>     public int getY() {
   ...>         return y;
   ...>     }
   ...>     
   ...>     public String toString() {
   ...>         return "Point[" + "x=" + x + ", y=" + y + "]";
   ...>     }      
   ...> }
|  replaced class Point

jshell> Point p = new Point(10, 10)
p ==> Point[x=10, y=10]

jshell> Point p = new Point()
|  Error:
|  constructor Point in class Point cannot be applied to given types;
|    required: int,int
|    found:    no arguments
|    reason: actual and formal argument lists differ in length
|  Point p = new Point();
|            ^---------^

τότε δεν δημιουργείται κρυφά ένας no argument default constructor.

Ένας έξυπνος τρόπος να μειώσουμε τις πιθανές αλλαγές, κι άρα και τις πιθανότητες λάθους στον κώδικά μας, είναι η χρήση της this() (μην την μπερδέψετε με το this που είδαμε πιο πάνω) όπως φαίνεται στο παρακάτω παράδειγμα:

final class Point {
    private final int x, y;

    public Point() {
        this(0, 0);
    }

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }
    
    public int getY() {
    return y;
    }
    
    public String toString() {
        return "Point[" + "x=" + x + ", y=" + y + "]";
    }      
}

Η σύνταξη this(0,0) στο σώμα του κατασκευαστή χωρίς παραμέτρους σημαίνει ότι καλεί τον άλλον κατασκευαστή περνώντας του ως ορίσματα τα 0, 0. Με αυτόν τον τρόπο μπορούμε να μειώσουμε κατά πολύ κώδικα που επαναλαμβάνεται στους διάφορους κατασκευαστές μιας κλάσης. Η τεχνική που ακολουθούμε είναι να καλούμε με την this() τον constructor με τις περισσότερες παραμέτρους που θα είναι κι αυτός που θα περιέχει την υλοποίηση. Είναι σημαντικό να σημειώσουμε εδώ ότι η this() πρέπει να είναι η πρώτη γραμμή σε μια μέθοδο κατασκευής, αλλιώς ο μεταγλωττιστής θα παραπονεθεί.

Στο σημείο αυτό θα πρέπει να προσθέσουμε ότι όπως μπορείτε να ορίσετε και τις δικές σας μεθόδους σε μια εγγραφή, μπορείτε να ορίσετε και compact constructors, όπως:

public record Point(int x, int y) { 
	public Point {
		if (x < 0 || y < 0) 
			throw new IllegalArgumentException(
			      "x and y must be positive");
	}
}

(θα μιλήσουμε για εξαιρέσεις (Exceptions) σε επόμενο μάθημα) ή ακόμα και εναλλακτικές μεθόδους κατασκευής (alternative constructors), π.χ.

public record Point(int x, int y) { 
	public static Point of(int x, int y) {
		return new Point(x, y);
	}
}

Ας δούμε ακόμα ένα παράδειγμα. Ας υποθέσουμε ότι θέλουμε να γράψουμε ένα παιχνίδι αγώνων αυτοκινήτων σε Java. Χρειαζόμαστε λοιπόν, μιαν αφαιρετική αναπαράσταση ενός αυτοκινήτου, δηλ. μια κλάση Car. Τι γνωρίσματα θέλουμε να έχουμε για την κλάση αυτή (η κατάστασή του); Μας ενδιαφέρει να γνωρίζουμε το μοντέλο του (π.χ. Tesla Model S), την μέγιστη ταχύτητά του, τον κυβισμό του κλπ. καθώς επίσης και την ταχύτητά του κάθε στιγμή (καθώς θα τρέχει σε αγώνες).

Τι συμπεριφορά θα θέλαμε να έχει το αυτοκίνητο; Θα θέλαμε να μπορούμε να επιταχύνουμε όταν πατάμε το γκάζι και να επιβραδύνουμε όταν πατάμε το φρένο. Επίσης, να στρίβουμε.

Τα παραπάνω τα συνοψίζουμε στην ακόλουθη κλάση Car:

class Car { // κλάση
  // ιδιότητες/γνωρίσματα
  String model;
  int maxSpeed;
  int ccm;
  int speed = 0;
  // μέθοδος δημιουργίας αντικειμένων - κατασκευαστής
  Car(String m, int s, int c) {
    model = m; maxSpeed = s; ccm = c;
  }
  // ενέργειες/μέθοδοι
  void accelerate() {
     if (speed <= maxSpeed - 10)
        speed+=10;
  }
  void decelerate() {
     if (speed >= 10)
        speed-=10;
  }
  public String toString() {
     return "Car[" + "model=" + model + ", maxSpeed=" + maxSpeed + ", ccm=" + ccm + ", speed=" + speed + "]"; 
  }
}

Η κλάση Car περιέχει 4 γνωρίσματα (model, maxSpeed, ccm, speed), τρεις μεθόδους (accelerate(), decelerate(), toString()) και μια μέθοδο κατασκευής.

// Αντικείμενα
jshell> Car audiA3 = new Car("Audi A3", 210, 1595);
audiA3 ==> Car[model=Audi A3, maxSpeed=210, ccm=1595, speed=0]

jshell> audiA3.speed
$1 ==> 0
    
jshell> audiA3.accelerate();

jshell> audiA3.speed
$2 ==> 10

Στο παραπάνω τμήμα κώδικα δημιουργήσαμε ένα νέο αντικείμενο audiA3 χρησιμοποιώντας τη δεσμευμένη λέξη new και καλώντας τον κατασκευαστή της κλάσης. Στη συνέχεια ζητάμε να δούμε την τρέχουσα ταχύτητά του καλώντας το γνώρισμα χωριζόμενο από το όνομα του αντικειμένου με την .. Γενικά, όπως είδαμε, δεν είναι καλή τακτική να καλούμε απευθείας τα γνωρίσματα ενός αντικειμένου. Στη συνέχεια, καλούμε τη μέθοδο accelerate() με το ίδιο τρόπο, η οποία έχει ως αποτέλεσμα ν’ αυξήσει την ταχύτητα του αυτ/του κατά 10 χλμ/ώ. Καλώντας πάλι το γνώρισμα speed βλέπουμε ότι τώρα πλέον το αυτοκίνητό μας κινείται με ταχύτητα 10 χλμ/ώ.

Με τον ίδιο τρόπο μπορούμε να δημιουργήσουμε όσα στιγμιότυπα (αντικείμενα) της κλάσης Car θέλουμε παρέχοντας απλώς τις κατάλληλες τιμές (δηλ. μοντέλο, τελική ταχύτητα και κυβισμό) στη μέθοδο κατασκευαστή της κλάσης Car.

Αλλά τι συμβαίνει όταν δημιουργούμε ένα αντικείμενο με την new (όταν δηλ. καλούμε τον constructor); Η JVM καλεί το Λ.Σ. να δεσμεύσει ένα χώρο μνήμης τόσο ώστε να χωρέσει το αντικείμενό μας. Όπως θα δούμε λίγο πιο κάτω, η μνήμη χωρίζεται σε δυο μεγάλες περιοχές, την στοίβα (stack) και τον σωρό (heap). Η ΕΜ της Java αποθηκεύει μεταβλητές πρωτογενών τύπων στη στοίβα γιατί γνωρίζει εξ’ αρχής πόσο χώρο μνήμης απαιτούν (βλ. Εικόνα 3.1.1). Στην περίπτωση των αντικειμένων όμως, ο χώρος μνήμης που θα χρειαστούν δεν είναι πάντοντε γνωστός εκ των προτέρων. Στην περίπτωση αυτή, η ΕΜ δεσμεύει έναν χώρο στην στοίβα για ν’ αποθηκεύσει την διεύθυνση μνήμης στο σωρό όπου θα αποθηκευθούν τα δεδομένα του αντικείμενου (βλ. Εικόνα 3.1.2). Αυτό που κάνει η new επομένως είναι να δεσμεύει ένα χώρο στη μνήμη σωρού και να επιστρέφει στην μεταβλητή αντικειμένου τη διεύθυνση αυτού του χώρου. Γι’ αυτό το λόγο, π.χ. η p ή η audiA3 δεν περιέχει το αντικείμενο που δημιουργήθηκε, αλλά έναν δείκτη (pointer) ή μια αναφορά (reference) στη διεύθυνση μνήμης που δημιουργήθηκε το αντικείμενο. Και φυσικά μπορούμε να έχουμε περισσότερους του ενός δείκτες (δηλ. μεταβλητές) που να δείχνουν στην ίδια διεύθυνση μνήμης (στο ίδιο αντικείμενο στη μνήμη σωρού).

Πώς μπορούμε να δηλώσουμε ένα στιγμιότυπο μιας κλάσης αλλά να το αρχικοποιήσουμε αργότερα;

jshell> Car teslaS = null;
teslaS ==> null

Η δεσμευμένη λέξη null δηλώνει το τίποτα. Στις γλώσσες προγραμματισμού σημαίνει ότι ο μεταγλωττιστής δεν δεσμεύει ακόμα μνήμη για το αντικείμενό μας. Ο δείκτης στη στοίβα για το αντικείμενό μας δεν δείχνει ακόμα σε κάποια δ/νση μνήμης σωρού.

Αν προσπαθήσουμε να καλέσουμε μια μέθοδο ενός αντικειμένου που δεν έχει αρχικοποιηθεί ακόμα τότε:

jshell> teslaS.toString()
|  Exception java.lang.NullPointerException: Cannot invoke "REPL.$JShell$33$Car.toString()" because "REPL.$JShell$34.teslaS" is null
|        at (#4:1)

λαμβάνουμε την διάσημη εξαίρεση (exception) NullPointerException, τον τρόμο και των φόβο των προγραμματιστών. Πολύ μεγάλη προσοχή λοιπόν, πάντα να αρχικοποιείτε τα αντικείμενά σας προτού τα χρησιμοποιήσετε.

Διεπαφές (Interface)

Η διεπαφή (interface) είναι ένα είδος κλάσης που περιέχει όμως μόνο σκελετούς μεθόδων (δηλ. χωρίς υλοποίηση) ή/και σταθερές. Π.χ.

jshell> public interface CarInterface {
   ...>   int defaultAcceleration = 10;
   ...>   void accelerate(int by);  
   ...>   void decelerate(int by);
   ...>   void turn(int byDegrees);
   ...> }
|  created interface CarInterface

Δεν μπορούμε να δημιουργήσουμε απευθείας αντικείμενα μιας διεπαφής (εκτός κι αν παρέχουμε υλοποιήσεις όλων των μεθόδων της). Θα μιλήσουμε αναλυτικά για διεπαφές σε επόμενο μάθημα.

Απαριθμημένοι Τύποι (Enum)

Είδαμε για πρώτη φορά τους απαριθμημένους τύπους όταν μιλήσαμε για τις εντολές ελέγχου.

enum Choice {
  RED_PILL, BLUE_PILL
}

Ο enum στην Java είναι ένα είδος κλάσης. Θα μιλήσουμε αναλυτικά για απαριθμημένους τύπους σε επόμενο μάθημα.

Αποθήκευση μεταβλητών στην μνήμη. Στοίβα (Stack) και Σωρός (Heap)

Οι περισσότερες γλώσσες προγραμματισμού χρησιμοποιούν δυο είδη μνήμης για να αποθηκεύσουν τα δεδομένα των προγραμμάτων τους. Τα δυο αυτά είδη μνήμης είναι:

Στοίβα (Stack)

Η στοίβα δουλεύει το ίδιο με μια στοίβα από πιάτα. Το πρώτο πιάτο που θα τοποθετήσουμε στη στοίβα θα είναι και το τελευταίο που θα πάρουμε πίσω καθώς πρώτα θα πρέπει να πάρουμε πρώτα όλα τα πιάτα που έχουμε τοποθετήσει πάνω του. Ονομάζεται και LIFO (Last In First Out). Είναι περιορισμένη σε μέγεθος. Σ’ αυτήν αποθηκεύονται μεταβλητές πρωτογενών τύπων δεδομένων.

Ας δούμε ένα παράδειγμα. Η παρακάνω εικόνα μας παρουσιάζει πώς η Java εκτελεί την πρόσθεση δυο ακεραίων αριθμών στη στοίβα.

Πρώτα αποθηκεύεται στη στοίβα η τιμή της μεταβλητής a και στη συνέχεια η τιμή της μεταβλητής b. Στην συνέχεια το πρόγραμμα ζητάει την πρόσθεση αυτών των δυο αριθμών. Μπορείτε να θεωρήσετε ότι ο τελεστής + είναι η συντομογραφία της μεθόδου int add(int a, int b);. Ακολούθως, ανακτούνται από τη στοίβα με την αντίστροφη σειρά από την οποία εισήχθηκαν στη στοίβα, δηλ. πρώτα η μέθοδος +, στη συνέχεια η τιμή της μεταβλητής b και τέλος η τιμή της μεταβλητής a, εκτελείται η πράξη και το αποτέλεσμα αποθηκεύεται πάλι στη στοίβα.

Εικόνα 3.1.1 Στοίβα (Stack)

Σωρός (Heap)

Ο σωρός δεν έχει κάποιον περιορισμό, θεωρητικά μπορεί να είναι όσο και η μνήμη RAM του Η/Υ σας. Η πρόσβαση σ’ αυτόν είναι όμως πιο αργή σε σχέση με την στοίβα. Στην μνήμη σωρού αποθηκεύονται τα αντικείμενα. Όταν δημιουργούμε ένα αντικείμενο με τον τελεστή new αυτό που συμβαίνει είναι να αποθηκευθεί στη στοίβα ένας δείκτης ο οποίος δείχνει σε μια θέση μνήμης στη μνήμη σωρού, όπως φαίνεται στην παρακάτω εικόνα. Το ίδιο ισχύει για τα αντικείμενα συστοιχιών (arrays) και τύπου String.

Εικόνα 3.1.2 Σωρός (Heap)

Πέρασμα παραμέτρων με αναφορά (By Reference)

Στο μάθημα που μιλήσαμε για τις μεθόδους μάθαμε ότι αν περάσουμε έναν πρωτογενή τύπο σαν όρισμα σε μια μέθοδο, τότε δημιουργείται ένα αντίγραφο αυτού στο σώμα της μεθόδου (pass by value). Αντιθέτως, αν περάσουμε ένα αντικείμενο σαν όρισμα σε μια μέθοδο, τότε περνάμε ένα αντίγραφο της αναφοράς μνήμης αυτού του αντικειμένου (by reference), κι ό,τι αλλαγές επικαλείται η μέθοδος σ’ αυτό το αντικείμενο, εφαρμόζονται στο ίδιο το αντικείμενο (δεν δημιουργείται κάποιο αντίγραφο του αντικειμένου, μόνο του δείκτη που δείχνει σε αυτό):

jshell> void setSpeed(Car car) {
   ...>    car.speed = 50;
   ...> }
|  created method setSpeed(Car)

jshell> setSpeed(audiA3)

jshell> audiA3.speed
$1 ==> 50

Εδώ βλέπουμε ότι περνώντας το αντικείμενο audiA3 που δημιουργήσαμε πιο πάνω στη μέθοδο setSpeed, όντως αλλάζει την ταχύτητα του αντικειμένου.

Εικόνα 3.1.3 Κλήση by sharing στη Java, µέθοδος setSpeed()

Στη Java δε γίνεται μια μέθοδος να δημιουργήσει ένα αντικείμενο και να το επιστρέψει ως μια τιμή μιας παραμέτρου της.

jshell> void createCar(Car car) {
   ...>    car = new Car("BMW X1", 200, 1996);
   ...> }
|  created method createCar(Car)

jshell> createCar(audiA3)

jshell> audiA3.model
$2 ==> "Audi A3"

Τι συνέβει; Όταν καλέσαμε τη μέθοδο createCar(audiA3) της περάσαμε ένα αντίγραφο της διεύθυνσης μνήμης του αντικειμένου audiA3. Η μέθοδος όμως δημιούργησε ένα νέο αντικείμενο (σε μια νέα διεύθυνση μνήμης) και έχασε τη διεύθυνση μνήμης του αντικειμένου audiA3. Το νέο αντικείμενο car που δημιούργησε χρησιμοποιείται μόνο μέσα στη μέθοδο createCar και δεν έχει αναφορά σ’ αυτό καμιά άλλη μέθοδος ούτε το κυρίως πρόγραμμα. Το αντικείμενο καταστρέφεται με το τέλος του σώματος της μεθόδου (δηλ. στο ‘}’).

Σημείωση! Ο όρος “call by reference” που χρησιμοποιείται στη Java δεν είναι 100% σωστός, όπως περιγράφεται σ’ αυτό το άρθρο. Πιο σωστός είναι ο όρος “call by sharing” για το λόγο που είδαμε στο προηγούμενο παράδειγμα, δηλ. δε μπορούμε να αλλάξουμε την αναφορά μνήμης που περνιέται ως παράμετρος. Στην ουσία δημιουργείται ένα αντίγραφο της διεύθυνσης μνήμης που δείχνει η παράμετρος με το οποίο αντίγραφο μπορεί η μέθοδος ν’ αλλάξει τα γνωρίσματα του αντικειμένου που δείχνει αυτή η διεύθυνση μνήμης (αν το αντικείμενο είναι μεταβλητό – mutable). Ή πιο απλά, όλες οι κλήσεις μεθόδων στη Java είναι στην ουσία “by value” καθώς δημιουργείται ένα αντίγραφο του ορίσματος που περνάμε (είτε αυτό είναι πρωτογενής τύπος είτε δείκτης σε αντικείμενο).

Εικόνα 3.1.4 Κλήση by sharing στη Java, μέθοδος createCar()

Αν θέλουμε όντως η μέθοδος να δημιουργήσει ένα νέο αντικείμενο, θα πρέπει η μέθοδος να επιστρέψει το νέο αντικείμενο όπως στην παρακάτω μέθοδο κατασκευής (factory method):

jshell> Car createCar(String model, int maxSpeed, int ccm) {
   ...>    return new Car(model, maxSpeed, ccm);
   ...> }
|  created method createCar(String,int,int)

Τύποι Επικάλυψης (Wrapper types)

Στα μαθήματα της πρώτης εβδομάδας μάθαμε για τους πρωτογενείς τύπους δεδομένων (raw data types) και την κλάση String. Η Java διαθέτει αντίστοιχες κλάσεις επικάλυψης για τους πρωτογενείς τύπους δεδομένων:

Πρωτογενής τύπος Τύπος αναφοράς
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

Η μετατροπή ενός πρωτογενούς τύπου στην αντίστοιχη κλάση του ονομάζεται εγκλεισμός (boxing) ενώ η αντίστροφη μετατροπή απεγκλεισμός (unboxing). Η γλώσσα κάνει αυτές τις μετατροπές αυτόματα τις περισσότερες φορές. Π.χ. η έκφραση:

jshell> Integer val = 10; // autoboxing int --> Integer 
val ==> 10

jshell> int ival = val; // auto-unboxing Integer --> int
ival ==> 10

χωρίς autoboxing θα ‘πρεπε να γραφτεί (όπως και συνέβαινε σε παλαιότερες εκδόσεις της γλώσσας):

jshell> Integer val = new Integer(10); // Boxing: int --> Integer 
val ==> 10

jshell> int ival = val.intValue(); // Unboxing: Integer --> int
ival ==> 10

Προσοχή! Στην ενότητα 1.6 μάθαμε για τη μετατροπή μεταξύ πρωτογενών τύπων δεδομένων (casting), η οποία συνοψίζεται στο παρακάτω διάγραμμα.

Εικόνα 1.6.2 Διάγραμμα μετατροπής τύπων δεδομένων

Το παραπάνω δεν ισχύει για τους τύπους επικάλυψης (wrapper classes). Π.χ. ενώ μπορούμε να αναθέσουμε απευθείας μια τιμή int σε μια μεταβλητή τύπου long χωρίς να παραπονεθεί ο μεταγλωττιστής, δεν μπορούμε να αναθέσουμε απευθείας μια τιμή Integer σε μια μεταβλητή τύπου Long (not assignable compatible):

jshell> int i = 10;
i ==> 10

jshell> long l = i;
l ==> 10

jshell> Integer ii = 10;
ii ==> 10

jshell> Long ll = ii;
|  Error:
|  incompatible types: java.lang.Integer cannot be converted to java.lang.Long
|  Long ll = ii;

Ονοματολογία

Κάθε γλώσσα προγραμματισμού ορίζει κάποιες συμβάσεις ονοματολογίας για τα διάφορα στοιχεία της γλώσσας οι οποίες όταν ακολουθούνται καθιστούν τα προγράμματα πιο ευανάγνωστα (βλ. πηγές [8,13]). Αυτές περιγράφονται συνοπτικά παρακάτω:

Ως γενικός κανόνας, για την ονομασία των ονομάτων που ορίζουμε εμείς στο πρόγραμμα, ακολουθείται, εκ συμβάσεως, ο “τρόπος καμήλας” (Camel case), δηλ. όταν ξεκινά μια νέα λέξη αυτή ξεκινά με κεφαλαίο γράμμα. Από κει και πέρα:

Περίληψη

Σε αυτήν την ενότητα εξερευνήσαμε τα βασικά στοιχεία των κλάσεων (classes) και των αντικειμένων (objects). Μάθαμε ότι τα αντικείμενα καθορίζονται από κλάσεις. Οι κλάσεις αντιπροσωπεύουν τη γενική έννοια των πραγμάτων, ενώ τα αντικείμενα αντιπροσωπεύουν συγκεκριμένες περιπτώσεις μιας κλάσης. Μπορούμε να έχουμε πολλά αντικείμενα μιας κλάσης.

Τα αντικείμενα έχουν μεθόδους (methods) που χρησιμοποιούμε για να επικοινωνήσουμε μαζί τους. Μπορούμε να χρησιμοποιήσουμε μια μέθοδο για να κάνουμε μια αλλαγή στο αντικείμενο ή για λήψη πληροφοριών από το αντικείμενο. Οι μέθοδοι μπορεί να έχουν παραμέτρους και οι παράμετροι έχουν τύπους. Οι μέθοδοι έχουν τύπους επιστροφής, οι οποίοι καθορίζουν τον τύπο δεδομένων που επιστρέφουν. Εάν ο τύπος επιστροφής είναι void, δεν επιστρέφουν τίποτα.

Τα αντικείμενα αποθηκεύουν δεδομένα σε πεδία (fields) (τα οποία έχουν και τύπους). Όλες οι τιμές δεδομένων ενός αντικειμένου μαζί αναφέρεται ως η κατάσταση (state) του αντικειμένου.

Τα αντικείμενα δημιουργούνται από ορισμούς κλάσεων που έχουν γραφτεί σε μια συγκεκριμένη γλώσσα προγραμματισμού. Μεγάλο μέρος του προγραμματισμού στη Java αφορά την εκμάθηση της σύνταξης ορισμών κλάσεων. Ένα μεγάλο πρόγραμμα Java θα έχει πολλές κλάσεις, η καθεμία με πολλές μεθόδους που καλούν η μία την άλλη με πολλούς διαφορετικούς τρόπους.

Για να μάθουμε να αναπτύσσουμε προγράμματα Java, πρέπει να μάθουμε πώς να γράφουμε ορισμούς κλάσεων, συμπεριλαμβανομένων των πεδίων και μεθόδων, και πώς να συνδυάσουμε αυτές τις κλάσεις. Οι υπόλοιπες ενότητες αυτού του μαθήματος πραγματεύονται αυτά τα ζητήματα.

Ασκήσεις

1) Να δημιουργήσετε μια κλάση Student για μαθητές λυκείου με τα εξής γνωρίσματα:

jshell> Student ioannis = new Student("Γιάννης", "Αντεκοτούμπο", 16);
Student{am=1, firstName=Γιάννης, lastName=Αντεκοτούμπο, age=16, classRoom=null}

jshell> System.out.println("AM: " + ioannis.getAm());   // 1
AM: 1

jshell> ioannis.setClassRoom("Β1");

jshell> System.out.println(ioannis.getClassRoom());     // Β1
B1

κλπ. Οι μέθοδοι θα πρέπει να ελέγχουν για την εγκυρότητα των ορισμάτων τους.

2) Να δημιουργήσετε μια κλάση SchoolClass η οποία θα μπορεί να δέχεται μέχρι 30 μαθητές τύπου Student. Η τάξη θα διαθέτει επίσης ένα αναγνωριστικό το οποίο θα είναι ίδιο με το γνώρισμα τάξη που ορίσατε για τον μαθητή στην προηγούμενη άσκηση. Ο χρήστης της κλάσης θα μπορεί να προσθέτει και να αφαιρεί μαθητές σ’ αυτή. Όταν ένας μαθητής προστίθεται στην τάξη, τότε θα πρέπει να εκχωρείτε αντίστοιχα και το γνώρισμα τάξη του αντικειμένου Student να είναι η σωστή τάξη.

jshell> ClassRoom b1 = new ClassRoom("B1", 30);
b1 ==> ClassRoom{name=Β1, size=30, numOfStudents=0}

jshell> b1.addStudent(ioannis);

jshell> ioannis.getClassRoom(); 
$1 ==> "B1"
    
jshell> b1
b1 ==> ClassRoom{name=Β1, size=30, numOfStudents=1}

jshell> b1.removeStudent(ioannis);

jshell> ioannis.getClassRoom();
$2 ==> null
    
jshell> b1
b1 ==> ClassRoom{name=Β1, size=30, numOfStudents=0}

Περαιτέρω μελέτη

Υπάρχουν δυο πολύ ενδιαφέροντες ιστοτόποι εκμάθησης της γλώσσας Java (στα Αγγλικά) τους οποίους συνιστούμε ανεπιφύλακτα για να μελετάτε τα μαθήματά τους παράλληλα με αυτό το μάθημα. Αν επιθυμείτε να παρακολουθήσετε παράλληλα και αυτά τα μαθήματα, καλό είναι να εγγραφείτε (register).

  1. CodeGym
  2. CodecAdemy

Αφού εγγραφείτε, στο CodecAdemy απλά πατήστε το Start για να ξεκινήσετε το πρώτο σας μάθημα.

Στο CodeGym, ακολουθήστε τον υπερσύνδεσμο Courses από το μενού στα αριστερά και στη συνέχεια JAVA DEVELOPER και μετά READING (A STORY LINE). Θα πρέπει να καταλήξετε στη σελίδα όπου εμφανίζεται το Java For Beginners.

Πηγές

  1. “The Java Tutorial”
  2. Darwin I. F. (2014), Java Cookbook, 3rd Ed., O’ Reilly.
  3. Deitel P., Deitel H. (2018), Java How to Program, 11th Ed., Safari.
  4. Downey A. B., Mayfield C. (2016), Think Java, O’ Reilly.
  5. Eckel B. (2006), Thinking in Java, 4th Ed., Prentice-Hall.
  6. Evans B. (2020), Records Come to Java, Java Magazine.
  7. Evans B. J., Flanagan D. (2019), Java in a Nutshell, 7th Ed., O’ Reilly.
  8. Google Java Style Guide
  9. Horstmann C. S. (2016), Core Java, Volume 1 Fundamentals, 10th Ed., Prentice-Hall.
  10. Horstmann C. S. (2018), Core Java SE 9 for the impatient, 2nd Ed., Addison-Wesley.
  11. Horstmann C., Big Java 5 - Chapter 2 - Using Objects
  12. Java Notes for Professionals
  13. Java Style Guide
  14. Juneau J. (2017), Java 9 Recipes, 3rd Ed., APress.
  15. Liguori R. & Liguori P. (2014), Java 8 Pocket Guide, O’Reilly.
  16. Μαργαρίτης Κ. (2004), Εισαγωγή στη Java
  17. Sierra K. & Bates B. (2022), Head First Java, 3rd Ed. Covers Java 8-17, O’Reilly.
  18. Τζίτζικας Γ. (2016), Μια Γεύση από Java, LeanPub.

Δ >