String 클래스는 Java.lang 패키지로 제공되는 Java 문자열 클래스이다.

별도의 import 없이 사용이 가능한데, 아래와 같이 선언을 할 수 있다.

 

 

위 둘의 차이는 이후 알아보기로 하자.

String 클래스는 한 번 인스턴스가 생성되면 수정할 수 없다. (immutable object)

 

 

값의 변경은 불가능하지만, 새 String을 만들어 바꿀 수는 있다.

 

 

위 코드 중에서 char[]를 String으로 바꾸는 것에 주목해보자.

아래와 같이 2가지 방법으로 바꿀 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package String;
 
public class StringClass {
 
    public static void main(String[] args) {
        String str = "ABCDFFG";
        char[] temp_arr = str.toCharArray();
        System.out.println(temp_arr); // ABCDFFG
        temp_arr[4= 'E';
//        str = new String(temp_arr);
        str = String.valueOf(temp_arr);
        System.out.println(str); // ABCDEFG
    }
 
}
 
cs

 

즉, new String(char[] array) 혹은 String.valuOf(char[] array)를 이용!

 

그리고, 두 문자열이 같은지 비교하는 메서드로 equlas 메서드가 있는데, 왜 == 로는 비교를 할 수 없는지 살펴보자.

처음 String을 선언할 때 두 가지 방법이 있다는 것과 연결되는 내용이다.

 

 

두 방법의 차이는 리터럴로 선언하느냐, 인스턴스 객체를 생성하여 선언하느냐의 차이.

그 차이는 다음과 같다.

 

 

리터럴로 선언한 경우에는 두 변수가 같은 "text" 값을 가리키고 있다.

하지만, 인스턴스로 선언한 경우에는 서로 다른 객체를 생성하고 그 안에 "test"를 참조하고 있다.

따라서 아래와 같은 결과가 나타난다.

 

 

그러므로! String을 비롯한 primitive type이 아닌 경우에는 equals 메서드로 비교를 하도록 주의하자. 결국 코딩테스트를 준비하는 과정에서는 너무 당연하지만 ==  대신 equals를 써야하는 것에 주의하는 것이 중요하니까.

 

String 클래스가 제공하는 메서드들은 다양하지만, 그중 일부를 정리하자면,

 

 

 이어서 BOJ에서 문자열 관련 문제를 풀며 추가 정리하도록 하자.

 

 

Theme. 추상 클래스

 선언만 있고 구현이 없는 메서드를 추상(abstract) 메서드라고 한다. 이러한 추상 메서드를 하나라도 포함한 클래스는 추상 클래스이다.

 추상 메서드와 추상 클래스는 키워드 abstract로 표시한다.

 하지만, 추상 클래스는 객체를 만들 수 없다. 따라서 서브 클래스를 만드는 용도로만 사용이 된다. (이때 추상 클래스가 수퍼 클래스가 되는 것)

 

블로그 내 "Scheduler 프로그램(java)" 게시물에서 추상 클래스를 사용하고 있는데,

superclass인 Event class를 다음과 같이 추상 클래스로 만들었고, 추상 메서드를 나타냈다.

 

 

이후, subclass인 OneDayEvent, DurationEvent, DeadlinedEvent class마다

isRelevant(MyDate date) 메서드를 구현하여 사용하였다. 상속관계이므로!

각 클래스마다 return 값이 다른데,

메인 클래스에서 events 배열의 각 칸이 참조하고 있는 subclass들의 객체에 따라 isRelevant(MyDate date) 메서드가 실행되도록 작성해주었다. 

 

Theme. 인터페이스

인터페이스는 "극단적인 추상 클래스"라고 말할 수 있다. 

추상 클래스와 이를 상속받는(extends) 클래스는 추상 클래스에서 보통 메서드와 데이터 멤버들은 상속 받고, 추상 메서드의 경우에는 자신(subclass)의 클래스 내에서 구현하는 것이지만, 

인터페이스는 오로지 추상 메서드만 가지고 있기 때문에 이를 implements 하는 클래스는 오로지 그 추상 메서드들을 실제로 구현해야 한다.

 

Theme. Interface vs Abstract class

추상 메서드로만 구성된 추상 클래스는 인터페이스와 완전히 동일한가?

그렇지 않다!

Java에서는 "다중 상속"을 허용하지 않는다. 하지만,  하나의 클래스가 여러 개의 Interface를 implement하는 것은 가능하다.

 

출처: 권오흠 교수님

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package CH1;
 
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;
 
public class Code22 {
    
