Part2 객체 지향 프로그래밍

YunGyu Choi·2023년 7월 13일
0

이것이 자바다

목록 보기
2/3

1. 참조타입

자바의 데이터 타입은 크게 기본 타입(primitive type)과 참조 타입(reference type)으로 나뉜다. 참조 타입은 객체의 번지를 참조하는 타입으로 배열, 열거, 클래스, 인터페이스 타입 등이 있다.

기본 타입 변수와 참조 타입 변수의 차이점

  • 기본 타입 : 선언된 변수는 값 자체를 저장
  • 참조 타입 : 객체가 생성된 메모리 번지(주소)를 저장
    (따라서 참조 타입에서 ==, != 연산은 번지가 같은지, 다른지 묻는 것이다.)

변수들은 모두 Stack 메모리 영역에 생성된다. 기본 타입 변수는 Stack영역에 직접 변수 값을 저장하고 있지만, 참조 타입 변수는 Heap영역에 변수 값을 저장하고 Stack영역에는 메모리 번지를 저장한다. (Stack 메모리 영역에 심볼 테이블이 생성된다고 나는 이해하였다.)

메모리 영역

  • 메소드 영역
    바이트 코드 파일을 읽은 내용이 저장되는 영역으로 클래스별로 상속, 정적 필드, 메소드 코드, 생성자 코드등이 저장된다.

  • 힙 영역
    객체가 생성되는 영역이다. 객체의 번지는 메소드 영역과 스택 영역의 상수와 변수에서 참조할 수 있다. (변수와 연결이 끊어지면 GC에 의해 메모리 릴리즈)

  • 스택 영역
    메소드를 호출할 때마다 생성되는 프레임이 저장되는 영역이다. (메소드 호출이 끝나면 자동 제거)

null 과 NullPointerException

  • 참조 타입 변수는 아직 번지를 가지고 있지 않다는 뜻으로 null값을 가질 수 있다.
  • null도 초기값으로 사용할 수 있기 때문에 null로 초기화된 참조 변수는 스택 영역에 생성된다.
  • NullPointerException은 참조 변수를 사용하면서 가장 많이 발생하는 예외 중 하나다.
    변수가 null인 상태에서 객체의 데이터나 메소드를 사용하려할 때 발생한다.
  • heap 영역에 생성된 객체가 어떤 변수도 참조하고 있지 않으면 쓰레기 객체가 되고, 이것은 Garbage Collector에 의해 제거된다.

1) 문자열

자바에서 문자열은 String 객체로 생성된다.

  • 문자열 비교
    자바는 문자열 리터럴이 동일하다면 String 객체를 공유하도록 설계되어 있다.
    하지만 new연산자로 새로 객체를 만들면 리터릴이 같아도 다른 번지에 저장된다.
    따라서 문자열 리터럴로 생성하느냐 new연산자로 생성하느냐에 따라 결과가 달라질 수 있다.
// 문자열 리터럴로 생성하는 경우, 같은 번지 참조
String name1 = "홍길동";
String name2 = "홍길동";
// new 생성자로 생성하는 경우, 다른 번지 참조
String name3 = "아무개";
String name4 = "아무개";
  • 문자 추출 : charAt(인덱스번호)
    문자열에서 특정위치 문자를 얻는 메소드이다. 매개값으로 주어진 인덱스번호의 문자를 반환한다.
String subject = "자바 프로그래밍";
cahr charValue = subject.charAt(3); // 반환 값 '프'
  • 문자열 길이 : length()
    문자열에서 문자의 개수를 반환한다.(공백 포함)
String subject = "자바 프로그래밍";
int length = subject.length(); 	// 공백까지 카운트하여 8 반환
  • 문자열 대체 : replace(바꾸기전, 바뀐후)
    문자열에서 특정 문자열을 다른 문자로 대체할 때 사용한다.
    String 객체는 변경이 불가능한 특성을 갖기 때문에 replace()가 반환하는 문자열은 원래 문자열의 수정본이 아니라 완전히 새로운 문자열이다.
String oldStr = "자바 프로그래밍";
String newStr = oldStr.replace("자바","Java");  // "Java 프로그래밍" 반환
  • 문자열 슬라이싱 : substring(시작인덱스), substring(이상,미만)
    정수 한 개만 입력하면 시작 부분부터 끝까지 슬라이싱 해준다.
    정수 두 개를 입력하면 해당 구간을 슬라이싱 해준다.
String ssn = 880815-1234567;
String fristNum = ssn.substring(0,6);	// 880815
String secondNum = ssn.sustring(7);		// 1234567
  • 문자열 찾기 : indexOf("찾을문자열")
    문자열에서 특정 문자열의 위치를 찾을 때 사용한다.
    반환값은 문자열의 시작 인덱스이다.
    만약 문자열이 포함되어 있지 않으면 -1을 반환한다.
String subject = "자바 프로그래밍";
int index = subct.indexOf("프로그래밍");		// 3
  • 문자열 분리 : split("구분자")
    문자열이 구분자를 사용하여 여러 개의 문자열로 구성되어 있을 경우 분리할 때 사용한다.
String board = "번호,제목,내용,성명";
String[] arr = board.split(",");	// arr = [[번호],[제목],[내용],[성명]]

2) 배열

연속된 공간에 동질적인 자료형을 나열하고 인덱스를 부여한 자료구조이다.
자바에서 배열의 길이는 늘리거나 줄일 수 없다.

  • 배열의 선언
타입[] 배열명;	
// 참조할 배열이 없다면 null로 초기화할 수 있다.
타입[] 배열명 = null;

// 값 목록으로 배열 생성
타입[] 배열명 = {0,1,2 ... };

// 주의 사항 : 배열 선언 뒤 값 목록을 변수에 대입할 수 없다.
타입[] 배열명; 	// 선언
변수명 = {0,1,2 ...};	// 컴파일 에러
String[] names = null;
names = new String[] {"홍길동", "아무개" ...};

// new연산자로 배열 선언
타입[] 배열명 = new 타입[길이];	// new연산자로 생성하면 기본 값으로 초기화 된다.
//데이터 할당
배열명[인덱스번호] =;

기본 타입 배열을 new 연산자로 생성하면 기본 값으로 초기화 된다.
정수형은 0, 실수형은 0.0, 문자형은 '\u0000', boolean은 false, 참조형은 null이다.

  • 배열 길이(배열에 저장할 수 있는 항목 수) : length
    다차원배열인 경우 1차원 배열의 길이를 반환한다.
배열변수.length;	//배열 길이 반환

다차원 배열

배열의 요소로 또 다른 배열이 대입된 경우

// 값 목록으로 배열 생성
타입[][] 배열명 = {
	{1,2,3},	// 1차원 배열의 0인덱스
    {4,5,6}	// 1차원 배열의 1인덱스
};

// new연산자로 배열 선언 : 
타입[][] 배열명 = new 타입[1차원수][2차원수];

// 배열의 길이가 다른경우
int[][] scores = new int[2][]	// 1차원은 고정2개인데 2차원은 다름
scores[0] = new int[3];	// 1차원 인덱스번호 0에 3칸 배열 생성
scores[1] = new int[2];	// 1차원 인덱스번호 1에 2칸 배열 생성

객체를 참조하는 배열

기본타입 배열은 각 항목에 값을 직접 저장하지만, 참조 타입(클래스, 인스턴스) 배열은 각 항목에 객체의 번지를 저장한다.

배열 복사 : System.arraycopy(원본배열, 원본배열복사시작인덱스, 새 배열, 새 배열붙여넣기시작인덱스, 복사항목수)

배열은 한 번 생성하면 길이를 변경할 수 없다. 더 많은 저장 공간이 필요한 경우 더 큰 길이의 배열을 새로 만들고 이전 배열로 부터 항목들을 복사해야 한다.

// 원본 배열이 arr1이고 새 배열이 arr2일 경우 arr1의 모드 항목을 arr2로 복사하는 코드
System.arraycopy(arr1, 0, arr2, 0, arr1.length);

