수학과의 좌충우돌 프로그래밍

[JAVA] 07. java.lang 패키지 본문

프로그래밍 언어/Java

[JAVA] 07. java.lang 패키지

ssung.k 2021. 1. 13. 23:05

java.lang 패키지

java.lang 패키지는 자바프로그래밍에 가장 기본이 되는 클래스들을 포함하고 있습니다.

그렇기 때문에 해당 패키지에 클래스들은 import문 없이도 사용할 수 있습니다.

 

Object 클래스

Object 클래스는 모든 클래스의 최고 조상이기 때문에 멤버들은 모든 클래스에서 바로 사용가능합니다.

멤버변수는 따로 없으며 11개의 메서드를 가지고 있습니다.

 

equals(Object obj)

매개변수로 객체의 참조변수를 받아서 비교하여 그 결과를 boolean값으로 알려주는 역할을 합니다.

public boolean equals(Object obj){
     return (this == obj);
}

 

두 객체의 같고 다름을 참조변수의 값으로 판단합니다.

그렇기 때문에 서로 다른 두 객체를 equals 메서드로 비교하면 항상 false를 반환합니다.

 

결국 equals메서드는 두 개의 참조변수가 같은 객체를 참조하고 있는지만 판단할 수 있습니다.

equals 메서드를 오버라이딩해서 인스턴스가 가지고 있는 변수를 비교하도록 해봅시다.

class Person {
    long id;
    
    public boolean equals(Object obj) {
        if (obj instanceof Person)
            return id == ((Person)obj).id;
        else 
            return false;
    }
}

 

String 클래스 역시 equals 메서드를 그대로 사용하지 않고 위와 유사하게 오버라이딩하여 String 인스턴스가 갖는 문자열 값을 비교하도록 되어있습니다.

 

 

hashCode()

해당 메서드는 해시함수를 구현한 메서드입니다.

일반적으로 해시코드가 같은 두 객체가 존재하는 것이 가능하지만 Object클래스에 정의된 hashCode메서드는 객체의 주소값으로 해시코드를 만들어 반환하기 때문에 32bit JVM에서는 서로 다른 두 객체는 결코 같은 해시코드를 가질 수 없습니다.

하지만 64bit JVM에서는 8 byte 주소값으로 해시코드(4byte)를 만들기 때문에 해시코드가 중복될 수 있습니다.

 

위에서 equals 메서드를 오버라이딩한 것과 같이 hashCode도 적절히 오버라이딩해야 클래스의 인스턴스 변수 값으로 객체의 같고 다름을 판단할 수 있습니다.

 

 

toString()

인스턴스에 대한 정보를 문자열로 반환하는 메서드입니다.

Obect클래스에 정의된 toString()은 아래와 같습니다.

public String toString() {
    return getClass().getName()+"@"+Integer.toHexString(hashCode());
}

 

하지만 해당 정보는 가독성이 떨어지고 활용가치가 없기 때문에 여러 인스턴스 변수를 조합하여 toString()을 오버라이딩하여 사용합니다.

 

clone()

자신을 복제하여 새로운 인스턴스를 생성하는 메서드입니다.

원래의 인스턴스는 보존한 체로 clone을 통해 새로운 인스턴스를 만들어 백업을 가능하게 합니다.

 

Object클래스에 정의된 clone()은 단순히 인스턴스 변수의 값만 복사하기 때문에 참조타입의 인스턴스 변수가 있는 클래스는 완전히 인스턴스 변수가 있는 클래스는 완전한 인스턴스 복제가 이루어지지 않습니다.

예를 들어 배열을 참조변수로 가지는 경우, 배열의 주소가 복사 되기 때문에 clone()으로 만들어진 새로운 인스턴스도 같은 배열을 참조하게 되는 문제가 생깁니다. 이 경우에는 별도로 오버라이딩이 필요합니다.

 

또한 인스턴스의 데이터를 보호하기 위해서 Cloneable 인터페이스를 구현한 클래스의 인스턴스만 clone() 메서드를 사용할 수 있습니다.

public class HelloWorld {

	public static void main(String[] args) {
		Point original = new Point(3);
		Point copy = (Point) original.clone();
		
		System.out.println(original);
    // x: 3

		System.out.println(copy);
    // x: 3
	}
}

class Point implements Cloneable {
	int x;
	
	Point(int x){
		this.x = x;
	}
	
	public String toString() {
		return "x: " + x;
	}
	
	public Object clone() {
		Object obj = null;
		try {
			obj = super.clone();
		} catch (CloneNotSupportedException e) {}
		
		return obj;
	}
}

 

 

공변 반환타입

JDK 1.5 부터 공변 반환타입이 추가되었습니다.

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

 

