자바에서의 SOLID 원칙은 객체지향 설계(OOD, Object-Oriented Design)의 다섯 가지 핵심 원칙을 말한다.

 

SOLID  원칙은 유지보수성과 확장성을 높이기 위한 가이드라인이며 다음 다섯 가지 원칙의 앞글자를 딴 약어이다.

 

  • SRP : 단일 책임 원칙
  • OCP : 개방 폐쇄 원칙
  • LSP : 리스코프 치환 원칙
  • ISP : 인터페이스 분리 원칙
  • DIP : 의존 역전 원칙
 

1. S - 단일 책임 원칙 (Single Responsibility Principle, SRP)

클래스는 하나의 책임만 가져야 한다.

  • 한 클래스가 하나의 기능을 가져야 한다는 뜻.
  • 예: ReportPrinter 클래스는 보고서를 출력하는 역할만 해야 하며, 보고서를 생성하거나 저장하는 책임은 가지면 안 됨.

 

class ReportPrinter {  // 출력하는 역할
    public void print(String report) {
        System.out.println("Printing" + report);
    }
}


class ReportScanner {  // 스캔하는 역할
    public void print(String report) {
        System.out.println("Scanning : "+report);
    }
}

2. O - 개방-폐쇄 원칙 (Open/Closed Principle, OCP)

소프트웨어 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

  • 기능을 수정할 때 기존 코드를 변경하지 않고, 새로운 클래스를 확장(상속, 위임 등) 하여 구현해야 함.
  • 간단하게 생각하면 추상화 사용을 통한 관계 구축(상속)을 권장하는 뜻이다.
  • 예: 직사각형(Rectangle)과 원(Circle)이라는 객체를 구현해야 할 때, Shape라는 추상화 클래스가 있다면 이것을 확장(상속)하여 구현 가능.

 

abstract class Shape {          
    abstract double area();
}

class Rectangle extends Shape {  // Shape을 확장(extends)
    double width, height;
    public Rectangle(double w, double h) { this.width = w; this.height = h; }
    double area() { return width * height; }
}

class Circle extends Shape {    // Shape을 확장(extends)
    double radius;
    public Circle(double r) { this.radius = r; }
    double area() { return Math.PI * radius * radius; }
}

3. L - 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.

  • 부모 클래스의 객체를 사용하는 프로그램에서 자식 클래스로 교체해도 문제가 없어야 함. (업캐스팅을 하여도 문제없어야 함)
  • 잘못된 상속은 이 원칙을 위배함. (자식 클래스에서 부모 클래스의 메서드를 오버라이딩하는 경우)
 
class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Eagle extends Bird {
    
    public int fly(int a) {   // 자기 멋대로 오버로딩을 해버림
        System.out.println("it flew " + a + " meters");
    }
}

 

→ 이건 LSP 위반. Eagle이 Bird의 fly를 오버로딩 해버렸기 때문에, 부모 클래스를 자식 클래스로 교체했을 때 원하는 결과가 나오지 않는다.


4. I - 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

  • 사용하지 않는 메서드를 구현하게 만드는 비대한 인터페이스는 나쁘다. 
  • 인터페이스는 목적에 맞게 작게 쪼개야 함. (SRP는 클래스를 분리, ISP는 인터페이스를 분리)
  • 한 번 인터페이스를 분리하여 구성해 놓고 나중에 수정사항이 생겨서 또 인터페이스를 분리하는 행위는 지양해야 한다.

 

interface Printer {   // 프린터의 인터페이스
    void print();
}

interface Scanner {   // 스캐너의 인터페이스
    void scan();
}

// 사용자는 필요한 인터페이스만 구현
class SimplePrinter implements Printer {
    public void print() {
        System.out.println("Printing...");
    }
}

5. D - 의존 역전 원칙 (Dependency Inversion Principle, DIP)

고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.

  • 구현 클래스가 아니라 인터페이스나 추상 클래스에 의존하게 만들어야 함.
  • 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 , 변화하기 어려운 것이나 거의 변화하가 없는 것에 의존하라는 뜻
  • 인터페이스나 추상 클래스는 일종의 설계도이기 때문에 변화가 없음. 만약 어떤 클래스를 참조해야 하는 상황이 생긴다면, 그 클래스를 직접 참조하는 것이 아니라 그 대상의 상위 요소(설계도)를 참조하라는 뜻.

 

interface MessageSender {
    void send(String message);
}

