Now Loading ...
-
☕️[Java] Java Stream이란 무엇일까요?
☕️[Java] Java Stream이란 무엇일까요?
Java Stream은 Java 8에서 도입된 데이터 처리 API로, 컬렉션(Collection), 배열(Array), 파일 등의 데이터 소스를 효율적으로 처리할 수 있게 해주는 기능입니다.
Stream을 사용하면 데이터의 필터링, 변환, 정렬, 집계 등의 작업을 함수형 스타일로 간결하게 작성할 수 있습니다.
1️⃣ Stream의 주요 특징.
1️⃣ 선언형 프로그래밍 스타일.
기존의 반복문을 사용한 명령형(Imperative) 코드와 달리, Stream은 선언형(Declarative) 스타일을 사용하여 더 간결하고 읽기 쉬운 코드를 작성할 수 있게 합니다.
2️⃣ 함수형 프로그래밍.
Stream은 함수형 프로그래밍을 지원하여, 람다 표현식(Lambda Expression)과 메서드 참조(Method Reference)를 사용하여 간결한 코드를 작성할 수 있습니다.
3️⃣ 데이터의 흐름.
Stream은 데이터 소스로부터 데이터 흐름을 처리하며, 데이터를 직접 저장하지 않습니다.
즉, 컬렉션이나 배열에 데이터를 저장하는 것이 아니라, 데이터를 처리하는 역속적인 작업을 제공합니다.
4️⃣ 지연 연산(Lazy Evaluation)
Stream은 지연 연산(Lazy Evaluation)을 사용하여 필요한 경우에만 데이터를 처리합니다.
중간 연산이 지연되어 최종 연산이 호출될 때만 실제로 데이터 처리가 이루어집니다.
5️⃣ 병렬 처리.
Stream API는 쉽게 병렬 처리를 수행할 수 있는 메서드를 제공하여, 멀티코어 시스템에서 성능을 최적화할 수 있습니다.
parallelStream() 메서드를 사용하면 병렬 처리를 활성화할 수 있습니다.
2️⃣ Stream의 기본 구성.
Stream은 크게 중간 연산과 최종 연산으로 구성됩니다.
👉 중간 연산.
필터링, 변환, 정렬 등과 같이 데이터의 흐름을 처리하는 작업을 수행하지만, 최종 연산이 호출되기 전까지는 실행되지 않습니다.(예: filter, map, sorted)
👉 최종 연산.
스트림의 데이터를 결과로 수집하거나 출력하는 연산으로, 최종 연산이 호출되면 스트림이 처리되고 종료됩니다.(예: collect, forEach, reduce)
3️⃣ Stream 사용 예시.
1️⃣ 컬렉션에서 Stream 사용.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Jhon", "Jane", "Jack", "Doe");
// 필터링과 변환
List<String> filteredNames = names.stream()
.filter(name -> name.startWith("J")) // "J"로 시작하는 이름만 필터링
.map(String::toUpperCase) // 대문자로 변환
.collect(Collectors.toList()); // 결과를 리스트로 수집
System.out.println(filteredNames) // 출력: [JHON, JANE, JACK]
}
}
4️⃣ Stream의 기본 연산.
👉 중간 연산.
1️⃣ filter(Predicate)
조건에 맞는 요소만 걸러냅니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList()); // [2, 4]
2️⃣ map(Function)
각 요소를 변환합니다.(예: 문자열을 대문자로 변환, 객체의 특정 필드를 추출)
List<String> words = Arrays.asList("apple", "banana", "cherry");
List<Integer> lengths = words.stream()
.map(String::length)
.collect(Collectors.toList()); // [5, 6, 7]
3️⃣ sorted()
요소를 정렬합니다. 기본적으로 오름차순 정렬하며, 커스텀 Comparator를 사용할 수도 있습니다.
List<Integer> sortedNumbers = numbers.stream()
.sorted
.collect(Collectors.toList()); // [1, 2, 3, 4, 5]
4️⃣ distinct()
중복된 요소를 제거합니다.
List<Integer> distinctNumbers = Arrays.asList(1, 2, 2, 3, 3, 3)
.stream()
.distinct()
.collect(Collectors.toList()); // [1, 2, 3]
👉 최종 연산.
1️⃣ collect(Collector)
스트림의 요소를 모아서 리스트, 집합 등의 컬렉션으로 변환합니다.
List<String> names = Arrays.asList("John", "Jane", "Jack");
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList()); // [JOHN, JANE, JACK]
2️⃣ forEach(Consumer)
각 요소에 대해 지정된 작업을 수행합니다.
출력이나 로그 작업 등에 사용됩니다.
names.stream()
.forEach(System.out::println);
3️⃣ reduce(BinaryOperator)
스트림의 요소를 누적하여 하나의 결과로 만듭니다.
주로 합계, 곱, 문자열 연결 등에 사용됩니다.
int sum = Arrays.asList(1, 2, 3, 4, 5)
.stream()
.reduce(0, Integer::sum) // 15
4️⃣ count()
스트림의 요소 개수를 반환합니다.
long count = names.stream()
.filter(name -> name.startWith("J"))
.count(); // 3
5️⃣ 병렬 스트림
Stream API는 병렬 처리를 쉽게 수행할 수 있도록 지원합니다.
paralleStream() 또는 parallel() 메서드를 사용하여 스트림을 병렬로 처리할 수 있습니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sum = numbers.parallelStream()
.reduce(0, Integer::sum); // 병렬로 합 계산
병렬 스트림은 멀티 코어 시스템에서 성능을 최적화할 수 있지만, 병렬로 처리할 때는 데이터 간의 독립성이 보장되어야 합니다.
6️⃣ 요약.
Java Stream은 데이터의 필터링, 변환. 정렬, 집계 등을 간결하고 선언적인 스타일로 작성할 수 있는 기능을 제공합니다.
중간 연산과 최종 연산을 조합하여 복잡한 데이터 처리를 간단하게 구현할 수 있으며, 병렬 처리를 통해 성능을 향상시킬 수도 있습니다.
Stream API를 사용하면 기존의 반복문과 조건문을 대체할 수 있어, 코드의 가독성과 유지보수성이 크게 향상됩니다.
-
☕️[Java] Java Optional이란 무엇일까요?
☕️[Java] ☕️[Java] Java Optional이란 무엇일까요?
Optional은 Java 8에서 도입된 null 값을 안전한게 처리하기 위한 클래스입니다.
null 값을 직접 다루는 것에 대한 문제점(예: NullPointException)을 줄이기 위해 설계되었으며, 값이 존재할 수도 있고, 존재하지 않을 수도 있는 상황을 표현할 수 있도록 도와줍니다.
1️⃣ Optional의 주요 목적.
1️⃣ null 처리의 안정성
Java에서 null을 반환하거나 사용하는 것은 매우 흔한 일이지만, 잘못된 null 처리는 NullPointException을 유발할 수 있습니다.
Optional을 사용하면 이런 문제를 예방할 수 있습니다.
2️⃣ 의도 명확성.
Optional을 사용하면, 메서드의 반환값이 값이 없을 수 있음을 명확하게 표현할 수 있습니다.
개발자는 반환값이 Optional 타입일 때 값이 없는 경우에 대한 처리를 고려할 수 있습니다.
3️⃣ 코드의 가독성 및 유지보수성.
Optional은 null 체크를 일일이 하는 대신, 값이 없을 수 있는 상황을 안전하게 다루기 위한 여러 메서드를 제공합니다.
이를 통해 코드의 가독성이 좋아지고 유지보수하기 쉬워집니다.
2️⃣ Optional의 생성 방법.
1️⃣ Optional.of(T value)
null이 아닌 값으로 Optional 객체를 생성합니다.
값이 null일 경우 NullPointException이 발생합니다.
Optional<String> optional = Optional.of("Hello");
2️⃣ Optional.ofNullable(T value)
전달된 값이 null일 수도 있고, 아닐 수도 있는 경우 사용합니다.
값이 null이면 Optional.empty() 반환하고, 그렇지 않으면 Optional.of(value)와 동일한 동작을 합니다.
Optional<String> optional = Optional.ofNullable(null); // 비어있는 Optional 반환
3️⃣ Optional.empty()
비어있는 Optional 객체를 생성합니다.
값이 없음을 명시적으로 나타낼 때 사용합니다.
Optional<String> emptyOptional = Optional.empty();
3️⃣ Optional의 주요 메서드.
1️⃣ isPresent()
Optional에 값이 있으면 true, 없으면 false를 반환합니다.
Optional<String> optional = Optional.of("Hello");
if (optional.isPresent()) {
System.out.println(optional.get()); // "Hello" 출력
}
2️⃣ ifPresent(Consumer<T> action)
Optional에 값이 있으면 해당 값으로 지정된 동작을 수행합니다.
값이 없으면 아무 일도 하지 않습니다.
Optional<String> optional = Optional.of("Hello");
optional.ifPresent(value -> System.out.println(value)) // "Hello" 출력
3️⃣ get()
Optional에 저장된 값을 반환합니다.
값이 없으면 NoSuchElementException이 발생하므로, 주의해서 사용해야 합니다.
Optional<String> optional = Optional.of("Hello");
String value = optional.get(); // "Hello"
4️⃣ orElse(T other)
Optional에 값이 있으면 그 값을 반환하고, 값이 없으면 기본값을 반환합니다.
Optional<String> optional = Optional.empty();
String value = optional.orElse("Default"); // "Default"
5️⃣ orElseGet(Supplier<? extends T> supplier)
Optional에 값이 있으면 그 값을 반환하고, 값이 없으면 람다 표현식이나 메서드를 통해 기본값을 제공할 수 있습니다.
Optional<String> optional = Optional.empty();
String value = optional.orElseGet(() -> "Generated Default"); // "Generated Default"
6️⃣ orElseThrow(Supplier<? extends X> exceptionSupplier)
Optional에 값이 있으면 그 값을 반환하고, 값이 없으면 지정한 예외를 던집니다.
Optional<String> optional = Optional.empty();
String value = optional.orElseThrow(() -> new IllegalArgumentException("Value not found")); // 예외 발생
7️⃣ map(Function<? super T, ? extends U> mapper)
Optional에 값이 있으면, 해당 값에 대해 함수형 변환을 적용한 결과를 새로운 Optional로 변환합니다.
값이 없으면 빈 Optional을 반환합니다.
Optional<String> optional = Optional.of("Hello");
Optional<Integer> length = optional.map(String::length); // "Hello"의 길이인 5를 포함한 Optional 반환
8️⃣ flatMap(Function<? super T, Optional<U>> mapper)
map()과 유사하지만, 중첩된 Optional을 평평하게 만듭니다.
Optional<Optional<T>> 형태가 아닌 Optional<T>를 반환합니다.
Optional<String> optional = Optional.of("Hello");
Optional<Integer> length = optional.flatMap(val -> Optional.of(val.length()));
4️⃣ Optional 사용 예시.
1️⃣ Optional을 이용한 안전한 null 처리.
public Optional<User> findUserById(Long id) {
User user = userRepositoty.findById(id);
return Optional.ofNullable(user);
}
public void printUserName(Long userId) {
Optional<User> userOpt = findUserById(userId);
userOpt.ifPresent(user -> System.out.println(user.getName()));
}
2️⃣ 기본값 제공.
Optional<String> optional = Optional.empty();
String value = optional.orElse("Default Value"); //"Default Value" 반환
5️⃣ Optional을 사용하지 않아야 할 경우.
컬렉션 타입
Optional을 사용하여 빈 리스트나 배열을 표현하기보다는, 단순히 빈 컬렉션을 반환하는 것이 더 일반적이고 권장됩니다.
필드 선언
클래스의 필드로 Optional을 사용하는 것은 메모리 오버헤드가 발생할 수 있어 권장하지 않습니다.
Optional은 반환 타입으로 사용하는 것이 좋습니다.
필수 값
절대 null이 되어서는 안 되는 필드나 매개변수에 Optional을 사용할 필요는 없습니다.
6️⃣ 요약.
Optional은 null 값으로 인한 문제를 예방하고, 안전하게 값을 처리할 수 있도록 설계된 클래스입니다.
값이 있을 수도, 없을 수도 있는 상황을 더 명확하게 표현하며, 다양한 메서드를 통해 안전하게 값의 여부를 확인하고 처리할 수 있게 도와줍니다.
이를 통해 코드의 가독성과 유지보수성이 크게 향상됩니다.
-
☕️[Java] 클래스, 객체, 인스턴스
☕️[Java] 클래스, 객체, 인스턴스.
클래스(Class), 객체(Object), 인스턴스(Instance)는 객체 지향 프로그래밍(Object-Oriented Programming, OOP)에서 중요한 개념이며, 서로 밀접하게 관련이 있지만 다른 의미를 가집니다.
🙋♂️ 객체 지향 프로그래밍(Object-Oriented Programming, OOP)
1️⃣ 클래스(Class)
클래스(Class)는 “객체(Object)를 정의하기 위한 청사진(설계도)”입니다.
클래스(Class)는 객체(Object)의 속성(필드)과 행동(메서드)을 정의합니다.
즉, 클래스는 어떤 유형의 객체(Object)를 만들 것인지에 대한 정보를 가지고 있으며, 객체(Object)를 생성하는 데 사용되는 틀 역할을 합니다.
예시.
// 클래스 선언.
public class Car {
// 필드(속성)
String model;
String color;
// 메서드(행동)
void drive() {
System.out.println("The car is driving.");
}
}
위의 Car 클래스는 자동차의 모델과 색상을 나타내는 속성(필드)과, 자동차가 달리는 행동을 정의하는 메서드를 가지고 있습니다.
이 자체는 실제 객체(Object)가 아니라 객체(Object)를 만들기 위한 설계도(blueprint, 청사진)입니다.
2️⃣ 객체(Object)
객체(Object)는 클래스(Class)에 의해 생성된 실체로, 클래스의 인스턴스(Instance)라고도 불립니다.
객체(Object)는 클래스(Class)에서 정의된 속성(필드)과 행동(메서드)를 실제로 가지고 있는 메모리 상의 실체입니다.
즉, 클래스의 설계도(blueprint, 청사진)을 바탕으로 만들어진 실질적인 데이터입니다.
객체(Object)는 클래스(Class)에서 정의된 구조에 따라 동작하며, 프로그램 내에서 데이터와 기능을 담당합니다.
객체(Object) 생성 예시.
public class Main {
public static void main(String[] args) {
// 객체 생성
Car myCar = new Car(); // Car 클래스의 객체 생성.
myCar.model = "Volvo xc60";
myCar.color = "Black";
myCar.drive(); // 출력: The car is driving
}
}
위 코드에서 Car myCar = new Car();는 Car 클래스(Class)의 객체(Object)를 생성합니다.
이 myCar 객체(Object)는 Car 클래스(Class)에서 정의한 속성(필드)과 행동(메서드)를 가지고 있으며, 프로그램에서 실제로 사용됩니다.
3️⃣ 인스턴스(Instance)
인스턴스(Instance)는 특정 클래스(Class)에서 생성된 객체(Object)를 의미합니다.
즉, 클래스(Class)의 인스턴스(Instance)는 그 클래스(Class)에 의해 생성된 실체 객체(Object)를 말합니다.
흔히 인스턴스(Instance)와 객체(Object)는 같은 의미로 사용되지만, “인스턴스(Instance)”는 특정 클래스(Class)에서 만들어진 객체(Object)라는 의미에 좀 더 초점을 맞추고 있습니다.
예를 들어, myCar는 Car 클래스의 인스턴스이며, 또한 객체입니다.
인스턴스(Instance) 예시.
Car myCar = new Car(); // Car 클래스의 인스턴스 생성.
Car yourCar = new Car(); // 또 다른 Car 클래스의 인스턴스 생성.
위 코드에서 myCar와 yourCar는 모두 Car 클래스의 인스턴스(Instance)입니다.
즉, 동일한 클래스(Class)에서 만들어진 여러개의 객체(인스턴스)를 생성할 수 있습니다.
4️⃣ 요약.
클래스(Class) : 객체(Object)를 생성하기 위한 설계도(blueprint, 청사진)입니다. 속성(필드)과 행동(메서드)를 정의합니다.
객체(Object) : 클래스(Class)에 의해 생성된 실체입니다. 프로그램 내에서 사용되며, 클래스의 인스턴스(Instance)라고도 불립니다.
인스턴스(Instance) : 특정 클래스에서 생성된 객체(Object)를 가리키는 용어입니다. 클래스의 실체로서 메모리 내에 존재하는 객체(Object)를 의미합니다.
객체(Object)와 인스턴스(Instance)는 서로 거의 같은 의미로 사용되며, 클래스(Class)는 이 객체(Object)들을 만들기 위한 설계도(blueprint, 청사진)입니다.
-
☕️[Java] java에 대하여 간단하게 알아보기
☕️[Java] java에 대하여 간단하게 알아보기.
1️⃣ 각 코드 행의 의미.
int size = 27;
String name = "Fido";
Dog myDog = new Dog(name, size);
int x = size - 5;
if (x < 15) myDog.bark(8);
while (x > 3) {
myDog.play();
}
int[] numList = {2, 4, 6, 8};
System.out.println("Hello");
System.out.pring("Dog: " + name);
String num = "8";
int z = Integer.parseInt(num);
try {
readTheFile("myFile.txt");
}
catch (FileNoFoundException ex) {
System.out.print("File not found.")
}
‘size’라는 정수 변수를 선언하고 27이라는 값을 대입합니다.
‘name’라는 문자열 변수를 선언하고 Fido라는 값을 대입합니다.
‘myDog’라는 Dog 변수를 선언하고 ‘name’과 ‘size’를 사용해 새 Dog 객체를 만듭니다.
27(‘size’의 값)에서 5를 뺀 값을 ‘x’라는 변수에 대입합니다.
만약 x(값이 22)가 15보다 작으면 개가 여덟 번 짖도록 합니다.
x가 3보다 크면 반복문을 사용합니다.
개가 놀도록 지시합니다(play() 메서드를 실행시킴)
반복문이 끝나는 부분입니다. {} 안에 있는 것들이 조건에 따라 반복됩니다.
‘numList’라는 정수 배열을 선언하고 2,4,6,8을 집어넣습니다.
“Hello”를 출력합니다. (아마 명령행으로 출력될것입니다.)
명령행에 “Dog: Fido”를 출력합니다.
‘num’라는 문자열 변수를 선언하고 “8”이라는 값을 대입합니다.
“8”이라는 문자열을 8이라는 숫자값으로 변환합니다.
무언가를 시도해 봅니다. 잘 안 될 수도 있습니다.
“myFile.txe”를 읽습니다
“시도하는 부분”이 끝이므로 여러 가지를 함께 시도할 수도 있습니다.
시도했던 것이 실패하면 실행됩니다.
실패했을 경우에 명령행에 “File not found”라고 출력합니다.
중괄호({}) 안의 모든것은 ‘try’ 부분이 실패한 경우에 수행할 작업입니다.
2️⃣ 자바 코드의 구조
소스 파일 안에는 “클래스”가 들어갑니다.
클래스 안에는 “메서드”가 들어갑니다.
메서드 안에는 “명령문”이 들어갑니다.
소스 파일 안에는 무엇이 들어갈까요?
소스 코드 파일(.java라는 확장자가 붙은 파일)은 일반적으로 클래스(class) 를 한 개씩 정의합니다.
클래스는 보통 프로그램의 한 부분을 나타내지만, 아주 작은 애플리케이션 중에는 단 한 개의 클래스만으로 이뤄진 것도 있습니다.
클래스는 한 쌍의 중괄호({})로 둘러싸인 형태여야 합니다.
클래스 안에는 무엇이 들어갈까요?
하나의 클래스 안에는 하나 이상의 메서드(method) 가 들어갑니다.
예를 들어서,(개를 나타내는) Dog 클래스에는 (짖는 것을 의미하는) bark 라는 메서드가 들어갈 수 있으며, 이 메서드에는 개가 짖는 방법을 지시하는 내용이 들어갑니다.
메서드는 반드시 클래스 안에서 선언되어야 합니다.
즉, 클래스의 중괄호 안에 위치해야 합니다.
메서드 안에는 무엇이 들어갈까요?
메서드를 감싸는 중괄호 안에는 메서드에서 처리할 일을 지시하는 내용이 들어갑니다.
메서드 코드는 기본적으로 일련의 명령문을 모아 놓은 것이므로 지금은 메서드를 일종의 함수나 프로시저와 비슷한 것으로 생각하면 됩니다.
🙋♂️ 프로시저(Procedure)
-
☕️[Java] jdbc란?
☕️[Java] jdbc란?
JDBC(Java Database Connectivity) 는 Java 애플리케이션에서 데이터베이스에 연결하고, 쿼리를 실행하며, 결과를 처리하기 위한 표준 API입니다.
JDBC는 Java에서 데이터베이스와 상호작용하는 방법을 정의하며, 데이터베이스와 애플리케이션 간의 통신을 가능하게 합니다.
이를 통해 개발자는 다양한 데이터베이스에서 일관된 방식으로 데이터에 접근할 수 있습니다.
1️⃣ JDBC의 주요 개념.
1. JDBC 드라이버(JDBC Driver)
JDBC는 드라이버 기반의 API입니다.
JDBC 드라이버는 특정 데이터베이스에 맞는 연결을 가능하게 하며, 데이터베이스에 대한 SQL 명령을 전달하고 결과를 받아 옵니다.
각 데이터베이스는 JDBC 표준을 따르는 드라이버를 제공해야 하며, MySQL, PostgreSQL, Oracle 등의 데이터베이스에는 각각의 JDBC 드라이버가 있습니다.
🙋♂️ 드라이버란?
2. JDBC API
JDBC는 데이터베이스와 통신하기 위한 다양한 인터페이스와 클래스를 포함하고 있습니다.
이를 통해 개발자는 데이터베이스 연결, SQL 쿼리 실행, 결과 처리, 트랜잭션 관리 등의 작업을 할 수 있습니다.
2️⃣ JDBC의 주요 작업 흐름
1. 데이터베이스 연결 설정(Establishing Connection)
애플리케이션은 JDBC 드라이버를 사용하여 데이터베이스에 연결을 설정합니다.
연결이 성공하면, 이를 통해 데이터베이스에 SQL 쿼리를 보낼 수 있습니다.
2. SQL 쿼리 실행(Executing SQL Queries)
연결이 설정되면, SQL 쿼리를 데이터베이스에 전달하여 데이터를 조회하거나 삽입, 업데이트, 삭제할 수 있습니다.
3. 결과 처리(Processing Results)
SQL 쿼리의 결과는 Java에서 처리할 수 있는 형식으로 반환됩니다.
조회 쿼리의 경우, ResultSet 객체로 결과가 반환되며, 이를 통해 데이터를 순차적으로 읽을 수 있습니다.
4. 트랜잭션 관리(Transaction Management)
JDBC는 트랜잭션 처리를 지원합니다.
데이터의 일관성을 보장하기 위해 트랜잭션을 커밋하거나 롤백할 수 있습니다.
5. 연결 해제(Closing Connection)
모든 작업이 끝나면, 데이터베이스 연결을 해제해야 합니다.
이를 통해 자원을 적절하게 반환하고 연결을 종료할 수 있습니다.
3️⃣ JDBC 코드 예시.
import java.sql.Connection;
import java.sql.DriverManagerl
import java.sql.ResultSet;
import java.sql.Statement;
public class JdbcExample {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet = resultSet = null;
try {
// 1. JDBC 드라이버 로드 (예: MySQL, JDBC 드라이버)
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 데이터베이스 연결 설정
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "password");
// 3. SQL 쿼리 실행
statement = connection.createStatement();
resultSet = statement.executeQuery("SELECT * FROM Users");
// 4. 결과 처리
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
String email = resultSet.getString("email");
System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 리소스 해제
try {
if (resultSet != null) resultSet.close();
if (statement != null) statement.close();
if (connection != null) connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
4️⃣ 주요 JDBC 객체
1. Connection
데이터베이스와 연결을 나타내는 객체입니다.
이 객체는 연결을 생성, 유지, 종료하는 데 사용되며, SQL 쿼리 실행에 필요한 통로 역할을 합니다.
연결은 DriverManager.getConnection() 메서드를 통해 생성됩니다.
2. Statement
SQL 쿼리를 데이터베이스로 보내기 위한 객체입니다.
Statement는 정적 SQL 쿼리를 실행하는 데 사용되며, 쿼리 결과를 받아 처리할 수 있습니다.
3. PreparedStatement
Statement의 확장된 버전으로, 미리 컴파일된 SQL 쿼리를 실행할 수 있습니다.
이는 동적 데이터가 포함된 쿼리(파라미터화된 쿼리)를 처리할 때 사용되며, 보안성과 성능을 개선할 수 있습니다.
예시
PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM Users WHERE id = ?");
pstmt.setInt(1, 1); // 1번째 매개변수에 1을 설정
ResultSet rs = pstmt.executeQuery();
4. ResultSet
SQL 쿼리 실행 결과를 담는 객체입니다.
ResultSet은 데이터베이스로부터 반환된 결과를 테이블 형식으로 제공하며, 이를 통해 결과를 순차적으로 읽고 처리할 수 있습니다.
5. DriverManager
JDBC 드라이버를 관리하는 클래스입니다.
데이터베이스 연결을 생성할 때 DriverManager.getConnection() 메서드를 사용하여 적절한 JDBC 드라이버를 통해 연결을 설정합니다.
5️⃣ JDBC의 장점.
1. 플랫폼 독립성.
JDBC는 Java 표준 API로, 데이터베이스 시스템과 독립적으로 동작합니다.
다양한 데이터베이스 시스템에 대한 통일된 방식으로 접근할 수 있습니다.
2. 표준화된 API.
JDBC는 Java 표준 API로, 데이터베이스 시스템과 독립적으로 동작합니다.
다양한 데이터베이스 시스템에 대한 통일된 방식으로 접근할 수 있습니다.
3. 광범위한 데이터베이스 지원.
JDBC는 MySQL, PostgreSQL, Oracle, SQL Server 등 다양한 데이터베이스 시스템을 지원합니다.
각각의 데이터베이스는 JDBC 드라이버를 통해 연결됩니다.
4. 트랜잭션 지원.
JDBC는 데이터베이스 트랜잭션을 관리할 수 있으며, 자동 커밋 또는 수동 트랜잭션 관리를 통해 데이터 일관성을 보장합니다.
6️⃣ JDBC의 단점.
1. 저수준 API.
JDBC는 SQL 쿼리 실행과 같은 저수준 작업을 다루기 때문에 코드가 길어지고 복잡해줄 수 있습니다.
많은 설정 코드와 예외 처리가 필요합니다.
2. SQL 종속성.
JDBC는 SQL 쿼리를 직접 사용하기 때문에, 데이터베이스마다 SQL 문법의 차이로 인해 호환성 문제가 발생할 수 있습니다.
즉, 데이터베이스 간에 이식성이 완벽하지 않을 수 있습니다.
3. ORM의 등장.
Hibernate, JPA와 같은 ORM(Object-Relational Mapping) 프레임워크는 JDBC보다 더 높은 수준의 추상화를 제공하여, 데이터베이스와의 상호작용을 더 쉽게 관리할 수 있습니다.
아로 인해 복잡한 JDBC 코드를 간소화할 수 있습니다.
7️⃣ 요약
JDBC는 Java 애플리케이션에서 데이터베이스에 연결하고, SQL 쿼리를 실행하며, 결과를 처리하는 표준 API입니다.
JDBC는 다양한 데이터베이스 시스템과 상호작용할 수 있는 일관된 인터페이스를 제공하며, 연결 설정, SQL 쿼리 실행, 트랜잭션 관리, 결과 처리와 같은 작업을 수행합니다.
JDBC는 저수준 API로서 강력한 기능을 제공하지만, ORM 프레임워크가 복잡한 데이터베이스 작업을 보다 쉽게 처리할 수 있게 함으로써 최근에는 JDBC를 기반으로 더 고수준의 라이브러리가 자주 사용되고 있습니다.
🙋♂️ 라이브러리와 프레임워크의 차이점
-
☕️[Java] Class 클래스
☕️[Java] Class 클래스.
자바에서 Class 클래스는 클래스의 정보(메타데이터)를 다루는데 사용됩니다.
Class 클래스를 통해 개발자는 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메소드에 대한 정보를 조회하고 조작할 수 있습니다.
1️⃣ Class 클래스의 주요 기능
타입 정보 얻기.
클래스의 이름, 슈퍼클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있습니다.
리플랙션.
클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 등의 작업을 할 수 있습니다.
동적 로딩과 생성.
Class.forName() 메서드를 사용하여 클래스를 동적으로 로드하고, newInstance() 메서드를 통해 새로운 인스턴스를 생성할 수 있습니다.
애노테이션 처리.
클래스에 적용된 애노테이션(annotation)을 조회하고 처리하는 기능을 제공합니다.
예시.
예를 들어, String.class는 String 클래스에 대한 Class 객체를 나타내며, 이를 통해 String 클래스에 대한 메타데이터를 조회하거나 조작할 수 있습니다.
package langReview.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);
System.out.println("field.getType() = " + field.getType() + " " + field.getName());
}
// 모든 메서드 출력
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
// 상위 클래스 정보 출력
System.out.println("Superclass: " + clazz.getSuperclass());
// 인터페이스 정보 출력
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 = private final byte[] java.lang.String.value
field.getType() = class [B value
field = private final byte java.lang.String.coder
field.getType() = byte coder
field = private int java.lang.String.hash
field.getType() = int hash
field = private boolean java.lang.String.hashIsZero
field.getType() = boolean hashIsZero
field = private static final long java.lang.String.serialVersionUID
field.getType() = long serialVersionUID
field = static final boolean java.lang.String.COMPACT_STRINGS
field.getType() = boolean COMPACT_STRINGS
field = private static final java.io.ObjectStreamField[] java.lang.String.serialPersistentFields
field.getType() = class [Ljava.io.ObjectStreamField; serialPersistentFields
field = private static final char java.lang.String.REPL
field.getType() = char REPL
field = public static final java.util.Comparator java.lang.String.CASE_INSENSITIVE_ORDER
field.getType() = interface java.util.Comparator CASE_INSENSITIVE_ORDER
field = static final byte java.lang.String.LATIN1
field.getType() = byte LATIN1
field = static final byte java.lang.String.UTF16
field.getType() = byte UTF16
method = byte[] java.lang.String.value()
method = public boolean java.lang.String.equals(java.lang.Object)
method = public int java.lang.String.length()
method = public java.lang.String java.lang.String.toString()
method = static void java.lang.String.checkIndex(int,int)
method = public int java.lang.String.hashCode()
method = public void java.lang.String.getChars(int,int,char[],int)
method = public int java.lang.String.compareTo(java.lang.Object)
method = public int java.lang.String.compareTo(java.lang.String)
method = public int java.lang.String.indexOf(java.lang.String,int,int)
method = static int java.lang.String.indexOf(byte[],byte,int,java.lang.String,int)
method = public int java.lang.String.indexOf(java.lang.String,int)
method = public int java.lang.String.indexOf(int)
method = public int java.lang.String.indexOf(int,int)
method = public int java.lang.String.indexOf(int,int,int)
method = public int java.lang.String.indexOf(java.lang.String)
method = public static java.lang.String java.lang.String.valueOf(long)
method = public static java.lang.String java.lang.String.valueOf(char[])
method = public static java.lang.String java.lang.String.valueOf(java.lang.Object)
method = public static java.lang.String java.lang.String.valueOf(char[],int,int)
method = public static java.lang.String java.lang.String.valueOf(float)
method = public static java.lang.String java.lang.String.valueOf(double)
method = public static java.lang.String java.lang.String.valueOf(char)
method = public static java.lang.String java.lang.String.valueOf(boolean)
method = public static java.lang.String java.lang.String.valueOf(int)
method = byte java.lang.String.coder()
method = private static java.lang.Void java.lang.String.rangeCheck(char[],int,int)
method = static int java.lang.String.checkBoundsOffCount(int,int,int)
method = private static java.nio.charset.Charset java.lang.String.lookupCharset(java.lang.String) throws java.io.UnsupportedEncodingException
method = private static char java.lang.String.decode2(int,int)
method = private static int java.lang.String.decodeUTF8_UTF16(byte[],int,int,byte[],int,boolean)
method = private static int java.lang.String.scale(int,float)
method = private static int java.lang.String.decodeWithDecoder(java.nio.charset.CharsetDecoder,char[],byte[],int,int) throws java.nio.charset.CharacterCodingException
method = private static java.lang.String java.lang.String.newStringNoRepl1(byte[],java.nio.charset.Charset)
method = static java.lang.String java.lang.String.newStringUTF8NoRepl(byte[],int,int,boolean)
method = private static void java.lang.String.throwMalformed(byte[])
method = private static void java.lang.String.throwMalformed(int,int)
method = private static byte[] java.lang.String.encodeUTF8(byte,byte[],boolean)
method = private static byte[] java.lang.String.encode8859_1(byte,byte[],boolean)
method = private static byte[] java.lang.String.encode8859_1(byte,byte[])
method = private static byte[] java.lang.String.encodeASCII(byte,byte[])
method = private static byte[] java.lang.String.encodeWithEncoder(java.nio.charset.Charset,byte,byte[],boolean)
method = private static byte[] java.lang.String.safeTrim(byte[],int,boolean)
method = private static byte[] java.lang.String.encode(java.nio.charset.Charset,byte,byte[])
method = private static byte[] java.lang.String.getBytesNoRepl1(java.lang.String,java.nio.charset.Charset)
method = private static boolean java.lang.String.isASCII(byte[])
method = private static void java.lang.String.throwUnmappable(byte[])
method = private static void java.lang.String.throwUnmappable(int)
method = private static void java.lang.String.replaceNegatives(byte[],int)
method = private static boolean java.lang.String.isNotContinuation(int)
method = private static boolean java.lang.String.isMalformed3(int,int,int)
method = private static int java.lang.String.malformed3(byte[],int)
method = private static char java.lang.String.decode3(int,int,int)
method = private static boolean java.lang.String.isMalformed3_2(int,int)
method = private static int java.lang.String.decode4(int,int,int,int)
method = private static boolean java.lang.String.isMalformed4(int,int,int)
method = private static int java.lang.String.malformed4(byte[],int)
method = private static boolean java.lang.String.isMalformed4_2(int,int)
method = private static boolean java.lang.String.isMalformed4_3(int)
method = private static byte[] java.lang.String.encodeUTF8_UTF16(byte[],boolean)
method = boolean java.lang.String.isLatin1()
method = public char java.lang.String.charAt(int)
method = public int java.lang.String.codePointAt(int)
method = public int java.lang.String.codePointBefore(int)
method = public int java.lang.String.codePointCount(int,int)
method = public int java.lang.String.offsetByCodePoints(int,int)
method = static void java.lang.String.checkBoundsBeginEnd(int,int,int)
method = public byte[] java.lang.String.getBytes()
method = void java.lang.String.getBytes(byte[],int,int,byte,int)
method = void java.lang.String.getBytes(byte[],int,byte)
method = public void java.lang.String.getBytes(int,int,byte[],int)
method = public byte[] java.lang.String.getBytes(java.lang.String) throws java.io.UnsupportedEncodingException
method = public byte[] java.lang.String.getBytes(java.nio.charset.Charset)
method = public boolean java.lang.String.contentEquals(java.lang.CharSequence)
method = public boolean java.lang.String.contentEquals(java.lang.StringBuffer)
method = private boolean java.lang.String.nonSyncContentEquals(java.lang.AbstractStringBuilder)
method = public boolean java.lang.String.regionMatches(int,java.lang.String,int,int)
method = public boolean java.lang.String.regionMatches(boolean,int,java.lang.String,int,int)
method = public boolean java.lang.String.startsWith(java.lang.String)
method = public boolean java.lang.String.startsWith(java.lang.String,int)
method = static int java.lang.String.lastIndexOf(byte[],byte,int,java.lang.String,int)
method = public int java.lang.String.lastIndexOf(int)
method = public int java.lang.String.lastIndexOf(java.lang.String)
method = public int java.lang.String.lastIndexOf(int,int)
method = public int java.lang.String.lastIndexOf(java.lang.String,int)
method = public java.lang.String java.lang.String.substring(int,int)
method = public java.lang.String java.lang.String.substring(int)
method = public boolean java.lang.String.isEmpty()
method = public java.lang.String java.lang.String.replace(char,char)
method = public java.lang.String java.lang.String.replace(java.lang.CharSequence,java.lang.CharSequence)
method = public boolean java.lang.String.matches(java.lang.String)
method = public java.lang.String java.lang.String.replaceFirst(java.lang.String,java.lang.String)
method = public java.lang.String java.lang.String.replaceAll(java.lang.String,java.lang.String)
method = private java.lang.String[] java.lang.String.split(java.lang.String,int,boolean)
method = public java.lang.String[] java.lang.String.split(java.lang.String)
method = public java.lang.String[] java.lang.String.split(java.lang.String,int)
method = private java.lang.String[] java.lang.String.split(char,int,boolean)
method = public java.lang.String[] java.lang.String.splitWithDelimiters(java.lang.String,int)
method = static java.lang.String java.lang.String.join(java.lang.String,java.lang.String,java.lang.String,java.lang.String[],int)
method = public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.Iterable)
method = public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.CharSequence[])
method = public java.lang.String java.lang.String.toLowerCase()
method = public java.lang.String java.lang.String.toLowerCase(java.util.Locale)
method = public java.lang.String java.lang.String.toUpperCase()
method = public java.lang.String java.lang.String.toUpperCase(java.util.Locale)
method = public java.lang.String java.lang.String.trim()
method = public java.lang.String java.lang.String.strip()
method = public java.lang.String java.lang.String.stripLeading()
method = public java.lang.String java.lang.String.stripTrailing()
method = private int java.lang.String.indexOfNonWhitespace()
method = public java.util.stream.Stream java.lang.String.lines()
method = public java.lang.String java.lang.String.repeat(int)
method = private int java.lang.String.lastIndexOfNonWhitespace()
method = private static int java.lang.String.outdent(java.util.List)
method = public boolean java.lang.String.isBlank()
method = public char[] java.lang.String.toCharArray()
method = public static java.lang.String java.lang.String.format(java.util.Locale,java.lang.String,java.lang.Object[])
method = public static java.lang.String java.lang.String.format(java.lang.String,java.lang.Object[])
method = static void java.lang.String.repeatCopyRest(byte[],int,int,int)
method = public java.lang.Object java.lang.String.resolveConstantDesc(java.lang.invoke.MethodHandles$Lookup) throws java.lang.ReflectiveOperationException
method = public java.lang.String java.lang.String.resolveConstantDesc(java.lang.invoke.MethodHandles$Lookup)
method = public java.util.stream.IntStream java.lang.String.codePoints()
method = static java.lang.String java.lang.String.newStringNoRepl(byte[],java.nio.charset.Charset) throws java.nio.charset.CharacterCodingException
method = static byte[] java.lang.String.getBytesUTF8NoRepl(java.lang.String)
method = static byte[] java.lang.String.getBytesNoRepl(java.lang.String,java.nio.charset.Charset) throws java.nio.charset.CharacterCodingException
method = static int java.lang.String.decodeASCII(byte[],int,char[],int,int)
method = public boolean java.lang.String.equalsIgnoreCase(java.lang.String)
method = public int java.lang.String.compareToIgnoreCase(java.lang.String)
method = public boolean java.lang.String.endsWith(java.lang.String)
method = public java.lang.CharSequence java.lang.String.subSequence(int,int)
method = public java.lang.String java.lang.String.concat(java.lang.String)
method = public boolean java.lang.String.contains(java.lang.CharSequence)
method = public java.lang.String java.lang.String.indent(int)
method = public java.lang.String java.lang.String.stripIndent()
method = public java.lang.String java.lang.String.translateEscapes()
method = public java.util.stream.IntStream java.lang.String.chars()
method = public java.lang.Object java.lang.String.transform(java.util.function.Function)
method = public java.lang.String java.lang.String.formatted(java.lang.Object[])
method = public static java.lang.String java.lang.String.copyValueOf(char[],int,int)
method = public static java.lang.String java.lang.String.copyValueOf(char[])
method = public native java.lang.String java.lang.String.intern()
method = static void java.lang.String.checkOffset(int,int)
method = static java.lang.String java.lang.String.valueOfCodePoint(int)
method = public java.util.Optional java.lang.String.describeConstable()
method = private static java.lang.String java.lang.String.lambda$stripIndent$3(int,java.lang.String)
method = private static java.lang.String java.lang.String.lambda$indent$2(int,java.lang.String)
method = private static java.lang.String java.lang.String.lambda$indent$1(java.lang.String)
method = private static java.lang.String java.lang.String.lambda$indent$0(java.lang.String,java.lang.String)
Superclass: class java.lang.Object
Interface: java.io.Serializable
Interface: java.lang.Comparable
Interface: java.lang.CharSequence
Interface: java.lang.constant.Constable
Interface: java.lang.constant.ConstantDesc
Class 클래스는 다음과 같이 3가지 방법으로 조회할 수 있습니다.
Class clazz = String.class; // 1. 클래스에서 조회.
Class clazz = new String().getClass(); // 2. 인스턴스에서 조회.
Class clazz = Class.forName("java.lang.String"); // 3. 문자열로 조회
Class 클래스의 주요 기능.
getDeclaredFields()
클래스의 모든 필드를 조회합니다.
getDeclaredMethods()
클래스의 모든 메서드를 조회합니다.
getSuperclass()
클래스의 부모 클래스를 조회합니다.
getInterfaces()
클래스의 인터페이스들을 조회합니다.
2️⃣ 클래스 생성하기.
Class 클래스에는 클래스의 모든 정보가 들어있습니다.
이 정보를 기반으로 인스턴스를 생성하거나, 메서드를 호출하고, 필드의 값도 변경할 수 있습니다.
아래의 예제에서는 간단하게 인스턴스를 생성해보겠습니다.
package langReview.clazz;
public class Hello {
public String hello() {
return "hello!";
}
}
package langReview.clazz;
public class ClassCreateMain {
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를 사용하면 클래스의 메타 정보를 기반으로 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 작업을 할 수 있습니다.
이런 작업을 리플랙션이라고 합니다.
추가로 애노테이션 정보를 읽어서 특별한 기능을 수행할 수 있습니다.
최신 프레임워크들은 이런 기능을 적극 활용합니다.
-
☕️[Java] 래퍼 클래스 - 주요 메서드와 성능
☕️[Java] 래퍼 클래스 - 주요 메서드와 성능.
1️⃣ 주요 메서드.
래퍼 클래스가 제공하는 주요 메서드를 알아봅시다.
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 메서드입니다. 간단한 덧셈, 작은 값, 큰 값 연산을 수행합니다.
parseInt()와 valueOf()
언제 parseInt()를 사용하고 valueOf()를 사용해야 할까요?
원하는 타입에 맞는 메서드를 사용하면 됩니다.
valueOf("10") 는 래퍼 타입을 반환합니다.
parseInt("10") 는 기본형을 반환합니다.
Long.parseLong() 처럼 각 타입에 parseXxx() 가 존재합니다.
2️⃣ 래퍼 클래스와 성능.
래퍼 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공합니다.
그렇다면 더 좋은 래퍼 클래스만 제공하면 되지 기본형을 제공하는 이유는 무엇일까요?
다음 코드를 실행해서 기본형과, 래퍼 클래스의 성능 차이를 비교해봅시다.
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");
}
}
실행 결과 - M1 맥북 기준
sumPrimitive = 499999999500000000
기본 자료형 long 실행 시간: 346ms
sumWrapper = 499999999500000000
기본 자료형 Long 실행 시간: 1555ms
기본형 연산이 클래스보다 대략 5배 정도 빠른 것을 확인할 수 있습니다.
참고로 계산 결과는 시스템 마다 다릅니다.
기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지합니다.
예를 들어 int는 보통 4바이트의 메모리를 사용합니다.
래퍼 클래스의 인스턴스는 내부에 필드로 가지고 있는 기본형의 값 뿐만 아니라 자바에서 객체 자체를 다루는데 필요한 객체 메타데이터를 포함하므로 더 많은 메모리를 사용합니다.
자바 버전과 시스템마다 다르지만 대략 8~16바이트의 메모리를 추가로 사용합니다.
기본형, 래퍼 클래스 어떤 것을 사용해야 할까?
이 연산은 10억 번의 연산을 수행했을 때 0.3초와 1.5초의 차이입니다.
기본형이든 래퍼 클래스든 이것을 1회로 환산하면 둘 다 매우 빠르게 연산이 수행됩니다.
0.3초 나누기 10억, 1.5초 나누기 10억입니다.
일반적인 애플리케이션을 만드는 관점에서 보면 이런 부분을 최적화해도 사막의 모래알 하나 정도의 차이가 날 뿐입니다.
CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만 ~ 수십만 이상 연속해서 연산을 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려해야합니다.
그렇지 않은 일반적인 경우라면 코드를 유지보수하기 더 나은 것을 선택하면 됩니다.
유지보수 vs 최적화.
유지보수 vs 최적화를 고려해야 하는 상황이라면 유지보수하기 좋은 코드를 먼저 고민해야 합니다.
특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고해도 실질적인 도움이 되지 않는 경우가 많습니다.
코드 변경 없이 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 단순함 보다는 복잡함을 요구하고, 더 많은 코드들을 추가로 만들어야 합니다.
최적화를 위해 유지보수 해야 하는 코드가 더 늘어나는 것입니다.
그런데 진짜 문제는 최적화를 한다고 했지만 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있습니다.
특히 웹 애플리케이션의 경우 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만배 더 오래 걸립니다.
자바 메모리 내부에서 연산을 수천번에서 한 번으로 줄이는 것 보다, 네트워크 호출 한 번을 더 줄이는 것이 더 효과적인 경우가 많습니다.
권장하는 방법은 개발 이후에 성능 테스트를 해보고 정말 문제가 되는 부분을 찾아서 최적화 하는 것입니다.
-
☕️[Java] 래퍼 클래스 - 자바 래퍼 클래스
☕️[Java] 래퍼 클래스 - 자바 래퍼 클래스.
1️⃣ 자바 래퍼 클래스.
래퍼 클래스는 기본형을 객체로 감싸서 더 편리하게 사용하도록 도와주기 때문에 상당히 유용하다.
즉, 래퍼 클래스는 기본형의 객체 버전입니다.
자바의 래퍼 클래스(Wrapper Class)는 기본 데이터 타입(primitive type)을 객체(object)로 다루기 위해 제공되는 클래스입니다.
자바에는 8개의 기본 데이터 타입이 있으며, 각각에 대응하는 래퍼 클래스가 있습니다.
래퍼 클래스는 기본 타입을 객체로 다뤄야 할 때 유용하게 사용됩니다.
기본형에 대응하는 래퍼 클래스.
byte -> Byte
short -> Short
int -> Integer
long -> Long
float -> Float
double -> Double
char -> Character
boolean -> Boolean
기본 재퍼 클래스의 특징.
불변이다.
equals로 비교해야 합니다.
자바가 제공하는 래퍼 클래스의 사용법.
package langReview.wrapper;
public class WrapperClassMain {
public static void main(String[] args) {
Integer newInteger = new Integer(10); // 미래에 삭제 예정, 대신에 valueOf()를 사용
Integer integerObj = Integer.valueOf(10); // -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
1️⃣ 래퍼 클래스 생성 - 박싱(Boxing)
기본형을 래퍼 클래스로 변경하는 것을 마치 박스에 물건을 넣은 것 같다고 해서 박싱(Boxing) 이라고 합니다.
new Integer(10)은 직접 사용하면 안됩니다.
작동은 하지만, 향후 자바에서 제거될 예정입니다.
대신에 Integer.valueOf(10)을 사용하면 됩니다.
내부에서 new Integer(10)을 사용해서 객체를 생성하고 돌려줍니다.
추가로 Integer.valueOf()에는 성능 최적화 기능이 있습니다.
개발자들이 일반적으로 자주 사용하는 -128 ~ 127 범위의 Integer 클래스를 미리 생성해줍니다.
해당 범위의 값을 조회하면 미리 생성된 Integer 객체를 반환합니다.
해당 범위의 값이 없으면 new Integer()를 호출합니다.
마치 문자열 풀과 비슷하게 자주 사용하는 숫자를 미리 생성해두고 재사용합니다.
참고로 이런 최적화 방식은 미래에 더 나은 방식으로 변경될 수 있습니다.
2️⃣ intValue() - 언박싱(Unboxing)
래퍼 클래스는 객체이기 때문에 == 비교를 하면 인스턴스의 참조값(Reference)을 비교합니다.
래퍼 클래스는 내부의 값을 비교하도록 equals()를 재정의 해두었습니다.
따라서 값을 비교하려면 equals()를 사용해야 합니다.
참고로 래퍼 클래스는 객체를 그대로 출력해도 내부에 있는 값을 문자로 출력하도록 toString()을 재정의 해두었습니다.
2️⃣ 래퍼 클래스의 주요 사용 목적.
컬렉션과의 호환성.
자바의 컬렉션 프레임워크(List, Set, Map 등)는 객체만 저장할 수 있습니다.
따라서 기본 데이터 타입(primitive type)을 컬렉션에 저장하려면 해당 값을 래퍼 클래스의 객체로 변환해야 합니다.
유틸리티 메서드 제공.
래퍼 클래스는 기본 데이터 타입을 문자열로 변환하거나, 문자열을 기본 데이터 타입(primitive type)으로 변환하는 등의 유용한 유틸리티 메서드를 제공합니다.
상수 정의.
래퍼 클래스는 MAX_VALUE. MIN_VALUE 등과 같은 상수를 제공합니다.
이 상수들은 해당 타입의 최대값, 최소값 등을 표현합니다.
3️⃣ 래퍼 클래스의 주요 메서드.
valueOf(String s) : 문자열을 기본 데이터 타입의 래퍼 객체로 변환합니다.
parseInt(String s) : 문자열을 int로 변환합니다.
toString() : 객체의 값을 문자열로 변환합니다.
compareTo(T another) : 두 래퍼 객체를 비교하여 정수 값을 반환합니다.
4️⃣ 예시.
public class WrapperClassExample {
public static void main(String[] args) {
// 기본 타입을 래퍼 클래스로 변환 (박싱, Boxing)
Integer intObject = Integer.valueOf(42);
Double doubleObject = Double.valueOf(3.14);
Boolean booleanObject = Boolean.valueOf(true);
// 래퍼 클래스를 기본 타입으로 변환(언박싱, Unboxing)
int intValue = intObject.intValue();
double doubleValue = doubleObject.doubleValue();
boolean = booleanValue = booleanObject.booleanValue();
System.out.println("Integer object: " + intObject);
System.out.println("Double object: " + doubleObject);
System.out.println("Boolean object: " + booleanObject);
System.out.println("Unboxed int: " + intValue);
System.out.println("Unboxed double: " + doubleValue);
System.out.println("Unboxed boolean: " + booleanValue);
}
}
이 코드는 기본 데이터 타입과 래퍼 클래스 간의 변환을 보여주며, 박싱과 언박싱이 어떻게 동작하는지를 설명합니다.
이러한 기능들은 자바에서 기본 타입을 객체로 다루어야 하는 상황에서 매우 유용하게 사용됩니다.
-
-
☕️[Java] null이란?
☕️[Java] null이란?
1️⃣ null.
Java에서 null은 참조형 타입(Reference Type) 변수에 사용되는 특별한 값으로, 해당 변수가 어떤 객체도 가리키지 않고 있음을 나타냅니다.
즉, null은 변수에 객체의 참조가 없음을 의미합니다.
2️⃣ 주요 특징.
1. 참조형 타입에만 적용.
null은 기본형 타입(Primitive Type)에는 사용할 수 없으며, 오직 참조형 타입(예: String, Integer, List 등) 변수에만 사용할 수 있습니다.
기본형 타입의 변수는 null이 아닌, 각 타입의 기본값(예: int는 0, boolean은 false)을 가집니다.
2. 객체의 부재.
null은 변수에 객체가 할당되지 않았거나, 명시적으로 “객체가 없음”을 나타내고자 할 때 사용됩니다.
예를 들어, 객체가 생성되지 않거나 초기화되지 않은 상태에서 참조형 변수를 선언하면, 해당 변수는 기본적으로 null 값을 가집니다.
3. NullPointerException.
null 참조를 사용하려고 할 때(예: null 객체의 메서드를 호출하거나 필드에 접근할 때), Java는 NullPointerException을 발생시킵니다.
이는 매우 일반적인 런타임 오류 중 하나입니다.
3️⃣ null의 사용 예.
public class Main {
public static void main(String[] args) {
String str = null; // str은 어떤 객체도 가리키지 않음
if (str == null) {
System.out.println("str은 null입니다.");
}
// NullPointerException을 피하기 위해 null 검사를 수행해야 합니다.
if (str != null) {
System.out.println(str.length()); // str이 null이 아닌 경우에만 실행
}
// 만약 null 검사를 하지 않고 null 참조를 사용하면 NullPointerException 발생
try {
System.out.println(str.length());
} catch (NullPointerException e) {
System.out.println("NullPointerException 발생: " + e.getMessage());
}
}
}
위 코드에서 str 변수는 null로 초기화되었습니다.
str.length()를 호출하려고 하면, NullPointerException이 발생합니다.
이 오류를 피라기 위해서는 항상 null 검사를 수행해야 합니다.
4️⃣ null 사용의 주의점.
1. NullPointerException 방지.
코드를 작성할 때, null일 가능성을 항상 염두에 두어야 합니다.
null 검사를 적절히 수행하지 않으면 NullPointerException이 발생할 수 있습니다.
2. Optional 사용.
Java 8 이후에는 Optional 클래스를 사용하여 null을 안전하게 처리할 수 있습니다.
Optional은 값이 있거나 없음을 명시적으로 나타내어 null 참조를 피할 수 있게 해줍니다.
```java
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional optionalStr = Optional.ofNullable(null);
if (optionalStr.isPresent()) {
System.out.println(optionalStr.get());
} else {
System.out.println("값이 존재하지 않습니다.");
}
} } ```
위 코드에서 Optional.ofNullable(null)을 사용하여 null을 안전하게 처리할 수 있습니다.
5️⃣ 요약.
null은 참조형 타입 변수가 어떤 객체도 참조하지 않고 있음을 나타냅니다.
null을 잘못 사용하면 NullPointerException이 발생할 수 있으며, 이를 방지하기 위해서는 null 검사를 철저히 수행해야 합니다.
Java 8 이후에는 Optional 클래스를 사용하여 null 참조를 보다 안전하게 관리할 수 있습니다.
-
☕️[Java] 래퍼 클래스 - 기본형의 한계(2)
☕️[Java] 래퍼 클래스 - 기본형의 한계(2)
1️⃣ 기본형과 null.
기본형(Primitive Type)은 항상 값을 가져야 합니다.
하지만 때로는 데이터가 ‘없음’이라는 상태가 필요할 수 있습니다.
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을 반환한 것인지 명확하지 않습니다.
2️⃣ 참조형과 null.
객체의 경우 데이터가 없다는 null이라는 명확한 값이 존재합니다.
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을 반환합니다.
3️⃣ 요약.
기본형은 항상 값이 존재해야 합니다.
숫자의 경우 0, -1 같은 값이라도 항상 존재해야 합니다.
객체인 참조형은 값이 없다는 null을 사용할 수 있습니다.
null 값을 반환하는 경우 잘못하면 NullPointException이 발생할 수 있기 때문에 주의해서 사용해야 합니다.
-
☕️[Java] 래퍼 클래스 - 기본형의 한계(1)
☕️[Java] 래퍼 클래스- 기본형의 한계(1)
1️⃣ 기본형의 한계.
자바는 객체 지향 언어입니다.
그런데 자바 안에 객체가 아닌 것이 있습니다.
바로 int, double 같은 기본형(Primitive Type)입니다.
기본형은 객체가 아니기 때문에 다음과 같은 한계가 있습니다.
객체가 아님.
기본형 데이터는 객체가 아니기 때문에, 객체 지향 프로그래밍의 장점을 살릴 수 없습니다.
예를 들어 객체는 유용한 메서드를 제공할 수 있는데, 기본형은 객체가 아니므로 메서르듣 제공할 수 없습니다.
null 값을 가질 수 없습니다.
기본형 데이터 타입은 null 값을 가질 수 없습니다.
때로는 데이터가 없음이라는 상태를 나타내야 할 필요가 있는데, 기본형은 항상 값을 가지기 때문에 이런 표현을 할 수 없습니다.
2️⃣ 예시.
기본형의 한계를 이해하기 위해, 두 값을 비교해서 다음과 같은 결과를 출력하는 간단한 코드를 작성해 보겠습니다.
왼쪽의 값이 더 작다: -1
두 값이 같다: 0
왼쪽의 값이 더 크다: 1
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 객체 스스로 자기 자신의 값과 다른 값을 비교하는 메서드를 만드는 것이 더 유용할 것입니다.
3️⃣ 직접 만든 래퍼 클래스.
int를 클래스로 만들어 봅시다.
int는 클래스가 아니지만, int 값을 가지고 클래스를 만들면 됩니다.
다음 코드는 마치 int를 클래스로 감싸서 만드는 것 처럼 보입니다.
이렇게 특정 기본형을 감싸서(Wrap) 만드는 클래스를 래퍼 클래스(Wrapper class)라 합니다.
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를 통해 객체로 다룰 수 있게 되었습니다.
```java
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는 기본형이기 때문에 스스로 메서드를 가질 수 없습니다.
-
☕️[Java] Optional
☕️[Java] Optional.
1️⃣ Optional.
Optional은 Java 8에서 도입된 클래스이며, 주로 null 값을 안전하게 처리하기 위해 사용됩니다.
Optional은 값이 존재할 수도 있고 존재하지 않을 수도 있는 상황을 표현하는 컨테이너 역할을 하며, null을 직접 사용하는 것보다 안전하고 명시적인 방법을 제공합니다.
2️⃣ 주요 특징
1. 값의 존재 여부를 명확히 표현.
Optional은 값이 존재하는 경우(Optional이 비어있지 않음)와 값이 존재하지 않는 경우(Optional이 비어있음)를 명확하게 표현합니다.
이를 통해 null 참조로 인해 발생할 수 있는 오류를 예방할 수 있습니다.
2. 명시적인 null 처리.
Optional을 사용하면 값이 없을 때 취할 행동을 명시적으로 정의할 수 있습니다.
이로 인해 코드가 더 읽기 쉬워지고, NullPointerException을 피할 수 있습니다.
3. 함수형 프로그래밍 스타일 지원.
Optional은 다양한 메서드(map, flatMap, filter, ifPresent, orElse 등)를 제공하여 값의 존재 여부에 따라 함수형 프로그래밍 스타일로 작업을 수행할 수 있습니다.
3️⃣ Optional 생성.
Optional<String> emptyOptional = Optional.empty();
빈 Optional 생성 : Optional.empty()를 사용하여 비어 있는 Optional을 생성할 수 있습니다.
Optional<String> nonEmptyOptional = Optional.of("Hello");
null이 아닌 값으로 Optional 생성.
Optional.of()를 사용하여 null이 아닌 값으로 Optional을 생성할 수 있습니다.
값이 null인 경우 NullPointerException이 발생합니다.
Optional<String> nullableOptional = Optional.ofNullable(null);
null이 될 수 있는 값으로 Optional 생성.
Optional.ofNullable()을 사용하여 값이 null일 수도 있는 경우 Optional을 생성할 수 있습니다.
값이 null이면 빈 Optional이 생성됩니다.
Optional<String> nullableOptional = Optional.ofNullable(null);
4️⃣ Optional의 사용 예
1. 값이 존재하는지 확인(isPresent / ifPresent)
```java
Optional optional = Optional.of("Hello");
if (optional.isPresent()) {
System.out.println(optional.get()); // 값이 있으면 출력
}
optional.ifPresent(value -> System.out.println(value)); // 값이 있으면 람다 표현식 실행
- **2. 값이 없을 때 대체 값 제공(`orElse`, `orElseGet`)**
```java
String result = optional.orElse("Default Value"); // 값이 없으면 "Defaul Value"
String result = optional.orElseGet(() -> "Default Value from Supplier"); // 람다로 대체 값 제공
3. 값이 없으면 예외 던지기(orElseThrow)
String result = optional.orElseThrow(() -> new IllegalArgumentException("No value present"));
4. 값 변환(map, flatMap)
```java
Optional optional = Optional.of("Hello");
Optional lengthOptional = optional.map(String::length);
lengthOptional.ifPresent(length -> System.out.println("Length: " + length));
- **5. 조건에 따른 필터링(`filter`)**
```java
Optional<String> optional = Optional.of("Hello");
Optional<String> filtered = optional.filter(value.startsWith("H"));
filtered.ifPresent(System.out::println); // "Hello" 출력
5️⃣ Optional 사용의 이점.
1. 명시적 코드 작성.
Optional을 사용하면 코드에서 값이 존재할 수 있는지 여부를 명시적으로 다루게 되어 가독성이 높아집니다.
2. NullPointerException 방지.
Optional은 null 체크를 강제하여 NullPointerException 발생을 예방합니다.
3. 함수형 스타일.
Optional은 함수형 프로그래밍 스타일을 지원하여 간결하고 선언적인 코드를 작성할 수 있게 합니다.
6️⃣ Optional 사용 시 주의점.
1. 과도한 사용 자제.
모든 상황에서 Optional을 사용하는 것은 오히려 복잡성을 초래할 수 있습니다.
예를 들어, 필드에 Optional을 사용하거나, 매우 단순한 로직에 Optional을 사용하는 것은 바람직하지 않을 수 있습니다.
2. 성능.
Optional은 추가적인 객체를 생성하므로, 성능이 중요한 곳에서는 주의해서 사용해야 합니다.
7️⃣ 요약.
Optional은 null을 안전하게 처리하기 위한 클래스이며, 값이 있을 수도 있고 없을 수도 있는 상황을 명시적으로 표현할 수 있게 해줍니다.
Optional을 통해 코드의 가독성을 높이고, NullPointerException을 방지할 수 있습니다.
Optional은 함수형 프로그래밍 스타일을 지원하여 코드 작성의 유연성을 제공합니다.
-
☕️[Java] 람다(Lambda)
☕️[Java] 람다(Lambda).
1️⃣ 람다 표현식(Lambda Expression)
Java 8에서 도입된 기능으로, 익명 함수(Anonymous Function)를 간결하게 표현한 것입니다.
람다 표현식은 메서드로 전달되거나, 변수에 할당될 수 있는 짧은 형태의 코드를 작성할 수 있게 해줍니다.
주로 함수형 인터페이스(Functional Interface)를 구현하는 데 사용되며, Java에서 함수형 프로그래밍 스타일을 가능하게 합니다.
2️⃣ 람다 표현식의 기본 구조.
람다 표현식은 다음과 같은 형태로 작성됩니다.
(parameters) -> expression
혹은
(paramters) -> { statements; }
매개변수 목록(parameters)
메서드처럼 전달받을 인자를 정의합니다.
인자가 하나인 경우 괄호()를 생략 할 수 있습니다.
화살표 연산자 ->
람다 표현식의 매개변수와 본문(body)을 구분합니다.
본문(body)
람다가 실행할 코드를 정의합니다.
단일 표현식일 경우 중괄호{}를 생략할 수 있으며, 여러 문장일 경우 중괄호 {}로 묶습니다.
3️⃣ 예시
1. 기본적인 람다 표현식
// 기존 익명 내부 클래스 방식
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello, Lambda!");
}
};
// 람다 표현식 사용
Runnable lambdaRunnable = () -> System.out.prinln("Hello, Lambda!");
위 코드에서 Runnable 인터페이스를 구현하는 익명 내부 클래스를 람다 표현식으로 대체했습니다.
Runnable은 매개변수가 없고 반환값도 없는 run() 메서드를 가지고 있기 때문에, 이를 간단하게 () ->로 표현할 수 있습니다.
2. 매개변수와 함께 사용하는 람다 표현식
// 기존 방식
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
}
// 람다 표현식 사용
Comparator<Integer> lambdaComparator = (o1, o2) -> o1.compareTo(o2);
위 코드에서 Comparator 인터페이스를 구현하는 익명 내부 클래스를 람다 표현식으로 대체했습니다.
두 개의 매개변수 o1과 o2를 비교하는 코드를 람다 표현식으로 간결하게 작성할 수 있습니다.
4️⃣ 함수형 인터페이스와 람다.
람다 표현식은 함수형 인터페이스를 구현하는 데 사용됩니다.
함수형 인터페이스는 단 하나의 추상 메서드만을 가지는 인터페이스로, @FuncationalInterface 애노테이션으로 명시할 수 있습니다.
예시: 함수형 인터페이스
@FunctionalInterface
interface MyFuncationalInterface {
void doSomething();
}
public class Main {
public static void main(String[] args) {
MyFunctionalInterface myFunc = () -> System.out.println("Doing something!");
myFunc.doSomething(); // 출력: Doing something!
}
}
위 코드에서 MyFunctionalInterface는 함수형 인터페이스이며, 람다 표현식 () -> System.out.println("Doing something!") 은 이 인터페이스의 doSomething() 메서드를 구현합니다.
5️⃣ 자주 사용되는 함수형 인터페이스
Java 8에서는 자주 사용되는 함수형 인터페이스들이 java.util.function 패키지에 포함되어 있습니다.
Predicate<T> : T 타입의 인자를 받아서 boolean을 반환하는 함수입니다.
Predicate<String> isEmpty = s -> s.isEmpty();
Function<T, R> : T 타입의 인자를 받아 R 타입의 결과를 반환하는 함수입니다.
Function<Integer, String> intToString = i -> Integer.toString(i);
Consumer<T> : T 타입의 인자를 받아서 소비하는(반환값이 없는) 함수입니다.
Consumer<String> print = s -> System.out.println(s);
Supplier<T> : 인자를 받지 않고 T 타입의 결과를 반환하는 함수입니다.
Supplier<Double> randomValue = () -> Math.random();
6️⃣ 장점.
1. 코드 간결화 : 람다 표현식을 사용하면 코드가 훨씬 간결해지고 가독성이 좋아집니다.
2. 지연 실행 : 람다 표현식을 통해 코드를 지연 실행할 수 있어 성능 최적화에 유리합니다.
3. 병렬 처리 : 스트림 API와 함께 사용하여 병렬 처리 작업을 쉽게 구현할 수 있습니다.
7️⃣ 요약.
람다 표현식은 익명 함수의 일종으로, 함수형 인터페이스를 구현하는 간결한 방법을 제공합니다.
이를 통해 코드의 가독성과 유지보수성이 향상되며, 함수형 프로그래밍 패러다임을 Java에서 더욱 효과적으로 활용할 수 있게 됩니다.
-
☕️[Java] 기본형 타입(Primitive Type).
☕️[Java] 기본형 타입(Primitive Type).
1️⃣ 기본형 타입(Primitive Type)란?
기본형 타입(Primitive Type)은 Java에서 가장 단순하고 기본적인 데이터 유형으로, 실제 값을 직접 저장하는 변수 유형입니다.
기본형 타입(Primitive Type)은 참조형 타입(Reference Type)과 달리 객체가 아니며, 메모리 상에 직접 데이터를 저장합니다.
Java에는 8개의 기본형 타입(Primitive Type)이 있습니다.
1️⃣ 정수형.
byte : 8비트 정수(-128 ~ 127).
short : 16비트 정수(-32,768 ~ 32,767).
int : 32비트 정수(-2^31 ~ 2^31-1).
long : 64비트 정수(-2^63 ~ 2^63-1).
2️⃣ 실수형.
float : 32비트 부동소수점.
double : 64비트 부동소수점.
3️⃣ 문자형.
char : 16비트 유니코드 문자(0 ~ 65,535)
4️⃣ 논리형.
boolean : 참 또는 거짓(true/false)
2️⃣ 기본형 타입의 한계.
1. 객체로서의 기능 부족.
기본형 타입은 객체가 아니기 때문에 메서드나 속성을 가질 수 없습니다.
이로 인해 객체지향적인 프로그래밍에서 사용되는 많은 기능을 사용할 수 없습니다.
2. 컬렉션과의 호환성 부족.
기본형 타입은 Java의 컬렉션 프레임워크(List, Set, Map 등)에 직접적으로 사용할 수 없습니다.
컬렉션은 객체만을 저장할 수 있기 때문에, 기본형을 사용할 경우 래퍼 클래스(예: Integer, Double, Boolean)로 변환해야 합니다.
3. 유연성 부족.
기본형 타입은 값이 불변이며, 추가적인 속성이나 동작을 정의할 수 없습니다.
이를 보완하기 위해 래퍼 클래스와 같은 객체가 필요합니다.
3️⃣ 기본형 타입을 사용하는 이유.
1. 성능.
기본형 타입은 참조형 타입보다 메모리 사용량이 적고, 처리 속도가 빠릅니다.
기본형 타입은 실제 값을 직접 메모리에 저장하기 때문에, 메모리 접근이 빠르고, 가비지 컬렉션과 같은 추가적인 처리 없이 직접적으로 값을 사용할 수 있습니다.
2. 메모리 효율성.
기본형 타입은 참조형 타입보다 메모리 사용량이 적습니다.
예를 들어, int는 32비트를 사용하지만, Integer는 객체를 래핑하기 때문에 추가적인 메모리 오버헤드가 발생합니다.
3. 간결함.
기본형 타입은 간단하고 사용하기 쉽습니다.
복잡한 객체 대신, 단순한 값만 필요할 때 기본형 타입을 사용하는 것이 코드의 가독성을 높이고, 불필요한 복잡성을 줄일 수 있습니다.
4. 기본적인 연산 지원.
기본형 타입은 Java 언어 내에서 산술 연산, 비교 연산 등 기본적인 연산을 직접적으로 지원합니다.
이러한 연산은 참조형 타입보다 훨씬 빠르게 수행됩니다.
4️⃣ 기본형 타입의 사용 예.
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 20;
int sum = a + b; // 기본형 타입 간의 덧셈 연산
System.out.println("Sum: " + sum); // 출력: Sum: 30
// 기본형 타입을 사용할 경우 메모리 효율성과 성능이 우수
}
}
5️⃣ 기본형 타입과 래퍼 클래스.
Java는 기본형 타입을 객체로 감쌀 수 있는 래퍼 클래스를 제공합니다.
예를 들어, int의 래퍼 클래스는 Integer입니다.
래퍼 클래스는 기본형 타입을 객체로 변환하여 컬렉션과 같은 객체 기반 API와 호환성을 제공할 수 있습니다.
```java
public class Main {
public static void main(String[] args) {
Integer x = 10; // 오토박싱 (autoboxing)
int y = x; // 오토언박싱 (autounboxing)
// 래퍼 클래스는 메서드를 가질 수 있습니다.
int max = Integer.max(10, 20);
System.out.println(“Max: “ + max); // 출력: Max: 20
}
}
```
기본형 타입은 성능과 메모리 효율성 면에서 장점이 있지만, 객체의 유연성과 컬렉션 호환성 측면에서 한계가 있습니다.
이러한 이유로, Java에서는 상황에 따라 기본형 타입과 래퍼 클래스를 적절히 사용하게 됩니다.
-
-
-
☕️[Java] 메서드 체이닝 - Method Chaining
☕️[Java] 메서드 체이닝 - Method Chaining.
간단한 예제 코드로 메서드 체이닝(Method Chaining)에 대해 알아봅시다.
1️⃣ 예제 코드.
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
단순히 값을 누적해서 더하는 기능을 제공하는 클래스입니다.
add() 메서드를 호출할 때 마다 내부의 value에 값을 누적합니다.
add() 메서드를 보면 자기 자신(this)의 참조값을 반환합니다.(이 부분을 유의해서 봅시다.)
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() 메서드의 반환값을 사용해봅시다.
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)의 참조값을 반환했기 때문입니다.
그런데 이 방식은 처음 방식보다 더 불편하고, 코드도 잘 읽히지 않습니다.
이런 방식을 왜 사용할까요?
이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드에 호출에 사용해보겠습니다.
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)
메서드 체이닝 방식은 메서드가 끝나는 시점에 바로 . 을 찍어서 변수명을 생략할 수 있습니다.
메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문입니다.
이 참조값에 .을 찍어서 바로 자신의 메서드를 호출할 수 있습니다.
메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어줍니다.
2️⃣ StringBuilder와 메서드 체인(Chain)
StringBuilder는 메서드 체이닝 기법을 제공합니다.
StringBuilder의 append() 메서드를 보면 자기 자신의 참조값을 반환합니다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환합니다.
예) insert(), delete(), reverse()
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);
// StringBuild -> String
String string = sb.toString();
System.out.println("string = " + string);
}
}
위 코드를 메서드 체이닝 기법을 사용하면 아래와 같이 코드를 작성할 수 있습니다.
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
3️⃣ 정리.
“만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다” 는 말이 있습니다.
메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리해집니다.
참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용합니다.
-
☕️[Java] StringBuilder - 가변 String
☕️[Java] StringBuilder - 가변 String
1️⃣ 불변인 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, 메모리 자원을 더 많이 사용하게 됩니다.
그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모합니다.
참고: 실제로 문자열을 다룰 때 자바가 내부에서 최적화를 적용합니다.
2️⃣ StringBuilder
이 문제를 해결하는 방법은 단순합니다.
바로 불변이 아닌 가변 String이 존재하면 됩니다.
가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없습니다.
따라서 성능과 메모리 사용면에서 불변보다 더 효율적입니다.
이런 문제를 해결하기 위해 자바는 StringBuilder 라는 가변 String을 제공합니다.
물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 합니다.
StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]을 가지고 있습니다.
public final class StringBuilder {
char[] value; // 자바 9 이전
byte[] value; // 자바 9 이후
// 여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
(실제로는 상속 관계에 있고 부모 클래스인 AbstractStringBuilder에 value 속성과 length() 메서드가 있습니다.)
3️⃣ StringBuilder 사용 예시
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);
// StringBuild -> 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으로 변환하는 것이 좋습니다.
-
☕️[Java] String 최적화
☕️[Java] String 최적화
1️⃣ 자바의 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를 사용하지 않아도 됩니다.
대신에 문자열 더하기 연산(+)을 사용하면 충분합니다.
2️⃣ 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 객체를 생성했을 것입니다.
실행 결과
result = Hello Java Hello Java Hello Java...
time = 2564ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 2.6초가 걸렸습니다.
이럴때는 직접 StringBuilder를 사용하면 됩니다.
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 Hello Java...
time = 3ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 0.003초가 걸렸습니다.
3️⃣ 정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다.
StringBuilder를 직접 사용하는 것이 더 좋은 경우
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때
참고: StringBuilder vs StringBuffer
StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있습니다.
StringBuffer는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느립니다.
StringBuilder는 멀티 쓰레드 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠릅니다.
-
-
☕️[Java] JDBC(Java Database Connectivity)
☕️[Java] JDBC(Java Database Connectivity)
JDBC(Java Database Connectivity)는 자바 프로그램이 데이터베이스에 연결하고, SQL 쿼리를 실행하며, 데이터베이스로부터 결과를 가져오는 것을 가능하게 하는 자바 API입니다.
JDBC는 Java의 표준 API로, 다양한 관계형 데이터베이스 시스템(RDBMS)과의 상호 작용을 단순화하는 데 사용됩니다.
1️⃣ 주요 기능 및 개념.
1. 데이터베이스 연결
JDBC를 사용하면 자바 애플리케이션에서 데이터베이스에 연결할 수 있습니다.
이를 위해서는 데이터베이스의 URL, 사용자 이름, 비밀번호 등을 사용하여 Connection 객체를 생성합니다.
2. SQL 쿼리 실행
연결이 설정된 후, SQL 쿼리를 실행할 수 있습니다.
이는 Statement, PreparedStatment, CallableStatement 와 같은 JDBC 인터페이스를 통해 이루어집니다.
Statement는 정적 SQL 쿼리를 실행할 때 사용됩니다.
PreparedStatement는 동적 SQL 쿼리를 미리 컴파일하고, 반복 실행할 때 효율적으로 사용할 수 있습니다.
CallableStatement는 데이터베이스의 저장 프로시저를 호출할 때 사용됩니다.
3. 결과 처리
SQL 쿼리의 결과는 ResultSet 객체를 통해 얻을 수 있습니다.
ResultSet은 데이터베이스로 부터 검색된 데이터를 테이블 형식으로 제공합니다.
4. 트랜잭션 관리
JDBC는 데이터베이스 트랜젝션을 관리하는 기능도 제공합니다.
기본적으로 자동 커밋 모드이지만, 필요에 따라 수동으로 트랜잭션을 관리하고 커밋하거나 롤백할 수 있습니다.
5. 에러 처리
JDBC는 데이터베이스 관련 작업 중 발생하는 예외를 처리하기 위해 SQLException 클래스를 사용합니다.
이를 통해 에러 코드를 확인하고, 적절한 예외 처리를 할 수 있습니다.
2️⃣ JDBC의 구성 요소.
Driver
JDBC 드라이버는 특정 데이터베이스와 자바 애플리케이션 간의 통신을 담당합니다.
각 DBMS는 고유한 JDBC 드라이버를 제공합니다.
Connection
데이터베이스 연결을 표현하며, SQL 쿼리를 실행할 때 사용되는 객체를 생성합니다.
Statement
SQL 쿼리를 데이터베이스에 전달하는 역할을 합니다.
ResultSet
쿼리의 결과를 포함하며, 데이터를 읽을 수 있게 합니다.
3️⃣ JDBC의 작동 원리.
1. 드라이버 로드
애플리케이션이 사용할 JDBC 드라이버를 로드합니다.
2. 데이터베이스 연결
DriverManager.getConnection() 메서드를 사용하여 데이터베이스에 연결합니다.
3. SQL 쿼리 실행
Statement 나 PreparedStatement 객체를 사용하여 SQL 쿼리를 실행합니다.
4. 결과 처리
ResultSet 객체를 사용하여 쿼리 결과를 처리합니다.
5. 자원 해제
사용된 ResultSet, Statement, Connection 객체를 명시적으로 닫아 자원을 해제합니다.
JDBC는 데이터베이스와의 직접적인 통신을 가능하게 하며, Java 애플리케이션에서 데이터베이스 연동을 위해 널리 사용됩니다.
-
-
☕️[Java] String 클래스 - 기본
☕️[Java] String 클래스 - 기본
1️⃣ String 클래스 - 기본
자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있습니다.
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가지가 있습니다.
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"); // 변경
2️⃣ 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(int index) : 특정 인덱스의 문자를 반환합니다.
substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
indexOf(String str) : 특정 문자열이 시작되는 인덱스를 반환합니다.
toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다.
trim() : 문자열 양 끝의 공백을 제거합니다.
concat(String str) : 문자열을 더합니다.
3️⃣ String 클래스와 참조형.
String 은 클래스입니다.
따라서 기본형이 아니라 참조형입니다.
참조형은 변수에 계산할 수 있는 값이 들어오는 것이 아니라 x001 과 같이 계산할 수 없는 참조값이 들어있습니다.
따라서 원칙적으로 + 같은 연산을 사용할 수 없습니다.
public class StringConcatMain {
public static void main(String[] args) {
String a = "hello";
String b = " java";
String result1 = a.concat(b);
String result2 = a + b;
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2);
}
}
자바에서 문자열을 더할 때는 String 이 제공하는 concat() 과 같은 메서드를 사용해야 합니다.
하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공합니다.
실행 결과
result1 = hello java
result2 = hello java
-
-
-
☕️[Java] 불변 객체 - 문제와 풀이
☕️[Java] 불변 객체 - 문제와 풀이
1️⃣ 불변 객체 - 문제와 풀이.
문제 설명
MyDate 클래스는 불변이 아니어서 공유 참조시 사이드 이펙트가 발생합니다. 이를 불변 클래스로 만들어야 합니다.
새로운 불변 클래스는 ImmutableMyDate
새로운 실행 클래스는 ImmutableMyDateMain
1️⃣ 불변이 아닌 MyDate 클래스
// MyDate
public class MyDate {
private int year;
private int month;
private int day;
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public void setYear(int year) {
this.year = year;
}
public void setMonth(int month) {
this.month = month;
}
public void setDay(int day) {
this.day = day;
}
@Override
public String toString() {
return year + "-" + month + "-" + day;
}
}
// MyDateMain
public class MyDateMain {
public static void main(String[] args) {
MyDate date1 = new MyDate(2024, 9, 1);
MyDate date2 = date1;
System.out.println("date1 = " + date1);
System.out.println("date2 = " + date2);
System.out.println("2025 -> date1");
date1.setYear(2025);
System.out.println("date1 = " + date1);
System.out.println("date2 = " + date2);
}
}
실행 결과
date1 = 2024-9-1
date2 = 2024-9-1
2025 -> date1
date1 = 2025-9-1
date2 = 2025-9-1
2️⃣ 불변 클래스인 ImmutableMyDate
// ImmutableMyDate
public class ImmutableMyDate {
private final int year;
private final int month;
private final int day;
public ImmutableMyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public ImmutableMyDate withYear(int newYear) {
return new ImmutableMyDate(newYear, month, day);
}
public ImmutableMyDate withMonth(int newMonth) {
return new ImmutableMyDate(year, newMonth, day);
}
public ImmutableMyDate withDay(int newDay) {
return new ImmutableMyDate(year, month, newDay);
}
@Override
public String toString() {
return year + "-" + month + "-" + day;
}
}
// ImmutableMyDateMain
public class ImmutableMyDateMain {
public static void main(String[] args) {
ImmutableMyDate immutableDate1 = new ImmutableMyDate(2024, 9, 1);
ImmutableMyDate immutableDate2 = immutableDate1;
System.out.println("immutableDate1 = " + immutableDate1);
System.out.println("immutableDate2 = " + immutableDate2);
System.out.println("2025 -> immutableDate1");
// 방법 1.
//immutableDate1 = new ImmutableMyDate(2025, 9, 1);
// 방법 2. -> 이 방법을 더 지향함
// 주의: 불변 객체에서 값을 변경하는 메서드가 있을 경우에는 무조건 반환값을 받아서 참조를 가지고 가야 바뀐 값을 사용할 수 있습니다.
immutableDate1 = immutableDate1.withYear(2025); // x002
System.out.println("immutableDate1 = " + immutableDate1); // x002
System.out.println("immutableDate2 = " + immutableDate2); // x001
}
}
실행 결과
immutableDate1 = 2024-9-1
immutableDate2 = 2024-9-1
2025 -> immutableDate1
immutableDate1 = 2025-9-1
immutableDate2 = 2024-9-1
3️⃣ 참고 - withXxx()
불변 객체에서 값을 변경하는 경우 withYear() 처럼 “with” 로 시작하는 경우가 많습니다.
예를 들어, “coffee with sugar” 라고 하면, 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든가는 것을 의미합니다.
이 개념을 프로그래밍에 적용하면, 불변 객체의 메서드가 “with” 로 이름 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻합니다.
정리하면 “with” 는 관례처럼 사용되는데, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현합니다.
-
☕️[Java] 불변 객체 - 예제
☕️[Java] 불변 객체 - 예제
조금 더 복잡하고 의미있는 예제를 통해서 불변 객체의 사용 예를 확인해봅시다.
1️⃣ Address, ImmutableAddress 코드.
// Address
public class Address {
private String value;
public Address(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
// ImmutableAddress
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 + '\'' +
'}';
}
}
2️⃣ 변경 클래스 사용.
// MemberV1
public class MemberV1 {
private String name;
private Address address;
public MemberV1(String name, Address address) {
this.name = name;
this.address = address;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public String toString() {
return "MemberV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
// MemberMainV1
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='부산'}}
3️⃣ 불변 클래스 사용.
// MemberV2
public class MemberV2 {
private String name;
private ImmutableAddress address;
public MemberV2(String name, ImmutableAddress address) {
this.name = name;
this.address = address;
}
public ImmutableAddress getAddress() {
return address;
}
public void setAddress(ImmutableAddress address) {
this.address = address;
}
@Override
public String toString() {
return "MemberV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
MemberV2 는 주소를 변경할 수 없는, 불변인 ImmutableAddress 를 사용합니다.
// MemberMainV2
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 는 기존 주소를 그대로 유지합니다.
-
☕️[Java] 테스트 코드와 Reflection.
☕️[Java] 테스트 코드와 Reflection.
1️⃣ 전체 코드.
// Member
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// MemberRepository - Interface
import com.devkobe.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// MemoryMemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
// MemberService
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/*
* 회원 가입
*/
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
}
2️⃣ MemberService를 Test.
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void setUp() throws Exception {
memberService = new MemberService();
memberRepository = new MemoryMemberRepository();
// Reflection을 사용하여 memberRepository 필드에 값을 설정.
Field repositoryField = MemberService.class.getDeclaredField("memberRepository");
repositoryField.setAccessible(true);
repositoryField.set(memberService, memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
public void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long savedId = memberService.join(member);
// then
Optional<Member> foundMember = memberRepository.findById(savedId);
assertThat(foundMember.isPresent()).isTrue();
assertThat(foundMember.get().getName()).isEqualTo("spring");
}
@Test
public void 중복_회원_제외() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// join
memberService.join(member1);
// then
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
memberService.join(member2);
});
assertThat(exception.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
@Test
public void 전체회원조회() {
// given
Member member1 = new Member();
member1.setName("spring1");
Member member2 = new Member();
member2.setName("spring2");
memberService.join(member1);
memberService.join(member2);
// when
List<Member> members = memberService.findMembers();
// then
assertThat(members.size()).isEqualTo(2);
assertThat(members).contains(member1, member2);
}
}
3️⃣ 모르는 코드 설명.
"@BeforeEach" 애노테이션.
JUnit 5에서 테스트 메서드가 실행되기 전에 매번 호출되는 메서드에 사용됩니다.
이 애노테이션이 붙은 메서드는 각 테스트 메서드가 실행되기 직전에 실행되므로, 테스트 환경을 초기화하거나 준비 작업을 수행하는 데 유용합니다.
🙋♂️ 이 애노테이션의 주요 역할은 다음과 같습니다.
테스트 환경 초기화
각 테스트 메서드가 실행될 때마다 동일한 초기 상태를 보장하기 위해 사용됩니다.
예를 들어, 테스트할 객체를 새로 생성하거나, 필요한 데이터를 설정하는 등의 작업을 수행합니다.
반복 작업 처리
여러 테스트에서 반복적으로 수행해야 하는 설정 작업이 있을 때, "@BeforeEach" 를 사용하여 중복 코드를 줄일 수 있습니다.
독립적인 테스트 보장
테스트 간의 상호 의존성을 없애고, 각 테스트가 독립적으로 실행되도록 보장할 수 있습니다.
이를 통해 테스트 간에 상태가 공유되지 않도록 하여 신뢰성 있는 테스트를 구현할 수 있습니다.
"@BeforeEach" 는 테스트 환경을 일관되게 유지하고,
"@AfterEach" 애노테이션.
JUnit 5에서 각 테스트 매서드가 실행된 후에 실행되는 메서드를 붙이는 애노테이션 입니다.
이 메서드는 테스트가 완료된 후에 정리(clean-up) 작업을 수행하는 데 사용됩니다.
🙋♂️ 이 애노테이션의 주요 역할은 다음과 같습니다.
자원 정리
테스트 중에 사용된 자원(예: 파일, 데이터베이스 연결, 네트워크 연결 등)을 해제하거나 정리하는 데 사용됩니다.
이는 메모리 누수나 리소스 잠금을 방지할 수 있습니다.
테스트 환경 복원
테스트 실행 중에 변경된 상태나 데이터를 초기 상태로 되돌려, 다른 테스트에 영향을 미치지 않도록 합니다.
이는 테스트 간의 독립성을 유지하는 데 중요한 역할을 합니다.
로그 남기기
테스트가 끝난 후 테스트 결과나 상태에 대한 로그를 기록할 수 있습니다.
이를 통해 테스트 결과를 모니터링하거나 디버깅할 때 유용할 수 있습니다.
"@AfterEach" 는 테스트 후에 정리 작업을 자동으로 수행하여, 코드의 안정성과 유지보수성을 높이는 데 중요한 역할을 합니다.
Reflection
Reflection 은 자바에서 런타임 시에 클래스, 인터페이스, 메서드, 필드 등의 정보를 동적으로 조사하고, 조작할 수 있는 기능을 제공합니다.
Reflection 을 사용하면 코드에서 특정 객체의 클래스 타입이나 메서드, 필드 등에 접근하고, 해당 요소들을 동적으로 호출하거나 값을 변경하는 것이 가능합니다.
🙋♂️ Reflection의 주요 개념과 역할은 다음과 같습니다.
클래스 정보 조사
Reflection을 사용하면 특정 객체의 클래스 타입을 런타임에 알아낼 수 있습니다.
예를 들어, Class<?> clazz = obj.getClass(); 를 사용하여 객체 obj 의 클래스 정보를 가져올 수 있습니다.
필드, 메서드, 생성자 접근
Reflection을 통해 클래스에 선언된 필드, 메서드 ,생성자에 접근할 수 있습니다.
이를 통해 특정 필드의 값을 가져오거나 설정하고, 메서드를 호출하거나 생성자를 통해 객체를 생성할 수 있습니다.
예를 들어, Field field = clazz.getDeclaredField("fieldName);" 를 사용하여 특정 필드에 접근할 수 있습니다.
접근 제어 무시
Reflection을 사용하면 private 으로 선언된 필드나 메서드에도 접근할 수 있습니다. setAccessible(true) 메서드를 사용하여 접근 제어자를 무시할 수 있습니다.
이는 보통 테스트나 프레임워크에서 사용되며 예를 들어, 프레임워크에서 자동으로 의존성을 주입하거나, 테스트에서 private 필드에 접근할 때 유용합니다.
런타임에 동적 객체 생성 및 메서드 호출
Reflection을 사용하여 런타임에 동적으로 객체를 생성하거나 메서드를 호출할 수 있습니다.
이는 매우 유연한 코드 작성을 가능하게 하지만, 일반적으로 성능 저하가 있을 수 있습니다.
애노테이션 처리
Reflection을 사용하여 클래스나 메서드에 선언된 애노테이션을 런타임에 읽어들이고 처리할 수 있습니다.
이는 주로 프레임워크에서 사용되며, 예를 들어, 스프링 프레임워크에서 애노테이션 기반으로 설정을 처리하는 경우가 있습니다.
🙋♂️ Reflection의 장단점은 다음과 같습니다.
장점
동적 기능 제공 : 코드의 유연성과 확장성을 높여줍니다. 런타임에 클래스나 메서드를 동적으로 호출할 수 있어, 컴파일 타임에 알 수 없는 구조를 처리할 수 있습니다.
프레임워크에서 유용 : 많은 자바 프레임워크, 예를 들어 스프링(Spring), 하이버네이트(Hibernate) 등은 Reflection을 활용하여 애플리케이션의 동작을 제어합니다.
단점
성능 이슈 : Reflection은 일반적인 메서드 호출에 비해 성능이 떨어질 수 있습니다. 따라서 중요한 애플리케이션에서는 중의가 필요합니다.
안전성 문제 : Reflection을 사용하면 컴파일 타임에 확인할 수 없는 동작이 많아, 잘못 사용하면 런타임 에러가 발생할 수 있습니다.
보안 이슈 : Reflection은 접근 제어를 무시할 수 있으므로, 잘못된 사용은 보안상의 취약점을 초래할 수 있습니다.
4️⃣ 코드 설명.
Reflection을 사용하여 memberRepository 설정.
MemberService 의 memberRepository 필드는 private final 로 선언되어 있으므로, 일반적인 방식으로 접근할 수 없습니다.
이 문제를 해결하기 위해 Reflection 을 사용하여 필드에 접근하고, 테스트용 MemoryMemberRepository 를 설정합니다.
@BeforeEach 애노테이션.
각 테스트가 실행되기 전에 MemberService 인스턴스를 생성하고. Reflection 을 사용하여 memberRepository 필드를 MemoryMemberRepository 인스턴스로 초기화합니다.
@AfterEach 애노테이션.
각 테스트가 완료된 후, MemoryMemberRepository 의 저장소를 초기화하여 테스트 간의 상태 간섭을 방지합니다.
테스트 메서드.
회원가입 : 새로운 회원을 가입시키고, 저장된 회원이 올바르게 반환되는지 검증합니다.
중복_회원_제외 : 중복된 이름으로 회원을 가입하려 할 때 IllegalStateException 이 발생하는지를 확인합니다.
전체회원조회 : 저장된 모든 회원이 올바르게 반환되는지 검증합니다.
이 방법을 통해 MemberService 를 수정하지 않고도 해당 클래스의 동작을 테스트할 수 있습니다.
다만, 실제 코드에서는 생성자를 통한 주입이나 다른 테스트 가능한 구조로 변경하는 것이 더 바람직합니다.
-
☕️[Java] 테스트 코드와 Dependancy Injection
☕️[Java] 테스트 코드와 Dependancy Injection
1️⃣ 전체 코드.
// Member
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// MemberRepository - Interface
import com.devkobe.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// MemoryMemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
// MemberService
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/*
* 회원 가입
*/
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
// MemberServiceTest
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
void findMembers() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// then
}
@Test
void findOne() {
}
}
2️⃣ 의존성 주입(DI, Dependency Injection).
의존성 주입(DI, Dependency Injection) 는 객체 지향 프로그래밍에서 객체 간의 의존성을 외부에서 주입하는 디자인 패턴입니다.
1️⃣ 의존성 주입(DI, Dependency Injection)의 개념.
의존성(Dependency)
클래스가 다른 클래스의 기능을 사용해야 하는 상황을 의미합니다.
예를 들어, MemberService 클래스는 회원 데이터를 처리하기 위해 MemberRepository 를 필요로 합니다. 여기서 MemberService 는 MemberRepository 에 의존합니다.
주입(Injection)
외부에서 객체를 생성하여 주입해주는 것을 의미합니다.
이는 보통 생성자 주입, 세터(Setter) 주입, 필드 주입 등의 방식으로 이루어집니다.
2️⃣ DI의 주요 목적.
느슨한 결합(Loose Coupling)
DI를 통해 클래스 간의 결합도를 낮출 수 있습니다.
클래스는 자신이 사용하는 의존 객체가 무엇인지 알 필요가 없고, 이로 인해 클래스 간의 결합이 느슨해집니다.
이는 코드의 유연성을 높여주며, 코드 변셩 시 영향 범위를 줄여줍니다.
유연성 및 확장성
의존 객체를 외부에서 주입받기 때문에, 애플리케이션이 다른 의존 객체로 쉽게 전환될 수 있습니다.
예를 들어, 테스트 환경에서 MemoryMemberRepository 를 사용하고, 실제 운영 환경에서는 데이터베이스 연결을 사용하는 JpaMemberRepository 를 사용할 수 있습니다.
테스트 용이성
DI를 통해 클래스 간의 의존 관계를 외부에서 주입받으면, 테스트 시에 쉽게 Mock 객체나 Stub 객체를 주입할 수 있어 테스트를 더 용이하게 만듭니다.
3️⃣ DI의 사용 방법.
1. 생성자 주입(Constructor Injection) : 의존성을 클래스의 생성자를 통해 주입하는 방법입니다. 위 코드에서는 생성자 주입이 사용되었습니다.
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
2. 세터 주입(Setter Injection) : 의존성을 세터 메서드를 통해 주입하는 방법입니다.
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
3. 필드 주입(Field Injection) : 의존성을 필드에 직접 주입하는 방법으로, 보통 @Autowired 와 같은 애노테이션을 사용합니다. 하지만 필드 주입은 테스트하기 어려울 수 있어 일반적으로 권장되지 않습니다.
DI는 코드의 재사용성, 유지보수성, 테스트 용이성을 높여주는 중요한 디자인 패턴으로, 스프링 프레임워크와 같은 의존성 주입 컨테이너에서 널리 사용됩니다.
3️⃣ 코드에서 의존성 주입(DI, Dependency Injection)이 사용된 부분과 설명.
위 코드에서 DI(Dependency Injection)가 사용된 부분은 MemberService 클래스의 인스턴스를 생성하는 부분입니다.
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
이 부분에서 MemberService 클래스의 생성자에 MemoryMemberRepository 객체를 주입하고 있습니다.
MemberService 클래스는 생성자에서 MemberRepository 인터페이스를 받아 사용합니다.
아렇게 함으로써, MemberService 는 직접적으로 MemoryMemberRepository 를 생성하지 않고 외부에서 주입받게 됩니다.
1️⃣ 설명.
memberRepository = new MemoryMemberRepository();
MemoryMemberRepository 객체를 생성합니다. 이는 MemberRepository 인터페이스의 구현체입니다.
memberService = new MemberService(memberRepository);
MemberService 객체를 생성 할 때, 생성자에 memberRepository 를 주입합니다.
이 방식은 MemberService 가 MemoryMemberRepository 에 강하게 결합되지 않도록 하여, 다른 구현체로 쉽게 변경할 수 있게 만듭니다.
예를 들어, 테스트 환경에서는 MemoryMemberRepository 를 사용하고, 실제 서비스에서는 데이터베이스와 연결된 구현체를 사용할 수 있습니다.
-
-
☕️[Java] 공유 참조와 사이드 이펙트
☕️[Java] 공유 참조와 사이드 이펙트
1️⃣ 공유 참조와 사이드 이펙트.
사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말합니다.
앞서 "b" 의 값을 부산으로 변경한 코드를 다시 분석해봅시다.
b.setValue("부산"); // b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
개발자는 "b" 의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했습니다.
하지만 "a" , "b" 는 같은 인스턴스를 참조합니다.
따라서 "a" 의 값도 함께 부산으로 변경되어 버립니다.
이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트(Side Effect)라 합니다.
프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그래밍의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생합니다.
이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있습니다.
2️⃣ 사이드 이펙트 해결 방안
생각해보면 문제의 해결방안은 아주 단순합니다.
다음과 같이 "a" 와 "b" 가 처음부터 서로 다른 인스턴스를 참조하면 됩니다.
Address a = new Address("서울");
Address b = new Address("서울");
public class RefMain1_2 {
public static void main(String[] args) {
Address a = new Address("서울"); // x001
Address b = new Address("서울"); // x002
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 = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 "b" 의 주소값만 부산으로 변경된 것을 확인할 수 있습니다.
그림 - 생성 코드
"a" 와 "b" 는 서로 다른 "Address" 인스턴스를 참조합니다.
그림 - 변경 코드
"a" 와 "b" 는 서로 다른 인스턴스를 참조합니다.
따라서 "b" 가 참조하는 인스턴스의 값을 변경해도 "a" 에는 영향을 주지 않습니다.
3️⃣ 여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다.
지금까지 발생한 모든 문제는 같은 객체(인스턴스)를 변수 "a" , "b" 가 함께 공유하기 때문에 발생했습니다.
따라서 객체를 공유하지 않으면 문제가 해결됩니다.
여기서 변수 "a", "b" 가 각각 다른 주소지로 변경할 수 있어야 합니다.
이렇게 하려면 서로 다른 객체를 참조하면 됩니다.
객체를 공유.
Address a = new Address("서울");
Address b = a;
이 경우 "a", "b" 둘 다 같은 "Address" 인스턴스를 바라보기 때문에 한 쪽의 주소만 부산으로 변경하는 것이 불가능합니다.
객체를 공유하지 않음.
Address a = new Address("서울");
Address ㅠ = 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; // 기존 객체 공유 참조
참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 됩니다.
즉, 객체의 공유를 막을 수 있는 방법이 없습니다!
기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않습니다.
하지만 참조형의 경우 참조값을 복사해서 대입하기 때문에 여러 변수에서 얼마든지 같은 객체를 공유할 수 있습니다.
객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있습니다.
물론 개발자가 신경써서 코드를 작성한다면 사이드 이펙트 문제를 일으키지 않을 수 있습니다.
하지만 실제로는 훨씬 더 복잡한 상황에서 이런 문제가 발생합니다.
```java
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='부산'}
-
☕️[Java] 불변 객체 - 도입
☕️[Java] 불변 객체 - 도입.
1️⃣ 불변 객체 - 도입.
공유하면 안되는 객체를 여러 변수에서 공유하기 때문에 문제가 발생했었습니다.
그렇다고 객체의 공유를 막을 수 있는 방법은 없습니다.
그러나 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아닙니다.
객체를 공유한다고 바로 사이드 이펙트가 발생하지 않습니다.
문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있습니다.
"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" 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것입니다.
2️⃣ 불변 객체 도입.
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 합니다.
"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()" 를 제거했습니다.
이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능합니다.
불변 클래스를 만드는 방법은 아주 단순합니다.
어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 됩니다.
```java
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" 가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면 새로운 인스턴스를 생성해서 할당해야 합니다.
3️⃣ 정리
불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있습니다.
객체의 공유 참조는 막을 수 없습니다.
그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생합니다.
사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 됩니다.
불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단됩니다.
불변 객체는 값을 변경할 수 없습니다.
따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 합니다.
이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않습니다.
🙋♂️ 참고 - 가변(Mutable) 객체 vs 불변(Immutable) 객체
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻입니다.
가변은 사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻입니다.
불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻입니다.
불변은 사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻입니다.
"Address" 는 가변 클래스입니다.
이 클래스로 객체를 생성하면 가변 객체가 됩니다.
"ImmutableAddress" 는 불변 클래스입니다.
이 클래스로 객체를 생성하면 불변 객체가 됩니다.
-
-
-
-
☕️[Java] Object와 OCP - 2
☕️[Java] Object와 OCP - 2.
1️⃣ Object.
Java에서 'Object' 는 모든 클래스의 최상위 부모 클래스이자, Java 클래스 계층 구조의 최상위에 위치하는 클래스입니다.
Java에서 모든 클래스는 암묵적으로 'Object' 클래스를 상속받으며, 이로 인해 'Object' 클래스가 제공하는 메서드를 사용할 수 있습니다.
이는 Java의 객체 지향 프로그래밍(OOP)에서 중요한 역할을 합니다.
1️⃣ Object 클래스의 주요 역할.
1. 최상위 클래스.
모든 Java 클래스는 'Objcet' 클래스를 상속받기 때문에 'Object' 클래스에서 제공하는 메서드는 모든 객체에서 사용할 수 있습니다.
2. 기본 메서드 제공.
'Object' 클래스는 모든 객체가 기본적으로 사용할 수 있는 몇 가지 중요한 메서드를 제공합니다.
예를 들어 다음과 같습니다.
'equals(Object obj)'
두 객체가 같은지를 비교합니다.
'hashCode()'
객체의 해시 코드를 반환합니다. 이 값은 객체를 식별하는 데 사용됩니다.
'toString()'
객체를 문자열로 표현합니다.
'clone()'
객체를 복제합니다.(단, 클래스에서 'Cloneable' 인터페이스를 구현해야 사용 가능)
'finalize()'
객체가 가비지 컬렉션되기 전에 호출됩니다.
3. 다형성 지원.
'Object' 타입으로 모든 객체를 참조할 수 있으므로, 다양한 객체를 처리하는 메서드나 컬렉션에서 유연성을 제공합니다.
예를 들어, Java의 컬렉션 프레임워크에서는 'Object' 타입을 사용하여 다양한 타입의 객체를 저장할 수 있습니다.
4. 공통 기능의 확장.
모든 클래스가 'Object' 를 상속받기 때문에, 'Object' 클래스의 메서드를 재정의(Override)하여 클래스에 맞는 동작을 구현할 수 있습니다.
예를 들어, 'toString()' 메서드를 재정의하여 객체의 상태를 의미 있는 문자열로 표현할 수 있습니다.
2️⃣ 예시.
다음은 'Object' 클래스의 메서드를 활용하는 간단한 예시입니다.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
public static void main(String[] args) {
Person person1 = new Person("Alice", 30);
Person person2 = new Person("Alice", 30);
System.out.println(person1.equals(person2)); // true
System.out.println(person1.toString()); // Person{name='Alice', age=30}
}
}
이 예시에서 'equals' 와 'toString' 메서드는 'Object' 클래스에서 제공하는 메서드를 재정의하여 'Person' 클래스의 객체에 적합한 동작을 정의하고 있습니다.
3️⃣ 결론.
Java의 'Object' 클래스는 모든 클래스의 공통 조상으로서 중요한 역할을 하며, 기본적인 객체 비교, 해시 코드 생성, 문자열 표현 등의 기능을 제공합니다.
이는 Java에서의 객체 지향 프로그래밍의 기초를 이루며, 다양한 클래스 간의 상호작용을 가능하게 합니다.
2️⃣ OCP
OCP는 “Open/Closed Principle”의 약자로, 객체 지향 설계의 중요한 원칙 중 하나입니다.
이 원칙은 Robert C. Martin에 의해 정의된 SOLID 원칙 중 하나로, 소프트웨어 설계의 유연성과 유지보수성을 높이기 위한 지침을 제공합니다.
1️⃣ OCP(Open/Closed Principle)의 정의
“소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 열려 있어야 하고, 수정에는 닫혀 있어야 한다.” 라는 원칙입니다.
확장에 열려 있어야 한다(Open for extension)
새로운 기능이나 요구사항이 추가될 때, 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다는 뜻입니다.
이를 통해 소프트웨어를 유연하게 확장할 수 있으며, 새로운 기능을 도입할 때 기존 코드에 영향을 주지 않게 됩니다.
수정에 닫혀 있어야 한다(Closed for modification)
기존에 잘 작동하던 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다는 뜻입니다.
이는 소프트웨어의 안정성을 유지하면서 변경의 영향을 최소화할 수 있습니다.
2️⃣ OCP의 구현 방법.
OCP를 구현하는 가장 일반적인 방법은 추상화 와 다형성 을 사용하는 것입니다.
추상 클래스나 인터페이스를 통해 기본 구조를 정의하고, 이를 상속하거나 구현하여 구체적인 기능을 확장합니다.
이렇게 하면 기존 코드베이스를 변경하지 않고도 새로운 기능을 추가할 수 있습니다.
3️⃣ 예시.
// 기존 코드: Shape 인터페이스 정의
interface Shape {
double calculateArea();
}
// 기존 코드: Rectangle 클래스 정의
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
// 확장 코드: Circle 클래스 정의 (기존 코드를 수정하지 않음)
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// 클라이언트 코드
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle(10, 20);
Shape circle = new Circle(5);
System.out.println("Rectangle Area: " + rectangle.calculateArea());
System.out.println("Circle Area: " + circle.calculateArea());
}
}
설명
이 예시에서 'Shape' 인터페이스는 면적을 계산하는 'calculateArea()' 메서드를 정의합니다.
'Rectangle' 클래스는 이 인터페이스를 구현하여 사각형의 면적을 계산하는 기능을 제공합니다.
이후, 'Circle' 클래스를 추가하면서 새로운 기능을 확장합니다.
이 과정에서 기존의 'Rectangle' 클래스나 'Shape' 인터페이스의 코드는 전혀 수정하지 않고 새로운 기능을 추가할 수 있었습니다.
이처럼 OCP를 잘 준수하면 소프트웨어가 변화하는 요구사항에 유연하게 대응할 수 있으며, 유지보수 비용을 줄이고 코드의 재사용성을 높일 수 있습니다.
3️⃣ Object와 OCP의 관계.
'Object' 와 'OCP(Open/Closed Principle)' 는 둘 다 객체 지향 프로그래밍의 개념과 밀접하게 관련되어 있지만, 그 역할과 목적은 다릅니다.
이 둘의 관계ㄴ를 이해하려면 먼저 각각의 역할을 간단히 요약하고, 그 후에 이들이 어떻게 상호작용하는지 설명할 수 있습니다.
1️⃣ ‘Object’ 클래스의 역할
Java에서 'Object' 클래스는 모든 클래스의 최상위 부모 클래스입니다.
모든 Java 클래스는 암묵적으로 'Object' 를 상속받으며, 'Object' 클래스에서 제공하는 메서드를 사용할 수 있습니다.
이 클래스는 Java에서 객체의 기본적인 기능(예: 'equalse', 'hashCode', 'toString')을 제공합니다.
'Object' 클래스 자체는 특정한 설계 원칙을 강제하지 않지만, 객체 지향 프로그래밍의 근본적인 기초를 제공합니다.
2️⃣ OCP(Open/Closed Principle)의 역할.
OCP는 소프트웨어 설계 원칙으로, 소프트웨어가 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다 는 원칙을 따릅니다.
이 원틱은 소프트웨어를 설계할 때 변화하는 요구사항에 유연하게 대응하고, 코드의 수정 없이도 기능을 확장할 수 있도록 구조화하는 방법론입니다.
이는 주로 인터페이스, 추상 클래스, 상속, 다형성 등을 통해 구현됩니다.
3️⃣ ‘Object’와 OCP의 관계.
1. 기본 클래스 구조 제공.
‘Object’ 클래스는 모든 클래스의 기본이 되며, OCP 원칙을 적용하기 위한 기본 구조를 제공합니다.
예를 들어, 모든 클래스가 'Object' 를 상속받기 때문에, 클래스 설계자는 'Object' 클래스에서 제공하는 메서드(예: 'equals', 'hashCode') 를 재정의할 수 있습니다.
이를 통해 각 클래스가 자신만의 독특한 행동을 가지도록 확장할 수 있습니다.
2. 추상화와 다형성의 기초.
OCP를 실현하기 위해서는 추상화와 다형성이 중요한데, 'Object' 클래스는 이 둘의 기초를 제공합니다.
모든 클래스는 'Object' 타입으로 참조될 수 있으므로, OCP를 구현할 때 다형성을 활용할 수 있습니다.
예를 들어, 다양한 타입의 객체를 'Object' 로 처리하면서도, 각 객체가 특정 행위를 다르게 구현하도록 설계할 수 있습니다.
3, OCP 준수에 대한 도움.
'Object' 클래스는 모든 클래스가 공통적으로 가져야 하는 기본적인 기능을 제공하기 때문에, 설계자는 이 기능을 바탕으로 필요한 부분만 재정의하여 기능을 확장할 수 있습니다.
이는 OCP를 준수하는 데 도움이 됩니다.
예를 들어, 특정 객체가 'equals' 메서드를 새롭게 구현함으로써 비교 방식을 확장하면서도, 기존의 'Object' 클래스 코드를 수정할 필요는 없습니다.
4️⃣ 결론.
'Object' 클래스는 모든 클래스의 기반이 되며, OCP를 준수하는 소프트웨어 설계에 있어 중요한 역할을 합니다.
Object 는 직접적으로 OCP를 구현하지 않지만, 그 위에서 개발자들이 OCP 원칙을 적용할 수 있도록 추상화와 다형성의 기초를 제공합니다.
OCP를 준수하는 설계를 통해 개발자는 객체 지향 프로그래밍의 이점을 극대화할 수 있습니다.
-
☕️[Java] Object와 OCP - 1
☕️[Java] Object와 OCP - 1.
1️⃣ 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);
}
}
1️⃣ 구체적인 것에 의존.
'BadObjectPrinter' 는 구체적인 타입인 'Car' , 'Dog' 를 사용합니다.
따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 됩니다.
이렇게 'BadObjectPrinter' 클래스가 구체적인 특정 클래스인 'Car' , 'Dog' 를 사용하는 것을 'BadObjectPrinter' 는 'Car' , 'Dog' 에 의존한다고 표현합니다.
자바에는 객체의 정보를 사용할 때, 다형적 참조 문제를 해결해줄 'Object' 클래스와 메서드 오버라이딩 문제를 해결해줄 'Object.toString()' 메서드가 있습니다.
(물론 직접 'Object' 와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있습니다.)
2️⃣ 추상적인 것에 의존.
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
위의 'ObjectPrinter' 클래스는 'Car' , 'Dog' 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 'Object' 클래스를 사용합니다.
이렇게 'ObjectPrinter' 클래스가 'Object' 클래스를 사용하는 것을 'Object' 클래스에 의존한다고 표현합니다.
'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()' 을 호출할 수 있습니다.
3️⃣ OCP 원칙.
Open : 새로운 클래스를 추가하고, 'toString()' 을 오버라이딩해서 기능을 확장할 수 있습니다.
Closed : 새로운 클래스를 추가해도 'Object' 와 'toString()' 을 사용하는 클라이언트 코드인 'ObjectPrinter' 는 변경하지 않아도 됩니다.
다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 'Car', 'Dog' 에 의존하는 것이 아니라 추상적인 'Object' 에 의존하면서 OCP 원칙을 지킬 수 있었습니다.
덕분에 새로운 클래스를 추가하고 toString() 메서드를 새롭게 오버라이딩해서 기능을 확장할 수 있습니다.
그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter 는 변경할 필요가 없습니다.
'ObjectPrinter' 는 모든 타입의 부모인 'Object' 를 사용하고, 'Object' 가 제공하는 'toString()' 메서드만 사용합니다.
따라서 'ObjectPrinter' 를 사용하면 세상의 모든 객체의 정보('toString()') 를 편리하게 출력할 수 있습니다.
2️⃣ 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' 에 의존합니다.
-
☕️[Java] toString()
☕️[Java] toString()
1️⃣ toString()
'Object.toString()' 메서드는 객체의 정보를 문자열 형태로 제공합니다.
그래서 디버깅과 로깅에 유용하게 사용됩니다.
이 메서드는 'Object' 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있습니다.
package langReview.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@b4c966a
java.lang.Object@b4c966a
1️⃣ Object.toString()
'Object' 가 제공하는 'toString()' 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공합니다.
2️⃣ println()과 toString()
'toString()' 의 결과를 출력한 코드와 'object' 를 'println()' 에 직접 출력한 코드의 결과가 완전히 같습니다.
Object object = new Object();
String string = object.toString();
// toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
'System.out.println' 메서드는 사실 내부에서 'toString()' 을 호출합니다.
'Object' 타입(자식 포함)이 'println()' 에 인수로 전달되면 내부에서 'obj.toString()' 메서드를 호출해서 결과를 출력합니다.
따라서 'println()' 을 사용할 때, 'toString()' 을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있습니다.
2️⃣ toString() 오버라이딩.
'Object.toString()' 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못합니다.
그래서 보통 'toString()' 을 재정의(Overriding, 오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적입니다.
package langReview.object.tostring;
public class Car {
private String carName;
public Car(String carName) {
this.carName = carName;
}
}
package langReview.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 langReview.object.tostring;
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
package langReview.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("=================================");
System.out.println("2. println 내부에서 toString 호출");
System.out.println(car);
System.out.println(dog1);
System.out.println(dog2);
System.out.println("=================================");
System.out.println("3. Object 다형성 활용");
ObjectPrinter.print(car);
ObjectPrinter.print(dog1);
ObjectPrinter.print(dog2);
}
}
실행 결과
1. 단순 toString 호출
langReview.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
=================================
2. println 내부에서 toString 호출
langReview.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
=================================
3. Object 다형성 활용
객체 정보 출력: langReview.object.tostring.Car@4e50df2e
객체 정보 출력: Dog{dogName='멍멍이1', age=2}
객체 정보 출력: Dog{dogName='멍멍이2', age=5}
'Car' 인스턴스는 'toString()' 을 재정의 하지 않았습니다.
따라서 'Object' 가 제공하는 기본 'toString()' 메서드를 사용합니다.
'Dog' 인스턴스는 'toString()' 을 재정의 한 덕분에 객체의 상태를 명확하게 확인할 수 있습니다.
1️⃣ ObjectPrinter.print(Object obj) 분석 - Car 인스턴스
ObjectPrinter.print(car);
void print(Object obj = car(Car)) { // 인수 전달
String string = "객체 정보 출력: " + obj.toString();
}
'Object obj' 의 인수로 'car(Car)' 가 전달됩니다.
메서드 내부에서 'obj.toString()' 을 호출합니다.
'obj' 는 'Object' 타입입니다.
따라서 'Object' 에 있는 'toString()' 을 찾습니다.
이때 자식에 재정의(Overriding, 오버라이딩)된 메서드가 있는지 찾아봅니다.
재정의된 메서드가 없을 경우에는 'Object.toString()' 을 실행합니다.
2️⃣ ObjectPrinter.print(Object obj) 분석 - Dog 인스턴스
ObjectPrinter.print(dog); // main에서 호출
void print(Object obj = car(Car)) { // 인수 전달
String string = "객체 정보 출력: " + obj.toString();
}
'Object obj' 의 인수로 'dog(Dog)' 가 전달됩니다.
메서드 내부에서 'obj.toString()' 을 호출합니다.
'obj' 는 'Object' 타입입니다.
따라서 'Object' 에 있는 'toString()' 을 찾습니다.
이때 자식에 재정의(Overriding, 오버라이딩)된 메서드가 있는지 찾아봅니다.
'Dog' 에 재정의된 메서드가 있습니다.
'Dog.toString()' 을 실행합니다.
🙋♂️ 참고 - 객체의 참조값 직접 출력
'toString()' 은 기본으로 객체의 참조값을 출력합니다.
그런데 'toString()' 이나 'hashCode()' 를 재정의하면 객체의 참조값을 출력할 수 없습니다.
이때는 다음 코드를 사용하면 객체의 참조값을 출력할 수 있습니다.
String refValue = Integer.toHexString(System.identityHashCode(dog1));
System.out.println("refValue = " + refValue);
실행 결과
refValue = 30dae81
-
-
-
-
-
-
☕️[Java] @RequiredArgsConstructor의 역할.
☕️[Java] @RequiredArgsConstructor 역할.
RequiredArgsConstructor 어노테이션은 Lombok 라이브러리에서 제공하는 기능 중 하나로, 클래스에 필수적인 생성자를 자동으로 생성하는 역할을 합니다.
이 어노테이션을 클래스에 적용하면, Lombok 이 그 클래스의 final 필드 또는 @NonNull 어노테이션이 붙은 필드를 인자로 받는 생성자를 자동으로 생성합니다.
1️⃣ @RequiredArgsConstructor의 주요 기능.
1. 자동 생성자 생성
클래스 내의 모든 final 필드와 @NonNull 어노테이션이 붙은 필드에 대한 생성자를 자동으로 생성합니다.
이 생성자는 이 필드들을 초기화하는 데 필요한 파라미터를 요구합니다.
2. 코드 간결화
수동으로 생성자를 작성하는 번거로움을 줄여줍니다.
특히 많은 필드를 가진 클래스에서 유용하게 사용될 수 있습니다.
3. 불변성 강화
final 필드를 사용함으로써 클래스의 불변성을 강화할 수 있습니다.
생성자를 통해 한 번 설정되면, 이 필드들의 값은 변경될 수 없습니다.
4. 의존성 주입 용이
Spring과 같은 프레임워크에서 생성자를 통한 의존성 주입을 사용할 때 유용합니다.
필요한 의존성을 생성자를 통해 주입받기 때문에, 컴포넌트 간의 결합도를 낮출 수 있습니다.
2️⃣ 사용 예시.
다음은 @RequiredArgsConstructor 어노테이션을 사용한 간단한 클래스 예제입니다.
import lombok.RequiredArgsConstructor;
import lombok.NonNull;
@RequiredArgsConstructor
public class UserData {
private final String username; // final 필드에 대한 생성자 파라미터 자동 포함.
@NonNull private String email; // @NonNull 필드에 대한 생성자 파라미터 자동 포함.
// 추가 메소드 등
}
위 코드에서 UserData 클래스에는 username 과 email 두 필드가 있으며, username 은 final 로 선언되어 수정할 수 없고, email 은 @NonNull 어노테이션이 붙어 null 값을 허용하지 않습니다.
Lombok은 이 두 필드를 초기화하는 생성자를 자동으로 생성합니다.
3️⃣ 주의 사항.
@RequiredArgsConstructor 는 필드가 많고, 특히 final 또는 @NonNull 필드가 있는 경우 유용합니다.
그러나 생성자를 통한 초기화가 필요하지 않은 필드에는 적용되지 않습니다.
Lombok을 사용하면 코드가 간결해지고 가독성이 향상되지만, 코드의 명시성이 다소 떨어질 수 있습니다.
따라서 Lombok 사용 시, 팀 내에서 Lombok에 대한 이해도가 충분한지 확인하는 것이 좋습니다.
Lombok 의 @RequiredArgsConstructor 는 반복적인 코드 작성을 줄여주고, 오류 가능성을 감소시키며, 더 깔끔하고 관리하기 쉬운 코드베이스를 유지하는 데 도움을 줄 수 있습니다.
-
☕️[Java] @Transactional의 역할과 의미.
☕️[Java] @Transactional의 역할과 의미.
@Transaction 어노테이션은 스프링 프레임워크에서 제공하는 선언적 트랜젝션 관리 기능을 활용하기 위해 사용됩니다.
이 어노테이션을 사용함으로써, 특정 메서드 또는 클래스 전체에 걸쳐 데이터베이스 트랜잭션의 경계를 설정할 수 있습니다.
트랜잭션은 일련의 연산들이 전부 성공적으로 완료되거나, 하나라도 실패할 경우 전체를 취소(롤백)하여 데이터의 일관성과 정합성을 보장하는 것을 목적으로 합니다.
1️⃣ @Transactional의 주요 기능과 특징.
1. 자동 롤백
@Transactional 이 적용된 메서드에서 런타임 예외(RuntimeException)가 발생하면, 그 트랜잭션에서 수행된 모든 변경이 자동으로 롤백됩니다.
이는 데이터의 일관성을 유지하는 데 필수적입니다.
2. 프로파게이션(Propagation)
트랜잭션의 전파 행위를 제어합니다.
예를 들어, 이미 진행 중인 트랜잭션이 있을 때 새로운 트랜잭션을 시작할 것인지, 아니면 기존 트랜잭션을 참여할 것인지 결정할 수 있습니다.
REQUIRED(기본값) : 이미 진행 중인 트랜잭션이 있다면 그 트랜잭션이 참여하고, 없다면 새로운 트랜잭션을 시작합니다.
REQUIRED_NEW : 항상 새로운 트랜잭션을 시작합니다. 이미 진행 중인 트랜잭션이 있다면 잠시 보류합니다.
3. 격리 수준(Isolation Level)
다른 트랜잭션이 데이터에 동시에 접근했을 때 발생할 수 있는 문제를 제어합니다.
예를 들어, READ_COMMITTEED, REPEATED_READ, SERIALIZABLE 등 다양한 격리 수준을 지정할 수 있습니다.
4. 읽기 전용(Read-Only)
트랜잭션을 읽기 전용으로 설정할 수 있어, 데이터 수정이 이루어지지 않는다는 것을 데이터베이스 최적화 엔진에 알려 성능을 향상시킬 수 있습니다.
5. 롤백 규칙(Rollback Rules)
특정 예외가 발생했을 때 롤백을 수행할지 아니면 커밋을 수행할지를 세밀하게 제어할 수 있습니다.
기본적으로 런타임 예외에서는 롤백을 수행하고, 체크 예외에서는 커밋을 수행합니다.
2️⃣ 사용 예제.
import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;
@Service
public class TransactionalService {
@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepository.findById(id);
}
@Transactional(rollbackFor = Exception.class)
public User updateUser(User user) {
return userRepository.save(user);
}
}
위 예시처럼, getUser 메서드는 데이터를 변경하지 않고 조회만 수행하기 때문에 readOnly = true 로 설정했습니다.
반면, updateUser 메서드는 데이터를 변경할 가능성이 있으므로, 모든 예외(Exception)가 발생할 경우 롤백하도록 설정했습니다.
@Transactional 을 사용함으로써 개발자는 복잡한 트랜잭션 관리 코드를 직접 작성하지 않고도, 스프링 프레임워크가 제공하는 선언적 방식을 통해 간단하게 트랜잭션을 관리할 수 있게 됩니다.
이는 애플리케이션의 데이터 처리 로직을 더욱 안정적이고 효율적으로 만듭니다.
-
☕️[Java] ObjectMapper 클래스, 직렬화와 역직렬화
☕️[Java] ObjectMapper 클래스, 직렬화와 역직렬화.
ObjectMapper 는 주로 JSON 데이터를 처리하기 위해 사용되는 Jackson 라이브러리의 핵심 클래스입니다.
이 클래스는 자바 객체와 JSON 형식 간의 직렬화(Serialization)와 역직렬화(Deserialization)를 수행합니다.
ObjectMapper 는 JSON 데이터를 자바 객체로 변환하거나 자바 객체를 JSON 데이터로 변환하는 등의 작업을 매우 효율적으로 처리할 수 있게 해줍니다.
1️⃣ 직렬화(Serialization)
ObjectMapper 를 사용하여 자바 객체를 JSON 문자열로 직렬화하는 과정은 다음과 같습니다.
import com.fasterxml.jackson.databind.ObjectMapper;
// 예시 자바 객체
pulbic class User {
public String name;
public int age;
}
// 직렬화 예제
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.name = "Kobe";
user.age = "30";
String json = mapper.writeValueAsString(user); // 자바 객체를 JSON 문자열로 변환
System.out.println(json);
2️⃣ 역직렬화(Deserialization)
ObjectMapper 를 사용하여 JSON 문자열을 자바 객체로 역직렬화하는 과정은 다음과 같습니다.
import com.fasterxml.jackson.databind.ObjectMapper;
// 예시 자바 객체
public class User {
public String name;
public int age;
}
// 역직렬화 예제
ObjectMapper mapper = new ObjectMapper();
String json = "{\"name\":\"Kobe\", \"age\":30}";
User user = mapper.readValue(json, User.class); // JSON 문자열을 자바 객체로 변환
Systeom.out.println(user.name + " is" + user.age + " year old.");
3️⃣ 주요 기능
다양한 데이터 포맷 지원 : ObjectMapper 는 JSON 외에도 XML, CSV 등 여러 데이터 포맷을 지원합니다.(Jackson 데이터 포맷 모듈 설치 필요).
유연성과 설정 : ObjectMapper 는 맞춤 설정이 가능하여, 다양한 JSON 직렬화/역직렬화 방법을 지원합니다.
예를 들어, 필드 이름의 자동 감지, 날짜 형식 지정, 무시할 필드 설정 등을 조정할 수 있습니다.
성능 : Jackson은 JSON 처리를 위해 최적화된 라이브러리 중 하나로, 대용량 데이터 처리에도 뛰어난 성능을 보입니다.
🤔 직렬화와 역직렬화란?
직렬화(Serialization)와 역직렬화(Deserialization)는 데이터 구조 또는 객체 상태를 저장하고 전송하기 위해 다루기 쉬운 데이터 포맷으로 변환하는 과정을 의미합니다.
컴퓨터 과학의 맥락에서 이 개념은 특히 중요하며, 객체 지향 프로그래밍에서 널리 사용됩니다.
1️⃣ 직렬화(Serialization)
직렬화는 객체의 상태(즉, 객체가 가진 데이터와 그 구조)를 일련의 바이트로 변환하는 과정입니다.
이 바이트 스트림은 나중에 파일, 데이터베이스 또는 네트워크를 통해 쉽게 저장하거나 전송할 수 있습니다.
예를 들어, 자바에서는 Serialization 인터페이스를 구현한 객체를 바이트 스트림으로 변환하여 파일 시스템에 저장하거나 네트워크를 통해 다른 시스템으로 보낼 수 있습니다.
2️⃣ 직렬화의 주요 목적.
영속성 : 객체의 상태를 영구적으로 저장하여 나중에 다시 로드할 수 있습니다.
네트워크 전송 : 객체를 네트워크를 통해 다른 시스템으로 전송하기 위해 사용됩니다.
데이터 교환 : 다양한 언어나 플랫폼 간의 데이터 교환이 가능하도록 합니다.
3️⃣ 역직렬화(Deserialization)
역직렬화는 직렬화된 바이트 스트림을 다시 원래의 객체 상태로 복원하는 과정입니다.
즉, 파일, 데이터베이스 또는 네트워크로부터 바이트 스트림을 읽어 들여서 실행 중인 프로그램에서 사용할 수 있는 실제 객체로 변환합니다.
이 과정은 직렬화의 반대 과정으로, 복원된 객체는 원복 객체와 동일한 상태를 가집니다.
4️⃣ 역직렬화의 주요 사용 사례.
객체 복원 : 저장되거나 전송된 데이터로부터 객체를 재구성합니다.
상태 복구 : 애플리케이션의 이전 상태를 복구하는 데 사용됩니다.
데이터 접근 : 다른 시스템에서 전송된 데이터를 로컬 시스템에서 접근하고 사용할 수 있게 합니다.
5️⃣ 데이터 포맷과 직렬화 도구
다양한 데이터 포맷(JSON, XML, YAML 등)과 여러 프로그래밍 언어 또는 라이브러리에서 직렬화와 역직렬화를 지원합니다.
자바에서는 ObjectMapper 를 사용해 JSON 데이터 포맷으로의 직렬화와 역직렬화를 처리하며, 이는 데이터를 쉽게 읽고 쓸 수 있는 구조로 만드는 데 유용합니다.
-
☕️[Java] attribute의 의미와 역할
☕️[Java] attribute의 의미와 역할.
Java 백엔드 개발에서 “attribute”라는 용어는 몇 가지 다른 맥락에서 사용될 수 있습니다.
주로 두 가지 의미로 사용되는 경우가 많은데, 클래스의 속성 을 의미하는 경우와 웹 개발에서 HTTP 요청이나 세션과 관련된 데이터를 지칭하는 경우입니다.
1️⃣ 클래스의 속성(Field or Property)
Java에서 클래스의 “attribute” 는 해당 클래스의 상태를 정의하는 변수를 말합니다.
이러한 변수들은 객체의 데이터이터를 저장하고, 클래스의 인스턴스들이 갖는 특징과 상태 정보를 나타냅니다.
예를 들어, ‘Person’ 클래스가 있다면, ‘name’, ‘age’ 같은 필드들이 이 클래스의 “attribute” 가 됩니다.
public class Person {
private String name; // Attribute
private int age; // Attribute
// Constructors, getters, setters 등
}
2️⃣ 웹 개발에서의 Attribute
웹 개발에서 “attribute” 는 주로 세션(Session)이나 요청(Request) 객체에 저장된 데이터를 지칭 합니다.
이 데이터는 사용자가 웹 사이트를 이용하는 동안 지속되거나 요청 동안에만 존재할 수 있습니다.
예를 들어, 사용자가 로그인을 하면 그 사용자의 정보를 세션 attribute로 저장하여 다른 페이지에서도 사용자 정보를 유지할 수 있게 합니다.
// 세션에 사용자 정보 저장
request.getSession().setAttribute("user", userObject);
// 세션에서 사용자 정보 가져오기
User user = (User) request.getSession().getAttribute("user");
이 두 가지 사용 사례는 Java 백엔드 개발에서 매우 흔하게 접할 수 있으며, 각각의 맥락에서 attribute가 가지는 의미와 역할을 이해하는 것은 중요합니다.
첫 번째 경우는 객체 지향 프로그래밍의 핵심 요소로 클래스의 속성을 정의합니다.
두 번째 경우에는 웹 애플리케이션의 상태 관리를 돕는 수단으로서 활용됩니다.
-
-
-
📝[Post] 자바다식(Java多識) - 2
자바다식(Java多識) 2편.
1. @AfterEach 어노테이션.
@AfterEach 어노테이션은 JUnit 5에서 제공하는 기능입니다.
각 테스트 메서드가 실행된 후에 수행되어야 하는 작업을 지정하는 데 사용됩니다.
이 어노테이션은 테스트 클래스 내의 메서드에 적용하여 테스트 메서드가 끄탄 후 필요한 정리 작업(cleanup)을 수행할 수 있도록 합니다.
주요 역할
1. 자원 해제 : 테스트 메서드가 사용한 자원(예: 파일, 데이터베이스 연결, 네트워크 연결 등)을 해제하는 데 사용됩니다.
2. 상태 초기화 : 테스트가 완료된 후 상태를 초기화하여 다음 테스트가 깨끗한 환경에서 실행될 수 있도록 합니다.
3. 로그 기록 : 테스트 실행 결과를 로그에 기록하거나 추가적인 분석을 위해 데이터를 저장하는 데 사용할 수 있습니다.
예제 코드
아래는 @AfterEach 어노테이션을 사용한 간단한 예제입니다.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MyTest {
@BeforeEach
void setUp() {
System.out.println("Setting up before each test");
}
@Test
void testMethod1() {
System.out.println("Executing test method 1");
}
@Test
void testMethod2() {
Systemo.out.println("Executing test method 2");
}
@AfterEach
void tearDown() {
System.out.println("Tearing down after each test")
}
}
실행 순서
setUp() : 각 테스트 메서드 실행 전 @BeforeEach 메서드가 호출됩니다.
testMethod1() : 첫 번째 테스트 메서드가 실행됩니다.
tearDown() : 첫 번째 테스트 메서드 실행 후 @AfterEach 메서드가 호출됩니다.
setUp() : 두 번째 테스트 메서드 실행 전 @BeforeEach 메서드가 다시 호출됩니다.
testMethod2() : 두 번째 테스트 메서드가 실행됩니다.
tearDown() : 두 번째 테스트 메서드 실행 후 @AfterEach 메서드가 호출됩니다.
요약
@AfterEach 어노테이션은 각 테스트 메서드 실행 후 호출되는 메서드를 지정합니다.
주로 자원 해제, 상태 초기화, 로그 기록 등의 작업을 수행하는 데 사용됩니다.
각 테스트 메서드마다 실행되므로, 테스트 간의 독립성을 유지하고 깨끗한 테스트 환경을 보장할 수 있습니다.
2. @Builder 어노테이션.
@Builder 는 Lombok 라이브러리에서 제공하는 어노테이션으로, 빌더 패턴을 간편하게 사용할 수 있도록 지원합니다.
빌더 패턴은 객체의 생성과 관련된 복잡성을 줄이고, 가독성을 높이며, 가변 객체를 만들지 않도록 도와줍니다.
특히, 많은 필드를 가진 객체를 생성할 때 유용합니다.
주요 특징 및 역할.
유연한 객체 생성
빌더 패턴을 사용하면 객체를 생성할 때 생성자나 정적 팩토리 메서드보다 더 유연하게 객체를 구성할 수 있습니다.
필요한 필드만 설정할 수 있고, 설정 순서에 구애받지 않습니다.
가독성 향상
많은 필드를 가진 객체를 생성할 때, 빌더 패턴을 사용하면 코드의 가독성이 높아집니다.
각 필드의 이름을 명시적으로 설정할 수 있어 어떤 값이 어떤 필드에 설정되는지 쉽게 할 수 있습니다.
불변 객체 생성
빌더 패턴을 사용하면 불변 객체를 쉽게 생성할 수 있습니다.
객체가 생성된 후에는 필드 값을 변경할 수 없습니다.
사용 예시
Lombok 없이 빌더 패턴 구현
public class User {
private final String name;
private final int age;
private final String email;
private User(UserBuilder builder) {
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
}
public static class UserBuilder {
private String name;
private int age;
private String email;
public UserBuilder setName(String name) {
this.name = name;
return this;
}
public UserBuilder setAge(int age) {
this.age = age;
return this;
}
public UserBuilder setEmail(String email) {
this.email = email;
return this;
}
public User build() {
return new User(this);
}
}
}
Lombok을 사용한 빌더 패턴 구현
Lombok의 @Builder 어노테이션을 사용하면 위의 코드가 크게 단축됩니다.
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class User {
private String name;
private int age;
private String email;
}
객체 생성 예시
위의 Lombok을 사용한 User 클래스를 이용해 객체를 생성하는 예시입니다.
public class Main {
public static void main(String[] args) {
User user = User.builder()
.name("devKobe")
.age(77)
.email(devKobe@gamil.com)
.build();
System.out.println(user.getName()); // devKobe
System.out.println(user.getAge()); // 77
System.out.println(user.getEmail()) // devKobe@gmail.com
}
}
위 예시처럼 Lombok의 @Builder 를 사용하면 빌더 패턴을 간단하게 구현하고 사용할 수 있습니다.
이로 인해 객체 생성 코드가 더 깔끔하고 직관적으로 변합니다.
-
📝[Post] 자바다식(Java多識) - 1
자바다식(Java多識) 1편.
1. ‘mainClassName’ 속성 추가.
메인 클래스의 경로를 지정해주는 속성을 추가하는 방법입니다.
초기 진입점을 지정해준다고 생각하면 됩니다.
application 블록 안에 메인 클래스 이름을 지정합니다. 예를 들어, 메인 클래스가 com.example.Main 이라고 가정합니다.
아래의 코드는 bundle.gradle 파일 내부에서 수정해야 합니다.
plugins {
id 'java'
id 'application'
}
application {
mainClassName = 'com.example.Main' // 여기에 메인 클래스의 경로를 입력합니다.
applicationDefaultJvmArgs = [
"-XX:+EnableDynamicAgentLoading",
"-Djdk.instrument.traceUsage"
]
}
repositories {
mavenCentral()
}
dependencies {
// Your dependencies here
}
메인 클래스 예시
예를 들어, 메인 클래스는 다음과 같이 생겼을 수 있습니다.
package com.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
2. @ExtendWith 어노테이션.
@ExtendWith 어노테이션은 Junit 5에서 제공하는 기능으로, 테스트 클래스나 메서드에 확장 기능을 추가할 수 있도록 해줍니다.
JUnit 5의 확장 모델은 다양한 확장 기능을 통해 테스트 실행의 특정 지점에서 사용자 정의 동작을 수행할 수 있게 합니다.
@ExtendWith 어노테이션의 역할
확장 클래스 지정 : @ExtendWith 어노테이션은 확장 클래스를 지정할 수 있습니다. 지정된 확장 클래스는 테스트 라이프사이클의 특정 지점에서 호출됩니다.
예를 들어, 테스트 실행 전후, 각 테스트 메서드 전후 등 다양한 시점에서 특정 동작을 추가할 수 있습니다.
컨텍스트 설정 및 주입 : 확장 기능을 통해 테스트 컨텍스트를 설정하고, 테스트 메서드에 필요한 객체나 리소스를 주입할 수 있습니다. 이를 통해 테스트 코드를 더 간결하고 모듈화할 수 있습니다.
조건부 실행 : 특정 조건에 따라 테스트 메서드를 실행하거나 건너뛸 수 있도록 지원합니다.
예를 들어, 특정 환경 설정이나 시스템 상태에 따라 테스트 실행 여부를 결정할 수 있습니다.
커스텀 어서션 및 보고 : 확장을 통해 사용자 정의 어서션 로직을 추가하거나 테스트 결과를 커스텀 방식으로 보고할 수 있습니다.
예제 코드
아래는 @ExtendWith 어노테이션을 사용한 간단한 예제입니다.
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.Test;
class MyExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
System.out.println("Before each test method");
}
}
@ExtendWith(MyExtension.class)
public class MyTest {
@Test
void testMethod1() {
System.out.println("Test method 1");
}
@Test
void testMethod2() {
System.out.println("Test method 2");
}
}
위 코드에서 MyExtension 클래스는 BeforeEachCallback 인터페이스를 구현하여 각 테스트 메서드가 실행되기 전에 메시지를 출력합니다.
@ExtendWith(MyExtension.class) 어노테이션을 통해 MyTest 클래스에 이 확장 기능을 추가했습니다.
따라서 각 테스트 메서드 실행 전에 “Before each test method” 메시지가 출력됩니다.
이처럼 @ExtendWith 어노테이션은 JUnit 5의 확장 모델을 활용하여 테스트에 필요한 다양한 기능을 추가할 수 있게 해줍니다.
3. 어서션(Assertion)
어서션(Assertion)은 프로그래밍 및 소프트웨어 테스트에서 코드의 특정 상태나 조건이 참인지 확인하는 데 사용되는 문장이나 명령문을 의미합니다.
어서션을 통해 코드의 논리적 일관성과 정확성을 검증할 수 있으며, 주로 디버깅과 테스트에 사용됩니다.
주요 기능과 목적.
조건 검증 : 어서션(Assertion)은 특정 조건이 참인지 검증합니다. 조건이 거짓이면 프로그램은 즉시 실행을 중단하고 오류를 보고합니다.
디버깅 도구 : 어서션(Assertion)은 개발 중에 코드의 오류를 조기에 발견하고 수정하는 데 도움이 됩니다. 코드의 가정이 잘못된 경우 어서션을 통해 문제를 빨리 찾을 수 있습니다.
문서화 : 어서션(Assertion)은 코드의 논리적 전제 조건을 명시적으로 표현하여, 코드가 어떤 상태에 작동해야 하는지 명확하게 나타냅니다.
어서션(Assertion)의 예
자바(Java)
public void setAge(int age) {
assert age > 0 : "Age must be positive";
this.age = age;
}
JUnit (Java)
```java
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class MyTest {
@Test
void testAddition() {
int result = 2 + 3;
assertEquals(5, result, "2 + 3 should equal 5");
} } ```
어서션 사용 시기
개발 중 : 개발자가 코드의 논리적 일관성을 검증하기 위해 사용합니다. 디버깅 과정에서 주로 사용되며, 프로덕션 환경에서는 보통 비활성화합니다.
테스트 코드 : 테스트 프레임워크(JUnit, TestNG 등)를 사용하여 테스트를 작성할 때, 특정 조건이 기대한 대로 동작하는지 확인합니다.
주의 사항
프로덕션 코드에서의 사용 : 어서션은 주로 개발 및 테스트 환경에서 사용되며, 프로덕션 환경에서는 비활성화되는 경우가 많습니다. 프로덕션 환경에서 조건 검증이 필요한 경우에는 예외 처리를 사용합니다.
부작용 없는 코드 : 어서션 내부에서는 부작용이 없는 코드를 사용하는 것이 좋습니다. 어서션은 상태를 변경하지 않고 조건만 검증해야 합니다.
요약.
어서션은 코드의 특정 조건이 참임을 검증하는 도구로, 디버깅과 테스트 과정에서 코드의 논리적 일관성을 유지하는데 중요한 역할을 합니다.
이를 통해 개발자는 코드의 가정과 실제 동작이 일치하는지 확인하고, 문제를 조기에 발견하여 수정할 수 있습니다.
-
☕️[Java] 프로그래밍 언어와 자바
변수 선언.
컴퓨터 메모리(RAM)은 수많은 번지들로 구성된 데이터 저장 공간입니다.
프로그램은 데이터를 메모리에 저장하고 읽는 작업을 비번히 수행합니다.
이때 데이터를 어디에, 어떤 방식으로 저장할지 정해져 있지 않다면 메모리 관리가 무척 어려워집니다.
이 문제를 해결하기 위해 변수(Variable)을 사용합니다.
변수(Variable)는 하나의 값을 저장할 수 있는 메모리 번지에 붙여진 이름입니다.
변수를 통해 프로그램은 메모리 번지에 값을 저장하고 읽을 수 있습니다.
자바의 변수는 다양한 타입의 값을 저장할 수 없습니다.
즉, 정수형 변수에는 정수값만 저장할 수 있고, 실수형 변수에는 실수값만 저장할 수 있습니다.
변수를 사용하려면 변수 선언이 필요합니다.
변수 선언은 어떤 타입의 데이터를 저장할 것인지 그리고 변수 이름이 무었인지 결정하는 것입니다.
int age; // 정수(int) 값을 저장할 수 있는 age 변수 선언
double value; // 실수(double) 값을 저장할 수 있는 value 변수 선언
변수 이름의 첫 번째 글자가 문자여야 하고, 중간부터는 문자, 숫자, $, _를 포함할 수 있습니다.
또한, 첫 문자를 소문자로 시작하되 캐멀 케이스로 작성하는 것이 관례입니다.
변수가 선언 되었다면 값을 저장할 수 있습니다.
이때 대입 연산자인 =를 사용합니다.
수학에서 등호(=)는 ‘같다’라는 의미이지만, 자바에서는 우측 값을 좌측 변수에 대입하는 연산자로 사용됩니다.
int score; // 변수 선언
score = 60; // 값 대입
변수 선언은 저장되는 값의 타입과 이름만 결정한 것이지, 아직 메모리에 할당된 것은 아닙니다.
변수에 최초로 값이 대입될 때 메모리에 할당되고, 해당 메모리에 값이 저장됩니다.
변수에 최초로 값을 대입하는 행위를 변수 초기화라고 하고, 이때의 값을 초기값이라고 합니다.
초기 값은 다음과 같이 변수를 선언함과 동시에 대입할 수도 있습니다.
int score = 90;
초기화되지 않은 변수는 아직 메모리에 할당되지 않았기 때문에 변수를 통해 메모리 값을 읽을 수 없습니다.
따라서 다음은 잘못된 코딩입니다.
int value; // <- 1.변수 value 선언
int result = value + 10; // <- 2.변수 value 값을 읽고 10을 더해서 변수 result에 저장
1 에서 변수 value가 선언되었지만, 초기화되지 않았기 때문엔 2 value + 10에서 value 변수값은 읽어올 수 없습니다.
따라서 위 코드는 다음과 같이 변경해야 합니다.
int value = 30; // 변수 value가 30으로 초기화됨
int result = value + 10; // 변수 value 값(30)을 읽고 10을 더해서 변수 result에 저장
다음 예제는 초기화되지 않은 변수를 연산식에 사용할 경우 컴파일 에러(The local variable value may not have been initializer)가 발생하는 것을 보여줍니다.
public class VariableInitializationExample {
public static void main(String[] args) {
// 변수 value 선언
int value;
// 연산 결과를 변수 result의 초기값으로 대입
int result = value + 10; // <------- 컴파일 오류
// 변수 result 값을 읽고 콘솔에 출력
System.out.println(result);
}
}
변수는 출력문이나 연산식에 사용되어 변수값을 활용합니다.
다음 예제는 변수를 문자열과 결합 후 출력하거나 연산식에서 활용하는 모습을 보여줍니다.
```java
public class VariableUseExample {
public static void main(String[] args) {
int hour = 3;
int minute = 5;
System.out.println(hour + “시간” + minute + “분”);
int totalMinute = (hour*60) + minute;
System.out.println("총" + totalMinute + "분"); } }
// 실행 결과
// 3시간 5분
// 총 185분
- 변수는 또 다른 변수에 대입되어 메모리 간에 값을 복사할 수 있습니다.
- 다음 코드는 변수 x 값을 변수 y 값으로 복사합니다.
```java
int x = 10; // 변수 x에 10을 대입
int y = x; // 변수 y에 변수 x값을 대입
다음 예제는 두 변수의 값을 교환하는 방법을 보여줍니다.
두 변수의 값을 교환하기 위해서 새로운 변수 temp를 선언한 것에 주목합시다.
```java
public class VariableExchangeExample {
public static void main(String[] args) {
int x = 3;
int y = 5;
System.out.println(“x:” + x + “, y:” + y);
int temp = x;
x = y;
y = temp;
System.out.println(“x:” + x + “, y:” + y);
}
}
// 실행 결과
// x:3, y:5
// x:5, y:3
```
-
-
☕️[Java] 다형성(Polymorphism)
1️⃣ 다형성(Polymorphism).
‘다형성(Polymorphism)’ 은 ‘객체 지향 프로그래밍(OOP)’ 의 중요한 개념 중 하나로, 같은 인터페이스를 통해 서로 다른 데이터 타입의 객체를 조작할 수 있도록 합니다.
다형성은 코드의 재사용성과 유연성을 높여주며, 유지보수를 쉽게 해줍니다.
Java에서 ‘다형성’ 은 주로 ‘상속’ 과 ‘인터페이스’ 를 통해 구현됩니다.
2️⃣ 다형성의 개념.
다형성은 “하나의 인터페이스로 여러 가지 형태를 구현할 수 있는 능력” 을 의미합니다.
이는 같은 메서드가 다양한 객체에서 다르게 동작할 수 있게 합니다.
3️⃣ 다형성의 두 가지 형태.
1️⃣ 컴파일 시간 다형성(Compile-time Polymorphism)
메서드 오버로딩(Method Overloading)을 통해 구현됩니다.
컴파일 시점에 어떤 메서드가 호출될지 결정됩니다.
같은 이름의 메서드를 여러 개 정의하지만, 매개변수의 타입이나 개수가 달라야 합니다.
2️⃣ 런타임 다형성 (Runtime Polymorphism)
메서드 오버라이딩(Method Overriding)을 통해 구현됩니다.
실행 시점에 어떤 메서드가 호출될지 결정됩니다.
부모 클래스의 메서드를 자식 클래스에서 재정의하여 사용합니다.
4️⃣ 컴파일 시간 다형성(Method Overloading).
메서드 오버로딩은 같은 클래스 내에서 같은 이름을 가진 메서드를 여러 개 정의하는 것입니다.
단, 매개변수의 수나 타입이 달라야 합니다.
💻 예제.
public class MathOperations {
// 정수 두 개의 합
public int add(int a, int b) {
return a + b;
}
// 실수 두 개의 합
public double add(double a, double b) {
return a + b;
}
// 새 개의 정수의 합
public int add(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
MathOperations mathOperations = new MathOperations();
System.out.println(mathOperations.add(1, 2)); // 3
System.out.println(mathOperations.add(1.5, 2.5)); // 4.0
System.out.println(mathOperations.add(1, 2, 3)); // 6
}
}
5️⃣ 런타임 다형성(Method Overriding).
메서드 오버라이딩은 자식 클래스가 부모 클래스의 메서드를 재정의하는 것을 말합니다.
이를 통해 자식 클래스의 객체가 부모 클래스의 메서드를 호출할 때, 자식 클래스의 메서드가 실행되도록 합니다.
💻 예제.
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog(); // Animal 타입으로 Dog 객체 생성
Animal myCat = new Cat(); // Animal 타입으로 Cat 객체 생성
myDog.makeSound(); // Dog barks
myCat.makeSound(); // Cat meows
}
}
6️⃣ 인터페이스를 통한 다형성.
인터페이스를 통해서도 다형성을 구현할 수 있습니다.
인터페이스는 메서드의 서명만을 정의하며, 이를 구현하는 클래스가 메서드의 구체적인 동작을 정의합니다.
💻 예제.
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}
public class Main {
public static void main(String[] args) {
Shape myShape1 = new Circle();
Shape myShape2 = new Square();
myShape1.draw(); // Drawing a Circle
myShape2.draw(); // Drawing a Square
}
}
7️⃣ 다형성의 장점.
코드 재사용성 : 상위 클래스나 인터페이스를 사용하여 다양한 하위 클래스나 구현체를 다룰 수 있어 코드의 재사용성이 높아집니다.
유연성 : 새로운 클래스나 기능을 추가할 때 기존 코드를 수정할 필요 없이 확장할 수 있습니다.
유지보수성 : 코드를 이해하고 유지보수하는 것이 더 쉬워집니다. 메서드의 호출이 어디서 어떻게 이루어지는지 명확하기 때문입니다.
8️⃣ 예제: 다형성의 실질적 사용.
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class PolymorphismExample {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
arrayList.add("ArrayList Item");
linkedList.add("LinkedList Item");
printList(arrayList); // ArrayList Item
printList(linkedList); // LinkedList Item
}
public static void printList(List<String> list) {
for (String item : list) {
System.out.println(item);
}
}
}
이 예제에서는 ‘List‘ 인터페이스를 사용하여 ‘ArrayList‘ 와 ‘LinkedList‘ 를 동일한 방식으로 처리합니다.
이를 통해 다양한 구현체를 다룰 수 있는 유연한 코드를 작성할 수 있습니다.
📝 결론.
다형성은 객체 지향 프로그래밍의 핵심 개념 중 하나로, 코드의 유연성과 재사용성을 크게 향상시킵니다.
이를 통해 다양한 형태의 객체를 동일한 방식으로 다룰 수 있으며, 새로운 기능을 쉽게 확장하고 유지보수할 수 있습니다.
다형성은 상속과 인터페이스를 통해 구현되며, 메서드 오버로딩과 오버라이딩을 통해 다양한 형태를 취할 수 있습니다.
-
-
☕️[Java] 제네릭(Generic)
1️⃣ 제네릭(Generic)
Java에서의 제네릭(Generic) 은 클래스나 메서드에서 사용할 데이터 타입을 나중에 지정할 수 있도록 하는 기능입니다.
제네릭을 사용하면 코드의 재사용성을 높이고, 컴파일 시 타입 안전성을 제공하며, 명시적 타입 캐스팅을 줄일 수 있습니다.
2️⃣ 제네릭(Generic)의 주요 개념.
타입 매개변수 :
제네릭 클래스나 메서드는 타입 매개변수를 사용하여 타입을 정의합니다. 이 타입 매개변수는 클래스나 메서드가 호출될 때 구체적인 타입으로 대체됩니다.
타입 안정성 :
제네릭을 사용하면 컴파일 시 타입을 검사하므로, 런타입에 발생할 수 있는 타입 오류를 줄일 수 있습니다.
재사용성 :
제네릭 클래스나 메서드는 다양한 타입에 대해 동작하도록 설계할 수 있어, 코드의 재사용성을 높입니다.
3️⃣ 제네릭 클래스.
제네릭 클래스는 클래스 선언에 타입 매개변수를 포함하여 정의합니다.
일반적으로 타입 매개변수는 한 글자로 표현 되며, ‘T(Tyep)‘, ‘E(Element)‘, ‘K(Key)‘, ‘V(Value)‘ 등이 자주 사용됩니다.
예제.
// Box 클래스
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
// Main 클래스
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println("String item: " + stringBox.getItem()); // String item: Hello
Box<Integer> integerBox = new Box<>();
integerBox.setItem(123);
System.out.println("Integer item: " + integerBox.getItem()); // Integer item: 123
}
}
4️⃣ 제네릭 메서드.
제네릭 메서드는 메서드 선언 타입 매개변수를 포함하여 정의합니다.
예제.
public class GenericMethodExample {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"A", "B", "C", "D"};
printArray(intArray); // 1 2 3 4 5
printArray(strArray); // A B C D
}
}
5️⃣ 제네릭 타입 제한 (Bounded Type Parameters)
제네릭 타입 매개변수에 제한을 걸어 특정 타입의 하위 클래스나 인터페이스만 허용할 수 있습니다.
상한 제한 (Upper Bound)
public class BoundedTypeExample<T extends Number> {
private T number;
public BoundedTypeExample(T number) {
this.number = number;
}
public void printNumber() {
System.out.println("Number: " + number);
}
public static void main(String[] args) {
BoundedTypeExample<Integer> intExample = new BoundedTypeExample<>(123);
intExample.printNumber(); // Number: 123
BoundedTypeExample<Double> doubleExample = new BoundedTypeExample<>(45.67);
doubleExample.printNumber(); // Number: 45.67
}
}
여기서 ‘T’ 는 ‘Number’ 클래스나 그 하위 클래스만 될 수 있습니다.
하한 제한 (Lower Bound)
하한 제한은 와일드카드(’? super T‘)를 사용하여 정의됩니다.
예를 들어 ‘List<? super Integer>‘ 는 ‘Integer‘ 의 상위 타입인 ‘Number‘, ‘Object‘ 등이 될 수 있습니다.
import java.util.ArrayList;
import java.util.List;
public class LowerBoundWildcardExample {
public static void addNumbers(List<? super Integer> list) {
for (int i = 0; i < 5; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // [0, 1, 2, 3, 4]
}
}
6️⃣ 제네릭의 제한 사항.
Primitive Type 사용 불가 : 제네릭은 참조 타입만 허용하며, 기본 타입은 사용할 수 없습니다.
// 올바르지 않음
Box<int> intBox = new Box<>(); // 컴파일 오류
정적 컨텍스트에서의 타입 매개변수 사용 : 정적 메서드나 정적 변수에서는 타입 매개변수를 사용할 수 없습니다.
public class GenericClass<T> {
private static T item; // 컴파일 오류
}
제네릭 배열 생성 불가 : 제네릭 배열을 직접 생성할 수 없습니다.
// 올바르지 않음
T[] array = new T[10]; // 컴파일 오류
제네릭은 Java의 강력한 기능으로, 타입 안전성을 높이고 코드의 재사용성을 극대화할 수 있게 해줍니다.
이를 적절히 활용하면 더 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
-
☕️[Java] IntStream
1️⃣ Java Docs - IntStream.
Module : java.base
Package : java.util.stream
Interface IntStream
All SuperInterfaces : AutoCloseble, BaseStream<Integer, IntStream>
AutoCloseble
BaseStream
Integer
IntStream
public interface IntStream extends BaseStream<Integer, IntStream>
순차 및 병렬 집계 연산을 지원하는 기본 int 값 요소의 시퀀스입니다. 이것은 Stream의 int 기본형 특수화입니다.
IntStream 이 Stream 의 한 형태로, int 값의 시퀀스를 처리하며 순차 및 병렬 연산을 지원한다는 의미입니다.
다음 예제는 Stream과 IntStream을 사용하여 빨간색 위젯의 무게 합계를 계산하는 집계 연산을 보여줍니다.
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
streams(스트림), stream operations(스트림 연산), stream pipelines(스트림 파이프라인), and parallelism(및 병렬 처리)에 대한 추가적인 명세는 Stream 클래스 문서와 java.util.stream 패키지 문서를 참조하십시오.
Since : 1.8
Nested Class Summary
Nested Classes
Modifier and Type: static interface
Interface: IntStream.Builder
Description: IntStream용 변경 가능한 빌더입니다.
2️⃣ IntStream.
IntStream 은 Java의 스트림 API(Stream API)의 일부로, 기본형 int 에 특화된 스트림을 나타냅니다.
IntStream 은 Java 8에서 도입된 스트림 API의 일부로, 컬렉션(리스트, 배열 등)과 같은 데이터 소스를 함수형 프로그래밍 스타일로 처리할 수 있게 해줍니다.
IntStream 은 Stream<Integer> 와는 달리 오토박싱과 언박싱의 오버헤드가 없는 것이 특징입니다.
🙋♂️ IntStream의 주요 기능
1. 생성:
IntStream 을 생성하는 방법은 여러가지가 있습니다.
예를 들어, 배열, 범위, 임의의 수 등을 사용하여 생성할 수 있습니다.
2. 연산:
스트림 연산은 두 가지로 나뉩니다.
중간 연산과 최종 연산.
중간 연산은 또 다른 스트림을 반환하고, 지연(lazy) 평가됩니다.
최종 연산은 스트림을 소비하여 결과를 반환합니다.
🙋♂️ IntStream 생성 방법.
1. of() 메서드:
고정된 개수의 int 값을 스트림으로 생성합니다.
IntStream stream = IntStream.of(1, 2, 3, 4, 5);
2. range() 및 rangeClosed() 메서드:
범위를 지정하여 스트림을 생성합니다. range 는 시작 값 포함, 끝 값 미포함, rangeClosed 는 시작 값과 끝 값을 모두 포함합니다.
IntStream stream = IntStream.range(0, 5); // 0, 1, 2, 3, 4, 5
IntStream closedStream = IntStream.rangeClosed(0, 5); // 0, 1, 2, 3, 4, 5
3. generate() 메서드:
람다 표현식을 사용하여 무한 스트림을 생성합니다.
🚨 주의: 무한 스트림은 반드시 제한을 걸아야 합니다.
IntStream stream = IntStream.generate(() -> 1).limit(5); // 1, 1, 1, 1, 1
4. iterate() 메서드:
초기값과 반복 함수로 스트림을 생성합니다.
IntStream stream = IntStream.iterate(0, n -> n + 2).limit(5); // 0, 2, 4, 6, 8
5. builder() 메서드:
IntStream.Builder 를 사용하여 스트림을 생성합니다.
IntStream.Builder builder = IntStream.builder()l
builder.add(1).add(2).add(3).add(4).add(5);
IntStream stream = builder.builder();
6. 배열에서 생성:
배열을 스트림으로 변환합니다.
int[] array = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(array);
🙋♂️ IntStream의 주요 메서드.
1. 중간 연산:
map() : 각 요소에 함수 적용.
filter() : 조건에 맞는 요소만 통과
distinct() : 중복 요소 제거
sorted() : 정렬
limit() : 스트림 크기 제한
skip() : 처음 n개 요소 건너뛰기
2. 최종 연산:
forEach() : 각 요소에 대해 액션 수행
toArray() : 배열로 변환
reduce() : 모든 요소를 누적하여 하나의 값으로
collect() : 컬렉션으로 변환
sum() : 합계 연산
average() : 평균 계산
min(), max() : 최소, 최대값 찾기
count() : 요소 개수 반환
💻 예제 코드
예제 1: 0에서 5까지 거꾸로 출력.
import java.util.stream.IntStream;
public class Reverse {
public static void main(String[] args) {
IntStream.rangeClosed(0, 5)
.map(i -> 5 - i)
.forEach(System.out::println);
}
}
/*
=== 출력 ===
5
4
3
2
1
0
*/
예제 2: 배열의 합계 계산
import java.util.stream.IntStream;
public class ArraySum {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
int sum = IntStream.of(array).sum();
System.out.println("sum = " + sum); // sum = 15
}
}
예제 3: 짝수 필터링
import java.util.stream.IntStream;
public class FilterEvenNumber {
public static void main(String[] args) {
IntStream.rangeClosed(1, 10)
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
}
}
/*
=== 출력 ===
2
4
6
8
10
*/
📝 요약
IntStream 은 Java의 스트림 API의 일부분으로, 기본형 int에 특화된 스트림입니다.
이를 통해 컬렉션이나 배열을 함수형 프로그래밍 스타일로 처리할 수 있습니다.
IntStream 은 다양한 생성 방법과 중간 및 최종 연산을 제공하여 효율적이고 직관적인 데이터 처리를 가능하게 합니다.
📚 참고 문헌.
Java Docs - IntStream
-
-
-
☕️[Java] 스트림
1️⃣ 스트림.
1. 스트림(Stream)
자바에서 스트림(Stream) API는 자바 8에서 도입되어 컬렉션의 요소를 선언적으로 처리할 수 있는 방법을 제공합니다.
스트림 API를 이용하면 데이터 요소의 시퀀스를 효율적으로 처리할 수 있으며, 데이터를 병렬로 처리하는 것도 간단할게 할 수 있습니다.
스트림을 이용하면 복잡한 데이터 처리 작업을 간결하고 명확한 코드로 작성할 수 있습니다.
1.2 스트림의 주요 특정.
1. 선언적 처리 : 스트림을 사용하면 무엇을 할 것인지(what)에 집중하여 작업을 설명할 수 있고, 어떻게 처리할 것인지(how)는 스트림 API가 알아서 최적화하여 처리합니다.
2. 파이프라이닝 : 스트림 연산은 파이프라인을 형성할 수 있으며, 여러 단계의 처리 과정을 연결하여 복잡한 데이터 처리를 효과적으로 할 수 있습니다.
3. 내부 반복 : 스트림은 “내부 반복”을 사용합니다. 즉, 데이터를 어떻게 반복할지 스트림이 처리하므로, 개발자는 각 요소에 어떤 처리를 할지만 정의하면 됩니다.
4. 불변성 : 스트림은 데이터를 수정하지 않습니다. 대신, 각 단계에서 결과를 내는 새로운 스트림을 생성합니다. 이는 함수형 프로그래밍의 특성을 반영합니다.
1.3 스트림의 작업 흐름.
스트림 API의 작업 흐름은 크게 세 부분으로 나눌 수 있습니다.
1. 스트림 생성 : 컬렉션, 배열, I/O 자원 등의 데이터 소스로부터 스트림을 생성합니다.
List<String> myList = Arrays.asList("a1", "a2", "b1", "b2", "c2", "c1");
Stream<String> myStrean = myList.stream();
2. 중간 연산(Intermediate operations) : 스트림을 변환하는 연산으로, 필터링, 매핑, 정렬 등이 있으며, 이 연산들은 연결 가능하고, 또한 게으르게(lazily) 실행됩니다.
myStream.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted();
3. 종단 연산(Terminal operations) : 스트림의 요소들을 소모하여 결과를 생성하는 연산입니다. 예를 들어, forEach, reduce, collect 등이 있으며, 이 연산을 수행한 후 스트림은 더 이상 사용할 수 없습니다.
myStream.forEach(System.out::println);
1.4 스트림과 병렬 처리.
스트림 API는 병렬 처리를 간단하게 지원합니다.
‘paralleStream()’ 을 호출하면 자동으로 여러 쓰레드에서 스트림 연산이 병렬로 수행됩니다.
이는 데이터가 큰 경우에 유용하며, 멀티코어 프로세서의 이점을 쉽게 활용할 수 있게 해줍니다.
1.5 📝 정리.
스트림은 자바에서 데이터 컬렉션을 함수형 스타일로 쉽게 처리할 수 있게 하는 강력한 도구입니다.
이는 코드의 간결성과 가독성을 높이는 데 큰 도움을 줍니다.
2. 중개 연산(Intermediate operations)
자바 스트림 API에서 중개 연산(Intermediate operations)은 스트림의 요소들을 처리하고 변형하는 연산들로서, 다른 스트림을 반환합니다.
중개 연산은 게으른(lazy) 특성을 가지며, 종단 연산(Terminal operation)이 호출되기 전까지는 실제로 실행되지 않습니다.
이런 특성은 연산의 체인을 구성할 때 성능 최적화에 도움을 줍니다.
2.1 중개 연산의 주요 특성.
게으른 실행(Lazy Execution) : 중개 연산은 호출되었을 때 즉시 실행되지 않습니다. 대신, 종단 연산이 호출될 때 까지 실행이 지연됩니다.
스트림 변환 : 각 중개 연산은 변형된 형태의 새로운 스트림을 변환합니다. 이는 연산을 연쇄적으로 연결할 수 있도록 합니다.
2.2 주요 중개 연산의 종류.
1. 필터링(Filtering)
‘filter(Predicate<T> predicate)’ : 주어진 조건(프리디케이트)에 맞는 요소만을 포함하는 스트림을 반환합니다.
List<String> names = Arrays.asList("Jo", "Lee", "Park", "Kang");
names.stream()
.filter(name -> name.startsWith("K"))
.forEach(System.out::println); // 출력: "Kang"
2. 매핑(Mapping)
‘map(Function<T, R> mapper)’ : 스트림의 각 요소에 주어진 함수를 적용하고, 함수 결과로 주성된 새 스트림을 반환합니다.
‘flatMap(Function<T, Stream<R>> mapper)’ : 각 요소에 함수를 적용한 결과로 생성된 여러 스트림을 하나의 스트림으로 평탄화합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
numbers.stream()
.map(number -> number * number)
.forEach(System.out::println); //출력: 1, 4, 9, 16
3. 정렬(Sorting)
‘sorted() :’ 자연 순서대로 스트림을 정렬합니다.
‘sorted(Comparator<T> comparator) :’ 주어진 비교자를 사용하여 스트림을 정렬합니다.
List<String> fruits = Arrays.asList("banana", "apple", "orange", "kiwi");
fruits.stream()
.sorted()
.forEach(System.out::println); // 출력: apple, banana, kiwi, orange
4. 제한(Limiting) 및 건너뛰기(Skipping)
‘limit(long maxSize)’ : 스트림의 요소를 주어진 크기로 제한합니다.
‘skip(long n)’ : 스트림의 처음 n개 요소를 건너뜁니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
.skip(2)
.limit(3)
.forEach(System.out::println); // 출력 3, 4, 5
5. 중복 제거(Distinct)
‘distinct()’ : 스트림에서 중복된 요소를 제거합니다.
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
numbers.stream()
.distinct()
.forEach(System.out::println); // 출력: 1, 2, 3, 4, 5
2.3 📝 정리.
중개 연산을 통해 데이터 스트림을 세밀하게 제어하고 원하는 형태로 데이터를 변형 할 수 있습니다.
이러한 연산들은 다양한 데이터 처리 작업에서 매우 유용하게 사용됩니다.
3. 최종 연산(Terminal operations)
자바 스트림 API에서 최종 연산(Terminal operations)은 스트림 파이프라인의 실행을 트리거하고 스트림의 요소를 소비하여 결과를 생성하거나 부작용(side effect)을 일으키는 연산입니다.
최종 연산이 호출되기 전까지 중간 연산들은 게으른(lazy) 방식으로 처리되며 실행되지 않습니다.
최종 연산 후에는 스트림이 소비되어 더 이상 사용할 수 없게 됩니다.
3.1 최종 연산의 주요 유형.
1. 수집(Collection)
‘collect(Collector<T, A, R> collector)’ : 스트림의 요소를 변환, 결합하고 컬렉션으로 또는 다른 형태로 결과를 수집합니다.
예를 들어, ‘toList()’, ‘toSet()’, ‘toMap()’ 등이 있습니다.
List<String> names = Array.asList("Alice", "Bob", "Charlie", "David");
List<String> list = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(list); // 출력 ["Alice"]
2. 집계(Aggregation)
‘count()’ : 스트림의 요소 개수를 반환합니다.
‘max(Comparator<T> comparator)’ : 스트림에서 최대값을 찾습니다.
‘min(Comparator<T> comparator)’ : 스트림에서 최소값을 찾습니다.
‘reduce(BinaryOperator<T> accumulator)’ : 스트림의 요소를 결합하여 하나의 결과를 생성합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 출력: 10
3. 반복(Iteration)
‘forEach(Consumer<T> action)’ : 각 요소에 대해 주어진 작업을 수행합니다. 스트림의 순서대로 실행됩니다.
List<String> names = Arrays.asList("Alist", "Bob", "Charlie", "David");
names.stream()
.forEach(System.out::println); // Alice, Bob, Charlie, David
4. 조건 검사(Checking)
‘allMatch(Predicate<T> predicate) :’ 모든 요소가 주어진 조건을 만족하는지 검사합니다.
‘anyMatch(Predicate<T>predicate) :’ 어떤 요소라도 주어진 조건을 만족하는지 검사합니다.
‘noneMatch(Predicate<T>predicate) :’ 모든 요소가 주어진 조건을 만족하지 않는지 검사합니다.
boolean allEven = numbers.stream()
.allMatch(n -> n % 2 == 0);
System.out.println(allEven); // 출력: false
5. 요소 검색(Finding)
‘findFirst()’ : 스트림의 첫 번째 요소를 Optional로 반환합니다.
‘findAny()’ : 스트림에서 임의의 요소를 Optional로 반환합니다. 병렬 스트림에서 유용합니다.
Optional<String> first = names.stream()
.findFirst();
first.ifPresent(System.out::println); // 출력 Alice
3.2 📝 정리.
이러한 최종 연산들은 스트림 처리를 완료하고 필요한 결과를 도출하기 위해 사용됩니다.
스트림 API를 통해 데이터 처리를 선언적이고 간결하게 할 수 있으며, 복잡한 로직을 효과적으로 관리할 수 있습니다.
-
☕️[Java] 람다식은 하나만!
람다식은 하나만!😆
자바에서는 “하나의 추상 메소드를 갖는 인터페에스에 대해서만 람다식을 직접 사용할 수 있습니다.”
이를 함수형 인터페이스라고 부르며, 람다식은 이런 함수형 인터페이스의 구현을 간단히 할 수 있는 방법을 제공합니다.
하지만 아래의 코드와 같이 인터페이스 내에 두 개의 추상 메서드 (‘plus’, ‘minus’)가 있기 때문에, 이 인터페이스를 람다식으로 직접 구현하는 것은 불가능합니다.
interface Carculator {
public abstract int plus(int x, int y);
public abstract int minus(int x, int y);
}
람다식을 사용하려면 함수형 인터페이스가 필요하므로, 두 메소드 각각을 위한 두 개의 별도의 인터페이스를 정의하거나 기존 인터페이스 중 하나를 수정해야 합니다.
아래의 코드는 이를 위해 각 메소드를 분리하여 두 개의 함수형 인터페이스를 만든 예시입니다.
interface Calculator {
public abstract int operation(int x, int y);
}
public class Main {
public static void main(String[] args) {
Calculator plus = (x, y) -> { return x + y; };
System.out.println(plus.operation(10,2)); // 12
Calculator minus = (x, y) -> { return x - y; };
System.out.println(minus.operation(10,2)); // 8
}
}
위 코드는 각 연산을 람다식으로 간단히 구현하고 있습니다.
만약 원래의 ‘Carculator’ 인터페이스를 유지고하고 싶다면 이를 직접적으로 람다식으로 구현할 수는 없으며, 대신 익명 클래스나 정규 클래스를 사용해야 합니다.
아래의 코드는 익명 클래스를 사용하는 방법을 보여줍니다.
Calculator calclator = new Calculator() {
@Override
public int plus(int x, int y) {
return x + y;
}
@Override
public int minus(int x, int y) {
return x - y;
}
}
-
☕️[Java] 람다식
1️⃣ 람다식.
1. 람다 표현식(Lambda Expression)
자바 프로그래밍에서 람다식 또는 람다 표현식(Lambda Expression)은 간결한 방식으로 익명 함수(anonymous function)를 제공하는 기능입니다.
자바 8부터 도입된 이 기능은 함수형 프로그래밍의 일부 개념을 자바에 도입하여, 코드를 더 간결하고 명료하게 만들어 주며 특히 컬렉션의 요소를 처리할 때 유용하게 사용됩니다.
1.2 람다식의 특징.
익명성 : 람다는 이름이 없기 때문에 익명으로 처리됩니다.
함수 : 람다는 메서드와 유사하지만, 독립적으로 존재할 수 있는 함수입니다.
전달성 : 람다 표현식은 메서드 인자로 전달되거나 변수에 저장될 수 있습니다.
간결성 : 코드의 간결성을 높여, 불필요한 반복을 줄여줍니다.
1.3 람다 표현식의 기본 구조.
람타 표현식은 주로 매개 변수를 받아들여 결과를 반환하는 식의 형태로 작성됩니다.
일반적인 형태는 다음과 같습니다.
(parameters) -> expression
또는
(parameters) -> { statements; }
매개 변수 : 괄호 안에 정의되며, 매개 변수의 타입을 명시할 수도 있고 생략할 수도 있습니다.
매개 변수가 하나뿐인 경우, 괄호도 생략할 수 있습니다.
화살표(->) : 매개 변수와 몸체를 구분짓는 역할을 합니다.
몸체 : 람다의 실행 로직을 담고 있으며, 식(expression) 또는 문장(statements)이 올 수 있습니다.
식은 단일 실행 결과를 반환하며, 중괄호는 생략할 수 있습니다.
문장은 중괄호 안에 작성되며, 여러 줄의 코드를 포함할 수 있습니다.
1.4 예시
Thread 실행하기
new Thread(() -> System.out.println("Hello from a thread")).start();
리스트의 각 요소 출력하기
List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
list.forEach(item -> System.out.println(item));
Comparator를 통한 정렬
List<String> cities = Arrays.asList("Seoul", "New York", "London");
Collections.sort(cities, (s1, s2) -> s1.compareTo(s2));
1.5 📝 정리.
람다 표현식은 이벤트 리스너, 스레드의 실행 코드 등 여러 곳에서 기존에 익명 클래스를 사용하던 부분을 대체하여 코드를 더 간결하게 만들 수 있습니다.
또한, 스트림 API와 함께 사용될 때 강력한 데이터 처리 기능을 제공하여 복잡한 컬렉션 처리를 단순화시킬 수 있습니다.
2. 람다식의 장점.
자바에서 람다식(Lambda Expression)을 사용하는 것은 여러 가지 장점을 제공합니다.
이러한 장점들은 프로그래밍 스타일, 코드의 간결성, 효율성 및 기능성 측면에서 특히 두드러집니다.
2.1 람다식의 주요 장점들.
1. 코드의 간결성 : 람다식을 사용하면 복잡한 익명 클래스를 사용할 필요가 없어지므로 코드를 훨씬 간결하게 작성할 수 있습니다.
예를 들어, 스레드를 실행하거나 이벤트 리스너를 설정할 때 몇 줄의 코드로 작성할 수 있습니다.
2. 가독성 향상 : 람다식은 기존의 익명 클래스보다 훨씬 읽기 쉽고 이해하기 쉬운 코드를 작성할 수 있게 합니다.
이는 유지보수 시간을 줄이고 코드의 질을 향상시키는 데 도움이 됩니다.
3. 함수형 프로그래밍 지원 : 람다식은 자바에 함수형 프로그래밍 스타일을 도입하였습니다.
이는 데이터 처리와 조작을 보다 선언적으로 표현할 수 있게 해, 복잡한 데이터 처리 로직을 간결하고 효율적으로 작성할 수 있도록 합니다.
4. 코드의 재사용성 증가 : 람다식을 사용하면 특정 동작을 수행하는 코드 블록을 쉽게 재사용할 수 있습니다.
람다식은 변수처럼 취급될 수 있어, 메소드에 인자로 전달하거나 변수에 할당하여 다른 위치에서 사용할 수 있습니다.
5. 병렬 실행 용이 : 자바 8 이후로 스트림 API와 결합된 람다식은 컬렉션 처리를 병렬로 쉽게 수행할 수 있게 해줍니다.
이는 ‘parallelStream()’ 과 같은 메소드를 사용하여 멀티코어 프로세서의 이점을 쉽게 활용할 수 있게 합니다.
6. 지연 실행(Lazy Evaluation) : 람다식은 지연 실행을 가능하게 합니다.
예를 들어, 조건이 충족될 때까지 코드 실행을 지연시키거나, 필요할 때만 데이터를 처리하도록 할 수 있습니다.
이는 성능 최적화에 도움을 줄 수 있습니다.
7. 함수 인터페이스와의 호환성 : 람다식은 단일 추상 메소드를 가진 인터페이스(함수 인터페이스)와 호환됩니다.
이는 많은 내장 함수 인터페이스(‘Runnable’, ‘Callable’, ‘Comparator’ 등)와 사용자 정의 함수 인터페이스에 람다식을 적용할 수 있음을 의미합니다.
2.2 📝 정리.
이러한 장점들로 인해 람다식은 자바 프로그래머들 사이에서 매우 인기 있는 기능이 되었으며, 모던 자바 코드에서는 필수적인 요소로 자리 잡고 있습니다.
3. 람다식의 단점.
자바에서 람다식을 사용하면 여러 가지 장점이 있지만, 몇 가지 단점 또는 주의할 점도 있습니다.
3.1 람다식의 사용과 관련된 단점.
1. 디버깅의 어려움 : 람다식은 익명 함수이기 때문에 디버깅이 더 복잡할 수 있습니다.
스택 트레이스에서 람다식이 어디에 위치하는지 명확하게 표시되지 않아 오류를 추적하기 어려울 수 있습니다.
2. 코드의 남용 : 람다식을 과도하게 사용하면 코드가 오히려 더 복잡해지고 이해하기 어려워질 수 있습니다.
특히 람다 내부에 긴 로직이나 조건문을 넣을 경우 가독성이 떨어질 수 있습니다.
3. 람다 캡처링 오버헤드 : 람다식은 주변 환경의 변수를 캡처(Capture)할 수 있습니다.
이 때, 람다가 외부 변수를 캡처 할 경우 추가적인 비용이 발생할 수 있으며, 이는 성능에 영향을 줄 수 있습니다.
4. 직렬화의 문제 : 람다식은 기본적으로 직렬화가 보장되지 않습니다.
따라서 람다식을 사용하는 객체를 직렬화하려고 할 때 문제가 발생할 수 있습니다.
이는 분산 시스템에서 특히 중요한 이슈가 될 수 있습니다.
5. 학습 곡선 : 자바 8 이전의 버전에 익숙한 개발자들에게 람다식과 스트림 API는 새로운 패러다임을 익혀야 하는 도전과제를 제시합니다.
이는 초기 학습 곡선을 가파르게 만들 수 있습니다.
6. 타입 추론의 복잡성 : 람다식에서는 자바 컴파일러가 타입을 추론해야 하는데, 때때로 이 추론이 개발자의 의도와 다른게 이루어질 수 있습니다.
이는 코드의 명확성을 떨어뜨릴 수 있습니다.
7. 함수형 인터페이스의 제약 : 람다식은 단 하나의 추상 메소드를 가진 함수형 인터페이스와 함꼐 사용됩니다.
때로는 이런 제약이 프로그램 설계를 더 복잡하게 만들 수 있습니다.
3.2 📝 정리.
람다식의 단점들은 주로 개발과 관련된 트레이드오프와 관련이 있으며, 이러한 단점을 이해하고 적절히 관리한다면 람다식을 효과적으로 사용할 수 있습니다.
-
☕️[Java] HashMap에 key 값은 항상 int 여야 할까요?
🤔 HashMap에 key 값은 항상 int 여야 할까요?
강의와 예제 코드를 열심히 보고 따라서 타이핑하고 있던 중 “문뜩!” 떠올랐습니다. 🤩
‘HashMap에 key 값은 항상 int 여야 할까요?🤔’
그래서 구글링과 챗 지피티 그리고 Java의 정석 도서를 살펴본 후 이 글을 쓰게 되었습니다 :)
🙅♂️ 대답은 “아니오!” 입니다.
자바 프로그래밍에서 ‘HashMap’ 의 키 값은 ‘int’ 형일 필요는 없다고 합니다.
‘HashMap’ 은 키로서 어떠한 객체도 사용할 수 있으며, 기는 자바의 ‘제네릭’ 을 통해 다양한 유형의 객체를 키로 사용할 수 있게 해준다고 합니다.
(오! “제네릭” 은 아직 안배웠지만 🥲 Swift에서 봐서 비슷한 느낌 같은데?!)
키 객체는 ‘Object’ 클래스의 ‘hashCode()’ 메소드와 ‘equals()’ 메소드를 적절히 구현해야 합니다.
(‘Object’ 클래스는 무엇이고, ‘hashCode()’ 메소드와 ‘equals()’ 메소드는 무엇인가?!! 🤪)
이는 ‘HashMap’ 이 키의 해시 코드를 사용하여 데이터를 저장하고 검색하기 때문입니다.
(도통 무슨 소리인지 몰라서 아래 “제네릭”. “Object 클래스”, “hashCode()”, “equals()”를 정리했어요 ㅎㅎ)
‘HashMap’ 을 사용할 때, 키로 사용되는 객체의 ‘hashCode()’ 메소드가 효율적이고 일관성 있는 값을 반환해야 합니다.
또한, ‘equalse()’ 메소드는 객체의 동등성을 정확하게 판단할 수 있어야 합니다.
이 두 메소드의 구현이 적절히 이루어져야 ‘HashMap’ 이 키의 중복 없이 정확하게 데이터를 관리할 수 있습니다.
예시 - String 객체를 키로 사용하는 ‘HashMap’
import java.util.HashMap;
public class Example {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
System.out.println(map.get("two")); // 출력: 2
}
}
위 예시에서 보듯, ‘String’ 외에도 사용자가 정의한 어떠한 객체든 ‘hashCode()’ 와 ‘equals()’ 가 적절히 구현되어 있다면 키로 사용할 수 있습니다.
따라서 ‘int’ 만을 키로 사용해야 하는 것은 아닙니다.
1️⃣ 제네릭(Generic).
자바에서 ‘제네릭(Generic)’ 은 클래스, 인터페이스, 메소드를 정의할 때 타입(Type)을 하나의 매개변수처럼 취급하여, 다양한 데이터 타입을 사용할 수 있도록 하는 프로그래밍 기법입니다.
제네릭을 사용하면 컴파일 시점에 타입 안정성을 제공하고, 타입 캐스팅을 줄여 코드를 더 간결하고 읽기 쉽게 만들 수 있습니다.
제네릭 기본 문법.
제네릭은 타입 매개변수를 사용하여 구현됩니다.
타입 매개변수는 보통 한 글자로 표현되며, 일반적으로 다음과 같은 문자를 사용합니다.
‘E’ : Element(컬렉션에서 사용되는 요소)
‘K’ : Key(키)
‘V’ : Value(값)
‘T’ : Type(일반적인 타입)
‘S’, ‘U’, ‘V’ 등 - 두 번째, 세 번째, 네 번째 타입을 나타내기 위해 사용
예시: 제네릭을 사용한 클래스와 메소드
// 제네릭 클래스 예시
public class Box<T> {
private T t; // T는 이 클래스가 다루는 객체의 타입을 매개변수화합니다.
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
// 제네릭 메소드 예시
public static <T> void printArray(T[] inputArray) {
for (T element : inputArray) {
System.out.print(element + " ");
}
System.out.println();
}
위 예시에서 ‘Box’ 클래스는 타입 매개변수 ‘T’ 를 사용하여 다양한 타입을 저장하고 반환할 수 있는 범용 컨테이너로 사용됩니다.
‘printArray’ 메소드는 어떤 배열 타입도 받아들일 수 있으며, 그 요소들을 출력합니다.
2️⃣ Object 클래스.
자바 프로그래밍에서 ‘Object’ 클래스는 자바의 클래스 계층 구조에서 가장 상위에 위치하는 클래스입니다.
모든 자바 클래스는 직접적이거나 간접적으로 ‘Object’ 클래스를 상속받습니다.
이는 ‘Object’ 클래스가 자바에서 모든 클래스의 근본(base)이라는 의미 입니다.
‘Object’ 클래스는 자바의 ‘java.lang’ 패키지에 포함되어 있으며, 모든 객체에 공통적으로 필요한 메서드를 제공합니다.
Object 클래스의 의의.
‘Object’ 클래스의 메서드들은 자바의 모든 클래스에 기본적인 기능을 제공합니다.
이로 인해, 개발자는 어떤 클래스를 만들 때도 이러한 기본적인 메서드들을 새로 작성하지 않고도, 필요에 따라 이를 상속받아 확장하거나 재정의할 수 있습니다.
‘Object’ 클래스는 자바의 모든 클래스와 객체에 공통적인 근복적인 메커니즘을 제공하는 중추적인 역할을 합니다.
3️⃣ Object 클래스의 hashCode() 메소드.
자바의 ‘Object’ 클래스에서 ‘hashCode()’ 메소드는 객체의 메모리 주소를 기반으로 계산된 정수 값을 반환하는 메소드입니다.
이 메소드는 객체의 해시 코드를 제공하며, 해시 기반 컬렉션(예: ‘HashMap’, ‘HashSet’, ‘Hashtable’ 등)에서 객체를 효율적으로 관리하기 위해 사용됩니다.
hashCode() 메소드의 주요 용도
1. 해시 테이블 사용 : ‘hashCode()’ 는 특히 해시 테이블을 사용하는 자료 구조에서 중요합니다.
객체의 해시 코드를 사용하여, 해당 객체가 저장되거나 검색될 해시 버킷을 결정합니다.
이로 인해 데이터의 삽입, 검색, 삭제 작업이 빠르게 수행될 수 있습니다.
2. 객체의 동등성의 빠른 검증 : ‘hashCode()’ 메소드는 ‘equals()’ 메소드와 함께 사용되어 객체의 동등성을 검사합니다.
두 객체가 같다면 반드시 같은 해시 코드를 반환해야 합니다.
따라서, 해시 코드가 다른 두 객체는 결코 같을 수 없으므로, ‘equals()’ 호출 전에 해시 코드를 먼저 확인함으로써 불필요한 비교를 줄일 수 있습니다.
4️⃣ Object 클래스의 equals() 메소드.
자바 프로그래밍에서 ‘Object’ 클래스의 ‘equals()’ 메소드는 두 객체가 동등한지 비교하는데 사용됩니다.
이 메소드는 ‘Object’ 클래스에서 모든 클래스로 상속되며, 특히 객체의 동등성을 판단할 때 중요한 역할을 합니다.
기본적으로, ‘Object’ 클래스의 ‘equals()’ 메소드는 두 객체의 참조가 같은지 확인합니다.
즉, 두 객체가 메모리상에서 같은 위치를 가리키는지 검사합니다.
-
☕️[Java] 컬렉션 프레임워크
1️⃣ 컬렉션 프레임워크
1. 컬렉션 프레임워크(Collection Framework)
자바 컬렉션 프레임워크는 자료 구조를 효율적으로 관리하고 조작할 수 있는 방법을 제공하는 통합 아키텍처입니다.
이 프레임워크는 다양한 인터페이스와 구현을 포함하며, 다양한 종류의 컬렉션들을 제어하고, 데이터 집합을 효율적으로 관리하기 위한 알고리즘을 제공합니다.
1.2 컬렉션 프레임워크의 구요 구성 요소.
1. 인터페이스(Interface) : 컬렉션 프레임워크의 핵심 인터페이스로는 ‘Collection’, ‘List’, ‘Queue’ 등이 있으며 각각 다른 형태의 데이터 집합을 추상화합니다.
예를 들어, ‘List’ 는 순서가 있는 데이터 집합을, ‘Set’ 은 중복을 허용하지 않는 데이터 집합을 나타냅니다.
2. 구현(Implementation) : 이러한 인터페이스를 실제로 구현한 클래스들로, ‘ArrayList’, ‘LinkedList’, ‘HashSet’, ‘TreeSet’, ‘PriorityQueue’ 등이 포함됩니다.
각 클래스는 컬렉션 인터페이스를 구현하며, 데이터의 특성에 따라 선택하여 사용할 수 있습니다.
3. 알고리즘(Algorithm) : 컬렉션 데이터를 처리하는 데 필요한 다양한 알고리즘이 제공됩니다.
이 알고리즘은 정렬, 검색, 순환 및 변환 등을 포함하며, 이들은 대부분 ‘Collections’ 클래스에 정적 메소드로 제공됩니다.
1.3 📝 정리.
컬렉션 프레임워크를 사용하면 데이터를 보다 효율적으로 처리할 수 있고, 기능의 재사용성 및 유지 보수성이 향상됩니다.
또한, 자바 개발자로서 다양한 데이터 컬렉션을 쉽게 처리하고, 표준화된 방법으로 데이터를 조작할 수 있는 능력을 갖추게 됩니다.
2. List 인터페이스.
자바 프로그래밍에서 ‘List’ 인터페이스는 ‘java.util’ 패키지의 일부로, 순서가 있는 컬렉션을 나타냅니다.
이 인터페이스를 사용하면 사용자가 목록의 특정 위치에 접근, 삽입, 삭제를 할 수 있는 동시에, 목록의 요소들이 입력된 순서대로 저장 및 관리됩니다.
‘List’ 는 중복된 요소의 저장을 허용하기 때문에, 같은 값을 가진 요소를 여러 개 포함할 수 있습니다.
2.1 List 인터페이스의 주요 메서드.
add(E e) : 리스트의 끝에 요소를 추가합니다.
add(int index, E element) : 리스트의 특정 위치에 요소를 삽입합니다.
remove(Object o) : 리스트에서 지정된 요소를 삭제합니다.
remove(int index) : 리스트에서 지정된 위치의 요소를 삭제합니다.
get(int index) : 지정된 위치에 있는 요소를 반환합니다.
set(int index, E element) : 리스트의 특정 위치에 요소를 설정(교체)합니다.
indexOf(Object o) : 객체를 찾고, 리스트 내의 첫 번째 등장 위치를 반환합니다.
size() : 리스트에 있는 요소의 수를 반환합니다.
clear() : 리스트에서 모든 요소를 제거합니다.
2.3 가장 널리 사용되는 구현체.
‘List’ 인터페이스는 다양한 구현체를 가지고 있으며, 가장 널리 사용되는 구현체는 ‘ArrayList’, ‘LinkedList’ 그리고 ‘Vector’ 입니다.
각 구현체는 내부적인 데이터 관리 방식이 다르므로, 사용 상황에 따라 적합한 구현체를 선택할 수 있습니다.
‘ArrayList’ : 내부적으로 배열을 사용하여 요소들을 관리합니다. 인덱스를 통한 빠른 접근이 가능하지만, 크기 조정이 필요할 때 비용이 많이 들 수 있습니다.
‘LinkedList :’ 내부적으로 양방향 연결 리스트를 사용합니다. 데이터의 삽입과 삭제가 빈번하게 일어나는 경우 유용합니다.
Vector : ‘ArrayList’ 와 비슷하지만, 모든 메소드가 동기화되어 있어 멀티스레드 환경에서 사용하기에 안전합니다.
2.4 📝 정리.
이러한 특성들로 인해 ‘List’ 인터페이스는 자바에서 데이터를 순차적으로 처리할 필요가 있는 다양한 애플리케이션에서 중요하게 사용됩니다.
3. Set 인터페이스.
자바 프로그래밍에서 ‘Set’ 인터페이스는 ‘java.util’ 패키지의 일부이며, 중복을 허용하지 않는 요소의 컬렉션을 나타냅니다.
‘Set’ 은 ‘Collection’ 인터페이스를 확장하는 인터페이스로서, 집합의 개념을 구현합니다.
이는 각 요소가 컬렉션 내에서 유일하게 존재해야 함을 의미합니다.
인덱스로 요소를 관리하는 ‘List’ 인터페이스와 달리, ‘Set’ 은 요소의 순서를 유지하지 않습니다.
3.1 Set의 주요 특징.
중복 불허 : 같은 요소의 중복을 허용하지 않으며, 이미 ‘Set’ 에 존재하는 요소를 추가하려고 시도하면 그 요소는 컬렉션에 추가되지 않습니다.
순서 보장 없음 : 대부분의 ‘Set’ 구현체는 요소의 저장 순서를 유지하지 않습니다. 그러나 ‘LinkedHashSet’ 과 같은 특정 구현체는 요소의 삽입 순서를 유지할 수 있습니다.
값에 의한 접근 : ‘Set’ 은 인덱스를 사용하지 않고 값에 의해 요소에 접근합니다.
3.2 주요 메서드.
‘Set’ 인터페이스는 ‘Collection’ 인터페이스에서 상속받은 다양한 메소드를 포함합니다.
주요 메서드는 다음과 같습니다.
add(E e): 요소 e를 Set에 추가합니다. 이미 존재하는 요소를 추가하려는 경우, 요소는 추가되지 않고 false를 반환합니다.
remove(Object o): 지정된 객체 o를 Set에서 제거합니다.
contains(Object o): Set이 지정된 객체 o를 포함하고 있는지 여부를 반환합니다.
size(): Set의 요소 개수를 반환합니다.
isEmpty(): Set이 비어 있는지 여부를 반환합니다.
clear(): Set의 모든 요소를 제거합니다
3.3 주요 구현체.
‘Set’ 인터페이스는 여러 가지 방법으로 구현될 수 있으며, 각 구현체는 다른 특성을 가집니다.
HashSet : 가장 널리 사용되는 ‘Set’ 구현체로, 해시 테이블을 사용하여 요소를 저장합니다. 요소의 삽입, 삭제, 검색 작업은 평균적으로 상수 시간(O(1))이 걸립니다.
LinkedHashSet : ‘HashSet’ 의 확장으로, 요소의 삽입 순서를 유지합니다.
TreeSet : 레드-블랙 트리 구조를 사용하여 요소를 저장합니다. 요소는 자연적 순서 또는 비교자에 의해 정렬됩니다.
이로 인해 삽입, 삭제, 검색 작업에 로그 시간(O(log n))이 걸립니다.
3.4 📝 정리.
‘Set’ 인터페이스는 주로 중복을 허용하지 않는 데이터 컬렉션을 다루는 데 사용되며, 특히 요소의 유일성을 보장하는데 유용합니다.
4. Map 인터페이스.
자바에서 ‘Map’ 인터페이스는 ‘java.util’ 패키지에 속하며, 키(key)와 값(value) 쌍으로 이루어진 데이터를 저장하는 자료구조를 정의합니다.
‘Map’ 은 키의 중복을 허용하지 않으면서 각 키는 하나의 값에 매핑됩니다.
값은 중복될 수 있지만, 각 키는 유일해야 합니다.
이러한 특성 때문에 ‘Map’ 은 키를 통해 빠르게 데이터를 검색할 수 있는 효율적인 수단을 제공합니다.
4.1 Map의 주요 특징.
키 기반 데이터 접근 : 키를 사용하여 데이터에 접근하므로, 키에 대한 빠른 검색, 삽입, 삭제가 가능합니다.
키의 유일성 : 같은 키를 다시 ‘Map’ 에 추가하려고 하면 기존 키에 연결된 값이 새 값으로 대체됩니다.
값의 중복 허용 : 같은 값을 가진 여러 키가 ‘Map’ 에 존재할 수 있습니다.
4.2 주요 메서드
‘Map’ 인터페이스는 데이터를 관리하기 위해 다음과 같은 주요 메소드를 제공합니다.
put(K key, V value): 키와 값을 Map에 추가합니다. 이미 키가 존재하면, 해당 키의 값이 새로운 값으로 업데이트 됩니다.
get(Object key): 지정된 키에 연결된 값을 반환합니다. 키가 존재하지 않는 경우, null을 반환합니다.
remove(Object key): 지정된 키와 그 키에 매핑된 값을 Map에서 제거합니다.
containsKey(Object key): Map에 특정 키가 있는지 검사합니다.
containsValue(Object value): Map에 특정 값이 하나 이상 있는지 검사합니다.
keySet(): Map의 모든 키를 Set 형태로 반환합니다.
values(): Map의 모든 값을 컬렉션 형태로 반환합니다.
entrySet(): Map의 모든 “키-값” 쌍을 Set 형태의 Map.Entry 객체로 반환합니다.
size(): Map에 저장된 “키-값” 쌍의 개수를 반환합니다.
clear(): Map의 모든 요소를 제거합니다.
4.3 주요 구현체
‘Map’ 인터페이스의 주요 구현체로는 다음과 같은 클래스들이 있습니다.
HashMap : 가장 일반적으로 사용되는 ‘Map’ 구현체로, 해시 테이블을 사용합니다.
요소의 순서를 보장하지 않으며, 키와 값에 ‘null’ 을 허용합니다.
LinkedHashMap : ‘HashMap’ 을 상속받아 구현된 클래스로, 요소의 삽입 순서를 유지합니다.
이는 순회 시 삽인된 순서대로 요소를 얻을 수 있게 해줍니다.
TreeMap : 레드-블랙 트리를 기반으로 하는 ‘Map’ 구현체로, 모든 키가 자연적 순서대로 정렬됩니다.
정렬된 순서로의 접근이 필요할 때 유용합니다.
Hashtable : ‘HashMap’ 과 유사하지만, 모든 메소드가 동기화되어 있어 멀티스레드 환경에서 사용하기에 안전합니다.
그러나 성능이 ‘HashMap’ 보다 느리고, 키와 값에 ‘null’ 을 허용하지 않습니다.
4.4 📝 정리.
‘Map’ 인터페이스는 다양한 애플리케이션에서 설정, 프로파일, 사용자 세션 등의 데이터를 키와 값의 형태로 관리할 때 유용하게 사용됩니다.
-
☕️[Java] 입출력(2)
1️⃣ 입출력(2)
1. 파일 출력.
자바 프로그래밍에서 파일 출력은 프로그램이 데이터를 쓰는 과정을 말합니다.
이 과정을 통해 프로그램은 실행 결과를 저장하거나, 사용자가 입력한 정보를 파일에 기록하고, 다른 프로그램이나 나중에 프로그램 자체가 다시 사용할 수 있는 형태로 데이터를 출력할 수 있습니다.
2. 파일 출력을 수행하기 위한 기본 방법들.
1. FileOutputStream 사용
‘FileOutputStream’ 클래스는 바이트 단위의 출력을 파일에 직접 쓸 때 사용됩니다.
이 클래스를 사용하면 이미지, 비디오 파일, 이진 데이터 등을 파일로 저장할 수 있습니다.
```java
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputExample {
public static void main(String[] args) {
String data = “Hello, this is a test.”;
try (FileOutputStream out = new FileOutputStream(“output.txt”)) {
out.write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
- **2. PrintWriter 사용**
- **'PrintWriter'** 는 문자 데이터를 출력할 때 사용됩니다.
- 이 클래스는 파일에 텍스트를 쓸 때 편리하며, 자동 플러싱 기능, 줄 단위 출력 등의 메소드를 제공합니다.
```java
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class PrintWriteExample {
public static void main(String[] args) {
try (PrintWriter writer = new PrintWriter(new FileWriter("output.txt", true))) {
writer.println("Hello, this is a test.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. FileWriter 사용
‘FileWriter’ 는 자바에서 파일에 텍스트 데이터를 쓰기 위한 간편한 방법 중 하나입니다.
이 클래스는 내부적으로 문자 데이터를 파일에 쓸 수 있도록 ‘OutputStreamWriter’ 를 사용하여 바이트 스트림을 문자 스트림으로 변환합니다.
‘FileWriter’ 는 텍스트 파일을 쉽게 작성할 수 있도록 해주며, 생성자를 통해 다양한 방식으로 파일을 열 수 있습니다.
```java
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterExample {
pulbic static void main(String[] args) {
try (FileWriter writer = new FileWriter(“output.txt”, true)) {
writer.write(“Hello, this is a test.”);
} catch (IOException e) {
e.printStackTrace();
}
}
}
- **4. BufferedWriter 사용**
- **'BufferedWrite'** 는 버퍼링을 통해 효율적으로 파일에 문자 데이터를 쓸 수 있도록 합니다.
- **'FileWriter'** 와 함께 사용되어, 더 큰 데이터를 처리할 때 성능을 개선합니다.
```java
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWriterExample {
public static void main(String[] args) {
String content = "Hello, this is a test.";
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. 파일 입력.
자바 프로그래밍에서 파일 입력은 프로그램이 파일로부터 데이터를 읽어들이는 과정을 말합니다.
이 데이터는 텍스트나 바이너리 형태일 수 있으며, 파일에서 데이터를 읽어 프로그램 내에서 사용할 수 있도록 만드는 것이 목적입니다.
파일 입력을 위해 자바는 다양한 입출력 클래스를 제공합니다.
2.1 주로 사용되는 파일 입력 방법.
1. FileInputStream 사용
‘FileInputStream’ 은 바이트 단위로 파일에서 데이터를 읽는 데 사용됩니다.
이 클래스는 이미지, 비디오 파일, 실행 파일등의 이진 데이터 처리에 주로 사용됩니다.
```java
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream(“input.dat”)) {
int content;
while ((content = fis.read()) != -1) {
// content 변수에 한 바이트씩 읽어들인 데이터를 저장
System.out.print((char) content);
}
} catch (IOExecption e) {
e.printStackTrace();
}
}
}
- **2. BufferedRead** 와 **FileReader 사용**
- **'BufferedReader'** 와 **'FileReader'** 는 텍스트 데이터를 효과적으로 읽기 위해 함께 사용됩니다.
- **'FileReader'** 는 파일에서 문자 데이터를 읽어들이며, **'BufferedReader'** 는 버퍼링을 통해 읽기 성능을 향상 시킵니다.
```java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader br new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = br.readline()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. Scanner 사용
‘Scanner’ 클래스는 텍스트 파일을 읽을 때 유용하며, 특히 토큰화(tokenizing)된 데이터를 처리할 때 편리합니다.
‘Scanner’ 는 정규식을 사용하여 입력을 구분자로 분리하고, 다양한 타입으로 데이터를 읽어들일 수 있습니다.
```java
import java.io.File;
import java.util.Scanner;
public class ScannerExample {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File(“input.txt”))) {
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
2.2 📝 정리.
이렇게 다양한 방법을 통해 파일로부터 데이터를 읽을 수 있으며, 각 방법은 사용하는 데이터 타입과 처리할 데이터의 양에 따라 선택할 수 있습니다.
파일에서 데이터를 읽는 것은 데이터를 처리하거나, 설정 정보를 불러오거나, 사용자 데이터를 읽는 등 다양한 목적으로 활용됩니다.
-
☕️[Java] 예외 처리
1️⃣ 예외 처리
예외 처리가 무엇인지 이해하고, 예외 처리 방법에 대해 직접 구현
1. 예외(Exception)
자바 프로그래밍에서 “예외(Exception)” 란 프로그램 실행 중에 발생하는 비정상적인 조건 또는 오류를 의미합니다.
이는 프로그램의 정상적인 흐름을 방해하며, 적절히 처리하지 않으면 프로그램이 비정상적으로 종료될 수 있습니다.
자바에서는 이러한 예외를 효과적으로 처리하기 위해 강력한 예외 처리 메커니즘을 제공합니다.
1.2 예외의 유형.
자바에서 예외는 크게 두 가지 유형으로 나눌 수 있습니다.
1. Checked Execptions
컴파일 시간에 체크되는 예외로, 컴파일러가 해당 예외를 처리하도록 요구합니다.
이 예외들은 주로 외부 시스템과의 상호 작요(파일 입출력, 네트워크 통신 등)에서 발생하며, 프로그래머가 이를 적절히 처리하도록 강제합니다.
2. Unchecked Exceptions
런타임에 발생하는 예외로, 주로 프로그래머의 실수로 인해 발생합니다.(예: 배령의 범위를 벗어나는 접근, null 참조 등.)
이러한 예외는 컴파일러가 체크하지 않으므로, 개발자가 예측하고 적절히 처리할 필요가 있습니다.
2. 예외 처리(Exception Handling)
자바 프로그래밍에서 예외 처리는 프로그램 실행 중에 발생할 수 있는 예외적인 상황, 즉 오류나 문제를 안전하게 관리하고 대처하는 방법을 말합니다.
예외 처리를 통해 프로그램의 비정상적인 종료를 막고, 오류 발생 시 적절한 대응을 할 수 있도록 합니다.
이는 프로그램의 안정성과 신뢰성을 높이는 데 중요한 역할을 합니다.
2.1 예외 처리의 주요 구성 요소.
1. try 블록
예외가 발생할 가능성이 있는 코드를 이 블록 안에 넣습니다.
만약 블록 안의 코드 실행 중에 예외가 발생하면, 즉시 해당 블록의 실행을 중단하고 ‘catch’ 블록으로 제어를 넘깁니다.
2. catch 블록
‘try’ 블록에서 발생한 특정 유형의 예외를 처리합니다.
프로그램이 예외를 안전하게 처리할 수 있도록 적절한 로직을 구현할 수 있습니다.
하나의 ‘try’ 블록에 여러 ‘catch’ 블록을 사용하여 다양한 종류의 예외를 각각 다르게 처리할 수 있습니다.
3. finally 블록
이 블록은 예외의 발생 여부롸 관계없이 실행되는 코드를 포함합니다.
주로 사용되는 목적은 자원 해제와 같은 정리 작업을 수행하기 위함입니다.
예를 들어 파일이나 네트워크 자원을 닫거나 해제할 때 사용됩니다.
4. throws 키워드
메소드 선언 시 사용되며, 해당 메소드가 예외를 직접 처리하지 않고 호출한 메소드로 예외를 전파하겠다는 것을 나타냅니다.
이를 통해 예외 처리의 책임을 메소드 호출자에게 넘길 수 있습니다.
2.2 예외 처리 예제.
public class ExceptionHandlingExample {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.err.println("Arithmetic Exception: Division by zero is not allowed.");
} finally {
System.out.println("This block is always executed.");
}
}
public static int divide(int numerator, in denominator) {
return numerator / denominator; // This ca throw ArithmeticException if denominator is zero.
}
}
이 예제에서 ‘divide’ 메소드는 분모가 0일 때 ArithmeticException 을 발생시킬 수 있습니다.
‘try’ 블록 안에서 이 메소드를 호출하고, 예외가 발생하면 ‘catch’ 블록에서 이를 잡아서 적절한 오류 메시지를 출력합니다.
또한, ‘finally’ 블록은 예외 발생 여부와 상관없이 실행되어 어떤 상황에서도 실행될 필요가 있는 코드를 포함할 수 있습니다.
3. throw 키워드.
자바 프로그래밍에서 ‘throw’ 키워드는 개발자가 의도적으로 예외를 발생시키기 위해 사용합니다.
이를 통해 특정 상황에서 프로그램의 흐름을 제어하거나, 특정 조건에서 오류를 발생시켜 예외 처리 메커니즘을 테스트하거나 강제할 수 있습니다.
‘throw’ 는 예외 객체를 생성하고 이를 던집니다(throw)
즉, 프로그램의 정상적인 실행 흐름을 중단하고 예외 처리 루틴으로 제어를 이동시킵니다.
3.1 ‘throw’ 사용법.
‘throw’ 를 사용할 때는 예외 객체를 생성해야 합니다.
이 객체는 ‘Throwable’ 클래스 또는 그 하위 클래스의 인스턴스여야 합니다.
자바에서는 대부분 ‘Exception’ 클래스 또는 그 서브클래스를 사용하여 예외를 생성하고 던집니다.
예제.
public class Main {
public static void main(String[] args) {
try {
checkAge(17);
} catch (Exception e) {
System.out.println("Exception caught: " + e.getMessage());
}
}
static void checkAge(int age) throws Execption {
if (age < 18) {
throw new Exception("Access denied - You must be at least 18 years old.");
}
System.out.println("Access granted - You are old enough!");
}
}
이 예제에서 ‘checkAge’ 메소드는 나이를 확인하고, 18세 미만인 경우 예외를 던집니다.
이 예외는 ‘throw new Exception(…)’ 을 통해 생성되고 던져집니다.
메인 메소드에서는 이 메소드를 ‘try’ 블록 안에서 호출하고, ‘catch’ 블록을 통해 예외를 잡아서 처리합니다.
결과적으로, 사용자가 18세 미만이면 “Access denided” 메시지를 포함하는 예외가 출력됩니다.
3.2 ‘throw’와 ‘throws’의 차이
‘throw’ : 예외를 실제로 발생시키는 행위입니다. 이는 메소드 내부에서 특정 조건에서 예외를 발생시킬 때 사용됩니다.
‘throws’ : 메소드 선언에 사용되며, 해당 메소드가 실행되는 동안 발생할 수 있는 예외를 명시적으로 선언합니다. 이는 호출자에게 해당 메소드를 사용할 때 적절한 예외 처리가 필요하다는 것을 알립니다.
4. throws 키워드.
자바 프로그래밍에서 ‘throws’ 키워드는 메소드 선언에 사용되며, 해당 메소드가 실행 도중 발생할 수 있는 특정 유형의 예외를 명시적으로 선언하는 데 사용됩니다.
‘throws’ 는 메소드가 예외를 직접 처리하지 않고, 대신 이를 호출한 메소드로 예외를 “던져”(전파하는) 사실을 알립니다.
이를 통해 예외 처리 책임을 메소드 호출자에게 넘기는 것입니다.
4.1 ‘throws’ 사용의 목적.
명시성
메소드가 발생시킬 수 있는 예외를 명시함으로써, 이 메소드를 사용하는 다른 개발자들에게 해당 메소드를 사용할 때 어떤 예외들을 처리해야 하는지 명확하게 알릴 수 있습니다.
강제 예외 처리
‘throws’ 로 선언된 예외는 대부분 “checked exception” 이며, 이는 메소드를 호출하는 코드가 반드시 이 예외들을 처리하도록 강제합니다(try-catch 블록을 사용하거나, 또 다시 ‘throws’ 로 예외를 전파하도록 함).
4.2 ‘throws’ 사용법 예제.
아래 예제에서는 ‘throws’ 를 사용하여 ‘IOException’ 을 처리하는 방법을 보여줍니다.
이 예외는 파일 입출력 작업에서 자주 발생합니다.
import java.io.*;
public class Main {
public static void main(String[] args) {
try {
readFile("example.txt");
} catch (IOExecption e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
public static void readFile(String filename) throws IOException {
File file = new File(filename);
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
int content;
while ((content = fis.read()) != -1) {
// Process the content
System.out.print((char) content);
}
} finally {
if (fis != null) {
fis.close();
}
}
}
}
이 예제에서 ‘readFile’ 메소드는 파일을 읽을 때 발생할 수 있는 IOException 을 처리하지 않고, 대신 ‘throws’ 키워드를 사용하여 이 예외를 메소드를 호출한 ‘main’ 메소드로 전달합니다.
‘main’ 메소드는 이 예외를 ‘catch’ 블록을 통해 처리합니다.
-
☕️[Java] 인터페이스
1️⃣ 인터페이스.
1. 인터페이스(Interface).
자바에서 인터페이스(Interface)는 메서드의 시그니처만을 정의하는 참조 타입입니다.
인터페이스는 클래스가 구현(implement) 해야 하는 동작의 설계를 제공하며, 구현하는 모든 클래스에 대해 특정 메소드들이 반드시 존재하도록 강제합니다.
이는 다형성을 지원하는 강력한 방법으로, 서로 다른 클래스들이 동일한 인터페이스를 구현함으로써 동일한 방식으로 처리될 수 있게 해 줍니다.
1.2 인터페이스의 주요 특징.
1. 메소드 선언만 포함 : 인터페이스는 메소드의 구현을 포함하지 않습니다.(자바 8 이전까지는).
메소드의 몸체는 인터페이스를 구현하는 클래스에서 제공됩니다.
2. 상수만 포함 가능 : 인터페이스는 상수만을 멤버로 가질수 있습니다.
모든 필드는 ‘public’, ‘static’, ‘final’ 로 선언됩니다.
3. 다중 구현 지원 : 한 클래스는 여러 인터페이스를 구현할 수 있으며, 이를 통해 다중 상속의 이점을 얻을 수 있습니다.
4. 디폴트 메소드와 정적 메소드 : 자바 8 이후부터는 인터페이스에 디폴트 메소드(구현을 포함하는 메소드)와 정적 메소드를 정의할 수 있게 되었습니다.
이를 통해 더 유연한 설계가 가능해졌습니다.
1.3 인터페이스 정의 예시.
public interface Vehicle {
void start();
void stop();
}
위 예제에서 ‘Vehicle’ 인터페이스는 ‘start’ 와 ‘stop’ 이라는 두 메소드를 정의합니다.
이 인터페이스를 구현하는 모든 클래스는 이 두 메소드의 구체적인 구현을 제공해야 합니다.
1.4 인터페이스 구현 예.
public class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car starts.");
}
@Override
public void stop() {
System.out.println("Car stops.");
}
}
‘Car’ 클래스는 ‘Vehicle’ 인터페이스를 구현합니다.
이 클래스는 start 와 ‘stop’ 메소드를 구체적으로 구현해야 합니다.
1.5 결론.
인터페이스는 클래스와 다른 클래스 사이의 계약을 정의하고, 특정 작업을 수행하는 메소드의 시그니처를 강제합니다.
이는 코드의 상호 운용성을 높이고, 다형성을 통한 유연한 프로그래밍 설계를 가능하게 합니다.
인터페이스를 사용함으로써 다양한 구현체를 동일한 방식으로 처리할 수 있어, 코드의 유지보수성과 확장성이 향상됩니다.
2. 상수(constant).
자바 프로그래밍에서 상수(constant)는 값이 선언 후 변경될 수 없는 변수를 의미합니다.
상수는 일반적으로 프로그램 전체에서 변하지 않는 값에 사용되며, 이는 코드의 읽기 쉬움과 유지 관리를 돕습니다.
자바에서 상수를 선언하기 위해 ‘final’ 키워드를 변수 선언과 함께 사용합니다.
2.1 상수의 특징.
1. 불변성 : 상수는 한 번 초기화되면 그 값이 변경될 수 없습니다.
2. 명확성 : 코드 내에서 직접적인 값보다는 의미 있는 이름을 가진 상수를 사용함으로써 코드의 가독성과 유지보수성이 향상됩니다.
3. 공용 사용 : 자주 사용되는 값이나 의미가 명확한 수치를 상수로 선언하여 코드 전바에 걸쳐 재사용할 수 있습니다.
2.2 상수 선언 예시.
상수를 선언하는 방법은 간단합니다.
‘final’ 키워드를 사용하여 변수를 선언하고, 초기화합니다.
일반적으로 상수의 이름은 대문자로 표기하며, 단어 간에는 언더스코어(‘_‘)를 사용합니다.
이는 상수임을 쉽게 식별할 수 있도록 도와줍니다.
public class Constants {
public static final int MAX_WIDTH = 800;
public static final int MAX_HEIGHT = 600;
public static final String COMPANY_NAME = "MyCompany";
}
위 예에서 ‘MAX_WIDTH’, ‘MAX_HEIGHT’, ‘COMPANY_NAME’ 은 모두 상수이며, 이들의 값은 선언된 후 변경될 수 없습니다.
2.3 상수 사용의 이점.
오류 감소 : 값이 한 번 설정되면 변경되지 않기 때문에, 예상치 못한 곳에서 값이 변경되어 발생할 수 있는 버그를 줄일 수 있습니다.
코드 재사용성 : 한 곳에서 값을 변경하면, 해당 상수를 사용하는 모든 위치에서 변경된 값이 적용됩니다. 이는 일관성 유지와 함께 코드 관리를 간소화합니다.
컴파일 시간 최적화 : 상수 값은 컴파일 시간에 결정되므로, 런타임에 추가적인 계산 비용이 들지 않습니다.
2.4 결론.
상수는 프로그램 내에서 변하지 않는 값을 나타내며, 코드의 안정성과 유지보수성을 높이는 데 중요한 역할을 합니다.
자바에서는 ‘final’ 키워드를 사용하여 이러한 상수를 쉽게 생성할 수 있습니다.
3. 클래스의 상속과 인터페이스의 구현을 동시에 사용.
자바에서는 클래스의 상속과 인터페이스의 구현을 동시에 사용하여 “다중 상속“과 유사한 효과를 얻을 수 있습니다.
이는 자바의 설계에서 클래스는 단일 상속만을 허용하지만, 인터페이스는 다중으로 구현할 수 있게 함으로써 이루어집니다.
3.1 단일 상속과 다중 인터페이스 구현.
단일 상속 : 자바에서 클래스는 단 하나의 상위 클래스만 상속받을 수 있습니다.
이는 C++ 같은 언어에서 볼 수 있는 다중 상속의 복잡성과 관련된 문제(예: 다이아몬드 문제)를 피하기 위함입니다.
다중 인터페이스 구현 : 한 클래스는 여러 인터페이스를 구현할 수 있습니다.
이는 인터페이스가 구체적인 구현을 포함하지 않기 때문에(자바 8 이전까지, 자바 8 이후에는 디폴트 메소드를 통해 일부 구현을 포함할 수 있음), 클래스가 여러 인터페이스를 구현함으로써 다중 상속의 효과를 나타낼 수 있습니다.
3.2 예시.
다음 예시에서 ‘Car’ 클래스는 ‘Vehicle’ 클래스를 상속받고, ‘Electric’ 및 ‘Autonomous’ 두 인터페이스를 구현하고 있습니다.
이를 통해 ‘Car’ 클래스는 ‘Vehicle’ 클래스의 속성과 메소드를 상속받으며, 동시에 두 인터페이스의 메소드를 구현해야 합니다.
class Vehicle {
void drive() {
System.out.println("This vehicle is driving.");
}
}
interface Electric {
void charge();
}
interface Autonomous {
void navigate();
}
class Car extends Vehicle implements Electric, Autonomous {
@Override
public void charge() {
System.out.println("The car is charging.");
}
@Override
public void navigate() {
System.out.println("The car is navigating autonomously.");
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car();
myCar.drive();
myCar.charge();
myCar.navigate();
}
}
3.4 결론.
자바에서는 한 클래스가 단일 상속을 통해 한 클래스의 기능을 상속받고, 동시에 여러 인터페이스를 구현함으로써 다중 상속의 효과를 얻을 수 있습니다.
이는 자바의 타입 시스템이 제공하는 유연성을 활용하는 좋은 예시로, 소프트웨어 설계에서 필요한 다양한 기능을 조합할 수 있게 해 줍니다.
-
-
☕️[Java] 내부 클래스
1️⃣ 내부 클래스.
내부 클래스의 개념과 종류 이해
익명 클래스 직접 구현
1. 내부 클래스(Inner Class).
자바 프로그래밍에서 내부 클래스(Inner Class)는 다른 클래스의 내부에 정의된 클래스를 말합니다.
내부 클래스는 주로 외부 클래스와 밀접한 관련이 있으며, 외부 클래스의 멤버들에 대한 접근을 용이하게 하기 위해 사용됩니다.
1.1 내부 클래스의 특징.
자바의 내부 클래스에는 몇 가지 특징이 있습니다.
이 특징들은 내부 클래스가 어떻게 사용되고, 그들이 주는 이점과 한계를 이해하는 데 도움이 됩니다.
1. 접근성과 밀접성 : 내부 클래스는 외부 클래스의 모든 필드와 메소드(프라이빗 포함)에 접근할 수 있습니다. 이는 내부 클래스가 외부 클래스와 밀접한 작업을 수행할 때 매우 유용합니다.
이러한 접근은 내부 클래스가 외부 클래스의 구현 세부사항에 깊이 연결될 수 있게 합니다.
2. 캠슐화 증가 : 내부 클래스를 사용하면 관련 있는 부분만을 그룹화하여 외부에 불필요한 정보를 노출하지 않고도 복잡한 코드를 더 잘 구조화할 수 있습니다.
이는 코드의 유지보수성과 가독성을 높이는 데 도움이 됩니다.
3. 코드의 응집성 : 내부 클래스는 특정 외부 클래스와 매우 강하게 연결되어 있기 때문에, 그 기능이 외부 클래스와 밀접하게 관련된 기능을 수행할 때 코드의 응집력을 높일 수 있습니다.
4. 더 나은 논리적 그룹핑 : 특정 기능을 내부 클래스에 구현함으로써, 관련 기능과 데이터를 함께 논리적으로 그룹화할 수 있습니다.
이는 전체 코드베이스를 통해 일관성을 유지하고, 기능별로 코드를 정리하는 데 도움이 됩니다.
5. 명시적인 컨텍스트 연결 : 내부 클래스는 명시적으로 그들의 외부 클래스의 인스턴스와 연결됩니다. 이는 그들이 외부 클래스의 상태와 행동에 따라 다르게 작동할 수 있음을 의미합니다.
6. 다중 상속의 일종의 구현 : 자바는 다중 상속을 지원하지 않지만, 내부 클래스를 통해 비슷한 효과를 낼 수 있습니다. 외부 클래스가 하나 이상의 내부 클래스를 가질 수 있고, 각 내부 클래스는 다른 클래스를 상속받을 수 있으므로 다양한 기능을 조합할 수 있습니다.
7. 메모리 및 성능 고려사항 : 내부 클래스는 외부 클래스의 인스턴스와 연결되어 있기 때문에, 외부 클래스의 인스턴스가 메모리에 남아 있는 동안에는 가비지 컬렉션에서 제거되지 않습니다. 이는 메모리 관리 측면에서 고려해야 할 사항입니다.
1.2 내부 클래스의 네 가지 유형.
1. 비정적 중첩 클래스(Non-static Nested Class) 또는 내부 클래스(Inner Class) : 이 클래스는 외부 클래스의 인스턴스 멤버처럼 동작하며, 외부 클래스의 인스턴스에 대한 참조를 가지고 있습니다. 외부 클래스의 인스턴스 멤버와 메소드에 접근할 수 있습니다.
2. 정적 중첩 클래스(Static Nested Class) : 이 클래스는 외부 클래스의 정적 멤버처럼 동작하며, 외부 클래스의 인스턴스 멤버에는 접근할 수 없지만, 정적 멤버에는 접근할 수 있습니다.
3. 지역 클래스(Local Class) : 특정 메소드 또는 초기화 블록 내부에 정의된 클래스로, 선언된 영역 내에서만 사용할 수 있습니다. 지역 클래스는 해당 메소드 내에서만 사용되므로, 외부로 노출되지 않습니다.
4. 익명 클래스(Anonymous Class) : 이름이 없는 클래스로, 일반적으로 단 한 번만 사용되며 주로 리스너(listener) 또는 작은 델리게이션 클래스로 사용됩니다. 클래스 선언과 인스턴스 생성이 동시에 이루어집니다.
-
☕️[Java] 다형성
1️⃣ 다형성.
1. 다형성(Polymorphism)
자바에서 말하는 다형성(Polymorphism)은 객체가 여러 형태를 취할 수 있는 능력을 말합니다.
이는 같은 이름의 메소드 호출이 객체의 타입에 따라 다은 동작을 수행할 수 있게 해 주어 코드의 유연성과 재사용성을 증가시킵니다.
자바에서는 주로 두 가지 형태의 다형성을 지원하는데, 이는 컴파일 시간 다형성과 런타임 다형성입니다.
1.2. 컴파일 시간 다형성(정적 다형성).
컴파일 시간 다형성은 주로 메소드 오버로딩을 통해 구현됩니다.
메소드 오버로딩은 동일한 메소드 이름을 가지면서 매개변수 타입, 순서, 개수가 다른 여러 메소드를 같은 클래스 내에 선언하는 것을 의미합니다.
이러한 메소드들은 컴파일 시에 그 타입에 따라 구별되어 처리됩니다.
1.3 컴파일 시간 다형성 예시.
public class Display {
public void print(int num) {
System.out.println("Printing integer: " + num);
}
public void print(String str) {
System.out.println("Printing string: " + str);
}
}
1.4 런타임 다형성(동적 다형성).
런타임 다형성은 메소드 오버라이딩을 통해 구현됩니다.
이 경우 서브클래스에서 상속받은 부모 클래스의 메소드를 재정의하여 동일한 메소드 호출이 서로 다른 클래스 객체에 대해 다른 동작을 할 수 있도록 합니다.
이는 실행 중에 결정되므로 동적 다형성이라고 합니다.
1.5 런타임 다형성 예시.
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
void sound() {
System.out.println("Cat meows");
}
}
public class TestPolymorphism {
public static void main(String[] args) {
Animal myAnimal = new Dog();
myAnimal.sound(); // 출력: Dog barks
myAnimal = new Cat();
myAnimal.sound(); // 출력: Cat meows
}
}
여기서 ‘Animal’ 클래스의 ‘sound()’ 메소드는 ‘Dog’ 와 Cat 클래스에서 오버라이딩되었습니다.
‘myAnimal’ 참조 변수는 ‘Animal’ 타입이지만, 참조하는 객체의 실제 타입에 따라 적절한 ‘sound()’ 메소드가 호출됩니다.
1.6 다형성의 장점.
유연성 : 다형성을 사용하면 프로그램을 더 유연하게 설계할 수 있습니다.
예를 들어, 다양한 지식 클래스의 객체들을 부모 클래스 타입의 컬렉션에 저장하고, 각 객체에 대해 공통된 인터페이스를 통해 작업을 수행할 수 있습니다.
코드 재사용과 유지 보수의 향상 : 공통 인터페이스를 사용함으로써 코드를 재사용하고, 새로운 클래스 타입을 추가하거나 기존 클래스를 수정할 때 유지 보수가 용이해집니다.
📝 정리.
이렇게 다형성은 객체 지향 프로그래밍의 중요한 특성 중 하나로, 프로그램의 다양한 부분에서 유용하게 활용됩니다.
2. instanceof
자바 프로그래밍에서 ‘instanceof’ 연산자는 특정 객체가 지정한 타입의 인스턴스인지를 검사하는 데 사용됩니다.
이 연산자는 객체의 타입을 확인할 때 유용하게 쓰이며, 주로 객체의 실제 타입을 판별하여 안전하게 형 변환을 하기 전이나 특정 타입에 따른 조건 분기를 실행할 때 사용됩니다.
2.1 instanceof 연산자의 사용법.
‘instanceof’ 는 구 개의 피 연산자를 비교합니다.
왼쪽 피연산자는 객체를 나타내며, 오른쪽 피연산자는 타입(클래스나 인터페이스)을 나타냅니다.
연산의 결과는 불리언 값입니다.
만약 왼쪽 피연산자가 오른쪽 피연산자가 지정하는 타입의 인스턴스면 ‘true’ 를, 그렇지 않으면 ‘false’ 를 반환합니다.
기본 구조
if (object instanceof ClassName) {
// 조건이 참일 때 실행될 코드
}
예시
class Animal {}
class Dog extends Animal {}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
Dog dog = new Dog();
Animal animalDog = new Dog();
System.out.println(animal instanceof Animal); // true
System.out.println(dog instanceof Animal); // true
System.out.println(animalDog instanceof Animal); // true
System.out.println(animal instanceof Dog); // false
}
}
이 예시에서 ‘dog instanceof Animal’ 은 ‘true’ 를 반환합니다.
왜냐하면 ‘Dog’ 클래스가 ‘Animal’ 클래스의 서브클래스이기 때문입니다.
하지만 ‘animal instanceof Dog’ 은 ‘false’ 를 반환하는데, 이는 ‘Animal’ 인스턴스가 ‘Dog’ 타입이 아니기 때문입니다.
2.2 instanceof의 주의점
1. null 검사 : ‘instanceof’ 는 객체 참조가 ‘null’ 일 때 항상 ‘false’ 를 반환합니다.
따라서 ‘null’ 값에 대한 추가적인 검사 없이도 안전하게 사용할 수 있습니다.
2. 다운캐스팅 검증 : 객체를 하위 클래스 타입으로 다운캐스팅하기 전에 ‘instanceof’ 를 사용하여 해당 객체가 실제로 해당 하위 클래스의 인스턴스인지를 확인하는 것이 안전합니다.
이를 통해 ‘ClassCastException’ 을 예발할 수 있습니다.
3. 인터페이스 검사 : ‘instanceof’ 는 클래스 뿐만 아니라 인터페이스 타입에 대해서도 사용할 수 있습니다. 객체가 특정 인터페이스를 구현하는지 여부를 검사할 수 있습니다.
📝 정리.
‘instanceof’ 는 다형성을 사용하는 객체 지향 프로그램에서 객체의 타입을 안전하게 확인하고, 타입에 맞는 적절한 동작을 수행하도록 도와주는 중요한 도구입니다.
3. 업캐스팅(Upcasting).
자바 프로그래밍에서 업캐스팅(Upcasting)은 서브클래스의 객체를 슈퍼클래스 타입의 참조로 변환하는 과정을 말합니다.
이는 일반적으로 자동으로 수행되며, 명시적으로 타입을 지정할 필요가 없습니다.
업캐스팅은 객체 지향 프로그래밍의 다형성을 활용하는 데 핵심적인 역할을 합니다.
3.1 업캐스팅의 특징과 이점.
1. 자동 형 변환 : 자바에서는 서브클래스의 객체를 슈퍼클래스 타입의 탐조 변수에 할당할 때 자동으로 업캐스팅이 발생합니다.
2. 안전성 : 업캐스팅은 항상 안전하며, 데이터 손실이나 오류 없이 수행됩니다. 이는 서브클래스가 슈퍼클래스의 모든 특성을 상속받기 때문입니다.
3. 다형적 행동 : 업캐스팅을 통해 서브클래스의 객체들을 슈퍼클래스 타입으로 다룰 수 있어, 다양한 타입의 객체들을 일관된 방식으로 처리할 수 있습니다. 이를 통해 코드의 유연성과 재사용성이 향상됩니다.
3.2 예시.
아래는 업캐스팅을 사용한 자바 코드 예시입니다.
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking");
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
Animal myAnimal = myDog; // Dog 객체를 Animal 타입으로 업캐스팅
myAnimal.eat(); // 호출 가능
// myAnimal.bark(); // 컴파일 에러, Animal 타입은 bark 메소드를 알지 못함
}
}
이 예시에서 ‘Dog’ 객체가 ‘Animal’ 타입으로 업캐스팅 되었습니다.
‘myAnimal’ 변수는 ‘Animal’ 클래스의 메소드만 호출할 수 있으며, ‘Dog’ 클래스의 ‘bark()’ 메소드는 호출할 수 없습니다.
3.3 업캐스팅 후의 제한사항.
업캐스팅을 한 후에는 원래 서브클래스의 특정 메소드나 속성에 접근할 수 없게 됩니다.
즉, 업캐스팅된 객체는 슈퍼클래스의 필드와 메소드만 사용할 수 있으며, 추가된 서브클래스의 특성은 사용할 수 없습니다.
이는 다형성의 한 예로서, 슈퍼 클래스 타입을 통해 다양한 서브클래스의 객체들을 통합적으로 다룰 수 있도록 해주며, 프로그램을 더 유연하고 확장 가능하게 만듭니다.
4. 다운캐스팅(Downcasting).
자바 프로그래밍에서 다운캐스팅(Downcasting)은 슈퍼클래스 타입의 객체 참조를 서브클래스 타입의 참조로 변환하는 과정을 말합니다.
다운캐스팅은 업캐스팅의 반대 과정으로, 업캐스팅된 객체를 다시 원래의 서브클래스 타입으로 변환할 때 사용됩니다.
다운캐스팅은 명시적으로 수행되어야 하며, 자바에서는 이 과정이 자동으로 이루어지지 않습니다.
4.1 다운캐스팅의 필요성.
업캐스팅을 통해 객체가 슈퍼클래스 타입으로 변환되면, 해당 객체는 슈퍼클래스의 메소드와 필드만 접근 가능합니다.
서브클래스에만 있는 메소드나 필드에 접근하려면 다운캐스팅을 사용하여 해당 객체를 다시 서브클래스 타입으로 변환해야 합니다.
4.2 다운캐스팅의 사용법과 주의사항.
다운캐스팅은 타입 캐스팅 연산자를 사용하여 수행되며, 반드시 ‘instanceof’ 연산자로 타입 체크를 먼저 수행하는 것이 안전합니다.
이는 변환하려는 객체가 실제로 해당 서브클래스의 인스턴스인지 확인하여 ‘ClassCastExecption’ 을 방지하기 위함입니다.
4.3 예시.
다운캐스팅을 사용하는 자바 코드 예시입니다.
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 업캐스팅
myAnimal.eat();
// 다운캐스팅 전에 instanceof로 체크
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal; // 다운캐스팅
myDog.bark(); // 이제 서브클래스의 메소드 호출 가능
}
}
}
이 예시에서 ‘Animal’ 타입의 ‘myAnimal’ 객체는 ‘Dog’ 클래스의 인스턴스입니다.
‘myAnimal’ 을 ‘Dog’ 타입으로 다운캐스팅하여 ‘Dog’ 클래스의 ‘bark()’ 메소드에 접근할 수 있습니다.
다운캐스팅을 수행하기 전에 ‘instanceof’ 를 사용해 ‘myAnimal’ 이 실제로 ‘Dog’ 의 인스턴스인지 확인함으로써 안정을 확보합니다.
4.4 주의사항.
다운캐스팅은 객체의 실제 타입이 캐스팅하려는 클래스 타입과 일치할 때만 안전하게 수행됩니다.
잘못된 다운캐스팅은 런타임에 ‘ClassCastException’ 을 발생시킬 수 있습니다.
📝 정리.
다운캐스팅은 특정 상황에서 필수적이며, 객체의 모든 기능을 활용하기 위해 사용되지만, 항상 타입 검사를 수행하고 신중하게 사용해야 합니다.
-
☕️[Java] 추상클래스
1️⃣ 추상클래스.
추상 클래스가 무엇인지 설명할 수 있음
abstract를 이용하여 추상 클래스 구현
1. 추상 메소드(Abstract Method)
자바 프로그래밍에서 추상 메소드(abstract method)는 선언만 있고 구현은 없는 메소드입니다.
이러한 메소드는 추상 클래스(abstract class)나 인터페이스(interface) 내부에서 선언될 수 있으며, 구체적인 행동은 하위 클래스에서 구현됩니다.
추상 메소드를 사용하는 주된 목적은 하위 클래스가 특정 메소드를 반드시 구현하도록 강제하는 것입니다.
이는 코드의 일관성을 유지하고, 다형성을 통한 유연한 프로그래밍 설계를 가능하게 합니다.
1.2 추상 메소드의 특징.
선언만 있고 구현이 없음 : 메소드 본체가 없으며, 메소드 선언은 세미콜론(’;’) 으로 끝납니다.
하위 클래스에서의 구현 필수 : 추상 메소드를 포함하는 클래스를 상속받는 모든 하위 클래스는 해당 메소드를 구현해야만 인스턴스 생성이 가능합니다.
‘abstract’ 키워드 사용 : 메소드 앞에 ‘abstract’ 키워드를 명시하여 추상 메소드임을 표시합니다.
1.3 추상 메소드 예시
다음은 추상 클래스와 추상 메소드의 간단한 예시입니다.
abstract class Animal {
// 추상 메소드
abstract void makeSound();
void breathe() {
System.out.println("Btrathing...");
}
}
class Dog extends Animal {
// 추상 메소드 구현
void makeSound() {
System.out.println("Bark!");
}
}
class Cat extends Animal {
// 추상 메소드 구현
void makeSound() {
System.out.println("Meow!");
}
}
위 예에서 ‘Animal’ 클래스는 ‘makeSound’ 라는 추상 메소드를 포함하고 있습니다.
‘Dog’ 와 ‘Cat’ 클래스는 ‘Animal’ 클래스를 상속받고 ‘makeSound’ 메소드를 각각 다르게 구현하고 있습니다.
이는 다형성의 좋은 예로, ‘Animal’ 타입의 참조를 사용하여 각각의 하위 클래스 객체를 다룰 때 동일한 메소드(‘makeSound’)를 호출하더라도 서로 다른 행동(개는 짖고, 고양이는 울음)을 보여줍니다.
1.4 결론.
추상 메소드는 프로그램의 확장성과 유지보수성을 향상시키는 객체 지향 설계의 핵심 요소입니다.
다양한 상황에 맞춰 동일한 인터페이스에 여러 구현을 제공할 수 있어 유연한 코드 작성이 가능합니다.
2. 추상 클래스(abstract class)
자바에서 추상 클래스(abstract class)는 완전하지 않은 클래스로, 추상 클래스 자체로는 인스턴스를 생성할 수 없습니다.
추상 클래스의 주요 목적은 다른 클래스들의 기본이 되는 클래스를 제공하여 코드의 재사용성을 높이고, 일관된 설계를 유도하는 것입니다.
추상 클래스는 하나 이상의 추상 메소드를 포함할 수 있으며, 또한 구현된 메소드도 포함할 수 있습니다.
2.1 추상 클래스의 특징.
1. 인스턴스 생성 불가 : 추상 클래스는 직접적으로 인스턴스를 생성할 수 없습니다. 반드시 상속을 통해 그 기능을 확장하고 구체적인 클래스에서 인스턴스를 생성해야 합니다.
2. 추상 메소드 포함 가능 : 추상 클래스는 하나 이상의 메소드를 포함할 수 있습니다. 추상 메소드는 선언만 있고 구현은 없으며, 이를 상속받은 구체적인 클래스에서 구현해야 합니다.
3. 구현된 메소드 포함 가능 : 추상 클래스는 구현된 메소드도 포함할 수 있어, 자식 클래스들이 이 메소드를 재사용하거나 오버라이드 할 수 있습니다.
4. 생성자 및 필드 포함 가능 : 추상 클래스는 자신의 생성자와 필드(변수)를 가질 수 있으며, 이는 상속받은 클래스에서 사용할 수 있습니다.
2.2 추상 클래스의 사용 예시.
abstract class Animal {
abstract void makeSound();
void eat() {
System.out.println("This animal is eating.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow!");
}
}
이 예에서 ‘Animal’ 은 추상 클래스로, ‘makeSound()’ 메소드를 추상 메소드로 포함하고 있습니다.
‘Dog’ 와 ‘Cat’ 은 ‘Animal’ 클래스를 상속받아 ‘makeSound()’ 메소드를 각각 구현합니다.
추상 클래스 ‘Animal’ 의 ‘eat()’ 메소드는 모든 동물이 공통적으로 사용할 수 있는 구현된 메소드입니다.
2.3 결론.
추상 클래스는 공통적인 특징을 가진 클래스들 사이의 일반적인 행동을 정의하고, 이를 상속받는 구체적인 클래스들이 이를 구현하도록 하는 데에 주로 사용됩니다.
이를 통해 코드의 재사용성과 유지보수성을 향상시키며, 객체 지향 설계의 일관성과 안정성을 보장할 수 있습니다.
3. 익명 클래스(anonymous class).
자바에서 익명 클래스(anonymous class)는 이름이 없는 클래스입니다.
이들은 주로 일회성 사용 목적으로 설계되며, 인터페이스나 추상 클래스를 간편하게 구현하거나, 기존 클래스를 임시로 확장하기 위해 사용됩니다.
익명 클래스는 일반적으로 이벤트 리스너나 작은 콜백 객체 같이 간단한 기능을 수행하는 데에 활용됩니다.
3.1 익명 클래스의 특징.
1. 이름이 없음 : 익명 클래스는 이름을 가지지 않습니다. 인스턴스 생성 시점에 정의됩니다.
2. 즉석에서 정의 및 사용 : 익명 클래스는 즉석에서 정의되어 바로 인스턴스가 생성됩니다. 보통 이들은 한 번만 사용되고 재사용되지 않습니다.
3. 상속 및 구현 : 익명 클래스는 상쉬 클래스를 상속하거나 인터페이스를 구현할 수 있습니다. 그러나 다중 상속은 지원하지 않습니다.
4. 오직 하나의 인스턴스만 생성 가능 : 익명 클래스로부터 직접적으로 두 개 이상의 객체를 생성할 수는 없습니다. 다시 사용하려면 클래스 정의를 반복해야 합니다.
5. 지역 클래스 비슷 : 지역 변수처럼 동작하여 주변 스코프의 변수를 참조할 수 있습니다. 자바 8 이전에는 final 변수만 참조 가능했으나, 자바 8부터는 effectively final(명시적으로 final로 선언되지 않았어도 값이 변경되지 않는 변수) 변수도 참조할 수 있습니다.
3.2 익명 클래스의 사용 예.
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("버튼이 클릭되었습니다!");
}
});
위 예제에서 ‘ActionListener’ 인터페이스는 익명 클래스를 통해 구현되었습니다.
‘button.addActionListener’ 메소드에 직접 전달되면서 버튼 클릭 시 “버튼이 클릭되었습니다!”를 출력하는 ‘actionPerformed’ 메소드를 포함하고 있습니다.
3.3 결론.
익명 클래스는 특정 인터페이스나 상위 클래스의 메소드를 구현하거나 오버라이드할 때 사용됩니다.
짧고 간단한 기능을 구현하는 데 유용하며, 코드의 간결성을 유지할 수 있게 도와줍니다.
하지만 복잡한 로직이나 반복적으로 사용될 기능에 대해서는 일반 클래스나 지역 클래스를 사용하는 것이 더 적합할 수 있습니다.
-
☕️[Java] 상속
1️⃣ 상속.
1. 상속(Inheritance)
자바 프로그래밍에서의 상속(Inheritance)은 한 클래스가 다른 클래스의 속성과 메소드를 물려받는 기능을 말합니다.
상속을 사용하면 기존 코드를 재사용하고 확장하는 것이 용이해져, 소프트웨어의 설계와 유지 보수가 효율적으로 이루어질 수 있습니다.
1.2 상속의 주요 개념.
1. 슈퍼클래스(부모 클래스) : 기능이 상속되는 클래스입니다.
예를 들어, ‘Vehicle’ 클래스가 있을 때 클래스의 속성(예: 속도)과 메소드(예: start, stop)를 다른 클래스가 상속받을 수 있습니다.
2. 서브클래스(자식 클래스) : 슈퍼클래스의 속성과 메소드를 상속받는 클래스입니다.
서브클래스는 메소드를 그대로 사용할 수도 있고, 필요에 따라 재정의(오버라이드)할 수도 있습니다.
예를 들어, ‘Car’ 클래스가 ‘Vehicle’ 클래스를 상속받는 경우, ‘Car’ 는 ‘Vehicle’ 의 모든 속성과 메소드를 사용할 수 있으며 추가적인 기능(예: 4륜 구동 기능)을 더할 수 있습니다.
3. 메소드 오버라이딩(Method Overriding) : 서브클래스가 슈퍼클래스에서 상속받은 메소드를 재정의하여 사용하는 것 입니다.
서브클래스는 상속받은 메소드를 자신의 필요에 맞게 변경할 수 있습니다.
4. 생성자 상속 : 자바에서 생성자는 상속되지 않습니다. 서브클래스의 생성자가 호출될 때, 슈퍼클래스의 생성자도 자동으로 호출되어야 하는데, 이는 ‘super()’ 키워드를 통해 명시적으로 호출해야 합니다.
📝 정리.
상속을 사용하면 코드의 중복을 줄이고, 각 클래스의 기능을 명확하게 구분지어 설계할 수 있어 프로그램 전체의 구조가 개선됩니다.
class 자식 클래스명 extends 부모 클래스명 { // 다중 상속 불가능
필드;
메소드;
...
}
2. 상속과 접근제어자와의 관계.
자바에서 상속과 접근 제어자(Access modifiers)는 클래스와 클래스 멤버(필드, 메소드)의 접근성을 결정하는 데 중요한 역할을 합니다.
접근 제어자는 클래스의 데이터를 보호하고, 코드의 유지 보수를 용이하게 하며, 외부로부터의 불필요한 접근을 막는 기능을 합니다.
상속에서 접근 제어자는 어떤 멤버가 서브클래스에게 상속될 수 있는지, 그리고 상속받은 멤버를 서브클래스가 어떻게 활용할 수 있는지 결정짓는 요소입니다.
2.1 주요 네 가지 접근 제어자.
1. private : 멤버가 선언된 클래스 내에서만 접근 가능합니다.
‘private’ 접근 제어자가 지정된 멤버는 상속되지 않습니다.
2. default(package-private) : 접근 제어자를 명시하지 않으면, 기본적으로 ‘default’ 접근이 적용됩니다.
이러한 멤버들은 같은 패키지 내의 다른 클래스에서 접근할 수 있지만, 다른 패키지의 서브클래스에서는 접근할 수 없습니다.
3. protected : ‘protected’ 멤버는 같은 패키지 내의 모든 클래스와 다른 패키지의 서브클래스에서 접근할 수 있습니다.
이 접근 제어자는 상속을 사용할 때 특히 유용하며, 서브클래스가 슈퍼클래스의 멤버를 활용하거나 수정할 수 있게 합니다.
4, public : ‘public’ 멤버는 모든 클래스에서 접근할 수 있습니다.
상속과 관련하여, ‘public’ 멤버는 서브클래스에 의해 자유롭게 상속되고 사용될 수 있습니다.
📝 정리.
상속과 접근 제어자의 관계에서 중요한 점은, 서브클래스가 상속받은 멤버에 접근할 수 있는 권한은 슈퍼클래스에서 해당 멤버에 지정된 접근 제어자에 의해 결정된다는 것 입니다.
예를 들어, 슈퍼클래스에서 ‘protected’ 로 선언된 메소드는 서브클래스에서 접근 가능하고 필요에 따라 오버라이딩할 수 있지만, ‘private’ 으로 선언된 메소드는 서브클래스에서 직접접으로 접근하거나 사용할 수 없습니다.
이러한 제한은 객체 지향 프로그래밍에서 캡슐화와 정보 은닉을 강화하는 데 도움을 줍니다.
3. super와 super().
자바에서 ‘super’ 키워드와 ‘super()’ 생성자 호출은 상속을 사용할 때 매우 중요한 역할을 합니다.
이들은 서브클래스가 슈퍼클래스와 상호작용할 수 있게 해 줍니다.
3.1 super와 super() 키워드의 사용 방식.
3.1.1 super 키워드.
‘super’ 키워드는 슈퍼 클래스의 필드나 메소드에 접근할 때 사용됩니다.
서브클래스에서 메소드를 어버라이드 했을 때, 슈퍼클래스의 버전을 호출하고 싶은 경우에 유용하게 사용할 수 있습니다.
이는 슈퍼클래스의 구현을 활용하면서 추가적인 기능을 서브클래스에 구현할 때 필요합니다.
예를 들어, 슈퍼클래스 ‘Vehicle’ 의 메소드 ‘start()’ 를 서브클래스 ‘Car’ 에서 오버라이드한 후, ‘Car’ 의 ‘start()’ 메소드에서 ‘super.start()’ 를 호출하면, ‘Vehicle’ 클래스의 ‘start()’ 메소드가 실행됩니다.
3.1.2 super() 생성자 호출.
‘super()’ 는 서브클래스의 생성자에서 슈퍼클래스의 생성자를 호출할 때 사용됩니다.
자바에서는 모든 클래스가 생성자를 가지며, 서브클래스의 생성자가 호출될 때 슈퍼클래스의 생성자도 자동으로 호출됩니다.
명시적으로 슈퍼클래스의 생성자를 호출하고자 할 때 ‘super()’ 를 사용합니다.
이 호출은 서브클래스의 생성자의 첫 번째 명령어로 위치해야 합니다.
슈퍼클래스의 생성자를 호출함으로써, 슈퍼 클래스의 인스턴스 변수들이 적절히 초기화될 수 있습니다.
예를 들어, 슈퍼클래스 ‘Vehicle’ 에 ‘Vehicle(int speed)’ 라는 생성자가 있고, 서브클래스 ‘Car’ 에서 이를 상속 받을 때, ‘Car’ 의 생성자에서 ‘super(100)’ 을 호출하면 ‘Vehicle’ 의 생성자가 호출죄어 ‘speed’ 변수가 ‘100’ 으로 초기화됩니다.
📝 정리.
이 두 사용법은 객체지향 프로그래밍에서 클래스의 계층을 통해 기능을 확장하고 관리하는 데 필수적입니다.
‘super’ 의 사용은 상속 관계에 있는 클래스 간의 코드를 재사용하고, 유지 관리를 쉽게 하며, 다형성을 구현하는 데 중요한 역할을 합니다.
4. 오버라이딩(Overriding)
자바 프로그래밍에서 오버라이딩(Overriding)은 서브클래스가 상속받은 슈퍼클래스의 메소드를 자신의 요구에 맞게 재정의하는 과정을 말합니다.
오버라이딩은 객체 지향 프로그래밍의 핵심 개념 중 하나로, 다형성을 가능하게 하며, 상속 받은 메소드를 서브클래스에서 새로운 방식으로 구현할 수 있도록 해줍니다.
4.1 오버라이딩 규칙.
오버라이딩을 할 때는 몇 가지 규칙을 따라야 합니다.
1. 메소드 이름과 시그니처 일치 : 오버라이딩할 메소드는 슈퍼클래스의 메소드와 동일한 이름, 매개변수 목록, 반환 타입을 가져야 합니다.
2. 접근 제어 : 오버라이딩하는 메소드는 슈퍼클래스의 메소드보다 더 제한적인 접근 제어를 가질 수 없습니다.
예를 들어, 슈퍼클래스의 메소드가 ‘public’ 이라면 서브클래스의 오버라이딩 메소드도 적어도 ‘public’ 이어야 합니다.
3. 반환 타입 : 오버라이딩하는 메소드의 반환 타입은 슈퍼클래스의 메소드 반환 타입과 같거나 그 하위 타입이어야 합니다.(이것은 공변 반환 타입이라고 함.)
4.2 오버라이딩의 예.
슈퍼클래스 ‘Animal’ 에 다음과 같은 메소드가 있다고 가정해 봅시다.
public class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
이제 ‘Dog’ 클래스가 ‘Animal’ 클래스를 상속받고 ‘makeSound()’ 메소드를 오버라이드하여 다음과 같이 구현할 수 있습니다.
public class Dog extends Animal {
@Override // 이 어노테이션은 선택적이지만, 오버라이딩임을 명시적으로 나타냅니다.
public void makeSound() {
System.out.println("Dog barks");
}
}
이 예에서 ‘Dog’ 클래스의 ‘makeSound()’ 메소드는 ‘Animal’ 의 makeSound() 메소드를 오버라이드하여 “Dog barks”를 출력하도록 재정의합니다.
4.3 오버라이딩의 중요성.
오버라이딩은 다음과 같은 이점을 제공합니다.
유연성 : 같은 메소드 호출이지만, 다양한 서브클래스에서 서로 다른 동작을 구현할 수 있습니다.
재사용성 : 기존의 코드를 변경하지 않고도, 상속받은 메소드를 새로운 요구에 맞게 확장할 수 있습니다.
유지보수 : 코드의 중복을 줄이고, 유지보수를 간편하게 할 수 있습니다.
📝 정리.
오버라이딩은 프로그램의 다형성을 구현하는 데 필수적인 기능으로, 상속받은 메소드를 사용하는 대신 서브클래스에 맞게 특화된 기능을 구현할 수 있도록 합니다.
-
☕️[Java] 클래스와 객체(2)
1️⃣ 클래스와 객체(2)
1. 오버로딩(Overloading).
자바 프로그래밍에서 오버로딩(Overloading)은 같은 클래스 내에서 메소드 이름이 같지만 매개변수의 타입이나 개수가 다른 여러 메소드를 정의하는 것을 의미합니다.
오버로딩을 사용하면 같은 기능을 하는 메소드라도 다양한 입력에 대응하여 유연하게 메소드를 호출할 수 있습니다.
오버로딩은 메소드만 가능하며, 생성자에도 적용될 수 있습니다.
1.2. 오버로딩의 규칙.
1. 메소드 이름이 같아야 합니다 : 오버로딩된 메소드들은 같은 이름을 공유합니다.
2. 매개변수 목록이 달라야 합니다 : 매개변수의 개수나 타입, 혹은 그 순서가 달라야 합니다. 매개변수의 차이를 통해 자바 컴파일러는 호출할 적절한 메소드를 결정합니다.
3. 반환 타입은 오버로딩을 구분하는 데 사용되지 않습니다 : 오버로딩된 메소드는 반환 타입이 다르더라도, 이는 오버로딩을 구분하는 데 사용되지 않습니다.
즉, 반환 타입만 다른 메소드는 오버로딩이 아닙니다.
4. 접근 제어자와 예외는 오버로딩을 구분하는 데 사용되지 않습니다 : 이 역시 메소드를 구별하는 데 사용되지 않습니다.
1.3. 오버로딩의 예.
public class Print {
// 오버로딩 예제: 같은 메소드 이름, 다른 매개변수 타입
public void display(int a) {
System.out.println("Integer: " + a);
}
public void display(String a) {
System.out.println("String: " + a);
}
// 오버로딩 예제: 같은 메소드 이름, 다른 매개변수 개수
public void display(int a, int b) {
System.out.println("Two integers: " + a + ", " + b);
}
// 오버로딩 예제: 같은 메소드 이름, 매개변수의 순서가 다름
public void display(String a, int b) {
System.out.println("String and integer: " + a + ", " + b);
}
}
public class Test {
public static void main(String[] args) {
Print prt = new Print();
prt.display(1); // 출력: Integer: 1
prt.display("Hello"); // 출력: String: Hello
prt.display(1, 2); // 출력: Two integers: 1, 2
prt.display("Age", 30); // 출력: String and integer: Age, 30
}
}
이 예제에서 ‘Print’ 클래스는 ‘display’ 라는 메소드를 여러 번 오버로딩했습니다.
매개변수의 타입, 개수, 순서에 따라 다른 메소드가 호출됩니다.
이를 통해 다양한 타입과 개수의 입력을 유연하게 처리할 수 있습니다.
📝 정리.
오버로딩을 통해 프로그램의 가독성을 향상시키고, 유사한 기능을 하는 메소드들을 하나의 이름으로 그룹화함으로써 프로그램을 더욱 직관적으로 만들 수 있습니다.
이러한 방식은 프로그래밍의 복잡성을 줄이고, 코드의 유지보수를 용이하게 합니다.
2. 접근제어자(Access Modifiers).
자바 프로그래밍에서 접근 재어자(Access Modifiers)는 클래스, 메서드, 변수 등과 같은 멤버들에 대한 접근 권한을 제어하는 키워드입니다.
이러한 접근 제어자를 사용함으로써 클래스의 캡슐화를 강화할 수 있으며, 객체의 데이터와 메서드를 외부에서 직접접으로 접근하거나 수정하는 것을 제한할 수 있습니다.
접근 제어자는 클래스의 멤버(변수, 메서드, 생성자 등)와 클래스 자체에 적용될 수 있습니다.
2.1 자바에서 사용하는 주요 접근 제어자
1. public : 어떤 클래스에서든 접근할 수 있도록 허용합니다.
public으로 선언됩 멤버는 어디서든 접근이 가능합니다.
2. protected : 같은 패키지 내의 클래스 또는 다른 패키지의 서브 클래스에서 접근할 수 있습니다.
3. default(package-private) : 접근 제어자를 명시하지 않은 경우, 같은 패키지 냐의 클래스들만 접근할 수 있습니다. 이를 종종 package-private라고도 합니다.
private : 해당 멤보를 선언한 클래스 내에서만 접근할 수 있습니다. 외부 클래스에서는 접근할 수 없어, 클래스 내부 구현을 숨기는 데 유용합니다.
2.2 접근 제어자의 사용 예제.
public class AccessExample {
public int publicVar = 10; // 어디서든 접근 가능
protected int protectedVar = 20; // 같은 패키지 또는 상속 받은 클래스에서 접근 가능
int defaultVar = 30; // 같은 패키지 내에서만 접근 가능
private int privateVar = 40; // 이 클래스 내에서만 접근 가능
public void show() {
System.out.println("publicVar: " + publicVar);
System.out.println("protectedVar: " + pretectedVar);
System.out.println("defaultVar: " + defaultVar);
System.out.println("privateVar: " + privateVar);
}
}
public class Test {
public static void main(String[] args) {
AccessExample example = new AccessExample();
System.out.println(example.publicVar); // 접근 가능
System.out.println(example.protectedVar); // 다른 패키지에 있지 않은 이상 접근 가능
System.out.println(example.defaultVar); // 같은 패키지에 있을 경우 접근 가능
// System.out.println(example.privateVar); // 컴파일 에러 발생, 접근 불가능
example.show(); // 모든 변수 출력 가능
}
}
위 예제에서는 다양한 접근 제어자가 적용된 변수들을 선언하고, 이에 대한 접근 가능성을 보여줍니다.
‘publicVar’ 은 어디서든 접근할 수 있지만, ‘privateVar’ 는 오직 선언된 클래스 내부에서만 접근할 수 있습니다.
‘protectedVar’ 과 ‘defaultVar’ 는 좀 더 제한적인 접근을 허용합니다.
📝 정리.
이렇게 접근 제어자를 통해 자바에서는 데이터 보호 및 캡슐화, 객체의 정확한 사용을 보장하여 프로그램의 안정성과 유지보수성을 향상시킬 수 있습니다.
3. static 키워드.
자바 프로그래밍에서 ‘static’ 키워드는 클래스의 멤버(필드, 메서드, 블록 또는 내부 클래스)를 클래스 레벨에 소속 시키는 역할을 합니다.
이는 특정 인스턴스에 속하기보다는 클래스 자체에 속한다는 의미입니다.
‘static’ 멤버는 클래스의 모든 인스턴스에 의해 공유되며, 클래스가 메모리에 로드될 때 생성되고, 클래스가 언로드될 때 소멸됩니다.
3.1 static의 특징.
1. 클래스 레벨에서 공유 : ‘static’ 필드는 클래스의 모든 인스턴스 간에 공유됩니다.
이는 특정 데이터를 모든 객체가 공유해야 할 필요가 있을 때 유용합니다.
2. 인스턴스 생성 없이 접근 기는 : ‘static’ 메서드나 필드는 객체의 인스턴스를 생성하지 않고도 클래스 이름을 통해 직접 접근할 수 있습니다.
3. 정적 초기화 블록 : ‘static’ 키워드를 사용한 블록(정적 블록)은 클래스가 처음 메모리에 로그 될 때 단 한 번 실행됩니다.
이는 ‘static’ 필드의 초기화에 사용할 수 있습니다.
3.2 static 필드와 메서드 사용 예.
public class Calculator {
// 정적 필드
public static int calculatorCount = 0;
// 정적 블록
static {
System.out.println("Calculator 클래스 로딩!");
}
// 생성자
public Calculator() {
calculatorCount++; // 생성될 때마다 계산기의 수를 증가
}
// 정적 메서드
public static int add(int a, int b) {
return a + b;
}
}
public class Test {
public static void main(String[] args) {
Calculator c1 = new Calculator();
Calculator c2 = new Calculator();
System.out.println("Created Calculators: " + Calculator.calculatorCount); // 2 출력
System.out.println("Sum: " + Calculator.add(5,3)); // 8 출력
}
}
이 예제에서는 ‘Calculator’ 클래스의 인스턴스 생성 횟수를 추적하는 ‘static’ 필드 ‘calculatorCount’ 와 정적 메서드 ‘add’ 를 사용합니다.
‘calculatorCount’ 는 ‘Calculator’ 의 모든 인스턴스에 의해 공유되며, ‘add’ 메서드는 인스턴스를 생성하지 않고도 호출할 수 있습니다.
3.3 static 사용 시 주의점
‘static’ 메서드 내에서는 인스턴스 변수나 메서드를 직접 사용할 수 없습니다.
‘static’ 은 남용하면 객체지향의 원칙을 해칠 수 있습니다.
예를 들어, 객체 간의 상태 공유가 과도하게 이루어져 객체 간의 결합도가 높아질 수 있습니다.
‘static’ 변수는 프로그램의 실행이 끝날 때까지 메모리에 남아 있으므로 메모리 사용에 주의해야 합니다.
3.4 static 메소드와 static 변수와의 관계성.
자바 프로그래밍에서 ‘static’ 메소드와 ‘static’ 변수는 두 가지 공통점을 가지고 있습니다.
둘 다 클래스 레벨에서 정의되며, 클래스의 모든 인스턴스 간에 공유됩니다.
이런 공통점 때문에, ‘static’ 메소드는 ‘static’ 변수에 직접 접근할 수 있지만, 일반 인스턴스 변수에는 접근할 수 없습니다.
3.5 static 변수.
‘static’ 변수는 클래스 변수라고도 하며, 특정 클래스의 모든 인스턴스에 의해 공유됩니다.
이 변수는 클래스가 메모리에 로드될 때 생성되고, 클래스가 언로드될 때까지 메모리에 존재합니다.
‘static’ 변수는 특히 클래스의 인스턴스들이 공통적으로 사용해야 하는 데이터를 저장하는데 유용합니다.
예를 들어, 모든 계산기 객체가 공유해야 하는 ‘calculatorCount’ 와 같은 경우에 사용됩니다.
3.6 static 메소드
‘static’ 메소드 역시 클래스 레벨에 정의되며, 이 메소드는 인스턴스 생성 없이 클래스 이름을 통해 직접 호출할 수 있습니다.
‘static’ 메소드는 인스턴스 필드나 메소드에 접근할 수 없습니다.
그 이유는 ‘static’ 메소드가 호출될 때 해당 클래스의 인스턴스가 존재하지 않을 수 있기 때문입니다.
따라서 ‘static’ 메소드는 오로지 ‘static’ 변수나 다른 ‘static’ 메소드에만 접근할 수 있습니다.
3.7 두 요소의 상호작용.
‘static’ 메소드에는 ‘static’ 변수에 자유롭게 접근하고 수정할 수 있습니다.
이는 ‘static’ 변수가 클래스에 속하고 메소드도 클래스 레벨에서 실행되기 때문입니다.
예를 들어, 어떤 클래스의 모든 인스턴스가 사용할 설정 값을 ‘static’ 변수에 저장하고, 이 값을 설정하거나 조회하는 ‘static’ 메소드를 제공할 수 있습니다.
3.8 예제
public class Counter {
public static int count = 0; // 'static' 변수
public static void increment() { // 'static' 메소드
count++; // 'static' 변수에 접근하여 값을 증가
}
public static void displayCount() {
System.out.println("Count: " + count); // 'static' 변수의 현재 값을 출력
}
}
public class Test {
public static void main(String[] args) {
Counter.increment();
Counter.increment();
Countet.displayCount(); // 출력: Count: 2
}
}
이 예제에서 ‘Counter’ 클래스는 ‘static’ 변수 ‘count’ 를 가지고 있으며, increment 메소드를 통해 이 변수의 값을 증가시키고, ‘displayCount’ 메소드를 통해 값을 출력합니다.
모든 ‘Counter’ 객체가 ‘count’ 값을 공유하며, ‘static’ 메소드를 통해 이 값을 조작할 수 있습니다.
📝 정리.
‘static’ 은 전역 변수나 전역 메서드와 유사한 효과를 제공하지만, 자바의 객체지향적 특성과 일관성을 유지하기 위해 적절히 사용되어야 합니다.
‘static’ 메소드와 ‘static’ 변수는 클래스 레벨에서 관리되어 클래스의 모든 인스턴스에 의해 공유되는 특성을 가지고 있습니다.
이를 통해 클래스 전체에 영향을 미치는 작업을 수행할 수 있습니다.
-
☕️[Java] 다차원 배열
1️⃣ 다차원 배열.
자바 프로그래밍에서 다차원 배열이란, 배열의 배열을 의미합니다.
이는 데이터를 행렬이나 그리드 형태로 구성할 수 있게 해주며, 주로 2차원 이상의 데이터 구조를 필요로 할 때 사용됩니다.
가장 흔한 형태는 2차원 배열이지만, 3차원 이상의 배열도 만들 수 있습니다.
1. 2차원 배열.
2차원 배열은 행렬과 비슷하게 생각할 수 있으며, 각 행과 열에 데이터를 저장합니다.
예를 들어, 숫자로 이루어진 표를 저장하거나 정보를 격자 형태로 관리할 때 유용합니다.
1.2 2차원 배열의 초기화
자바에서 이차원 배열을 초기화하는 방법은 크게 세 가지로 나눌 수 있습니다.
배열을 선언할 때 크기만 지정해 두거나, 선언과 동시에 특정 값을 사용하여 초기화하거나, 나중에 각 요소에 값을 할당할 수 있습니다.
아래는 각 방법에 대한 설명과 예제입니다.
1.1.1 크기만 지정하여 배열 선언하기.
이 방법은 배열의 행과 열의 크기를 지정해 초기화하지만, 배열의 각 요소는 자동으로 기본 값으로 설정됩니다.(예: int의 경우 0).
inu[][] array = new int[3][4]; // 3행 4열의 배열 생성
이렇게 선언된 배열은 모든 요소가 0으로 초기화됩니다.
1.1.2 선언과 동시에 초기값을 제공하여 배열 초기화하기.
배열을 선언하면서 동시에 초기값을 제공할 수 있습니다.
이 방법은 배열의 내용을 명확히 알고 있을 때 유용합니다.
int[][] array = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}; // 각 행에 대한 값들을 중괄호로 묶어서 초기화
이 예제에서 배열은 3행 4열의 구조로, 각 행의 값이 명시적으로 초기화되어 있습니다.
1.1.3 반복문을 사용하여 배열 초기화하기.
반복문을 사용하면 배열의 각 요소를 동적으로 초기화할 수 있습니다.
이 방법은 런타임에 따라 배열 값을 설정해야 할 때 유용합니다.
int[][] array = new int[3][4];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
array[i][j] = (i + 1) * (j + 1); // 각 요소를 행 인덱스와 열 인덱스의 곱으로 초기화
}
}
이 방법은 배열의 각 위치에 i와 j 인덱스에 의존하는 계산 결과를 저장합니다.
📝 정리.
이 세 가지 방법은 상황에 따라 각기 다른 이점을 제공하므로, 요구 사항에 맞게 선택하여 사용할 수 있습니다.
2. 3차원 배열.
3차원 배열은 데이터를 3차원 공간으로 구성하여 저장합니다.
이는 비디오 게임의 공간 데이터, 과학 실험 데이터 등 복잡한 정보를 구조화하는 데 사용될 수 있습니다.
2.1. 3차원 배열의 초기화.
자바에서 3차원 배열을 초기화하는 방법은 2차원 배열과 유사합니다.
크게 세 가지 방법으로 나눌 수 있습니다.
크기만 지정하여 선언하기.
선언과 동시에 구체적인 값으로 초기화하기
반복문을 사용하여 동적으로 초기화하기
아해는 각 방법에 대한 설명과 예시입니다.
2.1.1. 크기만 지정하여 배열 선언하기.
이 방법은 삼차원 배열의 각 차원의 크기를 지정합니다.
각 요소는 자동으로 기본값으로 설정됩니다.(예: ‘int’ 의 경우 0).
int[][][] array = new int[3][4][5] // 3개의 4x5 행렬을 갖는 삼차원 배열
이 배열은 3개의 2차원 배열을 가지며, 각 2차원 배열은 4행 5열 구조입니다.
2.1.2. 선언과 동시에 초기값을 제공하여 배열 초기화하기.
삼차원 배열을 선언하면서 바로 값을 지정할 수 있습니다.
이 방법은 각 요소의 초기값을 명확히 알고 있을 때 매우 유용합니다.
int [][][] array = {
{
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15},
{16, 17, 18, 19, 20}
},
{
{21, 22, 23, 24, 25},
{26, 27, 28, 29, 30},
{31, 32, 33, 34, 35},
{36, 37, 38, 39, 40}
},
{
{41, 42, 43, 44, 45},
{46, 47, 48, 49, 50},
{51, 52, 53, 54, 55},
{56, 57, 58, 59, 60}
}
}; // 각 행렬 및 행에 대한 값들을 중괄호로 묶어서 초기화
2.1.3. 반복문을 사용하여 배열 초기화하기
반복문을 사용해 삼차원 배열의 각 요소를 동적으로 초기화할 수 있습니다.
이 방법은 프로그램 실행 중에 배열 값을 설정해야 할 때 매우 유용합니다.
int[][][] array = new int[3][4][5];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
for (int k = 0; k < array[i][j].length; k++) {
array[i][j][k] = (i + 1) * (j + 1) * (k + 1) // 각 요소를 i, j, k 인덱스의 곱으로 초기화
}
}
}
이 예제에서는 각 위치에 해당하는 인덱스의 곱을 저장하여 배열을 초기화하고 있습니다.
이러한 초기화 방법은 특히 배열의 구조가 복잡할 때 배열을 효과적으로 관리할 수 있게 도와줍니다.
-
☕️[Java] 클래스와 객체(1)
1️⃣ 클래스와 객체(1)
1. 클래스(Class)
자바 프로그래밍 언어에서 클래스는 객체를 생성하기 위한 설계도 혹은 템플릿입니다.
클래스는 객체의 상태를 정의하는 필드(변수)와 객체의 행동을 정의하는 메서드(함수)로 구성됩니다.
클래스를 사용하는 주된 목적은 데이터와 그 데이터를 조작하는 방법들은 하나의 장소에 묶어 관리하기 위함입니다.
이를 통해 데이터 추상화, 캡슐화, 상속, 다형성 등의 객체지향 프로그래밍의 주요 개념들을 구현할 수 있습니다.
1.2 클래스의 구성 요소.
1. 필드(Field) : 객체의 데이터 또는 상태를 저장하는 변수입니다.
2. 메서드(Method) : 객체가 수행할 수 있는 행동을 정의한 코드 블록입니다.
메서드는 필드의 값을 처리하거나 다른 메서드를 호출할 수 있습니다.
3. 생성자(Constructor) : 클래스로부터 객체를 생성할 때 초기화를 담당하는 특별한 종류의 메서드입니다.
생성자는 클래스 이름과 같은 이름을 가집니다.
1.3 클래스 예제
자바에서의 간단한 클래스 예제를 살펴보겠습니다.
public class Car {
// 필드(변수)
private String color;
private String model;
// 생성자
public Car(String color, String model) {
this.color = color;
this.model = model;
}
// 메서드
public void drive() {
System.out.println(model + " 색상의 " + color + " 자동차가 주행 중입니다.");
}
}
// 객체 생성 및 사용
public class Test {
public static void main(String[] args) {
Car myCar = new Car("레드", "테슬라");
myCar.drive();
}
}
위의 예제에서 ‘Car’ 클래스는 ‘color’와 ‘model’이라는 두 개의 필드를 가지며, 이는 각각 자동차의 색상과 모델을 나타냅니다.
‘Car’ 클래스의 객체를 생성할 때는 ‘new’ 키워드와 함께 생성자를 호출하여 초기 상태를 설정합니다.
‘drive’ 메서드는 자동차가 주행하고 있음을 시뮬레이션하는 기능을 합니다.
📝 정리.
클래스를 사용함으로써 코드의 재사용성, 관리성 및 확장성이 향상되며, 대규모 소프트웨어 개발에서 필수적인 요소가 됩니다.
2. 객체(Object)와 인스턴스(Instance).
자바 프로그래밍에서 “객체(Object)”와 “인스턴스(Instance)”는 매우 중요한 개념입니다.
이 두 용어는 종종 서로 바꿔 쓰이지만, 각각의 의미에는 약간의 차이가 있습니다.
2.1 객체(Object).
객체는 소프트웨어 세계의 구성 요소로, 실제 세계의 객체를 모방한 것입니다.
객체는 데이터(속성)와 그 데이터를 조작할 수 있는 함수(메서드)를 캡슐화합니다.
객체는 클래스에 정의된 속성과 기능을 실제로 사용할 수 있도록 메모리상에 할당된 구조입니다.
객체의 개념은 클래스의 특성을 실제로 구현하는 것입니다.
2.2 인스턴스(Instance).
인스턴스는 클래스 타입에 따라 생성된 객체를 의미합니다.
예를 들어, ‘Car’ 클래스의 구체적인 객체(예: 빨간색 테슬라 자동차, 파란색 현대 자동차 등)는 모두 ‘Car’ 클래스의 인스턴스입니다.
즉, 인스턴스는 특정 클래스의 구현체입니다.
인스턴스라는 용어는 주로 객체가 메모리에 할당되어 실제로 생성되었음을 강조할 때 사용됩니다.
2.3 객체와 인스턴스의 관계.
간단히 말해, 모든 인스턴스는 객체입니다, 하지만 사용된 맥락에 따라 ‘인스턴스’라는 용어는 그 객체가 특정 클래스의 구현체임을 명시적으로 나타낼 때 사용됩니다.
예를 들어, 우리가 ‘new Car(“blue”, “Hyundai”)’ 를 통해 생성한 객체는 ‘Car’ 클래스의 인스턴스입니다.
2.4 예제 코드.
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public void speak() {
System.out.println(name + " makes a noise.");
}
}
public class Test {
public static void main(String[] args) {
// 여기서 'dog'는 Animal 클래스의 객체이자 인스턴스입니다.
Animal dog = new Animal("Dog");
dog.speak();
}
}
위 예제에서 ‘Animal’ 클래스가 있고, ‘main’ 메서드에서 ‘Animal’ 클래스의 새 객체를 생성합니다.
여기서 ‘dog’ 는 ‘Animal’ 클래스의 인스턴스이며 객체입니다.
‘dog’ 는 ‘Animal’ 클래스에 정의된 메서드와 필드를 사용할 수 있습니다.
📝 정리.
요약하면, 객체는 속성과 메서드를 갖는 소프트웨어의 기본 구성 단위이고, 인스턴스는 그 객체가 특정 클래스의 실제 구현체임을 의미합니다.
이 두 용어는 프로그래밍에서 클래스 기반의 객체를 생성하고 다룰 때 핵심적인 역할을 합니다.
클래스와 객체의 관계를 이해
기본 사용 방법과 생성자 및 this의
3. 메소드(Method).
자바 프로그래밍에서 메소드(Method)는 클래스에 속한 함수로서, 특정 작업을 수행하는 코드 블록입니다.
메소드는 객체의 행동을 정의하며, 클래스 내에서 정의된 데이터나 상태(필드)를 조작하는 데 사용됩니다.
메소드를 통해 객체지향 프로그래밍의 중요한 특징인 캡슐화와 추상화를 구현할 수 있습니다.
3.1 메소드의 주요 특징.
1. 재사용성 : 메소드는 코드의 재사용성을 증가시킵니다. 한 번 정의된 메소드는 여러 위치에서 호출되어 사용될 수 있습니다.
2. 모듈성 : 메소드를 사용함으로써 큰 프로그램을 작은 단위로 나누어 관리할 수 있습니다. 이는 코드의 가독성과 유지보수성을 향상시킵니다.
3. 정보 은닉 : 메소드를 통해 구현 세부사항을 숨기고 사용자에게 필요한 기능만을 제공할 수 있습니다.
3.2 메소드의 구성 요소.
1. 메소드 이름 : 메소드를 식별하는 데 사용되며, 메소드가 수행하는 작업을 설명하는 명확한 이름을 가집니다.
2. 매개변수 목록(Parameter List) : 메소드에 전달되는 인자의 타입, 순서, 그리고 개수를 정의합니다. 매개변수는 선택적일 수 있습니다.
3. 반환 타입 : 메소드가 작업을 수행한 후 반환하는 데이터의 타입입니다. 반환할 데이터가 없으면 ‘void’ 로 지정됩니다.
4. 메소드 바디 : 실제로 메소드가 수행할 작업을 구현하는 코드 블록입니다.
3.3 예제.
자바에서 간단한 메소드 예제를 보여드리겠습니다.
public class Calculator {
// 메소드 정의: 두 정수의 합을 반환
public int add(int num1, int num2) {
return num1 + num2;
}
// 메소드 정의: 두 정수의 차를 반환
public int subtract(int num1, int num2) {
return num1 - num2;
}
}
public class Test {
public static void main(String[] args) {
Calculator calc = new Calculator();
int result1 = calc.add(5, 3); // 8 반환
int result2 = calc.subtract(5, 3); // 2 반환
System.out.println("Addition Result: " + result1);
System.out.println("Subtraction Result: " + result2);
}
}
이 예제에서 ‘Calculator’ 클래스는 두 개의 메소드 ‘add’ 와 ‘subtract’ 를 가지고 있습니다.
각각의 메소드는 두 개의 정수를 받아 그 결과를 반환합니다.
이렇게 메소드를 사용하면 코드를 효율적으로 관리할 수 있으며, 필요에 따라 재사용할 수 있습니다.
📝 정리.
메소드는 자바 프로그래밍에서 기능을 모듈화하고 코드의 재사용을 가능하게 하는 핵심 요소입니다.
4. 접근 제어자(Access Modifiers)
자바 프로그래밍에서 접근 제어자(Access Modifiers)는 클래스, 메서드, 변수 등과 같은 멤버들에 대한 접근 권한을 제어하는 키워드입니다.
이러한 접근 제어자를 사용함으로써 클래스의 캡슐화를 강화할 수 있으며, 객체의 데이터와 메서드를 외부에서 직접적으로 접근하거나 수정하는 것을 제한할 수 있습니다.
접근 제어자는 클래스의 멤버(변수, 메서드, 생성자 등)와 클래스 자체에 적용될 수 있습니다.
4.1 자바에서 사용하는 주요 접근 제어자.
1. public : 어떤 클래스에서든 접근할 수 있도록 허용합니다.
public으로 선언된 멤버는 어디서든 접근이 가능합니다.
2. protected : 같은 패키지 내의 클래스 또는 다른 패키지의 서브 클래스에서 접근할 수 있습니다.
3. default(package-private) : 접근 제어자를 명시하지 않은 경우, 같은 패키지 내의 클래스들만 접근할 수 있습니다.
이를 종종 package-private라고도 합니다.
4. private : 해당 멤버를 선언한 클래스 내에서만 접근할 수 있습니다.
외부 클래스에서는 접근할 수 없어, 클래스 내부 구현을 숨기는 데 유용합니다.
4.2 접근 제어자의 사용 예제.
public class AccessExample {
public int publicVar = 10; // 어디서든 접근 가능
protexted int protectedVar = 20; // 같은 패키지 또는 상속받은 클래스에서 접근 가능
int defaultVar = 30; // 같은 패키지 내에서만 접근 가능
private int privateVar = 40; // 이 클래스 내에서만 접근 가능
public void show() {
System.out.println("publicVar: " + publicVar);
System.out.println("protectedVar: " + protectedVar);
System.out.println("defaultVar: " + defaultVar);
System.out.println("privateVar: " + privateVar);
}
}
public class Test {
public static void main(String[] args) {
AccessExample example = new AccessExample();
System.out.println(example.publicVar); // 접근 가능
System.out.println(example.protectedVar); // 다른 패키지에 있지 않은 이상 접근 가능
System.out.println(example.defaultVar); // 같은 패키지에 있을 경우 접근 가능
// System.out.println(example.privateVar); // 컴파일 에러 발생, 접근 불가능
example.show(); // 모든 변수 출력 가능
}
}
위 예제에서는 다양한 접근 제어자가 적용된 변수들을 선언하고, 이에 대한 접근 가능성을 보여줍니다.
‘publicVar’ 은 어디서든 접근할 수 있지만, ‘privateVar’ 는 오직 선언된 클래스 내부에서만 접근할 수 있습니다.
‘protectedVar’ 과 ‘defaultVar’ 는 좀 더 제한적인 접근을 허용합니다.
📝 정리.
이렇게 접근 제어자를 통해 자바에서는 데이터 보호 및 캡슐화, 객체의 정확한 사용을 보장하여 프로그램의 안정성과 유지보수성을 향상시킬 수 있습니다.
5. static 키워드.
자바 프로그래밍에서 ‘static’ 키워드는 특정 필드나 메소드, 또는 중첩 클래스를 클래스의 인스턴스가 아닌 클래스 자체에 소속되게 합니다.
이를 사용함으로써 해당 멤버는 클래스의 모든 인스턴스에 걸쳐 공유되며, 인스턴스 생성 없이 클래스 이름을 통해 직접 접근할 수 있습니다.
5.1 static의 주요 사용 사례.
1. 정적 필드(Static Fields) : 모든 인스턴스에 의해 공유되는 클래스 변수입니다.
예를 들어, 회사의 모든 직원이 같은 회사 이름을 공유할 때 사용할 수 있습니다.
2. 정적 메소드(Static Methods) : 인스턴스 변수에 접근할 필요 없이, 클래스 이름을 통해 직접 호출할 수 있는 메소드입니다.
유틸리티 함수나 핼퍼 함수를 작성할 때 자주 사용됩니다.
3. 정적 초기화 블록(Static Initialization Blocks) : 클래스가 처음 로딩될 때 한 번 실행되며, 정적 변수를 초기화하는 데 사용됩니다.
4. 정적 중첩 클래스(Static Nested Classes) : 다른 클래스 내부에 위치하면서도 독립적으로 사용될 수 있는 클래스입니다.
5.2 static 키워드의 장점과 단점.
장점.
메모리 효율성 : static 멤버는 클래스 로드 시 메모리에 한 번만 할당되고 모든 인스턴스가 공유하기 때문에 메모리 사용을 최소화할 수 있습니다.
편리성 : 객체 생성 없이 바로 접근할 수 있어, 유틸리티 함수 같은 공통 기능 구현에 유용합니다.
단점.
과도한 사용은 객체지향 원칙에 어긋남 : 객체 간의 결합도가 높아지고, 객체의 상태 관리가 어려워질 수 있습니다.
테스트가 어려워질 수 있슴 : static 메소드는 오버라이드가 불가능하며, 상태를 공유하기 때문에 병렬 테스트 환경에서 문제를 일으킬 수 있습니다.
5.3 예제.
public class Company {
// 정적 필드
public static String companyName = "Global Tech";
// 정적 메소드
public static void printCompanyName() {
System.out.println("Company Name: " + companyName);
}
}
public class Test {
public static void main(String[] args) {
// 객체 생성 없이 정적 메소드 호출
Company.printCompanyName();
}
}
이 예제에서 ‘Company’ 클래스에는 정적 필드 ‘companyName’ 과 정적 메소드 ‘printCompanyName()’ 이 있습니다.
‘main’ 메소드에서는 ‘Company’ 클래스의 객체를 생성하지 않고도 ‘printCompanyName()’ 메소드를 호출하려 회사 이름을 출력합니다.
📝 정리.
정적 멤버는 클래스와 관련된, 변하지 않는 값이나, 모든 인스턴스가 공유해야 하는 정보를 관리할 때 유용하게 사용됩니다.
6. 생성자(Constructor)
자바 프로그래밍에서 생성자(Constructor)는 클래스로부터 객체가 생성될 때 호출되는 특별한 유형의 메서드입니다.
생성자의 주요 목적은 새로 생성된 객체를 초기화하는 것으로, 객체의 기본 상태를 설정하는 데 사용됩니다.
생성자는 메서드처럼 보일 수 있지만, 리턴 타입이 없고 클래스 이름과 동일한 이름을 가집니다.
6.1 생성자 특징.
1. 클래스 이름과 동일 : 생성자의 이름은 항상 선언된 클래스의 이름과 동일해야 합니다.
2. 리턴 타입 없음 : 생성자는 값을 반환하지 않으며, 리턴 타입도 선언하지 않습니다.
3. 자동 호출 : 객체가 생성될 때 자동으로 호출됩니다.
이는 객체의 필드를 초기화하거나, 객체 생성 시 실행해야 할 다른 시작 루틴을 실행하는 데 사용할 수 있습니다.
4. 오버로딩 가능 : 하나의 클래스에 여러 생성자를 정의할 수 있습니다.
이를 생성자 오버로딩이라고 하며, 파라미터의 수나 타입에 따라 다른 생성자를 호출할 수 있습니다.
6.2 생성자의 유형.
1. 기본 생성자(Default Constructor) : 개발자가 명시적으로 생성자를 정의하지 않으면, 자바 컴파일러는 매개변수가 없는 기본 생성자를 제공합니다.
이 기본 생성자는 객체의 필드를 기본값으로 초기화합니다.
2. 매개변수가 있는 생성자(Parameterized Constructor) : 하나 이상의 매개변수를 받아 객체의 초기 상태를 세팅 할 수 있도록 해줍니다.
6.3 예제.
public class Person {
private String name;
private int age;
// 기본 생성자
public Person() {
this.name = "Unknown";
this.age = 0;
}
// 매개변수가 있는 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
public class Test {
public static void main(String[] args) {
// 기본 생성자를 사용하여 객체 생성
Person person1 = new Person();
person1.displayInfo(); // 출력: Name: Unknown, Age: 0
// 매개변수가 있는 생성자를 사용하여 객체 생성
Person person2 = new Person("Jhon", 25);
person2.displayInfo(); // 출력: Name: Jhon, Age: 25
}
}
이 예제에서 ‘Person’ 클래스는 두 가지 유형의 생성자를 가집니다.
하나는 매개변수가 없어 기본값으로 객체를 초기화하고, 다른 하나는 이름과 나이를 받아 객체를 초기화합니다.
📝 정리.
생성자를 사용함으로써 클래스의 인스턴스가 유효한 상태로 시작될 수 있도록 보장하며, 필요한 초기 설정을 자동으로 수행할 수 있습니다.
이는 객체 지향 프로그래밍에서 객체의 무결성을 유지하는 중요한 방법입니다.
7. this 키워드와 this() 생성자 호출.
자바에서 ‘this’ 키워드와 ‘this()’ 생성자 호출은 객체 자신을 참조하고 객체의 생성자를 호출하는 데 사용되는 중요한 요소입니다.
이들은 객체 내부에서 사용되며, 클래스의 멤버(필드, 메서드, 생성자)와 관련된 동작을 명확히 하는 데 유용합니다.
7.1 this 키워드.
‘this’ 키워드는 현재 객체, 즉 메서드나 생성자를 호출하는 인스턴스를 참조하는 데 사용됩니다.
주로 다음과 같은 상황에서 사용됩니다.
1. 필드와 매개변수 이름이 같을 때 구분 : 메서드나 생성자의 매개변수와 클래스의 필드 이름이 같을 때, 필드와 매개변수를 구분하기 위해 사용됩니다.
2. 메서드 체이닝 : 객체의 메서드를 연속적으로 호출할 때 ‘this’ 를 반환함으로써 메서드 체이닝을 구현할 수 있습니다.
3. 현재 객체를 다른 메서드에 전달 : 현재 객체의 참조를 다른 메서드에 전달할 때 사용됩니다.
7.2 this() 생성자 호출.
‘this()’ 는 같은 클래스의 다른 생성자를 호출하는 데 사용됩니다.
주로 생성자 오버로딩이 있을 때, 중복 코드를 최소화하고, 하나의 생성자에서 다른 생성자를 호출하여 필드 초기화 등의 공통 작업을 중앙집중적으로 관리할 수 있게 해줍니다.
1. 생성자 오버로딩 처리 : 클래스에 여러 생성자가 있을 때, ‘this()’ 를 사용하여 한 생성자에서 다른 생성자를 호출함으로써 공통 코드를 재사용할 수 있습니다.
2. 코드 간결성 유지 : 필수적인 초기화 작업을 주 생성자에만 명시하고, 나머지 생성자는 이 주 생성자를 호출하게 함으로써 코드의 간결성을 유지합니다.
7.3 예제.
public class Rectangle {
private int width;
private int height;
// 주 생성자
public Rectangle(int width, int height) {
this.width = width; // 'this'로 필드와 매개변수 구분
this.height = height;
}
// 부 생성자
public Rectangle() {
this(10, 10) // 'this()' 로 다른 생성자 호출
}
public void displaySize() {
System.out.println("Width: " + this.width + ", Height: " + this.height);
}
}
public class Test {
public static void main(String[] args) {
Rectangle rect1 = new Rectangle(30, 40);
rect1.displaySize(); // 출력: Width: 30, Height: 40
Rectangle rect2 = new Rectangle();
rect2.displaySize(); // 출력: Width: 10, Height: 10
}
}
이 예제에서 ‘Rectangle’ 클래스는 두 개의 생성자를 가지고 있습니다.
기본 생성자는 ‘this()’ 를 사용하여 주 생성자를 호출하고, 주 생성자에서는 ‘this’ 키워드를 사용하여 클래스 필드와 생성자 매개변수를 구분합니다.
이렇게 ‘this’ 와 ‘this()’ 를 사용함으로써 코드의 중복을 줄이고, 초기화 로직을 하나의 생성자에 집중할 수 있습니다.
📝 정리.
이처럼 ‘this’ 와 ‘this()’ 는 자바에서 클래스의 인스턴스 자신을 참조하거나 클래스 내 다른 생성자를 호출하는 데 매우 유용한 도구입니다.
-
☕️[Java] 반복문
1️⃣ 반복문
1. for 반복문.
자바 프로그래밍에서 ‘for’ 반복문은 특정 조건을 만족하는 동안 코드 블록을 반복해서 실행하도록 설계된 제어 구조입니다.
‘for’ 문은 초기화, 조건 검사, 반복 후 실행할 작업(일반적으로 증감)을 한 줄에 명시하여 코드의 가독성과 관리를 용이하게 합니다.
이는 반복 실행이 필요한 많은 상황에서 유용하게 사용됩니다.
1.2 for 반복문의 기본 구조.
‘for’ 문의 기본 구조는 다음과 같습니다.
for (초기화; 조건; 증감) {
// 반복해서 실행할 코드
}
초기화 : 반복문이 시작할 때 한 번 실행되는 부분으로, 반복문의 제어 변수를 초기 설정합니다.
조건 : 이 조건이 참(‘true’) 인 동안 반복문 내의 코드가 실행 됩니다. 조건이 거짓(‘false’)이 되면 반복문은 종료됩니다.
증감 : 각 반복의 끝에서 실행되며, 주로 제어 변수의 값을 증가시키거나 감소시키는데 사용됩니다.
1.3 for 반복문의 예시
기본 예시
for (int i = 0; i < 5; i++) {
System.out.println("i의 값은: " + i);
}
이 예제에서는 ‘i’ 를 0부터 시작하여 ‘i’ 가 5미만일 동안 반복하여, 매 반복마다 ‘i’ 를 1씩 증가시킵니다.
따라서 “i의 값은: 0” 부터 “i의 값은: 4” 까지 총 다섯 번의 출력을 하게 됩니다.
확장된 예시: 다중 제어 변수
for (int i = 0, j = 10; i < j; i++, j--) {
System.out.println("i = " + i + ". j = " + j);
}
이 예제에서는 두 개의 제어 변수 ‘i’ 와 ‘j’ 를 사용합니다.
‘i’ 는 증가하고 ‘j’ 는 감소하며, ‘i’ 가 ‘j’ 와 같거나 ‘j’ 보다 크게 되면 반복이 종료됩니다.
이런 패턴은 복잡한 반복 조건이 필요한 경우 유용하게 사용됩니다.
1.4 사용 사례
‘for’ 반복문은 배열이나 컬렉션과 같은 데이터 구조를 순회할 때 매우 유용합니다.
예를 들어, 배열의 모든 요소를 처리하거나 조작할 때 자주 사용됩니다.
int[] numbers = {1,2,3,4,5};
for (int i = 0; i < numbers.length; i++) {
System.out.println("배열 요소: " + numbers[i]);
}
여기서 ‘numbers.length’ 는 배열의 길이를 반환하며, ‘i’ 는 0에서 시작하여 배열의 크기 미만이 될 때까지 증가하면서 배열의 각 요소에 접근합니다.
📝 정리
‘for’ 반복문은 코드를 간결하게 하면서 반복적인 작업을 효과적으로 처리할 수 있도록 도와줍니다.
2. while 반복문.
자바 프로그래밍에서 ‘while’ 반복문은 특정 조건이 참(‘true’)인 동간 주어진 코드 블록을 반복적으로 실행하는 구조 입니다.
‘while’ 문은 주로 반복 횟수가 불확실할 때 또는 반복 횟수를 사전에 정확히 알 수 없을 때 사용됩니다.
2.1 while 반복문의 기본 구조.
‘while’ 문의 기본 구조는 다음과 같습니다.
while (조건) {
// 조건이 참인 동안 반복 실행될 코드
}
여기서 ‘조건’ 은 각 반복 이전에 평가되며, 이 조건이 참(‘true’)일 때 반복 블록 내의 코드가 실행됩니다. 조건이 거짓(‘false’)이 되면 반복문은 종료됩니다.
2.2 while 반복문 예시.
간단한 예
int i = 0;
while (i < 5) {
System.out.println("i의 값은: " + i);
i++; // i 값을 증가시켜 조건이 eventually 거짓이 되도록 함
}
이 예제에서는 ‘i’ 가 0부터 시작하여 5미만인 동안 반복문을 실행합니다.
반복문 내에서 ‘i’ 를 1씩 증가시켜 eventually 조건이 거짓이 되도록 합니다.
결과적으로 ‘i’ 의 값은 0부터 4까지 콘솔에 출력됩니다.
사용자 입력 받기
‘while’ 문은 사용자 입력을 받고, 그 입력에 따라 반복을 계속할지 여부를 결정할 때 유용하게 사용될 수 있습니다.
예를 들어, 사용자가 특정 문자를 입력할 때까지 입력을 계속 받는 프로그램은 다음과 같이 작성할 수 있습니다.
Scanner scanner = new Scanner(System.in);
String input = "";
while (!input.equals("종료")) {
System.out.println("문자열을 입력하세요. 종료하려면 '종료'를 입력하세요: ");
input = scanner.nextLine();
}
scanner.close();
2.3 주의사항.
‘while’ 반복문을 사용할 때는 반복문 내에서 조건이 eventually(결국) 거짓이 될 수 있도록 조치를 취해야 합니다.
그렇지 않으면, 조건이 항상 참으로 평가될 경우 무한 후프에 빠질 수 있습니다.
따라서 조건 변수를 적절히 조작하거나 적절한 로직을 구현하여 반복문이 적절한 시점에 종료될 수 있도록 해야 합니다.
📝 정리.
‘while’ 문은 그 구조가 간단하고 유연하여, 특정 조건 하에 반복 실행을 해야 할 때 매우 유용한 프로그래밍 도구입니다.
3. do-while 반복문.
자바 프로그래밍에서 ‘do-while’ 반복문은 조건을 검사하기 전에 최소 한 번은 코드 블록을 실행하는 반복문입니다.
이 구조는 ‘while’ 반복문과 비슷하지만, 조건의 참/거짓 여부에 관계없이 최소한 처음에는 반복문 내의 코드를 실행한다는 점이 다릅니다.
‘do-while’ 문은 주로 사용자 입력을 처리하거나, 조건이 반복문의 실행 후에 결정되어야 할 때 유용합니다.
3.1 do-while 반복문의 기본 구조.
‘do-while’ 문의 기본 구조는 다음과 같습니다.
do {
// 최소 한 번은 실행될 코드
} while (조건);
여기서 ‘조건’ 은 반복문의 끝에서 평가됩니다.
조건이 참(‘true’)이면, 코드 블록이 반복적으로 실행됩니다. 조건이 거짓(‘false’)이면, 반복이 종료됩니다.
3.2 do-while 반복문의 예시.
기본 예
int i = 0;
do {
System.out.println("i의 값은: " + i);
i++;
} while (i < 5);
이 예제에서는 ‘i’ 가 0부터 시작하여 ‘i < 5’ 인 동안 반복합니다.
‘do-while’ 문은 ‘i’ 의 초기 값에 상관없이 최소 한 번은 “i의 값은: 0”을 출력하고 시작합니다. 그 후 ‘i’ 가 5미만인 동안 계속해서 반복됩니다.
사용자 입력 받기
사용자로부터 입력을 받고, 특정 조겅(“종료” 문자열 입력)을 만족할 때까지 계속 입력을 받는 프로그램을 구현할 때 ‘do-while’ 문이 유용하게 사용됩니다.
Scanner scanner = new Scanner(System.in);
String input;
do {
System.out.println("문자열을 입력하세요. 종료하려면 '종료'를 입력하세요:");
input = scanner.nextLine();
} while (!input.equals("종료"));
scanner.close();
이 코드는 사용자가 “종료”를 입력할 때까지 계속해서 입력을 받습니다.
입력을 받는 동작은 최소 한 번은 실행되며, 이는 ‘do-while’ 문이 최조 실행을 보장하기 때문입니다.
3.3 주의사항.
‘do-while’ 반복문을 사용할 때, 조건을 적절히 설정하여 반복문이 적절한 시점에 종료되도록 해야 합니다. 그렇지 않으면 무한 루프에 빠질 수 있습니다.
또한, 조건 검사가 반복문의 끝에서 이루어지므로, 조건이 매우 빨리 거짓이 되어도 코드 블록이 한 번은 실행됨을 기억해야 합니다.
📝 정리.
‘do-while’ 문은 조건이 반복 블록 실행 후에만 알 수 있거나, 반복 블록을 적어도 한 번은 실행해야 하는 경우 특히 유용한 도구입니다.
4. continue.
자바 프로그래밍에서 ‘continue’ 문은 반복문 내에서 사용되며, 그것이 실행될 때 현재 반복의 나머지 부분을 건너뛰고 즉시 다음 반복으로 넘어가도록 합니다.
이를 통해 특정 조건에서 반복문의 다음 순환을 즉시 시작할 수 있게 해줍니다.
‘continue’ 는 주로 반복문 내에서 특정 조건에 대한 예외 처리나 불필요한 처리를 건너뛰기 위해 사용됩니다.
4.1 continue 기본 사용법
‘continue’ 문은 주로 ‘for’, ‘while’, 또는 ‘do-while’ 반복문 내에서 사용됩니다.
간단하게는 ‘continue’ 를 실행하면 반복문의 현재 순회에서 남은 코드를 실행하기 않고, 다음 반복으로 진행합니다.
4.2 continue 예시
‘for’ 반복문에서의 사용
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // 짝수인 경우, 출력을 건너뛰고 다음 반복으로 넘어감
}
System.out.println(i); // 홀수만 출력
}
이 코드는 0부터 9까지의 숫자 중에서 홀수만 출력합니다.
‘i’ 가 짝수일 경우, ‘continue’ 문이 실행되어 ‘System.out.println(i);’ 줄을 건너뛰고 다음 반복으로 넘어갑니다.
‘while’ 반복문에서의 사용
int i = 0;
while (i < 10) {
i++;
if (i % 2 == 0) {
continue; // 짝수인 경우, 아래의 출력을 건너뛰고 다음 반복으로 넘어감
}
System.out.println(i); // 홀수만 출력
}
이 예제에서도 ‘continue’ 는 짝수를 확인하는 조건에서 참일 경우 나머지 코드를 건너뛰고 다음 반복을 계속 진행하도록 합니다.
4.3 continue 특징 및 주의사항.
‘continue’ 문은 현재 수행 중인 반복의 나머지 부분을 건너뛰고, 반복문의 조건 검사로 직접 이동하여 다음 반복을 시작합니다.
‘continue’ 문을 사용할 때는 반복문이 무한 루프에 빠지지 않도록 주의해야 합니다.
예를 들어, ‘continue’ 문이 반복문의 변수 값을 변경하는 코드를 건너뛰면 그 변수의 값이 업데이트되지 않아 무한 루프가 발생할 수 있습니다.
‘continue’ 는 루프의 흐름을 제어하고, 코드의 읽기 어려움을 증가시킬 수 있으므로, 사용할 때는 명확한 이유가 있어야 합니다.
📝 정리.
‘continue’ 문은 코드를 보다 효율적으로 만들고, 필요 없는 조건을 빠르게 건너뛸 수 있게 도와줍니다.
그러나 코드의 가독성과 유지 관리에 영향을 줄 수 있으므로 신중하게 사용해야 합니다.
5. break문.
자바 프로그래밍에서 ‘break’ 문은 반복문(‘for’, ‘while’, ‘do-while’) 또는 ‘switch’ 문에서 현재 블록의 실행을 즉시 종료하고, 해당 블록의 바깥으로 제어를 이동시키는 역할을 합니다.
이는 반복문 또는 ‘switch’ 문 내에서 특정 조건을 만족할 때 추가적인 처리 없이 루프나 선택 구조를 벗어나기 위해 사용됩니다.
5.1 break 기본 사용법.
반복문에서의 사용 : ‘break’ 를 사용하여 무한 루프를 종료하거나 특정 조건이 만족될 때 반복문을 조기에 종료할 수 있습니다.
‘switch’ 문에서의 사용 : 각 ‘case’ 블록 뒤에 ‘break’ 를 사용하여 ‘switch’ 문을 종료하고, 다음 ‘case’ 로 넘어가지 않도록 합니다.
5.2 break 예시
반복문에서의 ‘break’
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // i가 5가 되면 for 루프를 종료
}
System.out.println(i);
}
이 코드에서는 ‘i’ 가 5에 도달하면 ‘break’ 문이 실행되어 ‘for’ 루프가 즉시 종료됩니다.
결과적으로, 0부터 4까지의 숫자만 출력됩니다.
‘while’ 반복문에서의 ‘break’
int i = 0;
while (true) { // 무한 루프
if (i == 5) {
break; // i가 5가 되면 while 루프를 종료
}
System.out.println(i);
i++;
}
이 예제에서도 ‘i’ 가 5일 때 ‘break’ 문을 사용하여 무한 루프를 종료합니다.
‘switch’ 문에서의 ‘break’
int number = 2;
switch (number) {
case 1:
System.out.println("Number is 1");
break;
case 2:
System.out.println("Number is 2");
break;
case 3:
System.out.println("Number is 3");
break;
default:
System.out.println("Number is not 1, 2, or 3");
}
‘switch’ 문에서 ‘number’ 가 2일 때 해당하는 ‘case’ 블록이 실행되고, ‘break’ 문으로 인해 ‘switch’ 문을 벗어나게 됩니다.
5.3 break문 특징 및 주의사항
‘break’ 문은 코드 실행을 즉시 중단시키므로, 효과적인 프로그램 흐름 제어를 가능하게 합니다.
반복문이나 ‘switch’ 문 내에서만 ‘break’ 문을 사용할 수 있습니다.
‘break’ 문을 사용할 때는 코드의 흐름을 명확히 이해하고 있어야 하며, 무분별한 사용은 코드의 가독성과 유지보수를 어렵게 만들 수 있습니다.
📝 정리.
‘break’ 는 코드의 복잡성을 줄이고 특정 조건에서 즉시 반복문을 종료할 수 있는 강력한 도구입니다.
그러나 그 사용은 코드의 구조를 명확하게 유지하는 방식으로 신중하게 이루어져야 합니다.
6. for-each문.
자바 프로그래밍에서 ‘for-each’ 문, 또는 강화된 ‘for’ 문(enhanced for loop)은 배열이나 컬렉션 프레임워크에 저장된 각 요소를 순회하기 위해 사용되는 구문입니다.
기존의 ‘for’ 문보다 간결하며, 코드를 읽고 작성하기가 더 쉬워 배열이나 컬렉션의 모든 요소에 접근할 때 일반적으로 권장되는 방식입니다.
6.1 for-each문 기본 구조.
‘for-each’ 문의 기본 구조는 다음과 같습니다.
for (타입 변수명 : 배열 또는 컬렉션) {
// 변수명을 사용한 코드
}
여기서 ‘타입’ 은 배열 또는 컬렉션에 저장된 요소의 타입을 말하고, ‘변수명’ 은 반복되는 각 요소를 참조하는 데 사용되는 변수 이름입니다.
‘배열 또는 컬렉션’ 은 순회할 배열이나 컬렉션 객체를 지정합니다.
6.2 for-each문 예시
배열 사용 예
int[] numbers = {1,2,3,4,5};
for(int number: numbers) {
System.out.println(number);
}
이 코드에서 ‘for-each’ 문은 ‘numbers’ 배열의 모든 요소를 순회합니다.
각 반복에서 ‘number’ 변수에 배열의 요소가 할당되며, 그 값을 출력합니다.
컬렉션 사용 예
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
for (String name : names) {
System.out.println(name);
}
이 예에서는 ‘names’ 리스트의 모든 요소를 순회합니다.
각 요소는 ‘name’ 변수에 할당되고, ‘System.out.println’ 을 통해 출력됩니다.
6.3 for-each문의 장점.
가독성 향상: ‘for-each’ 문은 간결하고 이해하기 쉬워, 코드의 가독성을 크게 향상시킵니다.
오류 감소: 전통적인 ‘for’ 문에서 발생할 수 있는 인덱스 관련 실수나 경계 조건 오류를 방지할 수 있습니다.
향상된 추상화: 컬렉션의 내부 구조나 크기를 몰라도 각 요소에 접근할 수 있습니다.
6.4 for-each문의 제한사항.
컬렉션 수정 불가: ‘for-each’ 문을 사용하는 동안 컬렉션을 수정할 수 없습니다.
예를 들어, 순회 중인 컬렉션에서 요소를 추가하거나 제거할 수 없습니다.
인덱스 접근 불가: ‘for-each’ 문은 각 요소에 대한 인덱스를 제공하지 않습니다.
특정 인덱스의 요소에 접근하거나 인덱스를 활용한 복잡한 로직이 필요한 경우에는 전통적인 ‘for’ 문을 사용해야 합니다.
📝 정리.
‘for-each’ 문은 자바에서 컬렉션과 배열을 효율적으로 처리할 수 있는 강력하고 사용하기 쉬운 도구입니다.
이를 통해 코드를 더욱 간결하고 안전하게 만들 수 있습니다.
-
☕️[Java] 조건문
1️⃣ 조건문
1. if문.
자바 프로그래밍에서 ‘if’ 문은 조건부 실행을 제어하는 기본적인 제어 구문입니다.
이를 통해 프로그램은 주어진 조건이 참(‘true’)인지 거짓(‘false’)인지에 따라 다른 행동을 취할 수 있습니다.
1.1 if문 기본 구조.
‘if’ 문의 기본 구조는 다음과 같습니다.
if (조건) {
// 조건이 참일 때 실행될 코드
}
여기서 ‘조건’ 은 boolean 타입의 표현식으로, 평가 결과가 ‘true’ 또는 ‘false’ 가 됩니다.
조건이 ‘true’ 일 때만 중괄호 ’{}’ 내부의 코드가 실행됩니다.
1.2 예시
예를 들어, 사용자의 나이가 성인 기준을 만족하는지를 확인하는 코드는 다음과 같습니다.
int age = 20;
if (age >= 18) {
System.out.println("성인입니다.");
}
이 코드에서 ‘age >= 18’ 이라는 조건이 참이면 “성인입니다.” 라는 메시지를 출력합니다.
1.3 ‘else’와 ‘else if’ 확장
‘if’ 문은 종종 ‘else’ 와 ‘else if’ 와 함께 사용되어 보다 복잡한 조건 로직을 구현할 수 있습니다.
if (조건1) {
// 조건1이 참일 때 실행될 코드
} else if (조건2) {
// 조건1이 거짓이고 조건2가 참일 때 실행될 코드
} else {
// 위의 모든 조건이 거짓일 때 실행될 코드
}
예를 들어, 점수에 따라 학점을 출력하는 코드는 다음과 같습니다.
int score = 85;
if (score >= 90) {
System.out.println("학점 A");
} else if (score >= 80) {
System.out.println("학점 B");
} else if (score >= 70) {
System.out.println("학점 C");
} else {
System.out.println("학점 D");
}
이 예제에서 ‘score’ 변수의 값에 따른 다른 학점을 출력합니다.
‘if’, ‘else if’, ‘else’ 구문은 점수 범위에 따라 조건적으로 실행되며, 가장 먼저 만족하는 조건의 블록만 실행됩니다.
📝 정리.
‘if’ 문은 프로그래밍에서 결정을 내리는 데 필수적인 구조이며, 다양한 조건에 따라 코드의 실행 흐름을 제어하는 데 사용됩니다.
2. switch문.
자바 프로그래밍에서 ‘switch’ 문은 다수의 조건 중 하나를 선택해 실행할 때 사용하는 조건문입니다.
이는 ‘if-else’ 조건문의 대안으로, 변수의 값에 따라 여러 실행 경로 중 하나를 선택할 수 있도록 해줍니다.
‘switch’ 문은 특히 특정 변수가 취할 수 있는 명확한 값들을 기반으로 다양한 케이스를 처리할 때 유용하게 사용됩니다.
2.1 switch문의 기본 구조.
‘switch’ 문의 기본 구조는 다음과 같습니다.
switch (표현식) {
case 값1:
// 표현식 결과가 값1과 일치할 때 실행할 코드
break;
case 값2:
// 표현식 결과가 값2와 일치할 때 실행할 코드
break;
// 추가적인 case들을 더 정의할 수 있습니다.
default:
// 어떤 case도 일치하지 않을 때 실행할 코드
}
2.2 switch 문의 주요 특징.
1. 표현식 은 주로 정수, 문자형 또는 열거형(enum) 데이터를 사용합니다. 자바 7 이상에서는 문자열(String)도 지원합니다.
2. case 라벨은 ‘switch’ 문 내에서 표현식의 결과와 일치하는 값을 가지며, 해당 값에 대한 실행 코드를 포함합니다.
3. break 문은 ‘switch’ 문을 종료하고 다음 코드로 넘어가도록 합니다. ‘break’ 가 없으면 다음 ‘case’ 로 계속 진행되어 “fall-through” 현상이 발생합니다.
4. default 섹션은 선택적으로 사용되며, 어떤 ‘case’ 도 일치하지 않을 때 실행됩니다.
2.3 switch 문 예시.
학생의 점수에 따라 학점을 부여하는 간단한 예를 들어보겠습니다.
int score = 92;
String grade;
switch (score / 10) {
case 10:
case 9:
grade = "A";
break;
case 8:
grade = "B";
break;
case 7:
grade = "C";
break;
case 6:
grade = "D";
break;
default:
greade = F;
}
System.out.println("학점: " + grade);
이 코드에서 ‘score / 10’ 의 결과값에 따라 다른 ‘case’ 블록이 실행됩니다.
‘92/10’ 은 ‘9’ 이므로, ‘grade’ 는 “A” 가 됩니다.
각 ‘case’ 는 ‘break’ 문으로 종료되므로, 해당 ‘case’ 실행 후, ‘switch’ 문을 벗어납니다.
📝 정리.
‘switch’ 문은 코드의 가독성을 높이고, 많은 조건 분기를 간결하게 처리할 수 있는 방법을 제공합니다.
이는 특히 각 조건이 명확할 때 더욱 유용하며, 코드의 구조를 명확하게 표현할 수 있습니다.
-
☕️[Java] 여러가지 연산자(1)
1️⃣ 각각의 연산자에 대한 이해
1. 항과 연산자.
1.1 단항 연산자(Unary Operator).
자바 프로그래밍에서 단항 연산자(Unary Operator)는 오직 한 개의 피연산자(operand)를 가지고 연산을 수행하는 연산자를 말합니다.
이들은 변수나 값에 직접 적용되며, 표현식의 결과를 반환합니다.
단항 연산자는 특히 간단한 수학 연산, 값의 부정, 또는 값의 증감 등에서 유용하게 사용됩니다.
1.2 자바에서 사용되는 주요 단항 연산자.
1. 부정 연산자('!')
불리언 값을 반전시킵니다.
예를 들어, '!true' 는 'false' 가 됩니다.
2. 단항 플러스 및 마이너스 연산자('+', '-')
'+' 는 일반적으로 숫자의 부호를 그대로 두지만, 명시적으로 사용할 수 있습니다.
'-' 는 숫자의 부호를 반전시킵니다.
예를 들어, '-5' 는 양수 '5' 를 음수로 변환합니다.
3. 증가 및 감소 연산자('++', '--')
'++' 연산자는 변수의 값을 1만큼 증가시킵니다.
'--' 연산자는 변수의 값을 1만틈 감소시킵니다.
이 연산자들은 전위(prefix) 형태(예: '++x')와 후위(postfix) 형태(예: 'x++')로 사용될 수 있습니다.
전위 형태는 변수를 증가시키고 표현식의 값을 반환하기 전에 증가된 값을 사용하고, 후위 형태는 표현식의 값을 반환한 후 변수를 증가시킵니다.
4. 비트 반전 연산자('~')
정수형 변수는 모든 비트를 반전시킵니다.
예를 들어, '~00000000' 은 '11111111' 이 됩니다.
이는 정수에 대해 비트 단위 NOT 연산을 수행합니다.
1.3 예제 사용.
public class UnaryDemo {
public static void main(String[] args) {
boolean a = false;
System.out.println(!a); // true
int num = 5;
System.out.println(-num); // -5
System.out.println(++num); // 6
System.out.println(num++); // 6, 그리고 num이 7이 됨
System.out.println(--num); // 6
System.out.println(num--); // 6, 그리고 num이 5가 됨
int b = 0b00000000; // 이진수로 0
System.out.println(~b); // 모든 비트가 1로 반전
}
}
이 예제에서는 다양한 단항 연산자들의 사용 방법과 그 효과를 보여줍니다.
단항 연산자들은 자바 프로그래밍에서 변수를 조작하거나 특정 연산을 더 간결하게 수행하는데 매우 유용합니다.
2.1 이항 연산자(Binary Operator)
자바 프로그래밍에서 이항 연산자(Binary Operator)는 두 개의 피연산자(operand)를 취해 연산을 수행하고 결과를 반환하는 연산자를 말합니다.
이항 연산자는 수학적 계산, 논리 비교, 값의 할당 등 다양한 작업에 사용됩니다.
2.2 자바에서 사용되는 주요 이항 연산자의 종류.
1. 산술 연산자.
’+’ (덧셈)
’-‘ (뺄셈)
‘*‘ (곱셈)
’/’ (나눗셈)
’%’ (나머지)
2. 비교 연산자.
’==’ (동등)
’!=’ (부등)
’>’ (크다)
’<’ (작다)
’>=’ (크거나 같다)
’<=’ (작거나 같다)
3. 논리 연산자.
‘&&’ (논리적 AND)
**’
‘** (논리적 OR)
4. 비트 연산자
‘&’ (비트 AND)
**’
‘** (비트 OR)
’^’ (비트 XOR)
5. 할당 연산자
’=’ (기본 할당)
’+=’, ‘-=’, ‘*=’, ‘/=’, ‘%=’ (복합 할당 연산자)
**‘&=’, ‘
=’, ‘^=’, ‘«=’, ‘»=’, ‘»>=’** (비트 복합 할당 연산자)
6. 시프트 연산자
’«‘ (왼쪽 시프트)
’»‘ (오른쪽 시프트, 부호 유지)
’»>’ (오른쪽 시프트, 부호 비트 없음)
2.3 예제 코드
public class BinaryOperatorsExample {
public static void main(String[] args) {
int a = 10, b = 5;
int sum = a + b; // 15
int difference = a - b; // 5
boolean isEqual = (a == b); // false
boolean isGreater = (a > b); // true
int bitAnd = a & b; // 비트 AND 연산
int shiftedLeft = a << 2; // 왼쪽으로 2 비트 시프트
System.out.println("Sum: " + sum);
System.out.println("Difference: " + difference);
System.out.println("Is Equal: " + isEqual);
System.out.println("Is Greater: " + isGreater);
System.out.println("Bitwise AND: " + bitAnd);
System.out.println("Left Shifted: " + shiftedLeft);
}
}
이항 연산자들은 기본적인 산술 연산부터 복잡한 논리 연산에 이르기까지 프로그래밍에서 광범위하게 사용됩니다.
이를 통해 효과적으로 데이터를 조작하고, 조건을 평가하며, 복잡한 문제를 해결할 수 있습니다.
3.1 삼항 연산자(Ternary Operator).
자바 프로그래밍에서 삼항 연산자(Ternary Operator), 또는 조건 연산자(Conditional Operator)라고 불리는 ’?:’ 는 세 개의 피연산자를 사용하는 유일한 연산자입니다.
이 연산자는 간결한 조건문을 구현할 때 사용되며, 간단한 조건식을 기반으로 두 가지 선택지 중 하나를 반환합니다.
3.2 삼항 연산자의 구조.
조건식 ? 값1 : 값2
1. 조건식 : 이 부분은 ‘true’ 또는 ‘false’ 를 반환하는 불리언 식입니다.
2. 값2 : 조건식이 ‘true’ 일 때 반환됩니다.
3. 값3 : 조건식이 ‘false’ 일 때 반환됩니다.
조건식의 평가 결과에 따라 ‘값1’ 또는 값2 중 하나가 결과값으로 선택되어 반환됩니다.
이 연산자는 일반적으로 간단한 조건에 따라 변수에 값을 할당하거나 특정 표현식의 결과를 결정할 때 유용하게 사용됩니다.
3.3 예제 사용.
public class TernaryExample {
public static void main(String[] args) {
int a = 10, b = 5;
int max = (a > b) ? a : b; // a와 b 중 큰 값을 max에 할당.
System.out.println("Max value: " + max);
String response = (a > b) ? "a is greater than b" : " b is greater or equal to a";
System.out.println(response);
}
}
이 예제에서 삼항 연산자를 사용하여 두 숫자 중 더 큰 숫자를 결정하고, 문자열 메시지도 조건에 따라 선택합니다.
이러한 사용은 코드를 더 간결하게 만들고, ‘if-else’ 구조를 보다 간단하게 대체할 수 있게 해 줍니다.
삼항 연산자는 그 효율성과 간결함 때문에 자바 프로그래밍에서 자주 사용되는 유용한 도구입니다.
그러나 복잡한 로직이나 여러 조건이 연속적으로 필요한 경우에는 가독성을 위해 전통적인 ‘if-else’ 문을 사용하는 것이 더 나을 수 있습니다.
-
☕️[Java] 여러가지 연산자(2)
1️⃣ 비트 연산자에 대한 이해
1. 2진법.
자바 프로그래밍에서의 이진법은 컴퓨터의 지본 숫자 시스템을 참조하는 것입니다.
컴퓨터는 데이터를 0과 1의 형태, 즉 이진수로 처리합니다.
자바에서도 이러한 이진법을 사용하여 데이터를 저장, 처리하며 다양한 연산을 수행할 수 있습니다.
1.1 자바에서 2진법을 사용하는 예.
1. 이진 리터럴 : 자바 7 이상부터는 정수를 이진 리터럴로 직접 표현할 수 있습니다.
예를 들어, ‘int x = 0b1010;’ 은 이진수 ‘1010’ 이고, 십진수로 10입니다.
2. 비트 연산자 : 자바는 비트 연산을 수행할 수 있는 여러 연산자를 제공합니다.
예를 들어, 다음과 같습니다.
'&' (AND 연산자)
'|' (OR 연산자)
'^' (XOR 연산자)
'~' (NOT 연산자)
'<<' (왼쪽 시프트)
'>>' (오른쪽 시프트)
'>>>' (부호 없는 오른쪽 시프트)
이들 연산자는 주로 효율적인 수치 계산, 저수준 프로그래밍, 암호와 작업 등에 사용됩니다.
3. 이진 데이터 조작 : 파일이나 네트워크를 통해 바이트 단위로 데이터를 읽고 쓸 때, 이진 형식으로 데이터를 처리합니다.
자바에서는 'byte' 자료형을 이용하여 이진 데이터를 직접 다룰 수 있습니다.
📝 정리
이진법을 사용하는 주된 이유는 컴퓨터 하드웨어가 전기 신호로 작동하기 때문에 0과 1, 즉 이진 상태를 나타내는 전기의 켜짐과 꺼짐 상태로 모든 데이터를 표현하기 편리하기 때문입니다.
이렇게 함으로써, 프로그래밍에서 더욱 직접적이고 효율적인 하드웨어 조작이 가능해집니다.
2. 2의 보수.
자바 프로그래밍에서의 2의 보수(2’s complement)는 음수를 표현하기 위한 방법입니다.
컴퓨터 시스템은 보통 이진법을 사용하여 데이터를 저장하고 처리하는데, 이진법에서 음수를 표현하기 위해 가장 널리 사용되는 방법이 2의 보수입니다.
2.1 2의 보수 생성 과정.
1. 원래 숫자의 이진 표현을 얻습니다.
예를 들어, 5의 이진 표현은 ‘0101’ 입니다.
2. 이진 표현의 모든 비트를 반전시킵니다.
즉, 0은 1로, 1은 0으로 변경합니다.
5의 경우 ‘0101’ 이 ‘1010’ 이 됩니다.
3. 반전된 값에 1을 더합니다.
이렇게 하면 ‘1011’ 이됩니다.
이렇게 생성된 ‘1011’ 은 -5를 나타냅니다.
이 방법은 자바를 포함한 대부분의 프로그래밍 언어와 컴퓨터 시스템에서 음수를 표현하는 표준 방법입니다.
2.2 2의 보수의 장점.
덧셈 연산만으로 뺄셈을 할 수 있습니다.
예를 들어, 5-5를 계산하려면 5와 -5의 2의 보수를 더하면 됩니다.
이진법으로는 ‘1010 + 1011 = 10000’ 이고, 최상위 비트(캐리 비트)는 무시합니다.
따라서 결과는 ‘0000’ 이 됩니다.
오버플로 처리가 간단합니다.
캐리 비트는 무시하면서 자연스럽게 오버플로를 처리할 수 있습니다.
2.3 자바에서의 활용.
자바에서는 정수형 타입(‘int’, ‘long’, ‘short’, ‘byte’) 이 이진법으로 2의 보수 형태로 저장되고 처리됩니다.
이는 자바의 모든 정수 연산에 내장된 메커니즘입니다.
예를 들어, 자바에서 ‘-5’ 를 선언하면 내부적으로는 ‘5’ 의 2의 보수인 ‘111…11011’ (32비트 시스템에서의 표현)으로 저장됩니다.
📝 정리
2의 보수 방식은 음수를 다루기 위한 효과적인 방법이며, 프로그래머가 별도의 조치를 취하지 않아도 시스템이 자동으로 처리해 주기 때문에 매우 편리합니다.
3. 비트 논리연산자.
자바 프로그래밍에서 비트 논리연산자는 비트 단위로 논리 연산을 수행하는 연산자입니다.
이들 연산자는 주로 정수 타입의 변수에 사용되며, 각 비트를 독립적으로 비교하여 결과를 반환합니다.
비트 논리연산자는 주로 저수준 프로그래밍, 효율적인 데이터 처리, 상태 플래그 관리, 암호화 등의 작업에 활용됩니다.
자바에서 사용되는 주요 비트 논리 연산자는 다음과 같습니다.
1. AND 연산자(‘&’) : 두 피연산자의 비트가 모두 1일 경우에만 결과의 해당 비트를 1로 설정합니다.
예를 들어, ‘5 & 3’ 은 이진수로 ‘0101 & 0011’ 을 계산하여 ‘0001’ 이 되므로, 결과는 ‘1’ 입니다.
**2. OR 연산자(‘
’) :** 두 피연산자 중 하나라도 비트가 1이면 결과의 해당 비트를 1로 설정합니다.
예를 들어, **‘5
3’** 은 이진수로 **‘0101
0011’** 을 계산하여 ‘0111’ 이 되므로, 결과는 ‘7’ 입니다.
3. XOR 연산자(‘^’) : 두 피연산자의 비트가 서로 다를 경우 결과의 해당 비트를 1로 설정합니다.
예를 들어, ‘5 ^ 3’ 은 이진수로 ‘0101 ^ 0011’ 을 계산하여 ‘0110’ 이 되므로, 결과는 ‘6’ 입니다.
4. NOT 연산자(‘~’) : 피연산자의 모든 비트를 반전시킵니다.(1은 0으로, 0은 1로).
예를 들어, ‘~5’ 는 이진수로 ‘~0101’ 을 계산하여 ‘…1010’(무한히 많은 1 다음에 1010)이 되고, 이는 보통 32비트 시스템에서 ‘-6’ 으로 해석됩니다.
5. 왼쪽 시프트(‘«’) : 모든 비트를 왼쪽으로 지정된 수만틈 이동시키고, 오른쪽은 0으로 채웁니다.
예를 들어.‘3 « 2’ 는 ‘0011’ 을 왼쪽으로 2비트 이동하여 ‘1100’ 이 되므로, 결과는 ‘12’ 입니다.
6. 오른쪽 시프트(‘»’) : 모든 비트를 오른쪽으로 지정된 수만큼 이동시키고, 왼쪽은 최상위 비트(부호 비트)의 값으로 채웁니다.
예를 들어, ‘-8 » 2’ 는 ‘11111000’ 을 오른쪽으로 2비트 이동하여 ‘11111110’ 이 되므로, 결과는 ‘-2’ 입니다.
7. 부호 없는 오른쪽 시프트(‘»>’) : 모든 비트를 오른쪽으로 지정된 수만큼 이동시키고, 왼쪽은 0으로 채웁니다. 이는 부호 비트를 무시하고, 순수하게 비트를 오른쪽으로 이동시키기 때문에 음수에 사용했을 때 결과가 달라집니다.
📝 정리
이러한 비트 논리 연산자들은 데이터의 특정 비트를 직접 조작할 필요가 있는 경우에 유용하며, 자바 프로그래밍에서 중요한 도구입니다.
-
☕️[Java] 변수와 자료형(4)
변수와 자료형(4)
1️⃣ 자료형에 대한 이해
1. List
자바 프로그래밍에서 List 는 일련의 요소를 저장하는 데 사용되는 순차적인 컬렉션을 나타냅니다.
이는 자바의 java.util.List 인터페이스를 통해 제공되며, 이는 주문된 컬렉션을 관리하기 위한 다양한 메소드를 제공합니다.
List 는 중복된 요소를 포함할 수 있고, 각 요소는 리스트 내에서 특정 위치를 가집니다.
사용자는 이 위치를 인덱스로 사용하여 리스트의 요소에 접근할 수 있습니다.
List 인터페이스의 주요 특징은 다음과 같습니다.
1. 순서 보장 : 리스트는 요소들이 추가된 순서를 유지하며, 각 요소는 특정 인덱스를 통해 접근할 수 있습니다.
2. 요소의 중복 허용 : 같은 값을 가진 요소를 여러 개 포함할 수 있습니다.
3. 동적 배열 : 리스트의 크기는 고정되어 있지 않고, 요소를 추가하거나 삭제함에 따라 동적으로 조절됩니다.
자바에서는 List 인터페이스를 구현하는 몇 가지 클래스가 있습니다.
가장 흔히 사용되는 구현체는 다음과 같습니다.
'ArrayList' : 내부적으로 배열을 사용하여 요소를 저장합니다.
요소의 추가와 인덱스를 통한 접근이 매우 빠르지만, 크기 조절이 필요할 때는 비용이 많이 들 수 있습니다.
'LinkedList' : 각 요소가 다음 요소에 대한 참조와 함께 저장되는 연결 리스트를 사용합니다.
요소의 추가와 삭제는 빠르지만, 인덱스를 통한 요소 접근은 시작부터 요소를 찾을 때까지 순차적으로 검색해야 하므로 시간이 더 걸립니다.
'Vector' : 'ArrayList' 와 비슷하지만, 다중 스레드 환경에서 안전하게 사용할 수 있도록 동기화된 메소드를 제공합니다.
📝 정리.
'List' 는 자바 컬렉션 프레임워크의 일부이며, 데이터를 관리하고 처리하는 데 매우 유용합니다.
프로그래머는 이러한 컬렉션을 사용하여 데이터를 유연하게 조작할 수 있습니다.
1.2 List의 주요 메서드.
자바의 'List' 인터페이스에는 여러 가지 중요한 메서드들이 포함되어 있으며, 이를 통해 리스트 내의 요소들을 조작하고 접근할 수 있습니다.
다음은 'List' 인터체이스에서 제공하는 몇 가지 주요 메서드들입니다.
'add(E e)': 리스트의 끝에 요소를 추가합니다.
'add(int index, E element)': 지정된 위치에 요소를 삽입합니다.
'addAll(Collection<? extends E> c)': 지정된 컬렉션의 모든 요소를 리스트의 끝에 추가합니다.
'addAll(int index, Collection<? extends E> c)': 지정된 위치부터 컬렉션의 모든 요소를 리스트에 추가합니다.
'clear()': 리스트에서 모든 요소를 제거합니다.
'contains(Object o)': 리스트가 특정 요소를 포함하고 있는지 확인합니다.
'get(int index)': 지정된 위치의 요소를 반환합니다.
'indexOf(Object o)': 주어진 요소의 첫 번째 인덱스를 반환합니다. 요소가 리스트에 없는 경우 -1을 반환합니다.
'lastIndexOf(Object o)': 주어진 요소의 마지막 인덱스를 반환합니다. 요소가 리스트에 없는 경우 -1을 반환합니다.
'isEmpty()': 리스트가 비어 있는지 확인합니다.
'iterator()': 리스트의 요소에 대한 반복자를 반환합니다.
'listIterator()': 리스트의 요소를 리스트 순서대로 반복하는 리스트 반복자를 반환합니다.
'remove(Object o)': 주어진 요소를 리스트에서 처음 발견되는 위치에서 제거하고, 그 결과를 반환합니다.
'remove(int index)': 지정된 위치에 있는 요소를 리스트에서 제거하고, 그 요소를 반환합니다.
'replaceAll(UnaryOperator<E> operator)': 주어진 연산자를 사용하여 리스트의 모든 요소를 대체합니다.
'size()': 리스트에 있는 요소의 수를 반환합니다.
'sort(Comparator<? super E> c)': 주어진 비교자를 사용하여 리스트를 정렬합니다.
'subList(int fromIndex, int toIndex)': 지정된 범위의 부분 리스트를 반환합니다.
'toArray()': 리스트 요소를 배열로 반환합니다.
📝 정리.
이 메서드들을 통해 리스트를 생성, 조회, 수정 및 관리하는 다양한 작업을 수행할 수 있습니다.
List 인터페이스를 사용함으로써 데이터를 효율적으로 처리하고 구조화할 수 있습니다.
2. Map
자바 프로그래밍에서 'Map' 은 키(key)와 값(value)의 쌍을 저장하는 객체입니다.
이는 키를 기반으로 빠르게 값을 검색할 수 있게 해주는 데이터 구조로, 각 키는 고유해야 합니다.(즉, 중복된 키를 가질 수 없습니다.)
'Map' 은 'java.util.Map' 인터페이스를 통해 정의되며, ‘HashMap‘, ‘TreeMap‘, ‘LinkedHashMap‘ 등 다양한 구현체를 가집니다.
자바의 ‘Map‘ 인터페이스는 키-값 쌍으로 데이터를 저장하고 관리하는 데 중점을 두는 데이터 구조로서, 특히 다음과 같은 주요 특징을 가지고 있습니다.
1. 키에 의한 값 접근 : ‘Map‘ 은 각 값에 고유한 키를 할당하며, 이 키를 사용하여 빠르게 해당 값을 검색할 수 있습니다.
이는 데이터베이스의 인덱스와 유사한 방식으로 작동합니다.
2. 키의 유일성 : 맵 내에서 모든 키는 고유해야 합니다.
즉, 같은 키가 두 번 이상 존재할 수 없으며, 새로운 키-값 쌍을 추가할 때 이미 존재하는 키를 사용하면 기존의 값이 새 값으로 대체됩니다.
3. 값의 중복 허용 : 키는 유일해야 하지만 값은 중복될 수 있습니다.
다른 키가 동일한 값을 가리킬 수 있습니다.
4. 순서의 유무 : 일반적인 ‘Map‘ 구현체들은 키-값 쌍의 순서를 보장하지 않습니다.
그러나 ‘LinkedHashMap‘ 과 같은 일부 구현체는 요소가 추가된 순서대로 반복할 수 있는 기능을 제공합니다.
‘TreeMap‘ 은 키에 따라 정렬된 순서를 유지합니다.
5. 비동기화 및 동기화 : 기본적으로 대부분의 ‘Map‘ 구현체는 동기화되지 않습니다.(‘HashMap‘). 이는 멀티 스레드 환경에서 동시 수정이 발생할 경우 안전하지 않을 수 있음을 의미합니다.
반면에 ‘Hashtable‘ 과 같은 구현체는 기본적으로 동기화가 되어 있어 멀티 스레드 환경에서 안전합니다.
또한, ‘Collections.synchronizeMap‘ 메소드를 사용하여 맵을 동기화된 맵으로 변환할 수 있습니다.
6. Null 허용 : 대부분의 ‘Map‘ 구현체는 키와 값으로 ‘null‘ 을 허용합니다.(‘HashMap‘, ‘LinkeHashMap‘).
하지만 ‘Hashtable‘ 은 ‘null‘ 키나 값을 허용하지 않으며, ‘TreeMap‘ 은 자연 정렬 또는 ‘Comparator‘ 가 ‘null‘ 을 처리할 수 있는 경우에만 ‘null‘ 키를 허용합니다.
📝 정리.
이러한 특징들로 인해 ‘Map‘ 은 다양한 애플리케이션에서 유연하고 효율적인 데이터 관리를 가능하게 합니다.
데이터를 쉽게 추가, 검색, 삭제할 수 있어 데이터 관리의 복잡성을 줄이고 성능을 최적화하는 데 기여합니다.
3. Generics.
자바 프로그래밍에서 제네릭스(Generics)는 클래스나 메소드에서 사용될 데이터 타입을 추상화하여 코드 작성 시점에는 구체적인 타입을 명시하지 않고, 객체 생성이나 메소드 호출 시점에 실제 사용할 타입을 지정할 수 있도록 하는 프로그래밍 기법입니다.
제네릭스(Generics)는 코드의 재사용성을 높이고, 타입 안정성을 강화하며, 캐스팅에 대한 오류 가능성을 줄이는 데 도움을 줍니다.
3.1 제네릭스(Generics)의 주요 특징.
1. 타입 안전성(Type Safety) : 제네릭스를 사용하면 컴파일 시점에 타입 체크가 가능하여, 실행 시점에서 발생할 수 있는 'ClassCastException' 과 같은 오류를 사전에 방지할 수 있습니다.
2. 재사용성(Reusability) : 하나의 코드를 다양한 타입에 대해 재사용할 수 있습니다.
예를 들어, 제네릭 클래스나 메소드를 정의하면, 다양한 타입의 객체를 저장하거나 처리하는 로직을 단 한번만 작성하여 여러 타입에 걸쳐 사용할 수 있습니다.
3. 코드 간결성(Code Clarity) : 캐스팅을 줄여 코드가 더욱 간결하고 읽기 쉬워집니다.
3.2 제네릭스의 기본 문법.
클래스 선언 : 클래스 이름 뒤에 '<T>' 를 추가하여 제네릭 클래스를 선언합니다.
‘T’ 는 타입 파라미터를 나타내며, 이는 클래스 내에서 사용될 데이터 타입을 대체하는 플레이스홀더 역할을 합니다.
public class Box<T> {
private T t; // T 타입의 객체를 위한 변수
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
메소드 선언 : 메소드 반환 타입 앞에 ’<T>‘ 를 추가하여 제네릭 메소드를 선언합니다.
public <T> T genericMethod(T t) {
return t;
}
제네릭 타입 제한(Bounded Type Parameters) : 특정 클래스의 하위 클래스만 타입 파라미터로 받도록 제한할 수 있습니다.
이는 'extends' 키워드를 사용하여 지정합니다.
public class Box<T extends Number> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
📝 정리.
제네릭스(Generics)를 사용함으로써 개발자는 보다 타입-안전하고 유지보수가 용이한 코드를 작성할 수 있으며, 실행 시 타입 관련 문제를 효과적으로 줄일 수 있습니다.
-
☕️[Java] 변수와 자료형(3)
변수와 자료형(3).
1️⃣ 자료형에 대한 이해.
1. String.
String 클래스는 불변(immutable)의 문자열을 다룹니다.
이는 한 번 생성된 String 객체의 내용이 변경될 수 없다는 것을 의미합니다.
문자열을 변경하려고 할 때마다 실제로 새로운 String 객체가 생성되고, 기존 객체는 변경되지 않습니다.
1.1 String의 주요 메소드.
charAt(int index) : 지정된 위치의 문자를 반환합니다.
concat(String str) : 현재 문자열의 끝에 지정된 문자열을 붙여 새로운 문자열을 반환합니다.
contains(CharSequence s) : 특정 문자열이 포함되어 있는지 확인합니다.
startsWith(String prefix) : 문자열이 특정 문자열로 시작하는지 확인합니다.
endsWith(String suffix) : 문자열이 특정 문자열로 끝나는지 확인합니다.
equals(Object anObject) : 문자열이 주어진 객체와 동일한지 비교합니다.
indexOf(int ch), indexOf(String str) : 주어진 문자 또는 문자열의 위치를 찾습니다.
length() : 문자열의 길이를 반환합니다.
replace(char oldChar, char newChar) : 문자열 중 일부 문자를 다른 문자로 대체합니다.
substring(int beginIndex, int endIndex) : 문자열의 부분을 추출합니다.
toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다.
trim() : 문자열의 앞뒤 공백을 제거합니다.
2. StringBuffer.
StringBuffer 클래스는 가변(mutable)의 문자열을 다루며, 문자열 변경 작업이 빈번할 때 사용하면 효율적입니다.
StringBuffer 객체는 내용을 직접 변경할 수 있어, 새로운 객체를 계속 생성하지 않아도 됩니다.
2.1 StringBuffer의 주요 메소드.
append(String str): 문자열의 끝에 주어진 문자열을 추가합니다.
delete(int start, int end): 문자열의 시작 인덱스부터 종료 인덱스 전까지의 부분을 삭제합니다.
deleteCharAt(int index): 지정된 위치의 문자를 삭제합니다.
insert(int offset, String str): 지정된 위치에 문자열을 삽입합니다.
replace(int start, int end, String str): 시작 인덱스부터 종료 인덱스 전까지의 문자열을 새로운 문자열로 대체합니다.
reverse(): 문자열의 순서를 뒤집습니다.
length(): 문자열의 길이를 반환합니다.
capacity(): 현재 버퍼의 크기를 반환합니다.
setCharAt(int index, char ch): 지정된 위치의 문자를 다른 문자로 설정합니다.
📝 정리.
StringBuffer 는 스레드에 안전(thread-safe)합니다, 즉 멀티스레드 환경에서 동시에 접근해도 안전하게 사용할 수 있습니다.
이는 내부적으로 메소드들이 동기화되어 있기 때문입니다.
반면, StringBuilder 는 StringBuffer 와 유사하지만 멀티스레드 환경에서의 동기화 자원이 없어 단일 스레드에서 더 빠르게 작동합니다.
이처럼 String 과 StringBuffer 는 각각의 특성에 맞게 선택하여 사용할 수 있으며, 성능과 사용상황에 따라 적절히 활용하면 됩니다.
3. Array.
자바 프로그래밍에서 배열(Array)은 동일한 타입의 여러 데이터를 연속적인 메모리 위치에 저장하기 위한 자료구조입니다.
배열은 고정된 크기를 가지며, 배열의 각 요소는 같은 데이터 타입을 가집니다.
배열을 사용하면 여러 데이터를 하나의 변수 이름으로 관리할 수 있어 코드를 간결하게 작성할 수 있습니다.
3.1 배열의 특징.
고정된 크기 : 배열은 생성 시 지정된 크기를 변경할 수 없습니다.
배열의 크기는 프로그램 실행 도중에 변경할 수 없으며, 더 많은 데이터를 저장해야 할 경우 새로운 배열을 생성하고 데이터를 복사해야 합니다.
인덱스 접근 : 배열의 각 요소는 인덱스를 통해 접근할 수 있습니다.
인덱스는 0부터 시작하여 배열의 크기 -1까지 번호가 할당됩니다.
동일 타입 : 모든 배열 요소는 동일한 데이터 타입을 가져야 합니다.
예를 들어, int 타입의 배열은 int 타입의 값만을 요소로 가질 수 있습니다.
3.2 배열의 선언과 초기화.
배열을 선언하고 사용하기 위해서는 다음 단계를 따라야 합니다.
1. 배열 선언 : 데이터 타입 뒤에 대괄호 [] 를 사용하여 배열을 선언합니다.
int[] myArray;
String[] stringArray;
2. 배열 생성 : new 키워드를 사용하여 배열을 생성하고, 배열의 크기를 지정합니다.
myArray = new int[10]; // 10개의 정수를 저장할 수 있는 배열
stringArray = new String[5]; // 5개의 문자열을 저장할 수 있는 배열
3. 배열 초기화 : 배열의 각 요소에 값을 할당합니다.
인덱스를 사용하여 접근할 수 있습니다.
myArray[0] = 50;
myArray[1] = 100;
stringArray[0] = "Hello";
stringArray[1] = "World";
3.3 배열 사용 예.
다음은 자바에서 int 배열을 선언, 생성, 초기화하는 예제 코드입니다.
public class ArrayExample {
public static void main(Stringp[] args) {
// 배열 선언과 동시에 생성
int[] numbers = new int[3];
// 배열 초기화
numbers[0] = 7;
numbers[1] = 20;
numbers[2] = 33;
// 배열 사용
System.out.println("첫 번째 숫자: " + numbers[0]);
System.out.println("두 번째 숫자: " + numbers[1]);
System.out.println("세 번째 숫자: " + numbers[2]);
}
}
📝 정리
이 예제에서는 numbers 라는 이름의 int 배열을 생성하고, 세 개의 정수를 저장한 후 출력합니다.
배열을 사용하는 이점 중 하나는 이처럼 단일한 이름으로 여러 데이터를 효율적으로 관리할 수 있다는 것입니다.
-
☕️[Java] 자바 - 변수와 자료형(2)
☕️ 자바 - 변수와 자료형(2)
1. 자료형에 대한 이해
자바 프로그래밍에서 사용되는 자료형은 크게 기본형(Primitive types) 과 참조형(Reference types) 두 가지로 나눌 수 있습니다.
각각의 자료형에 대해 설명드리겠습니다.
1️⃣ 기본형(Primitive types)
기본형 자료형은 실제 값을 저장하는 타입으로, 총 8가지가 있습니다.
정수형
byte : 8비트 정수형, 값의 범위는 -128에서 127까지.
short : 16비트 정수형, 값의 범위는 -32,768에서 32,767까지.
int : 32비트 정수형, 값의 범위는 약 -2.14억에서 2.14억까지.
long : 64비트 정수형, 값의 범위는 약 -9.22경에서 9.22경까지
실수형
float : 32비트 부동 소수점 형. 부정확할 수 있으며, 대략 6~7 자리의 정밀도를 가짐.
double : 64비트 부동 소수점 형. float보다 더 정밀하며, 대략 15자리의 정밀도를 가짐.
문자형
char : 단일 16비트 유니코드 문자를 저장.
논리형
boolean : true 또는 false 값만을 가짐.
2️⃣ 참조형(Reference types)
참조형 자료형은 객체의 참조(메모리 주소)를 저장합니다.
기본형과 달리 메모리의 특정 위치를 가리키는 포인터를 저장하므로, 객체의 크기에 관계없이 참조 변수 크기는 항상 일정합니다.
참조형의 예를 들면 다음과 같습니다.
클래스(Class)
예: String, Integer, File 등
인터페이스(Interface)
예: List, Map, Serializable 등
배열(Array)
예: int[], double[], String[] 등
1.1 인터페이스(Interface)?
자바에서 인터페이스(Interface)는 특정 클래스가 구현해야 할 메소드를 정의하는 “계약”의 역할을 합니다.
이는 클래스가 인터페이스에 정의된 모든 메소드를 반드시 구현하도록 강제합니다.
인터페이스는 메소드의 실제 구현을 포함하지 않고, 메소드의 시그니처(이름, 매개변수 리스트, 반환 유형)만을 정의합니다.
인터페이스를 사용하는 주된 목적은 다음과 같습니다.
1. 추상화(Abstraction) : 인터페이스를 통해 구현의 세부 사항을 숨기고, 사용자에게 필요한 기능만을 제공할 수 있습니다.
이렇게 함으로써 코드의 복잡성을 줄이고, 유지 관리가 쉬워집니다.
2. 다형성(Polymorphism) : 다양한 클래스들이 동일한 인터페이스를 구현함으로써, 다양한 타입의 객체를 동일한 방식으로 처리할 수 있습니다.
이는 코드의 유연성과 재사용성을 높입니다.
3. 결합도 감소(Decoupling) : 인터페이스를 통해 서로 다른 코드 부분 간의 결합도를 낮추어, 각 부분을 독립적으로 개발하고 테스트할 수 있게 합니다.
👉 인터페이스 예시
예를 들어, List 인터페이스는 add ,remove, get, size 등의 메소드를 정의하며, 이 인터페이스를 구현하는 ArrayList, LinkedList 등의 클래스는 이 메소드들을 실제로 구현해야 합니다.
이를 통해 사용자는 구체적인 리스트의 구현 방법을 몰라도 이 인터페이스를 통해 리스트를 사용할 수 있습니다.
이런 방식으로 인터페이스는 참조형 자료형 중 하나로서, 객체의 행동을 정의하고 다양한 구현을 가능하게 합니다.
-
-
☕️[Java] 자바 - 변수와 자료형(1)
변수와 자료형(1)
🙋♂️ 변수 이름 규칙
자바 프로그래밍에서 변수를 명명할 때 따라야 할 몇 가지 기본적인 규칙과 관례가 있습니다.
이러한 규칙을 준수하는 것은 코드의 가독성과 유지보수성을 높이는 데 중요합니다.
다음은 자바에서 변수 이름을 지정할 때 고려해야 할 주요 규칙들 입니다.
1️⃣ 기본 규칙.
**문자와 숫자, (Underscore), $ 사용가능 :** 변수 이름은 문자(letter) 나 밑줄(, Underscore) 또는 $(달러 기호)로 시작할 수 있습니다.
그러나 숫자로 시작할 수는 없습니다.
숫자로 시작할 수 없다 : 첫 글자로는 숫자로 변수 이름을 시작할 수 없습니다.
그러나 첫 글자 이후에는 숫자가 포함될 수 있습니다.
대문자와 소문자를 구분함 : 변수 이름을 명명시, 대문자와 소문자를 구분합니다.
예를 들어 int apple = 1;, int Apple = 2;, int APPLE = 3;은 모두 다른 변수로 취급됩니다.
공백을 허용하지 않음 : 변수 이름을 명명시 공백이 들어가서는 않됩니다.
예를 들어 int my friends = 7;과 같이 공백이 들어가서는 안됩니다.
특수 문자 제한 : ‘_(underscore)’와 ‘$’를 제외한 특수 문자는 변수명으로 사용할 수 없습니다.
예를 들어 ‘@’, ‘#’, ‘%’ 등은 변수 이름으로 사용할 수 없습니다.
자바 예약어 사용 금지 : int, class, static 등 자바에서 이미 의미를 갖는 예약어는 변수 이름으로 사용할 수 없습니다.
2️⃣ 표기법 및 관례(컨벤션)
카멜 케이스 : 첫 단어는 소문자로 시작하고, 이어지는 각 단어의 첫 글자는 대문자로 시작합니다.
예를 들어, firstName, totalAmount 등 입니다.
파스칼 케이스 : 각 문자의 첫 문자를 대문자로 표기합니다.
예를 들어, MyFriends, ToTalCount 등 입니다.
의미 있는 이름 : 변수 이름은 그 변수가 무엇을 의미하는지 명확하게 표현해야 합니다.
예를 들어, numberOfStudents 는 학생 수를, temperature는 온도를 나타내는 등의 명확한 이름을 사용하는 것이 좋습니다.
상수 이름 규칙 : 상수(변하지 않는 값)는 모두 대문자를 사용하며, 단어 사이는 밑줄(‘_‘)로 구분합니다.
예를 들어 MAX_HEIGHT, TOTAL_COUNT 등 입니다.
-
☕️[Java] 코테 맛보기(2) - 코테를 위한 자료구조와 알고리즘 개념 구현 방법 숙지
😋 코테 맛보기.
코딩 테스트를 준비하기 위해서 자료구조와 알고리즘은 매우 중요한 영역입니다.
이 분야들에 대한 깊이 있는 이해와 숙지는 테스트에서 성공적인 성과를 내는 데 결정적인 역할을 합니다.
다음은 코딩 테스트를 위해 필요한 자료구조와 알고리즘의 개념 및 구현 방법에 대한 가이드입니다.
1️⃣ 자료구조.
기본 자료구조 : 배열, 스택, 큐, 링크드 리스트.
이러한 자료구조들은 다양한 문제에서 데이터를 효율적으로 관리하는 기본적인 방법을 제공합니다.
고급 자료구조 : 트리(특히 이진 검색 트리), 힙, 그래프, 해시 테이블, 집합 등
이들은 보다 복잡한 데이터 관계를 다루는 데 사용되며, 특정 유형의 문제를 해결하는 데 특화되어 있습니다.
응용 자료구조 : 트라이, 세그먼트 트리, 유니온 파인드, 비트마스크 등.
이들은 특정 알고리즘 문제에 최적화된 솔루션을 제공합니다.
2️⃣ 알고리즘.
정렬 알고리즘 : 버블 정렬, 삽입 정렬, 선택 정렬, 퀵 정렬, 병합 정렬 등.
정렬은 많은 문제에서 데이터를 조작하는 기본적인 방법입니다.
검색 알고리즘 : 선형 검색, 이진 검색 등
데이터 내에서 특정 항목을 찾는 방법입니다.
재귀 알고리즘과 백트래킹 : 문제를 더 작은 문제로 나누어 해결하는 기법입니다.
동적 프로그래밍 : 복잡한 문제를 간단한 하위 문제로 나누어 해결하고, 그 결과를 저장하여 효율적으로 최종 결과를 도출합니다.
그래프 알고리즘 : 깊이 우선 탐색(DFS), 너비 우선 탐색(BFS), 최단 경로 문제(다익스트라, 플로이드-워셜), 최소 신장 트리(크루스칼, 프림) 등을 포함합니다.
3️⃣ 코테 준비 방법.
이론 학습 : 자료구조와 알고리즘의 이론을 철저히 학습합니다.
이론적인 이해는 효과적인 구현의 기초가 됩니다.
실습 연습 : 이론을 바탕으로 다양한 문제를 실제로 코딩해 봄으로써 실력을 키웁니다.
LeetCode, HackerRank, Codeforces, 백준, 프로그래머스 등의 플랫폼에서 문제를 풀어봅니다.
알고리즘 패턴 학습 : 자주 출제되는 문제 유형과 그에 대한 표준적인 해결 방법을 익힙니다.
시간 관리 연습 : 코딩 테스트에서는 제한된 시간 내에 문제를 해결해야 하므로, 시간 관리 능력을 향상시킬 필요가 있습니다.
📝 정리.
코테를 위한 준비 과정에서 이러한 자료구조와 알고리즘에 대한 이해와 숙련도는 문제를 정확하고 효율적으로 해결할 수 있는 능력을 직접적으로 높여 줍니다.
따라서, 이 분야에 대한 철저한 준비와 연습을 통해 자신감을 갖고 테스트에 임할 수 있습니다.
-
☕️[Java] 코테 맛보기(1) - 코테를 위한 자바 프로그래밍 언어 사용 숙련도
😋 코테 맛보기.
코딩 테스트를 위해 자바 프로그래밍 언어를 사용하려면 몇 가지 중요한 요소에 숙력도를 갖추어야 합니다.
자바는 많은 기업들이 코딩 테스트에 사용하는 언어 중 하나이며, 효과적인 문제 해결을 위해 다음과 같은 능력을 개발하는 것이 중요합니다.
효과적인 문제 해결을 위해 다음과 같은 능력을 개발하는 것이 중요합니다.
1️⃣ 기본 문법 숙지.
자바의 기본 문법과 프로그래밍 구조에 익숙해져야 합니다.
변수, 데이터 타입, 연산자, 제어문(if, for, while 등), 메소드 호출 등 기본적인 구성 요소를 이해하고 사용할 수 있어야 합니다.
2️⃣ 객체 지향 프로그래밍(OOP)이해.
자바는 객체 지향 프로그래밍 언어입니다.
클래스, 객체, 상속, 다형성, 캡슐화 등의 객체 지향 개념을 이해하고 이를 문제 해결에 적절히 적용할 수 있어야 합니다.
OOP 개념은 코드의 재사용성과 모듈성을 높여줘 효율적인 프로그래밍을 가능하게 합니다.
3️⃣ 표준 라이브러리 사용.
자바의 표준 라이브러리에는 다양한 자료구조와 알고리즘이 구현되어 있습니다.
java.util 패키지 내의 컬렉션 프레임워크(리스트, 맵, 셋 등)를 비롯해, 유용한 유틸리티 클래스들을 활용할 줄 알아야 합니다.
이러한 라이브러리들은 코딩 테스트에서 효율적인 코드 작성을 돕습니다.
4️⃣ 알고리즘과 자료구조.
다양한 알고리즘과 자료구조에 대한 이해가 중요합니다.
정렬, 탐색, 그래프 이론, 동적 프로그래밍 등의 알고리즘과 배열, 스택, 큐, 링크드 리스트, 트리 등의 자료구조에 대한 깊은 이해가 필요합니다.
이는 문제를 효과적으로 분석하고 최적의 해결책을 구현하는 데 결정적입니다.
5️⃣ 문제 해결 능력.
실제 코딩 테스트에서는 다양한 유형의 문제가 제시됩니다.
문제를 빠르게 이해하고 효과적인 해결책을 설계할 수 있는 능력이 필요합니다.
이는 실전 연습을 통해 향상시킬 수 있으며, 온라인 코딩 플랫폼에서 다양한 문제를 풀어 보는 것이 좋습니다.
6️⃣ 테스트와 디버깅
코드가 예상대로 동작하는지 검증하고, 오류를 찾아 수정할 수 있는 능력도 중요합니다.
자바에서 제공하는 디버깅 도구를 사용하여 코드를 단계별로 실행하고, 변순의 상태를 확인하며 문제를 진단할 수 있어야 합니다.
📝 정리.
코딩 테스트를 위한 자바 숙련도는 이론적 지식과 실제 적용 능력의 조합을 요구합니다.
이를 위해 개념 학습과 함께 많은 실습을 병행하는 것이 중요합니다.
시간을 정해두고 실전처럼 문제를 풀어보는 연습을 꾸준히 하면, 효과적으로 자바를 활용하여 코딩 테스트에서 좋은 성과를 낼 수 있을 것입니다.
-
☕️[Java] 자바 - 소개
자바 - 소개
🙋♂️ 1. 자바의 특징.
자바는 세계적으로 널리 사용되는 프로그래밍 언어로, 웹 개발, 모바일 애플리케이션, 대규모 시스템 구축 등 다양한 분야에 활용됩니다.
자바의 주요 특징들은 다음과 같습니다.
1️⃣ 플랫폼 독립성.
“Write Once, Run Anywhere”(WORA) : 자바 프로그램은 자바 가상 머신(JVM) 위에서 실행되기 때문에, 한 번 작성하면 어떤 플랫폼에서도 실행할 수 있습니다.
이는 자바 컴파일러가 소스 코드를 플랫폼 독립적인 바이트코드로 변환하기 때문입니다.
2️⃣ 객체 지향 프로그래밍(OOP).
자바는 객체 지향 프로그래밍 언어로, 캡슐화, 상속, 다형성 등을 완전히 지원합니다.
이는 코드 재사용, 유지 관리의 용이성 및 시스템 모듈화를 가능하게 합니다.
3️⃣ 강력한 표준 라이브러리.
자바는 방대한 표준 라이브러리를 제공하여, 네트워킹, 파일 시스템 접근, 그래픽 인터페이스 제작 등 다양한 작업을 쉽게 처리할 수 있도록 돕습니다.
4️⃣ 메모리 관리.
자동 가비지 컬렉션
자바는 사용하지 않는 객체를 자동으로 감지하고 메모리에서 제거하는 가비지 컬렉터를 내장하고 있습니다. 이는 개발자가 메모리 누수에 대해 걱정할 필요가 적어지게 해줍니다.
5️⃣ 보안.
자바는 샌드박스 환경에서 애플리케이션을 실행하여 시스템 리소스에 대한 무단 접근을 방지합니다.
또한, 클래스 로더, 바이트코드 검증기 등을 통해 애플리케이션이 안전하게 실행될 수 있도록 합니다.
6️⃣ 멀티스레딩.
자바는 내장된 멀티스레딩 기능을 지원하여, 여러 스레드가 동시에 실행되도록 하여 애플리케이션의 효율성을 높입니다.
이는 특히 네트워크 서버와 실시간 시스템에서 큰 장점입니다.
7️⃣ 로버스트와 포터빌리티.
자바 프로그램은 다른 플랫폼으로의 이동성이 뛰어나며, 높은 수준의 안정성을 제공합니다.
예외 처리 기능을 통해 오류를 쉽게 관리하고, 시스템의 안정성을 높일 수 있습니다.
📝 마무리.
자바의 이러한 특징들은 그것을 매우 유연하고, 다양한 애플리케이션 개발에 적합하게 만듭니다.
이로 인해 자바는 세계적으로 인기 있는 프로그래밍 언어 중 하나로 자리 잡게 되었습니다.
🙋♂️ 2. 자바 프로그램의 작성과 실행과정.
1️⃣ 소스 코드 작성.
개발자는 자바의 문법에 맞추어 .java 확장자 파일에 소스 코드를 작성합니다.
이 파일에는 하나 이상의 클래스가 포함되며, 각 클래스는 데이터와 메서드를 정의합니다.
2️⃣ 컴파일.
소스 코드 파일을 자바 컴파일러(javac)를 사용하여 컴파일합니다.
컴파일러는 소스 코드를 읽고, 문법 오류를 검사한 후, 바이트코드라는 중간 형태의 코드로 변환합니다.
이 바이트 코드는 .class 파일로 저장됩니다.
바이트코드는 플랫폼 독립적이기 때문에, 한 번 컴파일된 .class 파일은 다양한 운영 체제에서 실행될 수 있습니다.
3️⃣ 로딩.
자바 가상 머신(JVM)은 .class 파일을 로드합니다.
클래스 로더(component of JVM)가 이 작업을 수행하며, 필요한 클래스 파일들을 메모리에 로드합니다.
4️⃣ 링킹.
로드된 클래스 파일들은 링킹 과정을 거칩니다. 링킹은 검증, 준비, 그리고(선택적으로) 해석 단계를 포함합니다.
검증 : 로드된 바이트코드가 올바르게 포맷되었는지, 안전한지 검사합니다.
준비 : 클래스 변수와 기본값을 위한 메모리를 할당합니다.
해석 : 심볼릭 메모리 참조를 직접 참조로 변환합니다(선택적).
5️⃣ 초기화.
클래스 초기화 단계에서 정적 변수들에 대한 초기화가 수행되며, 정적 블록이 실행됩니다.
6️⃣ 실행.
프로그램 실행 동안 JVM 내부에서 가비지 컬렉터가 사용되지 않는 객체를 자동으로 감지하고, 할당된 메모리를 해제하여 메모리를 관리합니다.
📝 마무리.
자바의 이러한 실행 과정은 코드의 플랫폼 독립성을 보장하고, 안정적이며 보안적인 실행 환경을 제공합니다.
이 모든 과정은 개발자로부터 대부분 숨겨져 있으며, 개발자는 주로 소스 코드 작성과 일부 디버깅에 집중할 수 있습니다.
-
☕️[Java] 자바란?
자바란?
자바 언어 특징.
1. 타 언어에 비해 배우기 쉽습니다.
2. 플랫폼에 독립적입니다.
- 자바 언어가 플랫폼에 독립적인 이유는 그 설계 철학과 메커니즘에 근거합니다.
- 자바는 "한 번 작성하면, 어디서든 실행된다(Write Once, Run Anywhere, WORA)" 라는 철학을 실현하기 위해 개발되었습니다.
- 이를 가능하게 하는 핵심 요소는 자바 가상 머신(Java Virtual Machine, JVM)과 자바 바이트코드의 도입입니다.
자바의 플랫폼 독립성의 주요 요인
1. 자바 가상 머선(JVM)
JVM은 자바 바이트 코드를 실행할 수 있는 런타임 환경을 제공합니다. 자바 프로그램이 컴파일되면, 플랫폼에 독립적인 바이트코드로 변환됩니다.
이 바이트코드는 어떤 특정 하드웨어나 운영 체제의 기계어 코드가 아닌, JVM이 이해할 수 있는 중간 형태의 코드입니다.
JVM은 바이트코드를 받아 각 플랫폼에 맞는 기계어 코드로 변환하고 실행합니다.
따라서, 자바 애플리케이션은 다양한 운영 체제에서 JVM만 설치되어 있으면 실행될 수 있습니다.
2. 컴파일과 실행의 분리
자바 프로그램은 소스 코드(.java 파일)에서 바이트코드(.class 파일)로 컴파일되는 과정과, 실행 시 바이트 코드가 실제로 실행되는 과정으로 나누어집니다. 이 두 단계의 분리는 프로그램을 한 번 컴파일하면, 그 컴파일된 코드가 다양한 환경의 JVM에서 실행될 수 있게 합니다.
3. 표준화된 API
자바는 풍부하고 표준화된 API를 제공합니다. 이 API들은 플랫폼에 관계없이 일관된 방식으로 작동하므로, 개발자는 운영 체제의 특징을 신경 쓰지 않고도 애플리케이션을 개발할 수 있습니다. 예를 들어, 파일 시스템 접근, 네트워크 프로그래밍 등의 기능은 모든 플랫폼에서 동일한 자바 코드로 작동합니다.
4. 언어와 라이브러리의 독립성
자바 언어와 표준 라이브러리는 플랫폼에 특화된 구현으로부터 독립적입니다.
즉, 자바의 표준 라이브러리 구현은 다양한 하드웨어와 운영 체제에서 동일하게 작동하도록 설계되었습니다.
3. 객체지향 프로그래밍입니다.
객체지향 프로그래밍?
자바에서의 객체지향 프로그래밍(Object-Oriented Programming, OOP)은 소프트웨어를 설계하고 구현할 때 객체라는 개념을 중심으로 프로그래밍하는 방식을 말합니다.
객체지향 프로그래밍은 코드의 재사용성, 확장성 및 관리 용이성을 높이는 데 도움이 됩니다.
자바는 객체지향 언어의 특징을 강하게 반영하고 있으며, 다음과 같은 기본 원칙에 따라 프로그래밍 됩니다.
1. 캡슐화(Encapsulation)
객체의 데이터(속성)와 그 데이터를 조작하는 메소드를 하나의 단위로 묶는 것을 말합니다.
캡슐화를 사용하면 객체의 세부 구현 내용을 외부에서 알 필요 없이 객체가 제공하는 기능만을 사용할 수 있으며, 이는 코드의 유지보수를 용이하게 합니다.
2. 상속(Inheritance)
한 클래스가 다른 클래스의 특성을 상속 받아 사용할 수 있게 하는 것입니다.
이를 통해(상속을 통해) 기존 코드를 재사용하면서 확장할 수 있고, 코드의 중복을 줄이며 유지 보수가 쉬워집니다.
3. 다형성(Polymorphism)
같은 이름의 메소드가 다른 작업을 수행할 수 있도록 하여 메소드의 오버라이딩(Overriding)이나 오버로딩(Overloading)을 가능하게 합니다.
오버라이딩(Overriding) : 자식 클래스가 상속 받은 부모 클래스의 메소드를 재정의 하는 행위를 말합니다. 오버라이딩을 통해 자식 클래스는 상속 받은 메소드와 동일한 시그니처(메소드 이름, 매개변수 리스트)를 가지지만, 그 내용을 자신의 특정한 요구에 맞게 새롭게 구현할 수 있습니다. 오버라이딩된 메소드는 실행 시 다형성을 활용하여 해당 객체의 실제 타입에 따라 적절한 메소드가 호출됩니다.
예시 코드
java
class Animal {
void display() {
System.out.println("This is an animal.");
}
}
class Cat extends Animal {
@Override
void display() {
System.out.println("This is a cat.")
}
}
오버로딩(Overloading) : 같은 클래스 내에서 같은 이름의 메소드를 여러 개 정의할 수 있도록 하지만, 매개변수의 타입, 개수 또는 순서가 달라야 합니다. 이를 통해 메소드에 다양한 입력 파라미터를 제공할 수 있으며, 프로그래머가 같은 동작을 하는 메소드에 대해 다양한 옵션을 제공할 수 있습니다. 오버로딩은 컴파일 시간에 결정되며, 메소드 호출 시 전달된 매개변수에 따라 적절한 메소드가 선택됩니다.
예시 코드
class Display {
void show(int a) {
System.out.println("Number: " + a);
}
void show(String a) {
System.out.println("String: " + a);
}
void show(int a, int b) {
System.out.println("Two numbers: " + a + ", " + b);
}
}
이처럼 오버라이딩과 오버로딩은 자바 프로그래밍에서 메소드의 기능을 확장하거나 변경할 때 유용하게 쓰이는 기법입니다.
오버라이딩은 주로 다형성을 활용한 동적 바인딩을 목적으로 하며, 오버로딩은 같은 이름의 메소드에 여러 입력 형태를 제공하기 위해 사용됩니다.
4. 추상화(Abstraction)
복잡한 실제 상황을 단순화하는 과정에서 중요한 특징만을 추출하여 프로그램 코드에 반영하는 것을 의미합니다.
추상 클래스와 인터페이스를 통해 구현될 수 있습니다.
이러한 원칙들은 자바를 사용하여 복잡한 시스템을 개발할 때 코드의 모듈화를 가능하게 하고, 이로 인해 대규모 소프트웨어 개발과 프로젝트 관리가 용이해집니다.
코드의 모듈화(Modularization): 큰 프로그램을 작은 세부 모듈로 나누는 프로세스를 의미합니다. 이러한 모듈은 각각 독립적인 기능을 수행하며, 전체 시스템의 한 부분으로 기능합니다. 모듈화의 주요 목적은 프로그램의 관리를 용이하게 하고, 개발을 효율적으로 만들며, 코드의 재사용성을 높이는 것입니다.
모듈화의 주요 이점은 다음과 같습니다.
1. 유지보수성
모듈화된 코드는 각 모듈이 분리되어 있기 때문에, 하나의 모듈에서 발생한 문제가 다른 모듈에 미치는 영향을 최소화할 수 있습니다. 따라서 개별 모듈을 독립적으로 수정, 업데이트, 테스트할 수 있어 전체 코드베이스의 유지보수가 더 쉬워 집니다.
모듈(Module): 소프트웨어 설계에서 사용되는 기본 개념 중 하나로, 관련된 기능들을 논리적으로 그룹화하고 독립적으로 사용할 수 있는 코드의 단위를 의미합니다. 모듈은 프로그램의 특정 기능을 담당하며, 독립적인 개발, 테스트, 재사용이 가능하도록 설계됩니다. 모듈화된 코드는 대체로 명확하고 관리하기 쉬운 구조를 갖습니다.
모듈의 특징으로는 다음과 같습니다.
1. 독립성
모듈은 가능한 한 다른 모듈과 독립적으로 동작할 수 있어야 하며, 이를 통해 시스템의 복잡성을 줄이고, 각 모듈의 재사용성을 높일 수 있습니다.
2. 캡슐화
모듈은 자신의 구현 세부사항을 숨기고, 필요한 기능만을 외부에 제공하는 인터페이스를 통해 상호작용합니다. 이로 인해 모듈 간의 상호 의존성이 줄어들고, 변경 관리가 용이해집니다.
3. 인터페이스
모듈은 정의된 인터페이스를 통해 외부와 통신합니다. 인터페이스는 모듈이 제공하는 기능과 해당 기능을 어떻게 접근할 수 있는지를 명시합니다.
모듈의 예로는 다음으로 들 수 있습니다.
라이브러리
특정 기능을 제공하는 함수나 데이터 구조를 모아 놓은 코드 집합. 예를 들어, 수학 연산을 위한 수학 라이브러리, 데이터베이스 작업을 위한 데이터베이스 접근 라이브러리 등이 있습니다.
클래스
객체지향 프로그래밍에서 클래스는 속성(데이터)과 메소드(함수)를 캡슐화하여 모듈을 형성합니다. 클래스는 독립적으로 사용될 수 있으며, 다른 클래스와 상호작용할 수 있습니다.
패키지
관련된 여러 클래스나 모듈을 하나의 더 큰 단위로 그룹화한 것 입니다. 예를 들어, Java에서는 java.util 패키지가 여러 유틸리티 클래스와 인터페이스를 제공합니다.
모듈은 개발 과정을 체계화하고, 코드의 재사용성을 증가시키며, 유지 관리를 용이하게 하는 중요한 역할을 합니다.
모듈은 크기가 클 수도 있고 작을 수도 있으며, 프로젝트의 요구와 설계에 따라 그 범위와 기능이 결정됩니다.
2. 재사용성
잘 설계된 모듈은 다른 프로그램에서도 재사용할 수 있습니다. 이는 소프트웨어 개발 시간과 비용을 줄이는 데 도움이 되며, 일관된 기능을 여러 프로젝트에 걸쳐 사용할 수 있습니다.
3. 확장성
모듈화는 시스템의 확장성을 향상시킵니다. 새로운 기능이 필요할 때 기존 모듈을 수정하거나 새로운 모듈을 추가하기가 더 쉬워집니다. 이는 시스템의 유연성을 증가시키고, 변화하는 요구사항에 더 잘 대응할 수 있게 합니다.
4. 가독성
작은 모듈로 나뉘어진 코드는 각각의 모듈이 명확한 기능을 수행하기 때문에, 전체 코드의 구조를 이해하기가 더 쉽습니다. 개발자가 프로그램의 특정 부분만을 이해하고도 효과적으로 작업할 수 있습니다.
5. 팀 협업 향상
모듈화는 여러 개발자가 동시에 다른 모듈에서 작업할 수 있게 함으로써 팀 작업을 용이하게 합니다. 각 팀원이 특정 모듈에 집중할 수 있으며, 전체 프로젝트에 대한 의존성을 줄이면서 협업을 효율적으로 진행할 수 있습니다.
이처럼 코드의 모듈화는 소프트웨어 개발 과정에서 중요한 역할을 하며, 특히 대규모 프로젝트나 복잡한 시스템 개발에 있어 필수적인 접근 방식입니다.
4. Garbage Collector로 사용되지 않는 메모리를 자동적으로 정리해줍니다.
Garbage Collector(GC): 프로그램이 동적으로 할당한 메모리 영역 중에서 더 이상 사용하지 않는 부분을 자동으로 찾아서 해제하는 시스템을 말합니다. 이 과정을 통해 프로그램에서 발생할 수 있는 메모리 누수를 방지하고, 사용 가능한 메모리 리소스를 최적화합니다.
프로그램이 동적으로 할당한 메모리 영역 : 프로그램 실행 중에 필요에 따라 할당되고 해제되는 메모리를 말합니다. 이는 프로그램의 런타임 중에 사용자의 요구나 데이터의 양에 따라 변화하는 메모리 요구 사항을 수용하기 위해 사용됩니다. 동적 메모리 할당은 프로그램이 시작할 때 필요한 메모리 양을 미리 알 수 없는 경우나, 실행 도중에 메모리 사용량이 변할 때 유용합니다.
동적 메모리 할당의 특징은 아래와 같습니다.
1. 유연성 : 동적 메모리 할당은 프로그램 실행 중에 필요한 메모리 크기를 조정할 수 있게 해줍니다. 이로 인해 프로그램은 사용자의 입력, 파일 크기, 또는 다른 실행 시 요소들에 따라 메모리 사용을 최적화할 수 있습니다.
2. 효율성 : 필요할 때만 메모리를 할당하고, 더 이상 사용하지 않는 메모리를 해제함으로써 시스템 리소스를 보다 효율적으로 사용할 수 있습니다.
3. 메모리 관리 : 동적 메모리는 일반적으로 힙(Heap) 영역에서 관리됩니다. 힙은 프로그램의 데이터 영역 중 하나로, 동적으로 할당되는 객체와 데이터에 사용됩니다. 힙 영역의 크기는 프로그램 실행 도중에 확장되거나 축소될 수 있습니다.
동적 메모리 할당의 예는 다음과 같습니다.
자바에서는 new 키워드를 사용하여 객체를 생성할 때 동적 메모리 할당이 일어납니다. 예를 들어, new ArrayList() 를 호출하면, 자바 런타입은 필요한 메모리를 힙에서 할당하여 ArrayList 객체를 저장합니다.
객체 사용이 끝나면 자바의 GC가 더 이상 참조되지 않는 객체가 사용하던 메모리를 자동으로 해제합니다.
동적 메모리 할당은 프로그램이 더 유연하고 효율적으로 동작하도록 돕지만, 관리가 제대로 이루어지지 않을 경우 메모리 누수나 성능 저하 같은 문제를 초래할 수 있습니다. 따라서 프로그래머는 동적 메모리 관리를 신중하게 수행해야 합니다.
GC의 주요 기능은 다음과 같습니다.
1. 메모리 관리 자동화 : 프로그래머가 메모리 할당 및 해제를 직접 관리하는 대신 자바 런타입이 이를 자동으로 처리합니다. 이로 인해 개발자는 메모리 관리에 신경 쓰지 않고, 애플리케이션 로직 개발에 더 집중할 수 있습니다.
2. 메모리 누수 방지 : GC는 참조되지 않는 객체들을 정기적으로 청소하여 메모리 누수를 방지합니다. 객체가 더 이상 필요 없을 때 자동으로 메모리에서 제거됩니다.
3. 효율적인 메모리 사용 : 사용되지 않는 객체들을 정리함으로써 메모리를 효율적으로 사용하고, 애플리케이션의 성능을 유지할 수 있도록 도와줍니다.
GC의 작동 원리는 다음과 같습니다.
GC은 크게 두 단계로 진행됩니다.
1. 객체 탐지 : GC는 더 이상 어떤 객체에도 참조되지 않는 객체들을 탐지합니다. 이러한 객체들은 프로그램에서 더 이상 사용되지 않는 것으로 간주됩니다.
2. 메모리 회수 : 탐지된 객체들이 차지하고 있는 메모리를 해제합니다. 이 메모리는 다시 사용 가능한 상태가 되어, 새로운 객체를 위해 재할당될 수 있습니다.
GC 알고리즘
자바는 다양한 GC 알고리즘을 제공합니다. 대표적인 몇 가지는 다음과 같습니다.
Mark-and-Sweep : 사용 중인 객체를 “표시(mark)”하고, 표시되지 않은 객체를 “쓸어내는(sweep)” 방식입니다.
Generational GC : 객체를 세대별로 분류하여, 생성된지 얼마 되지 않은 객체들(Young Generation)과 오래된 객체들(Old Generation)을 다르게 관리합니다. 이 방식은 대부분의 객체가 생성 후 짧은 시간 내에 소멸된다는 관찰에 기반합니다.
Compacting : 사용 중인 객체들을 메모리의 한쪽으로 몰아넣어(Compact), 메모리의 연속성을 높이고, 메모리 단편화를 방지합니다.
GC은 메모리 관리를 자동화하지만, 때로는 성능 저하를 일으킬 수 있습니다. 특히 GC가 실행되는 동안에는 프로그램의 다른 모든 작업이 일시적으로 중단(Stopping the world)될 수 있기 때문에, GC 동작 방식과 설정을 잘 이해하고 조절하는 것이 중요합니다.
JVM(Java Virtual Machine)
JVM은 자바 애플리케이션을 실행하기 위한 가상 머신으로, 자바 바이트코드를 로컬 기계 코드로 변환하여 실행하는 역할을 합니다.
자바 바이트코드(Java Bytecode) : 자바 소스 코드가 컴파일된 후의 중간 형태입니다.
자바 소스 파일(.java 파일)을 자바 컴파일러가 컴파일하면, 결과적으로 생성되는 것이 .class 파일로 저장되는 자바 바이트코드입니다.
이 바이트코드는 기계어 코드는 아니지만, CPU가 직접 실행할 수는 없고, JVM이 이해하고 실행할 수 있는 명령어 세트로 구성되어 있습니다.
바이트코드는 플랫폼에 독립적이기 때문에, 한 번 컴파일된 자바 프로램은 어떤 JVM이 설치된 시스템에서든 실행할 수 있습니다.
이는 자바의 “한 번 작성하면, 어디서든 실행된다”라는 이점을 제공합니다.
로컬 기계 코드(Local Machine Code) : 로컬 기계 코드는 특정 하드웨어 플랫폼의 CPU가 직접 이해하고 실행할 수 있는 명령어 코드입니다.
이 코드는 플랫폼에 종속적이며, 다양한 운영 체제와 하드웨어 아키텍처는 각각의 기계어 코드를 가지고 있습니다.
자바 바이트코드는 JVM을 통해 실행될 때, 두 가지 방법 중 하나로 실행될 수 있습니다.
1. 인터프리터 : JVM은 바이트코드를 한 줄씩 읽고, 각 명령을 로컬 기계 코드로 변환하면서 실행합니다. 이 방법은 간단하지만, 실행 속도가 느릴 수 있습니다.
2. JIT 컴파일러(Just-In-Time Compiler) : 이 방식에서는 JVM이 바이트코드 전체 또는 핵심 부분을 분석하여, 실행 전에 전체 코드를 로컬 기계 코드로 한번에 변환합니다. 이렇게 하면 프로그램의 실행 속도가 크게 향상됩니다.
결국, 자바 바이트코드는 플랫폼 독립적인 중간 코드로서의 역할을 하며, 로컬 기계 코드는 실제 하드웨어에서 실행되기 위한 최종적인 코드 형태입니다. 이 두 코드의 변환과 실행은 JVM 내에서 처리되며, 사용자는 이 과정을 명시적으로 관리할 필요가 없습니다. 이것이 자바가 제공하는 큰 이점 중 하나입니다.
JVM은 자바의 “한 번 작성하면, 어디서든 실행된다(Write Once, Run Anywhere, WORA)” 라는 철학을 가능하게 하는 중요한 구성 요소입니다.
JVM 덕분에 자바 애플리케이션은 운영 체제나 하드웨어 플랫폼에 구애받지 않고 동일하게 실행될 수 있습니다.
JVM의 주요 기능.
1. 플랫폼 독립성 : 자바 프로그램은 JVM 위에서 실행되므로, JVM이 설치되어 있는 모든 운영 체제에서 같은 자바 프로그램을 실행할 수 있습니다.
이는 JVM이 플랫폼에 특화된 코드로 바이트 코드를 변환하기 때문입니다.
2. 메모리 관리 : JVM은 자동 메모리 관리 기능을 제공합니다. 이는 GC를 통해 메모리 할당과 해제를 관리하여, 프로그래머가 메모리 누수 없이 효율적인 메모리 사용을 할 수 있도록 돕습니다.
3. 보안 : 자바 바이트코드는 JVM에 의해 검증되며 실행되기 전에 다양한 검사를 통해 안전성이 확보됩니다.
이는 악의적인 코드 실행과 시스템 오류를 방지하는 데 도움이 됩니다.
4. 실행 환경 : JVM은 자바 애플리케이션에 필요한 실행 환경을 제공합니다.
이 환경은 클래스 로더, 바이트코드 실행 엔진, 쓰레드 관리 등을 포함합니다.
JVM의 구성 요소.
1. 클래스 로더(Class Loader) : 클래스 파일들을 읽고 바이트코드를 JVM 메모리에 로드하는 역할을 합니다.
2. 실행 엔진(Excution Engine) : 로드된 클래스 파일의 바이트코드를 실행합니다. 이 엔진은 바이트코드를 해것하거나 필요에 따라 JTI(Just-In-Time) 컴파일러를 사용하여 바이트코드를 직접 기계 코드로 변환하여 실행 속도를 높일 수 있습니다.
3. 가비지 컬렉터(Garbage Collector) : JVM이 사용하지 않는 메모리 자원을 자동으로 회수합니다.
4. 메모리(Runtime Data Area) : JVM은 프로그램 실행을 위해 필요한 다양한 메모리 영역을 관리합니다. 이는 힙(Heap), 스택(Stack), 메소드 영역(Method Area), 프로그램 카운터(Program Counter) 등이 포함됩니다.
Touch background to close