배열 항목 반복을 위한 향상된 for문

자바는 배열 및 컬렉션을 좀 더 쉽게 처리할 목적으로 향상된 for문을 제공한다.

for (타입변수 : 배열) {
	실행문
}	// 구조가 파이썬 반복문과 비슷

main()메소드 String[] args 매개변수의 용도

운영체제의 터미널에서 프로그램을 실행할 때는 요구하는 값이 있을 수 있다.
예를 들어 Java Sum 10 20이라는 덧셈 프로그램에서 10과 20은 문자열로 취급되며 String[] 배열의(그 배열의 이름은 args[]) 요소가 된다. 그리고 main 매서드 호출 시 매개값으로 전달된다. 각각 args[0]과 args[1]에 할당 되는 것이다.

3) 열거(Enum) 타입

데이터 중에는 몇 가지로 한정된 값을 갖는 경우가 있다. 예를 들어 요일은 월요일부터 일요일까지 7개 값을, 계절은 봄, 여름, 가을, 겨울 4개의 값을 갖는다. 그리고 이와 같이 한정된 값을 갖는 타입을 열거 타입(enumeration type)이라 한다.
열거 타입을 사용하기 위해서는 먼저 열거 타입 이름으로 소스 파일(.java)을 생성하고 한정된 값을 코드로 정의해야 한다. 열거 타입 이름은 첫 문자를 대문자로하는 캐멀 스타일로 지어주는 것이 관례다.

WeekDay.java

// 열거타입은 enum으로 선언
public enum WeekDay {	
	// 열거 상수 목록(열거 타입으로 사용할 수 있는 한정된 값)
    // 열거 상수는 관례상 언더바로 연결하는 스네이크 스타일로 지어주는 것이 관례다.
    MONDAY,		
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

// 열거 타입도 하나의 데이터 타입으므로 변수를 선언하고 사용해야 한다.
Week today;
Week reservationDay;
// 열거타입.열거상수 형태로 값을 대입한다.
Week today = Week.SUNDAY;

컴퓨터의 날짜 및 요일 시간 얻기 : Calendar

Calendar now = Calendar.getInstance();		// Calendar 객체 얻기
int year = now.get(Calendar.YEAR);			// 연
int month = now.get(Calendar.MONTH)+1;		// 월(1~12)
int day = now.get(Calendar.DAY_OF_MONTH);	// 일
int week = now.get(Calendar.DAY_OF_WEEK);	// 요일(1~7)
int hour = now.get(Calendar.HOUR);			// 시간
int minute = now.get(Calendar.MINUTE);		// 분
int second = now.get(Calendar.SECOND);		// 초

2. 클래스

1) 객체지향 프로그래밍

소프트웨어를 만들 때 부품에 해당하는 객체들을 먼저 만들고 이 객체들을 하나씩 조립해서 완성된 프로그램을 만드는 기법

  • 객체
    물리적으로 존재하거나 개념적인 것 중에서 다른 거과 식별 가능한 것이다.
    객체는 속성과 동작으로 구성된다. 자바에서는 속성을 필드, 동작을 메소드라고 부른다.
    현실 세계의 객체를 소프트웨어 객체로 설계하는 것을 객체 모델링이라고 한다.
  • 상호작용
    객체 지향 프로그램에서 객체들은 다른 객체와 서로 상호작용하면서 동작한다.
    객체들 사이의 상호작용 수단은 메소드이다. 객체가 다른 객체의 기능을 이용할 때 메소드를 호출한다.
  • 관계
    객체는 단독으로 존재할 수 있지만 대부분 다른 객체와 관계를 맺고 있다.
    (1) 집합 관계 : 완성품과 부품의 관계
    (2) 사용 관계 : 다른 객체의 필드를 읽고 변경하거나 메소드를 호출하는 관계
    (3) 상속 관계 : 부모-자식 관계
  • 객체 지향 프로그래밍 특징
    캡슐화, 상속, 다형성(큰 틀은 같은데 부품을 바꾸면 결과가 다양하게 나오는 성질)

2) 클래스

객체와 클래스

클래스는 객체를 생성할 때 필요한 설계도와 같은 것이다.
클래스의 용도는 크게 2가지 이다.

  • 라이브러리 클래스 : 실행할 수 없으며 다른 클래스에서 이용하는 클래스
  • 실행 클래스 : main() 메소드를 가지고 있는 실행 가능한 클래스

클래스의 구성

  • 필드 : 객체의 데이터(고유값, 상태)를 저장하는 역할
    (필드명은 첫문자 소문자인 캐멀스타일이 관례)
    초기값 미제공시 초기화 시 타입별 기본값을 보여준다.

  • 생성자 : new 연산자로 객체를 생성할 때 초기화 담당
    생성자가 성공적으로 실행되면 메모리 상의 객체 주소를 리턴한다.
    (오버로딩) 매개변수를 달리하는 생성자를 여러 개 선언하는 것
    생성자 오버로딩이 많아지는 경우 생성자 간의 중복코드가 발생할 수 있다.
    이런 경우 공통 코드는 한 생성자에게만 집중적으로 작성하고, 나머지 생성자는 this.(매개값..)를 사용하여 공통 코드를 가지고 있는 생성자를 호출하는 방법으로 개선할 수 있다.

    (오버라이팅) 상속받은 클래스에서 생성자 재정의 하는 것

  • 메소드 : 객체가 수행할 동작
    메소드는 호출 시 메소드 영역(스택 영역(LIFO))에 적재된다. 메소드는 코드 덩어리이므로 객체마다 저장하면 중복 저장으로 메모리 효율이 떨어진다. 따라서 메소드 코드는 메소드 영역에 두고 공유해서 사용한다. (단, 객체가 없으면 사용하지 못하도록 제한을 걸어둔다.)
    main메소드가 Static영역에서 실행되면서 가장먼저 스택 영역에 적재되고 필요한 메소드들을 호출하면서 스택을 쌓아가다가 모든 메소드가 종료되면 마지막에 main 메서드도 종료되면서 메모리 릴리즈 한다.
    (가변길이 매개변수) 매개변수가 가변적일 때 사용한다.

// 선언
int sum(int ... values) {
}

// 쉼표(,)로 구분해서 가변적으로 사용
int result = sum(1, 2, 3, 4 ...); 
int[] values = {1, 2, 3, 4 ...};
// 배열로도 삽입 가능
int result = sum(values); 		  
int result = sum(new int[] {1, 2, 3, 4 ...});

// 예제
public class Computer {
	// 가변길이 매개변수를 갖는 메소드 선언
    int sum(int ... values) {
    	// sum 변수 선언
        int sum = 0; 
        // values는 배열 타입의 변수처럼 사용
        for (int i = 0; i<values.length; i++) {
        	sum += values[i];
        }
        // 합산 결과 리턴
        return sum;
    }
}

인스턴스 멤버

인스턴스는 클래스라는 설계도로 실제로 메모리에 생성한 객체이다.(이때 객체는 heap 영역에 생성된다.) 필드와 메소드는 선언 방법에 따라 인스턴스 멤버와 정적 멤버로 분류될 수 있다. 인스턴스 멤버로 선언되면 객체 생성 할 수 있고 정적(static) 멤버로 선언되면 객체 생성 없이 사용할 수 있다. 인스턴스 멤버는 객체에 소속된 멤버이다. 따라서 객체가 있어야만 사용할 수 있다.

  • this 키워드
    객체 내부에서 인스턴스 멤버에 접근하기 위해서 this를 사용한다.
public class MinMax{
	// 필드 선언
	int max;
  	int min;
	// 메소드 선언
    public int sum(int max, int min){
        this.max = max;  	// this가 붙은건 클래스의 필드
        this.min = min;		// this가 안붙은건 매개변수로 받은 변수
        return this.max - this.min;	// 이름이 같을 때 구분하기위해 사용
    }
}

