1. String 클래스
구조
자바에서 문자를 다루는 대표적인 타입은 char과 String 2가지가 있다.
- 기본형 char은 문자 하나를 다룰 때 사용한다. 여러 문자를 나열하려면 char[]을 사용해야 한다.
- String 클래스를 사용하면 문자열을 편리하게 다룰 수 있다.
- 문자열은 매우 자주 사용되기 때문에 편의상 큰따옴표(")로 문자열을 감싸면 자바 언어에서 new String("...")와 같이 변경해 준다.
public class StringBasicMain {
public staic void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println("str1 = " + str1); // st1 = hello
System.out.println("str2 = " + str2); // st2 = hello
}
}
String 클래스는 대략 아래와 같이 구현돼 있다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
...
}
a. 속성(필드)
String 클래스는 개발자가 직접 다루기 불편한 char[], byte[]를 내부에 감추고, 문자열을 편리하게 다룰 수 있는 다양한 기능을 제공한다. 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공하기도 한다.
- 자바 9부터 String 클래스에서 char[] 대신 byte[]를 사용한다.
- 자바에서 문자 하나를 표현하는 char은 2-byte를 차지한다. 그러나 영어와 숫자는 보통 1-byte로 표현할 수 있기 때문에 문자열이 영어나 숫자로만 표현된 경우(Latin-1 인코딩) 1-byte를 사용하고, 그렇지 않은 나머지의 경우 2-byte인 UTF-16 인코딩을 사용한다. 따라서 메모리를 더 효율적으로 사용할 수 있다.
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
b. 기능(메서드)
String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공한다. 기능이 엄청 다양하기 때문에 필요한 경우 검색하거나 API 문서를 찾아보는 게 좋다. 주요 메서드는 다음과 같다. 나머지는 아래에서 더 자세하게 설명한다.
- length() = 문자열 길이 반환
- charAt(int index) = 특정 인덱스의 문자 반환
- substring(int beginIndex, int endIndex) = 문자열의 부분 문자열 반환
- indexOf(String str) = 특정 문자열이 시작되는 인덱스 반환
- toLowerCase(), toUpperCase() = 문자열을 소문자 또는 대문자로 변환
- trim() = 문자열 양 끝의 공백 제거
- concat(String str) = 문자열 붙이기
c. String 클래스와 참조형
String 클래스는 참조형이다. 따라서 원칙적으론 + 같은 연산을 사용할 수 없다.
- 자바에서 문자열을 더할 때는 String에서 제공하는 concat() 같은 메서드를 사용해야 한다.
- 그러나 문자열은 너무 자주 다뤄지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공한다.
public class StringConcatMain {
public static void main(String[] args) {
String a = "hello";
String b = " java";
String str1 = a.concat(b);
String str2 = a + b;
System.out.println("str1 = " + str1); // str1 = hello java
System.out.println("str2 = " + str2); // str2 = hello java
}
}
비교
String 클래스끼리 비교할 때는 항상 동등성(equals()) 비교를 해야 한다.
- 아래 코드를 보면, str1과 str2는 new String()을 사용해서 각각 인스턴스를 생성했다. 서로 다른 인스턴스이므로 동일성(==) 비교에 실패한다.
- 내부에 같은 값을 갖고 있기 때문에 논리적으로는 같다. 따라서 동등성(equals()) 비교에 성공한다.
- 문자열 리터럴의 경우엔 문자열 풀 때문에 다른 결과가 나온다. 자세한 설명은 밑에서 확인해 보자.
public class StringEqualsMain {
public static void main(String[] args) {
String str1 = new String("hi");
String str2 = new String("h2");
System.out.println("new String() == 비교: " + (str1 == str2));
// new String() == 비교: false
System.out.println("new String() equals 비교: " + (str1.equals(str2)));
// new String() equals 비교: true
String str3 = "hi";
String str4 = "hi";
System.out.println("리터럴 == 비교: " + (str3 == str4));
// 리터럴 == 비교: true
System.out.println("리터럴 equals 비교: " + (str3.equals(str4)));
// 리터럴 equals 비교: true
}
}
a. 문자열 풀(String Pool)
String str3 = "hi"와 같이 문자열 리터럴을 사용하는 경우, 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.
- 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면, 문자열 풀에 String 인스턴스를 미리 만들어둔다.
- 이때 같은 문자열이 있으면 만들지 않는다.
- String str3 = "hi"와 같이 문자열 리터럴을 사용하면, 문자열 풀에서 "hi"라는 문자를 가진 String 인스턴스를 찾고 참조값을 반환한다.
- String str4 = "hi"의 경우, "hi" 문자열 리터럴을 사용하므로 문자열 풀에서 str3와 같은 참조값을 사용한다.

