앞선 포스팅에서 조금 개선해서 Immutable을 설명하는걸 구현하려다가 아무의미없이 마무리되어버렸다...
학생의 , 이름, 나이, 점수 의 경우보단 학교를 바꾸는 상황을 가정하면 조금더 좋을것 같아 클래스를 추가했다.
4개의 클래스 모두 남겨놓는다.
package thisjavaexam.immutable;
public class Score {
//점수는 변경을 위해서 클래스를 별도 생
private int score;
//점수를 호출하기위한 생성자 생성
public Score(int score) {
this.score = score;
}
//게터 세터 생성
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
return String.valueOf(score);
}
}
package thisjavaexam.immutable;
public class Student {
//학생 이름,나이 를 선언 + Score 클래스를 변수로 선언
private String name;
private int age;
private Score score;
private School schools; //학교정보를 추가했다.
//Main클래스에서 사용하기위한 생성자 생성
Student(School schools, String name, int age, Score score){
this.schools = schools; //학교정보 참조를 추가했다.
this.name = name;
this.age = age;
this.score = score;
}
//각각 게터 , 세터 생성
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
//name의 문자열이 임의클래스(내가만든 이클래스) 를 호출하면서 발생될 참조값 반환을 방지하기위한 오버라이딩
// 학교정보를 추가했다.
@Override
public String toString() {
return "학교 : " + schools + ", 학생이름 : " + name + " ,나이 : " + age + " ,점수 : " + score;
}
}
package thisjavaexam.immutable;
public class School {
//학교를 추가 설정
private String schools;
public School(String schools) {
this.schools = schools;
}
public void setSchools(String schools) {
this.schools = schools;
}
public String getSchools(){
return schools;
}
@Override
public String toString() {
return schools;
}
}
package thisjavaexam.immutable;
public class StudentMain {
public static void main(String[] args) {
//학생정보를 불러오기 위한 생성자 생성 + 학교 생성
School schools = new School("서울고등학교");
Score score1 = new Score(90);
Score score2 = new Score(100);
Student std1 = new Student(schools,"홍길동",18,score1);
Student std2 = new Student(schools,"이몽룡",20,score2);
//Student클래스의 학생이름 나이 정보와, Score의 점수정보를 합쳐 출
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
//private 로 선언되었기 때문에 setter를 사용하
score2.setScore(80);
//다시 잘 변경되었는지 확인
System.out.println("학생2 점수를 80 점으로 변경");
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
//학생2를 부산고등학교로 전학 보내자
schools.setSchools("부산고등학교");
System.out.println("학생2 를 부산고로 전학 ");
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
//학생1을 다시 서울고등학교로 보내려면.. 새로운 생성자를 생성해야한다?
School schools1 = new School("서울고등학교"); // 생성자 std1의 첫번째 파라미터를 수정해야한다.
std1 = new Student(schools1,"홍길동",18,score1); // std1 의 재정의
System.out.println("학생1을 다시 서울고로 전학 ");
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
}
}
학생 2의 정보를 부산고등학교로 수정했을 뿐인데, 학생1의 정보까지 모두 부산고등학교로 바뀌어있다.
(이..이걸 구현하려고했던거다 앞선 포스트에...)
요 케이스를 해결하기위해 학교정보를 포함한 새로운 객체를 생성해야만 했다.
Schoolschools1=newSchool("서울고등학교");// 생성자 std1의 첫번째 파라미터를 수정해야한다.
std1=newStudent(schools1,"홍길동",18,score1);// std1 의 재정의
(요건 위의 코드에 붙어있다.)
그래서 마지막 정보는 서울,부산 두개의 학교에 나뉘어 처리된걸로 최종처리되었다.
당초 고등학교가 final로 선언된 불변객체였다면,
학생2 정보를 부산고등학교로 바꾸려고 했을때부터 오류가 발생했을것이고, 새로운 객체를 새로 생성해서 작업하도록 강제할 수 있었다.
그럼 School 클래스를 불변객체를 사용해보자
바꿀것은 3가지, 필드를 final선언 , setter는 사용할수없으니 삭제 , 그리고 외부에서 final선언된 학교를 변경하려 면 새로운 객체를 생성하도록 메서드를 만들어준다.
package thisjavaexam.immutable.real;
public class School {
//학교를 추가 설정
private final String schools; //선언값을 final 로 선언했다.
public School(String schools) {
this.schools = schools;
}
// 아래 셋터는 final이기에 사용할수 없다 . 삭제 !
// public void setSchools(String schools) {
// this.schools = schools;
// }
public String getSchools(){
return schools;
}
@Override
public String toString() {
return schools;
}
//외부에서 학교정보를 가져다 쓸 매서드를 만들고, 이 매서드는 new연산자를 통해서 새로운 인스턴스 객체를 만들어 준다.
public School withSchools(String newSchool) {
return new School(newSchool);
}
}
그리고 그 메서드를 불러내서, 새로 사용하면된다 .
package thisjavaexam.immutable.real;
public class StudentMain {
public static void main(String[] args) {
//학생정보를 불러오기 위한 생성자 생성 + 학교 생성
School schools = new School("서울고등학교");
Score score1 = new Score(90);
Score score2 = new Score(100);
Student std1 = new Student(schools,"홍길동",18,score1);
Student std2 = new Student(schools,"이몽룡",20,score2);
//Student클래스의 학생이름 나이 정보와, Score의 점수정보를 합쳐 출
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
//busanSchool에 대입한다 = 기존 schools의 withSchools매서드를 사용해서 부산고등학교를 생성한다.
School busanSchool = schools.withSchools("부산고등학교");
std2 = new Student(busanSchool, std2.getName(), std2.getAge(),score2); // 학생2 정보와 함께 새로 생성한 학교정보를 대입한다.
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
}
}
package thisjavaexam.immutable;
public class StudentMain {
public static void main(String[] args) {
//학생정보를 불러오기 위한 생성자 생성
Score score1 = new Score(90);
Score score2 = new Score(100);
Student std1 = new Student("홍길동",18,score1);
Student std2 = new Student("이몽룡",20,score2);
//Student클래스의 학생이름 나이 정보와, Score의 점수정보를 합쳐 출
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
}
}
우선 학생 의 점수를 변경해야 할때를 가정하여 간단하게 작성해본다.
필요한 클래스
1. 학생의 이름, 나이를 관리하는 "Student"클래스
2.점수를 변경해야할 "Score"클래스
3.실행할 Main클래스
점수 변경을 위한 Score 클래스 작성
package thisjavaexam.immutable;
public class Score {
//점수는 변경을 위해서 클래스를 별도 생
private int score;
//점수를 호출하기위한 생성자 생성
public Score(int score) {
this.score = score;
}
//게터 세터 생성
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
return String.valueOf(score);
}
}
학생 나이 선언을 위해 Student클래스 작성
package thisjavaexam.immutable;
public class Student {
//학생 이름,나이 를 선언 + Score 클래스를 변수로 선언
private String name;
private int age;
private Score score;
//Main클래스에서 사용하기위한 생성자 생성
Student(String name, int age, Score score){
this.name = name;
this.age = age;
this.score = score;
}
//각각 게터 , 세터 생성
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
//name의 문자열이 임의클래스(내가만든 이클래스) 를 호출하면서 발생될 참조값 반환을 방지하기위한 오버라이딩
@Override
public String toString() {
return " 학생이름 : " + name + " ,나이 : " + age + " ,점수 : " + score ;
}
}
실행을 위한 Main클래스 작성
실행 결과를 확인해본다 .
우선 의도한 대로 나오긴 했다.
이제 학생2의 정보를 점수만 80점으로 깍아봐야겠다.
//private 로 선언되었기 때문에 setter를 사용하
score2.setScore(80);
//다시 잘 변경되었는지 확인
System.out.println("학생2 점수를 80 점으로 변경");
System.out.println("학생1의 정보 = "+std1);
System.out.println("학생2의 정보 = "+std2);
어라? 의도한건 ... 학생2의 정보를 수정하면, 학생1의 정보를 동시에 바꾸는거였는데... 잘된다....
이러면 안되는데 ?
이번엔 학생2의 나이를 30으로 바꿔보자..
package thisjavaexam.immutable;
public class StudentMain {
public static void main(String[] args) {
//학생정보를 불러오기 위한 생성자 생성
Score score1 = new Score(90);
Score score2 = new Score(100);
Student std1 = new Student("홍길동",18,score1);
Student std2 = new Student("이몽룡",20,score2);
//Student클래스의 학생이름 나이 정보와, Score의 점수정보를 합쳐 출
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
//private 로 선언되었기 때문에 setter를 사용하
score2.setScore(80);
//다시 잘 변경되었는지 확인
System.out.println("학생2 점수를 80 점으로 변경");
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
//이제 학생이 이몽룡의 나이를 수정해보자
std2.setAge(30);
//다시 잘 된건지 확인
System.out.println("학생2의 나이를 30으로 변경");
System.out.println("학생1의 정보 = " + std1);
System.out.println("학생2의 정보 = " + std2);
}
}
"==" 으로 연산하는 "동일성" 은 두개의 객체가 참조주소가 동일한 객체를 가리키고 있는지를 본다. 즉, 같은주소에 데이터를 가르킨다면 값또한 같을꺼다.
"equals()"메서드를 사용한다면, 주소가 같은게아닌 해당 객체가 논리적으로 동일한지를 확인한다.. 즉 주소는 달라도 값이 동일하면OK.
public class UserV1 {
//String 타입 id1 선언
private String id1;
//생성자를 통해서 id1을 전달받음
public UserV1(String id1){
this.id1 = id1;
}
}
public class EqualsMainV1 {
public static void main(String[] args) {
// user1,user2로 같은 값을 인스턴스 생성
UserV1 user1 = new UserV1("id-100");
UserV1 user2 = new UserV1("id-100");
// == 과 equals를 사용하여 결과를 비교
System.out.println("Identity = " + (user1 == user2));
System.out.println("Equality = " + (user1.equals(user2)));
}
}
"=="의 경우 두개의 생성자로인해 주소값이 다를테니 당연히 false가 맞을테고,
equals()경우 같은 값인지만 볼텐데 false가 나오는건....
equals의 또 내부적인 코드가... 아래와 같기때문이라고 한다..
Object클래스의 내부
내부적인 코드에 ==가 있으니...결국은 false..라는건데
위 코드를 오버라이딩 한다.
public class UserV2 { //다른샘플
private String id;
public UserV2(String id) {
this.id = id;
}
@Override
public boolean equals(Object obj) { //overriding 한다.
UserV2 user = (UserV2) obj; //UserV2 타입의 user에 Object클래스의 obj를 다운캐스팅한다.
return id.equals(user.id); //값을 전달받은 id를 equals 처리한다.
}
}
처리하면 실행결과는 원했던 결과를 도출할 수 있다.
오버라이딩 결과
챗 지피티를 통해서 확인해보니 오버라이딩은..아래와 같이 하는게 정석이라고 하며 결과가 운이 좋아서 나온거라고 했다...
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // 동일성 체크
if (obj == null || getClass() != obj.getClass()) return false;
UserV1 user = (UserV1) obj;
return this.Id1.equals(user.Id1); // value 비교
}
내용을 보니 전달받은 객체가 동일하면 묻지도 따지지도말고 true를 반환하고...
obj 가 null이면 안되고, getClass() ! = obj.getClass() 이면 (클래스정보가 이미 틀리면) false를 반환하라고 되어있다.
나머지는 다운캐스팅해서 equals하는건동일한데....
솔직히 이해가 되질않았다.
질문 : 잠깐 public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false;
이코드는 너가말한 의미라면 오버라이딩된후 this == obj 면 true. 즉 오버라이딩하면 true(동등) 이 확인될꺼다
obj ==null. 널 값이거나, 클래스가 완전다르면 비교할 필요가 없다 .. 라는건데 결과적으로 내가 너한테 처음문의한것이 , 같은 클래스의 같은 값을 인스턴스만 2개로 분리해서 비교한건데,,,, 굳이 이 결과를 비교할 필요가있어 ?
✔ 이 계약을 깔끔히 지키려면: ==, null, getClass() 같은 체크는 반드시 들어가는 게 관례야.
관례를 물어본게 아니고...오버라이딩을 해야 값이 정상적으로 나오는건지, [if (this == obj) return true;] 요 값때문에 정상을 나온다는건지가 궁금했던거고...애초에 내가 짠 코드에선 "==" 는 존재하지도 않았기에 지피티가 오답을 준거 같았다...
재질의 자자.... 너 자꾸 말이 딴데로 새어나간다 ㅠㅠ "id-100".equals("id-100") → true 이말인즉슨 결국 오버라이딩 안해도 논리적으로는 true 가 나와야지..
public boolean equals(Object obj) { UserV2 user = (UserV2) obj; return id.equals(user.id);
이 코드는 qeuals(Object obj). >> Object 클래스의 이퀄 메서드를 obj에 받고 UserV2 타입의 user에 위의 obj를 다운캐스팅해서 대입하란거잖아 마지막으로 그 결과를 return 하는거고 즉..즉..그냥 이퀄을 쓴거야 , 나머지 구구절절한 코드는 오버라이딩의 작업일 뿐이고, 오버라이딩 안하면 equals는 false인데, 오버라이딩을 하면 true라는 말인거자나 내가 물어보는 핵심은 toString처럼 동적 바인딩 같은 시스템이 기저에 깔려있냐는거야
그제서야 답변을 다시해준다...
✅ 결론부터 말할게:
예, 맞아. equals()도 toString()과 똑같이 "동적 바인딩" 위에서 작동한다. 즉, 오버라이딩하면 실행 시점에 자식 클래스의 equals()가 호출된다.
그러니깐 너가말한 equals의 내부적인 == 때문에 발생된 일이기는 하고... 그 기저에는 동일한 연산을하더라도 오버라이딩을 하면 true로 연산되도록 동적 바인딩이 작업해준다 이거지 ? 또 if == ... true 말하지말고
✅ 네 말이 딱 맞다
같은 equals() 호출이더라도, 오버라이딩을 하면 JVM이 동적 바인딩을 통해 "내가 새로 정의한 equals() 메서드"를 실행해준다.
public class ToStringMain1 {
public static void main(String[] args) {
Object object = new Object(); //Object의 생성자 / toString을 호출하려면 생성자가 필요하다.
String string = object.toString(); // String 타입에 string에 object.toString()을 담았다.
//toString의 반환값을 출력
System.out.println(string); //toString을 활용해서 출력한 데이터인것이다.
//object를 직접 출력
System.out.println(object); //toString 없이 println을 통해서 출력한 것이다.
}
}
첫줄은 toString을 통해서 출력된 작업결과로 "(메서드명.참조주소-16진수)"로 출력해준다.
그런데... 둘째줄은 toString을 거치지 않은 결과물이지만, 결과가 똑같다.
"Println"의 내부적인 절차는
public void println(Object obj) {
String str = String.valueOf(obj); // 👈 여기서 obj.toString() 호출됨
...
}
public class Car {
private String carName;
public Car(String carName){
this.carName = carName;
}
}
public class ToStringMain {
public static void main(String[] args) {
Car car = new Car("model Y"); //Car 객체 생성, 부모클래스인 toString사용도 가능하다.
System.out.println("1. 단순 toString 호출");
System.out.println(car.toString()); //car.toString으로 출력했지만,
//Object클래스의 toString 을 사용한 결과이다.
System.out.println("2. println 내부에서 toString 호출");
System.out.println(car); // toString 을 거치지 않았지만, print문의 특성에 따라 toString이 발동되었다.
}
}
실행해보면 결과는 아래와 같다.
이것만 역시 결과는 같다.
그럼 결과를 추가적으로 보기위해서 ... Dog 클래스를 만들었다.
public class Dog {
private String dogName;
private int age;
//Dog의 생성자 이름과 나이를 받아 저장한다.
public Dog(String dogName, int age) {
this.dogName = dogName;
this.age = age;
}
//Object클래스의 toString메서드를 오버라이딩 했다. (재정의)
@Override
public String toString(){
return "dogName=" + dogName + "/" + "age=" + age;
}
}
그리고 toString을 재정의 했다.
public class ToStringMain {
public static void main(String[] args) {
Car car = new Car("model Y"); //Car의 생성자
Dog dog = new Dog("멍뭉이",2); //Dog의 생성자
System.out.println("1. 단순 toString 호출");
System.out.println(car.toString());
System.out.println(dog.toString());
System.out.println("2. println 내부에서 toString 호출");
System.out.println(car);
System.out.println(dog);
}
}
이에 따르는 결과는
첫번째 Car의 결과는 toString 의 구조대로, 클래서정보@16진수참조주소값이 정상출력되었고
두번째 Dog의 결과가 주소값이 아닌 우리가 흔하게 보는 "값" 이 출력되었다.
오버라이딩(재정의) 했을 뿐인데 이렇게 출력되는 무언가가 있는가?
Chat GPT
🔥 그 이유는 "동적 바인딩(Dynamic Dispatch)" 덕분이야
즉, toString()처럼 오버라이딩 가능한 메서드는
"실제 객체 타입" 기준으로 호출된다. JVM은 Object의 toString()이 오버라이딩되어 있다면, 실제 객체의 toString()을 호출함(이걸 "다형성(Polymorphism)" + "동적 바인딩" 이라고 부름)
그래서 동적 바인딩이 뭔데...
"다이나믹 디스패치(Dynamic Dispatch)는 그냥 JVM 내부에서 자동으로 처리되는 건가? 내가 직접 볼 수 있는 코드나 명시적인 규칙은 없나?"
👉 결론은 YES, 정확해.
다이나믹 디스패치는 JVM의 내부 설계에 의한 동작이지, 개발자가 직접 dynamicDispatch() 같은 코드를 호출하는 방식은 아니야.
✅ 정리: 다이나믹 디스패치는 "보이지 않지만 존재하는 룰"
그냥 외우는게 맞다고한다...
즉 결과적으로, 다형성으로 인한 Overriding + toString의 결과는 "주소가 아닌 값"을 그대로 출력해준다고한다.
정리하면
Dog(sub클래스) 의 생성자를 통해서 Object(super클래스)내부의 "toString"을 호출하면
참조주소를 뱉어내야 하지만!!
이때 JVM이 혹시~!~ sub클래스에 오버라이딩된 "내이름(toString) 이 있나?" 라고 쭉 훓어보고 1순위로 오버라이딩 된 결과를 출력해 버린다고 한다. (요게 다형성 + 오버라이딩)
요 키워드를 입력하고 사용해도 되지만 사실상 Object클래스는 자동으로 최상위클래스로 분류 되어있기 때문에
위의 임포트 문을 사용할 필요가 없다 .
이 캡쳐내용은 Object클래스의 실제모습이다.
toString(); // 객체 정보를 문자열로 반환 (주로 클래스명@해시값 또는 오버라이딩된 설명)
getClass(); // 객체의 클래스 정보를 반환 (런타임 클래스 확인용)
hashCode(); // 객체의 해시코드 반환 (HashMap 등에서 key로 사용할 때 필요)
equals(); // 두 객체가 같은지 비교 (==는 주소비교, equals는 값 비교 용도로 오버라이딩 가능)
etc...
이러한 메서드들을 어떤 클래스에서든 바로 사용 할 수 있는것이 묵시적으로 모든 클래스는 "Object" 클래스를 상속받고있다고 한다.
public class Child extends Parent{
...
public class Parent extends Object{ //extends Object 가 묵시적을 생략 한것임
요렇게 상속된 것처럼 , extends 처리되어있다. 실제적으로 extends가 없더라도 묵시적으로 모두 생략되어있다고 함.
그런데 interface 와 다르게 상속의 경우 , 1개의 부모만 상속받을 수있는데... 위의 Child 클래스는 어떻게 Object를 상속할 수 있냐면..
이런식으로 결과적으로 상속되었기 때문이다.
public class ObjectMain {
public static void main(String[] args) {
Child child = new Child(); //자녀클래스인 Child를 생성자로 생성을 했다.
//toString()은 오브젝트의 메서드
String string = child.toString(); //sub클래스(자녀클래스) 생성자로 child.toString(); 사용했다.
System.out.println(string);
이런식으로 결과적으로 상속받은 Object 클래스의 생성자를 만들지 않고, sub클래스 생성자를 통해서 "toString"을 사용할수 있다.