개발자입니다
[비트캠프] 53일차(11주차3일) - Java(오버라이딩, final, Object 클래스, String 클래스) 본문
[비트캠프] 53일차(11주차3일) - Java(오버라이딩, final, Object 클래스, String 클래스)
끈기JK 2023. 1. 18. 10:27
오버라이딩
Score2가 Score를 상속한다. compute() 를 재정의(overriding) → 서브 클래스의 역할에 맞춰 기능을 재정의!
Score2 score = new Score2(); 하면 Score의 필드가 먼저 생성되고 Score2의 필드가 생성된다. 그 주소 200이 score에 저장된다.
score.compute(); 하면 score의 주소 200이 compute()에 전달된다.
오버라이딩(overriding) - 오버라이딩을 하는 이유?
package com.eomcs.oop.ex06.c;
public class Exam0010 {
static class Score {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public void compute() {
this.sum = this.kor + this.eng + this.math;
this.aver = this.sum / 3f;
}
}
static class Score2 extends Score {
int music;
int art;
}
public static void main(String[] args) {
Score2 score = new Score2();
score.kor = 100;
score.eng = 100;
score.math = 100;
score.music = 50;
score.art = 50;
score.compute();
System.out.printf("%d(%f)\n", score.sum, score.aver);
// 합계와 평균이 틀린 이유?
// - 새로 추가한 필드에 맞춰서 계산을 해야 하는데
// 수퍼클래스의 compute() 메서드는
// 음악(music)과 미술(art) 점수를 고려하지 않고
// 기존 방식으로 계산하기 때문에
// 합계와 평균이 올바르지 않다.
}
}
package com.eomcs.oop.ex06.c;
public class Exam0020 {
static class Score {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public void compute() {
this.sum = this.kor + this.eng + this.math;
this.aver = this.sum / 3f;
}
}
static class Score2 extends Score {
int music;
int art;
// 오버라이딩(overriding)?
// => 상속 받은 메서드를 서브 클래스의 역할에 맞게 재정의하는 문법
// => 상속 받은 메서드와 똑 같은 시그너처(메서드명,파라미터 타입/개수/순서)로 메서드를 정의한다.
// => 오버라이딩 메서드는 원래의 메서드 보다 접근 범위가 같거나 커야 한다.
// 원래의 메서드 보다 접근 범위가 줄어들면 안된다.
public void compute() {
this.sum = this.kor + this.eng + this.math + this.music + this.art;
this.aver = this.sum / 5f;
}
}
public static void main(String[] args) {
Score2 score = new Score2();
score.kor = 100;
score.eng = 100;
score.math = 100;
score.music = 50;
score.art = 50;
score.compute(); // Score의 compute() 가 아니라 Score2에서 재정의한 메서드
System.out.printf("%d(%f)\n", score.sum, score.aver);
// Score2 클래스의 변경 사항에 맞춰
// compute() 메서드를 재정의 하였다.
// 그래서 합계와 평균이 제대로 나오는 것이다.
}
}
오버라이딩(overriding) - 문법이 존재하지 않는다면
package com.eomcs.oop.ex06.c;
public class Exam0110 {
static class A {
String name = "A";
String tel = "A: 010-1111-1111";
boolean working = true;
void print() {
System.out.println("A.print():");
System.out.printf(" => this.name(%s)\n", this.name);
System.out.printf(" => this.tel(%s)\n", this.tel);
System.out.printf(" => this.working(%s)\n", this.working);
}
}
static class A2 extends A {
int age = 20;
void print2() {
System.out.println("A2.print2():");
System.out.printf(" => this.name(%s)\n", this.name);
System.out.printf(" => this.tel(%s)\n", this.tel);
System.out.printf(" => this.working(%s)\n", this.working);
System.out.printf(" => this.age(%s)\n", this.age);
}
}
public static void main(String[] args) {
// 1) age 필드 추가하기 전
A obj1 = new A();
obj1.print(); // name, tel, working 값을 출력한다.
System.out.println("---------------------------------");
// 2) age 필드 추가한 후
A2 obj2 = new A2();
obj2.print(); // A 클래스의 print() 메서드 호출 : age 필드의 값이 출력되지 않는다.
System.out.println("---------------------------------");
// 아하! 그런데 문제가 있다.
// A 설계도에 있는 print()는 오직 이름, 전화번호, 재직여부 만 출력하게 되어 있다.
// 이 메서드를 변경해야 하는데,
// 기존의 A 클래스를 손대지 않고 print()의 기능을 변경할 순 없을까?
// 만약 없다면?
// => 다른 이름으로 메서드를 만들어야겠지!
// => 왜?
// 오버로딩에서는 파라미터 형식이 같은 메서드를 중복해서 만들 수 없다!
// 그래서 A2 클래스에서는 print2()라는 메서드를 추가하였다.
//
obj2.print2(); // A2 클래스의 print2() 메서드 호출
// 부모 클래스로부터 상속 받은 메서드를 재정의할 수 없어서
// 새 메서드를 만들어야 한다면,
// 같은(또는 유사한) 일을 하는 메서드에 대해
// 안타깝게도 다른 이름으로 메서드를 만들어야 하기 때문에
// 개발자는 여러 개의 메서드를 암기해야 하는 번거로움이 생긴다.
// => 이런 문제를 해결하기 위해 등장한 문법이 "오버라이딩(overriding)"이다!
}
}
오버라이딩(overriding) - 문법 사용
package com.eomcs.oop.ex06.c;
public class Exam0120 {
//오버라이딩(overrding)?
//=> 부모로부터 상속 받은 메서드 중에서
// 자신(서브클래스 입장에서)의 역할에 맞지 않는 메서드가 있다면,
// 자신(서브클래스 입장에서)의 역할에 맞춰 상속받은 메서드를 재정의하여
// 프로그래밍의 일관성을 확보하는 문법이다.
//
static class A {
String name = "A";
String tel = "A: 010-1111-1111";
boolean working = true;
void print() {
System.out.println("A.print():");
System.out.printf(" => this.name(%s)\n", this.name);
System.out.printf(" => this.tel(%s)\n", this.tel);
System.out.printf(" => this.working(%s)\n", this.working);
}
}
static class A3 extends A {
int age = 30;
// 상속 받은 메서드가 서브 클래스의 역할에 맞지 않다면
// 서브 클래스의 역할에 맞춰서 재정의 하라!
// 재정의?
// => 즉 부모의 메서드와 똑같은 메서드를 정의하는 것이다.
// => 그래서 부모의 메서드를 덮어쓰는(override) 것이다.
// => 이 메서드를 호출하는 개발자 입장에서는
// A 클래스와 같은 이름으로 된 메서드를 호출하기 때문에
// 별다른 구분없이 사용할 수 있다.
// => 부모 클래스의 메서드와 같은 시그너처(signature)를 갖고
// 리턴 타입도 같은 메서드를 만든다.
//
// * 메서드 시그너처(method signature) = 함수 프로토타입(function prototype)
// 메서드명, 파라미터 타입/개수/순서
void print() {
System.out.println("A3.print():");
System.out.printf(" => this.name(%s)\n", this.name);
System.out.printf(" => this.tel(%s)\n", this.tel);
System.out.printf(" => this.working(%s)\n", this.working);
System.out.printf(" => this.age(%s)\n", this.age);
}
}
public static void main(String[] args) {
A3 obj = new A3();
obj.name = "홍길동";
obj.age = 20;
// A3 클래스에서 A 클래스의 print() 메서드를 재정의 했기 때문에
// 다음 호출하는 메서드는 A3의 print() 메서드가 된다.
obj.print(); // A3에서 재정의한 print() 호출
}
}
오버라이딩(overriding) - 필드 오버라이딩
package com.eomcs.oop.ex06.c;
public class Exam0130 {
static class A {
String name;
String tel;
boolean working;
void print() {
System.out.println("A.print():");
System.out.printf(" => this.name(%s)\n", this.name);
System.out.printf(" => this.tel(%s)\n", this.tel);
System.out.printf(" => this.working(%s)\n", this.working);
}
}
static class A4 extends A {
// 필드 오버라이딩
// - 필드 오버라이딩은 메서드와 달리 변수의 타입이 달라도 된다.
//
String working;
void print2() {
System.out.println("A.print2():");
System.out.printf(" => this.name(%s)\n", this.name);
System.out.printf(" => this.tel(%s)\n", this.tel);
System.out.printf(" => this.working(%s)\n", this.working);
}
}
public static void main(String[] args) {
A4 obj = new A4();
obj.name = "홍길동"; // A의 name
obj.tel = "1111-1111"; // A의 tel
// obj.working = true; // A4의 working : obj의 클래스에서 먼저 필드를 찾기 때문이다.
obj.working = "취업";
obj.print();
// A의 print() 호출
// - A4 가 오버라이딩 한 필드를 사용하지 않는다.
// - 필드 오버라이딩은 그냥 새 필드를 추가한 것과 같다.
// - 가능한 수퍼 클래스의 필드와 같은 이름을 가진 필드를 만들지 말라!
// - A 클래스의 메서드에서 A 설계도에 따라 만든 필드를 먼저 찾는다.
System.out.println("-----------------");
obj.print2();
// - A4 클래스의 메서드에서는 A4 설계도에 따라 만든 필드를 먼저 찾는다.
}
}
오버라이딩(overriding) - 필드 및 메서드 오버라이딩
package com.eomcs.oop.ex06.c;
public class Exam0140 {
static class A {
String name;
String tel;
boolean working;
void print() {
System.out.println("A.print():");
System.out.printf(" => this.name(%s)\n", this.name);
System.out.printf(" => this.tel(%s)\n", this.tel);
System.out.printf(" => this.working(%s)\n", this.working);
}
}
static class A4 extends A {
// 필드 오버라이딩
// - 필드 오버라이딩은 메서드와 달리 변수의 타입이 달라도 된다.
//
String working;
// this.필드명
// - 현재 클래스에서 해당 필드를 찾는다. 없으면 상위 클래스로 따라 올라가면서 찾는다.
// super.필드명
// - 상위 클래스에서부터 해당 필드를 찾는다. 없으면 계속 상위 클래스로 따라 올라간다.
@Override
void print() {
System.out.println("A4.print():");
System.out.printf(" => this.name(%s)\n", this.name);
System.out.printf(" => this.tel(%s)\n", this.tel);
System.out.printf(" => this.working(%s)\n", this.working);
System.out.printf(" => super.working(%b)\n", super.working);
}
}
public static void main(String[] args) {
A4 obj = new A4();
obj.name = "홍길동"; // A의 name
obj.tel = "1111-1111"; // A의 tel
// obj.working = true; // A4의 working : obj의 클래스에서 먼저 필드를 찾기 때문이다.
obj.working = "취업";
obj.print(); // A4의 print() 호출 : A4에서 상속 받은 print()를 재정의 했기 때문이다.
}
}
필드 오버라이딩
A4가 A를 상속한다.
A4 obj = new A4(); 하면 A의 필드가 생성되고, A4의 필드가 생성된다. 그 주소 200이 obj에 저장된다.
obj.name 는 A.name (A의 name 인스턴스 필드라는 의미이다)
obj.tel 은 A의 tel 이다.
obj.working 은 A4의 working이다.
오버라이딩(overriding) - 오버라이딩 실수의 예
package com.eomcs.oop.ex06.c;
public class Exam0210 {
static class B {
void m(int a) {
System.out.println("B의 m()");
}
}
static class B2 extends B {
// 오버라이딩 문법
// => 메서드명, 파라미터 형식, 리턴 타입이 같아야 한다.
// => 파라미터의 이름은 달라도 된다. 상관없다.
//
void m(float x) {
// 그런데 이 메서드는 실수로 파라미터 타입을 float으로 했다.
// 이것은 오버라이딩이 아니라 오버로딩이 된 것이다.
// 즉 float 파라미터를 받는 m() 메서드가 추가된 것이다.
// 그런데 개발자는 오버라이딩(재정의)을 했다고 착각하고 사용할 것이다.
System.out.println("B2의 m()");
}
}
public static void main(String[] args) {
// 1) 오버라이딩을 잘못한 예:
B2 obj = new B2();
// B2에서 B의 m() 메서드를 오버라이딩(재정의) 했다고 착각하고
// 메서드를 사용할 수 있다.
// => 그런데 B2 클래스를 가보면 m()의 파라미터가 float이다.
// 즉 오버라이딩을 한 게 아니라 오버로딩을 한 것이 된다.
obj.m(100); // B의 m(int) 메서드를 호출
obj.m(3.14f); // B2의 m(float) 메서드를 호출
// 해결책?
// => 개발자들의 이런 실수가 많이 발생해서 자바에서는
// 오버라이딩 여부를 검사하는 특별한 문법을 추가하였다.
}
}
오버라이딩(overriding) - 오버라이딩 실수를 방지하는 방법
package com.eomcs.oop.ex06.c;
public class Exam0220 {
static class B {
void m(int a) {
System.out.println("B의 m()");
}
}
static class B3 extends B {
// 오버라이딩 문법 검사
// => 오버라이딩을 제대로 했는지 컴파일러에게 검사하도록 요청할 수 있다.
// => 어떻게?
// 메서드 정의 앞에 @Override를 붙여라
@Override // <= 컴파일러야, 내가 상속받은 메서드를 재정의한다고 했는데, 혹시 실수는 없는지 검사해 줄래?
void m(int x) {
// 오버라이딩을 한다고 하면서 파라미터의 타입이나 개수, 순서를 달리해서
// 오버로딩이 되는 경우가 있기 때문에
// 이를 방지하기 위해 오버라이딩을 하는 메서드 앞에
// @Override 애노테이션을 붙임으로써 잘못 사용하는 경우를 방지할 수 있다.
//
System.out.println("B3의 m()");
}
}
public static void main(String[] args) {
// 2) @Override를 이용해 오버라이딩의 실수를 방지한 예:
B3 obj = new B3();
obj.m(100); // B3의 m(int) 호출
// B3 클래스에서는 m() 메서드를 제대로 오버라이딩 하였다.
// @Override ?
// => 애노테이션 문법이다.
// => 컴파일러나 JVM에게 전달하는 특별한 주석이다.
// => 개발자도 자신의 애노테이션을 정의하고 사용할 수 있다.
// 나중에 따로 배운다!
}
}
Method Signature = 'function prototype' in C
메서드 선언문 첫 줄을 Method Signature라 한다. void는 sub class 가능하다. 파라미터의 변수명은 상관없다.
메서드 몸체를 method body라 한다.
오버라이딩(overriding) - private 메서드 오버라이딩?
package com.eomcs.oop.ex06.c;
public class Exam0310 {
//멤버의 접근 범위
//private : 같은 클래스
//(default) : 같은 클래스 + 같은 패키지
//protected : 같은 클래스 + 같은 패키지 + 서브 클래스
//public : 모두
//
static class C {
//private 접근 범위:
//=> 현재 클래스
private void m1() {}
//(default) 접근 범위:
//=> 현재 클래스 + 같은 패키지 소속 클래스
void m2() {}
//protected 접근 범위:
//=> 현재 클래스 + 같은 패키지 소속 클래스 + 서브 클래스
protected void m3() {}
//public 접근 범위:
//=> 모두
public void m4() {}
}
static class C2 extends C {
// 다음은 C의 메서드를 오버라이딩 한 게 아니다!
// 왜?
// - C의 m1()은 private이기 때문에 오직 C에서만 사용할 수 있다.
// - C2에서 접근할 수 없다. 접근할 수 없으니, 오버라이딩은 불가능하다!
//
// @Override
private void m1() {} // 컴파일 오류!
// 어? 강사님! @Override 빼니까 m1() 정의할 수 있는데요?
// => 이건 오버라이딩이 아니라 C2에 새로 메서드가 추가된 것이다.
// => 오버라이딩이라면 @Override 애노테이션을 붙였을 때 오류가 나지 말아야 한다!
//
// private void m1() {}
// 결론! 접근 권한이 없는 메서드는 오버라이딩 불가!
// 다음은 전형적인 오버라이딩의 예이다.
@Override void m2() {} // OK
@Override protected void m3() {} // OK
@Override public void m4() {} // OK
}
public static void main(String[] args) {
// C, C2, C3, C4 클래스의 주석을 확인하라!
}
}
오버라이딩(overriding) - 오버라이딩할 때 접근 범위 조정하기
package com.eomcs.oop.ex06.c;
public class Exam0320 {
static class C {
private void m1() {}
void m2() {}
protected void m3() {}
public void m4() {}
}
static class C3 extends C {
// 오버라이딩 메서드의 접근 범위를 확대하는 것은 괜찮다.
// => private 은 오버라이딩 자체가 불가능하기 때문에 접근 범위를 확대할 수 없다.
// @Override public void m1() {}
// @Override private void m2() {} // 컴파일 오류! 원래 접근 범위 보다 좁힐 수 없다.
// @Override void m2() {} // 접근 범위를 원래 대로 지정하는 것. OK!
// @Override protected void m2() {} // OK! default 보다 접근 범위를 확대하는 것은 허용된다.
@Override public void m2() {} // OK! default 보다 접근 범위를 확대하는 것은 허용된다.
// @Override private void m3() {} // 컴파일 오류! 원래 접근 범위 보다 좁힐 수 없다.
// @Override void m3() {} // 컴파일 오류! 원래 접근 범위 보다 좁힐 수 없다.
// @Override protected void m3() {} // 접근 범위를 원래 대로 지정하는 것. OK!
@Override public void m3() {} // OK! proteced 보다 접근 범위를 확대하는 것은 허용된다.
// @Override private void m4() {} // 컴파일 오류! 원래 접근 범위 보다 좁힐 수 없다.
// @Override void m4() {} // 컴파일 오류! 원래 접근 범위 보다 좁힐 수 없다.
// @Override protected void m4() {} // 컴파일 오류! 원래 접근 범위 보다 좁힐 수 없다.
@Override public void m4() {} // 접근 범위를 원래 대로 지정하는 것. OK!
}
public static void main(String[] args) {
// C, C2, C3, C4 클래스의 주석을 확인하라!
}
}
오버라이딩(overriding) - 오버라이딩할 때 접근 범위 조정하기 II
package com.eomcs.oop.ex06.c;
public class Exam0330 {
static class C {
private void m1() {}
void m2() {}
protected void m3() {}
public void m4() {}
}
static class C4 extends C {
// 오버라이딩 할 때(메서드를 재정의 할 때)
// => 원본 보다 접근 범위를 좁힐 수는 없다.
//
// @Override private void m2() {} // (default) ==> private : 컴파일 오류!
// @Override void m3() {} // protected ==> (default) : 컴파일 오류!
// @Override protected void m4() {} // public ==> protected : 컴파일 오류!
// 결론!
// 수퍼 클래스의 메서드를 자식 클래스가 재정의할 때
// 접근 범위를 확대할 순 있지만, 좁힐 수는 없다!
}
public static void main(String[] args) {
// C, C2, C3, C4 클래스의 주석을 확인하라!
}
}
오버라이딩(overriding) - 오버라이딩과 super 키워드
package com.eomcs.oop.ex06.c;
public class Exam0410 {
static class A {
void m() {
System.out.println("A의 m()");
}
}
static class A2 extends A {
@Override
void m() {
System.out.println("A2의 m()");
}
void test() {
// this 레퍼런스로 메서드를 호출하면,
// => 현재 클래스(this의 실제 클래스, 예: A2)에서 메서드를 찾아 호출한다.
// => 현재 클래스에 메서드가 없으면 수퍼 클래스에서 메서드를 찾는다.
// => 메서드를 찾을 때까지 최상위 클래스까지 따라 올라간다.
this.m();
// super 레퍼런스로 메서드를 호출하면,
// => 수퍼 클래스(메서드가 소속된 클래스의 수퍼 클래스, 예: A)에서 메서드를 찾아 호출한다.
// => 수퍼 클래스에 없으면 그 상위 클래스로 따라 올라간다.
// => 오버라이딩 하기 전의 메서드를 호출하고 싶을 때 유용하다.
super.m();
}
}
public static void main(String[] args) {
A2 obj = new A2();
obj.test();
}
}
A2가 A를 상속한다.
this.m() 은 현재 클래스부터 찾아간다.
super.m() 은 수퍼 클래스부터 찾아간다.
package com.eomcs.oop.ex06.c;
public class Exam0411 {
static class A {
void m() {
System.out.println("A의 m()");
}
}
static class A2 extends A {
@Override
void m() {
System.out.println("A2의 m()");
}
void test() {
// this 레퍼런스로 메서드를 호출하면,
// => 현재 클래스(this의 실제 클래스, 예: A3)에서 메서드를 찾아 호출한다.
// => 현재 클래스에 메서드가 없으면 수퍼 클래스에서 메서드를 찾는다.
// => 메서드를 찾을 때까지 최상위 클래스까지 따라 올라간다.
this.m();
// super 레퍼런스로 메서드를 호출하면,
// => 수퍼 클래스(메서드가 소속된 클래스의 수퍼 클래스, 예: A)에서 메서드를 찾아 호출한다.
// => 수퍼 클래스에 없으면 그 상위 클래스로 따라 올라간다.
// => 오버라이딩 하기 전의 메서드를 호출하고 싶을 때 유용하다.
super.m();
}
}
static class A3 extends A2 {
@Override
void m() {
System.out.println("A3의 m()");
}
}
public static void main(String[] args) {
A3 obj = new A3();
obj.test(); // A2의 test() 호출
}
}
A3가 A2를 상속하고, A2가 A를 상속한다.
A3 obj = new A3();
obj.test(); 하면 먼저 this.m() 가 실행된다. this에는 호출한 인스턴스의 주소가 들어가므로 A3에 m() 이 있는지 찾아본다. A3에 m()이 있으므로 실행한다. 그 다음 super.m() 이 실행된다. super는 자신이 속한 클래스의 상위 클래스부터 m() 을 찾는다. A에 m()이 있으므로 실행한다.
오버라이딩(overriding) - this/super의 사용
package com.eomcs.oop.ex06.c;
// this.메서드() 호출?
// => 현재 클래스(this의 실제 타입)부터 호출할 메서드를 찾아 올라 간다.
//
// super.메서드() 호출?
// => 부모 클래스(메서드가 소속된 클래스의 부모)부터 호출할 메서드를 찾아 올라 간다.
//
public class Exam0420 {
static class X {
void m1() {
System.out.println("X의 m1()");
}
void m2() {
System.out.println("X의 m2()");
}
}
static class X2 extends X {
@Override
void m1() {
System.out.println("X2의 m1()");
}
}
static class X3 extends X2 {
@Override
void m2() {
System.out.println("X3의 m2()");
}
}
static class X4 extends X3 {
@Override
void m1() {
System.out.println("X4의 m1()");
}
void test() {
this.m1(); // X4의 m1() : this가 실제 가리키는 인스턴스 클래스를 기준으로 메서드를 찾아 올라 간다.
super.m1(); // X2의 m1() : test()가 소속된 클래스를 기준으로 수퍼 클래스부터 메서드를 찾아 올라간다.
this.m2(); // X3의 m2()
super.m2(); // X3의 m2()
// super.super.m1(); // 컴파일 오류! 이런 문법은 없다! 무협지 문법!
}
}
public static void main(String[] args) {
X4 obj = new X4();
obj.test();
// 레퍼런스에서 super를 사용할 수 없다.
// obj.super.m1(); // 컴파일 오류!
}
}
X4 obj = new X4();
obj.test(); 실행한다.
this.m1() 으로 인스턴스 obj의 클래스 X4에서 m1()을 찾아본다. 있으므로 실행한다.
super.m1() 으로 obj의 클래스 X4의 수퍼 클래스 X3에서 m1()을 찾아본다. 없으므로 X2에서 찾아본다. 있으므로 실행한다.
this.m2() 로 obj의 클래스 X4에서 m2()를 찾아본다. 없으므로 X3에서 찾아본다. 있으므로 실행한다.
super.m2() 로 obj의 클래스 X4의 수퍼 클래스 X3에서 m2()를 찾아본다. 있으므로 실행한다.
오버라이딩(overriding) - this/super의 사용
package com.eomcs.oop.ex06.c;
// this.메서드() 호출?
// => 현재 클래스(this의 실제 타입)부터 호출할 메서드를 찾아 올라 간다.
//
// super.메서드() 호출?
// => 부모 클래스(메서드가 소속된 클래스의 부모)부터 호출할 메서드를 찾아 올라 간다.
//
public class Exam0421 {
static class X {
void m1() {System.out.println("X의 m1()");}
void m2() {System.out.println("X의 m2()");}
}
static class X2 extends X {
@Override void m1() {System.out.println("X2의 m1()");}
}
static class X3 extends X2 {
@Override void m2() {System.out.println("X3의 m2()");}
}
static class X4 extends X3 {
@Override void m1() {System.out.println("X4의 m1()");}
void test() {
this.m1(); // X5의 m1() : this가 실제 가리키는 인스턴스 클래스를 기준으로 메서드를 찾아 올라 간다.
super.m1(); // X2의 m1() : test()가 소속된 클래스를 기준으로 수퍼 클래스부터 메서드를 찾아 올라간다.
this.m2(); // X5의 m2()
super.m2(); // X3의 m2()
}
}
static class X5 extends X4 {
@Override void m1() {System.out.println("X5의 m1()");}
@Override void m2() {System.out.println("X5의 m2()");}
}
public static void main(String[] args) {
X5 obj = new X5();
obj.test();
}
}
X5 obj = new X5();
obj.test(); 하면 obj의 X5의 인스턴스 주소가 넘어간다. X4의 test() 가 실행된다.
this.m1() 으로 obj의 클래스 X5에서 m1()을 찾아 실행한다.
super.m1() 으로 메서드가 속한 X4 클래스의 수퍼 클래스부터 찾아본다. X3없고 X2에 m1() 이 있어 실행한다.
this.m2() 로 boj의 클래스 X5에서 m2()를 찾아 실행한다.
super.m2() 로 메서드가 속한 X4 클래스의 수퍼 클래스부터 찾아본다. X3에 있는 m2() 실행한다.
오버라이딩(overriding) - 준비
package com.eomcs.oop.ex06.c;
public class Exam0430 {
static class A {
String name = "A";
String tel = "A: 010-1111-1111";
boolean working = true;
void print() {
System.out.println("A.print():");
System.out.printf(" => this.name(%s)\n", this.name);
System.out.printf(" => this.tel(%s)\n", this.tel);
System.out.printf(" => this.working(%s)\n", this.working);
}
}
static class A2 extends A {
int age = 20;
@Override
void print() {
System.out.println("A2.print():");
System.out.printf(" => this.name(%s), super.name(%s)\n",
this.name, super.name);
System.out.printf(" => this.tel(%s), super.tel(%s)\n",
this.tel, super.tel);
System.out.printf(" => this.working(%s), super.working(%s)\n",
this.working, super.working);
System.out.printf(" => this.age(%s), super.age(컴파일 오류!) \n",
this.age /*, super.age*/);
}
}
static class A3 extends A {
int age = 30;
String tel = "A3: 010-1111-2222";
@Override
void print() {
System.out.println("A3.print():");
System.out.printf(" => this.name(%s), super.name(%s)\n", this.name, super.name);
System.out.printf(" => this.tel(%s), super.tel(%s)\n", this.tel, super.tel);
System.out.printf(" => this.working(%s), super.working(%s)\n", this.working, super.working);
System.out.printf(" => this.age(%s), super.age(컴파일 오류!) \n", this.age /*, super.age*/);
}
}
static class A4 extends A3 {
String age = "40";
boolean working = false;
@Override
void print() {
System.out.println("A4.print():");
System.out.printf(" => this.name(%s), super.name(%s)\n", this.name, super.name);
System.out.printf(" => this.age(%s), super.age(%s)\n", this.age, super.age);
System.out.printf(" => this.tel(%s), super.tel(%s)\n", this.tel, super.tel);
System.out.printf(" => this.working(%s), super.working(%s)\n", this.working, super.working);
}
}
public static void main(String[] args) {
A obj1 = new A();
obj1.print();
System.out.println("--------------------------------");
A2 obj2 = new A2();
obj2.print();
System.out.println("--------------------------------");
A3 obj3 = new A3();
obj3.print();
System.out.println("--------------------------------");
A4 obj4 = new A4();
obj4.print();
System.out.println("--------------------------------");
A3 obj5 = new A4();
obj5.print();
// 레퍼런스가 실제 자식 객체를 가리킨다면, 메서드를 찾을 때
// 자식 클래스의 오버라이딩 메서드를 먼저 찾는다.
// 따라서 obj5의 print()는 실제 obj5가 가리키는 A4의 print()를 호출한다.
System.out.println("--------------------------------");
}
}
Overriding 과 super 키워드
A4가 A3를 상속하고 A3, A2가 A를 상속한다.
*결론
- 필드를 오버라이딩 하지 말라!
*this.인스턴스 필드 → 항상 메서드가 소속된 클래스부터 필드를 찾는다.
메서드 호출시 this는 메서드를 호출한 클래스부터 찾는다. super는 super가 작성되어 있는 메서드를 갖는 클래스의 super 클래스부터 찾는다.
필드 호출시 this는 메서드가 소속된 클래스의 필드부터 찾는다. super는 메서드가 소속된 클래스의 super 클래스부터 찾는다.
메서드 호출시의 this만 다르다.
오버라이딩(overriding) - 레퍼런스가 가리키는 필드
package com.eomcs.oop.ex06.c;
public class Exam0510 {
static class A {
String name = "A";
String tel = "A: 010-1111-1111";
boolean working = true;
}
static class A2 extends A {
int age = 20;
}
static class A3 extends A {
int age = 30;
String tel = "A3: 010-1111-2222";
}
static class A4 extends A3 {
String age = "40";
boolean working = false;
}
public static void main(String[] args) {
A4 obj1 = new A4();
System.out.println(obj1.name); // "A"
System.out.println(obj1.age); // "40"
System.out.println(obj1.working); // false
System.out.println("-----------------------------");
// 레퍼런스를 형변환 하면 오버라이딩 하기 전,
// 해당 클래스의 인스턴스 변수를 가리킬 수 있다.
System.out.println(((A3)obj1).age); // 30
System.out.println(((A3)obj1).working); // true -> 없으면 수퍼 클래스의 필드를 가리킨다.
System.out.println("-----------------------------");
// 형변환 클래스에 존재하지 않는 필드는 가리킬 수 없다.
// System.out.println(((A)obj1).age); // 컴파일 오류!
System.out.println(((A)obj1).working); // true
}
}
형변환과 메서드 호출
상속 관계는 왼쪽과 같다.
X4 obj = new X4()
obj.m1(); 은 X4의 m1() 이다.
obj.m2(); 는 X3의 m2() 이다.
obj.x(); 는 X4의 x() 이다.
((X3) obj).m1(); 는 X4의 m1() 이다. X3 계층도에서 m1()의 호출이 가능하다면 (컴파일러 역할) → obj가 실제 가리키는 객체의 클래스에서부터 메서드를 찾아 올라간다 (JVM 역할)
((X3) obj).m2(); 는 X3의 m2() 이다.
((X3) obj).x(); 는 컴파일 오류! ((X3) obj) 에 x() 가 없어 컴파일시 에러난다.
오버라이딩(overriding) - 레퍼런스가 가리키는 메서드
package com.eomcs.oop.ex06.c;
public class Exam0511 {
static class X {
void m1() {
System.out.println("X의 m1()");
}
void m2() {
System.out.println("X의 m2()");
}
}
static class X2 extends X {
@Override
void m1() {
System.out.println("X2의 m1()");
}
}
static class X3 extends X2 {
@Override
void m2() {
System.out.println("X3의 m2()");
}
}
static class X4 extends X3 {
@Override
void m1() {
System.out.println("X4의 m1()");
}
}
public static void main(String[] args) {
X4 x4 = new X4();
x4.m1(); // X4.m1()
// 인스턴스 필드와 달리 메서드의 경우는
// 레퍼런스에 대한 형변환에 상관없이
// 실제 레퍼런스가 가리키는 클래스에서 메서드를 찾아 올라간다.
//
((X3)x4).m1(); // X4.m1()
((X2)x4).m1(); // X4.m1()
((X)x4).m1(); // X4.m1();
X3 x3 = x4;
X2 x2 = x4;
X x = x4;
x3.m1(); // X4.m1()
x2.m1(); // X4.m1()
x.m1(); // X4.m1()
}
}
오버라이딩과 리턴타입
상속 관계가 왼쪽 그림과 같다.
CarFactory에 create(): Car 메서드가 있다.
SedanFactory에 create(): 에서 CarFactory를 상속한다. 그러면 Car의 서브클래스도 가능! Sedan 또는 Tico, Truck, Car 가능하다.
TicoFactory에 create(): 에서 SedanFactory를 상속한다. 그러면 Sedan의 서브클래스도 가능! Tico 또는 Sedan 가능하다.
오버라이딩(overriding) - 리턴 타입
package com.eomcs.oop.ex06.c;
public class Exam0610 {
static class Car {}
static class Sedan extends Car {}
static class Truck extends Car {}
static class Tico extends Sedan {}
static class CarFactory {
Car create() {
return new Car();
}
}
static class SedanFactory extends CarFactory {
// 오버라이딩 메서드의 리턴 타입은
// 서브 클래스도 가능하다.
@Override
Sedan create() {
return new Sedan();
}
}
static class TicoFactory extends SedanFactory {
// 오버라이딩 메서드의 리턴 타입은
// 서브 클래스도 가능하다.
@Override
Tico create() {
return new Tico();
}
}
public static void main(String[] args) {
}
}
package com.eomcs.oop.ex06.c;
class Car {
String maker;
String model;
int cc;
void run() {System.out.println("자동차가 달린다!");}
}
class Sedan extends Car {
boolean sunroof;
public void openSunroof() {sunroof = true;}
public void closeSunroof() {sunroof = false;}
@Override
void run() {System.out.println("승용차가 달린다!");}
}
class Truck extends Car {
int weight;
public void dump() {weight = 0;}
@Override
void run() {System.out.println("트럭이 달린다!");}
}
class DumpTruck extends Truck {
@Override
public void dump() {weight = 0;}
@Override
void run() {System.out.println("덤프트럭이 달린다!");}
}
class CarFactory {
Car makeCar() {
return new Car();
}
}
class SedanFactory extends CarFactory {
// 메서드를 오버라이딩 할 때 리턴 타입으로 서브 클래스도 가능하다.
@Override
Sedan makeCar() {
return new Sedan();
};
}
class TruckFactory extends CarFactory {
// 메서드를 오버라이딩 할 때 리턴 타입으로 서브 클래스도 가능하다.
@Override
Truck makeCar() {
return new Truck();
};
}
class DumpTruckFactory extends TruckFactory {
// 메서드를 오버라이딩 할 때 리턴 타입으로 서브 클래스도 가능하다.
@Override
DumpTruck makeCar() {
return new DumpTruck();
};
}
class DumpTruckFactory2 extends TruckFactory {
// 메서드를 오버라이딩 할 때 리턴 타입으로 수퍼 클래스는 불가능하다.
// => 컴파일 오류!
//
// @Override
// Car makeCar() {
// return new Car();
// };
}
public class Exam0620 {
public static void main(String[] args) {
new CarFactory().makeCar().run();
new SedanFactory().makeCar().run();
new TruckFactory().makeCar().run();
new DumpTruckFactory().makeCar().run();
new DumpTruckFactory2().makeCar().run();
}
}
com.eomcs.oop.ex06.d
다형적 변수와 오버라이딩 - 레퍼런스와 메서드 호출
package com.eomcs.oop.ex06.d;
abstract class Car {
public abstract void run();
public void m() {}
}
class Sedan extends Car {
@Override
public void run() {
System.out.println("Sedan.run() 호출됨!");
}
}
public class Exam0210 {
public static void main(String[] args) {
// 1) 다형적 변수의 사용법에 따라,
// - super 클래스 레퍼런스로 하위 클래스의 인스턴스를 가리킨다.
Car car = new Sedan();
// 2) 오버라이딩 메서드 호출 규칙에 따라,
// - 레퍼런스가 실제 가리키는 객체의 클래스부터 메서드를 찾아 올라간다.
car.run();
}
}
com.eomcs.oop.ex06.e
final 사용법: 상속 불가!
package com.eomcs.oop.ex06.e;
// 클래스에 final 을 붙이면 이 클래스의 서브 클래스를 만들 수 없다.
// - 서브 클래스의 생성을 방지하여
// 기존 클래스를 대체하지 못하도록 할 때 사용한다.
// - 예)
// java.lang.String
//
final class A {
}
// final 클래스를 상속 받을 수 없다.
public class Exam0110 // extends A
{
}
final 사용법: 오버라이딩 불가!
package com.eomcs.oop.ex06.e;
class B {
// 메서드에 final 을 붙이면 서브 클래스에서 오버라이딩 할 수 없다.
// - 서브 클래스에서 변경하면 안되는 메서드인 경우에 사용한다.
// - 예)
// - 보안에 관련된 일을 하는 메서드
// - 템플릿 메서드 디자인 패턴에서처럼
// 전체적인 작업 흐름을 정의한 메서드의 경우
// 서브 클래스의 오버라이딩을 막는 것이 좋다.
//
final void m1() {
System.out.println("B.m() 호출!");
}
void m2() {
}
}
public class Exam0210 extends B {
// final 메서드는 오버라이딩 불가!
//
// @Override
// void m1() {
//
// }
// 일반 메서드는 오버라이딩 가능!
@Override
void m2() {
}
}
final 사용법: 상수 필드 만들기
package com.eomcs.oop.ex06.e;
class C {
// 필드에 final 을 붙이면 상수 필드가 된다.
// 생성자에서 초기화시켜야 한다.
//
final int v1;
public C() {
v1 = 100;
// v1 = 101; // final 필드는 딱 한 번만 값을 설정할 수 있다.
}
public void m1() {
// 상수 필드는 값을 변경할 수 없다.
// v1 = 200; // 컴파일 오류!
}
}
public final class Exam0310 {
public static void main(String[] args) {
C c = new C();
System.out.println(c.v1);
}
}
final 사용법: 상수 필드 만들기
package com.eomcs.oop.ex06.e;
class D {
// 변수 초기화 문장으로 값을 초기화시킬 수 있다.
// => 변수 초기화 문장은 컴파일 될 때 생성자로 복사되기 때문이다.
final int v1 = 100;
public D() {
// 초기화 문장에서 값을 설정했으면,
// 생성자에서 다시 값을 설정할 수 없다.
// 왜?
// - 초기화 문장은 결국 컴파일할 때
// 생성자의 첫 번째 문장으로 옮겨지기 때문이다.
// v1 = 200; // 컴파일 오류!
}
}
public final class Exam0320 {
public static void main(String[] args) {
D d = new D();
System.out.println(d.v1);
}
}
package com.eomcs.oop.ex06.e;
class E {
// 인스턴스 초기화 블록에서 값을 초기화시켜도 된다.
final int v1;
// 초기화 블록의 코드 또한 생성자에 복사된다.
{
v1 = 200; // OK!
// v1 = 300; // 컴파일 오류!
}
}
public final class Exam0330 {
public static void main(String[] args) {
E e = new E();
System.out.println(e.v1);
}
}
package com.eomcs.oop.ex06.e;
class F {
// 상수 필드는 인스턴스 마다 개별적으로 관리하지 않기 때문에
// 보통 스태틱 필드(클래스 필드)로 만든다.
// 공개할 경우 public 으로 선언한다.
//
public static final int v1 = 100;
// 스태틱 상수 필드는 스태틱 블록에서 초기화시킬 수 있다.
public static final int v2;
static {
v2 = 200;
}
}
public final class Exam0340 {
public static void main(String[] args) {
System.out.println(F.v1);
System.out.println(F.v2);
}
}
final 사용법: 로컬 변수
package com.eomcs.oop.ex06.e;
public final class Exam0410 {
public void m1() {
// 로컬 변수에 final을 붙이면 값을 변경할 수 없는 상수로 사용된다.
final int a = 100;
// final 로컬 변수는 값을 변경할 수 없다.
// a = 200; // 컴파일 오류!
// 변수를 선언할 때 값을 초기화시키지 않고,
final int b;
// 다음에 초기화시킬 수 있다.
b = 100;
// 일단 한 번 변수의 값이 초기화되면 변경할 수 없다.
// b = 200; // 컴파일 오류!
}
}
final 사용법: 파라미터
package com.eomcs.oop.ex06.e;
class G {
public void m1(final int a) {
// 파라미터는 메서드가 호출될 때 외부의 값을 받는 용도의 변수다.
// 메서드 안에서 파라미터 값을 임의로 변경하게 되면
// 처음 받은 파라미터 값을 사용하지 못하는 상황이 발생한다.
// 그래서 이런 상황을 피하고자,
// 보통 실무에서 파라미터를 final로 선언한다.
//
// a = 200; // 컴파일 오류!
System.out.println(a);
}
}
public final class Exam0510 {
public static void main(String[] args) {
G g = new G();
g.m1(100);
}
}
Object 클래스
Objec 클래스의 주요 메서드
Object 클래스의 메서드는 다음과 같다.
toString → "Fully-Qualified Name@해시번호" = FQName = QName
↑ 패키지명 + 클래스명 ↑ 각 인스턴스에 부여되는 식별번호 *주의! 인스턴스 주소가 아니다!
hashCode() → 해시번호(int)
equals() → 인스턴스 주소가 같은지 비교
getClass() → Class 객체 : 클래스 정보(클래스명, 필드, 메서드, 생성자, 수퍼클래스, 인터페이스)를 담은 객체
clone() → 인스턴스 복제 및 리턴
finalize() → GC에 의해 메모리 해제 직전 호출됨. C++에서는 소멸자(destructor)라 부른다. 보통 오버라이딩 하지않는다.
com.eomcs.basic.ex01
Object 클래스 - 자바 최상위 클래스
package com.eomcs.basic.ex01;
//클래스를 정의할 때 수퍼 클래스를 지정하지 않으면
//컴파일러는 자동으로 Object를 상속 받는다.
public class Exam0110 /*extends Object*/ {
static class My /*extends Object*/ {
}
public static void main(String[] args) {
// instanceof 연산자를 사용하여 해당 인스턴스가 Object 타입인지 확인해 보자.
// instanceof 연산자?
// => 레퍼런스가 가리키는 인스턴스가 지정한 클래스를 인스턴스 이거나 또는 조상으로 갖는지 검사한다.
Object obj = new My();
// Object의 레퍼런스에 My 인스턴스 주소를 저장할 수 있다는 것은
// My 클래스가 Object 크래스의 서브 클래스임을 증명하는 것이다.
System.out.println(obj instanceof My);
System.out.println(obj instanceof String);
System.out.println(obj instanceof Object);
// Object를 조상으로 갖는다면 당연히 Object의 메서드를 사용할 수 있을 것이다.
System.out.println(obj.toString());
System.out.printf("%08x, %1$d\n", obj.hashCode());
System.out.println(obj.equals("Hello"));
// 결론!
// => 자바의 모든 클래스는 Object의 자손이다.
}
}
Object 클래스의 주요 메서드
1) toString()
=> 클래스이름과 해시코드를 리턴한다.
2) equals()
=> 같은 인스턴스인지 검사한다.
3) hashCode()
=> 인스턴스를 식별하는 값을 리턴한다.
4) getClass()
=> 인스턴스의 클래스 정보를 리턴한다.
5) clone()
=> 인스턴스를 복제한 후 그 복제 인스턴스를 리턴한다.
6) finalize()
=> 가비지 컬렉터에 의해 메모리에서 해제되기 직전에 호출된다.
Object 클래스 - toString() 메서드에 대하여
package com.eomcs.basic.ex01;
public class Exam0120 {
static class My {
}
public static void main(String[] args) {
My obj = new My();
// Object에서 상속 받은 메서드
//
// 1) toString()
// - 클래스 정보를 간단히 출력한다.
// - 패키지명.클래스명@16진수해시값
// - 예) ch15.My1@1e81f4dc
//
System.out.println(obj.toString());
// println()에 넘겨주는 값이 String 타입이 아니라면
// println()은 그 객체에 대해 toString() 호출한 후 그 리턴 값을 출력한다.
// 따라서 다음 코드는 위의 코드와 같다.
System.out.println(obj);
// 해시값?
// - 인스턴스 마다 부여된 고유의 식별자이다.
// - 주의! 주소 아니다!
// - 인스턴스가 같은지 검사할 때 사용할 수 있다.
// - hashCode()를 재정의하지 않고 원래 메서드를 그대로 사용하면
// 무조건 인스턴스마다 새 해시값이 부여된다.
My obj2 = new My();
My obj3 = new My();
System.out.println(obj2 == obj3);
System.out.println(obj2.toString());
System.out.println(obj3.toString());
}
}
Object 클래스 - toString() 을 오버라이딩 할 때
package com.eomcs.basic.ex01;
public class Exam0121 {
static class My {
String name;
int age;
// 개발을 하다 보면 인스턴스의 현재 값을 간단히 확인하고 싶을 경우가 있다.
// 그럴 경우 toString()을 오버라이딩 하라!
@Override
public String toString() {
return "My [name=" + name + ", age=" + age + "]";
}
}
public static void main(String[] args) {
My obj1 = new My();
obj1.name = "홍길동";
obj1.age = 20;
System.out.println(obj1.toString());
// println()의 파라미터 값으로 문자열을 넘겨주지 않으면,
// println() 내부에서 파라미터로 넘어온 객체에 대해 toString() 호출한 후
// 그 리턴 값을 출력한다.
// 따라서 그냥 객체(주소)를 넘겨줘도 된다.
System.out.println(obj1);
}
}
우클릭 > generate toString()... 으로 위 문장 자동 생성 가능하다.
Object 클래스의 메서드를 오버라이딩 하기 - toString()
package com.eomcs.basic.ex01;
public class Exam0122 extends Object {
// 수퍼 클래스를 지정하지 않으면 기본으로 java.lang.Object 클래스가
// 수퍼 클래스로 지정된다.
static class Score /* extends Object */{
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public Score(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
this.sum = kor + eng + math;
this.aver = this.sum / 3f;
}
}
public static void main(String[] args) {
Score s1 = new Score("홍길동", 100, 100, 100);
String str = s1.toString();
// toString()?
// => Score 클래스의 수퍼 클래스인 Object의 메서드이다.
// => 클래스를 정의할 때 수퍼 클래스를 지정하지 않으면
// 자동으로 java.lang.Object 클래스가 수퍼 클래스로 지정된다.
// => 그래서 자바의 모든 클래스는 toString()을 호출할 수 있다.
// 즉 자바의 모든 클래스는 Object 클래스에 정의된 메서드를 호출할 수 있다.
System.out.println(str);
// toString()의 리턴 값?
// => Object의 toString()을 호출하면 다음 형식의 문자열을 리턴한다.
// "패키지 이름을 포함한 클래스명@인스턴스 식별자"
// 예) com.eomcs.oop.ex06.d.Exam0122$Score@12a74311
//
// 인스턴스 식별자?
// => 인스턴스의 주소가 아니다. 자바는 절대로 메모리 주소를 알려주지 않는다!
// => 단지 인스턴스를 식별할 때 사용하라고 JVM이 임의로 붙인 식별자이다.
// => 이 식별자를 "해시값(hashcode)"라 부른다.
//
}
}
Object 클래스의 메서드를 오버라이딩 하기 - toString()
package com.eomcs.basic.ex01;
public class Exam0123 {
static class Score {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public Score(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
this.sum = kor + eng + math;
this.aver = this.sum / 3f;
}
// Object로부터 상속 받은 toString()의 리턴 값이 마음에 들지 않는다면
// 재정의하라!
// => 보통 인스턴스의 내부 데이터를 문자열로 리턴하도록 변경한다.
// => 프로그램을 실행하는 중에 인스턴스의 내부 값을 빠르게 확인하고 싶을 때
// 개발자들이 종종 이 메서드를 오버라이딩 한다.
//
@Override
public String toString() {
return String.format("%s,%d,%d,%d,%d,%.1f",
this.name, this.kor, this.eng, this.math,
this.sum, this.aver);
}
}
public static void main(String[] args) {
Score s1 = new Score("홍길동", 100, 100, 100);
String str = s1.toString();
// Score 클래스에서 Object의 toString()을 오버라이딩 했기 때문에,
// Score의 toString()을 호출한다.
System.out.println(str);
// println()에 String이 아닌 객체를 넘기면,
// println()에서 내부적으로 그 객체에 대해 toString()을 호출하여
// 그 리턴 값을 출력한다.
// 따라서, 다음 코드는 위의 코드와 같은 결과를 출력한다.
System.out.println(s1);
// 그러니 println()으로 객체의 값을 출력할 때
// 굳이 toString()을 번거롭게 호출하지 말라!
}
}
Object 클래스 - equals()에 대하여
package com.eomcs.basic.ex01;
public class Exam0130 {
static class My {
String name;
int age;
}
public static void main(String[] args) {
My obj1 = new My();
obj1.name = "홍길동";
obj1.age = 20;
My obj2 = new My();
obj2.name = "홍길동";
obj2.age = 20;
System.out.println(obj1 == obj2);
// Object에서 상속 받은 equals()는 == 연산자와 마찬가지로 인스턴스가 같은지를 비교한다.
// 만약 그 내용물이 같은지 비교하고 싶다면 equals()를 재정의 하라!
System.out.println(obj1.equals(obj2));
}
}
Object 클래스 - equals() 오버라이딩
package com.eomcs.basic.ex01;
import java.util.Objects;
public class Exam0131 {
public static void main(String[] args) {
My obj1 = new My();
obj1.name = "홍길동";
obj1.age = 20;
obj1.tel = "1111-1111";
obj1.email = "hong@test.com";
obj1.gender = 1;
obj1.working = false;
My obj2 = new My();
obj2.name = "홍길동";
obj2.age = 20;
obj2.tel = "1111-1111";
obj2.email = "hong@test.com";
obj2.gender = 1;
obj2.working = false;
System.out.println(obj1 == obj2);
System.out.println(obj1.equals(obj2));
// 결론!
// => Object에서 상속 받은 것을 그대로 사용하면 equals()는 인스턴스가 같은지 비교한다.
// => 인스턴스의 내용물이 같은지 비교하도록 만들고 싶다면 equals()을 오버라이딩 하라!
// => String와 wrapper 클래스는 equals() 오버라이딩 하였다.
// => StringBuffer 클래스는 equals()를 오버라이딩 하지 않았다.
}
static class My {
String name;
int age;
String tel;
String email;
int gender;
boolean working;
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
My other = (My) obj;
return age == other.age && Objects.equals(email, other.email) && gender == other.gender
&& Objects.equals(name, other.name) && Objects.equals(tel, other.tel)
&& working == other.working;
}
}
}
우클릭 > Generate hashCode() and equals()... 로 자동 생성 가능하다.
Object 클래스의 메서드를 오버라이딩 하기 - equals()
package com.eomcs.basic.ex01;
public class Exam0132 {
static class Member /*extends Object */{
String name;
int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
}
public static void main(String[] args) {
Member m1 = new Member("홍길동", 20);
Member m2 = new Member("홍길동", 20);
System.out.println(m1 == m2);
// equals()
// => 이 메서드는 Object에 정의된 메서드이다.
// 따라서 자바의 모든 클래스는 이 메서드를 사용할 수 있다.
// => 두 개의 인스턴스가 같은 인스턴스인지 비교한다.
// => == 연산자와 동일하게 동작한다.
//
System.out.println(m1.equals(m2));
// m1과 m2는 다른 인스턴스이기 때문에 결과는 당연히 false 이다.
System.out.println("-----------------------");
String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
// 위의 실행 결과를 보면 s1과 s2가 서로 다른 인스턴스인데도 불구하고
// true를 리턴한다.
// 이유?
// => String 클래스에서 Object의 equals()를 오버라이딩 했기 때문이다.
// => 인스턴스가 다르더라도 문자열이 같으면 true를 리턴하도록
// equals() 메서드를 재정의하였다.
// => 그래서 String에 대해 equals()를 호출하면
// Member와 달리 true를 리턴한다.
//
// Member 객체에 대해서도 인스턴스가 다르더라도
// 데이터가 같으면 true를 리턴하도록 하고 싶은가?
// => String 클래스처럼 equals()을 오버라이딩 하라!
// => Exam0133.java를 보라!
}
}
Object 클래스의 메서드를 오버라이딩 하기 - equals()
package com.eomcs.basic.ex01;
import java.util.Objects;
public class Exam0133 {
static class Member /*extends Object */{
String name;
int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
// Object로부터 상속 받은 equals() 메서드를
// Member 클래스의 역할에 맞게 재정의해보자! => "오버라이딩"
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Member other = (Member) obj;
return age == other.age && Objects.equals(name, other.name);
}
}
public static void main(String[] args) {
Member m1 = new Member("홍길동", 20);
Member m2 = new Member("홍길동", 20);
Member m3 = new Member("홍길동", 21);
System.out.println(m1 == m2);
System.out.println(m1 == m3);
System.out.println(m2 == m3);
System.out.println(m1.equals(m2));
System.out.println(m1.equals(m3));
System.out.println(m2.equals(m3));
}
}
결론!
=> Object로부터 상속 받은 기본 메서드인 equals()는 같은 인스턴스인지를 비교하는 메서드이다.
=> 인스턴스의 데이터가 같은지를 비교하고 싶다면, 이 메서드를 재정의(오버라이딩) 해야 한다.
=> 오버라이딩의 예:
String, 랩퍼 클래스(Byte,Short,Integer,Long,Float,Double,Boolean,Character)
Object 클래스의 메서드를 오버라이딩 하기 - equals()
package com.eomcs.basic.ex01;
public class Exam0134 {
public static void main(String[] args) {
String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
// String 클래스에서 equals()를 오버라이딩 했기 때문에
// s1.equals(s2)의 결과가 true가 나온 것이다.
StringBuffer sb1 = new StringBuffer("Hello");
StringBuffer sb2 = new StringBuffer("Hello");
System.out.println(sb1 == sb2); // false
System.out.println(sb1.equals(sb2)); // false
// sb1.equals(sb2)의 리턴 값은 false이다.
// 이유?
// StringBuffer는 Object로부터 상속 받은 equals()를 오버라이딩 하지 않았다.
// 그래서 Object의 equals()가 호출된 것이다.
// Object의 equals()는 데이터가 같은지를 비교하는 것이 아니라,
// 인스턴스가 같은지를 비교한다.
}
}
java 17 API에서 java.lang.String 을 보면 메서드 오버라이딩 여부 확인 가능하다.
Methods declared in class 부분에 없으면 오버라이딩 한 것이다.
Object 클래스 - hashCode()에 대하여
package com.eomcs.basic.ex01;
public class Exam0140 {
static class My {
String name;
int age;
}
public static void main(String[] args) {
My obj1 = new My();
obj1.name = "홍길동";
obj1.age = 20;
My obj2 = new My();
obj2.name = "홍길동";
obj2.age = 20;
// Object에서 상속 받은 hashCode()는 인스턴스마다 고유의 4바이트 정수 값을 리턴한다.
// => 이 값은 toString()의 출력 값으로 사용된다.
System.out.println(Integer.toHexString(obj1.hashCode()));
System.out.println(Integer.toHexString(obj2.hashCode()));
System.out.println(obj1);
System.out.println(obj2);
}
}
hash value?
- 데이터를 다른 데이터와 구분하기 위해 사용하는 특별한 정수 값이다.
- 특정 수학 공식(MD4, MD5, SHA, PGP, CRC 등)에 따라 값을 계산한다.
- 데이터가 같은지 비교할 때 사용한다.
- 즉 모든 데이터를 바이트 단위로 일일이 비교하는 대신에 미리 생성된 정수 값을 비교함으로써
빠르게 두 값이 같은지 알아낼 수 있다.
- 매우 낮은 확률이지만 데이터가 다르더라도 같은 해시 값이 나올 수 있다.
당연하다. 큰 데이터를 특별한 계산 공식을 통해 4바이트 정수 값으로 표현한다는 것은
언제든 다른 데이터에 대해 같은 값이 나올 가능성을 내포하는 것이다.
다만 확률이 얼마나 낮은가의 문제다.
다른 데이터에 대해 같은 해시 값이 나올 확률이 낮을 수록 안심하고 사용할 수 있다.
- 현실과 비교하면 주민번호와 같다.
본인 여부를 확인할 때 주민 번호로 확인하면 매우 빠르게 결과를 알 수 있다.
원래는 본인인지 알아내려면 DNA 검사를 해야 한다.
그러면 너무 시간이 오래 걸린다.
그런데 지문이나 주민번호를 사용하면 빠르게 본인 여부를 알 수 있다.
- 이런 이유로 hash 값을 "디지털 지문"이라 부른다.
- 목적?
=> 데이터가 같은지 빠르게 비교하기 위함
- 응용
=> 본인 여부를 확인하는 인증서.
=> 파일의 위변조를 검사하는 용도.
예1) git 에서 커밋할 때 고유번호를 붙이는데 바로 해시 값이다.
예2) 파일 다운로드 사이트에서 제공하는 해시 값.
예3) 파일 공유사이트에서 파일을 구분할 때 해시 값 사용.
사용자가 파일 이름을 변경하더라도 데이터만 바꾸지 않는다면 파일의 해시 값은 같다.
- 해시 알고리즘
=> SHA, MD, PGP 등
Object 클래스 - hashCode() 오버라이딩
package com.eomcs.basic.ex01;
public class Exam0141 {
static class My {
String name;
int age;
@Override
public int hashCode() {
// String 클래스의 hashCode() 메서드는
// 같은 문자열에 대해 같은 해시값을 리턴한다.
// 이 능력을 이용하여 My 클래스의 인스턴스 해시값을 계산해보자.
//
String str = String.format("%s,%d", this.name, this.age);
return str.hashCode();
}
}
public static void main(String[] args) {
My obj1 = new My();
obj1.name = "홍길동";
obj1.age = 20;
My obj2 = new My();
obj2.name = "홍길동";
obj2.age = 20;
System.out.println(obj1 == obj2); // false
System.out.println(obj1.equals(obj2)); // false
System.out.println(Integer.toHexString(obj1.hashCode()));
System.out.println(Integer.toHexString(obj2.hashCode()));
System.out.println(obj1);
System.out.println(obj2);
}
}
package com.eomcs.basic.ex01;
import java.util.Objects;
public class Exam0142 {
public static void main(String[] args) {
My obj1 = new My();
obj1.name = "홍길동";
obj1.age = 20;
My obj2 = new My();
obj2.name = "홍길동";
obj2.age = 20;
System.out.println(obj1 == obj2); // false
System.out.println(obj1.equals(obj2)); // true
System.out.println(Integer.toHexString(obj1.hashCode()));
System.out.println(Integer.toHexString(obj2.hashCode()));
System.out.println(obj1);
System.out.println(obj2);
}
static class My {
String name;
int age;
// Map에 값을 저장하는 key로 사용할 때 hashCode()를 오버라이딩 하라!
// 보통 값이 같은지 비교할 때 equals()와 함께 사용된다.
// 그래서 hashCode()를 오버라이딩 할 때 equals()도 함께 오버라이딩 한다.
@Override
public int hashCode() {
return Objects.hash(age, name);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
My other = (My) obj;
return age == other.age && Objects.equals(name, other.name);
}
}
}
Object 클래스의 메서드를 오버라이딩 하기 - hashCode()
package com.eomcs.basic.ex01;
public class Exam0143 {
static class Score {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public Score(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
this.sum = kor + eng + math;
this.aver = this.sum / 3f;
}
}
public static void main(String[] args) {
Score s1 = new Score("홍길동", 100, 100, 100);
Score s2 = new Score("홍길동", 100, 100, 100);
// s1, s2의 Score 인스턴스는 서로 다른 인스턴스이다.
System.out.println(s1 == s2);
// Object에서 상속 받아 사용하는 hashCode()는
// 기본적으로 인스턴스 마다 고유의 값을 리턴한다.
// 그래서 다음 출력은 비록 같은 값을 갖고 있다하더라도
// 인스턴스가 다르기 때문에 해시코드의 값이 다르다!
// 주의!
// => 인스턴스 주소가 아니다!
System.out.printf("%d, %d\n",
s1.hashCode(), s2.hashCode());
}
}
package com.eomcs.basic.ex01;
public class Exam0144 {
static class Score {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public Score(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
this.sum = kor + eng + math;
this.aver = this.sum / 3f;
}
// hashCode()를 오버라이딩하면 원하는 값을 리턴할 수 있다.
@Override
public int hashCode() {
// 무조건 모든 Score 인스턴스가 같은 해시코드를 갖게 하자!
return 1000;
}
}
public static void main(String[] args) {
Score s1 = new Score("홍길동", 100, 100, 100);
Score s2 = new Score("홍길동", 100, 100, 100);
Score s3 = new Score("임꺽정", 90, 80, 70);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
// 해시코드?
// => 데이터를 식별할 때 사용하는 고유 아이디이다.
// => 보통 데이터를 특별한 공식(ex: MD4, MD5, SHA-1, SHA-256 등)으로
// 계산해서 나온 정수 값을 해시코드로 사용한다.
// => 그래서 해시코드를 데이터를 구분하는 지문과 같다고 해서
// '디지털 지문'이라고 부른다.
//
// hashCode()를 오버라이딩 할 때?
// => 인스턴스(메모리)가 다르더라도 같은 데이터를 갖는 경우
// 같은 것으로 취급하기 위해 이 메서드를 재정의한다.
// => 따라서 위의 예처럼 데이터가 같은지 따지지도 않고
// 모든 인스턴스에 대해 같은 해시코드를 리턴하는 것은
// 아무 의미없다!
// 이런 식으로 오버라이딩하는 것은 부질없는 짓이다!
}
}
package com.eomcs.basic.ex01;
public class Exam0145 {
static class Score {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public Score(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
this.sum = kor + eng + math;
this.aver = this.sum / 3f;
}
// 인스턴스가 다르더라도 데이터가 같으면
// 같은 해시 값을 리턴하도록 오버라이딩 해보자!
@Override
public int hashCode() {
// 가장 간단한 방법은 모든 값을 문자열로 만들어 붙인 다음에
// String 클래스에 있는 hashCode()를 사용하는 것이다.
// 왜? String 클래스에 있는 hashCode()는 문자열이 같은 경우
// 같은 해시 값을 리턴하도록 이미 오버라이딩 되어 있기 때문이다.
String value = String.format("%s,%d,%d,%d,%d,%.1f",
this.name, this.kor, this.eng, this.math,
this.sum, this.aver);
// 데이터가 같으면 문자열이 같을 것이고,
// 문자열이 같으면 해시코드의 리턴 값도 같을 것이다.
return value.hashCode();
}
}
public static void main(String[] args) {
Score s1 = new Score("홍길동", 100, 100, 100);
Score s2 = new Score("홍길동", 100, 100, 100);
Score s3 = new Score("홍길동", 100, 100, 99);
Score s4 = new Score("홍길동", 99, 100, 100);
// 인스턴스는 모두 다르다.
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1 == s4);
System.out.println(s2 == s3);
System.out.println(s2 == s4);
System.out.println(s3 == s4);
// s1과 s2는 비록 인스턴스가 다르지만, 데이터가 같기 때문에
// 같은 해시코드를 리턴한다.
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
System.out.println(s4.hashCode());
// hashCode()를 왜 오버라이딩 한다고?
// => 두 개의 인스턴스가 같은 데이터인지 비교하기 위해서!
}
}
HashSet과 hashCode()
HashSet : 인스턴스(주소) 목록을 보관
HashSet의 작동 원리는 다음과 같다.
HashSet에 줄번호가 있다. hashCode % 8 을 해서 나온 나머지 값에 따라 0~7번 줄에 배치한다.
new Student("홍길동", 20, false); → hashCode() 가 211 이라 가정한다. 211 % 8 = 3 이 나와 주소 200이 3번에 저장된다.
이런식으로 각 값들이 저장된다.
나중에 HashSet을 불러올때 0번 줄의 모든 값 ~ 7번 줄의 모든 값 순으로 불러오므로 보관한 순서가 보장되지 않는다.
Hash - 값을 보관할 위치를 계산할 때 사용
+ Set - 집합: 중복 저장 불가!
hash code 응용 - HashSet 과 hashCode(), equals()의 관계
package com.eomcs.basic.ex01;
import java.util.HashSet;
public class Exam0150 {
static class Student {
String name;
int age;
boolean working;
public Student(String name, int age, boolean working) {
this.name = name;
this.age = age;
this.working = working;
}
// @Override
// public int hashCode() {
// return 100;
// }
//
// @Override
// public boolean equals(Object obj) {
// return true;
// }
}
public static void main(String[] args) {
Student s1 = new Student("홍길동", 20, false);
Student s2 = new Student("홍길동", 20, false);
Student s3 = new Student("임꺽정", 21, true);
Student s4 = new Student("유관순", 22, true);
System.out.println(s1 == s2);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
System.out.println(s4.hashCode());
System.out.println("--------------------");
// 해시셋(집합)에 객체를 보관한다.
HashSet<Student> set = new HashSet<Student>();
set.add(s1);
set.add(s2);
set.add(s3);
set.add(s4);
// 해시셋에 보관된 객체를 꺼낸다.
Object[] list = set.toArray();
for (Object obj : list) {
Student student = (Student) obj;
System.out.printf("%s, %d, %s\n",
student.name, student.age, student.working ? "재직중" : "실업중");
}
}
}
집합?
=> 중복 값을 저장할 수 없다.
HashSet
=> 값을 저장할 때 해시값을 계산하여 저장 위치를 알아낸다.
=> 집합 방식으로 목록을 다룬다. 즉 중복 값을 저장하지 않는다.
=> 저장 과정:
1) equals()와 hashCode()를 호출하여 중복 여부를 검사한다.
2) equals()의 리턴 값도 true이고 hashCode()의 리턴 값도 같을 경우,
같은 객체로 판단하여 저장하지 않는다.
3) 저장할 때 저장 위치는 hashCode()의 리턴 값을 사용하여 계산한다.
'visualgo' 검색 후 아래 주요 알고리즘 학습할 것 : Sorting, Linked List, Hash Table, Binary Search Tree, Network Flow
hash code 응용 - 문제 해결!
package com.eomcs.basic.ex01;
import java.util.HashSet;
public class Exam0151 {
static class Student {
String name;
int age;
boolean working;
public Student(String name, int age, boolean working) {
this.name = name;
this.age = age;
this.working = working;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + (working ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (working != other.working)
return false;
return true;
}
}
public static void main(String[] args) {
Student s1 = new Student("홍길동", 20, false);
Student s2 = new Student("홍길동", 20, false);
Student s3 = new Student("임꺽정", 21, true);
Student s4 = new Student("유관순", 22, true);
System.out.println(s1 == s2);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
System.out.println(s4.hashCode());
System.out.println("--------------------");
// 해시셋(집합)에 객체를 보관한다.
HashSet<Student> set = new HashSet<Student>();
set.add(s1);
set.add(s2); // 이미 s2의 해시값과 같은 객체(s1)가 들어 있기 때문에 중복을 막기 위해 s2는 저장되지 않는다.
set.add(s3);
set.add(s4);
// 해시셋에 보관된 객체를 꺼낸다.
Object[] list = set.toArray();
for (Object obj : list) {
Student student = (Student) obj;
System.out.printf("%s, %d, %s\n",
student.name, student.age, student.working ? "재직중" : "실업중");
}
// 인스턴스가 다르더라도 인스턴스의 필드 값이 같을 경우
// HashSet에 중복 저장되지 않도록 하려면,
// hashCode()와 equals() 모두 오버라이딩 하라!
// => hashCode()는 같은 필드 값을 갖는 경우 같은 해시코드를 리턴하도록 변경하고,
// => equals()는 필드 값이 같을 경우 true를 리턴하도록 변경한다.
//
}
}
hash code 응용 II - HashMap의 key와 hashCode()
package com.eomcs.basic.ex01;
import java.util.HashMap;
public class Exam0152 {
static class MyKey {
String contents;
public MyKey(String contents) {
this.contents = contents;
}
@Override
public String toString() {
return "MyKey [contents=" + contents + "]";
}
}
public static void main(String[] args) {
HashMap<MyKey,Student> map = new HashMap<>();
MyKey k1 = new MyKey("ok");
MyKey k2 = new MyKey("no");
MyKey k3 = new MyKey("haha");
MyKey k4 = new MyKey("ohora");
MyKey k5 = new MyKey("hul");
map.put(k1, new Student("홍길동", 20, false));
map.put(k2, new Student("임꺽정", 30, true));
map.put(k3, new Student("유관순", 17, true));
map.put(k4, new Student("안중근", 24, true));
map.put(k5, new Student("윤봉길", 22, false));
// HashMap
// => 값을 저장할 때 key 객체의 해시코드를 이용하여 저장할 위치(인덱스)를 계산한다.
// => 따라서 해시코드가 같다면 같은 key로 간주한다.
//
// 값을 저장할 때 사용한 key 객체로 값을 찾아 꺼낸다.
System.out.println(map.get(k3));
// key를 사용하여 값을 꺼내보자.
MyKey k6 = new MyKey("haha");
System.out.println(map.get(k6)); // 엥? 값을 꺼낼 수가 없다.
// 두 키 객체 k3와 k6가 내용물이 같다 하더라도, (둘다 "haha"이다.)
// hashCode()의 리턴 값이 다르고, equals() 비교 결과도 false 라면
// HashMap 클래스에서는 서로 다른 key로 간주한다.
System.out.println(k3 == k6); // 인스턴스는 다르다.
System.out.printf("k3(%s), k6(%s)\n", k3, k6);
System.out.println(k3.hashCode()); // hash code는 다르다.
System.out.println(k6.hashCode()); // hash code는 다르다.
System.out.println(k3.equals(k6)); // equals()의 비교 결과도 다르다.
}
}
hash code 응용 II - MyKey의 hashCode()와 equals() 오버라이딩 하기
package com.eomcs.basic.ex01;
import java.util.HashMap;
public class Exam0153 {
static class MyKey2 {
String contents;
public MyKey2(String contents) {
this.contents = contents;
}
@Override
public String toString() {
return "MyKey2 [contents=" + contents + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((contents == null) ? 0 : contents.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MyKey2 other = (MyKey2) obj;
if (contents == null) {
if (other.contents != null)
return false;
} else if (!contents.equals(other.contents))
return false;
return true;
}
}
public static void main(String[] args) {
HashMap<MyKey2,Student> map = new HashMap<>();
MyKey2 k1 = new MyKey2("ok");
MyKey2 k2 = new MyKey2("no");
MyKey2 k3 = new MyKey2("haha");
MyKey2 k4 = new MyKey2("ohora");
MyKey2 k5 = new MyKey2("hul");
map.put(k1, new Student("홍길동", 20, false));
map.put(k2, new Student("임꺽정", 30, true));
map.put(k3, new Student("유관순", 17, true));
map.put(k4, new Student("안중근", 24, true));
map.put(k5, new Student("윤봉길", 22, false));
System.out.println(map.get(k3));
// 다른 key 객체를 사용하여 값을 꺼내보자.
MyKey2 k6 = new MyKey2("haha");
System.out.println(map.get(k6)); // OK! 값을 정상적으로 꺼낼 수 있다.
// k3와 k6는
// hashCode()의 리턴 값이 같다
// equals() 비교 결과도 true 이기 때문에
// HashMap 클래스에서는 서로 같은 key라고 간주한다.
System.out.println(k3 == k6); // 인스턴스는 다르다.
System.out.printf("k3(%s), k6(%s)\n", k3, k6);
System.out.println(k3.hashCode()); // hash code는 같다.
System.out.println(k6.hashCode()); // hash code는 같다.
System.out.println(k3.equals(k6)); // equals()의 비교 결과도 같다.
}
}
hash code 응용 III - Wrapper 클래스를 key 객체로 사용하기
package com.eomcs.basic.ex01;
import java.util.HashMap;
public class Exam0154 {
public static void main(String[] args) {
// hash 코드는 Map에서 값을 저장하기 위해 key로 사용한다.
HashMap<Integer,Student> map = new HashMap<>();
// Map은 값을 저장할 때 key를 이용한다.
// => key:
// 값을 저장할 위치를 계산할 때 사용한다.
// key 객체의 hashCode()를 호출하여 그 리턴 값을 사용하여 위치를 계산한다.
// 따라서 key 객체의 해시코드가 다르면 위치도 다르다.
// => map.put(key, value);
//
// key로 사용할 객체를 준비한다.
Integer k1 = new Integer(101);
Integer k2 = new Integer(102);
Integer k3 = new Integer(103);
System.out.println(k1 == k2);
System.out.println(k1 == k3);
System.out.println(k2 == k3);
// 위에서 준비한 key 객체를 가지고 Student 객체를 보관한다.
map.put(k1, new Student("홍길동", 20, false));
map.put(k2, new Student("임꺽정", 30, true));
map.put(k3, new Student("유관순", 17, true));
String str = new String("ohora");
//map.put(str, new Student("김구", 50, true)); // 컴파일 오류!
// HashMap 객체를 만들 때 key 타입으로 Integer를 사용하기로
// 선언했기 때문에 다른 타입을 키로 사용할 수 없다.
// put(Object key, Object value)
// => put() 메서드는 key로 넘겨받은 객체에 대해 hashCode()를 호출하여
// 정수 값을 얻는다.
// => 그렇게 리턴 받은 정수 값(해시 코드)를 사용하여 Student 객체를 저장할 위치를 계산한다.
// => 그런 후 그 위치에 해당하는 배열(배열로 관리한다면)에 저장한다.
//
// 다음과 같이 int를 key로 사용할 수 있다.
// => key 값으로 int를 넘겨준다면,
// 컴파일러가 컴파일 할 때 auto-boxing을 수행하여 Integer 객체를 만든다.
// 그리고 그 객체를 넘겨주는 것이다.
map.put(104 /* new Integer(104)*/, new Student("안중근", 24, true));
map.put(105 /* new Integer(105)*/, new Student("윤봉길", 22, false));
// 값을 저장할 때 사용한 key로 다시 값을 꺼내보자!
System.out.println(map.get(k2));
// k2와 같은 정수값을 가지는 key를 새로 생성한다.
Integer k6 = new Integer(102);
// k2와 같은 값을 갖는 k6로 값을 꺼내보자!
System.out.println(map.get(k6));
// 다음과 같이 k2와 k6는 분명히 다른 객체이다.
System.out.println(k2 == k6);
// 그러나 k2와 k6는 같은 해시코드를 갖는다.
System.out.println(k2.hashCode()); // hash code는 같다.
System.out.println(k6.hashCode()); // hash code는 같다.
// 또한 equals()의 리턴 값도 true이다.
System.out.println(k2.equals(k6)); // equals()의 비교 결과도 같다.
// 결론!
// => k2와 k6는 다른 객체지만,
// hashCode()의 리턴 값이 같고, equals()의 리턴 값이 true이기 때문에
// 두 객체는 같은 key로 간주된다.
// get(key) 실행원리!
// => key 파라미터로 받은 객체에 대해 hashCode() 호출하여 정수 값을 얻는다.
// => 그리고 정수 값을 이용하여 값이 저장된 위치를 찾는다.
// 원래의 키와 같은지 equals()로 한 번 더 비교한다.
// 만약 같다면 같은 key로 간주하여 해당 값을 꺼내 리턴한다.
//
// 따라서 k2로 저장한 값을 k6로 꺼낼 수 있다.
//
// Object 클래스에서 상속 받은 hashCode()는
// 인스턴스 필드의 값이 같은지 따지지 않고 무조건 인스턴스마다 고유의 해시 값을 리턴한다.
// 또한 equals()는 인스턴스 필드의 값이 같은지 비교하지 않고 같은 인스턴스인지만 따진다.
//
// 그러나 Integer 클래스 등의 wrapper 클래스와 String 클래스는
// Object 클래스에서 상속 받은 hashCode()와 equals()를 오버라이딩 했기 때문에
// 인스턴스가 다르더라도 인스턴스 필드의 값이 같으면
// hashCode()의 리턴 값이 같고 equals()가 true를 리턴한다.
//
// 그래서 wrapper 클래스와 String 클래스는 HashMap/Hashtable의
// key로 사용할 수 있는 것이다.
// 실무에서도 String이나 primitive type(결국 wrapper 객체로 전환되기 때문)을
// key로 자주 사용한다.
//
}
}
hash code 응용 III - String을 key로 사용하기
package com.eomcs.basic.ex01;
import java.util.HashMap;
public class Exam0155 {
public static void main(String[] args) {
HashMap<String,Student> map = new HashMap<>();
String k1 = new String("ok");
String k2 = new String("no");
String k3 = new String("haha");
String k4 = new String("ohora");
String k5 = new String("hul");
// String을 key로 사용해보자!
map.put(k1, new Student("홍길동", 20, false));
map.put(k2, new Student("임꺽정", 30, true));
map.put(k3, new Student("유관순", 17, true));
map.put(k4, new Student("안중근", 24, true));
map.put(k5, new Student("윤봉길", 22, false));
// k3 key로 저장한 값을 다시 k3 key로 꺼내보자!
System.out.println(map.get(k3));
// k3랑 같은 문자열을 갖는 String key를 생성한다.
String k6 = new String("haha");
// k3랑 k6는 서로 다른 인스턴스이다.
System.out.println(k3 == k6);
// 그러나 문자열은 같다.
// String은 같은 문자열일 경우 같은 해시코드를 리턴하도록 메서드를 오버라이딩 하였다.
System.out.println(k3.hashCode()); // hash code는 같다.
System.out.println(k6.hashCode()); // hash code는 같다.
// 또한 문자열이 같을 경우 equals()의 리턴 값이 true가 되도록 오버라이딩 하였다.
System.out.println(k3.equals(k6)); // equals()의 비교 결과도 같다.
// 즉 k3와 k6는 서로 다른 인스턴스 이지만,
// hashCode()의 리턴 값이 같고, equals()의 리턴 값이 true이기 때문에
// 두 객체는 같은 key로 간주한다.
// 그래서 k3로 저장된 값을 k6로 꺼낼 수 있다.
System.out.println(map.get(k6));
// 자바는 문자열에 대해 대소문자를 구분하기 때문에 "haha"와 "Haha"는 다른 객체로 취급한다.
// 당연히 hashCode()의 리턴 값이 다르고, equals()의 리턴 값은 false 이다.
// 그래서 k7은 k3와 같은 key가 아니다.
String k7 = new String("Haha");
System.out.println(map.get(k7));
}
}
Object 클래스 - getClass() : 해당 클래스의 정보를 리턴한다.
package com.eomcs.basic.ex01;
public class Exam0160 {
static class My {
}
public static void main(String[] args) {
My obj1 = new My();
// 레퍼런스를 통해서 인스턴스의 클래스 정보를 알아낼 수 있다.
Class classInfo = obj1.getClass();
// 클래스 정보로부터 다양한 값을 꺼낼 수 있다.
System.out.println(classInfo.getName()); // 패키지명 + 바깥 클래스명 + 클래스명
System.out.println(classInfo.getSimpleName()); // 클래스명
}
}
Object 클래스 - getClass() 와 배열
package com.eomcs.basic.ex01;
public class Exam0161 {
public static void main(String[] args) {
String obj1 = new String();
Class classInfo = obj1.getClass();
System.out.println(classInfo.getName()); // java.lang.String
// 배열의 클래스 정보
String[] obj2 = new String[10];
classInfo = obj2.getClass();
System.out.println(classInfo.getName()); //[Ljava.lang.String;
// int i = 100;
// classInfo = i.getClass(); // primitive type은 Object의 서브 클래스가 아니다.
int[] obj3 = new int[10];
classInfo = obj3.getClass();
System.out.println(classInfo.getName()); //[I
float[] obj4 = new float[10];
classInfo = obj4.getClass();
System.out.println(classInfo.getName()); //[F
double[] obj5 = new double[10];
classInfo = obj5.getClass();
System.out.println(classInfo.getName()); //[D
System.out.println(new byte[10].getClass().getName()); //[B
System.out.println(new short[10].getClass().getName()); //[S
System.out.println(new long[10].getClass().getName()); //[J
System.out.println(new char[10].getClass().getName()); //[C
System.out.println(new boolean[10].getClass().getName()); //[Z
}
}
Object 클래스 - getClass() 와 배열의 항목 이름
package com.eomcs.basic.ex01;
public class Exam0162 {
public static void main(String[] args) {
// 배열의 클래스 정보
String[] obj2 = new String[10];
Class classInfo = obj2.getClass();
System.out.println(classInfo.getName()); //[Ljava.lang.String;
// 배열 항목의 타입 정보를 가져온다.
Class compTypeInfo = classInfo.getComponentType();
System.out.println(compTypeInfo.getName()); //java.lang.String
// 값을 한 번 밖에 사용하지 않을 것이라면
// 위의 경우처럼 한 번씩 호출하고, 리턴 값을 가지고 또 호출하는 방식으로 값을 꺼내지 않는다.
// 체인(chain) 방식으로 호출한다.
System.out.println(obj2.getClass().getComponentType().getName()); //java.lang.String
}
}
Object 클래스 - clone() 사용법 I
package com.eomcs.basic.ex01;
// clone()은 인스턴스를 복제할 때 호출하는 메서드이다.
public class Exam0170 {
static class Score {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public Score() {}
public Score(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
this.sum = this.kor + this.eng + this.math;
this.aver = this.sum / 3f;
}
@Override
public String toString() {
return "Score [name=" + name + ", kor=" + kor + ", eng=" + eng + ", math=" + math + ", sum="
+ sum + ", aver=" + aver + "]";
}
}
public static void main(String[] args) {
Score s1 = new Score("홍길동", 100, 100, 100);
System.out.println(s1);
// 인스턴스 복제
// 방법1:
// => 직접 복제한다. 즉 새 객체를 만들어 기존 객체의 값을 저장한다.
Score s2 = new Score(s1.name, s1.kor, s1.eng, s1.math);
// s1과 s2는 서로 다른 인스턴스이다.
System.out.println(s1 == s2);
// s1과 s2에 들어 있는 값은 같다.
System.out.println(s1);
System.out.println(s2);
// 방법2:
// Object에서 상속 받은 clone()을 호출한다.
// Score s3 = s1.clone(); // 컴파일 오류!
//
// Object에서 상속 받은 clone()은 protected 이다.
// 따라서 같은 패키지에 소속된 클래스이거나 상속 받은 서브 클래스가 아니면 호출할 수 없다.
// 비록 Object의 서브 클래스라 할지라도 남의 인스턴스로 protected 멤버를 사용할 수 없다.
// 자신이 상속 받은 protected 멤버인 경우에만 접근할 수 있다.
// 서브 클래스이면서, 자신의 인스턴스로 접근할 때는
// protected 멤버를 사용할 수 있다.
Exam0170 obj = new Exam0170();
try {
obj.clone(); // 접근 가능! 단, 호출할 때 예외 상황을 처리해야 한다.
} catch (Exception e) {
e.printStackTrace();
}
// 해결책:
// => Object에서 상속 받은 clone()을 오버라이딩 하라!
// => Exam0171.java 를 살펴보라!
}
// 테스트용 인스턴스 메서드
void m1() throws Exception {
Score score = new Score();
// Exam0170이 상속 받은 clone()이 아니다.
// protected 멤버는 오직 자신이 상속 받은 경우에만 접근할 수 있다.
//
// score.clone(); // 컴파일 오류!
// Exam0170 클래스가 상속 받은 clone()은 접근 가능!
this.clone(); // OK!
}
}
Object 클래스 - clone() 사용법 II
package com.eomcs.basic.ex01;
// clone()은 인스턴스를 복제할 때 호출하는 메서드이다.
public class Exam0171 {
// 인스턴스를 복제할 수 있게 하려면,
// => Object에서 제공하는 clone()을 호출할 수 있어야 한다.
// => 그런데 clone()의 접근 범위가 protected 이라서
// 같은 패키지의 멤버이거나 서브 클래스가 아니면 호출할 수 없다.
// 해결책?
// => Object에서 상속 받은 clone()을 오버라이딩 한다.
// => 다른 패키지의 멤버가 호출하려면 public 으로 접근 제어의 범위를 넓혀야 한다.
// => 어떻게? 다음과 같이 오버라이딩 하라!
static class Score {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public Score() {}
public Score(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
this.sum = this.kor + this.eng + this.math;
this.aver = this.sum / 3f;
}
@Override
public String toString() {
return "Score [name=" + name + ", kor=" + kor + ", eng=" + eng + ", math=" + math + ", sum="
+ sum + ", aver=" + aver + "]";
}
// => Object에서 상속 받은 clone()을 오버라이딩하여 다른 패키지의 멤버도 사용할 수 있게
// public 으로 접근 범위를 넓혀라!
// => 오버라이딩은 접근 범위를 좁힐 수는 없지만, 넓힐 수는 있다.
// => 오버라이딩 할 때 리턴 타입을 해당 클래스의 타입으로 변경해도 된다.
@Override
public Score clone() throws CloneNotSupportedException {
// 복제를 위한 코드를 따로 작성할 필요가 없다.
// JVM이 알아서 해준다.
// 그냥 상속 받은 메서드를 오버라이딩하고, 접근 권한을 public 으로 확대한다.
// 원래의 clone() 메서드를 실행한 다음에
// 리턴 타입을 해당 클래스로 형변환 한다.
return (Score) super.clone();
}
}
public static void main(String[] args) throws Exception {
Score s1 = new Score("홍길동", 100, 100, 100);
Score s2 = s1.clone(); // 실행 오류! (run-time error)
// JVM은 다음 예외를 발생시킨다.
// => java.lang.CloneNotSupportedException:
// 즉 Score 클래스에서 복제를 허락하지 않는다는 뜻이다.
// 이유?
// => clone() 메서드의 사용을 활성화시키지 않아서 예외가 발생한 것이다.
// => 단지 clone()을 오버라이딩 했다고 끝나는 것이 아니다.
// Score 클래스에 복제 기능을 활성화시키는 설정을 해야 한다.
//
// clone() 메서드의 사용을 활성화시키는 방법?
// => 다음 소스를 확인하라!
//
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
}
ctrl + space 로 clone() 을 선택하면 쉽게 오버라이딩 할 수 있다.
Object 클래스 - clone() 사용법 III
package com.eomcs.basic.ex01;
public class Exam0172 {
// 인스턴스 복제 기능을 활성화하려면 Cloneable 인터페이스를 구현해야 한다.
// => 이 인터페이스에는 메서드가 선언되어 있지 않다.
// => 따라서 클래스는 따로 메서드를 구현할 필요가 없다.
// => Cloneable을 구현하는 이유는
// JVM에게 이 클래스의 인스턴스를 복제할 수 있음을 표시하기 위함이다.
// 이 표시가 안된 클래스는 JVM이 인스턴스를 복제해 주지 않는다. 즉 clone()을 호출할 수 없다.
static class Score implements Cloneable {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public Score() {}
public Score(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
this.sum = this.kor + this.eng + this.math;
this.aver = this.sum / 3f;
}
@Override
public String toString() {
return "Score [name=" + name + ", kor=" + kor + ", eng=" + eng + ", math=" + math + ", sum="
+ sum + ", aver=" + aver + "]";
}
@Override
public Score clone() throws CloneNotSupportedException {
// Object의 clone() 메서드는 Heap의 존재하는 인스턴스 변수들을 그대로 복제한다.
return (Score) super.clone();
}
}
public static void main(String[] args) throws Exception {
Score s1 = new Score("홍길동", 100, 100, 100);
Score s2 = s1.clone(); // 이제 예외가 발생하지 않는다!
// 복제 실행 오류가 발생하지 않는 이유?
// => Score 클래스의 복제 기능을 활성화시켰기 때문이다.
// class Score implements Cloneable {...}
//
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
}
clone() 메서드 : shallow copy
Engine engine = new Engine(); 로 cc, valve 필드의 인스턴스 생성된다.
Car car = new Car(); 로 maker, name, engine 필드의 인스턴스 생성된다.
이를 shallow(얕은) copy 로 clone() 하면 maker, name, engine 만 복제된다. 기존 car와 동일한 engine 주소를 갖고 있다.
clone() 메서드 : deep copy
위와 마찬가지로 인스턴스 생성한다.
car를 deep(깊은) copy 로 clone(), engine.clone() 하면 maker, name, engine 이 복제되고 engine의 필드 cc, valve도 복제된다.
Object 클래스 - clone() : shallow copy
package com.eomcs.basic.ex01;
public class Exam0173 {
static class Engine {
int cc;
int valve;
public Engine(int cc, int valve) {
this.cc = cc;
this.valve = valve;
}
@Override
public String toString() {
return "Engine [cc=" + cc + ", valve=" + valve + "]";
}
}
static class Car implements Cloneable {
String maker;
String name;
Engine engine;
public Car(String maker, String name, Engine engine) {
this.maker = maker;
this.name = name;
this.engine = engine;
}
@Override
public String toString() {
return "Car [maker=" + maker + ", name=" + name + ", engine=" + engine + "]";
}
@Override
public Car clone() throws CloneNotSupportedException {
return (Car) super.clone();
}
}
public static void main(String[] args) throws Exception {
Engine engine = new Engine(3000, 16);
Car car = new Car("비트자동차", "비트비트", engine);
// 자동차 복제
Car car2 = car.clone();
System.out.println(car == car2); // false
System.out.println(car);
System.out.println(car2);
System.out.println(car.engine == car2.engine); // true
// car와 car2의 엔진이 같다는 것은,
// 같은 engine의 객체 주소라는 뜻이다.
car.engine.cc = 2000;
System.out.println(car2.engine.cc);
//
// Object의 clone()은 해당 객체의 필드 값만 복제한다.
// 그 인스턴스 변수가 가리키고 있는 객체는 복제하지 않는다.
// 이런 방식의 복제를 "shallow copy(얕은 복제)"라 부른다.
//
// 그 객체의 인스턴스 변수가 가리키고 있는 객체까지 복제하는 것을
// "deep copy(깊은 복제)"라 부른다.
// deep copy는 개발자가 직접 clone() 메서드 안에
// deep copy를 수행하는 코드를 작성해야 한다.
}
}
Object 클래스 - clone() : deep copy
package com.eomcs.basic.ex01;
public class Exam0174 {
static class Engine implements Cloneable {
int cc;
int valve;
public Engine(int cc, int valve) {
this.cc = cc;
this.valve = valve;
}
@Override
public String toString() {
return "Engine [cc=" + cc + ", valve=" + valve + "]";
}
@Override
public Engine clone() throws CloneNotSupportedException {
return (Engine) super.clone();
}
}
static class Car implements Cloneable {
String maker;
String name;
Engine engine;
public Car(String maker, String name, Engine engine) {
this.maker = maker;
this.name = name;
this.engine = engine;
}
@Override
public String toString() {
return "Car [maker=" + maker + ", name=" + name + ", engine=" + engine + "]";
}
@Override
public Car clone() throws CloneNotSupportedException {
// deep copy
// => 포함하고 있는 객체에 대한 복제를 수행하려면 다음과 같이
// 개발자가 직접 포함하는 객체를 복제하는 코드를 작성해야 한다.
//
Car copy = (Car) super.clone();
copy.engine = this.engine.clone();
return copy;
}
}
public static void main(String[] args) throws Exception {
Engine engine = new Engine(3000, 16);
Car car = new Car("비트자동차", "비트비트", engine);
// 자동차 복제
// => Car의 clone()에서 Engine 객체도 복제할 것이다.
Car car2 = car.clone();
System.out.println(car == car2);
System.out.println(car);
System.out.println(car2);
System.out.println(car.engine == car2.engine);
// car의 엔진과 car2의 엔진이 다른 엔진인지 확인해보자!
car.engine.cc = 2000;
System.out.println(car2.engine.cc);
}
}
String 클래스
레퍼런스와 인스턴스
String s1 = new String("Hello"); 로 String 인스턴스를 Heap 영역에 만든다.
String s2 = new String("Hello"); 로 String 인스턴스를 Heap 영역에 만든다.
com.eomcs.basic.ex02
String - 문자열 객체 만들기
package com.eomcs.basic.ex02;
public class Exam0110 {
public static void main(String[] args) {
// String 레퍼런스
// - String은 자바 기본 타입이 아니다.
// - 클래스이다.
String s1; // s1은 String 인스턴스 주소를 담는 레퍼런스이다.
// String 인스턴스
// - 힙에 Hello 문자 코드를 저장할 메모리를 만들고 그 주소를 리턴한다.
// - 내용물의 동일 여부를 검사하지 않고 무조건 인스턴스를 생성한다.
// - 가비지가 되면 가비지 컬렉터에 의해 제거된다.
s1 = new String("Hello");
String s2 = new String("Hello");
// 인스턴스가 같은지를 비교해보면,
System.out.println(s1 == s2); // false => 서로 다른 인스턴스이다.
}
}
문자열 리터럴과 String 인스턴스
String s1 = "Hello"; 는 String literal ← 문자열 상수를 따로 보관하는 상수 메모리 영역에 만든다.
String s2 = "Hello"; 는 String literal ← 상수 메모리 영역에 같은 문자열의 인스턴스가 있다면 기존 인스턴스의 주소를 리턴한다.
String - 문자열 리터럴
package com.eomcs.basic.ex02;
public class Exam0111 {
public static void main(String[] args) {
// 문자열 리터럴
// - string constant pool 메모리 영역에 String 인스턴스를 생성한다.
// - 상수풀에 이미 같은 문자열의 인스턴스가 있다면, 그 주소를 리턴한다.
// - 왜? 메모리 절약을 위해 중복 데이터를 갖는 인스턴스를 생성하지 않는다.
// - JVM이 끝날 때까지 메모리에 유지된다.
//
String x1 = "Hello"; // 새 String 인스턴스의 주소를 리턴한다.
String x2 = "Hello"; // 기존의 String 인스턴스 주소를 리턴한다.
// 인스턴스의 주소를 비교해 보면,
System.out.println(x1 == x2); // true => 두 String 객체는 같다.
}
}
String - String 객체와 문자열 리터럴의 타입
package com.eomcs.basic.ex02;
public class Exam0112 {
public static void main(String[] args) {
String s1 = new String("Hello"); // Heap 영역에 String 인스턴스를 만든다.
String s2 = "Hello"; // String Pool 영역에 String 인스턴스를 만든다.
// 문자열 리터럴이 String 객체인지 확인해보자.
System.out.println(s1 == s2);
System.out.println(s1 instanceof String);
System.out.println(s2 instanceof String);
}
}
String - String 인스턴스를 상수풀에 생성하기: intern()
package com.eomcs.basic.ex02;
public class Exam0113 {
public static void main(String[] args) {
String s1 = new String("Hello"); // Heap 영역에 String 인스턴스를 생성한다.
// intern()
// - String 객체에 들어 있는 문자열과 동일한 문자열을 갖고 있는 String 객체를 상수풀에서 찾는다.
// - 있으면 그 String 객체의 주소를 리턴한다.
// - 없으면 상수풀에 String 객체를 생성한 후 그 주소를 리턴한다.
String s2 = s1.intern();
String s3 = "Hello"; // 해당 문자열을 가진 String 객체를 String Pool에서 찾는다.
// 있으면 그 객체를 리턴한다. 없으면 새 객체를 만들고 리턴한다.
System.out.println(s1 == s2);
System.out.println(s2 == s3);
}
}
package com.eomcs.basic.ex02;
public class Exam0114 {
public static void main(String[] args) {
String s1 = new String("Hello"); // Heap 영역에 String 인스턴스 생성
// 먼저 상수풀에 String 객체를 찾는다. 없으면 새로 만든다.
String s2 = "Hello";
// intern()
// - 지정된 String 객체를 상수풀에서 찾는다.
// - 있으면 그 String 객체의 주소를 리턴한다.
// - 없으면 상수풀에 String 객체를 생성한 후 그 주소를 리턴한다.
String s3 = s1.intern();
System.out.println(s1 == s2);
System.out.println(s2 == s3);
}
}
String - String 인스터스에 있는 값을 비교하기 : equals()
package com.eomcs.basic.ex02;
public class Exam0120 {
public static void main(String[] args) {
String s1 = new String("Hello");
String s2 = new String("Hello");
// 두 String 인스턴스는 분명히 서로 다르다.
System.out.println(s1 == s2);
// 두 인스턴스가 갖고 있는 문자열이 같은지를 비교하고 싶다면,
System.out.println(s1.equals(s2));
// equals()?
// - Object에 정의되어 있는 메서드이다.
// - 인스턴스가 같은지 비교한다.
//
// String의 equals()?
// - Object에서 상속 받은 것을 오버라이딩하였다.
// - 문자열이 같은지 비교한다.
//
}
}
String - String 인스터스에 있는 값을 비교하기 : equalsIgnoreCase()
package com.eomcs.basic.ex02;
public class Exam0121 {
public static void main(String[] args) {
String s1 = new String("Hello");
String s2 = new String("HELLO");
// equals()는 대소문자를 구분한다.
System.out.println(s1.equals(s2));
// 대소문자 구분없이 문자열을 비교하고 싶다면,
System.out.println(s1.equalsIgnoreCase(s2));
}
}
String - String.equals() vs Object.equals()
package com.eomcs.basic.ex02;
public class Exam0122 {
static class Member {
String name;
int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
}
public static void main(String[] args) {
Member m1 = new Member("홍길동", 20);
Member m2 = new Member("홍길동", 20);
// Member 인스턴스를 통해 호출하는 메서드는 모두
// Object 클래스의 메서드이다.
m1.toString();
m1.hashCode();
m1.equals(null);
// 비록 m1과 m2는 같은 값을 갖고 있지만 인스턴스가 다르다.
System.out.println(m1 == m2); // false
// Member는 Object에서 상속 받은 equals()를 오버라이딩 하지 않았다.
// 따라서 단순히 인스턴스가 같은지를 비교할 것이다.
System.out.println(m1.equals(m2)); // false
System.out.println(m1.toString());
System.out.println(m2.toString());
}
}
String - String.equals() 처럼 동작하게 만들기
package com.eomcs.basic.ex02;
import java.util.Objects;
public class Exam0123 {
static class Member {
String name;
int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
// String의 equals()처럼 내용이 같은지를 비교하도록 만들고 싶다면,
// Object에서 상속 받은 메서드를 오버라이딩 하라.
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Member other = (Member) obj;
return age == other.age && Objects.equals(name, other.name);
}
}
public static void main(String[] args) {
Member m1 = new Member("홍길동", 20);
Member m2 = new Member("홍길동", 20);
// 인스턴스는 다르지만,
System.out.println(m1 == m2); // false
// 인스턴스의 내용물이 같기 때문에 결과는?
System.out.println(m1.equals(m2)); // true
}
}
StringBuffer - 문자열 비교
package com.eomcs.basic.ex02;
public class Exam0124 {
public static void main(String[] args) {
StringBuffer b1 = new StringBuffer("Hello");
StringBuffer b2 = new StringBuffer("Hello");
// b1, b2는 서로 다른 인스턴스이다.
System.out.println(b1 == b2); // false
// StringBuffer는 Object에서 상속 받은 equals()를 오버라이딩 하지 않았다.
System.out.println(b1.equals(b2)); // false
// => 따라서 원래 Object의 equals()를 사용한다.
// => 즉 두 개의 인스턴스가 같은지를 비교한다.
// 그러니 결과는 false이다.
//
// 어떤 클래스에 equals()가 있나요?
// => 모든 클래스에 있다.
//
// 엥?
// => 자바의 모든 클래스는 Object 클래스에 정의된 기능을 상속 받는다.
// Object를 상속 받지 않을 수 없다.
// => 따라서 자바의 모든 클래스는 Object 클래스에 정의된 메서드를 사용할 수 있다.
//
// Object 클래스에 어떤 메서드가 있나요?
// => equals() : 인스턴스가 같은지를 비교한다.
// => toString() : 인스턴스의 클래스명과 해시코드를 리턴한다.
// => hashCode() : 인스턴스의 해시 코드를 리턴한다.
// => getClass() : 클래스 정보를 리턴한다.
// => clone() : 인스턴스를 복제하여 리턴한다.
// => finalize() : 가비지 컬렉터에 의해 제거되기 전에 호출된다.
// => wait() : 잠시 실행을 멈춘다. 외부에서 깨워줄 때까지 멈춘다.
// => notify() : wait()로 실행을 멈춘 것을 깨운다.
// => notifyAll() : wait()로 실행을 멈춘 것을 모두 깨운다.
// Object로부터 상속 받은 equals()가 원래는 인스턴스가 같은지를 비교한다고 했는데,
// 왜, String 클래스는 인스턴스가 아닌 문자열 값을 비교하나요?
// => String 클래스는 상속 받은 equals() 메서드를 재정의하였기 때문이다.
// => 이렇게 상속 받은 기능을 자신의 역할에 맞게끔 재정의하는 것을
// "오버라이딩(overriding)"이라 한다.
//
// StringBuffer는 재정의 하지 않아서 다르다고 결과가 나온건가요?
// => 예!
}
}
StringBuffer - 문자열 비교 II
package com.eomcs.basic.ex02;
public class Exam0125 {
public static void main(String[] args) {
StringBuffer b1 = new StringBuffer("Hello");
StringBuffer b2 = new StringBuffer("Hello");
// StringBuffer 에 들어 있는 문자열을 비교하려면?
// - StringBuffer에서 String을 꺼내 비교하라!
//
// String s1 = b1.toString();
// String s2 = b2.toString();
// System.out.println(s1.equals(s2));
//
System.out.println(b1.toString().equals(b2.toString()));
}
}
String - toString()
package com.eomcs.basic.ex02;
public class Exam0140 {
public static void main(String[] args) {
String s1 = new String("Hello");
String s2 = s1.toString();
// Object.toString()은 "클래스명@해시값" 을 리턴한다.
// String은 상속 받은 toString()을 오버라이딩 했다.
// => this 주소를 그대로 리턴한다.
System.out.println(s1 == s2); // true
System.out.println(s2);
}
}
String - toString(): 다형적 변수와 형변환
package com.eomcs.basic.ex02;
public class Exam0141 {
public static void main(String[] args) {
Object obj = new String("Hello"); // 인스턴스 주소가 100이라 가정하자;
String x1 = (String) obj; // x1 <--- 100
System.out.println(obj == x1);
// obj에 대해 toString()을 호출할 때,
// => 일단 obj 클래스에 선언된 멤버(필드와 메서드)만 사용할 수 있다.
// => 단 멤버는 실제 obj가 가리키는 클래스부터 찾아 올라 간다.
// => 위 예에서 obj가 가리키는 것은 String 이기 때문에
// => 이 경우 toString()을 호출할 때 String 클래스에서부터 찾는다.
// => String 클래스가 toString()을 오버라이딩 했기 때문에
// 결국 이 오버라이딩 메서드를 호출할 것이다.
String x2 = obj.toString(); // x2 <---- 100
System.out.println(x1 == x2);
// 레퍼런스를 통해 메서드를 호출할 때
// => 레퍼런스가 가리키는 객체의 클래스부터 메서드를 찾아 올라간다.
// => 따라서 obj가 가리키는 객체의 클래스가 String이기 때문에
// obj.toString()은 String 클래스부터 해당 메서드를 찾는다.
}
}
String - 다형적 변수와 형변환
package com.eomcs.basic.ex02;
public class Exam0142 {
public static void main(String[] args) {
Object obj = new String("Hello");
// obj가 String 객체를 가리키더라도
// obj의 타입이 Object이기 때문에 Object에 선언한 멤버만 사용할 수 있다.
// obj가 가리키는 원래 클래스의 메서드를 호출하고 싶다면
// 다음과 같이 원래 타입으로 형변환하라.
String str = ((String) obj).toLowerCase();
System.out.println(str);
// 또는 다음과 같이 원래 타입의 레퍼런스에 저장한 다음 사용하라.
String x1 = (String) obj;
str = x1.toLowerCase();
System.out.println(str);
}
}
String - equals(), hashCode()
package com.eomcs.basic.ex02;
public class Exam0150 {
static class Member {
String name;
int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
}
public static void main(String[] args) {
Member m1 = new Member("홍길동", 20);
Member m2 = new Member("홍길동", 20);
// 비록 m1과 m2는 같은 값을 갖고 있지만 인스턴스가 다르다.
System.out.println(m1 == m2); // false
// Object로부터 상속 받은 hashCode()는 인스턴스 마다 고유의 해시값을 리턴한다.
System.out.printf("%x, %x\n", m1.hashCode(), m2.hashCode());
// Object로부터 상속 받은 toString()은 "클래스명@해시값"을 리턴한다.
System.out.printf("%s, %s\n", m1.toString(), m2.toString());
// Object로부터 상속 받은 equals()는 인스턴스가 같은지 검사한다.
System.out.printf("%b\n", m1.equals(m2)); // false
System.out.println("-------------------------------------");
String s1 = new String("Hello");
String s2 = new String("Hello");
// 비록 s1과 s2는 같은 문자열을 갖고 있지만 인스턴스가 다르다.
System.out.println(s1 == s2);
// String 클래스는 Object로부터 상속 받은 hashCode()를 오버라이딩 했다.
// => 문자열이 같으면 같은 해시값을 리턴하게 만들었다.
System.out.printf("%x, %x\n", s1.hashCode(), s2.hashCode());
// String 클래스는 Object로부터 상속 받은 toString()을 오버라이딩 했다.
// => 스트링 인스턴스에 보관된 문자열을 리턴하도록 만들었다.
System.out.printf("%s, %s\n", s1.toString(), s2.toString());
// String 클래스는 Object로부터 상속 받은 equals()를 오버라이딩 했다.
// => 문자열이 같은지를 비교하도록 만들었다.
System.out.printf("%b\n", s1.equals(s2));
}
}
String - mutable vs immutable 객체
package com.eomcs.basic.ex02;
public class Exam0160 {
public static void main(String[] args) {
// String 객체는 immutable 객체이다.
// 즉 한 번 객체에 값을 담으면 변경할 수 없다.
String s1 = new String("Hello");
// String 클래스의 메서드는 원본 인스턴스의 데이터를 변경하지 않는다.
// 다만 새로 String 객체를 만들 뿐이다.
String s2 = s1.replace('l', 'x');
System.out.println(s1 == s2);
System.out.printf("%s : %s\n", s1, s2); // 원본은 바뀌지 않는다.
String s3 = s1.concat(", world!");
System.out.printf("%s : %s\n", s1, s3); // 원본은 바뀌지 않는다.
}
}
package com.eomcs.basic.ex02;
public class Exam0161 {
public static void main(String[] args) {
// StringBuffer 객체는 mutable 객체이다.
// 인스턴스의 데이터를 변경할 수 있다.
// 원래의 문자열을 변경하고 싶을 때 사용하는 클래스이다.
//
StringBuffer buf = new StringBuffer("Hello");
System.out.println(buf);
buf.replace(2, 4, "xxxx");// 원본을 바꾼다.
System.out.println(buf);
// println() 메서드에 넘겨주는 파라미터 값이 String 타입이 아닐 경우,
// println() 메서드 내부에서 해당 값에 대해 toString() 호출하여
// 그 리턴 값(String)을 출력한다.
// 만약에 해당 클래스에 toString() 메서드가 없으면 어떡하나요?
// => 자바의 모든 클래스는 toString()이 있다.
// => 왜? 자바의 모든 클래스는 Object 클래스를 상속 받는다.
// => Object 클래스에 toString()이 정의되어 있다.
}
}
String - 기타 메서드
package com.eomcs.basic.ex02;
public class Exam0170 {
public static void main(String[] args) {
// String 클래스의 스태틱 메서드 활용
String s1 = String.format("%s님의 나이는 %d입니다.", "홍길동", 20);
System.out.println(s1);
// 기본 데이터 타입의 값을 문자열로 만들기
String s2 = String.valueOf(true); // true => "true"
String s3 = String.valueOf(100); // 100 => "100"
String s4 = String.valueOf(100L); // 100L => "100"
String s5 = String.valueOf(3.14f); // 3.14f => "3.14"
String s6 = String.valueOf(3.14); // 3.14 => "3.14"
String s7 = String.valueOf('가'); // '가' => "가"
System.out.println(s2);
System.out.println(s3);
System.out.println(s4);
System.out.println(s5);
System.out.println(s6);
System.out.println(s7);
}
}
String - 기타 메서드 : join()
package com.eomcs.basic.ex02;
public class Exam0171 {
public static void main(String[] args) {
String[] arr = {"101", "제목", "내용", "4", "2021-2-2"};
// 1) 배열의 모든 값을 CSV 형식의 한 문자열로 만들기
// => format() 사용
String s1 = String.format("%s,%s,%s,%s,%s", arr[0], arr[1], arr[2], arr[3], arr[4]);
System.out.println(s1);
// => 가변 파라미터 자리에 배열을 전달해도 된다.
String s2 = String.format("%s,%s,%s,%s,%s", arr);
System.out.println(s2);
// => join() 사용
String s3 = String.join(",", arr[0], arr[1], arr[2], arr[3], arr[4]);
System.out.println(s3);
// => 가변 파라미터 자리에 배열을 전달해도 된다.
String s4 = String.join(",", arr);
System.out.println(s4);
String s5 = String.join("#", arr);
System.out.println(s5);
String s6 = String.join("0x0", arr);
System.out.println(s6);
}
}
String - 기타 메서드 : Arrays.copyOfRange()
package com.eomcs.basic.ex02;
import java.util.Arrays;
public class Exam0172 {
public static void main(String[] args) {
String[] arr = {"101", "제목", "내용", "4", "2021-2-2"};
// 배열에서 특정 범위의 항목을 복사하기
String[] arr2 = Arrays.copyOfRange(arr, 2, 4);
for (String s : arr2) {
System.out.println(s);
}
}
}
String - 다양한 생성자 활용
package com.eomcs.basic.ex02;
public class Exam0180 {
public static void main(String[] args) throws Exception {
// String 인스턴스 생성
String s1 = new String();
// => 내부적으로 문자의 코드 값을 저장할 char 배열(버전 1.8 까지)
// 또는 byte 배열(버전 1.9 부터)을 생성한다.
// => 생성자에서 넘겨주는 값을 배열에 저장한다.
// => 만약 생성자에 아무것도 넘겨주지 않으면 빈 배열이 생성된다.
System.out.println(s1); // 빈 문자열 출력
String s2 = new String("Hello"); // 문자열 리터럴로 String 인스턴스 생성
System.out.printf("s2=%s\n", s2);
char[] chars = {'H', 'e', 'l', 'l', 'o'};
String s3 = new String(chars); // char 배열로 String 인스턴스 생성
System.out.printf("s3=%s\n", s3);
byte[] bytes =
{(byte) 0xb0, (byte) 0xa1, (byte) 0xb0, (byte) 0xa2, 0x30, 0x31, 0x32, 0x41, 0x42, 0x43};
// 문자 코드 값이 저장된 바이트 배열로 String 인스턴스 생성
String s4 = new String(bytes);
System.out.printf("s4=%s\n", s4);
// 한글이 깨진다. 이유?
// => String 생성자는 파라미터로 받은 바이트 배열에 ISO-8859-1 문자 코드가 들어 있다가 간주한다.
// 즉 0xb0 0xa1 값이 한글 '가'가 아니라 0xb0와 0xa1 각각을 영어라 간주하고
// ISO-8859-1 에 정의된 문자표에 따라 유니코드(UTF-16)으로 바꿔 저장한다.
// 0xb0(ISO-8859-1) ==> 0x00b0(UTF-16)
// => 제대로 한글을 처리하려면?
// 생성자에 바이트 배열을 넘겨줄 때
// 배열에 들어 있는 코드 값이 어떤 문자표의 코드 값인지 알려줘야 한다.
//
String s5 = new String(bytes, "euc-kr");
System.out.printf("s5=%s\n", s5);
byte[] bytes2 =
{(byte) 0xac, (byte) 0x00, (byte) 0xac, (byte) 0x01, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63};
// 바이트 배열에 들어 있는 코드는 무슨 문자표로 작성했는지 정확하게 알려줘야 한다.
// 그래야 자바의 문자 코드로 제대로 변경할 수 있을 것이다.
String s6 = new String(bytes2, "utf-16");
System.out.printf("s6=%s\n", s6);
byte[] bytes3 = {(byte) 0xea, (byte) 0xb0, (byte) 0x80, (byte) 0xea, (byte) 0xb0, (byte) 0x81,
0x61, 0x62, 0x63};
String s7 = new String(bytes3, "utf-8");
System.out.printf("s7=%s\n", s7);
}
}
Wrapper 클래스 - 종류
package com.eomcs.basic.ex02;
public class Exam0210 {
public static void main(String[] args) {
// 자바는 primitive type의 data 를 객체처럼 다룰 수 있도록
// 각 타입에 대응하는 클래스를 제공한다.
// 예)
Byte b = new Byte((byte)100); // ==> byte
Short s = new Short((short)20000); // ==> short
Integer i = new Integer(3000000); // ==> int
Long l = new Long(60000000000L); // ==> long
Float f = new Float(3.14f); // ==> float
Double d = new Double(3.14159); // ==> double
Boolean bool = new Boolean(true); // ==> boolean
Character c = new Character((char)0x41); // ==> char
// 이렇게 primitive data type에 대응하여 만든 클래스를
// primitive data를 포장하는 객체라고 해서
// "랩퍼(wrapper) 클래스"라 부른다.
// 래퍼 클래스의 주요 용도:
// => primitive data type의 값을 객체로 주고 받을 때 사용한다.
// => primitive data type의 값을 객체에 담아 전달하고 싶다면
// 언제든 wrapper 클래스의 인스턴스를 만들면 된다.
//
// Java 9 부터 wrapper 클래스의 생성자가 deprecated 상태이다.
// 가능한 생성자를 사용하여 인스턴스를 생성하지 말라!
// deprecated(비난받는, 유지보수가 중단되어, 사용이 권장되지 않는)?
// - 사용하지 않는 것이 좋다고 결정되었고, 가까운 장래에 제거될 것이라는 의미.
//
// Wrapper 클래스의 인스턴스를 생성할 때는 생성자 대신 클래스 메서드를 사용하라.
Byte b2 = Byte.valueOf((byte)100);
Short s2 = Short.valueOf((short)20000);
Integer i2 = Integer.valueOf(3000000);
Long l2 = Long.valueOf(60000000000L);
Float f2 = Float.valueOf(3.14f);
Double d2 = Double.valueOf(3.14159);
Boolean bool2 = Boolean.valueOf(true);
Character c2 = Character.valueOf((char)0x41);
}
}
Wrapper 클래스 - primitive type 을 객체로 다루기
package com.eomcs.basic.ex02;
public class Exam0211 {
public static void main(String[] args) {
// Wrapper 클래스의 가장 큰 목적!
// => primitive 값을 포함하여 모든 값을 쉽게 주고 받기 위함이다.
// wapper 클래스가 없다면,
// 다음과 같이 각 타입에 대한 메서드가 따로 있어야 한다.
long l = 100L;
double d = 3.14;
boolean bool = true;
m(l);
m(d);
m(bool);
// 이렇게 wrapper 클래스를 사용하면
// 객체로 다룰 수 있다.
Long obj1 = Long.valueOf(l);
Double obj2 = Double.valueOf(d);
Boolean obj3 = Boolean.valueOf(bool);
m(obj1);
m(obj2);
m(obj3);
}
// 만약에 Wrapper 클래스가 없다면 다음과 같이
// 정수를 받는 메서드와 부동소수점을 받는 메서드,
// 논리값을 받는 메서드를 따로따로 정의해야 한다.
// => 이런 불편함을 없애기 위해 Wrapper 클래스를 만든 것이다.
// => 즉 primitive type을 객체처럼 다룰 수 있도록 만든 문법이다.
static void m(long value) { // byte, short, int, long, char
System.out.printf("long value=%s\n", value);
}
static void m(double value) {// float, double
System.out.printf("double value=%s\n", value);
}
static void m(boolean value) {// boolean
System.out.printf("boolean value=%s\n", value);
}
// wapper 클래스는 primitive type의 값을 객체로 다룰 수 있게 해준다.
// primitive type에 상관없이 Object 타입의 파라미터로 값을 받을 수 있다.
static void m(Object value) { // 모든 객체를 받을 수 있다.
System.out.printf("wrapper value=%s\n", value);
}
}
Wrapper 클래스 - wrapper 객체에 들어 있는 primitive type의 값 꺼내기
package com.eomcs.basic.ex02;
public class Exam0212 {
public static void main(String[] args) {
Long obj1 = Long.valueOf(100L);
Double obj2 = Double.valueOf(3.14);
Boolean obj3 = Boolean.valueOf(true);
long l = obj1.longValue();
double d = obj2.doubleValue();
boolean bool = obj3.booleanValue();
// 문자열로 꺼내기
// Wrapper 클래스도 String 클래스처럼 Object의 toString()을 오버라이딩 했다.
String s1 = obj1.toString(); // Object의 toString()이 아니다. Long에서 오버라이딩한 toString()이다.
String s2 = obj2.toString(); // Object의 toString()이 아니다. Double에서 오버라이딩한 toString()이다.
String s3 = obj3.toString(); // Object의 toString()이 아니다. Boolean에서 오버라이딩한 toString()이다.
// 다른 진수로 변환하여 문자열로 꺼내기
System.out.println(Long.toHexString(l));
System.out.println(Long.toOctalString(l));
System.out.println(Long.toBinaryString(l));
}
}
조언
*
과제
예제 예습
com.eomcs.basic.ex03 (ArrayList 사용법)
com.eomcs.basic.ex04 (LinkedList 사용법)