
지난주에 이어 JAVA 후반부의 내용을 배웠다. 중요한 내용들인 만큼 하루하루 소화해 내는데 바빴던 한 주였던 것 같다. 배운 내용은 아래와 같다.
Chap09. 다형성
Chap10. API
Chap11. 예외처리
Chap12. 입출력
Chap13. 제네릭스
Chap14. 컬렉션-List
내용 하나하나 하루만에 다 습득하기란 물론 불가능한 내용들이지만, 이번 회고도 새롭게 배우고 기억해야 하는 내용들 위주로 작성해 보겠다.
다형성이란 상속이라는 개념을 통해 하나의 인스턴스가 여러 타입을 가질 수 있음을 이야기한다. 이를 통해 인터페이스와 구현을 통해서 프레임워크를 제공하는 등 이는 자바에서 매우 핵심적인 내용이라고 배웠다.
먼저 상속에 대한 내용을 복기해 보자. 부모로부터 자식이 상속을 받는 이미지를 확인해보면
위와 같이 자식 클래스가 부모 클래스를 extend받고 인스턴스를 생성한다면 부모 클래스를 가진채 Heap 영역에 주소값을 가지고 생성된다. 그렇다면 다형성을 이미지로 표현하면 어떨까
부모 타입의 자식 인스턴스 주소값에 접속하여 자신의 클래스에 접속하는 이미지를 확인해볼 수 있다. 이는 그림에도 나와있듯이 타입을 지정해주는 클래스는 자기 자신에게 밖에 들어갈 수 밖에 없기 때문에 그러하다는 것이다.
→ Parent par = new Child(); 로 표현은 가능하지만, 사실 자식 클래스에는 직접 접근을 할 수 없는 상태이다. 이 때, 자식 클래스에 접근할 수 있도록 부모클래스를 Child로 형변환 시켜줄 수 있다. 이를 다운캐스팅 이라고 한다.
다형성은 먼저 동적 바인딩을 이해해야 할 것 같다. 부모 클래스와, 부모 클래스를 상속하는 자식 클래스가 있다고 해보자. 부모 클래스의 메소드를 자식 클래스에서 오버라이딩한 상태이다. 그리고 다형성을 적용하여 아래 코드와 같이 부모 타입으로 자식 인스턴스를 만들었다.
Parent p = new Child();
그러면 아래 이미지와 같이 된다.

위 그림 처럼, 컴파일하여 인스턴스 p 의 methodA() 에 접근한다면, 오버라이딩 된 메소드에 접근한다. 이 것이 동적 바인딩이다.
즉, 하위 클래스가 상위 클래스의 메소드를 상속받아 오버라이딩을 하면 우선권이 하위 클래스에 있다는 것이다.
동적 바인딩으로 인해 접근 못하는 영역을 접근할 수 있게 하는 것이 캐스팅, 즉 형변환이라는 것을 배웠다.
업케스팅 : 자식이 부모로 형변환
다운케스팅 : 부모가 자식으로 형변환
아래는 형변환의 명시적, 묵시적 표현 방법을 나타낸다.