위 Point의 경우, clone을 오버라이딩하며 반환 타입이 Object이고 그렇기 때문에 아래와 같이 추가적인 형변환이 필요합니다.

Point copy = (Point) original.clone();

 

이를 방지하기 위해 아래와 같이 작성이 가능합니다.

이제는 clone 메서드를 사용할 시 형변환이 필요없습니다.

public Point clone() {
  Object obj = null;
  try {
    obj = super.clone();
  } catch (CloneNotSupportedException e) {}

  return (Point)obj;
}

 

 

얕은 복사와 깊은 복사

복사에는 두 가지 방식이 있습니다.

clone()은 단순히 객체에 저장된 값을 그대로 복제할 뿐이라 객체가 참조하고 있는 객체까지 복제하지 않습니다.

이는 얕은 복사라고 하며, 내부적으로 단순히 clone()을 사용합니다.

 

깊은 복사의 경우, 안에 있는 객체까지 새로 만들어 기본의 객체와 새로 만든 객체의 의존성을 없앱니다.

 

 

getClass()

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

Class 객체란 이름이 'Class' 인 클래스의 객체입니다.

Class 클래스는 아래와 같이 정의되어 있습니다.

public final class Class implements ... {
  
}

 

Class 객체는 클래스의 모든 정보를 담고 있으며 클래스 당 한 개만 존재합니다.

그리고 클래스 파일이 클래스 로더에 의해서 메모리에 올라갈 때 자동으로 생성됩니다.

클래스 로더는 실행 시에 필요한 클래스를 동적으로 메모리에 로드하는 역할을 합니다.

다시 말해 파일 형태로 저장되어 있는 클래스를 읽어서 Class 클래스에 정의된 형식으로 변환하는 것입니다.

즉, 클래스 파일을 읽어서 사용하기 편한 형태로 저장해 놓은 것이 클래스 객체입니다.

 

Class 객체에 대한 참조를 얻는 방법은 여러가지가 있습니다.

Class classObj = new Point().getClass();
Class classObj = Point.class;
Class classObj = Class.forName("Point");

 

특히 forName()은 특정 클래스 파일, 예를 들어 데이터베이스 드라이버를 메모리에 올릴 때 주로 사용합니다.

public class HelloWorld {

	public static void main(String[] args) {
		Class classObj = new Point().getClass();

		System.out.println(classObj.getName());
    // Point

		System.out.println(classObj.toGenericString());
    // class Point
	}
}

 

 

String 클래스

자바에서는 문자열을 위한 클래스를 제공합니다.

 

변경 불가능한 클래스

String 클래스에는 문자열을 저장하기 위해서 문자열 배열 참조변수를 인스턴스 변수로 정의해놓고 있습니다.

public final class String implements java.io.Serializable, Compareble {
    private char[] value;
}

String 클래스는 앞에 final이 붙어있으므로 다른 클래스의 조상이 될 수 없습니다.

 

한 번 생성된 String 인스턴스가 갖고 있는 문자열을 읽어 올 수만 있고 변경할 수는 없습니다.

+ 연산자를 통해서 문자열을 결합하는 것은 매 연산 시 마다 새로운 문자열을 가진 String인스턴스가 생성되어 메모리 공간을 차지하므로 가능한 한 결합횟수를 줄이는 것이 좋습니다.

문자열 간의 결합이 많이 필요할 때는 StringBuffer 클래스를 사용하는 것이 좋습니다. 이는 내부 문자열을 변경하여 사용할 수 있습니다.

 

문자열의 비교

문자열을 만들 때는 두 가지 방법, 문자열 리터럴을 지정하는 방법과 String 클래스의 생성자를 사용해서 만드는 방법이 있습니다.

String 클래스의 생성자를 이용한 경우에는 new 연산자에 의해서 메모리할당이 이루어지기 때문에 항상 새로운 String 인스턴스가 생성되지만 문자열 리터럴은 이미 존재하는 것을 재사용합니다.

public class HelloWorld {

	public static void main(String[] args) {
		String str1 = "abc";
		String str2 = "abc";
		String str3 = new String("abc");
		String str4 = new String("abc");
		
		System.out.println(System.identityHashCode(str1)); // 607635164
		System.out.println(System.identityHashCode(str2)); // 607635164
		System.out.println(System.identityHashCode(str3)); // 529116035
		System.out.println(System.identityHashCode(str4)); // 242481580
	}
}

 

 

빈 문자열

자바에서는 길이가 0인 배열이 존재할 수 있습니다.

C언어에서는 길이가 0인 배열이 존재할 수 없습니다.

