본문으로 바로가기

클래스의 관계-상속/제어자/다형성/패키지/임포트

category JAVA 2019. 12. 8. 00:08

상속(extends)


상속의 정의와 구현

  • 프로그램에서의 상속은 기존 클래스의 재산을 다른 클래스에서 재사용하기 위한 것이다.
    재산 : 멤버 변수, 멤버 메서드(생성자와 초기화 블록은 상속의 대상이 아니다.)
  • 상속받은 클래스는 물려받은 멤버들을 자신의 것처럼 사용할 수 있기 때문에 코드의 절감효과를 가져오게 된다.
  • 또한, 부모의 코드를 변경하면 모든 자식 클래스들에게도 적용되므로 유지보수성 역시 향상된다.
  • 상속을 적용할때는 자식 클래스의 선언부에서 extends 키워드와 함게 조상 클래스 이름을 표시한다.

//부모 클래스
public class Person {
	//field
	String name;
	//method
	public void eat() {
		System.out.println("eating food");
	}
	
	public void jump() {
		System.out.println("jump! jump!");
	}
}
//상속받는 자식 클래스
public class SpiderMan extends Person {
	//field
	boolean isSpider;
	//method
	public void fireWeb() {
		System.out.println("거미줄 발사");
	}
}

 

 

 

다양한 관계

(1) 상속과 Object클래스

  • Object 클래스는 모든 클래스의 조상 클래스로 만약 클래스 선언부에 명시적인 extends 선언이 없는 경우는 무조건 extends Object 코드가 생략된 것이다.

(2) 단일 상속 지원(is a)

  • 하나의 클래스를 두개의 클래스가 상속 받을 수도 있다.
  • 하나의 클래스가 여러개의 클래스를 상속받을 수는 없다.

(3) 포함 관계(has a)

  • 포함관계는 연관 관계로 불린다.
  • 상속 이외에 클래스를 재활용하는 방법이다.
  • 단일 상속 지원 정책으로 하나만 상속할 수 있기때문에 상속 받고 싶은 나머지 클래스들은 멤버 변수로 처리해서 사용하는 것이 포함 관계이다.
  • 어떤 클래스를 상속받고 어떤 클래스를 포함할지는 프로젝트에 관점에 따르다. 
    'SpiderMan is a Person?' 관계의 질문을 통해 적절하다고 판단되면 상속받고 아니면 포함시키면 된다.
//포함관계에 있는 클래스
public class Spider {
	public void jump() {
		System.out.println("spider jump! jump!");
	}
	
	public void fireWeb() {
		System.out.println("거미줄 발사");
	}
}
//person 클래스를 상속받은 spiderman2 클래스
public class SpiderMan2 extends Person{
	//field
	Spider spider=new Spider();  //멤버변수로 spider 객체의 참조값 갖고 있기
	boolean isSpider;
	//method
	public void fireWeb() {
		if(isSpider) {
			spider.fireWeb();
		}else {
			System.out.println("거미줄 발사 불가");
		}
	}
}

 

 

메서드 오버라이딩(overriding:재정의)

조상 클래스에 정의된 기능을 자식 클래스에 적합하게 수정해서 재정의 하는 것을 Method Overriding(재정의)라고 한다.

주의! overloading과 헷갈리지 말것!
- overloading : 메서드 추가
- overriding : 기존 메서드 재정의 개념으로 기존 메서드 위에 덮어쓰는 over write이다.

 

메서드 오버라이딩 규칙

  1. 메서드 이름은 조상 클래스의 메서드 이름과 같아야한다.
  2. 매개변수의 개수, 타입, 순서는 조상클래스의 메서드와 같아야한다.
  3. 리턴 타입은 조상 클래스의 메서드와 같아야한다.
  4. 접근 제한자는 조상 클래스의 메서드보다 범위가 같거나 넓어야 한다.
  5. 조상 클래스의 메서드보다 더 상위의 예외를 던질 수는 없다.
