java.lang 패키지 알아보기
java.lang 패키지는 출력문을 나타내는 System 클래스, Object 클래스, String 클래스 등, 자바 프로그래밍에서 가장 기본이 되는 클래스들을 포함하는 패키지이다. 그래서 import 없이도 사용할 수 있다.
이번에는 java.lang 패키지와 그 하위요소들에 대해 알아보기로 한다.
1. Object 클래스
Object 클래스는 모든 클래스의 상위 클래스이다.
public class Object {
@IntrinsicCandidate
public Object() {}
@IntrinsicCandidate
public final native Class<?> getClass();
@IntrinsicCandidate
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
@IntrinsicCandidate
public final native void notify();
@IntrinsicCandidate
public final native void notifyAll();
public final void wait() throws InterruptedException {
wait(0L);
}
public final native void wait(long timeoutMillis) throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
if (timeoutMillis < 0) {
throw new IllegalArgumentException("timeoutMillis value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
timeoutMillis++;
}
wait(timeoutMillis);
}
@Deprecated(since="9")
protected void finalize() throws Throwable { }
}
Object 클래스의 모든 메서드 코드는 다음과 같다. 오직 11개의 메서드를 가지고 있으며 자바의 모든 클래스들은 모두 이 11개의 메서드를 상속받아 사용할 수 있다.
우선은 noitfy, notifyAll, wait 메서드는 스레드와 관련된 메서드들이기 때문에 여기서는 안다룬다고하고.. 나머지 메서드에 대해서 다뤄보기로한다.
equals(Object obj)
public boolean equals(Object obj) {
return (this == obj);
}
두 개의 객체를 동일성 여부를 조사한다.
public class Main {
public static void main(String[] args) {
Value v1 = new Value(10);
Value v2 = new Value(10);
System.out.println(v1.equals(v2)); //false
v1 = v2;
System.out.println(v1.equals(v2)); //true
}
}
class Value{
int number;
Value(int number){
this.number = number;
}
}
기본적인 equals 메서드는 == 연산이기 때문에 별다른 재정의를하지 않는 한 객체의 주소값을 조사할 것이고 주소값이 같지 않으면 false를 반환한다.
public class Main {
public static void main(String[] args) {
Value v1 = new Value(10);
Value v2 = new Value(10);
System.out.println(v1.equals(v2)); //true
v1 = v2;
System.out.println(v1.equals(v2)); //true
}
}
class Value{
int number;
Value(int number){
this.number = number;
}
@Override
public boolean equals(Object obj) {
return (this.number == ((Value)obj).number);
}
}
이렇게 프로그래머가 원하는대로 equals() 메서드를 재정의하면 특정 조건에 따라서 equals() 메서드를 실행했을 때 true가 나오게할 수 있다. 대표적으로 String 클래스에서 이렇게 재정의를 사용하여 주소값이 달라도 같은 문자열이라면 equals() 메서드를 사용했을 때 true를 반환하도록 하는 식으로 재정의한다.
다만 재정의를 위해서라면 반드시 메서드의 매개변수를 Object로 받도록 설정해야한다. 그렇지 않으면 상속이 아니라 다중정의가 되어버려서 문제가 생긴다.
hashCode()
@IntrinsicCandidate
public native int hashCode();
해시함수를 구현한 코드다. 찾고자하는 값을 입력한다면 그 값의 주소값을 기반으로한 해시코드를 하나 반환한다. 그래서 주소값이 같으면 해시코드도 같다고 나올 것이다.
다만 해시코드의 수는 고정되어있기 때문에 변수가 많아진다면 해시코드가 같은 두 객체가 존재할 수 있다.
다만 Object 클래스에 정의된 hashCode()는 객체의 주소값으로 해시코드를 만들어내기 때문에 32 bit JVM에서는 서로 다른 두 객체는 같은 해시값을 가질 수 없다.
32 bit JVM에서는 주소값을 4바이트로 저장하는데, 해시값은 int형이라 해시값도 4바이트여서 메모리를 풀로 쓰더라도 하나의 객체에 하나의 해시값을 매칭시킬 수 있다.
다만 64 bit JVM에서는 주소값을 8바이트로 저장하는데 (타입 상관없이, C와 동일), 해시값은 4바이트로 저장되기 때문에 해시코드가 중복될 수 있다. 저장할 수 있는 객체의 주소는 2^64개인 반면, 해시값은 2^32개로만 표기가 가능하기 때문.
그래서 객체가 같다는 것을 조사할 때는 일차적으로 hashCode() 메서드를 통해 두 객체의 해시코드 값이 같은지 조사하게되며, 해시값이 같다면 그 다음에 equals() 메서드를 호출하여 최종적으로 그 값이 같은지 여부를 조사하게 된다.
이 방식은 그냥 일반적인 String이나 참조형변수 객체를 가져오는 경우에는 equals() 메서드를 호출하면되긴하지만, 나중에 컬렉션에서 Set 객체를 활용할 경우 (Set 객체는 중복된 요소가 있을 경우 하나만 저장하는 컬렉션 객체다.) hashCode() -> equals() 호출을 통해 객체의 값이 같은지 여부를 조사하므로 일반적으로 equals() 메서드를 건드리면 값이 같을 경우 같은 해시코드를 반환하도록 hashCode() 값도 재정의해야한다.
public class Main {
public static void main(String[] args) {
Value v1 = new Value(10);
Value v2 = new Value(10);
System.out.println(v1.equals(v2));
v1 = v2;
System.out.println(v1.equals(v2));
}
}
class Value{
Integer number;
Value(int number){
this.number = number;
}
@Override
public int hashCode(){
return number.hashCode();
}
@Override
public boolean equals(Object obj) {
return (this.number == ((Value)obj).number);
}
}
대충 이런 식으로..
String이나 Integer 같은 자주 쓰이는 객체들에는 이미 hashCode() 메서드가 재정의되어 있으므로 인스턴스가 있을 경우 이걸 이용해서 hashCode()를 구현하는 경우가 있다.
또한 int, char, double 같은 기본형 변수들은 객체가 아니기 때문에 hashCode() 메서드가 없다. Integer, String, Double 같이 참조형 변수로 바꿔서 사용해야한다.
추가로 선언부에 native 키워드는 C 또는 C++ 등 다른 언어로 작성된 코드이며, 코드가 JNI를 사용하여 네이티브 코드로 구현되었음을 의미한다.
toString()
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
인스턴스의 정보를 문자열로 변환한다.
재정의되지 않은 toString() 메서드를 그대로 사용하면 "클래스이름@16진수해시코드" 값을 얻는다.
그래서 위에서 hashCode()를 만졌다면 해시코드가 같은 경우를 충족시켰을 때 그냥 toString을 이용해 출력하면 해시코드 부분은 같은 값을 출력하게된다.
아마 파이썬의 __str__와 대응되는 것 같은데, __str__ 쓸 때는 되게 머리아팠는데 왜 이건 괜찮아보이지
public class Main {
public static void main(String[] args) {
Value v1 = new Value(10);
System.out.println(v1.toString());
}
}
class Value{
Integer number;
Value(int number){
this.number = number;
}
@Override
public String toString() {
return (getClass().getName() + "@" + number.toString());
}
}
이렇게 재정의하여 원하는 문자열을 출력하도록 재정의할 수도 있다.
보통 toString() 메서드는 인스턴스, 클래스의 정보나 인스턴스 변수들의 값을 문자열로 변환하여 출력하는 것이 보통이다.
clone()
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
자기자신을 복제하여 새 인스턴스를 생성한다.
clone() 메서드를 사용하려는 객체는 Cloneable 인터페이스를 구현해야하며, 그렇지 않은 경우에는 예외가 발생한다.
Cloneable은 마커인터페이스로, 객체가 무단으로 복제되는 것을 막기 위해서 이 인터페이스의 구현체인 경우에만 clone() 이 가능하도록 설정되어있다. 만약 Cloneable 인터페이스를 확장했다면 개발자가 복제를 허용했다는 뜻이다.
public class Main{
public static void main(String[] args) {
Value v1 = new Value(10);
Value v2 = (Value) v1.clone();
System.out.println(v2);
}
}
class Value implements Cloneable{
Integer number;
Value(int number){
this.number = number;
}
@Override
public String toString(){
return "the number is " + number.toString();
}
@Override
public Object clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}
이렇게 사용가능하며, clone() 메서드는 protected 로 접근제어자가 설정되어있기 때문에 (다른패키지에서 호출할 경우 하위클래스를 제외하고는 접근불가) public으로 접근제어자를 재설정하여 재정의해야 사용할 수 있다.
공변 반환타입
공변 반환타입은 clone() 메서드의 기능에 다형성을 더한 것이다.
조상 메서드의 반환타입을 자손 메서드의 반환타입으로 설정하는 것이다.
@Override
public Object clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
Value v1 = new Value(10);
Value v2 = (Value) v1.clone();
위의 예시에서는 Object 타입으로 clone() 메서드의 반환타입이 지정되었고, 복사된 객체를 인스턴스로 만들 때 형변환하도록 했으나
@Override
public Value clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return (Value) obj;
}
Value v1 = new Value(10);
Value v2 = v1.clone();
이렇게 return문에서 형변환되도록 설정할 수 있다. JDK5부터 가능하다.
Object 클래스의 clone() 타입의 반환타입은 Object 타입인데, Object 타입은 모든 클래스의 상위클래스이므로 어느 타입이던 clone() 메서드의 반환타입이 되도록 설정할 수 있다.
얕은복사? 깊은복사?
public class Main{
public static void main(String[] args) {
Value v1 = new Value(10);
Value v2 = v1.clone();
System.out.println(v1.number.equals(v2.number));
}
}
class Value implements Cloneable{
Number number;
Value(int number){
this.number = new Number(number);
}
@Override
public Value clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return (Value) obj;
}
}
class Number {
int number;
Number(int number){
this.number = number;
}
}
실용성은 없지만 이런 코드가 있다고치자.
앞서 나온 clone() 메서드는 얕은 복사에 해당한다. 얕은 복사란 복제하려는 객체가 참조중인 인스턴스 객체(H라고 칭하겠다.) 가 있을 경우, 얕은복사 진행후 H객체는 복제되지않고 원본객체와 복제된 객체가 모두 그 객체를 참조 중이다.
즉, 원본과 복사본 모두 같은 객체를 참조하도록 복사하는 것을 얕은복사라고 할 수 있겠다.
다만 참조의 개념이 없는 기본형 객체에서는 큰 상관이 없고, 참조형객체들에게서 상관이 있을 것이다.
public class Main{
public static void main(String[] args) {
Value v1 = new Value(10);
Value v2 = v1.DeepCopy();
System.out.println(v1.number.equals(v2.number));
}
}
class Value implements Cloneable{
Number number;
Value(int number){
this.number = new Number(number);
}
@Override
public Value clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return (Value) obj;
}
public Value DeepCopy(){
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
Value v = (Value) obj;
v.number = new Number(this.number.number);
return v;
}
}
class Number {
int number;
Number(int number){
this.number = number;
}
}
깊은 복사는 복사하려는 객체가 참조 중인 객체까지 모두 복사해내는 것이다.
위는 깊은 복사 코드로, 얕은복사 코드에 복사된 객체가 참조 중인 객체까지 새로 만들어내어 반환하였다.
getClass()
자신이 속한 클래스의 Class 객체를 반환한다.
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement,
TypeDescriptor.OfField<Class<?>>,
Constable { ... }
Class 객체는 말 그대로 클래스 이름이 Class인 객체다.
모든 클래스마다 한 개씩 존재하며, 클래스파일이 메모리에 올라갈 때 생성된다. (클래스로더가 클래스를 메모리에 올릴 때 만든다.)
클래스로더는 클래스가 생성되면 동적으로 메모리에 클래스를 올린다. 기존에 생성된 객체가 존재하면 그 객체의 참조를 반환하며, 없다면 새 클래스를 만든다. 패키지 내 클래스가 없다면 ClassNotFoundException을 발생시킨다.
https://velog.io/@ddangle/Java-%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A1%9C%EB%8D%94%EB%9E%80
[Java] 클래스 로더란?
JVM 내의 클래스로더는 어떻게 동작하는지 어떤 종류가 있는지 알아보자!
velog.io
이걸 확인해보면 좋을듯하다.
어쨌든 클래스는 파일형태로 저장되어 있는데, 객체가 생성되면 클래스로더가 그 파일을 읽고, Class 클래스에 정의된 형식으로 변환해 메모리에 갖다두는 형식으로 클래스가 불러와진다. 이렇게 JVM이 읽고 실행시키기 좋은 형태로 저장한 것이 Class 객체고, 여기에 모든 클래스의 정보가 담겨있다.
이러한 Class 객체에 대한 참조를 가져오는 방법은
Value v1 = new Value(10);
Class cobj = v1.getClass();
cobj = Value.class;
cobj = Class.forName("Value");
이렇게 불러올 수 있다.
forName() 메서드는 ClassNotFoundException이 발생할 수 있으므로 예외처리를 하던가 메서드에 예외선언을 해야한다.
리플렉션 API
Class 객체를 사용하면 클래스의 생성자필드, 메서드 정보를 알아낼 수 있는데, 이를 리플렉션이라고한다.
런타임 시점에서 어떤 타입의 클래스를 사용해야할 지 모르지만, 런타임 시점에서 지금 실행 중인 클래스를 가져와서 사용해야하는 경우에 리플렉션이 사용된다.
실제 자바 개발에서는 딱히 쓰이는 일이 많지않고, 스프링 JPA, 아니면 DI 등 어느 객체가 들어오든 일괄적으로 메서드를 실행시켜야할 때 필요하다.
이부분은 아직은 굳이 필요없기도하고 나중에 알아보기로한다.
https://velog.io/@dongvelop/Java-%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98Reflection%EC%9D%B4%EB%9E%80
[Java] 리플렉션(Reflection)이란?
Java 리플렉션에 대해 학습합니다.
velog.io
이걸 참고하면 좋을듯하다.
