2장 객체 지향 디자인

2023. 12. 15. 23:32프로그래밍 공부/Data Structure

2.1 객체, 원리, 그리고 패턴

객체(object): 객체 지향 설계 패러다임에서 주된 '행위자'

객체는 클래스와 메소드를 포함한다.

클래스(class): 인스턴스 변수(instance variable)라고도 불리는 데이터 필드(field)의 사양

메소드(method): 객체가 실행할 수 있는 동작

 

각 클래스는 이 클래스의 인스턴스인 객체에 대한 간결하고 일관된 관점을 외부에 제시하며, 

불필요한 세부 사항을 너무 자세히 다루거나 객체의 내부 작업에 다른 사람이 접근할 수 있도록 하지 않다. 

이 컴퓨팅 관점은 여러 가지 목표를 달성하고 여러 가지 설계 원리를 통합하기 위한 것으로, 

여기서는 이 장에서 설명한다.

 

2.1.1 객체 지향 설계 목표

소프트웨어 구현은 견고성, 적응성 및 재사용성을 달성해야 한다(그림 2.1 참조)
그림 2.1: 객체 지향 설계의 목표.

견고성(robustness)

모든 훌륭한 프로그래머들은 정확한 소프트웨어를 개발하기를 원하는데, 

이는 프로그램이 프로그램의 응용 프로그램에서 예상되는 모든 입력에 대해 올바른 출력을 만들어내는 것을 의미한다. 

또한 우리는 소프트웨어가 강력하기(robust)를 바란다.

견고성(robustness): 응용 프로그램에 대해 명시적으로 정의되지 않은 예기치 않은 입력을 처리할 수 있는 것 

예를 들어, 프로그램이 양의 정수를 기대하고 있는데, 

그 대신 음의 정수가 주어지면, 프로그램은 이 오류로부터 우아하게 복구할 수 있어야 한다. 

더 중요한 것은, 소프트웨어 오류가 생명을 잃거나 부상으로 이어질 수 있는 생명에 중요한 응용 프로그램에서는, 

강력하지 않은 소프트웨어가 치명적일 수 있다는 것이다. 

1980년대 후반 방사선 치료기인 Therac-25와 관련된 사고로 인해 이러한 상황이 발생했다
1985년과 1987년 사이에 여섯 명의 환자가 심하게 과다복용을 했고, 

그들 중 일부는 방사선 과다복용으로 인한 합병증으로 사망했다. 

여섯 건의 사고 모두 소프트웨어 오류에서 비롯되었다.

 

적응성(Adaptability)

웹 브라우저 및 인터넷 검색 엔진과 같은 현대 소프트웨어 응용 프로그램은 

일반적으로 수년 동안 사용되는 큰 프로그램을 포함한다. 

따라서 소프트웨어는 환경의 변화하는 조건에 대응하여 시간이 지남에 따라 진화할 수 있어야 한다. 

따라서 품질 소프트웨어의 또 다른 중요한 목표는 적응성(adaptability, 진화가능성(evolvability)이라고도 함)을 달성하는 것이다. 

이 개념과 관련된 것은 휴대성이다.

휴대성(portability): 소프트웨어가 다양한 하드웨어 및 운영 체제 플랫폼에서 최소한의 변경만으로 실행될 수 있는 능력

자바로 소프트웨어를 작성하는 것의 장점은 언어 자체에 의해 제공되는 휴대성이다.

 

재사용성(Reusability)

소프트웨어를 재사용할 수 있기를 원하는 것은 적응성과 함께 한다.

재사용성(resualbility): 동일한 코드를 다양한 응용 분야에서 다양한 시스템의 구성 요소로 사용할 수 있어야 한다는 것

소프트웨어가 미래의 응용 분야에서 쉽게 재사용할 수 있도록 설계된다면 

양질의 소프트웨어를 개발하는 것은 비용이 많이 드는 기업이 될 수 있으며, 

비용은 어느 정도 상쇄될 수 있다.

 

그러나 Therac-25의 소프트웨어 오류의 주요 원인 중 하나는 

Therac-20의 소프트웨어를 부적절하게 재사용했기 때문에

(객체 지향적이지 않고 Therac-25와 함께 사용되는 하드웨어 플랫폼을 위해 설계되지 않았기 때문에) 

이러한 재사용은 주의해서 수행해야 한다.

 

2.1.2 객체 지향 설계 원칙

위에서 설명한 목표를 용이하게 하기 위한 객체 지향 접근법의 주요 원칙은 다음과 같다(그림 2.2 참조):
• 추상화
• 캡슐화
• 모듈화.
그림 2.2 : 객체 지향 설계의 원칙.

 

추상화

추상화(abstraction): 복잡한 시스템을 가장 기본적인 부분으로 증류하고 이 부분들을 단순하고 정확한 언어로 설명하는 것

일반적으로 시스템의 부분들을 설명하는 것은 그것들의 이름을 짓고 그것들의 기능을 설명하는 것을 포함한다.

추상화 패러다임을 데이터 구조 설계에 적용하면 추상적 데이터 타입(ADT)이 발생한다.

추상적 데이터 타입(abstract data type, ADT)

- 저장된 데이터의 타입, 그 데이터에서 지원되는 연산, 연산의 매개 변수 타입을 지정하는 데이터 구조의 수학적 모델

- ADT는 각 연산이 수행하는 을 지정하지만 수행하는 방법은 지정하지 않는다.

- 자바에서 ADT는 인터페이스로 표현될 수 있다.

   인터페이스: 빈 본문을 가지는 메소드 선언의 리스트

   (2.4절에서 자바 인터페이스에 대해 더 자세히 설명한다.)
- ADT는 클래스에 의해 자바로 모델링된 구체적인 자료 구조에 의해 구현된다.

 

클래스는 저장 중인 데이터와 클래스의 인스턴스인 객체가 지원하는 연산을 정의한다.

또한 인터페이스와 달리 클래스는 각 메소드의 본문에서 연산을 수행하는 방법을 지정한다.

Java 클래스는 메소드가 인터페이스에 선언된 모든 메소드를 포함할 때

= 인터페이스를 구현한다(implement an interface)

그러나 클래스는 인터페이스보다 더 많은 메소드를 가질 수 있다.

 

캡슐화

캡슐화(encapsulation): 소프트웨어 시스템의 다른 구성 요소가 각각의 구현의 내부 세부 사항을 드러내면 안 된다는 것

캡슐화의 주요 장점 중 하나는 프로그래머가 시스템의 세부 사항을 구현하는 데 있어 자유를 준다는 것이다.

프로그래머에 대한 유일한 제약은 외부인이 보는 추상적인 인터페이스를 유지하는 것이다.

 

모듈화

현대 소프트웨어 시스템은 일반적으로 전체 시스템이 제대로 작동하기 위해 

올바르게 상호 작용해야 하는 여러 가지 다른 구성 요소로 구성된다. 

이러한 상호 작용을 올바르게 유지하려면 이러한 여러 가지 구성 요소가 잘 구성되어야 한다. 

객체 지향 설계에서 이러한 코드 구조화 접근 방식은 모듈화 개념을 중심으로 한다.

모듈화(modularity): 소프트웨어 시스템의 여러 구성 요소를 별도의 기능 단위로 나누는 코드에 대한 구성 원리

 

계층적 조직

모듈화는 소프트웨어 재사용을 가능하게 하는 데 도움이 되는 구조이다. 

소프트웨어 모듈이 추상적인 방식으로 작성되어 일반적인 문제를 해결한다면, 

이러한 일반적인 문제의 사례가 다른 맥락에서 발생할 때 모듈을 재사용할 수 있다.


예를 들어, 벽의 구조적 정의는 집집마다 동일하며, 

일반적으로 2 x 4인치 스터드, 일정 거리 이격 등으로 정의된다. 

따라서, 조직화된 건축가는 한 집에서 다른 집으로 자신의 벽 정의를 재사용할 수 있다. 

예를 들어, 상업용 건물의 벽은 집의 그것과 유사할 수 있지만, 전기 시스템과 스터드 재료는 다를 수 있다.


소프트웨어 패키지의 다양한 구조적 구성 요소를 구성하는 자연스러운 방법은 

계층적 방식으로 유사한 추상적 정의를 수준별 방식으로 함께 그룹화하는 것이다. 

이러한 계층의 일반적인 용도는 조직도에 있으며, 

"a ranch is a house is a building."와 같이

각 링크가 올라가는 것을 "is a"로 읽을 수 있다. 

이러한 종류의 계층은 소프트웨어 설계에서 유용하며, 

공통 기능을 가장 일반적인 수준에서 함께 그룹화하고

특수화된 동작을 일반적인 동작의 확장으로 보기 때문이다.

그림 2.3: 건축 건물과 관련된 "is a"계층의 한 예.

2.1.3 설계 패턴

객체 지향 설계의 장점 중 하나는 재사용 가능하고 강력하며 적응력 있는 소프트웨어를 용이하게 한다는 것이다.

그러나 좋은 코드를 디자인하는 것은 단순히 객체 지향 방법론을 이해하는 것 이상의 것이 필요하다.

객체 지향 설계 기법의 효과적인 사용이 필요하다.


컴퓨팅 연구자들과 실무자들은 간결하고, 정확하며, 

재사용 가능한 양질의 객체 지향 소프트웨어를 설계하기 위한 다양한 조직 개념과 방법론을 개발했다. 

이 책과 특별히 관련이 있는 것은 설계 패턴의 개념이다.

설계 패턴(design pattern): "전형적인" 소프트웨어 디자인 문제에 대한 해결책을 설명한다. 

패턴은 다양한 상황에서 적용될 수 있는 솔루션의 일반적인 템플릿을 제공한다. 

