인터페이스(interface)
인터페이스는 클래스 혹은 프로그램이 제공하는 기능을 명시적으로 선언하는 역할을 한다.
인터페이스는 추상 메소드와 상수로만 이루어져 있다.
구현된 코드가 없기 때문에 인터페이스로 인스턴스로 생성할 수 없다.
package interfaceex;
public interface Calc {
//인터페이스에서 선언한 변수는 컴파일 과정에서 상수로 변함.
double PI = 3.14;
int ERR = -999999999;
//인터페이스에서 선언한 메소드는 컴파일 과정에서 추상 메소드로 변환됨
int add(int num1, int num2);
int substract(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
}
메소드는 public abstract 예약어를 명시적으로 쓰지 않아도 컴파일 과정에서 자동으로 추상 메소드로 변환된다.
또 인터페이스에 선언한 변수는 static final 예약어를 쓰지 않아도 컴파일 과정에서 값이 변하지 않는 상수로 변환된다.
인터페이스를 클래스가 사용하는 것을 '클래스에서 인터페이스를 구현한다.(implements)'라고 표현한다.
인터페이스에서는 인터페이스에 선언하나 기능을 클래스가 구현한다는 의미로 implements 예약어를 사용한다.
public class Calculator implements Calc{
}
이렇게 클래스에 인터페이스를 구현했을 경우 오류가 발생하는데,
이를 해결하기 위해서는 추상 메소드를 구현하거나, 클래스를 추상 클래스로 만들어야한다.
여기서 add(), substract() 2개만 구현하여 추상 클래스로 변환하여 오류를 해결한다.
package interfaceex;
public abstract class Calculator implements Calc{
@Override
public int add(int num1, int num2) {
// TODO Auto-generated method stub
return 0;
}
@Override
public int substract(int num1, int num2) {
// TODO Auto-generated method stub
return 0;
}
}
package interfaceex;
public abstract class Calculator implements Calc{
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
@Override
public int substract(int num1, int num2) {
return num1 - num2;
}
}
와 같이 코드를 짜고 Calculator 추상 클래스를 상속 받아 CompleteCalc 클래스를 만든다.
times(), divide() 추상 메소드를 CompleteCalc 클래스에서 구현한다.
package interfaceex;
public class CompleteCalc extends Calculator{
@Override
public int times(int num1, int num2) {
return num1 * num2;
}
@Override
public int divide(int num1, int num2) {
if(num2!=0) {
return num1/num2;
} else {
return Calc.ERR;
}
}
public void showInfo() {
System.out.println("Calc 인터페이스를 구현함");
}
}
package interfaceex;
public class CalculatorTest {
public static void main(String[] args) {
int num1 = 10;
int num2 = 2;
CompleteCalc c1 = new CompleteCalc();
System.out.println(c1.add(num1, num2));
System.out.println(c1.substract(num1, num2));
System.out.println(c1.times(num1, num2));
System.out.println(c1.divide(num1, num2));
c1.showInfo();
}
}
12
8
20
5
Calc 인터페이스를 구현함
Calculator 클래스는 인터페이스에서 선언한 추상 메소드 중 일부 메소드만 구현했으므로 추상 클래스이다.
그리고 이를 상속 받은 CompleteCalc 클래스는 Calculator 클래스에서 구현하지 않은 나머지 추상 메소드를 모두 구현하고 showInfo() 메소드를 추가로 구현했다.
상속 관계에서 하위 클래스는 상위 클래스 자료형으로 묵시적 형 변환할 수 있다. 인터페이스에서도 마찬가지인데, CompleteCalc 클래스는 상위 클래스인 Calculator형이면서, Calc 인터페이스를 구현하였으므로 Calc형이기도 하다.
Calc c2 = new CompleteCalc();
이 경우 Calc에서 선언한 추상 메소드는 있지만, CompleteCalc 클래스에서 추가로 구현한 showInfo()메소드는 없다.
즉, Calc형으로 선언한 변수에서 사용할 수 있는 메소드는 Calc 인터페이스에 선언한 메소드 뿐이다.
인터페이스를 구현한 클래스가 있을 때 그 클래스는 해당 인터페이스형으로 묵시적 형 변환이 이루어지며, 형 변환 되었을 때 사용할 수 있는 메소드는 인터페이스에서 선언한 메소드뿐이다.
인터페이스와 다형성
인터페이스는 클라이언트 프로그램에 어떤 메소드를 제공하는지 알려주는 명세(specification) 또는 약속의 역할을 한다.
※클라이언트는 프로그래밍에서는 서버와 대응되는 의미로 사용한다. 클라이언트는 기능을 사용하는 쪽이며, 서버는 기능을 제공하는 쪽이다.
인터페이스의 역할은 인터페이스를 구현한 클래스가 어떤 기능의 메소드를 제공하는지 명시하는 것이다. 그리고 클라이언트 프로그램은 인터페이스에서 약속한 명세대로 구현한 클래스를 생성해서 사용하면 된다.
인터페이스를 사용하면 다형성을 구현하여 확장성 있는 프로그램을 만들 수 있다. 즉 클라이언트 프로그램을 많이 수정하지 않고 기능을 추가하거나 다른 기능을 사용할 수 있다.
인터페이스 요소
인터페이스 상수
인터페이스는 추상 메소드로 이루어지므로 인스턴스를 생성할 수 없으며 멤버 변수도 사용할 수 없다.
인터페이스에 변수를 선언해도 오류가 발생하지 않는데 이는 컴파일하면 상수로 변환되기 때문이다.
디폴트 메소드
기본으로 제공되는 메소드이다. 디폴트 메소드는 인터페이스에서 구현하지만, 이후 인터페이스를 구현한 클래스가 생성되면 그 클래스에서 사용할 기본 기능이다. 디폴트 메소드를 선언할 때 default 예약어를 사용한다.
package interfaceex;
public interface Calc {
//인터페이스에서 선언한 변수는 컴파일 과정에서 상수로 변함.
double PI = 3.14;
int ERR = -999999999;
//인터페이스에서 선언한 메소드는 컴파일 과정에서 추상 메소드로 변환됨
int add(int num1, int num2);
int substract(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
default void description() {
System.out.println("정수 계산기를 구현한다.");
}
}
package interfaceex;
public class CalculatorTest {
public static void main(String[] args) {
int num1 = 10;
int num2 = 2;
CompleteCalc c1 = new CompleteCalc();
System.out.println(c1.add(num1, num2));
System.out.println(c1.substract(num1, num2));
System.out.println(c1.times(num1, num2));
System.out.println(c1.divide(num1, num2));
c1.showInfo();
c1.description();
Calc c2 = new CompleteCalc();
}
}
12
8
20
5
Calc 인터페이스를 구현함
정수 계산기를 구현한다.
인터페이스에 구현되어 있는 디폴트 메소드가 새로 생성한 클래스에서 원하는 기능과 맞지 않다면, 하위 클래스에서 디폴트 메소드를 재정의할 수 있다. Calc 인터페이스를 구현하는 Calculator 클래스에서 재정의할 수도 있고, Calculator 클래스를 상속받은 CompleteCalc 클래스에서 재정의할 수도 있다. Calculator 클래스를 상속받은 CompleteCalc 클래스에서 재정의 할 수도 있다.
package interfaceex;
public class CompleteCalc extends Calculator{
@Override
public int times(int num1, int num2) {
return num1 * num2;
}
@Override
public int divide(int num1, int num2) {
if(num2!=0) {
return num1/num2;
} else {
return Calc.ERR;
}
}
public void showInfo() {
System.out.println("Calc 인터페이스를 구현함");
}
//디폴트 메소드 descrption()을 재정의
@Override
public void description() {
// TODO Auto-generated method stub
super.description();
}
}
super.description()은 인터페이스에 선언한 메소드를 의미한다. 이 코드를 사용하지 않을거라면 지우고 새 코드를 작성하면 된다.
정적 메소드
정적 메소드는 static 예약어를 사용하여 선언하며 클래스 생성과 무관하게 사용할 수 있다. 정적 메소드를 사용할 때는 인터페이스 이름으로 직접 참고하여 사용한다.
package interfaceex;
public interface Calc {
//인터페이스에서 선언한 변수는 컴파일 과정에서 상수로 변함.
double PI = 3.14;
int ERR = -999999999;
//인터페이스에서 선언한 메소드는 컴파일 과정에서 추상 메소드로 변환됨
int add(int num1, int num2);
int substract(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
default void description() {
System.out.println("정수 계산기를 구현한다.");
}
static int total(int[] arr) {
int total = 0;
for(int i : arr) {
total += i;
}
return total;
}
}
package interfaceex;
public class CalculatorTest {
public static void main(String[] args) {
int num1 = 10;
int num2 = 2;
CompleteCalc c1 = new CompleteCalc();
System.out.println(c1.add(num1, num2));
System.out.println(c1.substract(num1, num2));
System.out.println(c1.times(num1, num2));
System.out.println(c1.divide(num1, num2));
c1.showInfo();
c1.description();
int[] arr = {1,2,3,4,5};
System.out.println(Calc.total(arr));
}
}
12
8
20
5
Calc 인터페이스를 구현함
정수 계산기를 구현한다.
15
private 메소드
인터페이스를 구현한 클래스에서 사용하거나 재정의할 수 없다. 기존에 구현된 코드를 변경하지 않고 인터페이스를 구현한 클래스에서 공통으로 사용하는 경우에 private 메소드로 구현하면 코드 재사용성을 높일 수 있다. 또한 클라이언트 프로그램에 제공할 기본 기능을 private 메소드로 구현하기도 한다.
pirvate 메소드는 코드를 모두 구현해야 하므로 추상 메소드에 private 예약어를 사용할 수는 없지만, static 예약어는 함께 사용할 수 있다. pirvate static 메소드는 정적메소드에서 호출하여 사용한다.
package interfaceex;
public interface Calc {
//인터페이스에서 선언한 변수는 컴파일 과정에서 상수로 변함.
double PI = 3.14;
int ERR = -999999999;
//인터페이스에서 선언한 메소드는 컴파일 과정에서 추상 메소드로 변환됨
int add(int num1, int num2);
int substract(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
default void description() {
System.out.println("정수 계산기를 구현한다.");
myMethod();
}
static int total(int[] arr) {
int total = 0;
for(int i : arr) {
total += i;
}
myStaticMethod();
return total;
}
private void myMethod() {
System.out.println("private 메소드");
}
private static void myStaticMethod() {
System.out.println("private static 메소드");
}
}
12
8
20
5
Calc 인터페이스를 구현함
정수 계산기를 구현한다.
private 메소드
private static 메소드
15
인터페이스 활용
한 클래스가 여러 클래스를 상속받으면 메소드 호출이 모호해지는 문제가 발생할 수 있다.
하지만 인터페이스는 한 클래스가 여러 인터페이스를 구현할 수 있다.
한 클래스가 여러 인터페이스를 구현할 수 있다.
package interfaceex;
public interface Buy {
void buy();
}
package interfaceex;
public interface Sell {
void sell();
}
package interfaceex;
public class Customer implements Buy, Sell{
@Override
public void sell() {
System.out.println("판매하기");
}
@Override
public void buy() {
System.out.println("구매하기");
}
}
두 인터페이스를 구현한 Customer 클래스는 Buy형이면서 Sell형이다.
package interfaceex;
public class CustomerTest {
public static void main(String[] args) {
Customer c1 = new Customer();
//Customer 클래스형인 c1을 Buy 인터페이스 형인 buyer에 대입하여 형 변환
//buyer는 Buy 인터페이스의 메소드만 호출 가능
Buy buyer = c1;
buyer.buy();
//Customer 클래스형인 c1을 Sell 인터페이스 형인 seller에 대입하여 형 변환
//seller는 Sell 인터페이스의 메소드만 호출 가능
Sell seller = c1;
seller.sell();
if(seller instanceof Customer) {
Customer c2 = (Customer)seller; //seller를 하위 클래스형인 Customer로 다시 형 변환
c2.buy();
c2.sell();
}
}
}
구매하기
판매하기
구매하기
판매하기
정적 메소드는 인스턴스 생성과 상관없이 사용할 수 있다. Customer 클래스가 Buy, Sell 인터페이스를 구현하고 Buy인터페이스와 Sell 인터페이스에 똑같은 pay() 정적메소드를 만들고, Buy.pay()와 Sell.pay()로 호출할 수 있기 때문에 문제가 되지 않는다. 하지만 디폴트 메소드는 인스턴스를 생성해야 홀출할 수 있는 메소드이기 때문에, 이름이 같은 디폴트 메소드가 두 인터페이스에 있으면 문제가 된다.
package interfaceex;
public interface Buy {
void buy();
default void order() {
System.out.println("구매 주문");
}
}
package interfaceex;
public interface Sell {
void sell();
default void order() {
System.out.println("판매 주문");
}
}
이 경우 Customer.java에서 오류가 발생하게 된다. 디폴트 메소드가 중복되어 있기 때문이다. 즉 Customer 클래스에서 재정의하라는 뜻이다.
package interfaceex;
public class Customer implements Buy, Sell{
@Override
public void sell() {
System.out.println("판매하기");
}
@Override
public void buy() {
System.out.println("구매하기");
}
//디폴트 메소드 order()를 Customer 클래스에서 재정의
@Override
public void order() {
System.out.println("고객 판매 주문");
}
}
Customer 클래스에서 디폴트 메소드를 재정의하면, Customer 클래스를 생성하여 사용할 때 재정의된 메소드가 호출된다.
package interfaceex;
public class CustomerTest {
public static void main(String[] args) {
Customer c1 = new Customer();
//Customer 클래스형인 c1을 Buy 인터페이스 형인 buyer에 대입하여 형 변환
//buyer는 Buy 인터페이스의 메소드만 호출 가능
Buy buyer = c1;
buyer.buy();
buyer.order();
//Customer 클래스형인 c1을 Sell 인터페이스 형인 seller에 대입하여 형 변환
//seller는 Sell 인터페이스의 메소드만 호출 가능
Sell seller = c1;
seller.sell();
seller.order();
if(seller instanceof Customer) {
Customer c2 = (Customer)seller; //seller를 하위 클래스형인 Customer로 다시 형 변환
c2.buy();
c2.sell();
}
c1.order();
}
}
구매하기
고객 판매 주문
판매하기
고객 판매 주문
구매하기
판매하기
고객 판매 주문
인터페이스 간에도 상속이 가능하다. 인터페이스 간 상속은 구현 코드를 통해 기능을 상속하는 것이 아니므로 형 상속(type inheritance)라고 부른다. 클래스의 경우에는 하나의 클래스만 상속 받을 수 있지만, 인터페이스는 여러 개를 동시에 상속받을 수 있다. 한 인터페이스가 여러 인터페이스를 상속받으멵, 상속받은 인터페이스는 상위 인터페이스에 선언한 추상 메소드를 모두 가지게 된다.
package interfaceex;
public interface X {
void x();
}
package interfaceex;
public interface Y {
void y();
}
package interfaceex;
public interface MyInterface extends X,Y {
void myMethod();
}
package interfaceex;
public class MyClassTest {
public static void main(String[] args) {
MyClass mc = new MyClass();
//상위 인터페이스 X형으로 대입하면 X에 선언한 메소드만 호출 가능
X xc = mc;
xc.x();
//상위 인터페이스 Y형으로 대입하면 Y에 선언한 메소드만 호출 가능
Y yc = mc;
yc.y();
//구현한 인터페이스형 변수에 대입하면 인터페이스가 상속한 모든 메소드 호출 가능
MyInterface ic = mc;
ic.x();
ic.y();
ic.myMethod();
}
}
x()
y()
x()
y()
myMethod()
생성한 클래스는 상위 인터페이스형으로 변환할 수 있다. 단 인터페이스로 형 변환을 하면 상위 인터페이스에 선언한 메소드만 호출할 수 있다.
인터페이스 구현과 클래스 상속 함께 쓰기
한 클래스에서 클래스 상속과 인터페이스 구현을 모두 할 수 있다.
package bookshelf;
import java.util.ArrayList;
public class Shelf {
//자료를 순서대로 저장할 ArrayList 선언
protected ArrayList<String> shelf;
//디폴트 생성자로 Shelf 클래스를 생성하면 ArrayList도 생성됨
public Shelf() {
shelf = new ArrayList<String>();
}
public ArrayList<String> getShelf() {
return shelf;
}
public int getCount() {
return shelf.size();
}
}
package bookshelf;
//처음 들어간 자료부터 꺼내어 쓰는 것을 Queue 자료 구조라고한다.
public interface Queue {
void enQueue(String title); //배열의 맨 마지막에 추가
String deQueue(); //배열의 맨 처음 항목 반환
int getSize(); //현재 Queue에 있는 개수 반환
}
package bookshelf;
public class BookShelf extends Shelf implements Queue{
//배열에 요소 추가
@Override
public void enQueue(String title) {
shelf.add(title);
}
//맨 처음 요소를 배열에서 삭제하고 반환
@Override
public String deQueue() {
return shelf.remove(0);
}
//배열 요소 개수 반환
@Override
public int getSize() {
return getCount();
}
}
package bookshelf;
public class BookShelfTest {
public static void main(String[] args) {
Queue sq = new BookShelf();
//순서대로 요소 추가
sq.enQueue("1");
sq.enQueue("2");
sq.enQueue("3");
//입력 순서대로 요소를 꺼내서 출력
System.out.println(sq.deQueue());
System.out.println(sq.deQueue());
System.out.println(sq.deQueue());
}
}
1
2
3
인터페이스는 클래스가 제공할 기능을 선언하고 설계하는 것이다. 만약 여러 클래스가 같은 메소드를 서로 다르게 구현한다면 인터페이스에 메소드를 선언한 다음 인터페이스를 구현한 각 클래스에서 같은 메소드에 대해 다양한 기능을 구현하면된다. 이것이 인터페이스를 이용한 다형성의 구현이다.
기본 클래스
Object 클래스
java.lang패키지
자바 프로그래밍에서 외부 패키지에 선언한 클래스를 사용할 때는 import문으로 클래스가 어느 패키지에 속해 있는지 선언해야한다. import문을 직접 쓰지 않아도 java.lang의 모든 하위 클래스를 참조할 수 있다.
모든 클래스의 최상위 클래스 Object
모든 클래스는 Object 클래스로부터 상속 받는다.
자주 사용되는 Object 메소드는
- String toString()
객체를 문자열로 표현하거나 반환한다. 재정의하여 객체에 대한 설명이나 특정 멤버 변수 값을 반환한다. - boolean equals(Object obj)
두 인스턴스가 동일한지 여부를 반환한다. 재정의하여 논리적으로 동일한 인스턴스임을 정의할 수 있다. - int hashCode()
객체의 해시 코드 값을 반환한다 - Object clone()
객체를 복제하여 동일한 멤버 변수 값을 가진 새로운 인스턴스를 생성한다. - Class getClass()
객체의 Class 클래스를 반환한다. - void finalize()
인스턴스가 힙 메모리에서 제거될 때 가비지 컬렉터에 의해 호출되는 메소드이다.
네트워크 연결 해제, 열려 있는 파일 스트림 해제 등을 구현한다. - void wait()
멀티스레드 프로그램에서 사용하는 메소드이다.
스레드를 '기다리는 상태'로 만든다. - void notify()
wait() 메소드에 의해 기다리고 있는 스레드를 실행가능한 상태로 가져온다.
toString() 메소드
객체 정보를 문자열로 바꾸어준다. Object 클래스를 상속받은 모든 클래스는 toString()을 재정의할 수 있다.
toString() 메소드의 원형은 생성된 인스턴스의 클래스 이름과 주소 값을 보여준다.
Object 클래스의 toString() 메소드
package object;
class Book{
int bookNum;
String bookName;
Book(int bookNum, String bookName){
this.bookNum = bookNum;
this.bookName = bookName;
}
}
public class ToStringEx {
public static void main(String[] args) {
Book b1 = new Book(1,"자바");
System.out.println(b1);
System.out.println(b1.toString());
}
}
object.Book@7c30a502
object.Book@7c30a502
b1 : 인스턴스 정보(클래스 이름. 주소값)
b1.toString() : toString() 메소드로 인스턴스 정보(클래스 이름. 주소값)을 보여줌
Object 클래스의 toString() 메소드의 원형은
getClass().getName() + '@' + integer.thHexString(hashCode())
클래스 이름@해시코드 값 임을 알 수 있다. 즉 클래스 이름과 16진수 해시 코드 값이 출력된다.
String과 Integer 클래스의 toString() 메소드
toString() 메소드가 호출된 경우라도 출력 결과가 '클래스이름@해시코드값'이 아닌 경우가 있다.
package object;
public class ToStringEx2 {
public static void main(String[] args) {
String str = new String("java");
System.out.println(str);
Integer i = new Integer(100);
System.out.println(i);
}
}
java
100
출력결과가 '클래스이름@해시코드값'이 아닌 이유는 String과 Integer 클래스는 toString()메소드를 미래 재정의해두었기 때문이다.
equals() 메소드
equals() 메소드의 기능은 두 인스턴스의 주소 값을 비교하여 boolean값을 반환해주는 것이다.
주소 값이 같다면 같은 인스턴스이다. 하지만 서로 다른 주소 값을 가질 때도 같은 인스턴스라고 정의할 수 있는 경우가 있다.
따라서 물리적 동일성(인스턴스의 메모리 주소가 같음)뿐 아니라 논리적 동일성(논리적으로 두 인스턴스가 같음)을 구현할 때도 equals() 메소드를 재정의하여 사용한다.
Object 클래스의 equals()메소드
생성된 두 인스턴스가 같다라는 의미는 두 변수가 같은 메모리 주소를 가리키고 있다는 뜻이다.
예를 들어
Student s1 = new Student(1,"홍길동");
Student s2 = s1;
두 변수는 동일한 인스턴스를 가리키고, 이때 equals() 메소드를 이용해 두 변수를 비교하면 동일하다는 결과가 나온다.
Student s1 = new Student(1,"홍길동");
Student s2 = s1;
Student s3 = new Student(1,"홍길동");
경우에는 s1, s2가 가리키는 인스턴스와 s3 가리키는 인스턴스는 서로 다른 주소를 가지고 있지만 저장된 정보는 같다.
이런 경우 논리적으로는 s1, s2, s3가 같은 것으로 처리하는 것이 맞을 것이다.
package object;
class Student{
int id;
String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public String toString() {
return id + ", " + name;
}
}
public class EqualsTest {
public static void main(String[] args) {
Student s1 = new Student(1,"홍길동");
Student s2 = s1;
Student s3 = new Student(1,"홍길동");
if(s1==s2) {
System.out.println("s1과 s2 주소가 같다.");
} else {
System.out.println("s1과 s2 주소가 다르다.");
}
if(s1.equals(s2)) {
System.out.println("s1과 s2는 동일하다.");
} else {
System.out.println("s1과 s2는 동일하지 않다.");
}
if(s1==s3) {
System.out.println("s1과 s3 주소가 같다.");
} else {
System.out.println("s1과 s3 주소가 다르다.");
}
if(s1.equals(s3)) {
System.out.println("s1과 s3운 동일하다.");
} else {
System.out.println("s1과 s3는 동일하지 않다.");
}
}
}
s1과 s2 주소가 같다.
s1과 s2는 동일하다.
s1과 s3 주소가 다르다.
s1과 s3는 동일하지 않다.
Object의 equals() 메소드는 두 인스턴스의 주소를 비교하는 것이기 때문에 같은 주소인 경우마 equals() 메소드의 결과가 true가 된다.
위의 예시에서 s1 참조 변수와 s2 참조 변수는 동일한 주소를 가리키므로 true이고,
s3의 경우는 다른 주소를 가리키므로 false이다.
인스턴스 주소가 다르다고 해서 정보가 같으면 같은 정보이다. 즉 두 인스턴스가 있을 때 ==는 단순히 물리적으로 같은 메모리 주소인지 여부를 확인할 수 있고, Object의 equals() 메소드는 재정의를 하여 논리적으로 같은 인스턴스인지 확인하도록 구현할 수 있다.
String과 Integer 클래스의 equals() 메소드
package object;
public class StringEquals {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1==str2);
System.out.println(str1.equals(str2));
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println(i1==i2);
System.out.println(i1.equals(i2));
}
}
false
true
false
true
Integer 클래스의 경우 정수 값이 같은 경우 true를 반환하도록 equals() 메소드가 재정의 되어 있음을 알 수 있다.
hashCode() 메소드
해시(hash)는 정보를 저장하거나 검색할 때 사용하는 자료 구조이다. 정보를 어디에 저장할 것인지, 어디서 가져올 것인지 해시 함수를 사용하여 구분한다. 해시 함수는 객체의 특장 정보(키 값)를 매개변수 값으로 넣으면 그 객체가 저장되어야 할 위치나 저장된 해시 테이블 주소(위치)를 반환한다. 따라서 객체 정보를 알면 해당 객체의 위치를 빠르게 검색할 수 있다.
해시 함수[(hash(key)]는 개발하는 프로그램 특성에 따라 다르게 구현된다.
index = hash(key)
index : 저장위치
hash : 해시 함수
key : 객체 정보
자바에서는 인스턴스를 힙 메모리에 생성하여 관리할 때 해시 알고리즘을 사용한다.
hashCode = hash(key);
객체의 해시 코드 값(메모리 위치 값)이 반환됨
위에서 toString() 메소드의 원형 getClass().getName() + '@' + integer.thHexString(hashCode())
참조 변수를 출력할 때 16진수 숫자값이 해시 코드 값이고, 이 값은 JVM이 힙 메모리에 저장한 '인스턴스의 주소값'이다.
즉 두 인스턴스가 같다면 hashCode() 메소드에서 반환하는 해시 코드 값이 같아야한다. 따라서 논리적으로 같은 두 객체도 같은 해시 코드 값을 반환하도록 hashCode() 메소드를 재정의해야한다.
즉, equals() 메소드를 재정의했다면 hashCode() 메소드도 재정의 해야한다.
String, Integer 클래스의 hashCode()메소드
package object;
public class HashCodeTest {
public static void main(String[] args) {
String str1 = new String("a");
String str2 = new String("a");
System.out.println(str1.hashCode());
System.out.println(str2.hashCode());
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println(i1.hashCode());
System.out.println(i2.hashCode());
}
}
97
97
1
1
package object;
class Student{
int id;
String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public String toString() {
return id + ", " + name;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Student) {
Student std = (Student)obj;
if(this.id==std.id)
return true;
else return false;
}
return false;
}
//해시 코드 값으로 학번을 반환하도록 메소드 재정의
@Override
public int hashCode() {
return id;
}
}
public class EqualsTest {
public static void main(String[] args) {
Student s1 = new Student(1,"홍길동");
Student s2 = s1;
Student s3 = new Student(1,"홍길동");
if(s1==s2) {
System.out.println("s1과 s2 주소가 같다.");
} else {
System.out.println("s1과 s2 주소가 다르다.");
}
if(s1.equals(s2)) {
System.out.println("s1과 s2는 동일하다.");
} else {
System.out.println("s1과 s2는 동일하지 않다.");
}
if(s1==s3) {
System.out.println("s1과 s3 주소가 같다.");
} else {
System.out.println("s1과 s3 주소가 다르다.");
}
if(s1.equals(s3)) {
System.out.println("s1과 s3운 동일하다.");
} else {
System.out.println("s1과 s3는 동일하지 않다.");
}
System.out.println("s1의 hashCode : " + s1.hashCode());
System.out.println("s2의 hashCode : " + s2.hashCode());
System.out.println("s3의 hashCode : " + s3.hashCode());
System.out.println("s1의 실제 주소값 : " + System.identityHashCode(s1));
System.out.println("s2의 실제 주소값 : " + System.identityHashCode(s2));
System.out.println("s3의 실제 주소값 : " + System.identityHashCode(s3));
}
}
s1과 s2 주소가 같다.
s1과 s2는 동일하다.
s1과 s3 주소가 다르다.
s1과 s3운 동일하다.
s1의 hashCode : 1
s2의 hashCode : 1
s3의 hashCode : 1
s1의 실제 주소값 : 142666848
s2의 실제 주소값 : 142666848
s3의 실제 주소값 : 1060830840
s1과 s2는 학법이 같기 때문에 논리적으로 같은지 확인하는 equals() 메소드 출력 값이 true이다. 또한 같은 해시 코드값을 반환하고 있다. hashCode() 메소드를 재정의했을 때 실제 인스턴스의 주소 값은 System.identityHashCode() 메소드를 사용하면 알 수 있다. s1과 s3의 실제 메모리 주소 값이 다르다는 것을 알 수 있다. s1과 s3는 논리적으로 같지만 실제로는 다른 인스턴스이다.
clone() 메소드
객체 원본을 유지해 놓고 복사본을 사용한다거나, 기본 틀(prototype)의 복사본을 사용해 동일한 인스턴스를 만들어 복잡한 생성 과정을 간단히 하려는 경우 clone() 메소드를 사용할 수 있다. clone() 메소드는 Object에 선언되며, 객체를 복제해 또 다른 객체를 반환해 주는 메소드이다.
protected Object clone();
package object;
class Point{
int x;
int y;
Point(int x, int y){
this.x = x;
this.y = y;
}
public String toString() {
return "x = " + x + ", y = " + y;
}
}
//객체를 복제해도 된다는 의미로 Cloneable 인터페이스를 함께 선언
class Circle implements Cloneable{
Point point;
int radius;
Circle(int x, int y, int radius){
this.radius = radius;
point = new Point(x,y);
}
public String toString() {
return "원점은 " + point + "이고, 반지름은 " + radius;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class ObjectCloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Circle circle = new Circle(10, 20, 30);
//clone() 메소드를 사용해 circle 인스턴스를 copyC에 복제함.
Circle copyCircle = (Circle)circle.clone();
System.out.println(circle);
System.out.println(copyCircle);
System.out.println(System.identityHashCode(circle));
System.out.println(System.identityHashCode(copyCircle));
}
}
원점은 x = 10, y = 20이고, 반지름은 30
원점은 x = 10, y = 20이고, 반지름은 30
2047526627
1908316405
clone() 메소드를 사용하려면 객체를 복제해도 된다는 의미로 클래스에 Conealbe 인터페이스를 구현해야한다.
Object의 clone() 메소드는 클래스의 인스턴스를 새로 복제하여 생성해준다. 멤버 변수가 동일한 인스턴스가 다른 메모리에 새로 생선되는 것이다.
※Cloneable 인터페이스를 선언해도 별도로 구현해야하는 메소드는 없다. 구현할 메소드가 없는 인터페이스를 마커 인터페이스(marker interface)라고 한다.
'KDT > Java' 카테고리의 다른 글
240111 Java (0) | 2024.01.11 |
---|---|
240110 Java (0) | 2024.01.10 |
240104 Java (0) | 2024.01.04 |
240103 Java (0) | 2024.01.03 |
231228 Java (0) | 2023.12.28 |