java.lang 패키지와 유용한 클래스
java.lang 패키지와 유용한 클래스
java.lang 패키지는 자바프로그래밍에 가장 기본이 되는 클래스들을 포함하고 있다. 그렇기 때문에 java.lang 패키지의 클래스들은 import문 없이도 사용할 수 있게 되어 있다. 그 동안 String 클래스나 System 클래스를 import문 없이 사용할 수 있었던 이유가 바로 java.lang 패키지에 속한 클래스들이기 때문이었던 것이다.
java.lang 패키지
Object 클래스
Object 클래스는 멤버변수는 없고 오직 11개의 메서드만 가지고 있다.
equals(Object obj)
매개변수로 객체의 참조변수를 받아서 비교하여 그 결과를 booelan 값으로 알려주는 역할을 한다.* 아래의 코드는 Object 클래스에 정의되어 있는 equals 메서드의 실제 내용이다.
1 | public boolean equals(Object obj) { |
두 객체의 같고 다름을 참조변수의 값으로 판단한다.
String 클래스는 Object 클래스의 equals 메서드를 그대로 사용하는 것이 아니라 이처럼 오버라이딩을 통해서 String 인스턴스가 갖는 문자열 값을 비교하도록 되어있다.
String 클래스 뿐만 아니라 Date, File, Wrapper 클래스(Integer, Double 등)의 equals 메서드도 주소값이 아닌 내용을 비교하도록 오버라이딩되어 있다. 그러나 StringBuffer 클래스는 오버라이딩되어 있지 않다.
hashCode()
이 메서드는 해싱(hashing)기법에 사용되는 ‘해시함수(hash function)’를 구현한 것이다. 해싱은 데이터관리기법 중의 하나인데 다량의 데이터를 저장하고 검색하는 데 유용하다.*
해시함수는 찾고자하는 값을 입력하면, 그 값이 저장된 위치를 알려주는 해시코드(hashcode)를 반환한다.
Object 클래스에 정의된 hashCode 메서드는 객체의 주소값을 이용해서 해시코드를 만들어 반환하기 때문에 서로 다른 두 객체는 결코 같은 해시코드를 가질 수 없다. (해싱기법을 사용하는 HashMap이나 HashSet과 같은 클래스에 저장할 객체라면 반드시 hashCode 메서드를 오버라이딩해야 한다.)
String 클래스는 문자열의 내용이 같으면, 동일한 해시코드를 반환하도록 hashCode 메서드가 오버라이딩되어 있기 때문에, 문자열의 내용이 같은 str1과 str2에 대해 hashCode()를 호출하면 항상 동일한 해시코드값을 얻는다.
반면에 System.identifyHashCode(Object x)는 Object 클래스의 hashCode 메서드처럼 객체의 주소값으로 해시코드를 생성하기 때문에 모든 객체에 대해 항상 다른 해시코드값을 반환할 것을 보장한다. 그래서 str1과 str2가 해시코드는 같지만 서로 다른 객체라는 것을 알 수 있다.
toString()
인스턴스에 대한 정보를 문자열(String)로 제공할 목적으로 정의한 것이다. 인스턴스의 정보를 제공한다는 것은 대부분의 경우 인스턴스 변수에 저장된 값들을 문자열로 표현한다는 뜻이다.
Object클래스에 정의된 toString()은 아래와 같다.
1 | public String toString() { |
clone()
자신을 복제하여 새로운 인스턴스를 생성하는 일을 한다. Object 클래스에 정의된 clone()은 단순히 인스턴스변수의 값만을 복사하기 때문에 참조 변수 타입의 인스턴스 변수가 정의되어 있는 클래스는 완전한 인스턴스 복제가 이루어지지 않는다.
cloneable 인터페이스를 구현한 클래스에서만 clone()을 호출할 수 있다. 또한 clone()을 오버라이딩하면서 접근 제어자를 protected에서 public으로 변경해야 한다.
clone()은 단순히 객체에 저장된 값을 그대로 복제할 뿐, 객체가 참조하고 있는 객체까지 복제하지는 않는다. 반면에 원본이 참조하고 있는 객체까지 복제하는 것을 ‘깊은 복사’라고 한다. 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 원본의 변경이 복사본에 영향을 미치지 않는다.
공변 반환타입
JDK 1.5부터 ‘공변 반환타입’ 이라는 것이 추가되었다. 이 기능은 오버라이딩할 때 조상 메서드의 반환타입을 자손 클래스의 타입으로 변경하는 것이다. 따라서 clone()의 반환타입을 Object가 아닌 자손의 타입으로 변경가능하다.
‘공변 반환타입’을 사용하면 조상의 타입이 아닌 실제로 반환되는 자손 객체의 타입으로 반환할 수 있어서 번거로운 형변환이 줄어든다는 장점이 있다.
getClass()
자신이 속한 클래스의 Class객체를 반환하는 메서드인데, Class 객체는 이름이 ‘Class’인 클래스의 객체이다. Class 객체는 아래와 같이 정의되어 있다.
1 | public final class Class implements ... { // Class 클래스 |
Class 객체는 클래스의 모든 정보를 담고 있으며, 클래스당 단 1개만 존재한다. 그리고 클래스 파일이 ‘클래스 로더(ClassLoader)’에 의해서 메모리에 올라갈 때, 자동적으로 생성된다.
클래스 로더는 실행 시에 필요한 클래스를 동적으로 메모리에 로드하는 역할을 한다. 먼저 기존에 생성된 클래스 객체가 메모리에 존재하는지 확인하고, 있으면 객체의 참조를 반환하고 없으면 클래스 패스(classpath)에 지정된 경로를 따라서 클래스 파일을 찾는다. 못 찾으면 ClassNotFoundException이 발생하고, 찾으면 해당 클래스 파일을 읽어서 Class 객체로 변환한다.
파일 형태로 저장되어 있는 클래스를 읽어서 Class 클래스에 정의된 형식으로 변환하는 것이다. 즉, 클래스 파일을 읽어서 사용하기 편한 형태로 저장해 놓은 것이 클래스 객체이다. (클래스 파일을 메모리에 로드하고 변환하는 일은 클래스 로더가 한다.)
Class 객체를 얻는 방법
Class 객체에 대한 참조를 얻는 방법은 여러 가지가 있다.
1
2
3 Class cObj = new Card().getClass(); // 생성된 객체로 부터 얻는 방법
Class cObj = Card.class; // 클래스 리터럴(*.class)로 부터 얻는 방법
Class cObj = Class.forName("Card"); // 클래스 이름으로 부터 얻는 방법특히 forName()은 특정 클래스 파일, 예를 들어 데이터베이스 드라이버를 메모리에 올릴 때 주로 사용한다. Class 객체를 이용하면 클래스에 정의된 멤버의 이름이나 개수 등, 클래스에 대한 모든 정보를 얻을 수 있기 때문에 Class 객체를 통해서 객체를 생성하고 메서드를 호출하는 등 보다 동적인 코드를 작성할 수 있다.
String 클래스
기존의 다른 언어에서는 문자열을 char형의 배열로 다루었으나 자바에서는 문자열을 위한 클래스를 제공한다.
변경 불가능한(immutable) 클래스
String 클래스는 문자열을 저장하기 위해서 문자형 배열 변수(char[]) value를 인스턴스 변수로 정의해놓고 있다. 인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 인스턴스변수(value)에 문자형 배열(char[])로 저장되는 것이다.
1 | public final class String implements java.io.Serializable, Comparable { |
한번 생성된 String 인스턴스가 갖고 있는 문자열은 읽어 올 수만 있고, 변경할 수는 없다. 예를 들어 ‘+’ 연산자를 이용해서 문자열을 결합하는 경우 인스턴스내의 문자열이 바뀌는 것이 아니라 새로운 문자열이 담긴 String 인스턴스가 생성되는 것이다.
덧셈 연산자 ‘+’를 사용해서 문자열을 결합하는 것은 매 연산 시 마다 새로운 문자열을 가진 String 인스턴스가 생성되어 메모리공간을 차지하게 되므로 가능한 한 결합횟수를 줄이는 것이 좋다.
문자열을 다루는 작업이 많이 필요한 경우에는 String 클래스 대신 StringBuffer 클래스를 사용하는 것이 좋다. StringBuffer 인스턴스에 저장된 문자열은 변경이 가능하므로 하나의 StringBuffer 인스턴스만으로도 문자열을 다루는 것이 가능하다.
문자열의 비교
문자열을 만들 때는 두 가지 방법, 문자열 리터럴을 지정하는 방법과 String 클래스의 생성자를 사용해서 만드는 방법이 있다.
1 | Strig str1 = "abc"; // 문자열 리터럴 "abc"의 주소가 str1에 저장됨 |
String 클래스의 생성자를 이용한 경우에는 new 연산자에 의해서 메모리할당이 이루어지기 때문에 항상 새로운 String 인스턴스가 생성된다. 그러나 문자열 리터럴은 이미 존재하는 것을 재사용하는 것이다. (문자열 리터럴은 클래스가 메모리에 로드될 때 자동적으로 미리 생성된다.)
equals()를 사용했을 때는 두 문자열의 내용(“abc”)을 비교하기 때문에 두 경우 모두 true를 결과로 얻는다. 하지만, 각 String 인스턴스의 주소를 등가비교연산자 “==”로 비교했을 때는 결과가 다르다.
문자열 리터럴
자바 소스파일에 포함된 모든 문자열 리터럴은 컴파일 시에 클래스 파일에 저장된다. 이대 같은 내용의 문자열 리터럴은 한번만 저장된다.* 문자열 리터럴도 String 인스턴스이고 한번 생성하면 내용을 변경할 수 없기 때문에 하나의 인스턴스를 공유하면 되기 때문이다.
String 리터럴들은 컴파일 시에 클래스파일에 저장된다. 클래스 파일에는 소스파일에 포함된 모든 리터럴의 목록이 있다. 해당 클래스 파일이 클래스 로더에 의해 메모리에 올라갈 때, 이 리터럴의 목록에 있는 리터럴들이 JVM내에 있는 ‘상수 저장소(constant pool)’에 저장된다.
빈 문자열(empty string)
길이가 0인 배열이 존재할 수 있다. char형 배열도 길이가 0인 배열을 생성할 수 있고, 이 배열을 내부적으로 가지고 있는 문자열이 바로 빈 문자열이다.*
‘String s = “”;’ 과 같은 문장이 있을 때, 참조변수 s가 참조하고 있는 String 인스턴스는 내부에 ‘new char[0]’과 같이 길이가 0인 char형 배열을 저장하고 있는 것이다.
그러나 ‘String s = “”;’과 같은 표현이 가능하다고 해서 ‘char c = ‘’;’와 같은 표현도 가능한 것은 아니다. char형 변수에는 반드시 하나의 문자를 지정해야한다.
일반적으로 변수를 선언할 때, 각 타입의 기본값으로 초기화 하지만 String은 참조형 타입의 기본값인 null 보다는 빈 문자열로, char형인 기본값은 ‘₩u0000’ 대신 공백으로 초기화 하는 것이 보통이다. (‘₩u0000’은 유니코드의 첫 번째 문자로써 아무런 문자도 지정되지 않은 빈 문자이다.)
문자 인코딩 변환
getBytes(String charsetName)를 사용하면, 문자열의 문자 인코딩을 다른 인코딩으로 변경할 수 있다. 자바가 UTF-16을 사용하지만, 문자열 리터럴에 포함되는 문자들은 OS의 인코딩을 사용한다.
1 | byte[] utf8_str = "가".getBytes("UTF-8"); // 문자열을 UTF-8로 분환 |
서로 다른 문자 인코딩을 사용하는 컴퓨터 간에 데이터를 주고받을 때는 적절한 문자 인코딩이 필요하다.
UTF-8은 한글 한 글자를 3 byte로 표현하고, CP949는 2 byte로 표현한다.
기본형 값을 String으로 변환
기본형을 문자열로 변경하는 방법은 간단하다. 숫자에 빈 문자열””을 더해주기만 하면 된다. 이 외에도 valueOf()를 사용하는 방법도 있다. 성능은 valueOf()가 더 좋지만, 빈 문자열을 더하는 방법이 간단하고 편하기 때문에 성능향상이 필요한 경우에만 valueOf()를 쓰자.
참조변수에 String을 더하면, 참조변수가 가리키고 있는 인스턴스의 toString()을 호출하여 String을 얻은 다음 결합한다.
String을 기본형 값으로 변환
이전에는 parseInt()와 같은 메서드를 많이 섰는데, 메서드의 이름을 통일하기 위해 valueOf()가 나중에 추가되었다. valueOf(String s)는 메서드 내부에서 그저 parseInt(String s)를 호출할 뿐이므로, 두 메서드는 반환 타입만 다르지 같은 메서드이다.
1 | public static Integer valueOf(String s)throws NumberFormatException { |
StringBuffer 클래스와 StringBuilder 클래스
String 클래스는 인스턴스를 생성할 때 지정된 문자열을 변경할 수 없지만 StringBuffer 클래스는 변경이 가능하다. 내부적으로 문자열 편집을 위한 버퍼(buffer)를 가지고 있으며, StringBuffer 인스턴스를 생성할 때 그 크기를 지정할 수 있다.
StringBuffer 클래스는 String 클래스와 같이 문자열을 저장하기 위한 char형 배열의 참조변수 인스턴스로 선언해 놓고 있다. StringBuffer 인스턴스가 생성될 때, char형 배열이 생성되며 이 때 생성된 char형 배열을 인스턴스변수 value가 참조하게 된다.
1 | public final class StringBuffer implements java.io.Serializable { |
StringBuffer의 생성자
StringBuffer 클래스의 인스턴스를 생성할 때, 적절한 길이의 char형 배열이 생성되고, 이 배열은 문자열을 저장하고 편집하기 위한 공간(buffer)으로 사용된다.
StringBuffer 인스턴스를 생성할 때는 생성자 StringBuffer(int length)를 사용해서 StringBuffer 인스턴스에 저장될 문자열의 길이를 고려하여 충분히 여유있는 크기로 지정하는 것이 좋다. StringBuffer 인스턴스를 생성할 때, 버퍼의 크기를 지정해 주지 않으면 16개의 문자를 저장할 수 있는 크기의 버퍼를 생성한다.
1 | public StringBuffer(int length) { |
StringBuffer 인스턴스로 문자열을 다루는 작업을 할 때, 버퍼의 크기가 작업하려는 문자열의 길이보다 작을 때는 내부적으로 버퍼의 크기를 증가시키는 작업이 수행된다.
배열의 길이는 변경될 수 없으므로 새로운 길이의 배열을 생성한 후에 이전 배열의 값을 복사해야 한다.
StringBuffer의 비교
String 클래스에서는 equals메서드를 오버라이딩해서 문자열의 내용을 비교하도록 구현되어 있지만, StringBuffer 클래스는 equals메서드를 오버라이딩하지 않아서 StringBuffer클래스의 equals메서드를 사용해도 등가비교연산자(==)로 비교한 것과 같은 결과를 얻는다.
1 | StringBuffer sb = new StringBuffer("abc"); |
반면에 toString()은 오버라이딩되어 있어서 StringBuffer 인스턴스에 toString()을 호출하면, 담고있는 문자열을 String으로 변환한다.
그래서 StringBuffer 인스턴스에 담긴 문자열을 비교하기 위해서는 StringBuffer 인스턴스에 toString()을 호출해서 String 인스턴스를 얻은 다음, 여기에 equals 메서드를 사용해서 비교해야한다.
StringBuilder란?
StringBuffer는 멀티쓰레드에 안전(thread safe)하도록 동기화되어 있다. 멀티쓰레드로 작성된 프로그램이 아닌 경우, StringBuffer의 동기화는 불필요하게 성능만 떨어뜨리게 된다.
그래서 StringBuffer에서 쓰레드의 동기화만 뺀 StringBuilder가 새로 추가되었다.
래퍼(wrapper) 클래스
경우에 따라 기본형(primitive type) 변수도 어쩔 수 없이 객체로 다뤄야 하는 경우가 있다. 예를 들면, 매개변수로 객체를 요구할 때, 기본형 값이 아닌 객체로 저장해야할 때, 객체 간의 비교가 필요할 때 등등의 경우에는 기본형 값들을 객체로 변환하여 작업을 수행해야 한다.
이 때 사용되는 것이 래퍼(wrapper)
클래스이다. 8개의 기본형을 대표하는 8개의 래퍼클래스가 있는데, 이 클래스들을 이용하면 기본형 값을 객체로 다룰 수 있다.
래퍼 클래스들은 객체생성 시에 생성자의 인자로 주어진 각 자료형에 알맞은 값을 내부적으로 저장하고 있으며, 이에 관련된 여러 메서드가 정의되어 있다.
래퍼 클래스들은 모두 equals()가 오버라이딩되어 있어서 주소값이 아닌 객체가 가지고 있는 값을 비교한다. 오토박싱이 된다고 해도 Integer객체에 비교연산자를 사용할 수 없다. 대신 compareTo()를 제공한다.
그리고 toString()도 오버라이딩되어 있어서 객체가 가지고 있는 값을 문자열로 변환하여 반환한다.
문자열을 숫자로 변환하기
1 | int i = new Integer("100").intValue(); |
타입.parse타입(String s)
의 반환값이 기본형(primitive type)이고, 타입.valueOf()
는 반환값이 래퍼 클래스 타입이라는 차이가 있다.
문자열 -> 기본형
int i = Integer.parseInt(“100”);문자열 -> 래퍼 클래스
Integer i = Integer.valueIf(“100”);
JDK 1.5부터 도입된 ‘오토박싱(autoboxing)’ 기능 때문에 반환값이 기본형일 때와 래퍼 클래스일 때의 차이가 없어졌다. 그래서 그냥 구별없이 valueOf()를 쓰는 것도 괜찮은 방법이다. 단, 성능은 valueOf()가 조금 더 느리다.
오토박싱 & 언박싱
JDK 1.5 이전에는 기본형과 참조형 간의 연산이 불가능했기 때문에, 래퍼 클래스로 기본형을 객체로 만들어서 연산해야 했다.
그러나 이제는 기본형과 참조형 간의 덧셈이 가능하다. 자바 언어의 규칙이 바뀐 것은 아니고, 컴파일러가 자동으로 변환하는 코드를 넣어주기 때문이다.
기본형 값을 래퍼 클래스의 객체로 자동 변환해주는 것을 ‘오토박싱’이라고 하고, 반대로 변환하는 것을 ‘언박싱’이라고 한다.
1 | ArrayList<Integer> list = new ArrayList<Integer>(); |
ArrayList에 숫자를 저장하거나 꺼낼 때, 기본형 값을 래퍼클래스의 객체로 변환하지 않아도 되므로 편리하다.
유용한 클래스
java.util.Objects 클래스
Object 클래스의 보조 클래스로 Math 클래스처럼 모든 메서드가 ‘static’이다. 객체의 비교나 널 체크(null check)에 유용하다.
IsNull()은 해당 객체가 널인지 확인해서 null이면 true를 반환하고 아니면 false를 반환한다. nonNull()은 isNull()과 정확히 반대의 일을 한다.
1 | static boolean isNull(object obj) |
그리고 requireNonNull()은 해당 객체가 널이 아니어야 하는 경우에 사용한다. 만일 객체가 널이면, NullPointerException을 발생시킨다. 두 번째 매개변수로 지정하는 문자열은 예외의 메시지가 된다.
1 | static <T> requireNonNull(T obj) |
Object 클래스에는 두 객체를 비교하는 메서드가 등가비교를 위한 equals()만 있고, 대소비교를 위한 compare()가 없는 것이 좀 아쉬웠다. 그래서인지 Objects에는 compare()가 추가되었다. compare()는 두 비교대상이 같으면 0, 크면 양수, 작으면 음수를 반환한다.
1 | static int compare(Object a, Object b, Comparator c) |
이 메서드는 a와 b 두 객체를 비교하는데, 두 객체를 비교하는데 사용할 비교 기준이 필요하다. 그 역할을 하는 것이 Comparator이다.
Objects 클래스의 equals()는 Object 클래스와는 달리, null 검사를 하지 않아도 된다. equals()의 내부에서 a와 b의 null 검사를 하기 때문에 따로 null 검사를 위한 조건식을 넣지 않아도 된다. 실제 메서드의 코드는 다음과 같다.
1 | public static boolean equals(Object a, Object b) { |
a와 b가 모두 null인 경우에는 참을 반환한다는 점을 빼고는 특별한 것이 없다. deepEquals() 메서드는 객체를 재귀적으로 비교하기 때문에 다차원 배열의 비교도 가능하다.
java.util.Scanner 클래스
Scanner는 화면, 파일, 문자열과 같은 입력소스로부터 문자데이터를 읽어오는데 도움을 줄 목적으로 JDK 1.5부터 추가되었다. Scanner에는 다음과 같은 생성자를 지원하기 때문에 다양한 입력소스로부터 데이터를 읽을 수 있다.
1 | Scanner(String source) |
java.util.StringTokenizer 클래스
StringTokenizer는 긴 문자열을 지정한 구분자(delimiter)를 기준으로 토큰(token)이라는 여러 개의 문자열로 잘라내는 데 사용된다. StringTokenizer를 이용하는 방법 이외에도 String의 split(String reges)이나 Scanner의 useDelimiter(Spring pattern)를 사용할 수도 있다.
위의 이 두 가지 방법은 정규식 표현(Regular expression)을 사용해야 하므로 정규식 표현에 익숙하지 않은 경우 StringTokenizer를 사용하는 것이 간단하면서도 명확한 결과를 얻을 수 있다.
그러나 StirngTokenizer는 구분자로 단 하나의 문자열 밖에 사용하지 못하기 때문에 복잡한 형태의 구분자로 문자열을 나누어야 하는 경우에는 정규식을 사용하는 메서드를 사용해야 한다.
split()은 빈 문자열도 토큰으로 인식하는 반면 StringTokenizer는 빈 문자열을 토큰으로 인식하지 않기 때문에 인식하는 토큰의 개수가 서로 다른 것을 알 수 있다.
이 외에도 성능의 차이가 있는데, split()은 데이터를 토큰으로 잘라낸 결과를 배열에 담아서 반환하기 때문에 데이터를 토큰으로 바로바로 잘라서 반환하는 StringTokenizer보다 성능이 떨어질 수밖에 없다. 그러나 데이터의 양이 많은 경우가 아니라면 별 문제가 되지 않으므로 크게 신경 쓸 부분은 아니다.
java.math.BigInteger 클래스
정수형으로 표현할 수 있는 값의 한계가 있다. 가장 큰 정수형 타입인 long으로 표현할 수 있는 값은 10진수로 19자리 정도이다. 이 값도 상당히 큰 값이지만, 과학적 계산에서는 더 큰값을 다뤄야할 때가 있다. 그럴 때 사용하면 좋은 것이 BigIntenger이다.
BigInteger는 내부적으로 int배열을 사용해서 값을 다룬다. 그래서 long 타입보다 훨씬 큰 값을 다룰 수 있는 것이다. 대신 성능은 long 타입보다 떨어질 수밖에 없다.
1 | final int signum; // 부호. 1(양수), 0, -1(음수) 셋 중의 하나 |
BigIntenger는 String처럼 불변(immutable)이다. 그리고 모든 정수형이 그렇듯이 BigInteger 역시 값을 ‘2의 보수’의 형태로 표현한다.
위의 코드에서 알 수 있듯이 부호를 따로 저장하고 배열에는 값 자체만 저장한다. 그래서 signum의 값이 -1, 즉 음수인 경우, 2의 보수법에 맞게 mag의 값을 변환해서 처리한다. 그래서 부호만 다른 두 값의 mag의 값을 변환해서 처리한다. 그래서 부호만 다른 두 값의 mag는 같고 signum은 다르다.
BigInteger는 불변이므로, 반환타입이 BigInteger이란 얘기는 새로운 인스턴스가 반환된다는 뜻이다.
비트 연산 메서드
워낙 큰 숫자를 다루기 위한 클래스이므로, 성능을 향상시키기 위해 비트단위로 연산을 수행하는 메서드들을 많이 갖고 있다. 따라서 가능하면 산술연산 대신 비트연산으로 처리하도록 노력해야 한다.
java.math.BigDecimal 클래스
double 타입으로 표현할 수 있는 값은 상당히 범위가 넓지만, 정밀도가 최대 13자리 밖에 되지 않고 실수형의 특성상 오차를 피할 수 없다. BigDecimal은 실수형과 달리 정수를 이용해서 실수를 표현한다. 실수의 오차는 10진 실수를 2진 실수로 정확히 변환할 수 없는 경우가 있기 때문에 발생하는 것이므로, 오차가 없는 2진 정수로 변환하여 다루는 것이다.