Βέλτιστες πρακτικές από το «Effective Java (3rd Edition)»
Απόσπασμα από το «Effective Java (3rd Edition) » του Joshua Bloch
1. Επίστρεφε κενές συλλογές ή πίνακες, όχι null
Αν η μέθοδος επιστρέφει ένα Set, μη γράφεις return null; για κενό αποτέλεσμα – γράψε αντί γι’ αυτό return Set.of();.
2. Σκέψου στατικές factory μεθόδους αντί για constructors
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
Δεν είναι πάντα καλύτερες, αλλά σε ορισμένες περιπτώσεις χρήσης προσφέρουν πολλά πλεονεκτήματα:
- Ένα πλεονέκτημα των στατικών factory μεθόδων είναι ότι, σε αντίθεση με τους constructors, έχουν ονόματα.
- Δεν είναι υποχρεωμένες να δημιουργούν νέο αντικείμενο κάθε φορά που καλούνται.
- Μπορούν να επιστρέψουν αντικείμενο οποιουδήποτε υποτύπου του τύπου επιστροφής τους.
- Η κλάση του επιστρεφόμενου αντικειμένου μπορεί να διαφέρει από κλήση σε κλήση, ανάλογα με τις παραμέτρους εισόδου.
- Η κλάση του επιστρεφόμενου αντικειμένου δεν χρειάζεται να υπάρχει όταν γράφεται η κλάση που περιέχει τη μέθοδο.
Μειονεκτήματα:
- Ο κύριος περιορισμός όταν παρέχεις μόνο στατικές factory μεθόδους είναι ότι κλάσεις χωρίς public ή protected constructors δεν μπορούν να αποκτήσουν υποκλάσεις.
- Ένα δεύτερο μειονέκτημα των στατικών factory μεθόδων είναι ότι οι προγραμματιστές τις βρίσκουν δύσκολα.
3. Σκέψου έναν builder όταν έχεις πολλές παραμέτρους στον constructor
Τα στατικά factories και οι constructors έχουν έναν κοινό περιορισμό: δεν κλιμακώνονται καλά όταν υπάρχουν πολλές προαιρετικές παράμετροι. Αυτό χτύπησε πολύ άσχημα κάποια projects στα οποία δούλεψα.
4. Επίβαλε την ιδιότητα singleton με ιδιωτικό constructor ή με τύπο enum
Ό,τι δεν χρειάζεται πολλαπλά στιγμιότυπα πρέπει να δημιουργείται μόνο μία φορά – είτε με @ApplicationScoped είτε με άλλο μοτίβο singleton.
5. Απότρεψε τη δημιουργία στιγμιοτύπων με ιδιωτικό constructor
Κάποιες φορές θα θέλεις να γράψεις μια κλάση που είναι απλώς μια ομάδα στατικών μεθόδων και στατικών πεδίων. Τέτοιες κλάσεις έχουν αποκτήσει κακή φήμη, επειδή κάποιοι τις καταχρώνται για να αποφύγουν να σκέφτονται με όρους αντικειμένων, αλλά έχουν και θεμιτές χρήσεις.
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. Απόφευγε τη δημιουργία περιττών αντικειμένων
String s = new String("bikini"); // DON'T DO THIS!
Ή:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
Λύση: Αυτό το πρόγραμμα δίνει τη σωστή απάντηση, αλλά είναι πολύ πιο αργό απ’ όσο θα έπρεπε, εξαιτίας ενός τυπογραφικού λάθους ενός χαρακτήρα. Η μεταβλητή sum δηλώνεται ως Long αντί για long, πράγμα που σημαίνει ότι το πρόγραμμα κατασκευάζει περίπου 2^31 περιττά στιγμιότυπα Long (σχεδόν ένα για κάθε φορά που το long i προστίθεται στο Long sum).
7. Απόφευγε τους finalizers και τους cleaners
Οι finalizers είναι απρόβλεπτοι, συχνά επικίνδυνοι και γενικά περιττοί. Η χρήση τους μπορεί να προκαλέσει ασταθή συμπεριφορά, κακή απόδοση και προβλήματα φορητότητας.
8. Προτίμησε το try-with-resources από το try-finally
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. Σκέψου να υλοποιήσεις το Comparable
Υλοποιώντας το Comparable, επιτρέπεις στην κλάση σου να συνεργάζεται με όλους τους πολλούς γενικούς αλγορίθμους και τις υλοποιήσεις συλλογών που βασίζονται σε αυτό το interface.
10. Ελαχιστοποίησε την προσβασιμότητα των κλάσεων και των μελών
Ο πιο σημαντικός παράγοντας που διακρίνει ένα καλοσχεδιασμένο component από ένα κακοσχεδιασμένο είναι ο βαθμός στον οποίο το component κρύβει τα εσωτερικά του δεδομένα και τις άλλες λεπτομέρειες υλοποίησης από τα άλλα components. Ένα καλοσχεδιασμένο component κρύβει όλες τις λεπτομέρειες υλοποίησής του, διαχωρίζοντας καθαρά το API του από την υλοποίησή του. Τα components επικοινωνούν τότε μόνο μέσω των API τους και αγνοούν την εσωτερική λειτουργία το ένα του άλλου. Η ιδέα αυτή, γνωστή ως απόκρυψη πληροφορίας ή ενθυλάκωση, είναι θεμελιώδης αρχή του σχεδιασμού λογισμικού.
11. Στις public κλάσεις, χρησιμοποίησε μεθόδους πρόσβασης, όχι public πεδία
Κάποιες φορές μπορεί να μπεις στον πειρασμό να γράψεις εκφυλισμένες κλάσεις που δεν εξυπηρετούν κανέναν άλλο σκοπό πέρα από την ομαδοποίηση πεδίων στιγμιοτύπου:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
Επειδή τα πεδία δεδομένων τέτοιων κλάσεων προσπελαύνονται απευθείας, οι κλάσεις αυτές δεν προσφέρουν τα οφέλη της ενθυλάκωσης. Δεν μπορείς να αλλάξεις την αναπαράσταση χωρίς να αλλάξεις το API, δεν μπορείς να επιβάλεις αναλλοίωτες συνθήκες και δεν μπορείς να εκτελέσεις βοηθητική ενέργεια όταν γίνεται πρόσβαση σε ένα πεδίο. Οι αδιάλλακτοι του αντικειμενοστραφούς προγραμματισμού θεωρούν ότι τέτοιες κλάσεις είναι ανάθεμα και πρέπει πάντα να αντικαθίστανται από κλάσεις με ιδιωτικά πεδία και public μεθόδους πρόσβασης (getters) και, για τις μεταβλητές κλάσεις, μεθόδους τροποποίησης (setters):
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
Σίγουρα οι αδιάλλακτοι έχουν δίκιο όσον αφορά τις public κλάσεις: αν μια κλάση είναι προσβάσιμη έξω από το package της, παρέχεις μεθόδους πρόσβασης για να διατηρήσεις την ευελιξία να αλλάξεις την εσωτερική αναπαράσταση της κλάσης. Αν μια public κλάση εκθέτει τα πεδία δεδομένων της, κάθε ελπίδα να αλλάξει η αναπαράστασή της χάνεται, επειδή ο κώδικας των clients μπορεί να είναι διασκορπισμένος παντού.
Ωστόσο, αν μια κλάση είναι package-private ή είναι ιδιωτική εμφωλευμένη κλάση, δεν υπάρχει τίποτα εγγενώς λάθος στο να εκθέτει τα πεδία δεδομένων της – με την προϋπόθεση ότι περιγράφουν επαρκώς την αφαίρεση που παρέχει η κλάση.
12. Ελαχιστοποίησε τη μεταβλητότητα
Μια immutable κλάση είναι απλώς μια κλάση της οποίας τα στιγμιότυπα δεν μπορούν να τροποποιηθούν. Όλη η πληροφορία που περιέχεται σε κάθε στιγμιότυπο είναι σταθερή για όλη τη διάρκεια ζωής του αντικειμένου, οπότε καμία αλλαγή δεν μπορεί ποτέ να παρατηρηθεί. Οι βιβλιοθήκες της πλατφόρμας Java περιέχουν πολλές immutable κλάσεις, μεταξύ των οποίων η String, οι boxed primitive κλάσεις, καθώς και οι BigInteger και BigDecimal. Υπάρχουν πολλοί καλοί λόγοι γι’ αυτό: οι immutable κλάσεις σχεδιάζονται, υλοποιούνται και χρησιμοποιούνται πιο εύκολα από τις mutable. Είναι λιγότερο επιρρεπείς σε λάθη και πιο ασφαλείς. Για να κάνεις μια κλάση immutable, ακολούθησε αυτούς τους πέντε κανόνες:
- Μην παρέχεις μεθόδους που τροποποιούν την κατάσταση του αντικειμένου (γνωστές ως mutators).
-
Φρόντισε να μην μπορεί να επεκταθεί η κλάση. Έτσι αποτρέπεις απρόσεκτες ή κακόβουλες υποκλάσεις από το να υπονομεύσουν την immutable συμπεριφορά της κλάσης, συμπεριφερόμενες σαν να έχει αλλάξει η κατάσταση
του αντικειμένου. Η αποτροπή υποκλάσεων επιτυγχάνεται συνήθως, για παράδειγμα, δηλώνοντας την κλάση
final. - Κάνε όλα τα πεδία final. Έτσι εκφράζεις καθαρά την πρόθεσή σου με τρόπο που επιβάλλεται από το σύστημα. Επίσης, είναι απαραίτητο για να εξασφαλιστεί σωστή συμπεριφορά αν μια αναφορά σε ένα νεοδημιούργητο στιγμιότυπο περάσει από ένα thread σε άλλο χωρίς συγχρονισμό, όπως περιγράφεται στο μοντέλο μνήμης.
- Κάνε όλα τα πεδία ιδιωτικά. Έτσι εμποδίζεις τους clients να αποκτήσουν πρόσβαση σε mutable αντικείμενα στα οποία αναφέρονται τα πεδία και να τα τροποποιήσουν απευθείας. Αν και τεχνικά επιτρέπεται μια immutable κλάση να έχει public final πεδία που περιέχουν primitive τιμές ή αναφορές σε immutable αντικείμενα, δεν συνιστάται, επειδή αποκλείει την αλλαγή της εσωτερικής αναπαράστασης σε μια μεταγενέστερη έκδοση.
-
Εξασφάλισε αποκλειστική πρόσβαση σε κάθε mutable component. Αν η κλάση σου έχει πεδία που αναφέρονται σε mutable αντικείμενα, φρόντισε οι clients της κλάσης να μην μπορούν να αποκτήσουν αναφορές σε
αυτά τα αντικείμενα. Ποτέ μην αρχικοποιείς ένα τέτοιο πεδίο με αναφορά αντικειμένου που παρείχε ο client και ποτέ μην επιστρέφεις το πεδίο από μια μέθοδο πρόσβασης. Φτιάχνε αμυντικά αντίγραφα στους constructors, στις μεθόδους πρόσβασης και στις
μεθόδους
readObject.
13. Προτίμησε τη σύνθεση από την κληρονομικότητα
Η κληρονομικότητα είναι ένας ισχυρός τρόπος επαναχρησιμοποίησης κώδικα, αλλά δεν είναι πάντα το καλύτερο εργαλείο για τη δουλειά. Όταν χρησιμοποιείται ακατάλληλα, οδηγεί σε εύθραυστο λογισμικό. Η κληρονομικότητα είναι ασφαλής μέσα σε ένα package, όπου οι υλοποιήσεις της υποκλάσης και της υπερκλάσης βρίσκονται υπό τον έλεγχο των ίδιων προγραμματιστών. Είναι επίσης ασφαλής όταν επεκτείνεις κλάσεις που έχουν σχεδιαστεί και τεκμηριωθεί ειδικά για επέκταση. Η κληρονομικότητα από συνηθισμένες συγκεκριμένες κλάσεις πέρα από τα όρια των packages, όμως, είναι επικίνδυνη.
Σε αντίθεση με την κλήση μεθόδων, η κληρονομικότητα παραβιάζει την ενθυλάκωση. Ίσως νομίζεις ότι είναι ασφαλές να επεκτείνεις μια κλάση αν απλώς προσθέτεις νέες μεθόδους και αποφεύγεις να κάνεις override υπάρχουσες μεθόδους. Αν και αυτό το είδος επέκτασης είναι πολύ ασφαλέστερο, δεν είναι χωρίς κίνδυνο. Αν η υπερκλάση αποκτήσει μια νέα μέθοδο σε μεταγενέστερη έκδοση και έχεις την ατυχία να έχεις δώσει στην υποκλάση μια μέθοδο με την ίδια υπογραφή και διαφορετικό τύπο επιστροφής, η υποκλάση σου δεν θα μεταγλωττίζεται πια. Αν έχεις δώσει στην υποκλάση μια μέθοδο με την ίδια υπογραφή και τον ίδιο τύπο επιστροφής με τη νέα μέθοδο της υπερκλάσης, τότε πλέον την κάνεις override, άρα εκτίθεσαι στα προβλήματα που περιγράφηκαν νωρίτερα. Επιπλέον, είναι αμφίβολο αν η μέθοδός σου θα τηρεί το συμβόλαιο της νέας μεθόδου της υπερκλάσης, επειδή εκείνο το συμβόλαιο δεν είχε ακόμη γραφτεί όταν έγραψες τη μέθοδο της υποκλάσης.
Ευτυχώς, υπάρχει τρόπος να αποφύγεις όλα τα παραπάνω προβλήματα. Αντί να επεκτείνεις μια υπάρχουσα κλάση, δώσε στη νέα σου κλάση ένα ιδιωτικό πεδίο που αναφέρεται σε ένα στιγμιότυπο της υπάρχουσας κλάσης. Αυτός ο σχεδιασμός ονομάζεται σύνθεση, επειδή η υπάρχουσα κλάση γίνεται component της νέας. Κάθε μέθοδος στιγμιοτύπου της νέας κλάσης καλεί την αντίστοιχη μέθοδο του περιεχόμενου στιγμιοτύπου της υπάρχουσας κλάσης και επιστρέφει τα αποτελέσματα. Αυτό ονομάζεται προώθηση (forwarding) και οι μέθοδοι της νέας κλάσης ονομάζονται μέθοδοι προώθησης. Η κλάση που προκύπτει θα είναι βράχος, χωρίς εξαρτήσεις από τις λεπτομέρειες υλοποίησης της υπάρχουσας κλάσης. Ακόμη και η προσθήκη νέων μεθόδων στην υπάρχουσα κλάση δεν θα έχει καμία επίπτωση στη νέα κλάση.
14. Προτίμησε τα interfaces από τις αφηρημένες κλάσεις
Η Java έχει δύο μηχανισμούς για να ορίσει έναν τύπο που επιτρέπει πολλαπλές υλοποιήσεις: τα interfaces και τις αφηρημένες κλάσεις. Από την εισαγωγή των default μεθόδων για interfaces στη Java 8, και οι δύο μηχανισμοί σού επιτρέπουν να παρέχεις υλοποιήσεις για ορισμένες μεθόδους στιγμιοτύπου.
Μια σημαντική διαφορά είναι ότι, για να υλοποιήσει τον τύπο που ορίζει μια αφηρημένη κλάση, μια κλάση πρέπει να είναι υποκλάση της αφηρημένης κλάσης. Επειδή η Java επιτρέπει μόνο απλή κληρονομικότητα, αυτός ο περιορισμός των αφηρημένων κλάσεων περιορίζει σοβαρά τη χρήση τους ως ορισμών τύπων. Κάθε κλάση που ορίζει όλες τις απαιτούμενες μεθόδους και τηρεί το γενικό συμβόλαιο επιτρέπεται να υλοποιεί ένα interface, ανεξάρτητα από το πού βρίσκεται η κλάση στην ιεραρχία κλάσεων. Οι υπάρχουσες κλάσεις μπορούν εύκολα να προσαρμοστούν εκ των υστέρων ώστε να υλοποιούν ένα νέο interface.
- Τα interfaces είναι ιδανικά για τον ορισμό mixins.
- Τα interfaces επιτρέπουν την κατασκευή μη ιεραρχικών πλαισίων τύπων.
- Τα interfaces επιτρέπουν ασφαλείς, ισχυρές βελτιώσεις λειτουργικότητας μέσω του ιδιώματος της wrapper κλάσης. Αν χρησιμοποιείς αφηρημένες κλάσεις για να ορίζεις τύπους, αφήνεις τον προγραμματιστή που θέλει να προσθέσει λειτουργικότητα χωρίς άλλη επιλογή πέρα από την κληρονομικότητα.
15. Generics: μη χρησιμοποιείς raw types
Δεν ανακαλύπτεις το σφάλμα παρά μόνο κατά την εκτέλεση, πολύ αφού έχει συμβεί, και σε κώδικα που μπορεί να απέχει πολύ από τον κώδικα που περιέχει το σφάλμα. Μόλις δεις το ClassCastException, πρέπει να ψάξεις σε όλο το codebase για την κλήση μεθόδου που έβαλε το νόμισμα στη συλλογή γραμματοσήμων. Ο μεταγλωττιστής δεν μπορεί να σε βοηθήσει.
16. Generics: εξάλειψε τις unchecked προειδοποιήσεις
Όταν προγραμματίζεις με generics, θα δεις πολλές προειδοποιήσεις του μεταγλωττιστή:
- προειδοποιήσεις unchecked cast
- προειδοποιήσεις unchecked method invocation
- προειδοποιήσεις unchecked parameterized vararg type
- προειδοποιήσεις unchecked conversion.
Όσο περισσότερη εμπειρία αποκτάς με τα generics, τόσο λιγότερες προειδοποιήσεις θα παίρνεις, αλλά μην περιμένεις ότι ο νέος κώδικας θα μεταγλωττίζεται καθαρά. Πολλές unchecked προειδοποιήσεις εξαλείφονται εύκολα. Για παράδειγμα, ας υποθέσουμε ότι γράφεις κατά λάθος αυτή τη δήλωση:
Set<Lark> exaltation = new HashSet();
Άλλαξέ τη σε:
Set<Lark> exaltation = new HashSet<>();
Κάποιες προειδοποιήσεις θα είναι πολύ πιο δύσκολο να εξαλειφθούν. Όταν παίρνεις προειδοποιήσεις που απαιτούν λίγη σκέψη, επίμενε! Εξάλειψε κάθε unchecked προειδοποίηση που μπορείς.
Αν εξαλείψεις όλες τις προειδοποιήσεις, είσαι σίγουρος ότι ο κώδικάς σου είναι typesafe, κάτι πολύ καλό. Σημαίνει ότι δεν θα πάρεις ClassCastException κατά την εκτέλεση και αυξάνει την εμπιστοσύνη σου
ότι το πρόγραμμά σου θα συμπεριφερθεί όπως το σχεδίασες. Αν δεν μπορείς να εξαλείψεις μια προειδοποίηση, αλλά μπορείς να αποδείξεις ότι ο κώδικας που την προκάλεσε είναι typesafe, τότε (και μόνο τότε) κατάστειλε
την προειδοποίηση με ένα annotation @SuppressWarnings("unchecked"). Αν καταστέλλεις προειδοποιήσεις χωρίς να αποδείξεις πρώτα ότι ο κώδικας είναι typesafe, δίνεις στον εαυτό σου ψεύτικη αίσθηση
ασφάλειας.
Ο κώδικας μπορεί να μεταγλωττιστεί χωρίς καμία προειδοποίηση, αλλά να εξακολουθεί να πετάει ClassCastException κατά την εκτέλεση.
Κάθε φορά που χρησιμοποιείς ένα annotation @SuppressWarnings("unchecked"), πρόσθεσε ένα σχόλιο που εξηγεί γιατί είναι ασφαλές.
17. Generics: όταν έχουν νόημα
Άουτς – μια κλάση που χρειάζεται απεγνωσμένα generics:
Κάτι τέτοιο θα έπρεπε να εντοπιστεί το αργότερο στο merge request!
18. Generics: πώς να μην το κάνεις
Συγγνώμη, δεν χρειάζονται λόγια. Σε παρακαλώ, μην κάνεις ΠΟΤΕ merge κάτι τέτοιο:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
Μια απλή δήλωση κλάσης δεν πρέπει ποτέ να χρειάζεται 5 λεπτά για να την καταλάβεις.
19. Προτίμησε τις λίστες από τους πίνακες
Οι πίνακες διαφέρουν από τους generic τύπους σε δύο σημαντικά σημεία. Πρώτον, οι πίνακες είναι συμμεταβλητοί (covariant). Αυτή η τρομακτική λέξη σημαίνει απλώς ότι, αν ο Sub είναι υποτύπος του Super, τότε
ο τύπος πίνακα Sub[] είναι υποτύπος του τύπου πίνακα Super[]. Τα generics, αντίθετα, είναι αμετάβλητα (invariant): για οποιουσδήποτε δύο διαφορετικούς τύπους Type1 και Type2,
το List<Type1> δεν είναι ούτε υποτύπος ούτε υπερτύπος του List<Type2>.
20. Προτίμησε τα lambdas από τις ανώνυμες κλάσεις
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
Αντί γι’ αυτό, κάνε το εξής:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. Ελαχιστοποίησε την εμβέλεια των τοπικών μεταβλητών
Η πιο ισχυρή τεχνική για να ελαχιστοποιήσεις την εμβέλεια μιας τοπικής μεταβλητής είναι να τη δηλώνεις εκεί όπου χρησιμοποιείται για πρώτη φορά...
22. Προτίμησε τους βρόχους for-each από τους παραδοσιακούς βρόχους for
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. Γνώρισε και χρησιμοποίησε τις βιβλιοθήκες
Ας υποθέσουμε ότι θέλεις να παράγεις τυχαίους ακέραιους μεταξύ του μηδενός και κάποιου άνω ορίου. Μπροστά σε αυτή τη συνηθισμένη εργασία, πολλοί προγραμματιστές θα έγραφαν μια μικρή μέθοδο που μοιάζει κάπως έτσι:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
Η μέθοδος μπορεί να φαίνεται καλή, αλλά έχει τρία ελαττώματα. Το πρώτο είναι ότι, αν το n είναι μια μικρή δύναμη του δύο, η ακολουθία τυχαίων αριθμών θα επαναλαμβάνεται μετά από αρκετά σύντομη περίοδο. Το δεύτερο ελάττωμα είναι ότι, αν το n δεν είναι δύναμη του δύο, κάποιοι αριθμοί θα επιστρέφονται κατά μέσο όρο συχνότερα από άλλους. Αν το n είναι μεγάλο, αυτό το φαινόμενο μπορεί να είναι αρκετά έντονο.
Το τρίτο ελάττωμα της μεθόδου random είναι ότι μπορεί, σε σπάνιες περιπτώσεις, να αποτύχει καταστροφικά, επιστρέφοντας αριθμό έξω από το καθορισμένο εύρος. Αυτό συμβαίνει επειδή η
μέθοδος προσπαθεί να αντιστοιχίσει την τιμή που επιστρέφει το rnd.nextInt() σε έναν μη αρνητικό int καλώντας το Math.abs. Αν το nextInt() επιστρέψει
Integer.MIN_VALUE, τότε και το Math.abs θα επιστρέψει Integer.MIN_VALUE, και ο τελεστής υπολοίπου (%) θα επιστρέψει αρνητικό αριθμό, με την προϋπόθεση ότι το
n δεν είναι δύναμη του δύο. Αυτό σχεδόν σίγουρα θα κάνει το πρόγραμμά σου να αποτύχει, και η αποτυχία μπορεί να είναι δύσκολο να αναπαραχθεί.
24. Απόφευγε τα float και double όταν χρειάζεσαι ακριβή αποτελέσματα
Χρησιμοποίησε BigDecimal για χρηματικά ποσά. Τα float και double προκαλούν σφάλματα στρογγυλοποίησης.
Ας υποθέσουμε, για παράδειγμα, ότι έχεις 1,03 $ στην τσέπη σου και ξοδεύεις 42 ¢. Πόσα χρήματα σου μένουν; Να ένα αφελές απόσπασμα προγράμματος που προσπαθεί να απαντήσει σε αυτή την ερώτηση:
System.out.println(1.03 - 0.42);
Δυστυχώς, τυπώνει 0.6100000000000001. Δεν είναι μεμονωμένη περίπτωση. Ας υποθέσουμε ότι έχεις ένα δολάριο στην τσέπη σου και αγοράζεις εννέα ροδέλες με δέκα σεντς η καθεμία. Πόσα
ρέστα παίρνεις;
System.out.println(1.00 - 9 * 0.10);
Σύμφωνα με αυτό το απόσπασμα προγράμματος, παίρνεις $0.09999999999999998.
Ίσως νομίζεις ότι το πρόβλημα λύνεται απλώς στρογγυλοποιώντας τα αποτελέσματα πριν από την εκτύπωση, αλλά δυστυχώς αυτό δεν λειτουργεί πάντα. Για παράδειγμα, ας υποθέσουμε ότι έχεις ένα δολάριο στην τσέπη σου και βλέπεις ένα ράφι με μια σειρά από νόστιμες καραμέλες που κοστίζουν 10 ¢, 20 ¢, 30 ¢ και ούτω καθεξής, μέχρι ένα δολάριο. Αγοράζεις μία από την καθεμία, ξεκινώντας από αυτή των 10 ¢, μέχρι να μη σου φτάνουν τα χρήματα για την επόμενη καραμέλα στο ράφι.
25. Προτίμησε τους primitive τύπους από τους boxed primitives
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. Αναφέρσου στα αντικείμενα μέσω των interfaces τους
Καλό παράδειγμα:
Set<Son> sonSet = new LinkedHashSet<>();
Κακό παράδειγμα:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. Προτίμησε τα interfaces από το reflection
Αλλιώς:
- Χάνεις όλα τα οφέλη του ελέγχου τύπων κατά τη μεταγλώττιση, συμπεριλαμβανομένου του ελέγχου εξαιρέσεων. Αν ένα πρόγραμμα επιχειρήσει να καλέσει μέσω reflection μια ανύπαρκτη ή μη προσβάσιμη μέθοδο, θα αποτύχει κατά την εκτέλεση, εκτός αν έχεις λάβει ειδικές προφυλάξεις.
- Ο κώδικας που απαιτείται για πρόσβαση μέσω reflection είναι άχαρος και φλύαρος. Είναι κουραστικός στο γράψιμο και δύσκολος στην ανάγνωση.
- Η απόδοση υποφέρει. Η κλήση μεθόδου μέσω reflection είναι πολύ πιο αργή από την κανονική κλήση μεθόδου.
28. Τήρησε τις γενικά αποδεκτές συμβάσεις ονοματοδοσίας
Η πλατφόρμα Java έχει ένα καλά εδραιωμένο σύνολο συμβάσεων ονοματοδοσίας, πολλές από τις οποίες περιέχονται στο The Java Language Specification [JLS, 6.1].
29. Χρησιμοποίησε τις εξαιρέσεις μόνο για εξαιρετικές συνθήκες
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
Τι κάνει αυτός ο κώδικας; Δεν είναι καθόλου προφανές με μια ματιά, και αυτός είναι αρκετός λόγος για να μην τον χρησιμοποιήσεις. Αποδεικνύεται ότι είναι ένα φρικτά κακοσχεδιασμένο ιδίωμα για τη διάσχιση των στοιχείων ενός
πίνακα. Ο ατέρμονος βρόχος τερματίζεται πετώντας, πιάνοντας και αγνοώντας ένα ArrayIndexOutOfBoundsException όταν
επιχειρεί να προσπελάσει το πρώτο στοιχείο έξω από τα όρια του πίνακα. Υποτίθεται ότι είναι ισοδύναμο με το τυπικό ιδίωμα διάσχισης πίνακα, που το αναγνωρίζει αμέσως
κάθε προγραμματιστής Java:
for (Mountain m : range) {
m.climb();
}
Οι εξαιρέσεις, όπως λέει και το όνομά τους, πρέπει να χρησιμοποιούνται μόνο για εξαιρετικές συνθήκες· δεν πρέπει ποτέ να χρησιμοποιούνται για τη συνηθισμένη ροή ελέγχου.
30. Χρησιμοποίησε checked exceptions για ανακτήσιμες συνθήκες και runtime exceptions για προγραμματιστικά σφάλματα
Χρησιμοποίησε checked exceptions για συνθήκες από τις οποίες είναι εύλογο να αναμένεται ότι ο καλών θα ανακάμψει. Αντιμετωπίζοντας τον χρήστη με μια checked exception, ο σχεδιαστής του API του επιβάλλει να ανακάμψει από τη συνθήκη. Ο χρήστης μπορεί να αγνοήσει αυτή την επιταγή πιάνοντας την εξαίρεση και αγνοώντας την, αλλά αυτό συνήθως είναι κακή ιδέα.
Χρησιμοποίησε runtime exceptions για να δηλώσεις προγραμματιστικά σφάλματα. Η συντριπτική πλειονότητα των runtime exceptions δείχνει παραβιάσεις προϋποθέσεων. Μια παραβίαση προϋπόθεσης σημαίνει απλώς ότι
ο client ενός API δεν τήρησε το συμβόλαιο που ορίζει η προδιαγραφή του API. Για παράδειγμα, το συμβόλαιο για την πρόσβαση σε πίνακα ορίζει ότι ο δείκτης πρέπει να είναι μεταξύ του μηδενός και του μήκους
του πίνακα μείον ένα, συμπεριλαμβανομένων των ορίων. Το ArrayIndexOutOfBoundsException δείχνει ότι αυτή η προϋπόθεση παραβιάστηκε.
31. Απόφευγε την περιττή χρήση checked exceptions
Συνοψίζοντας, όταν χρησιμοποιούνται με μέτρο, οι checked exceptions μπορούν να αυξήσουν την αξιοπιστία των προγραμμάτων· όταν γίνεται κατάχρησή τους, κάνουν τα API επώδυνα στη χρήση.
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
32. Προτίμησε τη χρήση τυπικών εξαιρέσεων
Ένα χαρακτηριστικό που ξεχωρίζει τους έμπειρους προγραμματιστές από τους λιγότερο έμπειρους είναι ότι οι έμπειροι επιδιώκουν και συνήθως επιτυγχάνουν υψηλό βαθμό επαναχρησιμοποίησης κώδικα.
Οι εξαιρέσεις δεν αποτελούν εξαίρεση στον κανόνα ότι η επαναχρησιμοποίηση κώδικα είναι καλό πράγμα. Οι βιβλιοθήκες της Java παρέχουν ένα σύνολο εξαιρέσεων που καλύπτει τις περισσότερες ανάγκες των περισσότερων API για πέταγμα εξαιρέσεων. Η επαναχρησιμοποίηση τυπικών εξαιρέσεων έχει αρκετά οφέλη. Το κυριότερο είναι ότι κάνει το API σου πιο εύκολο στην εκμάθηση και στη χρήση, επειδή ακολουθεί καθιερωμένες συμβάσεις που οι προγραμματιστές ήδη γνωρίζουν. Δεύτερο, με μικρή διαφορά, είναι ότι τα προγράμματα που χρησιμοποιούν το API σου διαβάζονται πιο εύκολα, επειδή δεν είναι γεμάτα με άγνωστες εξαιρέσεις. Τέλος (και λιγότερο σημαντικό), λιγότερες κλάσεις εξαιρέσεων σημαίνουν μικρότερο αποτύπωμα μνήμης και λιγότερο χρόνο φόρτωσης κλάσεων.
Ο πιο συχνά επαναχρησιμοποιούμενος τύπος εξαίρεσης είναι το IllegalArgumentException. Γενικά, αυτή είναι η εξαίρεση που πετάς όταν ο καλών περνά ένα όρισμα με ακατάλληλη τιμή. Για
παράδειγμα, αυτή θα ήταν η εξαίρεση που πρέπει να πεταχτεί αν ο καλών περνούσε αρνητικό αριθμό σε μια παράμετρο που δηλώνει πόσες φορές πρέπει να επαναληφθεί μια ενέργεια. Μια άλλη συχνά επαναχρησιμοποιούμενη
εξαίρεση είναι το IllegalStateException. Γενικά, αυτή είναι η εξαίρεση που πετάς αν η κλήση δεν επιτρέπεται λόγω της κατάστασης του αντικειμένου που τη δέχεται. Για παράδειγμα, αυτή θα ήταν
η εξαίρεση που πρέπει να πεταχτεί αν ο καλών επιχειρούσε να χρησιμοποιήσει ένα αντικείμενο πριν αυτό αρχικοποιηθεί σωστά.
Θα μπορούσε κανείς να πει ότι κάθε λανθασμένη κλήση μεθόδου καταλήγει σε μη έγκυρο όρισμα ή μη έγκυρη κατάσταση, αλλά για ορισμένα είδη μη έγκυρων ορισμάτων και καταστάσεων χρησιμοποιούνται τυπικά άλλες εξαιρέσεις. Αν ένας καλών
περάσει null σε μια παράμετρο για την οποία οι τιμές null απαγορεύονται, η σύμβαση ορίζει να πεταχτεί NullPointerException και όχι IllegalArgumentException.
Παρομοίως, αν ένας καλών περάσει τιμή εκτός εύρους σε μια παράμετρο που αντιπροσωπεύει δείκτη σε μια ακολουθία, πρέπει να πεταχτεί IndexOutOfBoundsException και όχι IllegalArgumentException.
Μην επαναχρησιμοποιείς απευθείας τις Exception, RuntimeException, Throwable ή Error. Αντιμετώπισε αυτές τις κλάσεις σαν να ήταν αφηρημένες. Το RuntimeException πάντως μπορεί να χρησιμοποιηθεί αν θέλεις
να μετατρέψεις μια checked exception σε runtime exception και ξέρεις πραγματικά τι κάνεις.
33. Μην αγνοείς τις εξαιρέσεις
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
Ένα κενό μπλοκ catch ακυρώνει τον σκοπό των εξαιρέσεων, που είναι να σε αναγκάζουν να χειριστείς τις εξαιρετικές συνθήκες. Το να αγνοείς μια εξαίρεση είναι σαν να αγνοείς έναν συναγερμό πυρκαγιάς – και να τον κλείνεις, ώστε κανείς άλλος να μην έχει την ευκαιρία να δει αν υπάρχει πραγματική φωτιά.