당면한 특정 문제에 특화될 수 있는 추상적인 방식으로 솔루션의 주요 요소를 설명한다. 

패턴을 식별하는 이름, 이 패턴이 적용될 수 있는 시나리오를 설명하는 컨텍스트, 

패턴이 적용되는 방법을 설명하는 템플릿, 패턴이 생성하는 것을 설명하고 분석하는 결과로 구성된다.


이 책에서는 여러 가지 설계 패턴을 제시하고, 

이를 데이터 구조와 알고리즘 구현에 일관되게 적용할 수 있는 방법을 제시한다.

이 설계 패턴은 알고리즘 설계 문제를 해결하기 위한 패턴과

소프트웨어 공학 문제를 해결하기 위한 패턴으로 구분된다.

 

우리가 논의하는 알고리즘 설계 패턴 중 일부는 다음과 같다:
• 재귀 (섹션 3.5)
• 분할 상환 분석(amortization) (6.1.4절)
• 분할 및 정복 (제11.1.1절)
• 감소 및 정복으로도 알려진 Prune-and-search(11.7.1절)
• 브루트 포스(섹션 12.2.1)
• 그리디 방법 (제12.4.2절)
• 다이나믹 프로그래밍(섹션 12.5.2).


마찬가지로, 우리가 논의하는 소프트웨어 엔지니어링 설계 패턴 중 일부는 다음과 같다:
• 위치(Position) (섹션 6.2.2)
• 어댑터(섹션 6.1.2)
• 반복자(섹션 6.3)
• 템플릿 방법(섹션 7.3.7, 11.6 및 13.3.2)
• 구성(Composition)(섹션 8.1.2)
• Comparator (제8.1.2절)
• Decorator(섹션 13.3.1).


그러나 여기서는 위에서 언급한 바와 같이 이 개념들을 각각 설명하기보다는 본문 전체에 걸쳐 소개한다.

알고리즘 공학이든 소프트웨어 공학이든 각 패턴에 대해 그 일반적인 용도를 설명하고

그 예를 하나 이상 구체적인 예를 들어 설명한다.

 

2.2 상속과 다형성

소프트웨어 프로젝트에서 흔히 볼 수 있는 계층적 관계를 활용하기 위해 

객체 지향 설계 접근 방식은 코드를 재사용하는 방법을 제공한다.

 

2.2.1 상속

객체 지향 패러다임은 상속(inheritance)이라는 기술을 통해

코드 재사용을 위한 모듈식 및 계층적 구성 구조를 제공한다.

이 기술은 전문 클래스가 일반 클래스의 코드를 재사용하여

보다 특정 클래스에 특화될 수 있는 일반 클래스를 설계할 수 있도록 한다.

기본 클래스(base class) 또는 슈퍼 클래스(superclass)라고도 하는 일반 클래스는

표준 인스턴스 변수와 다양한 상황에 적용되는 메소드를 정의할 수 있다.

슈퍼 클래스를 전문화하거나(specializes) 확장하거나(extends) 상속하는(inherits from) 클래스는

일반 메소드를 상속하기 때문에

이를 새로운 방식으로 구현할 필요가 없다.

이 특정 서브 클래스(subclass)에 특화된 메소드만 정의해야 한다.
 

예제 2.1: a(), b(), c()라는 필드와 함께 객체를 정의하는 클래스 S를 생각해 보자. 

S를 확장하고 추가적인 필드 y와 두 개의 메소드 d(), e()를 포함하는 클래스 T를 정의한다고 가정하자. 

클래스 T는 S에서 인스턴스 변수 x와 메소드 a(), b(), c()를 상속한다

그림 2.4의 클래스 상속 다이어그램에서 클래스 S와 클래스 T 사이의 관계를 보여준다. 

이러한 다이어그램의 각 상자는 클래스 이름, 필드(또는 인스턴스 변수) 및 메소드를

서브 사각형으로 포함하여 클래스를 나타낸다.


그림 2.4: 클래스 상속 다이어그램. 

각 상자는 이름, 필드 및 메소드가 포함된 클래스를 나타내고

상자 사이의 화살표는 상속 관계를 나타낸다.

객체 작성 및 참조

객체 o가 생성되면

해당 객체의 데이터 필드에 대한 메모리가 할당되고, 

이들 동일한 필드는 특정 시작 값으로 초기화된다.

일반적으로 객체 o에 대한 "링크" 역할을 하는 변수를

새 객체 o와 연관시키며,

o를 참조(reference)한다고 한다.

 

객체 o에 접근하고자 할 때 

(그 필드에 도달하거나 그 메소드를 실행하기 위한 목적으로), 

우리는 o의 메소드 중 하나(o가 속한 클래스에 의해 정의됨)의 실행을 요청하거나

o의 필드 중 하나를 검색할 수 있다. //여기부터

 

객체 p가 다른 객체 o와 상호작용하는 방법

1. o에게 o의 메소드 중 하나를 불러오는 "메시지"를 보낸다.

    예시 i) o는 자신의 설명을 출력하는 것

    예시 ii) o는 문자열로 변환하는 것

    예시 iii) o는 자신의 데이터 필드 중 하나의 값을 반환하는 것

2. p가 자신의 필드 중 하나에 직접 접근하는 것이지만,

    o가 p 허가와 같은 다른 객체에 접근할 수 있는 경우에만 가능하다. 

    예시) 자바 클래스 정수의 인스턴스는 인스턴스 변수로 정수를 저장하며,

    이 데이터에 접근하기 위한 여러 연산들을 제공하는데,

    여기에는 다른 숫자 유형으로 변환하는 방법,

    숫자 문자열로 변환하는 방법,

    숫자 문자열을 숫자로 변환하는 방법 등이 포함된다.

    그러나 이러한 세부 정보는 숨겨져 있기 때문에

    인스턴스 변수의 직접 접근은 허용되지 않는다.


다이나믹 디스패치

프로그램이 어떤 객체 o의 어떤 메소드 a()를 호출하려고 할 때, 

그것은 "o.a()"로서 점 연산자 구문(섹션 1.3.2)을 사용하여

일반적으로 표시되는 o에게 메시지를 보낸다.

이 호출에 해당하는 코드는 런타임 환경이 o의 클래스 T를 검사하여

클래스 T가 a() 메소드를 지원하는지 확인하고,

지원하는 경우 실행하도록 지시한다.

특히 클래스 T가 a() 메소드 자체를 정의하는지 확인하기 위해

런타임 환경은 클래스 T를 검사한다.

정의한 경우 이 메소드가 실행된다.

T가 a() 메소드를 정의하지 않으면 런타임 환경은 T의 슈퍼 클래스 S를 검사한다.

S가 a()를 정의하면 이 메소드가 실행된다.

반면 S가 a()를 정의하지 않으면 런타임 환경은 S의 슈퍼 클래스에서 검색을 반복한다.

이 검색은 a() 메소드를 찾을 때까지 클래스의 계층을 계속 진행하고 실행되며,

또는 a() 메소드 없이 최상위 클래스

(예를 들어 자바의 Object 클래스)에 도달하면

런타임 오류가 발생한다.

호출할 특정 메소드를 찾기 위해

메시지 o.a()를 처리하는 알고리즘을

동적 디스패치(dynamic dispatch)

(또는 동적 바인딩(dynamic binding)) 알고리즘이라고 하며,

이 알고리즘은 재사용되는 소프트웨어를 찾는 효과적인 메커니즘을 제공한다.

또한 객체 지향 프로그래밍의 또 다른 강력한 기술인 다형성(polymorphism)을 허용한다.


2.2.2 다형성

문자 그대로 '다형성'은 '여러 가지 형태'를 의미한다. 

객체 지향 설계의 맥락에서

객체 변수가 다른 형태를 취할 수 있는 능력을 말한다. 

자바와 같은 객체 지향 언어는 참조 변수를 사용하여 객체를 지정한다. 

참조 변수 o는 어떤 클래스 S의 관점에서

어떤 클래스의 객체를 참조할 수 있는지 정의해야 한다. 

그러나 이것은 o가 S를 확장하는 클래스 T에 속하는 모든 객체를 참조할 수도 있다는 것을 의미한다.

 

이제 S가 a() 메소드를 정의하고 T가 a() 메소드를 정의하면 어떻게 되는지 생각해보자. 

메소드 호출에 대한 동적 디스패치 알고리즘은 항상 적용되는 가장 제한적인 클래스로부터 검색을 시작한다. 

o가 클래스 T의 객체를 언급할 때,

그것은 S의 것이 아니라 o.a()를 요청할 때 T의 a() 메소드를 사용할 것이다. 

이 경우, T는 S의 메소드 a()를 재정의한다(override)고 한다. 

또는 o가 클래스 S의 객체(T 객체도 아닌)를 언급할 때, 그것은 실행될 것이다
o.a()를 요청할 때 S의 a() 메소드이다.

이와 같은 다형성은 a() 메소드를 올바르게 실행하려면

객체 o가 T의 인스턴스를 참조하는지 S의 인스턴스를 참조하는지 알 필요가 없으므로 유용하다.

따라서 객체 변수 o는 참조하는 객체의 특정 클래스에 따라

다형성(polymorphic)일 수도 있고 여러 가지 형태를 취할 수도 있다.

이러한 종류의 기능을 통해

전문 클래스 T는 클래스 S를 확장하고 S에서 표준 메소드를 상속하며

T의 객체의 특정 속성을 설명하기 위해 S에서 다른 메소드를 재정의할 수 있다.


자바와 같은 일부 객체 지향 언어들은 다형성과 관련된 유용한 기법을 제공하기도 하는데, 

이것을 메소드 오버로딩(overloading)이라고 한다. 

하나의 클래스 T에 같은 이름을 가진 메소드가 여러 개 있을 때