    static String[] words = new String[100000]; // 단어들의 목록
    static int[] count = new int[100000]; // 각 단어들의 등장 횟수
    static int n = 0// 목록에 저장된 단어의 개수
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while(true) {
            
            System.out.println("read, find, saveas, exit 중 선택하시오.");
            System.out.print("$ ");
            String command = sc.next(); // 명령어 입력(4가지 중 하나)
            if(command.equals("read")) { // 텍스트 파일을 읽고 인덱스 목록을 만든다.
                String filename = sc.next();
                makeIndex(filename);
            }
            else if(command.equals("find")) { // 특정 단어가 몇번 나오는지 횟수를 출력
                String str = sc.next();
                int index = findWord(str);
                if(index != -1) {
                     System.out.println(words[index] + "(은)는 " + count[index] + "번 나왔습니다.");
                }
                else {
                    System.out.println(words[index] + "(은)는 존재하지 않습니다.");
                }
            }
            else if(command.equals("saveas")) { // 인덱스 목록을 파일로 저장
                String filename = sc.next();
                saveAs(filename);
            }
            else if(command.equals("exit")) { // 종료
                break;
            }
            
        }
        sc.close();
        
        // 문자열과 나온 횟수 확인
        for (int i = 0; i < n; i++) {
            System.out.println(words[i] + " " + count[i]);
        }
        
        
    }
    
    // 텍스트 파일을 읽고 인덱스를 만드는 함수
    static void makeIndex(String fileName) {
        try {
            Scanner inFile = new Scanner(new File(fileName)); // 파일로부터 입력 받음
            // 파일을 처음부터 끝까지 모두 읽는다(파일에 등장하는 모든 단어들을 확인하기 위해) 
            while(inFile.hasNext()) {
                String str = inFile.next();
                addWord(str);
            }
            
            inFile.close();
        } catch (FileNotFoundException e) { // fileName의 파일이 존재하지 않는 경우
            System.out.println("No file");
            return;
        } 
    }
    
    // 어떤 문자열이 목록에 이미 있는지 없는지 확인하고 -> 이미 있으면 제외 없으면 추가
    static void addWord(String str) {
        int index = findWord(str); // returns -1 if not found
        if (index != -1) { // found
            count[index]++;
        }
        else { // not found
            words[n] = str; // 배열 내 값이 null인 것들(그 전까지는 문자열들로 채워져있고) 중 첫 번째에 문자열을 저장
            count[n] = 1// 새로운 문자열을 추가하면서 동시에 등장 횟수 1로 지정
            n++// 목록에 저장된 단어의 개수 +1
        }
    }
    
    // words 배열에 어떤 문자열이 존재하지는지, 존재한다면 그 위치(index)를 반환
    static int findWord(String str) {
        for (int i = 0; i < n; i++) {
            if(words[i].equalsIgnoreCase(str)) { // 대소문자 구분하지 않고 동일한지 비교
                return i; // 존재한다면 그 위치를 반환
            }
        }
        return -1// 존재하지 않는다면 -1을 반환
    }
    