//Person으로 부터 상속받은 jump()메서드 오버라이드 하기
public class SpiderMan2 extends Person{
	//field
	Spider spider=new Spider();  //멤버변수로 spider 객체의 참조값 갖고 있기
	boolean isSpider;
	//method
	public void fireWeb() {
		if(isSpider) {
			spider.fireWeb();
		}else {
			System.out.println("거미줄 발사 불가");
		}
	}
	@Override
	public void jump() {
		if(isSpider) {
			spider.jump();
		}else {
			System.out.println("Super jump! jump!");
		}
	}
}
//오버라이드한 메서드 출력해보기
public class SpiderManTest {
	public static void main(String[] args) {
		//SpiderMan2 type의 로컬변수 a에 SpiderMan2객체의 참조값 담기
		SpiderMan2 a=new SpiderMan2();
		a.jump(); //Super jump! jump!
		
		//Person type의 로컬변수 b에  SpiderMan2객체의 참조 값 담기
		Person b=new SpiderMan2();
		b.jump(); //Super jump! jump!
		
		//Person type의 로컬변수 c에  Person객체의 참조 값 담기
		Person c=new Person();
		c.jump();  //jump! jump!
	}
}

 

 

super 키워드

(1) 조상의 멤버를 참조하는 super

  • super는 조상객체를 참조한다.
  • static 영역에서는 사용할 수 없다. 
    super는 어떤 객체에 대한 참조인데 static영역은 객체가 없는 상태에서도 호출될 수 있어야하기 때문이다.w
  • 주의!
    - super를 통해서 접근하는 것은 직계 1촌 까지만 접근할 수 있으며 그 위로는 접근할 수 없다.
public class SpiderMan2 extends Person{
	//field
	Spider spider=new Spider();  //멤버변수로 spider 객체의 참조값 갖고 있기
	boolean isSpider;
	//method
	//super 예약어를 사용해 조상객체를 참조하여 메소드 출력하기
	@Override
	public void jump() {
		if(isSpider) {
			spider.jump();
		}else {
			super.jump();
		}
	}
}
//test
public class SpiderManTest {
	public static void main(String[] args) {
		//SpiderMan2 type의 로컬변수 a에 SpiderMan2객체의 참조값 담기
		SpiderMan2 a=new SpiderMan2();
		a.jump(); // jump! jump!
	}
}

 

 

 

**this, super 등 특수 참조 키워드들의 접근 범위**
변수는 자신이 선언된 곳에서부터 점점 영역을 확장해가며 최초로 만난 선언부에 연결된다.

  • 예를들어
    메서드 내부에서 사용된 변수는 메서드 내부의 로컬 변수->해당 클래스 멤버변수->부모클래스 멤버변수->조상클래스 멤버변수로 확장해가며 선언된 곳을 찾아간다. 
    최상위 객체인 Object까지 찾아봤는데도 선언된 변수가 없으면 오류가 발생한다.
  • this로 접근할 때는 로컬 변수에서 찾는 것은 생략하고 해당 클래스의 멤버 변수부터 검색한다.
  • super로 접근할 때는 부모 클래스의 멤버 변수부터 찾는다.

[예시]

package test.person;

public class Parent {
	String x="parent";
}


public class Child extends Parent{
	String x="Child";
	
	void method() {
		String x="method";
		System.out.println("x:"+x);
		System.out.println("x:"+this.x);
		System.out.println("x:"+super.x);
	}
}


public class ScopeTest{
	public static void main(String[] args) {
		Child child=new Child();
		child.method();
	}
}

/*
x : method
this.x : child
super.x : parent

<String x="method"; 삭제>
x : child
this.x : child
super.x : parent

<String x="Child"; 삭제>
x : parent
this.x : parent
super.x : parent

<String x="parent"; 삭제>
오류발생
*/

 

(2) 조상의 생성자를 호출하는 super

  • super는 조상의 생성자를 호출할 수 있다.
  • 명시적으로 this() 또는 super()를 이용해서 다른 생성자를 호출하는 코드가 없다면 컴파일러는 언제나 조상의 기본 생성자를 호출하는 super()를 생성자의 첫행에 삽입하게 된다.
    결론적으로 최상위 객체인 object까지 모든 조상객체가 다 만들어지는 구조가 된다.
  • 주의!
    - super도 this와 마찬가지로 생성자의 첫번째 행에만 올 수 있다.
     따라서 this()와 super()는 동시에 나올 수 없다.

