java.lang 패키지
java.lang 패키지는 자바프로그램에 가장 기본이 되는 클래스들을 포함하고 있습니다. 그렇기 때문에 java.lang 패키지의 클래스들은 import문 없이도 사용할 수 있게 되어 있습니다. String클래스나 System클래스를 사용할 때 import문 없이 사용할 수 있는 이유가 바로 java.lang 패키지에 속한 클래스들이기 때문입니다.
Object 클래스
Object클래스는 모든 클래스의 최고 조상이기 때문에 Object클래스의 멤버들은 모든 클래스에서 바로 사용 가능합니다.
| Object클래스의 메서드 | 설 명 |
| protected Object clone() | 객체 자신의 복사본을 반환 |
| public boolean equals(Object obj) | 객체 자신과 객체 obj가 같은 객체인지 비교 |
| protected void finalize() | 객체가 소멸될 때 가비지 컬렉터에 의해 자동적으로 호출 |
| public Class getClass() | 객체 자신의 클래스 정보를 담고 있는 Class인스턴스를 반환 |
| public int hashCode() | 객체 자신의 해시코드를 반환 |
| public String toString() | 객체 자신의 정보를 문자열로 반환 |
| public void notify() | 객체 자신을 사용하려고 기다리는 쓰레드를 하나만 깨웁니다 |
| public void notifyAll() | 객체 자신을 사용하려고 기다리는 모든 쓰레드를 깨웁니다 |
| public void wait() public void wait(long timeout) public void wait(long timeout, int nanos) |
다른 쓰레드가 notify()나 notifyAll()을 호출할 때까지 현재 쓰레드를 무한히 또는 지정된 시간동안 기다리게 합니다. |
Object클래스는 멤버 변수는 없고 오직 11개의 메서드만 가지고 있습니다. 이 메서드들은 모든 인스턴스가 가져야할 기본적인 것들입니다.
그 중에서 중요한 몇 가지만 살펴보겠습니다.
equals(Object obj)
매개변수로 객체의 참조 변수를 받아서 비교하여 그 결과를 boolean값으로 알려주는 역할을 합니다.
public boolean equals(Object obj) {
return (this == obj);
}
위 코드에서 알 수 있듯이 두 객체의 같고 다름을 참조변수의 값으로 판단합니다. 즉, 두 참조변수에 저장된 값(주소값)이 같은지를 판단하기 때문에 서로 다른 두 객체를 equals메서드로 비교하면 항상 false를 결과로 얻게 됩니다.
class Person {
long id;
Person(long id) {
this.id = id;
}
public boolean equals(Object obj) {
if (obj instanceof Person p)
return id == p.id;
return false;
}
}
만약 인스턴스가 갖고 있는 값이 같은지 비교하기 위해서는 위와 같이 equals메서드를 오버라이딩하여 주소가 아닌 객체에 저장된 내용을 비교하도록 변경해야 합니다.
String클래스 역시 Object클래스의 equals()를 그대로 사용하는 것이 아니라 이처럼 오버라이딩을 통해서 String인스턴스가 갖는 문자열의 내용을 비교하도록 되어있습니다.
String뿐만 아니라, Date, File, wrapper클래스의 equals()도 주소값이 아닌 내용을 비교하도록 오버라이딩되어 있습니다.
참고로, StringBuilder는 문자열 버퍼로 설계되어 equals() 메서드가 오버라이딩되어 있지 않습니다.
hashCode()
이 메서드는 해싱(hashing)기법에 사용되는 '해시 함수(hash function)'를 구현한 것입니다. 해싱은 자료 구조중 하나로 다량의 데이터를 저장하고 검색하는 데 유용합니다. 해시 함수는 찾고자하는 값을 입력하면, 그 값이 저장된 위치를 알려주는 해시 코드(hash code)를 반환합니다.
일반적으로 해시 코드가 같은 두 객체가 존재하는 것이 가능하지만, Object클래스에 정의된 hashCode()는 객체의 주소값으로 해시 코드를 만들어 반환하기 때문에 32bit JVM에서는 4byte 주소값을 가지기 때문에 서로 다른 두 객체는 결코 같은 해시 코드를 가질 수 없었지만, 64bit JVM에서는 8byte 주소값으로 해시 코드(int: 4byte)를 만들기 때문에 해시 코드가 중복될 수 있습니다.
앞에서 살펴본 것과 같이 두 인스턴스를 값으로 비교해야할 때, equals() 뿐만아니라 hashCode()도 적절히 오버라이딩해줘야 합니다.
class Person {
String name;
Person(String name) { this.name = name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person p = (Person) o;
return name.equals(p.name);
}
@Override
public int hashCode() {
return name.hashCode(); // equals와 동일한 필드 사용
}
}
그 이유는 HashMap과 HashSet의 경우 다음과 같은 순서로 동작하기 때문입니다.
- hashCode()로 객체가 들어갈 버킷(bucket) 위치를 계산
- 동일한 버킷에 여러 객체가 있으면 equals()로 동등성 비교
그래서 Object클래스의 hashCode() 문서를 보면 다음과 같이 명시되어 있습니다.
If two objects are equal according to the equals method, then calling the hashCode method on each of the two objects must produce the same integer result.
즉, equals()가 true면 hashCode()도 반드시 같아야 한다는 의미입니다.
하지만 반대로 hashCode()가 같다고 해서 equals()가 true일 필요는 없습니다. (hashCode 충돌 허용)
그리고 `System.identityHashCode(Object obj)` 메서드를 사용하면 오버라이딩된 hashCode()를 무시하고, 객체의 고유한 식별 값(주소값)을 기반으로 해시를 계산합니다.
toString()
이 메서드는 인스턴스의 정보를 문자열(String)로 제공할 목적으로 정의한 것입니다. 인스턴스의 정보를 제공한다는 것은 대부분의 경우 인스턴스 변수에 저장된 값들을 문자열로 표현한다는 뜻입니다.
Object클래스에 정의된 toString()은 아래와 같습니다.
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
클래스를 작성할 때 toString()을 오버라이딩하지 않는다면, 위와 같은 내용이 그대로 실행될 것입니다. 즉, toString()을 호출하면 클래스 이름에 16진수의 해시 코드를 얻습니다.
그러나 일반적으로 toString()은 인스턴스나 클래스에 대한 정보 또는 인스턴스 변수들의 값을 문자열로 변환하여 반환하도록 오버라이딩하는 것이 보통입니다.
@Override
public String toString() {
return "id : " + id + ", name : " + name;
}
clone()
이 메서드는 자신을 복제하여 새로운 인스턴스를 생성하는 일을 합니다. 어떤 인스턴스에 대해 작업을 할 때, 원래의 인스턴스는 보존하고 clone메서드를 이용해서 새로운 인스턴스를 생성하여 작업을 하면 작업이전의 값이 보존되므로 작업에 실패해서 원래의 상태로 되돌리거나 변경되기 전의 값을 참고하는데 도움이 될 것입니다.
Object클래스에 정의된 clone()은 단순히 인스턴스 변수의 값만 복사하기 때문에 참조 타입의 인스턴스 변수가 있는 클래스는 완전한 복제가 이루어지지 않습니다. 이러한 복사를 '얕은 복사(shallow copy)'라고 합니다.
public interface Cloneable {
// 빈 인터페이스
}
protected native Object clone() throws CloneNotSupportedException;
Object의 clone()을 사용하려면, 먼저 복제할 클래스가 Cloneable인터페이스를 구현해야 하고, clone()을 오버라이딩하면서 접근 제어자를 protected에서 public으로 변경해야 합니다. 그래야만 상속 관계가 없는 클래스에서도 clone()을 호출할 수 있습니다.
Object클래스의 clone()은 Cloneable을 구현하지 않은 클래스 내에서 호출되면 예외를 발생시킵니다. 빈 인터페이스인 Cloneable을 구현하는 이유는 클래스 작성자가 복제를 허용한다는 의미입니다.
공변 반환타입
JDK 5부터 '공변 반환타입(covariant return type)'이라는 것이 추가되었는데, 이 기능은 오버라이딩할 때 조상 메서드의 반환타입을 자손 타입으로 변경을 허용하는 것입니다.
@Override
public Person clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {}
return (Person)obj;
}
이처럼 '공변 반환타입'을 사용하면, 조상의 타입이 아닌, 실제로 반환되는 자손 객체의 타입으로 반환할 수 있어서 번거로운 형변환이 줄어든다는 장점이 있습니다.
얕은 복사와 깊은 복사
clone()은 단순히 객체에 저장된 값을 그대로 복제할 뿐, 객체가 참조하고 있는 객체까지 복제하지는 않습니다. 예를 들어, 기본형 배열인 경우에는 아무런 문제가 없지만, 객체 배열을 clone()으로 복제하는 경우에는 원본과 복제본이 같은 객체를 공유하므로 완전한 복제라고 보기 어렵습니다. 이러한 복제를 '얕은 복사(shallow copy)'라고 합니다. 얕은 복사에서는 원본을 변경하면 복사본도 영향을 받습니다.
int[] arr1 = {1, 2, 3, 4, 5};
Person[] arr2 = {new Person("Alice"), new Person("Bob")};
int[] arr1Clone = arr1.clone();
Person[] arr2Clone = arr2.clone();

