일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 스프링 db2 - 데이터 접근 기술
- @Aspect
- 스프링 mvc2 - 로그인 처리
- 코드로 시작하는 자바 첫걸음
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch12
- 스프링 db1 - 스프링과 문제 해결
- 자바 기본편 - 다형성
- 자바의 정석 기초편 ch2
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch7
- 스프링 mvc2 - 검증
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch4
- 스프링 mvc2 - 타임리프
- 게시글 목록 api
- 스프링 입문(무료)
- 자바의 정석 기초편 ch13
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch6
- 2024 정보처리기사 시나공 필기
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch11
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch8
- Today
- Total
나구리의 개발공부기록
프로젝트 환경 구성 및 java.lang 패키지 소개, Object 클래스,Object 다형성, Object 배열, toString(), Object와 OCP, equals()(동일성과 동등성, 구현) 본문
프로젝트 환경 구성 및 java.lang 패키지 소개, Object 클래스,Object 다형성, Object 배열, toString(), Object와 OCP, equals()(동일성과 동등성, 구현)
소소한나구리 2025. 1. 16. 15:49출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 프로젝트 환경 구성 및 java.lang 패키지 소개
1) 프로젝트 환경 구성
(1) 프로젝트 생성
- Name: java-mid1
- Location: 원하는 위치
- Build system: IntelliJ
- JDK: 자바 17 or 21
2) java.lang 패키지 소개
(1) java.lang
- 자바가 기본으로 제공하는 라이브러리(클래스 모음) 중에 가장 기본이 되는 것이 java.lang 패키지임
- lang은 Language(언어)의 줄임말로 자바 언어를 이루는 가장 기본이 되는 클래스들을 보관하는 패키지임
(2) java.lang 패키지의 대표적인 클래스들
- Object: 모든 자바 객체의 부모 클래스
- String: 문자열
- Integer, Long, Double 등: 래퍼 타입, 기본형 데이터 타입을 객체로 만든 것
- Class: 클래스 메타 정보
- System: 시스템과 관련된 기본 기능들을 제공
(3) import 생략 가능
- java.lang 패키지는 모든 자바 애플리케이션에 자동으로 임포트되기 때문에 임포트 구문을 사용하지 않아도 됨
- 해당 패키지의 클래스들은 너무 자주 사용하기 때문에 거의 자바 언어와 동일한 수준으로 취급하기 때문임
- System 클래스를 예로 설명해보면 System.out.println() 출력 코드를 호출할 때 import 구문이 없이 사용할 수 있음
package lang;
public class LangMain {
public static void main(String[] args) {
System.out.println("Hello java!");
}
}
2. Object 클래스
1) Object 클래스
(1) Object 클래스 예제
- 자바에서 모든 클래스(객체)의 최상위 부모 클래스는 항상 Object 클래스임
- 클래스에 상속받을 부모 클래스가 없으면 묵시적으로 Object 클래스를 상속 받는데, 자바가 extends Object 코드를 자동으로 넣어주기 때문에 해당 코드는 생략하는 것을 권장함
- Child 클래스 처럼 상속 받을 부모 클래스를 명시적으로 지정하면 자바가 Object 클래스를 자동으로 상속 받지 않음
- 즉, 상속 구조의 클래스에서 계속 위로 따라가다보면 언젠가 아무것도 상속받지 않는 클래스가 나올텐데, 그 클래스는 묵시적으로 Object 클래스를 상속받은 것임
package object;
// 묵시적 상속
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
// 위 코드는 아래 코드와 동일함
public class Parent extends Object{ ... }
package object;
// 명시적 상속
public class Child extends Parent {
public void ChildMethod() {
System.out.println("Child.ChildMethod");
}
}
** 묵시적(Implicit) vs 명시적(Explicit)
- 묵시적: 개발자가 코드에 직접 기술하지 않아도 시스템 또는 컴파일러에 의해 자동으로 수행되는 것을 의미
- 명시적: 개발자가 코드에 직접 기술해서 작동하는 것을 의미
(2) ObjectMain
- toString()은 Object 클래스의 메서드로 객체의 정보를 제공함
- Parent는 Object를 묵시적으로 상속 받았기 때문에 메모리에도 함께 생성이 됨
- child.toString()을 호출하면 본인 타입인 Child에서 toString()을 찾는데 없기 때문에 부모로 올라가고, 부모인 Parent에도 없기때문에 Parent의 부모인 Object로 올라가서 찾음
- Object에는 toString()이 있으므로 해당 메서드가 호출됨
package 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);
}
}
/* 실행 결과
Child.ChildMethod
Parent.parentMethod
object.Child@4f023edb
*/
2) 자바에서 Object 클래스가 최상위 부모 클래스인 이유
(1) 공통 기능 제공
- 객체의 정보를 제공하고 이 객체가 다른 객체와 같은지 비교하고 객체가 어떤 클래스로 만들어졌는지 확인하는 기능은 모든 객체에 필요한 기본 기능인데 이런 기능을 객체를 만들 때 마다 항상 새로운 메서드를 정의해서 만들어야 한다면 매우 번거로운 일이 될 것임
- 그리고 직접 만든다 해도 개발자마다 같은 기능을 개발자마다 toString(), objectInfo() 등등 처럼 각각 메서드의 이름을 다르게 만들 수도 있어서 일관성이 없음
- Object 클래스는 모든 객체에 필요한 공통 기능을 제공하고 최상위 부모 클래스에 위치하기 때문에 모든 객체는 이 기능을 편리하게 제공(상속)받아서 사용할 수 있게 됨
- toString(): 객체의 정보를 제공
- equals(): 객체의 같음을 비교
- getClass(): 객체의 클래스 정보를 제공
- 위의 주요 외에 다른 추가 기능도 있으며 개발자들은 모든 객체가 위와 같은 메서드를 지원한다고 알고 있기 때문에 프로그래밍이 단순화되고 일관성을 가지게 됨
(2) 다형성의 기본 구현
- Object는 모든 클래스의 부모클래스이므로 모든 객체를 참조할 수 있음
- 다형성을 지원하는 기본적인 메커니즘을 제공하기 때문에 모든 자바 객체는 Object 타입으로 처리될 수 있어 다양한 타입의 객체를 통합적으로 처리할 수 있게 해줌
- 즉, Object는 모든 객체를 다 담을 수 있으므로 타입이 다른 객체들을 어딘가에 보관해야 한다번 Object에 보관하면 됨
3. Object 다형성
1) Object 다형성
(1) 예제 설명
- Dog와 Car는 서로 아무런 관련이 없는 클래스지만 둘다 부모가 없으므로 Object를 자동으로 상속 받음
(2) Car, Dog
- 전혀 관계가없는 Car와 Dog클래스이지만 최고조상에 Object 클래스를 상속받고 있음
package lang.object.poly;
public class Car {
public void move() {
System.out.println("자동차 이동");
}
}
package lang.object.poly;
public class Dog {
public void sound() {
System.out.println("멍멍");
}
}
(3) ObjectPolyExample
- Object는 모든 타입의 부모이기 때문에 인스턴스 생성시 Object로 다형적 참조가 가능함
- action(Object obj) 메서드에서 오브젝트를 매개변수를 사용하므로 어떤 타입이든지 전달할 수 있음
package lang.object.poly;
public class ObjectPolyExample1 {
public static void main(String[] args) {
Dog dog = new Dog();
Car car = new Car();
// Object 타입으로 다형적 참조 가능
Object objDog = new Dog();
Object objCar = new Car();
action(dog);
action(car);
}
private static void action(Object obj) {
// 컴파일 오류, Object 클래스는 sound(), move()가 없음
// obj.sound();
// obj.move();
// 객체에 맞게 다운 캐스팅 필요
if (obj instanceof Dog dog) {
dog.sound();
} else if (obj instanceof Car car) {
car.move();
}
}
}
(4) Object 다형성의 한계
- 하지만 action() 메서드 안에서 obj.sound()를 호출하면 오류가 발생하는데 매개변수인 obj는 Object타입인데 Object에는 sound()가 없기 때문임
- obj.sound()메서드를 호출하면 Object에서 sound()를 찾는데, Object에서 없으면 조상 클래스에서 메서드를 찾아야 하지만 Object는 최고 조상이므로 더 찾을 곳이 없어서 오류가 발생함
- 자바 메커니즘상 아래로는 찾지 않고 유일하게 아래로 탐색하는 방법은 메서드 오버라이딩임
- sound()를 호출하려면 instanceof로 인스턴스를 확인하고 안전하게 다운캐스팅을 한 뒤 메서드에 접근해야함
(5) Object를 활용한 다형성의 한계
- Object는 모든 객체를 대상으로 다형적 참조할 수 있지만 Object를 통해 전달 받은 객체를 호출하려면 각 객체에 맞는 다운캐스팅 과정이 필요함
- 즉, Object는 모든 객체의 부모이므로 모든 객체를 담을 수 있지만 Object가 세상의 모든 메서드를 알고있지 않기 때문에 다시 원래의 타입으로 형변환을 해주어야 함
- 다형성을 제대로 활용하려면 자바 기본편에서 배운 것 처럼 다형적 참조 + 메서드 오버라이딩을 함께 사용해야 함
- 그러나 Object는 모든 객체의 부모이므로 모든 객체를 대상으로 다형적 참조를 할 수는 있지만 각 객체가 가진 고유한 기능(메서드)을 호출하려면 다운 캐스팅을 해야함
- 오버라이딩을 하고싶어도 Object가 가진 메서드는 자식 클래스에서 오버라이딩이 가능한데, Object 자체가 가지지 않은 메서드는 애초에 오버라이딩을 할 수도 없음
- Object를 언제 활용하면 좋은지는 강의에서 하나씩 배움
4. Object 배열
1) Object 배열
(1) ObjectPolyExample2
- Object는 모든 타입의 객체를 담을 수 있기 때문에 Object[]을 만들면 세상의 모든 객체를 담을 수 있는 배열을 만들 수 있음
- Object도 인스턴스를 생성할 수 있음
- size(Object[] objects)로 Object[] 배열을 매개변수로하여 다양한 타입의 객체들을 메서드에 전달할 수 있음
package lang.object.poly;
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);
}
}
/* 실행 결과
전달된 객체의 수는: 3
*/
(2) size() 메서드 설명
- size() 메서드는 배열에 담긴 객체의 수를 세는 역할을 담당하며 이 타입은 Object 타입만 사용함
- Object 타입의 배열은 세상의 모든 객체를 담을 수 있기 때문에 새로운 클래스가 추가되거나 변경되어도 이 메서드는 수정하지 않아도 되며 지금 만든 size() 메서드는 자바를 사용하는 곳이라면 어디든 사용될 수 있음
(3) Object와 같은 개념이 없으면?
- 모든 객체를 받을 수 있는 메서드를 만들 수 없고, 모든 객체를 저장할 수 있는 배열을 만들 수 없어 전반적으로 공통으로 사용할 메서드나 배열을 만들 수 없음
- 물론 XxxObject와 처럼 직접 클래스를 만들고 애플리케이션의 모든 클래스에 직접 정의한 XxxObject를 상속 받으면 되긴 하지만 매우 불편할 것임
- 그리고 나를 프로젝트를 넘어서 전세계 모든 개발자가 비슷한 클래스를 만들 것이고 서로 호환되지 않는 수많은 XxxObject 클래스들이 코드에 존재하게 될 것임
5. toString()
1) toString()
(1) ToStringMain1
- Object의 toString() 메서드는 객체의 정보를 문자열 형태로 제공하여 디버깅과 로깅에 유용하게 사용됨
- Object 클래스에 정의되어 있으므로 모든 클래스에서 사용할 수 있음
- Object의 인스턴스를 생성하여 해당 참조 변수를 toString()으로 변환하여 출력한 것과 object를 바로 출력한 결과가 동일함
package lang.object.tostring;
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@23fc625e
java.lang.Object@23fc625e
*/
(2) Object.toString()
- Object의 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시 코드)를 16진수로 제공함
** 참고
- 해시코드에 대한 정확한 내용은 이후에 별도로 다룰 예정이므로 지금은 객체의 참조값 정도로 생각하면 됨
- 해시코드는 원래는 숫자 타입인데 toHexString()으로 hashCode를 변환하여 16진수로 변경하여 toString()메서드로 출력됨
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
(3) pringln()과 toString()
- toString()의 결과를 출력한 코드와 object를 바로 println()에 직접 출력한 코드의 결과가 완전히 같은데 System.out.println() 메서드는 내부에서 toString()을 호출하기 때문임
- println() 메서드를 타고 들어가다보면 아래와 같은 코드에서 obj.toString()을 호출하고 있는 것을 확인할 수 있음
- 그래서 println()을 사용할 때 toString()을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있음
// println() 메서드를 타고 들어가다보면 아래와 같은 코드를 볼 수 있음
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
2) toString() 오버라이딩
(1) 설명
- Object의 toString() 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지는 못함
- 그래서 보통 toString()을 재정의(오버라이딩)해서 정보를 제공함
(2) Car, Dog
- Car는 toString()을 재정의 하지 않고 Dog는 IDE에서 지원하는 toString() 만들기 기능을 통해 toString() 메서드를 오버라이딩하여 재정의
- toString()를 재정의하고싶을 때에는 대부분 IDE의 도움을 받고 만약 원하는 형식이 있으면 직접 오버라이딩하면됨
package lang.object.tostring;
public class Car {
private String carName;
public Car(String carName) {
this.carName = carName;
}
}
package lang.object.tostring;
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 +
'}';
}
}
(3) ObjectPrinter
- 객체 정보 출력: 이라는 문자와 매개변수로 넘어온 객체의 toString() 결과를 합해서 출력하는 기능을 제공
package lang.object.tostring;
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력:" + obj.toString();
System.out.println(string);
}
}
(4) ToStringMain2
- Dog 클래스에서 toString()메서드를 오버라이딩 하였기 때문에 Dog 인스턴스를 생성한 변수는 toString()메서드를 호출 시 오버라이딩된 메서드가 호출되고 Car는 그대로 Object의 toString()이 호출되는 것을 확인할 수 있음
- 직접 만든 ObjectPrinter.printer() 메서드도 동일하게 출력되는 것을 확인할 수 있음
package lang.object.tostring;
public class ToStringMain {
public static void main(String[] args) {
Car car = new Car("Model Y");
Dog dog1 = new Dog("멍멍이1", 2);
Dog dog2 = new Dog("멍멍이2", 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@4f023edb
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
2. println 내부에서 toString 호출
lang.object.tostring.Car@4f023edb
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
3. Object 다형성 활용
객체 정보 출력:lang.object.tostring.Car@4f023edb
객체 정보 출력:Dog{dogName='멍멍이1', age=2}
객체 정보 출력:Dog{dogName='멍멍이2', age=5}
*/
(5) ObjectPrinter.print(...) 분석
- print(...)의 인수로 car(Car 타입 참조변수)가 전달되면 메서드 내부에서 obj.toString()을 호출하고, Car 인스턴스에는 toString()이 없기 때문에 조상인 Object 타입에서 toString()을 찾으고 오버라이딩 된 메서드도 있는지 찾음
- 오버라이딩 된 메서드가 없으므로 조상인 Object의 toString()이 호출됨
- print(...)의 인수로 dog(Dog 타입 참조변수)가 전달되면 메커니즘은 똑같이 진행하지만 Dog에는 toString()이 오버라이딩 되어있으므로 오버라이딩 된 toString()이 호출됨
** 참고 - 객체의 참조값 직접 출력
- toString()이나 hashCode()를 재정의하면 원래 Object 클래스에서 제공하고 있던 기능이였던 객체의 참조값을 출력할 수 없는데, 아래처럼 코드를 작성하면 객체의 참조값을 출력할 수 있음
- System.identityHashCode(객체): 숫자로 실제 참조값을 출력
- Integer.toHexString(숫자): 숫자를 16진수로 변경
System.out.println(Integer.toHexString(System.identityHashCode(car)));
// 실행결과: 4f023edb
6. Object와 OCP
1) Object와 OCP
(1) BadObjectPrinter
- 만약 Object가 없고 Object가 제공하는 toString()이 없다면 공통 부모가 없기 때문에 서로 아무 관계가 없는 객체의 정보를 출력하기가 어려울 것임
- 아래의 코드처럼 각각의 클래스 전용의 메서드를 작성해야 됨
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);
}
}
(2) 구체적인 것에 의존
- BadObjectPrinter는 구체적인 타입인 Car, Dog를 사용하므로 이후에 출력해야할 구체적인 클래스가 10개로 늘어나면 이에 맞추어 메서드도 10개로 계속 늘어나야 함
- 이렇게 BadObjectPrinter클래스가 구체적인 특정 클래스인 Dog, Car를 사용하는 것을 BadObjectPrinter 클래스가 Car, Dog에 의존한다고 표현함
- 그러나 자바는 객체의 정보를 사용할 때 다형적 참조 문제를 해결해줄 Object 클래스와 메서드 오버라이딩 문제를 해결해줄 Object.toString()메서드가 존재하고 있음
- 물론 직업 Object와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있지만 번거로워짐
(3) 추상적인 것에 의존
- 앞에서 만든 ObjectPrinter 클래스는 Car, Dog 처럼 구체적인 클래스를 사용하는 것이 아니라 추상적인 Object 클래스를 사용함
- 즉, ObjectParinter클래스가 Object 클래스에 의존한다고 표현하며 구체적인 것에 의존지 않고 추상적인 것에 의존함
- ObjectPrinter와 Object를 사용하는 구조는 다형적 참조와 메서드 오버라이딩을 적절하게 사용하여 다형성을 잘 활용하고 있는 코드임
- 다형적 참조: print(Object obj) 메서드의 매개변수에 Object 타입을 사용하여 다형적 참조를 사용함
- 메서드 오버라이딩: Object는 모든 클래스의 부모이므로 구체적인 클래스는 Object가 가지고 있는 toString() 메서드를 오버라이딩할 수 있기 때문에 추상적인 Object타입에 의존하면서 런타임시에 각 인스턴스의 toString()을 호출할 수 있음
** 추상적
- 여기서 말하는 추상적이라는 뜻은 추상 클래스나 인터페이스만 뜻하는 것은 아님
- Animal과 Dog, Cat의 관계를 예를 들면 Animal 처럼 부모 타입으로 올라갈 수록 개념은 더 추상적이게 되고 dog, Cat과 같이 하위 타입으로 갈 수록 구체적이게 됨
(3) OCP 원칙
- 다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 것에 의존하는 것이 아니라 추상적인 Object에 의존하면서 OCP 원칙을 지킬 수 있었음
- 새로운 클래스를 추가하고 toString() 메서드를 새롭게 오버라이딩해서 기능을 확장할 수 있으며 이러한 변화에도 클라이언트 코드인 ObjectPrinter는 변경할 필요가 없음
(4) System.out.println()
- ObjectPrinter는 System.out.println()의 작동 방식을 설명하기 위해 만든것인데 System.out.println()메서드도 Object 매개 변수를 사용하고 내부에서 toString()을 호출하며 이 출력 코드를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있음
- 자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용하고 있음
- 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 오버라이딩해서 사용할 수 있도록 설계되어 있음
** 참고 - 정적 의존관계 VS 동적 의존관계
- 정적 의존관계: 컴파일 시간에 결정되며 주로 클래스간의 관계를 의미함
- 앞서 의존 관계 그림이 정적 의존관계이며 프로그램을 실행하지 않고 클래스 내에서 사용하는 타입들로만 보면 쉽게 의존관계를 파악할 수 있음 - 동적 의존관계: 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계
- ObjectPrinter.print(Object obj)에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있음 - 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존관계를 뜻함
- ex) ObjectPrinter는 Object에 의존
7. equals()
1) 동일성과 동등성
(1) 두 객체가 같다는 표현의 2가지
- 동일성(Identity): == 연산자를 사용하여 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
- 동등성(Equality): equals() 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인
(2) 단어 정리
- "동일"은 완전히 같음을 의미하는 반면 "동등"은 같은 가치나 수준을 의미하긴 하지만 그 형태나 외관 등이 완전히 같지는 않을 수 있음
- 즉, 동일성은 물리적으로 같은 메모리에 있는 객체 인스턴스인지 참조값을 확인하는 것이고 동등성은 논리적으로 같은지를 확인하는 것임
- 동일성은 자바 머신 기준(메모리의 참조)이므로 물리적인 반면 동등성은 보통 사람이 생각하는 논리적인 기준에 맞추어서 비교함
- 아래처럼 동일한 회원번호를 가진 2개의 회원 객체가 있을 때 물리적으로는 다른 메모리에 있기 때문에 서로 다른 객체이지만 회원 번호를 기준으로 생각해보면 논리적으로는 같은 회원으로 볼 수 있으므로 동일성은 다르지만 동등성은 같음
User a = new User("id-100") //참조 x001
User b = new User("id-100") //참조 x002
(3) UserV1, EqualsMainV1
- UserV1 클래스의 인스턴스를 동일한 값으로 2개 생성하여 동일성과 동등성을 비교
- 실행 결과를 보면 동등성, 동일성을 비교한 결과값이 모두 false가 나옴
package lang.object.equals;
public class UserV1 {
private String id;
public UserV1(String id) {
this.id = id;
}
}
package lang.object.equals;
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
*/
(4) 설명
- 동일성 비교는 당연히 user1과 user2는 서로 다른 참조값을 가지고 있기 때문에 비교 결과는 false가 나옴
- 동등성 비교를 하는 equals() 메서드를 들어가서 살펴보면 Object가 기본으로 제공하는 equals()는 == 비교를 하고 있기 때문에 당연히 false가 결과로 나온 것임
- 동등성이라는 개념은 어떤 클래스는 주민번호를 기반으로, 어떤 클래스는 고객 연락처를 기반으로 각각의 클래스마다 다르게 동등성을 처리할 수 있는데 Object 클래스가 이것을 모두 알 수는 없음
- 즉, 논리적인 동등성 비교를 사용하고 싶다면 equals() 메서드를 오버라이딩을 하여 사용해야 하며 그렇지 않으면 Object의 equals() 메서드는 동일성 비교를 기본으로 제공함
2) 구현
(1) UserV2
- Object의 equals()메서드를 재정의하여 UserV2의 동등성 비교를 Id로 비교하도록 함
- equals() 메서드는 Object 타입을 매개변수로 사용하므로 객체의 특정 값을 사용하려면 다운캐스팅이 필요함
- 현재 인스턴스의 id 문자열과 비교 대상으로 넘어온 객체의 id 문자열을 비교 (문자열 비교는 equals()를 사용해야함)
package lang.object.equals;
import java.util.Objects;
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);
}
}
(2) EqualsMainV2
- 동일성 비교는 객체의 참조가 다르므로 당연히 false가 반환됨
- 그러나 동등성 비교는 UserV2에서 비교대상인 객체가 같은 id를 가지고 있으면 같은 User라고 판단하도록 equals()메서드를 오버라이딩한 덕분에 true로 반환됨
package lang.object.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
*/
(3) 정확한 equals() 구현
- 앞서 Userv2에서 구현한 equals() 메서드는 이해를 돕기위해 매우 같단히 만든 버전이고 실제로 정확하게 동작하려면 아래와 같이 구현해야하며, 정확한 equals() 메서드를 구현하는 것은 생각보다 쉽지 않음
- 대부분의 IDE는 정확한 equals() 코드를 자동으로 만들어주는 기능을 지원함
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
UserV2 userV2 = (UserV2) o;
return Objects.equals(id, userV2.id);
}
(4) equals() 메서드를 구현할 때 지켜야 하는 규칙
- 실무에서는 대부분 IDE가 만들어주는 equals()를 사용하므로 이 규칙을 외우기 보다는 한번 읽어보고 넘어가면 됨
- 반사성(Reflexivity): 객체는 자기 자신과 동등해야 함
- ex) x.equals(x)는 항상 true - 대칭성(Symmetry): 두 객체가 서로에 대해 동일하다고 판단하면 이는 양방향으로 동일해야 함
- ex) x.equals(y)가 true이면 y.equals(x)도 true - 추이성(Transitivity): 만약 한 객체가 두 번째 객체와 동일하고 두 번째 객체가 세 번째 객체와 동일하면 첫 번째 객체는 세 번째 객체와도 동일해야 함
- 일관성(Consistency): 두 객체의 상태가 변경되지 않는 한 equals() 메소드는 항상 동일한 값을 반환해야 함
- null에 대한 비교: 모든 객체는 null과 비교했을 때 false를 반환해야 함
** 참고
- 동등성 비교가 항상 필요한 것은 아니며 동등성 비교가 꼭 필요한 경우에만 equals()를 재정의하면 됨
- equals()와 hashCode()는 보통 함께 사용되는데 이 부분은 뒤에 컬렉션 프레임워크에서 자세히 설명함(IDE에서 지원하는 기능이 보통 equals()와 hashCode()를 같이 구현해줌)
8. 문제와 풀이
1) toString(), equals() 구현하기
(1) 문제 설명
- 다음 코드와 결과를 참고하여 Rectangle 클래스를 생성
- Rectangle 클래스에 IDE 기능을 사용해 toString()과 equals() 메서드를 실행 결과에 맞도록 재정의
- rect1과 rect2는 넓이(width)와 높이(height)를 가지며 넓이와 높이가 모두 같다면 동등성 비교에 성공해야 함
package lang.object.test;
public class RectangleMain {
public static void main(String[] args) {
Rectangle rect1 = new Rectangle(100, 20);
Rectangle rect2 = new Rectangle(100, 20);
System.out.println(rect1);
System.out.println(rect2);
System.out.println(rect1 == rect2);
System.out.println(rect1.equals(rect2));
}
}
실행 결과
Rectangle{width=100, height=20}
Rectangle{width=100, height=20}
false
true
(2) 정답
package lang.object.test;
import java.util.Objects;
public class Rectangle {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Rectangle rectangle = (Rectangle) o;
return width == rectangle.width && height == rectangle.height;
}
@Override
public int hashCode() {
return Objects.hash(width, height);
}
@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", height=" + height +
'}';
}
}
실행결과는 동일
2) 정리
(1) Object의 나머지 메서드
- clone(): 객체를 복사할 때 사용, 거의 사용하지 않음
- hashCode(): equals()와 hashCode()는 종종 함께 사용됨, 컬렉션 프레임워크에서 자세히 설명
- getClass(): 뒤에서 Class에서 설명
- notify(), notifyAll(), wait(): 멀티쓰레드용 메서드로 멀티쓰레드에서 다룸
'인프런 - 실전 자바 로드맵 > 실전 자바 - 중급 1편' 카테고리의 다른 글
String 클래스(기본, 비교, 불변 객체, 주요 메서드), StringBuilder - 가변 String, String 최적화, 메서드 체이닝(Method Chaining) (0) | 2025.01.18 |
---|---|
불변 객체, 기본형과 참조형의 공유, 공유 참조와 사이드 이펙트, 불변 객체(도입, 예제, 값 변경) (0) | 2025.01.17 |