메소드 오버로딩을 진행할 때, 각각의 하위 클래스의 메소드를 하나하나 작성하려면 하위 클래스의 갯수대로 작업 해야 하지만, 상위 클래스를 사용하여 오버롣딩을 하면 효율적으로 작업할 수 있음을 배웠다.
public void feed(Rabbit rabbit) {} -> 하위 클래스 ❌
public void feed(Tiger tiger) {} -> 하위클래스 ❌
// 위 작업을 상위클래스에 작성하면 한번에 끝난다.
public void feed(Animal animal) { -> 상위클래스 오버로딩! ⭕
animal.eat();
업캐스팅이 일어난다.public void feed(Animal타입..)
feed(Tiger타입...) => 업케스팅
이렇게 업케스팅되어 feed()가 동작한다면 결과는 아래와 같이 나올 것이다.
animal이 울음소리를 냅니다.
다형성을 사용하지 않는다면 리턴 메소드를 하위 클래스만큼 메소드를 정의해야 할 것이다. 리턴은 한 가지의 타입만 가능하기 때문이다.
다형성을 사용하여 상위 클래스만으로 메소드를 작성하여 리턴 받을 때 효율적으로 코드를 작성할 수 있다.
public class Application4 {
public static void main(String[] args) {
Application4 application4 = new Application4();
Animal randomAnimal = application4.getRandomAnimal();
randomAnimal.cry();
}
// 메소드
public Animal getRandomAnimal() {
int random = (int) (Math.random() * 2); //0, 1
return random==0?new Rabbit():new Tiger();//삼항 연산자 사용.
}
}
Tiger 타입이 나오든 Rabbit 타입이 나오든 Animal을 통해 리턴을 넘겨줄 수 있다.
instanceof 연산자는 레퍼런스 변수가 실제로 어떤 클래스 타입의 인스턴스인지 확인하여 true or false를 반환한다.

추상 클래스를 정의하는 몇 가지 규칙이 아래와 같이 있다.
추상메소드 0개 이상 포함하는 클래스
추상 클래스는 클래스 선언부에 abstract 키워드를 명시
인스턴스 생성 불가
(추상 클래스를 상속받는 하위 클래스를 이용해서 인스턴스를 생성해야한다. 이 때 추상 클래스는 상위 타입으로 사용될 수 있으며, 다형성을 이용할 수 있다.)
추상 메소드란??
⇒ 메소드의 선언부만 있고 구현부가 없는 메소드를 추상메소드라고 한다. 추상메소드의 경우 반드시 abstract키워드를 메소드 헤드에 작성해야 한다.
ex)
public abstract void method();
추상메소드 작성
추상클래스에 작성한 추상메소드는 반드시 후손이 오버라이딩해서 작성해야하며, 후손 클래스들의 메소드들의 공통 인터페이스로의 역할을 수행할 수 있다. 추상클래스에 작성한 메소드를 호출하게 되면 실제 후손 타입의 인스턴스가 가지는 메소드로는 다형성이 적용되어 동적바인딩에 의해 다양한 응답을 할 수 있게 된다.
추상 클래스 상속
추상클래스를 상속받아 구현할 때는 extends 키워드를 사용하며 → 자바에서는 extends로 클래스를 상속받을 시 하나의 부모 클래스만 가질 수 있다(단일상속).
부모에 선언된 추상 메소드는 자식클래스에서 반드시 오버라이딩 해야 한다.
메소드를 개발하여 사용하려 하는데 메소드가 사용자마다 그떄그때 달라지는 경우가 있다. 하지만 명칭은 통일된 경우, 일반 클래스를 추상 클래스로 만들어서, “같은 이름”으로 다르게 작동하는 오버라이딩을 강제성으로 부여한다. “같은 이름” 이란, 동일한 이름과 타입도 포함된다.

추상클래스의 자식의 메소드로 오버라이딩하여 동적 바인딩이 일어난다.
정적 바인딩 → Product, 동적 바인딩 → SmartPhone