객체 배열에 저장된 값은 참조하고 있는 객체의 주소값이기 때문에, 해당 주소값이 그대로 복제되어 같은 객체를 공유하게 됩니다.
반면에 원본이 참조하고 있는 객체까지 복제하는 것을 '깊은 복사(deep copy)'라고 하며, 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 원본의 변경이 복사본에 영향을 미치지 않습니다. 자바에서는 깊은 복사에 대한 내장 메서드를 지원하지 않기 때문에 직접 구현하거나 라이브러리를 사용해야 합니다.
getClass()
이 메서드는 자신이 속한 클래스의 Class객체를 반환하는 메서드인데, Class객체는 이름이 'Class'인 클래스의 객체입니다.
Class클래스는 아래와 같이 정의되어 있습니다.
public final class Class<T> implements ... {
...
}
Class객체는 클래스의 모든 정보를 담고 있으며, 클래스 당 1개만 존재합니다. 그리고 클래스 파일이 '클래스 로더(ClassLoader)'에 의해서 메모리에 올라갈 때, 자동으로 생성됩니다.
클래스 로더는 실행 시에 필요한 클래스를 동적으로 메모리에 올리는 역할을 하는데, 먼저 기존에 생성된 클래스 객체가 메모리에 존재하는지 확인하고, 있으면 객체의 참조를 반환하고 없으면 클래스 패스(classpath)에 지정된 경로를 따라서 클래스 파일을 찾습니다.
만약 클래스 파일을 못 찾으면 ClassNotFoundException이 발생하고, 파일을 찾으면 해당 클래스 파일을 읽어서 Class객체를 생성한 후 JVM 메모리의 메서드 영역(Metaspace)에 로드합니다.

그러니까 파일 형태로 저장되어 있는 클래스를 읽어서 Class클래스에 정의된 형식으로 변환하는 것입니다. 즉, 클래스 파일을 읽어서 사용하기 편한 형태로 저장해 놓은 것이 Class객체이고, 이 객체에는 클래스의 모든 정보가 담겨있습니다.
Class객체를 얻는 방법
클래스의 정보가 필요할 때, 먼저 Class객체애 대한 참조를 얻어 와야 하는데, 해당 Class객체에 대한 참조를 얻는 방법은 여러가지가 있습니다.
- Class<Person> clazz = new Person().getClass(); // 생성된 객체로 부터
- Class<Person> clazz = Person.class; // 클래스 리터럴로 부터
- Class<Person> clazz = Class.forName("Person"); // 클래스 이름으로 부터
특히 forName()은 특정 클래스 파일, 예를 들어 데이터베이스 드라이버를 메모리에 올릴 때 주로 사용합니다. 문자열(클래스명)만 바꾸면 코드 변경없이 다른 클래스의 객체를 생성할 수 있다는 점이 꽤 유용합니다.
Class객체를 이용하면 클래스에 정의된 멤버의 이름이나 개수 등, 클래스에 대한 모든 정보를 얻을 수 있기 때문에 Class객체를 통해서 객체를 생성하고 메서드를 호출하는 등 보다 동적인 코드를 작성할 수 있습니다.
Person p1 = new Person(); // new 연산자를 이용한 객체 생성
Person p2 = Person.class.newInstance(); // Class객체를 이용한 객체 생성
동적으로 객체를 생성하고 메서드를 호출하는 방법에 대해 알고 싶다면, '리플렉션 API(reflection API)'에 대해 알아보면 됩니다.
String 클래스
기존의 다른 언어에서는 문자열을 char형의 배열로 다루었으나 자바에서는 문자열을 위한 별도의 클래스를 제공합니다. 그것이 바로 String클래스인데, String클래스는 문자열을 저장하고 이를 다루는데 필요한 메서드를 함께 제공합니다.
불변(immutable) 객체
String클래스에는 문자열을 저장하기 위해서 문자형 배열 참조변수(char[ ]) value를 인스턴스 변수로 선언해 놓고 있습니다. 인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 인스턴스 변수(value)에 문자형 배열(char[ ])로 저장됩니다.
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence, ... {
private final char[] value;
}
JDK 9부터 메모리 절약을 위해 char[ ] 대신 byte[ ]을 사용합니다.
한 번 생성된 String인스턴스의 문자열은 불변(immutable)이기 때문에 읽어 올 수만 있고, 변경할 수는 없습니다.
String a = "a";
String b = "b";
a = a + b; // "ab"
위 코드와 같이 '+' 연산자를 사용해서 문자열을 결합하는 경우 인스턴스 내의 문자열이 바뀌는 것이 아니라, 새로운 문자열 "ab"가 담긴 String인스턴스가 생성되는 것입니다.
이처럼 덧셈 기호(+)를 이용해서 문자열을 결합하는 것은 매 연산 시마다 새로운 문자열을 가진 String인스턴스가 생성되어 메모리 공간을 차지하게 되므로 가능하면 결합 횟수를 최소화해야 합니다. 그래서 문자열간의 결합이나 추출 등 문자열을 다루는 작업이 많이 필요한 경우에는 String클래스 대신 StringBuffer클래스를 사용하는 것이 좋습니다. StringBuffer인스턴스에 저장된 문자열은 변경이 가능하므로 하나의 StringBuffer 인스턴스만으로도 문자열을 다루는 것이 가능합니다.
문자열 생성 및 비교
문자열을 만들 때는 다음 두 가지 방법이 있습니다.
- String str = "Hello"; // 문자열 리터럴로 지정
- String str = new String("Hello"); // String클래스의 생성자
생성자로 생성하면 new연산자에 의해서 메모리 할당이 이루어지기 때문에 항상 새로운 String인스턴스가 생성됩니다. 반면에 문자열 리터럴은 이미 존재하는 것을 재사용하는 것입니다. 문자열은 불변(immutable)이기 때문에 여러 곳에서 공유해도 아무런 문제가 없습니다.
문자열 리터럴은 컴파일 시에 클래스 파일에 저장되는데, 이 때 같은 내용의 문자열 리터럴은 한 번만 저장됩니다. 클래스 파일에는 소스 파일에 포함된 모든 리터럴 목록이 있는데 해당 클래스 파일이 클래스 로더에 의해 메모리에 올라갈 때, 이 리터럴의 목록에 있는 리터럴들이 JVM내에 있는 '상수 저장소(constant pool)'에 저장됩니다.

