Posts :book: 자바의 정석 #9.1: java.lang 패키지
Post
Cancel

:book: 자바의 정석 #9.1: java.lang 패키지

java.lang 패키지

Object 클래스

멤버변수는 없고 오직 11개의 메서드만 가지고 있다.

메서드

  • protected Object clone()
  • public boolean equals(Object ob)
  • protected void finalize(): 객체가 소멸될 때 가비지 컬렉터에 의해 자동으로 호출된다. 이 때 수행되어야 하는 코드가 있을 때 오버라이딩한다. (거의 사용 안함)
  • public Class getClass(): 객체 자신의 클래스 정보를 담고 있는 Class 인스턴스 반환
  • public int hashCode()
  • public String toString(): 객체 정보를 문자열로 반환
  • notify(): 객체 자신을 사용하려고 기다리는 스레드를 하나만 깨운다.
  • notifyAll(): 객체 자신을 사용하려고 기다니는 모든 스레드를 깨운다.
  • wait(): notify()나 notifyAll()을 호출할 때까지 현재 스레드를 또는 무한히 혹은 지정된 시간동안 기다리게 한다.

equals()

1
2
3
4
// 실제 소스코드
public boolean equals(Object obj) {
  return (this == obj);
}

두 객체의 같고 다름을 참조변수의 값으로 판단한다. 객체를 생성할 때 메모리의 비어있는 공간을 찾아 생성하므로 서로 다른 두 개의 객체가 같은 주소를 갖는 일은 없다.

객체의 주소가 아니라 값을 비교하도록 만드려면 Object의 equals()를 오버라이딩해야 한다.

1
2
3
4
5
6
public boolean equals(Object obj) {
  if (obj != null && obj instanceof Person)
    return id == ((Person)obj).id;
  else
    return false;
}

String 클래스 역시 Object 클래스의 equals메서드를 그대로 사용하는 것이 아니라 오버라이딩을 통해서 String 인스턴스가 갖는 문자열 값을 비교하도록 되어 있다. 따라서 문자열의 내용이 같으면 true 값을 얻는다. String 클래스뿐만 아니라 Date, File, wrapper 클래스도 equals()가 오버라이딩 되어 있다. 그러나 StringBuffer 클래스는 오버라이딩 되어 있지 않다.

hashCode()

이 메서드는 해싱 기법에 사용되는 해시 함수를 구현한 것이다. 해싱은 데이터 관리기법 중에 하나인데 다량의 데이터를 저장하고 검색하는 데 유용하다. 해시함수는 찾고자 하는 값을 입력하면, 그 값이 저장된 위치를 알려주는 해시코드를 반환한다.

Object 클래스에 정의된 hashCode메서드는 객체의 주소값을 ㅣㅇ용해서 해시코드를 만들어 반환하기 때문에 서로 다른 두 객체는 같은 해시코드를 가질 수 없다.

객체의 같고 다름을 판단해야 하는 경우라면 equals 메서드뿐 아니라 hashCode 메서드도 적절히 오버라이딩해야 한다. 특히 해싱기법을 사용하는 HashMap이나 HashSet과 같은 클래스에 저장할 객체라면 반드시 hashCode 메서드를 오버라이딩해야 한다.

String 클래스는 문자열의 내용이 같으면 동일한 해시코드를 반환하도록 이 메서드를 오버라이딩하였다. 반면 System.identityHashCode(Object x)는 Object 클래스처럼 객체의 주소값으로 해시코드를 생성하기에 모든 객체에 대해 항상 다른 해시코드 값을 반환할 것을 보장한다.

toString()

이 메서드는 인스턴스에 대한 정보를 문자열(String)로 제공할 목적으로 정의한 것이다.