| 클래스 종류 | Class | Abstract Class | Interface |
|---|---|---|---|
| 구성 요소 | 필드/메소드 | 필드/메소드/추상메소드 | 상수필드/추상메소드 |
| 상속을 했을 경우 부모 메소드의 강제성 | 없음 | 추상메소드 0개 이상 | 정해진 규칙 |
인터페이스 상수
인터페이스에는 상수와 추상 메소드만 작성 가능하다. 변수는 상수만 작성 가능하기에, 인터페이스에 작성되는 모든 변수는 상수이다. 인터페이스 필드에 작성되는 모든 변수는 public final static을 모두 포함한다.
인터페이스 생성자
인터페이스는 생성자를 가지지 않는다.
인터페이스는 구현부가 있는 non-static 메소드를 가질 수 없다.
public void nonStaticMethod() {} ⇒ ❌
인터페이스 메소드
인터페이스 안에 작성한 메소드는 묵시적으로 public static abstract 이라는 의미를 가진다. 다른 접근제한자 사용 불가하다.
따라서 인터페이스의 메소드를 오버라이딩하는 경우 반드시 접근제한자를 public으로 해야 오버라이딩 가능하다.
public static void staticMethod() {
System.out.println("InterProduct 클래스의 staticMethod 호출됨..");
}
public default void defaultMethod() {
System.out.println("InterProduct 클래스의 defaultMethod 호출됨..");
}
class Person extends SuperClass implements Instinct, Serializable {
@Override
public void eating(String food) {
System.out.println("사람은 " + food + "를 요리하고 식기를 활용해 음식을 먹는다.");
}
}
인터페이스는 클래스들의 상위 부모 역할을 주어 상수와 추상 메소드 등 공유 목적 필드와 메소드를 작성하여 공통성을 강제로 가지게 만들어졌다.
인터페이스는 특수적으로 하나의 클래스가 부모 클래스를 상속하는 것 외에 여러 개의 인터페이스를 구현(implement)할 수 있다.
wrapper 클래스는 기본 타입의 데이터를 인스턴스화 해야 하는 경우에 사용한다.
| 기본 타입 | 래퍼 클래스 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
박싱이란, 리터럴을 오브젝트화 시켜, 클래스에서 사용할 수 있는 메소드 들을 사용할 수 있게 만들어주었다.
boxing의 예시
int intVal = 20;
Integer boxingNumber1 = new Integer(intVal);
Integer boxingNumber1 = Integer.valueOf(intVal); //static메소드로 int -> Integer
int junBoxingNumber = boxingNumber1.intValue(); // Integer -> int
//오토박싱
Integer boxingNumber2 = intVal; //알아서 바뀜.
//오토 언박싱
int unBoxingNumber2 = boxingNumber2;
값을 비교해보자
int inum = 20;
Integer integerNum1 = Integer.valueOf(20);
Integer integerNum2 = Integer.valueOf(20);
Integer integerNum3 = 20;
Integer integerNum4 = 20;
/*값 비교*/
System.out.println("int와 Integer비교 : " + (inum == integerNum1));
System.out.println("(inum == integerNum3) = " + (inum == integerNum3));
/*주소값 비교*/
System.out.println("(integerNum1 == integerNum2) = " + (integerNum1 == integerNum2));
System.out.println("(integerNum1 == integerNum3) = " + (integerNum1 == integerNum3));

문자열(String) 값을 기본자료형 값으로 변경하는 것을 parsing이라고 한다.
byte b = Byte.parseByte("1");
short s = Short.parseShort("2");
int i = Integer.parseInt("4"); // 중요
long l = Long.parseLong("8"); //8L 8l 안됨
float f = Float.parseFloat("3.14f");
double d = Double.parseDouble("3.14");
boolean bool = Boolean.parseBoolean("true");
valueOf()
기본 자료형 값을 wrapper 클래스 타입으로 변환시키는 메소드
toString()
필드값을 문자열로 반환하는 메소드
/*parsing*/
byte b = Byte.parseByte("1");
short s = Short.parseShort("2");
int i = Integer.parseInt("4"); // 중요
long l = Long.parseLong("8"); //8L 8l 안됨
float f = Float.parseFloat("3.14f");
double d = Double.parseDouble("3.14");
boolean bool = Boolean.parseBoolean("true");
String c = Character.valueOf('a').toString(); // 'a' => "a"
String의 valueOf

