2017년 7월 7일 금요일

[Java] enum(Enumeration) 클래스

비교를 위한 상수가 필요하다면 어떻게 할까, 예를들어 옷집의 옷사이즈 small, medium, large 사이즈가 있다고 하자. small = 1, medium = 2, large = 3 이런식으로 정수형 상수로 대체하는것도 한가지 방편이다. 하지만 이런 방식은 상수(Constant)가 많아지면 많아질수록 사용자의 실수로 인해 에러를 만들 가능성이 존재한다. 또 코드를 수정하게 되면 엄청 골치아플것이다. 그래서 Java SE 1.5버젼부터 더 좋은 enum 클래스가 만들어졌다.
만약 무언가를 비교하기 위한 상수가 필요하다면 enum 클래스를 이용해 상수를 생성할 수 있다.


public enum Size {
    SMALL, MEDIUM, LARGE;
}
간단하게 Small 사이즈, Medium 사이즈, Large 사이즈를 위한 상수를 생성했다. 그냥 3줄짜리 클래스인데 가능하다. 이 enum 클래스에서 생성되는 인스턴스는 위 코드에
열거된 3개뿐이고, 이런 enum 클래스를 이용해서 새로운 객체를 생성하는것은 불가능하다. 그래서 enum 클래스의 인스턴스를 비교할려면 equals 같은 메소드를 쓰지 않고 그냥 연산자 "=="로 쉽게 비교가 가능하다.


Size x = Size.SMALL;
Sise y = Size.SMALL;
Size z = Size.LARGE;
x==y // true
x==z // false

또 원한다면 생성자 함수와 원하는 데이터 필드를 추가해 enum 클래스를 약간 손볼수도 있다. 다만 생성자 함수는 열거된 상수(Constant)들이 생성될때만 한번 호출된다. 그렇기 때문에 컴파일 시점이 지난 후에는 호출할 수 없다. 물론 accessor 메소드들은 언제든 접근이 가능하다.


public enum Size {
    SMALL("S"), MEDIUM("M"), LARGE("L");
    
    private String keyword;
    private Size(String k){
    this.keyword = k;
    }
    public String getKeyword(){
    return this.keyword;
    }
}

모든 열거된 형(Small, Medium, Large)들이 Enum 클래스를 상속한다. 그렇기 때문에 Enum 클래스에서 정의된 메소드들을 사용할 수 있다. 가장 자주 사용되는것은 toString 메소드로, 해당 타입의 이름을 반환한다. Size.SMALL.toString은 String 타입 "SMALL"을 반환하게 된다. 물론 오버라이딩해서 새로 정의할수도 있다. 이걸 반대로 이용하는 메소드는 valueOf 메소드 이다. 파라미터로 원하는 enum 클래스와 해당 타입의 이름값을 주면 그 상수를 반환한다.


Size.SMALL.toString(); // SMALL
Size.LARGE.toString(); // LARGE
Size.MEDIUM.toString(); // MEDIUM
Size x = Enum.valueOf(Size.class, "SMALL"); //Size.SMALL
Size y = Size.SMALL;
x == y // true

모든 enum 인스턴스는 static 속성을 가진 values 메소드를 가지고 있다. 이 메소드는 해당 클래스에 열거된 상수들의 배열을 반환한다. 또한 ordinal 메소드는 상수들의 배열에서 각 상수의 인덱스를 반환한다.


Size[] values = Size.values(); // {Size.SMALL, Size.MEDIUM, Size.LARGE}를 반환한다.
Size.MEDIUM.ordinal(); //1을 반환함
Size.SMALL.ordinal(); //0을 반환함
Size.LARGE.ordinal(); //2을 반환함

2017년 7월 6일 목요일

[Java] 추상 클래스(Abstract Class)와 추상 메소드(Abstract Method)

상속(Inheritance)이란 개념에 대해 조금 더 이야기해보자. 클래스가 클래스를 상속할때, 상위클래스로 올라가면 올라갈수록 클래스가 보편적(General)이게 되고 그렇기 때문에 더 추상적(Abstract)이게 된다. 어떠한 한 지점에 도달하게되면, 상위 클래스는 너무 보편적인 클래스가 되어버려 어떤 객체가 상속해도 될정도로 보편적인 기능들을 가지게 된다. 만일 우리가 회사 인사업무 프로그램을 설계한다면 Person 클래스는 매우 보편적인 객체가 되어버려, Employee, Manager, Executive, Boss, Intern 등의 어떤 클래스든 상속할수 있는 그런 객체가 될것이다. 자바 프로그램에서는 Object라는 클래스가 모든 클래스의 상위 클래스가 된다.

