자바(Java)는 "Write Once, Run Anywhere" (한 번 작성하면 어디서든 실행된다) 라는 철학을 바탕으로 만들어졌습니다. C/C++처럼 특정 운영체제(OS)에 종속적인 기계어로 바로 번역되는 것이 아니라, 바이트코드(Bytecode, .class)라는 중간 언어로 한 번 번역되기 때문입니다.
이 바이트코드를 읽고 각 운영체제(Windows, Mac, Linux)가 알아들을 수 있는 언어로 실시간 통역을 해주는 가상의 컴퓨터가 바로 JVM(Java Virtual Machine)입니다.
한국인 개발자가 '자바어(Java)'로 책을 쓰면 컴파일러가 이를 세계 공용어인 '바이트코드(Bytecode)'로 번역합니다.
각 나라에 배치된 똑똑한 현지 통역사(JVM)들은 이 세계 공용어를 읽고, 즉석에서 미국인(Windows), 프랑스인(Mac), 일본인(Linux)이 알아들을 수 있는 현지어(기계어)로 읽어줍니다.
| 구성 요소 | 역할 설명 | 비유 |
|---|---|---|
| Class Loader | 하드디스크에 있는 .class 바이트코드를 읽어서 메모리(RAM) 위로 끌어올림 |
창고에서 번역본 원고를 꺼내 책상에 올리는 조수 |
| Execution Engine | 메모리에 올라온 바이트코드를 실제 CPU가 이해할 수 있는 기계어로 번역(인터프리터/JIT)하며 실행 | 현지인들에게 원고를 즉석에서 소리 내어 읽어주는 통역사 |
| Garbage Collector (GC) | 프로그램이 실행되면서 더 이상 사용하지 않는 쓰레기 메모리를 주기적으로 찾아서 자동 청소 | 책상에 다 읽고 버려진 종이들을 조용히 치워주는 청소부 |
public class Main {
// 자바 프로그램의 시작점 (Entry Point)
// 컴파일 후 JVM이 가장 먼저 찾아 실행하는 메서드입니다.
public static void main(String[] args) {
System.out.println("Hello, Java World!");
// JRE(Java Runtime Environment) 및 OS 정보 확인
String javaVersion = System.getProperty("java.version");
String osName = System.getProperty("os.name");
System.out.println("현재 운영체제: " + osName);
System.out.println("현재 자바 버전: " + javaVersion);
System.out.println("JVM 덕분에 동일한 코드가 " + osName + " 환경에서도 완벽히 실행됩니다!");
}
}
자바는 아주 엄격한 타입 시스템을 가집니다. 변수에는 크게 원시 타입(Primitive Type)과 참조 타입(Reference Type) 두 가지가 있습니다.
int, double, boolean 같은 원시 타입은 데이터 그 자체가 Stack(스택) 메모리에 직접 저장되어 빠르고 가볍습니다. 반면 String이나 Class 객체 같은 참조 타입은 거대한 실제 데이터가 Heap(힙) 메모리에 저장되고, Stack에는 그 Heap 메모리의 '주소(리모컨)'만 보관됩니다.
원시 타입은 내 호주머니(Stack)에 쏙 들어가는 작은 '동전 상자'입니다. 값을 바로 꺼내 쓸 수 있죠.
참조 타입은 너무 커서 호주머니에 들어가지 않는 '자동차(Heap)'입니다. 호주머니에는 자동차 자체가 아닌, 자동차 문을 열 수 있는 스마트키(메모리 주소)만 보관합니다.
| 비교 항목 | 원시 타입 (Primitive Type) | 참조 타입 (Reference Type) |
|---|---|---|
| 저장 방식 | 값(Value) 자체를 스택(Stack)에 저장 | 객체는 힙(Heap)에 생성, 스택에는 주소 저장 |
| 종류 | int, double, boolean, char 등 8개 |
String, 배열([]), 개발자가 만든 Class 등 |
| 기본값(Default) | 0, 0.0, false 등 | null (주소가 없음을 의미) |
| 비교 연산 (==) | 실제 '값'이 같은지 비교 | 같은 객체를 가리키는지 '주소' 비교 (값 비교는 .equals() 사용) |
public class Main {
public static void main(String[] args) {
// 1. 원시 타입 (Primitive Type)
// 값 자체가 Stack 메모리에 저장됩니다.
int age = 30;
double height = 175.5;
boolean isStudent = false;
// 2. 참조 타입 (Reference Type)
// 실제 데이터는 Heap에, 변수(name)에는 Heap의 주소가 담깁니다.
String name = "Minstudio"; // (String Pool을 사용하지만 기본적으로 참조 타입입니다)
System.out.println("age: " + age);
System.out.println("height: " + height);
System.out.println("name: " + name);
// 3. 참조 타입의 주의점 (주소 비교 vs 값 비교)
// new 키워드로 명시적으로 각각 다른 Heap 메모리 영역에 "Java" 객체를 생성합니다.
String a = new String("Java");
String b = new String("Java");
System.out.println("\n--- 문자열(참조 타입) 비교 주의점 ---");
// == 연산자는 Heap에 저장된 '주소(리모컨)'를 비교합니다.
// a와 b는 서로 다른 객체를 가리키므로 주소가 다릅니다 (false).
System.out.println("a == b (주소 비교): " + (a == b));
// .equals() 메서드는 실제 상자 안의 '값'을 비교합니다.
// 둘 다 "Java"라는 글자를 담고 있으므로 true를 반환합니다.
System.out.println("a.equals(b) (값 비교): " + a.equals(b));
}
}
프로그램의 흐름을 조작하는 제어문과 반복문입니다. 조건에 따라 갈림길을 선택하는 if-else와 다중 분기문 switch, 그리고 지정된 횟수만큼 트랙을 도는 for문과 조건이 참일 때까지 반복하는 while문이 있습니다.
특히 Java 14부터는 화살표(->)를 사용하여 break 없이 간결하게 값을 반환할 수 있는 강화된 switch 표현식(Switch Expressions)이 추가되어 실무에서 매우 널리 사용되고 있습니다.
if-else는 목적지로 가는 도중 만나는 갈림길(Y자 도로)입니다. 조건표지판을 보고 좌회전할지 우회전할지 결정하죠.
for / while은 운동장의 달리기 트랙입니다. 정해진 바퀴 수(for)를 채우거나 체력이 다할 때까지(while) 계속 트랙을 돕니다.
switch는 수많은 문이 있는 호텔 복도입니다. 내 방 번호(변수 값)와 똑같은 문을 한 번에 찾아 들어갑니다.
| 비교 항목 | 전통적인 Switch (이전) | Switch 표현식 (Java 14+) |
|---|---|---|
| break 키워드 | 필수 (없으면 다음 case까지 실행됨) | -> 화살표 사용 시 break 불필요 |
| 값 반환 (Return) | 자체적으로 값을 반환하지 못함 (외부 변수에 할당해야 함) | 식을 평가하고 결과값을 바로 반환하여 변수에 할당 가능 |
| 다중 조건 | case 1: case 2: 로 길게 나열 |
case 1, 2 -> 처럼 콤마(,)로 그룹화 가능 |
for (String fruit : fruits) 와 같은 향상된 for문은 배열을 읽기만 할 때 매우 편리합니다. 하지만 배열 안의 값을 변경하거나, 현재 몇 번째 인덱스를 돌고 있는지(i) 알아야 할 때는 기존의 for (int i=0; i<length; i++) 방식을 사용해야 합니다.
public class Main {
public static void main(String[] args) {
System.out.println("--- 1. if-else 조건문 ---");
int score = 85;
if (score >= 90) {
System.out.println("A 등급");
} else if (score >= 80) {
System.out.println("B 등급");
} else {
System.out.println("C 등급");
}
System.out.println("\n--- 2. 모던 Java (14+) Switch 표현식 ---");
// Java 14 이상 지원: break 생략 가능, 화살표(->) 사용, 반환값 직접 할당
String day = "MON";
String status = switch (day) {
case "MON", "TUE", "WED", "THU", "FRI" -> "평일 (출근)";
case "SAT", "SUN" -> "주말 (휴식)";
default -> "알 수 없는 요일";
};
System.out.println("오늘의 상태: " + status);
System.out.println("\n--- 3. 향상된 for문 (for-each) ---");
// 배열의 처음부터 끝까지 순회하며 값을 하나씩 fruit 변수에 꺼내줍니다.
String[] fruits = {"Apple", "Banana", "Cherry"};
for (String fruit : fruits) {
System.out.println("과일 이름: " + fruit);
}
}
}
자바는 객체지향(Object-Oriented) 언어입니다. 객체지향 프로그래밍은 관련된 상태(변수)와 행동(메서드)을 하나의 덩어리로 묶어서 관리하는 패러다임입니다.
클래스(Class)는 객체를 만들기 위한 '설계도'입니다. 이 설계도를 이용해 메모리(Heap)에 실제로 만들어낸 실체를 인스턴스(Instance) 혹은 '객체'라고 부릅니다. 설계도는 하나지만, 상태가 서로 다른 인스턴스를 메모리가 허락하는 한 무한히 만들어낼 수 있습니다.
클래스(Class)는 쇠로 만들어진 '붕어빵 틀'입니다. 틀 자체는 먹을 수 없으며, 어떤 모양의 붕어빵이 나올지 형태만 정의되어 있습니다.
인스턴스(Instance)는 그 틀에 밀가루와 앙금을 넣어 구워낸 '실제 붕어빵'입니다. 팥을 넣으면 팥 붕어빵, 슈크림을 넣으면 슈크림 붕어빵이 되듯 각 인스턴스는 자신만의 독립적인 상태를 가집니다.
| 용어 | 설명 | 비유 |
|---|---|---|
| 클래스 (Class) | 객체를 만들어 내기 위한 설계도나 틀 | 붕어빵 틀 |
| 객체 (Object) | 클래스로 구현될 모든 실체의 포괄적인 이름 (개념적) | 붕어빵 그 자체 |
| 인스턴스 (Instance) | 클래스를 통해 메모리(Heap)에 실제로 구현된 특정 실체 (기술적) | 내가 방금 산 '팥 붕어빵' |
// 1. 붕어빵 틀(클래스) 정의
class FishBread {
// 필드 (상태/속성)
// private: 외부에서 마음대로 앙금을 바꾸지 못하게 캡슐화(보호)합니다.
private String filling;
// 생성자 (인스턴스가 new를 통해 생성될 때 최초로 호출되는 메서드)
public FishBread(String filling) {
this.filling = filling;
}
// 메서드 (행동/기능)
public void bake() {
System.out.println(this.filling + " 붕어빵이 따끈하게 구워졌습니다!");
}
}
public class Main {
public static void main(String[] args) {
// 2. new 연산자를 사용하여 붕어빵 틀에서 2개의 '인스턴스(객체)'를 찍어냅니다.
// 이 순간 힙(Heap) 메모리에 2개의 독립적인 공간이 생깁니다.
FishBread redBeanBread = new FishBread("팥");
FishBread creamBread = new FishBread("슈크림");
// 3. 인스턴스의 행동 실행
// 각각의 인스턴스는 자신만의 독립적인 상태(filling)를 가지고 행동합니다.
redBeanBread.bake();
creamBread.bake();
}
}
상속(Inheritance)은 부모 클래스가 가진 변수와 메서드를 자식 클래스가 그대로 물려받아 재사용하는 문법입니다. extends 키워드를 사용하며, 중복 코드를 획기적으로 줄여줍니다.
부모에게 물려받은 행동을 자식의 입맛에 맞게 재정의하는 것을 오버라이딩(Overriding)이라고 합니다. 이를 바탕으로, 부모 타입의 변수에 다양한 자식 객체들을 번갈아 끼워 넣으며 유연하게 동작시키는 객체지향의 꽃, 다형성(Polymorphism)을 구현할 수 있습니다.
부모 클래스(Animal)는 [소리내기] 버튼이 하나 있는 '만능 리모컨'과 같습니다.
이 리모컨을 강아지(Dog 객체)에게 향하고 누르면 "멍멍!" 소리가 나고, 고양이(Cat 객체)에게 향하고 누르면 "야옹~" 소리가 납니다. 리모컨(부모 변수)은 똑같지만, 연결된 가전제품(자식 인스턴스)이 무엇이냐에 따라 결과가 다르게(다형성) 나타나는 것이죠.
| 비교 항목 | 오버로딩 (Overloading) | 오버라이딩 (Overriding) |
|---|---|---|
| 개념 | 같은 이름의 메서드를 매개변수만 다르게 여러 개 만드는 것 | 부모에게 물려받은 메서드를 자식이 덮어쓰기(재정의) 하는 것 |
| 적용 위치 | 같은 클래스 내부 | 상속 관계 (부모-자식 간) |
| 메서드 이름 | 같아야 함 | 같아야 함 |
| 매개변수 (파라미터) | 반드시 달라야 함 (타입, 개수 등) | 반드시 완벽히 같아야 함 |
// 부모 클래스 (설계도 원본)
class Animal {
public void makeSound() {
System.out.println("동물이 알 수 없는 소리를 냅니다.");
}
}
// 자식 클래스 (Dog는 Animal을 확장/상속 받음)
class Dog extends Animal {
// 부모의 메서드를 자식의 입맛에 맞게 재정의 (Overriding)
@Override
public void makeSound() {
System.out.println("멍멍!");
}
}
// 자식 클래스 (Cat은 Animal을 확장/상속 받음)
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("야옹~");
}
}
public class Main {
public static void main(String[] args) {
// 다형성 (Polymorphism)의 핵심:
// '부모(Animal) 타입의 변수'에 '자식(Dog, Cat) 인스턴스'를 담을 수 있습니다. (업캐스팅)
Animal myDog = new Dog();
Animal myCat = new Cat();
// 겉보기엔 똑같이 Animal의 makeSound()를 호출하는 것 같지만,
// 실제 메모리에 들어있는 객체(Dog, Cat)에 따라 결과가 다르게 나타납니다.
myDog.makeSound(); // 출력: 멍멍!
myCat.makeSound(); // 출력: 야옹~
System.out.println("\n[배열을 이용한 다형성 처리]");
// 다형성 덕분에 서로 다른 객체들을 'Animal'이라는 하나의 배열로 묶어서 관리할 수 있습니다.
Animal[] pets = { new Dog(), new Cat() };
for (Animal pet : pets) {
pet.makeSound();
}
}
}
객체지향 설계에서 가장 중요한 원칙 중 하나는 "구현이 아닌 역할(인터페이스)에 의존하라"입니다.
자바의 인터페이스(Interface)는 껍데기(메서드 이름과 반환 타입)만 존재하고 내부는 완전히 비어있는 완벽한 '규약'입니다. 자바는 다중 상속을 지원하지 않지만, 인터페이스는 한 클래스가 여러 개를 동시에 구현(implements)할 수 있어 유연한 확장을 가능하게 합니다.
인터페이스는 컴퓨터의 'USB 포트 규격'입니다. 컴퓨터는 마우스인지 키보드인지 신경 쓰지 않습니다. 그저 "USB 규격을 맞췄는가?"만 확인하죠.
구현 클래스(마우스, 키보드)는 이 USB 규격(인터페이스)을 실제로 몸체에 장착(implements)하여 만든 제품들입니다. 규격만 맞추면 언제든 새로운 기기로 부품을 갈아 끼울 수 있는 엄청난 유연성이 생깁니다.
| 비교 항목 | 추상 클래스 (Abstract Class) | 인터페이스 (Interface) |
|---|---|---|
| 목적 | 관련성이 높은 클래스들의 공통 기능(코드)을 물려주기 위해 | 서로 관련 없는 클래스들이 동일한 규약(행동)을 지키게 하기 위해 |
| 메서드 구현 | 구현된 일반 메서드 + 추상 메서드 혼용 가능 | 기본적으로 모든 메서드가 구현부가 없는 추상 메서드 |
| 다중 상속 | 불가능 (단일 상속만 허용) | 가능 (여러 개 동시 구현 허용) |
| 키워드 | extends |
implements |
// 인터페이스 정의 (구현부가 없는 추상 메서드들의 집합, '규약')
interface UsbConnectable {
// 인터페이스 안의 메서드는 자동으로 public abstract 처리됩니다.
void connect();
}
// 클래스가 인터페이스의 규약을 따를 때는 'implements(구현)' 키워드를 사용합니다.
// 규약을 맺었으므로 반드시 내부의 connect() 메서드를 강제로 완성해야 합니다.
class Mouse implements UsbConnectable {
@Override
public void connect() {
System.out.println("마우스가 USB 포트에 연결되었습니다. 띠링!");
}
}
class Keyboard implements UsbConnectable {
@Override
public void connect() {
System.out.println("키보드가 USB 포트에 연결되었습니다. 타닥타닥.");
}
}
public class Main {
public static void main(String[] args) {
System.out.println("[USB 포트에 기기 연결 중...]");
// 다형성의 극대화:
// 컴퓨터(Main) 입장에서는 이것이 마우스인지 키보드인지 구체적으로 알 필요가 없습니다.
// 그저 'UsbConnectable 규격을 통과한 기기'라면 무엇이든 수용합니다.
UsbConnectable device1 = new Mouse();
UsbConnectable device2 = new Keyboard();
// 껍데기(인터페이스)에 대고 명령을 내리면, 연결된 실제 객체들이 알아서 동작합니다.
device1.connect();
device2.connect();
}
}
프로그램을 실행하다 보면 0으로 숫자를 나누거나, 없는 파일을 읽으려 하는 등 예상치 못한 오류(Exception)가 발생합니다. 아무런 조치를 취하지 않으면 프로그램은 그 즉시 강제 종료(크래시)됩니다.
자바에서는 try-catch-finally 구문을 통해 이러한 오류를 우아하게 수습합니다. 위험한 코드를 실행하다가 에러가 터져도, 프로그램이 죽지 않고 안전하게 다음 로직으로 넘어가게 만드는 핵심 방어 기제입니다.
try 블록은 공중 곡예사가 위험한 연기를 펼치는 무대입니다.
연기 도중 실수로 떨어지는 사고(Exception 발생)가 일어나면, 바닥에 깔아둔 그물망 (catch 블록)이 곡예사를 안전하게 받아줍니다. 덕분에 서커스(프로그램)는 중단되지 않고 계속 진행될 수 있습니다. finally는 공연이 끝난 뒤 무조건 그물망을 걷고 무대를 청소하는 정리 작업입니다.
| 키워드 | 역할 | 실행 조건 |
|---|---|---|
| try | 예외(에러)가 발생할 가능성이 있는 위험한 코드를 감싸는 구역 | 항상 실행됨 |
| catch | try 구역에서 발생한 예외를 낚아채서 수습(복구)하는 구역 | 예외가 터졌을 때만 실행됨 |
| finally | 자원 반납(파일 닫기, DB 연결 종료 등)을 위해 뒷정리를 하는 구역 | 에러가 나든 안 나든 무조건 100% 실행됨 |
public class Main {
public static void main(String[] args) {
int[] numbers = {1, 2, 3}; // 인덱스는 0, 1, 2만 존재합니다.
try {
// 예외가 발생할 가능성이 있는 위험한 무대
System.out.println("배열의 5번째 요소 접근 시도...");
// 배열 크기는 3인데 5번째(인덱스 4)에 접근하므로 여기서 오류 폭발!
// 오류가 터진 즉시 실행이 중단되고 곧바로 catch 블록으로 던져집니다.
System.out.println(numbers[4]);
System.out.println("에러 아래쪽의 이 코드는 영원히 실행되지 않습니다.");
} catch (ArrayIndexOutOfBoundsException e) {
// 특정 예외가 터졌을 때 안전하게 낚아채서 수습하는 그물망
System.out.println("경고: 배열의 범위를 벗어났습니다!");
System.out.println("에러 메시지: " + e.getMessage());
} catch (Exception e) {
// 그 외의 예상치 못한 모든 예외를 잡는 최상위 안전망 (항상 맨 아래에 배치)
System.out.println("알 수 없는 오류 발생: " + e.getMessage());
} finally {
// 예외 발생 여부와 상관없이 100% 무조건 실행되는 뒷정리 블록
System.out.println("finally: 예외 검사가 모두 종료되었습니다.");
}
// try-catch 덕분에 프로그램이 크래시(강제종료)되지 않고 살아서 진행됩니다.
System.out.println("프로그램이 강제 종료되지 않고 여기까지 정상적으로 도달했습니다.");
}
}
자바 배열이나 리스트에 다양한 타입의 데이터를 마구잡이로 넣다 보면, 나중에 꺼내 쓸 때 어떤 타입인지 몰라 런타임 에러가 발생하기 쉽습니다. 이를 방지하기 위해 제네릭(Generics) <T>을 사용합니다.
제네릭은 클래스나 메서드를 정의할 때 데이터의 타입을 미리 픽스하지 않고, 실제로 객체를 생성하는 시점에 타입을 꽂아주는(결정하는) 기법입니다. 덕분에 컴파일러가 강력한 타입 체크를 해주고, 형변환(Casting) 코드도 생략할 수 있어 코드가 훨씬 안전하고 깔끔해집니다.
Box<T>는 구멍이 뚫려있는 빈 이름표를 단 '만능 택배 상자'입니다.
개발자가 이 상자를 쓸 때 Box<String>이라고 선언하면, 이름표에 '문자열 전용'이라고 도장이 찍히며 오직 글자만 넣을 수 있는 상자로 변신합니다. Box<Integer>라고 선언하면 '숫자 전용' 상자로 변신하죠. 하나의 설계도로 여러 타입 전용 상자를 무한정 만들어낼 수 있습니다!
| 알파벳 | 의미 (Full Name) | 주요 사용처 |
|---|---|---|
| T | Type | 가장 일반적인 타입 (예: Box<T>) |
| E | Element | 컬렉션에 들어가는 요소 (예: List<E>) |
| K | Key | Map의 키 (예: Map<K, V>) |
| V | Value | Map의 값 또는 리턴 값 |
// 데이터 타입을 <T>라는 임시 변수(타입 파라미터)로 구멍을 뚫어놓은 제네릭 클래스 설계
class Box<T> {
private T item; // T는 컴파일(객체 생성) 시점에 String, Integer 등으로 완벽히 치환됩니다.
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
public class Main {
public static void main(String[] args) {
// 1. 객체를 생성할 때 <T> 자리에 들어갈 구체적인 진짜 타입을 명시합니다.
// (Java 7 이상부터는 뒤쪽 다이아몬드 연산자 <> 안의 타입을 생략할 수 있습니다)
Box<String> stringBox = new Box<>();
stringBox.setItem("소중한 반지");
// stringBox.setItem(100);
// ☝️ 만약 위 주석을 해제하면 에러 발생! String만 넣을 수 있도록 컴파일러가 완벽 차단합니다.
// 캐스팅(형변환) 코드 없이 바로 String 타입으로 안전하게 꺼내 쓸 수 있습니다.
String myItem = stringBox.getItem();
System.out.println("상자 안의 물건: " + myItem);
// 2. 완전히 똑같은 Box 클래스 설계도 하나로, 이번엔 '숫자 전용' 상자를 만들었습니다.
// (제네릭에는 int 같은 원시타입은 들어갈 수 없어 Wrapper 클래스인 Integer를 사용합니다)
Box<Integer> numberBox = new Box<>();
numberBox.setItem(999);
System.out.println("상자 안의 숫자: " + numberBox.getItem());
}
}
데이터를 효율적으로 보관하고 검색, 조작하기 위해 자바가 기본적으로 제공하는 '표준 자료구조 모음집'을 컬렉션 프레임워크(Collection Framework)라고 합니다.
단순한 배열(Array)은 크기가 고정되어 있어 실무에서 쓰기 불편하지만, 컬렉션들은 데이터가 늘어나면 자동으로 크기가 늘어나는 마법의 주머니입니다. 그중에서도 가장 널리 쓰이는 3대장 인터페이스가 바로 List, Set, Map입니다.
List (대기표): 은행 대기표처럼 들어온 '순서'가 유지됩니다. 1번 손님과 5번 손님이 우연히 같은 옷을 입었어도(데이터 중복) 각각 번호판(인덱스)이 다르므로 허용됩니다.
Set (장바구니): 마트 장바구니처럼 잡히는 대로 넣으므로 '순서'가 섞입니다. 하지만 똑같은 물건을 여러 개 넣어도 영수증엔 "사과 3개"처럼 하나로 퉁쳐집니다.(데이터 중복 불가)
Map (전화번호부): '이름(Key)'으로 '전화번호(Value)'를 찾습니다. 김철수라는 이름(Key)은 유일해야 하지만, 두 사람의 번호(Value)가 우연히 같을 수는 있습니다.
| 인터페이스 | 순서 유지 | 데이터 중복 | 가장 대표적인 구현 클래스 |
|---|---|---|---|
| List | 유지 O (인덱스) | 허용 O | ArrayList, LinkedList |
| Set | 유지 X | 불가 X | HashSet, TreeSet |
| Map | 유지 X | Key 불가 X / Value 허용 O | HashMap, TreeMap |
// 컬렉션 프레임워크를 사용하기 위해서는 java.util 패키지를 import 해야 합니다.
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
// 1. List (대기표: 순서 보장, 중복 허용)
// 다형성을 위해 왼쪽은 인터페이스(List), 오른쪽은 구현체(ArrayList)를 쓰는 것이 정석입니다.
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("Java"); // 중복 데이터를 넣어도 0번째, 2번째로 위치(인덱스)가 다르므로 괜찮습니다.
System.out.println("List (대기표): " + list);
// 출력: [Java, Python, Java]
// 2. Set (장바구니: 순서 섞임, 중복 무시)
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Python");
set.add("Java"); // 이미 "Java"가 있으므로, 이 줄은 조용히 무시됩니다.
System.out.println("Set (장바구니): " + set);
// 출력: [Java, Python] (순서는 내부 알고리즘에 의해 랜덤으로 나옵니다)
// 3. Map (전화번호부: Key-Value 쌍, Key 중복 불가)
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 85);
// Key인 "Alice"가 중복되었습니다. 에러가 나지는 않지만 덮어쓰기(수정)가 일어납니다.
scores.put("Alice", 95);
System.out.println("Map (전화번호부): " + scores);
// 출력: {Bob=85, Alice=95}
// Key를 던져주면 그에 맞는 Value를 꺼내옵니다.
System.out.println("Alice의 점수: " + scores.get("Alice"));
// 출력: 95
}
}
Java 8 버전에 도입된 람다(Lambda)와 스트림(Stream) API는 자바의 코딩 패러다임을 바꾼 혁명적인 기능입니다. 기존에는 컬렉션(리스트)의 데이터를 거를 때 지저분한 for문과 if문을 남발해야 했습니다.
스트림 API를 사용하면, 원본 데이터를 물줄기(Stream)에 올린 뒤, 조건에 맞게 거르고(filter), 형태를 변환하고(map), 최종적으로 묶는(collect) 과정을 마치 공장의 컨베이어 벨트처럼 선언적(Declarative)으로 매우 우아하게 작성할 수 있습니다.
stream()으로 상자에 담긴 과일들을 컨베이어 벨트에 하나씩 올려놓습니다.
filter()는 불량 사과를 골라내는 '거름망 로봇'입니다.
map()은 사과를 깎아서 예쁜 조각으로 만드는 '가공 로봇'입니다.
collect()는 가공이 끝난 과일 조각들을 다시 새로운 상자에 포장하는 '포장 로봇'입니다.
| 메서드 | 분류 | 역할 |
|---|---|---|
| stream() | 생성 | 컬렉션 데이터를 스트림(물줄기)으로 올려놓습니다. |
| filter() | 중간 연산 | 람다식 조건이 참(true)인 데이터만 살려두고 나머지는 걸러냅니다. |
| map() | 중간 연산 | 각 데이터를 다른 형태(문자열 변경, 객체 변환 등)로 가공합니다. |
| collect() | 최종 연산 | 컨베이어 벨트를 끝내고 결과를 다시 List나 Set 등으로 묶어냅니다. |
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
// 불변 리스트 초기화
List<String> names = Arrays.asList("Apple", "Banana", "Cherry", "Avocado", "Blueberry");
// 목표: 'A'로 시작하는 과일만 대문자로 바꿔서 새 리스트에 담기
// 1. [과거 방식 - Java 7 이전]
// 코드가 길어지고, 뎁스(if문)가 깊어져 가독성이 떨어집니다.
List<String> oldResult = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
oldResult.add(name.toUpperCase());
}
}
System.out.println("[과거 방식] 결과: " + oldResult);
// 2. [모던 자바 스트림 방식 - Java 8 이후]
// 선언적 코드(무엇을 할 것인가)에 집중하여 컨베이어 벨트처럼 직관적입니다.
List<String> streamResult = names.stream() // 1. 물줄기(스트림) 생성
.filter(name -> name.startsWith("A")) // 2. 중간 연산: 'A'로 시작하는 과일만 통과 (람다식)
.map(name -> name.toUpperCase()) // 3. 중간 연산: 모두 대문자로 가공 (람다식)
.collect(Collectors.toList()); // 4. 최종 연산: 결과를 다시 List로 묶어라
System.out.println("[모던 자바 스트림 방식] 결과: " + streamResult);
}
}
C나 C++ 같은 언어에서는 개발자가 메모리를 할당받은 후 다 쓰면 직접 해제(free)해야 했습니다. 실수로 해제하지 않으면 메모리 누수(Memory Leak)가 발생하여 서버가 뻗어버립니다.
반면 자바는 백그라운드에 가비지 컬렉터(Garbage Collector, GC)라는 로봇 빗자루가 항상 돌아다닙니다. GC는 Heap 메모리를 주기적으로 스캔하면서, Stack이나 다른 곳에서 "더 이상 참조(연결)하고 있지 않은 외딴섬 객체"를 발견하면 알아서 메모리를 청소해 줍니다. 덕분에 자바 개발자는 메모리 해제 스트레스 없이 비즈니스 로직에만 집중할 수 있습니다.
자바의 객체는 하늘을 떠다니는 풍선(Heap 메모리)이고, 변수는 그 풍선을 잡고 있는 손(Stack 메모리)입니다.
개발자가 풍선 줄을 놓아버리면(변수 = null 대입), 풍선은 둥둥 떠다니는 쓰레기(Garbage)가 됩니다. 백그라운드에 숨어있던 로봇 청소기(GC)는 주기적으로 하늘을 순찰하다가 아무도 줄을 잡고 있지 않은 풍선을 발견하면 펑! 터트려서 공간을 비워버립니다.
public class Main {
public static void main(String[] args) {
// 1. 객체 생성: Heap 메모리에 "홍길동" 객체 공간이 할당됩니다.
// Stack 메모리에 있는 'user' 변수가 그 주소를 잡고 있습니다. (연결 상태)
String user = new String("홍길동");
System.out.println("현재 유저 객체: " + user);
// 2. 연결 끊기 (풍선 줄 놓기)
// 변수에 null을 대입하여 Heap 객체와의 '연결 고리'를 의도적으로 끊습니다.
user = null;
System.out.println("유저 객체와의 연결을 끊었습니다. (null 대입)");
// 이제 "홍길동" 객체는 메모리에 존재하지만, 그 누구도 접근할 수 없는 '외딴섬(Unreachable)'이 됩니다.
// 백그라운드에서 조용히 돌고 있는 가비지 컬렉터(GC)가 주기적으로 하늘을 순찰하다가
// 이런 쓰레기(고아 객체)들을 찾아내어 메모리에서 영구히 삭제(청소)합니다.
// 3. GC 강제 호출 (실무에서는 절대 금지!)
// 개발자가 System.gc()를 호출해서 "웬만하면 지금 청소 좀 해줘" 라고 요청할 순 있지만,
// 실제 청소를 언제 할지는 100% JVM 맘대로입니다. (무시할 수도 있습니다)
// 강제로 호출하면 전체 시스템이 일시정지(Stop-the-world) 되므로 실무에선 절대 직접 쓰지 않습니다.
System.gc();
System.out.println("[시스템] 개발자가 명시적으로 GC 실행을 요청했습니다. (비권장)");
System.out.println("안심하세요! JVM이 알아서 메모리를 관리해줍니다.");
}
}