Seren dev's blog
article thumbnail
이 글은 남궁성 님의 "자바의 정석: 기초편" 책을 읽고 정리한 내용입니다.

상속

기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것 -> 코드의 재사용성을 높이고 코드의 중복을 제거하여 프로그램의 생산성과 유지보수에 크게 기여를 한다.

클래스 간의 상속관계를 그림으로 표현한 것을 상속계층도(class hierarchy)라고 한다.

 

- 자손 클래스는 조상클래스의 모든 멤버를 상속받는다. (단 생성자와 초기화 블럭은 상속되지 않는다.

 

클래스 간의 관계 - 포함관계

한 클래스의 멤버변수로 다른 클래스의 참조변수를 선언하는 것

Class Circle {
    Point c = new Point();
    int r;
}

Class Car {
    Engine e = new Engine();
    Door[] d = new Door[4];
}

 

~은 ~이다(is - a) : 상속관계

~은 ~을 가지고 있다(has - a) : 포함관계

원은 점이다. - Circle is a point.
원은 점을 가지고 있다. Circle has a point.
-> 포함관계

SportsCar은 Car이다. -> 상속관계

 

자바는 단일 상속만 가능하다.

다중 상속을 허용하면 여러 클래스로부터 상속받을 수 있기 때문에 복합적인 기능을 가진 클래스를 쉽게 작성할 수 있다는 장점이 있지만, 클래스 간의 관계가 매우 복잡해지고 서로 다른 클래스로부터 상속받은 멤버간의 이름이 같은 경우 구별할 수 있는 방법이 없다는 단점이 있다.

 

Object 클래스 - 모든 클래스의 조상

class Tv {
}

class Tv extends Object {
}

컴파일러는 자동적으로 'extends Object'를 추가하나다.

Object 클래스의 주요 메서드로 toString()equals(Object o)가 있다.

 

오버라이딩

조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩이라고 한다.

class Point {
    int x;
    int y;
    
    String getLocation() {
        return "x :" + x + ", y :" + y;
    }
}

class Point3D extends Point {
    int z;
    
    String getLocation() {
        return "x :" + x + ", y :" + y + ", z:" + z;
    }
}

오버라이딩의 조건

1. 선언부가 조상 클래스의 메서드와 일치해야 한다.

 

2. 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.

조상 클래스 메서드 접근제어자가 protected라면, 이를 오버라이딩하는 자손 클래스 메서드 접근 제어자는 protected나 public이어야 한다. 대부분의 경우 같은 범위의 접근 제어자를 사용한다.

(넓은 것) public -> protected -> (default) -> private (좁은 것)

 

3. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.

 

오버로딩(overloading) : 기존에 없는 새로운 메서드를 정의하는 것
오버라이딩(overriding) : 상속받은 메서드의 내용을 변경하는 것

 

참조변수 super / super() - 조상의 생성자

super: 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수

super() : 조상의 생성자를 호출하는데 사용

class Point {
    int x;
    int y;
    
    Point(int x, int y) {
    	this.x = x;
        this.y = y;
    }
}

class Point3D extends Point {
    int z;
    
    Point3D(int x, int y, int z) {
    	super(x, y);
        this.z = z;
    }
}

이처럼 클래스 자신에 선언된 변수는 자신의 생성자가 초기화를 책임지도록 작성하는 것이 좋다. 참고로 생성자는 상속되지 않는다.

 

패키지: 클래스의 묶음

서로 다른 패키지에 같은 클래스를 생성하는 것이 가능하다.

사실 클래스의 실제 이름(full name)은 패키지명을 포함한 것이다.

 

String 클래스의 이름은 java.lang.String이다.

 

클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리이다.

 

패키지 선언문은 반드시 소스파일에서 주석과 공백을 제외한 첫번째 문장이어야 하며 소문자를 원칙으로 한다.

 

소스파일에 자신이 속할 패키지를 지정하지 않은 클래스는 자동적으로 이름없는 패키지(unnamed package)에 속하게 된다.

 

import문

컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공한다.

모든 소스파일에서 import문은 package문 다음에, 클래스 선언문 이전에 위치해야 한다.

 

같은 패키지에서 여러 개의 클래스가 사용될 때 import 패키지명.* 을 이용해서 사용할 수 있다.

클래스 이름을 지정해주는 대신 '*'를 사용하면, 컴파일러는 해당 패키지에서 일치하는 클래스이름을 찾아야 하는 수고를 더 해야 할 것이다. 실행 시 성능상의 차이는 전혀 없다.

 

static import 문을 사용하면 static 멤버를 호출할 때 클래스 이름을 생략할 수 있다.

package lotto.domain;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

class LottoTest {
    @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.")
    @Test
    void createLottoByDuplicatedNumber() {
        assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5)))
                .isInstanceOf(IllegalArgumentException.class);
    }

 