따라서 문자열 풀 덕분에 같은 문자를 사용하는 경우, 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화할 수 있다.
- 추가로, 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 동일성(==) 비교에 성공한다.
참고
문자열 풀은 힙 영역을 사용한다. 그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있다.
참고
String 비교를 메서드를 만들어 String 매개변수를 넘기는 방식으로 진행한다면, 매개변수로 넘어오는 String 인스턴스가 new String()으로 만들어진 것인지, 문자열 리터럴로 만들어진 것인지 확인할 수 없다. 따라서 문자열 비교는 항상 equals()를 사용해서 동등성 비교를 해야 한다.
b. 불변 객체
String은 불변 객체이므로 생성 이후에 내부 문자열 값을 변경할 수 없다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 새로운 결과를 만들어서 반환한다.
- String은 자바 내부에서 문자열 풀을 통해 최적화한다. 만약 String 내부 값을 변경할 수 있다면, 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되는 사이드 이펙트가 발생한다. 따라서 String 클래스는 불변으로 설계돼 있다.
주요 메서드
String 클래스는 문자열을 편리하게 다루기 위한 다양한 메서드를 제공한다. 주요 메서드를 간략하게 학습해 보자.
a. 문자열 정보 조회
- length()
- 문자열 길이 반환
- isEmpty()
- 문자열이 비어있거나 길이가 0인지 확인
- isBlank()
- 문자열이 비어있거나 길이가 0 또는 공백(Whitespace)만 있는지 확인
- charAt(int index)
- 지정된 인덱스에 있는 문자를 반환
b. 문자열 비교
- equals(Object object)
- 두 문자열이 동일한지 비교
- equalsIgnoreCase(String anotherString)
- 두 문자열을 대소문자 구분 없이 비교
- compareTo(String anotherString)
- 두 문자열을 사전 순으로 비교
- compareToIgnoreCase(String str)
- 두 문자열을 대소문자 구분 없이 사전 순으로 비교
- startsWith(String prefix)
- 문자열이 특정 접두사로 시작하는지 확인
- endsWith(String suffix)
- 문자열이 특정 접미사로 끝나는지 확인
c. 문자열 검색
- contains(CharSequence s)
- 문자열이 특정 문자열을 포함하고 있는지 확인
- indexOf(String ch) / indexOf(String ch, int fromIndex)
- 문자열이 처음 등장하는 위치 반환
- lastIndexOf(String ch)
- 문자열이 마지막으로 등장하는 위치 반환
d. 문자열 조작 및 변환
- substring(int beginIndex) / substring(int beginIndex, int endIndex)
- 문자열의 부분 문자열 반환
- concat(String str)
- 문자열 끝에 다른 문자열 결합
- replace(CharSequence target, CharSequence replacement)
- 특정 문자열을 새 문자열로 대체
- replaceAll(String regex, String replacement)
- 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체
- replaceFirst(String regex, String replacement)
- 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체
- toLowerCase() / toUpperCase()
- 문자열을 소문자나 대문자로 변환
- trim()
- 문자열 양쪽 끝의 공백을 제거 (단순 Whitespace만 제거 가능)
- strip()
- Whitespace와 유니코드 공백을 포함해서 제거 (자바 11부터 가능)
- stripLeading()
- 문자 앞의 Whitespace와 유니코드 공백 제거
- stripTrailing()
- 문자 뒤의 Whitespace와 유니코드 공백 제거
e. 문자열 분할 및 조합
- split(String regex)
- 문자열을 정규 표현식을 기준으로 분할
- join(CharSequence delimiter, CharSequence... elements)
- 주어진 구분자로 여러 문자열을 결합
f. 기타 유틸리티
- valueOf(Object obj)
- 다양한 타입을 문자열로 변환
- toCharArray()
- 문자열을 문자 배열로 변환
- format(String format, Object... args)
- 형식 문자열과 인자를 사용해 새로운 문자열 생성
- %d(숫자), %s(문자열), %b(boolean), %f(실수)
- matches(String regex)
- 문자열이 주어진 정규 표현식과 일치하는지 확인
StringBuilder - 가변 String
a. 불변인 String 클래스의 단점
불변인 String 클래스의 단점은 문자를 더하거나 변경할 때마다 계속해서 새로운 객체를 생성해야 한다는 점이다.
- 아래 코드를 보면 n개 이상의 문자를 더하는 경우, 총 n-1개의 String 클래스가 추가로 생성된다. 여기서 마지막에 만들어진 new String("ABCD")만 사용되고 나머지는 GC의 대상이 된다.
- 결과적으로 컴퓨터의 리소스(CPU, 메모리)를 더 많이 사용하게 된다.
String s = "A" + "B" + "C" + "D";
String s = String("A") + String("B") + String("C") + String("D");
String s = new String("AB") + String("C") + String("D");
String s = new String("ABC") + String("D");
String s = new String("ABCD");
b. StringBuilder
위에서 말한 문제를 해결하기 위해 자바는 StringBuilder라는 가변 String을 제공한다. 물론 가변 객체기 때문에 사이드 이펙트를 주의해서 사용해야 한다.
- 실제로는 상속 관계에 있고, 부모 클래스인 AbstractStringBuilder에 value 속성과 length() 메서드가 있다.
public final class StringBuilder {
char[] value; // 자바 9 이전
byte[] value; // 자바 9 이후
// 여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
가변 객체이기 때문에 아래 코드처럼 메서드의 결과를 반환받지 않아도 된다.
- StringBuilder는 보통 문자열을 변경하는 동안에만 사용하다가 문자열 변경이 끝나면 불변 객체인 String으로 변환하는 것이 좋다.
public class StringBuilderMain {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("BC");
sb.append("D");
sb.insert(4, "Java");
sb.delete(4, 8);
sb.reverse();
String str = sb.toString();
}
}
가변(Mutable) vs 불변(Immutable)
- String은 불변하기 때문에 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다.
- 반면 StringBuilder는 가변적이기 때문에 하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상할 수 있다. 단, 사이드 이펙트를 주의해야 한다.
String 최적화
문자열을 합칠 때 대부분의 경우 최적화가 진행되므로 + 연산을 사용하면 된다.
a. 자바의 String 최적화
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐준다. 따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상된다.
// 컴파인 전
String helloWorld = "Hello, " + "World!";
// 컴파일 후
String helloWorld = "Hello, World!";
문자열 변수의 경우, 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없다.
- 최적화 방식은 자바 버전에 따라 달라진다. 자바 9부터는 StringConcatFactory를 사용해서 최적화를 진행한다.
String result = str1 + str2;
// 최적화
String result = new StringBuilder().append(str1).append(str2).toString();
b. String 최적화가 어려운 경우
아래와 같이 문자열을 루프 안에서 더하는 경우에는 최적화가 진행되지 않는다.
- 반복문의 루프 내부에서는 최적화가 진행되는 것처럼 보이지만, 반복 횟수만큼 객체를 생성해야 한다.
- 반복문 내에서의 문자열 연결은, 연결할 문자열의 개수와 내용이 런타임에 결정된다. 이 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없다.
sb.append("");public class LoopStringMain {
public static void main(String[] args) {
String result = "";
for (int i=0; i<100000; i++) {
result += "Hello";
// 최적화
// result = new StringBuilder().append(result).append("Hello").toString();
}
System.out.println("result = " + result);
}
}
이럴 때는 직접 StringBuilder를 사용하면 된다. StringBuilder를 직접 사용하는 것이 더 좋은 경우는 다음과 같다.
- 반복문을 반복해서 문자를 연결할 때
- 조건문을 통해 동적으로 문자열을 조합할 때
- 복잡한 문자열의 특정 부분을 변경해야 할 때
- 매우 긴 대용량 문자열을 다룰 때
sb.append("");public class LoopStringMain {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i=0; i<100000; i++) {
sb.append("Hello");
}
String result = sb.toString();
System.out.println("result = " + result);
}
}
StringBuilder vs StringBuffer
- StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있다. StringBuffer는 내부에 동기화가 돼 있어서 멀티 스레드 상황에 안전하지만, 동기화 오버헤드로 인해 성능이 느리다.
- StringBuilder는 멀티 쓰레드 상황에 안전하지 않지만, 동기화 오버헤드가 없으므로 성능이 빠르다.
메서드 체이닝 - Method Chaining
메서드가 자기 자신(this)의 참조값을 반환하는 경우, 반환된 참조값에 .(점, dot)을 사용해 메서드를 연속해서 호출할 수 있다.
public class MethodChainingMain {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
int result = adder.add(1).add(2).add(3).getValue);
System.out.println("result = " + result);
}
}
참고
StringBuilder는 메서드 체이닝 기법을 제공한다.
2. 래퍼 클래스
기본형의 한계와 자바 래퍼 클래스
int나 double 같은 기본형(Primitive Type)은 객체가 아니기 때문에 다음과 같은 한계가 있다.
- 객체가 아니므로 스스로 메서드를 제공할 수 없다.
- 객체 참조가 필요한 컬렉션 프레임워크를 사용할 수 없다.
- 제네릭을 사용할 수 없다.
- null 값을 가질 수 없다.
자바는 기본형에 대응하는 래퍼 클래스를 기본으로 제공한다. 래퍼 클래스는 불변 객체이며, 항상 동등성(equals()) 비교를 해야 한다.
기본형 / 래퍼 클래스 | 기본형 / 래퍼 클래스 | ||
byte | Byte | short | Short |
int | Integer | long | Long |
float | Float | double | Double |
char | Character | boolean | Boolean |
a. 박싱(Boxing), 언박싱(Unboxing), 동등성(equals()) 비교
박싱(Boxing) - valueOf()
- 기본형을 래퍼 클래스로 변경하는 메서드이다.
public class WrapperClassMain {
public static void main(String[] args) {
Integer integerObj = Integer.valueOf(10);
Long longObj = Long.valueOf(100);
Double doubleObj = Double.valueOf(10.5);
}
}
언박싱(unboxing) - xxxValue()
- 래퍼 클래스에 들어있는 기본형 값을 다시 꺼내는 메서드이다.
public class WrapperClassMain {
public static void main(String[] args) {
Integer integerObj = Integer.valueOf(10);
Long longObj = Long.valueOf(100);
Double doubleObj = Double.valueOf(10.5);
int intValue = integerObj.intValue();
long longValue = longObj.longValue();
double doubleValue = doubleObj.doubleValue();
}
}
동등성 비교 - equals()
- 래퍼 클래스는 객체이기 때문에 동일성(==) 비교를 하면 인스턴스의 참조값을 비교한다.
- 래퍼 클래스는 내부의 값을 비교하도록 equals()를 오버라이딩해 뒀다. 따라서 값을 비교하려면 동등성(equals()) 비교를 사용해야 한다.
public class WrapperClassMain {
public static void main(String[] args) {
Integer newInteger = new Integer(10); // 미래에 삭제 예정
Integer integerObj = Integer.valueOf(10);
Long longObj = Long.valueOf(100);
Double doubleObj = Double.valueOf(10.5);
...
System.out.println("== 비교: " + (newInteger == integerObj)); // false
System.out.println("equals() 비교: " + newInteger.equals(integerObj)); // true
}
}
b. 오토 박싱(Auto-Boxing), 오토 언박싱(Auto-Unboxing)
- 오토 박싱과 오토 언박싱은 컴파일러가 개발자 대신 valueOf(), xxxValue() 등의 코드를 추가해 주는 기능이다. 덕분에 기본형과 래퍼 클래스를 서로 편리하게 변환할 수 있다.
public class AutoBoxingMain {
public static void main(String[] args) {
int value = 10;
Integer boxedValue = value; // 오토 박싱(Auto-Boxing)
Integer boxedValue = Integer.valueOf(value); // 컴파일 단계에서 추가
int unboxedValue = boxedValue; // 오토 언박싱(Auto-Unboxing)
int unboxedValue = boxedValue.intValue(); // 컴파일 단계에서 추가
}
}
주요 메서드와 성능
a. Integer 주요 메서드
- valueOf()
- 래퍼 타입 반환 (숫자, 문자열 모두 지원)
- parseInt()
- 문자열을 기본형으로 변환
- 각 래퍼 타입에 parseXxx()처럼 사용
- compareTo()
- 내 값과 인수로 넘어온 값 비교 (내 값이 크면 1, 같으면 0, 작으면 -1 반환)
- sum() / min() / max()
- static 메서드로, 간단한 덧셈, 작은 값, 큰 값 연산 수행
b. 성능
래퍼 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공한다. 그러나 단순히 값을 반복해서 더하는 경우엔 기본형을 사용하는 게 훨씬 빠르기 때문에 기본형을 사용하는 게 낫다.
- 기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지한다.
- 예를 들어 int는 보통 4-byte의 메모리를 사용한다.
- 래퍼 클래스의 인스턴스는 내부에 필드로 갖고 있는 기본형의 값뿐만 아니라, 자바에서 객체 자체를 다루는 데 필요한 객체 메타 데이터를 포함하므로 훨씬 더 많은 메모리를 사용한다.
- 자바 버전과 시스템마다 다르지만, 대략 8~16-byte의 메모리를 추가로 사용한다.
유지 보수와 최적화 사이에서 고려해야 하는 상황이라면 유지 보수하기 좋은 코드를 먼저 고민해야 한다. 특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고 하더라도 실질적으로 도움이 되지 않는 경우가 많다.
- CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만~수십만 이상 연속해서 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려하자.
- 코드 변경 없이 성능 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 더 많은 코드를 추가로 만들어야 한다. 최적화를 위해 유지 보수해야 하는 코드가 더 늘어나는 것이다.
- 최적화를 한다고 했지만, 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있다. 특히 웹 애플리케이션의 경우, 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만 배 더 오래 걸린다. 따라서 자바 메모리 내부에서 발생하는 연산을 수천번에서 한 번으로 줄이는 것보다, 네트워크 호출 한 번을 줄이는 것이 더 효과적인 경우가 많다.
권장하는 방법은 개발 이후 성능 테스트를 진행한 다음 정말 문제가 되는 부분을 찾아서 최적화하는 것이다.
3. Class, System 클래스
Class 클래스
Class 클래스는 클래스의 정보(메타 데이터)를 다루는 데 사용된다. 이 클래스를 통해 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메서드에 대한 정보를 조회하고 조작할 수 있다. 주요 기능은 다음과 같다.
- 타입 정보 얻기
- 클래스의 이름, 슈퍼 클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있다.
- 리플렉션
- 클래스에 정의된 메서드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메서드를 호출하는 등의 작업을 할 수 있다. (이후에 별도로 다룬다.)
- 동적 로딩과 생성
- Class.forName() 메서드를 사용해 클래스를 동적으로 로드하고, newInstance() 메서드를 통해 새로운 인스턴스를 생성할 수 있다.
- 애노테이션 처리
- 클래스에 적용된 애노테이션(Annotation)을 조회하고 처리하는 기능을 제공한다. (이후에 별도로 다룬다.)
a. 타입 정보 읽기, 리플렉션 예시
예를 들어, String.class는 String 클래스에 대한 Class 객체를 나타내며, 이를 통해 String 클래스에 대한 메타 데이터를 조회하거나 조작할 수 있다.
public class ClassMetaMain {
public static void main(String[] args) throws Exception {
// Class 조회
// 1. 클래스에서 조회
Class clazz = String.class;
// 2. 인스턴스에서 조회
Class clazz = new String().getClass();
// 3. 문자열로 조회
Class clazz = Class.forName("java.lang.String");
// 클래스의 모든 필드 출력
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("Field: " +
field.getType() + " " + field.getName());
}
// 클래스의 모든 메서드 출력
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("Method: " + method);
}
// 클래스의 상위 클래스 정보 출력
System.out.println("Superclass: " + clazz.getSuperclass().getName());
// 클래스의 인터페이스 정보 출력
Class[] interfaces = clazz.getInterfaces();
for (Class i : interface) {
System.out.println("Interface: " + i.getName());
}
}
}
b. 동적 로딩과 생성 예시 - 클래스 생성하기
Class 클래스에 들어있는 클래스의 모든 정보를 기반으로 인스턴스를 생성하거나 메서드를 호출하고, 필드의 값도 변경할 수 있다.
public class Hello {
public String hello() {
return "hello";
}
}
///
public class ClassCreateMain {
public static void main(String[] args) throws Exception {
Class helloClass = Class.forName("lang.clazz.Hello");
Hello hello = (Hello) helloClass
.getDeclaredConstructor().newInstance();
String result = hello.hello();
System.out.println("result = " + result);
}
}
System 클래스
System 클래스는 다음과 같은 시스템과 관련된 기본 기능들을 제공한다.
- 표준 입력, 출력, 오류 스트림
- System.in, System.out, System.err은 각각 표준 입력, 표준 출력, 표준 오류 스트림을 나타낸다.
- 시간 측정
- System.currentTimeMillis()와 System.nanoTime()은 현재 시간을 밀리초 또는 나노초 단위로 제공한다.
- 환경 변수
- System.getenv()를 사용해 OS에서 설정한 환경 변수의 값을 얻을 수 있다.
- 시스템 속성
- System.getProperty(String key)로 특정 속성을 얻을 수 있다. 시스템 속성은 자바에서 사용하는 설정 값이다.
- 시스템 종료
- System.exit(int status)는 프로그램을 종료하고, OS에 프로그램 종료의 상태 코드를 전달한다.
- 상태 코드 0: 정상 종료
- 상태 코드 0 이외: 오류나 예외적인 종료
- System.exit(int status)는 프로그램을 종료하고, OS에 프로그램 종료의 상태 코드를 전달한다.
- 배열 고속 복사
- System.arraycopy(char[] orginArr, int startIdx, char[] copiedArr, int copyStartIdx, size)는 시스템 레벨에서 최적화된 메모리 복사 연산을 사용한다.
4. Math, Random 클래스
Math 클래스
Math 클래스는 수많은 수학 문제를 해결해 주는 클래스이다. 주요 메서드만 알아보고 필요하다면 추가로 검색하거나 API 문서를 찾아보자.
- 기본 연산 메서드
- abs(x) = 절댓값
- max(a, b) = 최댓값
- min(a, b) = 최솟값
- 지수 및 로그 연산 메서드
- exp(x) = e^x 계산
- log(x) = 자연로그
- log10(x) = 로그 10
- pow(a, b) = a의 b 제곱
- 반올림 및 정밀도 메서드
- ceil(x) = 올림
- floor(x) = 내림
- rint(x) = 가장 가까운 정수로 반올림
- round(x) = 반올림
- 삼각 함수 메서드
- sin(x) = 사인
- cos(x) = 코사인
- tan(x) = 탄젠트
- 기타 유용한 메서드
- sqrt(x) = 제곱근
- cbrt(x) = 세제곱근
- random() = 0.0과 1.0 사이의 무작위 값 생성
참고
아주 정밀한 숫자와 반올림 계산이 필요하다면 BigDecimal을 검색해 보자.
Random 클래스
랜덤의 경우 Math.random()을 사용해도 되지만, Random 클래스를 사용하면 더욱 다양한 랜덤값을 구할 수 있다. 참고로 Math.random()도 내부에서는 Random 클래스를 사용한다. (Random 클래스는 java.util 패키지 소속이다.)
a. 주요 메서드
- nextInt()
- 랜덤 int 값 반환
- nextDouble()
- 0.0d ~ 1.0d 사이의 랜덤 double 값 반환
- nextBoolean()
- 랜덤 boolean 값 반환
- nextInt(int bound)
- 0부터 bound 미만의 숫자를 랜덤으로 반환
- 1부터 bound까지의 숫자를 원한다면 결과에 +1 연산
public class RandomMain {
public static void main(String[] args) {
Random random = new Random();
// Random random = new Random(1); // Seed가 같으면 Random의 결과가 같다.
int randomInt = random.nextInt();
double randomDouble = random.nextDouble(); // 0.0d ~ 1.0d
boolean randomBoolean = random.nextBoolean();
// 범위 조회
int randomRange1 = random.nextInt(10); // 0 ~ 9까지 출력
int randomRange2 = random.nextInt(10) + 1; // 1 ~ 10까지 출력
}
}
b. 씨드 - Seed
랜덤은 내부에서 씨드(Seed) 값을 사용해서 랜덤 값을 구한다. 이 씨드 값이 같으면 항상 같은 결과가 출력된다.
- new Random()
- 생성자를 비워두면 내부에서 System.nanoTime()에 여러 가지 복잡한 알고리즘을 섞어서 씨드 값을 생성한다. 따라서 반복 실행해도 결과가 항상 달라진다.
- new Random(int seed)
- 생성자에 씨드 값을 직접 전달할 수 있다. 씨드 값이 같으면 여러 번 반복 실행해도 실행 결과가 같다.
- 씨드 값을 직접 사용하면 결과가 달라지는 랜덤 값을 구할 수 없지만, 결과가 고정되기 때문에 테스트 코드 같은 곳에서 같은 결과를 검증할 수 있다.
Random random = new Random(1);

