김영한의 실전 자바 - 중급 1편 강의 | 김영한 - 인프런
김영한 | , 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을 안다? 이걸로는 안됩니다!전 우아한형제들 기술이사, 누적 수강생 40만 명 돌
www.inflearn.com
java.lang 패키지
자바가 기본으로 제공하는 라이브러리(클래스 모음) 중에 가장 기본이 되는 것이 바로 `java.lang` 패키지이다. 여기서 `lang`은 `Language`(언어)의 줄임말이다. 쉽게 이야기해서 자바 언어를 이루는 가장 기본이 되는 클래스들을 보관하는 패키지를 뜻한다.
java.lang 패키지의 대표적인 클래스들
- `Object`: 모든 자바 객체의 부모 클래스
- `String`: 문자열
- `Integer`, `Long`, `Double`: 래퍼 타입, 기본형 데이터 타입을 객체로 만든 것
- `Class`: 클래스 메타 정보
- `System`: 시스템과 관련된 기본 기능들을 제공
`java.lang` 패키지는 모든 자바 애플리케이션에 자동으로 `import`된다. 따라서 임포트 구문을 사용하지 않아도 된다.
Object 클래스
자바에서 모든 클래스의 최상위 부모 클래스는 항상 `Object` 클래스이다.
public class Parent extends Object{
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
- 클래스에 상속 받을 부모 클래스가 없으면 묵시적으로 `Object` 클래스를 상속 받는다.
- 쉽게 이야기해서 자바가 `extends Object` 코드를 넣어준다.
- 따라서 `extends Object`는 생략하는 것을 권장한다.
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
- 클래스에 상속 받을 부모 클래스를 명시적으로 지정하면 `Object`를 상속 받지 않는다.
- 쉽게 이야기해서 이미 명시적으로 상속했기 때문에 자바가 `extends Object` 코드를 넣지 않는다.
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);
}
}
- `toString()`은 `Object` 클래스의 메서드이다. 이 메서드는 객체의 정보를 제공한다.
`Parent`는 `Object`를 뭇기적으로 상속 받았기 때문에 메모리에도 함께 생성된다.
- `child.toString()`을 호출한다.
- 먼저 본인의 타입인 `Child`에서 `toString()`을 찾는다. 없으므로 부모 타입으로 올라가서 찾는다.
- 부모 타입인 `Parent`에서 찾는다. 없으므로 부모 타입으로 올라가서 찾는다.
- 부모 타입인 `Object`에서 찾는다. `Object`에 `toString`이 있으므로 이 메서드를 호출한다.
자바에서 `Object` 클래스가 최상위 부모 클래스인 이유
모든 클래스가 `Object` 클래스를 상속 받는 이유는 다음과 같다.
- 공통 기능 제공
- 다형성의 기본 구현
공통 기능 제공
객체의 정보를 제공하고, 이 객체가 다른 객체와 같은지 비교하고, 객체가 어떤 클래스로 만들어졌는지 확인하는 기능은 모든 객체에게 필요한 기본 기능이다. 이런 기능을 객체를 만들 때마다 항상 새로운 메서드를 정의해서 만들어야 한다면 상당히 번거로울 것이다.
그리고 막상 만든다고 해도 개발자마다 서로 다른 이름의 메서드를 만들어서 일관성이 없을 것이다.
`Object`는 모든 객체에 필요한 공통 기능을 제공한다. `Object`는 최상위 부모 클래스이기 때문에 모든 객체는 공통 기능을 편리하게 제공(상속) 받을 수 있다.
`Object`가 제공하는 기능
- 객체의 정보를 제공하는 `toString()`
- 객체의 같음을 비교하는 `equals()`
- 객체의 클래스 정보를 제공하는 `getClass()`
- 기타 여러가지 기능
다형성의 기본 구현
부모는 자식을 담을 수 있다. `Object`는 모든 클래스의 부모 클래스이다. 따라서 모든 객체를 참조할 수 있다. `Object` 클래스는 다형성을 지원하는 기본적인 메커니즘을 제공한다. 모든 자바 객체는 `Object`타입으로 처리될 수 있으며, 이는 다양한 타입의 객체를 통합적으로 처리할 수 있게 해준다. 타입이 다른 객체들을 어딘가에 보관해야 한다면 바로 `Obejct`에 보관하면 된다.
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`는 모든 타입의 객체를 담을 수 있다. 따라서 `Object[]`을 만들면 세상의 모든 객체를 담을 수 있는 배열을 만들 수 있다.
public class ObjectPolyExample2 {
public static void main(String[] args) {
Dog dog = new Dog();
Car car = new Car();
Object object = new Object(); //Object 인스턴스도 만들 수 있다.
Object[] objects = {dog, car, object};
size(objects);
}
private static void size(Object[] objects) {
System.out.println("전달된 객체의 수는: " + objects.length);
}
}
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@a09ee92
java.lang.Object@a09ee92
Object.toString()
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
- `Object`가 제공하는 `toString()` 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공한다.
println()과 toString()
`toString()`의 결과를 출력한 코드와 `object`를 `println()`에 직접 출력한 코드의 결과는 완전히 같다.
//toString() 반환값 출력
String string = object.toString();
System.out.println(string);
//object 직접 출력
System.out.println(object);
`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();
}
따라서 `println()`을 사용할 때, `toString()`을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있다.
toString() 오버라이딩
`Object.toString()` 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못한다. 그래서 보통 `toString()`을 재정의(오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적이다.
public class Car {
private String carName;
public Car(String carName) {
this.carName = carName;
}
}
public class Dog {
private String dogName;
private int age;
public Dog(String dogName, int age) {
this.dogName = dogName;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"dogName='" + dogName + '\'' +
", age=" + age +
'}';
}
}
Object와 OCP
만약 `Object`가 없고, 또 `Object`가 제공하는 `toString()` 이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것이다. 여기서 아무 관계가 없다는 것은 공통의 부모가 없다는 뜻이다.
그렇다면 다음의 `BadObjectPrinter` 클래스와 같이 각각의 클래스마다 별도의 메서드를 작성해야 할 것이다.
public class BadObjectPrinter {
public static void print(Car car) { //Car 전용 메서드
String string = "객체 정보 출력: " + car.carInfo(); //carInfo() 메서드 만듬
System.out.println(string);
}
public static void print (Dog dog){ //Dog 전용 메서드
String string = "객체 정보 출력: " + dog.dogInfo(); //dogInfo() 메서드 만듬
System.out.println(string);
}
}
구체적인 것에 의존
`BadObjectPrinter`는 구체적인 타입인 `Car`, `Dog`를 사용한다. 따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 된다. 이렇게 `BadObjectPrinter` 클래스가 구체적인 특정 클래스인 `Car`, `Dog`를 사용하는 것을 `BadObjectPrinter`는 `Car`, `Dog`에 의존한다고 표현한다.
추상적인 것에 의존
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
`ObjectPrinter` 클래스는 `Car`, `Dog`와 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 `Object` 클래스를 사용한다. 이렇게 `ObjectPrinter` 클래스가 `Object` 클래스를 사용하는 것을 `ObjectPrinter` 클래스가 `Object` 클래스에 의존한다고 표현한다.
`ObjectPrinter`는 구체적인 것에 의존하는 것이 아니라 추상적인 것에 의존한다.
`ObjectPrinter`와 `Object`를 사용하는 구조는 다형성을 매우 잘 활용하고 있다. 다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻이다.
- 다형적 참조: `print(Object obj)`, `Object` 타입을 매개변수로 사용해서 다형적 참조를 사용한다. `Car`, `Dog` 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있다.
- 메서드 오버라이딩: `Object`는 모든 클래스의 부모이다. 따라서 `Dog`, `Car`와 같은 구체적인 클래스는 `Object`가 가지고 있는 `toString()` 메서드를 오버라이딩할 수 있다. 따라서 `print(Object obj)` 메서드는 `Dog`, `Car`와 같은 구체적인 타입에 의존(사용)하지 않고, 추상적인 `Object` 타입에 의존하면서 런타임에 각 인스턴스의 `toString()`을 호출할 수 있다.
OCP 원칙
- Open: 새로운 클래스를 추가하고, `toString()`을 오버라이딩해서 기능을 확장할 수 있다.
- Closed: 새로운 클래스를 추가해도 `Object`와 `toString()`을 사용하는 클라리언트 코드인 `ObjectPrinter`는 변경하지 않아도 된다.
`System.out.println()` 메서드는 `Object` 매개변수를 사용하고 내부에서 `toString()`을 호출한다. 따라서 `System.out.println()`를 사용하면 세상의 모든 객체의 정보(`toString()`)을 편리하게 출력할 수 있다.
자바는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용한다.
`toString()` 메서드와 같이, 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 오버라이딩해서 사용할 수 있도록 설계되어 있다.
정적 의존 관계 vs 동적 의존 관계
- 정적 의존관계는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미한다. 앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계이다. 쉽게 이야기해서 프로그램을 실행하지 않고, 클래스 내에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 잇다.
- 동적 의존관계는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계이다. 앞서 `ObjectPrinter.print(Object obj)`에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있다. 어떤 경우에는 `Car` 인스턴스가 넘어오고, 어떤 경우에는 `Dog` 인스턴스가 넘어온다. 이렇게 런타임에 어떤 인스턴스를 사용하는지를 나타내는 것이 동적 의존관계이다.
- 참고로 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존관계를 뜻한다.
- 예) `ObjectPrinter`는 `Object`에 의존한다.
equals
`Object`는 동등성 비교를 위한 `equals()` 메서드를 제공한다.
자바는 두 객체가 같다라는 표현을 2가지로 분리해서 제공한다.
- 동일성(Identity): `==` 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
- 동등성(Equality): `equals()` 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인
단어 정리
"동일"은 완전히 같음을 의미한다. 반면 "동등"은 같은 가치나 수준을 의미하지만 그 형태나 외관 등이 완전히 같지는 않을 수 있다.
쉽게 이야기해서 동일성은 물리적으로 같은 메모리에 있는 객체 인스턴스인지 참조값을 확인하는 것이고, 동등성은 논리적으로 같은지 확인하는 것이다.
동일성은 자바 머신 기준이고 메모리의 참조가 기준이므로 물리적이다. 반면 동등성은 보통 사람이 생각하는 논리적인 기준에 맞추어 비교한다.
User a = new User("id-100") //참조 x001
User b = new User("id-100") //참조 x002
이 경우 물리적으로 다른 메모리에 있는 객체이지만, 회원 번호를 기준으로 생각해보면 논리적으로는 같은 회원으로 볼 수 있다. 따라서 동일성은 다르지만 동등성은 같다.
예제
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 //결과
동등성 비교
Object.equals() 메서드
public boolean equals(Object obj) {
return (this == obj);
}
`Object`가 기본으로 제공하는 `equals()`는 `==`으로 동일성 비교를 제공한다.
equals 실행 순서 예시
user1.equals(user2)
return (user1 == user2) //Object.equals 메서드 안
return (x001 == x002) //Object.equals 메서드 안
return false
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);
}
}
- `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
정확한 equals() 구현
`UserV2`에서 구현한 `equals()`는 이해를 돕기 위해 매우 간단히 만든 버전이고, 실제로 정확하게 동작하려면 다음과 같이 구현해야 한다.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserV2 userV2 = (UserV2) o;
return Objects.equals(id, userV2.id);
}
equals() 메서드를 구현할 때 지켜야 하는 규칙
- 반사성(Reflexivity): 객체는 자기 자신과 동등해야 한다. (`x.equals(x)`는 항상 `true`)
- 대칭성(Symmetry): 두 객체가 서로에 대해 동일하다고 판단하면, 이는 양방향으로 동일해야 한다. (`x.equals(y)`가 `true`면 `y.equals(x)`도 `true`)
- 추이성(Transitivity): 만약 한 객체가 두 번째 객체와 동일하고, 두 번째 객체가 세 번째 겍체와 동일하다면, 첫 번째 객체는 세 번째 겍체와도 동일해야 한다.
- 일관성(Consistency): 두 객체의 상태가 변경되지 않는 한, `equals()` 메서드는 항상 동일한 값을 반환해야 한다.
- null에 대한 비교: 모든 객체는 `null`과 비교했을 때 `false`를 반환해야 한다.
'Java' 카테고리의 다른 글
[Java/김영한] String 클래스 (0) | 2025.03.22 |
---|---|
[Java/김영한] 불변 객체 (1) | 2025.03.08 |
[Java/김영한] 다형성 (1) | 2025.03.03 |
[Java/김영한] 상속 (1) | 2025.02.28 |
[Java/김영한] final (0) | 2025.02.28 |