이런 상위클래스의 추상성과 보편성을 이야기하는 이유는, 어떤 분야의 객체들을 설계할 때, 그 객체들이 모두다 가지는 공통기능이 있을것이고, 이런 공통 기능들을 모은 최상위 수퍼클래스를 설계함으로 하위 클래스들의 부담을 줄일 수 있기 때문이다. 예를들어, 학교 인사업무 프로그램에서 Person 클래스는 모든 종류의 직원 클래스들과 학생 클래스들의 최상위 클래스가 된다. Person 클래스를 상속하는 Student, Employee 클래스, Employee 클래스를 상속하는 Professor, Manager, Executive 클래스, 뭐 이런식으로 계속 계층적 구조가 이어질것이다. 이 경우에 모든 직원 클래스들은 이름이란 데이터 필드를 가지게 되고, name 필드와 이에대한 acccesor, mutator 메소드들은 Person 클래스에 존재해도 잘 작동하게 된다. 그리고 모든 섭클래스마다 name 필드와 메소드들을 추가해줄 수고도 덜게된다.

그런데 만약에 Student, Employee, Professor 등의 클래스의 각 객체의 정보를 종합해 반환하는 getInfo 메소드가 필요할때, 이 getInfo 메소드를 Person 클래스에 추가해야될까? Person 클래스는 단순히 "이름" 정보만을 가지고 있는 클래스이고 getInfo 메소드가 반환해야하는 정보를 종합하기에는 가진 정보가 많이 부족한 클래스이다. 하지만 섭클래스들은 이 메소드를 무조건 가져야하고 우리는 최상위 수퍼 클래스인 Person 클래스에 이 메소드를 추가함으로써 Person 타입에서도 해당 메소드를 호출할수있게 해야할 필요가 있다. 물론 getInfo 메소드가 그냥 이름만을 반환하거나 빈 String 객체를 반환하도록 해도 된다. 하지만 더 좋은 방법이 있다. abstract 키워드를 이용해 해당 메소드를 추상메소드로 정의한다면, 메소드의 내용을 적지 않아도 Person 타입 변수에서 해당 메소드를 호출하는게 가능하다. 그리고 이런 abstract 메소드를 하나라도 가진 클래스는 abstract 클래스가 되게되고 abstract 키워드를 클래스 선언에 포함해야된다.


public abstract class Person {
 private String name;
 private int age;
        ... 
 public abstract void getInfo();
        ...
}

abstract 메소드가 섭클래스에서 정의될 메소드들을 미리 기안을 짜놓는다고 생각하면 된다. abstract 클래스를 상속할때, 섭클래스는 abstract 메소드들을 정의하거나 아니면 정의되지 않은 상태로 놔둘 수 있다. 하지만 정의하지 않은 abstract 메소드가 있다면 섭클래스도 abstract 클래스가 된다. 추상(abstract) 클래스에서는 인스턴스 생성이 불가능하다. 즉 우리는 Person 클래스의 생성자 함수를 호출해 Person 타입 객체를 생성하지 못한다. 물론 Person 클래스의 섭클래스 중 abstract 클래스가 아닌 클래스에서는 생성이 가능하고, 생성된 객체를 Person 타입으로 취급할 수도 있다.


Person p = new Person("Jason", 17); // 에러가 발생한다
Person s = new Student("Jenny", 21); // 정상 작동한다.
System.out.println(s.getInfo()); // 정상 작동한다.

이때 추상형(Abstract Type) 변수 s는 비추상클래스인 Student 클래스의 인스턴스를 참조하므로 정상 작동한다. 그리고 Student 클래스에서 getInfo 메소드도 정의했을테니 Person 타입 변수 s를 통해서 getInfo 메소드도 호출 가능하다. 만약 Person 클래스에 추상 메소드(Abstract Method) getInfo가 없었다면 호출이 불가능했을것이다.

[Java] 메소드 호출(Method Call)과 다이나믹 바인딩(Dynamic Binding)