    // 파일로 저장(출력 = 파일 쓰기)
    static void saveAs(String filename){
        try {
            PrintWriter pw = new PrintWriter(new FileWriter(filename));
            for (int i = 0; i < n; i++) {
                pw.println(words[i] + " " + count[i]);
            }
            pw.close();
        } catch (IOException e) {
            System.out.println("파일 저장 실패");
            return;
        }
    }
    
 
}
 
cs

위 코드를 실행시켜 보자.

먼저, 사전에 sample.txt라는 파일을 만들어 두었고, "saveas"를 입력하여 문자열(텍스트)을 파일로 출력(저장)할 때는 output.txt라는 파일을 만들어 저장하도록 한다.

sample.txt 파일의 내용은 다음과 같다. 

sample.txt
0.00MB

이제, read, find, saveas, exit를 사용한 결과를 살펴보자.

output.txt는

output.txt
0.00MB

 

결과를 살펴봤을 때, 발생할 수 있는 문제점들이 있다.

1. 소수점, 쉼표 등의 특수기호가 단어에 포함되는 점

2. 숫자 등이 단어로 취급된다는 점

3. 단어들이 알파벳 순으로 정렬되지 않았다.

이를 해결하는 방법은??

 

해결방법을 찾기 위해 String 클래스의 기본 메서드들을 살펴보자.

 

Theme. String 클래스 기본 메서드

 

1. 문자열 동일성 검사

A라는 문자열이 B라는 문자열과 동일한 문자열인지를 알아보고 싶다?

A.equals(B);  즉, "equals( String )"

equals 메서드는  boolean 값을 반환한다.

만약, 대소문자 구분없이 문자열이 서로 동일한 문자열인지를 알아보고 싶다면?

A.equalsIgnoreCase(B);

 

간단한 예제를 살펴보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package CH1;
 
public class StringClass {
    public static void main(String[] args) {
        String str1 = "java";
        String str2 = "Java";
        boolean b1 = str1.equals(str2);
        boolean b2 = str1.equalsIgnoreCase(str2);
 
        System.out.println(b1); // false
        System.out.println(b2); // true
 
    }
}
 
cs

 

2. 문자열 사전식 순서

A라는 문자열과 B라는 문자열 중에 어떤 문자열이 사전식 순서로 더 앞서는지를 알고 싶다면?

A.compareTo(B); 즉, "compareTo( String )"

compareTo 메서드는 int값을 반환한다.

이때, 결과를 잘 해석해야 한다.

만약 A.compareTo(B)의 결과가 0보다 작다면 A가 B보다 사전식 순서가 앞선다는 의미이고, 0보다 크다면 B가 A보다 사전식 순서가 앞선다는 의미이다. 완전히 동일한 문자열일 경우에는 결과가 0이다.

결과는 자릿수의 차이에 따라 다양한 정수로 나오지만 0보다 작냐 크냐로 이해하는 것이 좋다.

왜냐하면 tip이라면 tip일 수있는 것이 A.compareTo(B)의 결과가 0보다 작으면( < 0 ) A < B(A가 앞선다)로 기억하고,

0보다 크면 ( > 0 ) A > B(B가 앞선다)로 기억하면 되기 때문이다.

추가로 compareToIgnoreCase ( String ) 메서드도 존재하는데, 이는 대소문자를 무시하고 사전식 순서를 비교하는 것이다.

 

예제를 통해 알아보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package CH1;
 
public class StringClass {
    public static void main(String[] args) {
        String str1 = "apple";
        String str2 = "banana";
 
        int result1 = str1.compareTo(str2);
        System.out.println(result1);// -1
        int result2 = str2.compareTo(str1);
        System.out.println(result2); // 1
 
    }
}
 
cs

 

3. 문자열 길이

어떤 문자열(A)의 길이를 알고 싶다면?

A.length()

( 주의할 것은 배열의 길이를 알고 싶을 때는 array.length로 차이가 있다는 것이다. )

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package CH1;
 