각각 다른 시그니처가 있다면

오버로딩이 발생한다. 

메소드의 시그니처(signature): 메소드의 이름과 해당 메소드에 전달되는 인수의 타입 및 수를 조합한 것

따라서 클래스에 있는 여러 메소드의 이름이 동일할 수 있더라도

서로 다른 시그니처, 즉 실제로 서로 다른 경우 컴파일러에 의해 구별될 수 있다. 

메소드 오버로딩을 허용하는 언어의 경우 

런타임 환경은 호출 중인 메소드와 일치하는 첫 번째 메소드를 찾기 위해 

클래스 계층을 검색하여 특정 메소드 호출을 실행할 실제 메소드를 결정한다. 

 

예시)

메소드 a()를 정의하는 클래스 T가

메소드 a(x,y)를 정의하는 클래스 U를 확장한다고 가정한다. 

클래스 T의 객체 o가 "o.a(x,y)" 메시지를 수신하면

호출되는 메소드는 (두 매개변수 x, y를 가진) U 버전이다. 

따라서 진정한 다형성은 동일한 시그니처를 가지지만

다른 클래스에서 정의되는 메소드에만 적용된다.


상속, 다형성, 메소드 오버로딩은 재사용 가능한 소프트웨어의 개발을 지원한다. 

표준 인스턴스 변수와 메소드를 상속하는 클래스를 정의하고 

새로운 클래스의 객체의 특별한 측면을 다루는 

보다 구체적인 인스턴스 변수와 메소드를 정의할 수 있다.


2.2.3 Java에서 상속 사용

자바에서 클래스의 상속을 사용하는 방법에는 전문화확장이라는 두 가지 주요 방법이 있다.

 

전문화

전문화(specialization) - 일반적인 클래스를 특정한 서브클래스에 특화시키는 것이다. 

그러한 서브클래스는 일반적으로 그들의 슈퍼클래스와 "is" 관계를 갖는다. 

서브클래스는 슈퍼클래스의 모든 방식을 이어받는다.

 

상속된 각 메소드에 대해 

그 메소드가 전문화를 위해 작동하는지 여부와 무관하게 올바르게 작동한다면 

추가 작업이 필요하지 않다. 

반대로 슈퍼클래스의 일반 메소드가 서브클래스에서 올바르게 작동하지 않는다면, 

우리는 메소드를 재정의하여 서브클래스에 대해 올바른 기능을 갖도록 해야 한다. 

 

예시)

메소드 drink와 메소드 sniff가 있는 일반 클래스 Dog를 가질 수 있다. 

이 클래스를 Bloodhound 클래스로 전문화하는 것은 아마도 불가능할 것이다
모든 개들이 거의 같은 방식으로 마시기 때문에, 우리는 drink 메소드를 무시할 것을 요구한다.

하지만 블러드하운드는 일반 개보다 훨씬 더 예민한 후각을 가지고 있기 때문에,

우리는 냄새 맡는 방법을 무시할 것을 요구할 수 있다.

이러한 방식으로, Bloodhound 클래스는 슈퍼클래스인 Dog의 방법을 전문화한다.

 

확장

확장(extension) - 상속을 활용하여 슈퍼클래스의 메소드에 대해 작성된 코드를 재사용하지만

슈퍼클래스에는 없는 새로운 메소드를 추가하여 기능을 확장한다. 

 

예시)

Dog 클래스로 돌아가면

Dog 클래스의 모든 표준 메소드를 상속하지만 

새로운 메소드인 Herd를 추가하는 서브클래스 BorderCollie를 생성할 수 있다. 

이 경우 BorderColies에는 표준 반려견에는 없는 목축 본능이 있기 때문이다. 

새로운 메소드를 추가하여 표준 반려견의 기능을 확장하고 있다.


Java에서 각 클래스는 정확히 하나의 다른 클래스를 확장할 수 있다. 

클래스 정의에서 extends 절을 명시적으로 사용하지 않더라도

정확히 하나의 다른 클래스에서 여전히 상속되는데,

이 경우에는 클래스 java.lang.Object이다. 

이 속성 때문에 Java는 클래스 간에 단일 상속(single inheritance)만 허용한다고 한다.


메소드 재정의 타입

새로운 클래스의 선언 안에서

Java는 정제(refinement)대체(replacement)라는 두 가지 종류의 재정의 메소드를 사용한다. 

 

1) 재정의의 대체 타입에서

메소드는 재정의하는 슈퍼 클래스의 메소드를 완전히 대체한다

(위에서 언급한 Bloodhound의 sniff 메소드에서와 같이). 

Java에서 클래스의 모든 일반적인 메소드는 이러한 타입의 재정의 동작을 활용한다.


2) 그러나 재정의의 정제 타입에서

메소드는 슈퍼클래스의 메소드를 대체하는 것이 아니라 

슈퍼클래스의 메소드에 추가 코드를 추가한다. 

자바에서 모든 생성자는

생성자 체이닝(constructor chaining)이라는 체계인 재정의의 정제 타입을 활용한다.

즉, 생성자는 슈퍼클래스의 생성자를 호출하는 것으로 실행을 시작한다. 

이 호출은 명시적으로 또는 암묵적으로 수행될 수 있다. 

슈퍼클래스의 생성자를 명시적으로 호출하려면

super라는 키워드를 사용하여 슈퍼클래스를 참조한다. 

(예를 들어, super()는 인수 없이 슈퍼클래스의 생성자를 호출한다.) 

그러나 생성자의 본문에서 명시적인 호출이 이루어지지 않으면 

컴파일러는 자동으로 생성자의 첫 번째 행으로 super()에 대한 호출을 삽입한다.

(이 일반 규칙에 대한 예외는 다음 절에서 설명한다.)

 

요약하면, 자바에서 생성자는 메소드를 재정의하는 정제 타입을 사용하는 반면,

일반적인 메소드는 대체를 사용한다.

 

키워드 this

때로는 자바 클래스에서 해당 클래스의 현재 인스턴스를 참조하는 것이 편리하다. 

자바는 이러한 참조를 위해 this이라고 불리는 키워드를 제공한다. 

예를 들어 현재 객체를 어떤 메소드에 매개변수로 전달하고자 하는 경우 this 참조는 유용하다. 

이것의 또 다른 응용은 코드 조각 2.1에 주어진 프로그램과 같이 

현재 블록에 정의된 변수와 이름 충돌이 있는 현재 객체 내부의 필드를 참조하는 것이다.

 

코드 조각 2.1:

현재 객체의 필드와 동일한 이름을 가진 지역 변수 사이를 명확히 구분하기 위해 

참조 this의 사용을 보여주는 샘플 프로그램이다.

public class ThisTester {
  public int dog = 2; //instance variable
  public void clobber() {
    int dog = 5;      // a differrent dog!
    System.out.println("The dog local variable = " + dog);
    System.out.println("The dog field = " + this.dog);
    }
  public static void main(String args[]){
    ThisTester t = new ThisTester();
    t.clobber();
  }
}


이 프로그램이 실행되면 다음을 인쇄한다:
The dog local variable = 5.0
The dog field = 2

 

자바의 상속 사례

상속과 다형성에 대한 위의 몇 가지 개념을 좀 더 구체적으로 만들기 위해

자바의 몇 가지 간단한 예를 생각해 보겠다.

 

특히, 우리는 수열을 이용하여 수열을 출력하는 것을 고려한다. 

수열은 각각의 수가 이전 수들 중 하나 이상의 수에 의존하는 수열이다. 

 

예시)

산술 진행(arithmetic progression)은 다음 수를 덧셈으로 결정하고

기하 진행(geometric progression)은 다음 수를 곱셈으로 결정한다. 

어쨌든 진행은 첫 번째 값을 정의하는 방법을 필요로 하며 현재 값을 식별하는 방법도 필요하다.

 

코드 조각 2.2에 표시된 Progression이라는 클래스를 정의하는 것으로 시작하는데, 

이 클래스는 숫자 진행의 표준 필드와 메소드를 정의한다. 

구체적으로 다음 두 개의 long 정수 필드를 정의한다:
• first: 진행의 첫 번째 값;
• cur: 현재 진행 값과 다음 세 가지 방법을 사용한다:
          firstValue(): 진행을 첫 번째 값으로 재설정하고 해당 값을 반환한다.
          nextValue(): 진행 단계를 다음 값으로 진행하고 해당 값을 반환한다.
          printProgression(n): 진행을 재설정하고 진행의 처음 n개 값을 인쇄한다.

 

메소드 printProgression은 어떤 값도 반환하지 않는다는 의미에서 출력이 없다. 

메소드 firstValue와 nextValue는 모두 long 정수 값을 반환한다. 

즉, firstValue와 nextValue는 함수이고 printProgression은 프로시저이다.


Progression 클래스에는 생성자인 Progression() 메소드도 포함된다. 

생성자는 이 클래스의 객체가 생성될 때 모든 인스턴스 변수를 설정한다는 점을 기억하자. 

Progression 클래스는 전문 클래스가 상속되는 일반적인 슈퍼 클래스이므로 

이 생성자는 Progression 클래스를 확장하는 각 클래스의 생성자에 포함될 코드이다.
코드 조각 2.2: 일반 숫자 진행 클래스.

/**
 * A class for numeric progressions.
 */
public class Progression P
  
  /** First value of the progression. */
  protected long first;
  
  /** Current value of the progression. */
  protected long cur;
  
  /** Default constructor. */
  Progression() {
    cur = first = 0;
  }
  
  /** Resets the progression to the first value.
   *
   * @return first value
   */
  protected long firstValue() {
    cur = first
    return cur;
  }
  
  /** Advances the progression to the next value.
   *
   * @return next value of the progression
   */
  protected long nextValue() {
    return ++cur; //default next value
  }
 
  /** Print the first n values of the progression.
   *
   * @param n number of values to print
   */
  public void printProgression(int n) {
    System.out.print(firstValue());
    int temp = n;
    while(temp > 0){
      System.out.print(nextValue());
      temp--;
    }
  }
}