어떤 코드에서 자식 클래스 타입 객체를 만들때 초기화 파라미터를 넘겨주게 된다.

이 파라미터 중에는 자식 클래스가 선언한 멤버 변수도 있지만, 조상클래스에서 선언된 멤버변수들도 포함될 수 있다.

조상 클래스에 선언된 멤버변수들은 조상 클래스의 생성자에 의해 초기화가 이루어지므로 조상 클래스 생성자를 호출해서 초기화 하고 자식 클래스에서는 자식이 선선한 멤버 변수들만 초기화를 진행하면 된다.

이때 조상 클래스의 생성자를 호출하기 위해 super()가 사용된다.

코드와 메모리구조

 

 

 

패키지(package)와 임포트(import)


패키지(package)

  • 클래스들을 체계적으로 관리하기 위해 '패키지'를 사용한다.
    (윈도우에서 파일을 관리할때 디렉토리를 이용하는 것과 비슷하다)
  • 패키지의 이름은 포함된 클래스들의 용도를 유추할 수 있는 의미있는 이름으로 작성한다.
  • 필요에 따라 하위 패키지를 둘 수 있고 각 단계는 '.(dot)'을 이용해 계층적으로 관리한다.
  • 패키지의 단계를 만들때 일반적으로 사용되는 규칙
    예를들어 example.com회사를 다닌다면 package명은 com.example로 작성한다.

 

 

임포트(import)

 

  • 소스코드를 작성할 때 다른 패키지의 클래스를 사용하려면 패키짐여이 포함된 클래스 이름을 사용해야 한다. 하지만, 매번 패키지명을 붙여서 작성하는 것은 비효율적이다.
  • 클래스의 코드를 작성하기 전에 import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스코드에 사용되는 클래스이름에서 패키지명은 생략할 수 있다.
  • import문의 역할은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하는 것이다. 컴파일 시에 컴파일러는 import문을 통해 소스파일에 사용된 클래스들의 패키지를 알아 낸 다음, 모든 클래스이름 앞에 패키지명을 붙여 준다.
  • import는 다른 패키지에 선언된 클래스를 사용하기 위해 필요한 키워드이다.
  • package 선언과 class선언 사잉에 위치하며, package선언과 달리 필요한 클래스가 있을때 마다 사용할 수 있다.
  • import 선언
import 패키지명.클래스명;
import 패키지명.*   // *는 패키지내의 모든 클래스 import. 단, 하위 패지키의 클래스는 import 하지 않음
  • 클래스의 이름이 여러 패키지에서 동일하게 사용되는 경우

  • System, String, Object와 같은 클래스는 모두 java.lang 패키지에 속해있다.
    따라서 원칙상 java.lang 패키지를 import해야하지만 해당 패키지는 너무 자주 사용되는 패키지이기 때문에 별도로 import지 않아도 자동으로 import 된다.

(1) static import

  • static import는 static으로 설정된 멤버에 대해 바로 import를 처리한다. 그리고 이런 멤버들을 호출할때 클래스 이름을 생략하고 바로사용할 수 있다.

 

 

 

제어자(modifier)


제어자(modifier)

  • 제어자란 클래스, 변수, 메서드 작성시 함께 사용돼서 부가적인 의미를 부여해주는 키워드이다.
  • 제어자의 종류
    - 접근 제어자 : 접근 제어자는 멤버 변수 등을 사용할 수 있는 범위를 지정하는 키워드로 
                        public, protected, default, private 네가지 종류가 사용된다.
    - 그 외의 제어자 : static , final, abstract, synchronized 등으로 각각 제어자별로 특별한 목적이 있다.
  • 하나의 대상에 여러 제어자를 조합해서 사용할 수 있다. 
    그러나 접근 제어자는 하나만 사용할 수 있다.
  • 제어자를 작성하는 순서는 무관하지만 일반적으로 접근제어자를 제일 앞에 작성한다.

 

(1) final

  • 마지막, 더는 바뀔 수 없음을 의미한다.
  • fianl은 클래스, 변수, 메서드에 사용된다.
  • 관례상 final 변수, 메서드의 이름은 대문자로 작성한다.
  • final이 선언된 클래스는 상속(extends)받을 수 없는 클래스를 의미한다.
    이미 자체적으로 완벽한 변수나 클래스를 마음대로 오버라이드 할 수 없도록 설정하는 경우 많이 사용함