public class StringClass {
    public static void main(String[] args) {
        String str1 = "apple";
        String str2 = "banana";
 
        int len1 = str1.length();
        int len2 = str2.length();
 
        System.out.println(len1); // 5
        System.out.println(len2); // 6
    }
}
 
cs

 

4. 특정 위치의 문자

ABCDEFG라는 문자열이 있을 때, 3번째 위치에 존재하는 문자 C를 반환하고 싶다면?

charAt( int ) 메서드를 이용한다.

이때, 위치 인덱스는 0부터 시작한다. 따라서, 3번째 위치는 charAt(2)를 통해 값을 반환할 수 있다.

또한, charAt 메서드의 반환값의 자료형은 char type이다.

 

1
2
3
4
5
6
7
8
9
10
package CH1;
 
public class StringClass {
    public static void main(String[] args) {
        String str = "ABCDEFG";
        char ch = str.charAt(2);
        System.out.println(ch); // C
    }
}
 
cs

 

5. 지정한 문자의 위치 검색

ABCDEFG라는 문자열이 있을 때, C가 존재하는 위치를 알고 싶다면?

indexOf ( char ) 메서드를 이용한다.

이때, 위치 인덱스는 0부터 시작한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
package CH1;
 
public class StringClass {
    public static void main(String[] args) {
        String str = "ABCDEFG";
        int index1 = str.indexOf("C");
        int index2 = str.indexOf("F");
        
        System.out.println(index1); // 2
        System.out.println(index2); // 5
    }
}
 
cs

 

6. 지정한 범위의 부분 문자열

 ABCDEFG라는 문자열에서 CDE라는 문자열만 떼어내서 반환하고 싶다면?

substring( int, int ) 메서드를 사용한다. 

주의할 것이 있다! 해당 메서드를 보다 자세히 살펴보면,

substring( 시작하는 인덱스, 끝나는 인덱스 + 1 ) 이다. 

예를 들어, substring( 1, 4 )의 경우에는 인덱스 기준 1부터 3까지의 위치에 존재하는 문자열을 반환하게 된다.

 

1
2
3
4
5
6
7
8
9
10
11
package CH1;
 
public class StringClass {
    public static void main(String[] args) {
        String str1 = "ABCDEFG";
        String str2 = str1.substring(2,5); 
        
        System.out.println(str2); // CDE
    }
}
 
cs

 

7. 문자열 자르기

012-3456-789라는 문자열에서 기호가 아닌 문자열만을 잘라내고 싶다면?

구분자(delimiter)를 기준으 split()메서드를 활용한다.

split() 메서드는 그 결과를 String[] 타입의 배열에 각각 저장한다.  

 

 만약, "."을 기준으로 split을 하고 싶을 때, split( "." )처럼 작성하면 안된다.  왜냐하면 split 함수에서 구분자를 정규표현식으로 받기 때문이다. 이 경우 split("\\.")으로 작성해주어야 한다.

"."뿐만 아니라 아래의 특수문자들도 \\를 붙여야 한다.

 

| ? * ( ) [ ] { } \

 

 

위와 같은 구분자뿐만 아니라 공백을 기준으로도 split을 할 수 있는데, 

 

 

 

 

이제, 위에서 언급한 문제점들을 해결해보자.

trimming이라는 함수를 만들어서, 단어의 앞뒤에 존재하는 특수문자 등을 제거하도록 한다. 

이 trimming이라는 함수는 단어를 목록에 저장하기 전에 실행되어야 하므로,

makeIndex 함수 안에서 addWord 함수 이전에 실행되도록 위치시킨다.

 

trimming 함수에서는 어떤 문자열이 있을 때,

1. 앞에서부터 알파벳이 아닌 문자열들이 존재하는 동안

2. 뒤에서부터 알파벳이 아닌 문자열들이 존재하는 동안

while문을 돌리도록 한다.

이때, 어떤 char이 알파벳인지 아닌지를 판별하기 위해, isLetter( char )라는 메서드를 사용하도록 한다. 반환값의 자료형은 boolean이다.