제어자(modifier)

접근 제어자 : public, protected, (default), private

그 외 : static, final, abstract, native, transient, synchronized, volatile, strictfp

static - 클래스의, 공통적인

static 변수(클래스 변수)는 모든 인스턴스가 공유한다. 인스턴스를 생성하지 않아도 사용가능하다.

static 멤버변수 - 모든 인스턴스에 공통적으로 사용되는 클래스 변수
- 인스턴스를 생성하지 않고도 사용 가능
- 클래스가 메모리에 로드될 때 생성
메서드 - 인스턴스를 생성하지 않고도 사용 가능
- static 메서드 내에서는 인스턴스 멤버들을 직접 사용할 수 없다.

final - 변경 불가능

final 클래스 - 변경될 수 없는, 확장할 수 없는 클래스
- final 클래스는 다른 클래스의 조상이 될 수 없다.
메서드 - 변경될 수 없는 메서드
- final로 지정된 메서드는 오버라이딩을 통해 재정의 될 수 없다.
멤버변수 - 값을 변경할 수 없는 상수
지역변수

ex) final 클래스의 대표적인 예는 String, Math

abstract - 미완성의

abstract 클래스 - 추상클래스
- 클래스 내에 추상 메서드가 선언되어 있음을 의미
- 인스턴스 생성불가
메서드 - 선언부만 작성하고 구현부는 작성하지 않은 추상메서드
abstract class AbstractTest {
    abstract void move();
}

접근 제어자

private : 같은 클래스 내에서만 접근 가능

(default) : 같은 패키지 내에서만 접근 가능

protected : 같은 패키지 내에서, 다른 패키지의 자손 클래스에서 접근 가능

public : 접근 제한 없음

제어자 같은 클래스 같은 패키지 자손클래스 전체
public o o o o
protected o o o  
(default) o o    
private o      

 

접근 제어자를 사용하는 이유

- 클래스의 내부에 선언된 데이터를 외부로부터 보호하기 위함

- 외부에는 불필요한, 내부적으로만 사용되는 부분을 감추기 위함

=> 데이터 감추기data hiding, 캡슐화encapsulation

 

다형성(polymorphism)

여러 가지 형태를 가질 수 있는 능력

자바에서는 한 타입의 참조변수여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 구현함

class Tv {
}

class SmartTv extends Tv {
}

Tv t = new SmartTv();
class Car {}
class FireEngine extends Car {}
class Ambulance extends Car {}

FireEngine f = new FireEngine();

Car c = (Car)f; //OK, 생략 가능
FireEngine f2 = (FireEngine)c; //OK

 

서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 혀용되지 않는다.

class Point {
    int x;
    int y;

    String getLocation() throws IOException, SQLException {
        return "x :" + x + ", y :" + y;
    }
}

class Point3D extends Point {
    int z;

    String getLocation() throws IOException {
        return "x :" + x + ", y :" + y + ", z:" + z;
    }
}

public class Main {

    public static void main(String[] args) throws IOException {

        Point p = new Point();
        p.x = 10;
        p.y = 10;
        Point3D  p2 = (Point3D)p;

    }
}

Exception in thread "main" java.lang.ClassCastException: class Point cannot be cast to class Point3D (Point and Point3D are in unnamed module of loader 'app')
at Main.main(Main.java:31)

 

        Point3D  p3 = new Point3D();

        Point p = (Point)p3;
        Point3D  p2 = (Point3D)p; // OK

        System.out.println(p2);

 

참조변수 instanceof 타입(클래스명)

참조변수가 참조하고 있는 인스턴스의 실제 타입을 알 수 있다.

어떤 타입에 대한 instanceof 연산의 결과가 true라는 것은 검사 타입으로 형변환이 가능하다는 뜻이다.

