[다형성]클래스 형변환, instanceof, 추상 클래스, interface

EUNJI LEE·2023년 4월 12일
0

JAVA

목록 보기
6/12

다형성(Polymorphism)

다형성은 객체 지향 프로그램의 큰 특징 중 하나로 여러 개의 형태를 갖는다는 의미이다. 상속 관계에 있는 클래스의 부모 클래스 타입으로 선언된 변수에는 자식 클래스의 객체를 저장할 수 있다. 다형성 덕분에 프로그램의 코드를 유연하게 작성할 수 있다는 장점을 가진다.

PolySuper ps=new PolySuper();
ps=new PolySub();
//PolySuper이라는 부모 클래스를 저장한 변수에 자식 클래스를 저장할 수 있다.

Object o;
o=new PolySuper();
//Object 클래스는 최상위 부모 클래스로 어떤 클래스도 저장할 수 있다.

PolySub psb=new PolySub();
psb=new PolySuper();
//위 코드는 오류 발생

💡 자식 클래스로 선언된 변수에 부모 클래스의 객체를 저장은 불가능하다. 자식 클래스가 부모 클래스를 포함하지 못하기 때문이다. 아래와 같은 오류 코드가 발생한다.
⚠️Type mismatch: cannot convert from 자식클래스 to 부모클래스

💡업 캐스팅

상속 관계가 만들어졌을 때 부모, 자식 클래스 사이에서 부모 타입의 참조형 변수가 모든 자식 타입의 객체 주소를 받을 수 있게 되는 것을 업 캐스팅이라고 한다. 이때 사용한 참조형 변수는 원래 부모 타입이었던 멤버만 참조할 수 있다.

위에 코드에서 선언했던 것처럼 부모 클래스의 주소를 저장한 변수에 자식 클래스를 넣는 것이 업 캐스팅이다.

💡다운 캐스팅

업 캐스팅된 후, 자식 클래스의 객체의 주소를 받았던 부모 참조형 변수를 가지고 자식의 멤버에 접근해야하는 경우 자식 멤버에는 접근이 안 되기 때문에 부모 클래스를 형변환 해서 사용해줘야 한다.

자식 타입 참조형 변수=(자식 클래스)부모 타입 참조형 변수; 형태로 사용할 수 있다.

PolySuper ps=new PolySuper();
PolySub psb=new PolySub();
psb=(PolySub)ps; //다운 캐스팅

💡 그렇다면 ps=(PolySub)ps; 같은 형태도 다운 캐스팅으로 볼 수 있느냐 하면 그건 아니다. 연산 순서에 따라 형변환이 먼저 이루어져서 다운 캐스팅이 실제로 진행 되었다고 해도 다운 캐스팅된 ps가 다시 대입되면서 자동으로 업 캐스팅된다.

//Person :  부모 클래스, Student : 자식 클래스
Person p;
p=new Student("LEE",26,1,"컴퓨터공학과");
System.out.println((p.getName()+" "+p.getAge()+" "
           +((Student)p).getGrade()+" "+((Student)p).getMajor()));

✅ 위와 같은 경우에도 연산자 우선 순위에 주의해야 한다. 형변환을 먼저 괄호로 묶어줘야 부모 타입의 참조 변수인 p에 바로 접근하지 않기 때문에 오류가 발생하지 않는다. 괄호로 묶어주지 않으면 아래처럼 타입을 찾을 수 없다는 오류 메시지가 뜬다.
⚠️The method 자식 메소드() is undefined for the type 자료형

instanceof 연산자

현재 참조형 변수가 어떤 클래스 형의 객체 주소를 참조하고 있는지 확인할 때 사용한다. 클래스 타입이 맞으면 true, 아니면 false를 반환한다.

if(p instanceof Student) {//Student 클래스인지 확인.
		System.out.println(p.getName()+" "+p.getAge()+" "
        +((Student)p).getGrade()+" "+((Student)p).getMajor());
		}else if(p instanceof Teacher) {//Teacher 클래스인지 확
			System.out.println(p.getName()+" "+p.getAge()+" "
           +((Teacher)p).getSubject()+" "+((Teacher)p).getSalary());
		}
