이 글은 Java의 이해: 클래스와 객체, Java의 이해: 추상화, 캡슐화, 상속, 다형성 글을 참고한다.
Car 클래스 소스코드
public class Car {
// 필드(멤버 변수)
private String brand; // 자동차의 브랜드를 나타내는 변수
private String color; // 자동차의 색상을 나타내는 변수
private int speed; // 자동차의 속도를 나타내는 변수
// 생성자(브랜드와 색상을 입력받아 Car 객체를 생성)
public Car(String brand, String color) {
this.brand = brand;
this.color = color;
this.speed = 0; // speed는 초기값으로 0을 가진다.
}
// 메소드(멤버 함수)
public void accelerate(int amount) {
speed += amount; // amount 만큼 속도가 증가
}
public void brake(int amount) {
speed -= amount; // amount 만큼 속도가 감소
}
// 자동차의 정보를 출력하는 메소드(브랜드, 색상, 속도 출력)
public void displayInfo() {
System.out.println("Brand: " + brand);
System.out.println("Color: " + color);
System.out.println("Speed: " + speed);
}
}
위의 코드는 Car라는 클래스를 정의한 예시이다. 이 클래스는 자동차를 나타내며, brand, color, speed라는 필드와 accelerate, brake, displayInfo라는 메소드를 가지고 있다.
Shape 클래스 소스코드(추상화)
// Shape 추상 클래스(도형을 나타내는 추상 클래스)
abstract class Shape {
abstract double calculateArea(); // 도형의 넓이를 계산하는 추상 메소드
abstract double calculatePerimeter(); // 도형의 둘레를 계산하는 추상 메소드
}
// Circle 클래스(Shape 클래스를 상속받는 클래스로, 구체적인 원을 나타냄)
class Circle extends Shape {
private double radius; // 원의 반지름을 저장하는 변수
public Circle(double radius) {
this.radius = radius;
}
@Override
double calculateArea() {
return Math.PI * radius * radius; // 원의 넓이를 계산하여 반환
}
@Override
double calculatePerimeter() {
return 2 * Math.PI * radius; // 원의 둘레를 계산하여 반환
}
}
// Circle 객체를 생성하고 메소드를 호출하여 원의 넓이와 둘레 출력
public class Main {
public static void main(String[] args) {
Circle circle = new Circle(5.0); // 반지름이 5.0인 원 객체 생성
System.out.println("Circle Area: " + circle.calculateArea()); // 원의 넓이 출력
System.out.println("Circle Perimeter: " + circle.calculatePerimeter()); // 원의 둘레 출력
}
}
위의 코드에서 Shape 클래스는 도형을 나타내는 추상 클래스이다. calculateArea()와 calculatePerimeter()라는 추상 메소드를 가지고 있다. 이 메소드들은 각각 도형의 넓이와 둘레를 계산하는 메소드이다.
이 예시 코드에서 추상화를 사용하여 Shape 클래스를 추상화된 범용 도형으로 정의하고, 이를 상속받는 Circle 클래스에서 구체적인 원에 대한 계산을 구현한다. 추상 클래스를 사용함으로써 공통된 특징과 동작을 가진 클래스들을 일반화하고, 이를 확장한 구체적인 클래스에서 구체적인 동작을 구현할 수 있다.
Person 클래스 소스코드(캡슐화)
public class Person {
// 필드(멤버 변수)
private String name; // 사람의 이름을 저장하는 필드
private String occupation; // 사람의 직업을 저장하는 필드
private int age; // 사람의 나이를 저장하는 필드
// 생성자(이름, 나이, 직업을 입력받아 'Person' 객체를 생성)
public Person(String name, int age, String occupation) {
this.name = name;
this.age = age;
this.occupation = occupation;
}
// 메소드(멤버 함수)
public void introduce() {
System.out.println("이름: " + name);
System.out.println("나이: " + age);
System.out.println("직업: " + occupation);
}
// 생일을 축하하고 나이를 1 증가시키는 메소드
public void celebrateBirthday() {
age++;
System.out.println(name + "이/가 생일을 축하합니다! 나이가 " + age + "세가 되었습니다.");
}
// 직업을 변경하는 메소드
public void changeOccupation(String newOccupation) {
System.out.println(name + "의 직업이 " + occupation + "에서 " + newOccupation + "으로 변경되었습니다.");
occupation = newOccupation;
}
}
이 예시 코드에서 Person 클래스는 캡슐화를 구현하고 있다.
필드는 private으로 선언되어 클래스 외부에서 직접 접근할 수 없으며, 메소드를 통해 필드에 접근하고 값을 변경할 수 있다. 이를 통해 데이터의 보호와 외부 접근 제한이 이루어지게 된다.
Vehicle 클래스 소스코드(상속)
// Vehicle 클래스(차량을 나타내는 기본 클래스)
public class Vehicle {
// 필드(멤버 변수)
private String brand;
private int year;
// 생성자
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}
// 메소드(멤버 함수)
public void displayInfo() {
System.out.println("Brand: " + brand);
System.out.println("Year: " + year);
}
}
// Car 클래스(Vehicle 클래스를 상속하여 확장하는 클래스)
public class Car extends Vehicle {
// 추가된 필드(멤버 변수)
private int numOfDoors;
// 생성자
public Car(String brand, int year, int numOfDoors) {
super(brand, year);
this.numOfDoors = numOfDoors;
}
// 추가된 메소드(멤버 함수)
public void startEngine() {
System.out.println("Engine started!");
}
}
// Main 클래스(Car 객체를 생성하고 메소드를 호출하여 결과를 출력)
public class Main {
public static void main(String[] args) {
Car myCar = new Car("Toyota", 2021, 4);
myCar.displayInfo();
myCar.startEngine();
}
}
위의 예시 코드에서 Car 클래스는 Vehicle 클래스를 상속받아 brand과 year 필드를 상속받고, numOfDoors 필드를 추가로 가지고 있다. 또한, startEngine() 메소드를 추가로 가지고 있다.
상속을 통해 Car 클래스는 Vehicle 클래스의 속성과 기능을 재사용하면서 확장할 수 있다. Car 클래스의 인스턴스를 생성하고 메소드를 호출하여 결과를 출력하는 Main 클래스의 main 메소드를 통해 상속 관계가 잘 동작하는 것을 확인할 수 있다.
Animal 클래스 소스코드(다형성)
class Animal {
public void makeSound() {
System.out.println("동물이 소리를 낸다.");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("강아지가 짖는다.");
}
public void fetch() {
System.out.println("강아지가 물건을 가져온다.");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("고양이가 야옹 소리를 낸다.");
}
public void scratch() {
System.out.println("고양이가 발톱으로 긁는다.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound(); // 강아지가 짖는다.
animal2.makeSound(); // 고양이가 야옹 소리를 낸다.
// 다운캐스팅하여 추가적인 메소드 호출
Dog dog = (Dog) animal1;
dog.fetch(); // 강아지가 물건을 가져온다.
Cat cat = (Cat) animal2;
}
}
다형성은 같은 타입으로 여러 객체를 다룰 수 있도록 하는 기능을 말하며, 이를 통해 코드의 유연성과 재사용성을 높일 수 있다. 이 소스코드에서는 다음의 3가지 부분에서 다형성이 나타나고 있다.
1. 객체의 다형성
Animal 타입의 변수 animal1과 animal2가 생성되었는데, 이들은 실제로 각각 Dog와 Cat 객체를 참조하고 있다. 이것은 다형성의 한 예로, 상위 클래스 타입으로 하위 클래스의 객체를 참조할 수 있으며, 이를 통해 여러 종류의 객체를 동일한 타입으로 다룰 수 있다.
2. 메소드의 오버라이딩
Animal 클래스에 정의된 makeSound() 메소드는 Dog와 Cat 클래스에서 오버라이딩되어 재정의된다. 따라서 animal1.makeSound()와 animal2.makeSound() 호출 시, 각 객체의 실제 타입에 따라 오버라이딩된 메소드가 실행되어 해당 객체의 동작이 수행된다.
3. 다운캐스팅
Animal 타입의 변수 animal1과 animal2를 Dog와 Cat 타입의 변수인 dog와 cat에 각각 다운캐스팅한다. 다운캐스팅을 통해 원래의 하위 클래스의 메소드와 멤버에 접근할 수 있게 된다.
참고) @Override 어노테이션과 // Override 주석의 차이
@Override와 // Override는 둘 다 메소드를 오버라이딩한다는 의미를 나타내는 주석이다. 그러나 사용 방식과 의미에서 차이가 있다.
@Override는 자바의 어노테이션(annotation)이다. 이 어노테이션은 컴파일러에게 해당 메소드가 부모 클래스에서 상속받은 메소드를 오버라이딩한다는 것을 알려준다. 이를 통해 컴파일러는 오버라이딩 관련된 오류를 체크할 수 있다. @Override 어노테이션은 메소드 선언 위에 붙여 사용한다.
// Override는 주석(comment)이다. 주석은 코드를 설명하거나 비활성화시킬 때 사용된다. // Override 주석은 개발자에게 해당 메소드가 부모 클래스에서 상속받은 메소드를 오버라이딩한다는 것을 나타내는 용도로 사용된다. 이 주석은 컴파일러나 실행에 직접적인 영향을 주지 않는다.
요약하면, @Override 어노테이션은 컴파일러에게 오버라이딩을 체크하도록 지시하는 반면, // Override 주석은 개발자에게 해당 메소드가 오버라이딩되었음을 나타내는 용도로 사용된다는 차이가 있다.