equals() 메서드로 비교할 때는 두 경우 모두 문자열의 내용을 비교하기 때문에 true를 얻지만, 각 String인스턴스의 주소를 등가비교연산자(==)로 비교하면 결과가 달라집니다.
빈 문자열(empty string)
자바는 길이가 0인 배열을 선언할 수 있습니다. char배열도 길이가 0인 배열을 생성할 수 있고, 이 배열을 내부적으로 가지고 있는 문자열이 바로 빈 문자열입니다.
String str = "";
위와 같은 문장이 있을 때, 참조변수 str이 참조하고 있는 String인스턴스는 내부에 'new char[0]' 과 같이 길이가 0인 char배열을 저장하고 있는 것입니다.
char[] chArr = new char[0];
int[] iArr = {};
길이가 0이기 때문에 아무런 문자도 저장할 수 없는 배열이라 무의미하게 느껴질 수도 있지만, 이러한 표현도 가능합니다.
그러나 `char ch = '';` 와 같은 표현은 불가능합니다. char형 변수에는 반드시 하나의 문자를 지정해야 합니다.
C 언어에서는 char배열의 문자열이 끝났음을 표시하기 위해 널 문자('\0')를 사용하여 유효한 문자열의 범위를 구분합니다.
반면, 자바의 String클래스는 내부적으로 문자 배열(char[])과 문자열의 길이 정보를 함께 저장하여, 널 문자 없이도 유효한 문자열의 길이를 관리합니다.
일반적으로 변수를 선언할 때, 각 타입의 기본값으로 초기화를 하지만 String은 참조형 타입의 기본값인 null 보다는 빈 문자열로, char형은 기본값인 '\u0000' 대신 공백으로 초기화하는 것이 보통입니다.