정적(Static) 멤버

  • 정적 멤버는 메소드 영역의 클래스에 고정적으로 위치하는 멤버이다. 그렇기 때문에 정적 멤버는 객체를 생성할 필요 없이 클래스가 메모리에 로딩되면 바로 사용할 수 있다.
  • 정적 필드는 클래스가 메모리에 로딩될 때 초기화 되고 생성자에 의해서는 초기화 되지 않는다.
  • 정적 메소드와 정적 블록은 객체가 없어도 실행된다는 특징 때문에 인스턴스 필드나 인스턴스 메소드를 사용할 수 없다. 또한 객체 자신의 참조인 this도 사용할 수 없다. (객체를 생성해야 사용할 수 있는 것들을 사용할 수 없다.)
  • 만약 정적 메소드와 정적 블록에서 인스턴스 멤버를 사용하고 싶다면 객체를 먼저 생성하고 참조 변수로 접근해야 한다. (main()메소드도 정적 메소드이므로 마찬가지이다.)

final 필드와 상수

인스턴스 필드와 정적 필드는 언제든지 값을 변경할 수 있다. 그러나 경우에 따라서는 값을 변경하는 것을 막고 읽기만 허용해야 할 때가 있다. 이때 final 필드와 상수를 선언해서 사용한다.

  • final 선언
    final 필드는 초기값이 저장되면 이것이 최종적인 값이 되어서 프로그램 실행 도중에 수정할 수 없게 된다.
// 필드 선언 시 초기값 대입
final 타입 필드명 = 초기값;

// 생성자로 초기값 대입
public class Korean {
	final nation;
	public Korean(String nation) {
    	this.nation = nation;	// 매개변수로 들어오는게 초기값
    }
}
  • 상수 선언
    원주율과 같은 불변값을 저장하는 필드를 상수 필드라고 한다.
    상수는 객체마다 저장할 필요가 없고, 여러 개의 값을 가져도 안되기 때문에 Static이면서 final인 특성을 가져야 한다.

초기 값은 선언 시에 주는 것이 일반적이지만, 복잡한 초기화가 필요한 경우에는 정적 블록에서 초기화 할 수 있다. (상수명은 모두 대문자로 하는 것이 관례이다.)

// 선언시 초기값 대입
static final 타입 상수명 = 초기값;

// static block에서 대입
static final 상수명;
static {
	상수명 = 초기값;
}

3) 패키지

패키지는 클래스를 식별하는 용도로 사용된다.

패키지 선언

// 패키지 선언
pacakge 상위패키지.하위패키지;

public class 클래스명 {
	...
}

패키지 이름은 모두 소문자로 작성하는 것이 관례이다.
패키지 이름이 서로 중복되지 않도록 회사 도메인 이름의 역순으로 작성하고
마지막에는 프로젝트 이름을 붙여주는 것이 일반적이다.

ex) com.samsung.projectname
ex) com.lg.proejectname

import 문

같은 패키지에 있는 클래스는 아무런 조건 없이 사용할 수 있지만, 다른 패키지에 있는 클래스를 사용하기 위해서는 import문을 통해 어떤 패키지의 클래스를 사용할 것인지 명시해야 한다.
또한 import 문은 하위 패키지를 포함하지 않기 때문에 하위 패키지도 다 import 해야한다.

import com.hankook.*;	// *는 모두 가져오겠다는 의미
import com.hankook.projectname.*;

만약 서로 다른 패키지에 동일한 클래스이름이 존재한다면 풀네임으로 어떤 패키지의 클래스인지 명시해줘야 컴파일 에러가 발생하지 않는다.

접근 제한자(Access Modifier)

  • public : 모든 패키지에서 아무 제약없이 접근 가능하다.
    (클래스는 다른 곳에서 사용하려고 만든거니까 public많이 쓰고 클래스 내부 멤버 메서드도
    public을 많이 쓴다. 그러나 멤버 변수는 private을 쓰기도 한다.)
  • private : 모든 패키지에서 접근 불가, 객체 내부에서만 접근 가능하다.
  • protected : 같은 패키지 또는 상속 관계에서 하위 클래스에서 상위 클래스 접근 가능하다.
  • default : 동일한 패키지에서만 접근 가능(접근제한자가 붙지 않은 상태 의미)

Getter와 Setter

객체의 필드(데이터)를 외부에서 마음대로 읽고 변경할 경우 객체의 무결성을 저해할 수 있다. 따라서 외부에서 필드 접근을 막고 대신 메소드를 통해 필드에 접근하는 것이 선호된다. 이때 사용하는 것이 Getter()와 Setter() 메소드이다.

  • Setter() : 해당 필드값 변환
  • Getter() : 해당 필드값 반환
  • toString() : getter로 일일이 하나씩 가져오기 불편할 때 필드 값 한번에 반환
  • 생성자 이용 객체 초기화 : 생성자로 필드에 값을 삽입

싱글톤 패턴

  • 애플리케이션 전체에서 단 한개의 객체만 생성해서 사용하고 싶을 때 사용한다.
  • 생성자를 private 접근 제한해서 외부에서 new 연산자로 생성자를 호출할 수 없도록 막는 것이 핵심이다.
  • 생성자를 호출할 수 없으니 외부에서 마음대로 객체를 생성하는 것이 불가능하다.
  • 대신 싱글톤 패턴이 제공하는 정적 메소드를 통해 간접적으로 객체를 얻는다.
public class 클래스 {
	// private 접근 권한을 갖는 정적 필드 선언과 초기화
    private static 클래스 singleton = new 클래스();	// 자신의 타입으로 정적 필드 선언
   
    // pricate 접근 권한을 갖는 생성자 선언
    pricate 클래스() {}
	
	// public 접근 권한을 갖는 정적 메소드 선언	
    public static 클래스 getInstance() {		// 정적 필드값을 리턴하는 메소드
    	return singleton;
    }
}

3. 상속(Inheritance)

상속은 중복 코드를 줄여준다. 이미 잘 개발된 클래스를 재사용할 수 있기 때문이다. 상속의 또 다른 장점은 클래스의 수정을 최소화할 수 있다는 것이다. 부모 클래스를 수정하면 상속 받은 자식클래스들은 중복되는 코드는 따로 수정하지 않아도 된다.

현실에서는 부모가 자식을 선택하지만, 프로그램에서는 자식이 부모를 선택하여 상속 받는다.
또한, 다른 언어와 달리 자바는 다중 상속을 허용하지 않는다.(자식당 부모클래스 1개만 가능)

public class 자식클래스 extends 부모클래스{
}

1) 상속관계

부모 생성자 호출

현실에서 부모없는 자식이 있을 수 없듯이 자바에서도 자식 객체를 생성하면 부모 객체가 먼저 생성된 다음 자식 객체가 생성된다. (컴파일 과정에서 super()메소드가 자식 생성자 맨 첫줄에 삽입된다.)
그러나 super()는 부모 클래스의 기본 생성자만 호출할 수 있다. 만약 부모 클래스의 기본 생성자가 없고, 매개변수를 갖는 생성자만 있다면 super(매개값, ...); 코드를 자식 생성자의 첫줄에 직접 넣어야한다.

메소드 재정의(오버라이팅)

물려 받은 부모 클래스의 메소드를 자식 클래스에 맞게 덮어 쓰는 작업이다.
오버라이팅되면 부모 메소드는 숨겨지고, 자식 메소드가 우선 사용된다.

주의사항

  • 부모 메소드의 선언부(리턴타입, 메소드이름, 매개변수)와 동일해야한다.
  • 접근 제한을 더 강하게 오버라이딩 할 수 없다(public -> private 불가)
  • 새로운 예외를 throws할 수 없다.

자바는 개발자의 실수를 줄이기 위해 정확히 오버라이딩 되었는지 확인하는 @Override 어노테이션을 제공한다. @Overried를 붙이면 컴파일 단계에서 정확히 오버라이딩 되었는지 체크하고, 문제가 있다면 컴파일 에러를 출력해준다.

부모 메소드 호출