class EmailSender implements MessageSender {
    public void send(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class NotificationService {
    private MessageSender sender;

    // 생성자 주입
    public NotificationService(MessageSender sender) {
        this.sender = sender;
    }

    public void notify(String message) {
        sender.send(message);
    }
}

 

블로그 이미지

Ahan

책, 영화, 게임! 인생의 활력 요소가 되는 취미들을 하자!

,

객체지향 프로그래밍(OOP)란?

 

자바의 OOP(Object-Oriented Programming, 객체지향 프로그래밍)는 객체(Object)를 중심으로 프로그램을 설계하고 구성하는 패러다임이다. 자바는 객체지향 언어로 설계되었기 때문에 OOP의 4대 핵심 개념을 중심으로 구조화된다.

 


객체란?

여기서 객체현실 세계의 사물이나 개념을 프로그램 안에서 표현한 실체를 의미한다.

즉, 어떤 "것"을 코드로 표현했을 때, 그 "것"이 바로 객체이다.

객체는 속성(필드)과 동작(메서드)을 함께 가지고 있는 독립된 단위이다.

 

 

객체의 예시를 하나 들어보겠다.

강아지를 객체로 표현한다면 다음과 같다.

  • 속성(필드): 이름, 나이, 품종, 색깔 등 → 데이터
  • 동작(메서드): 짖는다, 뛴다, 먹는다 등 → 기능
public class Dog {
    // 속성(필드, 데이터)
    String name; // 개 이름
    int age;     // 개 나이
	
    // 동작(메서드)
    void bark() {
        System.out.println("멍멍!");
    }
}

 

이렇게 클래스를 정의하고 실제로 사용할 때 객체를 생성해서 쓰면 된다.

Dog myDog = new Dog();  // Dog 클래스를 이용해 "객체" 생성
myDog.name = "콩이";
myDog.age = 2;
myDog.bark(); // 출력: 멍멍!

 

 


4대 핵심 개념

다시 OOP로 돌아와서 4대 핵심 개념에 대해 적어보겠다.

 

OOP의 4대 핵심 개념은 다음과 같다:

  1. 캡슐화(Encapsulation)
  2. 상속(Inheritance)
  3. 다형성(Polymorphism)
  4. 추상화(Abstraction)

예시 코드와 함께 각 개념의 정의와 목적을 알아보겠다.

 

1. 캡슐화 (Encapsulation)

  • 정의: 객체의 속성과 메서드를 하나로 묶고, 외부에서 직접 접근하지 못하도록 보호하는 것
  • 목적: 데이터 보호, 유지보수 용이성
  • 예시:
public class User {
    private String name; // 외부에서 직접 접근 불가

    public String getName() {  // 외부에서 호출 가능
        return name; // getter
    }

    public void setName(String name) { // 외부에서 호출 가능
        this.name = name; // setter
    }
}

 

캡슐화는 관련이 있는 변수와 함수를 하나의 클래스로 묶고, 외부에서 쉽게 접근하지 못하도록 은닉하는 게 핵심이다. 이것을 정보은닉(Information Hiding)이라 한다.

 

정보은닉의 장점은 다음과 같다.

  • 유지보수나 확장 시 오류의 범위를 최소화 할 수 있다.
  • 객체의 정보손상 및 오용을 방지한다.
  • 조작법이 바뀌어도 사용법 자체는 바뀌지 않는다 (함수 호출)
  • 데이터가 바뀌어도 다른 객체에 영향을 주지 않기 때문에 독립성이 보장된다.
  • 모듈화가 가능하다.

위에 예시로 든 User 클래스의 경우 데이터(String name)은 직접 접근이 불가능하고, 해당 데이터를 변경하기 위해서는 함수(getName(), setName())으로만 가능하다. 

 


2. 상속 (Inheritance)

  • 정의: 기존 클래스(부모)의 속성과 기능을 새로운 클래스(자식)가 물려받는 것
  • 목적: 코드 재사용성 증가, 계층적 구조화
  • 예시:
public class Animal {                 // 부모 클래스
	String name;
    public void eat() {
        System.out.println("먹는다");
    }
}

public class Dog extends Animal {    // 부모 Animal을 상속 받은 자식 클래스
    public void bark() {
        System.out.println("짖는다");
    }
}

 

상속 개념은 쉽게 말해 부모 클래스(상위 클래스)와 자식 클래스(하위 클래스)가 있으며, 자식 클래스는 부모 클래스의 대부분의 것을 상속 받아 그대로 쓸 수 있다는 것을 의미한다. (private 접근 제한을 갖는 데이터 및 함수는 상속받지 못한다.)

상속을 하는 이유는 간단하다. 이미 만들어진 클래스를 재사용하여 코드의 재사용성을 늘리고, 개발 시간을 단축시키기 위해서다.

 

위에 예시로 든 Dog는 Animal의 name과 bark()를 상속 받아서 사용 가능하다.

Dog doggy = new Dog();
dog.name = "White";   // Dog 내부에는 없지만 Animal의 name을 상속 받아서 사용 가능
dog.bark();          
dog.eat();             // 위와 마찬가지

 

 


3. 다형성 (Polymorphism)

  • 정의: 같은 타입의 참조 변수가 다양한 객체를 참조할 수 있도록 함
  • 종류:
    • 오버로딩(Overloading): 같은 메서드 이름을 다른 파라미터(매개변수)를 주어 중복 정의
    • 오버라이딩(Overriding): 부모 메서드를 자식이 재정의
  • 예시:
class Animal {
    public void sound() {
        System.out.println("소리");
    }
}

class Cat extends Animal {           // Animal을 상속 받음

    // 오버라이딩
    @Override					     
    public void sound() {            // Animal의 sound()를 Cat의 sound()로 재정의
        System.out.println("야옹");
    }
    
    // 오버로딩
    public void eat() {            
        System.out.println("냠냠");
    }
    
    
    public void eat(String eatSound) {   // eat()와 같은 이름이지만 다른 매개변수를 가졌음 
        System.out.println(eatSound);
    }

    
    
}

Animal a = new Cat();
a.sound(); // "야옹"
String catFood = "챱챱"
a.eat(); // "냠냠"
a.eat(catFood); // "챱챱"

 

다형성이란 하나의 객체나 메소드가 여러 가지 다른 형태를 가질 수 있는 것을 말한다.

서로 상속 관계에 있는 부모 클래스와 자식 클래스 사이에서만 사용된다. 

업캐스팅과 다운캐스팅도 알아두면 좋다.

 


4. 추상화 (Abstraction)

  • 정의: 복잡한 내부 구현은 숨기고, 필요한 부분만 노출하는 것
  • 방법:
    • 추상 클래스: abstract class와 abstract method
    • 인터페이스: 다중 구현 지원
  • 예시:
// abstract class 추상화 클래스
// 일반 메서드  또는 멤버 변수를 가질 수 있다.

abstract class Machine {
    public String country;   // 멤버 변수
     
    public void stop() {     // 일반 메서드
		System.out.println("정지");
    }
    
    // abstract method 추상화 메서드
    abstract void operate();    // Machine을 상속 받는 하위 클래스는 operate를 구현해야한다.
}

class Printer extends Machine {
    void operate() {            // operate를 구현한다.
        System.out.println("인쇄 중...");
    }
}


// interface 인터페이스
// 상수와 추상메서드만 가질 수 있다. 
// (java 8 버전부터는 default와 static 메서드가 추가되어 사용 가능하다.)

public interface Animal {
    public static final String name = "이름";
    final int weight = 10;
    static int age = 1;
    

    abstract void sound();    // Animal을 상속 받는 하위 클래스는 sound를 구현해야한다.
}

public interface Feline {
    abstract void move();    // Feline을 상속 받는 하위 클래스는 move를 구현해야한다.
}

// 다중 상속 가능
class Cat implements Animal,Feline {
    void sound() {            // sound를 구현한다.
        System.out.println("야옹");
    }
    
    void move() {            // move를 구현한다.
        System.out.println("사뿐사뿐");
    }
    
}

 

추상화는 상위 클래스에서 클래스들의 공통적인 속성과 동작을 정의하고 이를 하위 클래스에서 구현하는 것이다.

간단하게 말하자면 개집 구조의 설계도만 작성하고 이를 만들어서 꾸미는 것은 제작자한테 맡기는 것이다.

 

추상화는 abstract 제어자를 가지는 abstract class와 abstract method

혹은 인터페이스(interface)로 만들 수 있다.

 

예시에 적은 차이점과 더불어 대략적인 두 클래스의 차이점은 다음과 같다 :

항목 abstract class interface
목적 공통 기능의 틀 제공 기능의 계약 정의
상속/구현 키워드 extends implements
다중 상속 안됨 (단일 클래스만 상속 가능) 가능 (여러 인터페이스 구현 가능)
필드 (멤버 변수) 선언 가능 (일반 변수, protected 등) final(불변 필드) 혹은 상수만 가능
메서드 일반 메서드, 추상 메서드 둘 다 가능 Java 8 이전: 추상 메서드만
Java 8 이후: default, static 메서드도 가능
생성자 생성자 선언 가능 (new Cat) 생성자 선언 불가
접근 제어자 다양한 접근자 사용 가능 (protected, private) 모든 메서드/필드는 암묵적으로 public

 

 

 

이상으로 자바의 객체지향 프로그래밍에 대해 간단하게 적어봤다.

블로그 이미지

Ahan

책, 영화, 게임! 인생의 활력 요소가 되는 취미들을 하자!

,

자바 공부를 하다가 보면 가끔 이런 식으로 쓰인 부분들이 있다.

 

List<String> filtered = stream
    .filter(name -> name.startsWith("A")) // 여기같이 표현
    .map(String::toUpperCase)  // 이거는 '메서드 참조'이다

 

name -> name.startsWith("A")
이게 뭐지?

 

이건 람다식 혹은 람다라고 하는 코드 작성법이다.

 

자바의 람다식(lambda expression)은 익명 함수(이름이 없는 함수)를 간결하게 표현하는 방식으로, 주로 함수형 인터페이스를 구현할 때 사용된다. Java 8부터 도입되었으며, 코드의 가독성을 높이고 간결하게 작성할 수 있는 장점이 있다.

 

 

기본 문법은 다음과 같다 :

(매개변수) -> { 실행문 }


// 매개변수 하나, 실행문 하나
x -> System.out.println(x)

// 매개변수 여러 개, 반환값 있는 경우
(x, y) -> x + y

// 실행문 여러 줄
(x, y) -> {
    int result = x + y;
    return result;
}

 

 

실제로도 보면 작성도 쉽고 읽기도 쉽다.

 

장점과 단점에 대해 좀 더 자세하게 알아보자.

 

람다식의 장점

1. 코드가 간결해짐

  • 익명 클래스보다 훨씬 짧고 읽기 쉬움.
// 전통적인 방식
Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Hello!");
    }
};