Arithmetic Progression 클래스

다음으로 코드 조각 2.3에 제시된 ArithProgression 클래스를 살펴본다. 

이 클래스는 산술 진행을 정의하며,

다음 값은 이전 값에 고정 증분(inc)을 추가하여 결정된다. 

ArithProgression은

필드 first와 cur과 메소드 firstValue() 및 printProgression(n)을 

Progression 클래스에서 상속한다. 

증분을 저장하기 위해 새 필드 inc를 추가하고, 증분을 설정하기 위해 두 개의 생성자를 추가한다. 

마지막으로 산술 진행에 대한 다음 값을 얻는 방식에 맞게 nextValue() 메소드를 재정의한다.


여기서 다형성이 작동한다. 

진행 참조가 ArithProgression 객체를 가리키고 있을 때 

사용되는 것은 ArithProgression 메소드 firstValue()와 nextValue()이다. 

이 다형성은 printProgression(n)의 상속된 버전 내부에서도 마찬가지인데, 

여기서 첫 번째 Value()와 nextValue() 메소드에 대한 호출은 

"현재" 객체(Java에서는 이를 가리킴)에 대해 암시적이기 때문에

이 경우 ArithProgression 클래스가 된다.


생성자 및 키워드 this 예제

ArithProgression 클래스의 정의에서 매개 변수를 사용하지 않는 기본 생성자와 

정수 매개 변수를 진행에 대한 증분으로 사용하는 매개 변수를 사용하는 매개 변수를 두 개 추가했다. 

기본 생성자는 실제로 매개 변수를 호출하며 키워드 this와 증분 매개 변수의 값으로 1을 전달한다. 

이 두 생성자는 메소드가 실제로 이름, 호출하는 객체의 클래스 및

메소드에 전달되는 인수 타입(시그니처)에 의해 지정되므로 메소드 오버로딩을 보여준다.

이 경우 오버로딩은 생성자(기본 생성자와 매개 변수 생성자)에 대한 것이다.


this(1)을 기본 생성자의 첫 번째 문장으로 매개 변수 생성자에게 호출하면 

섹션 2.2.3에서 논의된 일반 생성자 연결 규칙에 대한 예외가 트리거된다. 

즉, 생성자 C'의 첫 번째 문장이 이 참조를 사용하여 

동일한 클래스의 다른 생성자 C'를 호출할 때마다 

슈퍼클래스 생성자는 암시적으로 C를 호출하지 않는다. 

슈퍼클래스 생성자는 결국 명시적으로 또는 암시적으로 체인을 따라 호출된다. 

특히, 우리의 ArithProgression 클래스의 경우,

슈퍼클래스(Progression)의 기본 생성자는 

ArithProgression의 매개 변수 생성자의 첫 번째 문장으로 암시적으로 호출된다.
우리는 섹션 1.2에서 생성자에 대해 더 자세히 설명한다.

코드 조각 2.3: 코드 조각 2.2에 표시된 일반 진행 클래스를 상속하는 산술 진행을 위한 클래스이다.

/**
 * Arithmetic progression.
 */
class ArithProgression extends Progression {
  
  /** Increment. */
  protected long inc;
  
  // Inherits variables first and cur.
  
  /** Default constructor setting a unit increment. */
  ArithProgression() {
    this(1);
  }
  
  /** Parametric constructor providing the increment. */
  ArithProgression(long increment) {
    inc = increment;
  }
  
  /** Advances the progression by adding the increment to the current value.
   *
   * @return next value of the progression
   */
  protected long nextValue() {
    cur += inc;
    return cur;
  }
  
  // Inherits methods firstValue() and printProgression(int)
}


Geometric Progression 클래스

다음으로 코드 조각 2.4에 표시된 GeomProgression이라는 클래스를 정의해 보겠다.

GeomProgression은 기하학적 진행을 단계적으로 수행하여 출력한다.

여기서 다음 값은 이전 값에 고정된 베이스, base를 곱하여 결정된다.

 


기하학적 진행은 다음 값을 결정하는 방법을 제외하고는 일반적인 진행과 같다.

따라서 GeomProgression은 Progression 클래스의 하위 클래스로 선언된다. 

ArithProgression 클래스와 마찬가지로 GeomProgression 클래스는

Progression으로부터 필드 first와 cur를 상속하고 

메소드는 firstValue와 printProgression을 상속한다.

코드 조각 2.4: 기하학적 진행을 위한 클래스.

/**
 * Geometric Progression.
 */
class GeomProgression extends Progression {
  
  /** Base. */
  protected long base;
  
  // Inherits variables first and cur.
  
  /** Default constructor setting base 2. */
  GeomProgression() {
    this(2);
  }
  
  /** Parametric constructor providing the base.
   * 
   * @param b base of progression.
   */
  GeomProgression(long b) {
    base = b;
    first = 1;
    cur = first;
  }
  
  /** Advances the progression by multiplying the base with the current value.
   *
   * @return next value of the progression
   */
  protected long nextValue() {
    cur *= base;
    return cur;
  }
  
  // Inherits methods firstValue() and printProgression(int)
}


Fibonacci Progression 클래스

또 다른 종류의 진행을 나타내는 FibonacciProgression 클래스를 정의한다. 

여기서 다음 값은 현재 값과 이전 값의 합으로 정의된다. 

저희는 코드 조각 2.5에서 클래스 FibonacciProgression의 사용을 참고하자.

/**
 * Fibonacci Progression.
 */
class FibonacciProgression extends Progression {
  
  /** Previous value */
  long prev;
  
  // Inherits variables first and cur.
  
  /** Default constructor setting 0 and 1 as the first two values. */
  FibonacciProgression() {
    this(0, 1);
  }
  
  /** Parametric constructor providing the first and second values.
   * 
   * @param value1 first value.
   * @param value2 second value
   */
  FibonacciProgression(long value1, long value2) {
    first = value1;
    prev = value2 - value1; // fictitious value preceding the first
  }
  
  /** Advances the progression by adding the previous value to the current value.
   *
   * @return next value of the progression
   */
  protected long nextValue() {
    long temp = prev;
    prev = cur;
    cur += temp;
    return cur;
  }
  
  // Inherits methods firstValue() and printProgression(int)
}


 
진행을 시작하는 다른 방법을 제공하기 위해 FibonacciProgression 클래스에서 매개변수화된 생성자이다.
코드 조각 2.5: 피보나치 진행을 위한 클래스.
 일반적인 진행 클래스에서 세 가지 다른 진행 클래스가 어떻게 도출되는지 시각화하기 위해 

그림 2.5에 상속 다이어그램을 제공한다.
 
그림 2.5 : 클래스 진행 및 해당 서브클래스에 대한 상속 다이어그램.


예제를 완료하기 위해 코드 조각 2.6과 같은 클래스 TestProgression을 정의한다. 

이 클래스에서는 클래스 ArithProgression, GomeProgression 및

FibonacciProgression의 객체를 차례로 참조하므로

main 메소드를 실행하는 동안 변수 prog가 다원화된다. 

TestProgression 클래스의 main 메소드가 Java 런타임 시스템에 의해 호출되면

코드 조각 2.7과 같은 출력이 생성된다.

 

이 절에서 제시한 예제는 작지만 자바의 상속에 대한 간단한 예를 보여준다. 

하지만 진행 클래스와 그 하위 클래스, 

그리고 테스터 프로그램에는 몇 가지 단점이 있는데, 

이는 즉각적으로 명확하지 않을 수 있다. 

한 가지 문제점은 기하학적인 진행과 피보나치 진행이 빠르게 증가하고 

긴 정수들의 불가피한 넘침을 처리할 수 있는 조항이 없다는 것이다. 

예를 들어, 340 > 263 이후로

base가 b = 3인 기하학적 진행은 40번 반복한 후에 long 정수가 오버플로우될 것이다. 

마찬가지로 94번째 피보나치 수는 263보다 크므로

94번 반복한 후에 피보나치 진행은 long 정수가 오버플로우될 것이다. 

또 다른 문제점은 피보나치 진행에 임의의 시작 값을 허용하지 않을 수 있다는 것이다. 

예를 들어, 0과 -1로 시작하는 피보나치 진행을 허용할까? 

자바 프로그램을 실행하는 동안 발생하는 입력 오류 또는 오류 조건을 처리하려면 

이를 처리하기 위한 메커니즘이 필요하다. 

다음 항목에 대해 설명하겠다.
  
코드 조각 2.6: 진행 클래스를 테스트하기 위한 프로그램.

/** Test program for the progression classes */
class TestProgression
  public static void main(String[] args) {
    Progression prog;
    // test ArithProgression
    System.out.println("Arithmetic progression with default increment:");
    prog = new ArithProgression();
    prog.printProgression(10);
    System.out.println("Arithmetic progression with increment 5:");
    prog = new ArithProgression(5);
    prog.printProgression(10);
    // test GeomProgression
    System.out.println("Geometric progression with default base:");
    prog = new GeomProgression();
    prog.printProgression(10);
    System.out.println("Geometric progression with base 3:");
    prog = new GeomProgression(3);
    prog.printProgression(10);
    // test FibonacciProgression
    System.out.println("Fibonacci progression with default start values:");
    prog = new FibonacciProgression();
    prog.printProgression(10);
    System.out.println("Fibonacci progression with start values 4 and 6:");
    prog = new FibonacciProgression(4,6);
    prog.printProgression(10);
  }
}


