Skip to the content.

1.7 Συμβολοσειρές

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


< Δ

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

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

Εισαγωγή

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

jshell> String name = "Αγαμέμνων" 
name ==> "Αγαμέμνων"
jshell> // <=> char data[] = {'Α', 'γ', 'α', 'μ', 'έ', 'μ', 'ν', 'ω', 'ν'}
jshell> String name = "Οδυσσέας" 
name ==> "Οδυσσέας"
jshell> String surname = "Ελύτης" 
name ==> "Ελύτης"
jshell> name + " " + surname
$1==> "Οδυσσέας Ελύτης"

Στο παραπάνω παράδειγμα είδαμε πώς μπορούμε να ορίσουμε αλφαριθμητικά καθώς και τη συγχώνευσή τους με τον τελεστή +.

Σε αντίθεση με τους πρωτογενείς τύπους δεδομένων που είδαμε στο προηγούμενο μάθημα, ο τύπος δεδομένων String είναι μια κλάση (class). Θα μιλήσουμε για κλάσεις και αντικείμενα (objects) σε επόμενο μάθημα.

Μια συμβολοσειρά μπορεί να αναπαρασταθεί ως μια συστοιχία χαρακτήρων (char[]). (Θα μιλήσουμε για τις συστοιχίες, arrays, στο επόμενο μάθημα). Ο πρώτος χαρακτήρας της συμβολοσειράς βρίσκεται στη θέση 0, ο 2ος στη θέση 1 κ.ο.κ.

Πράξεις συμβολοσειρών

Ο τύπος δεδομένων String είναι μια κλάση (class) όπως θα μάθουμε στα επόμενα μαθήματα. Μια κλάση αποτελείται από μεθόδους (methods) με τις οποίες μπορούμε να εκτελέσουμε πράξεις πάνω στα αντικείμενα (objects) της κλάσης String. Μην ανησυχείτε αν δεν καταλάβατε την παραπάνω πρόταση. Θα επανέλθουμε σε αυτήν σε επόμενα μαθήματα. Μια πλήρης λίστα των μεθόδων της String στο ΑΡΙ της Java.

Πίνακας 1.7.1 Πράξεις/μέθοδοι συμβολοσειρών

+, concat() Συγχώνευση (δημιουργεί ένα νέο String)
+= Συγχώνευση και εκχώρηση
equals() Έλεγχος για ισότητα
equalsIgnoreCase() Έλεγχος για ισότητα αγνοώντας πεζά/κεφαλαία
compareTo() Σύγκριση συμβολοσειρών σε λεξικογραφική σειρά
compareToIgnoreCase() Σύγκριση συμβολοσειρών σε λεξικογραφική σειρά αγνοώντας πεζά/κεφαλαία
toLowerCase() Μετατροπή σε πεζά
toUpperCase() Μετατροπή σε κεφαλαία
contains(s) Περιέχει το s;
indexOf(s) Σε ποιο δείκτη ξεκινά το s, ή -1 αν δε βρέθηκε
length() Μήκος συμβολοσειράς
substring(αρχή, τέλος) Eξαγωγή συμβολοσειράς από την αρχή μέχρι το χαρακτήρα πριν από το τέλος
split() Διαχωρισμός αλφαριθμητικών
join() Συνένωση συμβολοσειρών με κάποιο συνενωτικό χαρ.
toCharArray() Μετατροπή σε ακολουθία χαρακτήρων
trim() Αφαίρεση κενών χαρακτήρων από εμπρός/πίσω
replace(s1, s2) Αντικατάσταση του s1 με το s2
repeat(n) π.χ. "*".repeat(3) επιστρέφει "***"
charAt(n) n-στός χαρακτήρας
startsWith(s) Αρχίζει με τη συμβολοσειρά s
endsWith(s) Τελειώνει με τη συμβολοσειρά s
isEmpty() Είναι κενή συμβολοσειρά;
valueOf() Μετατρέπει το όρισμα σε συμβολοσειρά

Η substring() επιστρέφει ένα τμήμα της συμβολοσειράς από τη θέση αρχή μέχρι και τον χαρακτήρα πριν από το τέλος. Αν παραλλείψουμε το τέλος, τότε εξάγει το τμήμα από την αρχή μέχρι το τέλος της συμβολοσειράς. Αν παραλλείψουμε την αρχή, τότε επιστρέφει το τμήμα από την αρχή της συμβολοσειράς μέχρι το χαρακτήρα πριν από το τέλος.

Παραδείγματα:

jshell> String s1="Καλήν εσπέραν άρχοντες"
s1 ==> "Καλήν εσπέραν άρχοντες"