오버라이딩 하면 부모가 100줄 코드를 가지고 있고 자식은 거기에 1줄만 추가하고 싶어도
자식클래스에서 부모 코드의 100줄을 다시 작성해야 한다.
이 문제는 자식 메소드와 부모 메소드의 공동 작업 처리 기법으로 해결할 수 있다.
자식 메소드 내에서 부모 메소드를 호출하는 것이다.

class Parent {
	public void 메소드() {
    	작업코드1
    }    
}

class Child extends Parent {
	public void 메소드() {
		super.메소드();
		작업코드2
	}
}

final 클래스와 final 메소드

final 필드는 초기값 설정 후 값을 변경할 수 없는 필드가 된다.

final 타입 필드명 = 초기값;

final 클래스는 더 이상 상속할 수 없는 클래스가 된다.

public final class 클래스명 {
}

final 메소드는 더 이상 오버라이딩 할 수 없는 메소드가 된다.

public final 리턴타입 메소드명(매개변수...) {
}

2) 다형성

클래스의 타입 변환

클래스의 타입 변환은 상속 관계에 있는 클래스 사이에서 발생한다.

  • 자동 타입 변환(promotion)
    자식타입의 객체를 부모타입의 변수에 대입할 때 자동으로 타입 변환 되는 것이다. 또한 바로 위의 부모가 아니더라도 상속 계층에서 상위 타입이라면 자동 타입변환 가능하다.
    예를 들어 동물(부모) 변수에 고양이(자식)객체를 넣어도 대입이 된다는 것이다. 이때 자식 객체와 부모 변수는 타입만 다를 뿐 힙 영역에 동일한 인스턴스를 가리키게 된다.
    단 부모 타입으로 자동 타입 변환 된 이후에는 부모 클래스에 선언된 필드와 메소드에만 접근 가능하다.
    주의! 변수는 자식 인스턴스를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로 한정된다. 그러나 자식 클래스에서 오버라이딩된 메소드가 있다면 부모 메소드 대신 오버라이딩된 메소드가 호출된다. 이것은 다형성과 관련이 있기 때문에 꼭 알아두어야 한다.
Cat cat = new Cat();
Animal animal = cat;	// Cat 타입이 자동으로 Animal 타입으로 변환된다.
  • 강제 타입 변환(Casting)
    자식타입은 부모타입으로 자동 타입 변환되지만, 부모타입은 자식타입으로 자동 타입 변환 되지 않는다. 따라서 캐스팅 연산자로 강제 타입 변환 시켜줘야한다.
    그렇다고해서 부모 타입 객체를 자식 타입으로 무조건 강제 변환할 수 있는 것도 아니다. 자식 객체가 부모 타입으로 먼저 자동 변환된 후 다시 자식타입으로 변환할 때 강제 타입 변환을 사용할 수 있는 것이다.
    부모타입으로 변환된 자식객체가 다시 자식 타입으로 선언된 필드와 메소드를 쓰고 싶을 때, 강제 타입 변환을 해서 자식객체에 선언된 필드와 메소드를 사용한다.
자식타입 변수명 = (자식타입) 부모타입객체;

Parent parent = new Child();	// 자동 타입 변환
Child child = (Child) parent;	// 강제 타입 변환

다형성

다형성이란 사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질이다. 프로그램의 객체는 부품과 같아서, 프로그램을 구성하는 객체를 바꾸면 프로그램의 실행 성능이 다르게 나올수 있다.
이때 객체 사용 방법이 동일하다는 것은 동일한 메소드를 가지고 있다는 뜻이다.

(예시) 타이어를 상속하는 한국타이어와 금호타이어가 클래스가 있다고 가정한다. 두 타이어는 같은 부모클래스를 상속하므로 부모의 메소드를 동일하게 가지고 있다고 할 수 있다. 만약 한국타이어와 금호타이어가 부모 메소드를 오버라이딩하고 있다면 두 타이어가 다르기 때문에 실행 결과도 다르게 나온다.(오버라이딩 안해도 사용하는 필드가 다르면 다르게 나온다.) 이것이 다형성이다.

다형성을 구현하기 위해서는 자동 타입변환과 메소드 재정의가 필요하다.

  • 필드 다형성
    필드 다형성은 필드 타입은 동일하지만, 대입되는 객체가 달라져서 실행 결과가 다양하게 나올 수 있는 것을 말한다.

  • 매개변수 다형성
    참조형 매개변수는 메소드 호출시 자신과 동일한 타입 or 자식 타입의 인스턴스를 넘겨줄 수 있다.자식 타입의 인스턴스를 넘겨줄 수 있는 이유는 '자동 타입 변환' 덕분이다.
    자식 타입의 인스턴스가 왔고 자식 클래스가 부모 클래스의 메소드를 오버라이딩하고 있다면 메소드 호출시 오버라이딩 된 자식 클래스의 메소드가 실행된다.
    따라서 매개변수로 무엇이 들어오느냐에 따라 실행 결과가 다양하게 나올 수 있는 것이다.

객체 타입 확인 : 객체 instanceof 타입

어떤 객체가 매개변수로 들어오는지에 따라 결과가 다양하게 바뀐다면, 그 객체를 확인할 필요가 있다.(반드시 매개변수가 아니더라도 변수가 참조하는 객체의 타입을 확인할 때 instanceof 연산자를 사용한다.)

boolean result = 객체 instanceof 타입

// Java 11 까지
public void method(Parent parent) {
	if(parent instanceof Child) {
    	Child A = (Child) parent;	// 강제 타입 변환
        // A 변수 사용 가능
    }
}

// Java 12 부터 우측타입 변수 사용할 수 있어 강제 타입 변환이 필요없다.
	if(parent instaceof Child A) {
    	// A 변수 사용 가능
    }

3)추상 클래스

사전적 의미로 추상은 실체 간에 공통되는 특성을 추출한 것이다.

추상 클래스

객체를 생성할 수 있는 클래스를 실체 클래스라고 한다면, 이 클래스들의 공통적인 필드나 메소드를 추출해서 선언한 클래스를 추상 클래스라고 한다.

추상 클래스는 실체 클래스들의 부모 역할을 한다. 따라서 실체 클래스는 추상 클래스를 상속해서 공통적인 필드나 메소드를 물려받을 수 있다.

단, 추상 클래스는 새로운 실체 클래스를 만들기 위한 부모 클래스로만 사용되기 때문에 new 연산자를 사용하여 객체를 직접 생성할 수 없다.

public abstract class 추상클래스명 {
	// 필드
    // 생성자
    // 메소드
}

추상 매소드

자식 클래스들이 가지고 있는 공통 메소드를 뽑아내어 추상 클래스로 작성할 때, 메소드 선언부(리턴타입, 메소드명, 매개변수)만 동일하고 실행 내용은 자식 클래스마다 달라야하는 경우가 많다.

이런 경우 추상 클래스는 추상 메소드를 선언할 수 있다. 일반 메소드와 차이점은 abstract 키워드가 붙고, 메소드 실행 내용인 중괄호 {}가 없다는 것이다.

실행내용이 없기 때문에 자식 클래스에서 반드시 오버라이딩 해야한다.

abstract 리턴타입 메소드명(매개변수, ...);

봉인된 클래스(sealed class)

기본적으로 final 클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있다. 그러나 Java 15 부터는 무분별한 자식 클래스 생성을 방지하기 위해 봉인된(sealed) 클래스가 도입되었다.

예를 들어 Person의 자식 클래스는 Employee와 Manager만 가능하고, 그 이외에는 자식 클래스가 될 수 없도록 할 수 있다.

public sealed class Person permits Employee, Manager{...}

봉인된 클래스를 상속받는 클래스는 final, 또다른 sealed, non-sealed 키워드를 사용해 선언해야 한다.

public sealed class Employee extends Person {...}
public non-sealed class Manager extends Person {...}
// non-sealed는 봉인을 해제하는 키워드
public class Director extends Manager{...}

4. 인터페이스