1. String 클래스
구조
자바에서 문자를 다루는 대표적인 타입은 char과 String 2가지가 있다.
- 기본형 char은 문자 하나를 다룰 때 사용한다. 여러 문자를 나열하려면 char[]을 사용해야 한다.
- String 클래스를 사용하면 문자열을 편리하게 다룰 수 있다.
- 문자열은 매우 자주 사용되기 때문에 편의상 큰따옴표(")로 문자열을 감싸면 자바 언어에서 new String("...")와 같이 변경해 준다.
public class StringBasicMain {
public staic void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println("str1 = " + str1); // st1 = hello
System.out.println("str2 = " + str2); // st2 = hello
}
}
String 클래스는 대략 아래와 같이 구현돼 있다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
...
}
a. 속성(필드)
String 클래스는 개발자가 직접 다루기 불편한 char[], byte[]를 내부에 감추고, 문자열을 편리하게 다룰 수 있는 다양한 기능을 제공한다. 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공하기도 한다.
- 자바 9부터 String 클래스에서 char[] 대신 byte[]를 사용한다.
- 자바에서 문자 하나를 표현하는 char은 2-byte를 차지한다. 그러나 영어와 숫자는 보통 1-byte로 표현할 수 있기 때문에 문자열이 영어나 숫자로만 표현된 경우(Latin-1 인코딩) 1-byte를 사용하고, 그렇지 않은 나머지의 경우 2-byte인 UTF-16 인코딩을 사용한다. 따라서 메모리를 더 효율적으로 사용할 수 있다.
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
b. 기능(메서드)
String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공한다. 기능이 엄청 다양하기 때문에 필요한 경우 검색하거나 API 문서를 찾아보는 게 좋다. 주요 메서드는 다음과 같다. 나머지는 아래에서 더 자세하게 설명한다.
- length() = 문자열 길이 반환
- charAt(int index) = 특정 인덱스의 문자 반환
- substring(int beginIndex, int endIndex) = 문자열의 부분 문자열 반환
- indexOf(String str) = 특정 문자열이 시작되는 인덱스 반환
- toLowerCase(), toUpperCase() = 문자열을 소문자 또는 대문자로 변환
- trim() = 문자열 양 끝의 공백 제거
- concat(String str) = 문자열 붙이기
c. String 클래스와 참조형
String 클래스는 참조형이다. 따라서 원칙적으론 + 같은 연산을 사용할 수 없다.
- 자바에서 문자열을 더할 때는 String에서 제공하는 concat() 같은 메서드를 사용해야 한다.
- 그러나 문자열은 너무 자주 다뤄지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공한다.
public class StringConcatMain {
public static void main(String[] args) {
String a = "hello";
String b = " java";
String str1 = a.concat(b);
String str2 = a + b;
System.out.println("str1 = " + str1); // str1 = hello java
System.out.println("str2 = " + str2); // str2 = hello java
}
}
비교
String 클래스끼리 비교할 때는 항상 동등성(equals()) 비교를 해야 한다.
- 아래 코드를 보면, str1과 str2는 new String()을 사용해서 각각 인스턴스를 생성했다. 서로 다른 인스턴스이므로 동일성(==) 비교에 실패한다.
- 내부에 같은 값을 갖고 있기 때문에 논리적으로는 같다. 따라서 동등성(equals()) 비교에 성공한다.
- 문자열 리터럴의 경우엔 문자열 풀 때문에 다른 결과가 나온다. 자세한 설명은 밑에서 확인해 보자.
public class StringEqualsMain {
public static void main(String[] args) {
String str1 = new String("hi");
String str2 = new String("h2");
System.out.println("new String() == 비교: " + (str1 == str2));
// new String() == 비교: false
System.out.println("new String() equals 비교: " + (str1.equals(str2)));
// new String() equals 비교: true
String str3 = "hi";
String str4 = "hi";
System.out.println("리터럴 == 비교: " + (str3 == str4));
// 리터럴 == 비교: true
System.out.println("리터럴 equals 비교: " + (str3.equals(str4)));
// 리터럴 equals 비교: true
}
}
a. 문자열 풀(String Pool)
String str3 = "hi"와 같이 문자열 리터럴을 사용하는 경우, 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.
- 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면, 문자열 풀에 String 인스턴스를 미리 만들어둔다.
- 이때 같은 문자열이 있으면 만들지 않는다.
- String str3 = "hi"와 같이 문자열 리터럴을 사용하면, 문자열 풀에서 "hi"라는 문자를 가진 String 인스턴스를 찾고 참조값을 반환한다.
- String str4 = "hi"의 경우, "hi" 문자열 리터럴을 사용하므로 문자열 풀에서 str3와 같은 참조값을 사용한다.