코드 조각 2.7: 코드 조각 2.6에 표시된 TestProgression 프로그램의 출력.
 

Arithmetic progression with default increment:
0 1 2 3 4 5 6 7 8 9
Arithmetic progression with increment 5:
0 5 10 15 20 25 30 35 40 45
Geometric progression with default base:
1 2 4 8 16 32 64 128 256 512
Geometric progression with base 3:
1 3 9 27 81 243 729 2187 6561 19683
Arithmetic progression with default start values:
0 1 1 2 3 5 8 13 21 34
Arithmetic progression with start values 4 and 6:
4 6 10 16 26 42 68 110 178 288


2.3 예외

예외는 프로그램 실행 중에 발생하는 예기치 않은 이벤트이다.

예외는 오류 상태의 결과일 수도 있고 단순히 예상치 못한 입력일 수도 있다. 

어쨌든 자바와 같은 객체 지향 언어에서는 예외가 객체 그 자체라고 생각할 수 있다.

 

2.3.1 던지기 예외

예외(exception): 코드에 의해 던져진(thrown) 객체들로, 일종의 예기치 않은 상태에 직면한다. 

또한 객체 메모리 부족과 같은 예기치 않은 상태에 직면할 경우 자바 런타임 환경에 의해 던져질 수도 있다. 

던져진 예외는 예외를 어떻게든 "처리"하는 다른 코드에 의해 잡히거나(caught), 프로그램이 예기치 않게 종료된다. 

(예외를 잡는 것에 대해서는 나중에 더 자세히 말하겠다.)


자바 코드 조각이 실행 중에 어떤 종류의 문제를 발견하고 예외 객체를 던졌을(throw) 때 예외가 발생한다. 

예외 객체의 클래스에는 설명적인 이름을 지정하는 것이 편리하다. 

예를 들어 5개의 요소만 있는 시퀀스에서 열 번째 요소를 삭제하려고 하면 

코드가 BoundaryViolationException을 던질 수 있다. 

예를 들어 다음 코드 조각을 사용하여 이 작업을 수행할 수 있다:
(InsertIndex >= A.length) {

     throw new
     BoundaryViolationException("No element at index" +)
 insertIndex);
}
예외를 던져야 할 때 예외 객체를 인스턴스화하는 것이 편리한 경우가 많다. 

따라서 일반적으로 throw 문은 다음과 같이 작성된다:
throw new exception_type(param0, param1, ..., paramn-1);
exception_type: 예외의 타입

parami: 예외에 대한 생성자의 매개 변수의 리스트를 형성한다.


예외는 자바 런타임 환경 자체에서도 발생한다. 

예를 들어 위의 예제와 대응되는 것이 ArrayIndexOutOfBoundsException이다. 

만약 우리가 6개의 요소를 가진 배열을 가지고 있고 아홉 번째 요소를 요구한다면, 

자바 런타임 시스템은 이 예외를 발생시킬 것이다.


Throws 절

메소드가 선언되면, 메소드가 던질 수 있는 예외들을 지정하는 것이 적절하다. 

이 규칙은 기능적인 목적과 정중한 목적을 동시에 가지고 있다. 

우선, 그것은 사용자들에게 무엇을 기대해야 하는지 알려준다. 

그것은 또한 자바 컴파일러에게 어떤 예외들을 준비해야 하는지 알려준다. 

다음은 그러한 메소드 정의의 한 예이다:
public void goShopping( ) throws

ShoppingListTooSmallException,
                 OutOfMoneyException {
     // method body...
}


메소드에 의해 던져질 수 있는 모든 예외를 명시함으로써, 

우리는 이 메소드를 사용함으로써 발생할 수 있는 모든 예외적인 경우를 다른 사람이 처리할 수 있도록 준비한다. 

예외를 선언하는 것의 또 다른 이점은 우리 메소드에서 그러한 예외를 포착할 필요가 없다는 것이다. 

때때로 이것은 다른 코드가 예외로 이어지는 상황을 야기하는 책임이 있는 경우에 적절하다.
다음은 "통과"되는 예외를 보여준다:
public void getReadyForClass() throws

ShoppingListTooSmallException,
                       OutOfMoneyException {
   goShopping( );  // I don't have to try or catch the exceptions
                               // which goShopping() might throw because
                                // GetReadyForClass() will just pass these along
   makeCookiesForTA();
 }
함수는 그것이 원하는 만큼 많은 예외를 던진다고 선언할 수 있다. 

그러한 목록은 던져질 수 있는 모든 예외가 같은 예외의 하위 클래스일 경우에 다소 단순화될 수 있다. 

이 경우에, 우리는 어떤 방법이 적절한 슈퍼 클래스를 던졌다고 선언하기만 하면 된다.


Throwable의 종류

Java는 Exception과 Error 클래스를 Throwable의 하위 클래스로 정의한다.

Throwable은 던지고 잡을 수 있는 모든 객체를 나타낸다. 

또한 클래스 RuntimeException을 Exception의 하위 클래스로 정의한다. 

Error 클래스는 메모리 부족과 같은 런타임 환경에서 발생하는 비정상적인 상태에 사용된다. 

Exception 클래스는 예외 계층의 루트이다. 

특수화된 예외(예: BoundaryViolationException)는 

Exception 또는 RuntimeException 중 하나의 하위 클래스로 정의되어야 한다. 

RuntimeException의 하위 클래스가 아닌 예외는

버릴 수 있는 모든 메소드의 throws 절에 선언되어야 한다.

 

2.3.2 예외 잡기

예외가 던져지면, 그 예외는 반드시 잡아내야 하고(caught),

그렇지 않으면 프로그램이 종료될 것이다. 

어떤 특정한 방법이든 예외는 호출 방법으로 전달되거나 그 방법으로 잡아낼 수 있다. 

예외를 잡아내면, 그것은 분석되고 처리될 수 있다. 

예외를 처리하는 일반적인 방법론은

예외를 던질지도 모르는 코드 조각을 실행하기 위해 "시도(try)"하는 것이다. 

만약 예외가 던져진다면, 

그 예외를 처리하는 코드를 포함하는

미리 정의된 catch 블록으로 제어의 흐름이 점프함으로써

그 예외를 잡아낸다(caught).
자바의 try-catch 블록에 대한 일반적인 구문은 다음과 같다:

try
    main_block_of_statements 

catch(exception_type1 variable1)
    block_of_statements1 

catch(exception_type2 variable2)
    block_of_statements2

...
finally
    block_of_statementsn

 

적어도 하나의 catch 부분이 있어야 하지만 finally 부분은 옵션이다. 

각 exception_typei: 일부 예외의 타입

각 변수 i: 유효한 Java 변수 이름


자바 런타임 환경에서는 main_block_of_statements 문의 블록을 실행하여 

이와 같은 try-catch 블록을 수행하기 시작한다. 

이 실행에서 예외가 발생하지 않으면 

선택적인 마지막 부분을 포함하지 않는 한 

전체 try-catch 블록의 마지막 줄 뒤에 첫 번째 문으로 제어 흐름이 계속된다. 

finally 부분이 존재하는 경우 예외가 발생하는지 여부에 관계없이 실행된다. 

따라서 이 경우 예외가 발생하지 않으면 

try-catch 블록을 통해 실행이 진행되고 마지막 부분으로 점프한 다음 

try-catch 블록의 마지막 줄 뒤에 첫 번째 문으로 계속 진행된다.

 

반면 main_block_of_statement문인 블록에서 예외가 발생하면

해당 시점에서 try-catch 블록의 실행이 종료되고 

exception_type이 입력된 예외와 가장 밀접하게 일치하는 catch 블록으로 실행이 점프한다. 

catch 문의 변수는 일치하는 catch 문의 블록에서 사용할 수 있는 예외 객체 자체를 참조한다. 

해당 catch 블록의 실행이 완료되면

제어 흐름이 선택적인 finally 블록(존재하는 경우)으로 전달되거나, 

finally 블록이 없으면

전체 try-catch 블록의 마지막 행 바로 뒤에 첫 번째 문으로 전달된다. 

그렇지 않으면 입력된 예외와 일치하는 catch 블록이 없으면 

선택적인 finally 블록(존재하는 경우)으로 제어가 전달되고 

예외가 존재하는 경우 다시 호출 메소드로 전환된다.
다음 예제 코드 조각을 생각해 보자:
int index = Integer.MAX_VALUE; // 2.14 Billion

try // This code might have a problem...
 {
   String toBuy = shoppingList[index];
}
catch (ArrayIndexOutOfBoundsException aioobx)
{
   System.out.println("The index "+index+" is outside the array.");
}


만약 이 코드가 던져진 예외를 잡지 못한다면, 

제어 흐름은 메소드를 즉시 종료하고

우리 메소드를 호출한 코드로 되돌아간다. 

거기서 자바 런타임 환경은 다시 캐치 블록을 찾을 것이다. 

이 메소드를 호출한 코드에 캐치 블록이 없으면, 

컨트롤의 흐름은 이것을 호출한 코드로 점프하는 등의 과정을 거치게 된다. 

결국 예외를 캐치한 코드가 없다면, 

자바 런타임 시스템(우리 프로그램의 제어 흐름의 원점)은 예외를 캐치할 것이다. 

이때 오류 메시지와 스택 추적이 화면에 출력되고 프로그램이 종료된다.

 

다음은 실제 런타임 오류 메시지이다:
java.lang.NullPointerException: Returned a null locator
at java.awt.Component.handleEvent(Component.java:900)
at java.awt.Component.postEvent(Component.java:838)
at java.awt.Component.postEvent(Component.java:845)
at sun.awt.motif.MButtonPeer.action(MButtonPeer.java:39)
at java.lang.Thread.run(Thread.java)

 

