Java.lang 패키지
자바가 기본으로 제공하는 라이브러리(클래스 모음) 중에 가장 기본이 되는 것이 java.lang 패키지 입니다.
자바 언어를 이루는 가장 기본이 되는 클래스들을 보관하는 패키지를 뜻합니다.
java.lang 패키지의 대표적인 클래스들
- Object: 모든 자바 객체의 부모 클래스
- String: 문자열
- Interger, Long, Double: 래퍼 타입, 기본형 데이터 타입을 객체로 만든 것
- Class: 클래스 메타 정보
- System: 시스템과 관련된 기본 기능들을 제공
import 생략 가능
java.lang 패키지는 모든 자바 애플리케이션에 자동으로 임포트(import) 됩니다.
따라서 임포트 구문을 사용하지 않아도 됩니다.
package lang;
import java.lang.System; // 생략가능
public class LangMain {
public static void main(String[] args) {
System.out.println("hello java.lang");
}
}
- import java.lang.System; 코드를 삭제해도 정상 작동한다.
Object 클래스
- 자바에서 모든 클래스의 최상위 부모 클래스는 항상 Object 클래스이다.
- 자바에서 모든 객체의 최종 부모는 Object 이다.
// 부모가 없으면 묵시적으로 Object 클래스를 상속받는다.
public class Parent{
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
public class Parent extends Object{
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
클래스에 상속 받을 부모 클래스가 없으면 묵시적으로 Object 클래스를 상속 받습니다. (생략권장)
public class Child extends Parent{ // 명시적 상속
public void childMethod() {
System.out.println("Child.childMethod");
}
}
public class ObjectMain {
public static void main(String[] args) {
Child child = new Child();
child.childMethod();
child.parentMethod();
// toString()은 Object 클래스의 메서드
// 객체에 대한 정보를 출력해줌(참조 값)
String string = child.toString();
System.out.println(string);
}
}
Parent는 Object를 묵시적으로 상속 받았기 때문에 메모리에도 함께 생성됩니다.
- child.toString( )을 호출한다.
- 먼저 본인 타입인 Child 에서 toString( )을 찾아보지만 없어서 부모 타입으로 올라가서 찾는다.
- 부모 타입인 Parent에서 찾아보지만 없어서 부모 타입으로 올라가서 찾는다.
- 부모 타입인 Object에서 찾는다. Object에 toString( ) 메서드를 호출한다.
"그렇다면 자바에서 Object 클래스가 최상위 부모 클래스인 이유는 뭘까?"
- 공통 기능 제공
- 다형성의 기본 구현
Object는 모든 객체에 필요한 공통 기능을 제공합니다.
Object는 최상위 부모 클래스이기 때문에 모든 객체는 공통 기능을 편리하게 제공(상속) 받을 수 있습니다.
공통 기능 제공
toString( ) 같은 기능의 객체를 만들 때 마다 항상 새로운 메서드를 정의해서 만들어야 한다면 개발자마다
서로 다른 이름의 메서드를 만들어서 일관성이 깨질 수 있습니다.개발자는 모든 객체가 앞서 설명한 메서드를
지원한다는 것을 알고 있다면, 프로그래밍이 단순화되고, 일관성을 가지게 됩니다.
Object가 제공하는 기능은 다음과 같습니다.
- 객체의 정보를 제공하는 toString( )
- 객체의 같음을 비교하는 equals( )
- 객체의 클래스 정보를 제공하는 getClass( )
- 기타 여러가지 기능
다형성의 기본 구현
Object는 모든 클래스의 부모 클래스입니다. 따라서 모든 객체를 참조할 수도 있습니다.
Object 클래스는 다형성을 지원하는 기본적인 메커니즘을 제공하고 있습니다.
모든 자바 객체는 Object 타입으로 처리될 수 있으며, 이는 다양한 타입의 객체를 통합적으로 처리할 수 있게 도와줍니다. 쉽게 이야기하면 Object는 모든 객체를 다 담을 수 있고, 타입이 다른 객체들을 어딘가에 보관해야 한다면
Object에 보관하면 됩니다.
Object 다형성
Object는 모든 클래스의 부모 클래스입니다.
그림과 같이 Object는 모든 객체를 참조할 수 있습니다.
public class Car {
public void move() {
System.out.println("자동차 이동");
}
}
public class Dog {
public void sound() {
System.out.println("개 소리");
}
}
public class ObjectPolyExample1 {
public static void main(String[] args) {
Dog dog = new Dog();
Car car = new Car();
action(dog);
action(car);
}
private static void action(Object obj) {
//obj.sound(); 컴파일 오류, Object는 sound()가 없다.
//obj.move(); 컴파일 오류, Object는 move()가 없다.
//객체에 맞는 다운 캐스팅 필요
if (obj instanceof Dog dog) {
dog.sound();
} else if (obj instanceof Car car) {
car.move();
}
}
}
Object는 모든 타입의 부모입니다.
부모는 자식을 담을 수 있으므로 앞의 코드를 다음과 같이 변경해도 됩니다.
Object dog = new Dog();
Object car = new Car();
Object 다형성의 장점
- action(Object obj) 메서드는 Object 타입의 매개변수를 사용한다.
- 따라서 어떤 객체든지 인자로 전달할 수 있다.
action(dog) // main에서 dog 전달
void action(Object obj = dog) // Object는 자식인 Dog 타입을 참조할 수 있다.
actionc(car) // main에서 car 전달
void action(Object obj = car) // Object는 자식인 Car 타입을 참조할 수 있다.
Object 다형성의 한계
//main에서 dog, car 전달
private static void action(Object obj) {
obj.sound(); 컴파일 오류, Object는 sound()가 없다.
obj.move(); 컴파일 오류, Object는 move()가 없다.
- action( ) 메서드 안에서 obj.sound( )를 호출하면 오류가 발생한다.
- 왜냐하면 매개 변수인 obj는 Object 타입이기 떄문이다.
- Object에는 sound( ) 메서드가 없다.
obj.sound( ) 호출
- obj.sound( ) 호출
- obj는 Object 타입이므로 Object 타입에서 sound( )를 찾는다.
- Object에서 sound( )를 찾을 수 없다. Object는 최종 부모이므로 더 이상 올라가서 찾을 수 없다
Dog 인스턴스의 sound( ) 를 호출하려고 하면 다운캐스팅을 해야한다.
if (obj instanceof Dog dog) {
dog.sound();
}
- Object obj 의 참조값을 Dog dog 로 다운캐스팅 하면서 전달한다.
- dog.sound( ) 를 호출하면 Dog 타입에서 sound( )를 찾아서 호출한다.
Object를 활용한 다형성의 한계
Object는 모든 객체를 대상으로 다형적 참조를 할 수 있다.
- Object는 모든 객체의 부모이므로 모든 객체를 담을 수 있다.
Object를 통해 전달 받은 객체를 호출하려면 각 객체에 맞는 다운캐스팅 과정이 필요하다.
- Object가 세상의 모든 메서드를 알고 있는 것은 아니다.
Object 본인이 보유한 toString( ) 같은 메서드는 자식 클래스에서 오버라이딩 할 수 있지만, sound( ), move( ) 같은
Object에 속하지 않은 메서드는 메서드 오버라이딩을 활용할 수 없습니다. 그렇기 때문에 다형성을 활용하기에는
한계가 있습니다. 그렇다면 Object를 언제 활용하면 좋을까요?
Object 배열
Object는 모든 타입의 객체를 담을 수 있었습니다.
그렇다면 Object [ ] 을 만들면 세상의 모든 객체를 담을 수 있는 배열을 만들 수 있지 않을까요?
public class ObjectPolyExample2 {
public static void main(String[] args) {
Dog dog = new Dog();
Car car = new Car();
Object object = new Object();
Object[] objects = {dog, car, object};
size(objects);
}
private static void size(Object[] objects) {
System.out.println("전달된 객체의 수는: " + objects.length);
}
}
size( ) 메서드
- 이 메서드는 Object 타입만 사용한다.
- Object 타입의 배열은 세상의 모든 객체를 담을 수 있기 때문에 새로운 클래스가 추가되거나 변경되어도 이 메서드를 수정하지 않아도 된다.
- 지금 만든 size( ) 메서드는 자바를 사용하는 곳이라면 어디든지 사용할 수 있다.
Object가 없다면?
- void action(Object obj)와 같이 모든 객체를 받을 수 있는 메서드를 만들 수 없다.
- Object [ ] objects 처럼 모든 객체를 저장할 수 있는 배열을 만들 수 없다.
물론 Object가 없어도 직접 클래스를 만들고 모든 클래스에서 직접 정의한 클래스를 상속 받으면 됩니다.
하지만 하나의 프로젝트를 넘어서 전세계 모든 개발자가 비슷한 클래스를 만들 것이고, 서로 호환되지 않는
직접 정의한 수 많은 Object 클래스들이 넘쳐날 것입니다.
toString( )
Object.toString( ) 메서드는 객체의 정보를 문자열 형태로 제공해줍니다.
그래서 디버깅과 로깅에 유용하게 사용되곤 합니다.
이 메서드는 Object 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있습니다.
public class ToStringMain1 {
public static void main(String[] args) {
Object object = new Object();
String string = object.toString();
//ToString() 반환 값 출력
System.out.println(string);
//object 직접 출력
System.out.println(object);
}
}
실행 결과
java.lang.Object@b4c966a
java.lang.Object@b4c966a
Object.toString( )
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hasCode());
}
- Object가 제공하는 toString( ) 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)을 16진수로 제공합니다.
"println( )과 toString( ) 의 값은 왜 같을까?"
사실 System.out.println( ) 메서드는 내부에서 toString( ) 을 호출합니다.
Object 타입(자식 포함) 이 println( )에 인수로 전달되면서 내부에서 obj.toString( ) 메서드를 호출해서 결과를 출력합니다.
public void println(Object x) {
String s = String.valueOf(x);
// ....
}
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
toString( ) 오버라이딩
Object.toString( ) 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나태지 못합니다.
그래서 보통 toString( ) 을 재정의(오버라이딩) 해서 보다 유용한 정보를 제공하는 것이 일반적입니다.
public class Dog {
private String dogName;
private int age;
public Dog(String dogName, int age) {
this.dogName = dogName;
this.age = age;
}
// Alt + Insert -> toString()
@Override
public String toString() {
return "Dog{" +
"dogName='" + dogName + '\'' +
", age=" + age +
'}';
}
}
public class Car {
private String carName;
public Car(String carName) {
this.carName = carName;
}
}
package lang.object.tostring;
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
package lang.object.tostring;
public class ToStringMain2 {
public static void main(String[] args) {
Car car = new Car("ModelY");
Dog dog1 = new Dog("멍멍이1", 2);
Dog dog2 = new Dog("멍멍이1", 5);
System.out.println("1. 단순 toString 호출");
System.out.println(car.toString());
System.out.println(dog1.toString());
System.out.println(dog2.toString());
System.out.println("2. println 내부에서 toString 호출");
System.out.println(car);
System.out.println(dog1);
System.out.println(dog2);
System.out.println("3. Object 다형성 활용");
ObjectPrinter.print(car);
ObjectPrinter.print(dog1);
ObjectPrinter.print(dog2);
}
}
실행 결과
1. 단순 toString 호출
lang.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이1', age=5}
2. println 내부에서 toString 호출
lang.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이1', age=5}
3. Object 다형성 활용
객체 정보 출력: lang.object.tostring.Car@4e50df2e
객체 정보 출력: Dog{dogName='멍멍이1', age=2}
객체 정보 출력: Dog{dogName='멍멍이1', age=5}
- Car 인스턴스는 toString( )을 재정의 하지 않았다. 따라서 Object가 기본적으로 제공하는 toString( ) 메서드를 사용한다.
- Dog 인스턴스는 toString( )을 재정의 한 덕분에 객체의 상태를 명확하게 확인할 수 있다.
ObjectPrinter.print(car) 분석
ObjectPrinter.print(car); // main에서 호출
void print(Object obj = car) { // 인수 전달(Car타입)
String string = "객체 정보 출력: " + obj.toString();
}
- Object obj의 인수로 car(Car) 가 전달된다.
- 메서드 내부에서 obj.toString( )을 호출한다.
- obj는 Object 타입이다. 따라서 Object에 있는 toString( )을 찾는다.
- 이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아본다. 재정의된 메서드가 없다.
- Object.toString( )을 실행한다.
ObjectPrinter.print(dog) 분석
ObjectPrinter.print(dog) 분석
void print(Object obj = dog) { // 인수 전달(Dog 타입)
String string = "객체 정보 출력: " + obj.toString();
}
- Object obj의 인수로 dog(Dog) 가 전달된다.
- 메서드 내부에서 obj.toString( )을 호출한다.
- obj는 Object 타입이다. 따라서 Object에 있는 toString( )을 찾는다.
- 이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아본다. 재정의된 메서드가 있다.
- Dog.toString( )을 실행한다.
참고 - 객체의 참조값 직접 출력
toString( )은 기본적으로 객체의 참조값을 출력하게 되어 있습니다.
toString( )이나, hasCode( ) 메서드를 재정의 하면 참조값을 출력할 수 없습니다.
아래 코드를 사용하면 직접 객체의 참조값을 출력할 수 있습니다.
String refValue = Integer.toHexString(System.identityHashCode(dog1));
System.out.println("refValue = " + refValue);
Object와 OCP
만약 Object가 없고, 또 Object가 제공하는 toString( )이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것 입니다.(공통의 부모가 없는 경우) 각각의 클래스마다 별도의 메서드를 작성해야 합니다.
반면 ObjectPrinter 와 Object를 사영하는 구조는 다형성을 매우 잘 활용하고 있습니다.
다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻입니다.
ObjectPrinter의 print( )
- 다형적 참조: print(Object obj), Object 타입을 매개 변수로 사용해서 다형적 참조를 사용한다. Car, Dog 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있다.
- 메서드 오버라이딩: Object는 모든 클래스의 부모이다. 따라서 Dog, Car 와 같은 구체적인 클래스는 Object가 가지고 있는 toString( ) 메서드를 오버라이딩 할 수 있다. 따라서 pring(Object obj) 메서드는 Dog, Car 와 같은 구체적인 타입에 의존(사용) 하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 toString( )을 호출할 수 있다.
OCP 원칙
- Open: 새로운 클래스를 추가하고, toString( )을 오버라이딩 해서 기능을 확장할 수 있다.
- Closed: 새로운 클래스를 추가해도 Object와 toString( )을 사욯하는 클라이언트 코드인 ObjectPrinter는 변경하지 않아도 된다.
다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 Car, Dog 에 의존하는 것이 아니라 추상적인 Object 에 의존하면서 OCP 원칙을 지킬 수 있었습니다. 덕분에 새로운 클래스를 추가하고 toString( ) 메서드를 새롭게 오버라이딩 해서 기능을 확장 할 수 있습니다. 그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter는 변경할 필요도 없죠.
equals( )
Object는 동등성 비교를 위한 equals( ) 메서드를 제공합니다.
자바는 두 객체가 같다라는 표현을 2가지로 분리해서 제공합니다.
- 동일성(indentity): == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
- 동등성(Equality): equals( ) 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인
예를 들어 같은 회원 번호를 가진 객체가 2개 있다고 가정하겠습니다.
User a = new User("id-100"); // 참조 x001
User b = new User("id-100"); // 참조 x002
이 경우 물리적으로 다른 메모리에 있는 다른 객체이지만, 회원번호를 기준으로 생각해보면 논리적으로는 같은 회원으로 볼 수 있습니다. 따라서 동일성은 다르지만, 동등성은 같습니다.
문자의 경우도 마찬가지 입니다.
String str1 = "hello";
String str2 = "hello";
이 경우 물리적으로는 각각의 "hello" 문자열이 다른 메모리에 존재할 수 있지만, 논리적으로는 같은 "hello" 라는 문자열 입니다. (이 경우에는 자바가 같은 메모리를 사용하도록 최적화 한다고 합니다.)
public class UserV1 {
private String id;
public UserV1(String id) {
this.id = id;
}
}
public class EqualsMainV1 {
public static void main(String[] args) {
UserV1 user1 = new UserV1("id-100");
UserV1 user2 = new UserV1("id-100");
System.out.println("identity = " + (user1 == user2));
System.out.println("equality = " + (user1.equals(user2)));
}
}
실행 결과
identity = false
equality = false
동일성 비교
user1 == user2
x001 = x002
false //결과
동등성 비교
public boolean equals(Object obj) {
return (this == obj);
}
Object가 기본으로 제공하는 equals( )는 == 으로 동일성 비교를 제공합니다.
user1.equals(user2)
return (user1 == user2) // Object.equals 메서드안
return (x001 == x002) // Object.equals 메서드안
return false
동등성이라는 개념은 각각의 클래스 마다 다릅니다 .어떤 클래스는 주민등록번호를 기반으로 동등성을 처리할 수도 있고,
어떤 클래스는 고객의 연락처를 기반으로 동등성을 처리할 수 있습니다. 어떤 클래스는 회원 번호를 기반으로 동등성을 처리할 수 있습니다. 따라서 동등성 비교를 사용하고 싶다면 equals( ) 메서드를 재정의 해야 합니다.
그렇지 않으면 Object는 동일성 비교를 기본으로 제공하게 됩니다.
equals( ) 구현
public class UserV2 {
private String id;
public UserV2(String id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
UserV2 user = (UserV2) obj;
return id.equals(user.id);
}
}
- Object의 equals( ) 메서드를 재정의 했다.
- UserV2의 동등성은 id(고객번호)로 비교한다.
- equals( )는 Object 타입을 매개 변수로 사용한다. 따라서 객체의 특정 값을 사용하려면 다운캐스팅이 필요하다.
- 여기서는 현재 인스턴스(this)에 있는 id 문자열과 비교 객체의 id 문자열을 비교한다.
- UserV2에 있는 id는 String 이다. 문자열 비교는 == 이 아니라 equals( )를 사용해야 한다.
public class EqualsMainV2 {
public static void main(String[] args) {
UserV2 user1 = new UserV2("id-100");
UserV2 user2 = new UserV2("id-100");
System.out.println("identity = " + (user1 == user2));
System.out.println("equality = " + (user1.equals(user2)));
}
}
실행 결과
identity = false
equality = true
- user1, user2 는 서로 다른 객체이지만 둘 다 같은 id(고객 번호)를 가지고 있으므로 동등하다.
위에 재정의한 equals( )는 매우 간단히 만든 버전이고, 실제로 정확하게 동작하려면 다음과 같이 구현해야 합니다.
대부분의 IDE는 정확한 equals( ) 코드를 자동으로 만들어줍니다.
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
UserV2 userV2 = (UserV2) object;
return Objects.equals(id, userV2.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
equals( ) 메서드를 구현할 때 지켜야 하는 규칙
- 반사성(Reflexivity): 객체는 자기 자신과 동등해야 한다. (x.equals(x)는 항상 true)
- 대칭성(Symmetry): 두 객체가 서로에 대해 동일하다고 판단하면, 이는 양방향으로 동일해야 한다.(x.equals(y)가 ture 이면 y.eqlus(x)도 ture)
- 추이성(Transitivity): 만약 한 객체가 두번째 객체와 동일하고, 두 번째 객체가 세 번째 객체와 동일하다면, 첫번째 객체는 세 번째 객체와도 동일해야 한다.
- 일관성(Consistency): 두 객체의 상태가 변경되지 않는 한, equals( ) 메서드는 항상 동일한 값을 반환해야 한다.
- null에 대한 비교: 모든 객체는 null과 비교했을 때 false 를 반환해야 한다.
정리
- 동등성 비교가 항상 필요한 것은 아니다. 동등성 비교가 필요한 경우에만 equals( )를 재정의하면 된다.
- equals( )와 hasCode( )는 보통 함께 사용된다.
'Java' 카테고리의 다른 글
[Java] String 클래스가 불변 객체 라고? (0) | 2024.04.02 |
---|---|
[Java] 불변 객체, 불변 클래스 (1) | 2024.04.01 |
[Java] 다형성이 중요한 이유? (0) | 2024.03.26 |
[Java] 인터페이스 (0) | 2024.03.21 |
[Java] 다형성의 활용 (0) | 2024.03.19 |