String s = "" 과 같은 문장이 있을 때 참조변수 s가 참조하고 있는 String 인스턴스는 내부에 new char[0] 과 같이 길이가 0인 char형 배열을 저장하고 있는 것입니다.

 

하지만 char c = '' 와 같은 표현은 허용되지 않습니다. char형 변수는 반드시 하나의 문자를 지정해야 합니다.

일반적으로 변수를 선언할 때 각 타입의 기본값으로 초기화를 하지만 String은 null대신 빈 문자열로 초기화합니다.

 

 

유니코드의 보충문자

String 클래스의 메서드 중에서 매개변수 타입이 char인 것도 있고 int인 것도 있습니다.

int lastIndexOf(int ch)
String replace(char old, char nw)

 

그 이유는 확장된 유니코드 때문입니다.

유니코드는 원래 2byte, 즉 16비트 문자체계인데 부족하여 20비트로 확장하였습니다.

그래서 하나의 문자를 char타입으로 다루지 못하고 int타입으로 다루기도 합니다.

확장에 의해 새로 추가된 문자들을 보충문자라고 합니다.

 

lastIndexOf는 매개변수 타입이 int 이므로 보충문자를 지원하고 replace는 char이므로 보충문자를 지원하지 않습니다.

 

 

문자 인코딩 변환

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

자바가 UTF-16을 사용하지만 문자열 리터럴에 포함되는 문자들은 OS의 인코딩을 사용합니다.

public static void main(String[] args) {
  String str = "가";
  try {
    byte[] utf8_bytes = str.getBytes("UTF-8");
    String utf8_str = new String(utf8_bytes, "UTF-8"); 
    // getBytes는 byte[]을 반환하므로 String타입으로 변환하기 위해서는 다음과 같이 해줘야합니다.
    System.out.println(utf8_str);
    // 가
  } catch (UnsupportedEncodingException e) {
    e.printStackTrace();
  }
}

 

 

기본형과 String 간의 변환

기본형을 String으로 바꾸기 위해서는 valueOf()를 사용할 수 있습니다.

또한 빈문자열과의 연산을 통해서도 String으로 바꿀 수 있습니다.

public static void main(String[] args) {
  Boolean canBeChanged = true;

  System.out.println(canBeChanged + ""); // true
  System.out.println((canBeChanged + "").getClass().getName()); // java.lang.String

  System.out.println(String.valueOf(canBeChanged)); // true
  System.out.println(String.valueOf(canBeChanged).getClass().getName()); // java.lang.String
}

성능은 valueOf()가 더 좋지만 빈 문자열을 더하는 방법이 더 간편하여 상황에 맞게 사용하면 됩니다.

 

String을 기본형으로 바꾸기 위해서도 valueOf()를 사용할 수 있습니다.

또한 parse{자료형}도 가능합니다.

public static void main(String[] args) {
  String num = "100";

  System.out.println(Integer.parseInt(num));	// 100
  System.out.println(Integer.valueOf(num));   // 100
}

 

Integer.valueOf(num)의 반환타입은 int가 아니라 Integer인데 int로 자동 변환됩니다.

valueOf는 내부적으로 parse{자료형}를 호출합니다.

 

 

StringBuffer클래스와 StringBuilder클래스

String 클래스는 문자열을 변경할 수 없는데 반해, StringBuffer 클래스는 변경이 가능합니다.

내부적으로 버퍼를 가지고 있어 인스턴스 생성 시 그 크기를 지정할 수 있습니다.

문자열이 버퍼의 길이를 넘어서게 되면 버퍼의 길이를 늘려주는 작업이 추가로 수행되어야 하기 때문에 버퍼의 크기를 여유롭게 잡아주는 것이 좋습니다.

public final class StringBuffer implements java.io.Serializable {
    private char[] value;
}

 

StringBuffer의 생성자

public StringBuffer(int length){
    value = new char[length];
    shared = false;
}

// 버퍼의 크기를 지정하지 않으면 버퍼의 크기는 16
public StringBuffer(){
    this(16);
}

// 지정된 문자열의 길이보다 16이 더 크게 버퍼를 생성
public StringBuffer(String str){
    this(str.length()+16);
    append(str);
}

 

StringBuffer의 인스턴스로 문자열을 다룰 때 버퍼의 크기가 작업하려는 문자열의 크기보다 작으면 내부적으로 버퍼의 크기를 증가시킵니다.

배열의 길이를 변경할 수 없으므로 새로운 길이의 배열을 생성한 후에 이전 배열의 값을 복사합니다.

 

StringBuffer의 변경

append 메서드를 통해 StringBuffer 인스턴스에 문자열을 추가할 수 있습니다.

이 때 append 메서드는 StringBuffer의 주소를 반환합니다.