우리가 메소드를 호출할 때 어떤 일이 일어날까, 왜 수퍼클래스의 메소드를 호출했는데 상속받은 객체의 메소드가 호출되는지를 알려면 자바(Java)에서의 메소드 호출법을 이해해야한다. 예를들어 우리가 클래스 c의 인스턴스인 x 객체의 f(y)라는 함수를 호출한다 가정할 때, x.f(y)이런식으로 호출할것이다. 이때 우리눈에 보이지 않는 컴퓨터세상에서는 어떤일이 일어나는지를 알아보자.
  1. 컴파일러가 선언된 객체의 형(type)과 메소드 이름을 찾아본다. 아마 파라미터만 다르고 f 라는 이름을 가진 메소드들이 많을것이다. 컴파일러는 클래스 c와 c의 수퍼클래스의 모든 메소드f 를 종합한다. 이제 컴파일러가 호출 대상 메소드들을 모두 종합했다.
  2. 컴파일러가 메소드 호출시 제공된 아규먼트(argument)들의 형(type)을 분석한다. 제공된 아규먼트들과 일치하는 파라미터를 가진 메소드 f가 있다면 해당 메소드를 호출한다. 이 과정을 overloading resolution이라고 한다. 참고로 overloading이란 이름은 같으나 파라미터가 틀린, 즉 method signature(메소드 이름+파라미터)가 틀린 메소드들을 선언하는것을 의미한다. 우리 가정상에서는 x가 String 타입일시 f(String x)라는 시그니쳐를 가진 메소드가 호출되고, x가 int 타입이라면 f(int x) 시그니쳐를 가진 메소드가 호출된다. 만약 double형에서 int형으로, 섭클래스 객체에서 수퍼클래스 객체로 타입변환이 필요하다면 이 과정은 조금 더 복잡해지게된다. 만약 컴파일러가 타입변환을 적용하고 서도 파라미터가 일치하는 메소드를 찾지 못했다면 에러가 발생한다. 그렇지 않다면, 컴파일러가 호출할 메소드의 이름과 파라미터 타입을 결정하게된다. 
  3. 만약 메소드가 private, static, final 속성을 가지거나, 생성자(Constructor) 메소드라면, 컴파일러가 바로 해당 메소드를 호출한다. 이 과정을 static binding이라고 한다.
  4. 만약 3번의 경우가 아니라면, 메소드는 implicit parameter, 즉 x 객체의 실제 타입에따라 결정된다.  이 과정을 dynamic binding이라고 한다.
  5. 프로그램이 실행되는동안 메소드 호출을 위해 dynamic binding이 진행될 때, 자바 가상머신은 implicit parameter, x가 참조하는 객체의 실제 타입에 맞는 메소드를 호출하게된다. x의 실제 타입이 클래스 X가 되고, 클래스 X는 클래스 W를 상속한다고 할때, 2번과정에서 찾은 메소드 f들 중에 클래스 X에 선언된 메소드 f가 있다면 그 메소드를 호출한다. 만약 아니라면, X가 상속하는 클래스, 즉 클래스 W에 선언된 메소드 f를 찾아본다. 그게 아니라면 하위 -> 상위 클래스로 선언된 메소드들을 찾아간다. 이 과정은 매우 많은 반복이 이뤄지는 과정이라, 가상머신이 미리 각 클래스의 메소드 시그니쳐들을 모아둔 데이터베이스를 작성하고, 실제로는 데이터베이스를 찾아보는 형식으로 진행된다. 
이게 간략하게 요약한 메소드호출의 과정이다. 물론 더 세세하게 파고든다면 엄청 긴 과정이되겠지만... 

다이나믹 바인딩(Dynamic Binding)은 매우 중요한 특징이다. 다이나믹 바인딩은 원래 있던 코드를 수정하지 않고도 프로그램을 확장할 수 있는 확장성을 준다. 언제든 상속(Inheritance)을 통해 설계를 확장할 수 있기 때문인데, 다이나믹 바인딩이 없다면 메소드 오버라이딩(method overriding)과 같은 상속만의 특성도 작동하기 때문이다.

2017년 7월 5일 수요일

[Java] 상속(Inheritance)과 다형성(Polymorphism)

상속(Inheritance)