trimming 함수 부분을 작성한 코드를 살펴보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static String trimming(String str) {
        // i는 문자열의 앞에서부터, j는 뒤에서부터
        int i = 0, j = str.length() - 1;
        while( i < str.length() && !Character.isLetter(str.charAt(i))) {
            i++;
        }
        while( j >= 0 && !Character.isLetter(str.charAt(j))) {
            j--;
        }
        // 알파벳이 존재하는 문자열의 시작 위치(i), 끝 위치(j)를 얻게됨
        if(i > j) // substring에서 오류가 발생하므로
            return null
        else
            return str.substring(i, j + 1); // substring은 [  )
    }
cs

 

이제, 단어들을 알파벳 순으로 정렬하는 과정을 작성해보자. 

 

이미 정렬되어 있는 목록의 경우 기존에 존재하던 단어가 목록에 추가될 때는 정렬을 해줄 필요가 없다.

따라서, 새로운 문자열이 추가되는 케이스에 대하여 정렬하는 것에 초점을 맞춘다.

 

정리하자면, 정렬되어 있는 목록에 새로운 단어를 추가하는데, 동시에 정렬이 되도록 하고자 한다.

단어를 추가하기 위해 목록의 뒤에서부터 사전식 순서를 비교하도록 한다. 앞에서부터 비교하면 새로운 단어를 넣기 위해 배열 전체를 한칸씩 뒤로 옮겨야하므로 뒤에서부터 비교하는 것이 더 효율적이기 때문이다.

즉, ordered list에 어떤 값을 insert할 때는 뒤에서부터 비교하면서 insert하는 것이 좋다.

