이때 탐욕 알고리즘을 이용해서 짐을 박스에 담을 수 있는 최적의 선택을 했을 때 박스가 몇개 필요한가?
해결
import java.util.ArrayList;
import java.util.List;
public class movingStuff {
public int movingStuff(int[] stuff, int limit) {
int box = 0; // 박스에 넣은 짐 수(최대 2)
int bstuff = 0; // 박스에 담은 짐 무게
int result = 1; // 필요한 박수 수
int max = 0; // 남은 짐의 최댓값
int maxIndex = 0; // 짐 최댓값의 인덱스
List<Boolean> boo = new ArrayList<>(); // 짐을 확인했는지 확인할 배열 (모두 false)
// 배열을 리스트로 변경
ArrayList<Integer> slist = new ArrayList<Integer>();
// 배열을 값을 리스트에 추가
for (int i = 0; i < stuff.length; i++) {
slist.add(stuff[i]);
boo.add(false);
}
// 짐 리스트를 모두 포장할 때까지 반복
while (!slist.isEmpty()) {
// 한 번 확인한 max는 초기화
max = 0;
// 만약 다음 최대값을 가져오는데
for (int i = 0; i < slist.size(); i++) {
if (max < slist.get(i) && boo.get(i) == false) {
max = slist.get(i);
maxIndex = i;
}
}
// 확인한 가장 무거운 짐은 true로 변경
boo.set(maxIndex, true);
// 짐 배열에서 가장 큰 값 가져옴
// 가져온 제일 무거운 짐을 넣었을 때 limit가 넘지 않을 지 확인
if (bstuff + max <= limit) {
// 안넘으면 박스에 추가
bstuff += max;
box++;
// 넣은 짐은 리스트에서 제거
slist.remove(maxIndex);
boo.remove(maxIndex);
if(slist.isEmpty()) break;
// 추가한 후에 박스안의 짐이 2개가 되면
if (box == 2) {
// 박스 변수를 비움
box = 0;
bstuff = 0;
// 아직 담을 짐이 있다면 박스 새로 추가
result++;
// boo 초기화
for (int k = 0; k < boo.size(); k++) {
boo.set(k, false);
}
}
} else if (!boo.contains(false)) {
// 새로운 박스 가져옴
box = 0;
bstuff = 0;
// 아직 담을 짐이 있다면 박스 새로 추가
result++;
// boo 초기화
for (int k = 0; k < boo.size(); k++) {
boo.set(k, false);
}
}
}
return result;
}
}
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int x = sc.nextInt();
int y = sc.nextInt();
int w = sc.nextInt();
int h = sc.nextInt();
int horizon, length;
if((double)x < (double)w / 2) {
horizon = x;
}
else horizon = w - x;
if((double)y < (double)h / 2) {
length = y;
}
else length = h - y;
System.out.print(Math.min(horizon, length));
}
}
🔎 백준의 유용한 기능
백준과 깃헙 연동하기(백준허브 확장자)
크롬 확장자 중에 백준허브를 추가(Add to Chrome)
깃헙 아이디와 Authenticate
Repository 추가 또는 기존 Repository 주소 입력해서 연동
백준에서 문제를 풀고 제출하면 자동으로 내 깃헙 repository에 commit이 됩니다!⭐️
백준 티어 보기
백준에는 티어가 존재하는줄 몰랐는데
브론즈, 실버, 골드,... 등 티어가 존재한다.
solved를 이용하면 백준에서 문제의 티어를 볼 수 있다.
이외에도 백준 티어를 깃헙 프로필에 보이게 할 수도 있다. ㅋㅋ
근데 아직 백준 티어가 브론즈 바닥이라서 나중에 당당하게 티어를 보여줄 만큼 올라갔을 때 깃허브 프로필에 올려야 겠다..
자바가 처음 나온 90년대에는 객체 지향 언어가 각광받았지만 지금은 함수형 프로그래밍이 다시 각광받고 있다.
이런 패러다임에 맞춰서 자바 8버전부터 함수형 프로그래밍을 할 수 있도록 '람다식(Lambda Expressions)'을 지원한다.
자바에서 제공하는 함수형 프로그래밍 방식을 '람다식'이라고 한다.
람다식을 사용하면 간결하게 함수를 만들고 사용할 수 있다.
람다식은 먼저 인터페이스를 작성하고 사용하려는 인터페이스를 익명으로 구현하는 방법이다.
➤ 람다식의 작성
(매개변수) -> {실행코드}
람다식 문법 ⬇️
public class LambdaTest {
public static void main (String[] args) {
x -> {System.out.println(x);} // 매개변수가 한 개인 경우 괄호 생략 가능 ⭕️
x, y -> {System.out.println(x * y);} // 매개변수가 두 개 이상인 경우 괄호 생략 ❌
(x, y) -> System.out.println(x * y); // 실행문이 한 개인 경우 괄호 생략 가능 ⭕️
(x, y) -> {return x + y;} // 하지만 실행문이 한 개여도 return문인 경우 괄호 생략 ❌
(x, y) -> x + y; // 실행문이 한 개이고 return문인 경우 return과 중괄호 모두 생략 ⭕️
}
}
<작성규칙>
매개변수가 하나일 경우는 괄호를 생략 가능하지만 2개 이상인 경우엔 생략할 수 없다.
실행문이 하나인 경우에도 중괄호를 생략할 수 있지만 하나인 실행문이 return문인 경우에는 생략할 수 없다.
실행문에 return문만 있는 경우는 return과 중괄호 모두 생략하는 것이 올바르다.
➤ 함수형 인터페이스
자바에서 메소드를 사용하려면 클래스를 만들고 객체롤 메소드를 호출해야 한다.
인터페이스는 클래스마다 공통적으로 사용하는 기능을 메소드마다 다르게 정의할 수 있게 해준다.
하지만 함수를 만들어서 사용할 때는 함수형 인터페이스를 만들면 같은 식에 다른 값을 넣어서 재사용할 수 있어 편리하다.
🌱 함수형 인터페이스를 작성하기
package lambdaPractice;
@FunctionalInterface
public interface BigNum {
int getMax(int num1, int num2); // 추상 메서드 선언
// int add(int num1, int num2); // 함수형 인터페이스는 한 개의 추상 메서드만 선언할 수 있음!⚠️
}
위에는 두개의 숫자를 받아서 더 큰 숫자를 반환하는 함수를 포함한 함수형 인터페이스이다.
위에를 보면 작성하는 규칙이 있다.
1. '@FunctionalInterface' 애너테이션을 사용해서 컴파일러에 함수형 인터페이스임을 알릴 수 있다.
2. 한 개의 추상 메소드만 하나만 정의할 수 있다.
🌱 함수형 인터페이스 구현하기!
package lambdaPractice;
public class BigNumTest {
public static void main(String[] args) {
BigNum big = (x, y) -> (x >= y) ? x : y;
System.out.println(big.getMax(10, 12));
System.out.println(big.getMax(5,1));
}
}
작성한 함수형 인터페이스형인 BigNum형 big 함수를 선언해주었다.
매개변수로 x와 y를 받아서 'x >= y' 가 true이면 x, false이면 y를 반환하도록 만들었다.
➤ 왜 함수형 프로그래밍을 이용할까?
자바에서 람다식을 이용한 함수형 프로그램 코드가 객체를 만들어서 사용하는 객체지향형 코드보다 훨씬 간결하고 직관적이다.
함수식을 통해서 용도를 한눈에 이해할 수 있게 된다.
그리고 재사용하기도 용이하고 확장성 있는 프로그래밍이 가능하다.
함수형 인터페이스를 한번 구현하면 구현한 객체명으로 같은 식을 계속 사용할 수 있다.
만약 중간에 식을 조금 수정해야 한다면 다시 만들 필요없이 람다식을 이용해서 만들어 놓은 객체에 함수식을 고쳐주면 된다.
함수가 입력받은 자료 이외에 외부 자료에 영향을 미치지 않기 때문에 여러 자료를 동시에 처리하는 병렬 처리에 적합하다.
➤ 메서드 레퍼런스
함수형 인터페이스와 비슷한 기능을 제공하는 것이 메서드 레퍼런스이다.
메서드 레퍼런스는 작성해놓은 클래스의 객체를 생성하지 않고도 클래스 내의 메서드를 이용할 수 있게 해준다.
람다식에서 제공되는 메개변수의 메서드를 호출하여 매개변수를 매개값으로 사용하는 경우도 있다.
(a,b) -> {a.instanceMethod(b);};
클래스 :: instanceMethod
정적 메서드 레퍼런스와 작성방법은 동일하지만 a의 인스턴스 메서드가 참조된다는 점에서 다른 코드이다.
➤ 생성자 참조
생성자를 참조하는 람다식 표현도 작성 가능하다.
(a, b) -> {return new Object(a, b);};
위의 코드를 생성자 참조 표현으로 간단하게 표현할 수 있다.
클래스 :: new
❑ 스트림(Stream)
➤ 스트림이란?
스트림은 배열, 컬렉션의 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자 이다.
정렬이나 특정 값을 제외하고 출력하는 등 여러 자료 처리에 대한 기능을 구현해 놓은 클래스이다.
스트림을 사용하면 List, Set, Map, 배열 등 다양한 데이터 소스를 스트림형으로 변환하고 다양한 메서드를 제공한다.
처리해야하는 자료에 상관없이 같은 방식으로 메서드를 호출할 수 있기 때문에 자료를 추상화 했다고 표현한다.
➤ 스트림 생성하기
배열을 스트림으로 생성하기 : Arrays.stream(arr)
Collection 인터페이스에는 Stream<E> stream() 메서드가 정의되어 있기 때문에
Collection을 구현한 클래스에서는 모두 클래스명.stream()으로 스트림 클래스를 만들 수 있다.
int[] arr = new int[];
Arrays.stream(arr); // 배열을 stream형으로 만들기
List<String> list = new ArrayList<String>();
Stream<String> stream = list.stream(); // ArrayList를 stream형으로 만들기
stream.allMatch(a -> a%2 == 0); // 모든 요소들이 조건을 만족하는지 조사
stream.anyMatch(a -> a%2 == 0); // 최소 한 개 이상의 요소가 조건을 만족하는지 조사
stream.noneMatch(a -> a%2 == 0); // 모든 요소들이 조건을 만족하지 않는지 조사
public class OutClass { // 외부 클래스
private int num = 1; // private으로 선언한 인스턴스 변수
private static int sNum = 2; // private으로 선언한 정적 변수
static class InStaticClass { // 정적 내부 클래스
int inNum = 4; // 정적 내부 클래스의 인스턴스 변수
static int sInNum = 5; // 정적 내부 클래스의 정적 변수
void inTest() {
// num += 10; // 오류⚠️ (정적 내부 클래스에서는 외부 클래스의 인스턴스 변수 사용 불가)
System.out.println("InStaticClass inNum = " + inNum + "(내부 클래스의 인스턴스 변수 사용)");
System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 정적 변수 사용)");
System.out.println("OutClass sNum = " + sNum + "(외부 클래스의 정적 변수 사용)");
}
static void sTest() {
// num += 10; // 오류⚠️ (정적 내부 클래스에서는 외부 클래스의 인스턴스 변수 사용 불가)
// inNum += 10; // 오류⚠️ (정적 메소드에서는 인스턴스 변수 사용 불가)
sInNum += 100; // 내부 클래스의 정적 변수 사용 가능
System.out.println("OuterClass sNum = " + sNum + "(외부 클래스의 정적 변수 사용)");
System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 정적 변수 사용)");
}
}
}
class main {
public static void main(String[] args) {
OutClass.InStaticClass sInClass = new OutClass.InStaticClass();
System.out.println("정적 내부 클래스 일반 메소드 호출");
sInClass.inTest();
System.out.println();
System.out.println("정적 내부 클래스의 정적 메소드 호출");
OutClass.InStaticClass.sTest();
}
}
<출력 결과>
정적 내부 클래스 일반 메소드 호출
InStaticClass inNum = 4(내부 클래스의 인스턴스 변수 사용)
InStaticClass sInNum = 5(내부 클래스의 정적 변수 사용)
OutClass sNum = 2(외부 클래스의 정적 변수 사용)
정적 내부 클래스의 정적 메소드 호출
OuterClass sNum = 2(외부 클래스의 정적 변수 사용)
InStaticClass sInNum = 105(내부 클래스의 정적 변수 사용)
정적 내부 클래스 내의 일반 메소드와 정적 메소드는 변수 유형에 따라 사용가능 여부가 다르다.
표로 정리하면 아래와 같다.
정적 내부 클래스 메소드
변수 유형
사용 가능 여부
일반 메서드 void Method()
외부 클래스의 인스턴스 변수
x
외부 클래스의 정적 변수
o
정적 내부 클래스의 인스턴스 변수
o
정적 내부 클래스의 정적 변수
o
정적 메서드 static void sMethod()
외부 클래스의 인스턴스 변수
x
외부 클래스의 정적 변수
o
정적 내부 클래스의 인스턴스 변수
x
정적 내부 클래스의 정적 변수
o
정적 내부 클래스의 정적 메소드에서는 외부 클래스나 내부 클래스에서 정적 변수만 사용 할 수 있다.
➤ 다른 클래스에서 정적 내부 클래스 사용하기
정적 내부 클래스는 외부 클래스를 생성하지 않고도 내부 클래스 자료형으로 바로 선언하여 생성할 수 있다.
OutClass.InStaticClass sInClass = new OutClass.InStaticClass();
또한 정적 내부 클래스에 선언한 메소드나 변수는 private이 아닌 경우에 다른 클래스에서도 사용할 수 있다.
class LocalTest { // 외부 클래스
int num = 10;
static int sNum = 100;
Runnable getMyPet(int pNum){ // 메소드
int mNum = 20;
// static int insNum = 200; // 메소드내 정적 변수 선언 불가
class MyPet implements Runnable{ // 🔥지역내부 클래스
int inNum = 30; // 컴파일러가 자동으로 final 추가
// num += 10; // 외부 클래스의 멤버변수 변경 불가
// mNum += 10; // 지역변수는 상수로 바뀌므로 변경하면 오류 발생
// pNum += 10; // 메소드의 지역변수는 상수로 바뀌므로 변경하면 오류 발생
public void run() {
System.out.println("외부 클래스의 멤버 변수 num : " + num);
System.out.println("외부 클래스의 정적 변수 sNum : " + sNum);
System.out.println("외부클래스의 메소드내 지역변수 mNum : " + mNum);
System.out.println("지역 내부 클래스의 멤버 변수 inNum : " + inNum);
System.out.println("지역 내부 클래스를 감싸고 있는 메소드의 매개변수 pNum : " + pNum);
}
}
return new MyPet();
}
}
만든 지역 내부 클래스는 이렇게 사용된다.
public class LocalInnerPractice {
public static void main(String[] args) {
LocalTest myPet = new LocalTest();
Runnable runnable = myPet.getMyPet(10);
runnable.run();
}
}
<출력 결과>
외부 클래스의 멤버 변수 num : 10
외부 클래스의 정적 변수 sNum : 100
외부클래스의 메소드내 지역변수 mNum : 20
지역 내부 클래스의 멤버 변수 inNum : 30
지역 내부 클래스를 감싸고 있는 메소드의 매개변수 pNum : 10
지역내부 클래스를 생성하는건 생소하기도 하고 복잡하다.
일단은 외부 클래스를 반드시 만들어 줘야 하고
메소드를 실행해서 return으로 지역 내부 클래스 객체를 생성해서 반환하도록 설계해주면 된다.
※ 참고) 메소드 내의 지역 변수는 static 변수로 선언할 수 없다.
자바에서는 static local variable을 지원하지 않는다.
static local variables are not allowed!
static 변수는 field 에만 정의되는 변수이다.
메소드는 실행될 때만 stack 메모리에 올라갔다가 실행이 끝나면 stack메모리에서 사라진다.
그런데 정적 변수는 컴파일과 동시에 클래스 정보와 함께 메모리를 할당받고 프로그램 실행중에는 계속 메모리 공간에 있어서 접근할 수 있다.
class Person {
Runnable getRunnable(int i) {
int num = 100;
return new Runnable() { // 익명 내부 클래스, Runnable 인터페이스 생성
@Override
public void run() {
// num += 10; // 오류 발생
// i += 20; // 오류 발생
System.out.println("매개변수 i : " + i);
System.out.println("지역 변수 num : " + num);
}
}; // 끝나는 중괄호 뒤에 ; 붙임
}
Runnable runnable = new Runnable() { // 익명 내부 클래스를 변수에 대입
@Override
public void run() {
System.out.println("Runnable이 구현된 익명 클래스 변수");
}
};
}
생성한 익명 내부 클래스를 사용해보자
public class AnnoymousInnerTest {
public static void main(String[] args) {
Person p = new Person();
Runnable runnable = p.getRunnable(10);
runnable.run();
p.runnable.run();
}
}
<출력 결과>
매개변수 i : 10
지역 변수 num1 : 100
Runnable이 구현된 익명 클래스 변수
익명 클래스는 끝나는 부분에 세미콜론(;)을 작성해서 컴파일러에 익명 클래스가 끝났다고 알려줘야 한다.
익명 내부 클래스는 단 하나의 인터페이스나 단 하나의 추상 클래스를 바로 생성할 수 있다.
인터페이스는 인스턴스를 생성할 수 없는데..???
인터페이스의 인스턴스를 만들려면 구현을 다하지 않은 메소드를 작성해줘야 한다.
익명 내부 클래스를 호출할 때는 지역 내부 클래스 처럼
인스턴스를 생성해서 호출해도 되고
예시 코드의 'p.runnable.run()' 처럼 클래스내의 인터페이스 이름으로 바로 호출할 수도 있다.
익명 내부 클래스는 아직은 이해하기 어려운 개념이라는 생각이 든다. 🙁
익명 내부 클래스의 용도는 주로 안드로이드에서 버튼이나 텍스트 상자 같은 위젯(widget)의 이벤트를 처리하는 핸들러를 구현하는데 사용? 한다고 한다.
어떤 변수가 하나의 참조 자료형이 아닌 여러 참조 자료형을 사용할 수 있도록 프로그래밍하는 것
여러 참조 자료형이 쓰일 수 있을 때 미리 특정한 자료형을 지정하지 않고 제네릭으로 선언해서 클래스나 메소드가 사용되는 시점에 어떤 자료형 사용할지 지정됨
➤제네릭 클래스 정의하기
클래스명 <T> {
}
ex)
class GenericDeclaration <T> {
private T variable;
public T getVariable() {
return variable;
}
public void setVariable(T variable) {
this.variable = variable;
}
}
여러 자료형으로 바꾸어 사용할 부분에 자료형 대신 T 작성
<> : 다이아몬드 연산자
T를 자료형 매개변수(Type parameter, 타입 매개변수)
T : 대입된 자료형
GenericDeclaration <T> : 제네릭 자료형, 매개변수화된 자료형
ArrayList<Integer> array = new ArrayList<>();
선언하는 부분의 <>에 자료형을 작성했기 때문에 생성부분의 <> 안은 생략 가능
컴파일러가 유추 가능하기 때문
주로 <>안에 T를 많이 사용 (Type)
E (Element), K (Keyt), N (Number), V (Value), R (Result) 등 있음
위의 예시 뿐만 아니라 A, B, C 등 사용자가 정의해서 사용해도 된다.
⤵️아래는 저의 견해입니다.
타입 매개변수는 수학의 변수 x라고 생각해주면 코드를 작성하고 읽기가 용이하다.
수학에서 변수 x는 어떤 수든 대입할 수 있다.
하지만 식에서 x에 특정 수를 대입하는 순간 식 안의 모든 x는 그 수로 정해져서 계산된다.
예를 들어서 x+1-2x 라는 식이 있다고 가정해보자.
이때 x에 1을 대입하려고 하면 x, -2x 모두 x=1을 대입하고 계산해야 한다.
x에는 x=1 을 대입하고 -2x에는 x=2를 대입하는 식으로 계산해선 안된다.
변수 x는 같은 수를 의미하므로 모든 x엔 같은 수로 대입해줘야 한다.
프로그래밍에서 제네릭을 컴파일하는 과정에서도 마찬가지이다.
컴파일러는 같은 알파벳의 타입 매개변수는 모두 같은 변수라고 생각한다.
따라서 타입 매개변수 T는 인스턴스가 만들어질 때 같은 타입이 대입된다.
➤ static 변수와 타입 매개변수
static 변수, static 메소드 내에서는 타입 매개변수 사용할 수 없음
🔎 이유는?
제네릭의 타입이 정해지는 것은 인스턴스가 생성되는 순간이다.
하지만 static은 클래스 생성 시 클래스 정보와 함께 메모리에 올라가는데 타입을 지정해주지 않으면 메모리를 할당할 수 없다.
따라서 타입 매개변수의 자료형이 결정되는 시점보다 static 변수나 메소드가 생성되는 시점이 더 빠르기 때문에 문제가 발생하게 된다.
➤ 왜 제네릭이 직접 형 변환하는 것보다 안정적일까?
Object 등의 상위 클래스 타입으로 정의하고 직접 형변환 해주면 제네릭보다 불안정함
제네릭 클래스는 컴파일러가 일단 대입된 자료형이 잘 쓰였는지 확인하고
class 파일을 생성할 때 타입 매개변수를 사용한 곳에 지정한 자료형에 따라 컴파일함
➤ 제네릭 메소드(Generic Method)
클래스 전체가 아닌 클래스 내부의 특정 메소드만 제네릭으로 선언
매개변수나 메소드 시그니처에 타입 매개변수 T를 사용한 메소드
메소드가 호출되는 시점에 제네릭 타입이 결정됨
정의되는 시점에는 어떤 타입이 입력되는지 알 수 없음
-> length()와 같은 클래스내 메소드는 정의하는 시점에 사용 불가
-> 최상위 클래스 Object의 메소드는 사용 가능
일반 메소드, static 메소드에서 모두 활용 가능
➤ 제네릭의 장점
- 여러 참조 자료형마다 코드를 작성하지 않아도 되서 코드가 간결하고 직관적
- 매번 형 변환을 할 필요가 없기 때문에 오류의 확률 줄어듬 -> 안정성
- 새로운 자료형을 사용할 땐 그 자료형만 생성하고 사용할때 <> 안에 자료형 넣기만 하면됨
class Animal {
void info(){
System.out.println("나는 동물 입니다.");
}
}
class Dog extends Animal{
@Override
void info() {
System.out.println("나는 개입니다.🐕");
}
void woof() {
System.out.println("멍멍멍멍");
}
}
class Cat extends Animal {
@Override
void info() {
System.out.println("나는 고양이입니다.🐱");
}
void meow() {
System.out.println("야옹~야옹~");
}
}
class Lizard extends Animal {
String sexuality;
@Override
void info() {
System.out.println("나는 도마뱀입니다.🦎");
}
}
Animal의 info클래스를 각각의 하위 클래스에서 이름에 맞게 재정의를 해줬다.
public class AnimalTest {
public static void main(String args[]) {
// 다형성을 이용하여 같은 참조변수로 선언하기
Animal dog = new Dog();
Animal cat = new Cat();
Animal lizard = new Lizard();
// 재정의한 메소드 실행
dog.info();
cat.info();
lizard.info();
}
}
<출력결과>
나는 개입니다.🐕
나는 고양이입니다.🐱
나는 도마뱀입니다.🦎
상위 클래스인 Animal로 타입을 지정하고 new로 만든 인스턴스는 각각 하위 클래스로 지정해줄 수 있다.
이렇게 하위 클래스로 만들고 상위 클래스로 타입을 바꾸는 것을 '업캐스팅'이라고 한다.
상속 계층도에서 위로 올라가서 업캐스팅이라고 생각하자. (반대는 아래로 가니까 다운캐스팅)
자바에서는 업캐스팅은 별다른 키워드 없이 암묵적으로 가능하다.
이것을 '묵시적 형변환' 이라고도 한다.
하지만 반대로 암묵적으로 다운캐스팅(상위클래스 ➡️ 하위클래스)은 불가하다!!
상위 클래스로 인스턴스를 만든 후에 하위 클래스로 다운 캐스팅하게 되면 사진처럼 빨간 줄이 그어지고 오류가 난다.
❏ 왜 업캐스팅은 묵시적으로 되지만 다운캐스팅은 안될까?
하위 클래스들은 상위 클래스보다 구체적이고 메소드나 멤버변수가 같거나 많다.
하지만 상위 클래스는 더 일반적으로 만들어서 메소드나 멤버변수가 같거나 적다.
그래서 상위 클래스로 만든 인스턴스를 하위 클래스 타입의 메모리 공간에 집어넣으면
일부 메소드와 멤버변수를 사용할 수 없는 일이 발생한다.
실제로 클래스 내에 정의된 멤버변수와 메소드를 쓸 수 없어서 에러가 발생한다.
반대로 하위 클래스로 만든 인스턴스를 상위 클래스 타입의 메모리에 넣는건
인스턴스 만들 떄 있던 일부 기능이나 속성을 사용하지는 못하더라도 상위 클래스에 정의된 모든 메소드나 멤버변수는 사용할 수 있다.
내용물은 하위 클래스더라도 참조 타입은 상위 클래스이기 때문에 문제될 것은 없는 것이다.
참고) 상위 클래스 위의 상위 클래스들도 묵시적으로 형변환이 가능할까?
상속 계층이 여러 층일 때 여러 층 위의 상위 클래스로의 형변환도 묵시적으로 이루어진다.
❏ instanceof 를 통한 다운 캐스팅
상위 클래스로 형변환 했던 하위 클래스를 다시 원래 자료형으로 돌려놓을 수는 없을까?
하위 클래스로 직접적으로 형변환 할 수는 없지만 instanceof라는 키워드를 사용하면 원래 형태가 하위 클래스인지 확인할 수는 있다.
instanceof 예약어는 왼쪽에 있는 변수의 원래 인스턴스 형이 오른쪽 클래스 자료형인지 확인해준다.
➡️ 원래 자료형이 맞으면 true / 틀리면 false 를 반환한다.
주로 if 문과 함께 사용한다.
하위 클래스를 업 캐스팅하면 상위 클래스에 없는 해당 클래스의 고유한 메소드나 필드는 사용할 수 없게 된다.
위에 작성한 예시 코드에서 Dog와 Cat 클래스에는 상위 클래스에서 재정의한 메소드가 아닌 woof()와 meow()메소드가 있다.
그리고 Lizard에는 스트링 타입의 성별 멤버 변수가 선언되있다.
상위 클래스에서 정의 하지 않은 woof()와 meow(), sexuality를 업캐스팅한 변수에서 접근해보았다.
public static void main(String args[]) {
// 다형성을 이용하여 같은 참조변수로 선언하기
Animal dog = new Dog();
Animal cat = new Cat();
Animal lizard = new Lizard();
Lizard example = new Animal();
// 하위클래스에만 있는 메소드 실행
dog.woof(); // cannot find symbol 오류
cat.meow(); // cannot find symbol 오류
// 하위 클래스에만 있는 필드 실행
lizard.sexuality = "female"; // cannot find symbol 오류
}
실행하게 되면 'cannot find symbol' 에러가 뜨게 된다.
상위 클래스로 형변환하면서 하위 클래스에만 있던 메소드와 필드는 메모리 할당이 되지 않아 사용할 수 없게 된 것이다.
이럴 때 instanceof 를 사용해서 원래 자료형을 확인하고 맞으면 원래 인스턴스형강제로 다운캐스팅 해주면 된다.
원래 인스턴스형으로 변환할 때는 괄호안에 원래 자료형을 반드시 명시해야한다.
상위 클래스로 형변환 했던 객체를 다시 원래 자료형으로 형변환하는 것을 '다운캐스팅'이라고 부르기도 한다.
public static void main(String[] args) {
// 상위 클래스로 형변환
Animal dog = new Dog();
Animal cat = new Cat();
Animal lizard = new Lizard();
// instanceof를 이용한 다운캐스팅
if (dog instanceof Dog) {
Dog dog1 = (Dog)dog;
dog1.woof();
}
if (cat instanceof Cat) {
Cat cat1 = (Cat)cat;
cat1.meow();;
}
if (lizard instanceof Lizard) {
Lizard lizard1 = (Lizard)lizard;
lizard1.sexuality = "female";
System.out.println(lizard1.sexuality);
}
}