객체지향프로그래밍(Object Oriented Programming)에서 중요한 개념인 상속(Inheritance)은 말그대로 객체가 다른 객체를 상속하는 개념이다. 객체가 다른 객체를 상속함으로써, 상속받는 객체는 상속하는 객체의 데이터필드, 메소드에 접근해 사용할 수 있다. 자바도 객체지향 프로그래밍 언어인만큼 상속(Inheritance)이라는 컨셉을 적용한 언어이다. 차량 관리 프로그램에서 차량 객체를 만드는 클래스를 생각해보자. 아마 대부분 차량 객체 하나로 다 잘 만들 수 있을것이다. 하지만, 특수 차량이 존재하고, 특수 차량은 그냥 차량과 달리 특수한 관리가 필요하다고 할때, 아마도 대부분의 경우에는 차량용 객체뿐만 아니라 특수차량용 객체가 별도로 필요하게된다. 이 경우에 객체 설계자는 특수차량 클래스를 완전 새로 적고 기능을 추가할수도 있지만, 특수차량 클래스와 "차량"이라는 특징을 공유하는 유사한 클래스, 즉 차량 클래스에서 프로그래밍 해놓은 데이터필드나, 메소드들을 상속하여 가져와서 사용할수도 있다. 이 상속으로 인해서 특수챠량 클래스와 그냥 차량 클래스는 "is-a" 관계를 가진다. 그러니까 모든 "특수차량"은 "차량"이다라는 관계가 된다. 그리고 추후 차량 클래스를 상속하는 대형차량, 소형차량 등의 다른 섭클래스(Sub-Class)들도 같은 관계에 놓이게 된다. 그리고 상속(Inheritance)은 한 세대에서 끝나는것이 아니라 쭉 이어질 수 있다. 즉 최상위 클래스에서 하위 클래스 몇십개로 이어지는것도 가능하다.

섭클래스(Subclass)는 extends 키워드를 이용해 쉽게 선언할 수 있다.


public class SpecialVehicle extends Vehicle{
...
}

extends 키워드와 상속할 클래스명을 붙임으로써 섭클래스는 상속하는 클래스의 메소드와 데이터필드에 접근할 수 있다. 보통 상속받는 클래스는 subclass, derived class, child class라고 불리며 상속하는 클래스는 superclass, base class, parent class라고 불린다. 보통은 수퍼클래스(superclass)와 섭클래스(subclass)라는 용어가 많이 쓰인다. 위 경우에서는 Vehicle 클래스가 수퍼클래스이며, SpecialVehicle 클래스가 섭클래스이고, 섭클래스가 수퍼클래스인 Vehicle 클래스보다 더 많은 기능을 가진다. SpecialVehicle 클래스가 Vehicle 클래스보다 더 많은 데이터필드와 메소드들을 캡슐화하고 있다. 특수차량은 다른 차량들에 장착되지않은 부스터를 장착하고 있다 가정하자. 그렇다면 SpecialVehicle 클래스는 새로운 데이터필드와 메소드를 가지게된다


public class SpecialVehicle extends Vehicle{
   private String boosterModel;
   ...
   public void setBoosterModel(String b){
      this.boosterModel = b;
   }
   ...
}

사실 메소드와 데이터필드를 추가하는 것은 일반 Vehicle 클래스와 다를게 없다. 하지만 boosterModel 데이터필드와 이 필드의 accessor, mutotar 메소드는 오직 SpecialVehicle 객체를 통해서만 접근이 가능하다. 하지만 원래 Vehicle 클래스가 가지고 있는 model, brand 등의 accessor, mutator 메소드는 SpecialVehicle 객체를 통해서도 접근이 가능하다. 그리고 SpecialVehicle 클래스에서는 Vehicle 클래스의 private 데이터 필드들과 메소드들에도 접근이 가능하다. 이때문에 보통 객체 설계시, 수퍼클래스는 보편적인(General) 메소드들과 데이터필드를 가지고, 섭클래스는 조금 특화된(Specialized) 메소드들과 데이터필드를 가지게된다. OOP에서 공통적인 기능들을 모아 수퍼클래스를 만들어 섭클래스의 부담을 줄이는게 상속의 핵심기능이다.

만일 수퍼클래스가 가진 몇몇 메소드들이 현재 클래스에 맞지 않아 현재 클래스에 특화시킬 필요가 있다면 메소드를 오버라이딩(Overriding) 하면 된다. 예를들어 특수차량에는 일반 보혐료와 추가적인 특수 보험료가 붙는다 가정할 때, 메소드를 오버라이딩하여 수정할 수 있다.


//수퍼클래스
public class Vehicle {
 ...
 private double insuranceFee;
 ...
 public double getInsuranceFee() {
  return insuranceFee;
 }
 ...
}

