1. Wrapper 클래스
(1) Wrapper 클래스 개요 및 특성
Java의 기본 자료형인 boolean, byte, short, int, long, float, double, char는 클래스로 정의된 것이 아니기에 객체가 아니다. 그렇기 때문에 매서드에서 객체를 매개 인자로 요구함에도 어쩔 수 없이 기본 자료형 변수를 입력해야 하는 경우처럼 기본형 자료를 객체로 변경해야할 때가 있는데, 이 때 사용되는 것이 래퍼(Wrapper) 클래스다.
Wrapper 클래스는 Wrapper라는 이름의 클래스가 아니라, 기본 자료형을 객체화 시킨 클래스를 총칭하는 것이다. 앞선 포스팅의 어떤 예제에서 문자열로 지정된 숫자를 정수형으로 변경할 때 사용했던 Integer.parseInt() 매서드를 떠올려보자. 이 parseInt() 매서드가 정의된 클래스의 이름이 Integer인데, 이는 Java의 기본 자료형인 정수형을 객체화하기 위한 클래스다.
정수형 외의 다른 기본 자료형 역시 각자의 Wrapper 클래스를 가진다.
기본 자료형 | Wrapper 클래스 이름 |
boolean | Boolean |
byte | Byte |
char | Character |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
Wrapper 역시 클래스기 때문에 Java의 최상위 클래스인 java.lang.Object 클래스를 상속받는다. 단, Boolean과 Character를 제외한 나머지는 java.lang.Object를 직접 상속받지 않고, java.lang.Object를 상속받는 추상 클래스인 Number를 상속받는다. 즉, Boolean과 Character에게 java.lang.Object는 부모지만, 나머지 Wrapper 클래스는 Number 클래스가 부모고, java.lang.Object는 조부모다.
이 Number 클래스는 내부에 intValue(), shortValue() 등등 Value()로 끝나는 이름의 매서드가 추상 매서드로 정의되어 있으며, 반드시 하위 클래스인 Wrapper에서 이들 매서드의 내용을 정의하도록 되어있다. 이들 추상 매서드는 Wrapper 인스턴스에 지정된 값을 Java의 기본 자료형으로 반환하는 기능을 하며, 이로 인해 Number 클래스 하위의 Wrapper 클래스는 서로 다른 기본 자료형으로 변환이 가능하다.
또한 Wrapper 클래스는 모두 final 제어자가 지정되어 있기 때문에 다른 클래스에서 Wrapper 클래스를 상속하는 것이 불가능하다.
(2) Wrapper 클래스의 인스턴스 생성
Wrapper 클래스 역시 일반적인 클래스의 인스턴스 생성방식과 동일하게 인스턴스를 생성하면 된다. 여기에 더해서, 기본 자료형을 객체화한 클래스다보니, 일부 Wrapper 클래스는 일반적인 상황에서 허용되지 않을 인스턴스 생성 방식도 허용이 된다. 마치 String 클래스가 String 변수명 = new String("ABC") 가 아닌 String 변수명 = "ABC"로도 생성이 가능한 것처럼 말이다.
모든 Wrapper 클래스는 Objects 클래스를 상속하기 때문에 equals()와 toString() 클래스도 사용이 가능하다. 단, 일반적인 클래스와는 달리, Wrapper 클래스는 equals()와 toString() 매서드를 오버라이딩 했기 때문에, 각 인스턴스의 주소값을 비교하거나 출력하지 않고, 참조변수에 대입된 값을 비교하고 출력한다.
또한 Number 클래스를 상속받는 Wrapper 클래스는 각 자료형이 사용할 수 있는 최댓값과 최솟값, 메모리 사이즈, 타입명 등이 상수 형태로 정의되어 있다.
MIN_VALUE: 각 자료형의 최솟값 저장 변수
MAX_VALUE: 각 자료형의 최댓값 저장 변수
SIZE: 각 자료형의 메모리 할당 크기를 bit 단위로 저장하는 변수
BYTES: 각 자료형의 메모리 할당 크기를 Byte 단위로 저장하는 변수
TYPE: 각 자료형의 최솟값 저장 변수
(3) 문자열을 수의 형태로 변환
Wrapper 클래스의 매서드들을 Documentation으로 확인해보신 분들이라면 의아함을 느낄텐데, Number 클래스를 상속받는 Wrapper 클래스들은 문자열을 각자의 자료형으로 변환하는 매서드를 세 개나 가지고 있다.
* {자료형}Value() 매서드 (예: intValue(), shortValue()...)
* parse{자료형}() 매서드 (예: parseInt(), parseShort()...)
* valueOf() 매서드
{자료형}Value() 매서드야 추상클래스인 Number에 정의된 추상 매서드이므로 어느정도 납득이 가지만 나머지 두 개는 생소하다. 우선 사용 방법을 확인해보자.
intValue()의 경우, 반드시 Integer의 인스턴스를 통해 호출이되어야하는 non static 매서드다. 따라서 intValue()를 사용하기 위해 얼마 사용하지도 않을 인스턴스를 생성해서 아까운 메모리만 소모하게 되는 꼴이다. 이러한 이유로 등장한 것이 parseInt()인데, 이 매서드는 static 매서드기 때문에 별도의 인스턴스 생성이 요구되지 않는다. 따라서 Java에서 문자열의 숫자를 기본형으로 변경할 때 parse{자료형}() 매서드가 주로 사용된다.
마지막의 valueOf()는 어디에 사용하는 매서드일까? 매서드가 반환하는 값을 보면, valueOf()의 반환값이 저장되는 변수의 자료형은 기본 자료형이 아닌 참조변수 자료형, Integer다. 즉, valueOf()는 해당 매서드를 호출한 Wrapper() 클래스를 참조 자료형으로 삼아 값을 저장한다. getClass() 매서드를 각 변수에 지정해보면 조금 더 명확해진다.
이제 저 위의 변수 값들을 비교해보자. get_number1과 get_number3은 자료형이 서로 다르니, equals나 조건식으로 같은 값인지 검색하면 false가 나타나지 않을까? 앞에서 int 4와 double 4.0은 다른 값이 나타난 것을 확인했었다. 하지만...
int와 Integer는 비교에 있어서 아무런 문제가 발생하지 않는다. 왜 그럴까?
(4) 오토박싱(Autoboxing)과 언박싱(Unboxing)
처음 Java가 나타났을 때만 하더라도, 기본 자료형과 Wrapper 클래스 인스턴스 값을 비교하거나 연산하는 것은 불가능했다. 서로 다른 자료형을 비교, 연산하기 때문에 당연한 일이었다. 하지만 매번 형변환(Casting)을 통해 연산을 하는 과정이 Java를 만든 사람들에게도 크나큰 불편함을 주었는지, JDK 1.5 버전부터는 컴파일러에서 intValue() 매서드를 사용하여 Integer 객체의 값을 int 형으로 변환시켜주는 과정, 또는 그 반대의 작업을 자동으로 진행하게 만들어주었다. 후자와 같이 기본형을 Wrapper로 변경하는 작업을 오토박싱(Autoboxing)이라고하며, 전자와 같이 Wrapper를 기본형으로 변경하는 작업을 언박싱(Unboxing)이라고 한다.
사실, 이 부분은 컴파일러가 알아서 진행해주는 부분이라 설명 외에는 쓸 말이 없다. 이제 Java에서 사용할 수 있는 유용한 클래스와 매서드가 있는 java.util 패키지를 간략히 더 알아보고 포스팅을 정리하려 한다.
2. 정규식 사용 - java.util.regex 패키지
java.util 패키지는 프로그래밍에 필요한 여러 도구를 제공하는 패키지라 보면 된다. 다수의 유용한 패키지와 클래스들이 있지만 가장 먼저 특정 패턴을 찾아내는 regex 패키지부터 살펴보려 한다.
필자가 전화번호, IP 주소, 주민등록번호가 혼합되어 작성된 어떠한 데이터를 받았다고 가정해보자.
필자는 이 정보들 중 개인의 주민등록번호만 추출하고 싶다. 물론 위의 경우라면 split이나 조금 뒤에서 언급할 StringTokenizer를 사용하면 조금 더 쉽게 주민등록번호를 추출할 수 있지만, 만약 자료가 다음과 같이 혼잡하게 왔다고 가정해보자.
이 경우 특정 문자열을 추출하기 위해 java.util.Regex 클래스를 사용한다. Regex는 정규표현식(Regex Expression)을 의미하는데, 숫자, 문자, 공백 등을 특정 기호로 치환하여 정형화된 자료를 추출하는데 사용한다. 예를 들어, 지금 필자가 추출하려는 전화번호의 경우, 맨 앞 010, 대시, 중간 숫자 4자리, 대시, 끝 숫자 네 자리로 구성되어 있다. 이를 정규표현식으로 표현하면 다음과 같다.
010-\d{4}-\d{4}
주민번호도 마찬가지다.
\d{6}-\d{7}
정규표현식은 본 포스팅에서 모두 다루기에는 내용이 많기 때문에 문자, 숫자 등을 표현하는 방식에 대해서는 이곳을 참고하자.
[ Pattern과 Match 클래스 ]
정규 표현식의 사용 방식은 regex 패키지 내에 존재하는 Pattern과 Matcher 클래스를 이용한다. Pattern은 찾고자 하는 정규표현식을 지정하는 역할을 하며, Matcher는 Pattern의 인스턴스에 지정된 정규표현식과 문자열이 일치하는지 여부를 판단하는 역할을 한다.
정규표현식의 사용 예시를 보기 위해, 먼저 위에서 작성한 메모장 내용을 String으로 저장하자. 앞서 말한대로 필자는 주민등록번호만 추출할 예정이다.
주민등록번호를 정규표현식으로 바꾸면 \\d{6}-\\d{7}이다. 숫자 6자리 + "-" + 숫자 7자리로 구성된 패턴이다. 이를 Pattern 클래스 내 static 매서드인 compile() 내 매개인자로 사용하여, Pattern 타입의 변수에 저장하면 된다.
이 Pattern 클래스 내에는 또 다른 static 매서드인 matcher() 가 존재한다. matcher() 매서드의 매개변수 인자는 정규표현식을 비교할 문자열이다. 따라서 data 변수를 그대로 매개변수에 넣으면 되겠다는 생각을 많이 하겠지만... matcher()의 경우 정규표현식이 문자열과 완전히 일치해야만 결과를 돌려주기 때문에 data 변수를 매개인자로 사용하면 아무런 반응이 나타나지 않는다(이 부분은 조금 뒤에서 살펴보자).
따라서 필자는 data 변수의 값들을 split(",")으로 나눈 결과를 Pattern.matches()로 확인할 것이다. 하지만 문자열 내에는 개행문자인 "\n" 역시 존재하기 때문에 split을 진행하기 전, 개행문자를 모두 ", "으로 변경해주어야 한다.
이제 변경된 문자열을 split(", ") 적용하여 문자열 배열을 만들고, for 문으로 id_p.matcher() 매서드를 적용해보자.
id_p.matcher() 매서드이 결과값을 받은 Matcher 클래스는, static 매서드로 matches()라는 녀석을 가진다. Matcher 객체 변수는 id_p.matcher()로부터 생성된 정규표현식 검사 결과를 저장하는데, 정규표현식과 일치하는 결과가 있는 경우 true를, 그렇지 않은 경우 false를 반환한다. 따라서, 아래와 같이 matches() 매서드를 사용하여 if 조건문을 달게되면, 정규표현식을 만족하는 문자들만 화면에 출력하게 된다.
앞서서 Pattern.matcher()의 매개 인자로 data를 넣으면 아무런 값이 출력되지 않는다고 언급했다. 당연한 것이, data 전체 문자열은 문자로 시작하기 때문에 "\\d{6}-\\d{7}"과는 전혀 일치하지 않기 때문이다.
그럼, 문자열 내에 정규표현식과 일치하는 문자열이 포함되어 있는 경우, 이 문자열만 추출할 수 있는 방법은 없을까? 당연히 있다.
Matcher 클래스에는 matcher() 외에 find()라는 클래스가 존재한다. 이 매서드는 matcher()로 인해 정규표현식에 해당하는 문자열이 매개변수값 안에 존재하는 경우, 그 수에 맞게 true를 순차적으로 반환한다.
또 다른 매서드인 group()은 find()에 의해 발견된 문자열 값들을 고스란히 반환하는 매서드다. 따라서 다음과 같이 find()와 group() 매서드를 번갈아 사용하면, 필자가 추출하고자 하는 주민등록번호만 고스란히 출력할 수 있다.
이 코드를 조금 더 세련되 보이게 바꾸면 아래와 같이 나타낼 수 있다.
Python에서 정규표현식을 사용해보신 분들이라면 느끼시겠지만, Java의 정규표현식과 표현방식이 다를 뿐 사용 방법은 유사한 편이다. 아무래도 Python의 기능들이 Java를 참고한 것들이 있어서 그런 듯 하다. Python에서는 정규표현식과 일치하는 문자열마저도 세부 그룹으로 나누는 기능이 있는데 이는 Java에서도 제공하는 기능이다. 사용 예시는 정규표현식으로 추출한 문자를 한 번 더 분류해야하는 경우 사용한다. 주민등록번호는 생년월일과 고유 번호로 나뉘기 때문에 Java의 그룹화를 사용하면, 이 값들 역시 쉽게 분류가 가능해진다.
정규 표현식 내에 소괄호 두 개가 보이는데, 이들이 바로 그룹화를 담당한다. 첫 번째 소그룹은 1번, 두 번째 그룹은 2번으로 분류되며 group() 매서드 내에 그룹 번호(Group Name)를 작성하면 해당 그룹과 일치하는 문자열만 출력된다. 자세한 내용은 Java의 Documentation 문서를 참조하자.
3. java.util.Objects 클래스
Java의 클래스로부터 최상위 클래스인 Object의 매서드를 사용한다고 가정해보자. 만약 클래스로부터 만든 인스턴스가 인스턴스 메모리를 참조하지 않고, null 값을 참조한다면, equals()나 toString() 등의 상속 매서드를 인스턴스로부터 호출하는 것이 불가능해진다. 만약 null인 객체로부터 이들 매서드를 호출한다면 아래와 같이 NullPointerException 오류를 만나게 될 것이다.
Object 클래스의 매서드는 반드시 클래스의 인스턴스로부터 호출이 되어야 사용이 가능하다는 단점이 있다. 이 때문에 Object 클래스의 매서드를 사용해야하는 경우, 반드시 인스턴스가 null 값이 아닌지 확인하는 코드 또는 예외처리 코드가 필수로 들어가야만 한다.
Object 클래스의 매서드를 사용하다보면, null 값 확인으로 인해 쓸데없이 코드가 늘어나게 되는데, 이러한 불편함을 해소하기 위해 나타난 클래스가 바로 java.util.Objects 클래스다. java.util.Objects 클래스는 대부분의 Object 클래스 매서드와 동일한 이름의 매서드를 가지고 있다. 하지만 Object 클래스의 매서드들과 달리, Objects 클래스는 static으로 제어자가 정의되어 있기 때문에 반드시 인스턴스로부터 호출되지 않아도 된다. Object와 동일한 이름을 가지나 매개 변수의 수나 제어자가 달리 적용되었기 때문에 오버라이딩으로 정의된 매서드들이 아니다.
두 클래스의 equals() 매서드의 사용 예시를 비교해보자.
java.util.Objects 클래스에서 사용할 수 있는 매서드의 종류는 아래와 같다. 대부분 Object 클래스에서 볼 수 있는 것이며, 추가로 초록색 박스 안의 매서드에 대해서만 간략히 설명을 더 진행하려한다.
java.util.Objects 클래스는 아무래도 null 값을 가지는 객체에 대한 검사 기능이 추가된 클래스이기 때문에 Object에 없는 isNull()과 nonNull()이라는 매서드가 존재한다. 이들 매서드의 기능은 이름 그대로 매우 명확하다. 매개변수로 들어온 참조 객체가 null이면 true를 반환하는 기능은 isNull()이 담당하고, 반대로 참조객체가 null이 아닌 경우 true를 반환하는 기능은 nonNull() 매서드가 담당한다.
마지막으로 deepEquals() 매서드에 대해 알아보자. 이 deepEquals()는 보통 배열에 지정된 값의 비교에 많이 사용한다. 아래와 같이 두 개의 동일한 정수형 배열을 생성하고, 이 두 배열의 값을 equals()와 deepEquals()로 비교한다고 해보자.
deepEquals() 내 인자로 들어온 두 값을 비교하는데, 만약 매개 변수가 배열일 경우, Arrays.deepEquals() 매서드를 사용하여 값들을 비교하고, 그렇지 않은 경우 첫 매개변수 인자의 equals() 매서드를 사용하여 비교한다.
4. java.util.Random 클래스
Random 클래스는 말 그대로 난수를 생성하는데 사용하는 클래스다. 로또 번호나 온라임 게임의 랜덤 박스 같이 무작위 선정이 필요한 곳에 사용한다.
난수를 생성하는 방법은 단순히 Random 클래스의 인스턴스를 생성자로 호출한 뒤, 해당 인스턴스를 통해 nextInt() 또는 nextDouble() 매서드를 호출하면 된다. 인스턴스가 생성되면 nextInt(), nextDouble() 등등 Scanner의 매서드들과 마찬가지로 입력받은 문자를 해당 타입으로 돌리는 매서드를 사용하여 값을 추출하면 된다.
무작위 숫자를 출력하는 Random은 인스턴스 생성 시 seed라고 불리는 long 타입의 값을 참조하여 random 숫자를 만든다. 새 인스턴스 생성 시 이 seed 값이 모두 다르게 지정되기 때문에 위의 예시처럼 모두 다른 값이 나오게 되는 것이다. 이 seed 값은 생성자의 매개 인자로도 설정할 수 있는데, 동일한 seed 값을 지정하면 무작위 숫자는 동일한 값이 출력된다.
Random 클래스의 seed 값은 사용자가 일일이 지정하기에는 귀찮은 부분이 있다. 그렇기 때문에 이 seed 값은 현재의 시간과 관련된 값을 많이 사용한다. 특정 일자로부터 현재시간까지 흐른 초(second)를 값으로 넣거나, 현재 시간의 나노 초 시간을 대입하는 등이 그 예시다.
인스턴스를 생성하지 않고 seed 값을 바꾸는 방법도 매서드로 제공한다. setSeed() 라는 매서드인데, 인자로 지정하고자 하는 seed 값을 입력해주면 된다. 이 seed 값이 동일한 경우 출력되는 무작위 숫자는 모두 동일하다.
만약 로또처럼 1부터 46까지, 그러니까 정해진 범위 내의 숫자만을 무작위로 추출하고 싶다면, nextDouble() 값을 갈무리해서 사용하면 된다. nextDouble() 값은 0과 1 사이의 소수값만 출력되므로, nextDouble() 반환값에 46만 곱하면 0~45까지의 값이 무작위로 선정된다. 이 선정된 값에 1만 더하면 1 ~ 46 범위 내의 무작위 값을 선택하는 코드가 완성된다.
5. java.util.Scanner 클래스
필자의 Java 초창기 포스팅에서 Scanner 클래스로 단순히 키보드로 입력받은 글자를 나타내는 방법에 대해 소개했었다. 하지만 Scanner는 키보드와 같은 표준입력 외에도 파일이나 앞서 보았던 정규표현식 객체를 매개변수로 받아 표현하는 것도 가능하다. 이번 포스팅에서는 Scanner를 통한 파일의 내용을 입력받는 법에 대해서만 간략히 알아보려 한다. 추후 입출력(I/O)와 관련된 포스팅에서 다시 파일 관련 작업을 상세히 설명하겠지만, Scanner로도 파일 내용을 읽어들이는 방법이 있다고 알고만 있으면 될 듯 하다.
파일의 내용을 입력받기 위해서는 파일에 대한 정보를 Java에서 알고 있어야 한다. Java는 객체지향언어이므로 이 파일 또한 하나의 객체로 관리되는데, 이와 관련된 클래스는 java.io 패키지에 존재하는 File 클래스다.
필자는 프로젝트 폴더(UtilClasses)에 test.txt라는 파일을 만들고 문구를 입력한 뒤 저장하였다. 이제 이 파일을 Scanner로 불러보려 한다.
Scanner의 인스턴스 참조변수인 sc_file은 키보드로 표준 입력값을 받을 때와 동일한 매서드를 사용하여 파일 내용을 읽어들인다. Line마다 내용을 읽기를 원하면 위의 예시에서 사용한 nextInt() 매서드를 사용하면 된다. hasNext()로 true 값이 확인된 내용에 대해서만 화면에 출력하는데 이는 정규표현식의 find(), group()의 관계와 매우 유사하다.
6. java.util.StringTokenizer 클래스
이번 포스팅에서 마지막으로 살펴볼 StringTokenizer다. StringTokenizer는 문자열을 구분자 기준으로 나누는 역할을 한다. String 클래스의 split()이나 StringJoiner.useDelimiter()와 동일한 역할을 하나, 매개인자를 통째로 구분자로 사용하는 split(), useDelimiter()와 달리, 여러 구분자를 매개인자에 표시할 수 있다는 장점이 있다. 즉
str.split(",");
... split("\n")
과 같이 번거롭게 여러 줄을 표시하는 대신,
StringTokenizer st = new StringTokenizer(str, ",\n");
와 같이 한 줄로 표시가 가능하다. 물론 이 때문에 복잡한 문자열을 구분자로 사용하기에는 적절하지 않다.
위에서 읽은 test.txt 파일 내용을 단어 단위로 잘라 표시해보려 한다. 구분자는 알파벳 'a', 공백 두 가지만 지정해보려 한다.
StringTokenizer는 위에서 보았던 Scanner와 마찬가지로, 다음 token(값)이 존재하는지 확인하는 hasMoreToken()이라는 매서드가 존재한다. 이 매서드로 다음 값의 존재가 확인이 된다면 nextToken() 매서드로 해당 값을 출력할 수 있다.
만약 구분자도 함께 출력하고 싶다면 StringTokenizer 생성자의 세 번째 인자를 입력해야 한다. 이 세 번 째 인자의 입력값은 boolean 타입인데, 구분자로 사용된 문자열도 출력할지 여부를 결정한다.
다음 포스팅에서는 날짜와 시간을 다루는 클래스에 대해 알아보려 한다.
Fin.
'Java > Java Basic' 카테고리의 다른 글
[Java Basic] 30. 형식화 클래스 (0) | 2022.08.26 |
---|---|
[Java Basic] 29. 날짜 및 시간 관련 클래스 (0) | 2022.08.24 |
[Java Basic] 27. java.lang.StringBuffer, StringBuilder 클래스, 문자열 인코딩 (0) | 2022.08.16 |
[Java Basic] 26. java.lang.String 클래스 (0) | 2022.08.15 |
[Java Basic] 25. java.lang.Object 클래스 (0) | 2022.08.12 |
댓글