void doWork(Car c) {
    if (c instanceof FireEngine) {
        FireEngine fe = (FireEngine)c;
        fe.water();
        ...

 

Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();

 

추상 클래스 abstract class

미완성 클래스 = 미완성 메서드(추상 메서드)를 포함하고 있다는 뜻

인스턴스 생성 불가

상속을 통해서 자손클래스에 의해서만 완성될 수 있다.

 

추상 클래스는 추상 메서드를 포함하고 있다는 것을 제외하고는 일반 클래스와 전혀 다르지 않다. 추상 클래스에도 생성자가 있으며, 멤버변수와 메서드도 가질 수 있다.

추상 메서드 abstract method

선언부만 작성하고 구현부는 작성하지 않음

미완성 상태로 남겨 놓는 이유는 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 추상 클래스에서는 선언부만을 작성하고, 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워두는 것이다.

추상 클래스로부터 상속받는 자손클래스는 오버라이딩을 통해 조상인 추상클래스의 추상메서드를 모두 구현해주어야 한다.

만일 상속받는 추상메서드 중 하나라도 구현하지 않는다면, 자손클래스 역시 추상클래스로 지정해주어야 한다.

 

상속 : 자손클래스를 만드는데 조상 클래스 사용

추상화 : 기존의 클래스의 공통부분을 뽑아내서 조상 클래스를 만드는 것

abstract class Unit {
    int x, y;
    abstract void move(int x, int y);
    void stop() {
        System.out.println("stop");
    }
}

class Marine extends Unit {
    void move(int x, int y) {
        
    }
    void stimPack() {
        
    }
}

stop 메서드는 선언부와 구현부 모두 공통적

move 메서드는 클래스마다 실제 구현 내용이 다를 것이므로 추상화 메서드로 작성

-> Unit 클래스를 상속받아서 작성되는 모든 클래스는 move 메서드를 반드시 구현해야 한다.

 

Unit[] group = {new Marine(), new Tank(), new Dropship() };
for () {
    group[i].move(100, 200);
}

메서드는 참조변수의 타입에 관계없이 실제 인스턴스에 구현된 것이 호출된다.

 

인터페이스

일종의 추상클래스

추상화 정도가 높음

일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.

오로지 추상메서드와 상수만을 멤버로 가질 수 있다.

interface 인터페이스 이름 {
    public static final 타입 상수이름 = 값;
    public abstract 메서드이름(매개변수);
}

- 모든 멤버변수public static final 이어야 하며, 생략 가능

- 모든 메서드public abstract 이어야 하며, 생략 가능 (JDK1.8부터 static 메서드와 디폴트 메서드 추가 가능)

 

인터페이스는 다중상속이 가능하다.

상속과 구현을 동시에 할 수도 있다.

만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, abstract를 붙여서 추상클래스로 선언해야 한다.

 

리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

인터페이스의 장점

1. 개발시간을 단축시킬 수 있다.

2. 표준화가 가능하다.

3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.

4. 독립적인 프로그래밍이 가능하다.

- 인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있다.

 

디폴트 메서드

원래 인터페이스의 모든 메서드는 추상메서드이어야 한다는 규칙이 있다.

하지만 인터페이스도 변경이 필요할 때가 있고, 그럴 때마다 메서드를 추가하면 구현한 클래스는 새로 추가된 메서드를 구현해야 한다.

그래서 디폴트 메서드를 추가한다. 키워드 앞에 default를 붙이며, 일반 메서드처럼 몸통 {}이 있어야 하고 public이며  public은 생략 가능하다.

interface MyInterface {
    void mothod();
    default void newMethod() {
    }
}

새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우 해결방법

1. 여러 인터페이스의 디폴트 메서드 간의 충돌

- 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩

2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌

- 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

 

class Ex7_11 {
	public static void main(String[] args) {
		Child3 c = new Child3();
		c.method1();
		c.method2();
		MyInterface.staticMethod(); 
		MyInterface2.staticMethod();
	}
}

class Child3 extends Parent3 implements MyInterface, MyInterface2 {
	public void method1() {	
		System.out.println("method1() in Child3"); // 오버라이딩
	}			
}

class Parent3 {
	public void method2() {	
		System.out.println("method2() in Parent3");
	}
}

interface MyInterface {
	default void method1() { 
		System.out.println("method1() in MyInterface");
	}

	default void method2() { 
		System.out.println("method2() in MyInterface");
	}

	static void staticMethod() { 
		System.out.println("staticMethod() in MyInterface");
	}
}

interface MyInterface2 {
	default void method1() { 
		System.out.println("method1() in MyInterface2");
	}

	static void staticMethod() { 
		System.out.println("staticMethod() in MyInterface2");
	}
}

 

내부 클래스

- 클래스 내에 선언된 클래스

- 두 클래스가 서로 긴밀한 관계

- 한 클래스를 다른 클래스의 내부 클래스로 선언하면 두 클래스의 멤버들 간에 서로 쉽게 접근할 수 있고, 외부에는 불필요한 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다.

 

내부 클래스의 장점

- 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.

- 코드의 복잡성을 줄 일 수 있다(캡슐화).

class A {
    ...
    class B {
    }
}
내부 클래스 B는 외부 클래스A를 제외하고 다른 클래스에서는 잘 사용되지 않는 것이어야 한다.

내부 클래스의 종류와 특징

인스턴스 클래스 외부 클래스의 멤버변수 선언위치에 선언하며 외부 클래스의 인스턴스 멤버처럼 다루어진다.
주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다.
static 클래스 외부 클래스의 멤버변수 선언위치에 선언하며 외부 클래스의 static 멤버처럼 다루어진다.
주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목적으로 선언된다.
local 클래스 외부 클래스의 메서드나 초기화 블럭안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다.
익명 클래스(번외) 클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스 (일회용)
class Outer {
    class InstanceInner {]
    static class StaticInner {}
    
    void myMethod() {
        class LocalInner {}
    }
}

인스턴스 클래스와 static 클래스는 외부 클래스의 멤버변수와 같은 위치에 선언되며, 외부 클래스의 멤버와 같이 간주된다.

내부 클래스도 abstarct나 final과 같은 제어자 사용가능

private, protected 사용가능

 

class Ex7_12 { 
	class InstanceInner { 
		int iv = 100; 
//		static int cv = 100;            // 에러! static변수를 선언할 수 없다. 
		final static int CONST = 100;   // final static은 상수이므로 허용
	} 

   static class StaticInner { 
		int iv = 200; 
		static int cv = 200;    // static 클래스만 static 멤버를 정의할 수 있다. 
	} 

	void myMethod() { 
		class LocalInner { 
			int iv = 300; 
//			static int cv = 300;             // 에러! static변수를 선언할 수 없다. 
			final static int CONST = 300;    // final static은 상수이므로 허용 
		} 
	} 

	public static void main(String args[]) { 
		System.out.println(InstanceInner.CONST); 
		System.out.println(StaticInner.cv); 
	} 
}

static 클래스만 static 멤버를 가질 수 있다.

final static은 상수이므로 모든 내부 클래스에서 사용 가능하다.

 

class Ex7_13 {
	class InstanceInner {}
	static class StaticInner {}

	// 인스턴스멤버 간에는 서로 직접 접근이 가능하다.
	InstanceInner iv = new InstanceInner();
	// static 멤버 간에는 서로 직접 접근이 가능하다.
	static StaticInner cv = new StaticInner();

	static void staticMethod() {
      // static멤버는 인스턴스멤버에 직접 접근할 수 없다.
//		InstanceInner obj1 = new InstanceInner();	
		StaticInner obj2 = new StaticInner();

      // 굳이 접근하려면 아래와 같이 객체를 생성해야 한다.
      // 인스턴스클래스는 외부 클래스를 먼저 생성해야만 생성할 수 있다.
		Ex7_13 outer = new Ex7_13();
		InstanceInner obj1 = outer.new InstanceInner();
	}

	void instanceMethod() {
      // 인스턴스메서드에서는 인스턴스멤버와 static멤버 모두 접근 가능하다.
		InstanceInner obj1 = new InstanceInner();
		StaticInner obj2 = new StaticInner();
		// 메서드 내에 지역적으로 선언된 내부 클래스는 외부에서 접근할 수 없다.
//		LocalInner lv = new LocalInner();
	}

	void myMethod() {
		class LocalInner {}
		LocalInner lv = new LocalInner();
	}
}

인스턴스 멤버(인스턴스 메서드)는 같은 클래스에 있는 인스턴스 멤버와 static 멤버 모두 직접 호출이 가능하다.

static 멤버(static 메서드)는 외부클래스의 인스턴스 멤버를 객체 생성 없이 사용할 수 없다.

 

class Outer {
	private int outerIv = 0;
	static  int outerCv = 0;

	class InstanceInner {
		int iiv  = outerIv;  // 외부 클래스의 private멤버도 접근가능하다.
		int iiv2 = outerCv;
	}

	static class StaticInner {
// 스태틱 클래스는 외부 클래스의 인스턴스멤버에 접근할 수 없다.
//		int siv = outerIv;
		static int scv = outerCv;
	}

	void myMethod() {
		int lv = 0;
		final int LV = 0;  // JDK1.8부터 final 생략 가능

		class LocalInner {
			int liv  = outerIv;
			int liv2 = outerCv;
//	외부 클래스의 지역변수는 final이 붙은 변수(상수)만 접근가능하다.
//			int liv3 = lv;	// 에러!!!(JDK1.8부터 에러 아님)
			int liv4 = LV;	// OK
		}
	}
}

인스턴스 내부 클래스

- 외부 클래스의 인스턴스 멤버이기 때문에 외부 클래스의 인스턴스 변수와 static 변수 모두 사용 가능

static 내부 클래스

- 외부 클래스의 인스턴스 멤버 사용 불가능 / static 멤버 사용 가능

local 내부 클래스

- 외부 클래스의 인스턴스 멤버와 static 멤버 모두 사용 가능

- 지역 클래스가 포함된 메서드에 정의된 final 지역변수 사용가능

final 만 가능한 이유는 메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도, 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다.

JDK1.8부터 편의성으로 생략 가능, 컴파일러가 자동으로 붙여줌

 

class Outer3 {
	int value = 10;	// Outer3.this.value

	class Inner {
		int value = 20;   // this.value

		void method1() {
			int value = 30;
			System.out.println("            value :" + value);
			System.out.println("       this.value :" + this.value);
			System.out.println("Outer3.this.value :" + Outer3.this.value);
		}
	}
}

class Ex7_16 {
	public static void main(String args[]) {
		Outer3 outer = new Outer3();
		Outer3.Inner inner = outer.new Inner();
		inner.method1();
	}
}

//결과
//value :30
//this.value :20
//Outer3.this.value :10

 

연습 문제

import java.io.IOException;

class Outer {
    class Inner {
        int iv = 100;
    }
    
    static class StaticInner {
        int iv = 200;
    }
}

public class Main {

    public static void main(String[] args) throws IOException {

        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        System.out.println(inner.iv);

        Outer.StaticInner staticInner = new Outer.StaticInner();
        System.out.println(staticInner.iv);
    }
}

 

익명 클래스

- 이름이 없음

- 클래스의 선언과 객체의 생성을 동시에 하기 때문에 한번만 사용될 수 있고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스

new 조상클래스이름() {
    //멤버 선언
}

new 구현인터페이스이름() {
    //멤버 선언
}

이름이 없기 때문에 생성자 X

하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나, 둘 이상의 인터페이스를 구현할 수 없다.

오로지 단 하나의 클래스만 상속받거나 단 하나의 인터페이스만 구현할 수 있다.

사용 예제

class Ex7_17 {
	Object iv = new Object(){ void method(){} };         // 익명 클래스
	static Object cv = new Object(){ void method(){} };  // 익명 클래스

	void myMethod() {
		Object lv = new Object(){ void method(){} };      // 익명 클래스
	}
}

 

익명 클래스 사용 전

import java.awt.*;
import java.awt.event.*;

class Ex7_18 {
	public static void main(String[] args) {
		Button b = new Button("Start");
		b.addActionListener(new EventHandler());
	}
}

class EventHandler implements ActionListener {
	public void actionPerformed(ActionEvent e) {
		System.out.println("ActionEvent occurred!!!");
	}
}

익명 클래스 사용 후

import java.awt.*;
import java.awt.event.*;

class Ex7_19 {
	public static void main(String[] args) {
		Button b = new Button("Start");
		b.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					System.out.println("ActionEvent occurred!!!");
				}
			} // 익명 클래스의 끝
		);
	} // main의 끝
}
728x90
profile

Seren dev's blog

@Seren dev

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!