인터페이스는 사전적 의미로 두 장치를 연결하는 접속기다. 여기서 두 장치를 서로 다른 객체로 본다면, 인터페이스는 두 객체를 연결하는 역할을 한다고 할 수 있다.

예를 들어 객체 A가 인터페이스 메소드를 호출하면, 인터페이스는 객체 B의 메소드를 호출하고 그 결과를 받아 객체 A에게 반환한다.

객체 A가 객체 B의 메소드를 직접 호출하면 간단할 텐데 왜 중간에 인터페이스를 두는 것일까?
객체 A가 객체B를 직접사용하는 경우 다형성이 떨어질 수 있기 때문이다.

예를 들어 객체 A가 객체 B를 직접 사용하다가 객체 A가 객체 C를 사용하고 싶다면, 객체 A의 소스코드를 객체 C로 변경해야한다. 그러나 인터페이스 메소드를 호출하게 되면 인터페이스에 코드를 수정해서 간단하게 처리할 수 있다.

인터페이스 선언

인터페이스 선언은 class 키워드 대신 interface 키워드를 사용한다.
중괄호 안에는 인터페이스가 가지는 멤버들을 선언할 수 있다.

public interface 인터페이스명 {...}
	// public 상수 필드
    // public 추상 메소드
    // public 디폴트 메소드
    // public 정적 메소드
    // private 메소드
    // private 정적 메소드 

인터페이스 생성

  • Eclipse : New - Interface
  • Intelli J : New - Java Class - Interface

구현 클래스 선언

객체 A가 인터페이스의 추상 메소드를 호출하면 인터페이스는 객체 B의 메소드를 실행한다. 그렇다면 객체 B는 인터페이스에 선언된 추상 메소드와 동일한 선언부를 가진 (재정의된) 메소드를 가지고 있어야 한다. 여기서 객체 B를 인터페이스를 구현한(implement) 객체 라고 한다.

public class B implements 인터페이스명 {...}

implements 키워드는 해당 클래스가 인터페이스를 사용할 수 있다는 표시이며, 인터페이스의 추상 메소드를 재정의한 메소드가 있다는 뜻이다.

변수 선언과 구현 객체 대입

인터페이스도 하나의 object이므로 변수 타입으로 사용할 수 있다. 인터페이스는 참조 타입에 속하므로 인터페이스 변수에는 객체를 참조하고 있지 않다는 뜻으로 null을 대입할 수 있다.

만약 클래스가 implements 인터페이스명으로 선언되어 있지 않다면, 인터페이스 타입의 변수에 대입할 수 없다.

인터페이스 변수를 통해 메소드가 호출되면, 실제로 실행되는 것은 인터페이스를 구현한 객체의 메소드이다.

// 인터페이스 선언
public interface ex07_RemoteControl {
    //public 추상 메소드
    public void turnOn();
}

// 구현클래스 선언
public class ex07_Television implements ex07_RemoteControl {
    @Override
    public void turnOn() {
        System.out.println("TV를 켭니다.");
    }
}

// 메인에서 인터페이스 변수에 메소드 호출을 넣으면 구현 클래스 메소드를 반환
public class ex07_InterFaceExample {
    public static void main(String[] args) {
        ex07_RemoteControl rc ;         // 인터페이스 객체 선언
        rc = new ex07_Television();     // 인터페이스 객체에 대입
        rc.turnOn();                    // 인터페이스 객체에 메소드 호출
        }
}

// 출력결과 : TV를 켭니다.

인터페이스와 상수 필드

인터페이스는 public static final 특성을 갖는 불변의 상수 필드를 멤버로 가질 수 있다.
인터페이스에 선언된 필드는 모두 public static final 특성을 갖기 때문에 public static final을 생략하더라도 자동적으로 컴파일 과정에서 붙는다.

상수는 구현 객체와 관련 없는 인터페이스 소속 멤버이므로 인터페이스로 바로 접근해서 상수 값을 읽을 수 있다.

System.out.println(인터페이스명.상수필드명);

인터페이스와 추상 메소드

인터페이스는 구현 클래스가 재정의해야하는 public 추상 메ㅗ드를 맴버로 가질 수 있다.
구현 클래스에서 인터페이스의 추상 메소드를 재정의할 때 주의점은 인터페이스의 추상 메소드는 기본적으로 public 접근 제한자를 갖기 때문에 public 보다 더 강한 접근 제한으로 재정의 할 수 없다.

인터페이스와 디폴트 메소드

인터페이스에 완전한 실행 코드를 가진 디폴트 메소드를 선언할 수 있다. (추상 메소드는 실행부{}가 없지만 디폴트 메소드는 있다.)
선언 방법은 클래스 메소드와 동일하지만 차이점은 defualt 키워드가 리턴타입 앞에 붇는다는 것이다.

[public] default 리턴타입 메소드명(매개변수, ...) {...}

디폴트 메소드가 실행코드를 포함하고 있을지라도 이 메소드를 호출하기 위해서는 인터페이스 변수에 구현된 인스턴스 객체가 할당되어야 한다.

또한 디폴테 메소드는 구현 객체에서 오버라이딩 할 수 있다. 이때 주의할 점은 접근 제한자 public을 반드시 붙여햐하고 default 키워드를 지워야한다.

인터페이스와 정적 메소드

인터페이스에 정적 메소드도 선언할 수 있다. 추상 메소드와 디폴트 메소드는 구현 객체가 필요하지만, 정적 메소드는 구현 객체가 없어도 인터페이스만으로 호출할 수 있다.

// 인터페이스 정적 메소드 선언
 [public | private] static 리턴타입 메소드명(매개변수, ...) {...} 

// 인터페이스 정적 메소드 호출
 인터페이스명.메소드명();

인터페이스와 private 메소드

인터페이스의 상수 필드, 추상 메소드, 디폴트 메소드, 정적 메소드는 모두 public 접근 제한을 갖는다. 이 멤버들을 선언할 때에는 public을 생략하더라도 컴파일 과정에서 public 접근 제한자가 붙어 항상 외부에서 접근이 가능하다.
그러나 private 접근 제어자를 선언할 수 도 있다. private 메소드의 용도는 디폴트와 정적 메소드들의 중복 코드를 줄이는 것이다. 디폴트 메소드의 중복 코드 부분을 private 메소드로, 정적 메소드의 중복 코드 부분을 private 정적 메소드로 선언하여 코드의 중복을 줄일 수 있다.

다중 인터페이스 구현

구현 객체는 여러 개의 인터페이스를 implements할 수 있다. 구현 객체가 인터페이스 A와 인터페이스 B를 구현하고 있다면, 각각의 인터페이스를 통해 구현 객체를 사용할 수 있다.

구현 클래스는 다음과 같이 인터페이스 A와 인터페이스 B를 implements 뒤에 쉼표로 구분해서 작성해, 모든 인터페이스가 가진 추상 메소드를 재정의 해야한다.

public class 구현클래스명 implements 인터페이스A, 인터페이스B {
	// 모든 추상 메소드 재정의 
}

구현 객체가 어떤 인터페이스 변수에 대입되느냐에 따라 변수를 통해 호출할 수 있는 추상 메소드가 결정된다.

    인터페이스A 변수명 = new 구현클래스명(...);
    인터페이스B 변수명 = new 구현클래스명(...);

인터페이스 상속

인터페이스도 다른 인터페이스를 상속할 수 있으며, 클래스와는 달리 다중 상속을 허용한다.

public interface 자식인터페이스 extends 부모인터페이스1, 부모인터페이스2 {...}

주의! 자식 인터페이스의 구현 클래스는 자식 인터페이스의 메소드뿐만 아니라 부모 인터페이스의 모든 추상 메소드를 재정의해야 한다. 그리고 구현 객체는 자식 및 부모 인터페이스 변수에 대입될 수 있다.

	자식인터페이스 변수명 = new 구현클래스(...);
    부모인터페이스1 변수명 = new 구현클래스(...);
    부모인터페이스2 변수명 = new 구현클래스(...);