1
2
3
4
//Object 클래스에 정의된 toString()
public String toString() {
  return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

String 클래스는 toString()이 String 인스턴스가 갖고 있는 문자열을 반환하도록 오버라이딩 되어 있고, Date 클래스는 Date 인스턴스가 갖고 있는 날짜와 시간을 문자열로 변환하여 반환하도록 오버라이딩 되어 있다. 이처럼 toString()은 일반적으로 인스턴스나 클래스에 대한 정보 또는 인스턴스 변수들의 값을 문자열로 변환하여 반환하도록 오버라이딩되는 것이 보통이다.

조상에서 정의된 메서드를 자손에서 오버라이딩할 때는 조상에 정의된 접근범위보다 같거나 더 넓어야 한다.

1
2
3
public String toString() {
  return "kind: " + kind + ", number: " + number;
}

clone()

자신을 복제하여 새로운 인스턴스를 생성하는 일을 한다. 어떤 인스턴스에 대해 작업을 할 때 clone()을 이용해서 새로운 인스턴스를 생성해서 작업을 하면 작업이전의 값이 보존되므로 작업에 실패해서 원래의 상태로 되돌리거나 변경되기 전의 값을 참고하는 데 도움이 된다.

Object 클래스에 정의된 clone()은 인스턴스변수의 값만을 복사하기 때문에 참조변수 타입의 인스턴스 변수가 정의되어 있는 클래스는 완전한 인스턴스 복제가 이루어지지 않는다. 이런 경우 clone() 메서드를 오버라이딩해야 한다.

Cloneable 인터페이스를 구현한 클래스에서만 clone()을 호출할 수 있다. 이 인터페이스를 구현하지 않고 clone()을 호출하면 예외가 발생한다.

clone() 사용 방법

  • 복제할 클래스가 Cloneable 인터페이스를 구현해야 한다.
    • 클래스 작성자가 복제를 허용한다는 의미이다.
  • clone()을 오버라이딩하면서 접근제어자를 protected에서 public으로 변경한다.
  • 조상 클래스의 clone()을 호출하는 코드가 포함된 try catch 문을 작성한다.
1
2
3
4
5
6
7
8
9
10
class Point implements Cloneable {
  //..
  public Object clone() {
    Object obj = null;
    try {
      obj = super.clone();
    } catch (CloneNotSupportedExcepton e) {}
    return obj;
  }
}

배열을 복사할 때는 arraycopy 메서드를 사용해도 된다.

1
2
3
4
5
int[] arr = {1, 2, 3, 4, 5};
int[] arrClone = arr.clone();

int[] arrClone2 = new int[arr.length];
System.arraycopy(arr, 0, arrClone2, arr.length);

배열뿐만 아니라 Vecter, ArrayList, LinkedList, HAshSet, TreeSet, HashMap, TreeMap, Calendar, Date과 같은 클래스들이 이와 같은 방식으로 복제가 가능하다. clone()으로 복제가 가능한 클래스인지 확인하려면 Cloneable을 구현했는지 확인하면 된다.

공변 반환타입(covariant return type)

오버라이딩할 때 조상 메서드의 반환타입을 자손 클래스의 타입으로 변경을 허용

1
2
3
4
5
6
7
public Object clone() {
  Object obj = null;
  try {
    obj = super.clone();
  } catch (CloneNotSupportedExcepton e) {}
  return (Point)obj;
}

공변 반환타입을 사용하면 번거로운 형변환이 줄어든다.

1
2
// Point copy = (Point)original.clone();
Point copy = original.clone();

얕은 복사와 같은 복사

원본이 참조하고 있는 객체까지 복제하는 것을 깊은 복사라고 한다. 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 원본의 변경이 복사본에 영향을 미치지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Circle implements Cloneable {
  Point p;
  double r;
  Circle (Point p, double r) {
    this.p = p;
    this.r = r;
  }
  
    
  //깊은 복사
  public Circle shallowCopy() {
    Object obj = null;
    try {
      obj = super.clone();
    } catch (CloneNotSupportedException e) {}
    return (Circle) obj;
  }
    
  
  //깊은 복사
  public Circle deepCopy() {
    Object obj = null;
    try {
      obj = super.clone();
    } catch (CloneNotSupportedException e) {}
    
    Circle c = (Circle) obj;
    
    // 복제한 객체가 새로운 Point 인스턴스를 참조하도록 했다.
    c.p = new Point(this.p.x, this.p.y);
  }
}

class Point {
  int x;
  int y;
  Point(int x, int y) {
    this.x = x;
    this.y = y;
  }
}

getClass()

자신이 속한 클래스의 Class 객체를 반환하는 메서드이다.

Class 객체는 클래스의 모든 정보를 담고 있으며, 클래스당 단 1개만 존재한다. 클래스 파일이 클래스 로더에 의해 메모리에 올라갈 때, 자동으로 생성된다. 클래스 로더는 실행 시에 필요한 클래스를 동적으로 메모리에 로드하는 역할을 한다. 먼저 기존에 생성된 클래스 객체가 메모리에 존재하는 지 확인하고 있으면 객체의 참조를 반환하고 없으면 클래스 패스에 지정된 경로를 따라서 클래스 파일을 읽는다. 못 찾으면 ClassNotFoundException이 발생하고, 찾으면 해당 클래스 파일을 일거서 Class 객체로 반환한다.

즉 파일 형태로 저장되어 있는 클래스를 읽어서 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 객체를 통해서 객체를 생성하고 호출하는 등 보다 동적인 코드를 작성할 수 있다.

1
2
Card c = new Card(); // new 연산자를 이용해 객체 생성
Card c = Card.class.newInstance(); // Class 객체를 이용해서 객체 생성

String 클래스

변경 불가능한(immutable) 클래스

한 번 생성된 String 인스턴스가 갖고 있는 문자열은 읽어 올 수만 있고, 변경할 수는 없다. 덧셈 연산자를 사용해서 문자열을 결합하는 것은 매 연산 시마다 새로운 문자열을 가진 String 인스턴스가 생성되어 메모리 공간을 차지하게 된다. 대신 결합이나 추출 등 문자열을 다루는 작업이 많을 경우 StringBuffer클래스를 사용하는 것이 좋다. StringBuffer인스턴스에 저장된 문자열은 변경이 가능하다.

문자열의 비교

  • 문자열 리터럴을 지정하는 방법: 이미 존재하는 것을 재사용
    • 문자열 리터럴은 클래스가 메모리에 로드될 때 자동적으로 미리 생성된다.
  • String 클래스의 생성자를 사용해서 만드는 방법: new 연산자에 의해 메모리 할당이 이루어지기 때문에 항상 새로운 String 인스턴스 생성

문자열 리터럴

자바 소스파일에 포함된 모든 문자열 리터럴은 컴파일 시 클래스 파일에 저장된다. 이때 같은 내용의 문자열 리터럴은 한 번만 저장된다. 한 번 생성하면 내용을 변경할 수 없기 때문에 하나의 인스턴스를 공유하면 되기 때문이다. 클래스 파일에는 소스파일에 포함된 모든 리터럴의 목록이 있다. 해당 클래스 파일이 클래스 로더에 의해 메모리에 올라갈 때, 이 리터럴의 목록에 있는 리터럴들이 자동적으로 생성되어 JVM내에 있는 상수풀에 저장된다.

빈 문자열

빈배열이 가능한데, char형 빈배열을 내부적으로 가지고 있는 문자열이 빈 문자열이다. String을 초기화할 때는 보통 null보다는 빈문자열로 초기화한다.

자주 사용하는 메서드

  • String(char[] value)
  • String(StringBuffer buf)
  • char charAt(int index)
  • int compareTo(String str): 0, 1, -1 리턴
  • boolean contains(CharSequence s): 지정된 문자열이 포함되었는지 검사
  • boolean endsWith(String suffix)
  • boolean equals(Object obj)
  • int indexOf(int ch), indexOf(int ch, int pos). indexOf(String str)
  • String intern(): 문자열을 상수풀에 등록한다. 이미 상수풀에 같은 내용의 문자열이 있을 경우 그 문자열의 주소값을 반환한다.
  • String replace(old, nw): 문자열 중 문자열을 새로운 문자열로 바꾼 문자열을 반환한다. (문자, 문자열 리터럴, String 모두 파라미터로 올 수 잇음)
  • String replaceAll(old, nw): 일치하는 것을 모두 변경
  • String[] split(String regex): 문자열을 지정된 분리자로 나누어 문자열 배열에 담아 반환한다.
  • boolean startsWith(String prefix)
  • String substring(int begin), (int begin, itn end): 시작 위치부터 끝 위치에 포함된 문자열을 얻는다. 이 때, 시작범위의 문자는 범위에 포함되지만, 끝 위치의 문자는 포함되지 않는다. (begin <= x < end)
  • String toLowerCase()
  • String toUpperCase()
  • String toString(): String 인스턴스에 저장되어 있는 문자열을 반환한다.
  • String trim(): 문자열의 왼쪽 끝과 오른쪽 끝에 있는 공백을 없앤 결과를 반환한다. 이 때 문자열 중간에 잇는 공백은 제거되지 않는다.
  • static String valueOf(원시타입 + Object 객체): 지정된 값을 문자열로 변환하여 반환한다. 참조변수의 경우, toString()을 호출한 결과를 반환한다.

CharSequence: 인터페이스로 String, StringBuffer 등의 클래스가 구현하였다.

join()과 StringJoiner

join()은 여러 문자열 사이에 구분자를 넣어서 결합한다. split()과 반대의 작업을 한다고 생각하자.

1
2
3
4
String animals = "dog,cat,bear";
String[] arr = animals.split(",");
String str = String.join("-", arr);
System.out.println(str); //dog-cat-bear

java.util.StringJoiner 클래스를 사용할 수도 있다.

1
2
3
4
5
6
StringJoiner = new StringJoiner (",", "[", "]");
String[] str = {"aaa", "bbb", "ccc"};

for (String s : strArr)
  sj.add(s.toUpperCase());
System.out.println(sj.toString()); //[AAA, BBB, CCC]

유니코드의 보충문자

String 클래스의 메서드 중 메서드 타입이 int인 것들도 있다. 이는 확장된 유니코드를 다루기 위해서이다. 유니코드는 원래 2바이트(16비트) 문자체계인데 이것도 모자라서 20비트로 확장하게 되었다. 그래서 하나의 문자를 char 타입으로 다루지 못하고 int 타입으로 다룰 수밖에 없다. 이렇게 확장에 의해 추가된 문자들을 보충 문자(supplementary characters)라고 한다.

문자 인코딩 변환

getBytes(String charsetName)를 사용하면, 문자열의 문자 인코딩을 다른 인코딩으로 변경할 수 있다.

사용 가능한 문자인코딩의 목록은 System.out.println(java.nio.charset.Charset.availableCharsets())로 출력할 수 있다.

String.format()

형식화된 문자열을 만들어내는 메서드

기본형 값을 String으로 변환

  • String.valueOf() 메서드를 사용한다. => 성능이 더 좋다.

  • 빈 문자열을 더한다. => 더 간단하다.

String을 기본형 값으로 변환

  • Integer.valueOf(String s): Integer 객체 반환
  • Integer.parseInt(String s): int값 반환

두 메서드는 반환 타입만 다를 뿐 같은 역할을 한다. valueOfparseInt 이후에 메서드 이름을 통일하기 위해 추가된 메서드이다.

문자열 “A”를 문자 ‘A’로 변환하려면 char ch = “A”.charAt(0);과 같이 하면 된다.

parseInt()나 parseFloat()같은 메서드는 문자열에 공백 또는 문자가 포함되어 있는 경우 변환 시 예외가 발생할 수 있으므로 주의해야 한다. 그래서 문자열 양끝에 공백을 제거해주는 trim()을 습관적으로 같이 사용하기도 한다.

1
int val = Integer.parseInt(" 123 ".trim());

그러나 부호를 의미하는 ‘+’나 소수점을 의미하는 ‘.’, float형 값을 뜻하는 ‘f’와 같은 자료형 접미사는 자료형에 알맞는 변환을 할 경우에만 허용된다.

문자열을 숫자로 변환하는 과정에서는 예외가 발생하기 쉽기 때문에 주의를 기울이자.

StringBuffer

String 클래스는 인스턴스를 생성할 때 지정된 문자열을 변경할 수 없지만 StringBuffer 클래스는 변경이 가능하다. 내부적으로 문자열 편집을 위한 버퍼(buffer)를 가지고 있으며, StringBuffer 인스턴스를 생성할 때 그 크기를 지정할 수 있다. 지정하지 않으면 16개의 문자를 저장할 수 있는 크기의 버퍼를 생성한다. 작업하려는 문자가 배열의 길이보다 작을 때는 내부적으로 버퍼의 크기를 증가시키는 작업이 수행된다. 배열의 길이는 변경될 수 없으므로 새로운 길이의 배열을 생성한 후에 이전 배열의 값을 복사해야 한다. 이러면 인스턴스 char[] 변수는 길이가 증가된 새로운 배열을 참조하게 된다.

append()의 반환타입은 StringBuffer인데, 자신의 주소를 반환한다. 따라서 연속적으로 append를 호출할 수 있다. (sb.append("123").append("123"))

StringBuffer는 equals를 오버라이딩하지 않아 StringBuffer 객체로 equals()를 호출하면 등가비교연산자로 비교한 것과 같다. 반면 toString()은 오버라이딩되어 있다. 따라서 StringBuffer 인스턴스에 담긴 문자열을 비교하기 위해서는 toString()을 호출해서 String 인스턴스를 얻은 다음, 여기에 equals 메서드를 사용해서 비교해야 한다.

1
2
3
String s = sb.toString();
String s2 = sb2.toString();
System.out.println(s.equals(s2));

StringBuffer 클래스 역시 문자열을 다루기 위한 것이기 때문에 String 클래스와 유사한 메서드를 가지고 있다. 추가, 변경, 삭제와 같이 저장된 내용을 변경할 수 있는 메서드들이 추가로 제공된다.

  • StringBuffer(): 16문자를 담을 수 있는 버퍼를 가진 인스턴스 생성
  • StringBuffer(int length)
  • StringBuffer(String str)
  • StringBuffer append(원시타입 / char[] / Object / String )

  • int capacity(): 버퍼크기를 알려준다.
  • int length(): 버퍼에 담긴 문자열의 길이를 알려준다.
  • char charAt()
  • StringBuffer delete(int start, int end)
  • StringBuffer deleteCharAt(int index)
  • StringBuffer insert(int pos,원시타입/char[]/Object/String): 매개변수로 받은 값을 문자열로 변환하여 지정된 위치(pos)에 추가

  • StringBuffer replace(int start, int end, String str)

  • StringBuffer reverse(): 문자열의 순서를 거꾸로 나열한다.
  • void setCharAt(int index, char ch): 지정된 위치의 문자를 주어진 문자로 바꾼다.
  • void setLength(int newLength): 문자열의 길이를 변경한다. 길이를 늘리는 경우 나머지 빈 공간을 널문자 ‘\u0000’로 채운다.
  • String toString()
  • String subString(int start), (int start, int end)

StringBuilder

StringBuffer는 멀티스레드에 안전(thread safe)하도록 동기화되어있다. 그러나 멀티스레드로 작성된 프로그램이 아닌 경우, StringBuffer의 동기화는 불필요하게 성능만 떨어뜨리게 된다. 따라서 StringBuffer에서 스레드의 동기화만 뺀 StringBuilder가 새로 추가되었다. StringBuilder는 StringBuffer와 완전히 똑같은 기능으로 작성되어있어서 교체가 쉽다. 즉 StringBuffer타입의 레퍼런스와 생성자만 바꾸면 된다는 말이다.

하지만 성능향상이 반드시 필요한 경우를 제외하고는 기존에 작성한 코드에서 StringBuffer를 STringBuilder로 굳이 바꿀 필요는 없다.

This post is licensed under CC BY 4.0 by the author.

코어 자바스크립트 #2.11: 논리연산자

코어 자바스크립트 #2.12: null 병합 연산자 '?'

Loading comments from Disqus ...