// 람다식
Runnable r2 = () -> System.out.println("Hello!");

2. 가독성이 좋아짐

  • 함수의 목적이 명확하고, 코드 흐름이 더 직관적.
  • 특히 Stream API와 함께 쓰면 굉장히 깔끔한 코드가 가능.
List<String> list = Arrays.asList("Helldiver", "Stratagem", "Bug");
list.stream()
    .filter(s -> s.startsWith("H"))
    .forEach(System.out::println);

3. 병렬 처리 등 함수형 프로그래밍에 적합

  • Stream API와 함께 사용하면 병렬 작업도 쉽게 적용할 수 있다.
list.parallelStream().forEach(System.out::println);
 

4. 콜백 구현이 쉬움

  • 이벤트 처리, 비동기 작업 등에 콜백을 깔끔하게 구현 가능.

람다식의 단점

1. 복잡한 로직에는 부적합

  • 여러 줄로 작성할 경우 오히려 가독성이 떨어짐.
(x, y) -> {
    if (x > y) return x;
    else return y;
}

복잡하면 그냥 일반 메서드나 익명 클래스로 하는 게 더 명확할 수도 있음.

2. 디버깅이 어려움

  • 익명 함수이기 때문에 디버거에서 이름이 없어 추적하기 어려움.
  • 예외 처리 위치도 찾기 힘들 수 있음.

