java.lang 패키지는 자바프로그래밍에 가장 기본이 되는 클래스들을 포함하고 있다. 그렇기 때문에 java.lang 패키지의 클래스들은 import문 없이도 사용할 수 있게 되어 있다. 그 동안 String클래스나 System클래스를 import문 없이 사용할 수 있었던 이유가 바로 java.lang패키지에 속한 클래스들이기 때문이었던 것이다. 우선 java.lang패키지의 여러 클래스들 중에서도 자주 사용되는 클래스 몇 가지만을 골라서 학습해보자.
클래스의 상속을 학습할 때 Object 클래스에 대해서 이미 배웠지만, 여기서는 보다 자세히 알아보자. Object클래스는 모든 클래스의 최고 조상이기 때문에 Object클래스의 멤버들은 모든 클래스에서 바로 사용 가능하다.
Object 클래스의 메서드 | 설 명 |
---|---|
protected Object clone() | 객체 자신의 복사본을 반환한다. |
public boolean equals(Object obj) | 객체 자신과 객체 obj가 같은 객체인지 알려준다.(같으면 true) |
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()을 호출할 때까지 현재 쓰레드를 무한히 또는 지정된 시간(timeout, nanos)동안 기다리게 한다. (timeout은 천 분의 1초, nanos는 10^9분의 1초) |
Object 클래스는 멤버변수는 없고 오직 11개의 메서드만 가지고 있다.
기존의 다른 언어에서는 문자열을 char형의 배열로 다루었으나 자바에서는 문자열을 위한 클래스를 제공한다. 그것이 바로 String 클래스인데, String 클래스는 문자열ㅇㄹ 저장하고 이를 다루는데 필요한 메서드를 함께 제공한다.
String 클래스에는 문자열을 저장하기 위해서 문자열 배열 참조변수(char[]) value를 인스턴스 변수로 정의해놓고 있다. 인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 인스턴스변수(value)에 문자형 배열(char[])로 저장되는 것이다.
public final class String implements java.io.Serializable, Comparable {
private char[] value;
...
}
한번 생성된 String인스턴스가 갖고 있는 문자열은 읽어 올 수만 있고, 변경할 수는 없다.
예를 들어 아래의 코드와 같이 +
연산자를 이용해서 문자열을 결합하는 경우 인스턴스 내의 문자열이 바뀌는 것이 아니라 새로운 문자열("ab")이 담긴 String 인스턴스가 생성되는 것이다.
String a = "a";
String b = "b";
a = a + b;
이처럼 덧셈연산자 +
를 사용해서 문자열을 결합하는 것은 매 연산 시 마다 새로운 문자열을 가진 String 인스턴스가 생성되어 메모리공간을 차지하게 되므로 가능한 한 결합횟수를 줄이는 것이 좋다.
문자열간의 결합이나 추출 등 문자열을 다루는 작업이 많이 필요한 경우 StringBuffer
클래스를 사용하는 것이 좋다. StringBuffer
인스턴스에 저장된 문자열을 변경이 가능하므로 하나의 StringBuffer
인스턴스만으로도 문자열을 다루는 것이 가능하다.
문자열을 만들 때는 두 가지 방법, 문자열 리터럴을 지정하는 방법과 String 클래스의 생성자를 사용해서 만드는 방법이 있다.
String str1 = "abc"; // 문자열 리터럴 "abc"의 주소가 str1에 저장됨
String str2 = "abc"; // 문자열 리터럴 "abc"의 주소가 str2에 저장됨
String str3 = new String("abc"); // 새로운 String 인스턴스를 생성
String str4 = new String("abc"); // 새로운 String 인스턴스를 생성
String 클래스의 생성자를 이용한 경우에는 new 연산자에 의해서 메모리 할당이 이루어지기 때문에 항상 새로운 String 인스턴스가 생성된다. 그러나 문자열 리터럴은 이미 존재하는 것을 재사용하는 것이다.
equals()를 사용했을 때는 두 문자열의 내용("abc")을 비교하기 때문에 두 경우 모두 true로 결과로 얻는다. 하지만, 각 String 인스턴스의 주소를 등가비교연산자 ==
로 비교했을 때는 결과가 다르다.
public class StringComparisonExample {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");
// == 연산자를 사용한 비교
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
// equals 메서드를 사용한 비교
System.out.println(str1.equals(str2)); // true
System.out.println(str1.equals(str3)); // true
}
}
길이가 0인 배열이 존재할 수 있을까? 답은 Yes
이다. char형 배열도 길이가 0인 배열을 생성할 수 있고, 이 배열을 내부적으로 가지고 있는 문자열이 바로 빈 문자열이다. String s = "";
과 같은 문장이 있을 때, 참조변수 s가 참조하고 있는 String 인스턴스는 내부에 new char[0]
과 같이 길이가 0인 char형 배열을 저장하고 있는 것이다.
char[] chArr = new char[0]; // 길이가 0인 char배열
String 클래스는 인스턴스를 생성할 때 지정된 문자열을 변경할 수 없지만 StringBuffer 클래스는 변경이 가능하다. 내부적으로 문자열 편집을 위한 버퍼(buffer)를 가지고 있으며, StringBuffer 인스턴스를 생성할 때 그 크기를 지정할 수 있다.
이 때, 편집할 문자열의 길이를 고려하여 버퍼의 길이를 충분히 잡아주는 것이 좋다. 편집 중인 문자열이 버퍼의 길이를 넘어서게 되면 버퍼의 길이를 늘려주는 작업이 추가로 수행되어야하기 때문에 작업효율이 떨어진다.
StringBuffer 클래스는 String클래스와 유사한 점이 많다. 아래의 코드에서 알 수 있듯이, StringBuffer 클래스는 String 클래스와 같이 문자열을 저장하기 위한 char형 배열의 참조변수를 인스턴스 변수로 선언해 놓고 있다. StringBuffer 인스턴스가 생성될 때, char형 배열이 생성되며 이 때 생성된 char형 배열을 인스턴스 변수 value가 참조하게 된다.
public final class StringBuffer implements java.io.Serializable {
private char[] value;
...
}
StringBuffer 클래스의 인스턴스를 생성할 때, 적절한 길이의 char형 배열이 생성되고, 이 배열은 문자열을 저장하고 편집하기 위한 공간(buffer)으로 사용된다.
StringBuffer 인스턴스를 생성할 때는 생성자 StringBuffer(int length)를 사용해서 StringBuffer 인스턴스에 저장될 문자열의 길이를 고려하여 충분히 여유있는 크기로 지정하는 것이 좋다. StringBuffer 인스턴스를 생성할 때, 버퍼의 크기를 지정해주지 않으면 16개의 문자를 저장할 수 있는 크기의 버퍼를 생성한다.
public StringBuffer(int length) {
value = new char[length];
shared = false;
}
public StringBuffer() {
this(16); // 버퍼의 크기를 지정하지 않으면 버퍼의 크기는 16이 된다.
}
public StringBuffer(String str) {
this(str.length() + 16); // 지정한 문자열의 길이보다 16이 더 크게 버퍼를 생성한다.
append(str);
}
String과 달리 StringBuffer는 내용을 변경할 수 있다. 예를 들어 아래와 같이 StringBuffer를 생성하였다고 가정하자.
StringBuffer sb = new StringBuffer("abc");
sb.append("123"); // sb의 내용 뒤에 "123"을 추가한다.
append()는 반환타입이 StringBuffer인데 자신의 주소를 반환한다. 그래서 아래와 같은 문장이 수행되면, sb에 새로운 문자열이 추가되고 sb자신의 주소를 반환하여 sb2에는 sb의 주소인 0x100이 저장된다.
StringBuffer sb2 = sb.append("ZZ"); // sb의 내용뒤에 "ZZ"를 추가한다.
System.out.println(sb); // abc123zz
System.out.println(sb2); // abc123ZZ
sb와 sb2가 모두 같은 StringBuffer 인스턴스를 가리키고 있으므로 같은 내용이 출력된다. 그래서 하나의 StirngBuffer 인스턴스에 대해 아래와 같이 연속적으로 append()를 호출하는 것이 가능하다.
StringBuffer sb = new StringBuffer("abe");
sb.append("123").append("ZZ");
String 인스턴스간의 비교에 대해서 학습하면서 등가비교연산자 ==
에 의한 비교와 equals
메서드에 의한 비교의 차이점을 자세히 알아봤다.
String 클래스에서는 equals 메서드를 오버라이딩해서 문자열의 내용을 비교하도록 구현되어 있지만, StringBuffer 클래스는 equals 메서드를 오버라이딩하지 않아서 StringBuffer 클래스의 equals 메서드를 사용해도 등가비교연산자(==)로 비교한 것과 같은 결과를 얻는다.
StringBuffer sb = new StringBuffer("abc");
StringBuffer sb2 = new StringBuffer("abc");
System.out.println(sb == sb2); // false
System.out.println(sb.equals(sb2)); // false
반면에 toString()은 오버라이딩되어 있어서 StringBuffer 인스턴스에 toString()을 호출하면, 담고있는 문자열을 String으로 반환한다.
그래서 StringBuffer 인스턴스에 담긴 문자열을 비교하기 위해서는 StringBuffer 인스턴스에 toString()을 호출해서 String 인스턴스를 얻은 다음, 여기에 equals 메서드를 사용해서 비교해야 한다.
String s = sb.toString();
String s2 = sb2.toString();
System.out.println(s.equals(s2)); // true
StringBuffer는 멀티쓰레드에 안전(thread safe)하도록 동기화되어 있다. 아직은 멀티쓰레드나 동기화에 대해서 배우지 않았지만, 동기화가 StringBuffer의 성능을 떨어뜨린다는 것만 이해하면 된다. 멀티쓰레드로 작성된 프로그램이 아닌 경우, StringBuffer의 동기화는 불필요하게 성능만 떨어뜨리게 된다.
그래서 StringBuffer에서 쓰레드의 동기화만 뺀 StringBuilder가 새로 추가되었다. StringBuilder는 StringBuffer와 완전히 똑같은 기능으로 작성되어 있어서, 소스코드에서 StringBuffer대신 StringBuilder를 사용하도록 바꾸기만 하면 된다. 즉, StringBuffer 대신 StringBuilder를 사용하도록 바꾸기만 하면 된다. 즉, StringBuffer 타입의 참조변수를 선언한 부분과 StringBuffer의 생성자만 바꾸면 된다는 말이다.
StringBuffer sb;
sb = new StringBuffer();
sb.append("abc");
StringBuilder sb;
sb = new StringBuilder();
sb.append("abc");
StringBuffer도 충분히 성능이 좋기 때문에 성능향상이 반드시 필요한 경우를 제외하고는 기존에 작성한 코드에서 StringBuffer를 StringBuilder로 굳이 바꿀 필요는 없다.
Math클래스는 기본적인 수학계산에 유용한 메서드로 구성되어 있다. 임의의 수를 얻을 수 있는 random()과 반올림에 사용되는 round() 등이 이미 학습한 바 있다.
Math클래스의 생성자는 접근 제어자가 private이기 때문에 다른 클래스에서 Math인스턴스를 생성할 수 없도록 되어있다. 그 이유는 클래스 내에 인스턴스변수가 하나도 없어서 인스턴스를 생성할 필요가 없기 때문이다.
객체지향 개념에서 모든 것은 객체로 다루어져야 한다. 그러나 자바에서는 8개의 기본형을 객체로 다루지 않는데 이것이 바로 자바가 완전한 객체지향 언어가 아니라는 얘기를 듣는 이유이다. 그 대신 보다 높은 성능을 얻을 수 있었다.
때로는 기본형(primitive type) 변수도 어쩔 수 없이 객체로 다뤄야 하는 경우가 있다. 예를 들면, 매개변수로 객체를 요구할 때, 기본형 값이 아닌 객체로 저장해야할 때, 객체간의 비교가 필요할 때 등등의 경우에는 기본형 값들을 객체로 변환하여 작업을 수행해야 한다.
이 때 사용되는 것이 래퍼(wrapper)클래스이다. 8개의 기본형을 대표하는 8개의 래퍼클래스가 있는데, 이 클래스들을 이용하면 기본형 값을 객체로 다룰 수 있다.
기본형 | 래퍼클래스 |
---|---|
boolean | Boolean |
char | Character |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
java.util 패키지에는 많은 수의 클래스가 있지만 실제로 자주 쓰이는 것들은 그렇게 많지 않기 때문에 모든 클래스들을 간단히 설명하기 보다는 자주 사용되는 중요한 클래스들만을 골라서 다양한 용도로 활용하는 방법을 보여주고자 한다.
Object 클래스의 보조 클래스로 Math 클래스처럼 모든 메서드가 static
이다. 객체의 비교나 널 체크(null check)에 유용하다.
isNull()은 해당 객체가 널인지 확인해서 null이면 true를 반환하고 아니면 false를 반환한다. nonNull()은 isNull()과 정반대의 일을 한다. 즉, !Objects.isNull(obj)와 같다.
static boolean isNull(Object obj)
static boolean nonNull(Object obj)
그리고 requireNonNull()은 해당 객체가 널이 아니어야 하는 경우에 사용한다. 만일 객체가 널이면, NullPointerException을 발생시킨다. 두 번째 매개변수로 지정하는 문자열은 예외의 메시지가 된다.
난수를 얻는 방법을 생각하면 Math.random()이 떠오를 것이다. 이 외에도 Random클래스를 사용하면 난수를 얻을 수 있다. 사실 Math.random()은 내부적으로 Random클래스의 인스턴스를 생성해서 사용하는 것이므로 둘 중에서 편한 것을 사용하면 된다. 아래의 두 문장은 동등하다.
double randNum = Math.random();
double randNum = new Random().nextDouble(); // 위의 문장과 동일
예를 들어 1~6사이의 정수를 난수로 얻고자 할 때는 다음과 같다.
int num = (int) (Math.random() * 6) + 1;
int num = new Random().nextInt(6) + 1; // nextInt(6)은 0~6사이의 정수를 반환
Math.random()과 Random의 가장 큰 차이점이라면, 종자값(seed)을 설정할 수 있다는 것이다. 종자값이 같은 Random 인스턴스들은 항상 같은 난수를 같은 순서대로 반환한다. 종자값은 난수를 만드는 공식에 사용되는 값으로 같은 공식에 같은 값을 넣으면 같은 결과를 얻는 것처럼 같은 종자값을 넣으면 같은 난수를 얻게 된다.
정규식이란 텍스트 데이터 중에서 원하는 조건(패턴, pattern)과 일치하는 문자열을 찾아내기 위해 사용하는 것으로 미리 정의된 기호와 문자를 이용해서 작성한 문자열을 말한다. 원래 Unix에서 사용하던 것이고 Perl의 강력한 기능이었는데 Java를 비롯해 다양한 언어에서 지원하고 있다.
Pattern p = Pattern.compile("c[a-z]*");
matcher(CharSequence input)를 호출해서 Matcher 인스턴스를 얻는다.
if(m.matches())
Scanner는 화면, 파일, 문자열과 같은 입력소스로부터 문자데이터를 읽어오는데 도움을 줄 목적으로 JDK1.5부터 추가되었다. Scanner에는 다음과 같은 생성자를 지원하기 때문에 다양한 입력소스로부터 데이터를 읽을 수 있다.
Scanner(String source)
Scanner(File source)
Scanner(InputStream source)
Scanner(Readable source)
Scanner(ReadableByteChannel source)
Scanner(Path source) // JDK1.7부터 추가
또한 Scanner는 정규식 표현(Regualar expression)을 이용한 라인단위의 검색을 지원하며 구분자(delimiter)에도 정규식 표현을 사용할 수 있어서 복잡한 형태의 구분자도 처리가 가능하다.
Scanner useDelimiter(Pattern pattern)
Scanner useDelimiter(String pattern)
StringTokenizer는 긴 문자열을 지정된 구분자(delimeter)를 기준으로 토큰(token)이라는 여러 개의 문자열로 잘라내는 데 사용된다. 예를 들어 "100,200,300,400"
이라는 문자열이 있을 때 ,
를 구분자로 잘라내면 "100", "200", "300", "400"
이라는 4개의 문자열(토큰)을 얻을 수 있다.
StringTokenizer를 이용하는 방법 이외에도 아래와 같이 String의 split(String regex)이나 Scanner의 useDelimeter(String pattern)를 사용할 수도 있지만,
String result[] = "100,200,300,400".split(",");
Scanner sc2 = new Scanner("100,200,300,400").useDelimeter(",");
이 두가지 방법은 정규식 표현을 사용해야하므로 정규식 표현에 익숙하지 않은 경우 StringTokenizer를 사용하는 것이 간단하면서도 명확한 결과를 얻을 수 있을 것이다.
그러나 StringTokenizer는 구분자로 단 하나의 문자 밖에 사용하지 못하기 때문에 보다 복잡한 형태의 구분자로 문자열을 나누어야 할 때는 어쩔 수 없이 정규식을 사용하는 메서드를 사용해야 할 것이다.
정수형으로 표현할 수 있는 값의 한계가 있다. 가장 큰 정수형 타입인 long으로 표현할 수 있는 값은 10진수로 19자리 정도이다. 이 값도 상당히 큰 값이지만, 과학적 계산에서는 더 큰 값을 다뤄야할 때가 있다. 그럴 때 사용하면 좋은 것이 BigInterger이다.
BigInteger는 내부적으로 int배열을 사용해서 값을 다룬다. 그래서 long타입보다 훨씬 더 큰 값을 다룰 수 있는 것이다. 대신 성능은 long타입보다 떨어질 수 밖에 없다.
final int signum; // 부호. 1(양수), 0, -1(음수) 셋 중의 하나
final int[] mag; // 값(magnitude)
위의 코드에서 알 수 있듯이, BigInteger는 String처럼 불변(immutable)이다. 그리고 모든 정수형이 그렇듯이 BigInteger 역시 값을 2의 보수
의 형태로 표현한다.
double 타입으로 표현할 수 있는 값은 상당히 범위가 넓지만, 정말도가 최대 13자리 밖에 되지 않고 실수형의 특성상 오차를 피할 수 없다. BigDecimal은 실수형과 달리 정수를 이용해서 실수를 표현한다.