예외가 발견되면 프로그래머가 하고 싶어할 몇 가지가 있다.

한 가지 가능한 방법은 오류 메시지를 출력하여 프로그램을 종료하는 것이다.

예외를 처리하는 가장 좋은 방법이 무시하는 것인 흥미로운 경우도 있다

(이것은 빈 catch 블록을 가짐으로써 수행될 수 있다).

예외를 무시하는 것은

보통 프로그래머가 예외가 있었는지 없었는지를 신경 쓰지 않을 때 수행된다. 

예외를 처리하는 또 다른 합법적인 방법은

예외 조건을 더 정확하게 지정하는 또 다른 예외를 만들어 던지는 것이다.

다음은 이 접근 방식의 예이다:
catch(ArrayIndexOutOfBoundsException aioobx) {

  throw new ShoppingListTooSmallException(
         "Product index is not in the shopping list.");
    }


예외를 처리하는 가장 좋은 방법은 문제를 찾아 해결하고 실행을 계속하는 것이다.


2.4 인터페이스 및 추상 클래스

두 객체가 상호 작용하려면 각 객체가 받아들일 다양한 메시지,

즉 각 객체가 지원하는 메소드에 대해 "알아야 한다." 

이러한 "지식"을 강제하기 위해, 

객체 지향 설계 패러다임은 

클래스들이 그들의 객체들이 다른 객체들에게 제공하는 

애플리케이션 프로그래밍 인터페이스(API)

즉 단순히 인터페이스를 지정하도록 요구한다.

이 책의 데이터 구조에 대한 ADT 기반(ADT-based) 접근 방식(섹션 2.1.2 참조)에서는 

ADT를 정의하는 인터페이스를 타입 정의 및 이 타입의 메소드 모음으로 지정하고

각 메소드에 대한 인수는 지정된 타입이다. 

이 사양은 컴파일러나 런타임 시스템에 의해 적용되며 강력한 타이핑을 요구한다.

강력한 타이핑(strong typing): 실제로 메소드에 전달되는 매개 변수의 타입이 인터페이스에 지정된 타입과 엄격하게 일치해야 한다. 

인터페이스를 정의해야 하고 강력한 타이핑으로 정의를 적용해야 하는 것은 

프로그래머에게 부담을 주는 것은 인정하지만 이 부담은 제공하는 보상으로 상쇄된다. 

이는 캡슐화 원리를 적용하고 그렇지 않으면 눈에 띄지 않을 프로그래밍 오류를 자주 잡기 때문이다.

 

2.4.1 인터페이스 구현

자바에서 API를 시행하는 주요 구조 요소는 인터페이스이다. 

인터페이스(interface): 데이터도 없고 본문도 없는 메소드 선언의 모음

즉, 인터페이스의 메소드는 항상 비어 있다

(즉, 단순히 메소드 시그니처이다). 

클래스가 인터페이스를 구현할 때는 인터페이스에 선언된 모든 메소드를 구현해야 한다. 

이러한 방식으로 인터페이스는 구현 클래스가 특정한 시그니처를 가진 메소드를 가져야 하는 요구 사항을 시행한다.


예를 들어, 우리가 소유하고 있는 다양한 종류의 물건들과 

다양한 성질을 가진 골동품들의 리스트를 만들고 싶다고 가정해 보자. 

예를 들어, 우리는 다음과 같은 이유로 당사의 객체 중 일부를 판매 가능한 것으로 식별하고자 하는데, 

이러한 경우 코드 조각 2.8에 표시된 Sellable 인터페이스를 구현할 수 있다.

 

그런 다음 코드 조각 2.9

Sellable 인터페이스를 구현하는 구체적인 클래스인

Photograph 클래스를 정의할 수 있으며,

이는 Photograph Objects를 판매할 의사가 있음을 나타낸다: 
이 클래스는 필요에 따라 Sellable 인터페이스의 각 메소드를 구현하는 객체를 정의한다.

여기에 Photograph objects에 특화되어 있는 메소드인 isColor를 추가한다.


우리 컬렉션의 또 다른 종류의 객체는 우리가 전송할 수 있는 것일 수도 있다. 

그런 객체에 대해서는 코드 조각 2.10에 나와 있는 인터페이스를 정의한다.
코드 조각 2.8: 인터페이스 Sellable.

/** Interface for objects that can be sold. */
public interface Sellable {
  
  /** description of the object */
  public String description();
  
  /** list price in cents */
  public int listPrice();
  
  /** lowest price in cents we will accept */
  public int lowestPrice();
}

 

코드 조각 2.9 : Sallable 인터페이스를 구현하는 클래스 Photograph.

/** Class for photographs that can be sold. */
public class Photograph implements Sellable {
  private String descript;   // description of this photo
  private int price;         // the price we are setting
  private boolean color;     // true if photo is in color
  
  public Photograph(String desc, int p, boolean c) { //constructor
    descript = desc;
    price = p;
    color = c;
  }
  
  public String description() { return descript; }
  public int listPrice() { return price; }
  public int lowestPrice() { return price/2; }
  public boolean isColor() { return color; }
}


코드 조각 2.10: 인터페이스 Transportable.

/** Interface for objects that can be transported. */
public interface Transportable {  
  /** weight in grams */
  public int weight();  
  /** weather the object is hazardous */
  public boolean isHazardous();
}

 

그런 다음 판매, 포장 및 배송이 가능한 기타 골동품에 대해 

코드 조각 2.11에 표시된 클래스 BoxedItem을 정의할 수 있다. 

따라서 클래스 BoxedItem은 

Sellable 인터페이스와 Transportable 인터페이스의 방법을 구현하는 동시에 

상자형 배송의 보험 가치를 설정하고

배송을 위한 상자의 크기를 설정하는 전문화된 방법을 추가한다.
코드 조각 2.11 : Class Boxed Item.

/** Class for objects that can be sold, packed, and shipped */
public class BoxedItem implements Sellable, Transportable {
  private String descript;   // description of this item
  private int price;         // list price in cents
  private in weight;         // weight in grams
  private boolean haz;       // true if object is hazardous
  private int height=0;      // box height in centimeters
  private int width=0;       // box width in centimeters
  private int depth=0;       // box depth in centimeters
  /** Constructor */
  public BoxedItem(String desc, int p, int w, boolean h) { //constructor
    descript = desc;
    price = p;
    weight = w;
    haz = h;
  }
  
  public String description() { return descript; }
  public int listPrice() { return price; }
  public int lowestPrice() { return price/2; }
  public int weight() { return weight; }
  public boolean isHazardous() { return haz; }
  public int insuredValue() { return price*2; }
  public void setBox(int h, int w, int d){
    height = h;
    width = w;
    depth = d;
  }
}


클래스 BoxedItem은 Java에서 클래스와 인터페이스의 또 다른 기능을 보여준다. 

클래스는 여러 인터페이스를 구현할 수 있기 때문에 

여러 API를 준수해야 하는 클래스를 정의할 때 매우 유연하다. 

예를 들어, Java에서 클래스는 하나의 다른 클래스만 확장할 수 있지만 많은 인터페이스를 구현할 수 있다.

 

2.4.2 인터페이스에서의 다중 상속

다중 상속(multiple inhereitance): 둘 이상의 클래스에서 확장할 수 있는 기능

자바에서는 인터페이스에 다중 상속을 허용하지만 클래스에는 허용하지 않는다. 

이 규칙의 이유는

인터페이스의 메소드에는 바디가 없는 반면

클래스의 메소드에는 항상 바디가 없기 때문이다. 

따라서 자바에서 클래스에 다중 상속을 허용한다면 

동일한 시그니처를 가진 메소드가 포함된 두 클래스에서 클래스가 확장하려고 하면 혼란이 발생할 수 있다. 

그러나 인터페이스에는 메소드가 비어 있으므로 이러한 혼란이 발생하지 않는다. 

따라서 혼란이 수반되지 않고 인터페이스의 다중 상속이 유용한 경우가 있기 때문에 

자바에서는 인터페이스에 다중 상속을 사용할 수 있다.


인터페이스의 다중 상속을 위한 한 가지 용도는

mixin이라고 하는 다중 상속 기법을 근사화하는 것이다. 

자바와 달리 Smalltalk나 C++와 같은 일부 객체 지향 언어에서는 

인터페이스뿐만 아니라 구체적인 클래스의 다중 상속을 허용한다. 

이러한 언어에서는 mixin 클래스라고 불리는 클래스를 정의하는 것이 일반적인데, 

이 클래스는 독립형 객체로 만들어지기 위한 것이 아니라

기존 클래스에 추가적인 기능을 제공하기 위한 것이다. 

그러나 Java에서는 이러한 상속이 허용되지 않으므로

프로그래머들은 인터페이스와 근사화해야 한다. 

특히 인터페이스의 다중 상속은 

둘 이상의 관련 없는 인터페이스에서 메소드를 "혼합"하여

해당 메소드의 기능을 결합하는

인터페이스를 정의하는 메커니즘으로 사용할 수 있으며, 

추가적으로 더 많은 메소드를 추가할 수도 있다. 

 

이전 객체의 예를 다시 살펴 보면,

다음과 같이 비보험적인 항목에 대한 인터페이스를 정의할 수 있다:
public interface InsurableItem extends Transportable, Sellable{
    /** 보험에 가입된 가치를 센트 단위로 반환 */
    public int insuredValue();

}


이 인터페이스는

Transportable 인터페이스의 메소드와 Sellable 인터페이스의 메소드를 혼합하고

추가 방법인 insuredValue를 추가한다.

 

이러한 인터페이스를 통해 다음과 같이 BoxedItem을 번갈아 정의할 수 있다:

public class BoxedItem2 implements InsurableItem {