'\u0000'은 유니코드의 첫 번째 문자로써 아무런 문자도 지정되지 않은 빈 문자로, 공백 문자(' ')와는 다릅니다.
trim()과 strip()
trim()과 strip()은 문자열 좌우 공백을 제거할 때 사용하는 것은 같지만, 제거하는 공백의 종류가 다릅니다. trim()은 유니코드 이전에 주로 사용하던 아스키(ASCII)에서 정의한 공백(white space)인 공백 문자(' ')와 탭 문자('/t'), 개행 문자('/n') 등만 제거하지만, strip()은 유니코드에서 추가적으로 정의된 공백도 제거합니다.
그리고 문자열의 왼쪽 공백만 제거할 수 있는 stripLeading()과 오른쪽 공백만 제거할 수 있는 stripTrailing()도 JDK 11부터 추가되었습니다. 문자열이 여러 행일 경우, 모든 행의 공백을 제거하는 stripIndent()는 JDK 15부터 추가되었습니다.
join()과 StringJoiner
join()은 여러 문자열 사이에 구분자를 넣어서 결합합니다. 구분자로 문자열을 자르는 split()과 반대의 작업을 한다고 생각하면 됩니다.
String[] arr = {A, B, C};
String str = String.join(", ", arr); // A, B, C
join() 메서드는 내부적으로 StringJoiner를 사용하는 헬퍼 메서드로, StringJoiner를 직접 사용하면 접두사, 접미사를 설정하는 등의 더 세밀한 설정이 가능합니다.
유니코드 보충문자
유니코드는 원래 2byte, 즉 16비트 문자체계인데, 이걸로도 모자라서 20비트로 확장하게 되었습니다. 그래서 하나의 문자를 char타입으로 다루지 못하고, int타입으로 다룰 수밖에 없습니다. 확장에 의해 새로 추가된 문자들을 '보충 문자(supplementary charater)'라고 하는데, String클래스의 메서드 중에서는 보충 문자를 지원하는 것이 있고 지원하지 않는 것도 있습니다.
매개변수가 'int ch'인 것들은 보충 문자를 지원하는 것이고, 'char ch'인 것들은 지원하지 않는 것들입니다.
보충 문자에 대한 추가적인 내용은 [Java 21] (2) - variable 에서 다뤘습니다.
문자 인코딩 변환
getBytes(String charsetName)를 사용하면, 문자열의 문자 인코딩을 다른 인코딩으로 변경할 수 있습니다. 자바는 처음부터 기본 인코딩으로 UTF-16을 사용해왔는데, JDK 18부터 UTF-8로 바뀌었습니다. UTF-8은 한글 한 글자를 3byte로 표현하고, UTF-16은 한글 한 글자를 2byte로 표현합니다.
String str1 = "가"; // 기본 UTF-8
byte[] bArr = str.getBytes("CP949");
String str2 = new String(bArr, "CP949");
문자 인코딩 CP949는 EUC-KR의 확장 인코딩으로 한글 윈도우의 기본 인코딩이며, MS949라고도 합니다.
String.format()과 formatted()
format()은 형식화된 문자열을 만들어내는 간단한 방법으로, printf()하고 사용법이 완전히 똑같습니다.
// String.format()
String str = String.format("%d 더하기 %d는 %d입니다.", 3, 5, 8);
System.out.println(str);
// formatted()
String str = "%d 더하기 %d는 %d입니다.".formatted(3, 5, 8);
System.out.println(str);
String과 기본형 변환
기본형 -> String
숫자에 빈 문자열""을 더해주거나, valueOf()를 사용할 수 있습니다.
int i = 100;
String str1 = i + "";
String str2 = String.valueOf(i);
String -> 기본형
parseXxx()를 사용하거나, valueOf()를 사용할 수 있습니다. 예전에는 parseXxx()를 많이 사용했지만, 메서드의 이름을 통일하기 위해서 valueOf()가 나중에 추가되었습니다. valueOf()도 내부적으로는 parseXxx()를 호출하기 때문에 동일합니다. parseXxx()와 달리, valueOf()는 반환타입이 래퍼클래스이지만 오토박싱(auto-boxing)에 의해 기본형으로 자동 변환됩니다.
int i = Integer.parseInt("100");
int i2 = Integer.valueOf("100");
컴팩트 문자열(compact string)
앞서 말했듯이 예전에는 내부적으로 char배열을 갖고 있었으나, JDK 9부터는 보다 효율적인 메모리 사용을 위해 byte배열로 바뀌었으며, 이를 '컴팩트 문자열(compact string)'이라고 합니다.
자바는 고정길이 인코딩인 UTF-16을 오랫동안 사용해왔는데, 모든 문자가 2byte이기 때문에 문자를 다루기 쉽지만 영문자와 숫자 등의 1byte 문자들도 모두 2byte를 차지하는 저장공간에 저장되는 비효율이 있었습니다. 그래서 모든 문자가 1byte로 표현가능한 경우, 즉 Latin-1(ISO-8859-1) 인코딩으로 이루어져 있는 경우 1byte로 저장되게 바뀌어 메모리도 절약하고 성능도 향상되었습니다.
문자를 1byte로 저장할지 2byte로 저장할지는 JVM이 알아서 결정하므로 사용할 때 전혀 신경쓸 필요는 없습니다.
StringBuffer와 StringBuilder
StringBuffer도 String처럼 문자열을 다르기 위한 클래스이므로 문자열을 저장하기 위한 char배열을 인스턴스 변수로 선언해 놓고 있습니다. JDK 5부터 AbstractStringBuilder가 StringBuffer의 조상으로 추가되었으며, 아래의 코드에서 알 수 있듯이 char배열은 이 클래스로 옮겨졌습니다.
StringBuffer클래스도 String클래스와 동일하게 JDK 9부터는 byte배열로 변경되었습니다.
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
byte[] value;
다만 불변(immutable)인 String과 달리 StringBuffer는 가변(mutable)이라서 char배열의 내용을 변경할 수 있습니다. 그래서 StringBuffer는 char배열의 내용을 변경하는 메서드가 많습니다. String도 문자열(char배열)을 변경하는 메서드가 있지만, 기존의 문자열이 변경되는 것이 아닌 항상 새로운 문자열을 저장한 String 인스턴스가 반환됩니다. 반면 StringBuffer는 문자열의 내용을 변경할 수 있으므로 새로운 StringBuffer가 반환되지 않습니다.
StringBuffer의 생성자
StringBuffer클래스의 인스턴스를 생성할 때, 적절한 길이의 char배열이 생성되고, 이 배열은 문자열을 저장하고 편집하기 위한 공간(buffer)으로 사용됩니다. StringBuffer인스턴스를 생성할 때, 버퍼의 크기를 지정해주지 않으면 16개의 문자를 저장할 수 있는 크기의 버퍼를 생성합니다. 일반적으로 생성자 `StringBuffer(int capacity)`를 이용해서 저장될 문자열의 길이보다 여유있는 크기로 지정하는 것이 좋습니다.
public StringBuffer(int capacity) {
super(capacity);
}
public StringBuffer() { // 기본값 16
super(16);
}
public StringBuffer(String str) { // 지정한 문자열의 길이보다 16 더 크게 생성
super(str.length() + 16);
append(str);
}
StringBuffer를 생성할 때 버퍼의 크기가 결정되지만, 저장공간이 부족하면 자동으로 늘어납니다.
StringBuffer의 변경
append() 메서드를 사용하여 내용을 변경하는데, append()는 반환타입이 StringBuffer인데 자신의 주소(this)를 반환합니다. 그래서 하나의 StringBuffer인스턴스에 대해 아래와 같이 연속적으로 append()를 호출하는 것이 가능합니다.
StringBuffer sb = new StringBuffer("abc");
sb.append("123").append("efg");
StringBuffer의 비교
String은 equals메서드를 오버라이딩해서 문자열의 내용을 비교하도록 구현되어 있지만, StringBuffer는 equals메서드를 오버라이딩하지 않아서 StringBuffer의 equals메서드를 사용해도 등가 비교 연산자(==)로 비교한 것과 같은 결과를 얻습니다.
반면에 toString()은 오버라이딩되어 있어서 StringBuffer인스턴스에 toString()을 호출하면, 담고있는 문자열을 String으로 반환합니다. 그래서 StringBuffer인스턴스에 담긴 문자열을 비교하기 위해서는 StringBuffer인스턴스에 toString()을 호출해서 String인스턴스를 얻은 다음에 equals()메서드를 사용해서 비교해야 합니다.
StringBuilder
StringBuffer는 멀티쓰레드에 안전(thread-safe)하도록 동기화되어 있습니다. 그래서 멀티쓰레드로 작성된 프로그램이 아닌 경우, StringBuffer의 동기화는 불필요하게 성능만 떨어뜨리게 됩니다.
그래서 StringBuffer에서 쓰레드의 동기화만 뺀 StringBuilder가 새로 추가되었습니다. StringBuilder는 StringBuffer와 완전히 똑같은 기능으로 작성되어 있어서, 소스 코드에서 StringBuffer를 StringBuilder로 바꾸기만 하면 됩니다.
Math 클래스
Math클래스는 기본적인 수학계산에 유용한 메서드로 구성되어 있습니다. Math클래스의 생성자는 접근 제어자가 private이기 때문에 다른 클래스에서 Math인스턴스를 생성할 수 없도록 되어있습니다. 그 이유는 클래스 내에 인스턴스 변수가 하나도 없어서 인스턴스를 생성할 필요가 없기 때문입니다. Math클래스의 메서드는 모두 static이며, 이래와 같이 3개의 상수를 제공합니다.
public static final double E = 2.7182818284590452354;
public static final double PI = 3.14159265358979323846;
public static final double TAU = 2.0 * PI; // JDK 21부터 추가
올림, 버림, 반올림
올림
Math.ceil(3.7) // 4.0
Math.ceil(-3.7) // -3.0
버림
Math.floor(3.7) // 3.0
Math.floor(-3.7) // -4.0
반올림
소수점 n번째 자리에서 반올림한 값을 얻기 위해서는 round()를 사용해야 하는데, 이 메서드는 항상 소수점 첫째자리에서 반올림을 해서 정수값(long)을 결과로 돌려줍니다. 따라서 원하는 자리 수에서 반올림된 값을 얻기 위해서는 10^n 을 곱한 후 반올림을 하고, 다시 10^n 으로 나눠주면 됩니다.
// 소수점 셋째자리 반올림
double val = 90.7552;
double result = Math.round(val * 100)/100.0; // 90.76
rint() 역시 반올림을 구하는 메서드지만, 정확히 .5 일 때 결과값이 짝수가 되도록 반올림을 수행합니다. (Banker's rounding)
ex. Math.rint(3.7) -> 4.0
Math.rint(2.7) -> 2.0
예외를 발생시키는 메서드
메서드 이름에 'Exact'가 포함된 메서드들이 JDK 8부터 새로 추가되었습니다. 이들은 정수형간의 연산에서 발생할 수 있는 오버플로우(overflow)를 감지하기 위한 것입니다.
| int addExact(int x, int y) | x + y |
| int subtractExact(int x, int y) | x - y |
| int muliplyExact(int x, int y) | x * y |
| int incrementExact(int a) | a++ |
| int decrementExact(int a) | a-- |
| int negateExact(int a) | -a |
| int toIntExact(long value) | (int)value |
JDK 18부터 divideExact(), ceilDivExact(), floorDivExact()가 더 추가되었습니다.
연산자는 단지 결과를 반환할 뿐, 오버플로우의 발생여부에 대해 알려주지 않습니다. 그러나 위의 메서드들은 오버플로우가 발생하면, 예외(ArithmeticException)을 발생시킵니다.
int의 범위는 [-2,147,483,648 ~ 2,147,483,647]이기 때문에 negateExact(int a)에서도 오버플로우가 발생할 수 있습니다.
삼각함수와 지수, 로그
Math클래스에는 이름에서 알 수 있듯이 수학 관련 메서드들이 많이 있습니다. 보다 자세한 내용은 Java API를 참고하고, 몇 가지 자주 사용되는 메서드들의 사용방법만 확인해보겠습니다.

삼각함수는 매개변수의 단위가 '라디안(radian)'이므로, 45도를 '라디안(radian)'단위의 값으로 변환해서 사용해야 합니다. (180° = π rad)
StrictMath 클래스
Math클래스는 최대한의 성능을 얻기 위해 JVM이 설치된 OS의 메서드(native method)를 호출해서 사용합니다. 즉, OS에 의존적인 계산을 하고 있는 것입니다. 예를 들어 부동소수점 계산의 경우, 반올림의 처리방법 설정이 OS마다 다를 수 있기 때문에 자바로 작성된 프로그램임에도 불구하고 컴퓨터마다 결과가 다를 수 있습니다.
이러한 차이를 없애기 위해 성능은 다소 포기하는 대신, 어떤 OS에서 실행되어도 항상 같은 결과를 얻도록 Math클래스를 새로 작성한 것이 StrictMath클래스입니다.
래퍼(wrapper) 클래스
객체지향 개념에서 모든 것은 객체이어야 합니다. 그러나 자바에서는 8개의 기본형을 객체로 다루지 않는데, 이것이 바로 자바가 완전한 객체지향 언어가 아니라는 얘기를 듣는 이유입니다. 그 대신 보다 높은 성능을 얻을 수 있었습니다.
때로는 기본형(primitive type) 값도 어쩔 수 없이 객체로 다뤄야 하는 경우가 있습니다. 예를 들면, 매개변수로 객체를 요구할 때, 기본형 값이 아닌 객체로 저장해야할 때, 객체간의 비교가 필요할 때 등등의 경우에는 기본형 값들을 객체로 변환하여 작업을 수행해야 합니다. 이 때 사용되는 것이 래퍼(wrapper)클래스이며, 8개의 기본형을 대신할 수 있는 8개의 래퍼 클래스가 있습니다.
아래는 int형의 래퍼 클래스인 Integer클래스의 소스 코드 일부입니다.
public final class Integer extends Number implements Comparable {
...
private final int value;
...
}
이처럼 래퍼 클래스들은 내부에 기본형 값을 저장하고 있으며, 이 값은 문자열처럼 한 번 저장하면 변경할 수 없는 불변(immutable)입니다.
Integer i = new Integer(10); // 생성자(deprecated)
Integer i = Integer.valueOf(10); // 팩토리 메서드
래퍼 클래스의 객체를 생성하는 방법은 위와 같이 2가지가 있습니다. 그러나 JDK 9부터 생성자로 생성하는 것은 권장하지 않으므로, valueOf()라는 팩토리 메서드로 생성하는 것이 바람직합니다.
팩토리 메서드
객체를 생성해서 반환하는 메서드를 팩토리 메서드라고 하며, 객체를 생성하지 않고 호출할 수 있어야 하므로 static메서드입니다. 그래서 '정적 팩토리 메서드(static factory method)'라고도 합니다.
다음 코드는 Integer클래스의 팩토리 메서드인 valueOf()입니다.
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer객체 배열(캐시)에 `low(-128) ~ high(127)`범위의 Integer객체를 미리 생성해서 채워놓습니다. 그리고 이 범위 안의 값을 가진 Integer객체는 캐시에서 꺼내서 반환하고, 범위를 벗어난 객체는 새로 생성해서 반환합니다.
IntegerCache는 Integer클래스의 private static 내부 클래스로, IntegerCache.low의 값은 -128, IntegerCache.high의 값은 127을 기본값으로 갖습니다. IntegerCache.high의 값은 `-XX:AutoBoxCacheMax=<size>` 옵션으로 변경가능합니다.
아래의 코드에서 10은 캐시 범위에 속한 값이므로 등가 비교 연산, 즉 객체의 주소 비교가 true이고, 200은 캐시 범위를 벗어나므로 늘 새로운 객체를 반환하기 때문에 등가 비교 결과가 false입니다.
Integer.valueOf(10) == Integer.valueOf(10) // true
Integer.valueOf(200) == Integer.valueOf(200) // false
래퍼 클래스들은 모두 equals()가 오버라이딩되어 있어서 주소값이 아닌 객체가 가지고 있는 값을 비교합니다. 오토박싱이 된 Integer객체에는 비교 연산자를 직접 사용할 수가 없기 때문에 compareTo()를 제공합니다. compareTo()는 두 값이 같을 때는 0, 왼쪽 값이 클 때는 양수, 왼쪽 값이 작을 때는 음수를 반환합니다.
그리고 toString()도 오버라이딩되어 있어서 객체가 가지고 있는 값을 문자열로 변환하여 반환합니다. 이 외에도 래퍼 클래스들은 MAX_VALUE, MIN_VALUE, SIZE, BYTES, TYPE 등의 static상수를 공통으로 갖고 있습니다.
Number 클래스
Number 클래스를 추상클래스로 내부적으로 숫자를 멤버변수로 갖는 래퍼 클래스들의 조상입니다. 아래의 그림은 래퍼 클래스의 상속계층도인데, 기본형 중에서 숫자와 관련된 래퍼 클래스들은 모두 Number클래스의 자손이라는 것을 알 수 있습니다.

그 외에 Number클래스의 자손으로 BigInteger와 BigDecimal 등이 있는데, BigInteger는 long으로도 다룰 수 없는 큰 범위의 정수를, BigDecimal은 double로도 다룰 수 없는 큰 범위의 부동 소수점수를 처리하기 위한 것으로 연산자의 역할을 하는 다양한 메서드를 제공합니다.
public abstract class Number implements java.io.Serializable {
public abstract int intValue();
public abstract long longValue();
public abstract float floatValue();
public abstract double doubleValue();
public byte byteValue() {
return (byte)intValue();
}
public short shortValue() {
return (short)intValue();
}
...
}
위 코드는 Number클래스의 소스 코드로, 객체가 가지고 있는 값을 기본형으로 변환하여 반환하는 메서드들을 정의하고 있습니다.
오토박싱 & 언박싱(autoboxing & unboxing)
JDK 5이전에는 기본형과 참조형 간의 연산이 불가능했기 때문에, 래퍼 클래스로 기본형을 객체로 만들어서 연산해야 했습니다. 그러나 이제는 컴파일러가 자동으로 변환하는 코드를 넣어주기 때문에 가능합니다.

이 외에도 내부적으로 객체 배열을 가지고 있는 Vector클래스나 ArrayList클래스에 기본형 값을 저장해야할 때나, 형변환이 필요할 때도 컴파일러가 자동적으로 코드를 추가해 줍니다. 기본형 값을 래퍼 클래스의 객체로 자동 변환해주는 것을 '오토박싱(autoboxing)'이라고 하고, 반대로 변환하는 것을 '언박싱(unboxing)'이라고 합니다.
다음은 오토박싱의 몇가지 예시입니다.
int i = 10;
Integer intg = (Integer)i; // Integer.valueOf(i);
Object obg = (Object)i; // (Object)Integer.valueOf(i);
Long lng = 100L; // Long.valueOf(100L);
util classes
java.util 패키지에 많은 수의 클래스가 있지만 실제로 자주 쓰이는 것들은 그렇게 많지 않기 때문에 자주 사용되는 중요한 클래스들만 살펴보겠습니다.
Objects
Object클래스의 보조 클래스로 Math클래스처럼 모든 메서드가 'static'입니다. 객체의 비교나 널 체크(null check)에 유용합니다.
static import를 사용할 때 Object클래스와 메서드명이 같은 것은 충돌이 나기 때문에, 클래스 이름을 명시적으로 붙여줘야 합니다.
isNull(), nonNull()
isNull()은 해당 객체가 널인지 확인해서 null이면 true를 반환하고 아니면 false를 반환합니다. nonNull()은 isNull()과 정반대의 일을 합니다.
public static boolean isNull(Object obj) {
return obj == null;
}
public static boolean nonNull(Object obj) {
return obj != null;
}
requireNonNull()
requireNonNull()은 해당 객체가 널이 아니어야 하는 경우에 사용합니다. 만일 객체가 null이면, NullPointerException을 발생시킵니다. 두 번째 매개변수로 지정하는 문자열은 예외의 메시지가 됩니다.
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
public static <T> T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}
compare()
Object클래스에는 없던 대소 비교를 위한 메서드인 compare()는 두 비교대상이 같으면 0, 왼쪽이 크면 양수, 작으면 음수를 반환합니다.
public static <T> int compare(T a, T b, Comparator<? super T> c) {
return (a == b) ? 0 : c.compare(a, b);
}
equals(), deepEquals()
Object클래스에 있는 equals()와 달리, Objects클래스의 equals()는 null 체크를 따로 하지 않아도 된다는 장점이 있습니다.
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
deepEquals()는 객체를 재귀적으로 비교하기 때문에 다치원 배열의 비교도 가능합니다.
public static boolean deepEquals(Object a, Object b) {
if (a == b)
return true;
else if (a == null || b == null)
return false;
else
return Arrays.deepEquals0(a, b);
}
hashCode(), hash()
hashCode() 역시 내부적으로 null 체크를 한 후에 Object클래스의 hashCode()를 호출합니다. 단, 널일 때는 0을 반환합니다.
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
보통은 클래스에 선언된 인스턴스의 변수들의 hashCode()를 조합해서 반환하도록, hashCode()를 오버라이딩하는데, 그 대신 매개변수의 타입이 가변인자인 hash()를 사용하면 편리합니다.
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
다음은 equals() & hashCode()를 오버라이딩한 클래스의 예시입니다.
class Person {
String name;
int age;
public boolean equals(Object obj) {
if(obj instanceof Person p) {
return name.equals(p.name) && age == p.age;
}
}
public int hashCode() {
return Objects.hash(name, age);
}
}
Random
Math.random()을 사용하는 방법 외에도 Random클래스를 사용하면 난수를 얻을 수 있습니다. 사실 Math.random()은 내부적으로 Random클래스의 인스턴스를 생성해서 사용하는 것이므로 둘 중에서 편한 것을 사용하면 됩니다.
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}
Math.random()과 Random의 가장 큰 차이점이라면, 종자값(seed)을 설정할 수 있다는 것입니다.
Random의 생성자와 메서드
생성자 Random()은 아래와 같이 System.nanoTime()을 사용하여 설정하기 때문에 실행할 때마다 얻는 난수가 달라집니다.
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
// subclass might have overridden setSeed
this.seed = new AtomicLong();
setSeed(seed);
}
}
종자값이 같은 Random인스턴스들은 항상 같은 난수를 같은 순서대로 반환합니다.
정규식(regular expression) - regex
정규식이란 텍스트 데이터 중에서 원하는 조건(패턴, pattern)과 일치하는 문자열을 찾아 내기 위해 사용하는 것으로 미리 정의된 기호와 문자를 이용해서 작성한 문자열을 말합니다. 원래 Unix에서 사용하던 것이고 Perl의 강력한 기능이었는데, 요즘은 Java를 비롯해 많은 언어에서 지원하고 있습니다.
정규식을 이용하면 많은 양의 텍스트 파일 중에서 원하는 데이터를 손쉽게 뽑아낼 수 있고 입력된 데이터가 형식에 맞는지 체크할 수도 있습니다. Java API문서에서 java.util.regex.Pattern을 찾아보면 정규식에 사용되는 기호와 작성방법이 모두 설명되어 있지만, 이해하기가 쉽지 않습니다. 정규식을 자세히 설명하자면 책 한 권 분량이 될 정도로 광범위 하기 때문에 자주 쓰이는 정규식의 예시를 보고 학습해보겠습니다.
public static void main(String[] args) {
String[] data = {"bat", "baby", "bonus", "cA", "ca", "co", "c.", "c0",
"car", "combat", "count", "date", "disc"};
Pattern p = Pattern.compile("c[a-z]*");
for (int i = 0; i < data.length; i++) {
Matcher m = p.matcher(data[i]);
if (m.matches())
System.out.print(data[i] + ",");
}
}
data라는 문자열 배열에 담긴 문자열 중에서 지정한 정규식과 일치하는 문자열을 출력하는 예제입니다. Pattern은 정규식을 정의하는데 사용되고, Matcher는 정규식(패턴)을 데이터와 비교하는 역할을 합니다. 정규식을 정의하고 데이터를 비교하는 과정을 단계별로 설명하면 다음과 같습니다.
- 정규식을 매개변수로 Pattern클래스의 static메서드인 `Pattern compile(String regex)`를 호출하여 Pattern인스턴스를 얻습니다.
- 정규식으로 비교할 대상을 매개변수로 Pattern클래스의 `Matcher matcher(CharSequence input)`를 호출해서 Matcher인스턴스를 얻습니다.
- Matcher인스턴스에 `boolean matches()`를 호출해서 정규식에 부합하는지 확인합니다.
CharSequence는 인터페이스로, 이를 구현한 클래스는 CharBuffer, String, StringBuffer가 있습니다.
다음은 자주 쓰일 만한 패턴들을 정리한 표 입니다.
| 정규식 패턴 | 설 명 |
| c[a-z]* | c로 시작하는 영단어 |
| c[a-z] | c로 시작하는 두 자리 영단어 |
| c[a-zA-Z] | c로 시작하는 두 자리 영단어 (a~z 또는 A~Z, 대소문자 구분 X) |
| c[a-zA-Z0-9] c\w |
c로 시작하고 숫자와 영어로 조합된 두 글자 |
| .* | 모든 문자열 (.: 임의의 문자 1개) |
| c. | c로 시작하는 두 자리 문자열 |
| c.* | c로 시작하는 모든 문자열(기호 포함) |
| c\. | c와 일치하는 문자열 (.은 패턴작성에 사용되는 문자이므로 escape문자인 '\'를 사용) |
| c\d c[0-9] |
c와 숫자로 구성된 두 자리 문자열 |
| c.*t | c로 시작하고 t로 끝나는 모든 문자열 |
| [b|c].* [bc].* [b-c].* |
b 또는 c로 시작하는 문자열 |
| [^b|c].* [^bc].* [^b-c].* |
b 또는 c로 시작하지 않는 문자열 |
| .*a.* | a를 포함하는 모든 문자열 (*: 앞의 문자를 0회 이상 반복) |
| .*a.+ | a를 포함하는 모든 문자열 (+: 앞의 문자를 1회 이상 반복) |
| [b|c].{2} | b또는 c로 시작하는 세 자리 문자열 (b 또는 c 다음에 두 자리) |
escape 문자는 정규식 엔진에서 사용되는 것과 Java 문자열에서 사용되는 것이 별개이기 때문에 각각 필요합니다.
ex) 정규식 패턴: c\d
문자열 정규식 패턴: String s = "c\\d";
캡쳐 그룹(capturing group)
public static void main(String[] args) {
String source = "HP:011-1111-1111, HOME:02-999-9999 ";
String pattern = "(0\\d{1,2})-(\\d{3,4})-(\\d{4})";
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(source);
int i = 0;
while (m.find()) {
System.out.println( ++i + ": " + m.group() + " -> " + m.group(1)
+ ", " + m.group(2) + ", " + m.group(3));
}
}
정규식의 일부를 괄호( )로 나누어 묶어서 그룹화(grouping)할 수도 있습니다. 그룹화된 부분은 하나의 단위로 묶이는 셈이 되어서 한 번 또는 그 이상의 반복을 의미하는 '+' 나 '*' 가 뒤에 오면 그룹화된 부분이 적용대상이 됩니다. 그리고 그룹화된 부분은 `group(int i)`를 이용해서 나누어 얻을 수 있습니다.
group(int i)를 호출할 때 i가 실제 그룹의 수보다 많으면 IndexOutOfBoundsException이 발생합니다.
`find()`는 주어진 소스 내에서 패턴과 일치하는 부분을 찾아내면 true를 반환하고, 찾지 못하면 false를 반환합니다. find()를 호출해서 패턴과 일치하는 부분을 찾아낸 다음, 다신 find()를 호출하면 이전에 발견한 패턴과 일치하는 부분의 다음부터 다시 패턴매칭을 시작합니다.
또한 Matcher의 find()로 정규식과 일치하는 부분을 찾으면, 그 위치를 `start()`와 `end()`로 알아 낼 수 있고, `appendReplacement(StringBuffer sb, String replacement)`를 이용해서 원하는 문자열로 치환할 수도 있습니다. 이때 치환된 결과는 StringBuffer에 저장됩니다.
Scanner
Scanner는 화면, 파일, 문자열과 같은 입력소스로부터 문자데이터를 읽어오는데 도움을 줄 목적으로 JDK 5부터 추가되었습니다. Scanner에는 다음과 같은 생성자를 지원하기 때문에 다양한 입력소스로부터 데이터를 읽을 수 있습니다.
Scanner(String source)
Scanner(File source)
Scanner(InputStream source)
Scanner(Readable source)
Scanner(ReadableByteChannel source)
Scanner(Path source) // JDK 7부터
또한 Scanner는 정규식 표현(regular expression)을 이용한 라인 단위의 검색을 지원하며 구분자(delimiter)에도 정규식 표현을 쓸 수 있어서 복잡한 형태의 구분자도 처리가 가능합니다.
Scanner useDelimiter(Pattern pattern)
Scanner useDelimiter(String pattern)
JDK 6부터는 화면 입출력만 전문적으로 담당하는 java.io.Console이 새로 추가되었으나, Console은 잘 동작하지 않는 IDE도 있기 때문에 Scanner를 주로 사용했습니다. Scanner와 Console은 사용법이나 성능측면에서 거의 같기 때문에 어떤 것을 사용해도 상관없습니다.
Scanner에서는 입력받을 값이 String이 아닌 다른 기본 자료형일 경우에 사용할 수 있는 메서드를 제공함으로써, 입력받은 문자를 다시 변환하는 수고를 덜어줍니다.
Scanner sc = new Scanner(System.in);
String input = sc.nextLine();
sc.nextInt();
sc.nextLong();
sc.nextBoolean();
sc.nextByte();
sc.nextShort();
sc.nextDouble();
sc.nextFloat();
StringTokenizer
StringTokenizer는 긴 문자열을 지정된 구분자(delimiter)를 기준으로 토큰(token)이라는 여러 개의 문자열로 잘라내는 데 사용됩니다.
String str = "100,200,300,400";
String[] result = str.split(","); // regex
Scanner sc = new Scanner(str).useDelimiter(","); // regex
StringTokenizer를 이용하는 방법 외에도 위와 같이 String의 `split(String regex)`이나 Scanner의 `useDelimiter(String pattern)`을 사용할 수도 있지만, 이 두 가지 표현은 정규식 표현을 사용해야 하므로 정규식 표현에 익숙하지 않은 경우 StringTokenizer를 사용하는 것이 간단하면서 명확한 결과를 얻을 수 있습니다.
그러나 StringTokenizer는 구분자로 단 하나의 문자 밖에 사용하지 못하기 때문에 보다 특별한 형태의 구분자로 문자열을 나누어야 할 때는 어쩔 수 없이 정규식을 사용하는 메서드를 사용해야 합니다.
StringTokenizer의 생성자와 메서드
StringTokenizer의 주로 사용되는 생성자와 메서드는 다음과 같습니다.
| 생성자 / 메서드 | 설 명 |
| StringTokenizer(String str) | 문자열(str)을 기본 구분자(" \t\n\r\f")로 나누는 StringTokenizer를 생성합니다. |
| StringTokenizer(String str, String delim) | 문자열(str)을 지정된 구분자(delim)로 나누는 StringTokenizer를 생성합니다. (구분자는 토큰으로 간주되지 않습니다.) |
| StringTokenizer(String str, String delim, boolean returnDelims) |
문자열(str)을 지정된 구분자(delim)로 나누는 StringTokenizer를 생성합니다. returnDelim의 값을 true로 하면 구분자도 토큰으로 간주됩니다. |
| int countTokens() | 전체 토큰의 수를 반환합니다. |
| boolean hasMoreTokens() | 토큰이 남아있는지 알려줍니다. |
| String nextToken() | 다음 토큰을 반환합니다. |
앞서 설명했듯이 StringTokenizer는 구분자로 단 하나의 문자만을 사용하기 때문에 "+-*/=()" 전체가 하나의 구분자가 아니라 각각의 문자가 모두 구분자라는 것에 주의해야 합니다. 따라서 다음 문장에서는 구분자가 총 7개 `+, -, *, /, =, (, )`가 적용됩니다.
StringTokenizer st = new StringTokenizer(expression, "+-*/=()");
또한 split()은 빈 문자열도 토큰으로 인식하는 반면, StringTokenizer는 빈 문자열을 토큰으로 인식하지 않습니다.
이 외에도 성능의 차이가 있는데, split()은 데이터를 토큰으로 잘라낸 결과를 배열에 담아서 반환하기 때문에, 데이터를 토큰으로 바로바로 잘라서 반환하는 StringTokenizer보다 성능이 떨어질 수 밖에 없습니다. 그러나 이것은 데이터의 양이 많은 경우가 아니라면 별 문제가 되지 않습니다.
BigInteger
정수형으로 표현할 수 있는 값에는 한계가 있습니다. 가장 큰 정수형 타입인 long으로 표현할 수 있는 값은 10진수로 19자리 정도입니다. 이 값도 상당히 큰 값이지만, 과학적 계산에서는 더 큰 값을 다뤄야할 때가 있습니다. 그럴 때 사용하면 좋은 것이 BigInteger입니다.
BigInteger는 내부적으로 int배열을 사용해서 값을 다루기 때문에 long타입보다 훨씬 큰 값을 다룰 수 있는 것입니다. 대신 성능은 long타입보다 떨어질 수 밖에 없습니다.
public class BigInteger extends Number implements Comparable<BigInteger> {
final int signum; // 부호: 1, 0, -1
final int[] mag; // 값(magnitude)
...
}
위 코드에서 알 수 있듯이, BigInteger도 String처럼 불변(immutable)입니다. 그리고 모든 정수형이 그렇듯이 BigInteger 역시 값을 '2의 보수'의 형태로 표현합니다. 위 코드에서 알 수 있듯이 부호를 따로 저장하고 배열에는 값 자체만 저장합니다. 그래서 signum의 값이 -1, 즉 값이 음수인 경우에 2의 보수법에 맞게 mag의 값을 변환해서 처리합니다. 그래서 부호만 다른 두 값의 mag는 같고 signum만 다르게 저장됩니다.
BigInteger의 생성
BigInteger를 생성하는 방법은 여러 가지가 있는데, 정수형 리터럴로는 표현할 수 있는 값의 한계가 있기 때문에 문자열로 숫자를 표현하는 것이 일반적입니다.
BingInteger val;
val = new BigInteger("12345678901234567890"); // 문자열
val = new BigInteger("FFFF", 16); // n진수 문자열
val = BigInteger.valueOf(123456789L); // 숫자
다른 타입으로의 변환
BigInteger를 문자열, 또는 byte배열로 변환하는 메서드는 다음과 같습니다.
String toString() // 문자열로 변환
String toString(int radix) // n진법 문자열로 변환
byte[] tyByteArray() // byte배열로 변환
그리고 부모인 Number로부터 상속받은 기본형으로 변환하는 메서드도 갖고 있습니다.
byte byteValue()
short shortValue()
int intValue()
long longValue()
float floatValue()
double doubleValue()
정수형으로 변환하는 메서드 중에서 이름 끝에 "Exact"가 붙은 것들은 변환한 결과가 변환한 타입의 범위에 속하지 않으면 ArithmeticException을 발생시킵니다.
byte byteValueExact()
short shortValueExact()
int intValueExact()
long longValueExact()
BigInteger의 연산
BigInteger에는 정수형에 사용할 수 있는 모든 연산자와 수학적인 계산을 쉽게 해주는 메서드들이 정의되어 있습니다.
BigInteger add(BigInteger val)
BigInteger subtract(BigInteger val)
BigInteger multiply(BigInteger val)
BigInteger divide(BigInteger val)
BigInteger mod(BigInteger val)
BigInteger remainder(BigInteger val)
mod()와 remainder()는 모두 나머지를 구하는 연산이지만, mod()는 나누는 값이 음수면 ArithmeticException을 발생시킵니다.
BigInteger는 불변이므로, 반환타입이 BigInteger란 얘기는 새로운 인스턴스가 반환된다는 뜻입니다.
BigInteger도 Integer처럼 캐시 기능이 있어서, [-16 ~ 16] 범위의 값인 경우 valueOf()는 캐시된 객체를 반환합니다.
비트 연산 메서드
워낙 큰 숫자를 다루기 위한 클래스이므로, 성능을 향상시키기 위해 비트단위로 연산을 수행하는 메서드를 많이 가지고 있습니다.
| int bitCount() | 2진수로 표현했을 때, 1의 개수(음수는 0의 개수)를 반환 |
| int bitLength() | 2진수로 표현했을 때, 값을 표현하는데 필요한 bit수 |
| boolean testBit(int n) | 우측에서 n번째 index의 비트가 1이면 true, 0이면 false |
| BigInteger setBit(int n) | 우측에서 n번째 index의 비트를 1로 변경 |
| BigInteger clearBit(int n) | 우측에서 n번째 index의 비트를 0으로 변경 |
| BigInteger flipBit(int n) | 우측에서 n번째 index의 비트를 반대로 전환 (0 <-> 1) |
예를 들어 정수가 짝수인지 확인할 때, 연산 메서드를 사용하면 다음과 같습니다.
BigInteger bi = new BigInteger("4");
if (bi.remainder(new BigInteger("2")).equals(BigInteger.ZERO)) {
...
}
하지만 짝수는 제일 오른쪽 비트가 0이라는 것을 이용해, 비트 연산 메서드를 사용하면 다음과 같습니다.
BigInteger bi = new BigInteger("4");
if (!bi.testBit(0)) {
...
}
이처럼, 가능하면 산술연산 대신 비트연산으로 처리하도록 노력해야합니다.
BigDecimal
double타입으로 표현할 수 있는 값은 상당히 범위가 넓지만, 정밀도가 최대 13자리 밖에 되지 않고 실수형의 특성상 오차를 피할 수 없습니다. BigDecimal은 실수형과 달리 정수를 이용해서 실수를 표현합니다. 실수의 오차는 10진 실수를 2진 실수로 정확히 변환할 수 없는 경우가 있기 때문에 발생하는 것이므로, 실수를 오차가 없는 2진 정수로 변환하여 다루는 것입니다. 실수를 정수와 10의 제곱의 곱으로 표현하니다.

