* Object 클래스 (equals, hashCode, toString)
equals() :
2개의 객체가 가리키는 곳이 동일한 메모리 주소일 경우에만 동일한 객체가 되고
주소값이 다른 객체는 서로 다른 객체라 판단하는 메서드.
== 와 같은 기능을 가짐.
우리가 왜 equals() 메서드를 재정의해야할까?
자바를 처음 배울 때 equals()를 어디에 썼는지 보면 문자열 비교를 할 때 equals를 썼다.
String a1 = new String("a");
String a2 = new String("a");
System.out.println(a1 == a2); // false
System.out.println(a1.equals(a2)); // true
우리가 보기엔 a1의 값과 a2의 값이 일치한다. 그렇지만 실제 만들어질 때 a1의 주소값과 a2의 주소값이 다르기 때문에 == 연산을 할 시 false이다.
지금 equals를 할 때 true가 나오는 이유는, String 클래스에서 equals 메서드를 오버라이딩 해놓았기 때문이다.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value);
}
String 클래스에서 오버라이딩한 equals() 메서드이다.
지금 보면 해당 문자열의 값이 같은지를 비교해서 반환하고 있다.
이를 통해, 우리는 우리가 원하는 같다라는 기능을 표현하기 위해 equals() 메서드를 오버라이딩할 필요성을 느끼게 되었다.
AnyObject anyObject1 = new AnyObject(100, "댕댕이", true);
AnyObject anyObject2 = new AnyObject(100, "댕댕이", true);
AnyObject anotherObject = new AnyObject(300, "인간", false);
// 동일성 비교 // 즉 같은 메모리에 존재?
System.out.println(anyObject1 == anyObject2); // false
// 동등성 비교 // 값이 동일한지
System.out.println(anyObject1.equals(anyObject2)); // false
// 동등성 비교
System.out.println(anotherObject.equals(anyObject1)); // false
현재 AnyObject에는 equals() 메서드를 오버라이딩하지 않은 상태에서 코드를 실행해보았다.
equals() 메서드를 재정의해주지 않았기 때문에, 현재 false가 나옴을 볼 수 있다. 즉, 현재 equals는 == 와 같은 기능을 하고 있는 것이다.
그렇다면 이제 재정의를 해보자.
여기서 나는, AnyObject의 세번째 변수의 값은 신경쓰지 않고 첫번째와 두번째 값만이 같으면 같은 객체로 취급하고 싶다 가정해보자.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AnyObject anyObject = (AnyObject) o;
return num == anyObject.num && Objects.equals(name, anyObject.name);
}
첫번째 변수인 num, 두번째 변수인 name이 일치한지 and 연산자로 비교후 반환하고 있다.
그러면 다시 코드를 실행해보자.
AnyObject anyObject1 = new AnyObject(100, "댕댕이", true);
AnyObject anyObject2 = new AnyObject(100, "댕댕이", false);
AnyObject anotherObject = new AnyObject(300, "인간", false);
// 동일성 비교 // 즉 같은 메모리에 존재?
System.out.println(anyObject1 == anyObject2); // false
// 동등성 비교 // 값이 동일한지
System.out.println(anyObject1.equals(anyObject2)); // true
// 동등성 비교
System.out.println(anotherObject.equals(anyObject1)); // false
분명히 anyObject1과 anyObject2의 메모리 주소는 다를 것이다.
그리고 현재 세 번째 변수의 값이 다르지만 우리가 재정의한 equals()에 의해 같다라고 나타내고 있다.
hashCode(): 두 객체의 동일성을 검사하는 연산자
HashSet, HashTable, HashMap 등의 Hash를 사용하는 객체들은 해당 객체가 의미상으로 동일한지 판단하기 위해 hashCode 메서드를 호출한다.
그렇기에 equals와 마찬가지로, 해당 객체가 같은지 말해주려면 메서드를 재정의해줘야한다.
Hash 관련 컬렉션들이 같은 객체인지를 구분하는 법은 이렇다.
hashCode 비교 -> equals 비교로 진행된다.
그렇다면 의문이 든다. hashCode가 일치하더라도 equals를 통해 다른게 판명하게 되는 거면 처음부터 equals로 판단하면 되지 않나?
싶을 수 있다.
하지만 우선 hashCode를 통하면 상당수의 다른 것들이 필터링 되기 때문에 분명히 의미가 있는 과정이다!
// Hash 관련 컬렉션이 같은 인스턴스를 구분하는 방법
// hashCode 비교 -> equals 비교
SomeObject sameObject1 = new SomeObject(100, "고양이");
SomeObject sameObject2 = new SomeObject(100, "고양이");
System.out.println(sameObject1.hashCode()); // 1078694789
System.out.println(sameObject2.hashCode()); // 1831932724
Set<SomeObject> set = new HashSet<>();
set.add(sameObject1);
set.add(sameObject2);
System.out.println(set.size()); // 2
현재 hashCode와 equals 메서드를 재정의하지 않은 상태이다.
hashCode는 재정의하지 않는다면, 기본적으로 메모리 주소를 기반으로 값을 만들기 때문에 두 개의 객체의 값은 당연히 다를 것이다.
당연히 HashSet은 hashCode가 다르니, 다른 객체라 인지했기에 set에 두 개의 값이 모두 들어갈 것이다.
그렇다면 이제 hashCode를 재정의해보자.
@Override
public int hashCode() {
return Objects.hash(intField, stringField);
}
해당 변수들의 값을 토대로 hashCode를 만들었다.
그렇기에 만약 두 변수의 값이 같다면, 같은 hashCode가 나올 것이다.
// Hash 관련 컬렉션이 같은 인스턴스를 구분하는 방법
// hashCode 비교 -> equals 비교
SomeObject sameObject1 = new SomeObject(100, "고양이");
SomeObject sameObject2 = new SomeObject(100, "고양이");
System.out.println(sameObject1.hashCode()); // 44153024
System.out.println(sameObject2.hashCode()); // 44153024
Set<SomeObject> set = new HashSet<>();
set.add(sameObject1);
set.add(sameObject2);
System.out.println(set.size()); // 2
어라? hashCode의 값은 동일한데, 서로 다른 객체라 인지하고 값이 들어가 set의 크기가 2이다.
hash 관련 컬렉션은 hashCode로 한 번 거르고 그 이후에 equals를 호출한다 했다.
난 지금 equals를 재정의 해주지 않았기 때문에 == 와 마찬가지로 기능이 작동해서 다르게 인지한 것이다.
그렇기에 추가적으로 equals도 재정의를 해줘야한다.
toString(): 객체가 가지고 있는 정보나 값들을 문자열로 만들어 리턴하는 메소드
SomeObject someObject1 = new SomeObject(1, "댕댕이");
SomeObject someObject2 = new SomeObject(100, "냐옹이");
System.out.println(someObject1); //SomeObject@404b9385
System.out.println(someObject1.toString()); //SomeObject@404b9385
System.out.println(someObject2); //SomeObject@6d311334
System.out.println(someObject2.toString()); //SomeObject@6d311334
우리가 someObject1을 println하면 어떤 정보를 원할까? 당연히 해당 객체가 가지고 있는 변수의 값을 원하지 않을까?
이를 재정의해보자.
@Override
public String toString() {
return "SomeObject{" +
"intField=" + intField +
", stringField='" + stringField + '\'' +
'}';
}
이런식으로 하면 이제 우리가 원하는 형태의 문자열을 얻을 수 있다.
또 눈치챌 수 있었겠지만, 해당 객체를 toString을 하지 않고 그냥 println해도 toString을 한 것과 동일하게 작동한다.
* Optional
CompanyRepository companyRepository = new CompanyRepository();
String string = companyRepository.getValue(3);
System.out.println("string=" + string);
//NullPointerException을 피하려면 이렇게 Null 체크를 해야함.
if(string != null)
System.out.println(string.toUpperCase());
해당 Repository에서 값을 가져올 때, 해당 값이 존재하지 않아 null이 올 수 있다.
물론 이렇게 if문을 통해 분기 처리를 해 해결할 수 있다.
하지만 이렇게 할 경우,
(값을 조회하는 로직) // (null인지 여부 판단 로직) 이 분리되어 있어 관리가 힘들다. 나는 이걸 한꺼번에 관리하고 싶다.
이를 위해 Optional을 사용할 수 있다.
우선 Optional이 무엇인지 알아보자.
Optional 클래스는 쉽게 생각해, 어떤 값을 한 겹을 감싼다고 생각하면 된다.
왜 사용할까? 핵심은 null이 올 수 있는 값을 감싸기 때문에, 참조하더라도 NullPointerException이 발생하지 않게 도와준다.
Optional을 생성하는 법을 보자.
1. Optional.empty() : Optional은 감싸는 Wrapper 클래스이기 때문에 값이 없을 수도 있다.
Optional<String> optional = Optional.empty();
2. Optional.of() : 만약 어떤 데이터가 절대 null이 아니라면, Optional.of()로 생성할 수 있다.
그런데, 우리가 결국에 Optional을 사용하는 이유 자체가 null이 있을 수도 있음을 시사하는 것이니 과연 쓸일이 많을까..?
3. Optional.ofNullable() : 어떤 데이터가 null이 올 수도 있고, 아닐 수도 있는 경우에 해당 메서드를 통해 생성한다.
그럼 이제 활용해보자.
MapRepository mapRepository = new MapRepository();
Optional<String> string = mapRepository.getOptionalValue("NOT_EXIST_KEY");
string.ifPresentOrElse(
str -> System.out.println(str.toUpperCase()), // Optional이 Empty가 아닐 때 실행
() -> {
throw new RuntimeException("키가 존재하지 않습니다."); // Optional이 Empty일 때 실행
}
);
먼저 이렇게 ifPresentOrElse라는 함수 이름명 답게 해당 값이 비어있지 않다면 / 해당 값이 null일 경우를 한번에 처리할 수 있다.
아니면 이런 식으로 반환 받을 수도 있다!
MapRepository mapRepository = new MapRepository();
String not_exist_key = mapRepository.getOptionalValue("NOT_EXIST_KEY").orElseThrow(RuntimeException::new);
System.out.println(not_exist_key.toUpperCase());
* If문 제거하기
(해당 내용은 블로그 게시글에 조금 더 상세히 적겠습니다..!!)
* Getter와 Setter
public class CalculateCommand {
private CalculateType calculateType;
private int num1;
private int num2;
public CalculateCommand(CalculateType calculateType, int num1, int num2) {
this.calculateType = calculateType;
this.num1 = num1;
this.num2 = num2;
}
public CalculateType getCalculateType() {
return calculateType;
}
public int getNum1() {
return num1;
}
public int getNum2() {
return num2;
}
}
CalculateType calculateType = calculateCommand.getCalculateType();
int num1 = calculateCommand.getNum1();
int num2 = calculateCommand.getNum2();
int result = calculateType.calculate(num1, num2);
결합도와 응집도를 연결지어서 생각해봐야 한다.
만약 CalculateCommand 클래스에서 내가 CalculateType 부분을 지웠다 가정해보자.
어떤 일이 일어날까?
public class CalculateCommand {
private int num1;
private int num2;
public CalculateCommand(int num1, int num2) {
this.num1 = num1;
this.num2 = num2;
}
public int getNum1() {
return num1;
}
public int getNum2() {
return num2;
}
}
int num1 = calculateCommand.getNum1();
int num2 = calculateCommand.getNum2();
CalculateType 부분들을 다 지웠다.
단순히 클래스 내의 부분들뿐만이 아닌 해당 클래스를 사용한 다른 함수들에서도 해당 부분들을 다 지워줘야한다.
즉 이런 상황은 결합도는 높고(즉 의존도가 높고) 응집도가 낮은 상황이다.
내가 사용하는 함수의 변수의 이름을 간접적으로 알고 있는 상황이기 때문이다.
지금이야 변수 하나 지운 것이지만 변수가 수정되거나 추가되고 하면 더 많은 곳들을 찾아다니며 값을 수정해줘야하는 일이 생길 것이다.
그럼 어떻게 해야할까?
조금 더 본질적으로 돌아가서,, 메인 함수에서 CalculateCommand를 가지고 하고자 하는 행위는 무엇일까?
값을 계산한 결과값을 얻고 싶은 것이다.
즉, 계산을 위한 커맨드, 인자 값들이 아닌, 그 결과값을 알고 싶을 뿐이다.
그러면 계산하는 주체가 CalculateCommand가 되면 되지 않을까??
public class CalculateCommand {
private CalculateType calculateType;
private int num1;
private int num2;
public CalculateCommand(CalculateType calculateType, int num1, int num2) {
this.calculateType = calculateType;
this.num1 = num1;
this.num2 = num2;
}
public int getCalculateResult() {
CalculateType calculateType = this.calculateType;
int num1 = this.num1;
int num2 = this.num2;
int result = calculateType.calculate(num1, num2);
return result;
}
}
int result = calculateCommand.getCalculateResult();
return result;
이러면 어떤 일이 생길까?
다른 함수에서는 CalculateCommand 클래스에 어떤 값들이 있는지 전혀 모른다.
그저 해당 클래스에서 결과값을 반환받을 뿐이다.
이러면 나는 CalculateCommand 클래스 내부에서 변화가 생겨도 이를 사용하는 메인함수나 다른 함수에서 수정할 필요가 없어진다.
항상 코드를 짜며 하는 행위의 역할과 주체가 누구인지를 조금 고민해볼 필요가 있어보인다.
고민할 때 중시할 부분은 다른 함수가 해당 객체에 의존적인지를 고민해보자.
'TIL' 카테고리의 다른 글
2023.09.25 월 TIL (0) | 2023.09.25 |
---|---|
2023.9.20 수 TIL (1) | 2023.09.20 |