3. 오용 시 가독성 하락

  • 지나치게 중첩된 람다식은 오히려 코드 이해를 방해함.
list.stream()
.filter(x -> x.length() > 3)
.map(x -> x.toUpperCase())
.sorted((a, b) -> a.compareTo(b))
.forEach(System.out::println);
// 너무 길어지면 눈 아픔...

4. 익명 클래스와의 기능 차이

  • 익명 클래스는 다중 메서드를 구현하거나 상태를 가질 수 있지만, 람다식은 그럴 수 없음
 
익명 클래스는 내부에서 여러 메서드를 정의할 수 있지만
람다식은 오직 하나의 추상 메서드만 표현 가능

 

 

 


 

이런 람다식이지만 이걸 활용하기 위해서는 함수형 인터페이스를 써야한다는 전제가 있다.

 

 

함수형 인터페이스란?

  • 자바에서 람다식은 익명 구현 객체를 생성하는 문법이다.
  • 이때 어떤 인터페이스를 구현하는 객체가 생성되는데, 추상 메서드가 하나만 있어야 어떤 메서드를 구현할지 애매하지 않기 때문에 그 조건이 필요함.
@FunctionalInterface
interface MyFunction {
    void run();
}
 

@FunctionalInterface 어노테이션은 선택 사항이지만, 붙이면 컴파일러가 실수로 두 개 이상의 추상 메서드를 만들지 않도록 도와준다.

 

// 컴파일 에러 발생!
@FunctionalInterface
interface InvalidInterface {
    void method1();
    void method2(); // 에러!
}
 

1. 함수형 인터페이스의 특징

  • 단 하나의 추상 메서드만 존재
  • default, static 메서드는 여러 개 있어도 무관 (이것들은 추상 메서드가 아님)
  • @FunctionalInterface는 선택이지만, 붙이는 걸 권장

2. 예시: 직접 만든 함수형 인터페이스와 람다식 사용

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

public class Test {
    public static void main(String[] args) {
        Calculator add = (a, b) -> a + b;
        Calculator multiply = (a, b) -> a * b;

        System.out.println(add.calculate(3, 4));      // 7
        System.out.println(multiply.calculate(3, 4)); // 12
    }
}

3. 자바에서 제공하는 주요 함수형 인터페이스

Java 8부터 java.util.function 패키지에서 많이 제공하고, 이걸 기반으로 스트림 API, 람다식 등에서 사용함.

 

인터페이스 추상 메서드 설명
Function<T, R> R apply(T t) T를 받아 R로 변환
Consumer<T> void accept(T t) T를 받아 소비 (반환 없음)
Supplier<T> T get() T를 반환 (입력 없음)
Predicate<T> boolean test(T t) T를 받아 조건 검사 (boolean 반환)
UnaryOperator<T> T apply(T t) T를 받아 T를 반환 (Function의 특수 형태)
BinaryOperator<T> T apply(T t1, T t2) T 둘을 받아 T를 반환

3-1. 예시:

1. Predicate<T>

  • T를 받아 조건 검사 후 boolean 반환
 
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isPositive = x -> x > 0;

        System.out.println(isPositive.test(10)); // true
        System.out.println(isPositive.test(-5)); // false
    }
}

 

2. Supplier <T> — "공급자"

  • 아무것도 받지 않고, T를 리턴하는 함수형 인터페이스
  • 메서드: T get()
import java.util.function.Supplier;

public class SupplierExample {
    public static void main(String[] args) {
        Supplier<String> stringSupplier = () -> "HELLDIVERS 2 - MISSION READY!";

        System.out.println(stringSupplier.get());
    }
}