//final로 선언된 클래스
public final class PerfectClass {}

//final로 선언된 클래스를 상속 받으려하는 경우
public class FinalClassTest extends PerfectClass{}
//The type FinalClassTest cannot subclass the final class PerfectClass
  • final이 선언된 메서드는 오버라이드 할 수 없는 메서드를 의미한다.
    클래스 자체는 상속받도록 허용하지만, 특정 메서드만 오버라이딩을 못하게 하는 것
//final선언한 메서드
public class PerfectClass {
	public final void finalMthod() {}
}

//오버라이드 하려고 한는 메서드
public class FinalClassTest extends PerfectClass{
	public void finalMthod() {
		System.out.println("dd");
	}
}
//Cannot override the final method from PerfectClass
  • final이 선언된 변수는 더이상 값을 바꿀 수 없는 변수를 말한다.
    즉, 변수를 '상수화'해 읽기 전용으로 사용하는 것이다.
//숫자의 상수화 : 복잡한 숫자열을 간단히 쓰겠다.
final double PI=3.14159;

//문자열의 상수화 : 복잡한 문자열에 이름을 붙여놓고 간단히 쓰겠다.
final String GREETING_KOR="안녕하세요";
final String GREETING_CHN="니하오";
final String GREETING_ENG="헬로";
final String GREETING_JPN="곤니찌와";

 

 

(2) blank final

  • final 변수는 일반적으로 선언과 동시에 초기화하는 경우가 많다. 그런데 가끔 초기화를 진행하지 않는 경우있는데 이런 경우를 blank final이라고 한다.
public class BlankFinalTest {
	//필드
	final String name; // The blank final field name may not have been initialized
}
  • blank final을 사용하는 경우는 객체마다 다른 값을 가져야 하는데 객체 내에서 값을 바꿀 수 없게 하려는 경우이다.

[예시]

public class FinalMemberClass{
	//필드
	final String name;  //final로 선언된 String type의 name 변수(blank fianl의 형태)
	//생성자
	public FinalMemberClass(String name) {
		this.name=name;
	}
}

public class BlankFinalTest {
	public static void main(String[] args) {
    	//객체를 생성하면서 각각 다른 값을 할당
		FinalMemberClass fmc1=new FinalMemberClass("짱구");
		FinalMemberClass fmc2=new FinalMemberClass("짱아");
		//생성된 객체를 통해 새로운 값 할당하는 테스트
		fmc1.name="훈이"; //The final field FinalMemberClass.name cannot be assigned
	}
}

 

 

접근 제어자(Access Modifier)

  • 접근 제어자(Access Modifier)는 멤버 등에서 사용되며 해당 요소를 외부에서 사용할 수 있게 할 것 인지를 제어한다.
  • 접근 제어자를 작성하지 않으면 default로 설정된다.

접근 제어자의 사용범위

 

접근 제어자의 접근 가능 범위

 

데이터 은닉과 보호(Encapsulation)

사람을 모두 객체로 본다면 우리가 가진 멤버 변수 중에는 은행잔고도 있을 것이다.

바로 이 은행잔고에 모두 바로 접근(public)할 수 있다면 어떻게 될까?

package modifier.encapsulation;

//은행잔고
class UnbelievableUserInfo {
	//이름은 null이 될 수 없음
	public String name="홍길동";
	//계좌는 0보다 커야함.
	public int account=10000;
}

//은행잔고에 접근하기 
class UnbelievableTest {
	public static void main(String[] args) {
		UnbelievableUserInfo info=new UnbelievableUserInfo();
		System.out.println("사용자 정보 : "+info.name+", "+info.account);
        //객체에 직접 접근하여 값 수정
		info.name=null;
		info.account=-10000;
		System.out.println("사용자 정보 : "+info.name+", "+info.account);
	}
}

주석으로 UnbelievableUserInfo 클래스 필드의 범위를 정해 놓았으나 public field를 어디에서나 접근할 수 있도록 public 으로 설정해 놓았기 때문에 외부에서 손 쉽게 객체에 접근하여 사용자 정보를 수정하였다.

 