jshell> String s2=" άρχοντες"
s2 ==> "άρχοντες"

jshell> s1.equals(s2)	// έλεγχος ισότητας
$3 ==> false

jshell> s1.compareTo(s2)	// σύγκριση, == 0 σημαίνει ίσα, < 0 σημαίνει το s1 είναι πριν το s1, > 0 σημαίνει το s1 είναι μετά το s2
$4 ==> 890

jshell> s1.length()		// μήκος αλφαριθμητικού
$5 ==> 22

jshell> s1.toLowerCase()
$6 ==> "καλήν εσπέραν άρχοντες"

jshell> s1.contains("Καλήν")
$7 ==> true

jshell> s1.indexOf(s2)  // η λέξη "άρχοντες" βρίσκεται στη θέση 13 της συμβολοσειράς s1
$8 ==> 13

jshell> s1.substring(6,13)   // εξάγουμε το τμήμα που ξεκινά από τη θέση 6 μέχρι τη θέση 12
$9 ==> "εσπέραν"

jshell> s1.split(" ")
$10 ==> String[3] { "Καλήν", "εσπέραν", "άρχοντες" } // θα μιλήσουμε για πίνακες/συστοιχίες σε επόμενο μάθημα

jshell> String.join(" + ", "1", "2", "3")
$11 ==> "1 + 2 + 3"

jshell> s2.trim()	// αποκόπτει κενά μπρος και πίσω από το αλφαριθμητικό
$12 ==> "άρχοντες"

jshell> s2.charAt(0)	// επιστρέφει τον 1ο χαρακτήρα του αλφαριθμητικού s2
$13 ==> ' '

jshell> s1.replace("εσπ", "ημ")		// Προσοχή! Δεν αντικαθιστά το s1, αλλά επιστρέφει ένα νέο αλφαριθμητικό
$14 ==> "Καλήν ημέραν άρχοντες"

jshell> s1
s1 ==> "Καλήν εσπέραν άρχοντες"

jshell> String s3 = s1.replace("εσπ", "ημ")  // επιστρέφει ένα νέο αλφαριθμητικό
s3 ==> "Καλήν ημέραν άρχοντες"

jshell> s2.endsWith("ες")
$15 ==> true

jshell> String.valueOf(0b100)
$16 ==> "4"

jshell> Integer.toString(0b100)
$17 ==> "4"

jshell> String empty = ""
empty ==> ""

jshell> String quotedStr = "\""
quotedStr ==> """

jshell> String multilineStr= "Σε γνωρίζω από την κόψη\n του σπαθιού την τρομερή"
"Σε γνωρίζω από την κόψη\n του σπαθιού την τρομερή"
    
jshell> System.out.println(multilineStr);
multilineStr ==> "Σε γνωρίζω από την κόψη
 του σπαθιού την τρομερή"

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

Ποιο αναλυτικά, όταν δημιουργείτε ένα αλφαριθμητικό χρησιμοποιώντας τη σύνταξη:

jshell> String s1 = "Γιάννης";
s1 ==> "Γιάννης"

τότε το s1 δημιουργείται μέσα στο λεγόμενο σύνολο αλφαριθμητικών (string pool). Σ’ αυτήν την περίπτωση μπορείτε να γράψετε:

jshell> String s2 = "Γιάννης";
s2 ==> "Γιάννης"

jshell> s1 == s2
$3 ==> true

Αν όμως χρησιμοποιήσετε τη λέξη κλειδί new της γλώσσας, όπως θα δούμε σε επόμενα μαθήματα, για να δημιουργήσουμε ένα νέο αντικείμενο τύπου String, τότε το αλφαριθμητικό δημιουργείται εκτός του string pool με αποτέλεσμα:

jshell> String s3 = new String("Γιάννης");
s3 ==> "Γιάννης"

jshell> s1 == s3
$4 ==> false

jshell> s2 == s3
$5 ==> false

jshell> s1.equals(s3)
$6 ==> true

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

Η compareTo συγκρίνει δυο συμβολοσειρές λεξικογραφικά. Λεξικογραφική σύγκριση στη Java σημαίνει ότι:

Έτσι:

Αν θέλουμε η συμβολοσειρά μας να περιέχει τον χαρακτήρα “ τότε θα πρέπει να προσημάνουμε το “ με το backslash ():

jshell> String s = "Hello \"Kosta\"";
s ==> "Hello \"Kosta\""

jshell> System.out.println(s);
Hello "Kosta"

Πίνακας 1.7.2 Ειδικοί χαρακτήρες

\" Διπλό εισαγωγικό
\' Μονό εισαγωγικό
\\ Χαρακτήρας backslash ()
\n Νέα γραμμή (newline ή LF – line feed)
\r Νέα γραμμή (CR – carriage return)
\t Στηλοθέτης (tab)
\f Χαρακτήρας FF (form feed)
\b Χαρακτήρας backspace (BS)
\<octal> Οκταδικός κωδικός (0-255)
\uΧΧΧΧ Χαρακτήρας unicode (16-bit) όπου κάθε Χ είναι ένα δεκαεξαδικό ψηφίο

Μπλοκ κειμένου

Στην έκδοση 13 εμφανίστηκε η δυνατότητα ορισμού μπλοκ κειμένου (text blocks) που επιτρέπουν να δημιουργούμε συμβολοσειρές με την μορφοποίηση που θέλουμε. Τα text blocks έγιναν κανονικό χαρακτηριστικό της γλώσσας στην έκδοση 15 (οπότε δεν απαιτείται πλέον να δώσετε --enable-preview στο jshell). Πριν την έκδοση 13, για να μπορέσετε να γράψετε ένα μορφοποιημένο κείμενο στη γλώσσα θα ‘πρεπε να το γράψετε κάπως έτσι:

String html = "<html>\n" +
              "  <body>\n" +
              "    <p>Hello, world</p>\n" + 
              "  </body>\n" +
              "</html>";

Από την έκδοση 13 και μετά μπορούμε να γράψουμε:

jshell> String html = """
     <html>
        <body>
            <p>Hello, world</p>
        </body>
     </html>