3. Consumer <T> — "소비자"

  • T를 받아서 소비만 하고, 리턴값은 없음
  • 메서드: void accept(T t)
import java.util.function.Consumer;

public class ConsumerExample {
    public static void main(String[] args) {
        Consumer<String> printStratagem = stratagem -> System.out.println("Deploying: " + stratagem);

        printStratagem.accept("Eagle Airstrike");
    }
}

4. Function <T, R> — "변환자"

  • T를 받아서 R로 변환
  • 메서드: R apply(T t)
import java.util.function.Function;

public class FunctionExample {
    public static void main(String[] args) {
        Function<String, Integer> stringLength = str -> str.length();

        System.out.println("Length: " + stringLength.apply("Super Earth"));
    }
}

 

 

 


개인적으로 람다식을 잘 사용을 안 해서 아직도 어색하다. 

나중에 시간이 되면 람다식 연습도 해봐야겠다.

 

블로그 이미지

Ahan

책, 영화, 게임! 인생의 활력 요소가 되는 취미들을 하자!

,

자바의 Stream API는 Java 8부터 도입된 기능으로, 컬렉션(Collection) 데이터를 처리할 때 선언형(Declarative) 방식으로 간결하고 효율적으로 작업할 수 있도록 도와주는 기능이다.

쉽게 말하면, 데이터를 필터링하고, 변형하고, 집계하는 일련의 작업을 파이프라인처럼 연결해서 처리할 수 있는 도구라고 보면 된다.

 

Stream API는 단독으로 뭔가 하는 게 아니라
"데이터 → Stream → 연산들"이라는 전체 흐름 속에서 사용된다는 점이 핵심이다.

 


1. Stream 만드는 방법 (데이터 → Stream)

Stream API를 쓰기 위해서는 반드시 "스트림 객체"를 만들어야 한다.
즉, Stream은 “데이터 소스”로부터 시작해서, 그 위에 중간 연산과 최종 연산을 쌓는 구조이다.

가장 많이 쓰는 순서부터 한 번 작성해 보겠다.

1. 컬렉션(Collection)으로부터

List<String> list = List.of("A", "B", "C");
Stream<String> stream = list.stream();
  • .stream() → 순차 스트림
  • .parallelStream() → 병렬 스트림 (멀티코어 처리)

list.stream()은  실제로 Stream<String> stream = list.stream(); 이렇게 변수에 담는 걸 생략한 것 뿐이다.

 


2. 배열로부터

String[] arr = {"A", "B", "C"};
Stream<String> stream = Arrays.stream(arr);

 

  • Collections를 상속 받지 않은 Arrays(배열)은 Arrays.stream()을 이용하여 stream객체를 생성 가능하다.

3. Stream 클래스의 정적 메서드 사용

Stream<String> stream = Stream.of("A", "B", "C");
  • .of() → 가변 인자를 스트림으로
  • .generate(Supplier) → 무한 스트림 (ex: 랜덤값 반복 생성)
  • .iterate(seed, UnaryOperator) → 무한 반복 스트림

예:

Stream<Integer> nums = Stream.iterate(1, n -> n + 1); // 1, 2, 3, ...

Stream<String> stream = Stream.of("a", "b", "c"); 

Stream<int[]> intArrayStream = Stream.of(new int[]{1, 2, 3});

 


4. 파일 등 외부 자원

Stream<String> lines = Files.lines(Paths.get("file.txt"));

 


5. Stream.builder() 을 활용하여 수동으로 만들 때

Stream.Builder<String> builder = Stream.builder();

builder.add("apple");
builder.add("banana");
builder.add("cherry");

Stream<String> stream = builder.build();

 

간단한 버전 :

Stream<String> stream = Stream.<String>builder()
    .add("apple")
    .add("banana")
    .add("cherry")
    .build();

 

주의할 점 :

  • .build()를 호출한 후에는 .add()를 더 이상 사용할 수 없음 (불변 스트림 생성)
  • 너무 많은 요소를 직접 add()로 넣는 건 귀찮음 → 이럴 땐 List.stream()이나 Stream.of()가 더 나음

Stream 만들기 요약 정리

방법 설명
list.stream() 컬렉션에서 스트림 생성
Arrays.stream() 배열에서 스트림 생성
Stream.of(...) 가변 인자 스트림 생성
Stream.builder() 수동으로 add() 해서 스트림 생성
Stream.generate() Supplier 기반 무한 스트림
Stream.iterate() 초기값과 함수 기반 반복 스트림

 

 

 


 

 

Stream 객체를 만들었다면 이걸 활용해야 한다.

 

만들어지 Stream 객체를 사용하기 위해서는 2단계 과정을 더 거쳐야한다. 바로 중간 연산최종 연산이다.

 

 

1. 중간 연산 (Intermediate Operations)

스트림을 변형시키고 연결만 하고, 실제 실행은 최종 연산이 호출될 때까지 지연됨 (lazy evaluation).

 

