Επόμενο: Αρχεία
Σκοπός αυτού του κεφαλαίου είναι να εξοικειωθούμε με τη χρήση των κειμένων, ένα βασικό κομμάτι κάθε γλώσσας προγραμματισμού.
Βασικά εργαλεία μας θα είναι ο βασικός τύπος char, και η κλάση String όπου είδαμε στο προηγούμενο κεφάλαιο.
1. Χαρακτήρες κειμένου - char
Όπως έχουμε δει ένας χαρακτήρας char περιέχει 2 byte πληροφορίας.
Προσοχή: στην C, ο τύπος char είναι 1 byte, στην Java 2!!
Υπάρχουν λοιπόν 2^16 διαφορετικοί char, ή με άλλα λόγια 65536! Καταλαβαίνετε ότι αυτό το νούμερο «αρκεί» για να καλύψει όλα (ή σχεδόν όλα) τα αλφάβητα που χρησιμοποιούν οι άνθρωποι σήμερα, καθώς και ειδικά σύμβολα όπως σημεία στίξης, μαθηματικοί τελεστές κτλ.
Για να μη μακρυγορούμε, υπάρχει «χώρος» για κάθε χαρακτήρα που μπορείτε να σκεφτείτε!
1.1 Κωδικοποίηση χαρακτήρων
Πρέπει να υπάρξει λοιπόν μία αντιστοιχία:
- Από τη μία έχουμε 2 byte, δηλαδή μία σειρά από 16 ψηφία (0 και 1)
- Από την άλλη τον χαρακτήρα που θέλουμε να εκτυπώσουμε, δηλαδή μία σειρά από pixel που θέλουμε να εμφανίσουμε στην οθόνη.
Τίθεται λοιπόν η ερώτηση πως αυτά τα δύο συνδέονται. Δηλαδή ποια σειρά από 0 και 1 αντιστοιχεί σε ποιον χαρακτήρα; Και ποιος το καθορίζει αυτό;
Η απάντηση ίσως σας απογοητεύσει. Ακόμα δεν υπάρχει μία καθολική συμφωνία ότι «αυτή η σειρά 0 και 1 αντιστοιχεί σε αυτόν τον χαρακτήρα». Ίσως και να μην υπάρξει ποτέ!
Αντίθετα υπάρχουν πολλοί τέτοιοι «χάρτες» που κάνουν την αντιστοίχηση και ονομάζονται κωδικοποιήσεις χαρακτήρων.
το linux, οι περισσότεροι ιστότοποι, και τα λογισμικά της Apple χρησιμοποιούν μία κωδικοποίηση με όνομα UTF-8.
Η Microsoft έχει τις δικές της κωδικοποιήσεις χαρακτήρων, αγνοώντας (ως συνήθως) όλο τον υπόλοιπο κόσμο. H πιο πρόσφατη (γράφω στις 15-11-2013) ονομάζεται WINDOWS-1252.
Η κωδικοποίηση που χρησιμοποιεί η Java για τις «εσωτερικές» υποθέσεις της ονομάζεται UTF-16. Το 16 φυσικά δηλώνει τα 16 bit πληροφορίας που καταλαμβάνει ο κάθε χαρακτήρας.
1.2 Η κωδικοποίηση UTF-16
Τα παρακάτω σε καμία περίπτωση δε χρειάζεται να τα απομνημονεύσετε. Τα γράφω μόνο για να σας δώσω «μία γεύση» για το πως δουλεύει η αντιστοιχία.
Επίσης το πρόθεμα 0x συμβολίζει δεκαεξαδικό νούμερο.
Σύμφωνα με αυτή τη σύμβαση, οι πρώτοι χαρακτήρες είναι μη εκτυπώσιμοι. Πχ ο πρώτος χαρακτήρας 0000 είναι ο κενός χαρακτήρας, ο 0x000A είναι ο γνωστός μας \n που αλλάζει παράγραφο, ο 0x0020 είναι το κενό διάστημα κ.α.
Από το 0x0021 και άνω αρχίζουν οι εκτυπώσιμοι χαρακτήρες (σύμβολα όπως τα !"#$).
Στην περιοχή 0x0030-0x0039 βρίσκονται τα ψηφία 0-9.
Τα Ελληνικά γράμματα θα τα βρείτε (σε μία περίεργη σειρά) από το 0x0391 και άνω.
1.3 Δήλωση χαρακτήρων
Μπορούμε να δηλώσουμε χαρακτήρες είτε με τον κωδικό τους, είτε απλώς αναγράφοντάς τους:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
char ch = 'a';
//ή
char ch = '\u0061'; //τα 4 ψηφία μετά το \u αναπαριστούν δεκαεξαδικό αριθμό!!
//ή
char ch = 97;
Οι τρεις εκφράσεις είναι πανομοιότυπες, γιατί ο λατινικός χαρακτήρας 'a' στο UTF-16 αντιστοιχεί στην τιμή 0x0061 ή 97 στο δεκαδικό σύστημα.
Προσέξτε ότι στις δύο πρώτες περιπτώσεις χρησιμοποιούμε τα μονά εισαγωγικά, και στην δεύτερη βάζουμε το πρόθεμα \u. Η τιμή που ακολουθεί το \u διαβάζεται στο δεκαεξαδικό σύστημα και η σωστή ορθογραφία επιβάλλει να αναγράφονται και τα 4 ψηφία (εξού και τα μηδενικά στην αρχή).
1.4 Μαθηματικές πράξεις με χαρακτήρες
Εδώ ξεδιπλώνεται όλη η μαγεία της κωδικοποίησης. Η Java μας δίνει τη δυνατότητα να κάνουμε πράξεις με μεταβλητές τύπου char, όπως ακριβώς θα κάναμε με ένα ακέραιο τύπο δεδομένων.
Για παράδειγμα:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
char ch = 'a';
System.out.println(ch);
ch++;
System.out.println(ch);
Ας αναλογιστούμε για λίγο τι κάνει ο παραπάνω κώδικας: δημιουργεί ένα char με τιμή 0x0061 (='a') και το τυπώνει στην οθόνη.
Ύστερα εκτελεί την εντολή ch++, και το ch γίνεται 0x0062 (='b'). Σαν αποτέλεσμα θα τυπωθεί στην οθόνη το a και το b!
Μπορούμε φυσικά να χρησιμοποιήσουμε όλους τους γνωστούς τελεστές ++ -- + - * / κτλ... όλοι αυτοί θα δράσουν στην τιμή του χρακτήρα βλέποντάς την σαν ακέραιο αριθμό.
Υπάρχει όμως ένα λεπτό σημείο που πρέπει να προσέξουμε:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
char ch = 'a';
ch = ch+2;
Ο παραπάνω κώδικας θα «νευριάσει» τον compiler επειδή η πράξη ch+2 επιστρέφει έναν int, και όχι char. Για να είμαστε λοιπόν απόλυτα σωστοί θα πρέπει να γράψουμε
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
char ch = 'a';
ch = (char) (ch+2); //Τώρα το ch έγινε 'c'!
Φυσικά ο τύπος char μπορεί να συμμετάσχει και σε συγκρίσεις όπως με τους τελεστές == > < κτλ. Έτσι μη διστάσετε να ελέγξετε αν ο χαρακτήρας ch είναι «μικρότερος από 180», ή «ίσος με 0x20»...
Μπορούμε επίσης να χρησιμοποιούμε όσο συχνά θέλουμε το type casting. Για παράδειγμα όλα τα ακόλουθα είναι σωστά:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
int i=97; //'a'
long l=98; //'b'
short s=99; //'c'
byte b=100; //'d'
System.out.print( (char)i );
System.out.print( (char)l );
System.out.print( (char)s );
System.out.print( (char)b );
System.out.print( (char)101 );
Τώρα όμως τα δεδομένα μετατρέπονται σε char, και στην οθόνη τυπώνεται το "abcde", δηλαδή οι χαρακτήρες υπ' αριθμόν 97, 98 ... 101, σύμφωνα με τη σύμβαση UTF-16.
Νά και ένα χρήσιμο πρόγραμμα:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
//αρχείο CharTest.java
public class CharTest {
public static void main(String arg[]) {
java.util.Scanner sc = new java.util.Scanner(System.in);
char ch=0;
String str="";
System.out.print("Πιέστε σκέτο Enter για τον επόμενο χαρακτήρα, δώστε p για τον προηγούμενο και q για έξοδο\n\n");
for (;;) {
System.out.print("Δεκαδικό= "+(int)ch+"\t\tΧαρακτήρας= "+ch+"\t");
str=sc.nextLine();
if (str.equals("")) ch++;
else if (str.equals("p")) ch--;
else if (str.equals("q")) break;
} //for
} //main
}
Πατήστε πολλές φορές Enter και θα δείτε όλους τους χαρακτήρες UTF-16 να περνούν από μπροστά σας. Θα δείτε «περίεργα πράγματα» να συμβαίνουν πριν το 33, όπως τον κέρσορα να αλλάζει παραγράφους, να σβήνει χαρακτήρες κτλ. Είναι οι μη-εκτυπώσιμοι, ειδικοί χαρακτήρες που λέγαμε πιο πριν.
2. Συμβολοσειρές - String
Όπως έχετε ήδη καταλάβει, ένα String είναι μία σειρά από char. Όλοι οι char έχουν μήκος 2 byte, άρα ένα String πιάνει χώρο στη μνήμη 2*μήκος σε χαρακτήρες.
Το String είναι ένα αντικείμενο της κλασης String. Από τη στιγμή που δημιουργείται δεν αλλάζει. Αν θέλουμε να το αλλάξουμε δημιουργούμε ένα καινούριο String, και βάζουμε τη μεταβλητή μας να «δείχνει» στο νέο αντικείμενο. Πχ:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
String str = "Γειά σου";
//...
//...
//...
str = "Τι κάνεις;"; //αυτή η εντολή φτιάχνει ένα καινούριο String, δεν αλλάζει το αρχικό...
Αν θέλουμε να συγκρίνουμε αν δύο String είναι ίδια ή όχι, χρησιμοποιούμε την μέθοδο equals, και ΟΧΙ τον τελεστή ==, όπως έχουμε ήδη ξεκαθαρίσει στο κεφάλαιο με τα αντικείμενα. Ο τελεστής == συγκρίνει pointers, όχι αντικείμενα!
2.1 Μέθοδοι της Κλάσης String
Για τις μεθόδους της κλάσης αυτής μιλήσαμε και στο κεφάλαιο 9. Εδώ θα παραθέσω και πάλι λίγες από αυτές, καθώς και ένα μικρό πρόγραμμα που δείχνει τη χρήση τους.
String( char[] ch ): Constructor που δημιουργεί ένα String, βάζοντας στη σειρά τα στοιχεία του πίνακα char[].
String( char[] ch, int offset, int count ): Ίδιο με το παραπάνω, μόνο που δεν χρησιμοποιεί όλο τον πίνακα ch. Χρησιμοποιεί μόνο "count" στοιχεία, αρχίζοντας από το στοιχείο offset+1.
String ( String s ): Απλώς δημιουργεί ένα νέο String που είναι αντίγραφο του ορίσματος s.
char charAt( int index ): Επιστρέφει τον χαρακτήρα στη θέση index.
char[] toCharArray(): Επιστρέφει το String με τη μορφή πίνακα char.
int indexOf( int ch ): Το ch παρ' όλο που είναι int, φανταστείτε πως αντιπροσωπεύει ένα char. Η μέθοδος αυτή επιστρέφει τη θέση στο String μας που συναντάμε για πρώτη φορά αυτόν τον χαρακτήρα. Επιστρέφει -1 αν δεν τον βρει καθόλου.
int lastIndexOf( int ch ): Ακριβώς όπως η indexOf, αλλά αρχίζει την αναζήτηση από το τέλος.
int length(): Επιστρέφει το μήκος του String σε χαρακτήρες.
boolean startsWith( String s ): Επιστρέφει true αν το String αρχίζει με το s.
boolean endsWith( String s ): Επιστρέφει true αν το String τελειώνει με το s.
boolean contains( String s ): Ελέγχει αν το String που κάλεσε τη συνάρτηση «περιέχει» το όρισμα s. (Στην πραγματικότητα το όρισα της μεθόδου αυτής είναι CharSequence και όχι String. Αγνοήστε όμως αυτή τη λεπτομέρεια μέχρι να μιλήσουμε για intefaces.
boolean isEmpty(): Επιστρέφει true αν το String μας είναι το "". Προσοχή! Όχι να είναι null, να έχει απλώς άδειο κείμενο.
boolean equals( Object ob ): Αυτή η μέθοδος μας έρχεται από την κλάση Object. Επιστρέφει true αν το όρισμά μας είναι ένα String, και περιέχει το ίδιο κείμενο με το String που κάλεσε τη μέθοδο.
String replace( char oldchar, char newchar ): Αντικαθιστά με newchar, όλους τους χαρακτήρες που είναι ίσοι με oldchar.
String toUpperCase(): Επιστρέφει ένα νέο String παίρνοντας το παλιό και κάνοντας όλα τα γράμματά του κεφαλαία.
String toLowerCase(): Ομοίως, μόνο που κάνει όλα τα γράμματα πεζά.
String[] split(String regex): Επιστρέφει ένα πίνακα από τα κομμάτια του αρχικού String. Τα κομμάτια τέμνονται όπου η μέθοδος συναντήσει το όρισμα regex.
Και το προγραμματάκι που ξεκαθαρίζει πως δουλεύουν τα παραπάνω:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
//αρχείο StringMethods.java
public class StringMethods {
public static void main(String args[]) {
String inp="";
java.util.Scanner sc = new java.util.Scanner(System.in);
System.out.print("\nΔώστε ένα κείμενo, ή q για να τερματίσετε το πράγραμμα.");
for (;;) {
System.out.print("\n>");
inp = sc.nextLine();
if (inp.equals("q")) System.exit(0);
try { System.out.print("\tinp.charAt(5)= "+inp.charAt(5)+"\n"); }
catch (Exception ex) { System.out.print("\tinp.charAt(5)= δεν υπάρχει\n"); }
System.out.print("\tinp.indexOf('φ')= "+inp.indexOf('φ')+"\n");
System.out.print("\tinp.lastIndexOf('φ')= "+inp.lastIndexOf('φ')+"\n");
System.out.print("\tinp.length()= "+inp.length()+"\n");
System.out.print("\tinp.startsWith(\"προ\")= "+inp.startsWith("προ")+"\n");
System.out.print("\tinp.endsWith(\"ος\")= "+inp.endsWith("ός")+"\n");
System.out.print("\tinp.contains(\"γρ\")= "+inp.contains("γρ")+"\n");
System.out.print("\tinp.isEmpty()= "+inp.isEmpty()+"\n");
System.out.print("\tinp.equals(\"πρόγραμμα Java\")= "+inp.equals("πρόγραμμα Java")+"\n");
System.out.print("\tinp.replace('ν', 'Δ')= "+inp.replace('ν','Δ')+"\n");
System.out.print("\tinp.toUpperCase()= "+inp.toUpperCase()+"\n");
System.out.print("\tinp.toLowerCase()= "+inp.toLowerCase()+"\n");
} //for
} //main
}
2.2 Μετατροπή βασικών τύπων σε String και αντίστροφα
Μία πολύ βασική απαίτηση που έχουμε από τα κείμενα που παίρνουμε από το χρήστη είναι να μπορούν να μετατραπούν σε βασικούς τύπους (int, float, long κτλ). Ομοίως για τους βασικούς τύπους έχουμε την απαίτηση να μπορούν να μετατραπούν σε κείμενο για να τους τυπώσουμε στην οθόνη, ή για κάποιο άλλο σκοπό. Η Java μας δίνει πολλά εργαλεία για αυτό το σκοπό.
Ας αρχίσουμε από το πιο εύκολο: πως να μετατρέπουμε ένα βασικό τύπο σε String. Υπάρχουν δύο τρόποι, ο προφανής και ο πονηρός.
Ο προφανής είναι να χρησιμοποιήσουμε τις static μεθόδους valueOf της κλάσης String. Αυτές οι μέθοδοι παίρνουν σαν όρισμα ένα βασικό τύπο, και επιστρέφουν ένα κείμενο που περιγράφει την τιμή του σε «ανθρώπινη» γλώσσα (γιατί η απ' ευθείας την τιμή των τύπων είναι βέβαια μία σειρά από 0 και 1).
Για παράδειγμα:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
int i=98000;
long l= 987745356759L;
char ch='k';
boolean b=false;
float f=3.14159;
double d=2.718281828;
String str1 = String.valueOf(l);
String str2 = String.valueOf(i);
//ή και απ ευθείας χρήση, πχ:
System.out.print( String.valueOf(b) );
Αν θέλουμε να μετατρέψουμε short ή byte σε κείμενο, απλώς τους μετατρέπουμε σε int με type casting και κάνουμε τη δουλειά μας...
Ας δούμε όμως και τον πονηρό τρόπο Γράφουμε απλώς την τιμή ή τη μεταβλητή του βασικού τύπου, και ύστερα «προσθέτουμε» το "", δηλαδή ένα κενό String (ή τέλος πάντων ένα οποιοδήποτε String). Κάπως έτσι:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
int i=98000;
String str1 = i+""; //αυτό ήταν!
//ή
long l=987745356759L;
String str2 = l+"\n";
Περνάμε τώρα στην μετατροπή κειμένου σε βασικούς τύπους.
Εδώ τα πράγματα είναι πιο δύσκολα, και θα μπορούμε να κάνουμε μία «σοβαρή δουλειά» μόνο μετά το τέλος του επομένου κεφαλαίου.
Ο λόγος είναι βασικά ότι ενώ οι βασικοί τύποι μπορούν να μετατραπούν πάντα σε κείμενο, το αντίθετο δε συμβαίνει.
Για παράδειγμα θέλουμε να διαβάσουμε ένα κείμενο από το χρήστη και να το μετατρέψουμε σε σε ακέραιο αριθμό. Έστω λοιπόν ότι ο χρήστης μας δίνει το κείμενο "234β" για κάποιο λόγο. Αν πάμε να μετατρέψουμε το κείμενο αυτό σε αριθμό το πρόγραμμα θα κρασάρει για λόγους που όλοι μας καταλαβαίνουμε.
Παρ όλα αυτά, ελπίζοντας πως το κείμενο που θα μετατρέψουμε θα είναι έγκυρο, έχουμε δύο τρόπους για να το αντιμετωπίσουμε:
1. Χρησιμοποιούμε τις static μεθόδους Integer.parseInt( String text ), Float.parseFloat( String text ), Boolean.parseBoolean( String text ) κτλ με όρισμα το κείμενο που περιέχει την πληροφορία που θέλουμε.
2. Δημιουργούμε ένα κατάλληλο αντικείμενο (πχ Scanner sc = new Scanner(text) με όρισμα το String) και διαβάζουμε τον τύπο με τις μεθόδους nextInt(), nextFloat() κτλ του Scanner, ή κάποιες αντίστοιχες αν δημιουργήσαμε άλλο αντικείμενο αντί για Scanner.
Η πρώτη περίπτωση θέλει ιδιαίτερη προσοχή γιατί το κείμενο πρέπει να είναι σκέτο. Δεν μπορεί να γράψουμε:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
int i= Integer.parseInt( "ο αριθμός είναι 98000");
Το σωστό είναι σκέτο:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
int i= Integer.parseInt( "98000");
3. Κωδικοποιήσεις χαρακτήρων; Μην ανησυχείτε!
Ίσως στους διορατικούς αναγνώστες να δημιουργήθηκε η εξής απορία: «Η Java δουλεύει σε UTF-16. Το σύστημά μου όμως δουλεύει σε UTF-8 ή σε WINDOWS-1252 κτλ. Τι γίνεται όταν αυτά τα δύο πάνε να συνεργαστούν; Όταν πχ. τυπώνω χαρακτήρες στην οθόνη, όταν γράφω ή διαβάζω αρχεία από το σκληρό μου δίσκο; Δεν θα προκληθεί χάος;»
Η απάντηση (ευτυχώς) είναι ότι τέτοια πράγματα δεν πρέπει να μας ανησυχούν καθόλου!
Ας μην ξεχνάμε την ουσία της Java: το Virual Machine. Εμείς γράφουμε ένα bytecode. Το Virtual Machine που είναι εγκατεστημένο στον κάθε υπολογιστή παίρνει το bytecode μας και το διαβάζει όπως αυτό νομίζει. Αυτός είναι και ο λόγος που κάνουμε compile «εδώ», όμως το πρόγραμμά μας εκτελείται σωστά και «εκεί».
Κάθε φορά λοιπόν που πάμε να κάνουμε βασικές διαδικασίες όπως τύπωμα χαρακτήρων στην οθόνη, διάβασμα ή γράψιμο σε αρχεία, το Virtual Machine έχει φροντίσει να φτιάξει τα κατάλληλα αντικείμενα με τον κατάληλο τρόπο. Για παράδειγμα το System.out από το οποίο τυπώνουμε είναι ένα αντικείμενο PrintStream. Το Virtual Machine φτιάχνει τα αντικείμενα PrintStream με τέτοιο τρόπο ώστε να μετατρέπουν αυτόματα τους UTF-16 χαρακτήρες σε UTF-8 (ή σε οτιδήποτε άλλο θέλει το σύστημα). Έτσι οι χαρακτήρες εμφανίζονται κανονικά στην οθόνη. Το ίδιο γίνεται για αντικείμενα όπως το Scanner, PrintWriter και άλλα που χρησιμοποιούνται πχ. για να γράφουν ή να διαβάζουν αρχεία του σκληρού μας δίσκου.
Αν εμείς παρ' όλα αυτά επιμένουμε (για τους λόγους μας) να γράψουμε ή να διαβάσουμε με μία συγκεκριμένη κωδικοποίηση χαρακτήρων, διαφορετική από την προεπιλεγμένη του εκάστοτε συστήματος, μπορούμε. Τέτοιες εργασίες όμως πολύ σπάνια έχουν νόημα και γι αυτό είναι εκτός της εμβέλειας αυτού του οδηγού. Οι ενδιαφερόμενοι ας ανατρέξουν στην κλάση Charset.
Επιπλέον, προγράμματα όπως ο gedit στο linux, το LibreOffice ή άλλα παρόμοια, ξέρουν να αναγνωρίζουν πολλές άλλες κωδικοποιήσεις εκτός από την «δική τους». Ακόμα λοιπόν και αν γράφαμε σε UTF-16 ή κάτι άλλο, δε θα υπήρχε τόσο μεγάλο πρόβλημα...
Προηγούμενο: Exceptions
Επόμενο: Αρχεία
Η εργασία υπάγεται στην άδεια Creative Commons Αναφορά-Παρόμοια διανομή 3.0 Ελλάδα