// 섭클래스
public class SpecialVehicle extends Vehicle{
 ...
 private double specialInsuranceFee;
 ...
 public double getInsuranceFee() {
  return super.getInsuranceFee() + specialInsuranceFee;
 }
 ...
}

우선 Vehicle 클래스의 insuranceFee는 private 변수이다. 그러기 때문에 Vehicle 클래스 내부에서 선언된 메소드들만 접근이 가능하다. 그래서 SpecialVehicle 클래스에서는 직접적인 접근이 불가능하다. 대신 간접적으로 accessor 메소드를 이용해 접근해야 한다. 하지만 메소드의 이름이 둘다 일치한다. 그래서 그냥 해당메소드를 부를 경우에는 그 메소드가 수퍼클래스에 존재해도 현재 클래스의 메소드를 우선적으로 호출하기 때문에, 재귀(Recurrence)현상이 일어나게 된다. 참고로 오버라이딩(overriding)되는 메소드는 수퍼클래스의 메소드 접근자가 public일시 같은 접근자를 가지고 있어야된다. 즉 public이면 public 접근자를 가져야 되고, 그렇지 않다면 컴파일시 에러가 발생한다. 여기서 상위 클래스, 즉 수퍼클래스를 가리키는 super 키워드를 이용해서 상위 클래스를 참조하도록 하면 해결이 된다. 다만 super 키워드는 this 키워드와는 달리 현재 객체를 참조하도록하는게 아니라, 컴파일러에게 직접적으로 수퍼클래스의 메소드, 변수를 참조하도록 요청하는 키워드이기 때문에 this와는 달리 생각해야한다.

super 키워들 이용하면 수퍼클래스의 생성자함수를 이용해 섭클래스의 생성자함수를 완료할 수 있다.


public class SpecialVehicle extends Vehicle{
 ...
 private double specialInsuranceFee;
 ...
 public SpecialVehicle(String brand, String model, double insuranceFee, double specialInsuranceFee,...) {
  super(brand, model, insuranceFee, ...);
                this.specialInsuranceFee = specialInsuranceFee;
                ...
 }
 ...
}

이렇게 super 키워드를 이용해 수퍼클래스의 생성자함수 호출도 가능하다. 참고로 만약 섭클래스의 컨스트럭터가 수퍼클래스의 컨스트럭터를 호출하는 구문이 없다면, 컴파일러는 자동적으로 파라미터 없이 수퍼클래스의 컨스트럭터를 호출하며, 수퍼클래스에 파라미터를 받지 않는 컨스트럭터가 없다면 에러가 발생한다

이제 한번 사용해보자.


Vehicle[] vs = new Vehicle[2];
vs[0] = new Vehicle("Hyundai", "Sonata", 44.6);
vs[1] = new SpecialVehicle("Volvo", "X841", 30, 100.5); //일반보험료 30에 특수보험료 100.5
for(Vehicle e:vs){
 System.out.println(e.getInsuranceFee()); 
}
// 44.6
// 130.5

놀랍게도 Vehicle 타입으로 생성된 배열이지만 Vehicle 타입에 종속된 SpecialVehicle 객체도 배열에 저장할 수 있다.
그리고 보험료 출력시 vs[0] 객체는 일반 보험료를 출력했지만, vs[1] 객체는 특수보험료를 포함한 총 보험료값을 출력했다. SpecialVehicle 타입이 아닌 Vehicle 타입 e 객체를 이용해 메소드를 호출했는데, 프로그램이 알맞은 메소드를 호출해 맞는 결과를 가져왔다. 이렇게 e와 같은 객체 변수가 여러 타입을 참조할 수 있는것을 다형성(Polymorphism)이라고 한다. 그리고 프로그램이 실행되는 시점에서 알맞는 메소드를 호출하는것을 Dynamic Binding이라고 한다.


public class SpecialVehicle extends Vehicle{

 private double specialInsuranceFee;
        ...
 public void setSpecialInsuranceFee(double specialInsuranceFee) {
  this.specialInsuranceFee = specialInsuranceFee;
 }
        ...
}

public class Test {
 public static void main(String[] args) {
  Vehicle[] vs = new Vehicle[2];
  vs[0] = new Vehicle("Hyundai", "Sonata", 44.6);
  vs[1] = new SpecialVehicle("Volvo", "X841", 30, 100.5);
  vs[1].setSpecialInsuranceFee(55.5); 
                // 컴파일 에러가 발생한다
 }
}