오버라이딩 되어 있으므로 원하는 타입을 작성하면 문자열로 변경해준다.
강제로 예외를 발생
throw new 예외클래스명();
예외처리방법
Throws는 예외가 발생했을 때, 메소드를 통하여 예외를 넘기는 것이다. 최종 목적지는 main이며, main에 예외가 넘어왔을 땐 프로그램이 예외를 발생시키고 중단시킨다.
public static void main(String[] args) throws Exception {
예외 발생 가능성이 있는 코드를 작성하는 곳이다.
public static void main(String[] args) {
ExceptionTest exceptionTest = new ExceptionTest();
try {
/*try : 예외 발생 가능성이 있는 코드를 작성 */
exceptionTest.checkEnoughMoney(10000,50000);
exceptionTest.checkEnoughMoney(10000,5000);
System.out.println("=========== 상품 구입 완료 ============");
} catch (Exception e) {
/*catch : try 블럭 안에서 예외가 발생할 경우 catch블럭의 코드가 실행된다.*/
System.out.println("=========== 상품 구입 불가 ============");
e.printStackTrace(); // 콘솔창에 에러 내용 확인할 수 있음
}
System.out.println("프로그램을 종료합니다.");
}
Exception e : catch문내에 발생한 모든 예외가 e 로 들어가게 된다. 이 e를 이용해서 코드를 작성하거나 예외를 저장할 수 있다.
e.printStackTrace : 예외가 발생한 에러 메시지를 콘솔창에다가 띄어준다.
예외가 발생한 상황이다.

에러가 발생한 시점에서는 예외처리로 넘어가기 때문에 try 문에서 중단된다.

예외가 발생하더라도 try-catch문 이후로는 순차적으로 흘러간다.
사용자 정의 예외처리
사용자 정의 예외 클래스를 만들기 위해서는 Exception 클래스를
상속받으면 된다.
경우에 따라서는 더 상위 타입인 Throwable 클래스나 하위 타입의 클래스를 상속받기도 한다.
Exception 클래스를 상속받는다.
상속받으면 위처럼 exception의 것들을 상속받을 수 있다.
super() 생성자를 통해 타고 올라가면 사용자 정의 → Exception → Throwable 까지 올라가게 된다. Throwable에서는 생긴 예외를 stack으로 쌓아서 사용자 콘솔에 출력해준다.
JDK 1.7에 추가된 문법이다.
close 해야 하는 인스턴스의 경우 try앞에[ 괄호 안에서 생성하면 해당 try-catch 블럭이 완료될 때 자동으로 close 처리해준다.
try (BufferedReader in = new BufferedReader(new FileReader("test.dat"));){
// 위 try() 안에 있는 내용은 사용되고 알아서 닫힌다.
String s;
while((s = in.readLine()) != null){
System.out.println(s);
}
} catch (FileNotFoundException/* | EOFException*/ e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
예외 상황별로 catch블럭을 따로 작성해서 처리할 수도 있고, 상위 타입의 Exception을 이용해서 통합적으로 처리할 수도 있다.
단, 상위 타입의 Exception 블럭이 먼저 기술되면 아래로 코드가 도달할 수 없으므로 컴파일 에러가 발생한다.
서술 순서는 하위 타입 ⇒ 상위 타입으로 기재한다
catch (PriceNegativeException | MoneyNegativeException | NotEnoughMoneyException e)
아래 코드들은 Negative Money 예외처리 코드들을 모두 정리해 놓은 글로, 사용자 정의 예외처리를 어떻게 정의하는지 나타낸다.
package com.lhw.section02.userexception;
import com.lhw.section02.userexception.exception.MoneyNegativeException;
import com.lhw.section02.userexception.exception.NegativeException;
import com.lhw.section02.userexception.exception.NotEnoughMoneyException;
import com.lhw.section02.userexception.exception.PriceNegativeException;
public class Application {
public static void main(String[] args) {
ExceptionTest exceptionTest = new ExceptionTest();
try {
/*상품 가격이 음수일 경우*/
exceptionTest.checkEnoughMoney(-1000, 1000);
/*잔액이 음수일 경우*/
exceptionTest.checkEnoughMoney(1000, -1000);
/*(잔액 < 가격) 인 경우*/
exceptionTest.checkEnoughMoney(1000, 500);
/*예외 상황별로 catch블럭을 따로 작성해서 처리할 수도 있고, 상위 타입의 Exception*/
/* } catch (Exception ex) { //아래의 내용을 상위 클래스로 받아줄 수 있다.
ex.printStackTrace();
}*/
// 아래 내용은 좀 더 상세하게 나누어서 작업해주는 부분이다.
/* 예외 클래스를 catch하는 순서 : 더 하위 클래스를 먼저 작성
* 상위가 먼저 올라오면, 어떤 하위 클래스에서 예외가 발생했는지
* 확인할 수 없다.*/
} catch (PriceNegativeException e) {
e.printStackTrace();
} catch (MoneyNegativeException e) {
System.out.println(e.getMessage());
} catch (NotEnoughMoneyException e) {
System.out.println(e.getMessage());;
} catch (NegativeException e) {
System.out.println(e.getMessage());
}
}
}
package com.lhw.section02.userexception;
import com.lhw.section02.userexception.exception.MoneyNegativeException;
import com.lhw.section02.userexception.exception.NegativeException;
import com.lhw.section02.userexception.exception.NotEnoughMoneyException;
import com.lhw.section02.userexception.exception.PriceNegativeException;
/*
* ExceptionTest 클래스는 예외를 직접적으로 발견하여 throw하고, 메소드를 throws
* 를 하는 공간이다. 이 곳에서부터 예외가 시작된다.
*/
public class ExceptionTest {
/* Throw 와 Throws
* throw는 예외 클래스 객체(인스턴스)를 생성해 주는 것이고
* throws는 해당 메소드가 감지할 예외 클래스를 읽어오는 것이다.
*/
public void checkEnoughMoney(int price, int money) throws
PriceNegativeException, MoneyNegativeException, NotEnoughMoneyException {
if (price < 0) {
throw new PriceNegativeException("상품 가격이 음수일 수 없습니다..");
}
if (money < 0) {
throw new MoneyNegativeException("잔액은 음수일 수 없습니다.");
}
if (money < price) {
throw new NotEnoughMoneyException("가진 돈 보다 상품 가격이 더 높습니다.");
}
}
}
package com.lhw.section02.userexception.exception;
/* 예외 클래스이다.
* 사용자 지정 예외를 지명해주고, 어떤 역할을 할지만 기술한다.
* 사용자 지정 예외 클래스라면 `Exception`을 상속받아야 한다.
* 하지만 때에 따라 더 상위인 Throwable이나 하위 클래스를 상속 받아도 된다.*/
public class NotEnoughMoneyException extends Exception {
/* 아래 생성자는 매개변수 생성자로, NotEnoughMoney예외를 생성할 때
* 반드시 해당 매개변수를 받아오게 강제화 하는 것이다.*/
public NotEnoughMoneyException(String s) {
super(s);
}
}
package com.lhw.section02.userexception.exception;
/*
* 사용자 정의 예외 클래스를 만들기 위해서는 Exception 클래스를
* 상속받으면 된다.경우에 따라서는 더 상위 타입인 Throwable
* 클래스나 하위 타입의 클래스를 상속받기도 한다.
* */
public class NegativeException extends Exception {
public NegativeException(String message) {
super(message);
}
}
package com.lhw.section02.userexception.exception;
/* PriceNegativeException 또한 예외 클래스이다.
* 이 클래스는 사용자 지정 예외인 NegativeException을 상속받는다.
* 음수가 들어왔을 때의 예외 처리하는 경우 중, 가격이 음수인 경우만
* 발생하는 예외를 처리해주는 클래스이기에, Exception이 아니라
* 훨씬 하위 클래스, 그리고 사용자 지정 예외 클래스를 가져왔다.*/
public class PriceNegativeException extends NegativeException {
/*
상속구조를 통해서 나중에 필요로 할 수 있는 다형성을
염두에 두고 세부로 나눈 것이다.
원래 상속을 하면 부모의 것을 따라가야한다. 기존에는
부모에 기본 생성자가 있기 때문에 아무것도 안해도 된다.
하지만 얘는 지금 부모가 매개변수 생성자를 가지고 있으니
얘도 매개변수 생성자를 만들어 줘야한다.
*/
public PriceNegativeException(String message) {
super(message);
}
}
package com.lhw.section02.userexception.exception;
/* MoneyNegative도 PriceNegative와 마찬가지로 NegativeException을
* 상속받으며, 매개변수 생성자가 있다.*/
public class MoneyNegativeException extends NegativeException {
public MoneyNegativeException(String message) {
super(message);
}
}
내용이 많아 아래 링크에 따로 정리해 두었다.
입출력 링크
내용이 많아 아래 링크에 따로 정리해 두었다.
제네릭 링크
내용이 많이 아래 링크에 정리한다.
컬렉션 링크