구현 객체가 자식인터페이스 변수에 대입되면 자식 및 부모 인터페이스의 추상 메소드를 모두 호출할 수 있다. 그러나 부모인터페이스 변수에 대입되면 해당 부모 인터페이스에 선언된 추상 메소드만 호출 가능하다.

인터페이스 타입 변환

인터페이스의 타입 변환은 인터페이스와 구현 클래스 간에 발생한다. 인터페이스 변수에 구현 객체를 대입하면 구현 객체는 인터페이스 타입으로 자동 타입 변환 된다. 반대로 인터페이스 타입을 구현 클래스 타입으로 변환시킬 수 있는데, 이때는 강제 타입 변환이 필요하다.

  • 자동 타입 변환
    부모 클래스가 인터페이스를 구현하고 있다면, 자식 클래스도 인터페이스 타입으로 자동 타입 변환될 수 있다.

  • 강제 타입 변환
    강제타입 변환은 캐스팅 기호를 사용해서 인터페이스 타입을 구현 클래스 타입으로 변환시키는 것이다.

구현클래스 변수명 = (구현클래스) 인터페이스변수;

구현 객체가 인터페이스 타입으로 자동 변환되면, 인터페이스에 선언된 메소드만 사용 가능하다. 만약 구현 객체에서 정의한 메소드를 다시 사용하고 싶다면 강제 타입 변환으로 구현 객체로 돌아가야한다.

인터페이스 다형성

현업에서는 상속보다 인터페이스를 통해 다형성을 구현하는 경우가 더 많다. 상속의 다형성과 마찬가지로 인터페이스 역시 다형성을 구현하기 위해 재정의와 자동 타이 변환 기능을 이용한다. 인터페이스의 추상 메소드는 구현 클래스에서 재정의 해야하며, 재정의되는 내용은 구현 클래스마다 다르다. 구현 객체는 인터페이스 타입으로 자동 타입 변환되고, 인터페이스 메소드 호출 시 구현 객체의 재정의된 메소드가 호출되어 다양한 실행 결과를 얻을 수 있다.

인터페이스 객체 타입 확인 : instanceOf

상속에서 객체타입을 확인하는 것과 같은 방식으로 instanceOf 연산자를 사용한다.

봉인된 인터페이스

봉인된 클래스와 마찬가지로 sealed와 permits을 사용하여 봉인한다.


5. 중첩 선언과 익명 개체

1) 중첩 클래스(Nested Class)

중첩 클래스란 클래스 내부에 선언한 클래스를 말한다. 클래스가 여러 클래스와 관계를 맺는 경우 독립적으로 선언하는 것이 좋으나, 특정 클래스만 관계를 맺을 경우 중첩 클래스로 선언하는 것이 유지 보수에 도움이 되는 경우가 많다.

중첩 클래스를 사용하면 클래스의 맴버를 쉽게 사용할 수 있고 외부에는 중첩 관계를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점이 있다.

중첩 클래스의 종류

  • 멤버 클래스 : 클래스의 멤버로서 선언되는 중첩 클래스
  • 로컬 클래스 : 메소드 내부에 선언되는 중첩 클래스

중첩 클래스도 하나의 클래스이기 때문에 컴파일하면 바이트코드 파일(.class)이 별도로 생성된다. 바이트 코드의 이름은 다음과 같이 결정된다.

바깥클래스명 $ 멤버클래스명.class
바깥클래스명 $1 로컬클레스명.class

멤버 클래스

  • 인스턴스 멤버 클래스
    인스턴시 멤버 클래스는 다음과 같이 A클래스의 멤버로 선언된 B 클래스를 말한다.
    인스턴스 멤버 클래스 B는 주로 클래스 A 내부에서 사용되므로 private 접근 제한을 갖는 것이 일반적이다. 객체 B는 클래스 A 내부 어디에서나 생성할 수는 없고, 인스턴스 필드값, 생성자, 인스턴스 메소드에서 생성할 수 있다. 객체 A가 있어야 객체 B도 생성할 수 있기 때문이다.
[public] class A {
	[public | private] class B {
    }
}
  • 정적 멤버 클래스
    정적 멤버 클래스는 static 키워드와 함께 A 클래스의 멤버로 선언된 B 클래스를 말한다. 정적 멤버 클래스B는 A 클래스 내부에서 사용되기도 하지만, A 클래스 외부에서 A와 함께 사용되는 경우가 더 많기 때문에 주로 default 또는 public 접근 제한을 가진다. B 객체는 A 클래스 내부 어디든 객체를 생성할 수 있다. (static이니까)
[public] class A {
	[public | private] static class B {
    }
}

로컬 클래스

생성자 또는 메소드 내부에서 선언된 클래스이다. 로컬 클래스는 몇가지 특징이 있다.

  • 생성자와 메소드가 실행도리 동안에만 객체를 생성할 수 있다.
  • 로컬 클래스 내부에는 필드, 생성자, 메소드 선언이 올 수 있다.
    (정적 필드와 정적 메소드는 Java 17부터 가능하다.)
  • 로컬 변수를 로컬 클래스에서 사용할 경우 로컬 변수는 final의 특성을 갖게 된다.
    (Java 8 이후부터는 명시적으로 final 키워드를 붙이지 않아도 되지만 붙여주는게 좋다.)

바깥 멤버 접근

중첩 클래스는 바깥 클래스와 긴밀한 관계를 맺으면서 바깥 클래스의 멤버(필드, 메소드)에 접근할 수 있다. 하지만 중첩 클래스가 어떻게 선언되었느냐에 따라 접근 제한이 있을 수 있다.

  • 인스턴스 멤버 클래스 : 바깥 클래스의 모든 필드, 메소드에 접근 가능
  • 정적 멤버 클래스 : 바깥 클래스의 정적 필드, 정적 메소드에 접근 가능
    (정적 멤버 클래스는 바깥 객체가 없어도 사용 가능해야 하므로 바깥 클래스의 인스턴스 필드와 인스턴스 메소드는 사용하지 못한다.)
// 바깥 멤버 접근
바깥클래스이름.this.변수명|메소드명

// 자기 멤버 접근
this.변수명|메소드명

중첩 인터페이스

중첩 인터페이스는 클래스의 멤버로 선언된 인터페이스를 말한다. 인터페이스를 클래스 내부에 선언하는 이유는 해당 클래스와 긴밀하게 관계를 맺는 구현 객체를 만들기 위해서다.

class A {
	[public | private] [static] interface B {
	// 상수 필드
    // 추상 메소드
    // 디폴트 메소드
    // 정적메소드
    }
}

객체 A없이 인터페이스를 사용하기 위해서 static 키워드를 추가할 수 있다.


2) 익명 객체(anonymous)

익명 객체는 이름이 없는 객체를 말한다. 명시적으로 클래스를 선언하지 않기 때문에 쉽게 객체를 생성할 수 있다. 주로 필드값, 로컬변수값, 매개변수값으로 사용된다.

익명 객체는 클래스를 상속하거나 인터페이스를 구현해야만 생성할 수 있다. 클래스를 상속해서 만들 경우 익명 자식 객체라고 한다.인터페이스를 구현해서 만들 경우 익명 구현 객체라고 한다.

익명 자식 객체

익명 자식 객체는 부모 클래스를 상속받아 다음과 같이 생성된다. 이렇게 생성된 객체는 부모 타입의 필드, 로컬변수, 매개변수의 값으로 대입할 수 있다.

new 부모생성자(매개값, ...) {
	// 필드
    // 메소드
}

{}중괄호 안의 필드와 메서드는 중괄호 안에서만 쓰고 버린다. 주로 부모 메소드를 재정의하는 코드가 온다.

익명 구현 객체

익명 구현 객체는 인터페이스를 구현해서 다음과 같이 생성된다. 이렇게 생성된 객체는 인터페이스 타입의 필드, 로컬변수, 매개변수의 값으로 대입할 수 있다.

new 인터페이스() {
	//필드
    //메소드
}

6. 라이브러리와 모듈

1) 라이브러리

