※ 객체지향으로 개발해야 하는 이유
- 개발 과정에서 자주 발생하는 문제를 객체지향 프로그래밍이라는 방법론으로 해결하기 위해 사용
- 변수와 함수를 밀접하게 하나의 클래스 소속으로 만들었기 때문에 관리가 쉽고 테스트가 용이함
1. 클래스와 상속
1) 상속과 다형성
- 상속은 extends 키워드 사용
- 상속을 전제로 부모타입 변수에는 부모타입이나 자식타입의 변수를 모두 대입 가능
- 부모타입으로는 다형성적으로 대입된 객체가 자식타입이어도 부모측 자원만 호출 가능
public static void main(String[] args) {
Parent parent = new Parent(); // 부모타입 변수에 부모타입 객체 대입
Parent parentTypeChild = new Child(); // 부모타입 변수에 자식타입 객체 대입, 다형성
Child child = new Child();
parent.process();
System.out.println("--------------");
parentTypeChild.process();
//parentTypeChild.childProcess(); // Child 타입객체 내부에 선언된 요소는 Parent로 호출 불가
System.out.println("--------------");
child.process();
child.childProcess();
}
2) 메서드 오버라이딩 (재정의)
- 부모클래스에서 상속받은 메서드를 자식에서 재정의하는 것
- 메서드의 시그니처(리턴타입, 메서드명, 요구하는 파라미터)는 같아야 함
- 오버라이딩이 정의되었다면 부모 타입으로도 자식 측의 재정의 메서드 호출 가능
public static void main(String[] args) {
Parent parent = new Parent();
Parent parentTypeChild = new Child(); // 다형성
parent.process();
parentTypeChild.process(); // 오버라이딩이 전제된 메서드는 부모타입으로도 자식측 메서드 자동 호출
}
3) 메서드 오버로딩 (중복정의)
- 메서드명이 같아도 요구 파라미터의 개수나 타입이 다르면 허용
- 리턴자료만 다른 것은 허용 불가
// 메서드 오버로딩 요건
// 1. 메서드 이름을 중복하여 여러 개 선언
// 2. 단, 선언된 같은 이름의 메서드 간 요구 파라미터의 개수나 타입은 달라야 함
// 3. 리턴타입의 동일여부는 오버로딩에 영향을 주지 않음
public int add(int num1, int num2) {
return num1 + num2;
}
// return 타입만 다르게 오버로딩 하는 것은 불가능
// public long add(int num1, int num2) {
// return num1 + num2;
//}
public double add(double num1, double num2) {
return num1 + num2;
}
public long add(long num1, long num2) {
return num1 + num2;
}
4) 상속 관련 시 주의사항
- 상속 시 메서드와 필드를 모두 재사용할 수는 있지만 기본적으로는 필드 재사용을 전제로 해야 함
- 메서드 재사용을 위해 상속을 쓴다면 전략 패턴 구성 (Composite) 활용
☆ 리스코프 치환 원칙 : 부모타입으로 할 수 있는 일은 자식타입으로도 할 수 있어야 함
2. 추상 클래스와 인터페이스
1) 추상클래스
- abstract 키워드 활용
- 일반적으로 인스턴스를 생성할 수 없음 (생성자에서 추상메서드에 대한 오버라이딩을 직접 해주면 가능)
- 일반적으로 하나 이상의 추상 메서드 포함, 해당 추상 메서드를 상속하며 오버라이딩해야만 인스턴스 생성
public abstract class AbstractClass {
public void implementedMethod() {
System.out.println("AbstractClass 내부에서 직접 구현된 메서드");
this.abstractMethod(); // 추후 구현될 템플릿 메서드
}
abstract public void abstractMethod();
};
public class ExtendedClass extends AbstractClass {
@Override
public void abstractMethod() {
System.out.println("ExtendedClass에서 정의된 추상 메서드");
}
}
public class AbstractClassExMain {
public static void main(String[] args) {
// 추상클래스의 인스턴스를 직접 생성해주고 싶다면?
AbstractClass abstractClass = new AbstractClass() {
@Override
public void abstractMethod() {
// 생성자에서 직접 추상메서드를 구현해주면 상속 없이 생성 가능
System.out.println("Abstract Class 내부에서 정의한 abstractMethod()");
}
};
abstractClass.implementedMethod();
abstractClass.abstractMethod();
System.out.println("--------------");
AbstractClass extendedClass = new ExtendedClass();
extendedClass.implementedMethod();
extendedClass.abstractMethod();
}
}
2) 인터페이스
- 다중상속이 가능
- 디폴트 메서드를 이용하여 정의된 메서드와 정의되지 않은 메서드 호출 가능
public interface SomeInterface {
void someMethod();
default void defaultMethod() {
// default 키워드를 메서드에 붙이면 인터페이스 내부에서도 구현된 메서드를 가질 수 있음
this.someMethod();
}
}
public interface AnotherInterface {
void anotherMethod();
}
public class ImplementsClass implements SomeInterface, AnotherInterface { // 다중상속 가능
@Override
public void anotherMethod() {
// 리스코프 치환 원칙 : 자식의 실행 커버리지는 부모의 실행 커버리지보다 넓어져서는 안 됨.
System.out.println("ImplementsClass의 anotherMethod()");
}
@Override
public void someMethod() {
System.out.println("ImplementsClass의 someMethod()");
}
}
public class InterfaceExMain {
public static void main(String[] args) {
SomeInterface someInterface = new ImplementsClass();
AnotherInterface anotherInterface = new ImplementsClass();
someInterface.someMethod();
anotherInterface.anotherMethod();
ImplementsClass implementClass = new ImplementsClass();
// SomeInterface, anotherInterface로는 양쪽 모두를 호출할 수 없음
//someInterface.anotherMethod();
//anotherInterface.someMethod();
// ImplementsClass 구현체 타입으로는 양쪽 모두 호출 가능
implementClass.anotherMethod();
implementClass.someMethod();
}
}
※ 일반적인 상황에서는 인터페이스를 쓰면 정답인 경우가 많다.
※ 추상클래스를 인터페이스 대신 사용하는 경우
① 인스턴스 변수(필드)를 정의해야 하는 경우 : 인터페이스는 상수만 정의 가능
② 생성자가 필요한 경우 : 인터페이스는 내부에 생성자 정의 불가
③ Object 클래스의 메서드를 오버라이딩 하고 싶은 경우
3. ENUM(이늄)
열거상수 이늄을 이용하면 강력한 객체지향 코딩 기법을 배울 수 있다.
import java.util.function.BiFunction;
public enum CalculateType {
// basic과는 다르게 연산 종류와 연산 메커니즘을 함께 정의
// 관련된 자료들이 잘 모여있음(응집도가 높아졌음)
ADD((num1, num2) -> num1 + num2),
MINUS((num1, num2) -> num1 - num2),
MULTIPLY((num1, num2) -> num1 * num2),
DIVIDE((num1, num2) -> num1 / num2);
// 위에 붙인 익명함수들은 Bifunction이라는 타입으로 멤버변수를 정의해야 사용 가능
// 따라서, 생성자에서 BiFunction을 주입받도록 처리
CalculateType(BiFunction<Integer, Integer, Integer> expression){
this.expression = expression;
}
private BiFunction<Integer, Integer, Integer> expression;
public int calculate(int num1, int num2) {
return this.expression.apply(num1, num2);
}
}
이늄 자료를 위와 같이 정의하면 CalculateType은 총 4개의 자료만 가질 수 있다.
이때 타입에 따른 연산까지 함께 이늄에 정의해둘 수 있고, BiFunction은 입력자료형과 리턴자료형을 정의할 수 있도록 해준다.
함수를 마치 하나의 객체처럼 다룰 수 있게 해준다.
public class CalculateCommand {
private CalculateType calculateType; // ADD, MINUS, MULTIPLY, DIVIDE 중 하나만 대입 가능
private int num1;
private int num2;
public CalculateCommand(CalculateType calculateType, int num1, int num2) {
this.calculateType = calculateType;
this.num1 = num1;
this.num2 = num2;
}
public CalculateType getCalculateType() {
return calculateType;
}
public int getNum1() {
return num1;
}
public int getNum2() {
return num2;
}
}
public class Client {
public int process(CalculateCommand calculateCommand) {
CalculateType calculateType = calculateCommand.getCalculateType();
int num1 = calculateCommand.getNum1();
int num2 = calculateCommand.getNum2();
// basic쪽과는 달리 client에 상세한 계산 로직이 포함되지 않음
// 클라이언트는 어떤 연산을 수행할지만 알고, 해당 로직의 상세한 내용을 모름
// 클라이언트는 해당 로직에 대한 책임이 없음
// 수정이 필요할 때 클라이언트측 코드를 볼 필요가 없음
int result = calculateType.calculate(num1, num2);
return result;
}
}
public class AdvExMain {
public static void main(String[] args) {
// 클라이언트가 요청할 때 calculateCommand 객체가 제공한 데이터를 사용
CalculateCommand calculateCommand = new CalculateCommand(CalculateType.ADD,100,3);
Client client = new Client();
int result = client.process(calculateCommand);
System.out.println(result);
}
}
4. 예외
1) checked exception vs unchecked exception
- checked exception : Exception 객체를 상속한 예외, 컴파일 시 예외처리를 문법적으로 강제
- unchecked exception : RuntimeException 객체를 상속한 예외, 예외처리를 따로 강제하지 않음
2) 왜 checked exception을 쓰지 않는가?
- 대부분의 경우 로직만으로는 예외를 처리할 수 없는 경우가 많음
ex) 사용자에게 파일 이름을 입력받아 서버에서 리턴해주는 로직
파일 잘못 입력 시 FileNotFoundException이 발생하는데, 사용자에게 파일을 입력해달라는 말 말고 직접적으로 처리할 수 없는데 굳이 예외처리 구문을 사용할 필요가 없음
- checked exception의 경우 내부에서 어떤 동작을 하다가 예외를 발생시켰는지 알 수 있는 경우가 많음 → 캡슐화 원칙이 깨지는 문제
5. Object 클래스
모든 클래스의 부모이자 Object 클래스가 가진 메서드들을 오버라이딩하여 사용하는 경우가 많음
1) .equals()
- 동일성 : 두 대상이 "똑같은 대상"이어야 성립 → ==를 이용해 비교
- 동등성 : 대상은 다르지만 어떤 다른 기준에 의해 같음을 확인했을 때 성립 → equals() 활용
2) .hashCode()
- 주로 equals와 함께 사용
- map 자료 등에 대해 비교 시 주로 hashcode를 이용해 1차적인 필터링을 하고, 그 결과에 대해 equals로 조회
'네트워크캠퍼스 > JAVA' 카테고리의 다른 글
객체지향적 코드 작성 (0) | 2024.01.29 |
---|---|
프로세스와 쓰레드 (0) | 2024.01.24 |
자바 API (0) | 2024.01.18 |
자바의 예외처리전략 (0) | 2024.01.18 |
예외처리 (0) | 2024.01.17 |