한가지 주의할점은 vs[1]을 통해서는 SpecialVehicle 클래스의 메소드를 호출하는것은 불가능하다.
vs[1]이 선언된 타입은 Vehicle이지만 setSpecialInsuranceFee 메소드는 SpecialVehicle 클래스에만 존재하기때문에 그렇다. 즉 Vehicle 타입의 객체는 SpecialVehicle 메소드들중 오버라이딩(Overriding)된 것들만 호출 가능하다.

만약 클래스가 상속되지 못하게 하고싶다거나 상속은 되나 메소드를 수정하지 않았으면 좋겠다면 final 키워드를 이용하면 된다. 예를들어 SpecialVehicle 클래스를 가장 하위 클래스로 만들고 싶다면


public final class SpecialVehicle extends Vehicle{
  ...
}
이 경우에는 클래스 상속도 불가능하고 자동적으로 모든 메소드들이 final 속성을 지니게 된다.

만약 상속되는 SpecialVehicle 클래스나, specialize 메소드를 하위클래스에서 오버라이딩하지 않게 하고싶다면

public final class SpecialVehicle extends Vehicle{
  ...
  public final void specilize(int x){
  ...
  }
}
이 경우에는 클래스 상속은 가능하나 해당 메소드를 오버라이딩하는것은 불가능하다.

다른 자료형처럼 상속관계에 놓여있는 자료형끼리도 캐스팅(Casting), 형변환이 가능하다. 예를들면 double 에서 int로 변환하고 싶다면 double 변수앞에 (int)라는 키워드를 놓아주기만 하면 되듯이, Vehicle형 객체를 SpecialVehicle형 객체로 변환하고 싶다면 (SpecialVehicle) 키워드를 앞에 적으면 된다.


Vehicle[] vs = new Vehicle[2];
vs[0] = new Vehicle("Hyundai", "Sonata", 44.6);
vs[1] = new SpecialVehicle("Volvo", "X841", 30, 100.5);
SpecialVehicle a = (SpecialVehicle) vs[1];

우리는 vs[1] 객체를 통해서는 SpecialVehicle 클래스의 모든 기능에 접근하지 못하지만, a 객체를 통해서는 SpecialVehicle 클래스의 모든 기능을 사용할 수 있다. 자바에서 모든 변수는 형(type)을 가진다. 타입은 변수가 참조하는 객체의 종류와 무었을 할수있는지를 알려준다. 그렇기 때문에 Vehicle 타입의 객체인 vs[1]를 통해서는 SpecialVehicle의 메소드를 호출하지 못한것이다. 그리고 만약 String과 Vehicle 타입처럼 아무런 관계에 있지않은 객체끼리 변환을 시도하면 에러를 얻게 된다.

다형성(Polymorphism)


앞의 상속(Inheritance) 예제를 보면 B 클래스가 A 클래스를 상속할시에 "B는 A다(B is A)" 관계에 놓이게 된다. 만약 이 관계가 아니게되면 A 클래스는 B 클래스를 상속하는것이 아니다. 다른 관계가 있다면, 두 클래스가 교체 법칙(Substitution Principle)에 적용된다는것인데, 만약 적용된다면 프로그램이 언제든디 수퍼클래스의 객체를 원할 때, 섭클래스의 객체가 사용될 수도 있게된다. 정리하자면, B 클래스가 A 클래스를 상속할 때, 프로그램이 B 객체를 A 객체처럼 사용할수도 있다는뜻이다. 이는 자바프로그램에서 객체형(Object Type) 변수가 다형적(Polymorphic)이기 때문이다. 그래서 모든 Vehicle 형의 변수는 위의 예제에서 본것처럼 모든 하위클래스의 객체도 참조할수있다. 위 예제를 보면 Vehicle 타입의 배열에도 SpecialVehicle 타입의 객체가 저장된다.

2017년 7월 4일 화요일

[Java] 패키지(Package)의 개념

자바에서 패키지(Package)란 클래스들의 모음이다. 패키지를 통해 편리하게 프로젝트를 관리할 수 있고 내 코드들을 다른 라이브러리로부터, 또 다른 라이브러리들끼리 서로 구분가능하다. 기본적인 자바 라이브러리 또한 java.lang, java.util. java.net java.awt와 같은 다양한 패키지들를 통해 분류된다. 자바 라이브러리는 우리가 일반 폴더 구조에서 볼수있는 계층적 패키지구조를 가지고 있다. 상위 패키지에 하위패키지가 종속되는 형식이다. 그래서 모든 자바 패키지들은 가장 상위 패키지인 java와 javax 패키지에 종속되어 있다.