""";
html ==> "     <html>\n        <body>\n            <p>Hell ...   </body>\n     </html>\n"

Τα 3 εισαγωγικά πρέπει να είναι μόνα τους σε ξεχωριστή γραμμή. Δηλ. """<html> είναι λάθος. Αν δεν θέλουμε να υπάρχει αλλαγή γραμμής μετά από κάθε γραμμή τότε μπορούμε να γράψουμε \ για να αποτρέψουμε την αλλαγή γραμμής, π.χ.

jshell> String html = """
     <html>
        <body>
            <p>Hello, \
world</p>
        </body>
     </html>
""";
html ==> "     <html>\n        <body>\n            <p>Hell ...   </body>\n     </html>\n"

Το \s, τέλος, αποτρέπει να αποκοπούν τα κενά που ακολουθούν το κείμενο (σβήνονται εξ’ ορισμού).

Εφαρμογές

Ας δούμε μερικά προγράμματα όπου η String είναι χρήσιμη.

Ας ξεκινήσουμε με την 1η από τις παρακάτω ασκήσεις.

Η άσκηση μας ζητάει να γράψουμε ένα πρόγραμμα που να μας ζητάει τα αρχικά ενός ονοματεπώνυμου. Θα υποθέσουμε ότι το ονοματεπώνυμο αποτελείται από ένα όνομα και ένα επώνυμο (καθώς μπορεί να αποτελείται από περισσότερα ονόματα και σε άλλες χώρες κι από περισσότερα επώνυμα). Ποιόν αλγόριθμο μπορείτε να σκεφτείτε;

Παρατηρούμε ότι το όνομα και το επώνυμο χωρίζονται με κενό. Επομένως, ψάχνοντας στις πιο πάνω μεθόδους πρέπει να βρούμε με ποια μέθοδο μπορούμε να βρούμε τη θέση του κενού σε ένα αλφαριθμητικό. Για το σκοπό αυτό υπάρχει η μέθοδος indexOf(). Ας δούμε λοιπόν πώς μπορούμε να εφαρμόσουμε τα παραπάνω για το παράδειγμά μας:

jshell> String name = "Γιάννης Κωστάρας"
name ==> "Γιάννης Κωστάρας"

jshell> int spaceLocation = name.indexOf(" ")
spaceLocation ==> 7

Οπότε τώρα θα πρέπει να διαχωρήσουμε το αλφαριθμητικό μας σε δυο μέρη, ένα πριν το κενό που θα αποτελέσει το όνομα κι ένα μετά το κενό που θα αποτελέσει το επώνυμο. Για το σκοπό αυτό θα χρησιμοποιήσουμε τη μέθοδο substring():

jshell> String firstName = name.substring(0, spaceLocation)
firstName ==> "Γιάννης"

jshell> String lastName = name.substring(spaceLocation+1)
lastName ==> "Κωστάρας"

Πώς θα πάρουμε τα αρχικά του ονόματος και του επώνυμου;

Θα μπορούσαμε να τροποποιήσουμε το πρόγραμμά μας ώστε να δουλεύει με είσοδο από τον χρήστη ως εξής:

jshell> Scanner sc = new Scanner(System.in);
sc ==> java.util.Scanner[delimiters=\p{javaWhitespace}+] ... \E][infinity string=\Q∞\E]

jshell> String name = sc.nextLine();
James Gosling

name ==> "James Gosling"

κ.ο.κ. Τώρα μπορείτε να δοκιμάσετε με οποιοδήποτε ονοματεπώνυμο και να δείτε ότι το πρόγραμμά μας δουλεύει σωστά. Πιο πολύπλοκα ονόματα, αφήνονται ως άσκηση στον αναγνώστη.

Ας δούμε και την άσκηση 2.

jshell> var days="ΚυρΔευΤριΤετΠεμΠαρΣαβ";
days ==> "ΚυρΔευΤριΤετΠεμΠαρΣαβ"

jshell> int dayIndex = sc.nextInt() 
0
dayIndex ==> 0

Παρατηρούμε ότι κάθε μέρα έχει μήκος 3 χαρακτήρες. Οπότε είναι πολύ εύκολο να υπολογίσουμε τη θέση κάθε μέρας. Αρκεί να πολλ/σουμε τον δείκτη της ημέρας με το 3 για να μεταφερθούμε στην αντίστοιχη ημέρα. Η πρώτη μέρα είναι στη θέση μηδέν, ενώ η τελευταία στη θέση 6.

jshell> var sunday = days.substring(dayIndex, 3)
sunday ==> "Κυρ"

jshell> int dayIndex = sc.nextInt() 
5
dayIndex ==> 5

jshell> var saturday = days.substring(dayIndex, 3)
|  Exception java.lang.StringIndexOutOfBoundsException: Range [5, 3) out of bounds for length 21
...

Ουπς! Κάτι κάναμε λάθος. Για να υπολογίσουμε πιο σωστά τα όρια. Π.χ. η "Δευ" ξεκινά στη θέση 3 και τελειώνει στη θέση 6. Η "Τρι" ξεκινά στη θέση 6 και τελειώνει στη θέση 9. Η "Τετ" ξεκινά στη θέση 9 και τελειώνει στη θέση 12. Μπορείτε να βρείτε τη φόρμουλα; Για να πάρουμε τη "Δευ" θα πρέπει να δώσουμε dayIndex = 1. Πώς από το 1 μπορούμε να συνάγουμε την αρχή 3 και το τέλος 6; 1x3=3. Και για να βρούμε το τέλος του, είπαμε ότι προσθέτουμε 3, το μήκος κάθε ημέρας. Επομένως, το τέλος υπολογίζεται ως 1x3+3=6. Για να δοκιμάσουμε τον αλγόριθμό μας για την Τρι. Εδώ θα δώσουμε dayIndex = 2, επομένως η φόρμουλά μας θα γίνει 1x3=6 και 2x3+3=9 για την αρχή και το τέλος του αλφαριθμητικού, δηλ. τα νούμερα που θέλουμε. Η φόρμουλά μας επομένως θα είναι:

jshell> var saturday = days.substring(dayIndex*3, dayIndex*3+3)
saturday ==> "Παρ"

Τι θα συμβεί αν ο χρήστης δώσει dayIndex μικρότερο του μηδενός ή μεγαλύτερο του 6; Σε αυτήν την περίπτωση θα προκύψει ένα λάθος, ή εξαίρεση (exception) του προγράμματος, σαν αυτό που είδαμε πιο πάνω. Αυτό είναι ένα λογικό λάθος (bug) και στο επόμενο μάθημα θα μάθουμε πώς μπορούμε να διαχειριστούμε τέτοιες περιπτώσεις.

Ασκήσεις

  1. Να γράψετε ένα πρόγραμμα που επιστρέφει τα αρχικά ενός ονοματεπώνυμου (π.χ. αν String name = "Γιάννης Κωστάρας";, επιστρέφει "Γ.Κ.")
  2. Δοθέντος του αλφαριθμητικού days="ΚυρΔευΤριΤετΠεμΠαρΣαβ" γράψτε ένα πρόγραμμα στο οποίο θα δίνετε τον δείκτη της ημέρας, π.χ. (0 = Κυριακή, 1 = Δευτέρα κ.ο.κ.) και το πρόγραμμά σας θα επιστρέφει τη σωστή ημέρα, π.χ. αν δώσετε 1 θα πρέπει να σας επιστρέψει "Δευ".

Πηγές

  1. Horstmann C., Big Java 5 - Chapter 4 - Fundamental Data Types
  2. Laskey J. & Marks S. (2019), “Programmer’s Guide To Text Blocks”.

< Δ