이에 대한 코드는

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 어떤 문자열이 목록에 이미 있는지 없는지 확인하고 -> 이미 있으면 제외 없으면 추가
    static void addWord(String str) {
        int index = findWord(str); // returns -1 if not found
        if (index != -1) { // found
            count[index]++;
        }
        else { // not found
            int i = n-1;
            while(i >=0 && words[i].compareTo(str) > 0) {
                words[i+1= words[i];
                count[i+1= count[i];
                i--;
            }
            words[i+1= str; // 배열 내 값이 null인 것들(그 전까지는 문자열들로 채워져있고) 중 첫 번째에 문자열을 저장
            count[i+1= 1// 새로운 문자열을 추가하면서 동시에 등장 횟수 1로 지정
            n++// 목록에 저장된 단어의 개수 +1
        }
    }
cs

이때, "인덱스"를 이해하는 데 부가적인 설명을 덧붙이자면,

위 코드에서 str이 words[5]와 words[6] 사이에 들어가야 정렬이 되는 상황이라고 가정해보자.

순차적으로,

1. i == 6일때, words[6]이 한칸 뒤로 이동

2. i--로 i == 5가 됨

3. i == 5일때, words[5]는 뒤로 이동하지 않음

4. str은 words[6] 자리. 즉, i == 6의 위치에 들어가게 됨. 이때 i == 5이므로 i+1 위치에 str이 들어가는 것

 

최종적으로 완성된 코드는 다음과 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package CH1;
 
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;
 
public class Code23 {
    
    static String[] words = new String[100000]; // 단어들의 목록
    static int[] count = new int[100000]; // 각 단어들의 등장 횟수
    static int n = 0// 목록에 저장된 단어의 개수
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while(true) {
            
            System.out.println("read, find, saveas, exit 중 선택하시오.");
            System.out.print("$ ");
            String command = sc.next(); // 명령어 입력(4가지 중 하나)
            if(command.equals("read")) { // 텍스트 파일을 읽고 인덱스 목록을 만든다.
                String filename = sc.next();
                makeIndex(filename);
            }
            else if(command.equals("find")) { // 특정 단어가 몇번 나오는지 횟수를 출력
                String str = sc.next();
                int index = findWord(str);
                if(index != -1) {
                     System.out.println(words[index] + "(은)는 " + count[index] + "번 나왔습니다.");
                }
                else {
                    System.out.println(words[index] + "(은)는 존재하지 않습니다.");
                }
            }
            else if(command.equals("saveas")) { // 인덱스 목록을 파일로 저장
                String filename = sc.next();
                saveAs(filename);
            }
            else if(command.equals("exit")) { // 종료
                break;
            }
            
        }
        sc.close();
        
        // 문자열과 나온 횟수 확인
        for (int i = 0; i < n; i++) {
            System.out.println(words[i] + " " + count[i]);
        }
        
        
    }
    
    // 텍스트 파일을 읽고 인덱스를 만드는 함수
    static void makeIndex(String fileName) {
        try {
            Scanner inFile = new Scanner(new File(fileName)); // 파일로부터 입력 받음
            // 파일을 처음부터 끝까지 모두 읽는다(파일에 등장하는 모든 단어들을 확인하기 위해) 
            while(inFile.hasNext()) {
                String str = inFile.next();
                
                String trimmed = trimming(str);
                if(trimmed != null) {
                    String t = trimmed.toLowerCase(); // 소문자로
                    addWord(t);
                }
            }
            
            inFile.close();
        } catch (FileNotFoundException e) { // fileName의 파일이 존재하지 않는 경우
            System.out.println("No file");
            return;
        } 
    }
    
    static String trimming(String str) {
        // i는 문자열의 앞에서부터, j는 뒤에서부터
        int i = 0, j = str.length() - 1;
        while( i < str.length() && !Character.isLetter(str.charAt(i))) {
            i++;
        }
        while( j >= 0 && !Character.isLetter(str.charAt(j))) {
            j--;
        }
        // 알파벳이 존재하는 문자열의 시작 위치(i), 끝 위치(j)를 얻게됨
        if(i > j) // substring에서 오류가 발생하므로
            return null
        else
            return str.substring(i, j + 1); // substring은 [  )
    }
    
    
    // 어떤 문자열이 목록에 이미 있는지 없는지 확인하고 -> 이미 있으면 제외 없으면 추가
    static void addWord(String str) {
        int index = findWord(str); // returns -1 if not found
        if (index != -1) { // found
            count[index]++;
        }
        else { // not found
            int i = n-1;
            while(i >=0 && words[i].compareTo(str) > 0) {
                words[i+1= words[i];
                count[i+1= count[i];
                i--;
            }
            words[i+1= str; // 배열 내 값이 null인 것들(그 전까지는 문자열들로 채워져있고) 중 첫 번째에 문자열을 저장
            count[i+1= 1// 새로운 문자열을 추가하면서 동시에 등장 횟수 1로 지정
            n++// 목록에 저장된 단어의 개수 +1
        }
    }
    
    // words 배열에 어떤 문자열이 존재하지는지, 존재한다면 그 위치(index)를 반환
    static int findWord(String str) {
        for (int i = 0; i < n; i++) {
            if(words[i].equalsIgnoreCase(str)) { // 대소문자 구분하지 않고 동일한지 비교
                return i; // 존재한다면 그 위치를 반환
            }
        }
        return -1// 존재하지 않는다면 -1을 반환
    }
    
    // 파일로 저장(출력 = 파일 쓰기)
    static void saveAs(String filename){
        try {
            PrintWriter pw = new PrintWriter(new FileWriter(filename));
            for (int i = 0; i < n; i++) {
                pw.println(words[i] + " " + count[i]);
            }
            pw.close();
        } catch (IOException e) {
            System.out.println("파일 저장 실패");
            return;
        }
    }
    
 
}
 
cs

알파벳(소문자) 단어들만 존재하고, 사전식 순서로 정렬된 모습을 확인할 수 있다.

 

+ Recent posts