    / / ... 클래스 BoxedItem과 동일한 코드를 구현한다
}
이 경우 insuredValue라는 방법은 선택 사항이 아니지만 다음과 같다
이전에 제공된 BoxedItem 선언에서 선택사항이다.

 

mixin에 근접한 자바 인터페이스로는 아래와 같다:

java.lang.Cloneable: 클래스에 복사 기능을 추가한다.
java.lang.Comparable: 클래스에 비교 기능을 추가한다(해당 인스턴스에 자연스러운 순서를 지정)
java.util.Observer: 특정 "관찰 가능한" 객체가 상태를 변경할 때 알림을 받으려는 클래스에 업데이트 기능을 추가한다.

 

2.4.3 추상적 수업과 강력한 타이핑

추상 클래스: 방법 및/또는 인스턴스 변수의 구체적인 정의뿐만 아니라

빈 메소드 선언(즉, 본문이 없는 메소드 선언)을 포함하는 클래스

 

따라서 추상 클래스는 인터페이스와 완전한 구체적인 클래스 사이에 놓여 있다. 

인터페이스와 마찬가지로 추상 클래스는 인스턴스화되지 않을 수 있으며, 

즉 추상 클래스로부터 어떤 객체도 생성될 수 없다. 

 

추상 클래스의 하위 클래스는 그 자체가 추상적이지 않은 한

그 슈퍼 클래스의 추상적인 방법에 대한 구현을 제공해야 한다.

그러나 추상 클래스 A는

구체적인 클래스와 마찬가지로

또 다른 추상 클래스를 확장할 수 있으며, 

추상적이고 구체적인 클래스는 A를 더 확장할 수도 있다. 

궁극적으로 추상적이지 않은 새로운 클래스를 정의해야 한다.

그 새로운 클래스는 추상적인 슈퍼 클래스를 확장(하위 클래스)해야 한다.

그리고 이 새로운 클래스는 모든 추상적인 방법에 대한 코드를 입력해야 한다. 

따라서 추상 클래스는 상속의 사양 스타일을 사용하지만

전문화 및 확장 스타일도 허용한다(2.2.3절 참조).

 

java.lang.Number 클래스

우리는 이미 추상 클래스의 예를 본 것으로 나타났다. 

즉, Java number 클래스(표 1.2)는 java.lang.Number라는 추상 클래스를 전문으로 한다. 

java.lang.Integer 및 java.lang.Double과 같은 각 구체적인 숫자 클래스는 

java.lang.Number 클래스를 확장하고

슈퍼 클래스의 추상적인 메소드에 대한 세부 정보를 채운다. 

특히, intValue, floatValue, doubleValue 및 longValue의 메소드는 모두 java.lang.Number이다. 

각 구체적인 숫자 클래스는 이러한 메소드의 세부 정보를 지정해야 한다.


강력한 타이핑

자바에서 객체는 다양한 종류의 존재로 볼 수 있다. 

객체 o의 주요 타입은 o가 인스턴스화된 시점에 지정된 클래스 C이다. 

또한 o는 C의 각 슈퍼 클래스 S에 대해 S의 타입이며

C가 구현한 각 인터페이스에 대해 I의 타입이다.


그러나 변수는 한 가지 타입(클래스 또는 인터페이스)으로만 선언될 수 있으며, 

이는 변수가 사용되는 방식과 특정 메소드가 변수에 작용하는 방식을 결정한다. 

마찬가지로 메소드에는 고유한 반환 타입이 있다.

일반적으로 수식에는 고유한 타입이 있다.


Java는 모든 변수를 입력하고 메소드가 예상되는 타입을 선언하여 반환하도록 적용함으로써 

강력한 타이핑을 사용하여 버그를 방지한다.

 

그러나 타입에 대한 엄격한 요구사항들로, 

때때로 타입을 다른 타입으로 변경하거나 변환할 필요가 있다. 

그러한 변환들은 명시적인 캐스팅(cast) 연산자에 의해 지정되어야만 할 수도 있다.

 

우리는 이미 기본 타입에 대한 변환 및 캐스팅이 어떻게 작동하는지에 대해 논의했다(섹션 1.3.3).

다음으로 참조 변수에 대해 어떻게 작동하는지에 대해 논의한다.

 

2.5 캐스팅 및 제네릭

이 섹션에서는 참조 변수 간의 캐스팅과 많은 경우 

명시적인 캐스팅을 피할 수 있는 제네릭(generics)이라는 기법에 대해 논의한다.

 

2.5.1 캐스팅

객체에 대한 타입 변환 방법으로 논의를 시작한다.


확장 변환

확장 변환(widening conversion): T 타입을 "넓은" U 타입으로 변환할 때 발생한다. 

다음은 확장 변환의 일반적인 경우이다:
• T와 U는 클래스 타입이고 U는 T의 슈퍼 클래스이다
• T와 U는 인터페이스 타입이고 U는 T의 수퍼 인터페이스이다
• T는 인터페이스 U를 구현하는 클래스이다.
확연한 캐스팅 없이 식의 결과를 변수로 저장하기 위해 확장 변환이 자동으로 수행된다. 

따라서 T에서 U로의 변환이 확장 변환일 때

타입 T의 결과를 타입 U의 변수 v로 직접 할당할 수 있다. 

 

아래 예제 코드 조각은

정수형(새로 구성된 Integer 객체)의 식을

Number 타입의 변수로 할당할 수 있음을 보여준다.
Integer i = new Integer(3);
Number n = i; // 정수에서 Number로 확장 변환

확장 변환의 정확성은 컴파일러에 의해 확인될 수 있으며, 

그 유효성은 프로그램 실행 중 자바 런타임 환경에 의한 테스트를 필요로 하지 않는다.

 

축소 변환

축소 변환(narrowing conversion): T형이 "더 좁은"형 S형으로 변환될 때 발생한다. 

다음은 축소 변환의 일반적인 경우이다:

• T와 S는 클래스 타입이고 S는 T의 하위 클래스이다
• T와 S는 인터페이스 타입이고 S는 T의 하위 인터페이스이다
• T는 클래스 S에 의해 구현된 인터페이스이다.
일반적으로 참조 타입의 축소 변환은 명시적인 캐스팅가 필요하다. 

또한, 축소 변환의 정확성은 컴파일러에 의해 확인되지 않을 수 있다. 

따라서 프로그램 실행 중 자바 런타임 환경에서 유효성을 테스트해야 한다.


아래 예제 코드 조각은 캐스트를 사용하여

Number 타입에서 Integer 타입으로 축소 변환을 수행하는 방법을 보여준다.
Number n = new Integer(2); // 정수에서 숫자로 확장 변환
Integer i = (Integer) n; // 숫자에서 정수로 축소 변환

 

첫 번째 문에서는 Number 타입의 변수 n에 Integer 클래스의 객체가 새로 생성되어 할당된다. 

따라서 이 할당에서는 확장 변환이 발생하므로 캐스팅이 필요하지 않다. 

두 번째 문에서는 캐스트를 사용하여 Integer 타입의 변수 i에 n을 할당한다. 

이 할당이 가능한 이유는 n이 Integer 타입의 객체를 참조하기 때문이다. 

그러나 변수 n은 Number 타입이므로 축소 변환이 발생하여 캐스팅이 필요하다.

 

캐스팅 예외

자바에서는 객체 o가 실제로 S 타입이라면

T 타입의 객체 참조 o를 S 타입으로 캐스트할 수 있다. 

반면 객체 o가 S 타입이 아닌 경우에는

S 타입으로 캐스트하려고 하면 ClassCastException이라는 예외가 발생한다. 

 

다음 코드 조각에서 이 규칙을 설명한다:
Number n;
Integer i;
n = new Integer(3);

i = (Integer) n; // 합법적이다
n = new Double(3.1415);
i = (Integer) n; // 불법이다!

이와 같은 문제를 방지하고 캐스팅을 수행할 때마다

코드에 try-catch 블록을 추가하는 것을 방지하기 위해 

Java는 객체 캐스팅이 올바른지 확인하는 방법을 제공한다.

 

instanceof : 객체 변수가 특정 클래스의 객체를 참조하는지

(또는 특정 인터페이스를 구현하는지) 테스트할 수 있는 연산자

 

 

이 연산자를 사용하기 위한 구문은

object_reference instanceof reference_type

object_reference: 객체 참조에 대해 평가하는 표현

reference_type: 일부 기존 클래스, 인터페이스 또는 열거의 이름이다(섹션 1.1.3).

 

만약 object_reference가 reference_type의 인스턴스라면

위의 식을 true로 반환한다.

그렇지 않으면 false를 반환한다.

 

따라서 다음과 같이 수정하여 위의 코드 조각에

ClassCastException가 포함되는 것을 방지할 수 있다:
Number n;
Integer i;
n = new Integer(3);
if (n instanceof Integer)
  i = (Interger) n; // 이것은 합법적인 

n = new Double(3.1415);
if (n instanceof Integer)
  i = (Integer) n; // 시도를 안 할 것이다

 

인터페이스를 사용한 캐스팅

인터페이스를 사용하면 객체가 특정 메소드를 구현하도록 강제할 수 있지만 

구체적인 객체와 인터페이스 변수를 사용하려면 캐스팅이 필요한 경우가 있다. 

코드 조각 2.12에 나와 있는 것처럼 Person 인터페이스를 선언한다고 가정한다. 

Person 인터페이스의 equalTo와 같은 메소드는 Person 타입의 매개 변수 하나를 사용한다. 

따라서 Person 인터페이스를 구현하는 모든 클래스의 객체를 이 메소드에 전달할 수 있다.
코드 조각 2.12 : 인터페이스 Person.

public interface Person {  
  public boolean equalTo(Person other); // is this the same person?
  public String getName(); // get this person's name
  public int getAge(); // get this person's age
}