//어떤 클래스인지에 따라 출력할 내용을 구분할 수 있다.
  • 객체 배열의 다형성 적용+instanceof 연산자 활용
    배열의 단점이었던 자료형, 길이 문제를 해결할 수 있다.
    Person[] persons=new Person[9];
    		persons=new Person[] {
    				new Student("KIM",20,2,"컴퓨터공학"),
    				new Student("LEE",22,1,"국어국문"),
    				new Teacher("PARK",30,"web",5500),
    				new Teacher("JOO",35,"java",6000),
    				new Employee("CHOI",26,"디자인팀","대리"),
    				new Employee("WOO",32,"홍보팀","팀장") }
    
    //persons에 저장된 student, teacher, employee의 수와 전체 저장 사람 수 구하기
    		int per=0, stu=0, teach=0, emp=0;
    		for(Person p:persons) {
    			if(p!=null) {
    				if(p instanceof Student) {
    					stu++;
    				}else if(p instanceof Teacher) {
    					teach++;
    				}else if(p instanceof Employee) {
    					emp++;
    				}
    				per++;
    			}
    		}
    		System.out.println("student : "+stu+", teacher : "+teach
                     +", employee : "+emp+", Person : "+per);
    		//student : 2, teacher : 2, employee : 2, Person : 6 출력

바인딩

컴파일 되면서 코드가 각 메모리 어딘가에 저장되면서 주소값을 저장하게 되고 그 값을 더이상 바꿀 수 없게 된다. 여기서 실제 실행할 메소드 코드와 호출하는 코드를 연결 시키는 것을 바인딩이라고 말한다. 프로그램이 실행되기 전에 컴파일이 되면서 값이 확정되면 정적 바인딩이라고 한다. 모든 메소드는 컴파일 시점에 호출될 함수가 결정됨으로 정적 바인딩된다.

💡동적 바인딩

컴파일 시 정적 바인딩이 된 메소드를 실행하는 객체 타입을 기준으로 바인딩 되는 것을 말한다. 상속 관계에서 이뤄지는 바인딩으로 다형성이 적용된 경우, 메소드 오버라이딩이 되어있으면 정적 바인딩 메소드 코드보다 오버라이딩된 메소드 코드를 우선적으로 수행한다. 결국 실행할 오버라이드 메소드가 있는 자식 클래스가 실행되는 것을 말한다.

여태 상속 관계에 있던 클래스에서 Override 했던 메소드들이 기존의 Object 클래스의 메소드가 실행되는 것이 아닌 Override했던 메소드가 그대로 실행된 것이 동적 바인딩이다.

추상 클래스

몸체 없는(추상) 메소드를 포함한 클래스를 말하며 추상 클래스는 클래스 선언부에 abstract 키워드를 사용해서 구분한다. final 예약어와 상반되는 개념이라고 볼 수 있다. 일반 클래스와 동일하게 필드, 메서드, 생성자를 선언할 수 있다.

일관된 인터페이스를 제공한다는 장점이 있다. 꼭 필요한 기능을 강제화 시킬 수 있다.

[접근 제한자] abstract class 클래스명{ } 형태로 사용한다.

public abstract class Test{
     public abstract void test();
}

💡 추상 클래스는 상속 없이 생성해서 사용할 수 없다. 대신 추상 클래스를 타입으로 선언하면 상속 받는 클래스들을 넣어서 사용 가능하다. 추상 클래스를 생성해서 사용하려고 하면 아래와 같은 오류가 뜬다.
⚠️Cannot instantiate the type 추상클래스명

추상 메소드

구현부가 없는 메소드를 말한다. 추상 메소드의 선언부에 abstract를 사용해서 구분한다. 추상 메소드는 오버라이딩이 강제화된 메소드로 상속 관계에서 반드시 구현해야 한다. 자식 클래스에서 반드시 구현해야 할 메소드가 있을 때 사용한다.

[접근 제한자] abstract 반환형 메소드명(자료형 변수명); 형태로 선언한다.

public abstract void test();

------------------------------
//자식 클래스 내부
@Override
public abstract void test(){
  System.out.println("추상 메소드 사용");
} //추상 메소드 사용 출력
💡 추상 메소드 선언 시 메소드의 선언문만 작성한다. 메소드의 구현부는 자식 클래스에서 작성해서 사용한다.