scale은 0부터 Integer.MAX_VALUE사이에 있는 값입니다. 그리고 BigDecimal은 정수를 저장하는데 BigInteger를 사용합니다.
또한 BigDecimal도 BigInteger처럼 불변(immutable)입니다.
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal; // 정수(unscaled value)
private final int scale; // 지수(scale) - 소수점 자리수
private transient int precision; // 정밀도(precision) - 유효 숫자
...
}
예를 들어 123.45는 `12345 x 10 ^ -2`로 표현할 수 있으며, 이 값이 BigDecimal에 저장되면, intVal의 값은 12345가 되고 scale의 값은 2가 됩니다. 그리고 precision의 값은 5가 되는데, 이 값은 정수의 전체 자리수를 의미합니다.
BigDecimal val = new BigDecimal("123.45");
System.out.println(val.unscaledValue()); // 12345
System.out.println(val.scale()); // 2
System.out.println(val.precision()); // 5
BigDecimal의 생성
BigDecimal을 생성하는 방법은 여러 가지가 있는데, 문자열로 숫자를 표현하는 것이 일반적입니다. 기본형 리터럴로 표현할 수 있는 값의 한계가 있기 때문입니다.
BigDecimal val;
val = new BigDecimal("123.4567890"); // 문자열
val = new BigDecimal(123.456); // double 리터럴
val = new BigDecimal(123456); // int, long 리터럴
val = BigDecimal.valueOf(123.456); // valueOf(double)
val = BigDecimal.valueOf(123456); // valueOf(int)
그리고 한 가지 주의할 점은, double타입의 값을 매개변수로 갖는 생성자를 사용하면 오차가 발생할 수 있다는 것입니다.
System.out.println(new BigDecimal(0.1)); // 0.1000000000000000055511...
System.out.println(new BigDecimal("0.1")); // 0.1
그 이유는 double형 0.1의 내부 이진 부동소수점 근사값을 그대로 받아서 그것을 십진수(BigDecimal) 로 변환하기 때문입니다.
다른 타입으로의 변환
BigDecimal을 문자열로 변환하는 메서드는 다음과 같습니다.
String toPlainString() // 항상 다른 기호없이 숫자로만 표현
String toString() // 필요하면 지수형태로 표현
대부분의 경우 이 두 메서드의 반환결과가 같지만, BigDecimal을 생성할 때 `1.0e-22`와 같은 지수형태의 리터럴을 사용했을 때 다른 결과를 얻는 경우가 있습니다.
그리고 BigDecimal도 Number로 부터 상속받은 기본형으로 변환하는 메서드를 갖고 있습니다.
byte byteValue()
short shortValue()
int intValue()
long longValue()
float floatValue()
double doubleValue()
BigDecimal을 정수형으로 변환하는 메서드 중에서 이름 끝에 'Exact'가 붙은 것들은 변환한 결과가 변환한 타입의 범위에 속하지 않으면 ArithmeticException을 발생시킵니다.
byte byteValueExact()
short shortValueExact()
int intValueExact()
long longValueExact()
BigInteger toBigIntegerExact()
BigDecimal의 연산
BigDecimal에는 실수형에 사용할 수 있는 모든 연산자와 수학적인 계산을 쉽게 해주는 메서드들이 정의되어 있습니다.
BigDecimal add(BigDecimal val)
BigDecimal subtract(BigDecimal val)
BigDecimal multiply(BigDecimal val)
BigDecimal divide(BigDecimal val)
BigDecimal mod(BigDecimal val)
BigDecimal remainder(BigDecimal val)
BigInteger와 마찬가지로 BigDecimal은 불변이므로, 반환타입이 BigDecimal인 경우 새로운 인스턴스가 반환됩니다.
한 가지 주의해야할 점은 연산결과의 정수(value), 소수점 자리수(scale), 유효숫자(precision)가 달라진다는 것입니다.