패키지(Package)를 사용하는 가장 큰 이유는 클래스명의 고유성을 보장하기 위해서이다. 만약 다른 두 프로그래머가 서로 다른 목적을 가진 클래스를 생성했는데 이름 같다 가정할 때, 서로 다른 패키지를 사용함으로써 충돌을 피할수 있다. 그리고 패키지 이름의 고유성을 보장하기 위해 최상위 패키지명에 "com.muckycode"와 같은 방식으로 인터넷 도메인 주소를 꺼꾸로 사용하기도 한다. 인터넷 도메인은 이미 고유성을 가지고 있기때문에 다른 충돌을 걱정할 필요가 없기 때문이다.

클래스는 같은 패키지내 모든 클래스들과 다른 패키지내 모든 public 클래스들을 사용할수있다. 다른 클래스를 사용하는데는 2가지 방법이 있다.
하나는 클래스 선언전 import 키워들 이용해서 패키지 전부 또는 클래스 1개만을 가져와 사용하는것과, 패키지 전체 주소를 가져와 사용하는것이다.


// 패키지 주소 전체를 이용하는 방식

java.util.Date today = new java.util.Date();
java.util.Date yesterday = new java.util.Date();
위방식은 클래스 사용시마다 매번 주소를 입력해야되기 때문에 엄청 귀찮다. 그래서 보통은 import 방식을 쓰게된다.


// import 키워드를 이용하는 방식

import java.util.*
import java.math.BigInteger;

public class Test {
...
}

import 방식은 하나의 클래스에만 적용가능하지만, * 문자를 이용해서 패키지의 모든 하위 클래스에도 적용이 가능하다.
import 방식을 이용하는것은 편리하지만, 만약 서로다른 패키지가 같은 이름을 가진 클래스를 가지고 있다면 이는 컴파일시 에러로 이어진다. 컴파일러가 무슨 클래스를 로드해야될지 모르기 때문인데, 이를 피할려면 특정 클래스의 주소를 import하는 방식을 사용해야한다. 만약 같은 이름을 가진 클래스들을 모두 사용해야한다면 패키지 전체 주소를 이용하는 방식을 사용해야한다. 예를들면 Date라는 클래스는 java.util 패키지에도 존재하고, java.sql 패키지에도 존재한다. 두 패키지를 모두 사용한다는 가정하에 import 방식을 이용하면


import java.util.*;
import java.sql.*;

public class Test {

 public static void main(String[] args) {
  Date newDate = new Date();
 }
}
// Exception in thread "main" java.lang.Error: Unresolved compilation problems: 
// The type Date is ambiguous
// The type Date is ambiguous

위와 같은 컴파일 에러를 얻게된다. 그래서 이럴때는 import 방식 말고 전체 주소를 이용하는 방식을 이용해야한다.


java.util.Date utilDate = new java.util.Date();
java.sql.Date sqlDate = new java.sql.Date(0);
}

import 방식은 static 클래스, 메소드, 데이터 필드들도 적용이 가능하다.
자주쓰는 System.out 객체는 static 클래스인 System을 import 하거나, static 객체인 System.out을 import하여 간소화할수 있다.


import static java.lang.System.out;
import static java.lang.Math.*;
public class Test {

 public static void main(String[] args) {
  out.println("Test"); //Test
  double x = sqrt(2); // Math.sqrt(2)가 간소화됐다.
  out.println(x); // 1.4142135623730951
 }
}
}

클래스를 패키지에 종속시키려면 클래스 선언전에 package 키워드를 이용해 어떤 패키지에 속해있는지 정해줘야한다. 만약 정해주지 않는다면, 클래스는 기본적으로 "default package"에 속하게 되고 "default package"는 아무런 패키지 이름을 가지지 않는다. 그리고 해당 클래스를 파일 디렉토리상으로도 패키지 구조와 일치하게 이동해야 한다.


package com.muckycode.java.app

public class myMathApp {
...
}

위 클래스는 com.muckycode.java.app 패키지에 속하게 되고, com폴더 -> muckycode폴더 -> java폴더 -> app폴더 내에 존재해야한다.