이런경우 멤버 변수에 직접 접근할 수 없도록 접근 제어자로 접근 제한을 두어야한다.

 

소중한 데이터들은 보통 private 접근제어자를 갖는다.

이 경우 멤버변수가 선언된 클래스에서만 해당 멤버 변수에 대한 직접 접근이 가능해진다.

 

그렇다면 외부 클래스에서는 private로 선언된 변수를 사용할 수 없게되는 것인가?

직접적으로는 접근할 수 없지만 public 등으로 공개되는 메서드를 제공해서 private 멤버에 접근할 수있느 통로로 활용할 수 있다.

이때, 일반적으로 값을 조회하는 메서드들의 이름은 get으로 시작하며 getter라고 부른다.

값을 설정하는 메서드들의 이름은 set으로 시작하며 setter라고 부른다.

 

통로로 활용되는 메서드(setter, getter) 내에는 정보 보호를 위한 필요한 로직을 넣을 수 있으므로 부적절한 값의 설정을 방지할 수 있다.

 

이와 같은 과정을 데이터 은닉과 보호, 즉 Encapsulation이라 한다. 

Encapsulation가 적용된 클래스를 보자

 

객체의 생성 제어와 Singleton 디자인 패턴

  • 객체가 여러개 필요한 경우는 객체마다 가지는 멤버 변수의 값이 다른 경우이다.
    따라서 멤버 변수가 없거나 같은 값만 사용된다면 굳이 여러 객체를 만들 필요가 없다.
  • 디자인 패턴(Design Pattern)이란 같은 상황에 부닥쳤던 많은 개발자가 이뤄낸 작업의 정리 결과이다.
    '어떤 목적을 달성하기 위한 프로그래밍 가이드' 정도로 생각하면된다.
  • 아래와 같은 문제를 처리하기 위한 디자인 패턴에는 싱글턴(Singleton) 디자인 패턴이 있다.
    싱글턴 디자인 패턴의 출발점은 대상 객체를 외부에서 직접 생성할 수 없어야한다.
  • 자바에서 데이터베이스를 사용하기 위해서는 java.sql.Connection 타입의 객체가 필요하다.
    이 객체는 네트워크를 통해 application과 DB를 연결하는 비싼 자원이다.
    (비싼 자원 ; 시간이 오래걸리거나 메모리 등의 시스템 리소스가 많이 드는 것)
    따라서 계속 연결과 반환을 반복하는 것 보다는 하나 또는 몇 개를 미리 만들어 놓고 재사용하는 것이 유리하다.
package ch07.modifier;

class SingletonClass {
	//클래스 내부에서만 생성자에 접근할 수 있도록 설정 
	private SingletonClass() {}
	
	// private으로 선언하고 SingletonClass 객체를 생성해서 멤버 변수에 담아 놓기
	private static SingletonClass instance=new SingletonClass();
	
	//SingletonClass 객체를 만드는 getter 메서드 설정
	public static SingletonClass getInstance() {
		return instance;
	}
	
	public void sayHello() {
		System.out.println("hello");
	}
}


public class SingletonTest {
	public static void main(String[] args) {
		SingletonClass a=SingletonClass.getInstance();
		SingletonClass b=SingletonClass.getInstance();
		System.out.printf("a,b객체는 같은가? ", a==b); //true
		//객체의 메서드 사용하기
		a.sayHello();
	}
}

 

SingletonClass의 객체가 필요할 때는 객체를 생성하는 것이 아니라 getInstance()메서드를 호출하면되고, 이 메서드의 리턴은 언제나 하나의 객체일 것이다.

 

 

 

다형성(Polymorphism)


다형성(Polymorphism)

 

  • 상속관계에 있을때 조상 클래스 타입으로 자식 클래스 객체를 레퍼런스 할 수 있다.
public class SpiderManTest {
	public static void main(String[] args) {
		SpiderMan2 a=new SpiderMan2();
		Person b=new SpiderMan2();
		Object c=new SpiderManTest();
	}
}

 

다형성의 활용

배열에서의 다형성 활용

배열은 같은 타입의 데이터를 묶어서 관리하는 것이 특징이다.