인터페이스(interface)

상수형 필드와 추상 메소드만을 작성할 수 있는 추상 클래스이다. 메소드의 통일성을 부여하기 위해서 추상 메소드만 따로 모아 놓은 것으로 상속 관계에서 인터페이스 내에 정의된 모든 추상 메소드를 구현해야 한다.

인터페이스를 사용하면 해당 객체가 다양한 기능을 제공할 때도 인터페이스에 해당하는 기능만을 사용하게 제한할 수 있다. 공통으로 사용하는 기능의 일관성을 제공할 수 있다는 장점도 있다.

[접근 제한자] interface 인터페이스명 { } 형태로 사용한다.

public interface BasicInter {
	public abstract void test();
	int calc(int a, int b);
	~~private int ***age***;~~
  public static final int ***AGE***=19;
//final static은 보통 대문자로 쓴다.
	~~int age~~;
//초기값을 지정하지 않았을 때도 오류 발생
//⚠️The blank final field 변수명 may not have been initialized
}

✅ 메소드 선언부는 추상 메소드만 가능하기 때문에 public abstract 예약어는 생략이 가능하다.

💡 인터페이스에 변수는 public final static으로 선언된 변수만 가능하다. private int age; 를 선언해도 age가 static으로 선언한 것처럼 기울어지는데 이건 final static이 자동으로 붙은 것이다.
그리고 접근 제한자는 public으로 하지 않았을 경우 아래 같은 오류 메시지가 뜬다.
⚠️Illegal modifier for the interface field 인터페이스명.변수; only public, static & final are permitted

💡인터페이스 상속

인터페이스도 상속이 가능하며 인터페이스는 다중 상속도 가능하다. 인터페이스도 하나의 클래스처럼 사용하기 때문에 자식 인터페이스를 받은 클래스에서는 부모 인터페이스에만 접근이 가능하다. 자식 인터페이스에는 접근이 불가능하고 클래스와 마찬가지로 형변환 해서 사용할 수 있다.

public interface SubInterface extends SuperInterface1, SuperInterface2{
//추상 메소드 선언부
}

익명 클래스

인터페이스는 클래스에 껴서 불러와야 한다. 한 메소드에서만 사용하고 말거라면 클래스 하나를 선언해서 사용하기 번거롭기 때문에 익명 클래스라는 것을 이용할 수 있다. 메소드 안에 선언해서 사용하고 메소드 종료 시 사용이 끝나는 클래스인터페이스명 참조변수명=new 인터페이스명() { };형태로 구현할 수 있다.

public void extraInterface() {
		BasicInter bi=new BasicInter() { //메소드 안에서만 생성되고 종료되는 클래스
			@Override
			public void test() {
				System.out.println("익명 클래스 구현");
			}
		};
}
✅ 마찬가지로 익명 클래스 안에도 선언된 추상 메소드는 강제화된다.

FunctionalInterface 람다표현식

인터페이스에서 선언되어있는 추상 클래스가 한 개일 경우 간단하게 표현할 수 있는 방식을 람다식이라고 한다. 표현할 메소드가 하나일 때만 가능하고 ([매개변수])->{return} 방법으로 사용할 수 있다.

//매개변수, 반환값이 없을 때
InterfactTest ift=new InterfaceTest();
ift=()->{System.out.println("람다식 표현");};
ift.test(); //람다식 표현 출력
		
//매개변수가 있고, 반환값이 없을 때
CalculatorInterface ci=(int a, int b)->{System.out.println(a+b);};
ci.calc(20, 30); //50 출력
		
//매개변수, 반환값이 있을 때
StringInterface si=(String a)->{return a+" 람다";};
System.out.println(si.strCheck("반환값도 있는 FunctionInterface"));
//반환값도 있는 FunctionInterface 람다 출력

💡 리턴 값이 있을 때 더 구현할 로직이 없고 바로 리턴을 구현하면 return 예약어를 생략할 수 있다. { }중괄호를 사용하지 않고 si=(String b)->b+"로직 없이 바로 리턴"; 같은 방법으로 사용할 수 있다.

profile
천천히 기록해보는 비비로그

0개의 댓글