라이브러리는 프로그램 개발 시 활용할 수 있는 클래스와 인터페이스들을 모아놓은 것이다.
일반적으로 JAR(Java ARchive)압축파일 형태로 존재한다. JAR 파일에는 클래스와 인터페이스의 바이트 코드 파일(~.class)들이 압축되어 있다.

특정 클래스와 인터페이스가 여러 응용프로그램을 개발할 때 공통으로 자주 사용된다면 JAR 파일로 압축해서 라이브러리로 관리하는 것이 좋다. (참고 : 이클립스는 JavaProject를 생성해서 클래스와 인터페이스를 개발하고 최종 산출물로 JAR 파일을 만드는 기능이 있다.)

프로그램 개발 시 라이브러리를 사용하려면 라이브러리 JAR파일을 ClassPath에 추가해야한다. ClassPath란 말 그대로 클래스를 찾기 위한 경로이다. ClassPath에 라이브러리를 추가하는 방법은 다음과 같다.

  • 콘솔에서 프로그램을 실행할 경우
    java 명령어를 실행할 때 -classpath로 제공
    CLASSPATH 환경 변수에 경로를 추가

  • 이클립스 프로젝트에서 실행할 경우
    프로젝트의 Build Path에 추가


2) 모듈

Java 9부터 지원하는 모듈은 패키지 관리 기능까지 포함한 라이브러리다. 일반 라이브러리는 내부에 포함된 모든 패키지에 외부 프로그램에서 접근이 가능하지만, 모듈은 일부 패키지를 은닉하여 접근할 수 없게끔 할 수 있다.

또한 모듈은 자신이 실행할 때 필요로하는 의존 모듈을 모듈 기술자(module-info.java)에 기술할 수 있기 때문에 모듈 간의 의존 관계를 쉽게 파악할 수 있다.

모듈도 라이브러리이므로 JAR파일 형태로 배포할 수 있다. 응용프로그램을 개발할 때 원하는 기능을 모듈(JAR) 파일을 다운로드해서 이용하면 된다.

대규모 응용프로그램은 기능별로 모듈화(modulization)해서 개발할 수도 있다. 모듈별로 개발하고 조립하는 방식을 사용하면 재사용성 및 유지보수에 유리하다.

응용프로그램 모듈화

응용프로그램은 하나의 프로젝트로도 개발 가능하지만, 이것을 기능별로 서브 프로젝트(모듈)로 쪼갠 다음 조합해서 개발할 수도 있다. 응용프로그램의 규모가 커질수록 협업과 유지보수 측면에서 서브 모듈로 쪼개서 개발하는 것이 유리하며, 이렇게 개발된 모듈들은 다른 응용프로그램에서도 재사용 가능하다.

모듈 생성

  • 모듈 소스 코드 작성
  • [마우스 오른쪽] - [Export] - [Java] - [JAR file] - [src폴더만 체크]
// 의존성 설정 module-info.java
module 모듈명 {
	requires 모듈명
}

패키지 은닉

모듈은 모듈 기술자(module-info.java)에서 export 키워드를 사용해 내부 패키지 중 외부에서 사용할 패키지를 지정한다. export되지 않은 패키지는 자동적으로 은닉된다.

// module-info.java
module 모듈명 {
	exprot 패키지명;
}

전이 의존

ㄱ이라는 모듈이 a, b라는 두 모듈에 의존하고 있다가
ㄱ이 a를 a가 b를 의존하는 형태로 바뀐 경우,
ㄱ이 b의 요소들을 더 이상 쓸 수 없어 컴파일오류가 발생할 수 있다.
이런 경우 중간에 있는 a모듈에 trasitive 키워드 설정해 해결할 수 있다.

// 의존성 설정 module-info.java
module 모듈명 {
	requires transitive 모듈명
}

집합 모듈

집합 모듈은 여러 모듈을 모아놓은 모듈을 말한다. 자주 사용되는 모듈들을 일일이 requires하는 번거로움을 피하고 싶을 때 집합 모듈을 생성하면 편리하다. 집합 모듈은 자체적인 패키지를 가지지 않고, 모듈 기술자에 전이 의존 설정만 한다.
(집합 모듈이 여러 모듈들에게 의존하고 그것을 요구하는 모듈에게 넘겨주는 매커니즘인듯)

module 집합모듈명 {
	requires transitive 모듈1
    requires transitive 모듈2
}

리플렉션 허용

은닉된 패키지는 기본적으로 다른 모듈에 의해 리플랙션을 허용하지 않는다. 리플렉션이란 실행 도중에 타입(클래스, 인터페이스 등)을 검사하고 구성 멤버를 조사하는 것을 말한다. 경우에 따라서는 은닉된 패키지도 리플랙션을 허용해야할 때가 있다.

// 모듈 전체를 리플렉션 허용
open module 모듈명 {
	...
}

// 지정된 패키지에 대해 리플렉션 허용
module 모듈명 {
	opens 패키지1;
    opens 패키지2;
}

// 지정된 패키지에 대해 특정 외부 모듈에서만 리플렉션 허용
module 모듈명 {
	opens 패키지1 to 외부모듈명, 외무보듈명, ...;
    opens 패키지2 to 외부모둘명;
}

7. 예외 처리

에러는 컴퓨터 하드웨어 고장으로 인해 응용프로그램 실행 오류가 발생하는 것이고, 예외는 잘못된 사용 또는 코딩으로 인한 오류이다. 예외가 발생되면 프로그램이 종료되지만 예외처리를 통해 실행상태를 유지할 수 있다.

  • 일반 예외(Exception)
    컴파일러가 예외 처리 코드 여부를 검사하는 예외

  • 실행 예외(Runtime Exception)
    컴파일러가 예외 처리 코드 여부를 검사하지 않는 예외

자바는 예외가 발생하면 예외 클래스로부터 객체를 생성한다. 이 객체는 예외 처리 시 사용한다. 자바의 모든 에러와 예외 클래스는 Throwable을 상속받아 만들어지고, 추가적으로 예외 클래스는 java.lang.Exception클래스를 상속받는다.

실행 예외는 RuntimeException과 그 자식 클래스에 해당한다. 그 밖의 예외 클래스는 모두 일반 예외이다. 자바는 자주 사용되는 예외 클래스를 표준 라이브러리로 제공한다.

1) 예외 처리하기

예외가 발생했을 때 프로그램의 갑작스러운 종료를 막고 정상 실행을 유지할 수 있도록 처리하는 코드를 예외 처리 코드라고 한다. 예외 처리는 try - catch - finally 블록으로 구성된다. (try문과 catch문에서 return문이 실행되더라도 finally는 항상 실행된다.)

try {
	// 예외 발생 가능 코드
} catch {
	// 예외 처리 : 예외 발생한 경우 실행
} finally {
	// 항상 실행 : 항상 실행(생략가능)
}
// NullPointerException 에러 발생 
public class ExceptionHandlingExample1 {
	public static void printLength(String data) {
    	int result = data.lengthh();
        System.out.println("문자 수: " + result);
    }

	public static void main(String[] args) {
    	System.out.println("[프로그램 시작]\n");
        printLength("ThisIsJava");
        printLength(null);
        System.out.println("[프로그램 종료]")
    }
}

// 예외 처리
public class ExceptionHandlingExample2 {
	public static void printLength(String data) {
    	try {
    		int result = data.length();
            System.out.println("문자 수:" + result)
    	} catch(NullPointerException e) {
        	// 예외 정보 얻는 방법 3가지
        	System.out.println(e.getMessage());
            System.out.println(e.toString());
            e.printStackTrace();
        } finally {
        	System.out.println("[마무리 실행]\n")
        }
    }

	public static void main(String[] args) {
    	System.out.println("[프로그램 시작]\n");
        printLength("ThisIsJava");
        printLength(null);
        System.out.println("[프로그램 종료]")
    }
} 

e.getMessage()는 예외가 발생한 이유만 리턴
e.toString()은 예외 발생 이유와 종류 리턴
e.printStackTrace()는 예외가 어디서 발생했는지 추적한 내용까지 출력