따라서 아래 예제에서 sb와 sb2는 같은 주소를 가집니다.

StringBuffer sb = new StringBuffer("hello");
StringBuffer sb2 = sb.append(" world"); 

System.out.println(sb);
// hello world
System.out.println(sb2);
// hello world

 

StringBuffer의 비교

String클래스에서는 equals메서드를 오버라이딩해서 문자열의 내용을 비교하도록 구현되어 있지만 StringBuffer에서는 equals 메서드를 사용해도 등가비교 연산자로 비교한 것과 같은 결과를 얻습니다.

StringBuffer sb = new StringBuffer("hello");
StringBuffer sb2 = new StringBuffer("hello");

System.out.println(sb==sb2);
// false
System.out.println(sb.equals(sb2));
// false

 

따라서 이 경우에는 String 인스턴스로 변경한 후에 equals 메서드를 통해 비교해야합니다.

StringBuffer sb = new StringBuffer("hello");
StringBuffer sb2 = new StringBuffer("hello");

String s = sb.toString();
String s2 = sb2.toString();

System.out.println(s.equals(s2));
// true

 

 

StringBuilder

위에서 살펴본 StringBuffer는 멀티쓰레드에 안전하도록 동기화되어 있습니다.

그렇기 때문에 성능에 영향을 미칩니다.

 

StringBuilder는 StringBuffer에서 쓰레드 동기화만 뺀 클래스입니다.

StringBuffer와 똑같이 사용할 수 있으며 더 좋은 성능을 기대할 수 있습니다.

 

물론 그렇다고 해서 StringBuilder가 성능이 나쁜 것이 아니기 때문에 성능향상이 반드시 필요한 경우에만 StringBuilder로 대체하도록 합시다.

 

 

래퍼(wrapper) 클래스

객체지향 개념에서 모든 것은 객체로 다루어져야 합니다.

그러나 자바에서는 8개의 기본형을 개체로 다루지 않는데 이 때문에 자바가 완전한 객체지향 언어가 아니라는 이야기를 듣기도 합니다.

그 대신 높은 성능을 기대할 수 있습니다.

 

때로는 기본형 변수도 어쩔 수 없이 객체로 다뤄야하는 경우가 있습니다.

이 때 사용하는 것이 래퍼 클래스입니다.

8개의 기본형을 대표하는 8개의 래퍼클래스가 있는데 이 클래스들을 이용하면 기본형 값을 객체로 다룰 수 있습니다.

기본형 래퍼클래스 생성자
boolean Boolean Boolean(boolean value)
Boolean(String s)
char Character Character(char value)
byte Byte Byte(byte value)
Byte(String s)
short Short Short(short value)
Short(String s)
int Integer Integer(int value)
Integer(String s)
long Long Long(long value)
Long(String s)
float Float Float(double value)
Float(float value)
Float(String s)
double Double Double(double value)
Double(String s)

 

래퍼 클래스들은 모두 equals()가 오버라이딩되어 있어서 주소값이 아닌 객체가 가지고 있는 값을 비교합니다.

 

 

Number 클래스

이 클래스는 추상클래스로 내부적으로 숫자를 멤버변수로 갖는 래퍼 클래스들의 조상입니다.

Ojbect
  Boolean
  Character
  Number
    Byte
    Short
    Integer
	  Long
    Float
	  Double
    BigInteger
    BigDecimal

앞에서 봤던 클래스 외에도 BigInteger, BigDecimal가 있습니다.

각각 long과 double로 다룰 수 없는 큰 범위의 수를 처리하기 위한 클래스입니다.

 

문자열을 숫자로 변환하기 위한 방법으로 타입.parse타입, 타입.valueOf() 를 앞에서 살펴보았습니다.

이 둘의 가장 결정적인 차이는 parse는 기본형을 반환하고, valueOf는 래퍼 클래스를 반환합니다.

 

 

오토박싱(autoboxing)과 언박싱(unboxing)

JDK 1.5 이전에는 기본형과 참조형 간의 연산이 불가능했지만 이제는 가능합니다.

가능한 이유는 자바 언어의 규칙이 바뀐 것이 아니라 컴파일러가 자동으로 변환하는 코드를 넣어주기 때문입니다.

 

아래의 예를 봅시다.

// 컴파일 전
int i = 5;
Integer iObj = new Integer(7);

int sum = i + iObj;

// 컴파일 후
int i = 5;
Integer iObj = new Integer(7);

int sum = i + iObj.intValue();

 

기본형 값을 래퍼 클래스의 객체로 자동 변환해주는 것을 오토박싱이라고 하고 반대로 래퍼 클래스를 기본형으로 변환하는 언박싱이라고 합니다.

Comments