예전에는 Person type의 배열을 만들면 Person type의 객체들만 담을 수 있다고 알고 있었다면 
다형성을 배웠으니 SpidermMan2은 Person type의 객체에 담을 수 있다.

//다형성을 배우기전
void beforePoly(){
	//배열의 방이 10개인 Person[] 배열
	Person[] persons=new Person[10];
    persons[0]=new Person();
    SpiderMan[] spiderMans=new SpiderMan[10];
    spiderMans[0]=new SpiderMan();
}


//다형성 배운 후
void afterPoly(){
	Person[] persons=new Person[10];
    persons[0]=new Person();
    persons[1]=new SpiderMan();
}

 

한 걸음 더 나아가 Object의 배열을 만들게 되면 어떤 타입의 객체라도 다 저장할 수 있게된다.

 

매개 변수에서의 다형성 활용

System.out.println()를 이용해 다양한 객체를 출력할 수 있다.

System.out.println(new Person());
System.out.println(new SpiderMan());

어떤 type의 매개변수가 올지 모르는 상태에서 어떻게 위와 같은 메서드를 만들어 놓았을까?

그 이유는 파라미터의 타입을 Object로 설정해 놓았기 때문이다.

public void println(Object x){
	//code 작성
}

자바 API를 보면 다수의 범용적인 메서드들이 Object type을 파라미터로 받도록 설정되어 있는데, 이는 다형성을 이용해 어떤 객체를 처리하기 위해서 이다.

 

(1) 다형성과 참조형 객체의 형변환

다형성을 이용했을 때 '메모리의 동작'에 대해 살펴보자

heap 영역에는 SpiderMan2 객체가 저장되지만 참조하는 type이 Person이므로 SpiderMan2의 고유한 기능을 알 수 없어 SpiderMan2의 메서드나 필드에 접근할 수 없다. 

만약 Object type으로 참조하고 있다면 Object영역만 사용할 수 있다.

또한 다형성이 가능한 이유는 메모리에 있는 내용이 참조하는 타입(Object, Person, SpiderMan2)의 내용을 다 가지고 있는 충분조건을 만족하기 때문이다.

 

SpiderMan2의 필드나 메서드를 사용할 수는 없을까?

단순히 참조하는 변수의 타입에 관한 문제이기 때문에 casting을 하여 타입을 바꿔주면된다.

public void reference(){
	//묵시적 형변환
    Person p=new SpiderMan();
    p.fireWeb();  //메모리에 있지만 사용불가
    //명시적 형변환
    SpiderMan2 sman=(SpiderMan2)p;
    sman.fireWeb();
}

 

강제로 casting을 하는 것은 문법적으로 전혀 문제가 없다.

하지만 실제 메모리에 있는 객체는 Person type이기 때문에 SpiderMan2 type이 되기 위한 충분한 조건을 갖추지 못 한 상태이다.

 따라서 런타임 시의 메모리 구성상 형변환을 알 수 없다.

이 상황에서 프로그램을 동작시키면 형변환을 처리하다가 java.lan.ClassCastException을 발생시키며 종료된다.

 

결론적으로 부모타입을 언제나 형변환 연산자를 통해 작식 타입으로 변경할 수는 없다.

먼저 메모리에 있는 객체가 형변환을 위한 충분조건을 갖고 있는지 확인을 해봐야한다. 

이때 instanceof 연산자를 사용하면된다.

 

(2) instanceof 연산자

instanceof 연산자는 실제 메모리에 있는 객체가 특정 클래스 타입인지를 boolean 타입으로 리턴한다.
결과가 ture로 확인되면 형변환 처리를 해야한다.

 

Person person=new Person();
if (person instanceof SpiderMan2){
	SpidrMan2 a=(SpiderMan2) person;
}

 

 

(3)참조변수 레벨에 따른 객체 연결

 

 

 

(4)공변 : 리턴타입

 

 

 

 

 

 

 

 

 

 

 

 

 

'JAVA' 카테고리의 다른 글

내부 클래스(Inner Class) / 람다식  (0) 2019.12.09
추상클래스(Abstract Class) / 인터페이스(Interface)  (0) 2019.12.09
클래스(class)와 객체(object, instance)  (0) 2019.12.01
변수  (0) 2019.11.30
연산자 Operator  (0) 2019.11.29