따라서 문자열 풀 덕분에 같은 문자를 사용하는 경우, 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화할 수 있다.
- 추가로, 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 동일성(==) 비교에 성공한다.
참고
문자열 풀은 힙 영역을 사용한다. 그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있다.
참고
String 비교를 메서드를 만들어 String 매개변수를 넘기는 방식으로 진행한다면, 매개변수로 넘어오는 String 인스턴스가 new String()으로 만들어진 것인지, 문자열 리터럴로 만들어진 것인지 확인할 수 없다. 따라서 문자열 비교는 항상 equals()를 사용해서 동등성 비교를 해야 한다.
b. 불변 객체
String은 불변 객체이므로 생성 이후에 내부 문자열 값을 변경할 수 없다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 새로운 결과를 만들어서 반환한다.
- String은 자바 내부에서 문자열 풀을 통해 최적화한다. 만약 String 내부 값을 변경할 수 있다면, 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되는 사이드 이펙트가 발생한다. 따라서 String 클래스는 불변으로 설계돼 있다.
주요 메서드
String 클래스는 문자열을 편리하게 다루기 위한 다양한 메서드를 제공한다. 주요 메서드를 간략하게 학습해 보자.
a. 문자열 정보 조회
- length()
- 문자열 길이 반환
- isEmpty()
- 문자열이 비어있거나 길이가 0인지 확인
- isBlank()
- 문자열이 비어있거나 길이가 0 또는 공백(Whitespace)만 있는지 확인
- charAt(int index)
- 지정된 인덱스에 있는 문자를 반환
b. 문자열 비교
- equals(Object object)
- 두 문자열이 동일한지 비교
- equalsIgnoreCase(String anotherString)
- 두 문자열을 대소문자 구분 없이 비교
- compareTo(String anotherString)
- 두 문자열을 사전 순으로 비교
- compareToIgnoreCase(String str)
- 두 문자열을 대소문자 구분 없이 사전 순으로 비교
- startsWith(String prefix)
- 문자열이 특정 접두사로 시작하는지 확인
- endsWith(String suffix)
- 문자열이 특정 접미사로 끝나는지 확인
c. 문자열 검색
- contains(CharSequence s)
- 문자열이 특정 문자열을 포함하고 있는지 확인
- indexOf(String ch) / indexOf(String ch, int fromIndex)
- 문자열이 처음 등장하는 위치 반환
- lastIndexOf(String ch)
- 문자열이 마지막으로 등장하는 위치 반환
d. 문자열 조작 및 변환
- substring(int beginIndex) / substring(int beginIndex, int endIndex)
- 문자열의 부분 문자열 반환
- concat(String str)
- 문자열 끝에 다른 문자열 결합
- replace(CharSequence target, CharSequence replacement)
- 특정 문자열을 새 문자열로 대체
- replaceAll(String regex, String replacement)
- 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체
- replaceFirst(String regex, String replacement)
- 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체
- toLowerCase() / toUpperCase()
- 문자열을 소문자나 대문자로 변환
- trim()
- 문자열 양쪽 끝의 공백을 제거 (단순 Whitespace만 제거 가능)
- strip()
- Whitespace와 유니코드 공백을 포함해서 제거 (자바 11부터 가능)
- stripLeading()
- 문자 앞의 Whitespace와 유니코드 공백 제거
- stripTrailing()
- 문자 뒤의 Whitespace와 유니코드 공백 제거
e. 문자열 분할 및 조합
- split(String regex)
- 문자열을 정규 표현식을 기준으로 분할
- join(CharSequence delimiter, CharSequence... elements)
- 주어진 구분자로 여러 문자열을 결합
f. 기타 유틸리티
- valueOf(Object obj)
- 다양한 타입을 문자열로 변환
- toCharArray()
- 문자열을 문자 배열로 변환
- format(String format, Object... args)
- 형식 문자열과 인자를 사용해 새로운 문자열 생성
- %d(숫자), %s(문자열), %b(boolean), %f(실수)
- matches(String regex)
- 문자열이 주어진 정규 표현식과 일치하는지 확인
StringBuilder - 가변 String
a. 불변인 String 클래스의 단점
불변인 String 클래스의 단점은 문자를 더하거나 변경할 때마다 계속해서 새로운 객체를 생성해야 한다는 점이다.
- 아래 코드를 보면 n개 이상의 문자를 더하는 경우, 총 n-1개의 String 클래스가 추가로 생성된다. 여기서 마지막에 만들어진 new String("ABCD")만 사용되고 나머지는 GC의 대상이 된다.
- 결과적으로 컴퓨터의 리소스(CPU, 메모리)를 더 많이 사용하게 된다.
String s = "A" + "B" + "C" + "D";
String s = String("A") + String("B") + String("C") + String("D");
String s = new String("AB") + String("C") + String("D");
String s = new String("ABC") + String("D");
String s = new String("ABCD");
b. StringBuilder
위에서 말한 문제를 해결하기 위해 자바는 StringBuilder라는 가변 String을 제공한다. 물론 가변 객체기 때문에 사이드 이펙트를 주의해서 사용해야 한다.
- 실제로는 상속 관계에 있고, 부모 클래스인 AbstractStringBuilder에 value 속성과 length() 메서드가 있다.
public final class StringBuilder {
char[] value; // 자바 9 이전
byte[] value; // 자바 9 이후
// 여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
가변 객체이기 때문에 아래 코드처럼 메서드의 결과를 반환받지 않아도 된다.
- StringBuilder는 보통 문자열을 변경하는 동안에만 사용하다가 문자열 변경이 끝나면 불변 객체인 String으로 변환하는 것이 좋다.
public class StringBuilderMain {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("BC");
sb.append("D");
sb.insert(4, "Java");
sb.delete(4, 8);
sb.reverse();
String str = sb.toString();
}
}
가변(Mutable) vs 불변(Immutable)
- String은 불변하기 때문에 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다.
- 반면 StringBuilder는 가변적이기 때문에 하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상할 수 있다. 단, 사이드 이펙트를 주의해야 한다.
String 최적화
문자열을 합칠 때 대부분의 경우 최적화가 진행되므로 + 연산을 사용하면 된다.
a. 자바의 String 최적화
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐준다. 따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상된다.
// 컴파인 전
String helloWorld = "Hello, " + "World!";
// 컴파일 후
String helloWorld = "Hello, World!";
문자열 변수의 경우, 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없다.
- 최적화 방식은 자바 버전에 따라 달라진다. 자바 9부터는 StringConcatFactory를 사용해서 최적화를 진행한다.
String result = str1 + str2;
// 최적화
String result = new StringBuilder().append(str1).append(str2).toString();
b. String 최적화가 어려운 경우
아래와 같이 문자열을 루프 안에서 더하는 경우에는 최적화가 진행되지 않는다.
- 반복문의 루프 내부에서는 최적화가 진행되는 것처럼 보이지만, 반복 횟수만큼 객체를 생성해야 한다.
- 반복문 내에서의 문자열 연결은, 연결할 문자열의 개수와 내용이 런타임에 결정된다. 이 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없다.
sb.append("");public class LoopStringMain {
public static void main(String[] args) {
String result = "";
for (int i=0; i<100000; i++) {
result += "Hello";
// 최적화
// result = new StringBuilder().append(result).append("Hello").toString();
}
System.out.println("result = " + result);
}
}
이럴 때는 직접 StringBuilder를 사용하면 된다. StringBuilder를 직접 사용하는 것이 더 좋은 경우는 다음과 같다.
- 반복문을 반복해서 문자를 연결할 때
- 조건문을 통해 동적으로 문자열을 조합할 때
- 복잡한 문자열의 특정 부분을 변경해야 할 때
- 매우 긴 대용량 문자열을 다룰 때
sb.append("");public class LoopStringMain {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i=0; i<100000; i++) {
sb.append("Hello");
}
String result = sb.toString();
System.out.println("result = " + result);
}
}
StringBuilder vs StringBuffer
- StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있다. StringBuffer는 내부에 동기화가 돼 있어서 멀티 스레드 상황에 안전하지만, 동기화 오버헤드로 인해 성능이 느리다.
- StringBuilder는 멀티 쓰레드 상황에 안전하지 않지만, 동기화 오버헤드가 없으므로 성능이 빠르다.
메서드 체이닝 - Method Chaining
메서드가 자기 자신(this)의 참조값을 반환하는 경우, 반환된 참조값에 .(점, dot)을 사용해 메서드를 연속해서 호출할 수 있다.
public class MethodChainingMain {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
int result = adder.add(1).add(2).add(3).getValue);
System.out.println("result = " + result);
}
}
참고
StringBuilder는 메서드 체이닝 기법을 제공한다.
2. 래퍼 클래스
기본형의 한계와 자바 래퍼 클래스
int나 double 같은 기본형(Primitive Type)은 객체가 아니기 때문에 다음과 같은 한계가 있다.
- 객체가 아니므로 스스로 메서드를 제공할 수 없다.
- 객체 참조가 필요한 컬렉션 프레임워크를 사용할 수 없다.
- 제네릭을 사용할 수 없다.
- null 값을 가질 수 없다.
자바는 기본형에 대응하는 래퍼 클래스를 기본으로 제공한다. 래퍼 클래스는 불변 객체이며, 항상 동등성(equals()) 비교를 해야 한다.
기본형 / 래퍼 클래스 | 기본형 / 래퍼 클래스 | ||
byte | Byte | short | Short |
int | Integer | long | Long |
float | Float | double | Double |
char | Character | boolean | Boolean |
a. 박싱(Boxing), 언박싱(Unboxing), 동등성(equals()) 비교
박싱(Boxing) - valueOf()
- 기본형을 래퍼 클래스로 변경하는 메서드이다.
public class WrapperClassMain {
public static void main(String[] args) {
Integer integerObj = Integer.valueOf(10);
Long longObj = Long.valueOf(100);
Double doubleObj = Double.valueOf(10.5);
}
}
언박싱(unboxing) - xxxValue()
- 래퍼 클래스에 들어있는 기본형 값을 다시 꺼내는 메서드이다.
public class WrapperClassMain {
public static void main(String[] args) {
Integer integerObj = Integer.valueOf(10);
Long longObj = Long.valueOf(100);
Double doubleObj = Double.valueOf(10.5);
int intValue = integerObj.intValue();
long longValue = longObj.longValue();
double doubleValue = doubleObj.doubleValue();
}
}
동등성 비교 - equals()
- 래퍼 클래스는 객체이기 때문에 동일성(==) 비교를 하면 인스턴스의 참조값을 비교한다.
- 래퍼 클래스는 내부의 값을 비교하도록 equals()를 오버라이딩해 뒀다. 따라서 값을 비교하려면 동등성(equals()) 비교를 사용해야 한다.
public class WrapperClassMain {
public static void main(String[] args) {
Integer newInteger = new Integer(10); // 미래에 삭제 예정
Integer integerObj = Integer.valueOf(10);
Long longObj = Long.valueOf(100);
Double doubleObj = Double.valueOf(10.5);
...
System.out.println("== 비교: " + (newInteger == integerObj)); // false
System.out.println("equals() 비교: " + newInteger.equals(integerObj)); // true
}
}
b. 오토 박싱(Auto-Boxing), 오토 언박싱(Auto-Unboxing)
- 오토 박싱과 오토 언박싱은 컴파일러가 개발자 대신 valueOf(), xxxValue() 등의 코드를 추가해 주는 기능이다. 덕분에 기본형과 래퍼 클래스를 서로 편리하게 변환할 수 있다.
public class AutoBoxingMain {
public static void main(String[] args) {
int value = 10;
Integer boxedValue = value; // 오토 박싱(Auto-Boxing)
Integer boxedValue = Integer.valueOf(value); // 컴파일 단계에서 추가
int unboxedValue = boxedValue; // 오토 언박싱(Auto-Unboxing)
int unboxedValue = boxedValue.intValue(); // 컴파일 단계에서 추가
}
}
주요 메서드와 성능
a. Integer 주요 메서드
- valueOf()
- 래퍼 타입 반환 (숫자, 문자열 모두 지원)
- parseInt()
- 문자열을 기본형으로 변환
- 각 래퍼 타입에 parseXxx()처럼 사용
- compareTo()
- 내 값과 인수로 넘어온 값 비교 (내 값이 크면 1, 같으면 0, 작으면 -1 반환)
- sum() / min() / max()
- static 메서드로, 간단한 덧셈, 작은 값, 큰 값 연산 수행
b. 성능
래퍼 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공한다. 그러나 단순히 값을 반복해서 더하는 경우엔 기본형을 사용하는 게 훨씬 빠르기 때문에 기본형을 사용하는 게 낫다.
- 기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지한다.
- 예를 들어 int는 보통 4-byte의 메모리를 사용한다.
- 래퍼 클래스의 인스턴스는 내부에 필드로 갖고 있는 기본형의 값뿐만 아니라, 자바에서 객체 자체를 다루는 데 필요한 객체 메타 데이터를 포함하므로 훨씬 더 많은 메모리를 사용한다.
- 자바 버전과 시스템마다 다르지만, 대략 8~16-byte의 메모리를 추가로 사용한다.
유지 보수와 최적화 사이에서 고려해야 하는 상황이라면 유지 보수하기 좋은 코드를 먼저 고민해야 한다. 특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고 하더라도 실질적으로 도움이 되지 않는 경우가 많다.
- CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만~수십만 이상 연속해서 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려하자.
- 코드 변경 없이 성능 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 더 많은 코드를 추가로 만들어야 한다. 최적화를 위해 유지 보수해야 하는 코드가 더 늘어나는 것이다.
- 최적화를 한다고 했지만, 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있다. 특히 웹 애플리케이션의 경우, 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만 배 더 오래 걸린다. 따라서 자바 메모리 내부에서 발생하는 연산을 수천번에서 한 번으로 줄이는 것보다, 네트워크 호출 한 번을 줄이는 것이 더 효과적인 경우가 많다.
권장하는 방법은 개발 이후 성능 테스트를 진행한 다음 정말 문제가 되는 부분을 찾아서 최적화하는 것이다.
3. Class, System 클래스
Class 클래스
Class 클래스는 클래스의 정보(메타 데이터)를 다루는 데 사용된다. 이 클래스를 통해 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메서드에 대한 정보를 조회하고 조작할 수 있다. 주요 기능은 다음과 같다.
- 타입 정보 얻기
- 클래스의 이름, 슈퍼 클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있다.
- 리플렉션
- 클래스에 정의된 메서드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메서드를 호출하는 등의 작업을 할 수 있다. (이후에 별도로 다룬다.)
- 동적 로딩과 생성
- Class.forName() 메서드를 사용해 클래스를 동적으로 로드하고, newInstance() 메서드를 통해 새로운 인스턴스를 생성할 수 있다.
- 애노테이션 처리
- 클래스에 적용된 애노테이션(Annotation)을 조회하고 처리하는 기능을 제공한다. (이후에 별도로 다룬다.)
a. 타입 정보 읽기, 리플렉션 예시
예를 들어, String.class는 String 클래스에 대한 Class 객체를 나타내며, 이를 통해 String 클래스에 대한 메타 데이터를 조회하거나 조작할 수 있다.
public class ClassMetaMain {
public static void main(String[] args) throws Exception {
// Class 조회
// 1. 클래스에서 조회
Class clazz = String.class;
// 2. 인스턴스에서 조회
Class clazz = new String().getClass();
// 3. 문자열로 조회
Class clazz = Class.forName("java.lang.String");
// 클래스의 모든 필드 출력
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("Field: " +
field.getType() + " " + field.getName());
}
// 클래스의 모든 메서드 출력
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("Method: " + method);
}
// 클래스의 상위 클래스 정보 출력
System.out.println("Superclass: " + clazz.getSuperclass().getName());
// 클래스의 인터페이스 정보 출력
Class[] interfaces = clazz.getInterfaces();
for (Class i : interface) {
System.out.println("Interface: " + i.getName());
}
}
}
b. 동적 로딩과 생성 예시 - 클래스 생성하기
Class 클래스에 들어있는 클래스의 모든 정보를 기반으로 인스턴스를 생성하거나 메서드를 호출하고, 필드의 값도 변경할 수 있다.
public class Hello {
public String hello() {
return "hello";
}
}
///
public class ClassCreateMain {
public static void main(String[] args) throws Exception {
Class helloClass = Class.forName("lang.clazz.Hello");
Hello hello = (Hello) helloClass
.getDeclaredConstructor().newInstance();
String result = hello.hello();
System.out.println("result = " + result);
}
}
System 클래스
System 클래스는 다음과 같은 시스템과 관련된 기본 기능들을 제공한다.
- 표준 입력, 출력, 오류 스트림
- System.in, System.out, System.err은 각각 표준 입력, 표준 출력, 표준 오류 스트림을 나타낸다.
- 시간 측정
- System.currentTimeMillis()와 System.nanoTime()은 현재 시간을 밀리초 또는 나노초 단위로 제공한다.
- 환경 변수
- System.getenv()를 사용해 OS에서 설정한 환경 변수의 값을 얻을 수 있다.
- 시스템 속성
- System.getProperty(String key)로 특정 속성을 얻을 수 있다. 시스템 속성은 자바에서 사용하는 설정 값이다.
- 시스템 종료
- System.exit(int status)는 프로그램을 종료하고, OS에 프로그램 종료의 상태 코드를 전달한다.
- 상태 코드 0: 정상 종료
- 상태 코드 0 이외: 오류나 예외적인 종료
- System.exit(int status)는 프로그램을 종료하고, OS에 프로그램 종료의 상태 코드를 전달한다.
- 배열 고속 복사
- System.arraycopy(char[] orginArr, int startIdx, char[] copiedArr, int copyStartIdx, size)는 시스템 레벨에서 최적화된 메모리 복사 연산을 사용한다.
4. Math, Random 클래스
Math 클래스
Math 클래스는 수많은 수학 문제를 해결해 주는 클래스이다. 주요 메서드만 알아보고 필요하다면 추가로 검색하거나 API 문서를 찾아보자.
- 기본 연산 메서드
- abs(x) = 절댓값
- max(a, b) = 최댓값
- min(a, b) = 최솟값
- 지수 및 로그 연산 메서드
- exp(x) = e^x 계산
- log(x) = 자연로그
- log10(x) = 로그 10
- pow(a, b) = a의 b 제곱
- 반올림 및 정밀도 메서드
- ceil(x) = 올림
- floor(x) = 내림
- rint(x) = 가장 가까운 정수로 반올림
- round(x) = 반올림
- 삼각 함수 메서드
- sin(x) = 사인
- cos(x) = 코사인
- tan(x) = 탄젠트
- 기타 유용한 메서드
- sqrt(x) = 제곱근
- cbrt(x) = 세제곱근
- random() = 0.0과 1.0 사이의 무작위 값 생성
참고
아주 정밀한 숫자와 반올림 계산이 필요하다면 BigDecimal을 검색해 보자.
Random 클래스
랜덤의 경우 Math.random()을 사용해도 되지만, Random 클래스를 사용하면 더욱 다양한 랜덤값을 구할 수 있다. 참고로 Math.random()도 내부에서는 Random 클래스를 사용한다. (Random 클래스는 java.util 패키지 소속이다.)
a. 주요 메서드
- nextInt()
- 랜덤 int 값 반환
- nextDouble()
- 0.0d ~ 1.0d 사이의 랜덤 double 값 반환
- nextBoolean()
- 랜덤 boolean 값 반환
- nextInt(int bound)
- 0부터 bound 미만의 숫자를 랜덤으로 반환
- 1부터 bound까지의 숫자를 원한다면 결과에 +1 연산
public class RandomMain {
public static void main(String[] args) {
Random random = new Random();
// Random random = new Random(1); // Seed가 같으면 Random의 결과가 같다.
int randomInt = random.nextInt();
double randomDouble = random.nextDouble(); // 0.0d ~ 1.0d
boolean randomBoolean = random.nextBoolean();
// 범위 조회
int randomRange1 = random.nextInt(10); // 0 ~ 9까지 출력
int randomRange2 = random.nextInt(10) + 1; // 1 ~ 10까지 출력
}
}
b. 씨드 - Seed
랜덤은 내부에서 씨드(Seed) 값을 사용해서 랜덤 값을 구한다. 이 씨드 값이 같으면 항상 같은 결과가 출력된다.
- new Random()
- 생성자를 비워두면 내부에서 System.nanoTime()에 여러 가지 복잡한 알고리즘을 섞어서 씨드 값을 생성한다. 따라서 반복 실행해도 결과가 항상 달라진다.
- new Random(int seed)
- 생성자에 씨드 값을 직접 전달할 수 있다. 씨드 값이 같으면 여러 번 반복 실행해도 실행 결과가 같다.
- 씨드 값을 직접 사용하면 결과가 달라지는 랜덤 값을 구할 수 없지만, 결과가 고정되기 때문에 테스트 코드 같은 곳에서 같은 결과를 검증할 수 있다.
Random random = new Random(1);