extends키워드를 사용하여 상속을 구현super()를 사용하여 상위 클래스의 생성자를 먼저 호출할 수 있다.super키워드를 사용할 수 있다.super키워드를 사용하여 간접적으로 접근할 수 있다. → 서브 클래스에서 상위 클래스의 멤버 변수를 private으로 선언한 경우, 서브 클래스에서 해당 변수에 직접 접근할 수 없다.implements키워드를 사용하여 구현인터페이스 상속은 extends 키워드를 사용하여 이루어지며, 하나 이상의 인터페이스를 상속할 수 있다.
예제 코드 → 코드를 보면 이해하기 쉽다.
public interface Shape {
void draw();
}
public interface Colored {
String getColor();
}
// 다중 상속
public interface ShapeColored extends Shape, Colored {
void fill();
}
public class Circle implements ShapeColored {
private String color;
public Circle(String color) {
this.color = color;
}
@Override
public void draw() {
System.out.println("Circle is drawn");
}
@Override
public String getColor() {
return color;
}
@Override
public void fill() {
System.out.println("Circle is filled with color " + color);
}
}
public static void main(String[] args) {
Circle circle = new Circle("red");
circle.draw(); // "Circle is drawn" 출력
System.out.println(circle.getColor()); // "red" 출력
circle.fill(); // "Circle is filled with color red" 출력
}
//추상 클래스 & 인터페이스 생성
public abstract class Animal {
public abstract void makeSound();
}
public interface Vehicle {
public void start();
public void stop();
}
//추상 클래스를 상속 받은 하위 클래스 & 인터페이스가 정의된 클래스
public class Dog extends Animal {
public void makeSound() {
System.out.println("멍멍");
}
}
public class Car implements Vehicle {
public void start() {
System.out.println("차가 출발합니다.");
}
public void stop() {
System.out.println("차가 멈춥니다.");
}
}
//추상 클래스 & 인터페이스 객체 생성
Animal animal = new Dog();
Vehicle vehicle = new Car();
default 메서드와 static 메서드는 자바 8부터 추가된 인터페이스의 기능
default 메서드
static 메서드
예제 코드
public interface MyInterface {
// 추상 메서드
void doSomething();
// default 메서드
default void doSomethingElse() {
System.out.println("Doing something else...");
}
// static 메서드
static void doAnotherThing() {
System.out.println("Doing another thing...");
}
}
public class MyClass implements MyInterface {
public void doSomething() {
System.out.println("Doing something...");
}
}
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.doSomething(); // "Doing something..." 출력
obj.doSomethingElse(); // "Doing something else..." 출력
MyInterface.doAnotherThing(); // "Doing another thing..." 출력
}
interface 다향성 → 형변환
인터페이스는 객체를 다형성으로 다루기 위한 매우 유용한 도구
자동 형변환은 상속 관계에 있는 클래스들 간의 형변환에서 사용되고, 인터페이스 강제 형변환은 인터페이스를 구현하는 클래스들 간의 형변환에서 사용
강제 형변환
자동 형변환
예제 코드
public interface Shape {
void draw();
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Circle is drawn");
}
}
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Rectangle is drawn");
}
}
//자동 형변환
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();
shape1.draw(); // "Circle is drawn" 출력
shape2.draw(); // "Rectangle is drawn" 출력
//강제 형변환
Shape shape3 = new Rectangle();
Rectangle rectangle = (Rectangle) shape3;
rectangle.draw(); // "Rectangle is drawn" 출력
인터페이스를 구현한 클래스를 인스턴스화하여 사용하는 것도 가능하지만, 해당 클래스의 특정한 구현에 의존하게 되어 유연성이 떨어질 수 있다. 또한, 인터페이스를 인스턴스화하고 구현 클래스를 대입하여 사용하면, 구현 클래스가 변경되더라도 인터페이스를 사용하는 코드는 수정하지 않아도 된다. 즉, 유지보수성과 확장성을 높여 준다.
예제 코드
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void makeSound() {
System.out.println("Animal sound");
}
}
class Cat extends Animal {
String color;
public Cat(String name, String color) {
super(name);
this.color = color;
}
public void scratch() {
System.out.println("Cat is scratching.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Animal("Bob");
Cat cat1 = new Cat("Whiskers", "Gray");
Animal animal2 = new Cat("Tom", "Black");
animal1.makeSound(); // Animal sound
cat1.makeSound(); // Animal sound
animal2.makeSound(); // Animal sound
// animal2.scratch(); // Error: Animal 클래스에는 scratch() 메서드가 없습니다.
// Cat 타입으로 강제 형변환
((Cat)animal2).scratch(); // Cat is scratching.
// instanceof 연산자로 타입 체크
if(animal2 instanceof Cat) {
((Cat)animal2).scratch(); // Cat is scratching.
}
}
}
→ 다형성 기능으로 인해 해당 클래스 객체의 원래 클래스명을 체크하는것이 필요한데 이때 사용할 수 있는 명령어가 instance of
// Car Class 생
public class Car {
private String model;
private String color;
private int year;
public Car(String model, String color, int year) {
this.model = model;
this.color = color;
this.year = year;
}
public String getModel() {
return model;
}
public String getColor() {
return color;
}
public int getYear() {
return year;
}
}
public class Main {
public static void main(String[] args) {
//new 키워드를 사용하여 Car 클래스의 객체를 생성
Car myCar = new Car("Tesla", "Red", 2022);
System.out.println("Model: " + myCar.getModel());
System.out.println("Color: " + myCar.getColor());
System.out.println("Year: " + myCar.getYear());
}
}
public class Rectangle {
// 인스턴스 변수
private int width;
private int height;
// 인스턴스 메서드
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
// 클래스 메서드
public static int getArea(int width, int height) {
return width * height;
}
}
public class Main {
public static void main(String[] args) {
// 인스턴스 메서드 사용
Rectangle rect = new Rectangle();
rect.setWidth(5);
rect.setHeight(10);
int area1 = rect.getArea(); // 50
// 클래스 메서드 사용
int area2 = Rectangle.getArea(5, 10); // 50
}
}
인스턴스 멤버 메서드는 인스턴스 멤버 변수를 참조하고 조작할 수 있다.
클래스 멤버 메서드는 인스턴스 멤버 변수를 직접 참조할 수 없으며, 클래스 멤버 변수만 참조할 수 있다.
클래스 멤버 메서드는 인스턴스 멤버 변수를 직접 참조할 수 없는 예시
public class MyClass {
private int value;
public void setValue(int newValue) {
value = newValue;
}
public int getValue() {
return value;
}
public static void add(int x, int y) {
// value 변수를 참조할 수 없으므로 오류가 발생합니다.
int sum = x + y + value; // compile error
System.out.println("Sum: " + sum);
}
}
final키워드를 사용하면 해당 대상이 더 이상 변경될 수 없다는 것을 나타냄public class FinalExample {
final int number = 10;
public static void main(String[] args) {
FinalExample example = new FinalExample();
System.out.println(example.number); // 출력 결과: 10
// example.number = 20; // 에러 발생, 변경할 수 없는 변수입니다.
}
}
final키워드는 메서드나 클래스에도 사용될 수 있다.final메서드를 정의하면 해당 메서드가 더 이상 Override될 수 없다.final클래스를 정의하면 해당 클래스가 상속될 수 없다.public class FinalMethodExample {
public final void printMessage() {
System.out.println("Hello, World!");
}
}
public class FinalClassExample final {
// ...
}
→ printMessage메서드는 더 이상 오버라이드될 수 없고, FinalClassExample클래스는 상속 불가
예제 코드
public class Person {
private String name;
private int age;
public Person() {
this("Unknown", 0);
}
public Person(String name) {
this(name, 0);
}
public Person(int age) {
this("Unknown", age);
}
**public Person(String name, int age) {
this.name = name;
this.age = age;
}**
public void printInfo() {
System.out.println("Name: " + this.name);
System.out.println("Age: " + this.age);
}
}
public class Person {
private String name;
private int age;
// 기본 생성자
public Person() {
this.name = "Unknown";
this.age = 0;
}
// 매개변수가 있는 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter/Setter 생략
}
public class MyClass {
private int privateVar;
int defaultVar;
protected int protectedVar;
public int publicVar;
private void privateMethod() {
// privateVar 접근 가능
}
void defaultMethod() {
// privateVar, defaultVar 접근 가능
}
protected void protectedMethod() {
// privateVar, defaultVar, protectedVar 접근 가능
}
public void publicMethod() {
// privateVar, defaultVar, protectedVar, publicVar 접근 가능
}
}
객체 간의 의존성을 최소화하는 것
DI(Dependency Injection) 사용하기
- 객체 간의 의존성을 관리하기 위한 디자인 패턴 중 하나
- DI를 사용하면 객체 간의 의존성을 외부에서 주입하도록 하여 느슨한 결합을 유지할 수 있다.
→ 객체 간 의존성이 높은 코드
public class Order {
private Item item;
private Payment payment;
public Order() {
item = new Item();
payment = new Payment();
}
public void processOrder() {
item.prepareItem();
payment.makePayment();
}
}
public class Item {
public void prepareItem() {
System.out.println("Preparing item for order");
}
}
public class Payment {
public void makePayment() {
System.out.println("Making payment for order");
}
}
public class Main {
public static void main(String[] args) {
Order order = new Order();
order.processOrder();
}
}
→ 객체 간 의존성을 낮추도록 수정한 코드
//인터페이스 사용
public interface Item {
void prepareItem();
}
public interface Payment {
void makePayment();
}
public class Order {
private Item item;
private Payment payment;
public Order(Item item, Payment payment) {
this.item = item;
this.payment = payment;
}
public void processOrder() {
item.prepareItem();
payment.makePayment();
}
}
public class ItemImpl implements Item {
@Override
public void prepareItem() {
System.out.println("Preparing item for order");
}
}
public class PaymentImpl implements Payment {
@Override
public void makePayment() {
System.out.println("Making payment for order");
}
}
public class Main {
public static void main(String[] args) {
Item item = new ItemImpl(); // 자동 형변환
Payment payment = new PaymentImpl(); // 자동 형변환
Order order = new Order(item, payment);
order.processOrder();
}
}
SOLID 윈칙 적용하기
SOLID는 객체 지향 프로그래밍에서 유지보수 가능하고 확장 가능한 소프트웨어를 만들기 위한 원칙
확장성이 낮은 예시 코드
public class Rectangle {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int calculateArea() {
return width * height;
}
}
→만약 원의 넓이를 계산하는 메소드를 추가하려면 Rectangle클래스를 수정해야 하기 때문에, 코드의 확장성이 매우 떨어진다.
확장성이 좋은 예시 코드
//인터페이스 사용
public interface Shape {
double calculateArea();
}
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle(5, 10);// 자동 형변환 사용
Shape circle = new Circle(5);
System.out.println("Rectangle area: " + rectangle.calculateArea());
System.out.println("Circle area: " + circle.calculateArea());
}
}
→ calculateArea()메소드를 인터페이스에서 선언하고, 구현 클래스에서 각각의 방식으로 구현함으로써 코드의 확장성이 향상된다. 또한, 재사용성이 좋은 코드이기도 하다.
종합 적으로 객체 지향 프로그래밍의 특징을 활용한 예시 코드
interface Shape {
double area();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public class Main {
public static void main(String[] args) {
Shape shape1 = new Circle(5);
Shape shape2 = new Rectangle(3, 4);
System.out.println("Circle area: " + shape1.area());
System.out.println("Rectangle area: " + shape2.area());
}
}
객체 지향 특징을 사용하지 않은 예시 코드
public class Main {
public static void main(String[] args) {
String shape = "circle";
double radius = 5;
double width = 3;
double height = 4;
double area = 0;
if (shape.equals("circle")) {
area = Math.PI * radius * radius;
} else if (shape.equals("rectangle")) {
area = width * height;
}
System.out.println(shape + " area: " + area);
}
}
→ 확실히 유지 보수성, 확장성, 재사용성 면에서 객체 지향 프로그래밍이 좋다.