연산 설명 예시
filter(Predicate) 조건에 맞는 요소만 통과 filter(x -> x > 10)
map(Function) 요소를 다른 형태로 변환 map(String::length)
flatMap(Function) 중첩된 스트림을 평탄화 flatMap(list -> list.stream())
sorted() 기본 정렬 sorted()
sorted(Comparator) 사용자 정의 정렬 sorted(Comparator.reverseOrder())
distinct() 중복 제거 distinct()
limit(n) 앞에서 n개만 선택 limit(5)
skip(n) 앞에서 n개 건너뜀 skip(3)
peek(Consumer) 디버깅용: 요소를 들여다봄 peek(System.out::println)

 

2. 최종 연산 (Terminal Operations)

스트림 연산을 실행하고 결과를 반환함.
여기서 연산이 실제로 수행됨.

 

연산 설명 반환형
forEach(Consumer) 각 요소에 대해 작업 void
collect(Collector) 요소들을 수집 (리스트, 집합 등으로) List, Set 등
reduce() 누적해서 하나의 값으로 합침 Optional<T>
count() 요소 수 계산 long
anyMatch(), allMatch(), noneMatch() 조건에 대한 논리값 boolean
findFirst(), findAny() 요소 하나 반환 Optional<T>
toArray() 배열로 변환 Object[] 또는 T[]

 

중간 연산의 경우 lazy하여 최종연산이 실행되기 전까지 아무 일도 일어나지 않는다. 

 

최종 연산은 한 번만 실행 가능하며, 중간 연산은 여러번 사용해도 된다.

 

3. 쉽게 기억하는 비유!

  • Stream = 컨베이어 벨트
  • 중간 연산 = 벨트 위에서 필터링, 변형, 정렬하는 장치들
  • 최종 연산 = 완성품을 포장하거나 소비하는 마지막 과정
  • 포장하고 나면, 벨트는 멈추고 재사용 불가

예시 :

List<String> names = List.of("Alice", "Bob", "Charlie", "David");

long count = names.stream()
    .filter(name -> name.length() > 3)  // 중간 연산
    .map(String::toUpperCase)          // 중간 연산
    .distinct()                        // 중간 연산
    .count();                          // 최종 연산

System.out.println(count); // 3

 

 


 

 

Stream API를 활용하려면 람다(Lambda)식도 알아야한다.

참 귀찮은 일이지만 다음 포스트에 한 번 람다식에 관해 써보겠다.

블로그 이미지

Ahan

책, 영화, 게임! 인생의 활력 요소가 되는 취미들을 하자!

,

자바에서 Set은 중복을 허용하지 않는 컬렉션(Collection)이다.

즉, 같은 값을 여러 번 넣으려고 해도 한 번만 저장된다.

Set은 java.util 패키지에 있고, 인터페이스 형태로 존재하며, 다음과 같은 상속 관계를 가지고 있다.

 

 

1. Set의 상속 구조 (인터페이스 중심)

java.lang.Iterable<E>
   ↑
java.util.Collection<E>
   ↑
java.util.Set<E> // 아래는 Set의 구현 클래스들이다.
├── HashSet<E>
│   └── LinkedHashSet<E>
└── SortedSet<E>
     └── NavigableSet<E>
          └── TreeSet<E>

 

2. Set의 대표적인 구현체(클래스)

 

대표적인 구현체로는 다음과 같은 것들이 있다:


2-1. HashSet

  • 가장 많이 사용되는 Set 구현체
  • 내부적으로 HashMap을 사용하여 구현됨
  • 순서를 보장하지 않음
  • null 값을 하나 저장할 수 있음
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // 중복이므로 무시됨
System.out.println(set); // 출력: [banana, apple] (순서는 보장되지 않음)

2-2. LinkedHashSet

  • HashSet과 거의 같지만, 입력 순서를 유지함
  • 내부적으로 LinkedList + HashMap 구조
Set<String> set = new LinkedHashSet<>();
set.add("apple");
set.add("banana");
set.add("cherry");
System.out.println(set); // 출력: [apple, banana, cherry]

3-3. TreeSet

  • 정렬된 상태로 저장됨
  • 기본은 오름차순 정렬 (또는 Comparator 사용 가능)
  • 내부적으로 Red-Black Tree 구조
Set<Integer> set = new TreeSet<>();
set.add(3);
set.add(1);
set.add(2);
System.out.println(set); // 출력: [1, 2, 3]

 

4.주요 메서드 요약 (Set 인터페이스 공통)


메서드 설명
add(E e) 요소 추가 (중복이면 추가 안 됨)
remove(Object o) 요소 제거
contains(Object o) 포함 여부 확인
size() 크기 확인
clear() 전체 요소 삭제
isEmpty() 비어 있는지 확인
iterator() 반복자 반환 (향상된 for문 사용 가능)

- Collections를 상속 받기에 Collections의 메서드들을 대부분 이용 가능하다.

 

 

5. Set 구현체의 시간 및 공간 복잡도

일전에 코딩 테스트 공부하다가 각 자료구조의 시간복잡도와 공간 복잡도에 대해 찾아본적이 있다.

하여 대표적으로 사용되는 구현체들의 시간복잡도에 대해 적어본다.

 

5-1. HashSet

  • 기반 자료구조: HashMap
  • 해시 함수를 기반으로 하기 때문에 빠름