곱셈에서는 두 피연산자의 scale을 더하고, 나눗셈에서는 뺍니다.
그리고 덧셈과 뺄셈에서는 둘 중에서 scale이 큰 쪽으로 맞춰집니다.
반올림 모드 - divide()와 setScale()
다른 연산과 달리 나눗셈을 처리하기 위한 메서드는 다음과 같이 다양한 버전이 존재합니다. 나눗셈의 결과를 어떻게 반올림 처리할 것인지와, 몇 번째 자리(scale)에서 반올림할 것인지를 지정할 수 있습니다. BigDecimal이 아무리 오차없이 실수를 저장한다해도 나눗셈에서 발생하는 오차는 어쩔 수 없습니다.
BigDecimal divide(BigDecimal divisor)
BigDecimal divide(BigDecimal divisor, int roundingMode)
BigDecimal divide(BigDecimal divisor, RoundingMode roundingMode)
BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
BigDecimal divide(BigDecimal divisor, MathContext mc)
roundingMode는 반올림 처리방법에 대한 것으로 BigDecimal에 정의된 'ROUND_'로 시작하는 상수들 중에 하나를 선택해서 사용하면 됩니다. RoundingMode는 이 상수들을 열거형으로 정의한 것으로 나중에 추가되었는데, 가능하면 열거형을 사용하는 것이 좋습니다.
| 상수 | 설 명 |
| CEILING | 올림 |
| FLOOR | 내림 |
| UP | 양수일 때는 올림, 음수일 때는 내림 |
| DOWN | 양수일 때는 내림, 음수일 때는 올림 |
| HALF_UP | 반올림(5이상 올림, 5미만 버림) |
| HALF_EVEN | 반올림(Banker's rounding: 정확히 .5 일 때 결과값이 짝수가 되도록 반올림) |
| HALF_DOWN | 반올림(6이상 올림, 6미만 버림) |
| UNNECESSARY | 나눗셈의 결과가 딱 떨어지는 수가 아니면 ArithmeticException 발생 |
java.math.MathContext
MathContext는 반올림 모드와 정밀도(precision)을 하나로 묶어 놓은 것일 뿐 별다른 것은 없습니다. 한 가지 주의할 점은 일반적으로 divide()에서는 scale(소수점 자리수)을 기준으로 반올림을 하는데, MathContext를 사용하면 precision(유효 숫자)기준 반올림을한다는 것입니다.
BigDecimal bd1 = new BigDecimal("123.456");
BigDecimal bd2 = new BigDecimal("1.0");
bd1.divide(bd2, 2, HALF_UP)); // 123.46
bd1.divide(bd2, new MathContext(2, HALF_UP))); // 1.2E+2
그래서 위 결과를 보면, 일반적인 divide는 2번째 소수점 자리수까지 나오도록 반올림을 한 반면, MathContext를 사용한 divide는 유효 숫자가 2자리가 되도록 반올림을 하여 `120`이 되고 여기에 scale을 반영하면 `1.2E+2`가 됩니다.
scale의 변경
BigDecimal을 10으로 곱하거나 나누는 대신 scale의 값을 변경함으로써 같은 결과를 얻을 수 있습니다. BigDecimal의 scale을 변경하려면 setScale()을 이용하면 됩니다.
BigDecimal setScale(int newScale)
BigDecimal setScale(int newScale, int roundingMode)
BigDecimal setScale(int newScale, RoundingMode roundingMode)
setScale()로 scale의 값을 변경하는 것은 단순히 소수점을 옮기는 연산이 아니라, 지정한 scale에 맞춰 값을 반올림하거나 버리는 연산이기 때문에 반올림 모드를 지정해 주지 않으면 ArithmeticException이 발생할 수 있습니다.
'Lang > Java' 카테고리의 다른 글
| [Java 21] (11) - collections framework (0) | 2025.11.03 |
|---|---|
| [Java 21] (10) - date, time and formatting (0) | 2025.10.31 |
| [Java 21] (8) - exception handling (0) | 2025.10.20 |
| [Java 21] (7) - Object-oriented Programming 2 (0) | 2025.10.03 |
| [Java 21] (6) - Object-oriented Programming 1 (0) | 2025.09.29 |