devkobe24.com
AWS
Algorithm
2024
Archive
AWS_archive
CPP_DS
CS_archive
DataStructure
Database
HackTheSwift
Java_archive
Leet-Code
MySQL
Network_archive
OS
Post
Read English Book
SQL_archive
Spring & Spring Boots
TIL
Web
CS
2024
DB
Interview
Java
Java多識
Java
Network
2024
SQL
2024
Spring
Home
Contact
Copyright © 2024 |
Yankos
Home
>
Archive
> Java_archive
Now Loading ...
Java_archive
☕️[Java] Math, Random 클래스
Math, Random 클래스 Math 클래스 Math는 수 많은 수학 문제를 해결해주는 클래스입니다. 너무 많은 기능을 제공하기 때문에 대략 이런 것이 있구나 하는 정도면 충분합니다. 실제 필요할 때 검색하거나 API 문서를 찾아봅시다. 1. 기본 연산 메서드 abs(x) : 절대값 max(a, b) : 최대값 min(a, b) : 최소값 2. 지수 및 로그 연산 메서드 exp(x) : e^x 계산 log(x) : 자연 로그 log10(x) : 로그 10 pow(a, b) : a의 b 제곱 3. 반올림 및 정밀도 메서드 ceil(x) : 올림 floor(x) : 내림 rint(x) : 가장 가까운 정수로 반올림 round(x) : 반올림 4. 삼각 함수 메서드 sin(x) : 사인 cos(x) : 코사인 tan(x) : 탄젠트 5. 기타 유용한 메서드 sqrt(x) : 제곱근 cbrt(x) : 세제곱근 random() : 0.0과 1.0 사이의 무작위 값 생성 Math에서 자주 사용하는 기능들을 예제로 만들어서 실행해봅시다. package lang.math; public class MathMain { public static void main(String[] args) { System.out.println("max(10, 20): " + Math.max(10, 20)); // 최대값 System.out.println("min(10, 20): " + Math.min(10, 20)); // 최소값 System.out.println("abs(-10): " + Math.abs(-10)); // 절대값 // 반올림 및 정밀도 메서드 System.out.println("ceil(2.1): " + Math.ceil(2.1)); // 올림 System.out.println("floor(2.1): " + Math.floor(2.1)); // 내림 System.out.println("round(2.5): " + Math.round(2.5)); // 반올림 // 기타 유용한 메서드 System.out.println("sqrt(4): " + Math.sqrt(4)); // 제곱근 System.out.println("random(): " + Math.random()); // 0.0 ~ 1.0 사이의 double 값을 반환 } } 실행 결과 max(10, 20): 20 min(10, 20): 10 abs(-10): 10 ceil(2.1): 3.0 floor(2.1): 2.0 round(2.5): 3 sqrt(4): 2.0 random(): 0.45992290098234856 참고 : 아주 정밀한 숫자와 반올림 계산이 필요하다면 BigDecimal을 검색해봅시다. Random 클래스 랜덤의 경우 Math.random()을 사용해도 되지만 Random 클래스를 사용하면 더욱 다양한 랜덤값을 구할 수 있습니다. 참고로 Math.random()도 내부에서는 Random 클래스를 사용합니다. 참고로 Random 클래스는 java.util 패키지 소속입니다. package lang.math; import java.util.Random; public class RandomMain { public static void main(String[] args) { Random random = new Random(); int randomInt = random.nextInt(); System.out.println("randomIntL " + randomInt); double randomDouble = random.nextDouble(); // 0.0d ~ 1.0d 사이값이 출력됨 System.out.println("randomDouble: " + randomDouble); boolean randomBoolean = random.nextBoolean(); System.out.println("randomBoolean: " + randomBoolean); // 범위 조회 int randomRange1 = random.nextInt(10); // 0 ~ 9까지 출력 System.out.println("0 ~ 9: " + randomRange1); int randomRange2 = random.nextInt(10) + 1; // 1 ~ 10까지 출력 System.out.println("1 ~ 10: " + randomRange2); } } 실행 결과 실행 결과는 항상 다르다. randomIntL -1662566800 randomDouble: 0.14362030528813963 randomBoolean: false 0 ~ 9: 4 1 ~ 10: 10 random.nextInt(): 랜덤 int 값을 반환합니다. nextDouble() : 0.0d ~ 1.0d 사이의 랜덤 double 값을 반환합니다. nextBoolean() : 랜덤 boolean 값을 반환합니다. nextInt(int bound): 0 ~ bound 미만의 숫자를 랜덤으로 반환합니다. 예를 들어서 3을 입력하면 0, 1, 2를 반환합니다. 1부터 특정 숫자의 int 범위를 구하는 경우 nextInt(int bound)의 결과에 +1을 하면 됩니다. 씨드 - Seed 랜덤은 내부에서 씨드(Seed) 값을 사용해서 랜덤 값을 구합니다. 그런데 이 씨드 값이 같으면 항상 같은 결과가 출력됩니다. // Random random = new Random(); Random random = new Random(1); // seed가 같으면 Random의 결과가 같다. 실행 결과 Seed가 같으면 실행 결과는 반복 실행해도 같습니다. randomIntL -1155869325 randomDouble: 0.10047321632624884 randomBoolean: false 0 ~ 9: 4 1 ~ 10: 5 new Random(): 생성자를 비워두면 내부에서 System.nanoTime()에 여러가지 복잡한 알고리즘을 섞어서 씨드값을 생성합니다. 따라서 반복 실행해도 결과가 항상 달라집니다. new Random(int seed): 생성자에 씨드 값을 직접 전달할 수 있습니다. 씨드 값이 같으면 여러번 반복 실행해도 실행 결과가 같습니다. 이렇게 씨드 값을 직접 사용하면 결과 값이 항상 같기 때문에 결과가 달라지는 랜덤 값을 구할 수 없습니다. 하지만 결과가 고정되기 때문에 테스트 코드 같은 곳에서 같은 결과를 검증할 수 있습니다. 참고로 마인크래프트 같은 게임은 게임을 시작할 때 지형을 랜덤으로 생성하는데, 같은 씨드값을 설정하면 같은 지형을 생성할 수 있습니다.
Archive
· 2024-04-26
☕️[Java] System 클래스
System 클래스 System 클래스는 시스템과 관련된 기본 기능들을 제공합니다. package lang.system; import java.util.Arrays; public class SystemMain { public static void main(String[] args) { // 현재 시간(밀리초)를 가져옵니다. long currentTimeMillis = System.currentTimeMillis(); System.out.println("currentTimeMillis = " + currentTimeMillis); // 현재 시간(나노초)를 가져옵니다. long currentTimeNano = System.nanoTime(); System.out.println("currentTimeNano = " + currentTimeNano); // 환경 변수를 읽습니다. System.out.println("getenv = " + System.getenv()); // 시스템 속성을 읽습니다. System.out.println("properties = " + System.getProperties()); System.out.println("Java version = " + System.getProperty("java.version")); // 배열을 고속으로 복사합니다. char[] originalArray = { 'h', 'e', 'l', 'l', 'o' }; char[] copiedArray = new char[5]; System.arraycopy(originalArray, 0, copiedArray, 0, originalArray.length); // 배열 출력 System.out.println("copiedArray = " + copiedArray); System.out.println("Arrays.toString = " + Arrays.toString(copiedArray)); // 프로그램 종료 System.exit(0); } } 실행 결과 currentTimeMillis = 1713485140558 currentTimeNano = 694481339313708 getenv = {HOMEBREW_PREFIX=/opt/homebrew, MANPATH=/Users/kobe/.nvm/versions/node/v20.10.0/share/man:/opt/local/share/man:/opt/homebrew/share/man::, COMMAND_MODE=unix2003, INFOPATH=/opt/homebrew/share/info:... properties = {java.specification.version=21, sun.jnu.encoding=UTF-8, java.class.path=/Users/kobe/Desktop/practiceJavaMidPart1/java-mid1/out/production/java-mid1, java.vm.vendor=Oracle Corporation, sun.arch.data.model=64, java.vendor.... Java version = 21.0.2 copiedArray = [C@77459877 Arrays.toString = [h, e, l, l, o] Process finished with exit code 0 표준 입력, 출력, 오류 스트림: System.in, System.out, System.err은 각각 표준 입력, 표준 출력, 표준 오류 스트림을 나타냅니다. 시간 측정: System.currentTimeMillis()와 System.nanoTime()은 현재 시간을 밀리초 또는 나노초 단위로 제공합니다. 환경 변수: System.getProperties()를 사용해 현재 시스템 속성을 얻거나 System.getProperty(Stringkey)로 특정 속성을 얻을 수 있습니다. 시스템 속성은 자바에서 사용하는 설정 값입니다. 시스템 종료: System.exit(int status) 메서드는 프로그램을 종료하고, OS에 프로그램 종료의 상태 코드를 전달합니다. 상태 코드0 : 정상 종료 상태 코드 0이 아님: 오류나 예외적인 종료 배열 고속 복사: System.arraycopy는 시스템 레벨에서 최적화된 메모리 복사 연산을 사용합니다. 직접 반복문을 사용해서 배열을 복사할 때 보다 수 배 이상 빠른 성능을 제공합니다.
Archive
· 2024-04-19
☕️[Java] Class 클래스
Class 클래스. 자바에서 Class 클래스는 클래스의 정보(메타데이터)를 다루는데 사용됩니다. Class 클래스를 통해 개발자는 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메소드에 대한 정보를 조회하고 조작할 수 있습니다. Class 클래스의 주요 기능은 다음과 같습니다. 타입 정보 얻기: 클래스의 이름, 슈퍼클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있습니다. 리플렉션: 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 등의 작업을 할 수 있습니다. 동적 로딩과 생성: Class.forName() 메서드를 사용하여 클래스를 동적으로 로드하고, newInstance() 메서드를 통해 새로운 인스턴스를 생성할 수 있습니다. 애노테이션 처리: 클래스에 적용된 애노테이션(annotation)을 조회하고 처리하는 기능을 제공합니다. 예를 들어, String.class는 String 클래스에 대한 Class 객체를 나타내며, 이를 통해 String 클래스에 대한 메타데이터를 조회하거나 조작할 수 있습니다. 다음 코드를 실행해봅시다. package lang.clazz; import java.lang.reflect.Field; import java.lang.reflect.Method; public class ClassMetaMain { public static void main(String[] args) throws Exception { // Class 조회 Class clazz = String.class; // 1. 클래스에서 조회 //Class clazz = new String().getClass(); // 2. 인스턴스에서 조회 //Class clazz = Class.forName("java.lang.String"); // 3. 문자열로 조회 // 모든 필드 출력 Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { System.out.println("field = " + field.getType() + " " + field.getName()); } // 모든 메서드 출력 Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { System.out.println("method = " + method); } // 상위 클래스 정보 출력 System.out.println("Superclass: " + clazz.getSuperclass().getName()); // 인터페이스 정보 출력 Class[] interfaces = clazz.getInterfaces(); for (Class i : interfaces) { System.out.println("Interface: = " + i.getName()); } } } class vs clazz - class는 자바의 예약어입니다. 따라서 패키지명, 변수명으로 사용할 수 없습니다. 이런 이유로 자바 개발자들은 class 대신 clazz라는 이름을 관행으로 사용합니다. clazz는 class와 유사하게 들리고, 이 단어가 class를 의미한다는 것을 쉽게 알 수 있습니다. 주의! main() 옆에 throws Exception이 추가된 부분에 주의합시다. 이 코드가 없으면 컴파일 오류가 발생합니다. 실행 결과 field = class [B value ... method = byte[] java.lang.String.value() method = public boolean java.lang.String.equals(java.lang.Object) ... Superclass: java.lang.Object Interface: = java.io.Serializable Interface: = java.lang.Comparable ... Class 클래스는 다음과 같이 3가지 방법으로 조회할 수 있습니다. Class clazz = String.class; // 1. 클래스에서 조회 Class clazz = new String().getClass(); // 2. 인스턴스에서 조회 Class clazz = Clazz.forName("java.lang.String"); // 3. 문자열로 조회 Class 클래스의 주요 기능 getDeclaredFields(): 클래스의 모든 필드를 조회합니다. getDeclaredMethods(): 클래스의 모든 메서드를 조회합니다. getSuperclass(): 클래스의 부모 클래스를 조회합니다. getInterface(): 클래스의 인터페이스들을 조회합니다. 실행 결과를 보면 String 클래스의 다양한 정보를 확인할 수 있습니다. 클래스 생성하기 Class 클래스에는 클래스의 모든 정보가 들어있습니다. 이 정보를 기반으로 인스턴스를 생성하거나, 메서드를 호출하고, 필드의 값도 변경할 수 있습니다. 여기서는 간단하게 인스턴스를 생성해봅시다. package lang.clazz; public class ClassCreatMain { public static void main(String[] args) throws Exception { //Class helloClass = Hello.class; Class helloClass = Class.forName("lang.clazz.Hello"); Hello hello = (Hello) helloClass.getDeclaredConstructor().newInstance(); String result = hello.hello(); System.out.println("result = " + result); } } 실행 결과 result = hello! getDeclaredConstructor().newInstance() getDeclaredConstructor() : 생성자를 선택합니다. newInstance() : 선택된 생성자를 기반으로 인스턴스를 생성합니다. 리플랙션 - reflection Class를 사용하면 클래스의 메타 정보를 기반으로 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 작업을 할 수 있습니다. 이런 작업을 리플렉션이라고 합니다. 추가로 애노테이션 정보를 읽어서 특별한 기능을 수행할 수도 있습니다. 최신 프레임워크들은 이런 기능을 적극 활용합니다. 지금은 Class가 뭔지, 그리고 대략 어떤 기능들을 제공하는지만 알아두면 충분합니다. 지금은 리플랙션을 학습하는 것 보다 훨씬 더 중요한 기본기들을 학습해야 합니다.
Archive
· 2024-04-19
☕️[Java] 래퍼 클래스 - 주요 메서드와 성능
래퍼 클래스 - 주요 메서드와 성능. 래퍼 클래스 - 주요 메서드. 래퍼 클래스가 제공하는 주요 메서드를 알아봅시다. package lang.wrapper; public class WrapperUtilsMain { public static void main(String[] args) { Integer i1 = Integer.valueOf(10); // 숫자, 래퍼 객체 변환. Integer i2 = Integer.valueOf("10"); // 문자열, 래퍼 객체 변환. int intValue = Integer.parseInt("10"); // 문자열 전용, 기본형 변환. // 비교 int compareResult = i1.compareTo(20); System.out.println("compareResult = " + compareResult); // 산술 연산 System.out.println("sum: " + Integer.sum(10, 20)); System.out.println("min: " + Integer.min(10, 20)); System.out.println("max: " + Integer.max(10, 20)); } } 실행 결과 compareResult = -1 sum: 30 min: 10 max: 20 valueOf(): 래퍼 타입을 반환합니다. 숫자, 문자열을 모두 지원합니다. parseInt(): 문자열을 기본형으로 변환합니다. compareTo(): 내 값과 인수로 넘어온 값을 비교합니다. 내 값이 크면 1, 같으면 0, 내 값이 작으면 -1을 반환합니다. Integer.sum(), Integer.min(), Integer.max(): static 메서드 입니다. 간단한 덧셈, 작은 값, 큰 값 연산을 수행합니다. pareInt() vs valueOf() 원하는 타입에 맞는 메서드를 사용하면 됩니다. valueOf("10")는 래퍼 타입을 반환합니다. parseInt("10")는 기본형을 반환합니다. Long.parseLong() 처럼 각 타입에 parseXxx()가 존재합니다. 래퍼 클래스와 성능 래퍼 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공합니다. 그렇다면 더 좋은 래퍼 클래스만 제공하면 되지 기본형을 제공하는 이유는 무엇일까요? 다음 코드를 실행해서 기본형과, 래퍼 클래스의 성능 차이를 비교해봅시다. package lang.wrapper; public class WrapperVsPrimitive { public static void main(String[] args) { int iterations = 1_000_000_000; // 반복 횟수 설정, 10억 long startTime, endTime; // 기본형 long 사용 long sumPrimitive = 0; startTime = System.currentTimeMillis(); for (int i = 0; i < iterations; i++) { sumPrimitive += i; } endTime = System.currentTimeMillis(); System.out.println("sumPrimitive = " + sumPrimitive); System.out.println("기본 자료형 long 실행 시간: " + (endTime - startTime) + "ms"); // 래퍼 클래스 Long 사용 Long sumWrapper = 0L; startTime = System.currentTimeMillis(); for (int i = 0; i < iterations; i++) { sumWrapper += i; // 오토 박싱 발생 } endTime = System.currentTimeMillis(); System.out.println("sumWrapper = " + sumWrapper); System.out.println("래퍼 클래스 Long 실행 시간: " + (endTime - startTime) + "ms"); } } 단순히 값을 반복해서 10억 번 더합니다. 기본형 long에 더하는 것과 래퍼 클래스 Long에 더하는 부분으로 나누어 테스트 합니다. 결과 값은 같습니다. 실행 결과 - M1 맥북 기준 sumPrimitive = 499999999500000000 기본 자료형 long 실행 시간: 381ms sumWrapper = 499999999500000000 래퍼 클래스 Long 실행 시간: 1640ms 기본형 연산이 래퍼 클래스보다 대략 5배 정도 빠른 것을 확인할 수 있습니다. 참고로 계산 결과는 시스템 마다 다릅니다. 기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지합니다. 예를 들어 int 는 보통 4바이트의 메모리를 사용합니다. 래퍼 클래스의 인스턴스는 내부에 필드로 가지고 있는 기본형의 값 뿐만 아니라 자바에서 객체 자체를 다루는데 필요한 객체 메타데이터를 포함하므로 더 많은 메모리를 사용합니다. 자바 버전과 시스템마다 다르지만 대략 8-16바이트의 메모리를 추가로 사용합니다. 기본형, 래퍼 클래스 어떤 것을 사용? 이 연산은 10억 번의 연산을 수행했을 때 0.3초와, 1.5초의 차이입니다. 기본형이든 래퍼 클래스든 이것을 1회로 환산하면 둘다 매우 빠르게 연산이 수행됩니다. 0.3초 나누기 10억, 1.5초 나누기 10억이다. 일반적인 애플리케이션을 만드는 관점에서 보면 이런 부분을 최적화해도 사막의 모래알 하나 정도의 차이가 날 뿐입니다. CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만 ~ 수십만 이상 연속해서 연산을 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려합시다. 그렇지 않은 일반적인 경우라면 코드를 유지보수하기 더 나은 것을 선택하면 됩니다. 유지보수 vs 최적화 유지보수 vs 최적화를 고려해야 하는 상황이라면 유지보수하기 좋은 코드를 먼저 고민해야 합니다. 특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고해도 실질적인 도움이 되지 않는 경우가 많습니다. 코드 변경 없이 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 단순함 보다는 복잡함을 요구하고, 더 많은 코드들을 추가로 만들어야 합니다. 최적화를 위해 유지보수 해야 하는 코드가 더 늘어나는 것입니다. 그런데 진짜 문제는 최적화를 한다고 했지만 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있습니다. 특히 웹 애플리케이션의 경우 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만배 더 오래 걸립니다. 자바 메모리 내부에서 발생하는 연산을 수천번에서 한 번으로 줄이는 것 보다, 네트워크 호출 한 번을 더 줄이는 것이 더 효과적인 경우가 많습니다. 권장하는 방법은 개발 이후에 성능 테스트를 해보고 정말 문제가 되는 부분을 찾아서 최적화 하는 것입니다.
Archive
· 2024-04-18
☕️[Java] 래퍼 클래스 - 기본형의 한계 2
래퍼 클래스 - 기본형의 한계 2 기본형과 null 기본형은 항상 값을 가져야 합니다. 하지만 때로는 데이터가 ‘없음’이라는 상태가 필요할 수 있습니다. 다음 코드를 작성해봅시다. package lang.wrapper; public class MyIntegerNullMain0 { public static void main(String[] args) { int[] intArr = {-1, 0, 1, 2, 3}; System.out.println(findValue(intArr, -1)); // -1 System.out.println(findValue(intArr, 0)); System.out.println(findValue(intArr, 1)); System.out.println(findValue(intArr, 100)); // -1 } private static int findValue(int[] intArr, int target) { for (int value : intArr) { if (value == target) { return value; } } return -1; } } findValue()는 배열에 찾는 값이 있으면 해당 값을 반환하고, 찾는 값이 없으면 -1을 반환합니다. findValue()는 결과로 int를 반환합니다. int와 같은 기본형은 항상 값이 있어야 합니다. 여기서도 값을 반환할 때 값을 찾지 못하면 숫자 중 하나를 반환해야 하는데 보통 -1 또는 0을 사용합니다. 실행 결과 -1 0 1 -1 실행 결과를 보면 입력값이 -1일 때 -1을 반환합니다. 그런데 배열에 없는 값인 100을 입력해도 같은 -1을 반환합니다. 입력값이 -1일 때를 생각해보면, 배열에 -1 값이 있어서 -1을 반환한 것인지, 아니면 -1 값이 없어서 -1을 반환한 것인지 명확하지 않습니다. 객체의 경우 데이터가 없다는 null이라는 명확한 값이 존재합니다. 다음 코드를 작성해봅시다. package lang.wrapper; public class MyIntegerNullMain1 { public static void main(String[] args) { MyInteger[] intArr = {new MyInteger(-1), new MyInteger(0), new MyInteger(1)}; System.out.println(findValue(intArr, -1)); // -1 System.out.println(findValue(intArr, 0)); System.out.println(findValue(intArr, 1)); System.out.println(findValue(intArr, 100)); // null } private static MyInteger findValue(MyInteger[] intArr, int target) { for (MyInteger myInteger : intArr) { if (myInteger.getValue() == target) { return myInteger; } } return null; } } 실행 결과 -1 0 1 null 앞서 만든 MyInteger 래퍼 클래서를 사용했습니다. 실행 결과를 보면 -1을 입력했을 때는 -1을 반환합니다. 100을 입력했을 때는 값이 없다는 null을 반환합니다. 기본형은 항상 값이 존재해야 합니다. 숫자의 경우 0, -1 같은 값이라도 항상 존재해야 합니다. 반면에 객체인 참조형은 값이 없다는 null을 사용할 수 있습니다. 물론 null 값을 반환하는 경우 잘못하면 NullPointerException이 발생할 수 있기 때문에 주의해서 사용해야 합니다.
Archive
· 2024-04-17
☕️[Java] 래퍼 클래스 - 자바 래퍼 클래스
래퍼 클래스 - 자바 래퍼 클래스. 지금까지 설명한 래퍼 클래스는 기본형을 객체로 감싸서 더 편리하게 사용하도록 도와주기 때문에 상당히 유용합니다. 쉽게 이야기해서 래퍼 클래스는 기본형의 객체 버전입니다. 자바는 기본형에 대응하는 래퍼 클래스를 기본으로 제공합니다. byte -> Byte short -> Short int -> Integer long -> Long float -> Float double -> Double char -> Character boolean -> Boolean 그리고 자바가 제공하는 기본 래퍼 클래스는 다음과 같은 특징을 가지고 있습니다. 불변이다. equals로 비교해야 한다. 자바가 제공하는 래퍼 클래스의 사용법을 알아봅시다. package lang.wrapper; public class WrapperClassMain { public static void main(String[] args) { Integer newInteger = new Integer(10); // 미래에 삭제 예정, 대신에 valueOf()를 사용 Integer integerObj = Integer.valueOf(10); // valueOf() 사용, -128 ~ 127 자주 사용하는 숫자 값 재사용, 불변 Long longObj = Long.valueOf(100); Double doubleObj = Double.valueOf(10.5); System.out.println("newInteger = " + newInteger); System.out.println("integerObj = " + integerObj); System.out.println("longObj = " + longObj); System.out.println("doubleObj = " + doubleObj); System.out.println("내부 값 읽기"); int intValue = integerObj.intValue(); System.out.println("intValue = " + intValue); long longValue = longObj.longValue(); System.out.println("longValue = " + longValue); System.out.println("비교"); System.out.println("==: " + (newInteger == integerObj)); System.out.println("equals: " + (newInteger.equals(integerObj))); } } 실행 결과 newInteger = 10 integerObj = 10 longObj = 100 doubleObj = 10.5 내부 값 읽기 intValue = 10 longValue = 100 비교 ==: false equals: true 래퍼 클래스 생성 - 박싱(Boxing) 기본형을 래퍼 클래스로 변경하는 것을 마치 박스에 물건을 넣는 것 같다고 해서 박싱(Boxing) 이라고 합니다. new Integer(10)은 직접 사용하면 안됩니다. 내부에서 new Integer(10)을 사용해서 객체를 생성하고 돌려줍니다. 추가로 Integer.valueOf()에는 성능 최적화 기능이 있습니다. 개발자들이 일반적으로 자주 사용하는 -128~127 범위의 Integer 클래스를 미리 생성해줍니다. 해당 범위의 값을 조회하면 미리 생성된 Integer 객체를 반환합니다. 해당 범위의 값이 없으면 new Integer()를 호출합니다. 마치 문자열 풀과 비슷하게 자주 사용하는 숫자를 미리 생성해두고 재사용합니다. 참고로 이런 최적화 방식은 미래에 더 나은 방식으로 변경될 수 있습니다. intValue() - 언박싱(Unboxing) 래퍼 클래스에 들어있는 기본형 값을 다시 꺼내는 메서드입니다. 박스에 들어있는 물건을 꺼내는 것 같다고 해서 언박싱(Unboxing) 이라 합니다. 비교는 equals() 사용 래퍼 클래스는 객체이기 때문에 == 비교를 하면 인스턴스의 참조값을 비교합니다. 래퍼 클래스는 내부의 값을 비교하도록 equals()를 재정의 해두었습니다. 따라서 값을 비교하려면 equals()를 사용해야합니다. 참고로 래퍼 클래스는 객체를 그대로 출력해도 내부에 있는 값을 문자로 출력하도록 toString()을 재정의 해두었습니다.
Archive
· 2024-04-17
☕️[Java] 래퍼 클래스 - 오토 박싱
래퍼 클래스 - 오토 박싱. 오토 박싱 - Autoboxing 자바에서 int를 Integer로 변환하거나, Integer를 int로 변환하는 부분을 정리해봅시다. 다음과 같이 valueOf(), intValue() 메서드를 사용하면 됩니다. package lang.wrapper; public class AutoboxingMain1 { public static void main(String[] args) { // Primitive -> Wrapper int value = 7; Integer boxedValue = Integer.valueOf(value); // Wrapper -> Primitive int unboxedValue = boxedValue.intValue(); System.out.println("boxedValue = " + boxedValue); System.out.println("unboxedValue = " + unboxedValue); } } 실행 결과 boxedValue = 7 unboxedValue = 7 박싱: valueOf() 언박싱: xxxValue() (예: intValue(), doubleValue()) 개발자들이 오랜기간 개발을 하다 보니 기본형을 래퍼 클래스로 변환하거나 또는 래퍼 클래스를 기본형으로 변환하는 일이 자주 발생했습니다. 그래서 많은 개발자들이 불편함을 호소했습니다. 자바는 이런 문제를 해결하기 위해 자바 1.5부터 오토 박싱(Auto-boxing), 오토 언박싱(Auto-unboxing)을 지원합니다. 오토 박싱, 언박싱 package lang.wrapper; public class AutoboxingMain2 { public static void main(String[] args) { // Primitive -> Wrapper int value = 7; Integer boxedValue = value; // 오토 박싱(Auto-boxing) // Wrapper -> Primitive int unboxedValue = boxedValue; // 오토 언박싱(Auto-Unboxing) System.out.println("boxedValue = " + boxedValue); System.out.println("unboxedValue = " + unboxedValue); } } 실행 결과 boxedValue = 7 unboxedValue = 7 오토 박싱과 오토 언박싱은 컴파일러가 개발자 대신 valueOf, xxxValue() 등의 코드를 추가해주는 기능입니다. 덕분에 기본형과 래퍼형을 서로 편리하게 변환할 수 있습니다. 따라서 AutoboxingMain1과 AutoboxingMain2는 동일하게 작동합니다. Integer boxedValue = value; // 오토 박싱(Auto-boxing) Integer boxedValue = Integer.valueOf(value); // 컴파일 단계에서 추가 int unboxedValue = boxedValue; // 오토 언박싱(Auto-Unboxing) int unboxedValue = boxedValue.intValue(); // 컴파일 단계에서 추가
Archive
· 2024-04-17
☕️[Java] 래퍼 클래스 - 기본형의 한계 1
래퍼 클래스 - 기본형의 한계 1 기본형의 한계 자바는 객체 지향 언어입니다. 그런데 자바 안에 객체가 아닌 것이 있습니다. 바로 int, double 같은 기본형(Primitive Type)입니다. 기본형은 객체가 아니기 때문에 다음과 같은 한계가 있습니다. 객체가 아님: 기본형 데이터는 객체가 아니기 때문에, 객체 지향 프로그래밍의 장점을 살릴 수 없습니다. 예를 들어 객체는 유용한 메서드를 제공할 수 있는데, 기본형은 객체가 아니므로 메서드를 제공할 수 없습니다. 추가로 객체 참고가 필요한 컬렉션 프레임워크를 사용할 수 없습니다. 그리고 제네릭도 사용할 수 없습니다. null 값을 가질 수 없음: 기본형 데이터 타입은 null 값을 가질 수 없습니다. 때로는 데이터가 없음 이라는 상태를 나타내야 할 필요가 있는데, 기본형은 항상 값을 가지기 때문에 이런 표현을 할 수 없습니다. 기본형의 한계를 이해하기 위해, 두 값을 비교해서 다음과 같은 결과를 출력하는 간단한 코드를 작성해봅시다. 왼쪽의 값이 더 작다 -1 두 값이 같다 0 왼쪽의 값이 더 크다 1 package lang.wrapper; public class MyIntegerMethodMain0 { public static void main(String[] args) { int value = 10; int i1 = compareTo(value, 5); int i2 = compareTo(value, 10); int i3 = compareTo(value, 20); System.out.println("i1 = " + i1); System.out.println("i2 = " + i2); System.out.println("i3 = " + i3); } public static int compareTo(int value, int target) { if (value < target) { return -1; } else if (value > target) { return 1; } else { return 0; } } } 실행 결과 i1 = 1 i2 = 0 i3 = -1 여기서는 value와 비교 대상 값을 compareTo()라는 외부 메서드를 사용해서 비교합니다. 그런데 자기 자신인 value와 다른 값을 연산하는 것이기 때문에 항상 자기 자신의 값인 value가 사용됩니다. 이런 경우 만약 value가 객체라면 value 객체 스스로 가지 자신의 값과 다른 값을 비교하는 메서드를 만드는 것이 더 유용할 것입니다. 직접 만든 래퍼 클래스. int를 클래스로 만들어 봅시다. int는 클래스가 아니지만, int 값을 가지고 클래스를 만들면 됩니다. 다음 코드는 마치 int를 클래스로 감싸서 만드는 것 처럼 보입니다. 이렇게 특정 기본형을 감싸서(Wrap) 만드는 클래스를 래퍼 클래스(Wrapper class)라 합니다. package lang.wrapper; public class MyInteger { private final int value; public MyInteger(int value) { this.value = value; } public int getValue() { return value; } public int compareTo(int target) { if (value < target) { return -1; } else if (value > target) { return 1; } else { return 0; } } @Override public String toString() { return String.valueOf(value); } } MyInteger는 int value라는 단순한 기본형 변수를 하나 가지고 있습니다. 그리고 이 기본형 변수를 편리하게 사용하도록 다양한 메서드를 제공합니다. 앞에서 본 compareTo() 메서드를 클래스 내부로 캡슐화 했습니다. 이 클래스는 불변으로 설계했습니다. MyInteger 클래스는 단순한 데이터 조각인 int를 내부에 품고, 메서드를 통해 다양한 기능을 추가했습니다. 덕분에 데이터 조각에 불과한 int를 MyInteger를 통해 객체로 다룰 수 있게 되었습니다. package lang.wrapper; public class MyIntegerMethodMain1 { public static void main(String[] args) { MyInteger myInteger = new MyInteger(10); int i1 = myInteger.compareTo(5); int i2 = myInteger.compareTo(10); int i3 = myInteger.compareTo(20); System.out.println("i1 = " + i1); System.out.println("i2 = " + i2); System.out.println("i3 = " + i3); } } 실행 결과 i1 = 1 i2 = 0 i3 = -1 myInteger.compareTo()는 자기 자신의 값을 외부의 값과 비교합니다. MyInteger는 객체이므로 자신이 가진 메서드를 편리하게 호출할 수 있습니다. 참고로 int는 기본형이기 때문에 스스로 메서드를 가질 수 없습니다.
Archive
· 2024-04-15
☕️[Java] String 클래스 - 정리
String 클래스 - 정리 1. 자바에서 문자를 다루는 대표적인 타입 char String char 배열을 사용하면 문자열을 다룰 수 있으나 불편합니다. 그래서 String이라는 것을 Java에서 제공해줍니다. 2. String 클래스를 통해 문자열을 생성하는 2가지 방법. String str1 = "hello"; // 방법 1 -> 문자열 리터럴 String str2 = new String("hello"); // 방법 2 쌍따옴표 사용: 방법 1 객체 생성 : 방법 2 문자열 리터럴을 사용하더라도 결국 new String("hello"); 가 되는 것 입니다. 편의상 쌍따옴표로 문자열을 감싸면(문자열 리터럴) 자바 언어에서 new String("hello"); 와 같이 변경해 줍니다. 이 경우 실제로는 성정 최적화를 위해 문자열 풀을 사용합니다. 문자열은 참조형입니다. 3. String 클래스 내부 String 클래스는 대략 다음과 같이 생겼습니다. public final class String { // 문자열 보관 private final char[] value; // 자바 9 이전 private final byte[] value; // 자바 9 이후 // 여러 메서드 public String concat(String str) {...} public int length() {...} } 3. String 클래스 비교. String 클래스를 비교할 때는 == 비교가 아니라 항상 equals() 비교를 해야합니다. 동일성(Identity) : == 연산자를 사용해서 두 객체의 참조(Reference) 가 동일한 객체를 가리키고 있는지 확인합니다. 동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 같은지 확인합니다. 4. 문자열 리터럴, 문자열 풀. String str3 = "hello" 와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용합니다. 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어 둡니다. 이때 같은 문자열이 있으면 만들지 않습니다. String str3 - "hello"와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾습니다. 그리고 찾은 인스턴스의 참조(x003)를 반환합니다. String str4 = "hello"의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3과 같은 x003 참조를 사용합니다. 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있습니다. 따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공합니다. 참고 풀(Pool)은 자원이 모여있는 곳을 의미합니다. 프로그래밍에서 풀(Pool)은 공용 자원을 모아둔 곳을 뜻합니다. 여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고, 제거하는 것은 비효율적입니다. 대신에 이렇게 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 더 최적화할 수 있습니다. 참고로 문자열 풀은 힙 영역을 사용합니다. 그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있습니다. 5. String 클래스는 불변객체. String은 불변 객체입니다. 따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없습니다. public static void main(String[] args) { String str1 = "hello"; String str2 = str1.concat(" java"); System.out.println("str1 = " + str1); System.out.println("str1 = " + str2); } String은 불변 객체입니다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환합니다. 실행 결과 str1 = hello str2 = hello java String.concat()은 내부에서 새로운 String 객체를 만들어서 반환합니다. 따라서 불변과 기존 객체의 값을 유지합니다. 문자열에서 무언가 변경하는 것이 있으면 그 내부에서 인스턴스를 생성하여 반환합니다. 때문에 반환값을 받아서 사용해야 합니다. 6. String 클래스가 불변으로 설계된 이유. 사이드 이팩트 문제. 문자열 풀을 사용시에 불변으로 설계되어 있어야 안전하게 사용할 수 있기 때문입니다. 7. String 클래스 주요 메서드. 주요 메서드 블로그 글 1 주요 메서드 블로그 글 2 참고. CharSequence CharSequence 는 String, StringBuilder의 상위 타입입니다. 문자열을 처리하는 다양한 객체를 받을 수 있습니다. 8. StringBuilder - 가변 String 불변인 String 클래스의 단점: 뭔가 값을 더하거나 변경하거나 할 때마다 계속 새로운 객체를 만들어내야 한다는 점 입니다. 때문에 성능도 느려질 수 있습니다. (물론, 불변이기 때문에 안전하기는 합니다.) 이러한 단점들 때문에 StringBuilder 가 있습니다. StringBuilder: 가변 String 입니다. 값을 쭉 바꾸고 쓰면 되는데, 마지막에는 다시 String으로 바꾸는 toString()을 사용합니다. 즉, 다시 안전한 불변으로 바꾸는 작업입니다. 변경이 필요할 때 가변으로, 마지막에는 불변으로 바꿔서 쓰는것을 권장합니다. “뭔가 문자열을 변경할 일이 많다” -> StringBuilder 를 사용하면 됩니다. 9. String 최적화. 그러나 생각보다 StringBuilder를 사용할 때가 많이 없습니다. 그 이유는 자바가 String을 최적화하기 때문입니다. 컴파일러에서도 최적화를 하고, 변수로 되어있어도 컴파일러가 최적화를 수행합니다. 예를 들어 Java가 직접 StringBuilder를 사용합니다. String result = new StringBuilder().append(str1).append(str2).toString(); 10. String 최적화가 어려운 경우. 다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다. public class LoopStringMain { public static void main(String[] args) { long startTime = System.currentTimeMillis(); String result = ""; for (int i = 0; i < 100000; i++) { result += "Hello Java "; } long endTime = System.currentTimeMillis(); System.out.println("result = " + result); System.out.println("time = " + (endTime - startTime) + "ms"); } } 왜냐하면 대략 다음과 같이 최적화가 되기 때문입니다.(최적화 방식은 자바 버전에 따라 다릅니다.) String result = ""; for (int i = 0; i < 1000000; i++) { result = new StringBuilder().append(result).append("Hello Java ").toString(); } 반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다. 반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다. 이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 변할지 예측할 수 없습니다. 따라서, 이런 상황에서는 최적화가 어렵습니다. StringBuilder 는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것입니다. 이럴 때는 직접 StringBuilder 를 사용하면 됩니다. public class LoopStringMain { public static void main(String[] args) { long startTime = System.currentTimeMillis(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 100000; i++) { sb.append("Hello Java "); } String result = sb.toString(); long endTime = System.currentTimeMillis(); System.out.println("result = " + result); System.out.println("time = " + (endTime - startTime) + "ms"); } } 정리 문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다. StringBuilder를 직접 사용하는 것이 더 좋은 경우 반복문에서 반복해서 문자를 연결할 때 조건문을 통해 동적으로 문자열을 조합할 때 복잡한 문자열의 특정 부분을 변경해야 할 때 매우 긴 대용량 문자열을 다룰 때 11. 메서드 체이닝. 자기 자신의 값을 반환해서 메서드를 쭉 연결해서 사용할 수 있습니다. 자바의 많은 라이브러리들이 메서드 체이닝 기법을 사용하고 있습니다. 메서드 체이닝 블로그 글
Archive
· 2024-04-14
☕️[Java] String 최적화
String 최적화. 자바의 String 최적화 자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐줍니다. 문자열 리터럴 최적화 컴파일 전 String helloWorld = "Hello, " + "World!"; 컴파일 후 String helloWorld = "Hello, World!"; 따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상됩니다. String 변수 최적화 문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없습니다. String result = str1 + str2; 이런 경우 예를 들면 다음과 같이 최적화를 수행합니다.(최적화 방식은 자바 버전에 따라 달라집니다.) String result = new StringBuilder().append(str1).append(str2).toString(); 참고: 자바 9부터는 StringConcatFactory를 사용해서 최적화를 수행합니다. 이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 됩니다. 대신에 문자열을 더하기 연산(+)을 사용하면 충분합니다. String 최적화가 어려운 경우 다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다. package lang.string.builder; public class LoopStringMain { public static void main(String[] args) { long startTime = System.currentTimeMillis(); String result = ""; for (int i = 0; i < 100000; i++) { result += "Hello Java "; } long endTime = System.currentTimeMillis(); System.out.println("result = " + result); System.out.println("time = " + (endTime - startTime) + "ms"); } } 왜냐하면 대락 다음과 같이 최적화가 되기 때문입니다.(최적화 방식은 자바 버전에 따라 다릅니다.) String result = ""; for (int i = 0; i < 100000; i++) { result = new StringBuilder().append(result).append("Hello Java ").toString(); } 반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다. 반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다. 이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없습니다. 따라서, 이런 상황에서는 최적화가 어렵습니다. StringBuilder는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것입니다. 실행 결과 result = Hello Java Hello Java ... time = 2528ms 1000ms = 1초 M1 맥북을 기준으로 100000회 더했을 때 약 2.5초가 걸렸습니다. 이럴 때는 직접 StringBuilder를 사용하면 됩니다. package lang.string.builder; public class LoopStringBuilderMain { public static void main(String[] args) { long startTime = System.currentTimeMillis(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 100000; i++) { sb.append("Hello Java "); } long endTime = System.currentTimeMillis(); String result = sb.toString(); System.out.println("result = " + result); System.out.println("time = " + (endTime - startTime) + "ms"); } } 실행 결과 result = Hello Java Hello Java ... time = 4ms 1000ms = 1초 M1 맥북을 기준으로 100000회 더했을 때 약 0.004초가 걸렸습니다. 정리 문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다. StringBuilder를 직접 사용하는 것이 더 좋은 경우 반복문에서 반복해서 문자를 연결할 때 조건문을 통해 동적으로 문자열을 조합할 때 복잡한 문자열의 특정 부분을 변경해야 할 때 매우 긴 대용량 문자열을 다룰 때 참고: StringBuilder vs StringBuffer StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있습니다. StringBuffer는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느립니다. StringBuilder는 멀티 쓰레드에 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠릅니다.
Archive
· 2024-04-11
☕️[Java] 메서드 체이닝 - Method Chaining
메서드 체이닝 - Method Chaining. 간단한 예제 코드로 메서드 체이닝(Method Chaining)에 대해서 알아봅시다. package lang.string.chaining; public class ValueAdder { private int value; public ValueAdder add(int addValue) { value += addValue; return this; } public int getValue() { return value; } } 단순히 값을 누적해서 더하는 기능을 제공하는 클래스입니다. add() 메서드를 호출할 때 마다 내부의 value에 값을 누적합니다. add() 메서드를 보면 자기 자신(this)의 참조값을 반환합니다. 이 부분을 유의해서 봅시다. package lang.string.chaining; public class MethodChainingMain1 { public static void main(String[] args) { ValueAdder adder = new ValueAdder(); adder.add(1); adder.add(2); adder.add(3); int result = adder.getValue(); System.out.println("result = " + result); } } 실행 결과 result = 6 add() 메서드를 여러번 호출해서 값을 누적해서 더하고 출력합니다. 여기서는 add() 메서드의 반환값은 사용하지 않았습니다. 이번에는 add() 메서드의 반환값을 사용해봅시다. package lang.string.chaining; public class MethodChainingMain2 { public static void main(String[] args) { ValueAdder adder = new ValueAdder(); ValueAdder adder1 = adder.add(1); ValueAdder adder2 = adder1.add(2); ValueAdder adder3 = adder2.add(3); int result = adder3.getValue(); System.out.println("result = " + result); } } 실행 결과 result = 6 실행 결과는 기존과 같습니다. adder.add(1)을 호출합니다. add() 메서드는 결과를 누적하고 자기 자신의 참조값인 this(x001)를 반환합니다. adder1 변수는 adder와 같은 x001 인스턴스를 참조합니다. add()메서드는 자기 자신(this) 메서드의 참조값을 반환합니다. 이 반환값을 adder1, adder2, adder3에 보관했습니다. 따라서 adder, adder1, adder2, adder3은 모두 같은 참조값을 사용합니다. 왜냐하면 add() 메서드가 자기 자신(this)의 참조값을 반환했기 때문입니다. 그런데 이 방식은 처음 방식보다 더 불편하고, 코드도 더 잘 읽히지 않습니다. 이런 방식을 왜 사용하는 것 일까요? 이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드 호출에 사용해봅시다. package lang.string.chaining; public class MethodChainingMain3 { public static void main(String[] args) { ValueAdder adder = new ValueAdder(); int result = adder.add(1).add(2).add(3).getValue(); System.out.println("result = " + result); } } 실행 결과 result = 6 실행 순서 add() 메서드를 호출하면 ValueAdder 인스턴스 자신의 참조값(x001)이 반환됩니다. 이 반환된 참조값을 변수에 담아두지 않아도 됩니다. 대신에 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있습니다. 다음과 같은 순서로 실행됩니다. adder.add(1).add(2).add(3).getValue(); // value = 0 x001.add(1).add(2).add(3).getValue(); // value = 0, x001.add(1)을 호출하면 그 결과로 x001을 반환합니다. x001.add(2).add(3).getValue(); // value = 1, x001.add(2)을 호출하면 그 결과로 x001을 반환합니다. x001.add(3).getValue(); // value = 3, x001.add(3)을 호출하면 그 결과로 x001을 반환합니다. x001.getValue(); // value = 6 6 메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있습니다. 코드를 보면 .을 찍고 메서드를 계속 연결해서 사용합니다. 마치 메서드가 체인으로 연결된 것 처럼 보입니다. 이러한 기법을 메서드 체이닝이라고 합니다. 물론 실행 결과도 기존과 동일합니다. 기존에는 메서드를 호출할 때 마다 계속 변수명에 .을 찍어야 했습니다. 예) adder.add(1), adder.add(2) 매서드 체이닝 방식은 메서드가 끝나는 시점에 바로 .을 찍어서 변수명을 생략할 수 있습니다. 메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문입니다. 이 참조값에 .을 찍어서 바로 자신의 메서드를 호출할 수 있습니다. 메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어줍니다. StringBuilder와 메서드 체인(Chain) StringBuilder는 메서드 체이닝 기법을 제공합니다. StringBuilder의 append() 메서드를 보면 자기 자신의 참조값을 반환합니다. public StringBuilder append(String str) { super.append(str); return this; } StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환합니다. 예) insert(), delete(), reverse() 앞서 StringBuilder를 사용한 코드는 다음과 같이 개선할 수 있습니다. package lang.string.builder; public class StringBuilderMain1_2 { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); String string = sb.append("A").append("B").append("C").append("D") .insert(4, "Java") .delete(4,8) .reverse() .toString(); System.out.println("string = " + string); } } 실행 결과 string = DCBA 정리 “만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다” 는 말이 있습니다. 메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리합니다. 참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용합니다.
Archive
· 2024-04-11
☕️[Java] String 클래스 - 주요 메서드 2
String 클래스 - 주요 메서드 2 문자열 조작 및 변환 substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다. concat(String str) : 문자열의 끝에 다른 문자열을 붙입니다. replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체합니다. replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체합니다. replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체합니다. toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환합니다. trim() : 문자열 양쪽 끝의 공백을 제거합니다. 단순 Whitespace만 제거할 수 있습니다. strip() : Whitespace 와 유니코드 공백을 포함해서 제거합니다, 자바 11 package lang.string.method; public class StringChangeMain2 { public static void main(String[] args) { String strWithSpaces = " Java Programming "; System.out.println("소문자로 변환: " + strWithSpaces.toLowerCase()); System.out.println("대문자로 변환: " + strWithSpaces.toUpperCase()); System.out.println("공백 제거(trim): '" + strWithSpaces.trim() + "'"); System.out.println("공백 제거(strip): '" + strWithSpaces.strip() + "'"); System.out.println("앞 공백 제거(stripLeading): '" + strWithSpaces.stripLeading() + "'"); System.out.println("뒤 공백 제거(stripTrailing): '" + strWithSpaces.stripTrailing() + "'"); } } 실행 결과 소문자로 변환: java programming 대문자로 변환: JAVA PROGRAMMING 공백 제거(trim): 'Java Programming' 공백 제거(strip): 'Java Programming' 앞 공백 제거(stripLeading): 'Java Programming ' 뒤 공백 제거(stripTrailing): ' Java Programming' 문자열 분할 및 조합 split(String regex) : 문자열을 정규 표현식을 기준으로 분할합니다. join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합합니다. package lang.string.method; public class StringSplitJoinMain { public static void main(String[] args) { String str = "Apple,Banana,Orange"; // split() String[] splitStr = str.split(","); for (String s : splitStr) { System.out.println(s); } String joinStr = ""; for (int i = 0; i < splitStr.length; i++) { String string = splitStr[i]; joinStr += string; if (i != splitStr.length-1) { joinStr += "-"; } } System.out.println("joinStr = " + joinStr); // join() String joinedStr = String.join("-", "A", "B", "C"); System.out.println("연결된 문자열 = " + joinedStr); // 문자열 배열 연결 String result = String.join("-", splitStr); System.out.println("result = " + result); } } 실행 결과 Apple Banana Orange joinStr = Apple-Banana-Orange 연결된 문자열 = A-B-C result = Apple-Banana-Orange 기타 유틸리티. valueOf(Object obj) : 다양한 타입을 문자열로 변환합니다. toCharArray() : 문자열을 문자 배열로 변환합니다. format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성합니다. matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인합니다. package lang.string.method; public class StringUtilsMain1 { public static void main(String[] args) { int num = 100; boolean bool = true; Object obj = new Object(); String str = "Hello, Java!"; // valueOf 메서드 String numString = String.valueOf(num); System.out.println("숫자의 문자열 값: " + numString); String boolString = String.valueOf(bool); System.out.println("불리언의 문자열 값: " + boolString); String objString = String.valueOf(obj); System.out.println("객체의 문자열 값: " + objString); // 문자 + x -> 문자 String numString2 = "" + num; System.out.println("빈 문자열 + num: " + numString2); // toCharArray 메서드 char[] strCharArray = str.toCharArray(); System.out.println("문자열을 문자 배열로 변환 : " + strCharArray); for (char c : strCharArray) { System.out.print(c); } System.out.println(); } } 실행 결과 숫자의 문자열 값: 100 불리언의 문자열 값: true 객체의 문자열 값: java.lang.Object@a09ee92 빈 문자열 + num: 100 문자열을 문자 배열로 변환 : [C@30f39991 Hello, Java! package lang.string.method; public class StringUtilsMain2 { public static void main(String[] args) { int num = 100; boolean bool = true; String str = "Hello, Java!"; // format 메서드 String format1 = String.format("num: %d, bool: %b, str: %s", num, bool, str); System.out.println(format1); String format2 = String.format("숫자: %.2f ", 10.1234); System.out.println(format2); //printf System.out.printf("숫자: %.2f\n", 10.1234); // matches 메서드 String regex = "Hello, (Java!|World)"; System.out.println("'str'이 패턴과 일치하는가? " + str.matches(regex)); } } 실행 결과 num: 100, bool: true, str: Hello, Java! 숫자: 10.12 숫자: 10.12 'str'이 패턴과 일치하는가? true format 메서드에서 %d는 숫자 %b는 boolean, %s는 문자열을 뜻합니다.
Archive
· 2024-04-10
☕️[Java] StringBuilder - 가변 String
StringBuilder - 가변 String. 불변인 String 클래스의 단점. 불변인 String 클래스에도 단점이 있습니다. 다음 예를 봅시다. 참고로 실제로 작동하는 코드는 아닙니다. 두 문자를 더하는 경우 다음과 같이 작동합니다. "A" + "B"; String("A") + String("B"); // 문자는 String 타입. String("A").concat(String("B")); // 문자의 더하기는 concat을 사용. new String("AB") // String은 불변입니다. 따라서 새로운 객체가 생성됩니다. 불변인 String의 내부 값은 변경할 수 없습니다. 따라서 변경된 값을 기반으로 새로운 String 객체를 생성합니다. 더 많은 문자를 더하는 경우를 살펴봅시다. String str = "A" + "B" + "C" + "D"; String str = String("A") + String("B") + String("C") + String("D"); String str = new String("AB") + String("C") + String("D"); String str = new String("ABC") + String("D"); String str = new String("ABCD"); 이 경우 총 3개의 String 클래스가 추가로 생성됩니다. 그런데 문제는 중간에 만들어진 new String("AB"), new String("ABC")는 사용되지 않습니다. 최종적으로 만들어진 new String("ABCD")만 사용됩니다. 결과적으로 중간에 만들어진 new String("AB)", new String("ABC")는 제대로 사용되지도 않고, 이후 GC의 대상이 됩니다. 불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점입니다. 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 합니다. 결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 됩니다. 그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모합니다. 참고 : 실제로는 문자열을 다룰 때 자바가 내부에서 최적화를 적용합니다. StringBuilder 이 문제를 해결하는 방법은 단순합니다. 바로 불변이 아닌 가변 String이 존재하면 됩니다. 가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없습니다. 따라서 성능과 메모리 사용면에서 불변보다 더 효율적입니다. 이런 문제를 해결하기 위해 자바는 StringBuilder라는 가변 String을 제공합니다. 물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 합니다. StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]을 가지고 있습니다. public final class StringBuilder { char[] value; // 자바 9 이전 char[] value; // 자바 9 이후 // 여러 메서드 public StringBuilder append(String str) {...} public int length() {...} ... } (실제로는 상속 관계에 있고 부모 클래스인 AbstractStringBuilder에 value 속성과 length() 메서드가 있습니다.) StringBuilder 사용 예 실제 StringBuilder를 어떻게 사용하는지 확인해 봅시다. package lang.string.builder; public class StringBuilderMain1_1 { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); sb.append("A"); sb.append("B"); sb.append("C"); sb.append("D"); System.out.println("sb = " + sb); sb.insert(4, "Java"); System.out.println("sb = " + sb); sb.delete(4,8); System.out.println("delete = " + sb); sb.reverse(); System.out.println("reverse = " + sb); // StringBuilder(가변) -> String(불변)으로 바꿀 수 있음. String string = sb.toString(); System.out.println("string = " + string); } } StringBuilder 객체를 생성합니다. append() 메서드를 사용해 여러 문자열을 추가합니다. insert() 메서드로 특정 위치에 문자열을 삽입합니다. delete() 메서드로 특정 범위의 문자열을 삭제합니다. reverse() 메서드로 문자열을 뒤집습니다. 마지막으로 toString() 메소드를 사용해 StringBuilder의 결과를 기반으로 String을 생성해서 반환합니다. 실행 결과 sb = ABCD sb = ABCDJava delete = ABCD reverse = DCBA string = DCBA 가변(Mutable) vs 불변(Immutable) String은 불변합니다. 즉, 한 번 생성되면 그 내용을 변경할 수 없습니다. 따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려집니다. 이 과정에서 메모리와 처리 시간을 더 많이 소모합니다. 반면에, StringBuilder는 가변적입니다. 하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않습니다. 이로 인해 메모리 사용을 줄이고 성늘을 향상시킬 수 있습니다. 단 사이드 이펙트를 주의해야 합니다. StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋습니다.
Archive
· 2024-04-10
☕️[Java] String 클래스 - 주요 메서드 1
String 클래스 - 주요 메서드 1 주요 메서드 목록 String 클래스는 문자열을 편리하게 다루기 위한 다양한 메서드를 제공합니다. 여기서는 자주 사용하는 기능 위주로 나열했습니다. 참고로 기능이 너무 많기 때문에 메서드를 외우기 보다는 주로 사용하는 메서드가 이런 것이 있구나 대략 알아두고, 필요할 때 검색하거나 API 문서를 통해서 원하는 기능을 찾는 것이 좋습니다. 문자열 정보 조회 length() : 문자열의 길이를 반환합니다. isEmpty() : 문자열이 비어 있는지 확인합니다. (길이가 0). isBlank() : 문자열이 비어 있는지 확인합니다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11 charAt(int index): 지정된 인덱스에 있는 문자를 반환합니다. 문자열 비교 equals(Object anObject) : 두 문자열이 동일한지 비교합니다. equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교합니다. compareTo(String anotherString) : 두 문자열을 사전 순으로 비교합니다. compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교합니다. startWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인합니다. endWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인합니다. 문자열 검색 contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인합니다. indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환합니다. lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환합니다. 문자열 조작 및 변환 substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다. concat(String str) : 문자열의 끝에 다른 문자열을 붙입니다. replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체합니다. replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체합니다. replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체합니다. toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환합니다. trim() : 문자열 양쪽 끝의 공백을 제거합니다. 단순 Whitespace만 제거할 수 있습니다. strip() : Whitespace 와 유니코드 공백을 포함해서 제거합니다, 자바 11 문자열 분할 및 조합. split(String regex) : 문자열을 정규 표현식을 기준으로 분할합니다. join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합합니다. 기타 유틸리티. valueOf(Object obj) : 다양한 타입을 문자열로 변환합니다. toCharArray() : 문자열을 문자 배열로 변환합니다. format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성합니다. matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인합니다. 이제 본격적으로 하나씩 알아봅시다. 참고: CharSequence 는 String, StringBuilder의 상위 타입입니다. 문자열을 처리하는 다양한 객체를 받을 수 있습니다. 문자열 정보 조회 length() : 문자열의 길이를 반환합니다. isEmpty() : 문자열이 비어 있는지 확인합니다. (길이가 0). isBlank() : 문자열이 비어 있는지 확인합니다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11 charAt(int index): 지정된 인덱스에 있는 문자를 반환합니다. package lang.string.method; public class StringInfoMain { public static void main(String[] args) { String str = "Hello, Java!"; System.out.println("문자열의 길이: " + str.length()); System.out.println("문자열이 비어 있는지: " + str.isEmpty()); System.out.println("문자열이 비어 있거나 공백인지 1: " + str.isBlank()); System.out.println("문자열이 비어 있거나 공백인지 2: " + " ".isBlank()); char c = str.charAt(7); System.out.println("7번째 인덱스의 문자 = " + c); } } 실행 결과 문자열의 길이: 12 문자열이 비어 있는지: false 문자열이 비어 있거나 공백인지 1: false 문자열이 비어 있거나 공백인지 2: true 7번째 인덱스의 문자 = J 문자열 비교 equals(Object anObject) : 두 문자열이 동일한지 비교합니다. equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교합니다. compareTo(String anotherString) : 두 문자열을 사전 순으로 비교합니다. compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교합니다. startWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인합니다. endWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인합니다. package lang.string.method; public class StringComparisonMain { public static void main(String[] args) { String str1 = "Hello, Java!"; // 대문자 일부 있음 String str2 = "hello, java!"; String str3 = "Hello, World!"; System.out.println("str equals str2: " + str1.equals(str2)); System.out.println("str equalsIgnoreCase str2: " + str1.equalsIgnoreCase(str2)); System.out.println("'a' compareTo 'b': " + "a".compareTo("b")); System.out.println("'b' compareTo 'a': " + "b".compareTo("a")); System.out.println("'c' compareTo 'a': " + "c".compareTo("a")); System.out.println("str1 compareTo str3: " + str1.compareTo(str3)); System.out.println("str1 compareToIgnoreCase str2: " + str1.compareToIgnoreCase(str2)); System.out.println("str1 starts with 'Hello': " + str1.startsWith("Hello")); System.out.println("str1 ends with 'Java!': " + str1.endsWith("Java!")); } } 실행 결과 str equals str2: false str equalsIgnoreCase str2: true 'a' compareTo 'b': -1 'b' compareTo 'a': 1 'c' compareTo 'a': 2 str1 compareTo str3: -13 str1 compareToIgnoreCase str2: 0 str1 starts with 'Hello': true str1 ends with 'Java!': true 문자열 검색 contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인합니다. indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환합니다. lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환합니다. package lang.string.method; public class StringSearchMain { public static void main(String[] args) { String str = "Hello, Java! Welcome to Java world."; System.out.println("문자열에 'Java'가 포함되어 있는지: " + str.contains("Java")); System.out.println("'Java'의 첫 번째 인덱스: " + str.indexOf("Java")); System.out.println("인덱스 10부터 'Java'의 인덱스: " + str.indexOf("Java", 10)); System.out.println("'Java'의 마지막 인덱스: " + str.lastIndexOf("Java")); } } 실행 결과 문자열에 'Java'가 포함되어 있는지: true 'Java'의 첫 번째 인덱스: 7 인덱스 10부터 'Java'의 인덱스: 24 'Java'의 마지막 인덱스: 24
Archive
· 2024-04-09
☕️[Java] String 클래스 - 불변객체
String 클래스 - 불변객체 String은 불변 객체입니다. 따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없습니다. 다음 예를 봅시다. package lang.string.immutable; public class StringImmutable1 { public static void main(String[] args) { String str = "hello"; str.concat(" java"); System.out.println("str = " + str); } } String.concat() 메서드를 사용하면 기존 문자열에 새로운 문자열을 연결해서 합칠 수 있습니다. 이 경우 어떤 실행 결과가 나올까요? 불변 객체에서 학습한 내용을 떠올려봅시다. 실행 결과 str = hello 실행 결과를 보면 뭔가 이상합니다. 문자가 전혀 합쳐지지 않았습니다. 다음 코드를 봐봅시다. package lang.string.immutable; public class StringImmutable2 { public static void main(String[] args) { String str1 = "hello"; String str2 = str1.concat(" java"); System.out.println("str1 = " + str1); System.out.println("str2 = " + str2); } } String은 불변 객체입니다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환합니다. 실행 결과 str1 = hello str2 = hello java String.concat()은 내부에서 새로운 String 객체를 만들어서 반환합니다. 따라서 불변과 기존 객체의 값을 유지합니다. String이 불변으로 설계된 이유. String이 불변으로 설계된 이유는 앞서 불변 객체에서 배운 내용에 추가로 다음과 같은 이유도 있습니다. 문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경됩니다. 예를 들어봅시다. String은 자바 내부에서 문자열 풀을 통해 최적화를 합니다 만약 String 내부의 값을 변경할 수 있다면, 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제가 발생합니다. 다음의 경우 str3이 참조하는 문자를 변경하면 str4의 문자도 함께 변경되는 사이드 이펙트 문자가 발생합니다. String str3 = "hello" String str4 = "hello" String 클래스는 불변으로 설계되어서 이런 사이드 이펙트 문제가 발생하지 않습니다.
Archive
· 2024-04-09
☕️[Java] String 클래스 - 비교
String 클래스 - 비교 String 클래스를 비교할 때는 == 비교가 아니라 항상 equals() 비교를 해야합니다. 동일성(Identity) : == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인 동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 같은지 확인 package lang.string.equals; public class StringEqualsMain1 { public static void main(String[] args) { String str1 = new String("hello"); String str2 = new String("hello"); System.out.println("new String() == 비교: " + (str1 == str2)); System.out.println("new String() equals 비교:" + (str1.equals(str2))); String str3 = "hello"; String str4 = "hello"; System.out.println("리터럴 == 비교: " + (str3 == str4)); System.out.println("리터럴 equals 비교: " + (str3.equals(str4))); } } 실행 결과 new String() == 비교: false new String() equals 비교:true 리터럴 == 비교: true 리터럴 equals 비교: true str1과 str2는 new String()을 사용해서 각각 인스턴스를 생성했습니다. 서로 다른 인스턴스이므로 동일성(==) 비교에 실패합니다. 둘은 내부에 같은 "hello" 값을 가지고 있기 때문에 논리적으로 같습니다. 따라서 동등성(equals()) 비교에 성공합니다. 참고로 String 클래스는 내부 문자열 값을 비교하도록 equals() 메서드를 재정의 해두었습니다. String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용합니다. 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어둡니다. 이때 같은 문자열이 있으면 만들지 않습니다. String str3 - "hello"와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello" 라는 문자를 가진 String 인스턴스를 찾습니다. 그리고 찾은 인스턴스의 참조(x003)를 반환합니다. String str4 = "hello"의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3과 같은 x003 참조를 사용합니다. 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있습니다. 따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공합니다. 참고 : 풀(pool)은 자원이 모여있는 곳을 의미합니다. 프로그래밍에서 풀(pool)은 공용 자원을 모아둔 곳을 뜻합니다. 여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고, 제거하는 것은 비효율적입니다. 대신에 이렇게 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 더 최적화 할 수 있습니다. 참고로 문자열 풀은 힙 영역을 사용합니다. 그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있습니다. 그렇다면 문자열 리터럴을 사용하면 == 비교를 하고, new String()을 직접 사용하는 경우에만 equals() 비교를 사용하면 되지 않을까요? 다음 코드를 봅시다. package lang.string.equals; public class StringEqualsMain2 { public static void main(String[] args) { String str1 = new String("hello"); String str2 = new String("hello"); System.out.println("메서드 호출 비교1: " + isSame(str1, str2)); String str3 = "hello"; String str4 = "hello"; System.out.println("메서드 호출 비교2: " + isSame(str3, str4)); } private static boolean isSame(String x, String y) { return x == y; //return x.equals(y); } } 실행 결과 메서드 호출 비교1: false 메서드 호출 비교2: true main() 메서드를 만드는 개발자와 isSame() 메서드를 만드는 개발자가 서로 다르다고 가정해봅시다. isSame()의 경우 매개변수로 넘어오는 String 인스턴스가 new String()으로 만들어진 것인지, 문자열 리터럴로 만들어 진것인지 확인할 수 있는 방법이 없습니다. 따라서 문자열 비교는 항상 equals()를 사용해서 동등성 비교를 해야 합니다.
Archive
· 2024-04-08
☕️[Java] String 클래스 - 기본
String 클래스 - 기본. 자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있습니다. package lang.string; public class CharArrayMain { public static void main(String[] args) { char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'}; System.out.println(charArr); String str = "hello"; System.out.println("str = " + str); } } 실행 결과 hello str = hello 기본형인 char는 문자 하나를 다룰 때 사용합니다. char를 사용해서 여러 문자를 나열하려면 char[]을 사용해야 합니다. 하지만 이렇게 char[]을 직접 다루는 방법은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는 String 클래스를 제공합니다. String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있습니다. package lang.string; public class StringBasicMain { public static void main(String[] args) { String str1 = "hello"; String str2 = new String("hello"); System.out.println("str1 = " + str1); System.out.println("str2 = " + str2); } } 쌍따옴표: "hello" 객체 생성: new String("hello"); String은 클래스입니다. int, boolean 같은 기본형이 아니라 참조형입니다. 따라서 str1 변수에는 String 인스턴스의 참조값만 들어갈 수 있습니다. 따라서 다음 코드는 뭔가 어색합니다. String str1 = "hello"; 문자열은 매우 자주 사용됩니다. 그래서 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello")와 같이 변경해 줍니다.(이 경우 실제로는 성능 최적화를 위해 문자열 풀을 사용합니다.) String str1 = "hello"; // 기존 String str1 = new String("hello"); // 변경 String 클래스 구조 String 클래스는 대략 다음과 같이 생겼습니다. public final class String { // 문자열 보관 private final char[] value; // 자바 9 이전 private final byte[] value; // 자바 9 이후 // 여러 메서드 public String concat(String str) {...} public int length() {...} } 클래스이므로 속성과 기능을 가집니다. 속성(필드) private final char[] value; 여기에는 String의 실제 문자열 값이 보관됩니다. 문자 데이터 자체는 char[] 에 보관됩니다. String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공합니다. 그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공합니다. 참고: 자바 9 이후 String 클래스 변경 사항 자바 9부터 String 클래스에서 char[] 대신에 byte[]을 사용합니다. private final byte[] value; 자바에서 문자 하나를 표현하는 char는 2byte를 차지합니다. 그런데 영어, 숫자는 보통 1byte로 표현이 가능합니다. 그래서 단순 영어, 숫자로 표현된 경우 1byte를 사용하고(정확히는 Latin-1 인코딩의 경우 1byte 사용) 그렇지 않은 나머지의 경우 2byte인 UTF-16 인코딩을 사용합니다. 따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었습니다. 기능(메서드) String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공합니다. 기능이 방대하므로 필요한 기능이 있으면 검색하거나 API 문서를 찾아봅시다. 주요 메서드는 다음과 같습니다. length() : 문자열의 길이를 반환합니다. charAt(inte index) : 특정 인덱스의 문자를 반환합니다. substring(int beinIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다. indexOF(String str) : 특정 문자열이 시작되는 인덱스를 반환합니다. toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다. trim() : 문자열 양 끝의 공백을 제거합니다. concat(String str) : 문자열을 더합니다. String 클래스와 참조형 String은 클래스입니다. 따라서 기본형이 아니라 참조형입니다. 참조형은 변수에 계산할 수 있는 값들이 들어있는 것이 아니라 x001과 같이 계산할 수 없는 참조값이 들어있습니다. 따라서 원칙적으로 + 같은 연산을 사용할 수 없습니다. ```java package lang.string; public class StringConcatMain { public static void main(String[] args) { String a = “hello”; String b = “ jave”; String result1 = a.concat(b); String result2 = a + b; System.out.println("result1 = " + result1); System.out.println("result2 = " + result2); } } ``` - 자바에서 문자열을 더할 때는 `String`이 제공하는 `concat()`과 같은 메서드를 사용해야 합니다. - 하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 `+` 연산을 제공합니다. 실행 결과 result1 = hello jave result2 = hello jave
Archive
· 2024-04-06
☕️[Java] 불변 객체 - 정리
불변 객체 - 정리. 지금까지 왜 이렇게 불변 객체 이야기를 많이 했을까요? 자바에서 가장 많이 사용되는 String 클래스가 바로 불변 객체이기 때문입니다. 뿐만 아니라 자바가 기본으로 제공하는 Integer, LocalDate 등 수 많은 클래스가 불변으로 설계되어 있습니다. 따라서 불변 객체가 필요한 이유와 원리를 제대로 이해해야, 이런 기본 클래스들도 제대로 이해할 수 있습니다. 모든 클래스를 불변으로 만드는 것은 아닙니다. 우리가 만드는 대부분의 클래스는 값을 변경할 수 있게 만들어집니다. 예를 들어서 회원 클래스의 경우 회원의 여러 속성을 변경할 수 있어야 합니다. 가변 클래스가 더 일반적이고, 불변 클래스는 값을 변경하면 안되는 특별한 경우에 만들어서 사용한다고 생각하면 됩니다. 때로는 같은 기능을 하는 클래스를 하나는 불변으로 하나는 가변으로 각각 만드는 경우도 있습니다. 클래스를 불변으로 설계하는 이유는 더 많습니다. 캐시 안정성 멀티 쓰레드 안정성 엔티티 값 타입 지금은 이런 부분을 다 이해할 수는 없습니다. 관련 내용을 학습하다 보면 자연스럽게 이번에 배운 불변 객체가 떠오르면서 관련된 내용을 본질적으로 더 잘 이해할 수 있을 것입니다. 프로그래밍을 더 깊이있게 학습할 수 록 다양한 불변 클래스 이용 사례를 만나고 이해하게 됩니다. 따라서 지금은 불변 클래스가 어디에 사용되고, 어떻게 활용되는지 보다는 불변 클래스의 원리를 이해하는 정도면 충분합니다.
Archive
· 2024-04-05
☕️[Java] 불변 객체 - 값 변경
불변 객체 - 값 변경. 불변 객체를 사용하지만 그래도 값을 변경해야 하는 메서드가 필요하면 어떻게 해야할까요? 예를 들어서 기존 값에 새로운 값을 더하는 add()와 같은 메서드가 있다고 합시다. 먼저 변경 가능한 객체에서 값을 변경하는 간단한 예를 만들어봅시다. package lang.immutable.change; public class MutableMain { public static void main(String[] args) { MutableObj obj = new MutableObj(10); obj.add(20); // 계산 이후의 기존 값은 사라짐 System.out.println("obj = " + obj.getValue()); } } 실행 결과 obj = 30 MutableObj을 10이라는 값으로 생성합니다. 이후에 obj.add(20)을 통해서 10 + 20을 수행합니다. 계산 이후에 기존에 있던 10이라는 값은 사라집니다. MutableObj의 상태(값)가 10 -> 30으로 변경되었습니다. obj.getValue()를 호출하면 30이 출력됩니다. 이번에는 불변 객체에서 add() 메서드를 어떻게 구현하는지 알아봅시다. 참고로 불변 객체는 변하지 않아야 합니다. package lang.immutable.change; public class ImmutableObj { private final int value; public ImmutableObj(int value) { this.value = value; } public ImmutableObj add(int addValue) { int result = value + addValue; return new ImmutableObj(result); } public int getValue() { return value; } } 여기서 핵심은 add() 메서드 입니다. 불변 객체는 값을 변경하면 안됩니다! 그러면 이미 불변 객체가 아닙니다! 하지만 여기서는 기존 값에 새로운 값을 더해야 합니다. 불변 객체는 기존 값은 변경하지 않고 대신에 계산 결과를 바탕으로 새로운 객체를 만들어서 반환합니다. 이렇게 하면 불변도 유지하면서 새로운 결과도 만들 수 있습니다. package lang.immutable.change; public class ImmutableMain1 { public static void main(String[] args) { ImmutableObj obj1 = new ImmutableObj(10); ImmutableObj obj2 = obj1.add(20); // 계산 이후에도 기존값과 신규값 모두 확인 가능 System.out.println("obj1 = " + obj1.getValue()); System.out.println("obj2 = " + obj2.getValue()); } } 실행 결과 obj1 = 10 obj2 = 30 불변 객체를 설계할 때 기존 값을 변경해야 하는 메서드가 필요할 수 있습니다. 이때는 기존 객체의 값을 그대로 두고 대신에 변경된 결과를 새로운 객체에 담아서 반환하면 됩니다. 결과를 보면 기존 값이 그대로 유지되는 것을 확인할 수 있습니다. add(20)을 호출합니다. 기존 객체에 있는 10과 인수로 입력한 20을 더합니다. 이때 기존 객체의 값을 변경하면 안되므로 계산 결과를 기반으로 새로운 객체를 만들어서 반환합니다. 새로운 객체는 x002 참조를 가집니다. 새로운 객체의 참조값을 obj2에 대입합니다. 만약 여기서 다음과 같이 새로 생성된 반환 값을 사용하지 않으면 어떻게 될까요? package lang.immutable.change; public class ImmutableMain2 { public static void main(String[] args) { ImmutableObj obj1 = new ImmutableObj(10); obj1.add(20); // 계산 이후에도 기존값과 신규값 모두 확인 가능 System.out.println("obj1 = " + obj1.getValue()); } } 실행 결과 obj1 = 10 실행 결과처럼 아무것도 처리되지 않은 것 처럼 보일 것입니다. 불변 객체에서 변경과 관련된 메서드들은 보통 객체를 새로 만들어서 반환하기 때문에 꼭! 반환 값을 받아야 합니다.
Archive
· 2024-04-05
☕️[Java] 불변 객체 - 예제
불변 객체 - 예제 조금 더 복잡하고 의미있는 예제를 통해서 불변 객체의 사용 예를 확인해봅시다. 앞의 Address, ImmutableAddress를 그래로 활용합니다. 변경 클래스 사용 package lang.immutable.address; public class MemberMainV1 { public static void main(String[] args) { Address address = new Address("서울"); MemberV1 memberA = new MemberV1("회원A", address); MemberV1 memberB = new MemberV1("회원B", address); // 회원A, 회원B의 처음 주소는 모두 서울 System.out.println("memberA = " + memberA); System.out.println("memberB = " + memberB); // 회원 B의 주소를 부산으로 변경해야함 memberB.getAddress().setValue("부산"); System.out.println("부산 -> memberB.address"); System.out.println("memberA = " + memberA); System.out.println("memberB = " + memberB); } } 회원A와 회원B는 둘다 서울에 살고 있습니다. 중간에 회원B의 주소를 부산으로 변경해야 합니다. 그런데 회원A와 회원B는 같은 Address 인스턴스를 참조하고 있습니다. 회원B의 주소를 부산으로 변경하는 순간 회원A의 주소도 부산으로 변경됩니다. 실행 결과 memberA = MemberV1{name='회원A', address=Address{value='서울'}} memberB = MemberV1{name='회원B', address=Address{value='서울'}} 부산 -> memberB.address memberA = MemberV1{name='회원A', address=Address{value='부산'}} memberB = MemberV1{name='회원B', address=Address{value='부산'}} package lang.immutable.address; public class MemberMainV2 { public static void main(String[] args) { ImmutableAddress address = new ImmutableAddress("서울"); MemberV2 memberA = new MemberV2("회원A", address); MemberV2 memberB = new MemberV2("회원B", address); // 회원A, 회원B의 처음 주소는 모두 서울 System.out.println("memberA = " + memberA); System.out.println("memberB = " + memberB); // 회원B의 주소를 부산으로 변경해야함 //memberB.getAddress().setValue("부산"); // 컴파일 오류 memberB.setAddress(new ImmutableAddress("부산")); System.out.println("부산 -> memberB.address"); System.out.println("memberA = " + memberA); System.out.println("memberB = " + memberB); } } 회원B의 주소를 중간에 부산으로 변경하려고 시도합니다. 하지만 ImmutableAddress에는 값을 변경할 수 있는 메서드가 없습니다. 따라서 컴파일 오류가 발생합니다. 결국 memberB.setAddress(new ImmutableAddress("부산"))와 같이 새로운 주소 객체를 만들어서 전달합니다. 실행 결과 memberA = MemberV1{name='회원A', address=Address{value='서울'}} memberB = MemberV1{name='회원B', address=Address{value='서울'}} 부산 -> memberB.address memberA = MemberV1{name='회원A', address=Address{value='서울'}} memberB = MemberV1{name='회원B', address=Address{value='부산'}} 사이드 이펙트가 발생하지 않습니다. 회원A는 기존 주소를 그대로 유지합니다.
Archive
· 2024-04-04
☕️[Java] 공유 참조와 사이드 이펙트
공유 참조와 사이드 이펙트. 사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말합니다. 앞서 b의 값을 부산으로 변경한 코드를 다시 분석해 봅시다. b.setValue("부산"); //b의 값을 부산으로 변경해야함 System.out.println("부산 -> b"); System.out.println("a = " + a); // 사이드 이펙트 발생 System.out.println("b = " + b); 개발자는 b의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했습니다. 하지만 a, b는 같은 인스턴스를 참조합니다. 따라서 a의 값도 함께 부산으로 변경되어 버립니다. 이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트라고 합니다. 프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생합니다. 이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있습니다. 사이드 이펙트 해결방안 생각해보면 문제의 해결방안은 아주 단순합니다. 다음과 같이 a와 b가 처음부터 서로 다른 인스턴스를 참조하면 됩니다. Address a = new Address("서울"); Address b = new Address("서울"); 코드를 작성해봅시다. package lang.immutable.address; public class RefMain1_2 { public static void main(String[] args) { // 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다. Address a = new Address("서울"); Address b = new Address("서울"); System.out.println("a = " + a); System.out.println("b = " + b); b.setValue("부산"); System.out.println("부산 -> b"); System.out.println("a = " + a); System.out.println("b = " + b); } } 실행 결과 a = Address{value='서울'} b = Address{value='서울'} 부산 -> b a = Address{value='서울'} b = Address{value='부산'} 실행 결과를 보면 b의 주소값만 부산으로 변경된 것을 확인할 수 있습니다. a와 b는 서로 다른 Address 인스턴스를 참조합니다. a와 b는 서로 다른 인스턴스를 참조합니다. 따라서 b가 참조하는 인스턴스의 값을 변경해도 a에는 영향을 주지 않습니다. 여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다 지금까지 발생한 모든 문제는 같은 객체(인스턴스)를 변수 a, b가 함께 공유하기 때문에 발생했습니다. 따라서 객체를 공유하지 않으면 문제가 해결됩니다. 여기서 변수 a,b가 서로 각각 다른 주소지로 변경할 수 있어야 합니다. 이렇게 하려면 서로 다른 객체를 참조하면 됩니다. 객체를 공유 Address a = new Address("서울"); Address b = a; 이 경우 a, b 둘 다 같은 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 불가능합니다. 객체를 공유 하지 않음 Address a = new Address("서울"); Address b = new Address("서울"); 이 경우 a, b는 서로 다른 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 가능합니다. 이처럼 단순하게 서로 다른 객체를 참조해서, 같은 객체를 공유하지 않으면 문제가 해결됩니다. 쉽게 이야기해서 여러 변수가 하나의 객체를 공유하지 않으면 지금까지 설명한 문제들이 발생하지 않습니다. 그런데 여기에 문제가 있습니다. 하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것입니다 다음 예시를 봅시다. 참조값의 공유를 막을 수 있는 방법이 없습니다. Address a = new Address("서울"); Address b = a; // 참조값 대입을 막을 수 있는 방법이 없습니다. b = a와 같은 코드를 작성하지 않도록 해서, 여러 변수가 하나의 참조값을 공유하지 않으면 문제가 해결될 것 같습니다. 하지만 Address를 사용하는 개발자 입장에서 실수로 b = a라고 해도 아무런 오류가 발생하지 않습니다. 왜냐하면 자바 문법상 Address b = a와 같은 참조형 변수의 대입은 아무런 문제가 없기 때문입니다. 다음과 같이 새로운 객체를 참조형 변수에 대입하든, 또는 기존 객체를 참조형 변수에 대입하든, 다음 두 코드 모두 자바 문법상 정상인 코드입니다. Address b = new Address("서울"); // 새로운 객체 참조 Address b = a // 기존 객체 공유 참조 참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 됩니다. 쉽게 이야기해서 객체의 공유를 막을 수 있는 방법이 없습니다! 기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않습니다. 하지만 참조형의 경우 참조값을 복사해서 대입하기 때문에 여러 변수에서 얼마든지 같은 객체를 공유할 수 있습니다. 객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있습니다. 물론 개발자가 눈을 크게 잘 뜨고! 집중해서 코드를 잘 작성하면서 사이드 이펙트 문제를 일으키지 않을 수 있습니다. 하지만 실제로는 훨씬 더 복잡한 상황에서 이런 문제가 발생합니다. 다음 코드를 봅시다. package lang.immutable.address; public class RefMain1_3 { public static void main(String[] args) { // 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다. Address a = new Address("서울"); Address b = a; System.out.println("a = " + a); System.out.println("b = " + b); change(b, "부산"); System.out.println("a = " + a); System.out.println("b = " + b); } private static void change(Address address, String changeAddress) { System.out.println("주소 값을 변경합니다 -> " + changeAddress); address.setValue(changeAddress); } } 앞서 작성한 코드와 같은 코드입니다. 단순히 change() 메서드만 하나 추가되었습니다. 그리고 change() 메서드에서 Address 인스턴스에 있는 value 값을 변경합니다. main() 메서드만 보면 a의 값이 함께 부산으로 변경된 이류를 찾기가 더 어렵습니다. 실행 결과 a = Address{value='서울'} b = Address{value='서울'} 주소 값을 변경합니다 -> 부산 a = Address{value='부산'} b = Address{value='부산'} 여러 변수가 하나의 객체를 참조하는 공유 참조를 막을 수 있는 방법은 없습니다. 그럼 공유 참조로 인해 발생하는 문제를 어떻게 해결할 수 있을까요? 단순히 개발자가 공유 참조 문제가 발생하지 않도록 조심해서 코드를 작성해야 할까요?
Archive
· 2024-04-03
☕️[Java] 불변 객체 - 도입
불변 객체 - 도입 지금까지 발생한 문제를 잘 생각해보면 공유하면 안되는 객체를 여러 변수에서 공유했기 때문에 발생한 문제입니다. 하지만 앞서 살펴보았듯이 객체의 공유를 막을 수 있는 방법은 없습니다. 그런데 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아닙니다. 객체를 공유한다고 바로 사이드 이펙트가 발생하지 않습니다. 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있습니다. 앞의 예를 떠올려보면 a, b는 처음 시점에는 둘 다 "서울"이라는 주소를 사용해야 합니다. 그리고 이후에 b의 주소를 "부산"으로 변경해야 합니다. Address a = new Address("서울"); Address b = a; 따라서 처음에는 b = a와 같이 "서울"이라는 Address 인스턴스를 a, b가 함께 사용하는 것이, 다음 코드와 서로 다른 인스턴스를 사용하는 것 보다 메모리와 성능상 더 효율적입니다. 인스턴스가 하나이니 메모리가 절약되고, 인스턴스를 하나 생성하지 않아도 되니 생성 시간이 줄어서 성능상 효율적입니다. Address a = new Address("서울"); Address b = new Address("서울"); 여기까지는 Address b = a와 같이 공유 참조를 사용해도 아무런 문제가 없습니다. 오히려 더 효율적입니다. 진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생합니다. b.setValue("부산"); // b의 값을 부산으로 변경해야 합니다. System.out.println("부산 -> b"); System.out.println("a = " + a); // 사이드 이펙트 발생 System.out.println("b = " + b); 자바에서 여러 참조형 변수가 하나의 객체(인스턴스)를 참조하는 공유 참조 문제는 피할 수 없습니다. 기본형과 다르게 참조형인 객체는 처음부터 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었습니다. 따라서 이것은 문제가 아닙니다. 문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 더이선가 변경했기 때문입니다. 만약 Address 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것입니다. 불변 객체 도입 객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 합니다. 앞서 만들었던 Address 클래스를 상태가 변하지 않는 불변 클래스로 다시 만들어 봅시다. package lang.immutable.address; public class ImmutableAddress { private final String value; public ImmutableAddress(String value) { this.value = value; } public String getValue() { return value; } @Override public String toString() { return "Address{" + "value='" + value + '\'' + '}'; } } 내부 값이 변경되면 안됩니다. 따라서 value의 필드를 final로 선언했습니다. 값을 변경할 수 있는 setValue()를 제거했습니다. 이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능합니다. 불변 클래스를 만드는 방법은 아주 단순합니다. 어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 됩니다. package lang.immutable.address; public class RefMain2 { public static void main(String[] args) { // 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다. ImmutableAddress a = new ImmutableAddress("서울"); ImmutableAddress b = a; // 참조값 대입을 막을 수 있는 방법이 없다. System.out.println("a = " + a); System.out.println("b = " + b); // b.setValue("부산"); // 컴파일 오류 발생 b = new ImmutableAddress("부산"); System.out.println("부산 -> b"); System.out.println("a = " + a); // 사이드 이펙트 발생 System.out.println("b = " + b); } } ImmutableAddress의 경우 값을 변경할 수 있는 b.setValue() 메서드 자체가 제거되었습니다. 이제 ImmutableAddress 인스턴스의 값을 변경할 수 있는 방법은 없습니다. ImmutableAddress를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 사실을 알고, 이 객체가 불변 객체인 사실을 깨닫습니다. 예를 들어 b.setValue("부산")을 호출하려고 했는데, 해당 메서드가 없다는 사실을 컴파일 오류를 통해 인지한다. 따라서 어쩔 수 없이 새로운 ImmutableAddress("부산") 인스턴스를 생성해서 b에 대입한다. 결과적으로 a, b는 서로 다른 인스턴스를 참조하고, a가 참조하던 ImmutableAddress는 그대로 유지됩니다. 실행 결과 a = Address{value='서울'} b = Address{value='서울'} 부산 -> b a = Address{value='서울'} b = Address{value='부산'} 실행 결과를 보면 a의 값은 그대로 유지되는 것을 확인할 수 있습니다. 자바에서 객체의 공유 참조는 막을 수 없습니다. ImmutableAddress는 불변 객체입니다. 따라서 값을 변경할 수 없습니다. ImmutableAddress은 불변 객체이므로 b가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면 새로운 인스턴스를 생성해서 할당해야 합니다. 정리 불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있습니다. 객체의 공유 참조는 막을 수 없습니다. 그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생합니다. 사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어 사용하면 됩니다. 불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단됩니다. 불변 객체는 값을 변경할 수 없습니다. 따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 합니다. 이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않습니다. 참고 - 가변(Mutable) 객체 VS 불변(Immutable) 객체 가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻입니다.(사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻입니다.) 불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻입니다.(사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻입니다.) Address 는 가변 클래스입니다. 이 클래스로 객체를 생성하면 가변 객체가 됩니다. ImmutableAddress는 불변 클래스입니다. 이 클래스로 객체를 생성하면 불변 객체가 됩니다.
Archive
· 2024-04-03
☕️[Java] 기본형과 참조형의 공유
기본형과 참조형의 공유. 자바의 데이터 타입을 가장 크게 보면 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있습니다. 기본형(Primitive Type): 하나의 값을 여러 변수에서 절대로 공유하지 않습니다. 참조형(Reference Type): 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있습니다. 하나의 값을 공유하거나 또는 공유하지 않는다는 것이 무슨 뜻인지 예제를 통해 알아봅시다. 기본형 예제 기본형은 하나의 값을 여러 변수에서 절대로 공유하지 않습니다. 다음 예를 봅시다. package lang.immutable.address; public class PrimitiveMain { public static void main(String[] args) { // 기본형은 절대로 같은 값을 공유하지 않는다. int a = 10; int b = a; // a -> b, 값 복사 후 대입 System.out.println("a = " + a); System.out.println("b = " + b); b = 20; System.out.println("20 -> b"); System.out.println("a = " + a); System.out.println("b = " + b); } } 실행 결과 a = 10 b = 10 20 -> b a = 10 b = 20 기본형 변수 a와 b는 절대로 하나의 값을 공유하지 않습니다. b = a라고 하면 자바는 항상 값을 복사해서 대입 합니다. 이 경우 a에 있는 값 10을 복사해서 b에 전달합니다. 결과적으로 a와 b는 둘 다 10이라는 똑같은 숫자의 값을 가집니다. 하지만 a가 가지는 10과 b가 가지는 10은 복사된 완전히 다른 10입니다. 메모리 상에서도 a에 속하는 10과 b에 속하는 10이 각각 별도로 존재합니다. b = 20이라고 하면 b의 값만 20으로 변경됩니다. a의 값은 10으로 그대로 유지됩니다. 기본형 변수는 하나의 값을 절대로 공유하지 않습니다. 따라서 값을 변경해도 변수 하나의 값만 변경됩니다. 여기서는 변수 b의 값만 20으로 변경되었습니다. 너무 당연한 이야기 입니다. 그렇다면 이번에는 참조형 예제를 봐봅시다. 참조형 예제 package lang.immutable.address; public class RefMain1_1 { public static void main(String[] args) { // 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다. Address a = new Address("서울"); Address b = a; System.out.println("a = " + a); System.out.println("b = " + b); b.setValue("부산"); // b의 값을 부산으로 변경해야함. System.out.println("부산 -> b"); System.out.println("a = " + a); // 사이드 이펙트 발생 System.out.println("b = " + b); } } 처음에는 a, b 둘다 서울이라는 주소를 가져야 한다고 가정합니다. 따라서 Address b = a 코드를 작성했고, 변수 a, b 둘다 서울이라는 주소를 가집니다. 이후에 b의 주소를 부산으로 변경합니다. 그런데 실행 결과를 보면 b뿐만 아니라 a의 주소도 함께 부산으로 변경되어 버립니다. 실행 결과 a = Address{value='서울'} b = Address{value='서울'} 부산 -> b a = Address{value='부산'} b = Address{value='부산'} 순서대로 코드를 분석해봅시다. Address a = new Address("서울"); Address b = a; 참조형 변수들은 같은 참조값을 통해 같은 인스턴스를 참조할 수 있습니다. b = a라고 하면 a에 있는 참조값 x001을 복사해서 b에 전달합니다. 자바에서 모든 값 대입은 변수가 가지고 있는 값을 복사해서 전달합니다. 변수가 int 같은 숫자값을 가지고 있으면 숫자값을 복사해서 전달하고, 참조값을 가지고 있으면 참조값을 복사해서 전달합니다. 참조값을 복사해서 전달하므로 결과적으로 a, b는 같은 x001 인스턴스를 참조합니다. 기본형 변수는 절대로 같은 값을 공유하지 않습니다. 예) a = 10, b = 10과 같이 같은 모양의 숫자 10이라는 값을 가질 수는 있지만 같은 값을 공유하는 것은 아닙니다 서로 다른 숫자 10이 두 개 있는 것입니다. 참조형 변수는 참조값을 통해 같은 객체(인스턴스)를 공유할 수 있습니다. 여기서 b의 주소만 부산으로 변경했는데, a의 주소도 함께 부산으로 변경되어 버린 이유는 무엇일까요? 메모리 구조를 보면 바로 답이 나오겠지만, 개발을 하다 보면 누구나 이런 실수할 수 있을 것 같다는 생각도 함께 들 것입니다.
Archive
· 2024-04-02
☕️[Java] equals() - 2. 구현
equals() - 2. 구현 UserV2 예제 UserV2는 id(고객번호)가 같으면 논리적으로 같은 객체로 정의하겠습니다. package lang.object.equals; public class UserV2 { private String id; public UserV2(String id) { this.id = id; } @Override public boolean equals(Object obj) { UserV2 user = (UserV2) obj; return id.equals(user.id); } } Object의 equals() 메서드를 재정의했습니다. UserV2의 동등성은 id(고객번호)로 비교합니다. equals()는 Object 타입을 매개변수로 사용합니다. 따라서 객체의 특정 값을 사용하려면 다운캐스팅이 필요합니다. 여기서는 현재 인스턴스(this)에 있는 id 문자열과 비교 대상으로 넘어온 객체의 id 문자열을 비교합니다. UserV2에 있는 id는 String입니다. 문자열 비교는 ==이 아니라 equals()를 사용해야 합니다. package lang.object.equals; public class EqualsMainV2 { public static void main(String[] args) { UserV2 user1 = new UserV2("id-100"); UserV2 user2 = new UserV2("id-100"); System.out.println("identity = " + (user1 == user2)); System.out.println("equality = " + user1.equals(user2)); } } 실행 결과 identity = false equality = true 동일성(Identity) : 객체의 참조가 다르므로 동일성은 다릅니다. 동등성(Equality) : user1, user2는 서로 다른 객체이지만 둘다 같은 id(고객번호)를 가지고 있습니다. 따라서 동등합니다. 정확한 equals() 구현 앞서 UserV2에서 구현한 equals()는 이해를 돕기 위해 매우 간단히 만든 버전이고, 실제로 정확하게 동작하려면 다음과 같이 구현해야 합니다. 정확한 equals() 메서드를 구현하는 것은 생각보다 쉽지 않습니다. IntelliJ를 포함한 대부분의 IDE는 정확한 equals() 코드를 자동으로 만들어 줍니다. generator 단축키 : ⌘N(macOS) / Alt + Insert(Windows / Linux) @Override public boolean equals(Object o) { if (this == 0) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equalse(id, user.id); } equals() 메서드를 구현할 때 지켜야 하는 규칙 반사성(Reflexivity): 객체는 자기 자신과 동등해야 합니다.(x.equals(x)는 항상 true). 대칭성(Symmetry): 두 객체가 서로에 대해 동일하다고 판단하면, 이는 양방향으로 동일해야 합니다.(x.equals(y)가 true이면 y.equals(x)도 true). 추이성(Transitivity): 만약 한 객체가 두 번째 객체와 동일하고, 두 번째 객체가 세 번째 객체와 동일하다면, 첫 번째 객체는 세 번째 객체와도 동일해야 합니다. 일관성(Consistency): 두 객체의 상태가 변경되지 않는 한, equals() 메소드는 항상 동일한 값을 반환해야 합니다. null에 대한 비교: 모든 객체는 null과 비교했을 때 false를 반환해야 합니다. 실무에서는 대부분 IDE가 만들어주는 equalse()를 사용하므로, 이 규칙을 외우기 보다는 대략 이렇구나 정도로 한번 읽어보고 넘어가면 충분합니다. 정리 참고로 동일성 비교가 항상 필요한 것은 아닙니다. 동일성 비교가 필요한 경우에만 equals()를 재정의하면 됩니다. equals()와 hashCode()는 보통 함께 사용됩니다. 이 부분은 추후에 컬렉션 프레임워크를 학습후 정리하겠습니다.
Archive
· 2024-04-01
☕️[Java] equals() - 1.동일성과 동등성
equals() - 1.동일성과 동등성 Object는 동등성 비교를 위한 equals() 메서드를 제공합니다. 자바는 두 객체가 같다라는 표현을 2가지로 분리해서 제공합니다. 동일성(Idenriry) : == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인 동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인 단어 정리 “동일”은 완전히 같음을 의미합니다. 반면 “동등”은 같은 가치나 수준을 의미하지만 그 형태나 외관 등이 완전히 같지는 않을 수 있습니다. 쉽게 이야기해서 동일성은 물리적으로 같은 메모리에 있는 객체 인스턴스인지 참조값을 확인하는 것이고, 동등성은 논리적으로 같은지 확인하는 것입니다. 동일성은 자바 머신 기준이고 메모리 참조가 기준이므로 물리적입니다. 반면 동등성은 보통 사람이 생각하는 논리적인 기준에 맞추어 비교합니다. 예를 들어 같은 회원 번호를 가진 회원 객체가 2개 있다고 가정해봅시다. User a = new User("id-100") // 참조 x001 User b = new User("id-100") // 참조 x002 이 경우 물리적으로 다른 메모리에 있는 다른 객체이지만, 회원 번호를 기준으로 생각해보면 논리적으로는 같은 회원으로 볼 수 있습니다.(주민등록번호가 같다고 가정해도 됩니다.) 따라서 동일성은 다르지만, 동등성은 같습니다. 문자의 경우도 마찬가지입니다. String s1 = "hello"; String s2 = "hello"; 이 경우 물리적으로 각각의 “hello” 문자열이 다른 메모리에 존재할 수 있지만, 논리적으로는 같은 "hello"라는 문자열입니다.(사실 이 경우 자바가 같은 메모리를 사용하도록 최적화 합니다. 이 부분은 추후에 학습하겠습니다.) 예제를 통해서 동일성과 동등성을 비교해봅시다. package lang.object.equals; public class EqualsMainV1 { public static void main(String[] args) { UserV1 user1 = new UserV1("id-100"); UserV1 user2 = new UserV1("id-100"); System.out.println("identity = " + (user1 == user2)); System.out.println("equlity = " + (user1.equals(user2))); } } 실행 결과 identity = false equlity = false 동일성 비교 user1 == user2 x001 == x002 false // 결과 동등성 비교 Object.equals() 메서드 public boolean equals(Object obj) { return (this == obj); } Object가 기본적으로 제공하는 equals()는 ==으로 동일성 비교를 제공합니다. equals 실행 순서 예시 user1.equals(user2) return (user1 == user2) // Object.equals 메서드 안 return (x001 == x002) // Object.equals 메서드 안 return false false 동등성이라는 개념은 각각의 클래스 마다 다릅니다. 어떤 클래스는 주민등록번호를 기반으로 동등성을 처리할 수 있고, 어떤 클래스는 고객의 연락처를 기반으로 동등성을 처리할 수 있습니다. 어떤 클래스는 회원 번호를 기반으로 동등성을 처리할 수 있습니다. 따라서 동등성 비교를 사용하고 싶으면 equals() 메서드를 재정의해야 합니다. 그렇지 않으면 Object는 동일성 비교를 기본으로 제공합니다.
Archive
· 2024-03-31
☕️[Java] Object와 OCP
Object와 OCP. 만약 Object가 없고, 또 Object가 제공하는 toString()이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것입니다. 여기서 아무 관계가 없다는 것은 공통의 부모가 없다는 뜻 입니다. 아마도 다음의 BadObjectPrinter 클래스와 같이 각각의 클래스마다 별도의 메서드를 작성해야 할 것입니다. BadObjectPrinter public class BadObjectPrinter { public static void print(Car car) { // Car 전용 메서드 String string = "객체 정보 출력: " + car.carInfo(); // carInfo() 메서드 만듬 System.out.println(string); } public static void print(Dog dog) { // Dog 전용 메서드 String string = "객체 정보 출력: " + dog.dogInfo(); // dogInfo() 메서드 만듬 System.out.println(string); } } 구체적인 것에 의존 BadObjectPrinter는 구체적인 타입인 Car, Dog를 사용합니다. 따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 됩니다. 이렇게 BadObjectPrinter 클래스가 구체적인 특정 클래스인 Car, Dog를 사용하는 것을 BadObjectPrinter는 Car, Dog에 의존한다고 표현합니다. 다행히도 자바에는 객체의 정보를 사용할 때, 다형적 참조 문제를 해결해줄 Object 클래스와 메서드 오버라이딩 문제를 해결해줄 Object.toString() 메서드가 있습니다.(물론 직접 Object와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있습니다.) 추상적인 것에 의존 앞서 만든 ObjectPrinter 클래스는 Car, Dog 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 Object 클래스를 사용합니다. 이렇게 ObjectPrinter 클래스가 Object 클래스를 사용하는 것을 Object에 클래스에 의존한다고 표현합니다. public class ObjectPrinter { public static void print(Object obj) { String string = "객체 정보 출력: " + obj.toString(); System.out.println(string); } } ObjectPrinter는 구체적인 것에 의존하는 것이 아니라 추상적인 것에 의존합니다. 추상적 : 여기서 말하는 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아닙니다. Animal과 Dog, Cat의 관계를 떠올려봅시다. Animal 같은 부모 타입으로 올라갈 수록 개념은 더 추상적이게 되고, Dog, Cat과 같이 하위 타입으로 내려갈 수록 개념은 더 구체적이게 됩니다. ObjectPrinter와 Object를 사용하는 구조는 다형성을 매우 잘 활용하고 있습니다. 다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻입니다. ObjectPrinter의 print() 메서드와 전체 구조를 분석해봅시다. 다형적 참조 : print(Object obj), Object 타입을 매개변수로 사용해서 다형적 참조를 사용합니다. Car, Dog 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있습니다. 메서드 오버라이딩 : Object는 모든 클래스의 부모입니다. 따라서 Dog, Car와 같은 구체적인 클래스는 Object가 가지고 있는 toString() 메서드를 오버라이딩 할 수 있습니다. 따라서 print(Object obj) 메서드는 Dog, Car와 같은 구체적인 타입에 의존(사용)하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 toString()을 호출할 수 있습니다. OCP 원칙 OCP 원칙을 떠올려 봅시다. Open : 새로운 클래스를 추가하고, toString()을 오버라이딩해서 기능을 확장할 수 있습니다. Closed : 새로운 클래스를 추가해도 Object와 toString()을 사용하는 클라이언트 코드인 ObjectPrinter는 변경하지 않아도 됩니다. 다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 Car, Dog에 의존하는 것이 아니라 추상적인 Object에 의존하면서 OCP 원칙을 지킬 수 있었습니다. 덕분에 새로운 클래스를 추가하고 toString() 메서드를 새롭게 오버라이딩해서 기능을 확장할 수 있습니다. 그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter는 변경할 필요가 없습니다. ObjectPrinter는 모든 타입의 부모인 Object를 사용하고, Object가 제공하는 toString() 메서드만 사용합니다. 따라서 ObjectPrinter를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있습니다. System.out.println() 지금까지 설명한 ObjectPrinter.print()는 사실 System.out.println()의 작동 방식을 설명하기 위해 만든 것입니다. System.out.println() 메서드도 Object 매개변수를 사용하고 내부에서 toString()을 호출합니다. 따라서 System.out.println()를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있습니다. 자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용합니다. 지금까지 배운 toString() 메서드와 같이, 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 어버라이딩해서 사용할 수 있도록 설계되어 있습니다. 참고 - 정적 의존관계 vs 동적 의존관계 정적 의존관계는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미합니다. 앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계입니다. 쉽게 이야기해서 프로그램을 실행하지 않고, 클래스 내에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 있습니다. 동적 의존관계는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계입니다. 앞서 ObjectPrinter.print(Object obj)에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있습니다. 어떤 경우에는 Car 인스턴스가 넘어오고, 어떤 경우에는 Dog 인스턴스가 넘어옵니다. 이렇게 런타임에 어떤 인스턴스를 사용하는지를 나타내는 것이 동적 의존관계입니다. 참고로 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존관계를 뜻합니다. 예) ObjectPrinter는 Object에 의존합니다.
Archive
· 2024-03-31
☕️[Java] toString()
toString() Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공합니다. 그래서 디버깅과 로깅에 유용하게 사용됩니다. 이 메서드는 Object 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있습니다. 코드로 확인해봅시다. package lang.object.tostring; public class ToStringMain1 { public static void main(String[] args) { Object object = new Object(); String string = object.toString(); //toString() 반환값 출력 System.out.println(string); // object 직접 출력 System.out.println(object); } } 실행 결과 java.lang.Object@a09ee92 java.lang.Object@a09ee92 Object.toString() public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } Object가 제공하는 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공합니다 println()과 toString() 그런데 toString()의 결과를 출력한 코드와 object를 println()에 직접 출력한 코드의 결과가 완전히 같습니다. String string = object.toString(); //toString() 반환값 출력 System.out.println(string); // object 직접 출력 System.out.println(object); System.out.println() 메서드는 사실 내부에서 toString()을 호출합니다. Object 타입(자식 포함)이 println()에 인수로 전달되면 내부에서 obj.toString() 메서드를 호출해서 결과를 출력합니다. public void println(Object x) { String s = String.valueOf(x); // ... } public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString(); } 따라서 println()을 사용할 때, toString()을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있습니다. toString() 오버라이딩. Object.toString() 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못합니다. 그래서 보통 toString()을 재정의(오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적입니다. package lang.object.tostring; public class Car { private String carName; public Car(String carName) { this.carName = carName; } } package lang.object.tostring; public class Dog { private String dogName; private int age; public Dog(String dogName, int age) { this.dogName = dogName; this.age = age; } @Override public String toString() { return "Dog{" + "dogName='" + dogName + '\'' + ", age=" + age + '}'; } } package lang.object.tostring; public class ObjectPrinter { public static void print(Object obj) { String string = "객체 정보 출력: " + obj.toString(); System.out.println(string); } } package lang.object.tostring; public class ToStringMain2 { public static void main(String[] args) { Car car = new Car("Model Y"); Dog dog1 = new Dog("멍멍이1", 2); Dog dog2 = new Dog("멍멍이2", 5); System.out.println("1. 단순 toString 호출"); System.out.println(car.toString()); System.out.println(dog1.toString()); System.out.println(dog2.toString()); System.out.println("2. println 내부에서 toString 호출"); System.out.println(car); System.out.println(dog1); System.out.println(dog2); System.out.println("3. Object 다용성 활용"); ObjectPrinter.print(car); ObjectPrinter.print(dog1); ObjectPrinter.print(dog2); } } 실행 결과 1. 단순 toString 호출 lang.object.tostring.Car@452b3a41 Dog{dogName='멍멍이1', age=2} Dog{dogName='멍멍이2', age=5} 2. println 내부에서 toString 호출 lang.object.tostring.Car@452b3a41 Dog{dogName='멍멍이1', age=2} Dog{dogName='멍멍이2', age=5} 3. Object 다용성 활용 객체 정보 출력: lang.object.tostring.Car@452b3a41 객체 정보 출력: Dog{dogName='멍멍이1', age=2} 객체 정보 출력: Dog{dogName='멍멍이2', age=5} Car 인스턴스는 toString()을 재정의 하지 않습니다. 따라서 Object가 제공하는 기본 toString() 메서드를 사용합니다. Dog 인스턴스는 toString()을 재정의 한 덕분에 객체의 상태를 명확하게 확인할 수 있습니다. Object obj의 인수로 car(Car)가 전달됩니다. 메서드 내부에서 obj.toString()을 호출합니다. obj는 Object 타입입니다. 따라서 Object에 있는 toString()을 찾습니다. 이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아봅니다. 재정의된 메서드가 없습니다. Object.toString()을 실행합니다. ObjectPrinter.print(dog) // main에서 호출 void print(Object obj = dog(Dog)) { // 인수 전달 String string = "객체 정보 출력: " + obj.toString(); } Object obj의 인수로 dog(Dog) 가 전달 됩니다. 메서드 내부에서 obj.toString()을 호출합니다. obj는 Object 타입입니다. 따라서 Object에 있는 toString()을 찾습니다. 이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아봅니다. Dog에 재정의된 메서드가 있습니다. Dog.toString()을 실행합니다. 참고 - 객체의 참조값 직접 출력 toString()은 기본으로 객체의 참조값을 출력합니다. 그런데 toString()이나 hashCode()를 재정의하면 객체의 참조값을 출력할 수 없습니다. 이때는 다음 코드를 사용하면 객체의 참조값을 출력할 수 있습니다. String refValue = Integer.toHexString(System.identityHashCode(dog1)); System.out.println("refValue = " + refValue) 실행 결과 refValue = 72ea2f77
Archive
· 2024-03-29
☕️[Java] Object 다형성
Object 다형성. Object는 모든 클래스의 부모 클래스입니다. 따라서 Object는 모든 객체를 참조할 수 있습니다. 예제를 통해서 Object의 다형성에 대해서 알아봅시다. Dog와 Car은 서로 아무런 관련이 없는 클래스입니다. 둘다 부모가 없으므로 Object를 자동으로 상속 받습니다. package lang.object.poly; public class Car { public void move() { System.out.println("자동차 이동"); } } package lang.object.poly; public class Dog { public void sound() { System.out.println("멍멍"); } } package lang.object.poly; public class ObjectPolyExample1 { public static void main(String[] args) { Dog dog = new Dog(); Car car = new Car(); action(dog); action(car); } private static void action(Object obj) { //obj.sound(); // 컴파일 오류, Object는 sound()가 없다. //obj.move(); // 컴파일 오류, Object는 move()가 없다. // 객체에 맞는 다운캐스팅이 필요함. if (obj instanceof Dog dog) { dog.sound(); } else if (obj instanceof Car car) { car.move(); } } } 실행 결과 멍멍 자동차 이동 Object는 모든 타입의 부모입니다. 부모는 자식을 담을 수 있으므로 앞의 코드를 다음과 같이 변경해도 됩니다. Object dog = new Dog(): // Dog -> Object Object car = new Car(): // Car -> Object Object 다형성의 장점. action(Object obj) 메서드를 분석해봅시다. 이 메서드는 Object 타입의 매개변수를 사용합니다. 그런데 Object는 모든 객체의 부모입니다. 따라서 어떤 객체든지 인자로 전달 할 수 있습니다. action(dog) // main에서 dog 전달 void action(Object obj = dog(Dog)) //Object는 자식인 Dog 타입을 참조할 수 있습니다. action(car) void action(Object obj = car(Car)) // Object는 자식인 Car 타입을 참조할 수 있습니다. Object 다형성의 한계. action(dog) // main에서 dog 전달 private static void action(Object obj) { obj.sound(); // 컴파일 오류, Object는 sound()가 없습니다. } action() 메서드안에서 obj.sound()를 호출하면 오류가 발생합니다. 왜냐하면 매개변수인 obj는 Object 타입이기 때문입니다. Object에는 sound() 메서드가 없습니다. obj.sound() 호출 obj.sound()를 호출합니다. obj는 Object 타입이므로 Object 타입에서 sound()를 찾습니다. Object에서 sound()를 찾을 수 없습니다. Object는 최종 부모이므로 더는 올라가서 찾을 수 없습니다. 따라서 오류가 발생합니다. Dog 인스턴스의 sound()를 호출하려면 다음과 같이 다운캐스팅을 해야합니다. if (obj instanceof Dog dog) { dog.sound(); } Object obj의 참조값을 Dog dog로 다운캐스팅 하면서 전달합니다. dog.sound()를 호출하면 Dog 타입에서 sound()를 찾아서 호출합니다. Object를 활용한 다형성의 한계. Object는 모든 객체를 대상으로 다형적 참조를 할 수 있습니다. 쉽게 이야기해서 Object는 모든 객체의 부모이므로 모든 객체를 담을 수 있습니다. Object를 통해 전달 받은 객체를 호출하려면 각 객체에 맞는 다운캐스팅 과정이 필요합니다. Object가 세상의 모든 메서드를 알고 있는 것이 아닙니다. 다형성을 제대로 활용하려면 다형적 참조 + 메서드 오버라이딩을 함께 사용해야 합니다. 그런면에서 Object를 사용한 다형성에는 한계가 있습니다. Object는 모든 객체의 부모이므로 모든 객체를 대상으로 다형적 참조를 할 수 있습니다. 하지만 Object에는 Dog.sound(), Car.move()와 같은 다른 객체의 메서드가 정의되어 있지 않습니다. 따라서 메서드 오버라이딩을 활용할 수 없습니다. 결국 각 객체의 기능을 호출하려면 다운캐스팅을 해야 합니다. 참고로 Object 본인이 보유한 toString() 같은 메서드는 당연히 자식 클래스에서 오버라이딩 할 수 있습니다. 여기서 이야기하는 것은 앞서 설명한 Dog.sound(), Car.move() 같은 Object에 속하지 않은 메서드를 말합니다. 결과적으로 다형적 참조는 가능하지만, 메서드 오버라이딩이 안되기 때문에 다형성을 활용하기에는 한계가 있습니다.
Archive
· 2024-03-29
☕️[Java] Object 배열
Object 배열. 이번에는 Object 배열을 알아봅시다. Object는 모든 타입의 객체를 담을 수 있습니다. 따라서 Object[]을 만들면 세상의 모든 객체를 담을 수 있는 배열을 만들 수 있습니다. package lang.object.poly; public class ObjectPolyExample2 { public static void main(String[] args) { Dog dog = new Dog(); Car car = new Car(); Object object = new Object(); //Object 인스턴스도 만들 수 있습니다. Object[] objects = {dog, car, object}; size(objects); } private static void size(Object[] objects) { System.out.println("전달된 객체의 수는: " + objects.length); } } 실행 결과 전달된 객체의 수는: 3 Object[] objects = {dog, car, object}; // 쉽게 풀어서 설명하면 다음과 같습니다. Object objects[0] = new Dog(); Object objects[1] = new Car(); Object objects[2] = new Object(); Object 타입을 사용한 덕분에 세상의 모든 객체를 담을 수 있는 배열을 만들 수 있었습니다. size() 메서드 size(Object[] objects) 메서드는 배열에 담긴 객체의 수를 세는 역할을 담당합니다. 이 메서드는 Object 타입의 배열은 세상의 모든 객체를 담을 수 있기 때문에, 새로운 클래스가 추가되거나 변경되어도 이 메서드를 수정하지 않아도 됩니다. 지금 만든 size() 메서드는 자바를 사용하는 곳이라면 어디든지 사용될 수 있습니다. Object가 없다면? 만약 Object와 같은 개념이 없다면 어떻게 될까요? void action(Object obj)과 같이 모든 객체를 받을 수 있는 메서드를 만들 수 없습니다. Object[] objects 처럼 모든 객체를 저장할 수 있는 배열을 만들 수 없습니다. 물론 Object가 없어도 직접 MyObject와 같은 클래스를 만들고 모든 클래스에서 직접 정의한 MyObject를 상속 받으면 됩니다. 하지만 하나의 프로젝트를 넘어서 전세계 모든 개발자가 비슷한 클래스를 만들 것이고, 서로 호환되지 않은 수 많은 XxxObject들이 넘쳐날 것입니다.
Archive
· 2024-03-29
☕️[Java] java.lang 패키지 소개
java.lang 패키지 소개. 자바가 기본적으로 제공하는 라이브러리(클래스 모음)중에 가장 지본이 되는 것이 바로 java.lang 패키지입니다. 여기서 lang은 Language(언어)의 줄임말 입니다. 쉽게 이야기해서 자바 언어를 이루는 가장 기본이 되는 클래스들을 보관하는 패키지를 뜻합니다. java.lang 패키지의 대표적인 클래스들. Object : 모든 자바 객체의 부모 클래스 String : 문자열 Integer, Long, Double : 래퍼 타입, 기본형 데이터 타입을 객체로 만든 것 Class : 클래스 메타 정보 System: 시스템과 관련된 기본 기능들을 제공 “여기 나열한 클래스들은 자바 언어의 기본을 이루기 때문에 반드시 잘 알아두어야 합니다.” import 생략 가능 java.lang 패키지는 모든 자바 애플리케이션에 자동으로 임포트(import)됩니다. 따라서 임포트 구문을 사용하지 않아도 됩니다. 다른 패키지에 있는 클래스를 사용하려면 다음과 같이 임포트를 사용해야 합니다. package lang; import java.lang.System; public class LangMain { public static void main(String[] args) { System.out.println("hello.java"); } } System 클래스는 java.lang 패키지 소속입니다. 따라서 다음과 같이 임포트를 생략할 수 있습니다. package lang; public class LangMain { public static void main(String[] args) { System.out.println("hello, java"); } } import java.lang.System; 코드를 삭제해도 정상 동작합니다.
Archive
· 2024-03-28
☕️[Java] Object 클래스
Object 클래스. 자바에서 모든 클래스의 최상위 부모 클래스는 항상 Object 클래스입니다. 다음 그림과 예제 코드를 봐봅시다. package lang.object; // 부모가 없으면 묵시적으로 Object 클래스를 상속 받는다. public class Parent { public void parentMethod() { System.out.println("Parent.parentMethod"); } } 앞의 코드는 다음 코드와 같습니다. package lang.object; // 부모가 없으면 묵시적으로 Object 클래스를 상속 받는다. public class Parent extends Object { public void parentMethod() { System.out.println("Parent.parentMethod"); } } 클래스에 상속 받을 부모 클래스가 없으면 묵시적으로 Object 클래스를 상속 받습니다. 쉽게 이야기해서 자바가 extends Object 코드를 넣어줍니다. 따라서 extends Object는 생략하는 것을 권장합니다. package lang.object; public class Child extends Parent { public void childMethod() { System.out.println("Child.childMethod"); } } 클래스에 상속 받을 부모 클래스를 명시적으로 지정하면 Object를 상속 받지 않습니다. 쉽게 이야기해서 이미 명시적으로 상속했기 때문에 자바가 extends Object 코드를 넣지 않습니다. 묵시적(Implicit) vs 명시적(Explicit) 묵시적: 개발자가 코드에 직접 기술하지 않아도 시스템 또는 컴파일러에 의해 자동으로 수행되는 것을 의미합니다. 명시적: 개발자가 코드에 직접 기술해서 작동하는 것을 의미합니다. package lang.object; public class ObjectMain { public static void main(String[] args) { Child child = new Child(); child.childMethod(); child.parentMethod(); // toString()은 Object 클래스의 메서드입니다. String string = child.toString(); System.out.println(string); } } 실행 결과 Child.childMethod Parent.parentMethod lang.object.Child@452b3a41 실행 결과 그림 Parent는 Object를 묵시적으로 상속 받았기 때문에 메모리에도 함께 생성됩니다. child.toString()을 호출합니다. 먼저 본인의 타입인 Child에서 toString()을 찾습니다. 없으므로 부모 타입으로 올라가서 찾습니다. 부모 타입인 Parent에서 찾습니다. 없으므로 부모 타입으로 올라가서 찾습니다. 부모 타입인 Object에 toString()이 있으므로 이 메서드를 호출합니다. 정리 자바에서 모든 객체의 최종 부모는 Object입니다. 자바에서 Object 클래스가 최상위 부모 클래스인 이유. 모든 클래스가 Object 클래스를 상속 받는 이유는 다음과 같습니다. 공통 기능 제공 다형성의 기본 구현 공통 기능 제공. 객체의 정보를 제공하고, 이 객체가 다른 객체와 같은지 비교하고, 객체가 어떤 클래스로 만들어졌는지 확인하는 기능은 모든 객체에게 필요한 기본 기능입니다. 이런 기능을 객체를 만들 때 마다 항상 새로운 메서드를 정의해서 만들어야 한다면 상당히 번거로울 것입니다. 그리고 막상 만든다고 해도 개발자마다 서로 다른 이름의 메서드로 만들어서 일관성이 없을 것입니다. 예를 들어서 객체의 정보를 제공하는 기능을 만든다고 하면 어떤 개발자는 toString()으로 또 어떤 개발자는 objectInfo()와 같이 서로 다른 이름으로 만들 수 있습니다. 객체를 비교하는 기능을 만들 때도 어떤 개발자는 equals()로 어떤 개발자는 same()으로 만들 수 있습니다. Object는 모든 객체에 필요한 공통 기능을 제공합니다. Object는 최상위 부모 클래스이기 때문에 모든 객체는 공통 기능을 편리하게 제공(상속)받을 수 있습니다. Object가 제공하는 기능은 다음과 같습니다. 객체의 정보를 제공하는 toString() 객체의 같음을 비교하는 equals() 객체의 클래스 정보를 제공하는 getClass() 기타 여러가지 기능 개발자는 모든 객체가 앞서 설명한 메서드를 지원한다는 것을 알고 있습니다. 따라서 프로그래밍이 단순화되고, 일관성을 가집니다. 다형성의 기본 구현 부모는 자식을 담을 수 있습니다. Object는 모든 클래스의 부모 클래스 입니다. 따라서 모든 객체를 참조할 수 있습니다. Object 클래스는 다형성을 지원하는 기본적인 메커니즘을 제공합니다. 모든 자바 객체는 Object 타입으로 처리될 수 있으며, 이는 다양한 타입의 객체를 통합적으로 처리할 수 있게 해줍니다. 쉽게 이야기해서 Object는 모든 객체를 다 담을 수 있습니다. 타입이 다른 객체들을 어딘가에 보관해야 한다면 바로 Object에 보관하면 됩니다.
Archive
· 2024-03-28
☕️[Java] 다형성 - 역할 구현 예제 3
다형성 - 역할과 구현 예제 3 다형성을 활용하면 역할과 구현을 분리해서, 클라이언트 코드의 변경 없이 구현 객체를 변경할 수 있습니다. 다음 관계에서 Driver가 클라이언트입니다. 예제를 통해서 자세히 알아봅시다. 앞서 설명한 자동차 예제를 코드로 구현해보겠습니다. Driver: 운전자는 자동차(Car)의 역할에만 의존합니다. 구현인 K3, Model3 자동차에 의존하지 않습니다. Driver 클래스는 Car car 멤버 변수를 가집니다. 따라서 Car 인터페이스를 참조합니다. 인터페이스를 구현한 K3Car, Model3Car에 의존하지 않고, Car인터페이스에만 의존합니다. 여기서 설명하는 의존은 클래스 의존 관계를 뜩합니다. 클래스 상에서 어떤 클래스를 알고 있는가를 뜻합니다. Driver 클래스 코드를 보면 Car 인터페이스만 사용하는 것을 확인할 수 있습니다. Car: 자동차의 역할이고 인터페이스입니다. K3Car, Model3Car 클래스가 인터페이스를 구현합니다. package poly.car1; public interface Car { void startEngine(); void offEngine(); void pressAccelerator(); } package poly.car1; public class K3Car implements Car { @Override public void startEngine() { System.out.println("K3Car.startEngine"); } @Override public void offEngine() { System.out.println("K3Car.offEngine"); } @Override public void pressAccelerator() { System.out.println("K3Car.pressAccelerator"); } } package poly.car1; public class Model3Car implements Car{ @Override public void startEngine() { System.out.println("Model3Car.startEngine"); } @Override public void offEngine() { System.out.println("Model3Car.offEngine"); } @Override public void pressAccelerator() { System.out.println("Model3Car.pressAccelerator"); } } package poly.car1; public class Driver { private Car car; public void setCar(Car car) { System.out.println("자동차를 설정합니다: " + car); this.car = car; } public void drive() { System.out.println("자동차를 운전합니다."); car.startEngine(); car.pressAccelerator(); car.offEngine(); } } Driver는 멤버 변수로 Car car를 가집니다. setCar(Car car): 멤버 변수에 자동차를 설정합니다. 외부에서 누군가 이 메서드를 호출해주어야 Driver는 새로운 자동차를 참조하거나 변경할 수 있습니다. drive(): Car 인터페이스가 제공하는 기능들을 통해 자동차를 운전합니다. package poly.car1; public class CarMain1 { public static void main(String[] args) { Driver driver = new Driver(); // 차량 선택(k3) K3Car k3Car = new K3Car(); driver.setCar(k3Car); driver.drive(); // 차량 변경(k3 -> model3) Model3Car model3Car = new Model3Car(); driver.setCar(model3Car); driver.drive(); } } 실행 결과 자동차를 설정합니다: poly.car1.K3Car@4a574795 자동차를 운전합니다. K3Car.startEngine K3Car.pressAccelerator K3Car.offEngine 자동차를 설정합니다: poly.car1.Model3Car@23fc625e 자동차를 운전합니다. Model3Car.startEngine Model3Car.pressAccelerator Model3Car.offEngine 먼저 Driver와 K3Car를 생성합니다. driver.setCar(k3Car)를 호출해서 Driver의 Car car 필드가 K3Car의 인스턴스를 참조하도록 합니다. driver.drive()를 호출하면 x001을 참조합니다. car 필드가 Car 타입이므로 Car 타입을 찾아서 실행하지만 메서드 오버라이딩에 의해 K3Car의 기능이 호출됩니다. Model3Car를 생성합니다. driver.setCar(model3Car)를 호출해서 Driver의 Car car 필드가 Model3Car의 인스턴스를 참조하도록 변경합니다. driver.drive()를 호출하면 x002을 참조합니다. car 필드가 Car 타입이므로 Car 타입을 찾아서 실행하지만 메서드 오버라이딩에 의해 Model3Car의 기능이 호출됩니다.
Archive
· 2024-03-25
☕️[Java] OCP(Open-Closed Principle) 원칙
OCP(Open-Closed Principle) 원칙. 좋은 객체 지향 설계 원칙 중 하나로 OCP 원칙이라는 것이 있습니다. Open for extension : 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 합니다. Closed for modification : 기존의 코드는 수정되지 않아야 합니다. 확장에는 열려있고, 변경에는 닫혀 있다는 뜻인데, 쉽게 이야기해서 기존의 코드 수정 없이 새로운 기능을 추가할 수 있다는 의미입니다. 약간 말이 안 맞는 것 같지만 우리가 앞서 개발한 코드가 바로 OCP 원칙을 잘 지키고 있는 코드입니다. 새로운 차량의 추가. 여기서 새로운 차량을 추가해도 Driver의 코드는 전혀 변경하지 않습니다. 운전할 수 있는 차량의 종류가 계속 늘어나도 Car를 사용하는 Driver의 코드는 전혀 변경하지 않습니다. 기능을 확장해도 main() 일부를 제외한 프로그램의 핵심 부분의 코드는 전혀 수정하지 않아도 됩니다. 확장에 열려있다는 의미. Car 인터페이스를 사용해서 새로운 차량을 자유롭게 추가할 수 있습니다. Car 인터페이스를 구현해서 기능을 추가할 수 있다는 의미입니다. Car 인터페이스를 사용하는 클라이언트 코드인 Driver도 Car 인터페이스를 통해 새롭게 추가된 차량을 자유롭게 호출할 수 있습니다. 이것이 확장에 열려있다는 의미입니다. 코드 수정은 닫혀 있다는 의미. 새로운 차를 추가하게 되면 기능이 추가되기 때문에 기존 코드의 수정은 불가피합니다. 당연히 어딘가의 코드는 수정해야 합니다. 변하지 않는 부분. 새로운 자동차를 추가할 때 가장 영향을 받는 중요한 클라이언트는 바로 Car의 기능을 사용하는 Driver입니다. 핵심은 Car 인터페이스를 사용하는 클라이언트인 Driver의 코드를 수정하지 않아도 된다는 뜻입니다. 변하는 부분. main()과 같이 새로운 차를 생성하고 Driver에게 필요한 차를 전달해주는 역할은 당연히 코드 수정이 발생합니다. main()은 전체 프로그램을 설정하고 조율하는 역할을 합니다. 이런 부분은 OCP를 지켜도 변경이 필요합니다. 정리. Car를 사용하는 클라이언트 코드인 Driver 코드의 변경 없이 새로운 자동차를 확장할 수 있습니다. 다형성을 활용하고 역할과 구현을 잘 분리한 덕분에 새로운 자동차를 추가해도 대부분의 핵심 코드들을 그대로 유지할 수 있게 되었습니다. 전략 패턴(Strategy Pattern) 디자인 패턴 중에 가장 중요한 패턴을 하나 뽑으라고 하면 전략 패턴을 뽑을 수 있습니다. 전략 패턴은 알고리즘을 클라이언트 코드의 변경 없이 쉽게 교체할 수 있습니다. 방금 설명한 코드가 바로 전략 패턴을 사용한 코드입니다. Car 인터페이스가 바로 전략을 정의하는 인터페이스가 되고, 각각의 차량이 전략의 구체적인 구현이 됩니다. 그리고 전략을 클라이언트 코드(Driver)의 변경 없이 손쉽게 교체할 수 있습니다.
Archive
· 2024-03-25
☕️[Java] 다형성 - 역할 구현 예제 2
다형성 - 역할과 구현 예제 2 새로운 Model3 차량을 추가해야 하는 요구사항이 들어왔습니다. 이 요구사항을 맞추려면 기존에 Driver 코드를 많이 변경해야 합니다. 드라이버는 K3Car도 운전할 수 있고, Model3Car도 운전할 줄 있어야 합니다. 참고로 돌을 동시에 운전하는 것은 아닙니다. package poly.car0; public class CarMain0 { public static void main(String[] args) { Driver driver = new Driver(); K3Car k3Car = new K3Car(); driver.setK3Car(k3Car); driver.drive(); // 추가 Model3Car model3Car = new Model3Car(); driver.setK3Car(null); driver.setModel3Car(model3Car); driver.drive(); } } K3를 운전하던 운전자가 Model3로 차량을 변경해서 운전하는 코드입니다. driver.setK3Car(null)을 통해서 기존 K3Car의 참조를 제거합니다. driver.setModel3Car(model3Car)를 통해서 새로운 model3Car의 참조를 추가합니다. driver.drive()를 호출합니다. 실행 결과 자동차를 운전합니다. K3Car.startEngine K3Car.pressAccelerator K3Car.offEngine 자동차를 운전합니다. Model3Car.startEngine Model3Car.pressAccelerator Model3Car.offEngine 여기서 새로운 차량을 추가한다면 또 다시 Driver 코드를 많이 변경해야 합니다. 만약 운전할 수 있는 차량의 종류가 계속 늘어난다면 점점 더 변경해야 하는 코드가 많아질 것입니다. “이 코드의 본질적인 문제는 자동차가 늘어나는데, 자동차 운전자의 코드를 계속해서 뜯어 고쳐야한다는 것 입니다.” 이런 문제가 생기는 이유는 “역할과 구현을 분리하지 않았기 때문입니다”.
Archive
· 2024-03-24
☕️[Java] 좋은 객체 지향 프로그래밍이란?
좋은 객체 지향 프로그래밍이란? 객체 지향 특징 추상화 캡슐화 상속 다형성 객체 지향 프로그래밍? 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉 “객체” 들의 모임으로 파악하고자 하는 것입니다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있습니다.(협력) 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됩니다. 유연하고, 변경에 용이? 레고 블럭 조립하듯이 키보드, 마우스 갈아 끼우듯이 컴퓨터 부품 갈아 끼우듯이 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법 다형성(Polymorphism) 다형성의 실세계 비유 실세계와 객체 지향을 1:1로 매칭 X 그래도 실세계의 비유로 이해하기에는 좋음 역할과 구현으로 세상을 구분 운전자 - 자동차 공연무대 로미오와 줄리렛 공연 예시 운전자 - 자동차 공연 무대 키보드, 마우스, 세상의 표준 인터페이스들 정렬 알고리즘 할인 정책 로직 역할과 구현을 분리 역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해집니다. 장점 클라이언트는 대상의 역할(인터페이스)만 알면 됩니다. 클라이언트는 구현 대상의 내부 구조를 몰라도 됩니다. 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않습니다. 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않습니다. 역할과 구현을 분리 2 자바 언어 자바 언어의 다형성을 활용합니다. 역할 = 인터페이스. 구현 = 인터페이스를 구현한 클래스, 구현 객체. 객체를 설계할 때 역할과 구현을 명확히 분리합니다. 객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만듭니다. 객체의 협력이라는 관계부터 생각 혼자 있는 객체는 없습니다. 클라이언트: 요청, 서버 응답 수 많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가집니다. 자바 언어의 다형성 오버라이딩을 떠올려봅시다. 오버라이딩은 자바 기본 문법입니다. 오버라이딩 된 메서드가 실행합니다. 다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있습니다. 물론 클래스 상속 관계도 다형성, 오버라이딩 적용 가능합니다. 다형성의 본질 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있습니다. 다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야 합니다. 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있습니다. 역할과 구현을 분리 3 정리 실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있습니다. 유연하고, 변경이 용이합니다. 확장 가능한 설계입니다. 클라이언트에 영향을 주지 않는 변경이 가능합니다. 인터페이스를 안정적으로 잘 설계하는 것이 중요합니다. 한계 역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두에 큰 변경이 발생합니다. 자동차를 비행기로 변경해야 한다면? 대본 자체가 변경된다면? USB 인터페이스가 변경된다면? 인터페이스를 안정적으로 잘 설계하는 것이 중요합니다. 정리 다형성이 가장 중요합니다! 디자인 패턴 대부분은 다형성을 활용하는 것입니다. 스프링의 핵심인 제어의 역전(IoC), 의존관계 주입(DI)도 결국 다형성을 활용하는 것입니다. 다형성을 잘 활용하면 마치 레고 블럭 조립하듯이! 공연 무대의 배우를 선택하듯이! 구현을 편리하게 변경할 수 있습니다.
Archive
· 2024-03-23
☕️[Java] 다형성 - 역할 구현 예제 1
다형성 - 역할 구현 예제 1 앞서 설명한 내용을 더 깊이있게 이해하기 위해, 간단한 운전자와 자동차의 관계를 개발해봅시다. 먼저 다형성을 사용하지 않고, 역할과 구현을 분리하지 않고 단순하게 개발해봅시다. Driver는 K3Car를 운전하는 프로그램입니다. package poly.car0; public class K3Car { public void startEngine() { System.out.println("K3Car.startEngine"); } public void offEngine() { System.out.println("K3Car.offEngine"); } public void pressAccelerator() { System.out.println("K3Car.pressAccelerator"); } } package poly.car0; public class Driver { private K3Car k3Car; public void setK3Car(K3Car k3Car) { this.k3Car = k3Car; } public void drive() { System.out.println("자동차를 운전합니다."); k3Car.startEngine(); k3Car.pressAccelerator(); k3Car.offEngine(); } } package poly.car0; public class CarMain0 { public static void main(String[] args) { Driver driver = new Driver(); K3Car k3Car = new K3Car(); driver.setK3Car(k3Car); driver.drive(); } }
Archive
· 2024-03-23
☕️[Java] 클래스와 인터페이스 활용
클래스와 인터페이스 활용 이번에는 클래스 상속과 인터페이스 구현을 함께 사용하는 예를 알아보겠습니다. AbstractAnimal은 추상 클래스입니다. sound() : 동물의 소리를 내기 위한 sound() 추상 메서드를 제공합니다. move() : 동물의 이동을 표현하기 위한 메서드 입니다. 이 메서드는 추상 메서드가 아닙니다. 상속을 목적으로 사용됩니다. Fly는 인터페이스 입니다. 나는 동물은 이 인터페이스를 구현할 수 있습니다. Bird, Chicken은 날 수 있는 동물입니다. fly() 메서드를 구현해야 합니다. 예제 6 package poly.ex6; public class SoundFlyMain { public static void main(String[] args) { Dog dog = new Dog(); Bird bird = new Bird(); Chicken chicken = new Chicken(); soundAnimal(dog); soundAnimal(bird); soundAnimal(chicken); flyAnimal(bird); flyAnimal(chicken); } //AbstractAnimal 사용 가능 private static void soundAnimal(AbstractAnimal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } //Fly 인터페이스가 있으면 사용 가능 private static void flyAnimal(Fly fly) { System.out.println("날기 테스트 시작"); fly.fly(); System.out.println("날기 테스트 종료"); } } 실행 결과 동물 소리 테스트 시작 멍멍 동물 소리 테스트 종료 동물 소리 테스트 시작 짹짹 동물 소리 테스트 종료 동물 소리 테스트 시작 꼬끼오 동물 소리 테스트 종료 날기 테스트 시작 새 날기 날기 테스트 종료 날기 테스트 시작 닭 날기 날기 테스트 종료 soundAnimal(AbstractAnimal animal) AbstractAnimal를 상속한 Dog, Bird, Chicken을 전달해서 실행할 수 있습니다. 실행 과정 soundAnimal(bird)를 호출한다고 가정합시다. 메서드 안에서 animal.sound()를 호출하면 참조 대상인 x001 Bird 인스턴스를 찾습니다. 호출한 animal 변수는 AbstractAnimal 타입입니다. 따라서 AbstractAnimal.sound()를 찾습니다. 해당 메서드는 Bird.sound()에 오버라이딩 되어 있습니다. Bird.sound()가 호출됩니다. flyAnimal(Fly fly) Fly 인터페이스를 구현한 Bird, Chicken을 전달해서 실행할 수 있습니다. 실행과정 fly(bird)를 호출한다고 가정합시다. 메서드 안에서 fly.fly()를 호출하면 참조 대상인 x001 Bird 인스턴스를 찾습니다. 호출한 fly 변수는 Fly 타입입니다. 따라서 Fly.fly()를 찾습니다. 해당 메서드는 Bird.fly()에 오버라이딩 되어 있습니다. Bird.fly()가 호출됩니다.
Archive
· 2024-03-22
☕️[Java] 인터페이스 - 다중구현
인터페이스 - 다중구현 자바가 다중 상속을 지원하지 않는 이유 - 복습 자바는 다중 상속을 지원하지 않습니다. 그래서 extend 대상은 하나만 선택할 수 있습니다. 부모를 하나만 선택할 수 있다는 뜻입니다. 물론 부모가 또 부모를 가지는 것은 괜찮습니다. 만약 비행기와 자동차를 상속 받아서 하늘을 나는 자동차를 만든다고 가정해봅시다. 만약 그림과 같이 다중 상속을 사용하게 되면 AirplaneCar 입장에서 move()를 호출할 때 어떤 부모의 move()를 사용해야 할지 애매한 문제가 발생합니다. 이것을 다이아몬드 문제라 합니다. 그리고 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해질 수 있습니다. 이런 문제점 때문에 자바는 클래스의 다중 상속을 허용하지 않습니다. 대신에 인터페이스의 다중 구현을 허용하여 이러한 문제를 피합니다. 클래스는 앞서 설명한 이유로 다중 상속이 안되는데, 인터페이스의 다중 구현은 허용한 이유는 무엇일까요? 인터페이스는 모두 추상 메서드로 이루어져 있기 때문입니다. 다음 예제를 봅시다. InterfaceA, InterfaceB는 둘 다 같은 methodCommon()을 가지고 있습니다. 그리고 Child는 두 인터페이스를 구현했습니다. 상속 관계의 경우 두 부모 중에 어떤 한 부모의 methodCommon()을 사용해야 할지 결정해야 하는 다이아몬드 문제가 발생합니다. 하지만 인터페이스 자신은 구현을 가지지 않습니다. 대신에 인터페이스를 구현하는 곳에서 해당 기능을 모두 구현해야 합니다. 여기서 InterfaceA, InterfaceB는 같은 이름의 methodCommon()를 제공하지만 이것의 기능은 Child가 구현합니다. 그리고 오버라이딩에 의해 어차피 Child에 있는 methodCommon()이 호출됩니다. 결과적으로 두 부모 중에 어떤 한 부모의 methodCommon()을 선택하는 것이 아니라 그냥 인터페이스들을 구현한 Child에 있는 methodCommon()이 사용됩니다. 이런 이유로 인터페이스는 다이아몬드 문제가 발생하지 않습니다. 따라서 인터페이스의 경우 다중 구현을 허용합니다. 예제를 코드로 작성해봅시다. package poly.diamond; public class Child implements InterfaceA, InterfaceB { @Override public void methodA() { System.out.println("Child.methodA"); } @Override public void methodB() { System.out.println("Child.methodB"); } @Override public void methodCommon() { System.out.println("Child.methodCommon"); } } ìmplements InterfaceA, InterfaceB와 같이 다중 구현을 할 수 있습니다. implements 키워드 위에 ,로 여러 인터페이스를 구분하면 됩니다. methodCommon()의 경우 양쪽 인터페이스에 다 있지만 같은 메서드이므로 구현은 하나만 하면 됩니다. package poly.diamond; public class DiamondMain { public static void main(String[] args) { InterfaceA a = new Child(); a.methodA(); a.methodCommon(); InterfaceB b = new Child(); b.methodB(); b.methodCommon(); } } 실행 결과 Child.methodA Child.methodCommon Child.methodB Child.methodCommon a.methodCommon()을 호출하면 먼저 x001 Child 인스턴스를 찾는다. 변수 a가 InterfaceA 타입이므로 해당 타입에서 methodCommon()을 찾습니다. methodCommon()은 하위 타입인 Child에서 오버라이딩 되어 있습니다. 따라서 Child의 methodCommon()이 호출됩니다. b.methodCommon()을 호출하면 먼저 x001 Child 인스턴스를 찾습니다. 변수 b가 InterfaceB 타입으로 해당 타입에서 methodCommon()을 찾습니다. methodCommon()은 하위 타입인 Child에서 오버라이팅 되어 있습니다. 따라서 Child의 methodCommon()이 호출됩니다.
Archive
· 2024-03-22
☕️[Java] 인터페이스
인터페이스 “자바는 순수 추상 클래스를 더 편리하게 사용할 수 있는 인터페이스라는 기능을 제공합니다.” 순수 추상 클래스 public abstract class AbstractAnimal { public abstract void sound(); public abstract void move(); } “인터페이스는 class 가 아니라 interface 키워드를 사용하면 됩니다.” 인터페이스 public interface InterfaceAnimal { public abstract void sound(); public abstract void move(); } 인터페이스 - public abstract 키워드 생략 가능 public interface InterfaceAnimal { void sound(); void move(); } 순수 추상 클래스는 다음과 같은 특징을 갖습니다. 인스턴스를 생성할 수 없습니다. 상속시 모든 메서드를 오버라이딩 해야 합니다. 주로 다형성을 위해 사용됩니다. 인터페이스는 앞서 설명한 순수 추상 클래스와 같습니다. 여기에 약간의 편의 기능이 추가 됩니다. 인터페이스 메서드는 모두 public, abstract입니다. 메서드에 public abstract를 생략할 수 있습니다.(참고로 생략이 권장됩니다.) 인터페이스는 다중 구현(다중 상속)을 지원합니다. 인터페이스와 멤버 변수 public interface InterfaceAnimal { public static final int MY_PI = 3.14; } 인터페이스에서 멤버 변수는 public, static, final이 모두 포함되었다고 간주됩니다. final은 변수의 값을 한 번 설정하면 수정할 수 없다는 뜻입니다. 자바에서 static final을 사용해 정적이면서 고칠 수 없는 변수를 상수라 하고, 관례상 상수는 대문자에 언더스코어(_)로 구분합니다. 해당 키워드는 다음과 같이 생략할 수 있습니다.(생략이 권장됩니다.) public interface InterfaceAnimal { int MY_PI = 3.14; } 예제 5 클래스 상속 관계는 UML에서 실선을 사용하지만, 인터페이스 구현(상속)관계는 UML에서 점선을 사용합니다. package poly.ex5; public class InterfaceMain { public static void main(String[] args) { // 인터페이스 생성 불가 // InterfaceAnimal interfaceAnimal = new InterfaceAnimal(); Cat cat = new Cat(); Dog dog = new Dog(); Caw caw = new Caw(); soundAnimal(cat); soundAnimal(dog); soundAnimal(caw); } // 변하지 않는 부분 private static void soundAnimal(InterfaceAnimal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } } 실행 결과 동물 소리 테스트 시작 야옹 동물 소리 테스트 종료 동물 소리 테스트 시작 멍멍 동물 소리 테스트 종료 동물 소리 테스트 시작 음메 동물 소리 테스트 종료 앞서 설명한 순수 추상 클래스 예제와 거의 유사합니다. 순수 추상 클래스가 인터페이스가 되었을 뿐입니다. 클래스, 추상 클래스, 인터페이스는 모두 똑같습니다. 클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조상 모두 똑같습니다. 모두 자바에서는 .class로 로 다루어집니다. 인터페이스를 작성할 때도 .java에 인터페이스를 정의합니다. 인터페이스는 순수 추상 클래스와 비슷하다고 생각하면 됩니다. 상속 vs 구현 부모 클래스의 기능을 자식 클래스가 상속 받을 때, 클래스는 상속 받는다고 표현하지만, 부모 인터페이스의 기능을 자식이 상속 받을 때는 인터페이스를 구현한다고 표현합니다. 이렇게 서로 다르게 표현하는 이유를 알아봅시다. 상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적입니다. 하지만 인터페이스는 모든 메서드가 추상 메서드입니다. 따라서 물려 받을 수 있는 기능이 없고, 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 합니다. 따라서 구현한다고 표현합니다. 인터페이스는 메서드 이름만 있는 설계도이고, 이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현해야 합니다. 따라서 인터페이스의 경우 상속이 아니라 해당 인터페이스를 구현한다고 표현합니다. 상속과 구현은 사람이 표현하는 단어만 다를 뿐이지 자바 입장에서는 똑같습니다. 일반 상속 구조와 동일하게 작동합니다. 인터페이스를 사용해야 하는 이유. 모든 메서드가 추상 메서드인 경우 순수 추상 클래스로 만들어도 되고, 인터페이스를 만들어도 됩니다. 그런데 왜 인터페이스를 사용해야 할까요? 단순히 편리하다는 이유를 넘어서 다음과 같은 이유가 있습니다. 제약 : 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현하라는 규약(제약)을 주는 것입니다. USB 인터페이스를 생각해봅시다. USB 인터페이스에 맞추어 키보드, 마우스를 개발하고 연결해야 합니다. 그렇지 않으면 작동하지 않습니다. 인터페이스의 규약(제약)은 반드시 구현해야 하는 것입니다. 그런데 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있습니다. 이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 순수 추상 클래스가 아니게 됩니다. 인터페이스는 모든 메서드가 추상 메서드입니다. 따라서 이런 문제를 원천 차단할 수 있습니다. 다중 구현 : 자바에서 클래스 상속은 부모를 하나만 지정할 수 있습니다. 반면에 인터페이스는 부모를 여러명 두는 다중 구현(다중 상속)이 가능합니다. 좋은 프로그램은 제약이 있는 프로그램입니다. 참고 자바 8에 등장한 default 메서드를 사용하면 인터페이스도 메서드를 구현할 수 있습니다. 하지만 이것은 예외적으로 아주 특별한 경우에만 사용해야 합니다. 자바 9에서 등장한 인터페이스의 private 메서드도 마찬가지입니다. 지금 학습 단계에서는 이 부분들을 고려하지 않는 것이 좋습니다. 이 부분은 추후에 따로 학습하고 정리할 것 입니다.
Archive
· 2024-03-21
☕️[Java] 추상 클래스 2
추상 클래스 2 순수 추상 클래스: 모든 메서드가 추상 메서드인 추상 클래스 앞서 만든 예제에서 move()도 추상 메서드로 만들어야 한다고 가정해봅시다. 이 경우 AbstractAnimal 클래스의 모든 메서드가 추상 메서드가 됩니다. 이런 클래스를 “순수 추상 클래스” 라 합니다. move()가 추상 메서드가 되었으니 자식들은 AbstractAnimal의 모든 기능을 오버라이딩 해야 합니다. 예제 4 package poly.ex4; public class AbstractMain { public static void main(String[] args) { // 추상클래스 생성 불가 //AbstractAnimal animal = new AbstractAnimal(); Dog dog = new Dog(); Cat cat = new Cat(); Caw caw = new Caw(); soundAnimal(dog); soundAnimal(cat); soundAnimal(caw); moveAnimal(dog); moveAnimal(cat); moveAnimal(caw); } // 변하지 않는 부분 private static void soundAnimal(AbstractAnimal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } // 변하지 않는 부분 private static void moveAnimal(AbstractAnimal animal) { System.out.println("동물 이동 테스트 시작"); animal.move(); System.out.println("동물 이동 테스트 종료"); } } 실행 결과 동물 소리 테스트 시작 멍멍 동물 소리 테스트 종료 동물 소리 테스트 시작 야옹 동물 소리 테스트 종료 동물 소리 테스트 시작 음매 동물 소리 테스트 종료 동물 이동 테스트 시작 댕댕이 이동 동물 이동 테스트 종료 동물 이동 테스트 시작 냥냥이 이동 동물 이동 테스트 종료 동물 이동 테스트 시작 소 이동 동물 이동 테스트 종료 순수 추상 클래스 모든 메서드가 추상 메서드인 순수 추상 클래스는 코드를 실행할 바디 부분이 전혀 없습니다. public abstract class AbstractAnimal { public abstract void sound(); public abstract void move(); } 이러한 순수 추상 클래스는 실행 로직을 전혀 가지고 있지 않습니다. 단지 다형성을 위한 부모 타입으로써 껍데기 역할만 제공할 뿐입니다. 순수 추상 클래스는 다음과 같은 특징을 가집니다. 인스턴스를 생성할 수 없습니다. 상속시 자식은 모든 메서드를 오버라이딩 해야 합니다. 주로 다형성을 위해 사용됩니다. 상속하는 클래스는 모든 메서드를 구현해야 합니다. “상속시 자식은 모든 메서드를 오버라이딩 해야 합니다.”라는 특징은 상속 받는 클래스 입장에서 보면 부모의 모든 메서드를 구현해야 하는 것 입니다. 이런 특징을 잘 생각해보면 순수 추상 클래스는 마치 어떤 규격을 지켜서 구현해야 하는 것 처럼 느껴집니다. AbstractAnimal의 경우 sound(), move() 라는 규격에 맞추어 구현을 해야 합니다. 이것은 우리가 일반적으로 이야기하는 인터페이스와 같이 느껴집니다. 예를 들어서 USB 인터페이스를 생각해봅시다. USB 인터페이스는 분명한 규격이 있습니다. 이 규격에 맞추어 제품을 개발해야 연결이 됩니다. 순수 추상 클래스가 USB 인터페이스 규격이라고 한다면 USB 인터페이스에 맞추어 마우스, 키보드 같은 연결 장치들을 구현할 수 있습니다. 이런 순수 추상 클래스의 개념은 프로그래밍에서 매우 자주 사용됩니다. 자바는 순수 추상 클래스를 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공합니다.
Archive
· 2024-03-20
☕️[Java] 추상 클래스 1
추상 클래스 1 추상 클래스 동물(Animal)과 같이 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스를 추상 클래스라 합니다. 추상 클래스는 이름 그대로 추상적인 개념을 제공하는 클래스입니다. 따라서 실체인 인스턴스가 존재하지 않습니다. 대신에 상속을 목적으로 사용되고, 부모 클래스 역할을 담당합니다. abstract class AbstractAnimal {...} 추상 클래스는 클래스를 선언할 때 앞에 “추상”이라는 의미의 abstract 키워드를 붙여주면 됩니다. 추상 클래스는 기존 클래스와 완전히 같습니다. 다만 new AbstractAnimal()와 같이 직접 인스턴스를 생성하지 못하는 제약이 추가된 것입니다. 추상 메서드 부모 클래스를 상속 받은 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의할 수 있습니다. 이것을 추상 메서드라 합니다. 추상 메서드는 이름 그대로 추상적인 개념을 제공하는 메서드입니다. 따라서 실체가 존재하지 않고, 메서드 바디가 없습니다. public abstract void sound(); 추상 메서드는 선언할 때 메서드 앞에 추상이라는 의미의 abstract 키워드를 붙여주면 됩니다. 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다. 추상 메서드는 메서드 바디가 없습니다. 따라서 작동하지 않는 메서드를 가진 불완전한 클래스로 볼 수 있습니다. 따라서 직접 생성하지 못하도록 추상 클래스로 선언해야 합니다. 추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩 해서 사용해야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다. 추상 메서드는 자식 클래스가 반드시 오버라이딩 해야 하기 때문에 메서드 바디 부분이 없습니다. 바디 부분을 만들면 컴파일 오류가 발생합니다. 오버라이딩 하지 않으면 자식도 추상 클래스가 되어야 합니다. 추상 메서드는 기존 메서드와 완전히 같습니다. 다만 메서드 바디가 없고, 자식 클래스가 해당 메서드를 반드시 오버라이딩 해야 한다는 제약이 추가된 것입니다. 이제 추상 클래스와 추상 메서드를 사용해서 예제를 만들어봅시다. 예제 3 package poly.ex3; public abstract class AbstractAnimal { public abstract void sound(); public void move() { System.out.println("동물이 움직입니다."); } } AbstractAnimal은 abstract가 붙은 추상 클래스입니다. 이 클래스는 직접 인스턴스를 생성할 수 없습니다. sound()는 abstract가 붙은 추상 메서드입니다. 이 메서드는 자식이 반드시 오버라이딩 해야 합니다. “이 클래스는 move()라는 메서드를 가지고 있는데, 이 메서드는 추상 메서드가 아닙니다.” 따라서 자식 클래스가 오버라이딩 하지 않아도 됩니다. 참고로 추상 클래스라고 AbstractAnimal 처럼 클래스 이름 앞에 꼭 Abstract를 써야하는 것은 아닙니다. 그냥 Animal 이라는 클래스 이름으로도 충분합니다. 여기에서는 예제 코드를 다른 예제 코드와 구분해서 설명하기 위해 앞에 Abstract를 붙였습니다. package poly.ex3; public abstract class AbstractAnimal { public abstract void sound(); public void move() { System.out.println("동물이 움직입니다."); } } package poly.ex3; public class Cat extends AbstractAnimal { @Override public void sound() { System.out.println("야옹"); } } package poly.ex3; public class Caw extends AbstractAnimal { @Override public void sound() { System.out.println("음매"); } } package poly.ex3; public class Dog extends AbstractAnimal { @Override public void sound() { System.out.println("멍멍"); } } package poly.ex3; public class AbstractMain { public static void main(String[] args) { // 추상클래스 생성 불가 //AbstractAnimal animal = new AbstractAnimal(); Dog dog = new Dog(); Cat cat = new Cat(); Caw caw = new Caw(); cat.sound(); cat.move(); soundAnimal(dog); soundAnimal(cat); soundAnimal(caw); } // 변하지 않는 부분 private static void soundAnimal(AbstractAnimal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } } 실행 결과 야옹 동물이 움직입니다. 동물 소리 테스트 시작 멍멍 동물 소리 테스트 종료 동물 소리 테스트 시작 야옹 동물 소리 테스트 종료 동물 소리 테스트 시작 음매 동물 소리 테스트 종료 추상 클래스는 생성이 불가능합니다. 다음 코드의 주석을 풀고 실행하면 컴파일 오류가 발생합니다. // 추상클래스 생성 불가 //AbstractAnimal animal = new AbstractAnimal(); 컴파일 오류 - 인스턴스 생성 java: poly.ex3.AbstractAnimal is abstract; cannot be instantiated AbstractAnimal가 추상이어서 인스턴스 생성이 불가능하다는 뜻입니다. 추상 메서드는 반드시 오버라이딩 해야 합니다. 만약 자식에서 오버라이딩 메서드를 만들지 않으면 다음과 같이 컴파일 오류가 발생합니다. Dog의 sound() 메서드를 잠시 주석처리해봅시다. package poly.ex3; public class Dog extends AbstractAnimal { /* @Override public void sound() { System.out.println("멍멍"); } */ } 컴파일 오류 - 오버라이딩 X java: poly.ex3.Dog is not abstract and does not override abstract method sound() in poly.ex3.AbstractAnimal Dog는 추상클래스가 아닌데 sound()가 오버라이딩 되지 않았다는 뜻 입니다. 지금까지 설명한 제약을 제외하고 나머지는 모두 일반적인 클래스와 동일합니다. 추상 클래스는 제약이 추가된 클래스일 뿐입니다. 메모리 구조, 실행 결과 모두 동일합니다. 정리 추상 클래스 덕분에 실수로 Animal 인스턴스를 생성할 문제를 근본적으로 방지해줍니다. 추상 메서드 덕분에 동물의 자식 클래스를 만들때 실수로 sound() 오버라이딩 하지 않을 문제를 근본적으로 방지해줍니다.
Archive
· 2024-03-20
☕️[Java] 다형성 활용3
다형성 활용 3 이번에는 배열과 for문을 사용해서 중복을 제거해보겠습니다. package poly.ex2; public class AnimalPolyMain2 { public static void main(String[] args) { Dog dog = new Dog(); Cat cat = new Cat(); Caw caw = new Caw(); Animal[] animalArr = {dog, cat, caw}; // 변하지 않는 부분 for (Animal animal : animalArr) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } } } 실행 결과 동물 소리 테스트 시작 멍멍 동물 소리 테스트 종료 동물 소리 테스트 시작 야옹 동물 소리 테스트 종료 동물 소리 테스트 시작 음메 동물 소리 테스트 종료 배열은 같은 타입의 데이터를 나열할 수 있습니다. Dog, Cat, Caw는 모두 Animal의 자식이므로 Animal 타입입니다. Animal 타입의 배열을 만들고 다형적 참조를 사용하면 됩니다. // 둘은 같은 코드입니다. Animal[] animalArr = new Anima[]{dog, cat, caw}; Animal[] animalArr = {dog, cat, caw}; 다형적 참조 덕분에 Dog, Cat, Caw의 부모 타입인 Animal 타입으로 배열을 만들고, 각각을 배열에 포함했습니다. 이제 배열을 for문을 사용해서 반복하면 됩니다. // 변하지 않는 부분 for (Animal animal: animalArr) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } animal.sound()를 호출하지만 배열에는 Dog, Cat, Caw의 인스턴스가 들어있습니다. 메서드 오버라이딩에 의해 각 인스턴스의 오버라이딩 된 sound() 메서드가 호출됩니다. 조금 더 개선 package poly.ex2; public class AnimalPolyMain3 { public static void main(String[] args) { Animal[] animalArr = {new Dog(), new Cat(), new Cat()}; for (Animal animal : animalArr) { soundAnimal(animal); } } // 변하지 않는 부분 private static void soundAnimal(Animal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } } Animal[] animalArr를 통해서 배열을 사용합니다. soundAnimal(Animal animal) 하나의 동물을 받아서 로직을 처리합니다. 새로운 동물이 추가되어도 soundAnimal(...) 메서드는 코드 변경 없이 유지할 수 있습니다. 이렇게 할 수 있는 이유는 이 메서드는 Dog, Cat, Caw 같은 구체적인 클래스를 참조하는 것이 아니라 Animal이라는 추상적인 부모를 참조하기 때문입니다. 따라서 Animal을 상속 받은 새로운 동물이 추가되어도 이 메서드의 코드는 병경 없이 유지할 수 있습니다. 여기서 잘 보면 새로운 동물이 추가되었을 때 코드가 변하는 부분과 변하지 않는 부분이 있습니다. main()은 코드가 변하는 부분입니다. 새로운 동물을 생성하고 필요한 메서드를 호출합니다. soundAnimal(...)는 코드가 변하지 않는 부분입니다. “새로운 기능이 추가되었을 때 변하는 부분을 최소화 하는 것이 잘 작성된 코드입니다.” 이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋습니다. 남은 문제 지금까지 설명한 코드에는 사실 2가지 문제가 있습니다. Animal 클래스를 생성할 수 있는 문제 Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성. Animal 클래스를 생성할 수 있는 문제 Animal 클래스는 동물이라는 클래스입니다. 이 클래스를 다음과 같이 직접 생성해서 사용할 일이 있을까요? Animal animal = new Animal(); 개, 고양이, 소가 실제 존재하는 것은 당연하지만, 동물이라는 추상적인 개념이 실제로 존재하는 것은 이상합니다. 이 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없습니다. 하지만 Animal도 클래스이기 때문에 인스턴스를 생성하고 사용하는데 아무런 제약이 없습니다. 누군가 실수로 new Animal()을 사용해서 Animal의 인스턴스를 생성할 수 있다는 것 입니다. 이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지는 않습니다. Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성. 예를들어서 Animal을 상속 받은 Pig 클래스를 만든다고 가정해봅시다. 우리가 기대하는 것은 Pig 클래스가 sound() 메서드를 오버라이딩 해서 “꿀꿀”이라는 소리가 나도록 하는 것입니다. 그런데 개발자가 실수로 sound() 메서드를 오버라이딩 하는 것을 빠트릴 수 있습니다. 이렇게 되면 부모의 기능을 상속받습니다. 따라서 코드상 아무런 문제가 발생하지 않습니다. 물론 프로그램을 실행하면 기대와 다르게 “꿀꿀”이 아니라 부모 클래스에 있는 Animal.sound()가 호출될 것입니다. 좋은 프로그램은 제약이 있는 프로그램입니다. 추상 클래스와 추상 메서드를 사용하면 이런 문제를 한번에 해결할 수 있습니다.
Archive
· 2024-03-19
☕️[Java] 다형성 활용2
다형성 활용2 이번에는 앞서 설명한 예제를 다형성을 사용하여 변경해보겠습니다. 다형성을 사용하기 위해 여기서는 상속 관계를 사용합니다. Animal(동물) 이라는 부모 클래스를 만들고 sound() 메서드를 정의합니다. 이 메서드는 자식 클래스에서 오버라이딩 할 목적으로 만들었습니다. Dog, Cat, Caw는 Animal 클래스를 상속받았습니다. 그리고 각각 부모의 sound() 메서드를 오버라이딩 합니다. 기존 코드를 유지하기 위해 새로운 패키지를 만들고 새로 코드를 작성해보겠습니다. “주의! 패키지 이름에 주의합시다 import를 사용해서 다른 패키지에 있는 같은 이름의 클래스를 사용하면 안됩니다.” package poly.ex2; public class Dog extends Animal { @Override public void sound() { System.out.println("멍멍"); } } package poly.ex2; public class Cat extends Animal { @Override public void sound() { System.out.println("야옹"); } } package poly.ex2; public class Caw extends Animal { @Override public void sound() { System.out.println("음메"); } } package poly.ex2; public class Animal { public void sound() { System.out.println("동물 울음 소리"); } } package poly.ex2; public class AnimalPolyMain1 { public static void main(String[] args) { Dog dog = new Dog(); Cat cat = new Cat(); Caw caw = new Caw(); soundAnimal(dog); soundAnimal(cat); soundAnimal(caw); } private static void soundAnimal(Animal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } } 실행 결과 동물 소리 테스트 시작 멍멍 동물 소리 테스트 종료 동물 소리 테스트 시작 야옹 동물 소리 테스트 종료 동물 소리 테스트 시작 음메 동물 소리 테스트 종료 실행 결과는 기존 코드와 같습니다. 코드를 분석해봅시다. soundAnimal(dog)을 호출하면 soundAnimal(Animal animla)에 Dog 인스턴스가 전달됩니다. Animal animal = dog로 이해하면 됩니다. 부모는 자식을 담을 수 있습니다. Animal은 Dog의 부모입니다. 메서드 안에서 animal.sound() 메서드를 호출합니다. animal 변수의 타입은 Animal이므로 Dog 인스턴스에 있는 Animal 클래스 부분을 찾아서 sound() 메서드를 실행합니다. 그런데 하위 클래스인 Dog에서 sound() 메서드를 오버라이딩 했습니다. 따라서 오버라이딩한 메서드가 우선권을 가집니다. Dog 클래스에 있는 sound() 메서드가 호출되므로 “멍멍”이 출력됩니다. 이 코드의 핵심은 Animal animal 부분입니다. 다형적 참조 덕분에 animal 변수는 자식인 Dog, Cat, Caw의 인스턴스를 참조할 수 있습니다.(부모는 자식을 담을 수 있습니다.) 메서드 오버라이딩 덕분에 animal.sound()를 호출해도 Dog.sound(), Cat.sound(), Caw.sound()와 같이 각 인스턴스의 메서드를 호출할 수 있습니다. 만약 자바에 메서드 오버라이딩이 없었다면 모두 Animal의 sound()가 호출되었을 것입니다. 다형성 덕분에 이후에 새로운 동물을 추가해도 다음 코드를 그대로 재사용 할 수 있습니다. 물론 다형성을 사용하기 위해 새로운 동물은 Animal을 상속 받아야합니다. private static void soundAnimal(Animal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); }
Archive
· 2024-03-18
☕️[Java] 다형성 활용1
다형성 활용1 지금까지 학습한 다형성을 왜 사용하는지, 그 장점을 알아보기 위해 우선 다형성을 사용하지 않고 프로그램을 개발한 다음에 다형성을 사용하도록 코드를 변경해보겠습니다. 아주 단순하고 전통적인 동물 소리 문제로 접근해보겠습니다. 개, 고양이, 소의 울음 소리를 테스트하는 프로그램을 작성해봅시다. 먼저 다형성을 사용하지 않고 코드를 작성해봅시다. package poly.ex1; public class Dog { public void sound() { System.out.println("멍멍"); } } package poly.ex1; public class Cat { public void sound() { System.out.println("야옹"); } } package poly.ex1; public class Caw { public void sound() { System.out.println("음메"); } } package poly.ex1; public class AnimalSoundMain { public static void main(String[] args) { Dog dog = new Dog(); Cat cat = new Cat(); Caw caw = new Caw(); System.out.println("동물 소리 테스트 시작"); dog.sound(); System.out.println("동물 소리 테스트 종료"); System.out.println("동물 소리 테스트 시작"); cat.sound(); System.out.println("동물 소리 테스트 종료"); System.out.println("동물 소리 테스트 시작"); caw.sound(); System.out.println("동물 소리 테스트 종료"); } } 실행 결과 동물 소리 테스트 시작 멍멍 동물 소리 테스트 종료 동물 소리 테스트 시작 야옹 동물 소리 테스트 종료 동물 소리 테스트 시작 음메 동물 소리 테스트 종료 단순히 개, 고양이, 소 동물들의 울음 소리를 출력하는 프로그램입니다. 만약 여기서 새로운 동물이 추가되면 어떻게 될까요? 만약 기존 코드에 소가 없다고 가정해봅시다. 소가 추가된다고 가정하면 Caw 클래스를 만들고 다음 코드도 추가해야 합니다. // Caw를 생성하는 코드 Caw caw = new Caw(); // Caw를 사용하는 코드 System.out.println("동물 소리 테스트 시작"); caw.sound(); System.out.println("동물 소리 테스트 종료"); Caw를 생성하는 부분은 당연히 필요하니 크게 상관이 없지만, Dog, Cat, Caw를 사용해서 출력하는 부분은 계속 중복이 증가합니다. 중복 코드 System.out.println("동물 소리 테스트 시작"); dog.sound(); System.out.println("동물 소리 테스트 종료"); System.out.println("동물 소리 테스트 시작"); cat.sound(); System.out.println("동물 소리 테스트 종료"); System.out.println("동물 소리 테스트 시작"); caw.sound(); System.out.println("동물 소리 테스트 종료"); 중복을 제거하기 위해서는 메서드를 사용하거나, 또는 배열과 for문을 사용하면 됩니다. 그런데 Dog, Cat, Caw는 서로 완전히 다른 클래스입니다. 중복 제거 시도 메서드로 중복 제거 시도 메서드를 사용하면 다음과 같이 매개변수의 클래스를 Caw, Dog, Cat 중에 하나로 정해야 합니다. private static void soundCaw(Caw caw) { System.out.println("동물 소리 테스트 시작"); caw.sound(); System.out.println("동물 소리 테스트 종료"); } 따라서 이 메서드는 Caw 전용 메서드가 되고 Dog, Cat은 인수로 사용할 수 없습니다. Dog, Cat, Caw의 타입(클래스)이 서로 다르기 때문에 soundCaw 메서드를 함께 사용하는 것은 불가능합니다. 배열과 for문을 통한 중복 제거 시도 Caw[] cawArr = { cat, dog, caw }; // 컴파일 오류 발생! System.out.println("동물 소리 테스트 시작"); for (Caw caw : cawArr) { cawArr.sound() } System.out.println("동물 소리 테스트 종료"); 배열과 for문 사용해서 중복을 제거하려고 해도 배열의 타입을 Dog, Cat, Caw 중에 하나로 지정해야 합니다. 같은 Caw들을 배열에 담아서 처리하는 것은 가능하지만 타입이 서로 다른 Dog, Cat, Caw을 하나의 배열에 담는 것은 불가능합니다. 결과적으로 지금 상황에서는 해결방법이 없습니다. 새로운 동물이 추가될 때 마다 더 많은 중복 코드를 작성해야 합니다. 지금까지 설명한 모든 중복 제거 시도가 Dog, Cat, Caw의 타입이 서로 다르기 때문에 불가능합니다. “문제의 핵심은 바로 타입이 다르다는 점” 입니다. 반대로 이야기하면 Dog, Cat, Caw가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있다는 것입니다. 다형성의 핵심은 다형적 참조와 메서드 오버라이딩입니다. 이 둘을 활용하면 Dog, Cat, Caw 가 모두 같은 타입을 사용하고, 각자 자신의 메서드로 호출할 수 있습니다.
Archive
· 2024-03-18
☕️[Java] 다형성과 메서드 오버라이딩
다형성과 메서드 오버라이딩. “다형성을 이루는 또 하나의 중요한 핵심 이론은 바로 오버라이딩입니다.” 메서드 오버라이딩에서 꼭! 기억해야 할 점은 “오버라이딩 된 메서드가 항상 우선권을 가진다” 는 점입니다. 그래서 이름도 기존 기능을 덮어 새로운 기능을 재정의 한다는 뜻의 오버라이딩 입니다. 앞서 메서드 오버라이딩을 학습했지만 지금까지 학습한 메서드 오버라이딩은 반쪽짜리입니다. “메서드 오버라이딩의 진짜 힘은 다형성과 함께 사용할 때 나타납니다.” 다음 코드를 통해 다형성과 메서드 오버라이딩을 알아봅시다. Parent, Child 모두 Value 라는 같은 멤버 변수를 가지고 있습니다. 멤버 변수는 오버라이딩 되지 않습니다. Parent, Child 모두 method()라는 같은 메서드를 가지고 있습니다. Child에서 메서드를 오버라이팅 했습니다. 메서드는 오버라이딩 됩니다. package poly.overriding; public class OverridingMain { public static void main(String[] args) { // 자식 변수가 자식 인스턴스 참조 Child child = new Child(); System.out.println("Child -> Child"); System.out.println("value = " + child.value); child.method(); // 부모 변수가 부모 인스턴스 참조 Parent parent = new Parent(); System.out.println("Parent -> Parent"); System.out.println("value = " + parent.value); parent.method(); // 부모 변수가 자식 인스턴스 참조(다형적 참조) Parent poly = new Child(); System.out.println("Parent -> Child"); System.out.println("value = " + poly.value); // 변수는 오버라이딩 x poly.method(); // 메서드 오버라이딩! } } 실행 결과 Child -> Child value = child Child.method Parent -> Parent value = parent Parent.method Parent -> Child value = parent Child.method 그림을 통해 코드를 분석해봅시다. child 변수는 Child 타입 입니다. 따라서 child.value, child.method()를 호출하면 인스턴스의 Child 타입에서 기능을 찾아서 실행합니다. parent 변수는 Parent 타입 입니다. 따라서 parent.value, parent.method()를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아서 실행합니다. 이 부분이 중요합니다. poly 변수는 Parent 타입 입니다. 따라서 poly.value,poly.method()를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아서 실행합니다. poly.value: Parent 타입에 있는 value 값을 읽습니다. poly.method() : Parent 타입에 있는 method()를 실행하려고 합니다. 그런데 Child.method()가 오버라이딩 되어있습니다. “오버라이딩 된 메서드는 항상 우선권을 가집니다.” 따라서 Parent.method()가 아니라 Child.method()가 실행됩니다. 오버라이딩 된 메서드는 항상 우선권을 가집니다. 오버라이딩은 부모 타입에서 정의한 기능을 자식 타입에서 재정의하는 것입니다. 만약 자식에서도 오버라이딩하고 손자에서도 메서드를 오버라이딩을 하면 손자의 오버라이딩 메서드가 우선권을 가집니다. “더 하위 자식의 오버라이딩 된 메서드가 우선권을 가지는 것입니다.” 지금까지 다형성을 이루는 핵심 이론인 다형적 참조와 메서드 오버라이딩에 대해 학습했습니다. 다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능 메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의
Archive
· 2024-03-17
☕️[Java] instanceof
instanceof 다형성에서 참조형 변수는 이름 그대로 다양한 지식을 대상으로 참조할 수 있습니다. 그런데 참조하는 대상이 다양하기 때문에 어떤 인스턴스를 참조하고 있는지 확인하려면 어떻게 해야할까요? Parent parent1 = new Parent(); Parent parent2 = new Child() 여기서 Parent는 자신과 같은 Parent의 인스턴스도 참조할 수 있고, 자식 타입인 Child의 인스턴스도 참조할 수 있습니다. 이때 parent1, parent2 변수가 참조하는 인스턴스의 타입을 확인하고 싶다면 instanceof 키워드를 사용하면 됩니다. 예제를 봅시다. package poly.basic; public class CastingMain5 { public static void main(String[] args) { Parent parent1 = new Parent(); System.out.println("parent1 호출"); call(parent1); Parent parent2 = new Child(); System.out.println("parent2 호출"); call(parent2); } private static void call(Parent parent) { parent.parentMethod(); if (parent instanceof Child) { System.out.println("Child 인스턴스 맞음"); Child child = (Child) parent; child.childMethod(); } } } 실행 결과 parent1 호출 Parent.parentMethod parent2 호출 Parent.parentMethod Child 인스턴스 맞음 Child.childMethod call(Parent parent) 메서드를 봐봅시다. 이 메서드는 매개변수로 넘어온 parent가 참조하는 타입에 따라서 다른 명령을 수행합니다. 참고로 지금처럼 다운캐스팅을 수행하기 전에는 먼저 instanceof를 사용해서 원하는 타입으로 변경이 가능한지 확인한 다음에 다운캐스팅을 수행하는 것이 안전합니다. 해당 메서드를 처음 호출할 때 parent는 Parent의 인스턴스를 참조합니다. parent instanceof Child // parent는 Parent의 인스턴스 new Parent() instanceof Child // false parent는 Parent의 인스턴스를 참조하므로 false를 반환합니다. 해당 메서드를 다음으로 호출할 때 parent는 Child의 인스턴스를 참조합니다. parent instanceof Child // parent는 Child의 인스턴스 new Child() instanceof Child // true 참고로 instanceof 키워드는 오른쪽 대상의 자식 타입을 왼쪽에서 참조하는 경우에도 true를 반환합니다. parent instanceof Parent // parent는 Child의 인스턴스 new Parent() instanceof Parent // parent가 Parent의 인스턴스를 참조하는 경우: true new Child() instanceof Parent // parent가 Child의 인스턴스를 참조하는 경우: true 쉽게 이야기해서 오른쪽에 있는 타입에 왼쪽에 있는 인스턴스의 타입이 들어갈 수 있는지 대입해보면 됩니다. 대입이 가능하면 true, 불가능하면 false가 됩니다. new Parent() instanceof Parent Parent p = new Parent() // 같은 타입 true new Child() instancof Parent Parent p = new Child() // 부모는 자식을 담을 수 있다. true new Parent() instanceof Child Child c = new Parent() // 자식은 부모를 담을 수 없다. false new Child() instanceof Child Child c = new Child() // 같은 타입 true 자바 16 - Pattern Matching for instanceof 자바 16부터는 instanceof를 사용하면서 동시에 변수를 선언할 수 있습니다. 다음 코드를 참고합시다. package poly.basic; public class CastingMain6 { public static void main(String[] args) { Parent parent1 = new Parent(); System.out.println("parent1 호출"); call(parent1); Parent parent2 = new Child(); System.out.println("parent2 호출"); call(parent2); } private static void call(Parent parent) { parent.parentMethod(); // Child 인스턴스인 경우 childMethod() 실행 if (parent instanceof Child child) { System.out.println("Child 인스턴스 맞음"); child.childMethod(); } } } 실행 결과 parent1 호출 Parent.parentMethod parent2 호출 Parent.parentMethod Child 인스턴스 맞음 Child.childMethod 덕분에 인스턴스가 맞는 경우 직접 다운캐스팅 하는 코드를 생략할 수 있습니다.
Archive
· 2024-03-15
☕️[Java] 캐스팅의 종류
캐스팅의 종류 자식 타입의 기능을 사용하려면 다음과 같이 다운 캐스팅 결과를 변수에 담아두고 이후에 기능을 사용하면 됩니다. Child child = (Child) poly child.childMethod(); 하지만 다운캐스팅 결과를 변수에 담아두는 과정은 번거롭습니다. 이런 과정 없이 일시적으로 다운캐스팅을 해서 인스턴스에 있는 하위 클래스의 기능을 바로 호출할 수 있습니다. 다음 코드를 봐봅시다. 일시적 다운 캐스팅 package poly.basic; public class CastingMain2 { public static void main(String[] args) { // 부모 변수가 자식 인스턴스 참조(다형적 참조) Parent poly = new Child(); // 단 자식의 기능은 호출할 수 없습니다.(컴파일 오류 발생) // poly.childMethod(); // 일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅 ((Child) poly).childMethod(); } } 실행 결과 Child.childMethod ((Child) poly).childMethod() poly는 Parent 타입입니다. 그런데 이 코드를 실행하면 Parent 타입을 임시로 Child로 변경합니다. 그리고 메서드를 호출할 때 Child 타입에서 찾아서 실행합니다. 정확히는 poly가 Child 타입으로 바뀌는 것은 아닙니다. ((Child) poly).childMethod(); // 다운캐스팅을 통해 부모타입을 자식 타입으로 변환 후 기능 호출 ((Child) x001).childMethod(); // 참조값을 읽은 다음 자식 타입으로 다운캐스팅 참고로 캐스팅을 한다고 해서 Parent poly의 타입이 변하는 것은 아닙니다. 해당 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것입니다. 따라서 poly의 타입은 Parent로 그대로 유지됩니다. 이렇게 일시적 다운캐스팅을 사용하면 별도의 변수 없이 인스턴스의 자식 타입의 기능을 사용할 수 있습니다. 업캐스팅 다운캐스팅과 반대로 현재 타입을 부모 타입으로 변경하는 것을 업캐스팅이라고 합니다. 다음 코드를 봅시다. package poly.basic; public class CastingMain3 { public static void main(String[] args) { Child child = new Child(); Parent parent1 = (Parent) child; // 업캐스팅은 생략 가능, 생략 권장. Parent parent2 = child; parent1.parentMethod(); parent2.parentMethod(); } } 실행 결과 Parent.parentMethod Parent.parentMethod 다음 코드를 봐봅시다 Parent parent1 = (Parent) child; Child 타입을 Parent 타입에 대입해야 합니다. 따라서 타입을 변환하는 캐스팅이 필요합니다. 그런데 부모 타입으로 변환하는 경우에는 다음과 같이 캐스팅 코드인 (타입)를 생략할 수 있습니다. Parent parent2 = child Parent parent2 = new Child() “업캐스팅은 생략할 수 있습니다. 참고로 업캐스팅은 매우 자주 사용하기 때문에 생략을 권장합니다.” 자바에서 부모는 자식을 담을 수 있습니다. 하지만 그 반대는 안됩니다.(꼭 필요하다면 다운캐스팅을 해야 합니다.) 업캐스팅을 생략해도 되고, 다운캐스팅은 왜 개발자가 직접 명시적으로 캐스팅을 해야할까요?
Archive
· 2024-03-15
☕️[Java] 다운캐스팅과 주의점
다운캐스팅과 주의점. 다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있습니다. 다음 코드를 통해 다운캐스팅에서 발생할 수 있는 문제를 확인해봅시다. package poly.basic; // 다운캐스팅을 자동으로 하지 않는 이유 public class CastingMain4 { public static void main(String[] args) { Parent parent1 = new Child(); Child child1 = (Child) parent1; child1.childMethod(); // 문제 없음 Parent parent2 = new Parent(); Child child2 = (Child) parent2; // 런타임 오류 - ClassCastException child2.childMethod(); // 실행 불가 } } 실행 결과 Child.childMethod Exception in thread "main" java.lang.ClassCastException: class poly.basic.Parent cannot be cast to class poly.basic.Child (poly.basic.Parent and poly.basic.Child are in unnamed module of loader 'app') at poly.basic.CastingMain4.main(CastingMain4.java:12) 실행 결과를 보면 child1.childMethod()는 잘 호출되었지만, child2.childMethod()는 실행되지 못하고, 그 전에 오류가 발생합니다. 예제의 parent1의 경우 다운캐스팅을 해도 문제가 되지 않습니다. 예제의 parent2를 다운캐스팅하면 ClassCastException 이라는 심각한 런타임 오류가 발생합니다. 이 코드를 자세히 알아봅시다. Parent parent2 = new Parent() 먼저 new Parent()로 부모 타입으로 객체를 생성합니다. 따라서 메모리 상에 자식 타입은 전혀 존재하지 않습니다. 생성 결과를 parent2에 담아둡니다. 이 경우 같은 타입이므로 여기서는 문제가 발생하지 않습니다. Child child2 = (Child) parent2 다음으로 parent2를 Child 타입으로 다운캐스팅합니다. 그런데 parent2는 Parent로 생성되었습니다. 따라서 메모리상에 Child 자체가 존재하지 않습니다. Child 자체를 사용할 수 없는 것입니다. 자바에서는 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastExecption이라는 예외를 발생시킵니다. 예외가 발생하면 다음 동작이 실행되지 않고, 프로그램이 종료됩니다. 따라서 child2.childMethod() 코드 자체가 실행되지 않습니다. 업캐스팅이 안전하고 다운캐스팅이 위험한 이유 업캐스팅의 경우 이런 문제가 절대로 발생하지 않습니다. 왜냐하면 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 함께 생성되기 때문입니다! 따라서 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전합니다. 따라서 캐스팅을 생략할 수 있습니다. 반면에 다운캐스팅의 경우 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있습니다. 왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않습니다. 따라서 개발자가 이런 문제를 인지하고 사용해야 한다는 의미로 명시적으로 캐스팅을 해주어야 합니다. 클래스 A, B, C는 상속 관계입니다. new C()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B, C가 모두 생성됩니다. 따라서 C의 부모 타입인 A, B, C 모두 C 인스턴스를 참조할 수 있습니다. “상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않습니다.” A a = new C(): A로 업캐스팅 B b = new C(): B로 업캐스팅 C c = new C(): 자신과 같은 타입 new B()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B가 생성됩니다. 따라서 B의 부모 타입인 A, B 모두 B 인스턴스를 참조 할 수 있습니다. 상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않습니다. 하지만 객체를 생성할 때 하위 자식은 생성되지 않기 때문에 하위로 내려가는 다운캐스팅은 인스턴스 내부에 없는 부분을 선택하는 문제가 발생할 수 있습니다. A a = new B() : A로 업캐스팅 B b = new B() : 자신과 같은 타입 C c = new B() : 하위 타입은 대입할 수 없음, 컴파일 오류 C c = (C) new B() : 하위 타입으로 강제 다운캐스팅, 하지만 B 인스턴스에 C와 관련된 부분이 없으므로 잘못된 캐스팅, ClassCastException 런타임 오류 발생 컴파일 오류 vs 런타임 오류 컴파일 오류는 변수명 오류, 잘못된 클래스 이름 사용등 자바 프로그램을 실행하기 전에 발생하는 오류입니다. 이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류 입니다. 반면에 런타임 오류는 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류입니다. 런타임 오류는 매우 안좋은 오류입니다. 왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문입니다.
Archive
· 2024-03-15
☕️[Java] 다형성(Polymorphism) 시작
다형성(Polymorphism) 시작. “객체지향 프로그래밍의 대표적인 특징으로는 캡슐화, 상속, 다형성이 있습니다.” 그 중에서 “다형성” 은 객체지향 프로그래밍의 꽃이라고 불립니다. 앞서 학습한 캡슐화나 상속은 직관적으로 이해하기 쉽습니다. 반면에 “다형성은 제대로 이해하기도 어렵고, 잘 활용하기는 더 어렵습니다.” 하지만 좋은 개발자기 되기 위해서는 “다형성에 대한 이해가 필수” 입니다. “다형성(Polymorphism)” 은 이름 그대로 “다양한 형태”, “여러 형태” 를 뜻합니다. 프로그래밍에서 “다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻합니다” 보통 하나의 객체는 하나의 타입으로 고정되어 있습니다. 그런데 “다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 뜻입니다.” 본격적인 다형성 학습. 다형성을 이해하기 위해서는 크게 “2가지 핵심 이론” 을 알아야 합니다. 다형적 참조 메서드 오버라이딩 먼저 “다형적 참조”라 불리는 개념에 대해 알아봅시다. 다형적 참조. 다형적 참조를 이해하기 위해 다음과 같은 간단한 상속 관계를 코드로 만들어보겠습니다. 부모와 자식이 있고, 각각 다른 메서드를 가집니다. Parent package poly.basic; public class Parent { public void parentMethod() { System.out.println("Parent.parentMethod"); } } Child package poly.basic; public class Child extends Parent { public void childMethod() { System.out.println("Child.childMethod"); } } PolyMain /* * 다형적 참조: 부모는 자식을 품을 수 있다. */ package poly.basic; public class PolyMain { public static void main(String[] args) { // 부모 변수가 부모 인스턴스 참조 System.out.println("Parent -> Parent"); Parent parent = new Parent(); parent.parentMethod(); // 자식 변수가 자식 인스턴스 참조 System.out.println("Child -> Child"); Child child = new Child(); child.parentMethod(); child.childMethod(); // 부모 변수가 자신 인스턴스 참조(다형적 참조) System.out.println("Parent -> Child"); Parent poly = new Child(); poly.parentMethod(); } } Parent -> Parent Parent.parentMethod Child -> Child Parent.parentMethod Child.childMethod Parent -> Child Parent.parentMethod 그림을 통해 코드를 하나씩 분석해봅시다. 부모 타입의 변수가 부모 인스턴스 참조 부모 타입의 변수가 부모 인스턴스를 참조합니다. Parent parent = new Parent() Parent 인스턴스를 만들었습니다. 이 경우 부모 타입인 Parent를 생성했기 때문에 메모리 상에 Parent만 생성됩니다.(자식은 생성되지 않습니다.) 생성된 참조값을 Parent 타입의 변수인 parent에 담아둡빈다. parent.parentMethod()를 호출하면 인스턴스의 Parent 클래스에 있는 parentMethod()가 호출됩니다. 자식 타입의 변수가 자식 인스턴스 참조 자식 타입의 변수가 자식 인스턴스를 참조합니다. Child child = new Child() Child 인스턴스를 만들었습니다. 이 경우 자식 타입인 Child를 생성했기 때문에 메모리 상에 Child와 Parent가 모두 생성 됩니다. 생성된 참조값을 Child 타입의 변수인 child에 답아둡니다. child.childMethod()를 호출하면 인스턴스의 Child 클래스에 있는 childMethod()가 호출됩니다. 여기까지는 지금까지 배운 내용이므로 이해하는데 어려움은 없을 것입니다. 이제부터가 중요합니다. 부모 타입의 변수가 자식 인스턴스를 참조합니다. Parent poly = new Child() Child 인스턴스를 만들었습니다. 이 경우 자식 타입인 Child를 생성했기 때문에 메모리 상에 Child와 Parent가 모두 생성됩니다. 생성된 참조값을 Parent 타입의 변수인 poly에 담아둡니다. 부모는 자식을 담을 수 있습니다. 부모 타입은 자식 타입을 담을 수 있습니다. Parent poly는 부모 타입입니다. new Child()를 통해 생성된 결과는 Child 타입입니다. 자바에서 부모 타입은 자식 타입을 담을 수 있습니다. Parent poly = new Child() : 성공 반대로 자식 타입은 부모 타입을 담을 수 없습니다. Child child1 = new Parent() : 컴파일 오류 발생 다형적 참조 지금까지 학습한 내용을 떠올려보면 항상 같은 타입에 참조를 대입했습니다. 그래서 보통 한 가지 형태만 참조할 수 있습니다. Parent parent = new Parent() Child child = new Child() 그런데 Parent 타입의 변수는 다음과 같이 자신인 Parent는 물론이고, 자식 타입까지 참조할 수 있습니다. 만약 손자가 있다면 손자도 그 하위 타입도 참조할 수 있습니다. Parent poly = new Parent() Parent poly = new Child() Parent poly = new Grandson() : Child 하위에 손자가 있다면 가능 자바에서 부모 타입은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있습니다. “이것을 바로 다양한 형태를 탐조할수 있다고 해서 다형적 참조하고 합니다.” 다형적 참조와 인스턴스 실행 앞의 그림을 참고합시다. poly.parentMethod()를 호출하면 먼저 참조값을 사용해서 인스턴스를 찾습니다. 그리고 다음으로 인스턴스 안에서 실행할 타입도 찾아야 합니다. poly는 Parent 타입입니다. 따라서 Parent 클래스부터 시작해서 필요한 기능을 찾습니다. 인스턴스의 Parent 클래스에 parentMethod()가 있습니다. 따라서 해당 메스드가 호출됩니다. Parent poly = new Child() 이렇게 자식을 참조한 상황에서 poly가 자식 타입인 Child에 있는 childMethod()를 호출하면 어떻게 될까요? poly.childMethod()를 실행하면 먼저 참조값을 통해 인스턴스를 찾습니다. 그리고 다음으로 인스턴스 안에서 실행할 타입을 찾아야 합니다. 호출자인 poly는 Parent타입입니다. 따라서 Parent 클래스부터 시작해서 필요한 기능을 찾습니다. “그런데 상속 관계는 부코 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈 수는 없습니다.” Parent는 부모 타입이고 상위에 부모가 없습니다. 따라서 childMethod()를 찾을 수 없으므로 컴파일 오류가 발생합니다. 이런경우 childMethod()를 호출하고 싶으면 어떻게 해야할까요? “바로 캐스팅이 필요합니다.” 다형적 참조의 핵심은 부모는 자식을 품을 수 있다는 것입니다. 그런데 이런 “다형적 참조가 왜 필요하지?” 라는 의문이 들 수 있습니다. 이 부분은 다형성의 다른 이론들도 함께 알아야 이해할 수 있습니다. 지금은 우선 다형성의 문법과 이론을 익히는데 집중합시다.
Archive
· 2024-03-14
☕️[Java] 다형성과 캐스팅
다형성과 캐스팅. Parent poly = new Child()와 같이 부모 타입의 변수를 사용하게 되면 poly.childMethod()와 같이 자식 타입에 있는 기능을 호출할 수 없습니다. package poly.basic; public class CastingMain1 { public static void main(String[] args) { // 부모 변수가 자식 인스턴스 참조(다형적 참조) Parent poly = new Child(); // 단 자식의 기능은 호출할 수 없습니다.(컴파일 오류 발생) // poly.childMethod(); // 다운 캐스팅(부모 타입 -> 자식 타입) Child child = (Child) poly; // x001 child.childMethod(); } } 실행 결과 Child.childMethod poly.childMethod()를 호출하면 먼저 참조값을 사용해서 인스턴스를 찾습니다. 인스턴스 안에서 사용할 타입을 찾아야 합니다. poly는 Parent 타입입니다. Parent는 최상위 부모입니다. 상속 관계는 부모로만 찾아서 올라갈 수 있습니다. childMethod는 자식 타입에 있으므로 호출할 수 없습니다. 따라서 컴파일 오류가 발생합니다. 이럴때는 어떻게 하면 될까요? 호출하는 타입을 자식인 Child 타입으로 변경하면 인스턴스의 Child에 있는 childMethod()를 호출할 수 있습니다. 하지만 다음과 같은 문제에 봉착합니다. 부모는 자식을 담을 수 있지만 자식은 부모를 담을 수 없습니다. Parent parent = new Child() : 부모는 자식을 담을 수 있습니다. Parent parent = child // Child child 변수 : 부모는 자식을 담을 수 있습니다. 반면에 다음과 같이 자식은 부모를 담을 수 없습니다. Child child = poly // Parent poly 변수 부모 타입을 사용하는 변수를 자식 타입에 대입하려고 하면 컴파일 오류가 발생합니다. 자식은 부모를 담을 수 없습니다. 이때는 다운캐스팅이라는 기능을 사용해서 부모 타입을 잠깐 자식 타입으로 변경하면 됩니다. 다음 코드를 분석해봅시다. Child child = (Child) poly // Parent poly (타입) 처럼 괄호와 그 사이에 타입을 지정하면 참조 대상을 특정 타입으로 변경할 수 있습니다. 이렇게 특정 타입으로 변경하는 것을 “캐스팅” 이라고 합니다. 오른쪽에 있는 (Child) poly 코드를 먼저 봅시다. poly는 Parent 타입입니다. 이 타입을 (Child)를 사용해서 일시적으로 자식 타입인 Child 타입으로 변경합니다. 그리고 나서 왼쪽에 있는 Child child에 대입합니다. 실행 순서 Child child = (Child) poly // 다운캐스팅을 통해 부모타입을 자식 타입으로 변환한 다음에 대입 시도 Child child = (Child) x001 // 참조값을 읽은 다음 자식 타입으로 지정 Child child = x001 // 최종 결과 참고로 캐스팅을 한다고 해서 Parent poly의 타입이 변하는 것은 아닙니다. 해당 참조값을 꺼내고 참조값이 Child 타입이 되는 것입니다. 따라서 poly의 타입은 Parent로 기존과 같이 유지됩니다. 캐스팅 업캐스팅(upcasting): 부모 타입으로 변경 다운캐스팅(downcasting): 자식 타입으로 변경 캐스팅 용어 “캐스팅”은 영어 단어 “cast”에서 유래되었습니다. “cast”는 금속이나 다른 물질을 녹여서 특정한 형태나 모양으로 만드는 과정을 의미합니다. Child child = (Child) poly 경우 Parent poly라는 부모 타입을 Child라는 자식 타입으로 변경했습니다. 부모 타입을 자식 타입으로 변경하는 것을 “다운캐스팅”이라 합니다. (부모 -> 자식) 반대로 부모 타입으로 변경하는 것은 “업캐스팅”이라 합니다. (자식 -> 부모) 다운캐스팅과 실행 // 다운캐스팅(부모 타입 -> 자식 타입) Child child = (Child) poly; child.childMethod(); 다운캐스팅 덕분에 child.childMethod()를 호출할 수 있게 되었습니다. childMethod()를 호출하기 위해 해당 인스턴스를 찾아간 다음 Child 타입을 찾습니다. Child 타입에는 childMethod()가 있으므로 해당 기능을 호출할 수 있습니다. 앞의 그림을 참고합시다.
Archive
· 2024-03-14
☕️[Java] super - 생성자
super - 생성자. 상속 관계의 인스턴스를 생성하면 결국 메모리 내부에는 자식과 부모 클래스가 각각 만들어집니다. Child를 만들면 부모인 Parent까지 함께 만들어지는 것입니다. 따라서 생성자도 모두 호출되어야 합니다. “상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 합니다.(규칙)” 상속 관계에서 부모의 생성자를 호출할 때는 super(...)를 사용하면 됩니다. 예제를 통해 상속 관계에서 생성자를 어떻게 사용하는지 알아봅시다. ClassA package extends1.super2; public class ClassA { public ClassA() { System.out.println("ClassA 생성자"); } } ClaasA는 최상위 부모 클래스입니다. ClassB package extends1.super2; public class ClassB extends ClassA { public ClassB(int a) { super(); // 기본 생성자 생략 가능. System.out.println("ClassB 생성자 a=" + a); } public ClassB(int a, int b) { super(); // 기본 생성자 생략 가능 System.out.println("ClassB 생성자 a=" + a + " b=" + b); } } ClassB는 ClassA를 상속 받았습니다. 상속을 받으면 생성자의 첫 줄에 super(...)를 사용해서 부모 클래스의 생성자를 호출해야 합니다. 예외로 생성자 첫줄에 this(...)를 사용할 수는 있습니다. 하지만 super(...)는 자식의 생성자 안에서 언젠가는 반드시 호출해야 합니다. 부모 클래스의 생성자가 기본 생성자(파라미터가 없는 생성자)인 경우에는 super()를 생략할 수 있습니다. 상속 관계에서 첫줄에 super(...)를 생략하면 자바는 부모의 기본 생성자를 호출하는 super()를 자동으로 만들어 줍니다. 참고로 기본 생성자를 많이 사용하기 때문에 편의상 이런 기능을 제공합니다. ClassC package extends1.super2; public class ClassC extends ClassB { public ClassC() { super(10, 20); System.out.println("ClassC 생성자"); } } ClassC는 ClassB를 상속 받았습니다. ClassB는 다음 두 생성자가 있습니다. ClassB(int a) ClassB(int a, int b) 생성자는 하나만 호출할 수 있습니다. 두 생성자 중에 하나를 선택하면 됩니다. super(10, 20)를 통해 부모 클래스의 ClassB(int a, int b) 생성자를 선택했습니다. 참고로 ClassC의 부모인 ClassB에는 기본 생성자가 없습니다. 따라서 부모의 기본 생성자를 호출하는 super()를 사용하거나 생략할 수 없습니다. Super2Main package extends1.super2; public class Super2Main { public static void main(String[] args) { ClassC classC = new ClassC(); } } 실행 결과 ClassA 생성자 ClassB 생성자 a=10 b=20 ClassC 생성자 실행해보면 ClassA => ClassB => ClassC 순서로 실행됩니다. 생성자의 실행 순서가 결과적으로 최상위 부모부터 실행되어서 하나씩 아래로 내려오는 것 입니다. 따라서 초기화는 최상위 부모부터 이루어 집니다. 왜냐하면 자식 생성자의 첫 줄에서 부모 생성자를 호출해야 하기 때문입니다. 1~3까지의 과정 new ClassC()를 통해 ClassC 인스턴스를 생성합니다. 이때 ClassC()의 생성자가 먼저 호출되는 것이 맞습니다. 하지만 ClassC()의 생성자는 가장 먼저 super(...)를 통해 ClassB(...)의 생성자를 호출합니다. ClassB의 생성자도 부모인 ClassA()의 생성자를 가장 먼저 호출합니다. 4~6까지의 과정 ClassA()의 생성자는 최상위 부모입니다. 생성자 코드를 실행하면서 "ClassA 생성자"를 출력합니다. ClassA() 생성자 호출이 끝나면 ClassA()를 호출한 ClassB(...) 생성자로 제어권이 돌아갑니다. ClassB(...) 생성자가 코드를 실행하면서 "ClassB 생성자 a=10 b=20"를 출력합니다. 생성자 호출이 끝나면 ClassB(...)를 호출한 ClassC()의 생성자로 제어권이 돌아갑니다. ClassC()가 마지막으로 생성자 코드를 실행하면서 "ClassC 생성자"를 출력합니다. 정리 상속 관계의 생성자 호출은 결과적으로 부모에서 자식 순서로 실행됩니다. 따라서 부모의 데이터를 먼저 초기화하고 그 다음에 자식의 데이터를 초기화합니다. 상속 관계에서 자식 클래스의 생성자 첫줄에 반드시 super(...)를 호출해야 합니다. 단 기본 생성자(super())인 경우 생략 할 수 있습니다.
Archive
· 2024-03-13
☕️[Java] super - 부모 참조
super - 부모 참조. “부모와 자식의 필드명이 같거나 메서드가 오버라이딩 되어 있으면, 자식에서 부모의 필드나 메서드를 호출할 수 없습니다.” 이때 super 키워드를 사용하면 부모를 참조할 수 있습니다. super 는 이름 그대로 부모 클래스에 대한 참조를 나타냅니다. 다음 예를 봅시다. 부모의 필드명과 자식의 필드명 둘 다 value로 똑같습니다. 메서드도 hello()로 자식에서 오버라이딩 되어 있습니다. 이 때 자식 클래스에서 부모 클래스의 value와 hello()를 호출하고 싶다면 super 키워드를 사용하면 됩니다. Parent package extends1.super1; public class Parent { public String value = "parent"; public void hello() { System.out.println("Parent.hello"); } } Child package extends1.super1; public class Child extends Parent { public String value = "child"; @Override public void hello() { System.out.println("Child.hello"); } public void call() { System.out.println("this value = " + this.value); // this 생략 가능 System.out.println("super value = " + super.value); this.hello(); // this 생략 가능 super.hello(); } } call() 메서드를 봅시다. this는 자기 자신의 참조를 뜻합니다. this는 생략할 수 있습니다. super는 부모 클래스에 대한 참조를 뜻합니다. 필드 이름과 메서드 이름이 같지만 super를 사용해서 부모 클래스에 있는 기능을 사용할 수 있습니다. Super1Main package extends1.super1; public class Super1Main { public static void main(String[] args) { Child child = new Child(); child.call(); } } 실행 결과 this value = child super value = parent Child.hello Parent.hello 실행 결과를 보면 super를 사용한 경우 부모 클래스의 기능을 사용한 것을 확인할 수 있습니다. super 메모리 그림
Archive
· 2024-03-13
☕️[Java] 클래스와 메서드에 사용되는 final
클래스와 메서드에 사용되는 final. 클래스에 final 상속 끝! final로 선언된 클래스는 확장될 수 없습니다. 다른 클래스가 final로 선언된 클래스를 상속받을 수 없습니다. 예: public final class MyFinalClass {...} 메서드에 final 오버라이딩 끝! final로 선언된 메서드는 오버라이드 될 수 없습니다. 상속받은 서브 클래스에서 이 메서드를 변경할 수 없습니다. 예: public final void myFinalMethod() {...}
Archive
· 2024-03-13
☕️[Java] 상속과 접근 제어
상속과 접근 제어. 상속 관계와 접근 제어에 대해 알아보겠습니다. 참고로 접근 제어를 자세히 설명하기 위해 부모와 자식의 패키지를 따로 분리하였습니다. 접근 제어자를 표현하기 위해 UML 표기법을 일부 사용했습니다. +: public #: protected ~: default -: private 접근 제어자를 잠시 복습해봅시다. 접근 제어자의 종류 private: 모든 외부 호출을 막습니다. default(package-private): 같은 패키지 안에서 호출은 허용합니다. protected: 같은 패키지 안에서 호출은 허용합니다. 패키지가 달라도 상속 관계의 호출은 허용합니다. public: 모든 외부 호출을 허용합니다. 순서대로 private이 가장 많이 차단하고, public이 가장 많이 허용합니다. private -> default -> protected -> public 그림과 같이 다양한 접근 제어자를 사용하도록 코드를 작성해보겠습니다. Parent package extends1.access.parent; public class Parent { public int publicValue; protected int protectedValue; int defaultValue; private int privateValue; public void publicMethod() { System.out.println("Parent.publicMethod"); } protected void protectedMethod() { System.out.println("Parent.protectedMethod"); } void defaultMethod() { System.out.println("Parent.defaultMethod"); } private void privateMethod() { System.out.println("Parent.privateMethod"); } public void printParent() { System.out.println("==Parent 메서드 안=="); System.out.println("publicValue = " + publicValue); System.out.println("protectedValue = " + protectedValue); System.out.println("defaultValue = " + defaultValue); System.out.println("privateValue = " + privateValue); // 부모 메서드 안에서 모두 접근 가능 defaultMethod(); privateMethod(); } } Child package extends1.access.child; import extends1.access.parent.Parent; public class Child extends Parent { public void call() { publicValue = 1; protectedValue = 1; // 상속 관계 or 같은 패키지 // defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류 // privateValue = 1; // 접근 불가, 컴파일 오류 publicMethod(); protectedMethod(); // 상속 관계 or 같은 패키지 // defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류 // privateMethod(); // 접근 불가, 컴파일 오류 printParent(); } } ExtendsAccessMain package extends1.access; import extends1.access.child.Child; public class ExtendsAccessMain { public static void main(String[] args) { Child child = new Child(); child.call(); } } Parent와 Child의 패키지가 다르다는 부분에 유의합시다. 자식 클래스인 Child에서 부모 클래스인 Parent에 얼마나 접근할 수 있는지 확인해봅시다. publicValue = 1;: 부모의 public 필드에 접근합니다. public이므로 접근할 수 있습니다. protectedValue = 1;: 부모의 protected 필드에 접근합니다. 자식과 부모는 다른 패키지이지만, 상속 관계이므로 접근할 수 있습니다. defaultValue = 1;: 부모의 default 필드에 접근합니다. 자식과 부모가 다른 패키지이므로 접근할 수 없습니다. privateValue = 1;: 부모의 private 필드에 접근합니다. private은 모두 외부 접근을 막으므로 자식이라도 호출할 수 없습니다. 메서드의 경우도 앞서 설명한 필드와 동일합니다. Child package extends1.access.child; import extends1.access.parent.Parent; public class Child extends Parent { public void call() { publicValue = 1; protectedValue = 1; // 상속 관계 or 같은 패키지 // defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류 // privateValue = 1; // 접근 불가, 컴파일 오류 publicMethod(); protectedMethod(); // 상속 관계 or 같은 패키지 // defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류 // privateMethod(); // 접근 불가, 컴파일 오류 printParent(); } } 코드를 실행해보면 Child.call() -> Parent.printParent() 순서로 호출합니다. Child는 부모의 public, protexted 필드나 메서드만 접근할 수 있습니다. 반면에 Parent.printParent()의 경우 Parent 안에 있는 메서드이기 때문에 Parent 자신의 모든 필드와 메서드에 얼마든지 접근할 수 있습니다. 접근 제어와 메모리 구조. 본인 타입에 없으면 부모 타입에서 기능을 찾는데, 이때 접근 제어자가 영향을 줍니다. 왜냐하면 객체 내부에서는 자식과 부모가 구분되어 있기 때문입니다. 결국 자식 차입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과 같습니다.
Archive
· 2024-03-10
☕️[Java] 상속과 메서드 오버라이딩
상속과 메서드 오버라이딩. 부모 타입의 기능을 자식에서는 다르게 재정의 하고 싶을 수 있습니다. 예를 들어서 자동차의 경우 Car.move()라는 기능이 있습니다. 이 기능을 사용하면 단순히 “차를 이동합니다.” 라고 출력합니다. 전기차의 경우 보통 더 빠르기 때문에 전기차가 move()를 호출한 경우에는 “전기차를 빠르게 이동합니다.”라고 출력을 변경하고 싶습니다. 이렇게 부모에게서 상속 받은 기능을 자식이 재정의 하는 것을 “메서드 오버라이딩(Overriding)” 이라고 합니다. package extends1.overriding - Car package extends1.overriding; public class Car { public void move() { System.out.println("차를 이동합니다."); } // 추가 public void openDoor() { System.out.println("문을 엽니다."); } } 기존 코드와 같습니다. package extends1.overriding - GasCar package extends1.overriding; public class GasCar extends Car { public void fillUp() { System.out.println("기름을 주유합니다."); } } package extends1.overriding - HydrogenCar package extends1.overriding; public class HydrogenCar extends Car { public void fillHydrogen() { System.out.println("수소를 충전합니다."); } } package extends1.overriding - ElectricCar package extends1.overriding; public class ElectricCar extends Car { @Override public void move() { System.out.println("전기차를 빠르게 이동합니다."); } public void charge() { System.out.println("충전합니다."); } } ElectricCar는 부모인 Car의 move() 기능을 그대로 사용하고 싶지 않습니다. 메서드 이름은 같지만 새로운 기능을 사용하고 싶습니다. 그래서 ElectricCar의 move() 메서드를 새로 만들었습니다. 이렇게 부모의 기능을 자식이 새로 재정의하는 것을 “매서드 오버라이딩” 이라고 합니다. 이제 ElectricCar의 move()를 호출하면 Car의 move()가 아니라 ElectricCar의 move()가 호출됩니다. @Override @이 붙은 부분을 애노테이션(어노테이션, annotattion)이라 합니다. 애노테이션은 주석과 비슷한데, 프로그램이 읽을 수 있는 특별한 주석이라 생각하면 됩니다. 이 애노테이션은 상위 클래스의 매서드를 오버라이드하는 것임을 나타냅니다. 이름 그대로 오버라이딩한 매서드 위에 이 애노테이션을 붙여야 합니다. 컴파일러는 이 애노테이션을 보고 매서드가 정확히 오버라이드 되었는지 확인합니다. 오바라이딩 조건을 만족시키지 않으면 컴파일 에러를 발생시킵니다. 따라서 실수로 오버라이딩을 못하는 경우를 방지해줍니다. 예를 들어서 이 경우에 만약 부모에 move() 메서드가 없다면 컴파일 오류가 발생합니다. 참고로 이 기능은 필수는 아니지만 코드의 명확성을 위해 붙여주는 것이 좋습니다. package extends1.overrding - CarMain package extends1.overriding; public class CarMain { public static void main(String[] args) { ElectricCar electricCar = new ElectricCar(); electricCar.move(); GasCar gasCar = new GasCar(); gasCar.move(); } } 실행 결과 전기차를 빠르게 이동합니다. 차를 이동합니다. 실행 결과를 보면 electricCar.move()를 호출했을 때 오버라이딩한 ElectricCar.move() 메서드가 실행된 것을 확인할 수 있습니다. 오버라이딩과 클래스 Car의 move() 메서드를 ElectricCar에서 오버라이딩 했습니다. 오버라이딩과 메모리 구조 electricCar.move()를 호출합니다. 호출한 electricCar의 타입은 ElectricCar입니다. 따라서 인스턴스 내부의 ElectricCar 타입에서 시작합니다. ElectricCar 타입에 move() 메서드가 있습니다. 해당 메서드를 실행합니다. 이때 실행할 메서드를 이미 찾았으므로 부모 타입을 찾지 않스빈다. 오버로딩(Overloading)과 오버라이딩(Overriding) 메서드 오버로딩: 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것을 메서드 오버로딩(Overloading)이라고 합니다. 오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻입니다. 따라서 같은 이름의 메서드를 여러개 정의했다고 이해하면 됩니다. 메서드 오버라이딩: 메서드 오버라이딩은 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정을 의미합니다. 따라서 상속 관계에서 사용합니다. 부모의 기능을 자식이 다시 정의하는 것입니다. 오버라이딩을 단순히 해석하면 무언가를 넘어서 타는 것을 말합니다. 자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다고 이해하면 됩니다. 오버라이딩을 번역하면 무언가를 다시 정의한다고 해서 재정의라 합니다. 상속 관계에서는 기존 기능을 다시 정의한다고 이해하면 됩니다. 실무에서는 메서드 오버라이딩, 메서드 재정의 둘 다 사용합니다. 메서드 오버라이딩 조건 메서드 오버라이딩은 다음과 같은 까다로운 조건을 가지고 있습니다. 다음 내용은 아직 학습하지 않은 내용들도 있으므로 이해하려고 하기 보다는 참고만 합시다. 지금은 단순히 부모 메서드와 같은 메서드를 오버라이딩 할 수 있다 정도로 이해하면 충분합니다. 메서드 오버라이딩 조건 메서드 이름: 메서드 이름이 같아야 합니다. 메서드 매개변수(파라미터): 매개변수(파라미터) 타입, 순서, 개수가 같아야 합니다. 반환 타입: 반환 타입이 같아야 합니다. 단, 반환 타입이 하위 클래스 타입일 수 있습니다. 접근 제어자: 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안됩니다. 예를 들어, 상위 클래스의 메서드가 protected로 선언되어 있으면 하위 클래스에서 이를 public 또는 protected로 오버라이드 할 수 있지만, private 또는 default로 오버라이드 할 수 없습니다. 예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없습니다. 하지만 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있습니다. static, final, private: 키워드가 붙은 메서드는 오버라이딩 될 수 없습니다. static은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없습니다. 쉽게 이야기해서 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 됩니다. final 메서드는 재정의를 금지합니다. private 메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않습니다. 따라서 오버라이딩 할 수 없습니다. 생성자 오버라이딩: 생성자는 오버라이딩 할 수 없습니다.
Archive
· 2024-03-09
☕️[Java] 상속 - 시작
상속 - 시작. 상속 관계가 왜 필요한지 이해하기 위해 다음 예제 코드를 만들어서 실행해봅시다. 예제 코드 패키지 위치에 주의합시다. package extends1.ex1; public class ElectricCar { public void move() { System.out.println("차를 이동합니다."); } public void charge() { System.out.println("충전합니다."); } } package extends1.ex1; public class GasCar { public void move() { System.out.println("차를 이동합니다."); } public void fillUp() { System.out.println("기름을 주유합니다."); } } package extends1.ex1; public class CarMain { public static void main(String[] args) { ElectricCar electricCar = new ElectricCar(); electricCar.move(); electricCar.charge(); GasCar gasCar = new GasCar(); gasCar.move(); gasCar.fillUp(); } } 실행 결과 차를 이동합니다. 충전합니다. 차를 이동합니다. 기름을 주유합니다. 전기차(ElectricCar)와 가솔린차(GasCar)를 만들었습니다. 전기차는 이동(move()), 충전(charge()) 기능이 있습니다. 가솔린차는 이동(move()), 주유(fillUp()) 기능이 있습니다. 전기차와 가솔린차는 자동차(Car)의 좀 더 구체적인 개념입니다. 반대로 자동차(Car)는 전기차와 가솔린차를 포함하는 추상적인 개념입니다. 그래서인지 잘 보면 둘의 공통 기능이 보입니다. 바로 이동(move())입니다. 전기차든 가솔린차든 주유하는 방식이 다른 것이지 이동하는 것은 똑같습니다. 이런 경우 상속 관계를 사용하는 것이 효과적입니다.
Archive
· 2024-03-08
☕️[Java] 상속관계
상속관계 상속은 객체 지향 프로그래밍의 핵심 요소 중 하나로, 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해줍니다. 이름 그대로 기존 클래스의 속성과 기능을 그대로 물려받는 것입니다. 상속을 하려면 extends 키워드를 사용하면 됩니다. 그리고 extends 대상은 하나만 선택할 수 있습니다. 용어 정리 부모 클래스(슈퍼 클래스): 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스 자식 클래스(서브 클래스): 부모 클래스로부터 필드와 메서드를 상속받는 클래스 ⛔️주의⛔️ 지금부터 코드를 작성할 때 기존 코드를 유지하기 위해, 새로운 패키지에 기존 코드를 옮겨가면서 코드를 작성할 것입니다. 이름이 같기 때문에 패키지 명과 import 사용에 주의해야 합니다. 상속 관계를 사용하도록 코드를 작성해봅시다. 기존 코드를 유지하기 위해 ex2 패키지를 새로 만들겠습니다. package extends1.ex2; public class Car { public void move() { System.out.println("차를 이동합니다."); } } package extends1.ex2; public class ElectricCar extends Car { public void charge() { System.out.println("충전합니다."); } } package extends1.ex2; public class GasCar extends Car { public void fillUp() { System.out.println("기름을 주유합니다."); } } package extends1.ex2; public class CarMain { public static void main(String[] args) { ElectricCar electricCar = new ElectricCar(); electricCar.move(); electricCar.charge(); GasCar gasCar = new GasCar(); gasCar.move(); gasCar.fillUp(); } } 실행 결과 차를 이동합니다. 충전합니다. 차를 이동합니다. 기름을 주유합니다. 실행 결과는 기존 예제와 완전히 동일합니다. 상속 구조도 전기차와 가솔린차가 Car를 상속 받은 덕분에 electricCar.move(),gasCar.move()를 사용할 수 있습니다. 참고로 당연한 이야기지만 상속은 부모의 기능을 자식이 물려 받는 것입니다. 따라서 자식이 부모의 기능을 물려 받아서 사용할 수 있습니다. 반대로 부모 클래스는 자식 클래스에 접근할 수 없습니다. 자식 클래스는 부모 클래스의 기능을 물려 받기 때문에 접근할 수 있지만, 그 반대는 아닙니다. 부모 코드를 봐봅시다! 자식에 대한 정보가 하나도 없습니다. 반면에 자식 코드는 extends Parents를 통해서 부모를 알고 있습니다. 단일 상속 자바는 다중 상속을 지원하지 않습니다. 그래서 extend 대상은 하나만 선택할 수 있습니다. 부모를 하나만 선택할 수 있다는 뜻입니다. 물론 부모가 또 다른 부모를 하나 가지는 것은 괜찮습니다. 다중 상속 그림 만약 비행기와 자동차를 상속 받아서 하늘을 나는 자동차를 만든다고 가정해봅시다. 만약 그림과 같이 다중 상속을 사용하게 되면 AirplaneCar 입장에서 move()를 호출할 때 어떤 부모의 move()를 사용해야 할지 애매한 문제가 발생합니다. 이것을 다이아몬드 문제라고 합니다. 그리고 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해질 수 있습니다. 이런 문제점 때문에 자바는 클래스의 다중 상속을 허용하지 않습니다. 대신에 추후에 공부할 인터페이스의 다중 구현을 허용해서 이러한 문제를 피합니다.
Archive
· 2024-03-08
☕️[Java] 상속과 메모리 구조
상속과 메모리 구조. 이 부분을 제대로 이해하는 것이 앞으로 정말 중요합니다! 상속 관계를 객체로 생성할 때 메모리 구조를 확인해봅시다. ElectricCar electricCar = new ElectricCar(); new ElectricCar()를 호출하면 ElectricCar 뿐만 아니라 상속 관계에 있는 Car까지 함께 포함해서 인스턴스를 생성합니다. 참조값은 x001로 하나이지만 실제로 그 안에서는 Car, ElectricCar라는 두가지 클래스 정보가 공존하는 것입니다. 상속이라고 해서 단순하게 부모의 필드와 메서드만 물려 받는게 아닙니다. 상속 관계를 사용하면 부모 클래스도 함께 포함해서 생성됩니다. 외부에서 볼때는 하나의 인스턴스를 생성하는 것 같지만 내부에서는 부모와 자식이 모두 생성되고 공간도 구분됩니다. electricCar.charge() 호출 electricCar.charge()를 호출하면 참조값을 확인해서 x001.charge()을 호출합니다. 따라서 x001을 찾아서 charge()를 호출하면 되는 것입니다. 그런데 상속 관계의 경우에는 내부에 부모와 자식이 모두 존재합니다. 이때 부모인 Car를 통해서 charge()를 찾을지 아니면 ElectricCar를 통해서 charge()를 찾을지 선택해야 합니다. 이때는 “호출하는 변수의 타입(클래스)을 기준으로 선택합니다.” electricCar 변수의 타입이 ElectricCar 이므로 인스턴스 내부에 같은 타입인 ElectricCar를 통해서 charge()를 호출합니다. electricCar.move() 호출 electricCar.move()를 호출하면 먼저 x001 참조로 이동합니다. 내부에는 Car, ElectricCar 두가지 타입이 있습니다. 이때 호출하는 변수인 electricCar의 타입이 ElectricCar 이므로 이 타입을 선택합니다. 그런데 ElectricCar에는 move() 메서드가 없습니다. 상속 관계에서는 자식 타입에 해당 기능이 없으면 부모 타입으로 올라가서 찾습니다. 이 경우 ElectricCar의 부모인 Car로 올라가서 move()를 찾습니다. 부모인 Car에 move()가 있으므로 부모에 있는 move() 메서드를 호출합니다. 만약 부모에서도 해당 기능을 찾지 못하면 더 상위 부모에서 필요한 기능을 찾아봅니다. 부모에 보무로 계속 올라가면서 필드나 메서드를 찾는 것 입니다. 물론 계속 찾아도 없으면 컴파일 오류가 발생합니다. “지금까지 설명한 상속과 메모리 구조는 반드시 이해해야 합니다!” 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성됩니다. 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 합니다. 이때 호출자의 타입을 통해 대상 타입을 찾습니다. 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행합니다. 기능을 찾지 못하면 컴파일 오류가 발생합니다.
Archive
· 2024-03-08
☕️[Java] 상속과 기능 추가
상속과 기능 추가. 이번에는 상속 관계의 장점을 알아보기 위해, 상속 관계에 다음 기능을 추가해보겠습니다. 모든 차량에 문열기(openDoor()) 기능을 추가합니다. 새로운 수소차(HydrogenCar)를 추가합니다. 수소차는 fillHydrogen() 기능을 통해 수소를 충전할 수 있습니다. package ex3 - Car package extends1.ex3; public class Car { public void move() { System.out.println("차를 이동합니다."); } // 추가 public void openDoor() { System.out.println("문을 엽니다."); } } 모든 차량에 문열기 기능을 추가할 때는 상위 부모인 Car에 openDoor() 기능을 추가하면 됩니다. 이렇게 하면 Car의 자식들은 해당 기능을 모두 물려받게 됩니다. 만약 상속 관계가 아니었다면 각각의 차량에 해당 기능을 모두 추가해야 합니다. package ex3 - ElectricCar package extends1.ex3; public class ElectricCar extends Car { public void charge() { System.out.println("충전합니다."); } } 기존 코드와 같습니다. package ex3 - GasCar package extends1.ex3; public class GasCar extends Car { public void fillUp() { System.out.println("기름을 주유합니다."); } } 기존 코드와 같습니다. package ex3 - HydrogenCar package extends1.ex3; public class HydrogenCar extends Car { public void fillHydrogen() { System.out.println("수소를 충전합니다."); } } 수소차를 추가했습니다. Car를 상속받은 덕분에 move(), openDoor()와 같은 기능을 바로 사용할 수 있습니다. 수소차는 전용 기능인 수소 충전(fillHydrogen()) 기능을 제공합니다. package ex3 - CarMain package extends1.ex3; public class CarMain { public static void main(String[] args) { ElectricCar electricCar = new ElectricCar(); electricCar.move(); electricCar.charge(); electricCar.openDoor(); GasCar gasCar = new GasCar(); gasCar.move(); gasCar.fillUp(); gasCar.openDoor(); HydrogenCar hydrogenCar = new HydrogenCar(); hydrogenCar.move(); hydrogenCar.fillHydrogen(); hydrogenCar.openDoor(); } } 실행 결과 차를 이동합니다. 충전합니다. 문을 엽니다. 차를 이동합니다. 기름을 주유합니다. 문을 엽니다. 차를 이동합니다. 수소를 충전합니다. 문을 엽니다. 기능 추가와 클래스 확장 상속 관계 덕분에 중복은 줄어들고, 새로운 수소차를 편리하게 확장(extend)한 것을 알 수 있습니다.
Archive
· 2024-03-08
☕️[Java] final 변수와 참조
final 변수와 참조. final은 변수의 값을 변경하지 못하게 막습니다. 그런데 여기서 변수의 값이라는 것은 무엇일까요? 변수는 크게 기본형 변수와 참조형 변수가 있습니다. 기본형 변수는 10, 20 같은 값을 보관하고, 참조형 변수는 객체의 참조값을 보관합니다. final을 기본형 변수에 사용하면 값을 변경할 수 없습니다. final을 참조형 변수에 사용하면 참조값을 변경할 수 없습니다. 여기까지는 이해하는데 어려움이 없을 것입니다. 이번에는 약간 복잡한 예제를 만들어 봅시다. package final1; public class Data { public int value; } package final1; public class FinalRefMain { public static void main(String[] args) { final Data data = new Data(); //data = new Data(); // final 변경 불가 컴파일 오류 // 참조 대상의 값은 변경 가능 data.value = 10; System.out.println(data.value); data.value = 20; System.out.println(data.value); } } final Data data = new Data(); // data = new Data(); // final 변경 불가 컴파일 오류 참조형 변수 data에 final이 붙었습니다. 변수 선언 시점에 참조값을 할당했으므로 더는 참조값을 변경할 수 없습니다. data.value = 10; data.value = 20; 그러나 참조 대상의 객체 값은 변경할 수 있습니다. 참조형 변수 data에 final이 붙었습니다. 이 경우 참조형 변수에 들어있는 참조값을 다른 값으로 변경하지 못합니다. 쉽게 이야기해서 이제 다른 객체를 참조할 수 없습니다. 그런데 이것의 정확한 뜻을 잘 이해해야 합니다. 참조값만 변경하지 못한다는 뜻입니다. 이 변수 이외에 다른 곳에 영향을 주는 것이 아닙니다. Data.value는 final이 아닙니다. 따라서 값을 변경할 수 있습니다. 정리하면 참조형 변수에 final이 붙으면 참조 대상 자체를 다른 대상으로 변경하지 못하는 것이지, 참조하는 대상의 값은 변경할 수 있습니다.
Archive
· 2024-03-07
☕️[Java] final 변수와 상수 2
final 변수와 상수 2 상수(Constant) 상수는 변하지 않고, 항상 일정한 값을 갖는 수를 말합니다. 자바에서는 보통 단 하나만 존재하는 변하지 않는 고정된 값을 상수라 합니다. 이런 이유로 상수는 static final 키워드를 사용합니다. 자바 상수 특징 static final 키워드를 사용합니다. 대문자를 사용하고 구분은 _(언더스코어)로 합니다.(관례) 일반적인 변수와 상수를 구분하기 위해 이렇게 합니다. 필드를 직접 접근해서 사용합니다. 상수는 기능이 아니라 고정된 값 자체를 사용하는 것이 목적입니다. 상수는 값을 변경할 수 없습니다. 따라서 필드에 직접 접근해도 데이터가 변하는 문제가 발생하지 않습니다. package final1; // 상수 public class Constant { // 수학 상수 public static final double PI = 3.14; // 시간 상수 public static final int HOURS_IN_DAY = 24; public static final int MINUTES_IN_HOUR = 60; public static final int SECONDS_IN_MINUTE = 60; // 애플리케이션 설정 상수 public static final int MAX_USERS = 1000; } 애플리케이션 안에는 다양한 상수가 존재할 수 있습니다. 수학, 시간 등등 실생활에서 사용하는 상수부터, 애플리케이션의 다양한 설정을 위한 상수들도 있습니다. 보통 이런 상수들은 애플리케이션 전반에서 사용되기 때문에 public를 자주 사용합니다. 물론 특정 위치에서만 사용된다면 다른 접근제어자를 사용하면 됩니다. 상수는 중앙에서 값을 하나로 관리할 수 있다는 장점도 있습니다. 상수는 런타임에 변경할 수 없습니다. 상수를 변경하려면 프로그램을 종료하고, 코드를 변경한 다음에 프로그램을 다시 실행해야 합니다. 추가로 상수는 중앙에서 값을 하나로 관리할 수 있다는 장점도 있습니다. 다음 두 예제를 비교해봅시다. ConstantMain1 - 상수 없음 package final1; public class ConstantMain2 { public static void main(String[] args) { System.out.println("프로그램 최대 참여자 수 " + 1000); int currentUserCount = 999; process(currentUserCount++); process(currentUserCount++); process(currentUserCount++); process(currentUserCount++); } private static void process(int currentUserCount) { System.out.println("참여자 수:" + currentUserCount); if (currentUserCount > 1000) { System.out.println("대기자로 등록합니다."); } else { System.out.println("게임에 참여합니다."); } } } 이 코드에는 다음과 같은 문제가 있습니다. 만약 프로그램 최대 참여자 수를 현재 1000명에서 2000명으로 변경해야 하면 2곳의 변경 포인트가 발생합니다. 만약 애플리케이션의 100곳에서 이 숫자를 사용했다면 100곳을 모두 변경해야 합니다. 매직 넘버 문제도 있습니다. ConstatnMain2 - 상수 있음 package final1; public class ConstantMain2 { public static void main(String[] args) { System.out.println("프로그램 최대 참여자 수 " + Constant.MAX_USERS); int currentUserCount = 999; process(currentUserCount++); process(currentUserCount++); process(currentUserCount++); process(currentUserCount++); } private static void process(int currentUserCount) { System.out.println("참여자 수:" + currentUserCount); if (currentUserCount > Constant.MAX_USERS) { System.out.println("대기자로 등록합니다."); } else { System.out.println("게임에 참여합니다."); } } } Constant.MAX_USERS 상수를 사용했습니다. 만약 프로그램 최대 참여자 수를 변경해야 하면 Constant.MAX_USERS의 상수 값만 변경하면 됩니다. 매직 넘버 문제를 해결했습니다. 숫자 1000이 아니라 사람이 인지할 수 있게 MAX_USERS라는 변수명으로 코드를 이해할 수 있습니다.
Archive
· 2024-03-07
☕️[Java] final 변수와 상수 1
final 변수와 상수 1 final 키워드는 이름 그대로 끝! 이라는 뜻 입니다. 변수에 final 키워드가 붙으면 더는 값을 변경할 수 없습니다. 참고로 final은 class, method를 포함한 여러곳에 붙을 수 있습니다. final - 지역 변수 package final1; public class FinalLocalMain { public static void main(String[] args) { // final 지역 변수1 final int data1; data1 = 10; // 최초 한번만 할당 가능 // data1 = 20; // 컴파일 오류 // final 지역 변수2 final int data2 = 10; // data2 = 20; // 컴파일 오류 method(10); } static void method(final int parameter) { // parameter = 20; // 컴파일 오류 } } final 을 지역 변수에 설정할 경우 최초 한번만 할당할 수 있습니다. 이후에 변수의 값을 변경하려면 컴파일 오류가 발생합니다. final을 지역 변수 선언시 바로 초기화 한 경우 이미 값이 할당되었기 때문에 값을 할당할 수 없습니다. 매개변수에 final이 붙으면 메서드 내부에서 매개변수의 값을 변셩할 수 없습니다. 따라서 메서드 호출 시점에 사용된 값이 끝까지 사용됩니다. final - 필드(맴버변수) package final1; public class ConstructInit { final int value; public ConstructInit(int value) { this.value = value; } } final 을 필드에 사용할 경우 해당 필드는 생성자를 통해서 한번만 초기화 될 수 있습니다. package final1; public class ConstructInit { final int value; public ConstructInit(int value) { this.value = value; } } final 필드를 필드에서 초기화하면 이미 값이 설정되었기 때문에 생성자를 통해서도 초기화 할 수 없습니다. value 필드를 참고합시다. 코드에서 보는 것 처럼 static 변수에도 final을 선언할 수 있습니다. CONSTANT_VALUE로 변수 작명 방법이 대문자를 사용하였습니다. 이것은 상수입니다. package final1; public class FinalFieldMain { public static void main(String[] args) { // final 필드 - 생성자 초기화 System.out.println("생성자 초기화"); ConstructInit constructInit1 = new ConstructInit(10); ConstructInit constructInit2 = new ConstructInit(20); System.out.println(constructInit1.value); System.out.println(constructInit2.value); // final 필드 - 필드 초기화 System.out.println("필드 초기화"); FieldInit fieldInit1 = new FieldInit(); FieldInit fieldInit2 = new FieldInit(); FieldInit fieldInit3 = new FieldInit(); System.out.println(fieldInit1.value); System.out.println(fieldInit2.value); System.out.println(fieldInit3.value); // 상수 System.out.println("싱수"); System.out.println(FieldInit.CONST_VALUE); } } 실행 결과 생성자 초기화 10 20 필드 초기화 10 10 10 싱수 10 ConstructInit과 같이 생성자를 사용해서 final 필드를 초기화 하는 경우, 각 인스턴스마다 final 필드에 다른 값을 할당할 수 있습니다. 물론 final을 사용했기 때문에 생성 이후에 이 값을 변경하는 것은 불가능합니다. FieldInit과 같이 final 필드를 필드에서 초기화 하는 경우, 모든 인스턴스가 위 그림의 오른쪽과 같이 같은 값을 가집니다. 여기서는 FieldInit 인스턴스의 모든 value 값은 10이 됩니다. 왜냐하면 생성자 초기화와 다르게 필드 초기화는 필드의 코드에 해당 값이 미리 정해져있기 때문입니다. 모든 인스턴스가 같은 값을 사용하기 때문에 결과적으로 메모리를 낭비하게 됩니다.(물론 JVM에 따라서 내부 최적화를 시도할 수 있습니다.) 또 메모리 낭비를 떠나서 같은 값이 계속 생성되는 것은 개발자가 보기에 명확한 중복입니다. 이럴 때 사용하면 좋은 것이 바로 static 영역입니다. static final FieldInit.MY_VALUE는 static 영역에 존재합니다. 그리고 final 키워드를 사용해서 초기화 값이 변하지 않습니다 static 영역은 단 하나만 존재하는 영역입니다. MY_VALUE 변수는 JVM 상에서 하나만 존재하므로 앞서 설명한 중복과 메모리 비효율 문제를 모두 해결할 수 있습니다. 이런 이유로 필드에 final + 필드 초기화를 사용하는 경우 static을 붙여서 사용하는 것이 효과적입니다.
Archive
· 2024-03-07
☕️[Java] final 정리
final 정리. final은 매우 유용한 제약입니다. 만약 특정 변수의 값을 할당한 이후에 변경하지 않아야 한다면 final을 사용합시다. 예를 들어서 고객의 id를 변경하면 큰 문제가 발생한다면 final로 선언하고 생성자로 값을 할당합시다. 만약 어디선가 실수로 id 값을 변경한다면 컴파일러가 문제를 찾아줄 것입니다. package final1.ex; public class Member { private final String id; // final 키워드 사용 private String name; public Member(String id, String name) { this.id = id; this.name = name; } public void changeData(String id, String name) { // this.id = id; // 컴파일 오류 발생 this.name = name; } public void print() { System.out.println("id:" + id + ", name:" + name); } } changeData() 메서드에서 final인 id 값 변경을 시도하면 컴파일 오류가 발생합니다. package final1.ex; public class MemberMain { public static void main(String[] args) { Member member = new Member("dev", "kobe"); member.print(); member.changeData("dev.skyachieve", "kang"); member.print(); } } 실행 결과 id:dev, name:kobe id:dev, name:kang
Archive
· 2024-03-07
☕️[Java] static 메서드 3
static 메서드 3 용어 정리 멤버 메서드의 종류 인스턴스 메서드: static이 붙지 않은 멤버 메서드 클래스 메서드: static이 붙은 멤버 메서드 클래스 메서드, 정적 메서드, static 메서드등으로 부릅니다. static이 붙지 않은 멤버 메서드는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있습니다. 따라서 인스턴스 메서드라고 합니다. static이 붙은 멤버 메서드는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있습니다. 따라서 클래스 메서드라고 합니다. 참고로 방금 설명한 내용은 멤버 변수에도 똑같이 적용됩니다. 정적 메서드 활용 정적 메서드는 객체 생성이 필요 없이 메서드의 호출만으로 필요한 기능을 수행할 때 주로 사용합니다. 예를 들어 간단한 메서드 하나로 끝나는 유틸리티성 메서드에 자주 사용합니다. 수학의 여러가지 기능을 담은 클래스를 만들 수 있는데, 이 경우 인스턴스 변수 없이 입력한 값을 계산하고 반환하는 것이 대부분입니다. 이럴 때 정적 메서드를 사용해서 유틸리티성 메서드를 만들면 좋습니다. 정적 메서드 접근 법 static 메서드는 static 변수와 마찬가지로 클래스를 통해 바로 접근할 수 있고, 인스턴스를 통해서도 접근할 수 있습니다. DataCountMain - 추가 // 추가 // 인스턴스를 통한 접근 DecoData data3 = new DecoData(); data3.staticCall(); // 클래스를 통한 접근 DecoData.staticCall(); 실행 결과 - 추가된 부분 staticValue=4 staticValue=5 둘의 차이는 없습니다. 둘다 결과적으로 정적 메서드에 접근합니다. 인스턴스를 통한 접근 data3.staticCall() 정적 메서드의 경우 인스턴스를 통한 접근은 추천하지 않습니다. 왜냐하면 코드를 읽을 때 마치 인스턴스 메서드에 접근하는 것 처럼 오해할 수 있기 때문입니다. 클래스를 통한 접근 DecoData.staticCall() 정적 메서드는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확합니다. 따라서 정적 메서드에 접근할 때는 클래스를 통해서 접근합시다. static import 정적 메서드를 사용할 때 해당 메서드를 다음과 같이 자주 호출해야 한다면 static import 기능을 고려합시다. DecoData.staticCall(); DecoData.staticCall(); DecoData.staticCall(); 이 기능을 사용하면 다음과 같이 클래스명을 생략하고 메서드를 호출할 수 있습니다. staticCall(); staticCall(); staticCall(); DecoDataMain - static import 적용 package static2; // import static static2.DecoData.staticCall; import static static2.DecoData.*; public class DecoDataMain { public static void main(String[] args) { System.out.println("1.정적 호출"); staticCall(); // 클래스 명 생략 가능 ... } } 특정 클래스의 정적 메서드 하나만 적용하려면 다음과 같이 생략할 메서드 명을 적어주면 됩니다. import static static2.DecoData.staticCall; 특정 클래스의 모든 정적 메서드에 적용하려면 다음과 같이 *을 사용하면 됩니다. import static static2.DecoData.*; 참고로 import static은 정적 메서드 뿐만 아니라 정적 변수에도 사용할 수 있습니다. main() 메서드는 정적 메서드 인스턴스 생성 없이 실행하는 가장 대표적인 메서드가 바로 main() 메서드입니다. main() 메서드는 프로그램을 시작하는 시작점이 되는데, 생각해보면 객체를 생성하지 않아도 main() 메서드가 작동했습니다. 이것은 main() 메서드가 static이기 때문입니다. 정적 메서드는 정적 메서드만 호출할 수 있습니다. 따라서 정적 메서드인 main()이 호출하는 메서드에는 정적 메서드를 사용했습니다. 물론 더 정확히 말하자면 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있습니다. 따라서 정적 메서드인 main() 메서드가 같은 클래스에서 호출하는 메서드도 정적 메서드로 선언해서 사용했습니다. main() 메서드와 static 메서드 호출 예 public class ValueDataMain { public static void main(String[] args) { ValueData valueData = new ValueData(); add(valueData); } static void add(ValueData valueData) { valueData value++; System.out.println("숫자 증가 value=" + valueData.value); } }
Archive
· 2024-03-06
☕️[Java] static 변수3
static 변수 3 이번에는 static 변수를 정리해보겠습니다. 용어 정리 public class Data3 { public String name; public static int count; // static } 예제 코드에서 name, count는 둘 다 멤버 변수입니다. 멤버 변수(필드)는 static이 붙은 것과 아닌 것에 따라 다음과 같이 분류 할 수 있습니다. 멤버 변수(필드)의 종류. 인스턴스 변수: static이 붙지 않은 멤버 변수, 예) name static 이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있습니다. 따라서 인스턴스 변수라 합니다. 인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어집니다. 클래스 변수: static이 붙은 멤버 변수, 예) count 클래스 변수, 정적 변수, static 변수등으로 부릅니다. 용어를 모두 사용하니 주의합시다. static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있습니다. 따라서 클래스 변수라 합니다. 클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어집니다. 인스턴스와는 다른게 보통 여러곳에서 공유하는 목적으로 사용됩니다. 변수와 생명주기 지역 변수(매개변수 포함): 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관됩니다. 메서드가 종료되면 스택 프레임도 제거 되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거됩니다. 따라서 지역 변수는 생존 주기가 짧습니다. 인스턴스 변수: 인스턴스에 있는 멤버 변수를 인스턴스 변수라 합니다. 인스턴스 변수는 힙 영역을 사용합니다. 힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지 생존하기 때문에 보통 지역 변수보다 생존 주기가 깁니다. 클래스 변수: 클래스 변수는 메서드 영역의 static 영역에 보관 되는 변수입니다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간입니다. 클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성됩니다. 그리고 JVM이 종료될 때까지 생명주기가 이어집니다. 따라서 가장 긴 생명주기를 가집니다. static이 정적이라는 이유는 바로 여기에 있습니다. 힙 역역에 생성되는 인스턴스 변수는 동적으로 생성되고, 제거됩니다. 반면에 static인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거됩니다. 정적 변수는 이름 그대로 정적입니다. 정적 변수 접근 법 static 변수는 클래스를 통해 바로 접근할 수도 있고, 인스턴스를 통해서도 접근할 수 있습니다. DataCountMain3 마지막 코드에 다음 부분을 추가하고 실행해보겠습니다. DataCountMain3 - 추가 // 추가 // 인스턴스를 통한 접근 Data3 data4 = new Data3("D"); // 클래스를 통합 접근 System.out.println(Data3.count); 실행 결과 - 추가된 부분 4 4 둘 다 차이는 없습니다. 둘다 결과적으로 정적 변수에 접근합니다. 인스턴스를 통한 접근 data4.count 정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않습니다. 왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것 처럼 오해할 수 있기 때문입니다. 클래스를 통한 접근 Data3.count 정적 변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확합니다. 따라서 정적 변수에 접근할 때는 클래스를 통해서 접근합시다.
Archive
· 2024-03-05
☕️[Java] static 변수2
static 변수 2 static 변수 사용 특정 클래스에서 공용으로 함께 사용할 수 있는 변수를 만들 수 있다면 편리할 것입니다. static 키워드를 사용하면 공용으로 함께 사용하는 변수를 만들 수 있습니다 Data3 package static1; public class Data3 { public String name; public static int count; // static public Data3(String name) { this.name = name; count++; } } 기존 코드를 유지하기 위해 새로운 클래스 Data3을 만들었습니다. static int count 부분을 봐봅시다. 변수 타입(int) 앞에 static 키워드가 붙어있습니다. 이렇게 멤버 변수에 static을 붙이게 되면 static 변수, 정적 변수 또는 클래스 변수라 합니다. 객체가 생성되면 생성자에서 정적 변수 count의 값을 하나 증가시킵니다. DataCountMain3 package static1; public class DataCountMain3 { public static void main(String[] args) { Data3 data1 = new Data3("A"); System.out.println("A coutn=" + Data3.count); Data3 data2 = new Data3("B"); System.out.println("B coutn=" + Data3.count); Data3 data3 = new Data3("C"); System.out.println("C coutn=" + Data3.count); } } 코드를 보면 count 정적 변수에 접근하는 방법이 조금 특이한데 Data3.count 와 같이 클래스명에 .(dot)을 사용합니다. 마치 클래스에 직접 접근하는 것 처럼 느껴집니다. 실행 결과 A coutn=1 B coutn=2 C coutn=3 static 이 붙은 멤버 변수는 메서드 영역에서 관리합니다. static 이 붙은 멤버 변수 count는 인스턴스 영역에 생성되지 않습니다. 대신 메서드 영역에서 이 변수를 관리합니다. Data3("A") 인스턴스를 생성하면 생성자가 호출됩니다. 생성자에는 count++ 코드가 있습니다. count는 static이 붙은 정적 변수입니다. 정적 변수는 인스턴스 영역이 아니라 메서드 영역에서 관리합니다. 따라서 이 경우 메서드 영역에 있는 count의 값이 하나 증가됩니다. Data3("B") 인스턴스를 생성하면 생성자가 호출됩니다. count++ 코드가 있습니다. count는 static이 붙은 정적 변수입니다 메서드 영역에 있는 count 변수의 값이 하나 증가됩니다. Data3("C") 인스턴스를 생성하면 생성자가 호출됩니다. count++ 코드가 있습니다. count는 static이 붙은 정적 변수입니다. 메서드 영역에 있는 count 변수의 값이 하나 증가됩니다. 최종적으로 메서드 영역에 있는 count 변수의 값은 3이 됩니다. static이 붙은 정적 변수에 접근하려면 Data3.count와 같이 클래스명 + .(dot) + 변수명으로 접근하면 됩니다. 참고로 Data3의 생성자와 같이 자신의 클래스에 있는 정적 변수라면 클래스 명을 생략할 수 있습니다. static 변수를 사용한 덕분에 공용 변수를 사용해서 편리하게 문제를 해결할 수 있었습니다. 정리 static 변수는 쉽게 이야기해서 클래스인 붕어빵 틀이 특별히 관리하는 변수입니다. 붕어빨 틀은 1개이므로 클래스 변수도 하나만 존재합니다. 반면에 인스턴스인 붕어빵은 인스턴스의 수 만큼 변수가 존재합니다.
Archive
· 2024-03-05
☕️[Java] static 메서드 2
static 메서드 2 정적 메서드는 객체 생성없이 클래스에 있는 메서드를 바로 호출할 수 있다는 장점이 있습니다. 하지만 정적 메서드는 언제나 사용할 수 있는 것이 아닙니다. 정적 메서드 사용법 static 메서드는 static만 사용할 수 있습니다. 클래스 내부의 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있습니다. 클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없습니다. 반대로 모든 곳에서 static을 호출할 수 있습니다. 정적 메서드는 공용 기능입니다. 따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static을 호출할 수 있습니다. 예제를 통해 정적 메서드의 사용법을 확인해봅시다. DecoData package static2; public class DecoData { private int instanceValue; private static int staticValue; public static void staticCall() { //instanceValue++; // 인스턴스 변수 접근, compile error //instanceMethod(); // 인스턴스 메서드 접근, compile error staticValue++; // 정적 변수 접근 staticMethod(); // 정적 메서드 접근 } public void instanceCall() { instanceValue++; // 인스턴스 변수 접근 instanceMethod(); // 인스턴스 메서드 접근 staticValue++; // 정적 변수 접근 staticMethod(); // 정적 메서드 접근 } private void instanceMethod() { System.out.println("instanceValue=" + instanceValue); } private static void staticMethod() { System.out.println("staticValue=" + staticValue); } } 이번 예제에서는 접근 제어자를 적극 활용해서 필드를 포함한 외부에서 직접 필요하지 않은 기능은 모두 막아두었습니다. instanceValue는 인스턴스 변수입니다. staticValue는 정적 변수(클래스 변수)입니다. instanceMethod()는 인스턴스 메서드입니다. staticMethod()는 정적 메서드(클래스 메서드)입니다. staticCall() 메서드를 봐봅시다. 이 메서드는 정적 메서드입니다. 따라서 static 만 사용할 수 있습니다. 정적 변수, 정적 메서드에는 접근할 수 있지만, static이 없는 인스턴스 변수나 인스턴스 메서드에 접근하면 컴파일 오류가 발생합니다. 코드를 보면 staticCall() -> staticMethod()로 static에서 static을 호출하는 것을 확인할 수 있습니다. instanceCall() 메서드를 봐봅시다. 이 메서드는 인스턴스 메서드입니다. 모든 곳에서 공용인 static을 호출할 수 있습니다. 따라서 정적 변수, 정적 메서드에 접근할 수 있습니다. 물론 인스턴스 변수, 인스턴스 메서드에도 접근할 수 있습니다. DecoDataMain package static2; public class DecoDataMain { public static void main(String[] args) { System.out.println("1. 정적 호출"); DecoData.staticCall(); System.out.println("2. 인스턴스 호출1"); DecoData data1 = new DecoData(); data1.instanceCall(); System.out.println("3. 인스턴스 호출2"); DecoData data2 = new DecoData(); data2.instanceCall(); } } 실행 결과 1. 정적 호출 staticValue=1 2. 인스턴스 호출1 instanceValue=1 staticValue=2 3. 인스턴스 호출2 instanceValue=1 staticValue=3 정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유 정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있습니다. 그래서 인스턴스처럼 참조값의 개념이 없습니다. 특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출합니다. 따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없습니다. 물론 당연한 이야기지만 다음과 같이 객체의 참조값을 직접 매개변수로 전달하면 정적 메서드도 인스턴스의 변수나 메서드를 호출할 수 있습니다. public static void staticCall(DecoData data) { data.instanceValue++; data.instanceMethod(); }
Archive
· 2024-03-05
☕️[Java] static 메서드 1
static 메서드 1 이번에는 static이 붙은 메서드에 대해 알아보겠습니다. 이해를 돕기 위해 간단한 예제를 만들어보겠습니다. 특정 문자열을 꾸며주는 간단한 기능을 만들어보겠습니다. 예를 들어서 "hello"라는 문자를 꾸미면 앞 뒤에 *을 붙여서 *hello*와 같이 꾸며주는 기능입니다. 인스턴스 메서드 DecoUtil1 package static2; public class DecoUtil1 { public String deco(String str) { String result = "*" + str + "*" return result; } } deco()는 문자열을 꾸미는 기능을 제공합니다. 문자열이 들어오면 앞 뒤에 *을 붙여서 반환합니다. DecoMain1 package static2; public class DecoMain1 { public static void main(String[] args) { String s = "hello java"; DecoUtil1 utils = new DecoUtil1(); String deco = utils.deco(s); System.out.println("before: " + s); System.out.println("after: " + deco); } } 실행 결과 before: hello java after: *hello java* 앞서 개발한 deco() 메서드를 호출하기 위해서는 DecoUtil1의 인스턴스를 먼저 생성해야 합니다. 그런데 deco() 라는 기능은 멤버 변수도 없고, 단순히 기능만 제공할 뿐입니다. 인스턴스가 필요한 이유는 멤버 변수(인스턴스 변수)등을 사용하는 목적이 큰데, 이 메서드는 사용하는 인스턴스 변수도 없고 단순히 기능만 제공합니다. static 메서드 먼저 예제를 만들어서 실행해봅시다. DecoUtil2 package static2; public class DecoUtil2 { public static String deco(String str) { String result = "*" + str + "*"; return result; } } DecoUtil2는 앞선 예제와 비슷합니다. 하지만 메서드 앞에 static이 붙어있습니다. 이 부분에 주의합시다. 이렇게 하면 정적 메서드를 만들 수 있습니다. 그리고 이 정적 메서드는 정적 변수처럼 인스턴스 생성 없이 클래스 명을 통해서 바로 호출할 수 있습니다. DecoMain2 package static2; public class DecoMain2 { public static void main(String[] args) { String s = "hello java"; String deco = DecoUtil2.deco(s); System.out.println("before: " + s); System.out.println("after: " + deco); } } 실행 결과 before: hello java after: *hello java* DecoUtil2.deco(s); 코드를 봅시다. static이 붙은 정적 메서드는 객체 생성 없이 클래스명 + .(dot) + 메서드 명으로 바로 호출할 수 있습니다. 정적 메서드 덕분에 불필요한 객체 생성 없이 편리하게 메서드를 사용했습니다. 클래스 메서드 메서드 앞에서 static을 붙일 수 있습니다. 이것을 정적 메서드 또는 클래스 메서드라 합니다. 정적 메서드라는 용어는 static이 정적이라는 뜻이기 때문입니다. 클래스 메서드라는 용어는 인스턴스 생성 없이 마치 클래스에 있는 메서드를 바로 호출하는 것 처럼 느껴지기 때문입니다. 인스턴스 메서드 static이 붙지 않은 메서드는 인스턴스를 생성해야 호출할 수 있습니다. 이것을 인스턴스 메서드라 합니다.
Archive
· 2024-03-05
☕️[Java] static 변수1
static 변수 1 sttic 키워드는 주로 멤버 변수와 메서드에 사용됩니다. 먼저 멤버 변수에 static 키워드가 왜 필요한지 이해하기 위해 간단한 예제를 만들어봅시다. 특정 클래스를 통해서 생성된 객체의 수를 세는 단순한 프로그램입니다. 인스턴스 내부 변수에 카운트 저장 먼저 생성할 인스턴스 내부에 카운트를 저장하겠습니다. Data1 package static1; public class Data1 { public String name; public int count; public Data1(String name) { this.name = name; count++; } } 생성된 객체의 수를 세어야 합니다. 따라서 객체가 생성될 때 마다 생성자를 통해 인스턴스의 멤버 변수인 count 값을 증가시킵니다. 참고로 예제를 단순하게 만들기 위해 필드에 public을 사용했습니다. DataCountMain1 package static1; public class DataCountMain1 { public static void main(String[] args) { Data1 data1 = new Data1("A"); System.out.println("A count=" + data1.count); Data1 data2 = new Data1("B"); System.out.println("B count=" + data2.count); Data1 data3 = new Data1("C"); System.out.println("C count=" + data3.count); } } 객체를 생성하고 카운트 값을 출력합니다. 실행 결과 A count=1 B count=1 C count=1 이 프로그램은 당연히 기대한 대로 작동하지 않습니다. 객체를 생성할 때 마다 Data1 인스턴스는 새로 만들어집니다. 그리고 인스턴스에 포함된 count 변수도 새로 만들어지기 때문입니다. 처음 Data1("A") 인스턴스를 생성하면 count 값은 0으로 초기화 됩니다. 생성자에서 count++을 호출했으므로 count의 값은 1이 됩니다. 다음으로 Data1("B") 인스턴스를 생성하면 완전 새로운 인스턴스를 생성합니다. 이 새로운 인스턴스의 count 값은 0으로 초기화됩니다. 생성자에서 count++을 호출했으므로 count의 값은 1이 됩니다. 다음으로 Data1("C") 인스턴스를 생성하면 이전 인스턴스는 관계없는 새로운 인스턴스를 생성합니다. 이 새로운 인스턴스의 count 값은 0으로 초기화 됩니다. 생성자에서 count++을 호출했으므로 count의 값은 1이 됩니다. “인스턴스에 사용되는 멤버 변수 count값은 인스턴스끼리 서로 공유되지 않습니다.” 따라서 원하는 답을 구할 수 없습니다. 이 문제를 해결하려면 변수를 서로 공유해야 합니다. 외부 인스턴스에 카운트 저장 이번에는 카운트 값을 저장하는 별도의 객체를 만들어보겠습니다. Counter package static1; public class Counter { public int count; } Data2 package static1; public class Data2 { public String name; public Data2(String name, Counter counter) { this.name = name; counter.count++; } } DataCountMain2 package static1; public class DataCountMain2 { public static void main(String[] args) { Counter counter = new Counter(); Data2 data1 = new Data2("A", counter); System.out.println("A count=" + counter.count); Data2 data2 = new Data2("B", counter); System.out.println("B count=" + counter.count); Data2 data3 = new Data2("C", counter); System.out.println("C count=" + counter.count); } } 실행 결과 A count=1 B count=2 C count=3 Counter 인스턴스를 공용으로 사용한 덕분에 객체를 생성할 때 마다 값을 정확하게 증가시킬 수 있습니다. Data2("A") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킵니다. count 값은 1이 됩니다. Data2("B") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킵니다. count 값은 2가 됩니다. Data2("C") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킵니다. count 값은 3이 됩니다. 결과적으로 Data2의 인스턴스가 3개 생성되고, count 값도 인스턴스 숫자와 같은 3으로 정확하게 측정됩니다. 그런데 여기에는 약간 불편한 점들이 있습니다. Data2 클래스와 관련된 일인데, Counter라는 별도의 클래스를 추가로 사용해야 합니다. 생성자의 매개변수도 추가되고, 생성자가 복잡해집니다. 생성자를 호출하는 부분도 복잡해집니다.
Archive
· 2024-03-04
☕️[Java] static 변수1
static 변수 1 sttic 키워드는 주로 멤버 변수와 메서드에 사용됩니다. 먼저 멤버 변수에 static 키워드가 왜 필요한지 이해하기 위해 간단한 예제를 만들어봅시다. 특정 클래스를 통해서 생성된 객체의 수를 세는 단순한 프로그램입니다. 인스턴스 내부 변수에 카운트 저장 먼저 생성할 인스턴스 내부에 카운트를 저장하겠습니다. Data1 package static1; public class Data1 { public String name; public int count; public Data1(String name) { this.name = name; count++; } } 생성된 객체의 수를 세어야 합니다. 따라서 객체가 생성될 때 마다 생성자를 통해 인스턴스의 멤버 변수인 count 값을 증가시킵니다. 참고로 예제를 단순하게 만들기 위해 필드에 public을 사용했습니다. DataCountMain1 package static1; public class DataCountMain1 { public static void main(String[] args) { Data1 data1 = new Data1("A"); System.out.println("A count=" + data1.count); Data1 data2 = new Data1("B"); System.out.println("B count=" + data2.count); Data1 data3 = new Data1("C"); System.out.println("C count=" + data3.count); } } 객체를 생성하고 카운트 값을 출력합니다. 실행 결과 A count=1 B count=1 C count=1 이 프로그램은 당연히 기대한 대로 작동하지 않습니다. 객체를 생성할 때 마다 Data1 인스턴스는 새로 만들어집니다. 그리고 인스턴스에 포함된 count 변수도 새로 만들어지기 때문입니다. 처음 Data1("A") 인스턴스를 생성하면 count 값은 0으로 초기화 됩니다. 생성자에서 count++을 호출했으므로 count의 값은 1이 됩니다. 다음으로 Data1("B") 인스턴스를 생성하면 완전 새로운 인스턴스를 생성합니다. 이 새로운 인스턴스의 count 값은 0으로 초기화됩니다. 생성자에서 count++을 호출했으므로 count의 값은 1이 됩니다. 다음으로 Data1("C") 인스턴스를 생성하면 이전 인스턴스는 관계없는 새로운 인스턴스를 생성합니다. 이 새로운 인스턴스의 count 값은 0으로 초기화 됩니다. 생성자에서 count++을 호출했으므로 count의 값은 1이 됩니다. “인스턴스에 사용되는 멤버 변수 count값은 인스턴스끼리 서로 공유되지 않습니다.” 따라서 원하는 답을 구할 수 없습니다. 이 문제를 해결하려면 변수를 서로 공유해야 합니다. 외부 인스턴스에 카운트 저장 이번에는 카운트 값을 저장하는 별도의 객체를 만들어보겠습니다. Counter package static1; public class Counter { public int count; } Data2 package static1; public class Data2 { public String name; public Data2(String name, Counter counter) { this.name = name; counter.count++; } } DataCountMain2 package static1; public class DataCountMain2 { public static void main(String[] args) { Counter counter = new Counter(); Data2 data1 = new Data2("A", counter); System.out.println("A count=" + counter.count); Data2 data2 = new Data2("B", counter); System.out.println("B count=" + counter.count); Data2 data3 = new Data2("C", counter); System.out.println("C count=" + counter.count); } } 실행 결과 A count=1 B count=2 C count=3 Counter 인스턴스를 공용으로 사용한 덕분에 객체를 생성할 때 마다 값을 정확하게 증가시킬 수 있습니다. Data2("A") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킵니다. count 값은 1이 됩니다. Data2("B") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킵니다. count 값은 2가 됩니다. Data2("C") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킵니다. count 값은 3이 됩니다. 결과적으로 Data2의 인스턴스가 3개 생성되고, count 값도 인스턴스 숫자와 같은 3으로 정확하게 측정됩니다. 그런데 여기에는 약간 불편한 점들이 있습니다. Data2 클래스와 관련된 일인데, Counter라는 별도의 클래스를 추가로 사용해야 합니다. 생성자의 매개변수도 추가되고, 생성자가 복잡해집니다. 생성자를 호출하는 부분도 복잡해집니다.
Archive
· 2024-03-04
☕️[Java] 스택과 큐 자료구조
스택과 큐 자료구조. 자바 메모리 구조 중 스택 영역에 대해 알아보기 전에 먼저 스택(Stack)이라는 자료 구조에 대해서 알아봅시다. 스택 구조 다음과 같은 1, 2, 3 이름표가 붙은 블럭이 있다고 가정해봅시다. 이 블럭을 다음과 같이 생긴 통에 넣는다고 생각해봅시다. 위쪽만 열려있기 때문에 위쪽으로 블럭을 넣고, 위쪽으로 블럭을 빼야 합니다. 쉽게 이야기해서 넣는 곳과 빼는 곳이 같습니다. 블럭은 1 -> 2 -> 3 순서대로 넣을 수 있습니다. 이번에는 넣은 블럭을 빼봅시다. 블럭을 빼려면 위에서 부터 순서대로 빼야합니다. 블럭은 3 -> 2 -> 1 순서로 뺄 수 있습니다. 정리하면 다음과 같습니다. 1(넣기) -> 2(넣기) -> 3(넣기) -> 3(빼기) -> 2(빼기) -> 1(빼기) 후입 선출(LIFO, Last In First Out) 여기서 가장 마지막에 넣은 3번이 가장 먼저 나옵니다. 이렇게 가장 먼저 나오는 것을 “후입 선출”이라 하고, 이런 자료 구조를 “스택”이라 합니다. 선입 선출(FIFO, First In First Out) 후입 선출과 반대로 가장 먼저 넣은 것이 가장 먼저 나오는 것을 선입 선출이라고 합니다. 이런 자료 구조를 “큐(Queue)”라 합니다. 큐(Queue) 자료 구조 정리하면 다음과 같습니다. 1(넣기) -> 2(넣기) -> 3(넣기) -> 1(빼기) -> 2(빼기) -> 3(빼기) 이런 자료 구조는 각자 필요한 영역이 있습니다. 예를 들어 선착순 이벤트를 하는데 고객이 대기해야 한다면 큐 자료 구조를 사용해야 합니다.
Archive
· 2024-03-03
☕️[Java] 스택 영역과 힙 영역
스택 영역과 힙 영역. 이번에는 스택 영역과 힙 영역이 함께 사용되는 경우를 알아봅시다. Data package memory; public class JavaMemoryMain2 { public static void main(String[] args) { System.out.println("main start"); method1(); System.out.println("main end"); } static void method1() { System.out.println("method1 start"); Data data1 = new Data(10); method2(data1); System.out.println("method1 end"); } static void method2(Data data2) { System.out.println("method2 start"); System.out.println("data.value=" + data2.getValue()); System.out.println("method2 end"); } } main() -> method1() -> method2() 순서로 호출하는 단순한 코드입니다. method1()에서 Data 클래스의 인스턴스를 생성합니다. method1()에서 method2()를 호출할 때 매개변수에 Data 인스턴스의 참조값을 전달합니다. 실행 결과 main start method1 start method2 start data.value=10 method2 end method1 end main end 그림을 통해 순서대로 알아봅시다. 처음 main() 메서드를 실행합니다. main() 스택 프레임이 생성됩니다. main()에서 method1()을 실행합니다. method1() 스택 프레임이 생성됩니다. method1()은 지역 변수로 Data data1을 가지고 있습니다. 이 지역 변수도 스택 프레임에 포함됩니다. method1()은 new Data(10)를 사용해서 힙 영역에 Data 인스턴스를 생성합니다. 그리고 참조값을 data1에 보관합니다. method1()은 method2()를 호출하면서 Data data2 매개변수에 xoo1 참조값을 넘깁니다. 이제 method1()에 있는 data1과 method2()에 있는 data2 지역 변수(매개변수 포함)는 둘다 같은 x001 인스턴스를 참조합니다. method2()가 종료됩니다. method2()의 스택 프레임이 제거되면서 매개변수 data2도 함께 제거됩니다. method1()1이 종료됩니다. method1()의 스택 프레임이 제거되면서 지역 변수 data1도 함께 제거됩니다. method1()이 종료된 직후의 상태를 봅시다. method1()의 스택 프레임이 제거되고 지역 변수 data1도 함께 제거되었습니다. 이제 x001 참조값을 가진 Data 인스턴스를 참조하는 곳이 더는 없습니다. 참조하는 곳이 없으므로 사용되는 곳도 없습니다. 결과적으로 프로그램에서 더는 사용하지 않는 객체인 것입니다. 이런 객체는 메모리만 차지하게 됩니다. GC(가비지 컬렉션)은 이렇게 참조가 모두 사라진 인스턴스를 찾아서 메모리에서 제거합니다. 참고: 힙 영역 외부가 아닌, 힙 영역 안에서만 인스턴스끼리 서로 참조하는 경우에도 GC의 대상이 됩니다. 정리 지역 변수는 스택 영역에, 객체(인스턴스)는 힙 영역에 관리되는 것을 확인했습니다. 이제 나머지 하나가 남았습니다. 바로 메서드 영역입니다. 메서드 영역이 관리하는 변수도 있습니다. 이것을 이해하기 위해서는 먼저 static 키워드를 알아야합니다. static 키워드는 메서드 영역과 밀접한 연관이 있습니다.
Archive
· 2024-03-03
☕️[Java] 스택 영역
스택 영역. 다음 코드를 실행하면 스택 영역에서 어떤 변화가 있는지 확인해봅시다. JavaMemoryMain1 package memory; public class JavaMemoryMain1 { public static void main(String[] args) { System.out.println("main start"); method1(10); System.out.println("main end"); } static void method1(int m1) { System.out.println("method1 start"); int cal = m1 * 2; method2(cal); System.out.println("method1 end"); } static void method2(int m2) { System.out.println("method2 start"); System.out.println("method2 end"); } } 실행 결과 main start method1 start method2 start method2 end method1 end main end 호출 그림 처음 자바 프로그램을 실행하면 main()을 실행합니다. 이때 main()을 위한 스택 프레임이 하나 생성됩니다. main() 스택 프레임은 내부에 args라는 매개변수를 가집니다. main()은 method1()을 호출합니다. method1() 스택 프레임이 생성됩니다. method1()는 m1, cal 지역 변수(매개변수 포함)를 가지므로 해당 지역 변수들이 스택 프레임에 포함됩니다. method1()은 method2()를 호출합니다. method2 스택 프레임이 생성됩니다. method2()는 m2 지역 변수(매개변수 포함)를 가지므로 해당 지역 변수가 스택 프레임에 포함됩니다. 종료 그림 method2()가 종료됩니다. 이때 method2() 스택 프레임이 제거되고, 매개변수 m2도 제거됩니다. method2() 스택 프레임이 제거 되었으므로 프로그램은 method1()으로 돌아갑니다. 물론method1()을 처음부터 시작하는 것이 아니라 method1()에서 method2()를 호출한 지점으로 돌아갑니다. method1()이 종료됩니다. 이때 method1() 스택 프레임이 제거되고, 지역 변수(매개변수 포함) m1, cal도 제거됩니다. 프로그램은 main()으로 돌아갑니다. main()이 종료됩니다. 더 이상 호출할 메서드가 없고, 스택 프레임도 완전히 비워졌습니다. 자바는 프로그램을 정리하고 종료합니다. 정리 자바는 스택 영역을 사용해서 메서드 호출과 지역 변수(매개변수 포함)를 관리합니다. 메서드를 계속 호출하면 스택 프레임이 계속 쌓입니다. 지역 변수(매개변수 포함)는 스택 영역에서 관리합니다. 스택 프레임이 종료되면 지역 변수도 함께 제거됩니다. 스택 프레임이 모두 제거되면 프로그램도 종료됩니다.
Archive
· 2024-03-03
☕️[Java] 자바 메모리 구조
자바 메모리 구조. 자바 메모리 구조 - 비유 자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역, 3개로 나눌 수 있습니다. 메서드 영역: 클래스 정보를 보관합니다. 이 클래스 정보가 붕어빵 틀입니다. 스택 영역: 실제 프로그램이 실행되는 영역입니다. 메서드를 실행할 때 마다 하나씩 쌓입니다. 힙 영역: 객체(인스턴스)가 생성되는 영역입니다. new 명령어를 사용하면 이 영역을 사용합니다. 쉽게 이야기해서 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간입니다. 참고로 배열도 이 영역에 생성됩니다. 위 설명한 내용은 쉽게 비유로 한 것이고 실제는 다음과 같습니다. 메서드 영역(Method Are): 메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리합니다. 이영역은 프로그램의 모든 영역에서 공유합니다. 클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재합니다. static영역: static 변수들을 보관합니다. 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관합니다. 예를 들어서 프로그램에 "hello"라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리합니다. 이외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리합니다. 스택 영역(Stack Area): 자바 실행 시, 하나의 실행 스택이 생성됩니다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함합니다. 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임입니다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거됩니다. 힙 영역(Heap Area): 객체(인스턴스)와 배열이 생성되는 영역입니다. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거됩니다. 참고: 스택 영역은 더 정확히는 각 쓰레드별로 하나의 실행 스택이 생성됩니다. 따라서 쓰레드 수 만큼 스택 영역이 생성됩니다. 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나입니다. 쓰레드에 대한 부분은 멀티 쓰레드를 학습해야 이해할 수 있습니다. 메서드 코드는 메서드 영역에 자바에서 특정 클래스로 100개의 인스턴스를 생성하면, 힙 메모리에 100개의 인스턴스가 생깁니다. 각각의 인스턴스는 내부에 변수와 메서드를 가집니다. 같은 클래스로 부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만, 메서드는 공통된 코드를 공유합니다. 따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없습니다. 메서드는 메서드 영역에서 공통으로 관리되고 실행됩니다. 정리하면 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행합니다.
Archive
· 2024-03-03
☕️[Java] 캡슐화
캡슐화 캡슐화(Encapsulation)는 객체 지향 프로그래밍의 중요한 개념 중 하나입니다. 캡슐화는 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말합니다. 캡슐화를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있습니다. 캡슐화는 쉽게 이야기해서 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 너머지는 모두 내부로 숨기는 것입니다. 이전에 객체 지향 프로그래밍을 설명하면서 캡슐화에 대해 알아보았습니다. 이때는 데이터와 데이터를 처리하는 메서드를 하나로 모으는 것에 초점을 맞추었습니다. 여기서 한발짝 더 나아가 캡슐화를 안전하게 완성할 수 있게 해주는 장치가 바로 “접근 제어자” 입니다. 그럼 어떤 것을 숨기고 어떤 것을 노출해야 할까요? 1. 데이터를 숨겨라. 객체에는 속성(데이터)과 기능(메서드)이 있습니다. 캡슐화에서 가장 필수로 숨겨야 하는 것은 “속성(데이터)” 입니다. package access; public class Speaker { private int volume; Speaker(int volume) { this.volume = volume; } void volumeUp() { if (volume >= 100) { System.out.println("음량을 증가할 수 없습니다. 최대 음량입니다."); } else { volume += 10; System.out.println("음량을 증가합니다."); } } void volumeDown() { volume -= 10; System.out.println("volumeDown 호출"); } void showVolume() { System.out.println("현재 음량: " + volume); } } 위 코드에서의 Speaker의 volume을 봐봅시다. 객체 내부의 데이터를 외부에서 함부로 접근하게 둔다면, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있습니다. 결국 모든 안전망을 다 빠져나가게 됩니다. 따라서 캡슐화가 깨집니다. 우리가 자동자를 운전할 때 자동차 부품을 다 열어서 그 안에 있는 속도계를 직접 조절하지 않습니다. 단지 자동차가 제공하는 액셀 기능을 사용해서 액셀을 밟으면 자동차가 나머지는 다 알아서 하는 것입니다. 우리가 일상에서 생각할 수 있는 음악 플레이어를 떠올려봅시다. 음악 플레이어를 사용할 때 그 내부에 들어있는 전원부나, 볼륨 상태의 데이터를 직접 수정할 일이 있을까요? 우리는 그냥 음악 플레이어의 켜고, 끄고, 볼륨을 조절하는 버튼을 누를 뿐입니다. 그 내부에 있는 전원부나, 볼륨의 조절하는 버튼을 누를 뿐입니다. 그 내부에 있는 전원부나, 볼륨의 상태 데이터를 직접 수정하지는 않습니다. 전원 버튼을 눌렀을 때 실제 전원을 받아서 전원을 켜는 것은 음악 플레이어의 일입니다. 볼륨을 높였을 때 내부에 있는 볼륨 장치들을 움직이고 볼륨 수치를 조절하는 것도 음악 플레이어가 스스로 해야하는 일입니다. 쉽게 이야기해서 우리는 음악 플레이어가 제공하는 기능을 통해서 음악 플레이어를 사용하는 것입니다. 복잡하게 음악 플레이어의 내부를 까서 그 내부 데이터까지 우리가 직접 사용하는 것은 아닙니다. 객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 합니다. 2. 기능을 숨겨라. “객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있습니다.” 이런 기능도 모두 감추는 것이 좋습니다. 우리가 자동차를 운정하기 위해 자동차가 제공하는 복잡한 엔진 조절 기능, 배기 기능까지 우리는 알 필요가 없습니다. 우리는 단지 렉셀과 핸들 정도의 기능만 알면 됩니다. 만약 사용자에게 이런 기능까지 모두 알려준다면, 사용자가 자동차에 대해 너무 많은 것을 알아야 합니다. “사용자 입장에서 꼭 필요한 기능만 외부에 노출합시다. 나머지 기능은 모두 내부로 숨깁시다.” “정리하면 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화입니다.” 이번에는 잘 캡슐화된 예제를 하나 만들어봅시다. BankAccount package access; public class BankAccount { private int balance; public BankAccount() { balance = 0; } // public 메서드: deposit public void deposit(int amount) { if (isAmountValid(amount)) { balance += amount; System.out.println(amount + "원이 입금되었습니다."); } else { System.out.println("유효하지 않은 금액입니다."); } } // public 메서드: withdraw public void withdraw(int amount) { if (isAmountValid(amount) && (balance - amount >= 0)) { balance -= amount; System.out.println(amount + "원이 출금되었습니다."); } else { System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다."); } } // public 메서드: getBalance public int getBalance() { return balance; } private boolean isAmountValid(int amount) { // 금액이 0보다 커야함. return amount > 0; } } BankAccountMain package access; public class BankAccountMain { public static void main(String[] args) { BankAccount account = new BankAccount(); account.deposit(10000); account.withdraw(3000); System.out.println("balance = " + account.getBalance()); } } 위 코드는 은행 계좌 기능을 다룹니다. 위 코드는 다음과 같은 기능을 가지고 있습니다. private balance : 데이터 필드는 외부에 직접 노출하지 않습니다. BankAccount가 제공하는 메서드를 통해서만 접근할 수 있슷빈다. isAmountValid() : 입력 금액을 검증하는 기능은 내부에서만 필요한 기능입니다. 따라서 private을 사용했습니다. pulbic deposit() : 입금 withdraw() : 출금 getBalance() : 잔고 BankAccount를 사용하는 입장에서는 단 3가지 메서드만 알면 됩니다. 나머지 복잡한 내용은 모두 BankAccount 내부에 숨어있습니다. “만약 isAmountValid()를 외부에 노출할 경우 어떻게될까요?” BankAccount를 사용하는 개발자 입장에서는 사용할 수 있는 메서드가 하나 더 늘었습니다. 여러분이 BankAccount를 사용하는 개발자라면 어떤 생각을 할까요? 아마도 입금과 출금 전에 본인이 먼저 isAmountValid()를 사용해서 검증을 해야 하나? 라고 의문을 가질 것입니다. “만약 balance 필드를 외부에 노출하면 어떻게 될까요?” BankAccount를 사용하는 개발자 입장에서는 이 필드를 직접 사용해도 된다고 생각할 수 있습니다. “왜냐하면 외부에 공개하는 것은 그것을 외부에서 사용해도 된다는 뜻이기 때문입니다.” “결국 모든 검증과 캡슐화가 깨지고 잔고를 무한정 늘리고 출금하는 심각한 문제가 발생할 수 있습니다.” “접근 제어자와 캡슐화를 통해 데이터를 안전하게 보호하는 것은 물론이고, BankAccount를 사용하는 개발자 입장에서 해당 기능을 사용하는 복잡도도 낮출 수 있습니다.”
Archive
· 2024-03-01
☕️[Java] 접근 제어자의 사용 - 클래스 레벨
접근 제어자의 사용 - 클래스 레벨 클래스 레벨의 접근 제어자 규칙 클래스 레벨의 접근 제어자는 public, default만 사용할 수 있습니다. private, protected는 사용할 수 없습니다. public 클래스는 반드시 파일명과 이름이 같아야 합니다. 하나의 자바 파일에 public 클래스는 하나만 등장할 수 있습니다. 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스는 무한정 만들 수 있습니다. PublicClass.java 파일 package access.a; public class PublicClass { pulbic static void main(String[] args) { PublicClass publicClass = new PublicClass(); DefaultClass1 class1 = new DefaultClass1(); DefaultClass2 class2 = new DefaultClass2(); } } class DefaultClass1 { } class DefaultClass2 { } 패키지 위치는 package access.a입니다. 패키지 위치를 꼭 맞추어야 합니다. 주의합시다. PublicClass라는 이름의 클래스를 만들었습니다. 이 클래스는 public 접근 제어자입니다. 따라서 파일명과 이 클래스의 이름이 반드시 같아야 합니다. 이 클래스는 public이기 때문에 외부에서 접근할 수 있습니다. DefaultClass1, DefaultClass2는 default 접근 제어자입니다. 이 클래스는 default이기 때문에 같은 패키지 내부에서만 접근할 수 있습니다. PublicClass의 main()을 보면 각각의 클래스를 사용하는 예를 보여줍니다. PublicClass의 main()을 보면 각각의 클래스를 사용하는 예를 보여줍니다. PublicClass는 public 접근 제어입니다. 따라서 어디서든 사용할 수 있습니다. DefaultClass1, DefaultClass2와는 같은 패키지에 있으므로 사용할 수 있습니다. PublicClassInnerMain package access.a; public class PublicClassInnerMain { public static void main(String[] args) { PublicClass publicClass = new PublicClass(); DefaultClass1 class1 = new DefaultClass1(); DefaultClass2 class2 = new DefaultClass2(); } } 패키지 위치는 package access.a입니다. 패키지 위치를 꼭 맞춰줘야합니다. PublicClass는 public 클래스입니다. 따라서 외부에서 접근할 수 있습니다. PublicClassInnerMain와 DefaultClass1, DefaultClass2는 같은 패키지입니다. 따라서 접근할 수 있습니다. PublicClassOuterMain package access.b; //import access.a.DefaultClass1; //import access.a.DefaultClass2; import access.a.PublicClass; public class PublicClassOuterMain { public static void main(String[] args) { PublicClass publicClass = new PublicClass(); // 다른 패키지 접근 불가. //DefaultClass1 class1 = new DefaultClass1(); //DefaultClass2 class2 = new DefaultClass2(); } } 패키지 위치는 package accesss.b 입니다. 패키지 위치를 꼭 맞춰야합니다. PublicClass는 public 클래스입니다. 따라서 외부에서 접근할 수 있습니다. PublicClassOuterMain와 DefaultClass1, DefaultClass2는 다른 패키지입니다. 따라서 접근할 수 없습니다.
Archive
· 2024-02-28
☕️[Java] 접근 제어자의 사용 - 필드, 메서드
접근 제어자의 사용 - 필드, 메서드 다양한 상황에 따른 접근 제어자를 확인해봅시다. “주의! 지금부터는 패키지 위치가 매우 중요합니다. 패키지 위치에 주의하세요.” 필드, 메서드 레벨의 접근 제어자 AccessData package access.a; public class AccessData { public int publicField; int defaultField; private int privateField; public void publicMethod() { System.out.println("publicMethod 호출 " + publicField); } void defaultMethod() { System.out.println("defaultMethod 호출 " + defaultField); } private void privateMethod() { System.out.println("privateMethod 호출 " + privateField); } public void innerAccess() { System.out.println("내부 호출"); publicField = 100; defaultField = 200; privateField = 300; publicMethod(); defaultMethod(); privateMethod(); } } 패키지 위치는 package access.a입니다. 패키지 위치를 꼭 맞추어야 합니다. 주의합시다. 순서대로 public, default, private을 필드와 메서드에 사용했습니다. 마지막에 innerAccess()가 있는데, 이 메서드는 내부 호출을 보여줍니다. 내부 호출은 자기 자신에게 접근하는 것입니다. 따라서 private을 포함함 모든 곳에 접근할 수 있습니다. 이제 외부에서 이 클래스에 접근해 봅시다. AccessInnerMain package access.a; public class AccessInnerMain { public static void main(String[] args) { AccessData data = new AccessData(); // public 호출 가능 data.publicField = 1; data.publicMethod(); // 같은 패키지 default 호출 가능 data.defaultField = 2; data.defaultMethod(); // private 호출 불가 //data.privateField = 3; //data.privateMethod(); data.innerAccess(); } } 패키지 위치는 package access.a입니다. 패키지 위치를 꼭 맞춰야 합니다. 주의합시다. public은 모든 접근을 허용하기 때문에 필드, 메서드 모두 접근 가능합니다. defaul는 같은 패키지에서 접근할 수 있습니다. AccessInnerMain은 AccessData와 같은 패키지입니다. 따라서 default 접근 제어자에 접근할 수 있습니다. private은 AccessData 내분에서만 접근할 수 있습니다. 따라서 호출 불가입니다. AccessData.innerAccess() 메서드는 public입니다. 따라서 외부에서 호출할 수 있습니다. innerAccess() 메서드는 외부에서 호출되었지만 innerAccess() 메서드는 AccessData에 포함되어 있습니다. 이 메서드는 자신의 private 필드와 메서드에 모두 접근할 수 있습니다. 실행 결과 publicMethod 호출 1 defaultMethod 호출 2 내부 호출 publicMethod 호출 100 defaultMethod 호출 200 privateMethod 호출 300 AccessOuterMain package access.b; import access.a.AccessData; public class AccessOuterMain { public static void main(String[] args) { AccessData data = new AccessData(); // public 호출 가능 data.publicField = 1; data.publicMethod(); // 다른 패키지 default 호출 가능 //data.defaultField = 2; //data.defaultMethod(); // private 호출 불가 //data.privateField = 3; //data.privateMethod(); data.innerAccess(); } } 패키지 위치는 package access.b 입니다. 패키지 위치를 꼭 맞추어야 합니다. 주의합시다. public은 모든 접근을 허용하기 때문에 필드, 메서드 모두 접근할 수 있습니다. default는 같은 패키지에서 접근할 수 있습니다. access.b.AccessOuterMain은 access.a.AccessData와 다른 패키지 입니다. 따라서 default 접근 제어자에 접근할 수 없습니다. private은 AccessData 내부에서만 접근할 수 있습니다. 따라서 호출 불가입니다. AccessData.innerAccess() 메서드는 public입니다. 따라서 외부에서 호출할 수 있습니다. innerAccess() 메서드는 외부에서 호출되었지만 해당 메서드 안에서는 자신의 private 필드와 메서드에 접근할 수 있습니다. 실행 결과 publicMethod 호출 1 내부 호출 publicMethod 호출 100 defaultMethod 호출 200 privateMethod 호출 300 참고로 생성자도 접근 제어자 관점에서 메서드와 같습니다.
Archive
· 2024-02-28
☕️[Java] 접근 제어자의 종류
접근 제어자의 종류. 자바는 4가지 종류의 접근 제어자를 제공합니다. 접근 제어자의 종류 private: 모든 외부 호출을 막습니다. default(package-private): 같은 패키지안에서 호출은 허용합니다. protected: 같은 패키지 안에서 호출은 허용합니다. 패키지가 달라도 상속 관계의 호출은 허용합니다. public: 모든 외부 호출을 허용합니다. 순서대로 private이 가장 많이 차단하고, public이 가장 많이 허용합니다. private -> default -> protected -> public package-private 접근제어자를 명시하지 않으면 같은 패키지 안에서 호출을 허용하는 default 접근 제어자가 적용됩니다. default라는 용어는 해당 접근 제어자가 기본값으로 사용되기 때문에 붙여진 이름이지만, 실제로는 package-private이 더 정확한 표현힙니다. 왜냐하면 해당 접근 제어자를 사용하는 멤버는 동일한 패키지 내의 다른 클래스에서만 접근이 가능하기 때문입니다. 참고로 두 용어를 함께 사용합니다. 접근 제어자 사용 위치 접근 제어자는 필드와 메서드, 생성자에 사용됩니다. 추가로 클래스 레벨에도 일부 접근 제어자를 사용할 수 있습니다. 접근 제어자 예시 public class Speacker { // 클래스 레벨 private init volume; //필드 public Speaker(int volume) {} // 생성자 public void volumeUp() {} // 메서드 public void volumeDown() {} public void showVolume() {} } 접근제어자의 핵심은 속성과 기능을 외부로부터 숨기는 것입니다. private은 나의 클래스 안으로 속성과 기능을 숨길 때 사용, 외부 클래스에서 해당 기능을 호출할 수 없습니다. default는 나의 패키지 안으로 속성과 기능을 숨길 때 사용, 외부 패키지에서 해당 기능을 호출할 수 없습니다. protected는 상속 관계로 속성과 기능을 숨길 때 사용, 상속 관계가 아닌 곳에서 해당 기능을 호출할 수 없습니다. public은 기능을 숨기지 않고 어디서든 호출할 수 있게 공개합니다.
Archive
· 2024-02-28
☕️[Java] 접근 제어자 이해 2
접근 제어자 이해 2 이 문제를 근본적으로 해결하는 방법은 volume 필드를 Speaker 클래스 외부에서는 접근하지 못하게 막는 것 입니다. Speaker - volume 접근 제어자를 private으로 수정 package access; public class Speaker { private int volume; // private 사용 ... } private 접근 제어자는 모든 외부 호출을 막습니다. 따라서 private이 붙는 경우 해당 클래스 내부에서만 호출할 수 있습니다. volume 필드 - private 변경 후 volume 필드를 private을 사용해서 Speaker 내부에 숨겼습니다. 외부에서 volume 필드에 직접 접근할 수 없게 막은 것입니다. volume 필드는 이제 Speaker 내부에서만 접근할 수 있습니다. SpeakerMain 코드를 다시 실행해보겠습니다. // 필드에 직접 접근 System.out.println("volume 필드 직접 접근 수정"); speaker.volume = 200; // private 접근 오류 IDE에서 speaker.volume = 200 부분에 오류가 발생하는 것을 확인할 수 있습니다. 실행해보면 다음과 같은 컴파일 오류가 발생합니다. 컴파일 오류 메시지 java: volume has private access in access.Speaker volume 필드는 private으로 설정되어 있기 때문에 외부에서 접근할 수 없다는 오류입니다. volume 필드 직접 접근 - 주석 처리 // 필드에 직접 접근 System.out.println("volume 필드에 직접 접근 수정"); //speaker.volume = 200; //private 접근 오류 speaker.showVolume(); 이제 Speaker 외부에서 volume 필드에직접 접근하는 것은 불가능합니다. 이 경우 자바 컴파일러가 컴파일 오류를 발생시킵니다. 프로그램을 실행하기 위해서 volume 필드에 직접 접근하는 코드를 주석 처리합니다. 만약 Speaker 클래스를 개발하는 개발자가 처음부터 private을 사용해서 volume 필드의 외부 접근을 막아두었다면 어떠했을까요? 새로운 개발자도 volume 필드에 직접 접근하지 않고, volumeUp() 과 같은 메서드를 통해서 접근했을 것입니다. 결과적으로 Speaker가 폭발하는 문제는 발생하지 않았을 것입니다. 참고: 좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약을 제공하는 프로그램입니다.
Archive
· 2024-02-27
☕️[Java] 접근 제어자 이해 1
접근 제어자 접근 제어자 이해 1. 자바는 public, private 같은 접근 제어자(access modifier)를 제공합니다. 접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있습니다. 이런 접근 제어자가 왜 필요할까요? 예제를 통한 접근 제어자가 필요한 이유를 알아봅시다. 여러분은 스피커에 들어가는 소프트웨어 개발자 입니다. 스피커의 음량은 절대로 100을 넘으면 안된다는 요구사항이 있습니다(100을 넘어가면 스피커의 부품들이 고장납니다.) 스피커 객체를 만들어봅시다. 스피커는 음량을 높이고, 내리고, 현재 음량을 확인할 수 있는 단순한 기능을 제공합니다. 요구사항 대로 스피커의 음량은 100까지만 증가할 수 있습니다. 절대 100을 넘어가면 안됩니다. Speaker package access; public class Speaker { int volume; Speaker(int volume) { this.volume = volume; } void volumeUp() { if (volume >= 100) { System.out.println("음량을 증가할 수 없습니다. 최대 음량입니다."); } else { volume += 10; System.out.println("음량을 증가합니다."); } } void volumeDown() { volume -= 10; System.out.println("volumeDown 호출"); } void showVolume() { System.out.println("현재 음량: " + volume); } } 생성자를 통해 초기 음량 값을 지정할 수 있습니다. volumeUp() 메서드를 봅시다. 음량이 한 번에 10씩 증가합니다. 단 음량이 100을 넘게되면 더는 음량을 증가하지 않습니다. SpeakerMain package access; public class SpeakerMain { public static void main(String[] args) { Speaker speaker = new Speaker(90); speaker.showVolume(); speaker.volumeUp(); speaker.showVolume(); speaker.volumeUp(); speaker.showVolume(); } } 실행 결과 현재 음량: 90 음량을 증가합니다. 현재 음량: 100 음량을 증가할 수 없습니다. 최대 음량입니다. 현재 음량: 100 초기 음량 값을 90으로 지정했습니다. 그리고 음량을 높이는 메서드를 여러번 호출했습니다. 기대한 대로 음량은 100을 넘지 않았습니다. 프로젝트는 성공적으로 끝났습니다. 오랜 시간이 흘러 업그레이드 된 다음 버전의 스피커를 출시하게 되었습니다. 이때 새로운 개발자가 급하게 기존 코드를 이어받아서 개발하게 되었습니다. 참고로 새로운 개발자는 기존 요구사항을 잘 몰랐습니다. 코드를 실행해보니 이상하게 음량이 100이상 올라가지 않았습니다. 소리를 더 올리면 좋겠다고 생각한 개발자는 다양한 방면으로 고민했습니다. Speaker 클래스를 보니 volume 필드를 직접 사용할 수 있었습니다. volume 필드의 값을 200으로 설정하고 이 코드를 실행한 순간 스피터의 부풀들이 과부하가 걸리면서 폭발했습니다. SpeakerMain - 필드 직접 접근 코드 추가 package access; public class SpeakerMain { public static void main(String[] args) { Speaker speaker = new Speaker(90); speaker.showVolume(); speaker.volumeUp(); speaker.showVolume(); speaker.volumeUp(); speaker.showVolume(); // 필드에 직접 접근 System.out.println("volume 필드에 직접 접근 수정"); speaker.volume = 200; speaker.showVolume(); } } 실행 결과 현재 음량: 90 음량을 증가합니다. 현재 음량: 100 음량을 증가할 수 없습니다. 최대 음량입니다. 현재 음량: 100 volume 필드에 직접 접근 수정 현재 음량: 200 Speaker 객체를 사용하는 사용자는 Speaker의 volume 필드와 메서드에 모두 접근할 수 있습니다. 앞서 volumeUp()과 같은 메서드를 만들어서 음량이 100을 넘지 못하도록 개발했지만 소용이 없습니다. 왜냐하면 Speaker를 사용하는 입장에서는 volume 필드에 직접 접근해서 원하는 값을 설정할 수 있기 때문입니다. “이런 문제를 근본적으로 해결하기 위해서는 volume 필드의 외부 접근을 막을 수 있는 방법이 필요합니다.”
Archive
· 2024-02-27
☕️[Java] 패키지 활용
패키지 활용 실제 패키지가 어떤 식으로 사용되는지 예제를 통해서 알아봅시다. 실제 동작하는 코드는 아니지만, 큰 애플리케이션은 대략 이런식으로 패키지를 구성한다고 이해하면 됩니다. 참고로 이것은 정답이 가니고 프로젝트 규모와 아키텍처에 따라서 달라집니다. 전체 구조도 com.helloshop user User UserService product Product ProductService order Order OrderService OrderHistory com.helloshop.user 패키지 package com.helloshop.user; public class User { String userId; String name; } package com.helloshop.user; public class UserService { } com.helloshop.product 패키지 package com.helloshop.product; public class Product { String productId; int price; } package com.helloshop.product; public class ProductService { } com.helloshop.order 패키지 package com.helloshop.order; import com.helloshop.product.Product; import com.helloshop.user.User; public class Order { User user; Product product; public Order(User user, Product product) { this.user = user; this.product = product; } } package com.helloshop.order; import com.helloshop.product.Product; import com.helloshop.user.User; public class OrderService { public void order() { User user = new User(); Product product = new Product(); Order order = new Order(user, product); } } package com.helloshop.order; public class OrderHistory { } 패키지를 구성할 때 서로 관련된 클래스는 하나의 패키지에 모으고, 관련이 적은 클래스는 다른 패키지로 분리하는 것이 좋습니다.
Archive
· 2024-02-26
☕️[Java] 패키지 규칙
패키지 규칙. 패키지 규칙. “패키지의 이름과 위치는 폴더(디렉토리) 위치와 같아야 합니다.(필수)” 패키지 이름은 모두 소문자를 사용합니다.(관례) 패키지의 앞 부분에는 일반적으로 회사의 도메인 이름을 거꾸로 사용합니다. 예를 들어, com.company.myapp과 같이 사용합니다(관례) 이 부분은 필수는 아닙니다. 하지만 수 많은 외부 라이브러리가 함께 사용되면 같은 패키지에 같은 클래스 이름이 존재할 수도 있습니다. 이렇게 도메인 이름을 거꾸로 사용하면 이런 문제를 방지할 수 있습니다. 내가 오픈소스나 라이브러리를 만들어서 외부에 제공한다면 꼭 지키는 것이 좋습니다. 내가 만들 애플리케이션을 다른 곳에 공유하지 않고, 직접 배포한다면 보통 문제가 되지 않습니다. 패키지와 계층 구조. 패키지는 보통 다음과 같이 계층 구조를 이룹니다. a b c 이렇게 하면 다음과 같이 총 3개의 패키지가 존재하게 됩니다. a, a.b, a.c 계층 구조상 a 패키지 하위에 a.b 패키지와 a.c 패키지가 있습니다. 그런데 이것은 우리 눈에 보기에 계층 구조를 이룰 뿐입니다. a 패키지와 a.b,a.c 패키지는 서로 완전히 다른 패키지입니다. 따라서 a 패키지의 클래스에서 a.b 패키지의 클래스가 필요하다면 import 해서 사용해야 합니다. 반대도 물론 마찬가지 입니다. 정리 패키지가 계층 구조를 이루더라도 모든 패키지는 서로 다른 패키지입니다. 물론 사람이 이해하기 쉽게 계층 구조를 잘 활용해서 패키지를 분류하는 것은 좋습니다. 참고로 카테고리는 보통 큰 분류에서 세세한 분류로 점점 나누어집니다. 패키지도 마찬가지입니다.
Archive
· 2024-02-26
☕️[Java] 패키지 - 시작
패키지 - 시작. 쇼핑몰 시스템을 개발한다고 가정해봅시다. 다음과 같이 프로그램이 매우 작고 단순해서 클래스가 몇개 없다면 크게 고민할 거리가 없겠지만, 기능이 점점 추가되어서 프로그램이 아주 커지게 된다면 어떻게 될까요? 아주 작은 프로그램 Order User Product 큰 프로그램 User UserManager UserHistory Product ProductCatalog ProductImage Order OrderService OrderHistory ShoppingCart CartItem Payment PaymentHistory Shipment ShipmentTracker 매우 많은 클래스가 등장하면서 관련 있는 기능들을 분류해서 관리하고 싶을 것입니다. 컴퓨터는 보통 파일을 분류하기 위해 폴더, 디렉토리라는 개념을 제공합니다. 자바도 이런 개념을 제공하는데, 이것이 바로 패키지입니다. 다음과 같이 카테고리를 만들고 분류해봅시다. * user * User * UserManager * UserHistory * product * Product * ProductCatalog * ProductImage * order * Order * OrderService * OrderHistory * cart * ShoppingCart * CartItem * payment * Payment * PaymentHistory * shipping * Shipment * ShipmentTracker 여기서 user, product 등이 바로 패키지입니다. 그리고 해당 패키지 안에 관련된 자바 클래스들을 넣으면 됩니다. 패키지(package)는 이름 그대로 물건을 운송하기 위한 포장 용기나 그 포장 묶음을 뜻합니다. 패키지 사용 패키지 사용법을 코드로 확인해봅시다. 패키지를 먼저 만들고 그 다음에 클래스를 만들어야 합니다. 패키지 위치에 주의해야합니다. pcak.Data package pack; public class Data { public Data() { System.out.println("패키지 pack Data 생성") } } 패키지를 사용하는 경우 항상 코드 첫 줄에 package pack과 같이 패키지 이름을 적어주어야 합니다. 여기서는 pack 패키지에 Data 클래스를 만들었습니다. 이후에 Data 인스턴스가 생성되면 생성자를 통해 정보를 출력합니다. pack.a.User package pack.a; public class User { public User() { System.out.println("패키지 pack.a 회원 생성"); } } pack 하위에 a라는 패키지를 먼저 만듭니다. pack.a 패키지에 User 클래스를 만들었습니다. 이후에 User 인스턴스가 생성되면 생성자를 통해 정보를 출력합니다. 참고: 생성자에 public을 사용했습니다. 다른 패키지에서 이 클래스의 생성자를 호출하려면 public을 사용해야합니다. pack.PackageMain1 package pack; public class PackageMain1 { public static void main(String[] args) { Data data = new Data(); pack.a.User user = new pack.a.User(); } } pack 패키지 위치에 PackageMain1 클래스를 만들었습니다. 실행 결과 패키지 pack Data 생성 패키지 pack.a 회원 생성 사용자와 같은 위치: PackageMain1과 Data는 같은 pack이라는 패키지에 소속되어 있습니다. 이렇게 같은 패키지에 있는 경우에는 패키지 경로를 생략해도 됩니다. 사용자와 다른 위치: PackageMain1과 User는 서로 다른 패키지입니다. 이렇게 패키지가 다르면 pack.a.User와 같이 패키지 전체 경로를 포함해서 클래스를 적어주어야 합니다.
Archive
· 2024-02-25
☕️[Java] 패키지 - import
패키지 - import import package pack; public class PackageMain1 { public static void main(String[] args) { Data data = new Data(); pack.a.User user = new pack.a.User(); } } 위 코드와 같이 패키지가 다르다고 pack.a.User와 같이 항상 전체 경로를 적어주는 것은 불편합니다. “이때는 import를 사용하면 됩니다.” PackageMain2 package pack; import pack.a.User; public class PackageMain2 { public static void main(String[] args) { Data data = new Data(); User user = new User(); } } 실행 결과 패키지 pack Data 생성 패키지 pack.a 회원 생성 코드에서 첫줄에는 package를 사용하고, 다음 줄에는 import를 사용할 수 있습니다. import를 사용하면 다른 패키지에 있는 클래스를 가져와서 사용할 수 있습니다. import를 사용한 덕분에 코드에서는 패키지 명을 생략하고 클래스 이름만 적을 수 있습니다. 참고로 특정 패키지에 포함된 모든 클래스를 포함해서 사용하고 싶으면 import 시점에 *(별)을 사용하면 됩니다. 패키지 별(*)사용 tree 구조 * pack -> [Package] ├──* Data -> (Class) ├──* PackageMain1 -> (Class) ├──* PackageMain1 -> (Class) └──* a -> [Package] ├──* User -> (Class) └──* UserManager -> (Class) package pack; import pack.a.*; // pack.a의 모든 클래스 사용 public class PackageMain2 { public static void main(String[] args) { Data data = new Data(); User user = new User(); // import 사용으로 패키지 명 생략 가능 UserManager manager = new UserManager(); // import 사용으로 패키지 명 생략 가능 } } 이렇게 하면 pack.a 패키지에 있는 모든 클래스를 패키지 명을 생략하고 사용할 수 있습니다. 실행 결과 패키지 pack Data 생성 패키지 pack.a 회원 생성 패키지 pack.a 회원 매니저 생성 클래스 이름 중복 패키지 덕분에 클래스 이름이 같아도 패키지 이름으로 구분해서 같은 이름의 클래스를 사용할 수 있습니다. pack.a.User pack.b.User “이런 경우 클래스 이름이 둘다 User이지만 패키지 이름으로 대상을 구분할 수 있습니다.” 이렇게 이름이 같은 경우 둘다 사용하고 싶으면 어떻게 해야할까요? pack.b.User package pack.b; public class User { public User() { System.out.println("패키지 pack.b 회원 생성"); } } PackageMain3 package pack; import pack.a.User; public class PackageMain3 { public static void main(String[] args) { User userA = new User(); pack.b.User userB = new pack.b.User(); } } 같은 이름의 클래스가 있다면 import는 둘중 하나만 선택할 수 있습니다. 이때는 자주 사용하는 클래스를 import하고 나머지를 패키지를 포함한 전체 경로를 적어주면 됩니다. 물론 둘다 전체 경로를 적어준다면 import를 사용하지 않아도 됩니다.
Archive
· 2024-02-25
☕️[Java] 생성자 - 오버로딩 this()
생성자 - 오버로딩 this() 생성자도 메서드 오버로딩처럼 매개변수만 다르게 해서 여러 생성자를 제공할 수 있습니다. MemberConstruct - 생성자 추가 package construct; public class MemberConstruct { String name; int age; int grade; // 추가 MemberConstruct(String name, int age) { this.name = name; this.age = age; this.grade = 50; } MemberConstruct(String name, int age, int grade) { System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade); this.name = name; this.age = age; this.grade = grade; } } 기존 MemberConstruct에 생성자를 하나 추가해서 생성자가 2개가 되었습니다. MemberConstruct(String name, int age) MemberConstruct(String name, int age, int grade) 새로 추가한 생성자는 grade를 받지 않습니다. 대신에 grade는 50점이 됩니다. package construct; public class ConstructMain2 { public static void main(String[] args) { MemberConstruct member1 = new MemberConstruct("user1", 15, 90); MemberConstruct member2 = new MemberConstruct("user2", 16); MemberConstruct[] members = { member1, member2 }; for (MemberConstruct member : members) { System.out.println("이름:" + member.name + " 나이:" + member.age + "성적:" + member.grade); } } } 실행 결과 생성자 호출 name=user1,age=15,grade=90 이름:user1 나이:15성적:90 이름:user2 나이:16성적:50 생성자를 오버로딩 한 덕분에 성적 입력이 꼭 필요한 경우에는 grade가 있는 생성자를 호출하면 되고, 그렇지 않은 경우에는 grade가 없는 생성자를 호출하면 됩니다. grade가 없는 생성자를 호출하면 성적은 50점이 됩니다. this() 두 생성자를 비교해 보면 코드가 중복되는 부분이 있습니다. public MemberConstruct(String name, int age) { this.name = name; this.age = age; this.grade = 50; } public MemberConstruct(String name, int age, int grade) { this.name = name; this.age = age; this.grade = grade; } 바로 다음 부분입니다. this.name = name; this.age = age; 이때 this()라는 기능을 사용하면 생성자 내부에서 자신의 생성자를 호출할 수 있습니다. 참고로 this는 인스턴스 자신의 참조값을 가리킵니다. 그래서 자신의 생성자를 호출한다고 생각하면됩니다. MemberConstruct - this() 사용 package construct; public class MemberConstruct { String name; int age; int grade; // 추가 MemberConstruct(String name, int age) { this(name, age, 50); } MemberConstruct(String name, int age, int grade) { System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade); this.name = name; this.age = age; this.grade = grade; } } 이 코드는 첫번째 생성자 내부에서 두번째 생성자를 호출합니다. MemberConstruct(String name, int age) -> MemberConstruct(String name, int age, int grade) this()를 사용하면 생성자 내부에서 다른 생성자를 호출할 수 있습니다. 이 부분을 잘 활용하면 지금과 같이 중복을 제거할 수 있습니다. 물론 실행 결과는 기존과 같습니다. this() 규칙 this()는 생성자 코드의 첫줄에만 작성할 수 있습니다. 다음은 규칙 위반입니다. 이 경우 컴파일 오류가 발생합니다. public MemberConstruct(String name, int age) { Sytem.out.println("go"); this(name, age, 50); } this()가 생성자 코드의 첫줄에 사용되지 않았습니다.
Archive
· 2024-02-25
☕️[Java] 기본 생성자
기본 생성자 아래의 코드를 봐봅시다. 생각해보면 아래의 코드는 생성자를 만들지 않았는데, 생성자를 호출한 적이 있습니다. 확인해 봅시다. public class MemberInit { String name; int age; int grade; } public class MethodInitMain1 { public static void main(String[] args) { MemberInit member1 = new MemberInit(); ... } } 여기서 new MemberInit() 이 부분은 분명히 매개변수가 없는 다음과 같은 생성자가 필요할 것입니다. public class MemberInit { String name; int age; int grade; MemberInit() { // 생성자 필요 } } 기본 생성자 매개변수가 없는 생성자를 기본 생성자라 합니다. 클래스에 생성자가 하나도 없으면 자바 컴파일러는 매개변수가 없고, 작동하는 코드가 없는 기본 생성자를 자동으로 만들어줍니다. 생성자가 하나라도 있으면 자바는 기본 생성자를 만들지 않습니다. MemberInt 클래스의 경우 생성자를 만들지 않았으므로 자바가 자동으로 기본 생성자를 만들어준 것입니다. 예제를 통해 기본 생성자를 확인해봅시다. MemberDefault package construct; public class MemberDefault { String name; public MemberDefault() { System.out.println("생성자 호출"); } } 참고: 자바가 자동으로 생성해주는 기본 생성자는 클래스와 같은 접근 제어자를 같습니다. 물론 다음과 같이 기본 생성자를 직접 정의해도 됩니다. package construct; public class MemberDefault { String name; MemberDefault() { System.out.println("생성자 호출"); } } 실행 경과 생성자 호출 기본 생성자를 왜 자동으로 만들어줄까? 만약 가바에서 기본 생성자를 만들어주지 않는다면 생성자 기능이 필요하지 않은 경우에도 모든 클래스에 개발자가 직접 기본 생성자를 정의해야 합니다. 생성자 기능을 사용하지 않는 경우도 많기 때문에 이런 편의 기능을 제공합니다. 정리 생성자는 반드시 호출되어야 합니다. 생성자가 없으면 기본 생성자가 제공됩니다. 생성자가 하나라도 있으면 기본 생성자가 제공되지 않습니다. 이 경우 개발자가 정의한 생성자를 직접 호출해야합니다.
Archive
· 2024-02-25
☕️[Java] this
this Member - initMember() 추가 package construct; public class Member { String name; int age; int grade; // 추가 void initMember( String name, int age, int grade) { this.name = name; this.age = age; this.grade = grade; } } MethodInitMain3 package construct; public class MethodInitMain3 { public static void main(String[] args) { Member member1 = new Member(); member1.initMember("user1", 15, 90); Member member2 = new Member(); member2.initMember("user2", 16, 80); Member[] members = { member1, member2 }; for (Member member : members) { System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade); } } } initMember(...)는 Member에 초기값 설정 기능을 제공하는 메서드입니다. 다음과 같이 메서드를 호출하면 객체의 멤버 변수에 인자로 넘어온 값을 채웁니다. member1.initMember("user1", 15, 90) this Member의 코드를 다시 봐봅시다. initMember(String name...)의 코드를 보면 메서드의 매개변수에 정의한 String name과 Member의 멤버 변수의 이름이 String name으로 둘다 똑같습니다. 나머지 변수 이름도 name, age, grade로 모두 같습니다. “멤버 변수와 메스더의 매개변수의 이름이 같으면 둘을 어떨게 구분해야 할까요?” 이 경우 멤버 변수보다 매개변수가 코드 블럭의 안쪽에 있기 때문에 “매개변수가 우선 순위를” 가집니다. 따라서 initMember(String name, ...) 메서드 안에서 name이라고 적으면 매개변수에 접근하게 됩니다. 멤버 변수에 접근하려면 앞에 this.이라고 해주면 됩니다. 여기서 this는 인스턴스 자신의 참조값을 가리킵니다. 진행 과정 this.name = name; // 1. 오른쪽의 name은 매개변수에 접근 this.name = "user"; // 2. name 매개변수의 값 사용 x001.name = "user"; // 3. this.은 인스턴스 자신의 참조값을 뜻함. 따라서 인스턴스의 멤버 변수에 접근. this 제거 만약 이 예제에서 this를 제거하면 어떻게 될까요? this.name = name 다음과 같이 수정하면 name은 둘다 매개변수를 뜻하게 됩니다. 따라서 멤버변수의 값이 변경되지 않습니다. name = name; 정리 매개변수의 이름과 멤버 변수의 이름이 같은 경우 this를 사용해서 둘을 명확하게 구분해야 합니다. this는 인스턴스 자신을 가리킵니다. this의 생략 this는 생략할 수 있습니다. 이 경우 변수를 찾을 때 가까운 지역변수(매개변수도 지역변수입니다.)를 먼저 찾고 없으면 그 다음으로 멤버 변수를 찾습니다. 멤버 변수도 없으면 오루가 발생합니다. 다음 예제는 필드 이름과 매개변수의 이름이 서로 다릅니다. MemberThis package construct; public class MemberThis { String nameField; void initMember(String nameParameter) { nameField = nameParameter; } } “예를 들어서 nameField는 앞에 this가 없어도 멤버 변수에 접근합니다.” nameField는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾습니다. 이 경우 없으므로 멤버 변수에서 찾습니다. nameParametr는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾습니다. 이 경우 매개변수가 있으므로 매개변수를 사용합니다. this와 코딩 스타일 “다음과 같이 멤버 변수에 접근하는 경우에 항상 this를 사용하는 코딩 스타일도 있습니다.” MemberThis package construct; public class MemberThis { String nameField; void initMember(String nameParameter) { this.nameField = nameParameter; } } this.nameField를 보면 this를 생략할 수 있지만, 생략하지 않고 사용해도 됩니다. 이렇게 this를 사용하면 이 코드가 멤버 변수를 사용한다는 것을 눈으로 쉽게 확인할 수 있습니다. 그래서 과거에 이런 스타일을 많이 사용하기도 했습니다. 쉽게 이야기해서 this를 강제로 사용해서, 지역 변수(매개변수)와 멤버 변수를 눈에 보이도록 구분하는 것입니다. “하지만 최근에는 IDE가 발전하면서 IDE가 멤버 변수와 지역 변수를 색상으로 구분해줍니다.” 이런 점 때문에 this는 꼭 필요한 경우에만 사용해도 충분하다 생각한다. 예를 들어 이런 경우 this.name = name -> name이 중복되는 것.
Archive
· 2024-02-24
☕️[Java] 생성자 - 도입
생성자 - 도입 프로그래밍을 하다보면 객체를 생성하고 이후에 바로 초기값을 할당해야 하는 경우가 많습니다. 따라서 앞서 initMember(...)와 같은 메서드를 매번 만들어야 합니다. “그래서 대부분의 객체 지향 언어는 객체를 생성하자마자 즉시 필요한 기능을 좀 더 편리하게 수행할 수 있도록 생성자라는 기능을 제공합니다.” 생성자를 사용하면 객체를 생성하는 시점에 즉시 필요한 기능을 수행할 수 있습니다. 생성자는 앞서 살펴본 initMember(...)와 같이 메서드와 유사하지만 몇가지 다른 특징이 있습니다. 아래 코드를 보면서 이해해봅시다. MemberConstruct package construct; public class MemberConstruct { String name; int age; int grade; MemberConstruct(String name, int age, int grade) { System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade); this.name = name; this.age = age; this.grade = grade; } } “다음 부분이 바로 생성자입니다.” MemberConstruct(String name, int age, int grade) { System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade); this.name = name; this.age = age; this.grade = grade; } “생성자는 메서드와 비슷하지만 다음과 같은 차이가 있습니다.” 생성자의 이름은 클래스 이름과 같아야 합니다. 따라서 첫 글자도 대문자로 시작합니다. 생성자는 반환 타입이 없습니다. 비워두워야 합니다. 나머지는 메서드와 같습니다. ConstructMain1 package construct; public class ConstructMain1 { public static void main(String[] args) { MemberConstruct member1 = new MemberConstruct("user1", 15, 90); MemberConstruct member2 = new MemberConstruct("user2", 16, 80); MemberConstruct[] members = { member1, member2 }; for (MemberConstruct member : members) { System.out.println("이름:" + member.name + " 나이:" + member.age + "성적:" + member.grade); } } } 실행 결과 생성자 호출 name=user1,age=15,grade=90 생성자 호출 name=user2,age=16,grade=80 이름:user1 나이:15성적:90 이름:user2 나이:16성적:80 생성자 호출 생성자는 인스턴스를 생성하고 나서 즉시 호출됩니다. 생성자를 호출하는 방법은 다음 코드와 같이 new 명령어 다음에 생성자 이름과 매개변수에 맞추어 인수를 전달하면 됩니다. new 생성자이름(생성자에 맞는 인수 목록) new 클래스이름(생성자에 맞는 인수 목록) 참고로 생성자 이름이 클래스 이름이기 때문에 둘다 맞는 표현입니다. new MemberConstruct("user1", 15, 90) 이렇게 하면 인스턴스를 생성하고 즉시 해당 생성자를 호출합니다. 여기서는 Memeber 인스턴스를 생성하고 바로 MemberConstruct(String name, int age, int grade) 생성자를 호출합니다. 참고로 new 키워드를 사용해서 객체를 생성할 때 마지막에 괄호()도 포함해야 하는 이유가 바로 생성자 때문입니다. 객체를 생성하면서 동시에 생성자를 호출한다는 의미를 포함합니다. 생성자 장점 중복 호출 제거 생성자가 없던 시절에는 생성 직후에 어떤 작업을 수행하기 위해 다음과 같이 메서드를 직접 한번 더 호출해야 했습니다. “생성자 덕분에 객체를 생성하면서 동시에 생성 직후에 필요한 작업을 한번에 처리할 수 있게 되었습니다.” // 생성자 등장 전 MemberInit member = new MemberInit(); member.initMember("user1", 15, 90); // 생성자 등장 후 MemberConstruct member = new MemberConstruct("user1", 15, 90); 제약 - 생성자 호출 필수 위 코드에서 생성자 등장 전 코드를 보며 이해해봅시다. “이 경우 initMember(...)를 실수로 호출하지 않으면 어떻게 될까요?” 이 메서드를 실수로 호출하지 않아도 프로그램은 작동합니다. 하지만 학생의 이름과 나이, 성적 데이터가 없는 상태로 프로그램이 작동하게 됩니다. “만약에 이 값들을 필수로 반드시 입력해야 한다면, 시스템에 큰 문제가 발생할 수 있습니다.” 결국 아무 정보가 없는 유령 학생이 시스템 내부에 등장하게 됩니다. 생성자의 진짜 장점은 객체를 생성할 때 직접 정의한 생성자가 있다면 “직접 정의한 생성자를 반드시 호출” 해야 한다는 점입니다. 참고로 생성자를 메서드 오버로딩 처럼 여러개 정의할 수 있는데, 이 경우에는 하나만 호출하면 됩니다. “MemberConstruct 클래스의 경우 다음 생성자를 직접 정의했기 때문에 직접 정의한 생성자를 반드시 호출해야 합니다.” MemberConstruct(String name, int age, int grade) { ... } 다음과 같이 직접 정의한 생성자를 호출하지 않으면 컴파일 오류가 발생합니다. MemberConstruct member3 = new MemberConstruct(); // 컴파일 오류 발생 member3.name = "user1"; 컴파일 오류 메시지 java: constructor MemberConstruct in class construct.MemberConstruct cannot be applied to given types; required: java.lang.String,int,int found: no arguments reason: actual and formal argument lists differ in length 컴파일 오류는 IDE에서 즉시 확인할 수 있는 좋은 오류입니다. 이 경우 개발자는 객체를 생성할 때, 직접 정의한 생성자를 필수로 호출해야 한다는 것을 바로 알 수 있습니다. 그래서 필요한 생성자를 찾아서 다음과 같이 호출할 것입니다. MemberConstruct member = new MemberConstruct("user1", 15, 90); “생성자 덕분에 학생의 이름, 나이, 성적은 항상 필수로 입력하게 됩니다.” 따라서 아무 정보가 없는 유령 회원이 시스템 내부에 등장할 가능성을 원천 차단합니다. “생성자를 사용하면 필수값 입력을 보장할 수 있습니다.” 참고: 좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약이 있는 프로그램입니다.
Archive
· 2024-02-24
☕️[Java] 생성자 - 필요한 이유
생성자 - 필요한 이유. 객체를 생성하는 시점에 어떤 작업을 하고 싶다면 “생성자(Construct)” 을 이용하면 됩니다. 생성자를 알아보기 전에 먼저 생성자가 왜 필요한지 코드로 간단히 알아봅시다. MemberInit package construct; public class MemberInit { String name; int age; int grade; } MethodInitMain1 package construct; public class MethodInitMain1 { public static void main(String[] args) { MemberInit member1 = new MemberInit(); member1.name = "user1"; member1.age = 15; member1.grade = 90; MemberInit member2 = new MemberInit(); member2.name = "user2"; member2.age = 16; member2.grade = 80; MemberInit[] members = { member1, member2 }; for (MemberInit member : members) { System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade); } } } 실행 결과 아름:user1 나이:15 성적:90 아름:user2 나이:16 성적:80 회원 객체를 생성하고 나면 name, age, grade 같은 변수에 초기값을 설정합니다. 아마도 회원 객체를 제대로 사용하기 위해서는 객체를 생성하자 마자 이런 초기값을 설정해야 할 것입니다. 이 코드에는 회원의 초기값을 설정하는 부분이 계속 반복됩니다. 메서드를 사용해서 반복을 제거해봅시다. MethodInitMain2 package construct; public class MethodInitMain2 { public static void main(String[] args) { MemberInit member1 = new MemberInit(); initMember(member1, "user1", 15, 90); MemberInit member2 = new MemberInit(); initMember(member2, "user2", 16, 80); MemberInit[] members = { member1, member2 }; for (MemberInit member : members) { System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade); } } static void initMember(MemberInit member, String name, int age, int grade) { member.name = name; member.age = age; member.grade = grade; } } initMember(...) 메서드를 사용해서 반복을 제거했습니다. 그런데 이 메서드는 대부분 MemberInit 객체의 멤버 변수를 사용합니다. 우리는 앞서 객체 지향에 대해서 학습했습니다. 이런 경우 속성과 기능을 한 곳에 두는 것이 더 나은 방법입니다. 쉽게 이야기해서 MemberInit이 자기 자신의 데이터를 변경하는 기능(메서드)을 제공하는 것이 좋습니다.
Archive
· 2024-02-24
☕️[Java] 절차 지향 프로그래밍(3)
절차 지향 프로그래밍(3) - 메서드 추출 코드를 보면 다음과 같이 중복되는 부분들이 있습니다. // 볼륨 증가 data.volume++; System.out.println("음악 플레이어 볼륨:" + data.volume); // 볼륨 증가 data.volume++; System.out.println("음악 플레이어 볼륨:" + data.volume); 그리고 각각의 기능들은 이후에 재사용 될 가능성이 높습니다. 음악 플레이어 켜기, 끄기 볼륨 증가, 감소 음악 플레이어 상태 출력 “메서드를 사용해서 각각의 기능을 구분해봅시다.” MusicPlayerMain3 package oop1; public class MusicPlayerMain3 { public static void main(String[] args) { MusicPlayerData data = new MusicPlayerData(); // 음악 플레이어 켜기 on(data); // 볼륨 증가 volumeUp(data); // 볼륨 증가 volumeUp(data); // 볼륨 감소 volumeDown(data); // 음악 플레이어 상태 showStatus(data); // 음악 플레이어 끄기 off(data); } static void on(MusicPlayerData data) { data.isOn = true; System.out.println("음악 플레이어를 시작합니다."); } static void off(MusicPlayerData data) { data.isOn = false; System.out.println("음악 플레이어를 종료합니다."); } static void volumeUp(MusicPlayerData data) { data.volume++; System.out.println("음악 플레이어 볼륨:" + data.volume); } static void volumeDown(MusicPlayerData data) { data.volume--; System.out.println("음악 플레이어 볼륨:" + data.volume); } static void showStatus(MusicPlayerData data) { System.out.println("음악 플레이어 상태 확인"); if (data.isOn) { System.out.println("음악 플레이어 ON, 볼륨:" + data.volume); } else { System.out.println("음악 플레이어 OFF"); } } } 각각의 기능을 메서도로 만든 덕분에 각각의 기능이 모듈화 되었습니다. 덕분에 다음과 같은 장점이 생겼습니다. 중복 제거: 로직 중복이 제거되었습니다. 같은 로직이 필요하면 해당 메서드를 여러번 호출하면 됩니다. 변경 영향 범위: 기능을 수정할 때 해당 메서드 내부만 변경하면 됩니다. 메서드 이름 추가: 메서드 이름을 통해 코드를 더 쉽게 이해할 수 있습니다. 모듈화: 쉽게 이야기해서 레고 블럭을 생각하면 됩니다. 필요한 블럭을 가져다 꼽아서 사용할 수 있습니다. 여기서는 음악 플레이어의 기능이 필요하면 해당 기능을 메서드 호출 만으로 손쉽게 사용할 수 있습니다. 이제 음악 플레이어와 관련된 메서드를 조립해서 프로그램을 작성할 수 있습니다. 절차 지향 프로그래밍의 한계 지금까지 클래스를 사용해서 관련된 데이터를 하나로 묶고, 또 메서드를 사용해서 각각의 기능을 모듈화했습니다. 덕분에 상당히 깔끔하고 읽기 좋고, 유지보수 하기 좋은 코드를 작성할 수 있었습니다. 하지만 여기서 더 개선할 수는 없을까요? 작성한 코드의 한계는 바로 데이터와 기능이 분리되어 있다는 점입니다. 음악 플레이어의 데이터는 MusicPlayerData에 있는데, 그 데이터를 사용하는 기능은 MusicPlayerMain3에 있는 각각의 메서드에 분리되어 있습니다. 그래서 음악 플레이어와 관련된 데이터는 MusicPlayerData를 사용해야 하고, 음악 플레이어와 관련된 기능은 MusicPlayerMain3의 각 메서드를 사용해야 합니다. 데이터와 그 데이터를 사용하는 기능은 매우 밀접하게 연관되어 있습니다. 각각의 메서드를 보면 대부분 MusicPlayerData의 데이터를 사용합니다. 따라서 이후에 관련 데이터가 변경되면 MusicPlayerMain3 부분의 메서드들도 함께 변경해야 합니다. 그리고 이렇게 데이터와 기능이 분리되어 있으면 유지보수 관점에서도 관리 포인트가 2곳으로 늘어납니다. “객체 지향 프로그래밍이 나오기 전까지는 지금과 같이 데이터와 기능이 분리되어 있었습니다.” 따라서 지금과 같은 코드가 최선이였습니다. 하지만 객체 지향 프로그래밍이 나오면서 데이터와 기능을 온전히 하나로 묶어서 사용할 수 있게 되었습니다. “데이터와 기능을 하나로 온전히 묶는다는 것이 어떤 의미인지 이해하기 위해 간단한 예시 코드를 만들어봅시다” 이 예시 코드는 다음 포스팅에 이어집니다.
Archive
· 2024-02-23
☕️[Java] 절차 지향 프로그래밍(2)
절차 지향 프로그래밍(2) - 데이터 묶음 “절차 지향 프로그래밍(1)에 작성한 코드에 클래스를 도입합시다.” MusicPlayerData라는 클래스를 만들고, 음악 플레이어에 사용되는 클래스를 만들고, 음악 플레이어에 사용되는 데이터들을 여기에 묶어서 멤버 변수로 사용해봅시다. MusicPlayerData package oop1; public class MusicPlayerData { int volume = 0; boolean isOn = false; } 음악 플레이어에 사용되는 volume, isOn 속성을 MusicPlayerData의 멤버 변수에 포함했습니다. MusicPlayerMain2 package oop1; public class MusicPlayerMain2 { public static void main(String[] args) { MusicPlayerData data = new MusicPlayerData(); // 음악 플레이어 켜기 data.isOn = true; System.out.println("음악 플레이어를 시작합니다."); // 볼륨 증가 data.volume++; System.out.println("음악 플레이어 볼륨:" + data.volume); // 볼륨 증가 data.volume++; System.out.println("음악 플레이어 볼륨:" + data.volume); // 볼륨 감소 data.volume--; System.out.println("음악 플레이어 볼륨:" + data.volume); // 음악 플레이어 상태 System.out.println("음악 플레이어 상태 확인"); if (data.isOn) { System.out.println("음악 플레이어 ON, 볼륨:" + data.volume); } else { System.out.println("음악 플레이어 OFF"); } // 음악 플레이어 끄기 data.isOn = false; System.out.println("음악 플레이어를 종료합니다."); } } 음악 플레이어와 관련된 데이터는 MusicPlayerData 클래스에 존재합니다. 이 클래스를 사용하도록 기존 로직을 변경했습니다. 이후에 프로그램 로직이 더 복잡해져서 다양한 변수들이 추가되더라도 음악 플레이어와 관련된 변수들은 MusicPlayerData data 객체에 속해있으므로 쉽게 구분할 수 있습니다.
Archive
· 2024-02-23
☕️[Java] 객체 지향 프로그래밍
객체 지향 프로그래밍. 지금까지 개발한 음악 플레이어는 데이터와 기능이 분리되어 있습니다. 이제 데이터와 기능을 하나로 묶어서 음악 플레이어라는 개념을 온전히 하나의 클래스에 담아보도록 하겠습니다. 프로그램을 작성하는 절차도 중요하지만 “지금은 음악 플레이어라는 개념을 객체로 온전히 만드는 것이 더 중요합니다.” 음악 플레이어라는 객체를 지향해봅시다! 그러기 위해서는 프로그램의 실행 순서 보다는 “음악 플레이어 클래스를 만드는 것 자체에 집중해야 합니다.” “음악 플레이어가 어떤 속성(데이터)을 가지고 어떤 기능(메서드)을 제공하는지 이 부분에 초점을 맞추어야 합니다.” 지금부터 우리는 음악 플레이어를 개발하는 개발자가 될 것입니다. 이것을 어떨게 사용할지는 분리해서 생각해봅시다. 쉽게 이야기해서 음악 플레이어를 만들어서 제공하는 개발자와 음악 플레이어를 사용하는 개발자가 분리되어 있다고 생각하면 됩니다. 음악 플레이어 속성: volume, isOn 기능: on(), off(), volumeUp(), volumeDown(), showStatus() 이것을 가지고 음악 플레이어를 만들어봅시다. 객체 지향 음악 플레이어. MusicPlayer package oop1; public class MusicPlayer { int volume = 0; boolean isOn = false; void on() { isOn = true; System.out.println("음악 플레이어를 시작합니다."); } void off() { isOn = false; System.out.println("음악 플레이어를 종료합니다."); } void volumeUp() { volume++; System.out.println("음악 플레이어 볼륨:" + volume); } void volumeDown() { volume--; System.out.println("음악 플레이어 볼륨:" + volume); } void showStatus() { System.out.println("음악 플레이어 상태 확인"); if (isOn) { System.out.println("음악 플레이어 ON, 볼륨:" + volume); } else { System.out.println("음악 플레이어 OFF"); } } } MusicPlayer 클래스에 음악 플레이어에 필요한 속성과 기능을 모두 정의했습니다. 이제 음악 플레이어가 필요한 곳에서 이 클래스만 있으면 온전한 음악 플레이어를 생성해서 사용할 수 있습니다. 음악 플레이어를 사용하는데 필요한 모든 속성과 기능이 하나의 클래스에 포함되어 있습니다. MusicPlayerMain4 package oop1; public class MusicPlayerMain4 { public static void main(String[] args) { MusicPlayer player = new MusicPlayer(); // 음악 플레이어 켜기 player.on(); // 볼륨 증가 player.volumeUp(); // 볼륨 증가 player.volumeUp(); // 볼륨 감소 player.volumeDown(); // 음악 플레이어 상태 player.showStatus(); // 음악 플레이어 끄기 player.off(); } } 실행 결과 음악 플레이어를 시작합니다. 음악 플레이어 볼륨:1 음악 플레이어 볼륨:2 음악 플레이어 볼륨:1 음악 플레이어 상태 확인 음악 플레이어 ON, 볼륨:1 음악 플레이어를 종료합니다. “MusicPlayer를 사용하는 코드를 봅시다.” MusicPlayer 객체를 생성하고 필요한 기능(메서드)을 호출하기만 하면 됩니다. 필요한 모든 것은 MusicPlayer 안에 들어있습니다. MusicPlayer를 사용하는 입장에서는 MusicPlayer의 데이터인 volumn, isOn 같은 데이터는 전혀 사용하지 않습니다. MusicPlayer를 사용하는 입장에서는 이제 MusicPlayer 내부에 어떤 속성(데이터)이 있는지 전혀 몰라도 됩니다. MusicPlayer를 사용하는 입장에서는 단순하게 MusicPlayer가 제공하는 기능 중에 필요한 기능을 호출해서 사용하기만 하면 됩니다. 캡슐화 MusicPlayer를 보면 음악 플레이어를 구성하기 위한 속성과 기능이 마치 하나의 캡슐에 쌓여있는 것 같습니다. “이렇게 속성과 기능을 하나로 묶어서 필요한 기능을 메서드를 통해 외부에 제공하는 것을 캡슐화라 합니다.” “객체 지향 프로그래밍” 덕분에 음악 플레이어 객체를 사용하는 입장에서 진짜 음악 플레이어를 만들고 사용하는 것 처럼 친숙하게 느껴집니다. 그래서 코드가 더 읽기 쉬운 것은 물론이고, 속성과 기능이 한 곳에 있기 때문에 변경도 더 쉬워집니다. 예를 들어서 MusicPlayer 내부 코드가 변하는 경우에 다른 코드는 변경하지 않아도 됩니다. MusicPlayer의 volume이라는 필드 이름이 다른 이름으로 변한다고 할 때 MusicPlayer 내부만 변경하면 됩니다. 또 음악 플레이어가 내부에서 출력하는 메시지를 변경할 때도 MusicPlayer 내부만 변경하면 됩니다. 이 경우 MusicPlayer를 사용하는 개발자는 코드를 전혀 변경하지 않아도 됩니다. 물론 외부에서 호출하는 MusicPlayer의 메서드 이름을 변경한다면 MusicPlayer를 사용하는 곳의 코드도 변경해야 합니다.
Archive
· 2024-02-23
☕️[Java] 클래스와 메서드
클래스와 메서드. 클래스는 데이터인 멤버 변수 뿐 아니라 기능 역할을 하는 메서드도 포함할 수 있습니다. 먼저 멤버 변수만 존재하는 클래스로 간단한 코드를 작성해봅시다. ValueData package oop1; public class ValueData { int value; } ValueDataMain package oop1; public class ValueDataMain { public static void main(String[] args) { ValueData valueData = new ValueData(); add(valueData); add(valueData); add(valueData); System.out.println("최종 숫자=" + valueData.value); } static void add(ValueData valueData) { valueData.value++; System.out.println("숫자 증가 value=" + valueData.value); } } 실행 결과 숫자 증가 value=1 숫자 증가 value=2 숫자 증가 value=3 최종 숫자=3 ValueData 라는 인스턴스를 생성하고 외부에서 ValueData.value에 접근해 숫자를 하나씩 증가시키는 단순한 코드입니다. 코드를 보면 데이터인 value와 value의 값을 증가시키는 기능은 add() 메서드가 서로 분리되어 있습니다. 자바 같은 객체 지향 언어는 클래스 내부에 속성(데이터)과 기능(메서드)을 함께 포함할 수 있습니다. 클래스 내부에 멤버 변수 뿐만 아니라 메서드도 함께 포함할 수 있다는 뜻 입니다. “이번에는 숫자를 증가시키는 기능도 클래스에 함께 포함해서 새로운 클래스를 정의해봅시다.” package oop1; public class ValueData { int value; void add() { value++; System.out.println("숫자 증가 value=" + value); } } 이 클래스에는 데이터인 value와 해당 데이터를 사용하는 기능인 add() 메서드를 함께 정의했습니다. 이제 이 클래스가 어떻게 사용되는지 확인해봅시다. 참고: 여기서 만드는 add() 메서드에는 static 키워드를 사용하지 않습니다. 메서드는 객체를 생성해야 호출할 수 있습니다. 그런데 static이 붙으면 객체를 생성하지 않고도 메서드를 호출할 수 있습니다. static에 대한 자세한 내용은 추후에 포스팅하겠습니다. ValueObjectMain package oop1; public class ValueObjectMain { public static void main(String[] args) { ValueData valueData = new ValueData(); valueData.add(); valueData.add(); valueData.add(); System.out.println("최종 숫자=" + valueData.value); } } 실행 결과 숫자 증가 value=1 숫자 증가 value=2 숫자 증가 value=3 최종 숫자=3 인스턴스 생성 ValueData valueData = new ValueData(); valueData라는 객체를 생성했습니다. 이 객체는 멤버 변수 뿐만 아니라 내분에 기능을 수행하는 add() 메서드도 함께 존재합니다. 인스턴스의 메서드 호출 인스턴스의 메서드를 호출하는 방법은 멤버 변수를 사용하는 방법과 동일합니다. .(dot)을 찍어서 객체 접근한 다음에 원하는 메서드를 호출하면 됩니다. valueData.add(); // 1 x002.add(); // 2 x002 ValueData 인스턴스에 있는 add() 메서드를 호출합니다. 3: add() 메서드를 호출하면 메서드 내부에서 value++을 호출하게 됩니다. 이때 value에 접근해야 하는데, 기본으로 본인 인스턴스에 있는 멤버 변수에 접근합니다. 본인 인스턴스가 x002 참조값을 사용하므로 자기 자신인 x002.value에 접근하게 됩니다. 4: ++ 연산으로 value의 값을 하나 증가시킵니다. 정리 클래스는 속성(데이터, 멤버 변수)과 기능(메서드)을 정의할 수 있습니다. 객체는 자신의 메서드를 통해 자신의 멤버 변수에 접근할 수 있습니다. 객체의 메서드 내부에서 접근하는 멤버 변수는 객체 자신의 멤버 변수입니다.
Archive
· 2024-02-23
☕️[Java] 객체 지향 프로그래밍 vs 절차 지향 프로그래밍
객체 지향 프로그래밍 vs 절차 지향 프로그래밍 객체 지향 프로그래밍과 절차 지향 프로그래밍은 서로 대치되는 개념이 아닙니다. 객체 지향이라도 프로그램의 작동 순서는 중요합니다. 다만 어디에 더 초점을 맞추는가에 둘의 차이가 있습니다. 객체 지향의 경우 객체의 설계와 관계를 중시합니다. 반면 절차 지향의 경우 데이터와 기능이 분리되어 있고, 프로그램이 어떻게 작동하는지 그 순서에 초점을 맞춥니다. 절차 지향 프로그래밍 절차 지향 프로그래밍은 이름 그대로 절차를 지향합니다. 쉽게 이야기해서 실행 순서를 중요하게 생각하는 방식입니다. 절차 지향 프로그래밍은 프로그램의 흐름을 순차적으로 따르며 처리하는 방식입니다. 즉, “어떻게”를 중심으로 프로그래밍 합니다. 객체 지향 프로그래밍 객체 지향 프로그래밍은 이름 그대로 객체를 지향합니다. 쉽게 이야기해서 객체를 중요하게 생각하는 방식입니다. 객체 지향 프로그래밍은 실제 세계의 사물이나 사건을 객체로 보고, 이러한 객제들 간의 상호작용을 중심으로 프로그래밍하는 방식입니다. 즉 “무엇을” 중심으로 프로그래밍 합니다. 둘의 중요한 차이 절차 지향은 데이터와 해당 데이터에 대한 처리 방식이 분리되어 있습니다. 반면 객체 지향에서는 데이터와 그 데이터에 대한 행동(메서드)이 하나의 ‘객체’ 안에 함께 포함되어 있습니다. 객체란? 세상의 모든 사물을 단순하게 추상화해보면 속성(데이터)과 기능(메서드) 딱 2가지로 설명할 수 있습니다. 자동차 속성: 색상, 속도 기능: 엑셀, 브레이크, 문 열기, 문 닫기 동물 속성: 색상, 키, 온도 기능: 먹는다, 걷는다 게임 캐릭터 속성: 레벨, 경험치, 소유한 아이템들 기능: 이동, 공격, 아이템 획득 “객체 지향 프로그래밍은 모든 사물을 속성과 기능을 가진 객체로 생각하는 것 입니다.” 객체에는 속성과 기능만 존재합니다. 이렇게 단순화하면 세상에 있는 객체들을 컴퓨터 프로그램으로 쉽게 설계할 수 있습니다. 이런 장점들 덕분에 지금은 객체 지향 프로그래밍이 가장 많이 사용됩니다. 참고로 실세계와 객체가 항상 1:1로 매칭되는 것은 아닙니다. 객체 지향의 특징은 속성과 기능을 하나로 묶는 것 뿐만 아니라 캡슐화, 상속, 다형성, 추상화, 메시지 전달 같은 다양한 특징들이 있습니다.
Archive
· 2024-02-23
☕️[Java] 절차 지향 프로그래밍(1)
객체지향 프로그래밍(1) 절차 지향 프로그래밍 vs 객체 지향 프로그래밍 프로그래밍 방식은 크게 절차 지향 프로그래밍과 객체 지향 프로그래밍으로 나눌 수 있습니다. 절차 지향 프로그래밍 절차 지향 프로그래밍은 이름 그대로 절차를 지향합니다. 쉽게 이야기해서 실행 순서를 중요하게 생각하는 방식입니다. 절차 지향 프로그래밍은 프로그램의 흐름을 순차적으로 따르며 처리하는 방식입니다. 즉, “어떻게”를 중심으로 프로그래밍 합니다. 객체 지향 프로그래밍 객체 지향 프로그래밍은 이름 그대로 객체를 지향합니다. 쉽게 이야기해서 객체를 중요하게 생각하는 방식입니다. 객체 지향 프로그래밍은 실제 세계의 사물이나 사건을 객체로 보고, 이러한 객체들 간의 상호작용을 중심으로 프로그래밍하는 방식입니다. 즉, “무엇을” 중심으로 프로그래밍 합니다. 둘의 중요한 차이 절차 지향은 데이터와 해당 데이터에 대한 처리 방식이 분리되어 있습니다. 반변 객체 지향에서는 데이터와 그 데이터에 대한 행동(메서드)이 하나의 ‘객체’안에 함께 포함되어 있습니다. 절차 지향에서 객체 지향으로 점진적으로 코드를 변경해보면서 객체 지향 프로그래밍을 이해해봅시다. “음악 플레이어를 만들어보고 그것을 보면서 이해해봅시다.” 요구 사항: 음악 플레이어를 켜고 끌 수 있어야 합니다. 음악 플레이어의 볼륨을 증가, 감소할 수 있어야 합니다. 음악 플레이어의 상태를 확인할 수 있어야 합니다. 예시 출력: 음악 플레이어를 시작합니다. 음악 플레이어 볼륨:1 음악 플레이어 볼륨:2 음악 플레이어 볼륨:1 음악 플레이어 상태 확인 음악 플레이어 ON, 볼륨:1 음악 플레이어를 종료합니다. 절차 지향 음악 플레이어1 package oop1; public class MusicPlayerMain1 { public static void main(String[] args) { int volume = 0; boolean isOn = false; // 음악 플레이어 켜기 isOn = true; System.out.println("음악 플레이어를 시작합니다."); // 볼륨 증가 volume++; System.out.println("음악 플레이어 볼륨:" + volume); // 볼륨 증가 volume++; System.out.println("음악 플레이어 볼륨:" + volume); // 볼륨 감소 volume--; System.out.println("음악 플레이어 볼륨:" + volume); // 음악 플레이어 상태 System.out.println("음악 플레이어 상태 확인"); if (isOn) { System.out.println("음악 플레이어 ON, 볼륨:" + volume); } else { System.out.println("음악 플레이어 OFF"); } // 음악 플레이어 끄기 isOn = false; System.out.println("음악 플레이어를 종료합니다."); } } 실행 결과 음악 플레이어를 시작합니다. 음악 플레이어 볼륨:1 음악 플레이어 볼륨:2 음악 플레이어 볼륨:1 음악 플레이어 상태 확인 음악 플레이어 ON, 볼륨:1 음악 플레이어를 종료합니다. “순서대로 프로그램이 작동하도록 단순하게 작성했습니다. 이 코드를 점진적으로 변경해봅시다.” 다음 포스트인 “절차 지향 프로그래밍(2)”에서 이어서 포스팅하겠습니다.
Archive
· 2024-02-22
☕️[Java] NullPointerException
NullPointerException 택배를 보낼 때 주소지 없이 택배를 발송하려면 어떤 문제가 발생할까요? 만약 참조값 없이 객체를 찾아가면 어떤 문제가 발생할까요? 이 경우 NullPointerExecption이라는 예외가 발생하는데, 개발자를 가장 많이 괴롭히는 예외입니다. NullPointerExecption은 이름 그대로 null을 가리리키다(Pointer)인데, 이때 발생하는 예외(Exception)입니다. null은 없다는 뜻이므로 결국 주소가 없는 곳을 찾아갈 때 발생하는 예외입니다. 객체를 참조할 때는 .(dot)을 사용합니다. 이렇게 하면 참조값을 사용해서 해당 객체를 찾아갈 수 있습니다. 그런데 참조값이 null이라면 값이 없다는 뜻이므로, 찾아갈 수 있는 객체(인스턴스)가 없습니다. NullPointerExecption은 이처럼 null에 .(dot)을 찍었을 때 발생합니다. 예제를 통해서 확인해봅시다. package ref; public class NullMain2 { public static void main(String[] args) { Data data = null; data.value = 10; // NullPointerException 예외 발생 System.out.println("data = " + data.value); } } data 참조형 변수에는 null 값이 들어가 있습니다. 그런데 data.value = 10 이라고 하면 어떻게 될까요? data.value = 10; null.value = 10 // data에는 null 값이 들어있습니다. 결과적으로 null 값은 참조할 주소가 존재하지 않는다는 뜻입니다. 따라서 참조할 객체 인스턴스가 존재하지 않으므로 다음과 같이 java.lang.NullPointerExecption이 발생하고, 프로그램이 종료됩니다. 참고로 예외가 발생했기 때문에 그 다음 로직은 수행되지 않습니다. 실행 결과 Exception in thread "main" java.lang.NullPointerException: Cannot assign field "value" because "data" is null at ref.NullMain2.main(NullMain2.java:7) 멤버 변수와 null 앞선 예제와 같이 지역 변수의 경우에는 null 문제를 파악하는 것이 어렵지 않습니다. 다음과 같이 멤버 변수가 null인 경우에는 주의가 필요합니다. Data package ref; public class Data { int value; } BigData package ref; public class BigData { Data data; int count; } BigData 클래스는 Data data, int count 두 변수를 가집니다. NullMain3 package ref; public class NullMain3 { public static void main(String[] args) { BigData bigData = new BigData(); System.out.println("bigData.count=" + bigData.count); System.out.println("bigData.data=" + bigData.data); // NullPointerException System.out.println("bigData.data.value=" + bigData.data.value); } } 실행 결과 bigData.count=0 bigData.data=null Exception in thread "main" java.lang.NullPointerException: Cannot read field "value" because "bigData.data" is null at ref.NullMain3.main(NullMain3.java:11) BigData를 생성하면 BigData의 인스턴스가 생성됩니다. 이때 BigData 인스턴스의 멤버 변수에 초기화가 일어나는데, BigData의 data 멤버 변수는 참조형이므로 null로 초기화 됩니다. count 멤버 변수는 숫자이므로 0으로 초기화가 됩니다. bigData.count를 출력하면 0이 출력됩니다. bigData.data를 출력하면 참조값인 null이 출력됩니다. 이 변수는 아직 아무것도 참조하고 있지 않습니다. bigData.data.value를 출력하면 data의 값이 null이므로 null에 .(dot)을 찍게 되고, 따라서 참조할 곳이 없으므로 NullPointerException 예외가 발생하게 됩니다. 예외 발생 과정 bigData.data.value x001.data.value // bigData는 x001 참조값을 가집니다. null.value // x001.data는 null 값을 가집니다. NullPointerExecption // null 값에 .(dot)을 찍으면 예외가 발생합니다. “이 문제를 해결하려면 Data 인스턴스를 만들고 BigData.data 멤버 변수에 참조값을 할당하면 됩니다.” NullMain4 package ref; public class NullMain4 { public static void main(String[] args) { BigData bigData = new BigData(); bigData.data = new Data(); System.out.println("bigData.count=" + bigData.count); System.out.println("bigData.data=" + bigData.data); System.out.println("bigData.data.value=" + bigData.data.value); } } 실행 결과 bigData.count=0 bigData.data=ref.Data@3a71f4dd bigData.data.value=0 실행 과정 bigData.data.value x001.data.value // bigData는 x001 참조값을 가집니다. x002.value // x001.data는 x002 값을 가집니다. 0 // 최종 결과 참고: xoo1과 x002 는 sudo code입니다. 정리 “NullPointerException이 발생하면 null값에 .(dot)을 찍었다고 생각하면 문제를 쉽게 찾을 수 있습니다.”
Archive
· 2024-02-22
☕️[Java] 변수와 초기화
변수와 초기화 변수의 종류 멤버 변수(필드): 클래스에 선언. 지역 변수: 메서드에 선언, 매개변수도 지역 변수의 한 종류입니다. 멤버 변수, 필드 예시 public class Student { String name; int age; int grade; } name, age, grade 는 멤버 변수입니다. 지역 변수 예시 public class ClassStart3 { public static void main(String[] args) { Student student1; student1 = new Student(); Student student2 = new Student(); } } student1, student2는 지역 변수입니다. public class MethodChange1 { public static void main(String[] args) { int a = 10; System.out.println("메서드 호출 전: a = " + a); changePrimitive(a); System.out.println("메서드 호출 후: a = " + a); } static void changePrimitive(int x) { x = 20; } } a, x(매개변수, Parameter)는 지역변수입니다. 지역 변수는 이름 그대로 “특정 지역에서만 사용되는 변수라는 뜻입니다.” 예를 들어서 변수 x는 changePrimitive() 메서드의 블록에서만 사용됩니다. changePrimitive() 메서드가 끝나면 제거됩니다. a 변수도 마찬가지입니다. main() 메서드가 끝나면 제거됩니다. 변수의 값 초기화 멤버 변수: 자동 초기화. 인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화됩니다. 숫자(int) = 0, boolean = false, 참조형 = null(null 값은 참조할 대상이 없다는 뜻으로 사용됩니다.) 개발자가 초기값을 직접 지정할 수 있습니다. 지역 변수: 수동 초기화. 지역 변수는 항상 직접 초기화해야 합니다. “멤버 변수의 초기화를 살펴봅시다” InitData package ref; public class InitData { int value1; // 초기화 하지 않음 int value2 = 10; // 10으로 초기화 } value1은 초기값을 지정하지 않았고, value2는 초기값을 10으로 지정했습니다. InitMain package ref; public class initMain { public static void main(String[] args) { InitData data = new InitData(); System.out.println("value1 = " + data.value1); System.out.println("value2 = " + data.value2); } } 실행 결과 value1 = 0 value2 = 10 value1은 초기값을 지정하지 않았지만 멤버 변수는 자동으로 초기화 됩니다. 숫자는 0으로 초기화됩니다. value2는 10으로 초기값을 지정해두었기 때문에 객체를 생성할 때 10으로 초기화됩니다.
Archive
· 2024-02-21
☕️[Java] 참조형과 메서드 호출 - 활용
참조형과 메서드 호출 - 활용 아래의 코드 class1.ClassStart3 코드에는 중복되는 부분이 2가지가 있습니다. name, age, grade에 값을 할당하는 부분. 학생 정보를 출력하는 부분. package class1; public class ClassStart3 { public static void main(String[] args) { Student student1; student1 = new Student(); student1.name = "학생1"; student1.age = 15; student1.grade = 90; Student student2 = new Student(); student2.name = "학생2"; student2.age = 16; student2.grade = 80; System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade); System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade); } } “이러한 중복은 메서드를 통해 손쉽게 제거할 수 있습니다.” 메서드에 객체 전달 다음과 같이 코드를 작성해봅시다. Student package ref; public class Student { String name; int age; int grade; } ref 패키지에도 Student 클래스를 만듭니다. Method1 package ref; public class Method1 { public static void main(String[] args) { Student student1 = new Student(); initStudent(student1, "학생1", 15, 90); Student student2 = new Student(); initStudent(student2, "학생2", 16, 80); printStudent(student1); printStudent(student2); } static void initStudent(Student student, String name, int age, int grade) { student.name = name; student.age = age; student.grade = grade; } static void printStudent(Student student) { System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade); } } 참조형은 메서드를 호출할 때 참조값을 전달합니다. 따라서 메서드 내부에서 전달된 참조값을 통해 객체의 값을 변경하거나, 값을 읽어서 사용할 수 있습니다. initStudent(Student student, ...) : 전달한 학생 객체의 필드에 값을 설정합니다. printStudent(Student student, ...) : 전달한 학생 객체의 필드 값을 읽어서 출력합니다. initStudent() 메서드 호출 분석 initStudent(Student student, String name, int age, int grade) { student.name = name; student.age = age; student.grade = grade; } student1이 참조하는 Student 인스턴스에 값을 편리하게 할당하고 싶어서 initStudent() 메서드를 만들었습니다. 이 메서드를 호출하면서 student1을 전달합니다. 그러면 student1의 참조값(30f39991)이 매개변수 student에 전달됩니다. 이 참조값을 통해 initStudent() 메서드 안에서 student1이 참조하는 것과 동일한 30f39991 Student 인스턴스에 접근하고 값을 변경할 수 있습니다. 참고: 30f39991은 실제 student1의 참조값입니다. System.out.println("student1 참조값 : " + student1); 의 결과가 student1 참조값 : ref.Student@30f39991 이였습니다. 주의! package ref; import class1 Student; public class Method1 { ... } import class1.Student;이 선언되어 있으면 안됩니다. 이렇게 되면 class1 패키지에서 선언한 Student를 사용하게 됩니다. 이 경우에는 접근 제어자 때문에 컴파일 오류가 발생합니다. 만약 선언되어 있다면 삭제해야 합니다. 삭제하면 같은 패키지에 있는 ref.Student를 사용합니다. 메서드에서 객체 반환 조금 더 코드를 리팩토링 시켜봅시다. 다음 코드에도 중복이 있습니다. Student student1 = new Student(); initStudent(student1, "학생1", 15, 90); Student student2 = new Student(); initStudent(student2, "학생2", 16, 80); 바로 객체를 생성하고, 초기값을 설정하는 부분입니다. 이렇게 2번 반복되는 부분을 하나로 합져봅시다. “다음과 같이 기존 코드를 변경해봅시다.” Method2 package ref; public class Method2 { public static void main(String[] args) { Student student1 = createStudent("학생1", 15, 90); Student student2 = createStudent("학생2", 16, 80); printStudent(student1); printStudent(student2); } static Student createStudent(String name, int age, int grade) { Student student = new Student(); student.name = name; student.age = age; student.grade = grade; return student; } static void printStudent(Student student) { System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade); } } createStudent() 라는 메서드를 만들고 객체를 생성하는 부분도 이 메서드안에 함께 포함했습니다. “이제 이 메서드 하나로 객체의 생성과 초기값 설정을 모두 처리합니다.” 그런데 메서드 안에서 객체를 생성했기 때문에 만들어진 객체를 메서드 밖에서 사용할 수 있게 돌려주어야 합니다. 그래야 메서드 밖에서 이 객체를 사용할 수 있습니다. 메서드는 호출 결과를 반환(return)을 할 수 있습니다. 메서드의 반환 기능을 사용해서 만들어진 객체의 참조값을 메서드 밖으로 반환하면 됩니다. createStudent() 메서드 호출 분석 createStudent(String name, int age, int grade) { Student student = new Student(); student.name = name; student.age = age; student.grade = grade; return student; } 메서드 내부에서 인스턴스를 생성한 후에 참조값을 메서드 외부로 반환했습니다. 이 참조값만 있으면 해당 인스턴스에 접근할 수 있습니다. 여기서는 student1에 참조값을 보관하고 사용합니다. 진행 과정 Student student1 = createStudent("학생1", 15, 90); // 메서드 호출후 결과 반환 Student student1 = student(30f39991); // 참조형인 student를 반환 Student student1 = 30f39991; // student의 참조값 대입 student1 = 30f39991; createStudent() 는 생성한 Student 인스턴스의 참조값을 반환합니다. 이렇게 반환된 참조값을 student1 변수에 저장합니다. 앞으로는 student1을 통해 Student 인스턴스를 사용할 수 있습니다.
Archive
· 2024-02-21
☕️[Java] 기본형과 참조형(3) - 메서드 호출
기본형과 참조형(3) - 메서드 호출 대원칙: 자바는 항상 변수의 값을 복사해서 대입합니다. 자바에서 변수에 값을 대입하는 것은 변수에 들어 있는 값을 복사해서 대입하는 것입니다. 기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입합니다. 기본형이면 변수에 들어 있는 실제 사용하는 값을 복사해서 대입합니다. 참조형이면 변수에 들어 있는 참조값을 복사해서 대입합니다. 메서드 호출도 마찬가지입니다. 메서드를 호출할 때 사용하는 매개변수(parameter)도 결국 변수일 뿐 입니다. 따라서 메서드를 호출할 때 매개변수(parameter)에 값을 전달하는 것도 앞서 설명한 내용과 같이 값을 복사해서 전달합니다. 다음 예시 코드를 봐봅시다. 기본형과 메서드 호출 package ref; public class MethodChange1 { public static void main(String[] args) { int a = 10; System.out.println("메서드 호출 전: a = " + a); changePrimitive(a); System.out.println("메서드 호출 후: a = " + a); } static void changePrimitive(int x) { x = 20; } } 실행 결과 메서드 호출 전: a = 10 메서드 호출 후: a = 10 1. 메서드 호출 메서드를 호출할 때 매개변수 x에 변수 a의 값을 전달합니다. int x = a 이 코드는 다음과 같이 해석할 수 있습니다. 자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다 따라서 변수 a,x 각각 숫자 10을 가지고 있습니다. 2. 메서드 안에서 값을 변경 int a = 10; changePrimitive(a); 메서드 안에서 x = 20으로 새로운 값을 대입합니다. 결과적으로 x의 값만 20으로 변경되고, a의 값은 10으로 유지됩니다. 3. 메서드 종료 int a = 10; changePrimitive(a); 메서드 종료후 값을 확인해보면 a는 10이 출력되는 것을 확인할 수 있습니다. 참고로 메서드가 종료되면 매개변수 x는 제거됩니다. 참조형과 메서드 호출 package ref; public class MethodChange2 { public static void main(String[] args) { Data dataA = new Data(); dataA.value = 10; System.out.println("메서드 호출 전: dataA.value = " + dataA.value); changeReference(dataA); System.out.println("메서드 호출 전: dataA.value = " + dataA.value); } static void changeReference(Data dataX) { dataX.value = 20; } } 실행 결과 메서드 호출 전: dataA.value = 10 메서드 호출 전: dataA.value = 20 1. 메서드 호출 메서드를 호출할 때 매개변수 dataX에 변수 dataA의 값을 전달합니다. 이 코드는 다음과 같이 해석할 수 있습니다. int dataX = dataA; 자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다. 변수 dataA는 참조값 x001을 가지고 있다는 가정하에 dataA는 참조값을 복사해서 전달합니다. 따라서 변수 dataA, dataX 둘 다 같은 참조값인 x001을 가지게 됩니다. 이제 dataX를 통해서도 x001에 있는 Data 인스턴스에 접근할 수 있습니다. 2. 메서드 안에서 값을 변경 static void changeReference(Data dataX) { dataX.value = 20; } 메서드 안에서 dataX.value = 20으로 새로운 값을 대입합니다. 참조값을 통해 x001 인스턴스에 접근하고 그 안에 있는 value의 값을 20으로 변경했습니다. dataA, dataX 모두 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataX.value는 둘다 20이라는 값을 가집니다. 3. 메서드 종료 메서드 종료후 dataA.value의 값을 확인해보면 다음과 같이 20으로 변경된 것을 확인할 수 있습니다. 메서드 호출 전: dataA.value = 10 메서드 호출 전: dataA.value = 20 기본형과 참조형의 메서드 호출 자바에서 메서드의 매개변서(Parameter)는 항상 값에 의해 전달됩니다. 그러나 이 값이 실제 값이냐, 참조(메모리 주소)값이냐에 따라 동작이 달라집니다. 기본형: 메서드로 기본형 데이터를 전달하면, 해당 값이 복사되어 전달됩니다. 이 경우, 메서드 내부에서 매개변수(Parameter)의 값을 변경해도, 호출자의 변수 값에는 영향이 없습니다. 참조형: 메서드로 참조형 데이터를 전달하면, 참조값이 복사되어 전달됩니다. 이 경우, 메서드 내부에서 매개변수(Parameter)로 전달된 객체의 멤버 변수를 변경하면, 호출자의 객체도 변경됩니다.
Archive
· 2024-02-21
☕️[Java] null
null 친구에게 책을 보내려고 합니다. 우체국 택배를 보내려고 할 때 책은 준비가 되었지만, 깜빡하고 친구에게 주소를 물어보지 못했습니다. 즉, 보낼 주소지가 아직 결정되지 않았습니다. 이 때, 보낼 주소지가 결정될 때 까지는 주소지를 비워두어야 할 것 입니다. 참조형 변수에는 항상 객체가 있는 위치를 가리키는 참조값이 들어갑니다. 그런데 아직 가리키는 대상이 없거나, 가리키는 대상을 나중에 입력하고 싶을 경우에는 어떻게 해야할까요? 참조형 변수에서 아직 가리키는 대상이 없다면 null이라는 특별한 값을 넣어둘 수 있습니다. null은 값이 존재하지 않는, 없다는 뜻 입니다. “코드를 통해서 null 값에 대해 알아봅시다.” Data package ref; public class Data { int value; } NullMain1 package ref; public class NullMain1 { public static void main(String[] args) { Data data = null; System.out.println("1. data = " + data); data = new Data(); System.out.println("2. data = " + data); data = null; System.out.println("3. data = " + data); } } 실행 결과 1. data = null 2. data = ref.Data@30f39991 3. data = null Data data = null; Data 타입을 받을 수 있는 참조형 변수 data를 만들었습니다. 그리고 여기에 null 값을 할당했습니다. 따라서 data 변수에는 아직 가리키는 객체가 없다는 뜻입니다. data = new Data(); 이후에 새로운 Data 객체를 생성해서 그 참조값을 data 변수에 할당했습니다. 이제 data 변수가 참조할 객체가 존재합니다. 여기서 Data 인스턴스의 value 값은 0으로 초기화 되어있습니다. data = null; 마지막에는 data에 다시 null 값을 할당했습니다. 이렇게 하면 data 변수는 앞서 만든 Data 인스턴스를 더는 참조하지 않습니다. “GC(Garbage Collection) - 아무도 참조하지 않는 인스턴스의 최후” 마지막에 data에 null을 할당했습니다. 따라서 앞서 생성한 x001(sudo 참조값) Data 인스턴스를 더는 아무도 참조하지 않습니다. 이렇게 아무도 참조하지 않게 되면 x001이라는 참조값을 다시 구할 방법이 없습니다. 따라서 해당 인스턴스에 다시 접근할 방법이 없습니다. 이렇게 아무도 참조하지 않는 인스턴스는 사용되지 않고 메모리 용량만 차지할 뿐입니다. C와 같은 과거 프로그래밍 언어는 개발자가 직접 명령어를 사용해서 인스턴스를 메모리에서 제거해야 했습니다. 만약 실수로 인스턴스 삭제를 누락할 경우, 메모리에 사용하지 않는 객체가 가득해져서 메모리 부족 오류가 발생하게 됩니다. “자바는 이런 과정을 자동으로 처리해줍니다.” 아무도 참조하지 않는 인스턴스가 있으면 JVM의 GC(가비지 컬렉션)가 더 이상 사용하지 않는 인스턴스라 판단하고 해당 인스턴스를 자동으로 메모리에서 제거해줍니다. “객체는 해당 객체를 참조하는 곳이 있으면, JVM이 종료할 때 까지 계속 생존합니다.” 그런데 중간에 해당 객체를 참조하는 곳이 모두 사라지면 그때 JVM은 필요 없는 객체로 판단하고 GC(가비지 컬렉션)를 사용해서 제거합니다.
Archive
· 2024-02-21
☕️[Java] 기본형과 참조형(2) - 변수 대입
기본형과 참조형(2) - 변수 대입 “대원칙: 자바는 항상 변수의 값을 복사해서 대입합니다.” 자바에서 변수에 값을 대입하는 것은 변수에 들어 있는 값을 복사해서 대입하는 것입니다. 기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입합니다. “기본형” 이면 변수에 들어 있는 “실제 사용하는 값”을 복사해서 대입하고, “참조형” 이면 변수에 들어 있는 “참조값”을 복사해서 대입합니다. 이 대원칙을 이해하면 복잡한 상황에도 코드를 단순하게 이해할 수 있습니다. 기본형 대입 int a = 10; int b = a; 참조형 대입 Student s1 = new Student(); Student s2 = s1; 기본형은 변수에 값을 대입하더라도 실제 사용하는 값이 변수에 바로 들어 있기 때문에 값만 복사해서 대입한다고 생각하면 쉽게 이해할 수 있습니다. 그러나 참조형의 경우 실제 사용하는 객체가 아니라 객체의 위치를 가르키는 참조값만 복사가 됩니다. 쉽게 이야기해서 실제 건물이 복사가 되는 것이 아니라 건물의 위치인 주소만 복사되는 것입니다. 따라서 같은 건물을 찾아갈 수 있는 방법이 하나 늘어날 뿐입니다. “구체적인 예시를 통해서 변수 대입시 기본형과 참조형의 차이를 알아봅시다.” VarChange1 package ref; public class VarChange1 { public static void main(String[] args) { int a = 10; int b = a; System.out.println("a = " + a); System.out.println("b = " + b); // a 변경 a = 20; System.out.println("변경 a = 20"); System.out.println("a = " + a); System.out.println("b = " + b); // b 변경 b = 30; System.out.println("변경 b = 30"); System.out.println("a = " + a); System.out.println("b = " + b); } } 실행 결과 a = 10 b = 10 변경 a = 20 a = 20 b = 10 변경 b = 30 a = 20 b = 30 “코드를 순서대로 알아봅시다.” int a = 10; int b = a; 실행 결과 a = 10 b = 10 “변수의 대입은 변수에 들어있는 값을 복사해서 대입합니다.” 여기에서 변수 a에 들어있는 값 10을 복사해서 변수 b에 대입합니다. 변수 a 자체를 b에 대입하는 것이 아닙니다. a = 20; 실행 결과 a = 10 b = 20 변수 a에 값 20을 대입했습니다. 따라서 변수 a의 값이 10에서 20으로 변경되었습니다. “당연한 이야기지만 변수 b에는 아무런 영향을 주지 않습니다.” b = 30; 실행 결과 a = 20 b = 30 변수 b에 값 30을 대입했습니다. 변수 b의 값이 10에서 30으로 변경되었습니다. “당연한 이야기지만 변수 a에는 아무런 영향을 주지 않습니다.” 여기서 핵심은 int b = a라고 했을 때 변수에 들어있는 값을 복사해서 전달한다는 점입니다. 따라서 a = 20, b = 30이라고 했을 때 각각 본인의 값만 변경되는 것을 확인할 수 있습니다. 참조형과 변수 대입 아래의 예시 코드를 보고 참조형과 변수 대입에 대하여 알아봅시다. Data package ref; public class Data { int value; } VarChange2 package ref; public class VarChange2 { public static void main(String[] args) { Data dataA = new Data(); dataA.value = 10; Data dataB = dataA; System.out.println("dataA 참조값 = " + dataA); System.out.println("dataB 참조값 = " + dataB); System.out.println("dataA.value = " + dataA.value); System.out.println("dataB.value = " + dataB.value); // dataA 변경. dataA.value = 20; System.out.println("변경 dataA.value = 20"); System.out.println("dataA.value = " + dataA.value); System.out.println("dataB.value = " + dataB.value); // dataB 변경. dataB.value = 30; System.out.println("변경 dataA.value = 30"); System.out.println("dataA.value = " + dataA.value); System.out.println("dataB.value = " + dataB.value); } } 실행 결과 dataA 참조값 = ref.Data@30f39991 dataB 참조값 = ref.Data@30f39991 dataA.value = 10 dataB.value = 10 변경 dataA.value = 20 dataA.value = 20 dataB.value = 20 변경 dataA.value = 30 dataA.value = 30 dataB.value = 30 “코드를 한 줄씩 읽어가며 이해 해봅시다.” Data dataA = new Data(); dataA.value = 10; dataA 변수는 Data 클래스를 통해서 만들었기 때문에 “참조형”입니다. 이 변수는 “Data 형 객체의 참조값을 저장” 합니다. Data 객체를 생성하고, 참조값을 dataA에 저장합니다. 그리고 객체의 value 변수에 값 10을 저장했습니다. Data dataB = dataA; 실행 코드 Data dataA = new Data(); dataA.value = 10; Data dataB = dataA; System.out.println("dataA 참조값 = " + dataA); System.out.println("dataB 참조값 = " + dataB); System.out.println("dataA.value = " + dataA.value); System.out.println("dataB.value = " + dataB.value); 출력 결과 dataA 참조값 = ref.Data@30f39991 dataB 참조값 = ref.Data@30f39991 dataA.value = 10 dataB.value = 10 변수의 대입은 변수에 들어있는 “값을 복사해서 대입” 합니다. 변수 dataA에는 참조값 30f39991이 들어있습니다. 여기서는 변수 dataA에 들어있는 참조값 30f39991을 복사해서 변수 dataB에 대입합니다. 이제 dataA와 dataB에 들어있는 참조값은 같습니다. 따라서 둘 다 같은 30f39991 인스턴스를 가리킵니다. 참고로 변수 dataA가 가르키는 인스턴스를 복사하는 것이 아닙니다! “변수에 들어있는 참조값만 복사해서 전달하는 것 입니다.” dataA.value = 20; 실행 코드 dataA.value = 20; System.out.println("dataA.value = " + dataA.value); System.out.println("dataB.value = " + dataB.value); 출력 결과 dataA.value = 20 dataB.value = 20 dataA.value = 20 코드를 실행하면 dataA가 가르키는 30f39991 인스턴스의 value 값을 10에서 20으로 변경합니다. 그러나 dataA와 dataB는 같은 30f39991 인스턴스를 참조하기 때문에 dataA.value와 dataB.value는 둘 다 같은 값인 20을 출력합니다. dataB.value = 30; 실행 코드 dataB.value = 30; System.out.println("dataA.value = " + dataA.value); System.out.println("dataB.value = " + dataB.value); 출력 결과 dataA.value = 30 dataB.value = 30 dataB.value = 30 코드를 실행하면 dataB가 가르키는 30f39991 인스턴스의 value 값을 20에서 30으로 변경합니다. 그러나 dataA와 dataB는 같은 30f39991 인스턴스를 참조하기 때문에 dataA.value와 dataB.value는 같은 값인 30을 출력합니다. “여기서 핵심은 Data dataB = dataA 라고 했을 때 변수에 들어있는 값을 복사해서 사용한다는 점입니다.” 그런데 그 값이 참조값입니다. 따라서 dataA와 dataB는 같은 참조값을 가지게 되고, 두 변수는 같은 객체 인스턴스를 참조하게 됩니다.
Archive
· 2024-02-20
☕️[Java] 기본형과 참조형(1)
기본형과 참조형(1) 자바에서 참조형을 제대호 이해하는 것은 정말 중요합니다. 변수의 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류할 수 있습니다. 사용하는 값을 변수에 직접 넣을 수 있는 기본형 이전 포스팅의 예시 코드에서 본 Student student1 과 같이 객체가 저장된 메모리의 위치를 가르키는 참조값을 넣을 수 있는 참조형으로 분류할 수 있습니다. 기본형(Primitive Type) Int, long, double, boolean 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입을 “기본형” 이라고 합니다. 참조형(Reference Type) Student student1, int[] students와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입을 “참조형” 이라고 합니다. 참조형은 객체 또는 배열에 사용됩니다. 쉽게 이야기해서 기본형 변수에는 직접 사용할 수 있는 값이 들어있지만 참조형 변수에는 위치(참조값)이 들어가 있습니다. 참조형 변수를 통해서 뭔가 하려면 결국 참조값을 통해 위치로 이동해야 합니다. 기본형 vs 참조형 - 기본 “기본형” 은 숫자 10, 20과 같이 실제 사용하는 값을 변수에 담을 수 있습니다. 때문에 해당 값을 바로 사용할 수 있습니다. “참조형” 은 실제 사용하는 값을 변수에 담는 것이 아닙니다. 이름 그대로 실제 객체의 위치(참조, 주소)를 저장합니다. 참조형에는 객체와 배열이 있습니다. “객체” 는 .(dot)을 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있습니다. “배열” 은 []를 통해서 메모리 상에 생성된 배열을 찾아야가 사용할 수 있습니다. 기본형 vs 참조형 - 계산 기본형은 들어있는 값을 그대로 계산에 사용할 수 있습니다. 예) 더하고 빼고, 사용하고 등등,(숫자 같은 것들은 바로 계산할 수 있습니다.) 참조형은 들어있는 참조값을 그대로 사용할 수 없습니다. 주소지만 가지고는 할 수 있는게 없습니다. 주소지에 가야 실체가 있기 때문입니다. 예) 더하고 빼고 사용하고를 못합니다. 참조값만 가지고는 계산할 수 있는 것이 없습니다. “기본형은 연산이 가능하지만 참조형은 연산이 불가능합니다.” int a = 10, b = 20; int sum = a + b; 기본형은 변수에 실제 사용하는 값이 담겨있습니다. 따라서 +, -와 같은 연산이 가능합니다. Student s1 = new Student(); Student s2 = new Student(); s1 + s2 // 오류 발생. 참초형은 변수에 객체의 위치인 참조값이 들어있습니다. 참조값은 계산에 사용할 수 없습니다. 따라서 오류가 발생합니다. “물론 다음과 같이 .(dot)을 통해 객체의 기본형 멤버 변수에 접근한 경우에는 연산을 할 수 있습니다.” Student s1 = new Student(); s1.grade = 100; Student s2 = new Student(); s2 grade 90; int sum = s1.grade + s2.grade // 연산 가능 쉽게 이해하는 팁 “기본형을 제외한 나머지는 모두 참조형입니다.” 기본형은 소문자로 시작합니다. int, long, double, boolean 모두 소문자로 시작합니다. 기본형은 자바가 기본으로 제공하는 데이터 타입입니다. 이러한 기본형은 개발자가 새로 정의할 수 없습니다. 개발자는 참조형인 클래스만 직접 정의할 수 있습니다. 클래스는 대문자로 시작합니다. Student, String, 등… 클래스는 모두 참조형입니다. “참고 - String” 자바에서 String은 특별합니다. String은 사실 클래스입니다. 즉, 참조형입니다. 그런데 기본형처럼 문자 값을 바로 대입할 수 있습니다. 문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의 기능을 제공합니다. String에 대한 자세한 내용은 추후에 설명하겠습니다.
Archive
· 2024-02-20
☕️[Java] 배열 도입 - 리팩토링
배열 도입 - 리팩토링 아래 코드를 보면 배열을 사용한 덕분에 출력에서 for문을 “도입할 수 있게 되었습니다.” ClassStart5 package class1; public class ClassStart5 { public static void main(String[] args) { Student student1 = new Student(); student1.name = "학생1"; student1.age = 15; student1.grade = 90; Student student2 = new Student(); student2.name = "학생2"; student2.age = 16; student2.grade = 80; // 배열 선언 Student[] students = new Student[]{student1, student2}; // for문 적용 for (int i = 0; i < students.length; i++) { System.out.println("이름:" + students[i].name + " 나이:" + students[i].age + " 성적:" + students[i].grade); } } 배열 선언 최적화. 개발자가 직접 정의한 ‘Student‘ 타입도 일반적인 변수와 동일하게 배열을 생성할 때 포함할 수 있습니다. Student[] students = new Student[]{student1, student2}; 생성과 선언을 동시에 하는 경우 다음과 같이 더 최적화 할 수 있습니다. Student[] students = {student1, student2}; for문 최적화. 배열을 사용한 덕분에 for문을 사용해서 반복 작업을 깔끔하게 처리할 수 있습니다. for문 도입 for (int i = 0; i < students.length; i++) { System.out.println("이름:" + students[i].name + " 나이:" + students[i].age + " 성적:" + students[i].grade); } for문 - 반복 요소를 변수에 담아서 처리하기 for (int i = 0; i < students.length; i++) { Student s = students[i]; System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" + s.grade); } students[i].name, students[i].age 처럼 students[i]를 자주 접근하는 것이 번거롭다면 반복해서 사용하는 객체를 Students s와 같은 변수에 담아두고 사용해도 됩니다. 물론 이런 경우에는 다음과 같이 향상된 for문을 사용하는 것이 가장 깔끔합니다. 향상된 for문(Enhanced For Loop) for (int i = 0; i < students.length; i++) { Student s = students[i]; System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" + s.grade); }
Archive
· 2024-02-19
☕️[Java] 배열 도입
배열 도입 “배열” 을 사용하면 특정 타입을 연속한 데이터 구조로 묶어서 편리하게 관리할 수 있습니다. Student 클래스를 사용한 변수들도 Student 타입이기 때문에 학생도 “배열” 을 사용해서 “하나의 데이터 구조로 묶어서 관리” 할 수 있습니다. 클래스의 도입 포스팅 예시 코드 참고 -> 학생 클래스. Student 타입을 사용하는 “배열” 을 도입해봅시다. package class1; public class ClassStart4 { public static void main(String[] args) { Student student1 = new Student(); student1.name = "학생1"; student1.age = 15; student1.grade = 90; Student student2 = new Student(); student2.name = "학생2"; student2.age = 16; student2.grade = 80; Student[] students = new Student[2]; students[0] = student1; students[1] = student2; for (int i = 0; i < students.length; i++) { System.out.println("이름:" + students[i].name + " 나이:" + students[i].age + " 성적:" + students[i].grade); } } } 코드를 분석해 봅시다. Student student1 = new Student(); student1.name = "학생1"; student1.age = 15; student1.grade = 90; Student student2 = new Student(); student2.name = "학생2"; student2.age = 16; student2.grade = 80; Student 클래스를 기반으로 student1, student2 인스턴스를 생성합니다. 그리고 필요한 값을 채워둡니다. 배열에 참조값 대입 이번에는 Student를 담을 수 있는 배열을 생성하고, 해당 배열에 student1, student2 인스턴스를 보관해봅시다. Student[] students = new Student[2]; Student 변수를 2개 보관할 수 있는 사이즈 2의 배열을 만듭니다. Student 타입의 변수는 Student 인스턴스의 “참조값을 보관” 합니다. Student 배열의 각각의 항목도 “Student 타입의 변수일 뿐” 입니다. 따라서 “Student 타입의 참조값을 보관” 합니다. student1, student2 변수를 생각해보면 Student 타입의 참조값을 보관합니다. 배열에는 아직 참조값을 대입하지 않았기 때문에 참조값이 없다는 의미의 null 값으로 초기화 됩니다. 이제 배열에 객체를 보관해보겠습니다. students[0] = student1; students[1] = student2; // 자바에서 대입은 항상 변수에 들어 있는 값을 복사합니다. students[0] = x001; students[1] = x002; 잊지 말자 자바의 대원칙: “자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.” student1, student2에는 참조값이 보관되어 있습니다. 따라서 이 참조값이 배열에 저정됩니다. 또는 student1, student2에 보관된 참조값을 읽고 복사해서 배열에 대입한다고 표현합니다. 이제 배열은 x001, x002의 참조값을 가집니다. 참조값을 가지고 있기 때문에 x001(학생1), x002(학생), Student 인스턴스에 모두 접근할 수 있습니다. 너무 중요해서 한 번더 강조합니다 잊지 말자 자바의 대원칙: “자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.” students[0] = student1; students[1] = student2; // 자바에서 대입은 항상 변수에 들어 있는 값을 복사합니다. students[0] = x001; students[1] = x002; 자바에서 변수의 대입(=)은 모두 변수에 들어있는 값을 복사해서 전달하는 것입니다. 이 경우 오른쪽 변수인 student1, student2에는 참조값이 들어있습니다. 그래서 이 값을 복사해서 왼쪽에 있는 배열에 전달합니다. 따라서 기존 student1, student2에 들어있던 참조값은 당연히 그대로 유지됩니다. 주의!! 변수에는 인스턴스 자체가 들어있는 것이 아닙니다! “인스턴스의 위치를 가리키는 참조값이 들어있을 뿐입니다!!” 따라서 대입(=)시에 인스턴스가 복사되는 것이 아니라 참조값만 복사됩니다. 배열에 들어있는 객체 사용 배열에 들어있는 객체를 사용하려면 먼저 배열에 접근하고, 그 다음에 객체에 접근하면 됩니다. 이전에 설명한 그림과 코드를 함께 보면 쉽게 이해가 될 것입니다. 학생1 예제 System.out.println(students[0].name); // 배열 접근 시작 System.out.println(x005[0].name); // [0]를 사용해서 x005 배열의 0번 요소에 접근 System.out.println(x001.name); // .(dot)을 사용해서 참조값으로 객체에 접근 System.out.println("학생1"); 학생2 예제 System.out.println(students[1].name); // 배열 접근 시작 System.out.println(x005[1].name); // [0]를 사용해서 x005 배열의 1번 요소에 접근 System.out.println(x002.name); // .(dot)을 사용해서 참조값으로 객체에 접근 System.out.println("학생2");
Archive
· 2024-02-17
☕️[Java] 객체 사용
객체 사용 “클래스를 통해 생성한 객체를 사용하려면 먼저 메모리에 존재하는 객체에 접근해야 합니다.” 객체에 접근하려면 .(점, dot)을 사용하면 됩니다. // 객체 값 대입 student1.name = "학생1"; student1.age = 15; student1.grade = 90; // 객체 값 사용 System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade); 객체에 값 대입 객체가 가지고 있는 멤버 변수(name, age, grade)에 값을 대입하려면 먼저 객체에 접근해야 합니다. 객체에 접근하려면 .(점, dot) 키워드를 사용하면 됩니다. 이 키워드는 변수(student1)에 들어있는 참조값(x001)을 읽어서 메모리에 존재하는 객체에 접근합니다. 순서를 간단히 풀어보겠습니다. student1.name = "학생1" // 1. student1 객체의 name 멤버 변수에 값 대입 x001.name = "학생1" // 2. 변수에 있는 참조값을 통해 실제 객체에 접근, 해당 객체의 name 멤버 변수에 값 대입 student1.(dot)이라고 하면 student1 변수가 가지고 있는 참조값을 통해 실제 객체에 접근합니다. student1은 x001이라는 참조값을 가지고 있으므로 x001 위치에 있는 Student 객체에 접근합니다. 객체 값 읽기 객체의 값을 읽는 것도 앞서 설명한 내용과 같습니다. .(점, dot) 키워드를 통해 참조값을 사용해서 객체에 접근한 다음에 원하는 작업을 하면 됩니다. 아래 예시 코드를 봅시다. // 1. 객체 값 읽기. System.out.println("이름:" + student1.name); // 2. 변수에 있는 참조값을 통해 실제 객체에 접근하고, name 멤버 변수에 접근합니다. System.out.println("이름:" + x001.name); // 3. 객체의 멤버 변수의 값을 읽어옵니다. System.out.println("이름:" + "학생1");
Archive
· 2024-02-16
☕️[Java] 클래스, 객체, 인스턴스 정리
클래스, 객체, 인스턴스 정리. 클래스 - Class 클래스는 객체를 생성하기 위한 “틀” 또는 “설계도” 입니다. 클래스는 객체가 가져야 할 “속성(변수)” 와 “기능(메서드)” 를 정의합니다. 예를 들어 학생이라는 클래스는 속성(변수)으로 name, age, grade를 가집니다. 참고로 기능(메서드)은 추후에 설명합니다. 지금은 속성(변수)에 집중합시다. “틀” : 붕어빵 틀을 생각해봅시다. 붕어빵 틀은 붕어빵이 아닙니다! 이렇게 생긴 붕어빵이 나왔으면 좋겠다고 만드는 틀일 뿐입니다. 실제 먹을 수 있는 것이 아닙니다. 실제 먹을 수 있는 팥 붕어빵을 객체 또는 인스턴스라고 합니다. “설계도” : 자동차 설계도를 생각해봅시다. 자동차 설계도는 자동차가 아닙니다! 설계도는 실제 존재하는 것이 아니라 개념으로만 있는 것입니다. 설계도를 통한 생산한 실제 존재하는 흰색 테슬라 모델 Y 자동차를 객체 또는 인스턴스라고 합니다. 객체 - Object 객체는 클래스에서 정의한 속성과 기능을 가진 실체입니다. 객체는 서로 독립적인 상태를 가집니다. 예를 들어 위 코드에서 student1은 학생1의 속성을 가지는 객체이고, student2는 학생2의 속성을 가지는 객체입니다. student1 과 student2는 같은 클래스에서 만들어졌지만, 서로 다른 객체입니다. 인스턴스 - Instance 인스턴스는 특정 클래스로부터 생성된 객체를 의미합니다. 그래서 객체와 인스턴스라는 용어는 자주 혼용됩니다. 인스턴스는 주로 객체가 어떤 클래스에 속해 있는지 강조할 때 사용합니다. 예를 들어서 “student1 객체는 Student 클래스의 인스턴스다.” 라고 표현합니다. 객체 vs 인스턴스 둘 다 클래스에서 나온 실체라는 의미에서 비슷하게 사용되지만, 용어상 인스턴스는 “객체보다 좀 더 관계에 초점을 맞춘 단어입니다.” 보통 “student1은 Student의 객체이다.” 라고 말하는 대신 “student1은 Student의 인스턴스이다.” 라고 특정 클래스와의 “관계를 명확히 할 때 인스턴스” 라는 용어를 주로 사용합니다. 좀 더 쉽게 풀어보자면, 모든 인스턴스는 객체이지만, 우리가 “인스턴스” 라고 부르는 순간 “특정 클래스로부터 그 객체가 생성되었음을 강조하고 싶을 때입니다.” 예를 들어, student1은 객체이지만, 이 객체가 Student 클래스로부터 생성된다는 점을 명확히 하기 위해 student1을 Student의 인스턴스라고 부릅니다. 하지만 둘 다 클래스에서 나온 실체라는 핵심 의미는 같기 때문에 보통 둘을 구분하지 않고 사용합니다.
Archive
· 2024-02-16
☕️[Java] 클래스 도입
클래스 도입 클래스를 사용해서 “학생”이라는 개념을 만들고, 각 학생 별로 본인의 이름, 나이, 성적을 관리해봅시다. 우선 코드를 봐봅시다. package class1; public class Student { String name; int age; int grade; } class 키워드를 사용해서 학생 클래스(Student)를 정의합니다. 학생 클래스는 내부에 이름(name), 나이(age), 성적(grade) 변수를 가집니다. 이렇게 “클래스에 정의한 변수들을 멤버 변수, 또는 필드” 라 합니다. 멤버 변수(Member Variable) : 이 변수들은 특정 클래스에 소속된 멤버이기 때문에 이렇게 부릅니다. 필드(Field) : 데이터 항목을 가르키는 전통적인 용어입니다. 데이터베이스, 엑셀 등에서 데이터 각각의 항목을 필드라고 합니다. 자바에서 멤버 변수, 필드는 같은 뜻입니다. 클래스에 소속된 변수를 뜻합니다. 클래스는 관례상 대문자로 시작하고 낙타 표기법을 사용합니다. 예) Student, User, MemberService package class1; public class ClassStart3 { public static void main(String[] args) { Student student1; student1 = new Student(); student1.name = "학생1"; student1.age = 15; student1.grade = 90; Student student2 = new Student(); student2.name = "학생2"; student2.age = 16; student2.grade = 80; System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade); System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade); } } 실행 결과 이름:학생1 나이:15 성적:90 이름:학생2 나이:16 성적:80 클래스와 사용자 정의 타입 “타입” 은 데이터의 종류나 형태를 나타냅니다. int라고 하면 정수 타입, String이라고 하면 문자 타입입니다. 그러면 학생(Student)이라는 타입을 만들면 되지 않을까요? 클래스를 사용하면 int, String과 같은 타입을 직접 만들 수 있습니다. 사용자가 직접 정의하는 “사용자 정의 타입” 을 만들려면 설계도가 필요합니다. 이 “이 설계도가 바로 클래스” 입니다. “설계도”인 “클래스”를 사용해서 “실제 메모리에 만들어진 실체를 객체 또는 인스턴스” 라 합니다. “클래스”를 통해서 사용자가 원하는 종류의 데이터 타입을 마음껏 정의할 수 있습니다. 용어: 클래스, 객체, 인스턴스 클래스는 설계도이고, 이 설계도를 기반으로 실제 메모리에 만들어진 실체를 “객체 또는 인스턴스” 라 합니다. 둘다 같은 의미로 사용됩니다. 여기서는 학생(Student) 클래스를 기반으로 학생1(student1), 학생2(student2) 객체 또는 인스턴스를 만들었습니다. 코드 분석 1. 변수 선언. Student student1 // Student 변수 선언 Student student1 Student “타입” 을 받을 수 있는 변수를 선언합니다. int는 정수를, String은 문자를 담을 수 있듯이 “Student는 Student 타입의 객체(인스턴스)를 받을 수 있습니다.” 2. 객체 생셩. student1 = new Student() // Student 인스턴스 생성 student1 = new Student() 코드를 나누어 분석해봅시다. 객체를 사용하려면 먼저 설계도인 클래스를 기반으로 객체(인스턴스)를 생성해야 합니다. new Student()에서의 new는 새로 생성한다는 뜻 입니다. new Student()는 Student 클래스 정보를 기반으로 새로운 객체를 생성하라는 뜻입니다. 이렇게 하면 메모리에 실제 Student 객체(인스턴스)를 생성합니다. 객체를 생성할 때는 new 클래스명()을 사용하면 됩니다. 마지막에 ()도 추가해야합니다. Student 클래스는 String name, int age, int grade 멤버 변수를 가지고 있습니다. 이 변수를 사용하는데 필요한 메모리 공간도 함께 확보합니다. 3. 참조값 보관 student1 = x001l // Student 인스턴스 참조값 보관 객체를 생성시 자바는 메모리 어딘가에 있는 이 객체에 접근할 수 있는 참조값(주소)(x001)을 반환합니다. 여기셔 x001이라고 표현한 것이 참조값입니다.(실제로 x001처럼 표현되는 것은 아니고 이해를 돕기 위한 예시입니다.) new 키워드를 통해 객체가 생성되고 나면 참조값을 반환합니다. 앞서 선언한 변수인 Student student1에 생성된 객체의 참조값(x001)을 보관합니다. Student student1 변수는 이제 메모리에 존재하는 실제 Student 객체(인스턴스)의 참조값을 가지고 있습니다. student1 변수는 방금 만든 객체에 접근할 수 있는 참조값을 가지고 있습니다. 따라서 이 변수를 통해서 객체를 접근(참조)할 수 있습니다. 쉽게 이야기해서 student1 변수를 통해 메모리에 있는 실제 객체를 접근하고 사용할 수 있습니다. 참조값을 변수에 보관해야 하는 이유 객체를 생성하는 new Student() 코드 자체에는 아무런 이름이 없습니다. “이 코드는 단순히 Student 클래스를 기반으로 메모리에 실제 객체를 만드는 것 입니다.” 따라서 생성한 객체에 접근할 수 있는 방법이 필요합니다. 이런 이유로 객체를 생성할 때 반환되는 참조값을 어딘가에 보관해두어야 합니다. 앞서 Student student1 변수에 참조값(x001)을 저장해두었으므로 저장한 참조값(x001)을 통해서 실제 메모리에 존재하는 객체에 접근할 수 있습니다. 지금까지 설명한 내용을 간단히 풀어보면 다음과 같습니다. Student student1 = new Student(); // 1. Student 객체 생성 Student student1 = x001; // 2. new Student()의 결과로 x001 참조값 변환 student1 = x001; // 3. 최종 결과 이후에 학생2(student2)까지 생성하면 다음과 같이 Student 객체(인스턴스)가 메모리에 2개 생성됩니다. “각각의 참조값이 다르므로 서로 구분할 수 있습니다.” 참조값을 확인하고 싶다면 다음과 같이 객체를 담고 있는 변수를 출력해보면 됩니다. System.out.println(student1); System.out.println(student2); 출력 결과 class1.Student@30f39991 class1.Student@452b3a41 @ 앞은 패키지 + 클래스 정보를 뜻합니다. @ 뒤에 16진수는 참조값을 뜻합니다.
Archive
· 2024-02-16
☕️[Java] 클래스가 필요한 이유.
클래스가 필요한 이유. 먼저 아래의 학생 정보 출력 프로그램을 만들어 봅시다. 이 프로그램은 두 명의 학생 정보를 출력하는 프로그램입니다. 각 학생은 이름, 나이, 성적을 가지고 있습니다. 요구사항 첫 번째 학생의 이름은 “학생1”, 나이는 15, 성적은 90 입니다. 두 번째 학생의 이름은 “학생2”, 나이는 16, 성적은 80 입니다. 각 학생의 정보를 다음과 같은 형식으로 출력해야 합니다: "이름: [이름] 나이: [나이] 성적: [성적]" 변수를 사용해서 학생 정보를 저장하고 변수를 사용해서 학생 정보를 출력해야 합니다. 예시 출력 이름: 학생1 나이: 15 성적: 90 이름: 학생2 나이: 16 성적: 80 변수를 사용해서 이 문제를 풀어보면 다음과 같이 프로그램을 만들어볼 수 있습니다. package class1; public class ClassStart1 { public static void main(String[] args) { String firstStudentName = "학생1"; String secondStudentName = "학생2"; int firstStudentAge = 15; int secondStudentAge = 16; int firstStudentGrade = 90; int secondStudentGrade = 80; System.out.println("이름: " + firstStudentName + " 나이: " + firstStudentAge + " 성적: " + firstStudentGrade); System.out.println("이름: " + secondStudentName + " 나이: " + secondStudentAge + " 성적: " + secondStudentGrade); } } 위 코드에서는 학생 2명을 다루어야 하기 때문에 각각 다른 변수를 사용했습니다. 이 코드의 문제는 학생이 늘어날 때 마다 변수를 추가로 선언해야 하고, 또 출력하는 코드도 추가해야 합니다. 이런 문제를 어떻게 해결할 수 있을까요? 이번에는 위 코드를 “배열을 사용하여 리펙토링” 해봅시다. 아래의 코드는 “배열을 활용한 코드” 입니다. package class1; public class ClassStart2 { public static void main(String[] args) { String[] studentNames = { "학생1", "학생2" }; int [] studentAges = { 15, 16 }; int [] studentGrades = { 90, 80 }; for (int i = 0; i < studentNames.length; i++) { System.out.println("이름: " + studentNames[i] + " 나이: " + studentAges[i] + " 성적: " + studentGrades[i]); } } } 이전 코드보다 훨씬 깔끔해졌으며, 위 코드에서 문제가 됐었던 학생이 늘어날 때 마다 변수를 새롭게 추가할 필요도 없어졌습니다. 배열 사용의 한계. 하지만 배열을 사용해서 코드를 최소화하는데 성공했지만 “한 한생의 데이터가 studentNames[], studentAges[], studentGrades라는 3개의 배열에 나누어져 있습니다.” 따라서 “데이터를 변경할 때 매우 조심해서 작업해야 합니다.” 예를 들어서 학생 2의 데이터를 제거하려면 각각의 배열마다 학생2의 요소를 정확하게 찾아서 제거해주어야 합니다. 학생2 제거 String[] studentNames = { "학생1", "학생3", "학생4", "학생5" }; int [] studentAges = { 15, 17, 10, 16 }; int [] studentGrades = { 90, 100, 80, 50 }; 한 학생의 데이터가 3개의 배열에 나누어져 있기 때문에 3개의 배열을 각각 변경해야 합니다!! 그리고 한 학생의 데이터를 관리하기 위해 3개의 배열의 인덱스 순서를 항상 정확하게 맞추어야 합니다.(조금이라도 실수하면 😱) 이렇게 하면 특정 학생의 데이터를 변경시 실수할 가능성이 매우 높습니다. 이 코드는 컴퓨터가 볼 때는 아무 문제가 없지만, 사람이 관리하기에는 좋은 코드가 아닙니다. 😵💫 정리 지금처럼 이름, 나이, 성적을 각각 따로 나누어서 관리하는 것은 사람이 관리하기 좋은 방식이 아닙니다. 사람이 관리하기 좋은 방식은 학생이라는 개념을 하나로 묶는 것입니다. 그리고 각각의 학생 별로 본인의 이름, 나이, 성적을 관리하는 것 입니다.
Archive
· 2024-02-15
☕️[Java] 메서드 파트 정리.
정리 변수명 vs 메서드명 변수 이름은 일반적으로 명사를 사용합니다. 한편 메서드는 무언가 동작하는데 사용하기 때문에 일반적으로 동사로 시작합니다. 이런 차이점 외에는 변수 이름과 메서드 이름에 대한 규칙은 둘다 같습니다. 변수명 예): customerName, totalSum, employeeCount, isAvailable 메서드명 예): printReport(), calculateSum(), addCustomer(), getEmployeeCount(), setEmployeeName() 메서드 사용의 장점 코드 재사용 : 메서드는 특정 기능을 캡슐화하므로, 필요할 때마다 그 기능을 다시 작성할 필요 없이 해당 메서드를 호출함으로써 코드를 재사용할 수 있습니다. 코드의 가독성 : 이름이 부여된 메서드는 코드가 수행하는 작업을 명확하게 나타내므로, 코드를 읽는 사람에게 추가적인 문맥을 제공합니다. 모듈성 : 큰 프로그램을 작은, 관리 가능한 부분으로 나눌 수 있습니다. 이는 코드의 가독성을 향상시키고 디버깅을 쉽게 만듭니다. 코드 유지 관리 : 메서드를 사용하면, 코드의 특정 부분에서 문제가 발생하거나 업데이트가 필요한 경우 해당 메서드만 수정하면 됩니다. 이렇게 하면 전체 코드 베이스에 영향을 주지 않고 변경 사항을 적용할 수 있습니다. 재사용성과 확장성 : 잘 설계된 메서드는 다른 프로그램이나 프로젝트에서도 재사용할 수 있으며, 새로운 기능을 추가하거나 기존 기능을 확장하는 데 유용합니다. 추상화 : 메서드를 사용하는 곳에서는 메서드의 구현을 몰라도 됩니다. 프로그램의 다른 부분에서는 복잡한 내부 작업에 대해 알 필요 없이 메서드를 사용할 수 있습니다. 테스트와 디버깅 용이성 : 개별 메서드는 독립적으로 테스트하고 디버그할 수 있습니다. 이는 코드의 문제를 신속하게 찾고 수정하는데 도움이 됩니다. 따라서, 메서드는 효율적이고 유지 보수가 가능한 코드를 작성하는 데 매우 중요한 도구입니다. 자바에서의 대원칙. “자바는 항상 변수의 값을 복사해서 대입합니다.” 이 대원칙은 반드시 이해해야 합니다. 그러면 아무리 복잡한 상황에서도 코드를 단순하게 이해할 수 있습니다. package method; public class MethodValue0 { public static void main(String[] args) { int num1 = 5; int num2 = num1; num2 = 10; System.out.println("num1=" + num1); System.out.println("num2=" + num2); } } 실행 결과 num1 = 5 num2 = 10 용어: 메서드 시그니처(method signature) 메서드 시그니처 = 메서드 이름 + 매개변수 타입(순서) 메서드 시그니처는 자바에서 메서드를 구분할 수 있는 고유한 식별자나 서명을 뜻합니다. 메서드 시그니처는 메서드의 이름과 매개변수 타입(순서 포함)으로 구성되어 있습니다. 쉽게 이야기해서 메서드를 구분할 수 있는 기준입니다. 자바 입장에서는 각각의 메서드를 고유하게 구분할 수 있어야 합니다. 그래야 어떤 메서드를 호출 할 지 결정할 . 수있습니다. 따라서 메서드 오버로딩과 같이 메서드 이름이 같아도 메서드 시그니처가 다르면 다른 메서드로 간주합니다. 반환 타입은 시그니처에 포함되지 않습니다.
Archive
· 2024-02-15
☕️[Java] 메서드 리펙토링 - 입.출금
MethodEx3 package method.ex; public class MethodEx3 { public static void main(String[] args) { int balance = 10000; // 입금 int depositAmount = 1000; balance += depositAmount; System.out.println(depositAmount + "원을 입급하였습니다. 현재 잔액: " + balance + "원"); // 출금 int withdrawAmount = 2000; if (balance >= withdrawAmount) { balance -= withdrawAmount; System.out.println(withdrawAmount + "원을 출금하였습니다. 현재 잔액: " + balance + "원"); } else { System.out.println(withdrawAmount + "원을 출금하려 했으나 잔액이 부족합니다."); } System.out.println("최종 잔액: " + balance + "원"); } } 위 코드는 입금, 출금을 나타내는 코드입니다. “입금(deposit)” 과 “출금(withdraw)” 을 메서드로 만들어서 리펙토링 해보겠습니다. MethodEx3Ref package method.ex; public class MethodEx3Ref { public static void main(String[] args) { int balance = 10000; balance = deposit(10000, 1000); balance = withdraw(balance, 2000); System.out.println("최종 잔액: " + balance + "원"); } public static int deposit(int balance, int amount) { balance += amount; System.out.println(amount + "원을 입급하였습니다. 현재 잔액: " + balance + "원"); return balance; } public static int withdraw(int balance, int amount) { if (balance >= amount) { balance -= amount; System.out.println(amount + "원을 출금하였습니다. 현재 잔액: " + balance + "원"); } else { System.out.println(amount + "원을 출금하려 했으나 잔액이 부족합니다."); } return balance; } } 위 코드는 리펙토링을 한 코드입니다. 리펙토링 결과를 보면 main()은 세세한 코드가 아니라 전체 구조를 한눈에 볼 수 있게 되었습니다. “쉽게 이야기해서 책의 목자를 보는 것 같습니다.” 더 자세히 알고 싶으면 해당 메서드를 찾아서 들어가면 됩니다. 그리고 입금과 출금 부분이 메서드로 명확하게 분리되었기 때문에 이후에 변경 사항이 발생하면 관련된 메서드만 수정하면 됩니다. 특정 메서드로 수정 범위가 한정되기 때문에 더 유지보수 하기 좋습니다. “이런 리펙토링을 메서드 추출(Extract Method)이라 합니다.” 메서드를 재사용하는 목적이 아니어도 괜찮습니다. “메서드를 적절하게 사용해서 분류하면 구조적으로 읽기 쉽고 유지보수 하기 좋은 코드를 만들 수 있습니다.” 그리고 “메서드의 이름 덕분에 프로그램을 더 읽기 좋게 만들 수 있습니다.” // MethodEx3 내부코드에서의 // <=== 입금로직 int depositAmount = 1000; balance += depositAmount; System.out.println(depositAmount + "원을 입급하였습니다. 현재 잔액: " + balance + "원"); // ===> // <=== 출금로직 int withdrawAmount = 2000; if (balance >= withdrawAmount) { balance -= withdrawAmount; System.out.println(withdrawAmount + "원을 출금하였습니다. 현재 잔액: " + balance + "원"); } else { System.out.println(withdrawAmount + "원을 출금하려 했으나 잔액이 부족합니다."); } // ===> 위 코드 조각에서 볼 수 있듯 입금로직 부분과 출금로직 부분을 뽑아내어 // MethodEx3Ref 내부 메소드 // 입금 메서드 public static int deposit(int balance, int amount) { balance += amount; System.out.println(amount + "원을 입급하였습니다. 현재 잔액: " + balance + "원"); return balance; } // 출금 메서드 public static int withdraw(int balance, int amount) { if (balance >= amount) { balance -= amount; System.out.println(amount + "원을 출금하였습니다. 현재 잔액: " + balance + "원"); } else { System.out.println(amount + "원을 출금하려 했으나 잔액이 부족합니다."); } return balance; } 위의 두 deposit(입금) 메서드와 withdraw(출금) 메서드를 만들었습니다. 이런 리펙토링을 메서드 추출(Extract Method) 이라 합니다. 메서드가 단순하게 코드를 재사용하고 여러 곳에서 같이 사용한다는 것을 떠나서, 구조적으로 우선 비슷한 것을 그룹화하거나 카테고리화하면 이미 그것만으로도 큰 효용가치가 있습니다. 그래서 관련된 코드를 모아서 하나로 딱 묶어두는 것 입니다. 그러면 읽기도 좋고, 유지보수에도 좋습니다.
Archive
· 2024-02-14
☕️[Java] 메서드.
메서드의 필요성을 알기위해서 아래의 코드를 보고 실제로 느껴보겠습니다. 아래의 코드는 두 숫자를 입력 받아서 더하고 출력하는 단순한 기능을하는 프로그램입니다. 먼저 1+2를 수행하고, 그 다음으로 10 + 20을 수행합니다. package method; public class Method1 { public static void main(String[] args) { // 계산1 int a = 1; int b = 2; System.out.println(a + "+" + b + " 연산 수행"); int sum1 = a + b; System.out.println("결과1 출력: " + sum1); // 계산2 int x = 10; int y = 20; System.out.println(x + "+" + y + " 연산 수행"); int sum2 = x + y; System.out.println("결과2 출력:" + sum2); } } 위 코드는 다음과 같은 특징이 있습니다. 같은 연산을 두 번 수행합니다. 코드를 잘보면 계산 1 부분과, 계산 2 부분이 거의 같습니다. 계산 1 int a = 1; int b = 2; System.out.println(a + "+" + b + " 연산 수행"); int sum1 = a + b; 계산 2 int x = 10; int y = 20; System.out.println(x + "+" + y + " 연산 수행"); int sum2 = x + y; 계산 1과 2 둘 다 변수를 두 개 선언하고, 어떤 연산을 수행하는지 출력하고, 두 변수를 더해서 결과를 구합니다. 만약 프로그램의 여러 곳에서 이와 같은 계산을 반복해야 할 경우에는 같은 코드를 여러번 반복해서 작성해야 할 것입니다. 더 나아가서 어떤 연산을 수행하는지 출력하는 부분을 변경하거나 또는 제거하고 싶다면 해당 코드를 다 찾아다니면서 모두 수정해야 할 것 입니다. 함수(Function) 함수 정의 add(a, b) = a + b 이름이 add이고, a,b라는 두 값을 받는 함수입니다. 그리고 이 함수는 a + b 연산을 수행합니다. 함수 사용 add(1,2) -> 결과: 3 add(5,6) -> 결과: 11 add(3,5) -> 결과: 8 함수에 값을 입력하면, 함수가 가진 연산을 처리한 다음 결과를 출력합니다. 여기서는 단순히 a + b라는 연산을 수행합니다. 여러번 같은 계산을 해야 한다면 지금처럼 함수를 만들어두고(정의), 필요한 입력 값을 넣어서 해당 함수를 호출하면 됩니다. 그러면 계산된 결과가 나옵니다. 함수는 마치 마술상자와 같습니다. 함수를 호출할 때는 외부에서는 필요한 값만 입력하면 됩니다. 그러면 계산된 결과가 출력됩니다. 내부 구조는 어떻게 되어있는지 알 필요가 없습니다. 같은 함수를 다른 입력 값으로 여러번 호출할 수 있습니다. 여기서 핵심은 함수를 한 번 정의해두면 계속해서 재사용할 수 있다는 점입니다! 평균 함수 만약 두 수의 평균을 구해야 한다면 매번 (a + b) / 2라는 공식을 사용해야 할 것입니다. 이것을 함수로 만들어두면 다음과 같이 사용할 수 있습니다. 함수 정의 avg(a, b) = (a + b) / 2 함수 사용 avg(4, 6) -> 결과: 5 avg(10, 20) -> 결과: 15 avg(100, 200) -> 결과: 150 수하의 함수의 개념을 프로그래밍에 가지고 온다면 어떨까요? 필요한 기능을 미리 정의해두고 필요할 때 마다 호출해서 사용할 수 있기 때문에 앞서 고민한 문제들을 해결할 수 있을 것 같습니다. 프로그램 언어들은 오래 전 부터 이런 문제를 해결하기 위해 수학의 함수라는 개념을 차용해서 사용합니다.
Archive
· 2024-02-13
☕️[Java] 반환타입.
반환 타입. 반환 타입이 있으면 반드시 값을 반환해야 합니다. 반환 타입이 있는 메서드는 반드시 return을 사용해서 값을 반환해야 합니다. 이 부분은 특히 조건문과 함께 사용할 때 주의해야 합니다. MethodReturn1 package method; public class MethodReturn1 { public static void main(String[] args) { boolean result = odd(2); System.out.println(result); } public static boolean odd(int i) { if (i % 2 == 1) { return true; } } } 위 코드에서 if 조건이 만족할 때는 true가 반환됩니다. 하지만 조건을 만족하지 않는 경우에는 return문이 실행되지 않습니다. 따라서 위 코드를 실행하면 return 문을 누락했다는 컴파일 오류가 발생합니다. 컴파일 오류 java: missing return statement MethodReturn1 - 수정코드 package method; public class MethodReturn1 { public static void main(String[] args) { boolean result = odd(2); System.out.println(result); } public static boolean odd(int i) { if (i % 2 == 1) { return true; } else { return false; } } } 위와 같이 수정하면 ìf 조건을 만족하지 않아도 else를 통해 return문이 실행됩니다. return 문을 만나면 그 즉시 메서드를 빠져나갑니다. return 문을 만나면 그 즉시 해당 메서드를 빠져나갑니다. 다음 로직을 수행하는 메서드를 만들어보겠습니다. 18세 미만의 경우: 미성년자는 출입이 불가합니다. 18세 이상의 경우: 입장하세요. MethodReturn2 package method; public class MethodReturn2 { public static void main(String[] args) { // checkAge(10); checkAge(18); } public static void checkAge(int age) { if (age < 18) { System.out.println(age + "세, 미성년자는 출입이 불가합니다."); return; } System.out.println(age + "세, 입장하세요."); } } 18세 미만의 경우, “미성년자는 출입이 불가능합니다.”를 출력하고 바로 return문이 수행됩니다. 따라서 다음 로직을 수행하지 않고, 해당 메서드를 빠져나옵니다. 18세 이상의 경우, “입장하세요.”를 출력하고, 메서드가 종료됩니다. 참고로 반환 타입이 없는 void형이기 때문에 마지막 줄의 return은 생략할 수 있습니다. 반환 값 무시. 반환 타입이 있는 메서드를 호출했는데 만약 반환 값이 필요없다면 사용하지 않아도 됩니다. 예시 1) int sum = add(1,2) 반환된 값을 받아서 ‘sum’에 저장했습니다. 예시 2) add(1,2) 반환된 값을 사용하지 않고 버립니다. 여기서는 ‘예시 1’과 같이 호출 결과를 변수에 담지 않았습니다. 단순히 메서드만 호출했습니다.
Archive
· 2024-02-13
☕️[Java] 메서드정의
메서드 정의 메서드는 다음과 같이 정의합니다. public static int add(int a, int b) { // 메서드 본문, 실행 코드 } 제어자 반환타입 메서드이름(매개변수 목록) { 메서드 본문 } 제어자(Modifier) : public, static 과 같은 부분입니다. 제어자는 추후에 학습 후 정리하겠습니다. 반환 타입(Return Type) : 메서드가 실행 된 후 반환하는 데이터의 타입을 지정합니다. 메서드가 값을 반환하지 않는 경우, 없다는 뜻의 void를 사용해야 합니다. 예) void print(String str) 메서드 이름(Method Name) : 메서드의 이름입니다. 이 이름은 메서드를 호출하는 데 사용됩니다. 매개변수(Parameter) : 입력 값으로, 메서드 내부에서 사용할 수 있는 변수입니다. 매개변수(Parameter)는 옵션입니다. 입력값이 필요 없는 메서드는 매개변수(Parameter)를 지정하지 않아도 됩니다. 예) add() 메서드 본문(Method Body) : 실제 메서드의 코드가 위치합니다. 중괄호 {} 사이에 코드를 작성합니다. 매개변수가 없거나 반환 타입이 없는 경우 매개변수가 없고, 반환 타입도 없는 메서드를 확인해봅시다. package method; public class Method2 { public static void main(String[] args) { // 매개변수가 없기 때문에 전달하는 인수,인자도 없다. printHeader(); System.out.println("프로그램이 동작합니다."); printFooter(); } // 반환타입과 매개변수가 없는 함수 public static void printHeader() { System.out.println("= 프로그램을 시작합니다 ="); return; // void의 경우 생략 가능 } public static void printFooter() { System.out.println("= 프로그램을 종료합니다 ="); } } 실행 결과 = 프로그램을 시작합니다 = 프로그램이 동작합니다. = 프로그램을 종료합니다 = printHeader(), printFooter() 메서드는 매개변수가 없고, 반환 타입도 없습니다. 매개변수가 없는 경우 선언 : public static void printHeader()와 같이 매개변수를 비워두고 정의하면 됩니다. 호출 : printHeader();와 같이 인수를 비워두고 호출하면 됩니다. 반환 타입이 없는 경우 선언 : public static void printHeader()와 같이 반환 타입을 void로 정의하면 됩니다. 호출 : printHeader(); 와 같이 반환 타입이 없으므로 메서드만 호출하고 반환 값을 받지 않으면 됩니다. String str = printHeader(); 반환 타입이 void이기 때문에 이렇게 반환 값을 받으면 컴파일 오류가 발생합니다. void와 return 생략 모든 메서드는 항상 return을 호출해야 합니다. 그런데 반환 타입 void의 경우에는 예외로 printFooter()와 같이 생략해도 됩니다. 자바 컴파일러가 반환 타입이 없는 경우에는 return을 마지막줄에 넣어줍니다. 참고로 return을 만나면 해당 메서드는 종료됩니다.
Archive
· 2024-02-13
☕️[Java] 메서드(2)
메서드 정의 public static int add(int a, int b) { System.out.println(a + "+" + b + " 연산 수행"); int sum = a + b; return sum; } 위 코드가 바로 메서드입니다. 이것을 함수를 정의하는 것과 같이, 메서드를 정의한다고 표현합니다. 메서드는 수학의 함수와 유사하게 생겼습니다. 함수에 값을 입력하면, 어떤 연산을 처리한 다음에 결과를 반환합니다. 수학에 너무 집중하지 않아도 됩니다, 단순히 무언가 정의해두고 필요할 때 불러서 사용한다는 개념으로 이해하면 충분합니다. 메서드는 크게 “메서드 선언” 과 “매서드 본문” 으로 나눌 수 있습니다. 메서드 선언(Method Declaration) public static int add(int a, int b) 메서드의 선언 부분으로, 메서드 이름, 반환 타입, 파라미터(매개변수) 목록을 포함합니다. 이름 그대로 이런 메서드가 있다고 선언하는 것입니다. 메서드 선언 정보를 통해 다른 곳에서 해당 메서드를 호출할 수 있습니다. public static public: 다른 클래스에서 호출할 수 있는 메서드라는 뜻입니다. (접근 제어에서 학습할 예정) static: 객체를 생성하지 않고 호출할 수 있는 정적 메서드라는 뜻입니다. (자세한 내용은 추후에 정리) int add(int a, int b) int: 반환 타입을 정의합니다. 메서드의 실행 결과를 반환할 때 사용할 반환 타입을 지정합니다. add: 메서드의 이름입니다. 이 이름으로 메서드를 호출할 수 있습니다. (int a, int b): 메서드를 호출할 때 전달하는 입력 값을 정의합니다. 이 변수들은 해당 메서드 안에서만 사용됩니다. 이렇게 메서드 선언에 사용되는 변수를 영어로 파라미터(parameter), 한글로 매개변수라 합니다. 메서드 본문(Method Body) { System.out.println(a + "+" + b + " 연산 수행"); int sum = a + b; return sum; } 메서드가 수행해야 하는 코드 블록입니다. 메서드를 호출하면 메서드 본문이 순서대로 실행됩니다. 메서드 본문은 마술상자입니다. 메서드를 호출하는 곳에서는 메서드 선언은 알지만 메서드 본문은 모르기 때문입니다. 메서드의 실행 결과를 반환하려면 return문을 사용해야 합니다. return문은 다음에 반환할 결과를 적어주면 됩니다. return sum: sum 변수에 들어있는 값을 반환합니다. 메서드 호출 앞서 정의한 메서드를 호출해서 실행하려면 메서드 이름에 입력 값을 전달하면 됩니다. 보통 메서드를 호출한다고 표현합니다. int sum1 = add(5, 10); int sum2 = add(15, 20); 메서드를 호출하면 어떻게 실행되는지 순서대로 확인해봅시다. int sum1 = add(5, 10); // add라는 메서드를 숫자 5, 10을 전달하면서 호출합니다. int sum1 = 15; // add(5, 10)이 실행됩니다. 실행 결과 반환 값은 15입니다. 메서드를 호출하면 메서드는 계산을 끝내고 결과를 반환합니다. 쉽게 이야기하자면, 메서드 호출이 끝나면 해당 메서드가 반환한 결과 값으로 치환됩니다. 메서드 호출이 끝나면 더 이상 해당 메서드가 사용한 메모리를 낭비할 이유가 없습니다. 메서드 호출이 끝나면 메서드 정의에 사용한 파라미터 변수인 int a, int b는 물론이고, 그 안에서 정의한 int sum도 모두 제거 되기 때문입니다. 메서드 호출과 용어정리 메서드를 호출할 때는 다음과 같이 메서드에 넘기는 값과 매개변수(파라미터)의 타입이 맞아야 합니다. 물론 넘기는 값과 매개변수(파라미터)의 순서와 갯수도 맞아야 합니다. 호출: call("hello", 20) 메서드 정의: int call(String str, int age) 인수(Argument) 여기서 hello,20 처럼 넘기는 값을 영어로 Argument(아큐먼트), 한글로 인수 또는 인자라 합니다. 실무에서는 아규먼트, 인수, 인자라는 용어를 모두 사용합니다. 매개변수(Parameter) 메서드를 정의할 때 선언한 변수인 String str, int age를 매개변수, 파라미터라 합니다. 메서드를 호출할 때 인수를 넘기면, 그 인수가 매개변수에 대입됩니다. 실무에서는 매개변수, 파라미터 용어를 모두 사용합니다. 용어정리 인수라는 용어는 ‘인’과 ‘수’의 합성어로, ‘들어가는 수’라는 의미를 가집니다. 즉, 메서드 내부로 들어가는 값을 의미합니다. 인자도 같은 의미입니다. 매개변수, parameter는 ‘매개’와 ‘변수’의 합성어로 ‘중간에서 전달하는 변수’라는 의미를 가집니다. 즉, 메서드 호출부와 메서드 내부 사이에서 값을 전달하는 역할을 하는 변수라는 뜻입니다.
Archive
· 2024-02-13
☕️[Java] 향상된 for문
☕️[Java] 향상된 for문 향상된 for문(Enhanced For Loop)을 이해하려면 배열을 먼저 알아야 합니다. 각각의 요소를 탐색한다는 의미로 for-each문이라고도 많이 불립니다. 향상될 for문은 배열을 사용할 때 기존 for문 보다 더 편리하게 사용할 수 있습니다. 향상된 for문 정의 for (변수 : 배열 또는 컬렉션) { // 배열 또는 컬렉션의 요소를 순회하면서 수행할 작업 } 일반 for문 for(int i = 0; i < numbers.length, i++) { int number = numbers[i]; System.out.println(number); } 일반 for문은 배열에 있는 값을 순서대로 읽어서 number 변수에 넣고 출력합니다. 배열은 처음부터 끝까지 순서대로 읽어서 사용하는 경우가 많습니다. 그런데 배열의 값을 읽으려면 int i와 같은 인덱스를 탐색할 수 있는 변수를 선언해야 합니다. 그리고 i < numbers.length와 같이 배열의 끝 조건을 지정해주어야 합니다. 마지막으로 배열의 값을 하나 읽을 때 마다 인덱스를 하나씩 증가해야 합니다 개발자 입장에서는 그냥 배열을 순서대로 처음부터 끝까지 탐색하고 싶은데, 너무 번잡한 일을 해주어야 합니다. 그래서 향상된 for문이 등장했습니다. 향상된 for문 // 향상된 for문 for-each문 for (int number : numbers) { System.out.println(number); } 앞서 일반 for문과 동일하게 작동합니다. 향상된 for문은 배열의 인덱스를 사용하지 않고, 종료 조건을 주지 않아도 됩니다. 단순히 해당 배열을 처음부터 끝까지 탐색합니다. : 의 오른쪽에 numbers와 같이 탐색할 배열을 선택하고, :의 왼쪽에 int number와 같이 반복할 때 마다 찾은 값을 저장할 변수를 선언합니다. 그러면 배열의 값을 하나씩 꺼내서 왼쪽에 있는 number에 담과 for문을 수행합니다. for문의 끝에 가면 다음 값을 꺼내서 number에 담고 for문을 반복 수행합니다. numbers 배열의 끝에 도달해서 더 값이 없으면 for문이 완전히 종료 됩니다. 향상된 for문은 배열의 인덱스를 사용하지 않고도 배열의 요소를 순회할 수 있기 때문에 코드가 간결하고 가독성이 좋습니다. 향상된 for문을 사용하지 못하는 경우 향상된 for문을 사용하지 못하는 경우가 있습니다. 향상된 for문에는 증가하는 인덱스 값이 감추어져 있습니다. 따라서 int i와 같은 증가하는 인덱스 값을 직접 사용해야 하는 경우에는 향샹된 for문을 사용할 수 없습니다. // for-each문을 사용할 수 없는 경우, 증가하는 index 값이 필요할 때. for (int i = 0; i < numbers.length; i++) { System.out.println("numbers " + i + "번의 결과는: " + numbers[i]); } 이 예제에서는 증가하는 i 값을 출력해야 하므로 향상된 for문 대신에 일반 for문을 사용해야 합니다. 물론 다음과 같이 억지스럽게 향상된 for문을 사용하는 것이 가능하지만, 이런 경우 일반 for문을 사용하는 것이 더 좋다. int i = 0; for (int number : numbers) { System.out.println("number" + i +"번의 결과는: " + number); i++; }
Archive
· 2024-02-11
☕️[Java] 형변환 정리.
☕️[Java] 형변환 정리. 형변환 int => long => double 작은 범위에서 큰 범위로는 대입할 수 있습니다. 이것을 묵시적 형변환 또는 자동 형변환이라 합니다. 큰 범위에서 작은 범위의 대입은 다음과 같은 문제가 방생할 수 있습니다. 이때는 명식적 형변환을 사용해야 합니다. 소수점 버림 오버플로우 연산과 형변환 같은 타입은 같은 결과를 냅니다. 서로 다른 타입의 계산은 큰 범위로 자동 형변환이 일어납니다.
Archive
· 2024-02-09
☕️[Java] 스코프 존재 이유 1
☕️[Java] 스코프 존재 이유 1 package scope; public class Scope3_1 { public static void main(String[] args) { int m = 10; int temp = 0; if (m > 0) { temp = m * 2; System.out.println("temp = " + temp); } System.out.println("m = " + m); } } 조건이 맞으면 변수 m의 값을 2배 증가해서 출력하는 코드입니다. 여기서 2배 증가한 값을 저장해두기 위해 임시 변수 temp를 사용했습니다. 그런데 이 코드는 좋은 코드라고 보기는 어렵습니다. 그 이유는 임시 변수 temp는 if 조건이 만족할 때 임시로 잠깐 사용하는 변수입니다 그런데 임시 변수 temp가 main()코드 블록에 선언되어 있습니다. 이렇게 되면 다음과 같은 문제가 발생합니다. 비효율적인 메모리 사용: temp는 if코드 블록에서만 필요하지만, main() 코드 블록이 종료될 때 까지 메모리에 유지됩니다. 만약 if 코드 블록 안에 temp를 선언했다면 자바를 구현하는 곳에서 if 코드 블록의 종료 시점에 이 변수를 메모리에서 제거해서 더 효율적으로 메모리응 사용할 수 있습니다. package scope; public class Scope3_1 { public static void main(String[] args) { int m = 10; if (m > 0) { int temp = m * 2;// temp의 생존 시작 System.out.println("temp = " + temp); } // temp의 생존 종료 System.out.println("m = " + m); } } 코드 복잡성 증가: 좋은 코드는 군더더기 없는 단순한 코드입니다.temp는 if코드 블록에서만 필요하지만, main() 코드 블록이 종료될 때 까지 메모리에 유지됩니다.따라서 불필요한 메모리가 낭비됩니다.만약 if 코드 블록 안에 temp를 선언했다면 if가 끝나고 나면 temp를 전혀 생각하지 않아도 됩니다.즉, 머리속에서 생각할 변수를 하나 줄일 수 있다는 말입니다.그런데 지금 작성한 코드는 if 코드 블록이 끝나도 main() 어디서나 temp를 여전히 접근할 수 있습니다.누군가 이 코드를 유지보수 할 때 m은 물론이고 temp까지 계속 신경써야 합니다.스코프가 불필요하게 넓은 것입니다.지금은 코드가 매우 단순해서 이해하는데 어려움이 없겠지만 실무에서는 코드가 매우 복잡한 경우가 많습니다.
Archive
· 2024-02-07
☕️[JAVA] while문과 for문.
☕️[JAVA] while문과 for문. for문 장점: 초기화, 조건 체크, 반복 후의 작업을 한 줄에서 처리할 수 있어 편리하다. 정해진 횟수만큼의 반복을 수행하는 경우에 사용하기 적합하다. 루프 변수의 범위가 for 루프 블록에 제한되므로, 다른 곳에서 이 변수를 실수로 변경할 가능성이 적다. 단점: 루프의 조건이 루프 내부에서 변경되는 경우, for 루프는 관리하기 어렵다. 복잡한 조건을 가진 반복물을 작성하기에는 while문이 더 적합할 수 있다. while문 장점: 루프의 조건이 루프 내부에서 변경되는 경우, while 루프는 이를 관리하기 쉽다. for 루프보다 더 복잡한 조건과 시나리오에 적합하다. 조건이 충족되는 동안 계속해서 루프를 실행하며, 종료 시점을 명확하게 알 수 없는 경우에 유용하다. 단점: 초기화, 조건 체크, 반복 후의 작업이 분산되어 있어 코드를 이해하거나 작성하기 어려울 수 있다. 루프 변수가 while 블록 바깥에서도 접근 가능하므로, 이 변수를 실수로 변경하는 상황이 발생할 수 있다. 한줄로 정의하자면 정해진 횟수만큼 반복을 수행해야 하면 for문을 사용하고 그렇지 않으면 while문을 사용하면 된다. 물론 이것이 항상 정답은 아니니 기준으로 삼는 정도로 이해하자.
Archive
· 2024-02-07
☕️[JAVA] Packaing 옵션.
☕️[JAVA] Packaing 옵션. Spring Initializr에서 "Packaing" 옵션을 선택할 때 'Jar'와 'War' 중에 선택해야 합니다. 어떤 것을 선택해야 할지는 개발하려는 어플리케이션의 유형과 배포 환경에 따라 달라집니다. 각 포맷에 대한 설명. 1️⃣ Jar (Java Archive) Jar 파일은 Java 클래스 파일, 메타데이터, 리소스 파일을 하나의 파일로 압축한 포맷입니다. 스탠드얼론(Spring Boot 어플리케이션 권장 포맷): Jar 포맷은 내장된 서버(예: Tomcat, Jetty)를 사용하여 스프링 부트 어플리케이션을 스탠드얼론 어플리케이션으로 실행할 수 있게 합니다. 이는 별도의 웹 서버 설치 없이도 실행 가능하며, 마이크로서비스, 클라우드 어플리케이션 개발에 적합합니다. 간편한 배포와 실행: Jar 파일은 'java -jar' 명령어로 쉽게 실행할 수 있으며, 도커 컨테이너와 같은 환경에 배포하기도 용이합니다. 2️⃣ War (Web Application Archive) War 파일은 웹 어플리케이션에 필요한 Java 클래스 파일, JSP(JavaServer Pages), 서블릿, 리소스 파일, 메타데이터 등을 포함한 포맷입니다. 전통적인 웹 어플리케이션: War 포맷은 서블릿 컨테이너나 어플리케이션 서버(예: Tomcat, Jetty, WebLogic, WildFly)에 배포될 전통적인 웹 어플리케이션 개발에 사용됩니다. 이 경우, 어플리게이션 서버가 웹 어플리케이션을 실행하는데 필요한 환경을 제공합니다. 엔터프라이즈 환경: 복잡한 엔터프라이즈 환경에서는 여러 어플리케이션을 하나의 서버에 배포해야 할 필요가 있을 수 있으며, War 포맷이 이러한 요구 사항을 충족시킬 수 있습니다. 🙌 선택 기준 스탠드얼론 어플리케이션 개발 및 마이크로 아키텍처를 선호한다면 'Jar'를 선택하세요. 기존의 엔터프라이즈 환경에서 어플리케이션 서버를 사용해야 한다면 'War'를 선택하세요. Spring Boot는 두 가지 포맷 모두를 지원하므로, 프로젝트 요구 사항과 배포 환경에 맞게 최적의 옵션을 선택할 수 있습니다.
Archive
· 2024-02-06
☕️[Java] 변수 선언.
☕️[Java] 변수 선언. package variable; public class Var2 { public static void main(String[] args) { int number; // 변수 선언 number = 20; } } 자바의 변수 선언은 먼저 어떤 형(type)인지를 명시하고 그 뒤에 변수명을 명시합니다. 위 코드 조각에서는 int형 임을 명시하고, 그 뒤에 변수명 으로 number 을 명시했습니다.
Archive
· 2024-02-01
<
>
Touch background to close