연산 평균 시간복잡도 최악 시간복잡도
add, remove, contains O(1) O(n)
size, isEmpty O(1) O(1)

최악의 경우는 해시 충돌이 많이 날 때, 내부적으로 연결 리스트나 트리로 변형되면서 느려질 수 있음

  • 공간복잡도: O(n)
    • 요소 저장 외에도 내부적으로 해시 버킷 배열이 필요함

5-2. LinkedHashSet

  • 기반 자료구조: HashMap + LinkedList
  • 입력 순서를 유지하기 위해 연결 리스트 추가됨
연산 평균 시간복잡도 최악 시간복잡도
add, remove, contains O(1) O(n)
  • 공간복잡도: O(n)
    • HashSet보다 약간 더 많은 메모리를 사용 (순서를 기억하는 연결 포인터 때문에)

5-3. TreeSet

  • 기반 자료구조: TreeMap (구체적으로 Red-Black Tree)
  • 정렬이 필요할 때 사용
연산 시간복잡도
add, remove, contains O(log n)
first, last, ceiling, floor O(log n)
  • 공간복잡도: O(n)
    • 균형 트리 구조를 유지하기 위한 포인터들이 추가됨

5-4. 요약

 

구현체 시간복잡도 공간복잡도 특징
HashSet 평균 O(1), 최악 O(n) O(n) 가장 빠름, 순서 없음
LinkedHashSet 평균 O(1), 최악 O(n) O(n) 입력 순서 유지
TreeSet O(log n) O(n) 자동 정렬, 느림
  • 순서 필요 없다 + 빠른 성능 원함 → HashSet
  • 입력 순서 필요 → LinkedHashSet
  • 정렬된 데이터 필요 → TreeSet

6. 언제 사용되는가?

Set은 기본적으로 중복 없이 데이터만 저장하고 싶을 때 쓴다.

Map도 마찬가지로 키의 중복 없이 데이터를 저장을 하지만 값을 중복 없이 저장하지는 않는다.

추가로 Map은 <key,boolean> 형식으로 존재 여부 확인용으로 Set처럼 쓸 수 있지만 메모리 낭비에 가독성도 떨어진다.

 

예시 상황 : 

  • 유저 ID -> 중복 없는 데이터 리스트 만들기
  • 방문한 페이지 집합 관리 
Set<String> visitedPages = new HashSet<>();
visitedPages.add("homePage"); // 홈페이지 방문
visitedPages.add("aboutPage"); // 정보소개 페이지 방문
visitedPages.add("homePage"); // 홈페이지 방문 중복, 무시됨

 

 

7. 다른 구현체로 변환

// Set → List
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");

List<String> list = new ArrayList<>(set);
System.out.println(list); // [banana, apple] — 순서는 보장 안 됨


// List → Set
List<String> list = Arrays.asList("apple", "banana", "apple");
Set<String> setFromList = new HashSet<>(list); // 중복 제거됨


// Set → 배열
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");

Object[] arr = set.toArray(); // Object 배열 반환


// 배열 → Set
String[] fruits = {"apple", "banana", "apple"};
Set<String> setFromArray = new HashSet<>(Arrays.asList(fruits));
블로그 이미지

Ahan

책, 영화, 게임! 인생의 활력 요소가 되는 취미들을 하자!

,

Java의 Collections는 데이터를 효율적으로 저장, 검색, 조작할 수 있도록 도와주는 자료구조 클래스와 인터페이스들의 집합이다.

자바에서 많이 사용하는 List, Set, Map, Queue 등이 여기에 속한다.

 


1. Collections(Collections Framework)란?

  • Java.util 패키지에 포함되어 있다.
  • 데이터를 저장, 정렬, 검색, 수정 등의 작업을 효율적으로 할 수 있도록 설계된 인터페이스 + 구현 클래스들의 모음이야.

2. 주요 인터페이스와 구현체


 

인터페이스 특징 주요 구현 클래스
List 순서 O, 중복 허용 ArrayList, LinkedList, Vector
Set 순서 X, 중복 X HashSet, LinkedHashSet, TreeSet
Map 키-값 쌍 저장, 키 중복 X HashMap, TreeMap, LinkedHashMap
Queue FIFO 구조 LinkedList, PriorityQueue

 


3. Collections 클래스의 주요 메서드 정리

 

메서드 설명
sort(List<T> list) 리스트를 오름차순 정렬 (T는 Comparable 구현체여야 함)
sort(List<T> list, Comparator<? super T> c) 지정한 비교자(Comparator) 기준으로 정렬
reverse(List<T> list) 리스트의 순서를 역순으로 변경
shuffle(List<T> list) 리스트 요소들의 순서를 무작위로 섞음
swap(List<T> list, int i, int j) 리스트에서 두 요소의 위치를 맞바꿈
max(Collection<? extends T> coll) 컬렉션에서 가장 큰 요소 반환
min(Collection<? extends T> coll) 컬렉션에서 가장 작은 요소 반환
binarySearch(List<? extends Comparable<? super T>> list, T key) 정렬된 리스트에서 이진 탐색으로 요소 위치 찾기
fill(List<? super T> list, T obj) 리스트의 모든 요소를 지정한 값으로 채움
copy(List<? super T> dest, List<? extends T> src) src 리스트 내용을 dest 리스트에 복사
replaceAll(List<T> list, T oldVal, T newVal) 리스트에서 특정 값을 찾아 새 값으로 모두 바꿈
frequency(Collection<?> c, Object o) 컬렉션에서 특정 객체가 몇 번 나오는지 카운트
disjoint(Collection<?> c1, Collection<?> c2) 두 컬렉션이 서로 겹치는 요소가 없는지 확인