코드 조각 2.13에서 Person을 구현하는 클래스인 Student를 보여준다. 

equalTo는 인수(Person 타입의 declared)도 Student 타입이라고 가정하고 

캐스팅을 사용하여

Person 타입(인터페이스)에서 Student 타입(클래스)으로 축소 변환을 수행한다. 

이 경우 변환이 허용되는데, 

클래스 T에서 인터페이스 U로 축소 변환이므로 

T는 S(또는 T = S)를 확장하고 S는 U를 구현한다.
코드 조각 2.13 : 인터페이스 Person을 구현하는 클래스 학생.

public class Student implements Person {
  String id;
  String name;
  int age;
  public Student (String i, String n, int a) { // simple constructor
    id = i;
    name = n;
    age = a;
  }
  protected int studyHours() { return age/2; } // just a guess
  public String getID() { return id; } // ID of the student
  public String getName() { return name; } // from Person interface
  public int getAge() { return age; } // from Person interface
  public boolean equalTo(Person other){ // from Person interface
    Student otherStudent = (Student) other; // cast Person to Student
    return (id.equals (otherStudent.getID())); // compare IDs
  }
  public String toString() { // for printing
    return "Student(ID: " + id +
    ", Name: " + name + 
    ", Age : " + age + ")";
  }  
}


메소드 equalTo 구현에서 위의 가정으로 인해 

클래스 Student의 객체를 사용하는 응용 프로그램이 

다른 타입의 객체와 Student 객체를 비교하려고 시도하지 않거나 

메소드 equalTo의 캐스팅이 실패하는지 확인해야 한다. 

예를 들어, 응용 프로그램이 Student 객체의 디렉토리를 관리하고 

다른 타입의 Person 객체를 사용하지 않으면 가정이 충족된다.

 

인터페이스 타입에서 클래스 타입으로 축소 변환을 수행하는 기능을 통해 

저장된 요소에 대한 최소한의 가정만 하는 일반적인 종류의 자료 구조를 작성할 수 있다. 

코드 조각 2.14에서는 Person 인터페이스를 구현하는 

객체 쌍을 저장하는 디렉토리를 구축하는 방법을 설명한다. 

remove 메소드는 디렉토리 내용에 대한 검색을 수행하여 지정된 person 쌍이 존재하는 경우 

이를 제거하고 findOther 메소드와 마찬가지로 equalTo 메소드를 사용한다.
코드 조각 2.14 : 클래스 PersonPair 디렉토리 스케치.

public class PersonPairDirectory {
  // ... instance variables would go here ...
  public PersonPairDirectory () { /* defualt constructor goes here */}
  public void insert(Person person, Person other) { /* insert code goes here */ }
  public Person findOther(Person person) { return null; } // stub for find
  public void remove(Person person, Person other) { /* remove code goes here */ } 
}


  이제, 룸메이트 쌍을 나타내는 Student 객체 쌍으로 myDirectory를 채웠다고 가정해 보겠다.

주어진 Student 객체인 smart_one의 룸메이트를 찾기 위해 다음을 수행할 수 있다(잘못됨):
   Student cute_one = myDirectory.findOther(smart_one); // wrong!
위의 문장은 "explicit-cast-required" 컴파일 오류를 발생시킨다. 

여기서 문제는 명시적인 캐스트 없이 축소 변환을 수행하려고 한다는 것이다. 

즉, 메소드 findOther에 의해 반환되는 값이 Person 타입이다.

반면 할당된 변수 cute_one은

인터페이스 Person을 구현하는 클래스인 좁은 범위의 Student 타입이다. 

 

따라서, 저희는 다음과 같이 캐스트를 사용하여

Person 타입을 Student로 변환한다:
   Student cute_one = (Student)
 myDirectory.findOther(smart_one);

myDirectory.findOrder에 대한 호출이

정말로 Student 객체를 제공하는 것을 확인하는 한

메소드에 의해 반환된 Person 타입의 값에서

Student 타입의 값으로 캐스팅하는 것은 잘 작동한다.

 

일반적으로 인터페이스는 일반적인 자료 구조를 설계하는 데 유용한 도구가 될 수 있으며, 

이는 캐스팅을 사용하여 다른 프로그래머에 의해 특화될 수 있다.

 

2.5.2 제너릭

자바는 5.0부터 많은 명시적인 캐스트를 피하는 방식으로 추상적인 타입을 사용하기 위한 

제네릭 프레임워크(generic framework)를 포함하고 있다. 

제네릭 타입(generic type): 컴파일 시 정의되지 않지만 실행 시 완전히 지정되는 타입

제네릭 프레임워크를 통해

클래스를 형식 타입 매개변수(formal type parameter)의 집합의 관점에서 정의할 수 있으며, 

예를 들어 클래스의 일부 내부 변수의 타입을 추상화하는 데 사용할 수 있다. 

각도 괄호는 형식 타입 매개변수 리스트를 둘러싸는 데 사용된다. 

형식 타입 매개변수에 유효한 식별자가 사용될 수 있지만 

기존에는 단일 문자 대문자 이름이 사용된다. 

이러한 매개변수화된 타입으로 정의된 클래스가 주어지면 

실제 타입 매개변수(actual type parameter)를 사용하여

사용할 구체적인 타입을 표시함으로써

클래스의 객체를 인스턴스화한다.


코드 조각 2.15에서는 키-값 쌍을 저장하는 클래스 Pair을 보여주는데, 

여기서 키와 값의 타입은 각각 매개 변수 K와 V로 지정된다. 

주요 방법은 이 클래스의 두 가지 인스턴스를 만든다. 

하나는 String-Integer 쌍(예: 차원과 값을 저장하는 것)이고 

다른 하나는 Student-Double 쌍(예: 학생에게 주어진 등급을 저장하는 것)이다.

 

코드 조각 2.15: 코드 조각 2.13의 학생 클래스를 사용한 예.

public class Pair<K, V> {
  K key;
  V value;
  public void set(K k, V v) {
    key = k;
    value = v;
  }
  public K getKey() { return key; }
  public V getValue() { return value; }
  public String toString() {
    return "[" + getKey() + ", " + getValue() + "]";
  }
  public static void main (String[] args) {
    Pair<String, Integer> pair1 = new Pair<String, Integer>();
    pair1.set(new String("height"), new Integer(36));
    System.out.println(pair1);
    Pair<String, Integer> pair2 = new Pair<String, Double>();
    pair2.set(new String("A5976", "Sue", 19), new Double(9.5));
    System.out.println(pair2);


 이 방법의 실행 결과는 다음과 같다:

[height, 36]
[Student(ID: A5976, Name: Sue, Age: 19), 9.5]

 

 

이전 예제에서 실제 타입 매개 변수는 임의의 타입일 수 있다. 

실제 매개 변수의 타입을 제한하기 위해 아래와 같이 extends 절을 사용할 수 있다. 

여기서 클래스 PersonPairDirectoryGeneric은 클래스 Person을 확장한다고 명시함으로써 

부분적으로 지정된 제너릭 타입 매개 변수 P로 정의된다.

public class PersonPairDirectoryGeneric<P extends Person> {
  //... instance variables would go here ...
  public PersonPairDirectoryGeneric() { /* default constructor goes here */ }
  public void insert (P person, P other) { /* insert code goes here */ }
  public P findOther (P person) { return null; } // stub for find
  public void remove (P person, P other) { /* remove code goes here */ }
}

 

 

이 클래스는 코드 조각 2.14의 클래스 PersonPairDirectory와 비교되어야 한다. 

위 클래스가 주어지면 Student 타입의 객체 쌍을 저장하는 

PersonPairDirectoryGeneric의 인스턴스를 참조하는 변수를 선언할 수 있다:

PersonPairDirectoryGeneric<Student>
myStudentDirectory;


이러한 경우 메소드 findOther가 Student 타입의 값을 반환한다. 

따라서 캐스트를 사용하지 않는 다음 문장은 정확하다:

Student cute_one =
myStudentDirectory.findOther(smart_one);

 

 

제너릭 프레임워크를 사용하면 일반적인 버전의 메소드를 정의할 수 있다. 

이 경우 메소드 수식어 중 일반적인 정의를 포함할 수 있다. 

예를 들어 두 쌍 객체의 키가 Comparable 인터페이스를 구현하는 경우 

두 Pair 객체의 키를 비교할 수 있는 메소드의 정의를 아래에 나타낸다:

public static <K extends Comparable,V,L,W> int comparePairs(Pair<K,V> p, Pair<L,W> q) {
  return p.getKey().compareTo(q.getKey()); // p's key implements compare To
}


일반 타입과 관련된 중요한 주의사항이 있는데, 

즉 배열에 저장된 요소는 타입 변수나 매개 변수 타입이 될 수 없다. 

Java에서는 매개 변수 타입으로 배열을 정의할 수 있지만 

매개 변수 타입을 사용하여 새 배열을 만드는 것은 허용하지 않는다. 

다행히 매개 변수 타입으로 정의된 배열을

새로 생성된 매개변수가 아닌 배열로 초기화할 수 있다. 

그러나 이 후자의 메커니즘은

Java 컴파일러가 100% 타입 안전하지 않으므로 경고를 발행하도록 한다. 

다음에서 이 점을 설명한다:

public static void main(String[] args) { 
  Pair<String,Integer>[] a = new Pair[10]; // right, but gives a warning
  Pair<String,Integer>[] b = new Pair<String,Integer>[10]; // wrong
  a[0] = new Pair<String,Integer>(); // this is completely right
  a[0].set("Dog",10); // this and the next statement are right too
  System.out.println("First pair is "+a[0].getKey()+",
    "+a[0].getValue());
}