강의 자료: https://gitlab.com/easyspubjava/javacoursework/-/tree/master/Chapter2
객체 간의 상속
상속
새로운 클래스를 정의할 때 이미 구현된 클래스를 상속받아서 속성이나 기능을 확장하여 클래스 구현
이미 구현된 클래스보다 더 구체적인 기능을 가진 클래스를 구현해야 할 때 기존 클래스를 상속한다.
class B extends A{
}
- extends 키워드 뒤에는 단 하나의 클래스만 올 수 있다.
C++의 경우에는 여러 클래스를 상속받을 수 있는 것과는 대조적이다.
JAVA는 단 하나의 클래스만 상속받을 수 있도록 하여 모호성을 줄였다.
- 참고: JAVA 다중 상속
- JAVA에서는 클래스 다중상속은 불가능하지만 인터페이스 다중상속은 가능하다.
- 인터페이스는 추상클래스를 가지므로, 상속받은 클래스에서 반드시 구현해야 한다.
- https://junior-datalist.tistory.com/213
- JAVA에서 다중상속 문제 : 다이아몬드 문제
- 부모 클래스가 여러 개이고 부모 클래스들이 같은 메서드를 구현한다면, 상속받는 자식 클래스는 어떤 부모 클래스의 메서드를 호출할지 모른다.
- 클래스의 다중상속은 불가능하지만 인터페이스의 다중상속은 가능한 이유는, 인터페이스는 추상 클래스로 이루어져 있기 때문이다. 어차피 상속받은 것은 구현체가 아닌 단지 틀이고, 자식 클래스에서 상속받은 메서드들을 구현해야 한다. (인터페이스는 메서드 여러 개가 '선언'만 되어있다.)
- https://youngjinmo.github.io/2021/03/diamond-problem/
- 참고: JAVA - default 메서드와 다중상속
- method 상속 시 클래스(abstract 포함)가 interface보다 우선순위가 높다.(덜 추상적인 것이 먼저)
- default method는 이미 인터페이스에서 구현된 것이므로 상속받은 후 구현하지 않아도 된다.
- 여러 default 메서드 상속 시 구현체에서 overriding 해야 한다.
- https://velog.io/@indongcha/%EB%A9%94%EC%86%8C%EB%93%9C-%EC%83%81%EC%86%8D%EA%B3%BC-%EC%83%81%EC%86%8D%EC%9D%98-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84
- default method란? JAVA 8에서 등장한 인터페이스에 구현된 메서드
메소드라고 알고 있었는데 메서드가 맞나 보다.. 헷갈리네
직접 만들어보기
Customer가 있고, 하위 클래스로 VIPCustomer가 존재한다고 가정한다.
Customer
package ch03;
public class Customer {
protected int customerID;
protected String customerName;
protected String customerGrade;
int bonusPoint;
double bonusRatio;
String agentID;
double saleRatio;
public Customer(int customerID, String customerName) {
this.customerID = customerID;
this.customerName = customerName;
customerGrade = "SILVER";
bonusRatio=0.01;
System.out.println("Customer(int, String) call");
}
int calcPrice(int price) {
bonusPoint += price * bonusRatio;
return price;
}
public String showCustomerInfo(){
return customerName + "님의 등급은 " + customerGrade + "이며, 보너스 포인트는 " + bonusPoint + "입니다.";
}
public int getCustomerID() {
return customerID;
}
public void setCustomerID(int customerID) {
this.customerID = customerID;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getCustomerGrade() {
return customerGrade;
}
public void setCustomerGrade(String customerGrade) {
this.customerGrade = customerGrade;
}
}
VIPCustomer
package ch03;
public class VIPCustomer extends Customer{
double salesRatio;
String agentID;
public VIPCustomer(int customerID, String customerName) {
super(customerID, customerName); //상위 클래스 생성자 호출
bonusRatio = 0.05;
salesRatio = 0.1;
customerGrade = "VIP";
this.salesRatio = salesRatio;
this.agentID = agentID;
System.out.println("VIPCustomer(int, String) call");
}
public String getAgentID(){
return agentID;
}
}
super
상위 클래스의 참조값을 갖는다.
상위 클래스의 멤버변수를 불러올 수 있다.
ex ) super.bonusPoint
super()
상위 클래스의 기본 생성자를 호출한다.
super()는 default 생성자를 호출한다. 기본 생성자일 경우 직접 안 쓰더라도 상위 클래스 생성자에 자동으로 추가된다.
매개변수가 존재하는 생성자만 존재할 경우에는 자식 클래스에서 직접 호출해야 한다. ex) super("kim", 2);
하위 클래스 호출 시에 상위 클래스 생성자 호출 후, 하위 클래스 생성자가 호출된다.
package ch03;
public class CustomerTest {
public static void main(String[] args) {
Customer customerLee = new Customer(10010, "이순신");
customerLee.bonusPoint = 1000;
System.out.println("customerLee.showCustomerInfo() = " + customerLee.showCustomerInfo());
VIPCustomer customerKim = new VIPCustomer(10020, "김유신");
customerKim.setCustomerName("김유신");
customerKim.setCustomerID(10020);
customerKim.bonusPoint = 10000;
System.out.println("customerKim.showCustomerInfo() = " + customerKim.showCustomerInfo());
}
}
형변환(upcasting)
상위 클래스로 변수를 선언하고 하위 클래스의 생성자로 인스턴스 생성
Customer customerLee = new VIPCustomer();
자식대입
상위 클래스 타입의 변수에 하위 클래스 변수가 대입
int addCustomer(Customer customer){
}
VIPCustomer vCustomer = new VIPCustomer();
addCustomer(vCustomer); //하위 클래스로 변수 대입
형변환 후에는 그 클래스의 변수나 메서드만 사용가능하다.
메서드 재정의하기
overriding(오버라이딩) : 부모 클래스로부터 상속받은 메서드를 자식 클래스에서 재정의
상위 클래스에서 정의된 메서드의 구현 내용이 하위 클래스에서 구현할 내용가 맞지 않는 경우 하위 클래스에서 동일한 이름의 메서드를 재정의할 수 있다.
* overloading(오버로딩) : 매개변수 개수나 타입이 다른 여러 개의 같은 이름의 메서드
오버라이딩 vs 오버로딩
오버라이딩 : 상속받은 메서드를 재정의 - 상속관계에서 적용
오버로딩 : 기존에 없던 새로운 메서드 추가 (기존 메서드와 이름만 같은 새로운 메서드 추가) - 한 클래스 내에서 적용
오버라이딩은 부모 클래스 메서드의 접근제어자보다 더 넓은 접근제어자를 자식 클래스 메서드에 적용할 수 있다.
오버로딩의 경우 모든 접근제어자를 사용할 수 있다.
❕ 주의: 오버라이딩 시 function signature가 같아야 한다.(메서드 이름, 매개변수 개수, 리턴형)
JAVA에서 모든 메서드는 가상 메서드이다.
만약 Customer 클래스이지만 인스턴스는 VIPCustomer일 때는 어떤 calcPrice가 실행될까?
=> 정답: 실제 인스턴스의 메서드인 VIPCustomer의 calcPrice가 실행된다.
즉, 실제 인스턴스의 메서드가 호출된다.
이유: 자바의 모든 메서드는 가상 메서드이기 때문이다. 가상 메서드 테이블에서는 메서드와 주소 쌍을 가지고 있는데, 메서드가 재정의된 경우 재정의된 메서드의 주소를 가리킨다.
결국 실제 인스턴스가 무엇인지에 따라 호출되는 메서드가 달라진다.
다형성(polymorphism)
하나의 코드가 여러 자료형으로 구현되어 실현되는 것
같은 코드에서 여러 다른 실행 결과가 나옴
다형성을 잘 활용하면 유연하고 확장성 있고, 유지보수가 편리한 프로그램을 만들 수 있음.
코드
package ch06;
class Animal{
public void move(){
System.out.println("동물이 움직입니다");
}
}
class Human extends Animal{
@Override
public void move() {
System.out.println("사람이 두 발로 걷습니다.");
}
public void readBook(){
System.out.println("사람이 책을 읽습니다.");
}
}
class Tiger extends Animal{
@Override
public void move() {
System.out.println("호랑이가 네 발로 뜁니다.");
}
public void hunting(){
System.out.println("호랑이가 사냥을 합니다.");
}
}
class Eagle extends Animal{
@Override
public void move() {
System.out.println("독수리가 하늘을 날아 다닙니다.");
}
public void flying(){
System.out.println("독수리가 양날개를 쭉 펴고 날아다닙니다.");
}
}
public class AnimalTest {
public static void main(String[] args) {
Animal hAnimal = new Human();
Animal tAnimal = new Tiger();
Animal eAnimal = new Eagle();
AnimalTest test = new AnimalTest();
// 다형성
test.moveAnimal(hAnimal);
test.moveAnimal(tAnimal);
test.moveAnimal(eAnimal);
}
public void moveAnimal(Animal animal) {
animal.move();
}
}
출력
다형성 = 상속 + 메서드 오버라이딩(같은 이름 메서드) + 형변환(상위 클래스에서 캐스팅)
moveAnimal에서 인스턴스에 따라 다른 함수가 실행된다.
ArrayList를 통해 구현한 다형성
다형성을 사용하는 이유
여러 클래스를 하나의 타입으로 핸들링할 수 있음
다형성을 활용한 예제
package ch03;
import java.util.ArrayList;
public class CustomerTest {
public static void main(String[] args) {
ArrayList<Customer> customerList = new ArrayList<>();
Customer customerT = new Customer(10010, "Thomas");
Customer customerJ = new Customer(10010, "James");
Customer customerE = new GoldCustomer(10010, "Edward");
Customer customerP = new GoldCustomer(10010, "Percy");
Customer customerK = new VIPCustomer(10010, "Kim");
customerList.add(customerT);
customerList.add(customerJ);
customerList.add(customerE);
customerList.add(customerP);
customerList.add(customerK);
// 다형성 활용
for (Customer customer : customerList) {
System.out.println("customer.showCustomerInfo() = " + customer.showCustomerInfo());
}
int price = 10000;
for (Customer customer : customerList) {
int cost = customer.calcPrice(price);
System.out.println(customer.getCustomerName() + "님이" + cost+" 원 지불하셨습니다.");
System.out.println(customer.getCustomerName()+"님의 현재 보너스 포인트는 " + customer.bonusPoint+" 입니다.");
}
}
}
서로 다른 유형의 customer들을 customerList로 담아 한번에 수행할 수 있다.
상속은 언제 활용할까?
IS-A 관계 (상속)
일반적인 개념과 구체적인 개념과의 관계
상위 클래스 : 하위 클래스보다 일반적인 개념 ex) Employee
하위 클래스 : 상위 클래스보다 구체적인 개념들이 더해짐 ex) Engineer, Manager
- 클래스 간의 결합도가 높다.
- 상위 클래스의 수정이 하위 클래스에 영향을 미칠 수 있다.
- 계층구조가 복잡하거나 위계질서가 높으면 좋지 않음.
HAS-A 관계
클래스가 다른 클래스를 포함하는 관계 (변수로 선언)
코드 재사용의 가장 일반적인 방법
상속 X
다운캐스팅
instanceof를 이용하여 인스턴스의 형체크
코드
추상 클래스(abstract)
구현 코드 없이 메서드의 선언만 있는 추상 메서드를 포함한 클래스
int add(int x, int y); // 구현부 X, 메서드 선언
int add(int x, int y){ } // 구현부 존재, 메서드 정의(definition)
추상 클래스, 메서드는 이탤릭체(기울어진 체)
- 추상 클래스는 new 할 수 없다.
- 추상 클래스 내부에는 구현된 메서드도 존재한다.
- 모든 메서드가 구현된 추상 클래스의 의미 : 상속만을 하기 위해 존재한다. (모두 구현되어도 추상클래스이므로 new로 인스턴스화 불가)
추상 클래스를 활용한 템플릿 메서드 패턴
추상 클래스로 선언된 상위 클래스에서 템플릿 메서드를 활용하여 전체적인 흐름을 정의하고,
하위 클래스에서 다르게 구현되어야 하는 부분은 추상 메서드로 선언하여 하위 클래스에서 구현하도록 함
final
- final 변수 -상수
- final 메서드 : 재정의 불가능
- final class : 상속 불가 클래스 (클래스 재정의 막음)
하위 클래스에서 구현해야 하는 메서드 - 추상 메서드로 선언
하위 클래스에서 꼭 구현하지 않아도 되는 메서드 - 그냥 메서드로 선언하고(구현부에 문장 없이 만듦 {}) 후에 오버라이딩
하위 클래스에서 재정의하여 바뀌면 안 되는 메서드 - final 메서드
인터페이스(interface)
모든 메서드가 추상 메서드로 선언됨 - public abstract
모든 변수는 상수로 선언됨 - public static final
예제 코드
Calc.java - 인터페이스
package ch11;
public interface Calc {
double PI = 3.14;
int ERROR = -999999999;
int add(int num1, int num2);
int substract(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
}
Calculator.java - 인터페이스를 일부 구현한 추상 클래스
package ch11;
public abstract class Calculator implements Calc {
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
@Override
public int substract(int num1, int num2) {
return num1 - num2;
}
}
CompleteCalc.java - 인터페이스 모두를 구현한 클래스
package ch11;
public class CompleteCalc extends Calculator {
@Override
public int times(int num1, int num2) {
return num1 * num2;
}
@Override
public int divide(int num1, int num2) {
if(num2==0) return ERROR;
return num1 / num2;
}
public void showInfo(){
System.out.println("모두 구현했습니다.");
}
}
테스트 코드
package ch11;
public class CalculatorTest {
public static void main(String[] args) {
int num1=10; int num2=2;
Calc calc = new CompleteCalc();
System.out.println(calc.add(num1, num2));
System.out.println(calc.substract(num1, num2));
System.out.println(calc.times(num1, num2));
System.out.println(calc.divide(num1, num2));
}
}
Calc의 메서드만 접근 가능 (최종 타입의 메서드만 접근 가능), CompleteCalc 메서드에는 접근 불가
인터페이스 구현과 형변환
- 인터페이스를 구현한 클래스는 인터페이스 형으로 선언한 변수로 형 변환 할 수 있음
- Cal calc = new CompleteCalc();
- 클래스 상속과 달리 구현 코드가 없으므로 여러 인터페이스를 구현할 수 있음
인터페이스는 왜 쓰는가?
- 클래스나 프로그램이 제공하는 기능을 명시적으로 선언
- 클라이언트와 서버 사이에 서버가 제공하는 명세(specification)
- 클라이언트 프로그램은 인터페이스에 선언된 메서드 명세만 보고 이를 구현한 클래스를 사용할 수 있음
- 어떤 객체가 하나의 인터페이스 타입이라는 것은 그 인터페이스가 제공하는 모든 메서드를 구현했다는 의미
다형성 : 인터페이스를 구현한 다양한 객체를 사용함
인터페이스를 활용한 DAO 구현하기
DB에 회원 정보를 넣는 DAO(data access object)를 여러 DB 제품이 지원될 수 있게 구현함
환경파일(db.properties)에서 db 종류에 대한 정보를 읽고 그 정보에 맞게 dao 인스턴스를 생성하여 실행될 수 있게 함
package: 소스의 위계질서
구조는 다음과 같다.
domain package : 유저 정보(UserInfo) 객체를 만들고, 객체의 정보를 지정된 DBTYPE에 넣기 위해 dao를 db마다 하나씩 만든다.
db type에 따라 dao를 호출하기 위해 UserInfoDao 인터페이스를 만든 후 각각 구현한다.
web package : 실제 유저 정보 객체를 만들고 dao를 호출한다.
domain package
UserInfo.java
package ch13.domain.userinfo;
public class UserInfo { // 유저 정보
private String userId;
private String password;
private String userName;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
UserInfoDao.java - dao 인터페이스
package ch13.domain.userinfo.dao;
import ch13.domain.userinfo.UserInfo;
public interface UserInfoDao { //유저정보 dao 인터페이스
void insertUserInfo(UserInfo userInfo);
void updateUserInfo(UserInfo userInfo);
void deleteUserInfo(UserInfo userInfo);
}
mysql - UserInfoMySqlDao
package ch13.domain.userinfo.dao.mysql;
import ch13.domain.userinfo.UserInfo;
import ch13.domain.userinfo.dao.UserInfoDao;
public class UserInfoMySqlDao implements UserInfoDao {
@Override
public void insertUserInfo(UserInfo userInfo) {
System.out.println("Insert into MySQL DB userID - " + userInfo.getUserId());
}
@Override
public void updateUserInfo(UserInfo userInfo) {
System.out.println("Update into MySQL DB userID - " + userInfo.getUserId());
}
@Override
public void deleteUserInfo(UserInfo userInfo) {
System.out.println("delete into MySQL DB userID - " + userInfo.getUserId());
}
}
oracle - UserInfoOracleDao
package ch13.domain.userinfo.dao.oracle;
import ch13.domain.userinfo.UserInfo;
import ch13.domain.userinfo.dao.UserInfoDao;
public class UserInfoOracleDao implements UserInfoDao {
@Override
public void insertUserInfo(UserInfo userInfo) {
System.out.println("Insert into Oracle DB userID - " + userInfo.getUserId());
}
@Override
public void updateUserInfo(UserInfo userInfo) {
System.out.println("Update into Oracle DB userID - " + userInfo.getUserId());
}
@Override
public void deleteUserInfo(UserInfo userInfo) {
System.out.println("Delete into Oracle DB userID - " + userInfo.getUserId());
}
}
project 폴더 - db.properties
DBTYPE=MYSQL
web package
UserInfoClient.java
package ch13.web.userInfo;
import ch13.domain.userinfo.UserInfo;
import ch13.domain.userinfo.dao.UserInfoDao;
import ch13.domain.userinfo.dao.mysql.UserInfoMySqlDao;
import ch13.domain.userinfo.dao.oracle.UserInfoOracleDao;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
public class UserInfoClient {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("db.properties"); //파일 read
Properties prop = new Properties();
prop.load(fis); //DBTYPE-값 쌍을 읽음
String dbType = prop.getProperty("DBTYPE"); //DBTYPE의 값을 읽음
UserInfo userInfo = new UserInfo(); //유저 정보
userInfo.setUserId("12345");
userInfo.setPassword("DSFSD");
userInfo.setUserName("hijkljd");
UserInfoDao userInfoDao = null;
//dbType에 따른 dao 객체 생성
if (dbType.equals("ORACLE")) {
userInfoDao = new UserInfoOracleDao();
} else if (dbType.equals("MYSQL")) {
userInfoDao = new UserInfoMySqlDao();
} else{
System.out.println("db error");
return;
}
userInfoDao.insertUserInfo(userInfo);
userInfoDao.updateUserInfo(userInfo);
userInfoDao.deleteUserInfo(userInfo);
}
}
결과화면
스스로 한번 해봤는데 패키지가 달라도 같은 메서드 이름을 사용하면 다른 패키지의 메서드가 import 될 수 있음을 알았다.
패키지가 달라도 같은 이름 메서드는 유의해서 만들자.
인터페이스의 여러 가지 요소
default method (디폴트 메서드)
인터페이스를 구현하는 클래스들에서 공통으로 사용할 수 있는 기본 메서드
인터페이스 안에 구현된 메서드
하위 클래스에서 재정의 가능
인터페이스를 수정하거나 기능을 추가하는 경우 implements 하는 메서드를 모두 수정해야 하는 불편함을 해결하기 위해 나왔다.
JAVA 8 이후 생긴 개념이다.
static method (정적 메서드)
인스턴스 생성과 상관없이 인터페이스 타입으로 사용할 수 있는 메서드
인터페이스가 기본으로 제공하는 기능을 구현한다.
private method
인터페이스를 구현한 클래스에서 사용하거나 재정의 할 수 없음
인터페이스 내부에서만 사용하기 위해 구현하는 메서드
default method나 static method에서 사용함
JAVA 9 이후
사용예제
Calc.java - Interface
package ch14;
public interface Calc {
double PI = 3.14;
int ERROR = -999999999;
int add(int num1, int num2);
int substract(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
default void description() { //default method
System.out.println("정수의 사칙연산을 제공합니다.");
myMethod(); //인터페이스 내에서 private method 사용
}
static int total(int[] arr) { //static method
int total = 0;
for (int num : arr) {
total += num;
}
return total;
}
//private method
private void myMethod(){
System.out.println("myMethod");
}
private static void myStaticMethod(){
System.out.println("myStaticMethod");
}
}
CalculatorTest.java
package ch14;
public class CalculatorTest {
public static void main(String[] args) {
int num1=10; int num2=2;
Calc calc = new CompleteCalc();
System.out.println(calc.add(num1, num2));
System.out.println(calc.substract(num1, num2));
System.out.println(calc.times(num1, num2));
System.out.println(calc.divide(num1, num2));
calc.description(); //default method 호출
int[] arr = {1, 2, 3, 4, 5};
System.out.println(Calc.total(arr)); //static method 호출
//private method는 호출불가
}
}
여러 인터페이스 구현하기, 인터페이스의 상속
Buy.java - Interface
package ch15;
public interface Buy {
void buy();
default void order() {
System.out.println("Buy.order");
}
}
Sell.java - Interface
package ch15;
public interface Sell {
void sell();
default void order() {
System.out.println("Sell.order");
}
}
Customer.java
package ch15;
public class Customer implements Buy, Sell {
@Override
public void buy() {
System.out.println("Customer.buy");
}
@Override
public void sell() {
System.out.println("Customer.sell");
}
// default method 오버라이딩
@Override
public void order() {
// Buy.super.order(); //Bye의 order 사용
System.out.println("Customer.order");
}
public void hello() {
System.out.println("Customer.hello");
}
}
implements 한 두 인터페이스의 default method 이름이 겹치므로 하위 클래스에서 재정의 해야 한다.
인터페이스의 상속
인터페이스 사이에도 상속을 사용할 수 있다. (extends 키워드 사용)
인터페이스는 다중 상속이 가능하고 구현 코드의 상속이 아니므로 타입 상속이라고 한다.
X.java - Interface
package ch15;
public interface X {
void x();
}
Y.java - Interface
package ch15;
public interface Y {
void y();
}
MyInterface.java - 인터페이스 상속받은 인터페이스
package ch15;
public interface MyInterface extends X, Y {
void myMethod();
}
MyClass.java - 인터페이스 구현
package ch15;
public class MyClass implements MyInterface {
@Override
public void myMethod() {
System.out.println("MyClass.myMethod");
}
@Override
public void x() {
System.out.println("MyClass.x");
}
@Override
public void y() {
System.out.println("MyClass.y");
}
}
MyClassTest.java
package ch15;
public class MyClassTest {
public static void main(String[] args) {
MyClass myClass = new MyClass();
X xClass = myClass; //upcasting 경우 upcasting한 타입의 메서드만 사용가능
xClass.x();
Y yClass = myClass;
yClass.y();
MyClass iClass = myClass;
iClass.myMethod();
iClass.x();
iClass.y();
}
}
출력결과
클래스의 upcasting과 달리 인터페이스 upcasting은 메서드 범위가 더 작아질 수 있다.
X, Y -(상속)-> myClass... myClass를 X로 업캐스팅 시 X에서 선언된 메서드만 사용가능
클래스 상속과 인터페이스 구현
클래스와 인터페이스 둘 다 상속/구현시 extends를 먼저 쓰고 그 후에 implements를 쓴다.
Shelf.java
package ch15;
import java.util.ArrayList;
public class Shelf {
protected ArrayList<String> shelf;
public Shelf() {
shelf = new ArrayList<String>();
}
public ArrayList<String> getShelf() {
return shelf;
}
public int getCount() {
return shelf.size();
}
}
Queue.java
package ch15;
public interface Queue {
void enQueue(String title);
String deQueue();
int getSize();
}
BookShelf.java
package ch15;
public class BookShelf extends Shelf implements Queue {
@Override
public void enQueue(String title) { //인터페이스 메서드 구현
shelf.add(title);
}
@Override
public String deQueue() {
return shelf.remove(0);
}
@Override
public int getSize() {
return getCount();
}
}
🔹 복습해 보세요
✍️ 1번. 추상 클래스와 템플릿 메서드
if문 쓰지 않기!!!
풀이 방법
1. Player 클래스 만들기
PlayerLevel level을 변수로 가지고 3개의 메서드를 가진다.
2. PlayerLevel 인터페이스 만들기
4개의 추상 메서드, 1개의 default method(go()) 를 만든다.
3. PlayerLevel 인터페이스를 구현한 3개의 클래스 만들기
참고 : default method와 static method의 차이점
default method : 하위 클래스에서 재정의가 가능하다.
static method : static method는 상속이 되지 않는다. 재정의도 불가능하다. 이유는 static method는 컴파일 시점에 결정되는데, 오버라이딩의 경우 런타임에 결정되기 때문이다.
=> 만약 하위 클래스에서 재정의가 불가능하게 만들고 싶은데, static을 사용하고 싶지 않은 경우(객체마다 정의하고 싶을 때): final 메서드 사용하기.
참고 : static과 final 차이점
final : 값을 바꿀 수 없음
static: 객체마다 가질 필요가 없는 공용 변수/메서드, 객체 생성 없이 사용가능(클래스이름. 필드)
static final: 모든 영역에서(static) 고정으로 사용하는(final) 상수
출처: https://gobae.tistory.com/3
과제에서 개인적으로 수정한 것
조금 더 객체지향으로 만들기
- 선생님 코드의 테스트 함수를 보면 각 객체마다 레벨을 지정한다. 그렇게 하지 않고 upgradeLevel 함수를 호출하면 각 PlayerLevel 객체가 판단하여 레벨을 업그레이드하도록 했다. 그렇게 하기 위해서 Player에 setLevel 메서드를 만들고 PlayerLevel 인터페이스에 upgrade 메서드를 정의했다.
- 레벨마다 jump 횟수가 다른데, 선생님 코드를 보면 test 함수에서 jump 횟수를 레벨마다 지정했다. 그렇게 하지 않고 enum 클래스와 getLevelNum() 메서드를 만들어서 각 레벨마다 레벨 넘버를 스스로 반환하도록 했다.
- 어려웠던 점: level을 업그레이드 후에 setLevel 함수를 호출해서 그 레벨값으로 지정해 줘야 업그레이드가 반영이 된다. 메서드 내의 변수를 바꿔주었다고 해서 클래스 내의 변수가 바뀌는 것은 아니다. this.level=level로 반영하기!
Player.java
package ch16;
public class Player {
private PlayerLevel level;
public Player() {
level = new BeginnerLevel(); //초급자부터 시작
level.showLevelMessage();
}
public PlayerLevel getLevel(){
return level;
}
public PlayerLevel setLevel(PlayerLevel level){
this.level = level;
level.showLevelMessage();
return level;
}
public PlayerLevel upgradeLevel() {
level = level.upgrade();
return level;
}
public void play(){
level.go();
}
}
PlayerLevel 인터페이스
package ch16;
public interface PlayerLevel {
void run();
void jump();
void turn();
void showLevelMessage();
int getLevelNum();
default void go(){
int count = getLevelNum()+1;
run();
for (int i=0;i<count;i++) {
jump();
}
turn();
}
PlayerLevel upgrade();
}
BeginnerLevel.java
package ch16;
import static ch16.LevelInfo.BeginnerLevelInfo;
public class BeginnerLevel implements PlayerLevel {
@Override
public void run() {
System.out.println("천천히 달립니다.");
}
@Override
public void jump() {
System.out.println("jump 못하지롱");
}
@Override
public void turn() {
System.out.println("turn 못하지롱");
}
@Override
public void showLevelMessage() {
System.out.println("****** 초급자 레벨입니다. ******");
}
@Override
public int getLevelNum() {
return BeginnerLevelInfo.ordinal();
}
@Override
public PlayerLevel upgrade() {
return new AdvancedLevel();
}
}
AdvancedLevel.java
package ch16;
import static ch16.LevelInfo.AdvancedLevelInfo;
public class AdvancedLevel implements PlayerLevel {
@Override
public void run() {
System.out.println("빨리 달립니다.");
}
@Override
public void jump() {
System.out.println("높이 jump 합니다.");
}
@Override
public void turn() {
System.out.println("turn 못하지롱");
}
@Override
public void showLevelMessage() {
System.out.println("****** 중급자 레벨입니다. ******");
}
@Override
public int getLevelNum() {
return AdvancedLevelInfo.ordinal();
}
@Override
public PlayerLevel upgrade() {
return new SuperLevel();
}
}
SuperLevel.java
package ch16;
import static ch16.LevelInfo.SuperLevelInfo;
public class SuperLevel implements PlayerLevel{
@Override
public void run() {
System.out.println("엄청 빠르게 달립니다.");
}
@Override
public void jump() {
System.out.println("아주 높이 jump 합니다.");
}
@Override
public void turn() {
System.out.println("turn 합니다.");
}
@Override
public void showLevelMessage() {
System.out.println("****** 고급자 레벨입니다. ******");
}
@Override
public int getLevelNum() {
return SuperLevelInfo.ordinal();
}
@Override
public PlayerLevel upgrade() {
System.out.println("최고 레벨입니다.(업그레이드 불가)");
return this; //더이상 업그레이드 못함
}
}
LevelInfo.java - Enum
package ch16;
public enum LevelInfo {
BeginnerLevelInfo,
AdvancedLevelInfo,
SuperLevelInfo
};
PlayerTest.java - 테스트
package ch16;
public class PlayerTest {
public static void main(String[] args) {
Player player1 = new Player();
player1.play();
player1.setLevel(player1.upgradeLevel());
player1.play();
player1.setLevel(player1.upgradeLevel());
player1.play();
// player1.setLevel(player1.upgradeLevel());
}
}
출력결과
✍️ 2번. 인터페이스를 활용한 정책 프로그래밍
<< Test Program Code >>
public class SchedulerTest {
public static void main(String[] args) throws IOException {
System.out.println("전화 상담원 할당 방식을 선택하세요");
System.out.println("R : 한명씩 차례대로");
System.out.println("L : 대기가 적은 상담원 우선");
System.out.println("P : 우선순위가 높은 고객우선 숙련도 높은 상담원");
int ch = System.in.read();
Scheduler scheduler = null;
if ( ch == 'R' || ch =='r') {
scheduler = new RoundRobin();
}
else if ( ch == 'L' || ch =='l') {
scheduler = new LeastJob();
}
else if ( ch == 'P' || ch =='p') {
scheduler = new PriorityAllocation();
}
else {
System.out.println("지원되지 않는 기능입니다.");
return;
}
scheduler.getNextCall();
scheduler.sendCallToAgent();
}
}
예상 출력 결과
풀이 방법
1. Scheduler 인터페이스 만들기
2개의 메서드를 가진다.
2. 3개의 구현 클래스 만들기
Scheduler.java - Interface
package ch16.prac2;
public interface Scheduler {
void getNextCall();
void sendCallToAgent();
}
RoundRobin.java
package ch16.prac2;
public class RoundRobin implements Scheduler{
@Override
public void getNextCall() {
System.out.println("상담 전화를 순서대로 대기열에서 가져옵니다.");
}
@Override
public void sendCallToAgent() {
System.out.println("다음 순서의 상담원에게 배분합니다.");
}
}
LeastJob.java
package ch16.prac2;
public class LeastJob implements Scheduler{
@Override
public void getNextCall() {
System.out.println("상담 전화를 순서대로 대기열에서 가져옵니다.");
}
@Override
public void sendCallToAgent() {
System.out.println("현재 상담업무가 없거나 상담대기가 가장 적은 상담원에게 할당합니다.");
}
}
PriorityAllocation.java
package ch16.prac2;
public class PriorityAllocation implements Scheduler{
@Override
public void getNextCall() {
System.out.println("고객등급이 높은 고객의 call을 먼저 가져옵니다");
}
@Override
public void sendCallToAgent() {
System.out.println("업무 숙련도가 높은 상담원에게 먼저 배분합니다");
}
}
SchedulerTest.java - Test
package ch16.prac2;
import java.io.IOException;
public class SchedulerTest {
public static void main(String[] args) throws IOException {
System.out.println("전화 상담원 할당 방식을 선택하세요");
System.out.println("R : 한명씩 차례대로");
System.out.println("L : 대기가 적은 상담원 우선");
System.out.println("P : 우선순위가 높은 고객우선 숙련도 높은 상담원");
int ch = System.in.read();
Scheduler scheduler = null;
if ( ch == 'R' || ch =='r') {
scheduler = new RoundRobin();
}
else if ( ch == 'L' || ch =='l') {
scheduler = new LeastJob();
}
else if ( ch == 'P' || ch =='p') {
scheduler = new PriorityAllocation();
}
else {
System.out.println("지원되지 않는 기능입니다.");
return;
}
scheduler.getNextCall();
scheduler.sendCallToAgent();
}
}
출력 결과
자바의 유용한 클래스들
Object 클래스 - 모든 클래스의 최상위 클래스
java.lang 패키지
자동으로 import 됨, 많이 사용하는 기본 클래스들이 속한 패키지
모든 클래스는 Object 클래스를 상속받는다.
컴파일러가 extends Object를 추가한다.
toString() 메서드
객체의 정보를 String으로 바꾸어서 사용할 때 쓰임
(String이나 Integer 클래스는 이미 toString()이 재정의 되어 있음)
toString() 메서드 재정의 예시
package Chapter4.ch01;
class Book{
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
@Override
public String toString() {
return title +"," + author;
}
}
public class BookTest {
public static void main(String[] args) {
Book book = new Book("데미안", "헤르만 헤세");
System.out.println("book = " + book);
}
}
출력결과
Object 클래스의 메서드 활용
equals() 메서드
두 인스턴스의 주소값을 비교해서 true/false 반환
재정의하여 두 인스턴스가 논리적으로 동일한지 여부를 구현함
(인스턴스가 다르더라도 논리적으로 동일한 경우(같은 학번, 같은 아이디..) true 반환하도록 재정의)
hashCode() 메서드
인스턴스의 저장 주소를 int형으로 반환함
힙 메모리에 인스턴스가 저장되는 방식이 hash 방식이다.
hash: 정보 저장, 검색 자료구조 - key값에 대한 저장 위치를 반환해 주는 해시 함수 사용
index = hash(key)
🦈 equals시 해시코드도 같아야 한다?
equals와 HashCode는 같이 재정의해야 한다.
이유: hash를 사용하는 Collections(HashSet, HashMap, HashTable)을 사용할 때 문제가 발생한다.
set의 경우 같은 객체인지 판단할 때 1. hashCode값이 동일한지 판단하고 2. equals가 true인지 판단한다.
equals만 재정의하면 여전히 hash값은 다르기 때문에 다른 객체로 판단하여 set 저장 시 같은 객체가 2번 저장되는 경우가 발생한다.
출처: https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/
예제
Student.java
package Chapter4.ch02;
public class Student {
private int studentNum;
private String studentName;
public Student(int studentNum, String studentName) {
this.studentNum = studentNum;
this.studentName = studentName;
}
public String toString(){
return studentNum + "," + studentName;
}
@Override
public boolean equals(Object obj) { //equals 메서드 재정의
if (obj instanceof Student) {
Student std = (Student) obj; //downcasting
if(this.studentNum == std.studentNum) return true; //학번 같으면 true
else return false;
}
return super.equals(obj);
}
@Override
public int hashCode() {
return studentNum; //학번이 반환되도록 함(equals이면 같은 해시코드 반환)
}
}
테스트 코드
package Chapter4.ch02;
public class EqualsTest {
public static void main(String[] args) {
Student std1 = new Student(100, "Lee");
Student std2 = new Student(100, "Lee");
System.out.println(std1 == std2);
System.out.println(std1.equals(std2));
// 같을때(equals) 해시코드도 같아야한다.
System.out.println(std1.hashCode());
System.out.println(std2.hashCode());
// 진짜 해시코드도 구할 수 있다.
System.out.println(System.identityHashCode(std1));
System.out.println(System.identityHashCode(std2));
}
}
clone() 메서드
객체의 원본을 복제하는 데 사용하는 메서드
객체의 정보가 동일한 또 다른 인스턴스가 생성됨 - 객체 지향 프로그램에서의 정보 은닉, 객체 보호의 관점에서 위배될 수 있음
clonable 인터페이스 명시하여 clone() 메서드 사용 - clone 메서드 재정의해야 쓸 수 있음
Student.java
package Chapter4.ch02;
public class Student implements Cloneable{
private int studentNum;
private String studentName;
public Student(int studentNum, String studentName) {
this.studentNum = studentNum;
this.studentName = studentName;
}
public void setStudentName(String studentName) {
this.studentName = studentName;
}
public String toString(){
return studentNum + "," + studentName;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Test
package Chapter4.ch02;
public class EqualsTest {
public static void main(String[] args) throws CloneNotSupportedException {
Student std1 = new Student(100, "Lee");
Student std2 = new Student(100, "Lee");
std1.setStudentName("Kim");
Student copyStudent = (Student) std1.clone();
System.out.println(std1);
System.out.println(copyStudent);
}
}
출력결과
객체에서 clone함수 호출할 때 그때의 상태를 복제한다. clone() 함수 호출 후 객체가 바뀌면 그건 clone 한 객체에 반영이 안 된다.
String, StringBuilder, StringBuffer 클래스, text block
String 선언하기
String str1 = new String("abc"); //Heap 메모리에 인스턴스로 생성
String str2 = "abc"; //상수 풀(pool)에 있는 주소를 참조
- Heap 메모리는 생성될 때마다 다른 주소값을 가지지만, 상수 풀의 문자열은 모두 같은 주소 값을 가진다.
- str1과 new String("abc")는 다른 주소값, str2와 "abc"는 같은 주소값을 가짐
package Chapter4.ch02;
public class StringTest {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1 == str2); //false
String str3 = "abc";
System.out.println(str2 == str3); //true
}
}
- 한번 생성된 String은 불변(immutable) - final로 선언됨
- String을 연결(concat)하면 새로운 문자열이 생성됨(메모리 낭비 발생 가능)
package Chapter4.ch02;
public class StringTest {
public static void main(String[] args) {
String java = new String("java");
String android = new String("android");
System.out.println(System.identityHashCode(java));
java = java.concat(android); //이어붙임
System.out.println(java);
System.out.println(System.identityHashCode(java)); //이어붙인 주소(새로운 주소)
}
}
출력결과
메모리 낭비를 줄이기 위해 StringBuilder, StringBuffer를 사용하자. 😄
StringBuilder, StringBuffer 활용하기
내부적으로 가변적인 char []을 멤버 변수로 가짐 => 새로운 인스턴스를 생성하지않고 char[]을 변경함
문자열을 여러 번 연결하거나 변경할 때 사용하면 유용함
- 단일 스레드 - StringBuilder
- 멀티 스레드 - StringBuffer : 동기화(synchronization)를 보장
예제
package Chapter4.ch02;
public class StringBuilderTest {
public static void main(String[] args) {
String java = new String("java");
String android = new String("android");
StringBuilder buffer = new StringBuilder(java);
System.out.println(System.identityHashCode(buffer));
buffer.append(android);
System.out.println(System.identityHashCode(buffer)); //이어붙인 후에도 같은 해시코드
String test = buffer.toString();
System.out.println(test);
}
}
text Block 사용하기
java 14부터 사용가능
package Chapter4.ch02;
public class TextBlockTest {
public static void main(String[] args) {
String textBlocks = """
Hello,
hi,
how r u""";
System.out.println(textBlocks);
}
}
html을 textBlock으로 묶어서 유용하게 사용할 수 있다.
Class 클래스
자바의 모든 클래스와 인터페이스는 컴파일 후 class 파일이 생성된다.
Class 클래스는 컴파일된 class 파일을 로드하여 객체를 동적 로드하고, 정보를 가져오는 메서드가 제공된다.
Class.forName("클래스이름") : 클래스 동적 로드
Class c = String.class; // 클래스 이름으로 직접 Class 가져오기
String s = new String(); // 인스턴스 생성
Class c = s.getClass(); // 인스턴스의 클래스 가져오기
동적 로딩
컴파일 시간이 아닌 실행(runtime) 중에 데이터 타입을 binding
✨ 런타임 시에 원하는 클래스를 로딩하여 binding 가능
❕ 컴파일 시에 타입이 정해지지 않으므로, 동적 로딩 시 오류가 발생하면 프로그램에 심각한 오류 발생
예제
package Chapter4.ch02;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class StringTestf {
public static void main(String[] args) throws ClassNotFoundException {
Class c = Class.forName("java.lang.String"); //클래스 동적 로딩 가능
Constructor[] cons = c.getConstructors();
for (Constructor co : cons) { //생성자들 출력
System.out.println(co);
}
Method[] m = c.getMethods();
for (Method mth : m) { //메서드 출력
System.out.println("mth = " + mth);
}
}
}
Class의 newInstance() 메서드로 인스턴스 생성
new 키워드를 사용하지 않고 클래스 정보를 활용하여 인스턴스 생성 가능
reflection 프로그래밍 : Class 클래스를 사용하여 클래스의 정보 등을 알 수 있고, 인스턴스를 생성하고 메서드를 호출하는 방식의 프로그래밍
- 로컬 메모리에 객체 없는 경우, 원격 프로그래밍, 객체의 타입을 알 수 없는 경우에 사용한다.
- java.lang.reflect 패키지에 있는 클래스를 활용하여 프로그래밍
- 일반적으로 자료형을 알고 있는 경우엔 사용하지 않음
Person.java
package Chapter4.ch04;
public class Person {
private String name;
private int age;
public Person(){}
public Person(String name) {
this.name = name;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return name;
}
}
ClassTest.java - reflection 프로그래밍
package Chapter4.ch04;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class ClassTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class c1 = Class.forName("Chapter4.ch04.Person");
Person person = (Person) c1.newInstance(); //클래스의 인스턴스 생성
person.setName("Lee");
System.out.println(person);
Class c2 = person.getClass(); //instance 있으면 그 instance의 클래스 가져올 수 있음
Person p = (Person) c2.newInstance();
System.out.println(p); //이름 없으므로 null
Class[] parameterTypes = {String.class};
Constructor cons = c2.getConstructor(parameterTypes); //문자열 파라미터 가진 생성자
Object[] initargs = {"Kim"};
Person kimPerson = (Person) cons.newInstance(initargs); //생성자 호출하여 인스턴스 만들기
System.out.println(kimPerson);
//위는 아래 문장과 같다.
Person kim2 = new Person("Kim");
}
}
출력 결과
'Spring > 카테캠 - TIL' 카테고리의 다른 글
TIL [0511- 0514] : Spring Basic + Spring MVC 1 (2) | 2023.05.14 |
---|---|
TIL [0504-0505] : 객체지향 프로그래밍(예외처리, 로그, IO 스트림, 직렬화) (0) | 2023.05.06 |
TIL [0503-0504] : 객체지향 프로그래밍(내부클래스, 람다식, 스트림) (0) | 2023.05.05 |
TIL 0417 - 0423 (4) | 2023.04.23 |
TIL (230410 - 230416) (2) | 2023.04.22 |