간단한 예시

import java.util.*;

public class CollectionsExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(Arrays.asList(4, 2, 9, 1, 5));

        Collections.sort(numbers);  // [1, 2, 4, 5, 9]
        Collections.reverse(numbers);  // [9, 5, 4, 2, 1]
        Collections.shuffle(numbers);  // 무작위 섞기
        Collections.swap(numbers, 0, 1);  // 첫 번째와 두 번째 요소 교환

        int max = Collections.max(numbers);
        int min = Collections.min(numbers);
        int freq = Collections.frequency(numbers, 4);

        System.out.println("Max: " + max);
        System.out.println("Min: " + min);
        System.out.println("4의 개수: " + freq);
    }
}

 

 


Collections에 대해 간단하게 알아봤으니 이제는 Arrays에 대해 알아보겠다.

1. Arrays(java.util.Arrays) 클래스란?

  • 배열 전용 유틸리티 클래스 혹은 메서드 모음 (정렬, 복사, 비교 등)
  • 모든 메서드는 static(정적) → 객체 생성 없이 바로 사용 가능
  • java.util 패키지에 포함되어 있다.

2. 자주 사용하는 메서드

 

메서드 설명
Arrays.toString(array) 배열 내용을 문자열로 변환
Arrays.sort(array) 배열 정렬 (오름차순)
Arrays.copyOf(array, newLength) 배열을 복사하면서 크기 변경 가능
Arrays.equals(arr1, arr2) 두 배열의 내용이 같은지 비교
Arrays.fill(array, value) 배열을 하나의 값으로 모두 채움
Arrays.binarySearch(array, key) 정렬된 배열에서 이진 탐색 수행
Arrays.asList(array) 배열을 리스트로 변환 (단, 고정 크기)
Arrays.deepToString(array) 다차원 배열을 문자열로 변환
Arrays.deepEquals(arr1, arr2) 다차원 배열 내용 비교

3. 예시 코드

import java.util.Arrays;

public class ArraysExample {
    public static void main(String[] args) {
        int[] arr = {3, 5, 1, 2};

        Arrays.sort(arr); // 정렬
        System.out.println(Arrays.toString(arr)); // [1, 2, 3, 5]

        int[] copied = Arrays.copyOf(arr, 6); // 크기 6짜리로 복사
        System.out.println(Arrays.toString(copied)); // [1, 2, 3, 5, 0, 0]

        Arrays.fill(copied, 4); // 모든 요소를 4로 채움
        System.out.println(Arrays.toString(copied)); // [4, 4, 4, 4, 4, 4]

        int index = Arrays.binarySearch(arr, 3);
        System.out.println("3의 인덱스: " + index); // 2

        Integer[] nums = {10, 20, 30};
        System.out.println(Arrays.asList(nums)); // [10, 20, 30]
    }
}

 

 


 

Arrays와 Collections 차이

똑같이 java.util에 포함되어 있는 자료구조를 다루는 클래스들이지만

Arrays는 Collections에 존재하는 클래스가 아니다.

 

이는 둘이 다루는 자료구조의 차이 때문이다.

 

 

항목 Arrays(배열) Collections (ArrayList, HashSet 등)
크기 고정 가변
요소 추가/삭제 불가능 가능
저장 구조 배열 기반 다양한 구조 (배열, 연결리스트, 해시 등)
유연성 낮음 높음

 

 

Arrays가 다루는 자료구조인 배열은 크기가 고정되어 있으며 동일한 타입의 요소만 저장할 수 있고.

Collections가 다루는 자료구조들(List, Map, Queue, Set)은 크기가 가변적이며 제네릭스를 사용하여 다양한 타입의 요소를 저장할 수 있다는 차이가 있다.

 


 

다만 Arrays.asList()을 사용하여 배열을 리스트로 변환한 경우  Collections의 일부 메서드를 사용할 수 있다.

 

List<String> list = Arrays.asList("A", "B");
list.sort();  // Arrays의 sort가 아닌 Collections의 sort 이용
list.add("C"); // ❌ UnsupportedOperationException 발생
               // 예외가 발생하는 이유는 요소 추가/삭제 불가능한 배열을 가져와서 리스트의 형식으로 가져왔기 때문에
               // 자료형이 list로 바뀌었어도 요소 추가/삭제는 안된다고 한다.

 

 

 

이상으로 Arrays와 Collection에 대해 알아보았고 이 둘의 차이점에 대해서도 공부해 보았다.

블로그 이미지

Ahan

책, 영화, 게임! 인생의 활력 요소가 되는 취미들을 하자!

,