예외 종류에 따른 처리

try 블록에는 다양한 종류의 예외가 발생할 수 있다. 이 때 다중 catch를 사용하면 예외에 따라 예외 처리 코드를 다르게 작성할 수 있다. catch 블록의 예외 클래스는 try블록에서 발생된 예외의 종류를 말하는데, 해당 타입의 예외가 발생하면 catch 블록이 선택되어 실행된다.

try {
	// ArrayIndexOutofBoundsException 발생
	// NumberFormatException 발생
} catch(ArrayIndexOutofBoundsException e) {
	// 예외처리 1
} catch(NumberFormatException e) {
	// 예외처리 2
}

!주의 catch블록이 여러 개일지라도 catch블록은 하나만 실행된다. try블록에서 동시다발적으로 예외가 발생하지 않으며, 하나의 예외가 발생하면 즉시 catch블록으로 이동하기 때문이다.

따라서 처리해야할 예외 클래스들이 상속관계에 있을 땐 하위 클래스 catch블록을 먼저 작성하고 상위 클래스 catch 블록을 나중에 작성해야한다. 예외가 발생하면 cath블록은 위에서부터 차례로 검사 대상이 되는데, 하위 예외도 상위 클래스 타입이므로 상위 클래스 cath블록이 먼저 검사 대상이 되면 안된다.

다수의 예외를 하나의 catch 블록으로 처리하기

두 개 이상의 예외를 하나의 catch 블록으로 동일하게 예외 처리하고 싶은 경우 catch블록에 예외 클래스를 기호 |로 연결하면 된다.

try{
	// 예외가 일어날 수 있는 코드
} catch(NullPointerException | NumberFormatException e) {
	// 예외 처리
} finally {
	// 항상 실행
}

리소스 자동 닫기

리소스(resource)란 데이터를 제공하는 객체를 말한다. 리소스는 사용하기 위해 열어야(open)하며, 사용한 다음에는 닫아야(close) 한다. 리소스를 사용하다가 예외가 발생될 경우에도 안전하게 닫는 것이 중요하다. 그렇지 않으면 리소스가 불안정한 상태로 남아있게 된다.

아래 코드는 file.txt파일의 내용을 읽기 위해 FileINputStream 리소스를 사용하는데, 예외 발생 여부와 상관없이 finally 블록에서 안전하게 close한다.

FileInputStream fis = null;
try {
	fis = new FileInputStream("file.txt"); 	// 파일 열기
    ...
} catch(IOException e) {
	...
} finally {
	fis.close();	// 파일 닫기
}

좀 더 간단한 방법으로 자동으로 닫아줄 수 있다. 그러나 이 방법을 사용하기 위한 조건이 있다.
리소소가 java.lang.AutoCloseable 인터페이스를 구현해서 AutoCloseable 인터페이스의 close() 메소드를 오버라이딩 해야한다.

public class FileInputStream implements AutoCloseable {
	...
    @Override
    public void close() throws Exception {...}
}
try(FileInputStream fis = new FileInputStream("file.txt")) {
	...
} catch(IOException e) {
	...
}

복수 개의 리소스를 사용해야 한다면 다음과 같이 try() 괄호 안에 세미콜론으로 그분해서 리소스를 여는 코드를 복수 개 작성하면 된다 .

try(
	FileInputStream fis1 = new FileInputStream("file1.txt");
	FileInputStream fis2 = new FileInputStream("file2.txt")
    ) {
	...
} catch(IOException e) {
	...
}

Java 8 이전 버전은 반드시 try 괄호 안에서 리소스 변수를 선언해야 했지만,
Java 9 부터는 외부 리소스 변수를 사용할 수 있다.

FileInputStream fis1 = new FileInputStream("file1.txt");
FileInputStream fis2 = new FileInputStream("file2.txt");
try(fis1; fis2) {
	...
} catch(IOException e) {
	...
}

2) 예외 떠넘기기

메소드 내부에서 예외가 발생할 때 try-catch 블록으로 예외를 처리하는 것이 기본이지만, 메소드를 호출한 곳으로 예외를 떠넘길 수도 있다. 이때 사용하는 키워드가 throws이다.
Throws는 메소드 선언부 끝에 작성하는데, 떠넘길 예외 클래스를 표로 구분해서 나열해주면 된다.

리턴타입 메소드명(매개변수, ...) throws 예외클래스1, 예외클래스2, ... {
}

throws키워드가 붙어 있는 메소드에서 해당 예외를 처리하지 않고 떠넘겼기 때문에 이 메소드를 호출하는 곳에서 예외를 처리해야한다. 예를 들어 아래 코드는 ClassNotFoundException을 throws하는 method2()의 예외를 method1()에서 호출할 때 처리하고 있다.

public void method1() {
	try { 
    	method2();	// method2 호출
    } catch(ClassNotFoundException e) {
    	Sysytem.out.println("예외 처리: " + e.getMessage());
    }
}

public void method2() throws ClassNotFoundException {
	Class.forName("java.lang.String2");	// 던졌으니 호출할 때 처리
}

나열해야 할 예외가 많을 경우 throws Exception 또는 throws Throwable 만으로 모든 예외를 간단히 떠넘길 수 있다.

리턴타입 메소드명(매개변수,...) throws Exception {
}

main() 메소드에서도 throws 키워드를 사용해서 예외를 떠넘길 수 있는데, 결국 JVM이 최종적으로 예외 처리를 하게 된다. JVM은 예외의 내용을 콘솔에 출력하는 것으로 예외 처리를 한다.

public static void main(String[] args) throws Exception {
...
}

3) 사용자 정의 예외

은행의 뱅킹 프로그램에서 잔고보다 더 많은 출금 요청이 들어온 경우 잔고 부족 예외를 발생시킬 필요가 있다. 그러나 잔고 부족 예외는 표준 라이브러리에는 존재하지 않기 때문에 직접 예외 클래스를 정의해서 사용해야 한다. 이것을 사용자 정의 예외라고 한다.

사용자 정의 예외는 컴파일러가 체크하는 일반 예외로 선언할 수도 있고, 컴파일러가 체크하지 않는 실행 예외로 선언할 수도 있다.

  • 일반 예외는 Exception의 자식 클래스로 선언
  • 실행 예외는 RuntimeException의 자식 클래스로 선언
public class XXXException extends [Exception | RuntimeException] {
	public XXXException {	// 기본생성자
    }

    public XXXException(String messgage) {	// 예외 메시지 입력받는 생성자
    	super(message);
    }
}

사용자 정의 예외 클래스에는 기본 생성자와 예외 메시지를 입력받는 생성자를 선언한다. 예외 메시지는 부모 생성자 매개값으로 넘겨주는데, 그 이유는 예외 객체의 공통 메소드인 getMessage()의 리턴값으로 사용하기 위해서이다.

예외 발생시키기

자바에서 제공하는 표준 예외뿐만 아니라 사용자 정의 예외를 직접 코드에서 발생시키려면 throw키워드와 함께 예외 객체를 제공하면 된다. 예외의 원인에 해당하는 메시지를 제공하고 싶다면 생성자 매개값으로 전달한다.

throw new Exception("예외 메시지")
throw new RuntimeException("예외 메시지");
throw new InsufficientException("예외 메시지");

throw된 예외는 직접 try-catch블록으로 예외를 처리할 수도 있지만,

void method() {
	try {
    	...
        throw new Exception("예외 메시지")
        ...
    } catch(Exception e) {
    	String message = e.getMessage();
    }
}

대부분은 메소드를 호출한 곳에서 예외를 처리하도록 throws 키워드로 예외를 떠넘긴다.

void method() throws Exception {
	...
    throw new Exception("예외 메시지")
    ...
}
profile
velog에는 이론을 주로 정리하고, 코드와 관련된 것은 Git-hub로 관리하고 있어요. 포트폴리오는 링크된 Yun Lab 홈페이지를 참고해주시면 감사하겠습니다!

0개의 댓글